add pvelabs

This commit is contained in:
2026-08-26 18:01:49 -05:00
parent f519b2cedf
commit 29c7cae404
9 changed files with 211 additions and 8 deletions
+6
View File
@@ -18,6 +18,12 @@ SSL_CERTIFICATE_DIR=./config/letsencrypt/live/broker.example.com
SSL_CERTIFICATE_FILE=fullchain.pem SSL_CERTIFICATE_FILE=fullchain.pem
SSL_CERTIFICATE_KEY_FILE=privkey.pem SSL_CERTIFICATE_KEY_FILE=privkey.pem
# Optional public Apache Guacamole proxy. When enabled, this hostname is added
# to the Let's Encrypt certificate and proxied to the specified HTTP origin.
EXTERNAL_GUACAMOLE_ENABLED=false
EXTERNAL_GUACAMOLE_DOMAIN=
EXTERNAL_GUACAMOLE_UPSTREAM=
TAPM_DATABASE_DSN=file:/data/tapm.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL) TAPM_DATABASE_DSN=file:/data/tapm.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)
TAPM_PUBLIC_URL=http://broker.example.com:8680 TAPM_PUBLIC_URL=http://broker.example.com:8680
TAPM_GITEA_URL=http://git.example.com:8680 TAPM_GITEA_URL=http://git.example.com:8680
+24
View File
@@ -52,6 +52,30 @@ deployment to Let's Encrypt:
./manage.sh start ./manage.sh start
``` ```
## Optional Guacamole proxy
To expose a Guacamole service through the same Nginx and certificate, add these
settings to the deployment `.env` before running the TLS bootstrap:
```sh
EXTERNAL_GUACAMOLE_ENABLED=true
EXTERNAL_GUACAMOLE_DOMAIN=pvelabs.example.com
EXTERNAL_GUACAMOLE_UPSTREAM=http://10.0.0.10:8080
```
The external address is served at the domain root while Nginx preserves
Guacamole's native `/guacamole/` context path internally. `bootstrap` adds the
hostname to the existing certificate (or creates it) and configures the
required unbuffered WebSocket proxy:
```sh
./manage.sh bootstrap
./manage.sh start
```
The backend origin must be reachable from the Nginx container. The proxy is
available only with TLS enabled.
## Local checks ## Local checks
```sh ```sh
@@ -0,0 +1,6 @@
services:
nginx:
volumes:
- ./deploy/nginx/templates/external-guacamole-bootstrap.conf.template:/etc/nginx/templates/20-external-guacamole.conf.template:ro
environment:
EXTERNAL_GUACAMOLE_DOMAIN: ${EXTERNAL_GUACAMOLE_DOMAIN}
+7
View File
@@ -0,0 +1,7 @@
services:
nginx:
volumes:
- ./deploy/nginx/templates/external-guacamole-tls.conf.template:/etc/nginx/templates/20-external-guacamole.conf.template:ro
environment:
EXTERNAL_GUACAMOLE_DOMAIN: ${EXTERNAL_GUACAMOLE_DOMAIN}
EXTERNAL_GUACAMOLE_UPSTREAM: ${EXTERNAL_GUACAMOLE_UPSTREAM}
@@ -0,0 +1,13 @@
server {
listen 80;
server_name ${EXTERNAL_GUACAMOLE_DOMAIN};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 503 "TLS setup is in progress.\n";
add_header Content-Type text/plain;
}
}
@@ -0,0 +1,57 @@
map $http_upgrade $guacamole_connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name ${EXTERNAL_GUACAMOLE_DOMAIN};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name ${EXTERNAL_GUACAMOLE_DOMAIN};
ssl_certificate /etc/nginx/ssl/${SSL_CERTIFICATE_FILE};
ssl_certificate_key /etc/nginx/ssl/${SSL_CERTIFICATE_KEY_FILE};
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 1g;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
location / {
# Serve Guacamole at the domain root while retaining its native
# /guacamole/ context path on the internal Tomcat service.
proxy_pass ${EXTERNAL_GUACAMOLE_UPSTREAM}/guacamole/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $guacamole_connection_upgrade;
proxy_connect_timeout 30s;
proxy_send_timeout 30m;
proxy_read_timeout 30m;
proxy_request_buffering off;
proxy_buffering off;
}
}
+8 -1
View File
@@ -36,8 +36,12 @@ for required_file in \
compose.yaml \ compose.yaml \
compose.http.yaml \ compose.http.yaml \
compose.tls.yaml \ compose.tls.yaml \
compose.external-guacamole.bootstrap.yaml \
compose.external-guacamole.tls.yaml \
deploy/nginx/templates/http.conf.template \ deploy/nginx/templates/http.conf.template \
deploy/nginx/templates/tls.conf.template; do deploy/nginx/templates/tls.conf.template \
deploy/nginx/templates/external-guacamole-bootstrap.conf.template \
deploy/nginx/templates/external-guacamole-tls.conf.template; do
[[ -f "$required_file" ]] || [[ -f "$required_file" ]] ||
fail "repository file is missing or not a regular file: ${required_file}" fail "repository file is missing or not a regular file: ${required_file}"
done done
@@ -111,6 +115,9 @@ umask 077
printf 'SSL_CERTIFICATE_DIR=%s\n' "$certificate_dir" printf 'SSL_CERTIFICATE_DIR=%s\n' "$certificate_dir"
printf 'SSL_CERTIFICATE_FILE=fullchain.pem\n' printf 'SSL_CERTIFICATE_FILE=fullchain.pem\n'
printf 'SSL_CERTIFICATE_KEY_FILE=privkey.pem\n\n' printf 'SSL_CERTIFICATE_KEY_FILE=privkey.pem\n\n'
printf 'EXTERNAL_GUACAMOLE_ENABLED=false\n'
printf 'EXTERNAL_GUACAMOLE_DOMAIN=\n'
printf 'EXTERNAL_GUACAMOLE_UPSTREAM=\n\n'
printf 'TAPM_DATABASE_DSN=file:/data/tapm.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)\n' printf 'TAPM_DATABASE_DSN=file:/data/tapm.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)\n'
printf 'TAPM_PUBLIC_URL=%s://%s%s\n' "$public_scheme" "$broker_domain" "$public_port_suffix" printf 'TAPM_PUBLIC_URL=%s://%s%s\n' "$public_scheme" "$broker_domain" "$public_port_suffix"
printf 'TAPM_GITEA_URL=%s://%s%s\n' "$public_scheme" "$gitea_domain" "$public_port_suffix" printf 'TAPM_GITEA_URL=%s://%s%s\n' "$public_scheme" "$gitea_domain" "$public_port_suffix"
+60 -7
View File
@@ -59,6 +59,11 @@ select_compose_stack() {
else else
COMPOSE_FILES+=(-f compose.tls.yaml) COMPOSE_FILES+=(-f compose.tls.yaml)
fi fi
if [[ "$EXTERNAL_GUACAMOLE_ENABLED" == "true" ]]; then
[[ "$SSL_MODE" != "none" ]] ||
fail "EXTERNAL_GUACAMOLE_ENABLED requires TLS; run ./manage.sh set-ssl letsencrypt first"
COMPOSE_FILES+=(-f compose.external-guacamole.tls.yaml)
fi
} }
compose_stack() { compose_stack() {
@@ -86,6 +91,12 @@ require_stack_files() {
require_regular_file compose.tls.yaml require_regular_file compose.tls.yaml
require_regular_file deploy/nginx/templates/tls.conf.template require_regular_file deploy/nginx/templates/tls.conf.template
fi fi
if [[ "$EXTERNAL_GUACAMOLE_ENABLED" == "true" ]]; then
require_regular_file compose.external-guacamole.bootstrap.yaml
require_regular_file compose.external-guacamole.tls.yaml
require_regular_file deploy/nginx/templates/external-guacamole-bootstrap.conf.template
require_regular_file deploy/nginx/templates/external-guacamole-tls.conf.template
fi
} }
broker_config_ready() { broker_config_ready() {
@@ -123,6 +134,38 @@ require_tls_certificate() {
fail "TLS certificate key was not found at ${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_KEY_FILE}; run ./manage.sh bootstrap" fail "TLS certificate key was not found at ${SSL_CERTIFICATE_DIR}/${SSL_CERTIFICATE_KEY_FILE}; run ./manage.sh bootstrap"
} }
valid_hostname() {
[[ "$1" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$ &&
"$1" != *..* ]]
}
valid_http_origin() {
[[ "$1" =~ ^http://[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?(:[0-9]{1,5})?$ ]]
}
bootstrap_stack() {
local -a bootstrap_files=(-f compose.yaml)
if [[ "$EXTERNAL_GUACAMOLE_ENABLED" == "true" ]]; then
bootstrap_files+=(-f compose.external-guacamole.bootstrap.yaml)
fi
docker compose "${bootstrap_files[@]}" "$@"
}
issue_letsencrypt_certificate() {
local -a domain_args=(-d "$BROKER_DOMAIN" -d "$GITEA_DOMAIN")
if [[ "$EXTERNAL_GUACAMOLE_ENABLED" == "true" ]]; then
domain_args+=(-d "$EXTERNAL_GUACAMOLE_DOMAIN")
fi
docker compose --profile tools run --rm certbot certonly \
--webroot --webroot-path /var/www/certbot \
--non-interactive --agree-tos --expand \
--email "$LETSENCRYPT_EMAIL" \
--cert-name "$BROKER_DOMAIN" \
"${domain_args[@]}"
}
require_env() { require_env() {
[[ -f .env ]] || fail "copy .env.example to .env and configure it first" [[ -f .env ]] || fail "copy .env.example to .env and configure it first"
BROKER_DOMAIN="$(read_env_setting BROKER_DOMAIN)" BROKER_DOMAIN="$(read_env_setting BROKER_DOMAIN)"
@@ -134,12 +177,26 @@ require_env() {
SSL_CERTIFICATE_DIR="$(read_env_setting SSL_CERTIFICATE_DIR)" SSL_CERTIFICATE_DIR="$(read_env_setting SSL_CERTIFICATE_DIR)"
SSL_CERTIFICATE_FILE="$(read_env_setting SSL_CERTIFICATE_FILE)" SSL_CERTIFICATE_FILE="$(read_env_setting SSL_CERTIFICATE_FILE)"
SSL_CERTIFICATE_KEY_FILE="$(read_env_setting SSL_CERTIFICATE_KEY_FILE)" SSL_CERTIFICATE_KEY_FILE="$(read_env_setting SSL_CERTIFICATE_KEY_FILE)"
EXTERNAL_GUACAMOLE_ENABLED="$(read_env_setting EXTERNAL_GUACAMOLE_ENABLED)"
EXTERNAL_GUACAMOLE_DOMAIN="$(read_env_setting EXTERNAL_GUACAMOLE_DOMAIN)"
EXTERNAL_GUACAMOLE_UPSTREAM="$(read_env_setting EXTERNAL_GUACAMOLE_UPSTREAM)"
EXTERNAL_GUACAMOLE_ENABLED="${EXTERNAL_GUACAMOLE_ENABLED:-false}"
SSL_CERTIFICATE_FILE="${SSL_CERTIFICATE_FILE:-fullchain.pem}" SSL_CERTIFICATE_FILE="${SSL_CERTIFICATE_FILE:-fullchain.pem}"
SSL_CERTIFICATE_KEY_FILE="${SSL_CERTIFICATE_KEY_FILE:-privkey.pem}" SSL_CERTIFICATE_KEY_FILE="${SSL_CERTIFICATE_KEY_FILE:-privkey.pem}"
: "${BROKER_DOMAIN:?BROKER_DOMAIN is required}" : "${BROKER_DOMAIN:?BROKER_DOMAIN is required}"
: "${GITEA_DOMAIN:?GITEA_DOMAIN is required}" : "${GITEA_DOMAIN:?GITEA_DOMAIN is required}"
[[ "$BROKER_DOMAIN" != "$GITEA_DOMAIN" ]] || [[ "$BROKER_DOMAIN" != "$GITEA_DOMAIN" ]] ||
fail "BROKER_DOMAIN and GITEA_DOMAIN must be different hostnames" fail "BROKER_DOMAIN and GITEA_DOMAIN must be different hostnames"
case "$EXTERNAL_GUACAMOLE_ENABLED" in
true)
valid_hostname "$EXTERNAL_GUACAMOLE_DOMAIN" ||
fail "EXTERNAL_GUACAMOLE_DOMAIN must be a valid hostname"
valid_http_origin "$EXTERNAL_GUACAMOLE_UPSTREAM" ||
fail "EXTERNAL_GUACAMOLE_UPSTREAM must be an HTTP origin such as http://10.241.2.219:8080"
;;
false) ;;
*) fail "EXTERNAL_GUACAMOLE_ENABLED must be true or false" ;;
esac
if [[ -z "$GITEA_PUBLIC_URL" ]]; then if [[ -z "$GITEA_PUBLIC_URL" ]]; then
if [[ "$SSL_MODE" == "none" ]]; then if [[ "$SSL_MODE" == "none" ]]; then
http_port="$(read_env_setting HTTP_PORT)" http_port="$(read_env_setting HTTP_PORT)"
@@ -186,6 +243,7 @@ require_env() {
;; ;;
esac esac
export GITEA_PUBLIC_URL SSL_CERTIFICATE_DIR SSL_CERTIFICATE_FILE SSL_CERTIFICATE_KEY_FILE export GITEA_PUBLIC_URL SSL_CERTIFICATE_DIR SSL_CERTIFICATE_FILE SSL_CERTIFICATE_KEY_FILE
export EXTERNAL_GUACAMOLE_ENABLED EXTERNAL_GUACAMOLE_DOMAIN EXTERNAL_GUACAMOLE_UPSTREAM
select_compose_stack select_compose_stack
require_stack_files require_stack_files
} }
@@ -219,13 +277,8 @@ bootstrap)
if [[ "$SSL_MODE" == "none" ]]; then if [[ "$SSL_MODE" == "none" ]]; then
compose_stack up -d gitea nginx compose_stack up -d gitea nginx
elif [[ "$SSL_MODE" == "letsencrypt" ]]; then elif [[ "$SSL_MODE" == "letsencrypt" ]]; then
docker compose up -d gitea nginx bootstrap_stack up -d gitea nginx
docker compose --profile tools run --rm certbot certonly \ issue_letsencrypt_certificate
--webroot --webroot-path /var/www/certbot \
--non-interactive --agree-tos \
--email "$LETSENCRYPT_EMAIL" \
--cert-name "$BROKER_DOMAIN" \
-d "$BROKER_DOMAIN" -d "$GITEA_DOMAIN"
require_tls_certificate require_tls_certificate
compose_stack up -d gitea nginx compose_stack up -d gitea nginx
else else
+30
View File
@@ -14,6 +14,8 @@ cp -R \
"${TEST_ROOT}/compose.yaml" \ "${TEST_ROOT}/compose.yaml" \
"${TEST_ROOT}/compose.http.yaml" \ "${TEST_ROOT}/compose.http.yaml" \
"${TEST_ROOT}/compose.tls.yaml" \ "${TEST_ROOT}/compose.tls.yaml" \
"${TEST_ROOT}/compose.external-guacamole.bootstrap.yaml" \
"${TEST_ROOT}/compose.external-guacamole.tls.yaml" \
"${TEST_ROOT}/compose.direct.yaml" \ "${TEST_ROOT}/compose.direct.yaml" \
"${TEST_ROOT}/deploy" \ "${TEST_ROOT}/deploy" \
"$deployment/" "$deployment/"
@@ -67,6 +69,19 @@ grep -F -- '/var/cache/nginx:size=256m,mode=0755' \
printf 'FAIL: Nginx cache tmpfs is too small for Git pack streaming\n' >&2 printf 'FAIL: Nginx cache tmpfs is too small for Git pack streaming\n' >&2
exit 1 exit 1
} }
for external_file in \
"${TEST_ROOT}/deploy/nginx/templates/external-guacamole-bootstrap.conf.template" \
"${TEST_ROOT}/deploy/nginx/templates/external-guacamole-tls.conf.template"; do
[[ -f "$external_file" ]] || {
printf 'FAIL: missing Guacamole Nginx template: %s\n' "$external_file" >&2
exit 1
}
done
grep -F -- 'proxy_pass ${EXTERNAL_GUACAMOLE_UPSTREAM}/guacamole/;' \
"${TEST_ROOT}/deploy/nginx/templates/external-guacamole-tls.conf.template" >/dev/null || {
printf 'FAIL: Guacamole root proxy does not retain the internal context path\n' >&2
exit 1
}
for template in \ for template in \
"${TEST_ROOT}/deploy/nginx/templates/http.conf.template" \ "${TEST_ROOT}/deploy/nginx/templates/http.conf.template" \
"${TEST_ROOT}/deploy/nginx/templates/tls.conf.template"; do "${TEST_ROOT}/deploy/nginx/templates/tls.conf.template"; do
@@ -105,6 +120,7 @@ assert_env 'TAPM_GITEA_URL=http://git.test:8780'
assert_env 'TAPM_GITEA_INTERNAL_URL=http://gitea:3000' assert_env 'TAPM_GITEA_INTERNAL_URL=http://gitea:3000'
assert_env 'GITEA_PUBLIC_URL=http://git.test:8780/' assert_env 'GITEA_PUBLIC_URL=http://git.test:8780/'
assert_env 'TAPM_DEFAULT_DURATION=7d' assert_env 'TAPM_DEFAULT_DURATION=7d'
assert_env 'EXTERNAL_GUACAMOLE_ENABLED=false'
grep -F -- '-f compose.yaml -f compose.http.yaml up -d gitea nginx' \ grep -F -- '-f compose.yaml -f compose.http.yaml up -d gitea nginx' \
"$MOCK_DOCKER_LOG" >/dev/null || { "$MOCK_DOCKER_LOG" >/dev/null || {
printf 'FAIL: HTTP bootstrap did not select compose.http.yaml\n' >&2 printf 'FAIL: HTTP bootstrap did not select compose.http.yaml\n' >&2
@@ -131,6 +147,20 @@ assert_env 'SSL_MODE=letsencrypt'
assert_env 'TAPM_PUBLIC_URL=https://broker.test' assert_env 'TAPM_PUBLIC_URL=https://broker.test'
assert_env 'GITEA_PUBLIC_URL=https://git.test/' assert_env 'GITEA_PUBLIC_URL=https://git.test/'
printf '%s\n' \
'EXTERNAL_GUACAMOLE_ENABLED=true' \
'EXTERNAL_GUACAMOLE_DOMAIN=guacamole.test' \
'EXTERNAL_GUACAMOLE_UPSTREAM=http://10.0.0.10:8080' \
>>"${deployment}/.env"
"${deployment}/manage.sh" status >/dev/null
grep -F -- '-f compose.yaml -f compose.tls.yaml -f compose.external-guacamole.tls.yaml ps' \
"$MOCK_DOCKER_LOG" >/dev/null || {
printf 'FAIL: enabled Guacamole proxy was not added to the TLS stack\n' >&2
exit 1
}
printf '%s\n' 'EXTERNAL_GUACAMOLE_ENABLED=false' >>"${deployment}/.env"
"${deployment}/manage.sh" set-ssl none >/dev/null "${deployment}/manage.sh" set-ssl none >/dev/null
assert_env 'SSL_MODE=none' assert_env 'SSL_MODE=none'
assert_env 'TAPM_PUBLIC_URL=http://broker.test:8780' assert_env 'TAPM_PUBLIC_URL=http://broker.test:8780'