Skip to content

Security: HostNatPro/Aether

Security

docs/security.md

Aether Security Model

Threat Model

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.

Transport Security

TLS 1.3 Only

  • All production traffic uses HTTPS via rustls with 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)

Development Exception

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.

Agent Authentication (Ed25519)

Agents do not use Bearer tokens alone. Every request is signed.

Enrollment

  1. Operator creates a server and generates a one-time enrollment token (Argon2 hash stored server-side, 15 min TTL)
  2. Agent runs monitor-agent enroll --server-url https://… --token …
  3. Agent generates Ed25519 keypair locally; only public key sent to server
  4. Token sent in HTTPS body, never query string
  5. Server creates agent + agent_credentials, revokes token
  6. Enrollment response signed with server signing key (X-Server-Signature headers)
  7. Agent stores identity.json mode 0600

Per-Request Signing

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: 1

Canonical 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}

Server Verification Pipeline (strict order)

  1. TLS 1.3 established
  2. Rate limit check
  3. Read raw body bytes
  4. Compute SHA-256, compare X-Content-SHA256 (constant-time)
  5. Validate header presence and formats
  6. Check timestamp within skew window (default ±5 min)
  7. Lookup agent credential (public key) — fail if revoked
  8. Build canonical string, verify Ed25519 signature before JSON parse
  9. Insert nonce (agent_id, nonce) — conflict → NONCE_ALREADY_USED
  10. Insert/check request_id uniqueness
  11. Deserialize JSON body
  12. Execute handler

Critical: failed signature still consumes nonce (anti brute-force replay).

User Authentication

  • 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

RBAC

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.

Remote Configuration Security

Server-signed configs prevent tampering:

  1. Config content canonicalized to JSON bytes
  2. SHA-256 checksum stored and returned
  3. Ed25519 signature over {version, checksum, issued_at, …} using server_signing_keys
  4. Agent verifies 10 rules before apply:
    • Signature valid against known server keys
    • Checksum matches content
    • version > current_version (anti-rollback)
    • not_before ≤ now ≤ expires_at
    • agent_id matches identity
    • Schema version supported
    • Config passes domain validation
  5. Atomic file swap + scheduler rebuild
  6. Ack sent only after successful apply

Anti-Replay

PostgreSQL tables (not cache-only):

agent_request_nonces (agent_id, nonce) PRIMARY KEY
agent_processed_batches (agent_id, batch_id, payload_type) PRIMARY KEY

Nonce purge worker removes entries older than 24h.

SSRF Protection (External Monitors)

Before outbound HTTP/TCP checks:

  1. Resolve DNS
  2. Block: loopback, link-local, metadata 169.254.169.254, RFC1918 (unless org flag allow_private_targets)
  3. Re-resolve after redirects (max 3)
  4. Response body limit (default 1 MB)
  5. Timeout enforced per monitor config

Agent Host Hardening

systemd Unit Recommendations

[Service]
User=monitor-agent
Group=monitor-agent
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/monitor-agent
CapabilityBoundingSet=
AmbientCapabilities=

Docker Socket Collector

  • Optional, disabled by default in config
  • Requires membership in docker group — document privilege risk
  • Read-only container inspection only

File Permissions

File Mode
identity.json 0600
config.json 0640
queue.sqlite 0600

Audit Logging

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 Management

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

Security Testing

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-http absent in release builds

Incident Response

  • 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

There aren't any published security advisories