This document summarizes security-sensitive configuration and operational practices for Fireguard API.
- This guide focuses on configuration, runtime hardening, and operational safeguards.
- Application-specific flows and endpoints are documented in each module guide.
- Store secrets in environment variables or a secrets manager.
- Never commit production secrets to the repository.
- Protect
APP_SECRETandOAUTH_ENCRYPTION_KEYlike credentials. - Use strong, unique encryption keys per environment.
JWT keys:
- Use environment-specific
config/jwt/private.keyandconfig/jwt/public.key. - Keep private keys encrypted at rest and restrict filesystem permissions.
- Rotate keys and invalidate old tokens if required by policy.
- Every bearer token is signature-verified before any of its claims is trusted.
OAuth2Authenticatorvalidates the RSA signature and the expiry first, then branches on the token's origin. Both issuance paths sign withconfig/jwt/private.key— the login flow throughJwtTokenAdapter, the OAuth2 flow through League'sAuthorizationServer— so a single verification key covers both. The database lookup keys onjtiand never binds it back tosub, which is why the signature check must not be conditional: a branch that skipped it would let a forged token carrying a livejtiand an arbitrarysubauthenticate as that subject. - Refresh tokens are issued in HttpOnly cookies with SameSite=Strict.
- In production, cookies are marked Secure and use the
__Host-prefix. - Keep short access token lifetimes and use refresh tokens for renewals.
- Revoke tokens on logout and suspicious activity.
- Revoking a session invalidates its access token on the next request.
Login-flow tokens are not rows in the OAuth2 token table, so
OAuth2Authenticatorresolves them throughSessionStatusPortinstead. A token whose session was never recorded is accepted rather than rejected — session recording is deliberately best-effort, and treating an absent row as a revocation would lock a user out for the token's full lifetime after a failure they never saw. - Access tokens include email/roles/permissions by default for backward compatibility.
- To minimize token size and reduce data exposure, set
ACCESS_TOKEN_INCLUDE_EMAIL=falseandACCESS_TOKEN_INCLUDE_RBAC=false.
- To minimize token size and reduce data exposure, set
- Use the authorization code flow with PKCE for browser and public clients.
- Validate redirect URIs strictly and avoid wildcards.
- Limit allowed scopes for each client and rotate secrets regularly.
Rate limiters are defined in config/packages/rate_limiter.yaml:
loginmfa_verifyoauth_tokenoauth_authorizeoauth_introspectionoauth_revocationoauth_consent_checkoauth_consent_granttoken_refreshpassword_reset_requestpassword_resetpassword_reset_confirmmfa_resendotp_challenge_createotp_challenge_verifyinvitation_preview(per IP — the endpoint is public)calendar_feed(per IP — the public.icsmember feed endpoint)invitation_resend(per user)invitation_accept(per user)email_change_request(per user, 5/min — every accepted call emails an arbitrary address and answers the same email-taken question asregistration, so the budgets match)email_change_request_ip(per IP, 5/min — second dimension on the same endpoint: the per-user budget scales with the number of accounts an attacker controls, this one does not)email_change_confirm(per IP — the confirm endpoint is public, the emailed token is the credential)facility_geocode(per user, 30/min — each accepted call is proxied to the external geocoding provider, so the budget protects the shared outbound Nominatim channel and us, not a secret; the adapter additionally serializes the aggregate outbound flow to 1 req/s per Nominatim's usage policy)
Tune limits to match threat models and expected traffic. In test environments, limits may be overridden for determinism.
Security headers are automatically added to all responses via SecurityHeadersSubscriber.
Headers applied:
| Header | Value | Purpose |
|---|---|---|
X-Content-Type-Options |
nosniff |
Prevent MIME type sniffing |
X-Frame-Options |
DENY |
Prevent clickjacking (legacy) |
X-XSS-Protection |
0 |
Disabled (rely on CSP instead) |
Referrer-Policy |
strict-origin-when-cross-origin |
Control referrer information |
Content-Security-Policy |
default-src 'none'; frame-ancestors 'none' |
Restrictive CSP for API |
Permissions-Policy |
Restrictive | Block sensitive browser features |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
HSTS (production only) |
Configuration via environment variables:
# Enable/disable security headers (default: true)
SECURITY_HEADERS_ENABLED=true
# Custom CSP (leave empty for default restrictive policy)
SECURITY_HEADERS_CSP=
# HSTS max-age in seconds (default: 31536000 = 1 year)
SECURITY_HEADERS_HSTS_MAX_AGE=31536000For authenticated requests (with Authorization header), additional cache headers are set:
Cache-Control: no-store, no-cache, must-revalidate, privatePragma: no-cache
- Restrict
CORS_ALLOW_ORIGINto trusted origins. - Always serve OAuth and Auth endpoints over TLS in production.
- Avoid logging access tokens, refresh tokens, or raw credentials.
- Use domain events to build audit trails where required by compliance.
- Monitor for failed logins, token revocations, and abuse patterns.
- Security logs hash PII by default (email/ip); enable full PII with
SECURITY_LOG_INCLUDE_PII.
- Security-relevant events are persisted in the Audit module (
audit_events). - Audit events are hash-chained (
prev_hash+ payload hash) to detect tampering. - Audit APIs are read-only and protected by
audit.read. - Audit PII handling uses the same PII settings as security logs.
Security events are emitted as structured logs (JSON) on the security channel.
Core fields vary by event but follow this common shape:
{
"message": "OAuth2 token issued",
"context": {
"event": "oauth.token_issued_event",
"user_id": "uuid",
"client_id": "uuid",
"grant_type": "client_credentials",
"ip": "x.x.x.x",
"reason": "optional"
}
}Event-specific fields:
| Event | Fields |
|---|---|
auth.user_logged_in_event |
user_id, email (sanitized), email_hash, ip (sanitized), ip_hash |
auth.login_failed_event |
email (sanitized), email_hash, ip (sanitized), ip_hash, reason |
oauth.token_issued_event |
grant_type, client_id, user_id (optional), ip |
oauth.token_issue_failed_event |
grant_type, client_id, ip, reason |
oauth.token_refreshed_event |
user_id, ip |
oauth.token_refresh_failed_event |
user_id, ip, reason |
- Revoke active tokens and rotate keys when credentials are suspected to be compromised.
- Audit recent authentication and token-issuance events.
- Increase rate limits only after abuse investigations are complete.
Use your organization’s standard security reporting process for disclosures and incident handling.
- Periodic cleanup of expired/revoked auth data is available:
- Command:
php bin/console app:cleanup:auth-data --days=90 - Dry run:
php bin/console app:cleanup:auth-data --days=90 --dry-run
- Command:
- Default retention is set by
DATA_RETENTION_DAYS.