Aether protects against:
- Unauthorized agent impersonation
- Request replay and tampering
- Cross-tenant data access
- SSRF via external HTTP monitors
- Config rollback / MITM on agent config
- Credential theft from local agent storage
Out of scope for MVP: mTLS, OAuth/OIDC, 2FA, auto-update of agent binaries.
- All production traffic uses HTTPS via
rustlswith TLS 1.3 minimum - No
native-tls, no OpenSSL - Agent HTTP client built with
reqwest+rustls-tls - Server terminates TLS directly or behind modern reverse proxy (Caddy/Nginx with TLS 1.3)
Feature development-insecure-http allows plain HTTP only in tests and local dev. CI release builds use --no-default-features verification to ensure this feature is absent.
Agents do not use Bearer tokens alone. Every request is signed.
- Operator creates a server and generates a one-time enrollment token (Argon2 hash stored server-side, 15 min TTL)
- Agent runs
monitor-agent enroll --server-url https://… --token … - Agent generates Ed25519 keypair locally; only public key sent to server
- Token sent in HTTPS body, never query string
- Server creates
agent+agent_credentials, revokes token - Enrollment response signed with server signing key (
X-Server-Signatureheaders) - Agent stores
identity.jsonmode0600
Required headers on every agent API call:
X-Agent-ID: <uuid>
X-Request-ID: <uuidv7>
X-Timestamp: <RFC3339 ms UTC>
X-Nonce: <base64url, 16+ random bytes>
X-Content-SHA256: <base64url SHA-256 of wire body>
X-Signature: <base64url Ed25519>
X-Protocol-Version: 1Canonical string (UTF-8, \n separated, no trailing newline):
{METHOD}
{PATH}
{CANONICAL_QUERY}
agent_id={uuid}
request_id={uuid}
timestamp={RFC3339}
nonce={base64url}
content-type={lowercase}
content-encoding={identity|zstd}
content-sha256={base64url}
protocol-version={u16}
- TLS 1.3 established
- Rate limit check
- Read raw body bytes
- Compute SHA-256, compare
X-Content-SHA256(constant-time) - Validate header presence and formats
- Check timestamp within skew window (default ±5 min)
- Lookup agent credential (public key) — fail if revoked
- Build canonical string, verify Ed25519 signature before JSON parse
- Insert nonce
(agent_id, nonce)— conflict →NONCE_ALREADY_USED - Insert/check
request_iduniqueness - Deserialize JSON body
- Execute handler
Critical: failed signature still consumes nonce (anti brute-force replay).
- Passwords hashed with Argon2id (memory-hard parameters)
- Access tokens: short-lived JWT (15 min default)
- Refresh tokens: opaque, stored as SHA-256 hash in
user_sessions - Separate signing keys for access vs refresh JWTs
- Logout revokes refresh session
Roles per organization membership:
| Role | Permissions |
|---|---|
owner |
Full access, billing, delete org |
admin |
Manage users, servers, monitors, configs |
operator |
Manage servers/monitors, view incidents |
viewer |
Read-only |
Middleware extracts organization_id from route/context and verifies role ≥ required.
Server-signed configs prevent tampering:
- Config content canonicalized to JSON bytes
- SHA-256 checksum stored and returned
- Ed25519 signature over
{version, checksum, issued_at, …}usingserver_signing_keys - Agent verifies 10 rules before apply:
- Signature valid against known server keys
- Checksum matches content
version > current_version(anti-rollback)not_before ≤ now ≤ expires_atagent_idmatches identity- Schema version supported
- Config passes domain validation
- Atomic file swap + scheduler rebuild
- Ack sent only after successful apply
PostgreSQL tables (not cache-only):
agent_request_nonces (agent_id, nonce) PRIMARY KEY
agent_processed_batches (agent_id, batch_id, payload_type) PRIMARY KEYNonce purge worker removes entries older than 24h.
Before outbound HTTP/TCP checks:
- Resolve DNS
- Block: loopback, link-local, metadata
169.254.169.254, RFC1918 (unless org flagallow_private_targets) - Re-resolve after redirects (max 3)
- Response body limit (default 1 MB)
- Timeout enforced per monitor config
[Service]
User=monitor-agent
Group=monitor-agent
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/monitor-agent
CapabilityBoundingSet=
AmbientCapabilities=- Optional, disabled by default in config
- Requires membership in
dockergroup — document privilege risk - Read-only container inspection only
| File | Mode |
|---|---|
identity.json |
0600 |
config.json |
0640 |
queue.sqlite |
0600 |
All sensitive actions logged to audit_logs:
- User login/logout
- Agent enrollment
- Config changes
- Role changes
- API key creation/revocation
Retention: 1 year default.
| Secret | Storage |
|---|---|
| DB password | env / secrets manager |
| JWT signing keys | env, rotated periodically |
| Server signing private keys | server_signing_keys.private_key_encrypted |
| Agent private key | local identity.json only |
| Enrollment tokens | Argon2 hash in DB |
Mandatory integration tests in tests/integration/security_agent_auth.rs:
- Valid signature accepted
- Tampered body rejected
- Replayed nonce rejected
- Expired timestamp rejected
- Revoked credential rejected
- Config rollback rejected
development-insecure-httpabsent in release builds
- Revoke agent credential → immediate rejection on next request
- Rotate server signing keys → agents fetch new public keys on config poll
- Compromised enrollment token → single use + short TTL limits blast radius