Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6c764ea
ci/test-nginx: show mock-sentry logs (#1816)
alxndrsn Apr 18, 2026
0379dcf
ci/ghcr: update deprecated docker action versions (#1821)
alxndrsn Apr 18, 2026
ae72624
test/nginx/csp: rename backend-strict policy (#1823)
alxndrsn Apr 22, 2026
963b9dc
ci: only publish containers if tests passed (#1820)
alxndrsn Apr 22, 2026
94aa904
test/nginx/csp: remove duplicate test (#1860)
alxndrsn Apr 29, 2026
1431cb1
nginx/csp: blank.html: allow form-action 'self' (#1857)
alxndrsn Apr 29, 2026
e6df530
nginx/csp: enforce policy for blank.html (#1858)
alxndrsn Apr 29, 2026
9122bb2
csp: allow data: URLs in worker-src for web-forms (#1776)
alxndrsn Apr 30, 2026
711c6ad
nginx/csp: tighten favicon allowance for blank.html (#1855)
alxndrsn Apr 30, 2026
c9b359a
nginx/csp: allow favicons for backend requests (#1854)
alxndrsn Apr 30, 2026
d3eb696
Merge branch 'master' into next
matthew-white May 4, 2026
af64f1d
nginx/csp: enforce policy for central-backend (#1859)
alxndrsn May 5, 2026
625eb98
nginx: fix escaping in /fonts/ matcher (#1863)
alxndrsn May 7, 2026
97b3251
ci: simplify checkout (#1890)
alxndrsn May 9, 2026
fe411c8
Fixes: expected docker context is increased due to WF merge
sadiqkhoja May 14, 2026
448b96a
Merge pull request #1893 from sadiqkhoja/fixes/docker-context
sadiqkhoja May 14, 2026
1a9c7f9
Fixes: install openssl in service container
sadiqkhoja May 13, 2026
b9a1d3f
Merge pull request #1892 from sadiqkhoja/fixes/install-openssl
sadiqkhoja May 15, 2026
e4c7b83
service: move DB_SSL check back to runtime (#1889)
alxndrsn May 25, 2026
95717b1
nginx: enable Content Security Policies (#1909)
alxndrsn May 26, 2026
a4e5b06
test/nginx: fix comment typo (#1938)
alxndrsn Jun 4, 2026
48b7478
test/nginx/docker-compose: restrict open ports to local machine (#1927)
alxndrsn Jun 4, 2026
fc457ec
nginx: test stream interruption (#1939)
alxndrsn Jun 4, 2026
c182517
dev/docker-compose: restrict open ports to local machine (#1925)
alxndrsn Jun 4, 2026
18b9667
nginx: reject form previews with unexpected query params (#1947)
alxndrsn Jun 5, 2026
007fd5c
test: disable case-sensitive routing for express servers (#1953)
alxndrsn Jun 6, 2026
28206d5
ci: increase docker-context file size expecations (#1948)
alxndrsn Jun 6, 2026
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
61 changes: 0 additions & 61 deletions .github/workflows/ghcr.yml

This file was deleted.

89 changes: 79 additions & 10 deletions .github/workflows/test.yml → .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
name: Test
name: Test, Build, Publish

on:
push:
pull_request:
workflow_dispatch:
inputs:
publish_image:
description: 'Publish image to registry?'
required: true
type: boolean
default: false

jobs:
test-misc: # quick, simple checks
Expand All @@ -26,20 +33,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
fetch-tags: true
submodules: recursive
- run: cd test/envsub && ./run-tests.sh
test-nginx:
timeout-minutes: 4
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
fetch-tags: true
submodules: recursive
- uses: actions/setup-node@v5
with:
node-version: 24.14.1
Expand All @@ -59,27 +58,97 @@ jobs:
run: cd test/nginx && docker compose -f nginx.test.docker-compose.yml logs --no-log-prefix service
- if: always()
run: cd test/nginx && docker compose -f nginx.test.docker-compose.yml logs --no-log-prefix enketo
- if: always()
run: cd test/nginx && docker compose -f nginx.test.docker-compose.yml logs --no-log-prefix sentry-mock
test-secrets:
timeout-minutes: 2
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: ./test/test-secrets.sh
test-service:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
submodules: true
- uses: actions/setup-node@v5
with:
node-version: 24.14.1
- run: cd test/nginx && npm clean-install
- run: cd test/nginx && npm run test:service
test-images:
timeout-minutes: 10
needs:
- test-misc
- test-envsub
- test-nginx
- test-secrets
- test-service
runs-on: ubuntu-latest # TODO matrix to run on all expected versions?
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
fetch-tags: true
submodules: recursive
- run: ./test/check-docker-context.sh --min-size 50000 --max-size 60000 --min-count 1500 --max-count 1700
- run: ./test/check-docker-context.sh --min-size 90000 --max-size 110000 --min-count 1500 --max-count 1700
- run: ./test/test-images.sh
- if: always()
run: docker compose logs
build-push-image:
if: |
(github.event_name == 'workflow_dispatch' && inputs.publish_image == true) ||
(github.event_name != 'workflow_dispatch' && (
github.ref == 'refs/heads/master' ||
startsWith(github.ref, 'refs/tags/v')
))
needs:
- test-images
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
image: [nginx, service]
env:
REGISTRY: ghcr.io
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
fetch-tags: true
submodules: recursive
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Show Docker Context
run: ./test/check-docker-context.sh --report

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/central-${{ matrix.image }}

- name: Set up QEMU emulator for multi-arch images
uses: docker/setup-qemu-action@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Build and push ${{ matrix.image }} Docker image
uses: docker/build-push-action@v7
with:
file: ${{ matrix.image }}.dockerfile
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: 'linux/amd64,linux/arm64'
10 changes: 5 additions & 5 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ services:
profiles:
- central
ports:
- 5432:5432
- 127.0.0.1:5432:5432
environment:
POSTGRES_USER: jubilant
POSTGRES_PASSWORD: jubilant
Expand Down Expand Up @@ -39,7 +39,7 @@ services:
profiles:
- central
ports:
- 5001:80
- 127.0.0.1:5001:80
secrets:
profiles:
- central
Expand All @@ -61,16 +61,16 @@ services:
extra_hosts:
- "${DOMAIN}:host-gateway"
ports:
- 8005:8005
- 127.0.0.1:8005:8005
enketo_redis_main:
profiles:
- central
ports:
- 6379:6379
- 127.0.0.1:6379:6379
enketo_redis_cache:
profiles:
- central
ports:
- 6380:6380
- 127.0.0.1:6380:6380
volumes:
dev_secrets:
3 changes: 1 addition & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ services:
service:
build:
context: .
args:
DB_SSL: ${DB_SSL:-} # So that we can error out at build time if this is defined with a value of "true" (no longer supported from 2026.1).
dockerfile: service.dockerfile
depends_on:
- secrets
Expand Down Expand Up @@ -68,6 +66,7 @@ services:
- PGPASSWORD=${PGPASSWORD-${DB_PASSWORD:-odk}}
- PGAPPNAME=${PGAPPNAME-odkcentral}
# End of libpq connection env var preparation.
- DB_SSL=${DB_SSL:-null}
- DB_POOL_SIZE=${DB_POOL_SIZE:-10}
- EMAIL_FROM=${EMAIL_FROM:-no-reply@$DOMAIN}
- EMAIL_HOST=${EMAIL_HOST:-mail}
Expand Down
8 changes: 0 additions & 8 deletions files/nginx/backend.conf

This file was deleted.

42 changes: 28 additions & 14 deletions files/nginx/odk.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ map "$request_method::$uri$is_args$args" $cache_strategy {

# enketo
~^(GET|HEAD)::/-(/x)?/css/ "revalidate";
~^(GET|HEAD)::/-(/x)?/fonts/.*?v= "immutable";
~^(GET|HEAD)::/-(/x)?/fonts/.*\?v= "immutable";
~^(GET|HEAD)::/-(/x)?/fonts/ "revalidate";
~^(GET|HEAD)::/-(/x)?/images/ "revalidate";
~^(GET|HEAD)::/-(/x)?/js/build/chunks/ "immutable";
Expand Down Expand Up @@ -91,12 +91,18 @@ map $arg_st $redirect_single_prefix {
map $request_uri $central_frontend_csp {
# Web Forms CSP for /f/... and /projects/.../forms/... routes
~^/(?:f/[^/]+(?:/.*)?|projects/\d+/forms/[^/]+/(?:(?:draft/)?(?:preview|submissions/new(?:/offline)?)|submissions/[^/]+/edit)(?:/)?)(?:\?.*)?$
"default-src 'report-sample' 'none'; connect-src 'self' https:; font-src 'self' data:; form-action 'self'; frame-ancestors 'self'; frame-src 'self' https://getodk.github.io/central/; img-src blob: data: https:; manifest-src 'self'; media-src blob:; object-src 'none'; script-src 'report-sample' 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; worker-src 'report-sample' blob:; report-uri /csp-report";
"default-src 'report-sample' 'none'; connect-src 'self' https:; font-src 'self' data:; form-action 'self'; frame-ancestors 'self'; frame-src 'self' https://getodk.github.io/central/; img-src blob: data: https:; manifest-src 'self'; media-src blob:; object-src 'none'; script-src 'report-sample' 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; worker-src 'report-sample' blob: data:; report-uri /csp-report";

default
"default-src 'report-sample' 'none'; connect-src 'self' https://translate.google.com https://translate.googleapis.com; font-src 'self'; form-action 'self'; frame-ancestors 'none'; frame-src 'self' https://getodk.github.io/central/; img-src data: https:; manifest-src 'self'; media-src 'none'; object-src 'none'; script-src 'report-sample' 'self'; style-src 'report-sample' 'self'; style-src-attr 'unsafe-inline'; worker-src 'report-sample' blob:; report-uri /csp-report";
}

map $upstream_http_content_security_policy $central_backend_csp {
# pass through any Content-Security-Policy received from upstream services (central-backend, enketo)
"" "default-src 'report-sample' 'none'; form-action 'none'; frame-ancestors 'none'; img-src http://${DOMAIN}/favicon.ico; report-uri /csp-report";
default $upstream_http_content_security_policy;
}

server {
listen 443 ssl;
http2 on;
Expand All @@ -114,7 +120,7 @@ server {

server_tokens off;

add_header Content-Security-Policy-Report-Only "default-src 'report-sample' 'none'; connect-src https://translate.google.com https://translate.googleapis.com; img-src https://translate.google.com; report-uri /csp-report" always;
add_header Content-Security-Policy "default-src 'report-sample' 'none'; connect-src https://translate.google.com https://translate.googleapis.com; img-src https://translate.google.com; report-uri /csp-report" always;
include /usr/share/odk/nginx/common-headers.conf;

client_max_body_size 100m;
Expand Down Expand Up @@ -159,6 +165,10 @@ server {
# For that iframe to work, we'll need another path prefix (enketo-passthrough) under which we can
# reach Enketo — this one will not be intercepted.
location ~ ^/(?:-|enketo-passthrough)(?:/|$) {
if ($args ~* "(^|&|;)(x|%78|%58)?(f|%66|%46)(o|%6f|%4f)(r|%72|%52)(m|%6d|%4d)(=[^;&]*)?(&|;|$)" ) {
return 400;
}

rewrite ^/enketo-passthrough(/.*)?$ /-$1 break;
proxy_pass http://enketo:8005;
proxy_redirect off;
Expand All @@ -169,31 +179,35 @@ server {
# More lax CSP for enketo-express:
# Google Maps API: https://developers.google.com/maps/documentation/javascript/content-security-policy
# Use 'none' per directive instead of falling back to default-src to make CSP violation reports more specific
proxy_hide_header Content-Security-Policy;
proxy_hide_header Content-Security-Policy-Report-Only;
add_header Content-Security-Policy-Report-Only "default-src 'report-sample' 'none'; connect-src 'self' blob: https://maps.googleapis.com/ https://maps.google.com/ https://maps.gstatic.com/mapfiles/ https://fonts.gstatic.com/ https://fonts.googleapis.com/ https://translate.google.com https://translate.googleapis.com; font-src 'self' https://fonts.gstatic.com/; form-action 'self'; frame-ancestors 'self'; frame-src 'none'; img-src data: blob: jr: 'self' https://maps.google.com/maps/ https://maps.gstatic.com/mapfiles/ https://maps.googleapis.com/maps/ https://tile.openstreetmap.org/ https://translate.google.com; manifest-src 'none'; media-src blob: jr: 'self'; object-src 'none'; script-src 'report-sample' 'unsafe-inline' 'self' https://maps.googleapis.com/maps/api/js/ https://maps.google.com/maps/ https://maps.google.com/maps-api-v3/api/js/; style-src 'unsafe-inline' 'self' https://fonts.googleapis.com/css; style-src-attr 'unsafe-inline'; report-uri /csp-report" always;
add_header Content-Security-Policy "default-src 'report-sample' 'none'; connect-src 'self' blob: https://maps.googleapis.com/ https://maps.google.com/ https://maps.gstatic.com/mapfiles/ https://fonts.gstatic.com/ https://fonts.googleapis.com/ https://translate.google.com https://translate.googleapis.com; font-src 'self' https://fonts.gstatic.com/; form-action 'self'; frame-ancestors 'self'; frame-src 'none'; img-src data: blob: jr: 'self' https://maps.google.com/maps/ https://maps.gstatic.com/mapfiles/ https://maps.googleapis.com/maps/ https://tile.openstreetmap.org/ https://translate.google.com; manifest-src 'none'; media-src blob: jr: 'self'; object-src 'none'; script-src 'report-sample' 'unsafe-inline' 'self' https://maps.googleapis.com/maps/api/js/ https://maps.google.com/maps/ https://maps.google.com/maps-api-v3/api/js/; style-src 'unsafe-inline' 'self' https://fonts.googleapis.com/css; style-src-attr 'unsafe-inline'; report-uri /csp-report" always;

include /usr/share/odk/nginx/common-headers.conf;
}
# End of Enketo Configuration.

location ~ ^/v\d+/oidc/callback$ {
include /usr/share/odk/nginx/common-headers.conf;
include /usr/share/odk/nginx/backend.conf;
}

location ~ ^/v\d {
proxy_hide_header Content-Security-Policy-Report-Only;
add_header Content-Security-Policy-Report-Only "default-src 'report-sample' 'none'; form-action 'none'; frame-ancestors 'none'; report-uri /csp-report" always;
proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy $central_backend_csp always;

include /usr/share/odk/nginx/common-headers.conf;
include /usr/share/odk/nginx/backend.conf;

proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://service:8383;
proxy_redirect off;

# buffer requests, but not responses, so streaming out works.
proxy_request_buffering on;
proxy_buffering off;
proxy_read_timeout 2m;
}

location @blank.html {
root /usr/share/nginx/html;
try_files /blank.html =404;

add_header Content-Security-Policy-Report-Only "default-src 'report-sample' 'none'; connect-src https://translate.google.com https://translate.googleapis.com; form-action 'none'; frame-ancestors 'self'; img-src 'self' https://translate.google.com; report-uri /csp-report" always;
add_header Content-Security-Policy "default-src 'report-sample' 'none'; connect-src https://translate.google.com https://translate.googleapis.com; form-action 'self'; frame-ancestors 'self'; img-src http://${DOMAIN}/favicon.ico https://translate.google.com; report-uri /csp-report" always;
include /usr/share/odk/nginx/common-headers.conf;
}
location = /blank.html {
Expand All @@ -204,7 +218,7 @@ server {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;

add_header Content-Security-Policy-Report-Only "$central_frontend_csp" always;
add_header Content-Security-Policy "$central_frontend_csp" always;

include /usr/share/odk/nginx/common-headers.conf;
}
Expand Down
2 changes: 1 addition & 1 deletion files/nginx/setup-odk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ else
# strip out all ssl_* directives
perl -i -ne 's/listen 443.*/listen 80;/; print if ! /\bssl_/' /etc/nginx/conf.d/odk.conf
# force https because we expect SSL upstream
perl -i -pe 's/X-Forwarded-Proto \$scheme/X-Forwarded-Proto https/;' /usr/share/odk/nginx/backend.conf
perl -i -pe 's/X-Forwarded-Proto \$scheme/X-Forwarded-Proto https/;' /etc/nginx/conf.d/odk.conf
echo "starting nginx for upstream ssl..."
else
# remove letsencrypt challenge reply, but keep 80 to 443 redirection
Expand Down
Loading
Loading