Files
sub2api-cn-relay-manager/scripts/deploy/deploy_tksea_portal.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

202 lines
6.6 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
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
KEY="${KEY:-/home/long/下载/zjsea.pem}"
REMOTE="${REMOTE:-ubuntu@43.155.133.187}"
REMOTE_PORTAL_DIR="${REMOTE_PORTAL_DIR:-/var/www/sub2api-portal}"
REMOTE_NGINX_SITE="${REMOTE_NGINX_SITE:-/etc/nginx/sites-available/tksea}"
REMOTE_HOST_PORT="${REMOTE_HOST_PORT:-8080}"
REMOTE_CRM_PORT="${REMOTE_CRM_PORT:-18190}"
LOCAL_PORTAL_DIR="${LOCAL_PORTAL_DIR:-$ROOT_DIR/deploy/tksea-portal}"
REMOTE_STAGE_DIR="${REMOTE_STAGE_DIR:-/tmp/sub2api-portal-deploy}"
DRY_RUN="${DRY_RUN:-0}"
die() {
echo "$*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "missing command: $1"
}
run_cmd() {
if [[ "$DRY_RUN" == "1" ]]; then
printf 'DRY_RUN:'
printf ' %q' "$@"
printf '\n'
return 0
fi
"$@"
}
ssh_remote() {
run_cmd ssh -i "$KEY" -o StrictHostKeyChecking=no "$REMOTE" "$@"
}
scp_remote() {
run_cmd scp -i "$KEY" -o StrictHostKeyChecking=no "$@"
}
main() {
require_cmd python3
require_cmd ssh
require_cmd scp
[[ -d "$LOCAL_PORTAL_DIR" ]] || die "missing portal dir: $LOCAL_PORTAL_DIR"
[[ -f "$LOCAL_PORTAL_DIR/index.html" ]] || die "missing portal index: $LOCAL_PORTAL_DIR/index.html"
if [[ "$DRY_RUN" != "1" ]]; then
[[ -f "$KEY" ]] || die "missing ssh key: $KEY"
fi
local tmpdir patch_file portal_stage_dir
tmpdir="$(mktemp -d)"
trap "rm -rf $(printf '%q' "$tmpdir")" EXIT
patch_file="$tmpdir/patch_tksea_portal_nginx.py"
portal_stage_dir="$tmpdir/portal"
mkdir -p "$portal_stage_dir"
cp -R "$LOCAL_PORTAL_DIR/." "$portal_stage_dir/"
cat > "$patch_file" <<EOF
from pathlib import Path
import re
import textwrap
path = Path(${REMOTE_NGINX_SITE@Q})
text = path.read_text()
block = textwrap.dedent("""\
location = /portal {
return 302 /portal/;
}
location = /portal/admin {
return 302 /portal/admin/;
}
location = /kimi-portal {
return 302 /portal/;
}
# BEGIN sub2api-portal
location /portal/ {
alias ${REMOTE_PORTAL_DIR}/;
index index.html;
try_files \$uri \$uri/ /portal/index.html;
}
location /portal-proxy/ {
proxy_pass http://127.0.0.1:${REMOTE_HOST_PORT}/;
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-Proto \$scheme;
proxy_http_version 1.1;
}
location /portal-admin-api/ {
# 必须由受信登录/鉴权层把用户 subject 放进 \$portal_subject不能信任浏览器自带 header。
# 同时 CRM 需配置:
# SUB2API_CRM_TRUSTED_SUBJECT_HEADER=X-CRM-Authenticated-Subject
# SUB2API_CRM_TRUSTED_PROXY_SECRET_HEADER=X-CRM-Trusted-Proxy
# SUB2API_CRM_TRUSTED_PROXY_SECRET=<same-secret-as-nginx>
proxy_pass http://127.0.0.1:${REMOTE_CRM_PORT}/;
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-Proto \$scheme;
proxy_set_header X-Portal-Subject "";
proxy_set_header X-CRM-Authenticated-Subject \$portal_subject;
proxy_set_header X-CRM-Trusted-Proxy "REPLACE_WITH_SUB2API_CRM_TRUSTED_PROXY_SECRET";
proxy_http_version 1.1;
}
location = /v1/chat/completions {
proxy_pass http://127.0.0.1:${REMOTE_CRM_PORT}/v1/chat/completions;
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-Proto \$scheme;
proxy_http_version 1.1;
}
location /kimi-portal/ {
return 302 /portal/;
}
location /kimi-portal-proxy/ {
proxy_pass http://127.0.0.1:${REMOTE_HOST_PORT}/;
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-Proto \$scheme;
proxy_http_version 1.1;
}
location /kimi/ {
proxy_pass http://127.0.0.1:${REMOTE_HOST_PORT}/;
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-Proto \$scheme;
proxy_http_version 1.1;
}
location /kimi-v1/ {
proxy_pass http://127.0.0.1:${REMOTE_HOST_PORT}/v1/;
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-Proto \$scheme;
proxy_http_version 1.1;
}
# END sub2api-portal
""")
patterns = [
re.compile(r"\\n\\s*location = /portal \\{.*?# END sub2api-portal\\n\\n", re.S),
re.compile(r"\\n\\s*location = /kimi-portal \\{.*?# END kimi-portal\\n\\n", re.S),
]
for pattern in patterns:
if pattern.search(text):
text = pattern.sub("\\n" + block + "\\n", text, count=1)
path.write_text(text)
raise SystemExit(0)
needle = "\\n location / {\\n proxy_pass http://127.0.0.1:"
index = text.rfind(needle)
if index == -1:
raise SystemExit("failed to locate sub.tksea.top root location block")
text = text[:index] + "\\n" + block + text[index:]
path.write_text(text)
EOF
ssh_remote "mkdir -p $(printf '%q' "$REMOTE_STAGE_DIR")"
scp_remote -r "$portal_stage_dir" "$REMOTE:$REMOTE_STAGE_DIR/"
scp_remote "$patch_file" "$REMOTE:$REMOTE_STAGE_DIR/patch_tksea_portal_nginx.py"
ssh_remote "sudo install -d -m 755 $(printf '%q' "$REMOTE_PORTAL_DIR") && sudo cp -R $(printf '%q' "$REMOTE_STAGE_DIR/portal/.") $(printf '%q' "$REMOTE_PORTAL_DIR/") && sudo python3 $(printf '%q' "$REMOTE_STAGE_DIR/patch_tksea_portal_nginx.py") && sudo nginx -t && sudo systemctl reload nginx"
cat <<EOF
tksea portal deployed
remote: ${REMOTE}
portal url: https://sub.tksea.top/portal/
portal admin home url: https://sub.tksea.top/portal/admin/
logical groups admin url: https://sub.tksea.top/portal/admin/logical-groups.html
route health admin url: https://sub.tksea.top/portal/admin/route-health.html
accounts admin url: https://sub.tksea.top/portal/admin/accounts.html
provider admin url: https://sub.tksea.top/portal/admin/providers.html
batch import admin url: https://sub.tksea.top/portal/admin/batch-import.html
batch import admin url: https://sub.tksea.top/portal/admin-batch-import.html
legacy url: https://sub.tksea.top/kimi-portal/
portal dir: ${REMOTE_PORTAL_DIR}
nginx site: ${REMOTE_NGINX_SITE}
EOF
}
main "$@"