Skip to content

Authentication

IOLake supports AWS Signature Version 4 (SigV4) authentication for S3 API compatibility.

Overview

┌─────────────────────────────────────────────────────────────────┐
│                        S3 Client                                 │
│  (aws-cli, boto3, aws-sdk-rust, etc.)                           │
└─────────────────────────┬───────────────────────────────────────┘
                          │ Authorization: AWS4-HMAC-SHA256 ...
                          ▼
┌─────────────────────────────────────────────────────────────────┐
│                    IOLake Gateway                              │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                  Auth Middleware                           │  │
│  │  1. Parse Authorization header                             │  │
│  │  2. Lookup credentials from Metadata Service (with cache)  │  │
│  │  3. Compute canonical request                              │  │
│  │  4. Verify signature                                       │  │
│  │  5. Evaluate bucket policy                                 │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Credential Storage

Credentials are centrally managed by the Metadata Service, not the gateway. This ensures:

  • Persistence: Credentials survive gateway restarts
  • Consistency: All gateways see the same credentials
  • Centralized management: Single source of truth for IAM
┌─────────────┐      gRPC       ┌─────────────────┐
│   Gateway   │ ◄────────────► │  Metadata Svc   │
│             │ GetAccessKey   │                 │
│  (caches    │ ForAuth        │  - Users        │
│   5 min)    │                │  - Access Keys  │
└─────────────┘                └─────────────────┘

Initial Admin User

On first startup, the metadata service creates a default admin user and writes its credentials to admin-creds.env in its data directory, readable by its owner only. Meta logs the access key and where the file is, never the secret:

# A host install
sudo cat /var/lib/objectio/meta/admin-creds.env
# Kubernetes
# (<name>: the release name if it contains "iolake", else <release>-iolake)
kubectl exec -n <namespace> <name>-meta-0 -- cat /var/lib/objectio/meta/admin-creds.env
# Docker
docker exec iolake-meta cat /var/lib/objectio/meta/admin-creds.env

The file holds AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as shell exports. Create named admins of your own, then delete it.


AWS Signature Version 4

IOLake implements AWS SigV4, the standard authentication method for AWS services.

Authorization Header Format

Authorization: AWS4-HMAC-SHA256
Credential={access_key}/{date}/{region}/s3/aws4_request,
SignedHeaders={signed_headers},
Signature={signature}

Example:

Authorization: AWS4-HMAC-SHA256
Credential=AKIAIOSFODNN7EXAMPLE/20240115/us-east-1/s3/aws4_request,
SignedHeaders=host;x-amz-content-sha256;x-amz-date,
Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024

Required Headers

Header Description
Host Bucket endpoint
x-amz-date Request timestamp (ISO 8601)
x-amz-content-sha256 SHA-256 hash of request body; on the S3 API only (see below)

Presigned URLs (query-string SigV4) are accepted too; SigV2, in a header or a presigned URL, is refused with 400.

The signed body

The signature covers the body through x-amz-content-sha256, and the gateway holds the body to it. What each value means:

x-amz-content-sha256 What is checked
A SHA-256, hex The body's hash, as it arrives. A body that doesn't match is refused with 400 XAmzContentSHA256Mismatch, and nothing is stored
UNSIGNED-PAYLOAD Nothing: the client signed no body (presigned URLs send this, and SDKs over HTTPS often do)
STREAMING-AWS4-HMAC-SHA256-PAYLOAD Each chunk's signature, chained from the request's. A chunk whose signature doesn't match is refused with 403 SignatureDoesNotMatch, and nothing is stored
STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER As above, and the signature over the trailing checksum
STREAMING-UNSIGNED-PAYLOAD-TRAILER The trailing checksum (400 BadDigest when it doesn't match)

On the S3 API, a request without x-amz-content-sha256 signed no body, as if it sent UNSIGNED-PAYLOAD; every S3 SDK sends the header.

The other APIs on the gateway (the admin API, the Iceberg REST catalog, Unity Catalog) also take a request signed as standard SigV4 is for any service but S3: over the SHA-256 of its body, with no x-amz-content-sha256 header. That is how botocore's generic SigV4Auth signs, and so PyIceberg with rest.sigv4-enabled, and most SigV4 libraries that aren't S3 SDKs. The gateway reads the body and checks the signature over its hash, so a body changed after signing is refused (401 on the catalogs, 403 SignatureDoesNotMatch on the admin API). It reads at most 2 MiB that way: a larger body is refused with 413, and is signed by sending its hash in the header. A request signed over UNSIGNED-PAYLOAD without the header is accepted there as on the S3 API, and its body is not read. The IAM and STS query API (POST /) checks its form body's hash as well.

A trailer the request names in x-amz-trailer must come at the end of the body: one left off is refused with 400 MalformedTrailerError. Each streaming form is what some SDK sends by default for uploads (aws-cli, boto3, the Go and Java SDKs, minio-go); none needs setting up.

A refused signature is logged at debug only, with the canonical request the gateway computed, to tell a proxy that rewrites requests from a client that signs them wrong. Credentials, session tokens, cookies and SSE-C keys are left out of that log.


User Management

Using the CLI

iolakectl is a client of the admin API, signed with SigV4 like any request — see CLI for configuring it.

iolakectl user list
iolakectl user create alice --email alice@example.com
iolakectl user suspend <user_id>          # every key refused; nothing deleted
iolakectl user delete <user_id>

iolakectl key list <user_id>
iolakectl key create <user_id>                              # unscoped
iolakectl key create <user_id> --scope s3://logs/ --read-only
iolakectl key deactivate <access_key_id>
iolakectl key delete <access_key_id>

Admin API

The Admin API provides HTTP endpoints for user and access key management. It uses the same SigV4 authentication as the S3 API. The system admin can use all of it; a tenant admin can use it for its own tenant.

The full identity surface — suspending users, deactivating keys, IAM policies, groups, roles, per-tenant identity providers and STS — is in Identity and Access.

Admin API Endpoints

Endpoint Method Description
/_admin/users GET List all users
/_admin/users POST Create a new user
/_admin/users/{user_id} DELETE Delete a user
/_admin/users/{user_id}/access-keys GET List user's access keys
/_admin/users/{user_id}/access-keys POST Create access key for user
/_admin/access-keys/{access_key_id} DELETE Delete an access key

Authentication

The Admin API requires SigV4 authentication with admin credentials. Non-admin users receive a 403 Forbidden response.

# Configure admin credentials
export AWS_ACCESS_KEY_ID=AKIA...      # From meta service startup logs
export AWS_SECRET_ACCESS_KEY=...

List Users

curl -X GET https://s3.example.com/_admin/users \
  --aws-sigv4 "aws:amz:us-east-1:s3" \
  --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"

Response:

{
  "users": [
    {
      "user_id": "550e8400-e29b-41d4-a716-446655440000",
      "display_name": "admin",
      "arn": "arn:objectio:iam::user/admin",
      "status": "UserActive",
      "created_at": 1705320000,
      "email": ""
    }
  ],
  "is_truncated": false,
  "next_marker": null
}

Create User

curl -X POST https://s3.example.com/_admin/users \
  -H "Content-Type: application/json" \
  -d '{"display_name": "alice", "email": "alice@example.com"}' \
  --aws-sigv4 "aws:amz:us-east-1:s3" \
  --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"

Response:

{
  "user_id": "660e8400-e29b-41d4-a716-446655440001",
  "display_name": "alice",
  "arn": "arn:objectio:iam::user/alice",
  "status": "UserActive",
  "created_at": 1705320100,
  "email": "alice@example.com"
}

Create Access Key

curl -X POST https://s3.example.com/_admin/users/{user_id}/access-keys \
  --aws-sigv4 "aws:amz:us-east-1:s3" \
  --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"

Response:

{
  "access_key_id": "AKIAIOSFODNN7EXAMPLE",
  "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  "user_id": "660e8400-e29b-41d4-a716-446655440001",
  "status": "KeyActive",
  "created_at": 1705320200
}

Important: The secret_access_key is only returned when creating a key. Save it immediately!

Delete User

curl -X DELETE https://s3.example.com/_admin/users/{user_id} \
  --aws-sigv4 "aws:amz:us-east-1:s3" \
  --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"

Returns 204 No Content on success.

Delete Access Key

curl -X DELETE https://s3.example.com/_admin/access-keys/{access_key_id} \
  --aws-sigv4 "aws:amz:us-east-1:s3" \
  --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"

Returns 204 No Content on success.

Error Responses

Status Description
401 Unauthorized Authentication required (no-auth mode)
403 Forbidden Non-admin user attempted access
404 Not Found User or access key not found
500 Internal Server Error Server error

Bucket Policies

Bucket policies control access to buckets and objects using IAM-style JSON policies.

Policy Structure

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicRead",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-bucket/*"
        }
    ]
}

Policy Elements

Element Description
Version Policy version (always "2012-10-17")
Statement Array of permission statements
Sid Statement ID (optional)
Effect "Allow" or "Deny"
Principal Who the statement applies to
Action S3 actions (e.g., "s3:GetObject")
Resource ARN of bucket/objects
Condition Optional conditions

Supported Actions

S3 Object Actions: - s3:GetObject - s3:PutObject - s3:DeleteObject - s3:GetObjectVersion

S3 Bucket Actions: - s3:ListBucket - s3:GetBucketLocation - s3:GetBucketPolicy - s3:PutBucketPolicy - s3:DeleteBucketPolicy

Iceberg Namespace Actions: - iceberg:ListNamespaces - iceberg:CreateNamespace - iceberg:LoadNamespace - iceberg:DropNamespace - iceberg:UpdateNamespaceProperties

Iceberg Table Actions: - iceberg:ListTables - iceberg:CreateTable - iceberg:LoadTable - iceberg:UpdateTable - iceberg:DropTable - iceberg:RenameTable

Example Policies

Public Read Access:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::public-bucket/*"
        }
    ]
}

Cross-Account Access:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::123456789012:user/external-user"
            },
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::shared-bucket/*"
        }
    ]
}

IP-Based Restriction:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::secure-bucket",
                "arn:aws:s3:::secure-bucket/*"
            ],
            "Condition": {
                "NotIpAddress": {
                    "aws:SourceIp": "192.168.1.0/24"
                }
            }
        }
    ]
}

Setting Bucket Policy

# Using AWS CLI
aws s3api put-bucket-policy \
  --endpoint-url http://localhost:9000 \
  --bucket my-bucket \
  --policy file://policy.json

# Get current policy
aws s3api get-bucket-policy \
  --endpoint-url http://localhost:9000 \
  --bucket my-bucket

# Delete policy
aws s3api delete-bucket-policy \
  --endpoint-url http://localhost:9000 \
  --bucket my-bucket

Condition Keys

Supported condition keys for policy evaluation:

Key Description
aws:SourceIp The client's address: the connection's peer, or the forwarded client when the peer is a trusted proxy
aws:SourceVpce The named gateway endpoint the request arrived on; absent on an unnamed one
aws:SecureTransport Whether HTTPS was used (as reported by a TLS-terminating proxy)
aws:CurrentTime, aws:EpochTime Request time
aws:username The caller's ARN
s3:prefix A listing's prefix
s3:ExistingObjectTag/<key> A tag on the object being accessed
s3:x-amz-server-side-encryption, s3:x-amz-server-side-encryption-aws-kms-key-id The encryption a write asks for
obio:CredentialType Permanent, Sts (vended), AssumedRole or Anonymous
<issuer>:<claim> In a role's trust policy, the OIDC token's claims (see STS)

Every IAM condition operator is supported, with IfExists, ForAnyValue:/ForAllValues: and Null. A policy using an unknown operator is refused when it's saved.


Anonymous Access

An unsigned request is anonymous. It reaches only what a bucket policy grants to everyone, and new buckets block that until their owner lifts the block. See Public Access.

--no-auth turns authentication off entirely, so every request is allowed. Use it only for local testing.


Client Configuration

AWS CLI

# Configure credentials
aws configure
# Enter: Access Key ID, Secret Access Key, Region, Output format

# Or use environment variables
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=wJalr...
export AWS_DEFAULT_REGION=us-east-1

# Use with IOLake
aws s3 --endpoint-url http://localhost:9000 ls

Boto3 (Python)

import boto3

s3 = boto3.client(
    's3',
    endpoint_url='http://localhost:9000',
    aws_access_key_id='AKIA...',
    aws_secret_access_key='wJalr...',
    region_name='us-east-1'
)

# List buckets
response = s3.list_buckets()

AWS SDK for Rust

use aws_config::BehaviorVersion;
use aws_sdk_s3::Client;

let config = aws_config::defaults(BehaviorVersion::latest())
    .endpoint_url("http://localhost:9000")
    .load()
    .await;

let client = Client::new(&config);

let buckets = client.list_buckets().send().await?;

Security Best Practices

  1. Use HTTPS in Production
  2. Configure TLS certificates
  3. Redirect HTTP to HTTPS

  4. Rotate Access Keys

  5. Create new keys periodically
  6. Delete old keys after transition

  7. Use Least Privilege

  8. Grant minimum required permissions
  9. Use specific resource ARNs

  10. Enable Logging

  11. Track authentication failures
  12. Monitor for suspicious activity

  13. Secure Credential Storage

  14. Never hardcode credentials
  15. Use environment variables or secrets managers