fix(deploy): add optional Turnstile and customer mTLS proxy
This commit is contained in:
1 parent
e06a43c76f
commit
35068319e5
7 files changed
+186
-28
No files matched your search
+10
-1
@@ -13,7 +13,16 @@ Generated keys are written only to `/opt/overcooked/production.env` with mode
|
|||||||
600. The installer does not log generated values, activation codes, or Auth
|
600. The installer does not log generated values, activation codes, or Auth
|
||||||
configuration. Failure logs are redacted before display.
|
configuration. Failure logs are redacted before display.
|
||||||
|
|
||||||
|
Turnstile is optional. Empty site and secret keys disable only that abuse
|
||||||
|
prevention challenge; Auth, license validation, and login policy remain on.
|
||||||
|
|
||||||
|
E production control-plane traffic uses mTLS. The installer creates a local
|
||||||
|
CA and service/client certificates, then generates a minimal HTTPS proxy in the
|
||||||
|
installation directory and runs it from the approved S WebUI image. The proxy
|
||||||
|
has no public port and only forwards authenticated `/internal/v1/` calls to
|
||||||
|
S. The approved Media Node image is exported to the node-image archive used by
|
||||||
|
S node deployment.
|
||||||
|
|
||||||
The installer must stop on missing prerequisites, invalid public address,
|
The installer must stop on missing prerequisites, invalid public address,
|
||||||
occupied ports, failed image verification, failed schema migration, or failed
|
occupied ports, failed image verification, failed schema migration, or failed
|
||||||
health checks. It must never continue with a partially initialized database.
|
health checks. It must never continue with a partially initialized database.
|
||||||
|
|
||||||
@@ -12,6 +12,13 @@ only these public images:
|
|||||||
It does not download or start an Auth image, and it does not use Docker build
|
It does not download or start an Auth image, and it does not use Docker build
|
||||||
contexts.
|
contexts.
|
||||||
|
|
||||||
|
Because E production mode requires an encrypted control-plane connection, the
|
||||||
|
installer generates a minimal allowlisted HTTPS reverse proxy at install time
|
||||||
|
and runs it from the already-listed S WebUI image. The proxy only accepts
|
||||||
|
mTLS-authenticated `/internal/v1/` requests and `/health/live`, and forwards
|
||||||
|
only to the S control service; it is not a public endpoint and does not expose
|
||||||
|
Shell, SQL, Docker, or arbitrary file access.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -23,8 +30,6 @@ sudo env \\
|
|||||||
OVERCOOKED_SERVER_IP=203.0.113.10 \\
|
OVERCOOKED_SERVER_IP=203.0.113.10 \\
|
||||||
OVERCOOKED_AUTH_PUBLIC_KEYS='{"auth-1":"PUBLIC_KEY"}' \\
|
OVERCOOKED_AUTH_PUBLIC_KEYS='{"auth-1":"PUBLIC_KEY"}' \\
|
||||||
OVERCOOKED_ACTIVATION_CODE='ACTIVATION_CODE' \\
|
OVERCOOKED_ACTIVATION_CODE='ACTIVATION_CODE' \\
|
||||||
OVERCOOKED_TURNSTILE_SITE_KEY='TURNSTILE_SITE_KEY' \\
|
|
||||||
OVERCOOKED_TURNSTILE_SECRET_KEY='TURNSTILE_SECRET_KEY' \\
|
|
||||||
./install.sh
|
./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -33,9 +38,10 @@ with `OVERCOOKED_AUTH_URL`. Auth private keys, administrator credentials,
|
|||||||
license databases, provider credentials, and production secrets are never
|
license databases, provider credentials, and production secrets are never
|
||||||
included in this repository.
|
included in this repository.
|
||||||
|
|
||||||
The current S production image requires the Turnstile key pair shown above.
|
Turnstile is optional. Set `OVERCOOKED_TURNSTILE_SITE_KEY` and
|
||||||
The installer fails early if the pair is missing or incomplete; it never uses
|
`OVERCOOKED_TURNSTILE_SECRET_KEY` together to enable it; leaving both empty
|
||||||
placeholder credentials.
|
disables only the Turnstile challenge and does not disable Auth, license, or
|
||||||
|
login policy checks. A partial pair is rejected by S configuration validation.
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
@@ -48,6 +54,10 @@ The installer checks these host ports before changing service state and does
|
|||||||
not bind ports 80 or 443. Configure the firewall to allow the chosen public
|
not bind ports 80 or 443. Configure the firewall to allow the chosen public
|
||||||
ports and restrict database/Valkey traffic to the Docker networks.
|
ports and restrict database/Valkey traffic to the Docker networks.
|
||||||
|
|
||||||
|
S's E client uses the private backend Docker network (`http://e-server:8096`)
|
||||||
|
with its explicit private-network opt-in. E's control-plane client is a
|
||||||
|
separate production path and always uses the generated HTTPS/mTLS proxy.
|
||||||
|
|
||||||
The server address is selected from `OVERCOOKED_SERVER_IP`, or from the
|
The server address is selected from `OVERCOOKED_SERVER_IP`, or from the
|
||||||
interface selected by `ip -4 route get 1.1.1.1`. A detected RFC1918,
|
interface selected by `ip -4 route get 1.1.1.1`. A detected RFC1918,
|
||||||
loopback, link-local, or carrier-grade NAT address is rejected unless explicitly
|
loopback, link-local, or carrier-grade NAT address is rejected unless explicitly
|
||||||
@@ -73,6 +83,10 @@ image. S uses only the schema migration assets published in this repository.
|
|||||||
If either migration fails, the installer exits non-zero and does not start the
|
If either migration fails, the installer exits non-zero and does not start the
|
||||||
corresponding application service.
|
corresponding application service.
|
||||||
|
|
||||||
|
The installer also exports the approved Media Node image into
|
||||||
|
`/opt/overcooked/node-images/media-node.tar.gz`; S uses that archive when a
|
||||||
|
customer adds a node without a source checkout.
|
||||||
|
|
||||||
## Common 404/401 download failures
|
## Common 404/401 download failures
|
||||||
|
|
||||||
The customer command must fetch the raw script from the public Installer
|
The customer command must fetch the raw script from the public Installer
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
3395c27ad4158e70d47e7d002c141b1261f502875f2e7ac5b26ade51b7689138 install.sh
|
80d42d9797b20d0b8fd53ac9529cd579fae9fe294267ac8e8ab990ed961e88d1 install.sh
|
||||||
028d415f0c0aef0ed9a807573239079f9e7e8cb3ed796d9dec90f4be8cafdd05 compose/s.yml
|
e8cabc5a989d171206361e6ae8b4e0baf9bb9a611ac41106bbe42de0782e8975 compose/s.yml
|
||||||
fab213abb69e0141139c0d4234a7b1636588206f6588942a55c3380114069d92 compose/e.yml
|
203b6da918dee21dbfabbcf5466111bd92c7a8c9e754dad61e7e75e20513b880 compose/e.yml
|
||||||
c226c5e1feb5cd6948ee9b32897e77240e21793090d8f42234ca999450ec90b0 config/e-server.production.yaml
|
c226c5e1feb5cd6948ee9b32897e77240e21793090d8f42234ca999450ec90b0 config/e-server.production.yaml
|
||||||
3e3dc775017577fad3b36b6c21036dfcb6e530793de4c18c825dda888f62e4e3 s-migrate.sh
|
3e3dc775017577fad3b36b6c21036dfcb6e530793de4c18c825dda888f62e4e3 s-migrate.sh
|
||||||
07f432c281ba90565cd376b42967909b6ff2d19d04454a3ba4e3b184117d45af s-migrations.tar.gz
|
07f432c281ba90565cd376b42967909b6ff2d19d04454a3ba4e3b184117d45af s-migrations.tar.gz
|
||||||
+4
-1
@@ -62,6 +62,7 @@ services:
|
|||||||
- e-data:/data
|
- e-data:/data
|
||||||
- e-plugin-data:/data/plugins
|
- e-plugin-data:/data/plugins
|
||||||
- ${OVERCOOKED_ROOT:-/opt/overcooked}/config/e-server.production.yaml:/etc/overcooked-e/config.yaml:ro
|
- ${OVERCOOKED_ROOT:-/opt/overcooked}/config/e-server.production.yaml:/etc/overcooked-e/config.yaml:ro
|
||||||
|
- ${OVERCOOKED_ROOT:-/opt/overcooked}/control-plane:/run/overcooked-control-plane:ro
|
||||||
environment:
|
environment:
|
||||||
PRODUCT_E_CONFIG: /etc/overcooked-e/config.yaml
|
PRODUCT_E_CONFIG: /etc/overcooked-e/config.yaml
|
||||||
PRODUCT_E_ENVIRONMENT: production
|
PRODUCT_E_ENVIRONMENT: production
|
||||||
@@ -72,6 +73,9 @@ services:
|
|||||||
PRODUCT_E_CONTROL_PLANE_URL: ${PRODUCT_E_CONTROL_PLANE_URL:?required}
|
PRODUCT_E_CONTROL_PLANE_URL: ${PRODUCT_E_CONTROL_PLANE_URL:?required}
|
||||||
PRODUCT_E_CONTROL_PLANE_KEY_ID: ${PRODUCT_E_CONTROL_PLANE_KEY_ID:?required}
|
PRODUCT_E_CONTROL_PLANE_KEY_ID: ${PRODUCT_E_CONTROL_PLANE_KEY_ID:?required}
|
||||||
PRODUCT_E_CONTROL_PLANE_SHARED_SECRET: ${PRODUCT_E_CONTROL_PLANE_SHARED_SECRET:?required}
|
PRODUCT_E_CONTROL_PLANE_SHARED_SECRET: ${PRODUCT_E_CONTROL_PLANE_SHARED_SECRET:?required}
|
||||||
|
PRODUCT_E_CONTROL_PLANE_TLS_CERT_FILE: ${PRODUCT_E_CONTROL_PLANE_TLS_CERT_FILE:?required}
|
||||||
|
PRODUCT_E_CONTROL_PLANE_TLS_KEY_FILE: ${PRODUCT_E_CONTROL_PLANE_TLS_KEY_FILE:?required}
|
||||||
|
PRODUCT_E_CONTROL_PLANE_TLS_CA_FILE: ${PRODUCT_E_CONTROL_PLANE_TLS_CA_FILE:?required}
|
||||||
PRODUCT_E_LICENSE_ENABLED: ${PRODUCT_E_LICENSE_ENABLED:-true}
|
PRODUCT_E_LICENSE_ENABLED: ${PRODUCT_E_LICENSE_ENABLED:-true}
|
||||||
PRODUCT_E_AUTH_URL: ${PRODUCT_E_AUTH_URL:?required}
|
PRODUCT_E_AUTH_URL: ${PRODUCT_E_AUTH_URL:?required}
|
||||||
PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP: ${PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP:-false}
|
PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP: ${PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP:-false}
|
||||||
@@ -123,4 +127,3 @@ volumes:
|
|||||||
e-data:
|
e-data:
|
||||||
e-plugin-data:
|
e-plugin-data:
|
||||||
e-backup-staging:
|
e-backup-staging:
|
||||||
|
|
||||||
+33
-3
@@ -102,8 +102,8 @@ services:
|
|||||||
S_VERSION: ${S_VERSION:-customer}
|
S_VERSION: ${S_VERSION:-customer}
|
||||||
S_BACKUP_DIRECTORY: /var/lib/overcooked-s/backups
|
S_BACKUP_DIRECTORY: /var/lib/overcooked-s/backups
|
||||||
S_UPDATE_DIRECTORY: /var/lib/overcooked-s/updates
|
S_UPDATE_DIRECTORY: /var/lib/overcooked-s/updates
|
||||||
S_TURNSTILE_SITE_KEY: ${S_TURNSTILE_SITE_KEY:?required}
|
S_TURNSTILE_SITE_KEY: ${S_TURNSTILE_SITE_KEY:-}
|
||||||
S_TURNSTILE_SECRET_KEY: ${S_TURNSTILE_SECRET_KEY:?required}
|
S_TURNSTILE_SECRET_KEY: ${S_TURNSTILE_SECRET_KEY:-}
|
||||||
S_TURNSTILE_HOSTNAME: ${S_TURNSTILE_HOSTNAME:-}
|
S_TURNSTILE_HOSTNAME: ${S_TURNSTILE_HOSTNAME:-}
|
||||||
S_TELEGRAM_MTPROTO_URL: ${S_TELEGRAM_MTPROTO_URL:-http://telegram-disabled.invalid:8098}
|
S_TELEGRAM_MTPROTO_URL: ${S_TELEGRAM_MTPROTO_URL:-http://telegram-disabled.invalid:8098}
|
||||||
S_PORTAL_LOCATION: ${S_PORTAL_LOCATION:-Private Node}
|
S_PORTAL_LOCATION: ${S_PORTAL_LOCATION:-Private Node}
|
||||||
@@ -112,7 +112,7 @@ services:
|
|||||||
S_PORTAL_ANNOUNCEMENT: ${S_PORTAL_ANNOUNCEMENT:-}
|
S_PORTAL_ANNOUNCEMENT: ${S_PORTAL_ANNOUNCEMENT:-}
|
||||||
S_STORAGE_WEBHOOK_SECRET: ${S_STORAGE_WEBHOOK_SECRET:-}
|
S_STORAGE_WEBHOOK_SECRET: ${S_STORAGE_WEBHOOK_SECRET:-}
|
||||||
S_NODE_IMAGE_ARCHIVE: /var/lib/overcooked-s/node-images/media-node.tar.gz
|
S_NODE_IMAGE_ARCHIVE: /var/lib/overcooked-s/node-images/media-node.tar.gz
|
||||||
S_NODE_IMAGE_NAME: ${S_NODE_IMAGE_NAME:-overcooked-media-node:latest}
|
S_NODE_IMAGE_NAME: ${S_NODE_IMAGE_NAME:-git.leee.xin/overcooked/overcooked-media-node:latest}
|
||||||
S_NODE_CONTROLLER_HOST: ${S_NODE_CONTROLLER_HOST:-}
|
S_NODE_CONTROLLER_HOST: ${S_NODE_CONTROLLER_HOST:-}
|
||||||
volumes:
|
volumes:
|
||||||
- s-backups:/var/lib/overcooked-s/backups
|
- s-backups:/var/lib/overcooked-s/backups
|
||||||
@@ -133,6 +133,36 @@ services:
|
|||||||
logging: *logging
|
logging: *logging
|
||||||
mem_limit: 384m
|
mem_limit: 384m
|
||||||
|
|
||||||
|
control-proxy:
|
||||||
|
image: ${S_WEBUI_IMAGE:?required}
|
||||||
|
restart: unless-stopped
|
||||||
|
init: true
|
||||||
|
depends_on:
|
||||||
|
s-control: {condition: service_healthy}
|
||||||
|
command: ["node", "/run/overcooked-control-plane/control-proxy.mjs"]
|
||||||
|
volumes:
|
||||||
|
- ${OVERCOOKED_ROOT:-/opt/overcooked}/control-plane:/run/overcooked-control-plane:ro
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- node
|
||||||
|
- -e
|
||||||
|
- |
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const https = require("node:https");
|
||||||
|
const request = https.get("https://control-proxy:8443/health/live", {ca: fs.readFileSync("/run/overcooked-control-plane/ca.crt"), cert: fs.readFileSync("/run/overcooked-control-plane/client.crt"), key: fs.readFileSync("/run/overcooked-control-plane/client.key"), servername: "control-proxy", rejectUnauthorized: true}, (response) => { response.resume(); process.exit(response.statusCode === 200 ? 0 : 1); });
|
||||||
|
request.setTimeout(2000, () => { request.destroy(); process.exit(1); });
|
||||||
|
request.on("error", () => process.exit(1));
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
start_period: 10s
|
||||||
|
retries: 6
|
||||||
|
networks: [control-plane]
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
cap_drop: [ALL]
|
||||||
|
logging: *logging
|
||||||
|
mem_limit: 128m
|
||||||
|
|
||||||
webui:
|
webui:
|
||||||
image: ${S_WEBUI_IMAGE:?required}
|
image: ${S_WEBUI_IMAGE:?required}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
+113
-14
@@ -12,8 +12,6 @@ ACTIVATION_CODE=${OVERCOOKED_ACTIVATION_CODE:-}
|
|||||||
AUTH_URL=${OVERCOOKED_AUTH_URL:-http://148.135.97.10:18091}
|
AUTH_URL=${OVERCOOKED_AUTH_URL:-http://148.135.97.10:18091}
|
||||||
AUTH_PUBLIC_KEYS=${OVERCOOKED_AUTH_PUBLIC_KEYS:-}
|
AUTH_PUBLIC_KEYS=${OVERCOOKED_AUTH_PUBLIC_KEYS:-}
|
||||||
INSTALLER_BASE_URL=${OVERCOOKED_INSTALLER_BASE_URL:-https://git.leee.xin/OverCooked/OverCooked-Installer/raw/branch/main}
|
INSTALLER_BASE_URL=${OVERCOOKED_INSTALLER_BASE_URL:-https://git.leee.xin/OverCooked/OverCooked-Installer/raw/branch/main}
|
||||||
TURNSTILE_SITE_KEY=${OVERCOOKED_TURNSTILE_SITE_KEY:-}
|
|
||||||
TURNSTILE_SECRET_KEY=${OVERCOOKED_TURNSTILE_SECRET_KEY:-}
|
|
||||||
S_COMPOSE=$ROOT/compose/s.yml
|
S_COMPOSE=$ROOT/compose/s.yml
|
||||||
E_COMPOSE=$ROOT/compose/e.yml
|
E_COMPOSE=$ROOT/compose/e.yml
|
||||||
ENV_FILE=$ROOT/production.env
|
ENV_FILE=$ROOT/production.env
|
||||||
@@ -51,7 +49,8 @@ is_private_ipv4() {
|
|||||||
}
|
}
|
||||||
check_port() {
|
check_port() {
|
||||||
port=$1
|
port=$1
|
||||||
if command -v ss >/dev/null 2>&1 && ss -H -ltn 2>/dev/null | awk -v p=":$port" '$4 ~ p "$" || $4 ~ p "\\]$" { found=1 } END { exit !found }'; then
|
command -v ss >/dev/null 2>&1 || die "未找到 ss,无法安全检测端口 $port;请安装 iproute2 后重试"
|
||||||
|
if ss -H -ltn 2>/dev/null | awk -v p=":$port" '$4 ~ p "$" || $4 ~ p "\\]$" { found=1 } END { exit !found }'; then
|
||||||
die "端口 $port 已被占用,请停止占用进程或调整部署端口"
|
die "端口 $port 已被占用,请停止占用进程或调整部署端口"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -116,12 +115,9 @@ if [ -z "$ACTIVATION_CODE" ]; then
|
|||||||
printf '请输入授权码: ' >&2; IFS= read -r ACTIVATION_CODE
|
printf '请输入授权码: ' >&2; IFS= read -r ACTIVATION_CODE
|
||||||
fi
|
fi
|
||||||
[ -n "$ACTIVATION_CODE" ] || die "授权码不能为空"
|
[ -n "$ACTIVATION_CODE" ] || die "授权码不能为空"
|
||||||
if [ -z "$TURNSTILE_SITE_KEY" ] || [ -z "$TURNSTILE_SECRET_KEY" ]; then
|
|
||||||
die "当前 S 生产镜像需要 OVERCOOKED_TURNSTILE_SITE_KEY 和 OVERCOOKED_TURNSTILE_SECRET_KEY;请提供成对配置"
|
|
||||||
fi
|
|
||||||
check_port 18080; check_port 18083; check_port 8096
|
check_port 18080; check_port 18083; check_port 8096
|
||||||
|
|
||||||
install -d -m 700 "$ROOT" "$ROOT/config" "$ROOT/compose" "$ROOT/migrations" "$ROOT/node-images"
|
install -d -m 700 "$ROOT" "$ROOT/config" "$ROOT/compose" "$ROOT/migrations" "$ROOT/node-images" "$ROOT/control-plane"
|
||||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
fetch_asset() {
|
fetch_asset() {
|
||||||
relative=$1; destination=$2; source="$SCRIPT_DIR/$relative"
|
relative=$1; destination=$2; source="$SCRIPT_DIR/$relative"
|
||||||
@@ -134,11 +130,100 @@ fetch_asset() {
|
|||||||
fetch_asset compose/s.yml "$S_COMPOSE"
|
fetch_asset compose/s.yml "$S_COMPOSE"
|
||||||
fetch_asset compose/e.yml "$E_COMPOSE"
|
fetch_asset compose/e.yml "$E_COMPOSE"
|
||||||
fetch_asset s-migrate.sh "$ROOT/s-migrate.sh"
|
fetch_asset s-migrate.sh "$ROOT/s-migrate.sh"
|
||||||
|
chmod 644 "$ROOT/s-migrate.sh"
|
||||||
fetch_asset config/e-server.production.yaml "$ROOT/config/e-server.production.yaml"
|
fetch_asset config/e-server.production.yaml "$ROOT/config/e-server.production.yaml"
|
||||||
|
chmod 644 "$ROOT/config/e-server.production.yaml"
|
||||||
fetch_asset s-migrations.tar.gz "$ROOT/s-migrations.tar.gz"
|
fetch_asset s-migrations.tar.gz "$ROOT/s-migrations.tar.gz"
|
||||||
tar -xzf "$ROOT/s-migrations.tar.gz" -C "$ROOT/migrations"; rm -f "$ROOT/s-migrations.tar.gz"
|
tar -xzf "$ROOT/s-migrations.tar.gz" -C "$ROOT/migrations"; rm -f "$ROOT/s-migrations.tar.gz"
|
||||||
find "$ROOT/migrations" -type f ! -name '*.up.sql' -delete
|
find "$ROOT/migrations" -type f ! -name '*.up.sql' -delete
|
||||||
find "$ROOT/migrations" -type f -name '*.up.sql' -exec chmod 600 {} \;
|
find "$ROOT/migrations" -type f -name '*.up.sql' -exec chmod 644 {} \;
|
||||||
|
chmod 755 "$ROOT/migrations" "$ROOT/node-images"
|
||||||
|
|
||||||
|
# E requires an HTTPS control-plane origin in production. Generate a local
|
||||||
|
# CA and service/client certificates for the restricted proxy sidecar. The
|
||||||
|
# proxy is generated locally and runs from the already-approved S WebUI image;
|
||||||
|
# no additional image or source checkout is needed.
|
||||||
|
CONTROL_PLANE_DIR="$ROOT/control-plane"
|
||||||
|
cat > "$CONTROL_PLANE_DIR/control-proxy.mjs" <<'NODE_PROXY'
|
||||||
|
import fs from "node:fs";
|
||||||
|
import http from "node:http";
|
||||||
|
import https from "node:https";
|
||||||
|
|
||||||
|
const listenPort = 8443;
|
||||||
|
const options = {
|
||||||
|
key: fs.readFileSync("/run/overcooked-control-plane/server.key"),
|
||||||
|
cert: fs.readFileSync("/run/overcooked-control-plane/server.crt"),
|
||||||
|
ca: fs.readFileSync("/run/overcooked-control-plane/ca.crt"),
|
||||||
|
requestCert: true,
|
||||||
|
rejectUnauthorized: true,
|
||||||
|
minVersion: "TLSv1.2",
|
||||||
|
};
|
||||||
|
|
||||||
|
function reject(response, status, message) {
|
||||||
|
response.writeHead(status, {"content-type": "text/plain; charset=utf-8"});
|
||||||
|
response.end(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = https.createServer(options, (request, response) => {
|
||||||
|
let target;
|
||||||
|
try {
|
||||||
|
target = new URL(request.url ?? "/", "https://control-proxy");
|
||||||
|
} catch {
|
||||||
|
reject(response, 400, "invalid request target");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target.origin !== "https://control-proxy" || target.username || target.password ||
|
||||||
|
target.pathname.includes("..") || target.pathname !== "/health/live" && !target.pathname.startsWith("/internal/v1/")) {
|
||||||
|
reject(response, 404, "not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target.pathname === "/health/live") {
|
||||||
|
response.writeHead(200, {"content-type": "application/json"});
|
||||||
|
response.end('{"status":"live"}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {...request.headers};
|
||||||
|
delete headers.host;
|
||||||
|
delete headers.connection;
|
||||||
|
delete headers["x-forwarded-for"];
|
||||||
|
delete headers["x-forwarded-host"];
|
||||||
|
delete headers["x-forwarded-proto"];
|
||||||
|
headers.host = "s-control:8080";
|
||||||
|
const upstream = http.request({hostname: "s-control", port: 8080, method: request.method, path: `${target.pathname}${target.search}`, headers, timeout: 10000}, (upstreamResponse) => {
|
||||||
|
response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
|
||||||
|
upstreamResponse.pipe(response);
|
||||||
|
});
|
||||||
|
upstream.on("timeout", () => upstream.destroy(new Error("upstream timeout")));
|
||||||
|
upstream.on("error", () => {
|
||||||
|
if (!response.headersSent) reject(response, 502, "control plane unavailable");
|
||||||
|
else response.destroy();
|
||||||
|
});
|
||||||
|
request.pipe(upstream);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(listenPort, "0.0.0.0");
|
||||||
|
NODE_PROXY
|
||||||
|
chmod 644 "$CONTROL_PLANE_DIR/control-proxy.mjs"
|
||||||
|
chown 1000:1000 "$CONTROL_PLANE_DIR/control-proxy.mjs"
|
||||||
|
openssl genrsa -out "$CONTROL_PLANE_DIR/ca.key" 3072
|
||||||
|
openssl req -x509 -new -nodes -key "$CONTROL_PLANE_DIR/ca.key" -sha256 -days 3650 -subj "/CN=OverCooked customer control plane CA" -out "$CONTROL_PLANE_DIR/ca.crt"
|
||||||
|
openssl genrsa -out "$CONTROL_PLANE_DIR/server.key" 2048
|
||||||
|
openssl req -new -key "$CONTROL_PLANE_DIR/server.key" -subj "/CN=control-proxy" -out "$CONTROL_PLANE_DIR/server.csr"
|
||||||
|
printf '%s\n' 'subjectAltName=DNS:control-proxy,DNS:product-e' 'extendedKeyUsage=serverAuth' > "$CONTROL_PLANE_DIR/server.ext"
|
||||||
|
openssl x509 -req -in "$CONTROL_PLANE_DIR/server.csr" -CA "$CONTROL_PLANE_DIR/ca.crt" -CAkey "$CONTROL_PLANE_DIR/ca.key" -CAcreateserial -out "$CONTROL_PLANE_DIR/server.crt" -days 825 -sha256 -extfile "$CONTROL_PLANE_DIR/server.ext"
|
||||||
|
openssl genrsa -out "$CONTROL_PLANE_DIR/client.key" 2048
|
||||||
|
openssl req -new -key "$CONTROL_PLANE_DIR/client.key" -subj "/CN=overcooked-e" -out "$CONTROL_PLANE_DIR/client.csr"
|
||||||
|
printf '%s\n' 'extendedKeyUsage=clientAuth' > "$CONTROL_PLANE_DIR/client.ext"
|
||||||
|
openssl x509 -req -in "$CONTROL_PLANE_DIR/client.csr" -CA "$CONTROL_PLANE_DIR/ca.crt" -CAkey "$CONTROL_PLANE_DIR/ca.key" -CAcreateserial -out "$CONTROL_PLANE_DIR/client.crt" -days 825 -sha256 -extfile "$CONTROL_PLANE_DIR/client.ext"
|
||||||
|
rm -f "$CONTROL_PLANE_DIR/ca.key" "$CONTROL_PLANE_DIR/server.csr" "$CONTROL_PLANE_DIR/server.ext" "$CONTROL_PLANE_DIR/client.csr" "$CONTROL_PLANE_DIR/client.ext" "$CONTROL_PLANE_DIR/ca.srl"
|
||||||
|
chmod 644 "$CONTROL_PLANE_DIR/ca.crt" "$CONTROL_PLANE_DIR/server.crt" "$CONTROL_PLANE_DIR/client.crt"
|
||||||
|
chmod 640 "$CONTROL_PLANE_DIR/server.key" "$CONTROL_PLANE_DIR/client.key"
|
||||||
|
chown 1000:1000 "$CONTROL_PLANE_DIR/server.key"
|
||||||
|
# The proxy healthcheck (uid 1000) and E (uid 10001) both need the client key;
|
||||||
|
# a shared group keeps it unreadable to other container users.
|
||||||
|
chown 1000:10001 "$CONTROL_PLANE_DIR/client.key"
|
||||||
|
chmod 755 "$CONTROL_PLANE_DIR"
|
||||||
|
|
||||||
secret() { openssl rand -hex 32; }
|
secret() { openssl rand -hex 32; }
|
||||||
S_POSTGRES_PASSWORD=$(secret); S_VALKEY_PASSWORD=$(secret)
|
S_POSTGRES_PASSWORD=$(secret); S_VALKEY_PASSWORD=$(secret)
|
||||||
@@ -161,6 +246,8 @@ S_E_BASE_URL=http://e-server:8096
|
|||||||
S_E_PUBLIC_URL=http://$SERVER_IP:8096
|
S_E_PUBLIC_URL=http://$SERVER_IP:8096
|
||||||
S_E_KEY_ID=customer-e
|
S_E_KEY_ID=customer-e
|
||||||
S_E_INTERNAL_SECRET=$S_INTERNAL_SHARED_SECRET
|
S_E_INTERNAL_SECRET=$S_INTERNAL_SHARED_SECRET
|
||||||
|
# S's private backend client talks to E on the internal Docker network. This
|
||||||
|
# does not weaken E's production mTLS control-plane connection below.
|
||||||
S_E_ALLOW_INSECURE_HTTP=true
|
S_E_ALLOW_INSECURE_HTTP=true
|
||||||
S_AUTH_BASE_URL=$AUTH_URL
|
S_AUTH_BASE_URL=$AUTH_URL
|
||||||
S_AUTH_ALLOW_INSECURE_HTTP=$(case "$AUTH_URL" in http://*) echo true ;; *) echo false ;; esac)
|
S_AUTH_ALLOW_INSECURE_HTTP=$(case "$AUTH_URL" in http://*) echo true ;; *) echo false ;; esac)
|
||||||
@@ -173,16 +260,20 @@ S_PORTAL_SERVER_URL=http://$SERVER_IP:18080
|
|||||||
S_PORTAL_WEB_CLIENT_URL=http://$SERVER_IP:18083/login
|
S_PORTAL_WEB_CLIENT_URL=http://$SERVER_IP:18083/login
|
||||||
S_API_PORT_MAPPING=0.0.0.0:18080:8080
|
S_API_PORT_MAPPING=0.0.0.0:18080:8080
|
||||||
S_WEB_PORT_MAPPING=0.0.0.0:18083:3000
|
S_WEB_PORT_MAPPING=0.0.0.0:18083:3000
|
||||||
S_TURNSTILE_SITE_KEY=$TURNSTILE_SITE_KEY
|
S_TURNSTILE_SITE_KEY=${OVERCOOKED_TURNSTILE_SITE_KEY:-}
|
||||||
S_TURNSTILE_SECRET_KEY=$TURNSTILE_SECRET_KEY
|
S_TURNSTILE_SECRET_KEY=${OVERCOOKED_TURNSTILE_SECRET_KEY:-}
|
||||||
|
S_NODE_IMAGE_NAME=$REGISTRY/overcooked-media-node:$VERSION
|
||||||
PRODUCT_E_IMAGE=$REGISTRY/overcooked-e:$VERSION
|
PRODUCT_E_IMAGE=$REGISTRY/overcooked-e:$VERSION
|
||||||
PRODUCT_E_INSTANCE_ID=e-$INSTANCE_SUFFIX
|
PRODUCT_E_INSTANCE_ID=e-$INSTANCE_SUFFIX
|
||||||
PRODUCT_E_DATABASE_URL=postgres://product_e:$E_POSTGRES_PASSWORD@postgres:5432/product_e?sslmode=disable
|
PRODUCT_E_DATABASE_URL=postgres://product_e:$E_POSTGRES_PASSWORD@postgres:5432/product_e?sslmode=disable
|
||||||
PRODUCT_E_POSTGRES_PASSWORD=$E_POSTGRES_PASSWORD
|
PRODUCT_E_POSTGRES_PASSWORD=$E_POSTGRES_PASSWORD
|
||||||
PRODUCT_E_VALKEY_PASSWORD=$S_VALKEY_PASSWORD
|
PRODUCT_E_VALKEY_PASSWORD=$S_VALKEY_PASSWORD
|
||||||
PRODUCT_E_CONTROL_PLANE_URL=http://s-control:8080
|
PRODUCT_E_CONTROL_PLANE_URL=https://control-proxy:8443
|
||||||
PRODUCT_E_CONTROL_PLANE_KEY_ID=customer-s
|
PRODUCT_E_CONTROL_PLANE_KEY_ID=customer-s
|
||||||
PRODUCT_E_CONTROL_PLANE_SHARED_SECRET=$S_INTERNAL_SHARED_SECRET
|
PRODUCT_E_CONTROL_PLANE_SHARED_SECRET=$S_INTERNAL_SHARED_SECRET
|
||||||
|
PRODUCT_E_CONTROL_PLANE_TLS_CERT_FILE=/run/overcooked-control-plane/client.crt
|
||||||
|
PRODUCT_E_CONTROL_PLANE_TLS_KEY_FILE=/run/overcooked-control-plane/client.key
|
||||||
|
PRODUCT_E_CONTROL_PLANE_TLS_CA_FILE=/run/overcooked-control-plane/ca.crt
|
||||||
PRODUCT_E_LICENSE_ENABLED=true
|
PRODUCT_E_LICENSE_ENABLED=true
|
||||||
PRODUCT_E_AUTH_URL=$AUTH_URL
|
PRODUCT_E_AUTH_URL=$AUTH_URL
|
||||||
PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP=$(case "$AUTH_URL" in http://*) echo true ;; *) echo false ;; esac)
|
PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP=$(case "$AUTH_URL" in http://*) echo true ;; *) echo false ;; esac)
|
||||||
@@ -236,23 +327,31 @@ plugins:
|
|||||||
network_allowed_domains: []
|
network_allowed_domains: []
|
||||||
network_allowed_cidrs: []
|
network_allowed_cidrs: []
|
||||||
EOF
|
EOF
|
||||||
chmod 600 "$ROOT/config/e-server.production.yaml"
|
chmod 644 "$ROOT/config/e-server.production.yaml"
|
||||||
|
|
||||||
docker network inspect overcooked-control >/dev/null 2>&1 || docker network create overcooked-control >/dev/null
|
docker network inspect overcooked-control >/dev/null 2>&1 || docker network create overcooked-control >/dev/null
|
||||||
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" config >/dev/null
|
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" config >/dev/null
|
||||||
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" config >/dev/null
|
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" config >/dev/null
|
||||||
|
|
||||||
for image in "$REGISTRY/overcooked-e:$VERSION" "$REGISTRY/overcooked-s-control:$VERSION" "$REGISTRY/overcooked-s-webui:$VERSION" "$REGISTRY/overcooked-media-node:$VERSION"; do
|
MEDIA_NODE_IMAGE="$REGISTRY/overcooked-media-node:$VERSION"
|
||||||
|
for image in "$REGISTRY/overcooked-e:$VERSION" "$REGISTRY/overcooked-s-control:$VERSION" "$REGISTRY/overcooked-s-webui:$VERSION" "$MEDIA_NODE_IMAGE"; do
|
||||||
docker pull "$image"
|
docker pull "$image"
|
||||||
docker image inspect "$image" >/dev/null 2>&1 || die "镜像校验失败:$image"
|
docker image inspect "$image" >/dev/null 2>&1 || die "镜像校验失败:$image"
|
||||||
done
|
done
|
||||||
|
image_tar=$(mktemp "$ROOT/node-images/media-node.tar.XXXXXX")
|
||||||
|
image_archive=$(mktemp "$ROOT/node-images/media-node.tar.gz.XXXXXX")
|
||||||
|
docker save "$MEDIA_NODE_IMAGE" -o "$image_tar" || die "无法导出 Media Node 镜像归档"
|
||||||
|
gzip -c "$image_tar" > "$image_archive" || die "无法压缩 Media Node 镜像归档"
|
||||||
|
rm -f "$image_tar"
|
||||||
|
mv "$image_archive" "$ROOT/node-images/media-node.tar.gz"
|
||||||
|
chmod 644 "$ROOT/node-images/media-node.tar.gz"
|
||||||
|
|
||||||
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d postgres valkey
|
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d postgres valkey
|
||||||
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" run --rm --no-deps e-server migrate -config /etc/overcooked-e/config.yaml
|
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" run --rm --no-deps e-server migrate -config /etc/overcooked-e/config.yaml
|
||||||
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d postgres valkey
|
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d postgres valkey
|
||||||
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" run --rm --no-deps s-migrate
|
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" run --rm --no-deps s-migrate
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d s-control control-proxy webui
|
||||||
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d e-server
|
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d e-server
|
||||||
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d s-control webui
|
|
||||||
|
|
||||||
wait_for() {
|
wait_for() {
|
||||||
label=$1; url=$2; attempt=1
|
label=$1; url=$2; attempt=1
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ test -f compose/e.yml
|
|||||||
test -f config/e-server.production.yaml
|
test -f config/e-server.production.yaml
|
||||||
test -f s-migrations.tar.gz
|
test -f s-migrations.tar.gz
|
||||||
test "$(find s-migrations -type f -name '*.up.sql' | wc -l)" -gt 0
|
test "$(find s-migrations -type f -name '*.up.sql' | wc -l)" -gt 0
|
||||||
|
sha256sum -c SHA256SUMS >/dev/null
|
||||||
! rg -n '^([[:space:]]*)build:' compose
|
! rg -n '^([[:space:]]*)build:' compose
|
||||||
! rg -n 'overcooked-auth|overcooked-auth-webui' compose install.sh
|
! rg -n 'overcooked-auth|overcooked-auth-webui' compose install.sh
|
||||||
|
! rg -n 'S_TURNSTILE_(SITE|SECRET)_KEY: .*\\?required' compose
|
||||||
|
! rg -n 'S_INTERNAL_TLS_(ADDR|CERT_FILE|KEY_FILE|CLIENT_CA_FILE)' compose install.sh
|
||||||
|
rg -n 'control-proxy|PRODUCT_E_CONTROL_PLANE_TLS_' compose install.sh >/dev/null
|
||||||
echo 'static customer installer checks passed'
|
echo 'static customer installer checks passed'
|
||||||
|
|
||||||
Reference in new issue
Block a user