Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ HTTPS_PORT=443

# Optional: configure web user login session lifetime (in seconds)
# SESSION_LIFETIME=

# Optional: configure the per-worker database pool size
# DB_POOL_SIZE=

6 changes: 4 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ jobs:
submodules: recursive
- uses: actions/setup-node@v4
with:
node-version: 22.21.0
node-version: 22.21.1
- run: cd test/nginx && npm clean-install
- run: cd test/nginx && npm run lint
- run: cd test/nginx && ./run-tests.sh
- run: cd test/nginx && ./setup-tests.sh
- run: cd test/nginx && npm run test:nginx
- run: cd test/nginx && ./gixy.sh

- if: always()
run: cd test/nginx && docker compose -f nginx.test.docker-compose.yml logs --no-log-prefix nginx
Expand Down
7 changes: 4 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ services:
POSTGRES_PASSWORD: odk
POSTGRES_DB: odk
mail:
image: "registry.gitlab.com/egos-tech/smtp:1.1.1"
image: "registry.gitlab.com/egos-tech/smtp:1.1.5"
volumes:
- ./files/mail/rsa.private:/etc/exim4/dkim.key.temp:ro
environment:
Expand Down Expand Up @@ -55,6 +55,7 @@ services:
- DB_HOST=${DB_HOST:-postgres14}
- DB_USER=${DB_USER:-odk}
- DB_PASSWORD=${DB_PASSWORD:-odk}
- DB_POOL_SIZE=${DB_POOL_SIZE:-10}
- DB_NAME=${DB_NAME:-odk}
- DB_SSL=${DB_SSL:-null}
- EMAIL_FROM=${EMAIL_FROM:-no-reply@$DOMAIN}
Expand Down Expand Up @@ -137,7 +138,7 @@ services:
- SUPPORT_EMAIL=${SYSADMIN_EMAIL}
- HTTPS_PORT=${HTTPS_PORT:-443}
enketo_redis_main:
image: redis:7.4.6
image: redis:7.4.7
volumes:
- ./files/enketo/redis-enketo-main.conf:/usr/local/etc/redis/redis.conf:ro
- enketo_redis_main:/data
Expand All @@ -146,7 +147,7 @@ services:
- /usr/local/etc/redis/redis.conf
restart: always
enketo_redis_cache:
image: redis:7.4.6
image: redis:7.4.7
volumes:
- ./files/enketo/redis-enketo-cache.conf:/usr/local/etc/redis/redis.conf:ro
- enketo_redis_cache:/data
Expand Down
8 changes: 8 additions & 0 deletions files/nginx/backend.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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;
53 changes: 38 additions & 15 deletions files/nginx/odk.conf.template
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
server {
listen 443 default_server ssl;
server_tokens off;

ssl_certificate /etc/nginx/ssl/nginx.default.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.default.key;
Expand Down Expand Up @@ -74,6 +75,18 @@ map $arg_st $redirect_single_prefix {
default "/new${is_args}${args}${qp_deliminator}single=true";
}

# Use 'none' per directive instead of falling back to default-src to make CSP violation reports more specific
# Note: using $request_uri here remains safe while percent-encodings are not
# normalised in frontend URLs. Tracked at https://github.com/getodk/central/issues/1532
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 'none'; connect-src 'self' https:; font-src 'self' data:; frame-src 'none'; img-src blob: https:; manifest-src 'none'; media-src 'none'; object-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; worker-src blob:; report-uri /csp-report";

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

server {
listen 443 ssl;
http2 on;
Expand Down Expand Up @@ -130,8 +143,7 @@ server {
}
# To read single submission cookies
location = /-/single/check-submitted {
alias /usr/share/nginx/html/blank.html;
default_type text/html;
try_files $uri @blank.html;
}

# For that iframe to work, we'll need another path prefix (enketo-passthrough) under which we can
Expand All @@ -146,38 +158,49 @@ 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-Report-Only;
add_header Content-Security-Policy-Report-Only "default-src '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/; 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 '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";
#
# Rules set to 'none' here would fallback to default-src if excluded.
# They are included here to ease interpretation of violation reports.

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_set_header X-Forwarded-Proto $scheme;
proxy_pass http://service:8383;
proxy_redirect off;
proxy_hide_header Content-Security-Policy-Report-Only;
add_header Content-Security-Policy-Report-Only "default-src 'none'; report-uri /csp-report";

# buffer requests, but not responses, so streaming out works.
proxy_request_buffering on;
proxy_buffering off;
proxy_read_timeout 2m;
include /usr/share/odk/nginx/common-headers.conf;
include /usr/share/odk/nginx/backend.conf;
}

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

add_header Content-Security-Policy-Report-Only "default-src 'none'; connect-src https://translate.google.com https://translate.googleapis.com; img-src https://translate.google.com; report-uri /csp-report";
include /usr/share/odk/nginx/common-headers.conf;
}
location = /blank.html {
try_files $uri @blank.html;
}

location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;

# Rules set to 'none' here would fallback to default-src if excluded.
# They are included here to ease interpretation of violation reports.
add_header Content-Security-Policy-Report-Only "default-src 'none'; connect-src 'self' https://translate.google.com https://translate.googleapis.com; font-src 'self'; frame-src 'self' https://getodk.github.io/central/news.html; img-src * data:; manifest-src 'none'; media-src 'none'; object-src 'none'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; report-uri /csp-report";
add_header Content-Security-Policy-Report-Only "$central_frontend_csp";

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

location /csp-report {
proxy_pass https://${SENTRY_ORG_SUBDOMAIN}.ingest.sentry.io/api/${SENTRY_PROJECT}/security/?sentry_key=${SENTRY_KEY};
proxy_ssl_server_name on;
}
}
3 changes: 3 additions & 0 deletions files/nginx/redirector.conf
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Be VERY careful modifying this file - it is modified BY LINE NUMBER in setup-odk.sh
server {
# Listen on plain old HTTP and catch all requests so they can be redirected
# to HTTPS instead.
listen 80 reuseport;
listen [::]:80 reuseport;
server_name ${DOMAIN};
server_tokens off;

# Anything requesting this particular URL should be served content from
# Certbot's folder so the HTTP-01 ACME challenges can be completed for the
Expand All @@ -23,6 +25,7 @@ server {
server {
listen 80 default_server;
listen [::]:80 default_server;
server_tokens off;

return 421;
}
2 changes: 1 addition & 1 deletion files/nginx/setup-odk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ else
echo "starting nginx for upstream ssl..."
else
# remove letsencrypt challenge reply, but keep 80 to 443 redirection
perl -i -ne 'print if $. < 7 || $. > 14' /etc/nginx/conf.d/redirector.conf
perl -i -ne 'print if $. < 9 || $. > 16' /etc/nginx/conf.d/redirector.conf
echo "starting nginx for custom ssl and self-signed certs..."
fi
exec nginx -g "daemon off;"
Expand Down
3 changes: 2 additions & 1 deletion files/service/config.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"user": "${DB_USER}",
"password": "${DB_PASSWORD}",
"database": "${DB_NAME}",
"ssl": ${DB_SSL}
"ssl": ${DB_SSL},
"maximumPoolSize": ${DB_POOL_SIZE}
},
"email": {
"serviceAccount": "${EMAIL_FROM}",
Expand Down
3 changes: 2 additions & 1 deletion nginx.dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:22.21.0-slim AS intermediate
FROM node:22.21.1-slim AS intermediate

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
Expand Down Expand Up @@ -32,6 +32,7 @@ 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
2 changes: 1 addition & 1 deletion postgres14.dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM postgres:14.10
FROM postgres:14.20

COPY files/postgres14/start-postgres.sh /usr/local/bin/

Expand Down
2 changes: 1 addition & 1 deletion secrets.dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
FROM node:22.21.0-slim
FROM node:22.21.1-slim

COPY files/enketo/generate-secrets.sh ./
2 changes: 1 addition & 1 deletion service.dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG node_version=22.21.0
ARG node_version=22.21.1



Expand Down
23 changes: 23 additions & 0 deletions test/nginx/gixy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash -eu
set -o pipefail
shopt -s inherit_errexit

log() { echo >&2 "[$(basename "$0")] $*"; }

docker_compose() {
docker compose --file nginx.test.docker-compose.yml "$@"
}

log "Linting nginx config with gixy-ng..."
# gixy-ng is a maintained fork of gixy: https://github.com/dvershinin/gixy
# For version updates, see: https://pypi.org/project/gixy-ng/#history
docker_compose exec nginx bash -euc '
apt update
apt install -y python3-venv
python3 -m venv .venv
. .venv/bin/activate
pip install gixy-ng==0.2.12
gixy -lll
'

log "Completed OK."
4 changes: 4 additions & 0 deletions test/nginx/mock-http-server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const app = express();

app.use((req, res, next) => {
console.log(new Date(), req.method, req.originalUrl);

// always set CSP header to detect (or allow) leaks from backend through to the client
res.set('Content-Security-Policy-Report-Only', 'default-src NOTE:FROM-BACKEND');

next();
});

Expand Down
Loading