Only the current main branch — what runs in the test and prod OpenShift environments
— receives security fixes. Older images and PR environments are not supported. PR environments
are removed when the PR closes, or purged after a week by scheduled.yml.
DO NOT open public GitHub issues for security vulnerabilities.
Report privately through GitHub's private vulnerability reporting:
- Go to the repository's Security tab.
- Under Advisories in the sidebar, click Report a vulnerability.
- Fill in the advisory form. Only the repository maintainers can see it.
Please include the affected URL or component, steps to reproduce, and the impact you expect. While investigating, do not access, modify, or delete data that isn't yours.
A summary for maintainers of the automated checks and the design decisions that carry security weight. See ARCHITECTURE.md for the full system design.
| Control | Where | When |
|---|---|---|
Trivy — vulnerabilities, secrets, and IaC misconfiguration (CRITICAL,HIGH, fixable only) |
analysis.yml; results upload to the GitHub Security tab as SARIF |
Every PR, every push to main, and weekly on Sundays |
| SonarCloud, backend and frontend | analysis.yml |
Every PR and push to main |
| Frontend dependency and supply-chain scan | bcgov/action-test-and-analyse |
Every PR touching frontend/ |
| OWASP ZAP full scan against test | scheduled.yml |
Weekly, Saturdays (0 11 * * 6) |
| gitleaks secret scanning | .pre-commit-config.yaml |
On commit, once pre-commit install has been run |
| Dependency updates | Renovate (renovate.json, bcgov shared presets) |
Continuous |
Accepted Trivy exceptions live in .github/.trivyignore. Add a justification comment whenever
you add one.
- Browser login uses BC Gov SSO (CSS Keycloak) with FAM-managed roles.
oidc-client-tsuses authorization code with PKCE and a public client, stores OIDC state in tab-scopedsessionStorage, and sends the access token as a bearer token on/apicalls. - The backend requires
CSP_OIDC_ISSUER_URIandCSP_OIDC_CLIENT_IDwhen real authentication is enabled. It validates the RS256 signature against the realm's JWKS (key selected bykid), exact issuer andazp,typ=Bearer, expiry, supported identity provider, and audit username. - Roles come from
client_roles, falling back toresource_access.<configured client>.roles, and are matched case-insensitively (CspAuthorities). OnlyCSP_VIEW,CSP_APPROVE, andCSP_ADMINgrant the internalVIEW,APPROVE, andADMINauthorities, and only to IDIR/IDIR MFA identities. OnlyCSP_SUBMITTER_FOREST_CLIENT-<8 digits>grantsSUBMITTER, plus one forest-client authority per client, and only to Business BCeID identities. Bare roles, suffix aliases, foreign-app roles,FAM:bookkeeping roles and other identity providers grant no access. - All
/api/**endpoints require a staff authority, except the public/api/healthand the seven upload and submission-history endpoints, which also admitSUBMITTER. Those check thesubmissionUpload/submissionHistoryaction and the caller'sClientScope: a submitter can validate, submit and read only their own clients' submissions, and a scope is never empty. Write endpoints also use@PreAuthorizethroughPermissionServicefor action-level checks. The frontend's route, navigation and No CSP access gates match these checks for usability; the API enforces authorization. - Swagger UI and the OpenAPI document are not public. They are off unless
API_DOCS_ENABLED=true, which the OpenShift templates never pass, and even then need a staff authority. - Sessions are stateless: no cookie, no server-side session. CSRF protection is disabled deliberately, which is correct for bearer-token auth with no browser-automatable credential.
- Signing out clears local OIDC credentials and redirects through SiteMinder and the configured
CSS realm back to
/, usingid_token_hintwhen available. There is no legacy broker popup. - Idle sessions sign out after
idleTimeoutMinutes(default 30), independently of token expiry. Requests and user activity share token renewal. Expired/revoked refresh tokens end the session; temporary SSO/network failures preserve the credentials for recovery.
Mock auth bypasses CSS authentication and authenticates every request as local-dev-user with
the identity provider and roles in AUTH_MOCK_IDENTITY_PROVIDER and AUTH_MOCK_ROLES, or those
the local UI sends in X-Mock-Identity-Provider/X-Mock-Roles. Only MockRequestFilter reads
those headers, so they mean nothing when mock auth is off. Four independent things keep it out of deployed environments:
MockRequestFilteris annotated@ConditionalOnProperty(name = "auth.mock.enabled", havingValue = "true"), so the filter bean does not exist unless explicitly enabled.- The backend
prodprofile setsauth.mock.enabled: false. - OpenShift deploys
AUTH_MOCK_ENABLED=false, and the generatedconfig.jssetsmockUser: false. - The frontend uses mock auth only when served from a local hostname (
localhost,127.0.0.1,0.0.0.0,::1, or*.localhost), re-checked at runtime inenv.ts. There it is the default until CSS is configured orconfig.jssetsmockUser.
The environment variable overrides the profile.
AUTH_MOCK_ENABLED=truewill enable mock auth even under theprodprofile, because environment variables outrank profile YAML in Spring Boot. Never set it totruein a deployed environment.
- Oracle is reached over TCPS. An init container fetches the Oracle wallet into a shared volume and the entrypoint merges it into the JVM trust store at startup.
- The backend has no Route. All
/api/*traffic goes through the frontend's Caddy reverse proxy, on the same origin as the SPA — which is also why no CORS configuration exists. - NetworkPolicies (
common/openshift.init.yml) permit backend ingress only from frontend pods in the same zone and from the cluster monitoring namespace; frontend ingress only from the OpenShift router. - TLS terminates at the OpenShift edge router, so the backend only ever sees plain HTTP and its Spring HSTS configuration never fires. HSTS is set at the Caddy layer instead.
- Containers run as non-root with
readOnlyRootFilesystem: true, all capabilities dropped (drop: ["ALL"]), andautomountServiceAccountToken: false. - The frontend image strips
CAP_NET_BIND_SERVICEfrom the Caddy binary — OpenShift's restricted SCC refuses to exec a binary carrying file capabilities whenallowPrivilegeEscalation=false, and the app listens on 3000 so the capability is unneeded.
Caddy sets the security headers for both static responses and proxied /api/* responses
(frontend/Caddyfile):
Content-Security-Policy—default-src 'self', with the configured CSS issuer path allowed forconnect-srcdiscovery/token/refresh requests,frame-ancestors 'none',object-src 'none'.Strict-Transport-Security— one year,includeSubDomains.X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin.
The router's HAProxy balancing cookie is disabled
(haproxy.router.openshift.io/disable_cookies) — the frontend is stateless and needs no
session affinity, and the cookie was otherwise flagged by scans as SameSite=None.
- Credentials live only in GitHub Actions secrets, OpenShift Secrets, or ignored local
.envfiles, and are never committed..envandbackend/certs/*are git-ignored. - Local corporate-proxy certificates in
backend/certs/exist only in local builds and never reach CI/CD images. frontend/public/config.jsis a tracked, empty placeholder. Local real-auth settings use public issuer/client metadata in the git-ignoredfrontend/.env(template:frontend/.env.example). Neither may contain credentials, and machine-specific local overrides must not be committed. Deployed/config.jsis generated from GitHub Environment variablesCSP_OIDC_ISSUER_URIandCSP_OIDC_CLIENT_ID, not secrets. This public browser client has no client secret.