diff --git a/.gitea/workflows/nightly.yml b/.gitea/workflows/nightly.yml index 3353e6f7..870a5a99 100644 --- a/.gitea/workflows/nightly.yml +++ b/.gitea/workflows/nightly.yml @@ -112,6 +112,11 @@ jobs: # fail this check rather than pass it silently. curl -fsS $RESOLVE --max-time 10 -I "$URL/" \ | grep -Eqi 'strict-transport-security:[[:space:]]*max-age=31536000.*includeSubDomains.*preload' + # Permissions-Policy denies APIs the app does not use (camera, + # microphone, geolocation). A regression that loosens or drops the + # header now fails the smoke step. + curl -fsS $RESOLVE --max-time 10 -I "$URL/" \ + | grep -Eqi 'permissions-policy:[[:space:]]*camera=\(\),[[:space:]]*microphone=\(\),[[:space:]]*geolocation=\(\)' status=$(curl -s $RESOLVE -o /dev/null -w "%{http_code}" --max-time 10 "$URL/actuator/health") [ "$status" = "404" ] || { echo "expected 404 from /actuator/health, got $status"; exit 1; } echo "All smoke checks passed" diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 69e59dd6..3d5c99d1 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -106,6 +106,11 @@ jobs: # fail this check rather than pass it silently. curl -fsS $RESOLVE --max-time 10 -I "$URL/" \ | grep -Eqi 'strict-transport-security:[[:space:]]*max-age=31536000.*includeSubDomains.*preload' + # Permissions-Policy denies APIs the app does not use (camera, + # microphone, geolocation). A regression that loosens or drops the + # header now fails the smoke step. + curl -fsS $RESOLVE --max-time 10 -I "$URL/" \ + | grep -Eqi 'permissions-policy:[[:space:]]*camera=\(\),[[:space:]]*microphone=\(\),[[:space:]]*geolocation=\(\)' status=$(curl -s $RESOLVE -o /dev/null -w "%{http_code}" --max-time 10 "$URL/actuator/health") [ "$status" = "404" ] || { echo "expected 404 from /actuator/health, got $status"; exit 1; } echo "All smoke checks passed" diff --git a/infra/caddy/Caddyfile b/infra/caddy/Caddyfile index b5dfd345..fc6b02bb 100644 --- a/infra/caddy/Caddyfile +++ b/infra/caddy/Caddyfile @@ -18,6 +18,10 @@ Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" Referrer-Policy "strict-origin-when-cross-origin" + # Deny browser APIs the app does not use. Reduces blast radius of an + # XSS landing in a privileged origin: a payload cannot silently turn + # on the microphone or read geolocation. + Permissions-Policy "camera=(), microphone=(), geolocation=()" -Server } }