Security & Authentication
How Hoziron authenticates requests, enforces role-based access, protects against brute force attacks, and manages secrets.
Authentication Modes
Hoziron supports three authentication modes, configured via [auth].mode:
Disabled (default)
No authentication. All requests receive anonymous admin access. Suitable for local development and single-user deployments.
Local (API Key)
Hoziron-managed API keys with role-based access control.
[auth]
mode = "local"
[auth.rate_limit]
base_backoff_secs = 1
max_backoff_secs = 300
max_failed_attempts = 10
OIDC (Enterprise SSO)
JWT validation against an external Identity Provider (Azure AD, Keycloak, Okta, Auth0).
[auth]
mode = "oidc"
allow_local_service_keys = true # Allow API keys alongside OIDC for CI/CD
[auth.oidc]
issuer = "https://login.microsoftonline.com/{tenant}/v2.0"
audience = "api://hoziron-platform"
jwks_uri = "" # Auto-discovered if blank
role_claim = "roles" # Supports dot-path: "realm_access.roles"
allowed_algorithms = ["RS256", "ES256"]
jwks_cache_ttl_secs = 3600
[auth.oidc.role_mapping]
"HozironAdmins" = "admin"
"PlatformOps" = "operator"
"Developers" = "developer"
"ReadOnly" = "viewer"
API Key System
Key Generation
- 32 bytes of cryptographic randomness (via OS RNG)
- Base64url-encoded with
hzk_prefix:hzk_Ab3xYz... - Only shown once at creation — never stored or retrievable after
- First 12 characters stored as
prefixfor identification without exposure
Key Storage
- SQLite database at
$HOZIRON_HOME/data/auth.db - File permissions set to 0600 (owner read/write only) on Unix
- Only argon2id hashes are persisted (recommended params: 19 MiB memory, 2 iterations, 1 parallelism)
- Indexed by prefix for fast lookup, but authentication always scans ALL keys
Timing Attack Prevention
Authentication performs a constant-time scan of all active keys:
for each key in active_keys:
verify(token, key.hash) // argon2id verify (constant-time)
if match: record (but don't return early)
return recorded match or None
An attacker cannot determine how many keys exist or which position a valid key occupies based on response time.
Lockout Protection
The last admin key cannot be revoked. Attempting to revoke it returns:
{
"error": {
"category": "ValidationError",
"message": "Cannot revoke the last admin key — this would lock out all admin access"
}
}
Bootstrap Flow
When no keys exist yet, the first POST /auth/keys request is allowed without authentication (bootstrap bypass). This prevents the chicken-and-egg problem of needing a key to create the first key.
Key Expiration
Keys can optionally expire:
expires_atstored as ISO 8601- Checked at validation time — expired keys are treated as invalid
- Fail-closed on malformed expiry values (treated as expired)
Role-Based Access Control (RBAC)
Roles
| Role | Purpose |
|---|---|
admin | Full access — all operations including key management |
operator | Agent lifecycle, workflow management, schedules |
developer | Install skills/competencies, create workflows, send messages |
viewer | Read-only access to all resources |
service | Invoke agents, start workflow runs (for automated integrations) |
Permission Matrix (excerpt)
| Action | Admin | Operator | Developer | Viewer | Service |
|---|---|---|---|---|---|
| agent:create | ✓ | ✓ | |||
| agent:send_message | ✓ | ✓ | ✓ | ✓ | |
| agent:list | ✓ | ✓ | ✓ | ✓ | ✓ |
| workflow:run | ✓ | ✓ | ✓ | ✓ | |
| workflow:create | ✓ | ✓ | ✓ | ||
| config:write | ✓ | ||||
| auth:key_management | ✓ | ||||
| audit:read | ✓ | ✓ | |||
| competency:install | ✓ | ✓ | ✓ |
Authorization Check
Every endpoint checks auth_context.role.can_perform(action) before executing. Insufficient role returns:
{
"error": "forbidden",
"message": "Role 'viewer' is not authorized for action 'agent:create'"
}
Brute Force Protection
Per-IP exponential backoff on failed authentication attempts:
| Parameter | Default | Config Key |
|---|---|---|
| Base backoff | 1 second | auth.rate_limit.base_backoff_secs |
| Max backoff | 300 seconds (5 min) | auth.rate_limit.max_backoff_secs |
| Max failures tracked | 10 | auth.rate_limit.max_failed_attempts |
Rate limit state is:
- In-memory (per-process, reset on restart)
- Per-IP address
- Cleared on successful auth from that IP
- Stale entries evicted after 10 minutes of inactivity
OIDC / JWT Validation
For enterprise SSO integration:
Role Mapping
The role_claim field supports dot-path traversal for nested claims (Keycloak pattern):
role_claim = "realm_access.roles" # Navigates into nested JWT claims
When multiple IdP roles match the mapping, highest-privilege wins (admin > operator > developer > viewer > service).
Hybrid Mode
With allow_local_service_keys = true, OIDC mode falls back to local API key validation when JWT validation fails. This enables:
- CI/CD pipelines using API keys alongside human SSO
- Break-glass access for emergencies
Network Security
TLS
[server.tls]
enabled = true
cert_path = "/etc/hoziron/tls/cert.pem"
key_path = "/etc/hoziron/tls/key.pem"
- Native TLS termination for bare-metal/VM deployments
- In Kubernetes: disable (set
enabled = false) — ingress handles TLS - Certificate and key paths are validated at startup
IP Allowlist
[server]
allowed_ips = ["10.0.0.0/8", "192.168.1.100"]
- Outermost middleware layer — checked before auth
- Supports individual IPs and CIDR notation (IPv4 and IPv6)
- Health endpoint (
/health) always bypasses the allowlist - Metrics endpoint (
/metrics) always bypasses (Prometheus scraping) - Unix socket connections bypass (no IP to check)
CORS
[server.cors]
allowed_origins = ["https://dashboard.company.com"]
allow_credentials = true
max_age_secs = 3600
Validation rules:
- Cannot use wildcard
*withallow_credentials = true - Each origin must start with
http://orhttps:// - Empty origins list is rejected when CORS is configured
Request Limits
| Limit | Default | Purpose |
|---|---|---|
max_request_body_bytes | 10 MB | Prevents memory exhaustion from large payloads |
request_timeout_secs | 600 (10 min) | Prevents hung connections |
idle_timeout_secs | 300 (5 min) | Reserved for future connection-level enforcement |
Credential Security
API Key Secret Flow
Key principles:
config.tomlstores 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)
Vault vs Environment
| Method | Stored Where | Best For |
|---|---|---|
| Vault | $HOZIRON_HOME/vault/ (encrypted at rest) | Bare metal, persistent |
| Environment variables | Process env | Containers, orchestrator-managed |
.env file | $HOZIRON_HOME/.env | Local development |
Endpoints Always Accessible
Regardless of auth mode, IP allowlist, or any security configuration:
| Endpoint | Reason |
|---|---|
GET /health | Orchestrator liveness/readiness probes must always work |
GET /metrics | Prometheus scraping without API key |
POST /auth/keys (first key only) | Bootstrap — create first key without existing auth |