crates/keystone/README.md
OpenStack Keystone authentication integration for RustFS S3-compatible object storage.
Add to your Cargo.toml:
[dependencies]
rustfs-keystone = "0.0.5"
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(())
}
Configure via environment variables:
# 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
The KeystoneClient provides low-level API access to Keystone services:
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?;
The KeystoneAuthProvider provides high-level authentication with caching:
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);
}
The KeystoneIdentityMapper handles multi-tenancy and role mapping:
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");
}
KeystoneClient (API calls)
↓
KeystoneAuthProvider (Authentication + Caching)
↓
KeystoneIdentityMapper (Multi-tenancy + Role Mapping)
↓
RustFS Credentials
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:
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.
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.)
The Keystone integration provides seamless OpenStack authentication for RustFS S3 API. Here's how the complete request flow works:
When a client makes an S3 API request with a Keystone token:
curl -X GET http://rustfs:9000/mybucket/myobject \
-H "X-Auth-Token: gAAAAABk..."
Flow:
KeystoneAuthMiddleware extracts the X-Auth-Token headeraccess_key: keystone:<user_id> (special prefix to identify Keystone users)parent_user: Keystone usernameclaims: Project ID, roles, and other Keystone attributes in JSON formatkeystone: prefix, returns empty secret (bypasses AWS signature check)check_key_valid() retrieves credentials from task-local storageadmin role → owner permissions (full access)reseller_admin role → owner permissions (full access)When a client makes a standard S3 request:
aws s3 cp file.txt s3://mybucket/file.txt \
--endpoint-url http://rustfs:9000
Flow:
X-Auth-Token header found, request passes through unchangedWhen a token is invalid or expired:
Flow:
401 Unauthorized immediately<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>InvalidToken</Code>
<Message>Invalid Keystone token</Message>
<Details>Token validation failed: token expired</Details>
</Error>
The integration uses Keystone roles to determine RustFS permissions:
Owner Permissions (is_owner=true):
admin or reseller_admin rolesNon-Owner Permissions (is_owner=false):
Example:
{
"roles": ["admin", "member"]
}
→ is_owner=true (has admin role)
{
"roles": ["member", "reader"]
}
→ is_owner=false (no admin role)
The integration uses Tokio task-local storage to pass credentials between middleware and authentication handlers:
Why Task-Local Storage?
How It Works:
KEYSTONE_CREDENTIALS.scope()KEYSTONE_CREDENTIALS.try_with()To minimize Keystone API calls, the integration includes a high-performance token cache:
Cache Behavior:
moka::future::Cache for concurrent accessPerformance Impact:
To enable Keystone authentication in RustFS:
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
rustfs --address 127.0.0.1:9000 \
--access-key minioadmin \
--secret-key minioadmin \
volumes /data
rustfs/src/main.rs)KeystoneAuthLayer middleware from this crate in HTTP service stack (in rustfs/src/server/http.rs)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.
RustFS supports both Keystone and standard S3 authentication simultaneously:
X-Auth-Token header with Keystone tokenX-Auth-Token headerThis allows gradual migration from standard S3 auth to Keystone auth, or mixed environments where some users authenticate via Keystone and others via IAM.
Using Docker:
docker run -d --name keystone \
-p 5000:5000 \
-e KEYSTONE_ADMIN_PASSWORD=secret \
ghcr.io/openstack/keystone:latest
Or using DevStack:
# Follow DevStack installation guide
git clone https://opendev.org/openstack/devstack
cd devstack
./stack.sh
# 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
# 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.
# 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:
200 OKKeystone middleware: Authentication successful for user: admincurl -X PUT http://localhost:9000/test-keystone-bucket \
-H "X-Auth-Token: $KEYSTONE_TOKEN" \
-v
Expected Result:
200 OKecho "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:
200 OKcurl -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:
200 OKHello from Keystone!curl -X GET http://localhost:9000/ \
-H "X-Auth-Token: invalid-token-12345" \
-v
Expected Result:
401 Unauthorized<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>InvalidToken</Code>
<Message>Invalid Keystone token</Message>
<Details>...</Details>
</Error>
Keystone middleware: Authentication failed# Using AWS CLI with standard credentials
aws s3 ls s3:// \
--endpoint-url http://localhost:9000 \
--no-sign-request
Expected Result:
Keystone middleware: No X-Auth-Token header, passing through to S3 auth# 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:
204 No Content (bucket deleted)is_owner=true)# 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:
403 Forbidden (depending on bucket policy)is_owner=false)# 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:
Cache hit for second requestIssue: "Keystone authentication is not enabled"
RUSTFS_KEYSTONE_ENABLE=true is setIssue: "Connection refused" to Keystone
curl http://localhost:5000/v3RUSTFS_KEYSTONE_AUTH_URL points to correct Keystone endpointIssue: "Invalid token" errors
Issue: "SSL verification failed"
RUSTFS_KEYSTONE_VERIFY_SSL=falseIssue: Slow performance
RUSTFS_KEYSTONE_CACHE_SIZE=50000RUSTFS_KEYSTONE_CACHE_TTL=600Issue: Permissions denied
admin or reseller_admin roleis_owner valueThe token cache improves performance by caching validated tokens:
moka::future::Cache for concurrent accessWhen tenant prefixing is enabled:
mybucket → stored as project_id:mybucketDefault 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:
let mut mapper = KeystoneIdentityMapper::new(client, true);
mapper.add_role_mapping("CustomRole".to_string(), "CustomPolicy".to_string());
All operations return Result<T, KeystoneError>:
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),
}
Run tests with:
cargo test -p rustfs-keystone
The crate includes comprehensive test coverage:
Unit Tests (16 tests in src/ modules):
Integration Tests (10 tests in tests/integration/):
Total: 27 tests covering all public APIs and integration scenarios.
Integration tests require a running Keystone instance.
Licensed under the Apache License, Version 2.0. See LICENSE file for details.