FEATURES
🔐 Security Features
Encryption, secrets management, RBAC, API authentication, policy enforcement, audit integrity, email notifications, and safe state persistence.
📑 Table of Contents¶
- AES-256-GCM Encryption
- Setting the Encryption Key
- RBAC API Integration
- API Authentication
- CORS Protection
- Input Validation
- Secret Lifecycle
- Audit Trail Integrity
- File Permission Hardening
- Policy Engine
- Policy Gate on Deploy
- Confirmation Safety
- State File Locking
- Atomic State Persistence
- Kubernetes Resource Safety
- Rate Limiting
- Structured JSON Logging
- Webhook Delivery Security
- Email SMTP Notifications
- Cross-References
🔒 AES-256-GCM Encryption¶
aether encrypts all secret values at rest using AES-256-GCM (Galois/Counter Mode), a widely trusted authenticated encryption algorithm.
How It Works¶
User Key (any length)
│
▼
┌───────────────────┐
│ SHA-256 Hash │ Derives a fixed 32-byte key from your key material
└───────────────────┘
│
▼
┌───────────────────┐
│ Random 12-byte │ Generated per encryption operation via OsRng
│ Nonce │ (cryptographically secure)
└───────────────────┘
│
▼
┌───────────────────┐
│ AES-256-GCM │ Authenticated encryption
│ Encrypt │ Produces ciphertext + authentication tag
└───────────────────┘
│
▼
┌───────────────────┐
│ nonce ‖ cipher │ Nonce prepended to ciphertext
│ base64-encode │ Stored as a single base64 string
└───────────────────┘
Key Properties¶
| Property | Detail |
|---|---|
| Algorithm | AES-256-GCM (authenticated encryption with associated data) |
| Key derivation | SHA-256 hash of user-provided key → 32-byte AES key |
| Nonce | Random 12-byte nonce generated per encryption via OsRng |
| Output format | base64(nonce ‖ ciphertext ‖ auth_tag) |
| Nonce uniqueness | Same plaintext encrypted twice produces different ciphertexts |
| Integrity | GCM authentication tag detects tampering |
| Wrong key | Decryption fails: "AES-256-GCM decryption failed (wrong key or corrupted data)" |
| Short ciphertext | Rejected: "Invalid AES ciphertext: too short (missing nonce)" |
Development Fallback¶
When AETHER_SECRET_KEY is not set, aether derives a deterministic key from machine identity (hostname/username) and logs a warning. This key is stable across restarts but is discoverable by other users on the same machine.
⚠️ AETHER_SECRET_KEY not set — generating deterministic key from machine identity.
Set AETHER_SECRET_KEY for stable production encryption.
All encryption uses AES-256-GCM regardless of key source. The legacy Obfuscate (XOR) method is no longer supported — attempting to decrypt XOR-encrypted values produces a clear upgrade message.
Encryption Methods¶
| Method | When Used | Security Level | Icon |
|---|---|---|---|
Aes256 |
Always (both explicit key and machine-derived key) | 🟢 Production-grade | 🔒 |
Obfuscate |
Legacy (rejected on decrypt with upgrade instructions) | 🔴 Deprecated | ⚠️ |
VaultRef |
External vault reference (not stored locally) | 🟢 Delegated to vault | 🏛️ |
The SecretValue.method field tracks which encryption was used per key, allowing mixed-method storage within a single secret.
🔑 Setting the Encryption Key¶
Set the AETHER_SECRET_KEY environment variable to enable AES-256-GCM encryption.
# Set for the current session
export AETHER_SECRET_KEY="my-production-secret-key-at-least-16-chars"
# Or in your shell profile (~/.bashrc, ~/.zshrc)
echo 'export AETHER_SECRET_KEY="your-key-here"' >> ~/.bashrc
# Or per-command
AETHER_SECRET_KEY="key" aether secrets set db-creds password "s3cret"
Key Requirements¶
| Requirement | Detail |
|---|---|
| Minimum length | 16 bytes minimum enforced. Keys shorter than 16 bytes produce a warning. |
| Recommended length | 32+ characters for maximum entropy |
| Consistency | Same key must be used for both encryption and decryption |
| Rotation | Changing the key invalidates all previously encrypted values |
| Empty string | Falls back to machine-derived key (logged warning) |
| Character set | Any UTF-8 bytes are accepted (hashed via SHA-256 to 32 bytes) |
Production Recommendations¶
| Recommendation | Why |
|---|---|
| Store the key in a secrets manager (HashiCorp Vault, AWS Secrets Manager) | Avoid plaintext keys on disk |
| Use different keys per environment | Limit blast radius of key compromise |
| Rotate the key periodically | Limit exposure window |
| Never commit the key to source control | Prevents credential leakage |
| Use at least 32 characters | Maximizes entropy before hashing |
🔄 Secret Lifecycle¶
Create a Secret¶
aether secrets create db-credentials --namespace production
Creates an empty secret container with a default rotation policy:
| Policy Field | Default Value |
|---|---|
interval_days |
90 |
max_age_days |
365 |
notify_before_days |
14 |
Set a Key-Value Pair¶
aether secrets set db-credentials password "s3cret123"
aether secrets set db-credentials username "admin"
Each set operation:
- ✅ Encrypts the value using the active encryption method (AES-256-GCM or XOR fallback)
- ✅ Increments the key's version number (starts at 1)
- ✅ Updates the secret's
updated_attimestamp - ✅ Logs a
WRITEevent to the access audit trail
If the key already exists, its value is updated and the version increments.
Get a Decrypted Value¶
aether secrets get db-credentials password
# Output: s3cret123
Each get operation:
- Looks up the secret and key
- Decrypts using the method recorded in
SecretValue.method - Logs a
READevent to the access audit trail
Rotate a Secret Value¶
aether secrets set db-credentials password "new-s3cret-456"
Rotation is equivalent to a set with the same key name. The operation:
- Encrypts the new value
- Increments the version number
- Updates
last_rotatedtimestamp - Logs a
ROTATEevent
Delete a Key¶
Removing a specific key within a secret:
- Removes the key from the secret's data map
- Updates the
updated_attimestamp - Logs a
DELETEevent to the access audit trail
Delete an Entire Secret¶
Removing the entire secret container removes all keys, the rotation policy, and the access log.
Audit Rotation Status¶
aether secrets audit
Checks all secrets against their rotation policies and produces alerts:
| Severity | Condition | Message Example |
|---|---|---|
| ⚠️ Warning | Key age exceeds max_age_days - notify_before_days |
"Key 'password' in 'db-creds' expires in 10 days" |
| 🔴 Critical | Key age exceeds max_age_days |
"Key 'password' in 'db-creds' is 400 days old (max: 365)" |
List All Secrets¶
aether secrets list
Displays a table with name, namespace, key count, last updated, and rotation status. Values are never exposed in list output.
╭──────────────┬────────────┬──────┬─────────────────────┬──────────╮
│ Name │ Namespace │ Keys │ Updated │ Rotation │
├──────────────┼────────────┼──────┼─────────────────────┼──────────┤
│ db-creds │ production │ 2 │ 2026-01-15T10:00:00 │ OK │
│ api-keys │ staging │ 3 │ 2026-01-10T08:30:00 │ ⚠ Yes │
╰──────────────┴────────────┴──────┴─────────────────────┴──────────╯
Access Audit Trail¶
Every secret operation is logged in the secret's access_log:
| Field | Type | Description |
|---|---|---|
timestamp |
string (RFC 3339) | When the access occurred |
action |
SecretAction | READ, WRITE, DELETE, or ROTATE |
key |
string | Which key was accessed |
actor |
string | Who performed the action ("cli" for CLI, custom for API via *_with_actor() methods) |
The *_with_actor() variants (set_with_actor, get_and_log_with_actor, delete_key_with_actor, rotate_with_actor) allow the API server and other callers to pass the real actor identity for proper audit attribution.
🛂 RBAC API Integration¶
Aether supports role-based access control (RBAC) for API keys via the RbacStore. Each API key is assigned one of three roles that determine which operations are permitted.
Roles¶
| Role | Permissions | Description |
|---|---|---|
| Admin | Full access | Create/revoke keys, mutate workloads, read all data |
| Operator | Read + mutate workloads | Start, stop, delete, migrate workloads; cannot manage RBAC keys |
| Viewer | Read-only | List workloads, view logs, check health; no mutations |
API Endpoints¶
| Method | Path | Description | Required Role |
|---|---|---|---|
GET |
/api/rbac/keys |
List all RBAC keys (names and roles, no raw keys) | Admin |
POST |
/api/rbac/keys |
Create a new RBAC key with a specified role | Admin |
POST |
/api/rbac/keys/revoke |
Revoke an existing RBAC key | Admin |
How It Works¶
The API middleware checks incoming requests in this order:
- RBAC key check -- If the
Authorization: Bearer <key>header matches a key in theRbacStore, the request is authorized with the corresponding role. - Fallback to
AETHER_API_KEY-- If no RBAC key matches butAETHER_API_KEYis set and the Bearer token matches, the request is authorized with Admin-equivalent access. This preserves backward compatibility. - No auth configured -- If neither RBAC keys nor
AETHER_API_KEYare configured, all endpoints are public (local development mode).
Creating RBAC Keys¶
# Create an admin key
curl -X POST http://localhost:5090/api/rbac/keys \
-H "Authorization: Bearer <admin-key>" \
-H "Content-Type: application/json" \
-d '{"name": "ci-pipeline", "role": "operator"}'
# List all keys
curl http://localhost:5090/api/rbac/keys \
-H "Authorization: Bearer <admin-key>"
# Revoke a key
curl -X POST http://localhost:5090/api/rbac/keys/revoke \
-H "Authorization: Bearer <admin-key>" \
-H "Content-Type: application/json" \
-d '{"name": "ci-pipeline"}'
Endpoint Authorization Matrix¶
| Endpoint Category | Admin | Operator | Viewer |
|---|---|---|---|
| List/get workloads, logs, health | Yes | Yes | Yes |
| Start/stop/delete/migrate workloads | Yes | Yes | No |
| RBAC key management | Yes | No | No |
| Secrets management | Yes | Yes | No |
| Backup/restore | Yes | Yes | No |
🔑 API Authentication¶
The REST API server supports Bearer token authentication via the AETHER_API_KEY environment variable.
Setup¶
# Set an API key to enable authentication
export AETHER_API_KEY="my-secret-api-key-at-least-32-chars"
# Start the server
aether serve
How It Works¶
| Scenario | Behavior |
|---|---|
AETHER_API_KEY is set and non-empty |
All /api/* endpoints require Authorization: Bearer <key> header |
AETHER_API_KEY is not set or empty |
All endpoints are public (local development mode) |
/health and / (dashboard) |
Always public, no authentication required |
| Invalid or missing token | 401 Unauthorized response |
Example API Call¶
# With authentication
curl -H "Authorization: Bearer my-secret-api-key" \
http://localhost:5090/api/workloads
# Health check (always public)
curl http://localhost:5090/health
Startup Messages¶
🌐 Starting API server on http://127.0.0.1:5090
📊 Dashboard: http://127.0.0.1:5090
📋 API Health: http://127.0.0.1:5090/health
🔐 API authentication enabled (AETHER_API_KEY)
Without authentication:
⚠️ No AETHER_API_KEY set — API is unauthenticated. Set AETHER_API_KEY for production use.
🌐 CORS Protection¶
The API server restricts Cross-Origin Resource Sharing (CORS) to the server's own origin.
| Setting | Value |
|---|---|
| Allowed origin | http://{host}:{port} (e.g., http://127.0.0.1:5090) |
| Allowed methods | GET, POST, DELETE |
| Allowed headers | Any |
This prevents cross-origin attacks from untrusted browser contexts while allowing the built-in dashboard to function.
🛡️ Input Validation¶
Workload Names (API)¶
API endpoints validate workload names against DNS-1123 format:
| Rule | Detail |
|---|---|
| Characters | Lowercase alphanumeric (a-z, 0-9), hyphens (-), dots (.) |
| Length | 1–253 characters |
| Start/end | Must start and end with alphanumeric |
| Rejected | Uppercase, spaces, /, \, .., \0, special characters |
Path Validation¶
| Context | Rules |
|---|---|
| API spec paths | Must be relative, no .. traversal, no absolute paths |
| CLI template output | Blocks system directories (/etc, /proc, /sys, /dev, /boot, /sbin) |
| Backup names | No /, \, .., or null bytes |
📋 Audit Trail Integrity¶
Every audit event includes a SHA-256 integrity hash computed over its fields:
hash = SHA-256("aether-audit-integrity-{id}|{timestamp}|{action}|{workload}|{result}|{message}")
| Property | Detail |
|---|---|
| Hash algorithm | SHA-256 (hex-encoded) |
| Fields covered | id, timestamp, action, workload, result, message |
| Verification | AuditLog::verify_event_integrity(event) returns true if hash matches |
| Legacy events | Events without hashes are accepted (backward compatible) |
| Tamper detection | Modified events produce a hash mismatch |
Audit Integrity Verification API¶
The REST API exposes a GET /api/audit/verify endpoint that checks every audit event against its stored SHA-256 hash and returns an integrity report:
curl http://localhost:5090/api/audit/verify
{
"total": 142,
"verified": 142,
"tampered": 0,
"integrity": "ok",
"tampered_events": []
}
If any event has been modified, integrity returns "compromised" and tampered_events lists the affected event IDs.
📁 File Permission Hardening¶
Sensitive files are created with restrictive permissions on Unix systems:
| File Type | Permission | Octal | Description |
|---|---|---|---|
| Backup files | Owner read/write only | 0o600 |
State backups may contain workload config |
| Snapshot files | Owner read/write only | 0o600 |
Pre-deploy snapshots contain full workload state |
| Secrets store | Default (from state atomic write) | — | Protected by advisory file locking |
On non-Unix platforms (Windows), default filesystem permissions apply.
✅ Confirmation Safety¶
Destructive operations (delete, rollback, etc.) require explicit confirmation:
| Mode | Behavior |
|---|---|
| Interactive | Prompts [y/N] — defaults to No |
--yes flag |
Auto-confirms all prompts |
--quiet mode |
Defaults to No (safe) — requires --yes to auto-confirm |
--json mode |
Defaults to No (safe) — requires --yes to auto-confirm |
This prevents accidental destructive operations in non-interactive contexts (scripts, CI pipelines) unless explicitly opted in with --yes.
🛡️ Policy Engine¶
The policy engine evaluates workload specs against a set of rules before deployment. Two built-in policy sets are provided: production (strict) and development (relaxed).
10 Rule Types¶
| # | Rule | Production | Development | Description |
|---|---|---|---|---|
| 1 | Resource limits | ✅ Required | ⚠️ Warning | CPU, memory, and storage must be specified |
| 2 | Image registry | ✅ Required | ❌ Off | Image must come from an approved registry |
| 3 | Health checks | ✅ Required | ❌ Off | Liveness/readiness probes must be defined |
| 4 | Replicas | ✅ Minimum 2 | ❌ Off | High availability requires multiple replicas |
| 5 | Labels/tags | ✅ Required | ❌ Off | owner and project metadata must be set |
| 6 | Network policy | ✅ Required | ❌ Off | Network isolation must be configured |
| 7 | Secret references | ✅ Validated | ❌ Off | Referenced secrets must exist |
| 8 | Resource caps | ✅ Enforced | ❌ Off | Resources must not exceed maximum thresholds |
| 9 | Image tag | ✅ No :latest |
❌ Off | Immutable tags required in production |
| 10 | Namespace | ✅ Required | ❌ Off | Workload must specify a namespace |
Policy Sets¶
Production (--policy production):
- All 10 rules enforced
- Violations with Error severity block deployment
- Default policy set
Development (--policy development):
- Only resource limits checked (as warnings)
- Designed for fast iteration
- Nothing blocks deployment
CLI Usage¶
# Check against production policies (default)
aether policy-check
aether -s ./my-app.yaml policy-check
# Check against development policies
aether policy-check --policy development
🚦 Policy Gate on Deploy¶
When you run a deploy command (aether run, aether compose up, aether deploy), the policy engine automatically evaluates the workload spec before proceeding.
Gate Behavior¶
| Scenario | Result |
|---|---|
| All policies pass | ✅ Deployment proceeds normally |
| Any policy fails (Error severity) | ❌ Deployment is blocked with violation details |
--skip-policy flag |
⚠️ Policy checks are skipped entirely |
| Development policy set | ⚠️ Warnings only, deployment proceeds |
Skipping Policy Checks¶
# Skip policy gate (use with caution)
aether --skip-policy run --runtime kube
# Also works with compose
aether --skip-policy compose up
# And batch deploy
aether --skip-policy deploy ./specs/
REST API Policy Check¶
curl -X POST http://localhost:5090/api/policy/check \
-H "Content-Type: application/json" \
-d '{
"spec": { ... },
"policy_set": "production"
}'
| Field | Required | Default | Description |
|---|---|---|---|
spec |
Yes | -- | Workload spec as JSON |
policy_set |
No | "production" |
"production" or "development" |
🔐 State File Locking¶
aether uses advisory file locking (Unix flock) to prevent concurrent write corruption when multiple CLI processes or the API server modify state simultaneously.
How It Works¶
1. Create/open lock file: state.json.lock
2. Acquire exclusive advisory lock (LOCK_EX, blocking)
3. Write state to temporary file
4. Rename temporary file to state file
5. Release lock (file handle dropped)
Lock Properties¶
| Property | Detail |
|---|---|
| Lock type | Exclusive (LOCK_EX) -- only one writer at a time |
| Blocking | Yes -- concurrent processes wait rather than fail immediately |
| Lock file | <state-file>.lock (e.g., state.json.lock) |
| Release | Automatic when the file handle is dropped |
Error Handling¶
If the lock cannot be acquired:
Error: failed to acquire state file lock (is another aether process running?)
Platform Support¶
| Platform | Behavior |
|---|---|
| Linux / macOS | Full flock-based advisory locking via libc::flock |
| Windows / Other | No-op (lock always succeeds) |
⚛️ Atomic State Persistence¶
State writes use an atomic write pattern (write-to-temp + rename) to prevent corruption from crashes or power loss.
Write Sequence¶
1. Serialize state to pretty-printed JSON
2. Create parent directory if needed (mkdir -p)
3. Acquire advisory lock on state.json.lock
4. Write JSON to state.json.tmp (temporary file in same directory)
5. Rename state.json.tmp → state.json (atomic on POSIX filesystems)
6. Release lock (drop file handle)
Why This Matters¶
| Failure Scenario | Without Atomic Write | With Atomic Write |
|---|---|---|
| Crash during write | Corrupted/truncated JSON | Previous valid state preserved |
| Power loss mid-write | Partial data on disk | Either old or new state, never partial |
| Concurrent reads | May read partial data | Always reads a complete, valid file |
| Disk full | Half-written data | Temp file write fails, original preserved |
Files Involved¶
| File | Purpose |
|---|---|
~/.aether/state.json |
Current workload state (the source of truth) |
~/.aether/state.json.tmp |
Temporary write target (renamed on success) |
~/.aether/state.json.lock |
Advisory lock file (prevents concurrent writes) |
☸️ Kubernetes Resource Safety¶
Timeouts¶
All Kubernetes API calls are wrapped with a 5-minute timeout to prevent indefinite hangs when the API server is unresponsive:
| Operation | Timeout | Behavior on Timeout |
|---|---|---|
| Pod create/delete | 5 min | Error with descriptive message |
| Service/Ingress/PVC create | 5 min | Error + cleanup of created resources |
| ConfigMap/Secret create | 5 min | Error + cleanup |
| Pod logs | 5 min | Error returned to caller |
This matches the Podman adapter's 10-minute timeout for container commands.
Resource Cleanup on Failure¶
When pod creation fails after supporting resources (ConfigMap, Secret, PVC, Service, Ingress) have already been created, aether automatically cleans up orphaned resources:
1. Create ConfigMap ✓ (tracked)
2. Create Secret ✓ (tracked)
3. Create PVC ✓ (tracked)
4. Create Service ✓ (tracked)
5. Create Pod ✗ FAILED
→ Clean up: Service, PVC, Secret, ConfigMap (best-effort)
Conflict Handling¶
Resource creation distinguishes 409 Conflict (resource already exists) from other errors:
| HTTP Status | Behavior |
|---|---|
409 Conflict |
Logged as info, deployment continues |
403 Forbidden |
Error returned, deployment aborted |
500 Server Error |
Error returned, cleanup triggered |
| Network timeout | Error returned, cleanup triggered |
Namespace Validation¶
Both workload name and namespace are validated as DNS-1123 labels before any Kubernetes API calls.
🚦 Rate Limiting¶
The API server enforces a 200 concurrent request limit using tower middleware. When the limit is exceeded, additional requests receive a 503 Service Unavailable response until in-flight requests complete. This protects the server from resource exhaustion during traffic spikes or misbehaving clients.
| Property | Detail |
|---|---|
| Limit | 200 concurrent requests |
| Scope | All routes (API and dashboard) |
| Exceeded behavior | 503 Service Unavailable |
| Implementation | tower concurrency limit middleware |
📝 Structured JSON Logging¶
Set the AETHER_LOG_FORMAT environment variable to json to switch from human-readable log output to structured JSON lines:
AETHER_LOG_FORMAT=json aether serve
Each log entry is a single JSON object with timestamp, level, message, and target fields, making it suitable for ingestion by log aggregation systems such as Elasticsearch, Grafana Loki, or Datadog.
| Variable | Value | Effect |
|---|---|---|
AETHER_LOG_FORMAT |
json |
Structured JSON log output |
AETHER_LOG_FORMAT |
unset or any other value | Default human-readable output |
📡 Webhook Delivery Security¶
aether supports webhook notifications for events. Channels are managed with aether webhook commands.
Security Considerations¶
| Aspect | Detail |
|---|---|
| Transport | Use HTTPS URLs for webhook endpoints |
| Authentication | Include authentication tokens in webhook headers or URL parameters |
| HTTP method | Configurable: POST (default) or GET |
| Severity filter | Only events at or above the configured severity trigger delivery (info, warning, error, critical) |
| Retry queue | Failed deliveries are queued for retry |
Webhook Commands¶
# Add a webhook channel
aether webhook add alerts https://hooks.example.com/notify \
--method POST \
--severity warning
# List all channels
aether webhook list
# Send a test notification
aether webhook test alerts
# View pending deliveries in the retry queue
aether webhook queue
# Force-retry all queued webhooks now
aether webhook flush
# Remove a channel
aether webhook remove alerts
Severity Levels¶
| Level | Priority | Example Events |
|---|---|---|
info |
Lowest | Workload deployed, backup created |
warning |
Medium | Secret rotation due, resource warning |
error |
High | Deployment failed, health check failed |
critical |
Highest | Circuit breaker tripped, data corruption |
A webhook configured with --severity warning will receive warning, error, and critical events, but not info events.
📧 Email SMTP Notifications¶
Aether supports real SMTP email delivery for event notifications via the lettre crate. Configure an email notification channel using ChannelType::Email.
Configuration¶
Email channels are configured alongside other notification channels (Console, File, Webhook):
| Field | Required | Description |
|---|---|---|
smtp_host |
Yes | SMTP server hostname (e.g., smtp.gmail.com) |
smtp_port |
No | SMTP port (default: 587 for STARTTLS) |
smtp_username |
Yes | SMTP authentication username |
smtp_password |
Yes | SMTP authentication password |
from_address |
Yes | Sender email address |
to_addresses |
Yes | Array of recipient email addresses |
severity |
No | Minimum severity to trigger email (info, warning, error, critical) |
Security Considerations¶
| Aspect | Detail |
|---|---|
| Transport | Use TLS/STARTTLS for encrypted SMTP connections |
| Credentials | Store SMTP credentials in environment variables or secrets, not in config files |
| Rate limiting | Email channels respect the per-channel cooldown period to avoid inbox flooding |
| Severity filter | Only events at or above the configured severity trigger email delivery |
🔗 Cross-References¶
| Document | Relevance |
|---|---|
| Enterprise Security Guide | OIDC, isolation matrix, compliance roadmap |
| Deployment Topologies | HA and production layout |
| Compose Guide | Policy gate integration with compose deployments |
| Health Monitoring | Uptime tracking and health records |
| Plugin System | Custom runtime extensions |
| API Reference | Full REST API documentation |
| Quick Reference | Command cheat sheet |