herald: Orchestrating object storage services
Herald is an S3 proxy that supports:
- Protocol translation (S3 to S3, S3 to Swift).
- Backend routing based on bucket names.
- Flexible bucket mapping with glob support.
Run Herald in Docker with env-only config (no YAML). Point it at an S3-compatible backend (e.g. MinIO) and use any S3 client against Herald.
# Start Herald (default backend: S3 at host's MinIO). Port 3000.
docker run -p 3000:3000 \
-e HERALD_DEFAULT_PROTOCOL=s3 \
-e HERALD_DEFAULT_ENDPOINT=http://host.docker.internal:9000 \
-e HERALD_DEFAULT_REGION=us-east-1 \
-e HERALD_DEFAULT_ACCESS_KEY_ID=minioadmin \
-e HERALD_DEFAULT_SECRET_ACCESS_KEY=minioadmin \
ghcr.io/expnt/herald:latestUse the AWS CLI (or any S3 client) with Herald as the endpoint. The S3 API is
mounted at /s3; use path-style so bucket and key are in the path.
# List buckets via Herald
aws s3 ls --endpoint-url http://localhost:3000/s3
# List objects in a bucket
aws s3 ls --endpoint-url http://localhost:3000/s3 s3://my-bucket/Deployment resources:
- Images: ghcr.io/expnt/herald
- Helm chart: chart/ for Kubernetes.
Herald is configured via a YAML file (typically herald.yaml). The
configuration defines backends and how incoming requests are routed to them.
# Optional: require S3 SigV4 auth for incoming requests (see Auth section)
auth:
accessKeysRefs: [admin, readonly]
backends:
# Unique identifier for the backend
minio_stg_aa:
# Backend protocol: "s3" or "swift"
protocol: s3
# Default config values for backend
endpoint: http://127.0.0.1:9000
region: us-east-1
credentials:
accessKeyId: minioadmin
secretAccessKey: minioadmin
# Optional: auth to access buckets that route to this backend
# (bucket > backend > global)
auth:
accessKeysRefs: [app1]
# Bucket routing rules.
# Can be:
# 1. "*" to match all buckets not claimed by other backends
# 2. A glob pattern like "logs-*"
# 3. A map of bucket definitions for granular control
buckets:
# Simple bucket mapping (inherits backend settings)
my-bucket: {}
# Mapping with overrides; bucket-level auth overrides backend/global
external-data:
# Map proxy bucket "external-data" to backend bucket "data-v1"
bucket_name: data-v1
# Backend overrides for this specific bucket
endpoint: http://special-endpoint:9000
region: us-west-2
auth:
accessKeysRefs: [ci]
# Glob pattern support within the map
"test-*":
region: us-east-1
# Example Swift backend
swift_prd_bb:
protocol: swift
auth_url: http://keystone.example.com/v3
region: RegionOne
# Optional: map all buckets in this backend to a specific container
container: my-fixed-container
credentials:
username: my-user
password: my-password
project_name: my-project
user_domain_name: Default
project_domain_name: Default
# Route all archive buckets to Swift
buckets: "archive-*"
cors:
# Global CORS defaults
allowedOrigins: ["*"]
allowedMethods: ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"]
allowedHeaders: ["*"]
exposedHeaders: ["*"]
maxAge: 3600
credentials: falseWhen a request comes in for a bucket (e.g., GET /my-bucket/file.txt), Herald
resolves the backend using the following priority:
- Direct match: Looks for
my-bucketin all backends'bucketsmaps. - Glob match (map): Looks for glob patterns (like
test-*) in all backends'bucketsmaps. - Glob match (string): If a backend has
buckets: "string-*", it checks if the bucket name matches that pattern.
When several backends could match (e.g. two globs), the first backend in config order wins.
Herald can verify incoming S3 requests using AWS Signature Version 4 (SigV4). When auth is configured, only requests signed with one of the configured access keys are accepted. Credentials are never stored in the config file; you reference them by name (refs) and supply the actual keys via environment variables.
Auth is resolved at three levels with the same precedence as CORS: Bucket >
Backend > Global. The most specific definition wins (e.g. a bucket’s auth
overrides its backend’s auth).
At each level you set auth.accessKeysRefs: a list of ref names (strings). Each
ref maps to a pair of env vars:
HERALD_AUTH_<REF>_ACCESS_KEY_ID— access key idHERALD_AUTH_<REF>_SECRET_KEY— secret key
<REF> is the ref name in UPPERCASE (e.g. ref app1 →
HERALD_AUTH_APP1_ACCESS_KEY_ID). Only refs that have both env vars set are
used; missing refs are skipped.
If no auth is defined at any level for a request, Herald does not perform
SigV4 verification and the request is not gated by these credentials.
Herald supports fine-grained CORS control at three levels with the following precedence: Bucket > Backend > Global.
- Global: Defined at the root of the config file under
cors. - Backend: Defined within a backend block under
cors. Overrides global settings. - Bucket: Defined within a bucket definition under
cors. Overrides both backend and global settings.
If no CORS configuration is provided at any level, CORS is disabled and
Herald will not add any CORS-related headers to responses. Preflight OPTIONS
requests will be passed through to the backend.
If you enable CORS by providing configuration at any level, the following defaults are applied for any omitted fields:
| Field | Default Value | Description |
|---|---|---|
maxAge |
3600 |
Max age in seconds for preflight results |
allowedMethods |
GET, PUT, POST, DELETE, HEAD, OPTIONS |
Allowed HTTP methods |
allowedHeaders |
(Mirrors request) | Defaults to mirroring Access-Control-Request-Headers |
credentials |
false |
Whether to allow credentials |
allowedOrigins |
(None) | Headers only added if Origin matches an entry |
Example with overrides:
cors: # Global defaults
allowedOrigins: ["*"]
credentials: false
backends:
prod:
protocol: s3
cors: # Backend-level override
allowedOrigins: ["https://app.example.com"]
credentials: true
buckets:
assets:
cors: # Bucket-level override
allowedOrigins: ["https://cdn.example.com"]Configuration can be supplied or overridden via environment variables; env is
merged with YAML at load time (env wins for the same path). All config-related
vars use the HERALD_ prefix. Naming: HERALD_<KEY> applies to the default
backend or global (for top-level keys like auth/CORS); HERALD_<BACKEND>_<KEY>
applies to that backend. Keys are normalised (e.g. AUTH_URL → auth_url;
credential keys go under credentials).
| Var | Purpose | Default |
|---|---|---|
HERALD_CONFIG_PATH |
Path to YAML config file | herald.yaml |
HERALD_LOG_LEVEL |
Log level (e.g. DEBUG, INFO) |
(none; INFO) |
PORT |
HTTP server port | 3000 |
HERALD_AUTH_ACCESS_KEYS_REFS |
Global auth: comma-separated ref names | — |
HERALD_<BACKEND>_AUTH_ACCESS_KEYS_REFS |
Backend auth: comma-separated ref names | — |
HERALD_AUTH_<REF>_ACCESS_KEY_ID |
Access key for auth ref (SigV4) | — |
HERALD_AUTH_<REF>_SECRET_KEY |
Secret key for auth ref (SigV4) | — |
HERALD_PROTOCOL, HERALD_ENDPOINT, HERALD_REGION, HERALD_BUCKETS |
Default backend (S3) | — |
HERALD_<BACKEND>_PROTOCOL, HERALD_<BACKEND>_ENDPOINT, HERALD_<BACKEND>_REGION, HERALD_<BACKEND>_BUCKETS |
Backend (S3) | — |
HERALD_<BACKEND>_ACCESS_KEY_ID, HERALD_<BACKEND>_SECRET_ACCESS_KEY |
Backend S3 credentials | — |
HERALD_<BACKEND>_AUTH_URL, HERALD_<BACKEND>_CONTAINER, HERALD_<BACKEND>_USERNAME, HERALD_<BACKEND>_PASSWORD, HERALD_<BACKEND>_PROJECT_NAME, HERALD_<BACKEND>_USER_DOMAIN_NAME, HERALD_<BACKEND>_PROJECT_DOMAIN_NAME |
Backend (Swift) | — |
HERALD_CORS_ALLOWED_ORIGINS, HERALD_CORS_ALLOWED_METHODS, HERALD_CORS_ALLOWED_HEADERS, HERALD_CORS_EXPOSED_HEADERS, HERALD_CORS_MAX_AGE, HERALD_CORS_CREDENTIALS |
Global CORS (lists comma-separated) | — |
HERALD_<BACKEND>_CORS_<KEY> |
Backend CORS (same keys as above) | — |
- Health:
GET /healthreturns{ "status": "ok" }. Use it for liveness/readiness. - Logging: Set
HERALD_LOG_LEVEL(e.g.DEBUG,INFO) to control log verbosity. - Tracing: Optional OpenTelemetry: set
OTEL_EXPORTER_OTLP_ENDPOINT(andOTEL_SERVICE_NAME, defaultherald) to export traces to an OTLP collector.
- Docker: Images are published at
ghcr.io/expnt/herald. Use env vars (see table
above) or mount a
herald.yamland setHERALD_CONFIG_PATH. - Kubernetes: A Helm chart is in chart/.
Herald is an S3 proxy focused on routing, protocol translation, and core object operations. The following are not currently supported (or are partial):
- Bucket subresources: Bucket policies (
?policy), lifecycle (?lifecycle), versioning config (?versioning), tagging (?tagging), ACLs (?acl), website (?website), public access block (?publicAccessBlock), replication, logging, inventory, metrics, ownership controls. - Object subresources: Object ACLs, tagging, legal hold, retention (Object
Lock), S3 Select. Multi-Object Delete (
POST ?delete) is not implemented. Copy Object (PUTwithx-amz-copy-source) is supported. - Object operations: GetObjectAttributes (
?attributes) is not implemented. Checksum headers (x-amz-checksum-*) and conditional requests (If-Match, etc.) are not fully supported. - List enhancements:
encoding-type=url, special delimiter handling, ListObjectsV2FetchOwner, unordered listing behavior may not match S3. - Auth & IAM: No IAM policy evaluation, STS, or web identity federation. Anonymous access for public buckets/objects is not implemented. Invalid or missing SigV4 auth may not return 403/400 as expected.
- Validation & protocol: Bucket naming rules (length, format) are not
strictly enforced. HTTP 100 Continue (
Expect: 100-continue) is not supported. Some error codes and response fields may differ from S3.
For the full list of missing functionality and focus tests (from the s3-tests suite), see TODO.md.
