14 Security Hardening
This chapter lists what TraxrGRC enforces by itself and what you, as the operator, must do. Work through the operator checklist before go-live and again after each major change.
14.1 What the product enforces
| Area | Built-in behavior |
|---|---|
| Transport headers | Every response carries Strict-Transport-Security: max-age=31536000; includeSubDomains, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin and a Permissions-Policy that turns off camera, microphone, geolocation and payment. The X-Powered-By header is not sent. |
| Content Security Policy | Pages carry a per-request nonce policy: scripts only from the application with the request's nonce, no plugins, no framing (frame-ancestors 'none'), forms and connections to the application only. |
| Cross-site requests | Server actions and every browser route that changes data refuse requests whose Origin does not match the host. |
| Session secret | web refuses to start in production with a missing, short (under 32 characters) or placeholder AUTH_SECRET. |
| Sessions | Sessions end after the session length (default 12 hours) or the idle timeout (default 60 minutes). Rights are refreshed every minute; a password reset, two-step verification reset, deactivation, administrator or group change, or Sign out everywhere ends the user's sessions within a minute, and a user's own password change ends their other sessions. |
| Sign-in throttling | Five failures lock that email from that address for 15 minutes; 30 failures lock the address for every account. Accounts are never locked outright. Ten failures on one account in 15 minutes refuse every address that failed and alert administrators. |
| Directory and SSO | LDAP requires ldaps:// or StartTLS. SAML responses must be signed, answer a sign-in started from the application within 10 minutes, and name this instance as recipient; each is accepted once. LDAP and SSO never take over an existing password, administrator or break-glass account unless an administrator allowed the link. |
| Multi-factor authentication | Built-in two-step verification (TOTP authenticator apps, with ten single-use recovery codes) for password and LDAP sign-in, optional per user or required for everyone under Settings → Sessions. Single sign-on relies on the identity provider's MFA; SSO only makes it the only way in, with break-glass administrators keeping password sign-in for outages. |
| API rate limit | Each API token may make 600 requests a minute on /api/v1 by default (API_RATE_LIMIT_PER_MINUTE); further requests get 429 with Retry-After. |
| Stored secrets | The SMTP password, LDAP bind password, SAML private key and two-step verification secrets are encrypted in the database (AES-256-GCM). Recovery codes are stored as keyed HMAC-SHA256 hashes. Passwords are stored as bcrypt hashes. API, SCIM and upload-link tokens are stored as SHA-256 hashes. |
| Uploads | Attachments are limited to 25 MB each and to an allowlist of types (PDF, common images, text, Markdown, CSV, Office documents and ZIP), checked by extension, declared type and file content. Files are stored under random names and downloaded as attachments. Each user has an attachment storage quota (default 2048 MB). |
| Audit log | Append-only for the application: the application's database role can only read and add entries, triggers refuse changes, and a hash chain makes any later change detectable. Entries are kept at least 365 days. |
| Backups | In-app snapshots are signed and checked on restore; restore keeps the audit log and security state and revokes all API and SCIM tokens. |
| Containers | Non-root, read-only root filesystem, all capabilities dropped, no-new-privileges; web published on loopback only; the database never published. |
| Outbound calls | Telemetry and update checks are off. The application connects only to the services you configure (SMTP, LDAP, Microsoft Entra ID for AI assistants). |
14.2 Operator checklist
14.2.1 TLS and the network
- Serve the instance over HTTPS only, through the bundled Caddy or your own proxy.
- Give the instance a host name of its own. HSTS applies to the host name
on every port and to every name below it, for a year; anything else
served on that name must also use HTTPS. The header deliberately omits
preload; submitting the domain to browser preload lists is the domain owner's decision. - Leave
WEB_BINDat127.0.0.1unless a proxy on another host needs it, and then firewall port 3000 to that proxy. A client that reaches port 3000 directly can choose the address recorded for it in throttling and the audit log. - With your own proxy, overwrite or append
X-Forwarded-Forand setTRUSTED_PROXY_HOPSto match; apply the body size limits. - Allow inbound traffic to ports 80 and 443 only. Never publish the database port.
- Restrict outbound traffic from the server to the services the instance uses (SMTP, LDAP, ACME, Docker Hub and npm during builds).
14.2.2 Secrets
.envhas mode 600 and is owned by the account that runs Docker.AUTH_SECRET,POSTGRES_PASSWORD,APP_DB_PASSWORDand the optional keys were generated withopenssl rand, are unique to this instance, and are stored in the password vault.INITIAL_ADMIN_PASSWORDis removed from.envand from themigratecontainer.- Shell access to the server, membership of the
dockergroup and the database owner's password are limited to the people who operate the instance. Any of them can bypass every control in the application, including the audit log's protections.
14.2.3 Rotating AUTH_SECRET
Rotate AUTH_SECRET if it may have been exposed.
The consequences:
| Effect | Without the dedicated key | With the dedicated key set |
|---|---|---|
| Every session ends; everyone signs in again. | Always | Always |
| SAML sign-ins in progress fail and must be started again. | Always | Always |
| Stored SMTP password, LDAP bind password and SAML private key become unreadable and must be entered again under Settings → Integrations. | Yes | No, if SETTINGS_ENCRYPTION_KEY is set |
| In-app snapshots taken before the change can no longer be restored in the application. | Yes | No, if BACKUP_SIGNING_KEY is set |
Server backups made with deploy/backup.sh are unaffected. To rotate:
- Generate the new value:
openssl rand -base64 32. - Replace
AUTH_SECRETin.envand in the vault. - Apply it:
docker compose --profile tls up -d. - Sign in and re-enter the integration secrets if needed; send a test email.
SETTINGS_ENCRYPTION_KEY and BACKUP_SIGNING_KEY are rotated the same way,
with the effects in their own rows above.
14.2.4 Backups
deploy/backup.shruns nightly and its failures are alerted.- Backups are copied off the server and encrypted with age, with the private key kept off the server.
- Access to the off-site copies is as restricted as access to the application's data.
- A restore drill has passed within the last quarter.
14.2.5 Accounts and access
- Every person has their own account; no shared accounts.
- Administrators are few. Everyone else gets rights through groups and roles at the lowest level that works (own before all, read before update). See Users, Groups and Roles.
- At least two break-glass administrators exist, with passwords in the vault, and their sign-ins are reviewed (each one emails a security alert to every administrator).
- Two-step verification is required under Settings → Sessions for password and LDAP sign-in, or SSO only is on if your identity provider enforces MFA.
- Session length and idle timeout under Settings → Sessions match your policy.
- API tokens have the narrowest scopes and a maximum lifetime set under Settings → Sessions; unused tokens are revoked. See API Access and Integrations.
- Local passwords, where used, follow your password policy. The product requires at least 8 characters for a local password, whether set by an administrator or changed by its owner on My profile.
- Separation of duties stays on unless you have a documented reason to relax it.
14.2.6 Monitoring
- SMTP works, so administrators receive security alerts.
/api/healthis monitored externally.- The audit chain is verified daily and the chain head copied off the server.
- Audit log retention is set to your record-keeping requirement (365 days minimum). See Audit Log and Evidence Integrity.
14.2.7 Keeping up to date
-
Upgrade to new releases promptly with
deploy/update.sh; security fixes arrive only in releases. -
Keep the host operating system and Docker Engine patched.
-
The
postgres:18-alpineandcaddy:2.11-alpineimages are named by tag, sodocker compose pullfollowed bydocker compose --profile tls up -dpicks up their patch releases. If your change process requires pinned images, pin them by digest indocker-compose.override.yml:docker pull caddy:2.11-alpine docker image inspect --format '{{index .RepoDigests 0}}' caddy:2.11-alpineThen set the service's
image:tocaddy:2.11-alpine@sha256:.... A pinned image never changes until you change the digest, so you then take on applying its security fixes yourself.
14.3 Known limits
These are properties of the design that the operator controls compensate for:
- Anyone with the database owner's password or root on the server can disable the audit triggers and rewrite entries along with their hashes. Copying the chain head off the server after each verification detects such a rewrite.
- The application role can add audit entries, so a compromised application process could add false entries, though it cannot change or remove existing ones.
- An unsigned snapshot (from before snapshot signing) can still be restored after an administrator confirms it; the restore is recorded as unsigned.
- Upload links for AI assistants carry their token in the URL path; they expire after 15 minutes.