Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Optional values for development through the legacy root docker-compose.yml.
# Copy this file to .env. Database authentication remains the safe default.
# Never commit an institution credential or a populated .env file.
DF_AUTH_METHOD=database

# To exercise AAF locally, obtain a dedicated non-production registration from
# the identity owner, change DF_AUTH_METHOD to aaf, and fill every field below.
DF_AAF_ISSUER_URL=
DF_AAF_AUDIENCE_URL=http://localhost:4200
DF_AAF_CALLBACK_URL=http://localhost:4200/api/auth/jwt
DF_AAF_IDENTITY_PROVIDER_URL=
DF_AAF_UNIQUE_URL=
DF_AAF_AUTH_SIGNOUT_URL=
DF_SECRET_KEY_AAF=
16 changes: 11 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
FROM node:22
FROM node:22.23.2-bookworm-slim@sha256:83f487e0a63425e5b4d146fb5e5be574bcbe1b7b843d3ebafdd95eaf7767a7e5

ENV DEBIAN_FRONTEND noninteractive
# The upstream image ships npm 10.9.8. Upgrade to the latest Node 22-compatible
# npm 11 release to pick up patched bundled dependencies before dropping
# privileges.
RUN npm install --global npm@11.19.1 \
&& npm cache clean --force

ENV DEBIAN_FRONTEND=noninteractive
ENV USER=node
ENV NODE_ENV docker
ENV NODE_ENV=docker

# You can not use `${USER}` here, but reference `/home/node`.
ENV PATH="/home/node/.npm-global/bin:${PATH}"
Expand All @@ -26,5 +32,5 @@ RUN npm ci --force --include=optional

EXPOSE 9876

# Launch - install on launch so that node_modules are updated in volume
CMD /bin/bash -c 'npm install; npm start'
# Install on launch so that bind-mounted source and node_modules stay current.
CMD ["/bin/bash", "-c", "npm install && exec npm start"]
16 changes: 12 additions & 4 deletions deploy.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
### STAGE 1: Build ###
FROM node:22 AS build
FROM node:22.23.2-bookworm-slim@sha256:83f487e0a63425e5b4d146fb5e5be574bcbe1b7b843d3ebafdd95eaf7767a7e5 AS build

# Keep the build package manager aligned with the development image. npm 11
# removes the vulnerable dependencies bundled with this Node 22 base.
RUN npm install --global npm@11.19.1 \
&& npm cache clean --force

RUN apt-get update && apt-get install -y --no-install-recommends \
gettext-base \
Expand All @@ -11,10 +16,9 @@ USER node
# Copy in doubtfire-web code
WORKDIR /doubtfire-web
COPY package.json package-lock.json ./
RUN npm ci --force --include=optional
RUN npm ci --include=optional

COPY --chown=node:node . .
RUN chmod 777 src

ARG SENTRY_DSN
ARG SENTRY_ORG
Expand Down Expand Up @@ -42,7 +46,11 @@ RUN --mount=type=secret,id=sentry_auth_token,uid=1000 \


## STAGE 2: Host ###
FROM nginx:1.29.0-alpine
FROM nginx:1.30.4-alpine@sha256:97d490c12ba55b4946b01546d1c3ed324e8d41ab1c9fcb2a616aa470620e5b46

# The pinned Nginx image predates Alpine's OpenSSL 3.5.8 security update.
# Upgrade only the affected runtime libraries rather than the whole base.
RUN apk upgrade --no-cache libcrypto3 libssl3

# Remove the default Nginx configuration file
RUN rm -v /etc/nginx/nginx.conf
Expand Down
21 changes: 11 additions & 10 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: '3'
services:
doubtfire-api:
container_name: web-doubtfire-api
Expand All @@ -21,15 +20,17 @@ services:
DF_SECRET_KEY_ATTR: test-secret-key-test-secret-key!
DF_SECRET_KEY_DEVISE: test-secret-key-test-secret-key!

# Authentication method - can set to AAF or ldap
DF_AUTH_METHOD: database
DF_AAF_ISSUER_URL: https://rapid.test.aaf.edu.au
DF_AAF_AUDIENCE_URL: http://localhost:4200
DF_AAF_CALLBACK_URL: http://localhost:4200/api/auth/jwt
DF_AAF_IDENTITY_PROVIDER_URL: https://signon-uat.deakin.edu.au/idp/shibboleth
DF_AAF_UNIQUE_URL: https://rapid.test.aaf.edu.au/jwt/authnrequest/research/Ag4EJJhjf0zXHqlKvKZEbg
DF_AAF_AUTH_SIGNOUT_URL: https://sync-uat.deakin.edu.au/auth/logout
DF_SECRET_KEY_AAF: v4~LMFLzzwRGZdju\5QBa@FiHIN9
# Database authentication is the safe local default. Optional AAF
# development values must come from an ignored .env file; never commit an
# institution credential or registration URL here.
DF_AUTH_METHOD: ${DF_AUTH_METHOD:-database}
DF_AAF_ISSUER_URL: ${DF_AAF_ISSUER_URL:-}
DF_AAF_AUDIENCE_URL: ${DF_AAF_AUDIENCE_URL:-http://localhost:4200}
DF_AAF_CALLBACK_URL: ${DF_AAF_CALLBACK_URL:-http://localhost:4200/api/auth/jwt}
DF_AAF_IDENTITY_PROVIDER_URL: ${DF_AAF_IDENTITY_PROVIDER_URL:-}
DF_AAF_UNIQUE_URL: ${DF_AAF_UNIQUE_URL:-}
DF_AAF_AUTH_SIGNOUT_URL: ${DF_AAF_AUTH_SIGNOUT_URL:-}
DF_SECRET_KEY_AAF: ${DF_SECRET_KEY_AAF:-}

# Database settings - for development env
DF_DEV_DB_ADAPTER: mysql2
Expand Down
145 changes: 145 additions & 0 deletions scripts/verify-deployment-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
const assert = require('node:assert/strict');
const {readFileSync} = require('node:fs');
const {resolve} = require('node:path');

const root = resolve(__dirname, '..');
const nginx = readFileSync(resolve(root, 'nginx.conf'), 'utf8');
const dockerfile = readFileSync(resolve(root, 'deploy.Dockerfile'), 'utf8');
const developmentCompose = readFileSync(resolve(root, 'docker-compose.yml'), 'utf8');
const deploymentWorkflows = [
['default', readFileSync(resolve(root, '.github/workflows/deployment.yml'), 'utf8')],
[
'institution',
readFileSync(resolve(root, '.github/workflows/deployment-institution.yml'), 'utf8'),
],
];
const applicationBootstrap = readFileSync(resolve(root, 'src/main.ts'), 'utf8');
const scormPlayerTemplate = readFileSync(
resolve(root, 'src/app/common/scorm-player/scorm-player.component.html'),
'utf8',
);
const productionEnvironment = readFileSync(
resolve(root, 'src/environments/environment.prod.ts'),
'utf8',
);

const controlFiles = [
'/index.html',
'/ngsw.json',
'/ngsw-worker.js',
'/safety-worker.js',
'/worker-basic.min.js',
'/manifest.webmanifest',
];
const noStorePolicy = 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';

const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

for (const path of controlFiles) {
const escapedPath = escapeRegExp(path);

assert.match(
nginx,
new RegExp(`^\\s*${escapedPath}\\s+"${noStorePolicy}";`, 'm'),
`${path} must map to the no-store cache policy`,
);
assert.match(
nginx,
new RegExp(`location = ${escapedPath} \\{\\s*try_files \\$uri =404;\\s*\\}`, 'm'),
`${path} must use an exact location and return 404 when absent`,
);
}

assert.match(
nginx,
/add_header Cache-Control \$ontrack_control_file_cache_control always;/,
'the mapped cache policy must be added to responses',
);
assert.match(
nginx,
/try_files \$uri \$uri\/ \$uri\/index\.html \/index\.html;/,
'SPA routes must continue to fall back to /index.html',
);
for (const [name, workflow] of deploymentWorkflows) {
assert.doesNotMatch(
workflow,
/steps\.meta\.outputs\.labels/,
`${name} deployment metadata labels must reference the declared docker_meta step`,
);
assert.match(workflow, /steps\.docker_meta\.outputs\.labels/);

for (const line of workflow.split('\n')) {
if (/^\s*uses:/.test(line)) {
assert.match(
line,
/@[0-9a-f]{40}(?:\s+#.*)?$/,
`${name} production image workflow actions must be pinned to immutable commits`,
);
}
}

assert.match(workflow, /^\s*sbom:\s*true$/m);
assert.match(workflow, /^\s*provenance:\s*mode=max$/m);
}
assert.match(
dockerfile,
/^FROM node:22\.23\.2-bookworm-slim@sha256:[0-9a-f]{64} AS build$/m,
'the release build must pin the supported Node image digest',
);
assert.match(
dockerfile,
/npm install --global npm@11\.19\.1/,
'the release build must install the reviewed npm security update',
);
assert.match(
dockerfile,
/^FROM nginx:1\.30\.4-alpine@sha256:[0-9a-f]{64}$/m,
'the release runtime must pin the Nginx image digest',
);
assert.match(
dockerfile,
/^RUN apk upgrade --no-cache libcrypto3 libssl3$/m,
'the release runtime must install the reviewed Alpine OpenSSL security update',
);
assert.doesNotMatch(
dockerfile,
/npm ci[^\n]*--force/,
'the release build must not force npm resolution',
);
assert.doesNotMatch(
dockerfile,
/chmod\s+777/,
'the release build must not create world-writable source',
);
assert.match(
developmentCompose,
/DF_SECRET_KEY_AAF:\s*\$\{DF_SECRET_KEY_AAF:-\}/,
'optional development AAF credentials must come from the ignored environment',
);
assert.doesNotMatch(
developmentCompose,
/https?:\/\/[^\s$]*(?:aaf\.edu\.au|deakin\.edu\.au)/i,
'institution-specific AAF endpoints must not be committed in development Compose',
);
assert.doesNotMatch(
applicationBootstrap,
/(?:browserTracingIntegration|replayIntegration|tracesSampleRate|replaysSessionSampleRate|replaysOnErrorSampleRate)/,
'tracing and replay must stay disabled while SCORM uses credential-bearing paths',
);
assert.match(
applicationBootstrap,
/enableLogs:\s*false/,
'Sentry Logs must stay disabled for the errors-only telemetry policy',
);
assert.match(
scormPlayerTemplate,
/<iframe[^>]*data-sentry-block[^>]*referrerpolicy="no-referrer"/,
'the SCORM iframe must suppress referrers and remain blocked from any future replay integration',
);
assert.match(
productionEnvironment,
/enableDemoTools:\s*false/,
'production must keep demo tools disabled',
);

console.log('Deployment configuration checks passed.');
Loading