build: package standalone DWH authentication service
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
[Unit]
|
||||
Description=Standalone DWH API-key verifier
|
||||
After=local-fs.target
|
||||
Wants=local-fs.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=dwh-auth
|
||||
Group=www-data
|
||||
SupplementaryGroups=dwh-auth
|
||||
ExecStart=/usr/local/sbin/dwh-auth serve --registry-root /var/lib/dwh-auth --socket /run/dwh-auth/verify.sock
|
||||
Restart=on-failure
|
||||
RestartSec=2s
|
||||
RuntimeDirectory=dwh-auth
|
||||
RuntimeDirectoryMode=0750
|
||||
UMask=0007
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectClock=true
|
||||
ProtectControlGroups=true
|
||||
ProtectHostname=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectProc=invisible
|
||||
ReadOnlyPaths=/var/lib/dwh-auth
|
||||
ReadWritePaths=/run/dwh-auth
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
RestrictNamespaces=true
|
||||
RestrictRealtime=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
SystemCallArchitectures=native
|
||||
CapabilityBoundingSet=
|
||||
AmbientCapabilities=
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,7 @@
|
||||
# The registry is provisioned before dwh-auth starts. The SGID directories
|
||||
# preserve the dwh-auth group for records written by the operator CLI.
|
||||
d /var/lib/dwh-auth 2750 root dwh-auth - -
|
||||
d /var/lib/dwh-auth/active 2750 root dwh-auth - -
|
||||
d /var/lib/dwh-auth/revoked 2750 root dwh-auth - -
|
||||
# OpenReadOnly requires a pre-existing read-only shared-lock file.
|
||||
f /var/lib/dwh-auth/.writer.lock 0640 root dwh-auth - -
|
||||
@@ -0,0 +1,28 @@
|
||||
# Include these locations inside the HTTPS server that publishes /dwh/.
|
||||
# The verifier is deliberately reachable only through an internal subrequest.
|
||||
location = /_check_dwh_key {
|
||||
internal;
|
||||
proxy_method GET;
|
||||
proxy_pass http://unix:/run/dwh-auth/verify.sock:/verify;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header X-API-Key $http_x_api_key;
|
||||
}
|
||||
|
||||
location @dwh_auth_unavailable {
|
||||
return 503;
|
||||
}
|
||||
|
||||
location /dwh/ {
|
||||
limit_req zone=dwh_auth burst=100 nodelay;
|
||||
auth_request /_check_dwh_key;
|
||||
auth_request_set $dwh_key_id $upstream_http_x_dwh_key_id;
|
||||
error_page 500 =503 @dwh_auth_unavailable;
|
||||
|
||||
# Never forward the credential. Only the verifier's public identity may
|
||||
# reach the upstream for audit/rate attribution.
|
||||
proxy_set_header X-API-Key "";
|
||||
proxy_set_header X-DWH-Key-ID "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Include this file from the nginx http {} context. The map emits only a
|
||||
# public key ID for canonical v1 keys; all other input is one opaque class.
|
||||
map $http_x_api_key $dwh_public_key_class {
|
||||
default opaque;
|
||||
"~^thtdwh_v1\.([A-Za-z0-9_-]{16})\.[A-Za-z0-9_-]{43}$" v1:$1;
|
||||
}
|
||||
|
||||
# The secret is never part of this key. This limits each remote address and
|
||||
# public credential identity/class to 20 requests per second.
|
||||
map "$remote_addr:$dwh_public_key_class" $dwh_auth_rate_key {
|
||||
default "$remote_addr:$dwh_public_key_class";
|
||||
}
|
||||
limit_req_zone $dwh_auth_rate_key zone=dwh_auth:10m rate=20r/s;
|
||||
@@ -0,0 +1,18 @@
|
||||
FROM golang:1.26.5-bookworm@sha256:1ecb7edf62a0408027bd5729dfd6b1b8766e578e8df93995b225dfd0944eb651 AS build
|
||||
|
||||
WORKDIR /src/tools/dwh-auth
|
||||
COPY tools/dwh-auth/go.mod ./
|
||||
COPY tools/dwh-auth/ ./
|
||||
|
||||
RUN set -eux; \
|
||||
CGO_ENABLED=0 go test ./... -count=1; \
|
||||
mkdir -p /out; \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -trimpath -ldflags='-s -w' -o /out/dwh-auth-linux-amd64 ./cmd/dwh-auth; \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
|
||||
go build -trimpath -ldflags='-s -w' -o /out/dwh-auth-linux-arm64 ./cmd/dwh-auth; \
|
||||
test -s /out/dwh-auth-linux-amd64; \
|
||||
test -s /out/dwh-auth-linux-arm64
|
||||
|
||||
FROM scratch AS export
|
||||
COPY --from=build /out/ /
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root=$(cd "$(dirname "$0")/.." && pwd -P)
|
||||
default_output="$repo_root/dist/dwh-auth"
|
||||
output="$default_output"
|
||||
|
||||
usage() {
|
||||
echo "usage: $0 [--output ABS_CANONICAL]" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--output)
|
||||
(($# >= 2)) || usage
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$output" in
|
||||
# Explicit paths must already be canonical; the default is canonical by construction.
|
||||
/*)
|
||||
canonical_output=$(realpath -m "$output")
|
||||
[[ "$canonical_output" == "$output" ]] || { echo "build-dwh-auth: output must be canonical" >&2; exit 2; }
|
||||
;;
|
||||
*)
|
||||
echo "build-dwh-auth: output must be an absolute canonical directory" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
[[ "$output" != */../* && "$output" != */.. && "$output" != *'/./'* && "$output" != */. ]] || {
|
||||
echo "build-dwh-auth: output must be canonical" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
if [[ "$output" == / || "$output" == "$repo_root" || "$output" == /tmp || "$output" == /var || "$output" == /home ]]; then
|
||||
echo "build-dwh-auth: refusing broad output path" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
parent=$(dirname "$output")
|
||||
if [[ "$output" == "$default_output" && ! -e "$parent" ]]; then
|
||||
mkdir -p "$parent"
|
||||
fi
|
||||
[[ -d "$parent" && ! -L "$parent" ]] || {
|
||||
echo "build-dwh-auth: output parent must be an existing non-symlink directory" >&2
|
||||
exit 2
|
||||
}
|
||||
leaf=$(basename "$output")
|
||||
[[ "$parent/$leaf" == "$output" ]] || {
|
||||
echo "build-dwh-auth: output must be canonical" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
if [[ -e "$output" || -L "$output" ]]; then
|
||||
[[ -d "$output" && ! -L "$output" ]] || {
|
||||
echo "build-dwh-auth: output exists and is not a directory" >&2
|
||||
exit 2
|
||||
}
|
||||
if [[ -n "$(find "$output" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then
|
||||
echo "build-dwh-auth: refusing non-empty output directory" >&2
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
mkdir "$output"
|
||||
fi
|
||||
|
||||
command -v docker >/dev/null 2>&1 || {
|
||||
echo "build-dwh-auth: Docker is required for the pinned build" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
docker build \
|
||||
--file "$repo_root/docker/dwh-auth.Dockerfile" \
|
||||
--output "type=local,dest=$output" \
|
||||
"$repo_root"
|
||||
|
||||
for arch in amd64 arm64; do
|
||||
artifact="$output/dwh-auth-linux-$arch"
|
||||
[[ -s "$artifact" ]] || {
|
||||
echo "build-dwh-auth: builder did not produce $artifact" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root=$(cd "$(dirname "$0")/.." && pwd -P)
|
||||
cd "$repo_root"
|
||||
|
||||
die() {
|
||||
echo "dwh-auth build/deployment contract: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
builder_digest='golang:1.26.5-bookworm@sha256:1ecb7edf62a0408027bd5729dfd6b1b8766e578e8df93995b225dfd0944eb651'
|
||||
dockerfile=docker/dwh-auth.Dockerfile
|
||||
build_script=scripts/build-dwh-auth.sh
|
||||
service=deploy/dwh-auth/dwh-auth.service
|
||||
tmpfiles=deploy/dwh-auth/dwh-auth.tmpfiles.conf
|
||||
http=deploy/dwh-auth/nginx-http.conf.example
|
||||
location=deploy/dwh-auth/nginx-dwh-location.conf.example
|
||||
|
||||
[[ -f "$dockerfile" ]] || die "missing $dockerfile"
|
||||
[[ -f "$build_script" ]] || die "missing $build_script"
|
||||
[[ -f "$service" ]] || die "missing $service"
|
||||
[[ -f "$tmpfiles" ]] || die "missing $tmpfiles"
|
||||
[[ -f "$http" ]] || die "missing $http"
|
||||
[[ -f "$location" ]] || die "missing $location"
|
||||
|
||||
grep -Fq "FROM $builder_digest AS build" "$dockerfile" || die "builder image is not pinned"
|
||||
grep -Fq 'CGO_ENABLED=0' "$dockerfile" || die "CGO is not disabled"
|
||||
grep -Fq -- '-trimpath' "$dockerfile" || die "trimpath is missing"
|
||||
grep -Fq -- '-s -w' "$dockerfile" || die "strip flags are missing"
|
||||
if grep -Eq '^[[:space:]]*require([[:space:]]|\()' tools/dwh-auth/go.mod; then
|
||||
die "dwh-auth module declares dependencies"
|
||||
fi
|
||||
|
||||
for directive in \
|
||||
'User=dwh-auth' \
|
||||
'Group=www-data' \
|
||||
'SupplementaryGroups=dwh-auth' \
|
||||
'NoNewPrivileges=true' \
|
||||
'ProtectSystem=strict' \
|
||||
'ProtectHome=true' \
|
||||
'RestrictAddressFamilies=AF_UNIX' \
|
||||
'CapabilityBoundingSet=' \
|
||||
'UMask=0007'; do
|
||||
grep -Fq "$directive" "$service" || die "missing systemd directive: $directive"
|
||||
done
|
||||
grep -Fq 'RuntimeDirectory=dwh-auth' "$service" || die "runtime directory is missing"
|
||||
grep -Fq 'RuntimeDirectoryMode=0750' "$service" || die "runtime mode is missing"
|
||||
grep -Fq 'ReadOnlyPaths=/var/lib/dwh-auth' "$service" || die "registry is not read-only"
|
||||
grep -Fq 'ExecStart=/usr/local/sbin/dwh-auth serve --registry-root /var/lib/dwh-auth --socket /run/dwh-auth/verify.sock' "$service" \
|
||||
|| die "unexpected service command"
|
||||
|
||||
grep -Eq '^d[[:space:]]+/var/lib/dwh-auth[[:space:]]+2750[[:space:]]+root[[:space:]]+dwh-auth([[:space:]]|$)' "$tmpfiles" \
|
||||
|| die "tmpfiles root directory contract is missing"
|
||||
for child in active revoked; do
|
||||
grep -Eq "^d[[:space:]]+/var/lib/dwh-auth/$child[[:space:]]+2750[[:space:]]+root[[:space:]]+dwh-auth([[:space:]]|$)" "$tmpfiles" \
|
||||
|| die "tmpfiles $child directory contract is missing"
|
||||
done
|
||||
grep -Eq '^f[[:space:]]+/var/lib/dwh-auth/\.writer\.lock[[:space:]]+0640[[:space:]]+root[[:space:]]+dwh-auth([[:space:]]|$)' "$tmpfiles" \
|
||||
|| die "tmpfiles writer lock contract is missing"
|
||||
|
||||
grep -Fq 'auth_request /_check_dwh_key;' "$location" || die "DWH auth subrequest is missing"
|
||||
grep -Fq 'proxy_method GET;' "$location" || die "auth method is not GET"
|
||||
grep -Fq 'proxy_pass_request_body off;' "$location" || die "auth body is not disabled"
|
||||
grep -Fq 'proxy_set_header Content-Length "";' "$location" || die "auth body length is not cleared"
|
||||
grep -Fq 'proxy_set_header X-API-Key $http_x_api_key;' "$location" || die "key is not forwarded to auth"
|
||||
grep -Fq 'proxy_set_header X-API-Key "";' "$location" || die "key is not cleared upstream"
|
||||
grep -Fq 'proxy_set_header X-DWH-Key-ID "";' "$location" || die "public key ID is not cleared"
|
||||
[[ $(grep -Fc 'proxy_set_header X-DWH-Key-ID "";' "$location") -eq 1 ]] || die "public key ID must be cleared exactly once"
|
||||
grep -Fq 'proxy_pass http://unix:/run/dwh-auth/verify.sock:/verify;' "$location" || die "Unix auth socket is missing"
|
||||
grep -Fq 'location /dwh/ {' "$location" || die "DWH prefix location is missing"
|
||||
grep -Fq 'proxy_pass http://127.0.0.1:3001;' "$location" || die "DWH prefix is not preserved"
|
||||
grep -Fq 'limit_req zone=dwh_auth burst=100 nodelay;' "$location" || die "DWH rate limit is missing"
|
||||
grep -Fq 'error_page 500 =503 @dwh_auth_unavailable;' "$location" || die "auth infrastructure failure is not 503"
|
||||
grep -Fq 'map $http_x_api_key $dwh_public_key_class' "$http" || die "public rate-key map is missing"
|
||||
if rg -n 'limit_req_zone.*\$http_x_api_key|limit_req_zone.*\$dwh_key_secret|limit_req_zone.*\$request' "$http" "$location"; then
|
||||
die "rate key includes a secret or full request"
|
||||
fi
|
||||
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
out=$(mktemp -d)
|
||||
trap 'rm -rf "$out"' EXIT
|
||||
scripts/build-dwh-auth.sh --output "$out"
|
||||
for arch in amd64 arm64; do
|
||||
artifact="$out/dwh-auth-linux-$arch"
|
||||
[[ -s "$artifact" ]] || die "missing non-empty $artifact"
|
||||
file "$artifact" | grep -Eq 'ELF .*executable' || die "$artifact is not an ELF executable"
|
||||
readelf -h "$artifact" | grep -Eq 'OS/ABI:[[:space:]]+UNIX - (System V|GNU)' || die "$artifact is not a Linux ELF"
|
||||
if [[ "$arch" == amd64 ]]; then
|
||||
readelf -h "$artifact" | grep -Eq 'Machine:.*(X86-64|AMD64)' || die "$artifact has the wrong architecture"
|
||||
else
|
||||
readelf -h "$artifact" | grep -Eq 'Machine:.*AArch64' || die "$artifact has the wrong architecture"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo 'dwh-auth build and deployment contract passed'
|
||||
Reference in New Issue
Block a user