Files
rustfs/crates/keystone/README.md
T

730 lines
21 KiB
Markdown

# RustFS Keystone Integration
OpenStack Keystone authentication integration for RustFS S3-compatible object storage.
## Features
- **Keystone v3 API support** - Modern Keystone authentication
- **Token-based authentication** - Support for X-Auth-Token header
- **EC2 credentials** - S3 API compatibility with Keystone EC2 credentials
- **Multi-tenancy** - Project-based bucket isolation
- **Role mapping** - Map Keystone roles to RustFS IAM policies
- **Token caching** - High-performance token validation with caching
- **Swift compatibility** - Support for X-Storage-Token header
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
rustfs-keystone = "0.0.5"
```
## Usage
```rust
use rustfs_keystone::{KeystoneConfig, KeystoneClient, KeystoneAuthProvider};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load configuration from environment
let config = KeystoneConfig::from_env()?;
// Create Keystone client
let client = KeystoneClient::new(
config.auth_url.clone(),
config.get_version()?,
config.admin_user.clone(),
config.admin_password.clone(),
config.admin_project.clone(),
config.verify_ssl,
);
// Create authentication provider
let auth_provider = KeystoneAuthProvider::new(
client,
config.cache_size,
config.get_cache_ttl(),
);
// Authenticate with Keystone token
let token = "your-keystone-token";
let credentials = auth_provider.authenticate_with_token(token).await?;
println!("Authenticated user: {}", credentials.parent_user);
println!("Project: {:?}", credentials.claims);
Ok(())
}
```
## Configuration
Configure via environment variables:
```bash
# Enable Keystone
export RUSTFS_KEYSTONE_ENABLE=true
export RUSTFS_KEYSTONE_AUTH_URL=http://keystone:5000
export RUSTFS_KEYSTONE_VERSION=v3
# Admin credentials (optional, for privileged operations)
export RUSTFS_KEYSTONE_ADMIN_USER=admin
export RUSTFS_KEYSTONE_ADMIN_PASSWORD=secret
export RUSTFS_KEYSTONE_ADMIN_PROJECT=admin
# Multi-tenancy
export RUSTFS_KEYSTONE_TENANT_PREFIX=true
# Performance tuning
export RUSTFS_KEYSTONE_CACHE_SIZE=10000
export RUSTFS_KEYSTONE_CACHE_TTL=300
```
TLS certificate verification is enabled by default. Set
`RUSTFS_KEYSTONE_VERIFY_SSL=false` only for an explicitly trusted hop; it allows
MITM attacks against the Keystone connection and emits a startup warning.
## API Documentation
### KeystoneClient
The `KeystoneClient` provides low-level API access to Keystone services:
```rust
let client = KeystoneClient::new(
"http://keystone:5000".to_string(),
KeystoneVersion::V3,
Some("admin".to_string()),
Some("secret".to_string()),
Some("admin".to_string()),
true, // verify SSL
);
// Validate a token
let token_info = client.validate_token("token123").await?;
println!("User: {}, Project: {:?}", token_info.username, token_info.project_name);
// Get EC2 credentials
let ec2_creds = client.get_ec2_credentials("user_id", Some("project_id")).await?;
```
### KeystoneAuthProvider
The `KeystoneAuthProvider` provides high-level authentication with caching:
```rust
let provider = KeystoneAuthProvider::new(client, 10000, Duration::from_secs(300));
// Authenticate with token
let cred = provider.authenticate_with_token("token123").await?;
// Check if user is admin
if provider.is_admin(&cred) {
println!("User has admin privileges");
}
// Get project ID
if let Some(project_id) = provider.get_project_id(&cred) {
println!("User's project: {}", project_id);
}
```
### KeystoneIdentityMapper
The `KeystoneIdentityMapper` handles multi-tenancy and role mapping:
```rust
let mapper = KeystoneIdentityMapper::new(Arc::new(client), true);
// Apply tenant prefix to bucket name
let prefixed = mapper.apply_tenant_prefix("mybucket", Some("proj123"));
// Returns: "proj123:mybucket"
// Remove tenant prefix
let unprefixed = mapper.remove_tenant_prefix("proj123:mybucket", Some("proj123"));
// Returns: "mybucket"
// Map Keystone roles to RustFS policies
let roles = vec!["Member".to_string(), "admin".to_string()];
let policies = mapper.map_roles_to_policies(&roles);
// Returns: ["ReadWritePolicy", "AdminPolicy"]
// Check permissions
if mapper.has_permission(&roles, "s3:PutObject", "bucket/key") {
println!("User can write objects");
}
```
## Architecture
### Component Architecture
```
KeystoneClient (API calls)
KeystoneAuthProvider (Authentication + Caching)
KeystoneIdentityMapper (Multi-tenancy + Role Mapping)
RustFS Credentials
```
### Middleware Architecture
The keystone crate includes a Tower middleware (`KeystoneAuthMiddleware`) that integrates directly into RustFS's HTTP service stack. The middleware is self-contained within this crate and exported via the `middleware` module:
```rust
use rustfs_keystone::{KeystoneAuthLayer, KEYSTONE_CREDENTIALS};
// In RustFS HTTP service setup
let layer = KeystoneAuthLayer::new(keystone_auth_provider);
```
The middleware uses Tokio task-local storage (`KEYSTONE_CREDENTIALS`) to pass authenticated credentials between the middleware layer and authentication handlers without modifying the HTTP request.
### RustFS Integration Architecture
The Keystone integration uses a middleware-based approach that intercepts HTTP requests before they reach the S3 service layer:
```
HTTP Request
RemoteAddr/TrustedProxy Layers (Extract client IP)
SetRequestId/CatchPanic Layers (Request metadata)
ReadinessGate Layer (System health check)
KeystoneAuthMiddleware ⭐ (Token validation)
├─ No X-Auth-Token? → Pass through to S3 auth
├─ Has X-Auth-Token? → Validate with Keystone
│ ├─ Valid? → Store credentials in task-local storage → Continue
│ └─ Invalid? → Return 401 Unauthorized immediately
TraceLayer (Logging/observability)
S3 Service Layer
IAMAuth (Authentication)
├─ Keystone credential? (access_key starts with "keystone:")
│ ├─ Return empty secret_key (bypass signature validation)
│ └─ Retrieve credentials from task-local storage
└─ Standard credential? → Normal AWS Signature v4 validation
check_key_valid (Authorization)
├─ Keystone credential?
│ ├─ Get credentials from task-local storage
│ ├─ Check user roles (admin/reseller_admin = owner)
│ └─ Return (Credentials, is_owner)
└─ Standard credential? → Normal IAM validation
S3 Operation (PutObject, GetObject, etc.)
```
## Integration with RustFS
### How It Works
The Keystone integration provides seamless OpenStack authentication for RustFS S3 API. Here's how the complete request flow works:
#### 1. Request with Keystone Token
When a client makes an S3 API request with a Keystone token:
```bash
curl -X GET http://rustfs:9000/mybucket/myobject \
-H "X-Auth-Token: gAAAAABk..."
```
**Flow:**
1. **Middleware Intercepts**: The `KeystoneAuthMiddleware` extracts the `X-Auth-Token` header
2. **Token Validation**: Calls Keystone API to validate the token and retrieve user information
3. **Credential Mapping**: Creates RustFS credentials with:
- `access_key`: `keystone:<user_id>` (special prefix to identify Keystone users)
- `parent_user`: Keystone username
- `claims`: Project ID, roles, and other Keystone attributes in JSON format
4. **Task-Local Storage**: Stores credentials in async task-local storage (request-scoped)
5. **Pass Through**: Request continues to S3 service layer
6. **Authentication**: IAMAuth detects `keystone:` prefix, returns empty secret (bypasses AWS signature check)
7. **Authorization**: `check_key_valid()` retrieves credentials from task-local storage
8. **Role Check**: Determines if user is admin based on roles:
- `admin` role → owner permissions (full access)
- `reseller_admin` role → owner permissions (full access)
- Other roles → non-owner permissions (restricted access)
9. **S3 Operation**: Proceeds with appropriate permissions
#### 2. Request without Keystone Token
When a client makes a standard S3 request:
```bash
aws s3 cp file.txt s3://mybucket/file.txt \
--endpoint-url http://rustfs:9000
```
**Flow:**
1. **Middleware Pass-Through**: No `X-Auth-Token` header found, request passes through unchanged
2. **Standard S3 Auth**: AWS Signature v4 validation
3. **IAM Validation**: Normal RustFS IAM authentication
4. **S3 Operation**: Proceeds with IAM-based permissions
#### 3. Invalid Token Handling
When a token is invalid or expired:
**Flow:**
1. **Token Validation Fails**: Keystone returns error (invalid/expired token)
2. **Immediate 401**: Middleware returns `401 Unauthorized` immediately
3. **No Fallback**: Does NOT fall back to standard S3 authentication
4. **XML Error Response**: Returns S3-compatible error XML:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>InvalidToken</Code>
<Message>Invalid Keystone token</Message>
<Details>Token validation failed: token expired</Details>
</Error>
```
### Permission Model
The integration uses Keystone roles to determine RustFS permissions:
**Owner Permissions (is_owner=true):**
- Granted to users with `admin` or `reseller_admin` roles
- Full access to all operations (equivalent to root/admin access)
- Can create/delete buckets, manage policies, access all objects
**Non-Owner Permissions (is_owner=false):**
- Granted to users with other roles (member, reader, etc.)
- Restricted access based on bucket policies and IAM policies
- Cannot perform administrative operations
**Example:**
```json
{
"roles": ["admin", "member"]
}
```
`is_owner=true` (has admin role)
```json
{
"roles": ["member", "reader"]
}
```
`is_owner=false` (no admin role)
### Task-Local Storage
The integration uses Tokio task-local storage to pass credentials between middleware and authentication handlers:
**Why Task-Local Storage?**
- **Async-Safe**: Works correctly with async/await and Tokio runtime
- **Request-Scoped**: Automatically cleaned up when request completes
- **No Request Modification**: Credentials don't need to be added to HTTP headers/extensions
- **Thread-Safe**: Each async task has its own isolated storage
**How It Works:**
1. Middleware validates token and stores credentials using `KEYSTONE_CREDENTIALS.scope()`
2. Auth handlers retrieve credentials using `KEYSTONE_CREDENTIALS.try_with()`
3. Storage is automatically scoped to the current async task (request)
4. Storage is empty/inaccessible outside the scope
### Token Caching
To minimize Keystone API calls, the integration includes a high-performance token cache:
**Cache Behavior:**
- **Cache Hit**: Token found in cache → Returns cached credentials (no Keystone API call)
- **Cache Miss**: Token not in cache → Validates with Keystone → Caches result
- **Cache TTL**: Tokens are cached for configured duration (default: 300 seconds)
- **Cache Invalidation**: Expired entries are automatically removed
- **Thread-Safe**: Uses `moka::future::Cache` for concurrent access
**Performance Impact:**
- First request with token: ~50-100ms (network call to Keystone)
- Subsequent requests: ~1-2ms (cache lookup)
- Recommended cache size: 10,000 tokens (configurable)
### Configuration in RustFS
To enable Keystone authentication in RustFS:
1. **Set Environment Variables:**
```bash
export RUSTFS_KEYSTONE_ENABLE=true
export RUSTFS_KEYSTONE_AUTH_URL=http://keystone:5000
export RUSTFS_KEYSTONE_VERSION=v3
export RUSTFS_KEYSTONE_ADMIN_USER=admin
export RUSTFS_KEYSTONE_ADMIN_PASSWORD=secret
export RUSTFS_KEYSTONE_ADMIN_PROJECT=admin
export RUSTFS_KEYSTONE_ADMIN_DOMAIN=Default
export RUSTFS_KEYSTONE_CACHE_SIZE=10000
export RUSTFS_KEYSTONE_CACHE_TTL=300
export RUSTFS_KEYSTONE_VERIFY_SSL=true
```
2. **Start RustFS:**
```bash
rustfs --address 127.0.0.1:9000 \
--access-key minioadmin \
--secret-key minioadmin \
volumes /data
```
3. **RustFS will automatically:**
- Initialize Keystone client on startup (in `rustfs/src/main.rs`)
- Register `KeystoneAuthLayer` middleware from this crate in HTTP service stack (in `rustfs/src/server/http.rs`)
- Start accepting both Keystone and standard S3 authentication
The middleware is entirely self-contained in the `rustfs-keystone` crate and integrated into RustFS via the exported `KeystoneAuthLayer`. No separate middleware directory is required in the main RustFS binary.
### Dual Authentication Support
RustFS supports **both** Keystone and standard S3 authentication simultaneously:
- **Keystone Users**: Use `X-Auth-Token` header with Keystone token
- **IAM Users**: Use standard AWS Signature v4 authentication
- **No Conflict**: Requests are routed based on presence of `X-Auth-Token` header
- **Automatic Detection**: Middleware automatically detects authentication method
This allows gradual migration from standard S3 auth to Keystone auth, or mixed environments where some users authenticate via Keystone and others via IAM.
## Manual Testing
### Prerequisites
1. **Running Keystone Instance**
Using Docker:
```bash
docker run -d --name keystone \
-p 5000:5000 \
-e KEYSTONE_ADMIN_PASSWORD=secret \
ghcr.io/openstack/keystone:latest
```
Or using DevStack:
```bash
# Follow DevStack installation guide
git clone https://opendev.org/openstack/devstack
cd devstack
./stack.sh
```
2. **Running RustFS with Keystone Enabled**
```bash
# Configure Keystone
export RUSTFS_KEYSTONE_ENABLE=true
export RUSTFS_KEYSTONE_AUTH_URL=http://localhost:5000
export RUSTFS_KEYSTONE_VERSION=v3
export RUSTFS_KEYSTONE_ADMIN_USER=admin
export RUSTFS_KEYSTONE_ADMIN_PASSWORD=secret
export RUSTFS_KEYSTONE_ADMIN_PROJECT=admin
export RUSTFS_KEYSTONE_ADMIN_DOMAIN=Default
# Start RustFS
cargo run --bin rustfs -- \
--address 127.0.0.1:9000 \
--access-key minioadmin \
--secret-key minioadmin \
volumes /data
```
### Test Scenarios
#### Test 1: Get Keystone Token
```bash
# Request scoped token from Keystone
curl -X POST http://localhost:5000/v3/auth/tokens \
-H "Content-Type: application/json" \
-d '{
"auth": {
"identity": {
"methods": ["password"],
"password": {
"user": {
"name": "admin",
"domain": {"name": "Default"},
"password": "secret"
}
}
},
"scope": {
"project": {
"name": "admin",
"domain": {"name": "Default"}
}
}
}
}' -i
# Look for X-Subject-Token in response headers
# Example: X-Subject-Token: gAAAAABk1a2b3c...
```
Save the token from the `X-Subject-Token` header.
#### Test 2: List Buckets with Keystone Token
```bash
# Replace TOKEN with your actual token
export KEYSTONE_TOKEN="gAAAAABk1a2b3c..."
curl -X GET http://localhost:9000/ \
-H "X-Auth-Token: $KEYSTONE_TOKEN" \
-v
```
**Expected Result:**
- Status: `200 OK`
- Response: XML list of buckets
- Logs should show: `Keystone middleware: Authentication successful for user: admin`
#### Test 3: Create Bucket
```bash
curl -X PUT http://localhost:9000/test-keystone-bucket \
-H "X-Auth-Token: $KEYSTONE_TOKEN" \
-v
```
**Expected Result:**
- Status: `200 OK`
- Bucket created successfully
- Logs show Keystone credentials being used
#### Test 4: Upload Object
```bash
echo "Hello from Keystone!" > test.txt
curl -X PUT http://localhost:9000/test-keystone-bucket/test.txt \
-H "X-Auth-Token: $KEYSTONE_TOKEN" \
-T test.txt \
-v
```
**Expected Result:**
- Status: `200 OK`
- Object uploaded successfully
#### Test 5: Download Object
```bash
curl -X GET http://localhost:9000/test-keystone-bucket/test.txt \
-H "X-Auth-Token: $KEYSTONE_TOKEN" \
-o downloaded.txt \
-v
cat downloaded.txt
```
**Expected Result:**
- Status: `200 OK`
- File content: `Hello from Keystone!`
#### Test 6: Invalid Token (Negative Test)
```bash
curl -X GET http://localhost:9000/ \
-H "X-Auth-Token: invalid-token-12345" \
-v
```
**Expected Result:**
- Status: `401 Unauthorized`
- Response:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>InvalidToken</Code>
<Message>Invalid Keystone token</Message>
<Details>...</Details>
</Error>
```
- Logs show: `Keystone middleware: Authentication failed`
#### Test 7: No Token (Standard S3 Auth)
```bash
# Using AWS CLI with standard credentials
aws s3 ls s3:// \
--endpoint-url http://localhost:9000 \
--no-sign-request
```
**Expected Result:**
- Falls back to standard S3 authentication
- Works as normal (if anonymous access allowed)
- Logs show: `Keystone middleware: No X-Auth-Token header, passing through to S3 auth`
#### Test 8: Admin Role Permissions
```bash
# Create a user with admin role in Keystone
# Get token for admin user
curl -X DELETE http://localhost:9000/test-keystone-bucket \
-H "X-Auth-Token: $ADMIN_TOKEN" \
-v
```
**Expected Result:**
- Status: `204 No Content` (bucket deleted)
- Admin has owner permissions (`is_owner=true`)
#### Test 9: Non-Admin Role Permissions
```bash
# Create a user with only "member" role in Keystone
# Get token for member user
curl -X DELETE http://localhost:9000/test-keystone-bucket \
-H "X-Auth-Token: $MEMBER_TOKEN" \
-v
```
**Expected Result:**
- Status: `403 Forbidden` (depending on bucket policy)
- Member does not have owner permissions (`is_owner=false`)
#### Test 10: Token Caching Performance
```bash
# First request (cache miss)
time curl -X GET http://localhost:9000/ \
-H "X-Auth-Token: $KEYSTONE_TOKEN" \
-o /dev/null -s
# Second request (cache hit)
time curl -X GET http://localhost:9000/ \
-H "X-Auth-Token: $KEYSTONE_TOKEN" \
-o /dev/null -s
```
**Expected Result:**
- First request: ~50-100ms (includes Keystone API call)
- Second request: ~1-5ms (cache hit, no Keystone call)
- Logs show: `Cache hit` for second request
### Troubleshooting
**Issue: "Keystone authentication is not enabled"**
- Check `RUSTFS_KEYSTONE_ENABLE=true` is set
- Verify environment variables are exported before starting RustFS
- Check RustFS startup logs for "Keystone authentication initialized successfully"
**Issue: "Connection refused" to Keystone**
- Verify Keystone is running: `curl http://localhost:5000/v3`
- Check `RUSTFS_KEYSTONE_AUTH_URL` points to correct Keystone endpoint
- Verify network connectivity between RustFS and Keystone
**Issue: "Invalid token" errors**
- Check token hasn't expired (Keystone tokens typically expire after 1 hour)
- Request a fresh token
- Verify token format is correct (no newlines, extra spaces)
**Issue: "SSL verification failed"**
- Prefer installing Keystone's CA certificate in the system trust store
- If using a trusted non-production hop, set `RUSTFS_KEYSTONE_VERIFY_SSL=false`; this allows MITM attacks and emits a startup warning
**Issue: Slow performance**
- Increase cache size: `RUSTFS_KEYSTONE_CACHE_SIZE=50000`
- Increase cache TTL: `RUSTFS_KEYSTONE_CACHE_TTL=600`
- Check network latency to Keystone
**Issue: Permissions denied**
- Verify user's Keystone roles
- Check if user needs `admin` or `reseller_admin` role
- Review RustFS logs for `is_owner` value
## Token Cache
The token cache improves performance by caching validated tokens:
- **Cache Size**: Number of tokens to cache (default: 10,000)
- **Cache TTL**: Time-to-live for cached tokens (default: 300 seconds)
- **Thread-Safe**: Uses `moka::future::Cache` for concurrent access
## Multi-Tenancy
When tenant prefixing is enabled:
1. **Bucket Creation**: `mybucket` → stored as `project_id:mybucket`
2. **Bucket Listing**: Only shows buckets belonging to user's project
3. **Access Control**: Users can only access their project's buckets
## Role Mapping
Default role mappings:
| Keystone Role | RustFS Policy | Permissions |
|---------------|---------------|-------------|
| admin | AdminPolicy | Full access (s3:*) |
| Member | ReadWritePolicy | Read/write operations |
| _member_ | ReadOnlyPolicy | Read-only access |
| ResellerAdmin | AdminPolicy | Full access (s3:*) |
Add custom mappings:
```rust
let mut mapper = KeystoneIdentityMapper::new(client, true);
mapper.add_role_mapping("CustomRole".to_string(), "CustomPolicy".to_string());
```
## Error Handling
All operations return `Result<T, KeystoneError>`:
```rust
use rustfs_keystone::{KeystoneError, Result};
match auth_provider.authenticate_with_token(token).await {
Ok(cred) => println!("Success: {}", cred.parent_user),
Err(KeystoneError::InvalidToken) => eprintln!("Token is invalid"),
Err(KeystoneError::TokenExpired) => eprintln!("Token has expired"),
Err(e) => eprintln!("Error: {}", e),
}
```
## Testing
Run tests with:
```bash
cargo test -p rustfs-keystone
```
### Test Structure
The crate includes comprehensive test coverage:
**Unit Tests** (16 tests in `src/` modules):
- Config parsing and validation
- Client creation
- Auth provider functionality
- Identity mapping and role permissions
- Middleware token extraction and validation
**Integration Tests** (10 tests in `tests/integration/`):
- Middleware layer creation and configuration
- Task-local storage isolation and scope management
- Credential passing between middleware and auth handlers
- Nested and sequential scope behavior
- Multi-task concurrency safety
**Total: 27 tests** covering all public APIs and integration scenarios.
Integration tests require a running Keystone instance.
## License
Licensed under the Apache License, Version 2.0. See LICENSE file for details.