Security Hardening Checklist

What you'll accomplish: Walk through a production security checklist to ensure your Hoziron deployment is locked down.

The checklist

  • Enable TLS — native in-process TLS per surface ([surfaces.<name>.tls]), or at a reverse proxy in front of Hoziron
  • Restrict network access — set surfaces.<name>.allowed_ips (on a main-route surface) to known CIDR ranges
  • Enable authentication — configure [auth] with mode = "local" or mode = "oidc"; this is enforced, not just advisory — the daemon refuses to boot with auth disabled on a non-loopback bind (details)
  • Use role-based API keyshoziron-cli auth create-key --role operator --name "deploy-bot"
  • Store secrets properly — use a secrets manager, inject as env vars (never in config.toml)
  • Enable audit trail — configure [audit] section
  • Run as non-root — UID 65532 in Docker, dedicated user on bare metal
  • Mount config read-only — only /data should be writable
  • Use structured loggingHOZIRON_LOG_FORMAT=json for parseable, auditable logs
  • Restrict egress — configure your host/container firewall to allow outbound traffic only to LLM providers and internal services Hoziron needs
  • Review CORS settings — only enable if a web dashboard connects directly
  • Set key expiration — all non-admin keys should have --expires-in set
  • Verify audit chain integrityhoziron-cli security verify
  • Never bypass the credential vault — provider/registry/integration secrets should be api_key_env/auth_token_env references or vault entries, never inline in config.toml; vault:read/vault:manage are Admin-only by design, with no key read-back endpoint (details)
  • Review carrier-pii-policy.toml — confirm [pii_policy.llm].local_only covers every PII type that must never leave the box even tokenised, and that [[pii_policy.tool_rules]] only grants hydrate to destinations that are genuinely licence-designated systems of record (details)
  • Check licence validity and expiryhoziron-cli licence status; an expired or missing carrier licence is a hard boot/runtime block, not a soft warning
  • Confirm role assignment matches least privilege — use auditor (not admin) for compliance reviewers; use service (not operator) for machine-to-machine automation (details)

Network security

IP allowlist

[surfaces.api]
listen = "0.0.0.0:4200"
allowed_ips = ["10.0.0.0/8", "172.16.0.0/12"]

Set on a main-route surface only — it's enforced once per merged listener and covers everything sharing that address, including any surface mounted onto it and the unified /__server/health endpoint (unlike a surface's own /health route, which the middleware exempts by path). If you restrict allowed_ips on a listener, make sure your orchestrator's probe source IP is included, or point liveness/readiness probes at a surface's own /health path instead.

Credential management

  • config.toml stores the name of the env var (api_key_env = "ANTHROPIC_API_KEY"), never the value
  • Keys are resolved lazily at request time from the environment
  • Error messages reference the env var name, never the key value
  • The key store database has 0600 permissions (owner-only)

Container security

  • Read-only root filesystem
  • Non-root user (UID 65532)
  • No shell or package manager in distroless image
  • SIGTERM handling for graceful shutdown

Sovereignty attestation is a trust assumption

Hoziron's local_only PII guarantee (ADR-053) is structural for two of the three driver classes:

  • AlwaysLocal drivers (Ollama, vLLM, LM Studio, Lemonade) — always resolve Sovereignty::Local. No config can change this.
  • AlwaysCloud drivers (Anthropic, OpenAI) — always resolve Sovereignty::Cloud. No config can make these local; a sovereignty_attestation on either is a hard boot error.

The third class is where operator trust re-enters:

  • Ambiguous drivers (e.g. the OpenAI-compatible shim, used for in-house/self-hosted endpoints that speak a public protocol) resolve Sovereignty::Local only when the [[provider]] entry sets sovereignty_attestation:

    [[provider]]
    id     = "inhouse.shim"
    driver = "openai-compatible"
    url    = "http://10.4.2.7:8000/v1"
    sovereignty_attestation = "carrier confirms 10.4.2.7 is in-boundary"
    

What this does and does not guarantee:

  • Setting sovereignty_attestation is a claim, not a proof. The platform does not verify that the endpoint is actually inside your network boundary — it trusts the string the operator wrote. If your local_only PII policy (see PII Engine) is routing on the assumption this provider never leaves the building, that assumption rests entirely on this attestation being correct.
  • A carrier who misattests a cloud endpoint as local (by mistake, or deliberately) silently defeats local_only for that provider. Deliberate misattestation is out of scope for the platform to prevent — it's the carrier's call by design, and no boot check can distinguish "I meant to attest this" from "I lied."
  • The platform runs one best-effort boot check to catch the accidental case: if an attested-local provider's url doesn't parse to a loopback or RFC-1918 private address (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), it logs a warning naming the provider and URL. This is informational only — it never refuses to boot, and it is a literal-IP heuristic, not a topology check:
    • It can false-positive: an internal DNS name (inhouse.internal.corp), a VPN-peered address, or a non-RFC-1918 private range will all warn even when genuinely in-boundary. If your topology is legitimately more complex than "raw private IP," expect and ignore this warning.
    • It cannot catch a deliberate misattestation dressed up as a private IP, or any misattestation behind a hostname the carrier controls.
  • Treat sovereignty_attestation as an audited, operator-signed statement, not a technical control. The resolved reason (attested-local vs. native-local) is retained specifically so a POPIA auditor can distinguish "structurally guaranteed local" from "operator attested in-boundary" — review attested entries during audits with that distinction in mind.

Verification commands

# Check for security misconfigurations
hoziron-cli security status

# View recent audit trail
hoziron-cli security audit --limit 50

# Verify audit trail Merkle chain integrity
hoziron-cli security verify

# Full system diagnostics
hoziron-cli doctor

Next steps


Related: