Security Model
Sessions and tokens
Client sessions are opaque, server-side (stored in Redis) — not a JWT the browser can decode or tamper with. JWT_SECRET only signs two narrow-purpose, short-lived tokens: the Zabbix link-token and the MFA credential-reveal token, both handled internally by identity-service.
Sessions expire on two independent clocks:
SESSION_IDLE_MINUTES(default 30) — dies after this much inactivity.SESSION_ABSOLUTE_HOURS(default 8) — dies at this age regardless of activity.
Service-to-service calls (api-gateway → identity-service / inventory-service / etc.) carry a distinct X-Service-Token, an HS256 JWT signed with SERVICE_JWT_SECRET — a completely separate secret from user sessions, so a leaked service token can't be replayed as a user session or vice versa.
Credential vault
SSH credentials for managed hosts are encrypted at rest in inventory-service using AES-256-GCM, with the key derived via scrypt from ZA_VAULT_PASSWORD (≥ 32 chars) and ZA_VAULT_SALT. Credentials are never logged or exposed in plaintext outside the brief window a live SSH connection needs them.
Personal Access Tokens
API tokens are hashed with an extra pepper (API_TOKEN_PEPPER) on top of the base hash — even a full database dump doesn't let an attacker forge or crack tokens without also having the pepper, which lives only in .env.
SSH host-key verification
SSH_KNOWN_HOSTS_PATH controls whether outbound SSH connections (from terminal-service and automation-service) verify the target host's SSH key. Left empty, no verification happens (a warning is logged) — set it to a known_hosts file path to enforce strict host-key checking and mitigate MITM attacks against SSH connections to managed hosts.
Clickjacking protection
edge-proxy always sends frame-ancestors 'self' plus whatever origins are listed in FRAME_ANCESTORS. Leave it blank for same-origin-only. Never set FRAME_ANCESTORS=* — that would let any website frame the PAM console, which is a clickjacking vector directly against credential and session-control UI.
Log redaction
libs/securelog redacts password, secret, token, vault, and authorization fields from all structured logs across every service, regardless of where the value came from — this is a blanket protection, not something each call site has to opt into.
Webhook authentication
/webhook/zabbix/* (served by zabbix-integration-service) is reachable directly, bypassing api-gateway — its only authentication is ZABBIX_WEBHOOK_HMAC_SECRET, checked via the X-Monitor-Token header. The service refuses to start if this secret is unset, so there's no accidental unauthenticated deployment of that endpoint.
Zabbix module trust
The Zabbix frontend module authenticates entirely via ZABBIX_MODULE_SECRET — an HMAC secret shared between SeyalRun and the module's config.php. Treat it like a password: it is the only thing that lets the module assert "this Zabbix user, at this privilege level" to SeyalRun. SSO codes minted through this path are single-use and expire after 120 seconds.
Critically, Zabbix permissions never bypass SeyalRun's own authorization: Zabbix's host-write permission only controls whether the terminal icon is visible on the SSH Hosts page. Clicking it still goes through SeyalRun's own PAM authorization and credential gate — a user with Zabbix write access but no SeyalRun grant for that host gets a "request access" message, never a shell.
Fail-fast configuration
No secret in SeyalRun has a default value. Every service checks its required environment variables at startup and refuses to boot if any are missing — this is deliberate: a service silently running with a missing or default secret is a worse outcome than a container that won't start.