Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
c269c11
test/nginx/docker-compose: restrict open ports to local machine
May 31, 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 50000 --max-size 100000 --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'
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.

38 changes: 24 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 @@ -169,31 +175,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 +214,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
18 changes: 18 additions & 0 deletions files/service/scripts/start-odk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@
set -o pipefail
shopt -s inherit_errexit

# Check for illegal DB_SSL environment variable.
if ! [[ "${DB_SSL-}" = null ]]; then
echo "!!!"
echo "!!! You have the DB_SSL variable defined (in your .env file, probably)."
echo "!!! This variable is no longer supported from Central 2026.1 onwards."
echo "!!! There is a new way of configuring SSL for your database, please see:"
echo "!!!"
echo "!!! https://docs.getodk.org/central-install-digital-ocean/#using-a-custom-database-server"
echo "!!!"
echo "!!! Please refer to the Central 2026.1.0 release notes for more information on this change."
echo "!!!"
echo "!!! ODK Central backend will not start until this issue is resolved."
echo "!!!"
sleep 60 # reduce resource waste from quick restart and instant failure
exit 1
fi
unset DB_SSL

# Serialize (as a raw env block) the environment set up by docker, for later
# availability to processes running with a reset environment (such as cronjobs).
# See https://github.com/getodk/central/issues/1747 .
Expand Down
1 change: 0 additions & 1 deletion nginx.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ COPY files/nginx/setup-odk.sh \
/scripts/

COPY files/nginx/redirector.conf /usr/share/odk/nginx/
COPY files/nginx/backend.conf /usr/share/odk/nginx/
COPY files/nginx/common-headers.conf /usr/share/odk/nginx/
COPY files/nginx/robots.txt /usr/share/nginx/html
COPY --from=intermediate client/dist/ /usr/share/nginx/html
Expand Down
4 changes: 2 additions & 2 deletions service.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ $(grep -oP 'VERSION_CODEN
&& curl https://www.postgresql.org/media/keys/ACCC4CF8.asc \
| gpg --dearmor > /etc/apt/trusted.gpg.d/apt.postgresql.org.gpg

ARG DB_SSL
RUN [ -z "${DB_SSL}" ] || (/bin/echo -e '\n\n\n\n\nYou have the DB_SSL variable defined (in your .env file, probably).\nThis variable is no longer supported from Central 2026.1 onwards.\nThere is a new way of configuring SSL for your database, please see:\n\nhttps://docs.getodk.org/central-install-digital-ocean/#using-a-custom-database-server\n\nPlease refer to the Central 2026.1.0 release notes for more information on this change.\n\n\n\n\n'; exit 13)


FROM node:${node_version}-slim AS intermediate
RUN apt-get update \
Expand Down Expand Up @@ -52,6 +51,7 @@ RUN apt-get update \
procps \
postgresql-client-14 \
netcat-traditional \
openssl \
&& rm -rf /var/lib/apt/lists/* \
&& npm clean-install --omit=dev --no-audit \
--fund=false --update-notifier=false
Expand Down
Loading
Loading