Skip to content

Commit 8e29b9f

Browse files
NotYuShengclaude
andauthored
feat(offline): authenticated offline deployment (Keycloak overlay) (#563)
* feat(offline): optionally bundle Keycloak in pull-and-save-images Prompt whether to include the Keycloak image when saving images for an offline transfer, so authenticated offline deployments have the IdP available. The tag is read from docker-compose.prod.yml so it never drifts from the auth overlay; appending to the image list means both the pull and save loops pick it up. Honours INCLUDE_KEYCLOAK=true|false to skip the prompt (defaults to excluding on a non-TTY, so CI is unchanged). The run summary notes that the image alone isn't enough to run auth offline (realm export + auth-enabled compose/nginx still needed). Refs #559 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix/keycloak-image-parse-exit-and-portable-case Two review fixes on the Keycloak opt-in: - Append `|| true` to the KEYCLOAK_IMAGE command substitution so a missing docker-compose.prod.yml or a no-match grep no longer exits the whole script under `set -euo pipefail` (want_keycloak already handles an empty tag gracefully). - Replace the bash-4-only `${INCLUDE_KEYCLOAK,,}` with a portable case-insensitive `case` pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(offline): add authenticated offline deployment (Keycloak overlay) Add docker-compose.offline-prod.yml — the offline equivalent of the prod auth overlay, using pre-built images instead of building from source. It adds a Keycloak service (import-realm), turns on backend auth, and points nginx at an auth-enabled frontend image. pull-and-save-images.sh now, when Keycloak is included, also builds and saves an auth-enabled nginx image (tracepcap-nginx-auth) with the VITE_AUTH_* / VITE_OIDC_* build args, and the run summary points at the new overlay + PUBLIC_URL. Document the flow in offline-deployment.rst. This makes "save the Keycloak image" actually usable offline end to end. Refs #559 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(offline): pass VITE_APP_VERSION, fix api base, persist keycloak Address review + a smoke-test failure of the offline auth path: - Pass VITE_APP_VERSION to both nginx builds (resolved via git describe). Without it the frontend build fails outright — vite.config.ts requires it and git describe isn't available inside the Docker builder. - Default VITE_API_BASE_URL to /api/v1 (matches nginx/Dockerfile and the base compose) instead of /api, which routed the SPA to the wrong path. - Parse the Keycloak image tag from docker-compose.offline-prod.yml (the file that actually runs offline) so the saved image can't drift from it. - Add a persistent keycloak_data volume so users created in the admin console survive container recreate. - Warn against a trailing slash in PUBLIC_URL in the offline docs. Refs #559 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 323fa79 commit 8e29b9f

3 files changed

Lines changed: 159 additions & 8 deletions

File tree

docker-compose.offline-prod.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# docker-compose.offline-prod.yml
2+
#
3+
# Offline + authentication overlay. Layers OIDC auth (a bundled Keycloak) on top of the
4+
# offline stack, using pre-built images loaded via scripts/load-images.sh — the offline
5+
# equivalent of docker-compose.prod.yml (which builds from source and cannot run air-gapped).
6+
#
7+
# Prerequisites:
8+
# 1. On an internet-connected machine, INCLUDE Keycloak when saving images:
9+
# INCLUDE_KEYCLOAK=true bash scripts/pull-and-save-images.sh
10+
# This saves the Keycloak image AND an auth-enabled frontend (tracepcap-nginx-auth).
11+
# 2. Transfer images/, this file, docker-compose.offline.yml, keycloak/realm-export.json,
12+
# .env, and scripts/load-images.sh to the offline machine.
13+
# 3. On the offline machine: bash scripts/load-images.sh
14+
#
15+
# Start (layer this file ON TOP of the base offline file):
16+
# PUBLIC_URL=http://<host>:8888 \
17+
# docker compose -f docker-compose.offline.yml -f docker-compose.offline-prod.yml up -d
18+
#
19+
# PUBLIC_URL must be the exact origin you browse to (scheme + host + port). It pins Keycloak's
20+
# token issuer and the backend's issuer check; the browser must load the app via this same
21+
# origin. It does NOT track NGINX_PORT — default is http://localhost:8888. See
22+
# docs/configuration/authentication.rst and docs/getting-started/offline-deployment.rst.
23+
#
24+
# Manage users at <PUBLIC_URL>/admin (Keycloak admin console, served same-origin by nginx).
25+
# Default demo login: analyst / analyst. Default Keycloak admin: user / P@ssw0rd — override
26+
# both for any real deployment.
27+
services:
28+
keycloak:
29+
image: quay.io/keycloak/keycloak:26.0
30+
container_name: tracepcap-keycloak
31+
command: ["start-dev", "--import-realm"]
32+
environment:
33+
KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN:-user}
34+
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-P@ssw0rd}
35+
# Public, browser-facing base URL — determines the token `iss` claim and all KC URLs.
36+
KC_HOSTNAME: ${PUBLIC_URL:-http://localhost:8888}
37+
# Trust X-Forwarded-* from the nginx proxy (same-origin reverse proxy in front of KC).
38+
KC_PROXY_HEADERS: xforwarded
39+
KC_HTTP_ENABLED: "true"
40+
KC_HEALTH_ENABLED: "true"
41+
TZ: Asia/Singapore
42+
volumes:
43+
- ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json:ro
44+
# Persist Keycloak's embedded H2 store so accounts/credentials created at
45+
# runtime in the admin console survive container restart/recreate. (The
46+
# online prod overlay is ephemeral by the same start-dev design; persistence
47+
# matters more offline, where re-provisioning users is manual.)
48+
- keycloak_data:/opt/keycloak/data
49+
networks:
50+
- tracepcap-network
51+
52+
backend:
53+
environment:
54+
TRACEPCAP_AUTH_ENABLED: "true"
55+
# Public issuer (matches the token `iss`). Must equal PUBLIC_URL + /realms/tracepcap.
56+
KEYCLOAK_ISSUER_URI: ${PUBLIC_URL:-http://localhost:8888}/realms/tracepcap
57+
# Backend-reachable JWKS endpoint (internal docker network), fetched lazily so the
58+
# backend need not wait for Keycloak to be ready.
59+
KEYCLOAK_JWK_SET_URI: http://keycloak:8080/realms/tracepcap/protocol/openid-connect/certs
60+
depends_on:
61+
keycloak:
62+
condition: service_started
63+
64+
nginx:
65+
# Auth-enabled frontend built by pull-and-save-images.sh when Keycloak is included.
66+
# Overrides the plain tracepcap-nginx:latest from the base offline file.
67+
image: tracepcap-nginx-auth:latest
68+
depends_on:
69+
- backend
70+
- keycloak
71+
72+
volumes:
73+
keycloak_data:
74+
driver: local

docs/getting-started/offline-deployment.rst

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,49 @@ Step 3 — Load Images and Start the Stack (offline machine)
8686
# Start the stack using the offline compose file
8787
docker compose -f docker-compose.offline.yml up -d
8888
89+
Authenticated Offline Deployment (Keycloak)
90+
-------------------------------------------
91+
92+
The base offline stack runs with **no login**. To deploy offline *with* OIDC
93+
authentication, use the ``docker-compose.offline-prod.yml`` overlay — the
94+
offline equivalent of :doc:`../configuration/authentication` (which builds from
95+
source and so cannot run air-gapped).
96+
97+
**Step 1 (online machine)** — include Keycloak when saving images:
98+
99+
.. code-block:: bash
100+
101+
INCLUDE_KEYCLOAK=true bash scripts/pull-and-save-images.sh
102+
103+
This additionally saves the Keycloak image and an **auth-enabled frontend**
104+
(``tracepcap-nginx-auth``) alongside the normal tarballs.
105+
106+
**Step 2** — also transfer ``docker-compose.offline-prod.yml`` and
107+
``keycloak/realm-export.json`` (in addition to the files listed above).
108+
109+
**Step 3 (offline machine)** — load images, then start with the auth overlay,
110+
setting ``PUBLIC_URL`` to the exact origin you browse to (scheme + host + port):
111+
112+
.. code-block:: bash
113+
114+
bash scripts/load-images.sh
115+
116+
PUBLIC_URL=http://<host>:8888 \
117+
docker compose -f docker-compose.offline.yml -f docker-compose.offline-prod.yml up -d
118+
119+
.. warning::
120+
``PUBLIC_URL`` does **not** track ``NGINX_PORT`` (default
121+
``http://localhost:8888``). It pins Keycloak's token issuer and the backend's
122+
issuer check, so the browser must load the app via this same origin. Do **not**
123+
include a trailing slash (use ``http://<host>:8888``, not
124+
``http://<host>:8888/``) — it produces a double slash in the issuer URI and
125+
breaks JWT validation.
126+
127+
Create and manage logins at ``<PUBLIC_URL>/admin`` — see
128+
:doc:`../configuration/user-management`. The default demo login is
129+
``analyst`` / ``analyst`` and the default Keycloak admin is ``user`` /
130+
``P@ssw0rd``; **change both for any real deployment.**
131+
89132
LLM Configuration for Offline Use
90133
----------------------------------
91134

scripts/pull-and-save-images.sh

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ IMAGES_DIR="$ROOT_DIR/images"
2424

2525
BACKEND_IMAGE="tracepcap-backend:latest"
2626
NGINX_IMAGE="tracepcap-nginx:latest"
27+
# Auth-enabled frontend, built only when Keycloak is included. Kept as a separate
28+
# tag so the auth-off offline stack keeps using the plain nginx image unchanged.
29+
NGINX_AUTH_IMAGE="tracepcap-nginx-auth:latest"
2730

2831
# ---------------------------------------------------------------------------
2932
# Helper
@@ -65,12 +68,13 @@ mapfile -t DOCKERHUB_IMAGES < <(
6568
)
6669

6770
# Optionally include Keycloak, for an authenticated offline deployment. The image
68-
# tag is read from docker-compose.prod.yml (the auth overlay) so it never drifts.
71+
# tag is read from docker-compose.offline-prod.yml — the SAME file the offline host
72+
# starts — so the saved image can never drift from what the offline stack expects.
6973
# Appending to DOCKERHUB_IMAGES means the pull and save loops below both pick it
7074
# up automatically. Set INCLUDE_KEYCLOAK=true|false to skip the prompt (e.g. CI).
71-
PROD_COMPOSE="$ROOT_DIR/docker-compose.prod.yml"
75+
OFFLINE_PROD_COMPOSE="$ROOT_DIR/docker-compose.offline-prod.yml"
7276
KEYCLOAK_IMAGE="$(
73-
grep -E '^[[:space:]]*image:[[:space:]]*.*keycloak' "$PROD_COMPOSE" 2>/dev/null | \
77+
grep -E '^[[:space:]]*image:[[:space:]]*.*keycloak' "$OFFLINE_PROD_COMPOSE" 2>/dev/null | \
7478
sed -E 's/^[[:space:]]*image:[[:space:]]*"?([^" #]+)"?.*/\1/' | head -n1 || true
7579
)"
7680

@@ -115,15 +119,37 @@ docker build \
115119
-t "$BACKEND_IMAGE" \
116120
./backend
117121

122+
# vite.config.ts REQUIRES a version at build time and falls back to `git describe`,
123+
# which is unavailable inside the Docker builder (no .git in the build context) —
124+
# so resolve it here (git works in this checkout) and pass it explicitly, else the
125+
# frontend build fails. Override by exporting VITE_APP_VERSION.
126+
VITE_APP_VERSION="${VITE_APP_VERSION:-$(git -C "$ROOT_DIR" describe --tags --always --dirty 2>/dev/null || echo "offline-build")}"
127+
118128
echo " Building nginx (frontend)..."
119129
docker build \
120-
--build-arg "VITE_API_BASE_URL=${VITE_API_BASE_URL:-/api}" \
130+
--build-arg "VITE_API_BASE_URL=${VITE_API_BASE_URL:-/api/v1}" \
121131
--build-arg "VITE_SUPPORTED_FILE_TYPES=${VITE_SUPPORTED_FILE_TYPES:-.pcap,.pcapng,.cap}" \
122132
--build-arg "VITE_NETWORK_DIAGRAM_CONVERSATION_LIMIT=${VITE_NETWORK_DIAGRAM_CONVERSATION_LIMIT:-false}" \
133+
--build-arg "VITE_APP_VERSION=${VITE_APP_VERSION}" \
123134
-t "$NGINX_IMAGE" \
124135
-f ./nginx/Dockerfile \
125136
.
126137

138+
if [ "$SAVE_KEYCLOAK" = "1" ]; then
139+
echo " Building nginx (frontend, auth-enabled)..."
140+
docker build \
141+
--build-arg "VITE_API_BASE_URL=${VITE_API_BASE_URL:-/api/v1}" \
142+
--build-arg "VITE_SUPPORTED_FILE_TYPES=${VITE_SUPPORTED_FILE_TYPES:-.pcap,.pcapng,.cap}" \
143+
--build-arg "VITE_NETWORK_DIAGRAM_CONVERSATION_LIMIT=${VITE_NETWORK_DIAGRAM_CONVERSATION_LIMIT:-false}" \
144+
--build-arg "VITE_APP_VERSION=${VITE_APP_VERSION}" \
145+
--build-arg "VITE_AUTH_ENABLED=true" \
146+
--build-arg "VITE_OIDC_CLIENT_ID=tracepcap-frontend" \
147+
--build-arg "VITE_OIDC_REALM=tracepcap" \
148+
-t "$NGINX_AUTH_IMAGE" \
149+
-f ./nginx/Dockerfile \
150+
.
151+
fi
152+
127153
# ---------------------------------------------------------------------------
128154
# 3. Save all images as tars
129155
# ---------------------------------------------------------------------------
@@ -136,6 +162,9 @@ for img in "${DOCKERHUB_IMAGES[@]}"; do
136162
done
137163
save_image "$BACKEND_IMAGE" "tracepcap-backend.tar"
138164
save_image "$NGINX_IMAGE" "tracepcap-nginx.tar"
165+
if [ "$SAVE_KEYCLOAK" = "1" ]; then
166+
save_image "$NGINX_AUTH_IMAGE" "tracepcap-nginx-auth.tar"
167+
fi
139168

140169
# ---------------------------------------------------------------------------
141170
# Summary
@@ -154,11 +183,16 @@ echo " bash scripts/load-images.sh"
154183
echo " docker compose -f docker-compose.offline.yml up -d"
155184
echo ""
156185
if [ "$SAVE_KEYCLOAK" = "1" ]; then
157-
echo "Keycloak image was saved. To run auth offline you must ALSO transfer:"
186+
echo "Keycloak (auth) was included. To run authenticated offline, ALSO transfer:"
187+
echo " docker-compose.offline-prod.yml"
158188
echo " keycloak/realm-export.json"
159-
echo "and start with an auth-enabled compose (add the Keycloak service + auth env,"
160-
echo "and rebuild nginx with the VITE_AUTH_* args). Saving the image alone is not"
161-
echo "enough — see docs/configuration/authentication.rst."
189+
echo ""
190+
echo "and start with the auth overlay, setting PUBLIC_URL to the exact origin"
191+
echo "you browse to (scheme + host + port):"
192+
echo " PUBLIC_URL=http://<host>:8888 \\"
193+
echo " docker compose -f docker-compose.offline.yml -f docker-compose.offline-prod.yml up -d"
194+
echo ""
195+
echo "Then manage users at <PUBLIC_URL>/admin — see docs/configuration/user-management.rst."
162196
echo ""
163197
fi
164198
echo "To refresh nDPI: rebuild the backend image (it installs the latest nDPI),"

0 commit comments

Comments
 (0)