deploy: optional Uvicorn TLS with a name-constrained private CA for IP-only networks
scripts/make-tls.sh creates a CA whose critical nameConstraints permit only the listed IPv4 addresses (and the .invalid DNS subtree), plus an end-entity server certificate; run-backend.sh passes CONFLUENCE_WEB_TLS_CERT/KEY to uvicorn.
This commit is contained in:
parent
2dd2323f79
commit
c853fff995
2
.gitignore
vendored
2
.gitignore
vendored
@ -18,3 +18,5 @@ report/
|
||||
|
||||
# deployment secrets / local configuration
|
||||
deploy/*.env
|
||||
# private TLS keys and certificates
|
||||
deploy/tls/
|
||||
|
||||
7
Makefile
7
Makefile
@ -6,7 +6,7 @@ IMAGE ?= confluence-pi-agent:rev1
|
||||
FAKE_IMAGE ?= confluence-fake-agent:test
|
||||
ENV_FILE ?= deploy/confluence-web.env
|
||||
|
||||
.PHONY: install check check-python check-agent check-frontend check-docker check-browser build-image build-fake-image image-checks run run-dev clean-containers
|
||||
.PHONY: install check check-python check-agent check-frontend check-docker check-browser build-image build-fake-image image-checks run run-dev tls clean-containers
|
||||
|
||||
install:
|
||||
$(PYTHON) -m pip install -r requirements.txt
|
||||
@ -53,6 +53,11 @@ image-checks:
|
||||
run:
|
||||
scripts/run-backend.sh $(ENV_FILE)
|
||||
|
||||
## Name-constrained private CA + server certificate for IP-only TLS: make tls IP=172.26.10.20
|
||||
tls:
|
||||
@test -n "$(IP)" || { echo "usage: make tls IP=<lan-ip> [IP='<ip> <ip>']"; exit 2; }
|
||||
scripts/make-tls.sh $(IP)
|
||||
|
||||
## Network-free UI development against explicit fakes (no Docker).
|
||||
run-dev:
|
||||
CONFLUENCE_WEB_DEV_MODE=true CONFLUENCE_WEB_FRONTEND_DIST_DIR=$(CURDIR)/frontend \
|
||||
|
||||
46
README.md
46
README.md
@ -60,11 +60,47 @@ destination is reachable but cannot prove the PAT is valid; a wrong token then
|
||||
shows up as denied or missing pages. Credentials live only in browser memory and in the backend
|
||||
for the duration of a request; a page reload clears them.
|
||||
|
||||
Version 1 is a single-user deployment: one backend worker, one query at a time
|
||||
(a second query gets `busy`), bound to loopback. Do not bind it to a network
|
||||
interface without an authenticating reverse proxy in front; if you add one, it
|
||||
must accept `6*16 MiB + 64 KiB` request bodies, pass client aborts through
|
||||
promptly, avoid buffering bodies to disk, and allow at least 200 s per request.
|
||||
Version 1 is a small-team deployment: one backend worker, one query at a time
|
||||
(a second query gets `busy`), loopback by default. The application has no login
|
||||
of its own; anyone who can reach the port can run queries with their own PAT.
|
||||
Never bind it to a network interface over plain HTTP, because every request
|
||||
carries the user's PAT. If you put a reverse proxy in front, it must accept
|
||||
`6*16 MiB + 64 KiB` request bodies, pass client aborts through promptly, avoid
|
||||
buffering bodies to disk, and allow at least 200 s per request.
|
||||
|
||||
### Serving colleagues on an internal network (IP only, TLS)
|
||||
|
||||
Uvicorn terminates TLS itself; no reverse proxy or DNS name is needed. The
|
||||
certificate comes from a private CA that is **name-constrained** to the listed
|
||||
IP addresses, so colleagues who import it are trusting it for those addresses
|
||||
only. A certificate issued by the same CA for any hostname or any other address
|
||||
is rejected by browsers and by OpenSSL (`permitted subtree violation`).
|
||||
|
||||
```bash
|
||||
make tls IP=172.26.10.20 # creates deploy/tls/{ca,server}.{key,crt}; git-ignored
|
||||
```
|
||||
|
||||
Then in `deploy/confluence-web.env`:
|
||||
|
||||
```
|
||||
CONFLUENCE_WEB_BIND_HOST=172.26.10.20
|
||||
CONFLUENCE_WEB_TLS_CERT=./deploy/tls/server.crt
|
||||
CONFLUENCE_WEB_TLS_KEY=./deploy/tls/server.key
|
||||
```
|
||||
|
||||
Restart with `make run` and open `https://172.26.10.20:8000/`. Under HTTPS the
|
||||
session cookie gains the `Secure` flag automatically and the same-origin check
|
||||
expects `https://` origins.
|
||||
|
||||
Hand colleagues only `deploy/tls/ca.crt`, imported once as a trusted root
|
||||
(Windows: double-click, "Trusted Root Certification Authorities"; macOS:
|
||||
Keychain Access; Chrome on Linux: chrome://settings/certificates; Firefox:
|
||||
Settings, Certificates, Authorities). They can read the constraint themselves in
|
||||
the certificate details ("Name Constraints: Permitted IP ..."). Keep `ca.key`
|
||||
and `server.key` on the server host. The server certificate is valid for 825
|
||||
days (the browser maximum); rotate it with the same CA by deleting
|
||||
`deploy/tls/server.*` and re-running `make tls` with the CA files kept, or
|
||||
rotate everything by deleting the directory.
|
||||
|
||||
Network-free UI development against explicit fakes (no Docker, PAT `dev-pat`,
|
||||
URL `https://approved.example.com`):
|
||||
|
||||
@ -33,8 +33,14 @@ CONFLUENCE_WEB_CONTAINER_LABEL_VALUE=confluence-web-local
|
||||
#CONFLUENCE_WEB_DOCKER_HOST=unix:///run/user/1000/docker.sock
|
||||
|
||||
# --- HTTP / storage ---
|
||||
# Loopback only by default. To serve colleagues on an internal network, bind the
|
||||
# host's LAN address AND enable TLS below (PATs must not cross the network in clear).
|
||||
CONFLUENCE_WEB_BIND_HOST=127.0.0.1
|
||||
CONFLUENCE_WEB_BIND_PORT=8000
|
||||
# Optional TLS termination by Uvicorn. Generate with: scripts/make-tls.sh <lan-ip>
|
||||
# (name-constrained private CA; distribute deploy/tls/ca.crt to colleagues).
|
||||
#CONFLUENCE_WEB_TLS_CERT=./deploy/tls/server.crt
|
||||
#CONFLUENCE_WEB_TLS_KEY=./deploy/tls/server.key
|
||||
# Absolute path to the frontend tree (index.html, css/, js/, vendor/).
|
||||
CONFLUENCE_WEB_FRONTEND_DIST_DIR=./frontend
|
||||
# Private artifact storage (created 0700; purged on startup).
|
||||
|
||||
106
scripts/make-tls.sh
Executable file
106
scripts/make-tls.sh
Executable file
@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
# Create a name-constrained private CA and a server certificate for IP-only access.
|
||||
#
|
||||
# scripts/make-tls.sh <ip> [<ip>...] (default output dir: deploy/tls)
|
||||
# TLS_DIR=/path scripts/make-tls.sh <ip>
|
||||
#
|
||||
# The CA carries a critical nameConstraints extension that permits only the listed
|
||||
# IP addresses and no DNS names, so importing ca.crt as a trusted root lets a browser
|
||||
# accept certificates from this CA for those addresses only. The server certificate
|
||||
# is an end-entity certificate (CA:FALSE) with the addresses as subjectAltName.
|
||||
#
|
||||
# Distribute only ca.crt. Keep ca.key and server.key on the server host.
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "usage: $0 <ip> [<ip>...]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TLS_DIR="${TLS_DIR:-$ROOT/deploy/tls}"
|
||||
CA_DAYS="${CA_DAYS:-3650}"
|
||||
LEAF_DAYS="${LEAF_DAYS:-825}" # Chrome and Safari reject server certificates valid for longer.
|
||||
|
||||
for ip in "$@"; do
|
||||
if [[ ! "$ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then
|
||||
echo "only IPv4 addresses are supported: $ip" >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -e "$TLS_DIR/server.key" || -e "$TLS_DIR/server.crt" ]]; then
|
||||
echo "refusing to overwrite $TLS_DIR/server.*; delete them to rotate the server certificate" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -e "$TLS_DIR/ca.key" && ! -e "$TLS_DIR/ca.crt" ]] || [[ ! -e "$TLS_DIR/ca.key" && -e "$TLS_DIR/ca.crt" ]]; then
|
||||
echo "incomplete CA in $TLS_DIR (need both ca.key and ca.crt, or neither)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
umask 077
|
||||
mkdir -p "$TLS_DIR"
|
||||
|
||||
permitted=""
|
||||
san=""
|
||||
for ip in "$@"; do
|
||||
permitted+="permitted;IP:${ip}/255.255.255.255,"
|
||||
san+="IP:${ip},"
|
||||
done
|
||||
permitted="${permitted%,}"
|
||||
san="${san%,}"
|
||||
|
||||
# Only the listed addresses may appear in certificates from this CA. Any DNS name is
|
||||
# forced to match the reserved .invalid domain, which no real host can, so this root
|
||||
# can never vouch for a hostname.
|
||||
cat > "$TLS_DIR/server.ext" <<EOF
|
||||
basicConstraints=critical,CA:FALSE
|
||||
keyUsage=critical,digitalSignature,keyEncipherment
|
||||
extendedKeyUsage=serverAuth
|
||||
subjectKeyIdentifier=hash
|
||||
authorityKeyIdentifier=keyid
|
||||
subjectAltName=${san}
|
||||
EOF
|
||||
|
||||
if [[ -e "$TLS_DIR/ca.key" ]]; then
|
||||
echo "reusing existing CA $TLS_DIR/ca.crt (server certificate rotation)"
|
||||
else
|
||||
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
|
||||
-keyout "$TLS_DIR/ca.key" -out "$TLS_DIR/ca.crt" -days "$CA_DAYS" \
|
||||
-subj "/CN=Confluence Research internal CA ($1)" \
|
||||
-config <(printf '[req]\ndistinguished_name=dn\n[dn]\n') \
|
||||
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
|
||||
-addext "keyUsage=critical,keyCertSign,cRLSign" \
|
||||
-addext "nameConstraints=critical,${permitted},permitted;DNS:.invalid" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
|
||||
-keyout "$TLS_DIR/server.key" -out "$TLS_DIR/server.csr" -subj "/CN=Confluence Research server ($1)" \
|
||||
-config <(printf '[req]\ndistinguished_name=dn\n[dn]\n') >/dev/null 2>&1
|
||||
|
||||
openssl x509 -req -in "$TLS_DIR/server.csr" -CA "$TLS_DIR/ca.crt" -CAkey "$TLS_DIR/ca.key" \
|
||||
-CAcreateserial -out "$TLS_DIR/server.crt" -days "$LEAF_DAYS" \
|
||||
-extfile "$TLS_DIR/server.ext" >/dev/null 2>&1
|
||||
|
||||
rm -f "$TLS_DIR/server.csr" "$TLS_DIR/ca.srl" "$TLS_DIR/server.ext"
|
||||
chmod 0644 "$TLS_DIR/ca.crt" "$TLS_DIR/server.crt"
|
||||
|
||||
# Fails if an existing CA does not permit one of the requested addresses.
|
||||
if ! openssl verify -CAfile "$TLS_DIR/ca.crt" "$TLS_DIR/server.crt" >/dev/null; then
|
||||
echo "server certificate does not verify under $TLS_DIR/ca.crt (CA name constraints do not cover $*?)" >&2
|
||||
rm -f "$TLS_DIR/server.key" "$TLS_DIR/server.crt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
Created in $TLS_DIR:
|
||||
ca.crt distribute this one file; import as a trusted root (constrained to: $*)
|
||||
ca.key keep private; needed only to issue a new server.crt
|
||||
server.crt server certificate for: $*
|
||||
server.key server private key
|
||||
|
||||
Enable in the env file:
|
||||
CONFLUENCE_WEB_BIND_HOST=$1
|
||||
CONFLUENCE_WEB_TLS_CERT=$TLS_DIR/server.crt
|
||||
CONFLUENCE_WEB_TLS_KEY=$TLS_DIR/server.key
|
||||
EOF
|
||||
@ -36,6 +36,26 @@ if [[ "${CONFLUENCE_WEB_DEV_MODE:-false}" != "true" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Optional TLS termination by Uvicorn (see scripts/make-tls.sh). Both or neither.
|
||||
TLS_ARGS=()
|
||||
if [[ -n "${CONFLUENCE_WEB_TLS_CERT:-}" || -n "${CONFLUENCE_WEB_TLS_KEY:-}" ]]; then
|
||||
if [[ -z "${CONFLUENCE_WEB_TLS_CERT:-}" || -z "${CONFLUENCE_WEB_TLS_KEY:-}" ]]; then
|
||||
echo "CONFLUENCE_WEB_TLS_CERT and CONFLUENCE_WEB_TLS_KEY must be set together" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Relative paths resolve against the repository root, like the frontend directory.
|
||||
[[ "$CONFLUENCE_WEB_TLS_CERT" != /* ]] && CONFLUENCE_WEB_TLS_CERT="$ROOT/${CONFLUENCE_WEB_TLS_CERT#./}"
|
||||
[[ "$CONFLUENCE_WEB_TLS_KEY" != /* ]] && CONFLUENCE_WEB_TLS_KEY="$ROOT/${CONFLUENCE_WEB_TLS_KEY#./}"
|
||||
for f in "$CONFLUENCE_WEB_TLS_CERT" "$CONFLUENCE_WEB_TLS_KEY"; do
|
||||
if [[ ! -r "$f" ]]; then
|
||||
echo "TLS file not readable: $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
TLS_ARGS=(--ssl-certfile "$CONFLUENCE_WEB_TLS_CERT" --ssl-keyfile "$CONFLUENCE_WEB_TLS_KEY")
|
||||
fi
|
||||
|
||||
exec "$PYTHON" -m uvicorn backend.app:create_app --factory --workers 1 \
|
||||
--host "${CONFLUENCE_WEB_BIND_HOST:-127.0.0.1}" --port "${CONFLUENCE_WEB_BIND_PORT:-8000}" \
|
||||
--no-server-header --timeout-keep-alive 5 --limit-concurrency 32 --app-dir "$ROOT"
|
||||
--no-server-header --timeout-keep-alive 5 --limit-concurrency 32 --app-dir "$ROOT" \
|
||||
"${TLS_ARGS[@]}"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user