feat: unify ai workspace deployment auth

This commit is contained in:
Haitao Pan 2026-06-14 09:09:40 +08:00
parent 4b7c52057d
commit e2ae564745
7 changed files with 385 additions and 12 deletions

View File

@ -41,7 +41,8 @@ gateway_openclaw_upstream_port: 18789
gateway_openclaw_bind: loopback
gateway_openclaw_mode: local
gateway_openclaw_auth_mode: token
gateway_openclaw_gateway_token: ""
ai_workspace_auth_token: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_AUTH_TOKEN') | default('', true) }}"
gateway_openclaw_gateway_token: "{{ lookup('ansible.builtin.env', 'OPENCLAW_GATEWAY_TOKEN') | default(ai_workspace_auth_token, true) }}"
gateway_openclaw_trusted_proxies:
- 127.0.0.1
- "::1"

View File

@ -10,7 +10,8 @@ litellm_config_file: "{{ litellm_config_dir }}/config.yaml"
litellm_env_file: "{{ litellm_config_dir }}/litellm.env"
litellm_systemd_unit_path: "/etc/systemd/system/{{ litellm_service_name }}.service"
litellm_master_key: "{{ lookup('ansible.builtin.env', 'LITELLM_MASTER_KEY') | default('', true) }}"
ai_workspace_auth_token: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_AUTH_TOKEN') | default('', true) }}"
litellm_master_key: "{{ lookup('ansible.builtin.env', 'LITELLM_MASTER_KEY') | default(ai_workspace_auth_token, true) }}"
litellm_salt_key: "{{ lookup('ansible.builtin.env', 'LITELLM_SALT_KEY') | default(lookup('password', '/tmp/.litellm_salt_key length=32 chars=ascii_letters,digits'), true) }}"
litellm_ui_username: "{{ lookup('ansible.builtin.env', 'LITELLM_UI_USERNAME') | default('admin', true) }}"
@ -62,4 +63,4 @@ litellm_retry_after: 60
litellm_num_retries: 3
litellm_request_timeout: 600
litellm_max_parallel_requests: 1000
litellm_telemetry: false
litellm_telemetry: false

View File

@ -0,0 +1,199 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
init_vault_admin.sh --password <password> [options]
Options:
--username <name> Admin username. Default: admin
--password <password> Required. Password for the admin userpass account.
--vault-addr <addr> Vault API address. Default: http://127.0.0.1:8200
--root-token <token> Root token. Defaults to VAULT_TOKEN or
VAULT_SERVER_ROOT_ACCESS_TOKEN if set.
--issuer <label> TOTP issuer label. Default: Vault
--method-name <name> TOTP method name. Default: vault-admin-totp
--output-dir <dir> Enrollment output directory. Default: /tmp
--ui-url <url> UI login URL. Default: http://127.0.0.1:8200/ui/vault/auth?with=userpass
-h, --help Show this help message
EOF
}
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "missing required command: $1" >&2
exit 1
fi
}
b64decode() {
if base64 --help 2>&1 | grep -q -- '--decode'; then
base64 --decode
else
base64 -D
fi
}
USERNAME="admin"
PASSWORD=""
VAULT_ADDR="${VAULT_ADDR:-http://127.0.0.1:8200}"
ROOT_TOKEN="${VAULT_TOKEN:-}"
if [[ -z "$ROOT_TOKEN" && -n "${VAULT_SERVER_ROOT_ACCESS_TOKEN:-}" ]]; then
ROOT_TOKEN="${VAULT_SERVER_ROOT_ACCESS_TOKEN}"
fi
ISSUER="Vault"
METHOD_NAME="vault-admin-totp"
OUTPUT_DIR="/tmp"
UI_URL="http://127.0.0.1:8200/ui/vault/auth?with=userpass"
POLICY_NAME="vault-admins"
ENFORCEMENT_NAME="admin-userpass"
while [[ $# -gt 0 ]]; do
case "$1" in
--username)
USERNAME="${2:-}"
shift 2
;;
--password)
PASSWORD="${2:-}"
shift 2
;;
--vault-addr)
VAULT_ADDR="${2:-}"
shift 2
;;
--root-token)
ROOT_TOKEN="${2:-}"
shift 2
;;
--issuer)
ISSUER="${2:-}"
shift 2
;;
--method-name)
METHOD_NAME="${2:-}"
shift 2
;;
--output-dir)
OUTPUT_DIR="${2:-}"
shift 2
;;
--ui-url)
UI_URL="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$PASSWORD" ]]; then
echo "--password is required" >&2
usage >&2
exit 1
fi
if [[ -z "$ROOT_TOKEN" ]]; then
echo "root token missing: pass --root-token or export VAULT_TOKEN first" >&2
exit 1
fi
require_cmd vault
require_cmd jq
require_cmd curl
require_cmd base64
export VAULT_ADDR
export VAULT_TOKEN="$ROOT_TOKEN"
if ! vault status >/dev/null 2>&1; then
echo "unable to reach Vault at $VAULT_ADDR" >&2
exit 1
fi
if ! vault auth list -format=json | jq -e 'has("userpass/")' >/dev/null; then
vault auth enable userpass >/dev/null
fi
vault auth tune -listing-visibility=unauth userpass/ >/dev/null
tmp_policy="$(mktemp)"
trap 'rm -f "$tmp_policy"' EXIT
cat >"$tmp_policy" <<'POL'
path "*" {
capabilities = ["create", "read", "update", "delete", "list", "patch", "sudo"]
}
POL
vault policy write "$POLICY_NAME" "$tmp_policy" >/dev/null
vault write "auth/userpass/users/${USERNAME}" \
password="$PASSWORD" \
token_policies="$POLICY_NAME" >/dev/null
userpass_accessor="$(vault auth list -format=json | jq -r '."userpass/".accessor')"
methods_json="$(curl -sS \
-H "X-Vault-Token: ${VAULT_TOKEN}" \
-H "X-Vault-Request: true" \
-X LIST \
"${VAULT_ADDR}/v1/identity/mfa/method/totp")"
method_id="$(printf '%s' "$methods_json" | jq -r --arg method_name "$METHOD_NAME" '.data.key_info // {} | to_entries[]? | select(.value.name == $method_name) | .key' | head -n1)"
if [[ -z "$method_id" ]]; then
method_json="$(vault write -format=json identity/mfa/method/totp \
method_name="$METHOD_NAME" \
issuer="$ISSUER" \
period=30 \
digits=6 \
algorithm=SHA1 \
skew=1 \
max_validation_attempts=5)"
method_id="$(printf '%s' "$method_json" | jq -r '.data.method_id // .data.id')"
fi
bootstrap_json="$(vault write -format=json "auth/userpass/login/${USERNAME}" password="$PASSWORD")"
entity_id="$(printf '%s' "$bootstrap_json" | jq -r '.auth.entity_id')"
bootstrap_token="$(printf '%s' "$bootstrap_json" | jq -r '.auth.client_token')"
mkdir -p "$OUTPUT_DIR"
enrollment_json="${OUTPUT_DIR}/vault-${USERNAME}-totp.json"
enrollment_png="${OUTPUT_DIR}/vault-${USERNAME}-totp.png"
enrollment_uri="${OUTPUT_DIR}/vault-${USERNAME}-totp-uri.txt"
vault write identity/mfa/method/totp/admin-destroy \
method_id="$method_id" \
entity_id="$entity_id" >/dev/null 2>&1 || true
vault write -format=json identity/mfa/method/totp/admin-generate \
method_id="$method_id" \
entity_id="$entity_id" >"$enrollment_json"
jq -r '.data.barcode' "$enrollment_json" | b64decode >"$enrollment_png"
jq -r '.data.url' "$enrollment_json" >"$enrollment_uri"
chmod 600 "$enrollment_json" "$enrollment_png" "$enrollment_uri"
vault write "identity/mfa/login-enforcement/${ENFORCEMENT_NAME}" \
mfa_method_ids="$method_id" \
auth_method_accessors="$userpass_accessor" >/dev/null
vault token revoke "$bootstrap_token" >/dev/null || true
cat <<EOF
vault_addr=$VAULT_ADDR
username=$USERNAME
policy=$POLICY_NAME
method_id=$method_id
userpass_accessor=$userpass_accessor
entity_id=$entity_id
enrollment_json=$enrollment_json
enrollment_png=$enrollment_png
enrollment_uri=$enrollment_uri
ui_url=$UI_URL
EOF

View File

@ -2,3 +2,18 @@
script: files/setup.sh {{ domain }} {{ namespace }} {{ item.secret_name }} {{ vault_public_access | bool | lower }}
loop: "{{ tls }}"
when: inventory_hostname in groups[group]
- name: Bootstrap Vault admin userpass auth
ansible.builtin.script: >-
files/init_vault_admin.sh
--username {{ vault_admin_username | quote }}
--password {{ vault_admin_password | quote }}
--vault-addr {{ vault_admin_addr | quote }}
--root-token {{ vault_server_root_access_token | quote }}
--output-dir {{ vault_admin_output_dir | quote }}
--ui-url {{ vault_admin_ui_url | quote }}
no_log: true
when:
- not ansible_check_mode
- inventory_hostname in groups[group]
- vault_admin_init_enabled | bool

View File

@ -2,6 +2,14 @@ group: master
namespace: vault
# When false, disables the Ingress for public access.
vault_public_access: false
ai_workspace_auth_token: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_AUTH_TOKEN') | default('', true) }}"
vault_server_root_access_token: "{{ lookup('ansible.builtin.env', 'VAULT_SERVER_ROOT_ACCESS_TOKEN') | default(lookup('ansible.builtin.env', 'VAULT_TOKEN') | default(ai_workspace_auth_token, true), true) }}"
vault_admin_init_enabled: "{{ (vault_server_root_access_token | trim | length > 0) and (vault_admin_password | trim | length > 0) }}"
vault_admin_username: admin
vault_admin_password: "{{ lookup('ansible.builtin.env', 'VAULT_ADMIN_PASSWORD') | default(ai_workspace_auth_token, true) }}"
vault_admin_addr: "{{ lookup('ansible.builtin.env', 'VAULT_ADDR') | default('http://127.0.0.1:8200', true) }}"
vault_admin_ui_url: "{{ vault_admin_addr }}/ui/vault/auth?with=userpass"
vault_admin_output_dir: "{{ lookup('ansible.builtin.env', 'VAULT_ADMIN_OUTPUT_DIR') | default('/tmp', true) }}"
update_secret: true
tls:
- secret_name: vault-tls

View File

@ -3,7 +3,8 @@ xworkmate_bridge_service_name: xworkmate-bridge
xworkmate_bridge_service_user: ubuntu
xworkmate_bridge_service_group: ubuntu
xworkmate_bridge_service_home: "/home/{{ xworkmate_bridge_service_user }}"
xworkmate_bridge_auth_token: "{{ lookup('ansible.builtin.env', 'BRIDGE_AUTH_TOKEN') | default(lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true), true) }}"
ai_workspace_auth_token: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_AUTH_TOKEN') | default('', true) }}"
xworkmate_bridge_auth_token: "{{ lookup('ansible.builtin.env', 'BRIDGE_AUTH_TOKEN') | default(lookup('ansible.builtin.env', 'XWORKMATE_BRIDGE_AUTH_TOKEN') | default(lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default(ai_workspace_auth_token, true), true), true) }}"
xworkmate_bridge_review_auth_token: "{{ lookup('ansible.builtin.env', 'BRIDGE_REVIEW_AUTH_TOKEN') | default('', true) }}"
xworkmate_bridge_listen_host: 127.0.0.1
xworkmate_bridge_listen_port: 8787
@ -43,14 +44,15 @@ deploy_acp_gemini: true
deploy_acp_hermes: true
# Unified domain settings
xworkmate_bridge_domain: xworkmate-bridge.svc.plus
ai_workspace_public_domain: "{{ lookup('ansible.builtin.env', 'SERVER_DOMAIN') | default(lookup('ansible.builtin.env', 'ACP_BRIDGE_DOMAIN') | default(lookup('ansible.builtin.env', 'BRIDGE_DOMAIN') | default('xworkmate-bridge.svc.plus', true), true), true) }}"
xworkmate_bridge_domain: "{{ lookup('ansible.builtin.env', 'XWORKMATE_BRIDGE_DOMAIN') | default(ai_workspace_public_domain, true) }}"
# When false, disables public Caddy access to XWorkmate Bridge.
xworkmate_bridge_public_access: "{{ true if ai_workspace_security_level != 'strict' else false }}"
xworkmate_bridge_public_base_url: https://xworkmate-bridge.svc.plus
xworkmate_bridge_service_domain: xworkmate-bridge.svc.plus
xworkmate_bridge_service_public_base_url: https://xworkmate-bridge.svc.plus
xworkmate_bridge_public_base_url: "https://{{ xworkmate_bridge_domain }}"
xworkmate_bridge_service_domain: "{{ xworkmate_bridge_domain }}"
xworkmate_bridge_service_public_base_url: "https://{{ xworkmate_bridge_domain }}"
xworkmate_bridge_validation_origin: https://xworkmate.svc.plus
# Caddy configuration paths

View File

@ -11,14 +11,71 @@
xworkspace_console_root: /home/ubuntu/xworkspace
xworkspace_console_repo_dir: /home/ubuntu/xworkspace-console
xworkspace_console_dashboard_dir: /home/ubuntu/xworkspace-console/dashboard
xworkspace_console_api_dir: /home/ubuntu/xworkspace-console/api
xworkspace_console_scripts_dir: /home/ubuntu/xworkspace/scripts
xworkspace_console_config_dir: /home/ubuntu/.config/xworkspace
xworkspace_console_url: http://127.0.0.1:17000
xworkspace_console_port: 17000
xworkspace_console_api_port: 8788
xworkspace_console_ttyd_port: 7681
xworkspace_console_enable_ttyd: true
xworkspace_console_install_chrome: true
xworkspace_console_autostart_enabled: true
xworkspace_console_ttyd_binary_path: /usr/local/bin/ttyd
ai_workspace_auth_token: "{{ lookup('ansible.builtin.env', 'AI_WORKSPACE_AUTH_TOKEN') | default('', true) }}"
xworkspace_console_auth_token: >-
{{ lookup('ansible.builtin.env', 'XWORKSPACE_CONSOLE_AUTH_TOKEN')
| default(lookup('ansible.builtin.env', 'AI_WORKSPACE_AUTH_TOKEN'), true)
| default(lookup('ansible.builtin.env', 'BRIDGE_AUTH_TOKEN'), true)
| default(lookup('ansible.builtin.env', 'XWORKMATE_BRIDGE_AUTH_TOKEN'), true)
| default(lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN'), true) }}
xworkspace_console_review_auth_token: "{{ lookup('ansible.builtin.env', 'BRIDGE_REVIEW_AUTH_TOKEN') | default('', true) }}"
xworkspace_console_portal_services:
- key: litellm
name: LiteLLM Admin UI
url: http://localhost:4000/ui
openMode: iframe
healthUrl: http://127.0.0.1:4000/ui
description: Model routing and provider administration.
icon: chart
match:
- litellm
- lite
port: 4000
role: model-router
- key: openclaw
name: OpenClaw
url: http://127.0.0.1:18789/channels
openMode: external
healthUrl: http://127.0.0.1:18789/channels
description: Gateway dashboard.
icon: claw
match:
- openclaw
- gateway
port: 18789
role: gateway
- key: vault
name: Vault Server
url: http://127.0.0.1:8200/ui
openMode: external
healthUrl: http://127.0.0.1:8200/ui
description: Vault UI.
icon: shield
match:
- vault
port: 8200
- key: terminal
name: Terminal
url: http://127.0.0.1:7681
openMode: iframe
healthUrl: http://127.0.0.1:7681
description: Local ttyd terminal.
icon: terminal
match:
- ttyd
- terminal
port: 7681
tasks:
- name: Install Google Chrome apt repository prerequisites
ansible.builtin.apt:
@ -130,6 +187,7 @@
- "{{ xworkspace_console_scripts_dir }}"
- "{{ xworkspace_console_repo_dir }}"
- "{{ xworkspace_console_home }}/.config"
- "{{ xworkspace_console_config_dir }}"
- "{{ xworkspace_console_home }}/.config/autostart"
- "{{ xworkspace_console_home }}/.config/systemd"
- "{{ xworkspace_console_home }}/.config/systemd/user"
@ -390,6 +448,48 @@
npm install && npm run build
become_user: "{{ xworkspace_console_user }}"
- name: Deploy AI Workspace portal service configuration
ansible.builtin.copy:
dest: "{{ xworkspace_console_config_dir }}/portal-services.json"
owner: "{{ xworkspace_console_user }}"
group: "{{ xworkspace_console_user }}"
mode: "0644"
content: "{{ {'services': xworkspace_console_portal_services} | to_nice_json }}\n"
- name: Deploy AI Workspace portal token file
ansible.builtin.copy:
dest: "{{ xworkspace_console_config_dir }}/auth-token"
owner: "{{ xworkspace_console_user }}"
group: "{{ xworkspace_console_user }}"
mode: "0600"
content: "{{ xworkspace_console_auth_token }}\n"
no_log: true
- name: Deploy AI Workspace shared auth token file
ansible.builtin.copy:
dest: "{{ xworkspace_console_home }}/.ai_workspace_auth_token"
owner: "{{ xworkspace_console_user }}"
group: "{{ xworkspace_console_user }}"
mode: "0600"
content: "{{ xworkspace_console_auth_token }}\n"
no_log: true
- name: Deploy XWorkspace API environment
ansible.builtin.copy:
dest: "{{ xworkspace_console_config_dir }}/portal.env"
owner: "{{ xworkspace_console_user }}"
group: "{{ xworkspace_console_user }}"
mode: "0600"
content: |
AI_WORKSPACE_AUTH_TOKEN={{ xworkspace_console_auth_token }}
XWORKSPACE_CONSOLE_AUTH_TOKEN={{ xworkspace_console_auth_token }}
BRIDGE_AUTH_TOKEN={{ xworkspace_console_auth_token }}
BRIDGE_REVIEW_AUTH_TOKEN={{ xworkspace_console_review_auth_token }}
XWORKMATE_BRIDGE_AUTH_TOKEN={{ xworkspace_console_auth_token }}
INTERNAL_SERVICE_TOKEN={{ xworkspace_console_auth_token }}
XWORKSPACE_PORTAL_SERVICES_FILE={{ xworkspace_console_config_dir }}/portal-services.json
no_log: true
- name: Deploy XWorkspace Console service
ansible.builtin.copy:
dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-console.service"
@ -412,6 +512,29 @@
[Install]
WantedBy=default.target
- name: Deploy XWorkspace API service
ansible.builtin.copy:
dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-api.service"
owner: "{{ xworkspace_console_user }}"
group: "{{ xworkspace_console_user }}"
mode: "0644"
content: |
[Unit]
Description=XWorkspace status API
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory={{ xworkspace_console_api_dir }}
EnvironmentFile={{ xworkspace_console_config_dir }}/portal.env
ExecStart=/usr/bin/env go run .
Restart=always
RestartSec=2
[Install]
WantedBy=default.target
- name: Deploy AI Agentic Workspace ttyd service
ansible.builtin.copy:
dest: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-ttyd.service"
@ -426,7 +549,7 @@
[Service]
Type=simple
ExecStart=/usr/bin/ttyd -i lo -p {{ xworkspace_console_ttyd_port }} -O login bash
ExecStart={{ xworkspace_console_ttyd_binary_path }} -i lo -p {{ xworkspace_console_ttyd_port }} -O login bash
Restart=always
RestartSec=2
@ -440,6 +563,13 @@
state: link
become_user: "{{ xworkspace_console_user }}"
- name: Enable XWorkspace API service
ansible.builtin.file:
src: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-api.service"
dest: "{{ xworkspace_console_home }}/.config/systemd/user/default.target.wants/xworkspace-api.service"
state: link
become_user: "{{ xworkspace_console_user }}"
- name: Enable AI Agentic Workspace ttyd service
ansible.builtin.file:
src: "{{ xworkspace_console_home }}/.config/systemd/user/xworkspace-ttyd.service"
@ -490,17 +620,34 @@
- name: Reload systemd user daemon
ansible.builtin.shell: |
su - {{ xworkspace_console_user }} -c "systemctl --user daemon-reload"
uid="$(id -u {{ xworkspace_console_user }})"
loginctl enable-linger {{ xworkspace_console_user }} || true
systemctl start "user@${uid}.service" || true
runuser -u {{ xworkspace_console_user }} -- env XDG_RUNTIME_DIR="/run/user/${uid}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" systemctl --user daemon-reload
become: true
- name: Restart xworkspace-console service
ansible.builtin.shell: |
su - {{ xworkspace_console_user }} -c "systemctl --user restart xworkspace-console.service"
uid="$(id -u {{ xworkspace_console_user }})"
loginctl enable-linger {{ xworkspace_console_user }} || true
systemctl start "user@${uid}.service" || true
runuser -u {{ xworkspace_console_user }} -- env XDG_RUNTIME_DIR="/run/user/${uid}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" systemctl --user restart xworkspace-console.service
become: true
- name: Restart xworkspace-api service
ansible.builtin.shell: |
uid="$(id -u {{ xworkspace_console_user }})"
loginctl enable-linger {{ xworkspace_console_user }} || true
systemctl start "user@${uid}.service" || true
runuser -u {{ xworkspace_console_user }} -- env XDG_RUNTIME_DIR="/run/user/${uid}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" systemctl --user restart xworkspace-api.service
become: true
- name: Restart xworkspace-ttyd service
ansible.builtin.shell: |
su - {{ xworkspace_console_user }} -c "systemctl --user restart xworkspace-ttyd.service"
uid="$(id -u {{ xworkspace_console_user }})"
loginctl enable-linger {{ xworkspace_console_user }} || true
systemctl start "user@${uid}.service" || true
runuser -u {{ xworkspace_console_user }} -- env XDG_RUNTIME_DIR="/run/user/${uid}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" systemctl --user restart xworkspace-ttyd.service
become: true
- name: Hide XFCE desktop icons