Files
sub2api-cn-relay-manager/scripts/deploy/remote43_patched_stack_lib.sh
phamnazage-jpg 4e2ee087fd feat(vNext.4): implement trusted-subject security chain for portal user key self-service
- Add portal_auth.go: Portal user session auth with HMAC-signed cookies
- Add /api/portal/session/{login,logout,state} endpoints
- Update nginx config template: cookie-to-header trusted proxy pattern
- Update frontend: sync CRM session on login/logout
- Add TRUSTED_SUBJECT_DEPLOY_GUIDE.md with remote43 deployment steps
- Update EXECUTION_BOARD.md: mark trusted-subject blocking issue as resolved

This implements the secure chain:
  Browser → Portal → nginx (cookie→header) → CRM (verify proxy secret)

Required remote43 actions:
1. Generate 64-char hex secret
2. Update .env.crm with TRUSTED_* config
3. Update nginx with cookie map and header injection
4. Restart services

Fixes EXECUTION_BOARD.md 2026-06-08 blocking issue
2026-06-09 07:48:03 +08:00

291 lines
8.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
# remote43 patched stack 渲染 helper供部署脚本和测试脚本共享。
remote43_require_file() {
local path="$1"
local label="$2"
[[ -f "$path" ]] || {
echo "missing $label: $path" >&2
return 1
}
}
remote43_random_hex() {
local bytes="${1:?bytes required}"
python3 - "$bytes" <<'PY'
import secrets
import sys
print(secrets.token_hex(int(sys.argv[1])))
PY
}
remote43_write_env_file() {
local path="$1"
shift
: > "$path"
while [[ $# -gt 0 ]]; do
local key="$1"
local value="$2"
shift 2
case "$value" in
*$'\n'*)
echo "env value for $key must not contain newlines" >&2
return 1
;;
esac
printf '%s=%s\n' "$key" "$value" >> "$path"
done
}
render_remote43_host_env() {
local pg_container="$1"
local redis_container="$2"
local db_password="$3"
local db_name="$4"
local admin_email="$5"
local admin_password="$6"
local jwt_secret="$7"
local totp_key="$8"
cat <<EOF
AUTO_SETUP=true
DATABASE_HOST=$pg_container
DATABASE_PORT=5432
DATABASE_USER=sub2api
DATABASE_PASSWORD=$db_password
DATABASE_DBNAME=$db_name
REDIS_HOST=$redis_container
REDIS_PORT=6379
ADMIN_EMAIL=$admin_email
ADMIN_PASSWORD=$admin_password
JWT_SECRET=$jwt_secret
TOTP_ENCRYPTION_KEY=$totp_key
SECURITY_URL_ALLOWLIST_ENABLED=false
SECURITY_URL_ALLOWLIST_MODE=disabled
EOF
}
render_remote43_crm_env() {
local crm_port="$1"
local sqlite_dsn="$2"
local admin_token="$3"
local repo_root="${4:-}"
local admin_username="${5:-admin}"
local admin_password="${6:-$admin_token}"
local trusted_subject_header="${SUB2API_CRM_TRUSTED_SUBJECT_HEADER:-}"
local trusted_proxy_secret_header="${SUB2API_CRM_TRUSTED_PROXY_SECRET_HEADER:-X-CRM-Trusted-Proxy}"
local trusted_proxy_secret="${SUB2API_CRM_TRUSTED_PROXY_SECRET:-}"
local sqlite_dsn_q admin_token_q repo_root_q admin_username_q admin_password_q
local trusted_subject_header_q trusted_proxy_secret_header_q trusted_proxy_secret_q
printf -v sqlite_dsn_q '%q' "$sqlite_dsn"
printf -v admin_token_q '%q' "$admin_token"
printf -v repo_root_q '%q' "$repo_root"
printf -v admin_username_q '%q' "$admin_username"
printf -v admin_password_q '%q' "$admin_password"
printf -v trusted_subject_header_q '%q' "$trusted_subject_header"
printf -v trusted_proxy_secret_header_q '%q' "$trusted_proxy_secret_header"
printf -v trusted_proxy_secret_q '%q' "$trusted_proxy_secret"
cat <<EOF
SUB2API_CRM_LISTEN_ADDR=127.0.0.1:$crm_port
SUB2API_CRM_SQLITE_DSN=$sqlite_dsn_q
SUB2API_CRM_ADMIN_TOKEN=$admin_token_q
SUB2API_CRM_ADMIN_USERNAME=$admin_username_q
SUB2API_CRM_ADMIN_PASSWORD=$admin_password_q
SUB2API_CRM_ADMIN_SESSION_TTL=12h
SUB2API_CRM_REPO_ROOT=$repo_root_q
SUB2API_CRM_RECONCILE_WORKER_ENABLED=false
SUB2API_CRM_TRUSTED_SUBJECT_HEADER=$trusted_subject_header_q
SUB2API_CRM_TRUSTED_PROXY_SECRET_HEADER=$trusted_proxy_secret_header_q
SUB2API_CRM_TRUSTED_PROXY_SECRET=$trusted_proxy_secret_q
EOF
}
render_remote43_bootstrap_script() {
local remote_root="$1"
local host_env_file="$2"
local crm_env_file="$3"
local host_binary_name="$4"
local crm_binary_name="$5"
local data_dir="$6"
local crm_db_file="$7"
local crm_pid_file="$8"
local crm_log_file="$9"
local app_container="${10}"
local pg_container="${11}"
local redis_container="${12}"
local network_name="${13}"
local host_image="${14}"
local pg_image="${15}"
local redis_image="${16}"
local db_password="${17}"
local db_name="${18}"
local host_port="${19}"
local crm_port="${20}"
local host_container_port="${21}"
local remote_repo_root="${22}"
local remote_repo_bundle="${23}"
local remote_root_q host_env_q crm_env_q host_binary_q crm_binary_q
local data_dir_q crm_db_q crm_pid_q crm_log_q app_q pg_q redis_q
local network_q host_image_q pg_image_q redis_image_q db_password_q db_name_q
local host_port_q crm_port_q host_container_port_q remote_repo_root_q remote_repo_bundle_q
remote_root_q="$(printf '%q' "$remote_root")"
host_env_q="$(printf '%q' "$host_env_file")"
crm_env_q="$(printf '%q' "$crm_env_file")"
host_binary_q="$(printf '%q' "$host_binary_name")"
crm_binary_q="$(printf '%q' "$crm_binary_name")"
data_dir_q="$(printf '%q' "$data_dir")"
crm_db_q="$(printf '%q' "$crm_db_file")"
crm_pid_q="$(printf '%q' "$crm_pid_file")"
crm_log_q="$(printf '%q' "$crm_log_file")"
app_q="$(printf '%q' "$app_container")"
pg_q="$(printf '%q' "$pg_container")"
redis_q="$(printf '%q' "$redis_container")"
network_q="$(printf '%q' "$network_name")"
host_image_q="$(printf '%q' "$host_image")"
pg_image_q="$(printf '%q' "$pg_image")"
redis_image_q="$(printf '%q' "$redis_image")"
db_password_q="$(printf '%q' "$db_password")"
db_name_q="$(printf '%q' "$db_name")"
host_port_q="$(printf '%q' "$host_port")"
crm_port_q="$(printf '%q' "$crm_port")"
host_container_port_q="$(printf '%q' "$host_container_port")"
remote_repo_root_q="$(printf '%q' "$remote_repo_root")"
remote_repo_bundle_q="$(printf '%q' "$remote_repo_bundle")"
cat <<EOF
#!/usr/bin/env bash
set -euo pipefail
REMOTE_ROOT=$remote_root_q
HOST_ENV_FILE=$host_env_q
CRM_ENV_FILE=$crm_env_q
HOST_BINARY="\$REMOTE_ROOT/$host_binary_q"
CRM_BINARY="\$REMOTE_ROOT/$crm_binary_q"
DATA_DIR=$data_dir_q
CRM_DB_FILE=$crm_db_q
CRM_PID_FILE=$crm_pid_q
CRM_LOG_FILE=$crm_log_q
APP_CONTAINER=$app_q
PG_CONTAINER=$pg_q
REDIS_CONTAINER=$redis_q
NETWORK_NAME=$network_q
HOST_IMAGE=$host_image_q
PG_IMAGE=$pg_image_q
REDIS_IMAGE=$redis_image_q
DB_PASSWORD=$db_password_q
DB_NAME=$db_name_q
HOST_PORT=$host_port_q
CRM_PORT=$crm_port_q
HOST_CONTAINER_PORT=$host_container_port_q
REMOTE_REPO_ROOT=$remote_repo_root_q
REMOTE_REPO_BUNDLE=$remote_repo_bundle_q
mkdir -p "\$REMOTE_ROOT" "\$DATA_DIR" "\$(dirname "\$REMOTE_REPO_ROOT")"
chmod 755 "\$HOST_BINARY" "\$CRM_BINARY"
rm -f "\$DATA_DIR/install.lock" "\$DATA_DIR/config.yaml" "\$DATA_DIR/.installed"
rm -f "\$CRM_DB_FILE"
if [[ -f "\$CRM_PID_FILE" ]]; then
kill "\$(cat "\$CRM_PID_FILE")" >/dev/null 2>&1 || true
rm -f "\$CRM_PID_FILE"
fi
rm -f "\$CRM_LOG_FILE"
sudo -n docker rm -f "\$APP_CONTAINER" "\$PG_CONTAINER" "\$REDIS_CONTAINER" >/dev/null 2>&1 || true
sudo -n docker network inspect "\$NETWORK_NAME" >/dev/null 2>&1 || sudo -n docker network create "\$NETWORK_NAME" >/dev/null
sudo -n docker run -d --name "\$PG_CONTAINER" --network "\$NETWORK_NAME" \\
-e POSTGRES_USER=sub2api \\
-e POSTGRES_PASSWORD="\$DB_PASSWORD" \\
-e POSTGRES_DB="\$DB_NAME" \\
"\$PG_IMAGE" >/dev/null
sudo -n docker run -d --name "\$REDIS_CONTAINER" --network "\$NETWORK_NAME" \\
"\$REDIS_IMAGE" >/dev/null
sleep 10
sudo -n docker run -d --name "\$APP_CONTAINER" --network "\$NETWORK_NAME" \\
-p "127.0.0.1:\$HOST_PORT:\$HOST_CONTAINER_PORT" \\
--env-file "\$HOST_ENV_FILE" \\
-v "\$DATA_DIR:/app/data" \\
-v "\$HOST_BINARY:/app/sub2api:ro" \\
"\$HOST_IMAGE" /app/sub2api >/dev/null
if [[ -f "\$REMOTE_REPO_BUNDLE" ]]; then
if [[ -d "\$REMOTE_REPO_ROOT/.git" ]]; then
git -C "\$REMOTE_REPO_ROOT" fetch "\$REMOTE_REPO_BUNDLE" main
git -C "\$REMOTE_REPO_ROOT" reset --hard FETCH_HEAD
else
rm -rf "\$REMOTE_REPO_ROOT"
git clone "\$REMOTE_REPO_BUNDLE" "\$REMOTE_REPO_ROOT"
git -C "\$REMOTE_REPO_ROOT" checkout main
fi
git -C "\$REMOTE_REPO_ROOT" config user.name "Remote43 CRM"
git -C "\$REMOTE_REPO_ROOT" config user.email "remote43-crm@tksea.top"
fi
python3 - "\$HOST_ENV_FILE" "\$HOST_PORT" <<'PY'
import json
import pathlib
import subprocess
import sys
import time
env_path = pathlib.Path(sys.argv[1])
host_port = sys.argv[2]
values = {}
for line in env_path.read_text(encoding='utf-8').splitlines():
if '=' not in line:
continue
key, value = line.split('=', 1)
values[key] = value
payload = json.dumps({
'email': values['ADMIN_EMAIL'],
'password': values['ADMIN_PASSWORD'],
'turnstile_token': '',
}, ensure_ascii=False)
url = f"http://127.0.0.1:{host_port}/api/v1/auth/login"
for _ in range(60):
result = subprocess.run(
['curl', '-fsS', '-H', 'Content-Type: application/json', '-X', 'POST', url, '-d', payload],
text=True,
capture_output=True,
)
if result.returncode == 0 and 'access_token' in result.stdout:
raise SystemExit(0)
time.sleep(2)
raise SystemExit(f'host login did not become ready on {url}')
PY
nohup bash -lc 'set -a; source "\$1"; set +a; exec "\$2"' _ "\$CRM_ENV_FILE" "\$CRM_BINARY" >"\$CRM_LOG_FILE" 2>&1 &
echo \$! > "\$CRM_PID_FILE"
python3 - "\$CRM_PORT" <<'PY'
import subprocess
import sys
import time
url = f"http://127.0.0.1:{sys.argv[1]}/healthz"
for _ in range(30):
result = subprocess.run(['curl', '-fsS', url], text=True, capture_output=True)
if result.returncode == 0 and result.stdout.strip() == 'ok':
raise SystemExit(0)
time.sleep(1)
raise SystemExit(f'crm healthz did not become ready on {url}')
PY
printf 'host_base=http://127.0.0.1:%s\n' "\$HOST_PORT"
printf 'crm_base=http://127.0.0.1:%s\n' "\$CRM_PORT"
printf 'remote_host_env=%s\n' "\$HOST_ENV_FILE"
printf 'crm_log=%s\n' "\$CRM_LOG_FILE"
EOF
}