Fix Headplane credential failure handling and discover exact service
This commit is contained in:
@@ -97,4 +97,4 @@ Caddy 同时提供 /install.sh 稳定入口与 /releases/<40位小写Git提交>/
|
|||||||
|
|
||||||
## Headplane 准备与凭据
|
## Headplane 准备与凭据
|
||||||
|
|
||||||
`headplane-prepare.sh --mode prepare|key --hostname NAME --root /srv/platform/headscale --port 3000 --project PROJECT [--container CONTAINER]` 在已有 Headscale 准备根目录内创建持久目录和独立 cookie,或在精确项目的健康 Headscale 容器内签发 90 天管理 key。凭据仅写 root 0600 文件,不输出值;重跑保留原值,未知或中断状态停止检查。Komodo Stack 负责服务启动,控制端回收凭据并验证登录。完整顺序见 platform 的 `docs/headscale-repeatable-install.md`。
|
`headplane-prepare.sh --mode prepare|key --hostname NAME --root /srv/platform/headscale --port 3000 --project PROJECT` 在已有 Headscale 准备根目录内创建持久目录和独立 cookie,或按项目/服务标签和数据目录核对唯一健康 Headscale 容器后签发 90 天管理 key。凭据仅写 root 0600 文件,不输出值;重跑保留原值,未知或中断状态停止检查。Komodo Stack 负责服务启动,控制端回收凭据并验证登录。完整顺序见 platform 的 `docs/headscale-repeatable-install.md`。
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ safe_path() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
prepare() {
|
prepare() {
|
||||||
local root=$1 port=$2 binding=$3
|
local root=$1 port=$2 binding=$3 listeners
|
||||||
if [[ -e "$root/headplane-prepared" ]]; then
|
if [[ -e "$root/headplane-prepared" ]]; then
|
||||||
[[ -f "$root/headplane-prepared" && $(cat "$root/headplane-prepared") == "$binding" ]] || { fail 'Headplane binding differs'; return 1; }
|
[[ -f "$root/headplane-prepared" && $(cat "$root/headplane-prepared") == "$binding" ]] || { fail 'Headplane binding differs'; return 1; }
|
||||||
[[ -d "$root/headplane-data" && -d "$root/headplane-secrets" && -f "$root/headplane-secrets/cookie" ]] || { fail 'Incomplete preparation'; return 1; }
|
[[ -d "$root/headplane-data" && -d "$root/headplane-secrets" && -f "$root/headplane-secrets/cookie" ]] || { fail 'Incomplete preparation'; return 1; }
|
||||||
@@ -21,7 +21,8 @@ prepare() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
[[ ! -e "$root/headplane-data" && ! -e "$root/headplane-secrets" ]] || { fail 'Unregistered or interrupted Headplane preparation; inspect before recovery'; return 1; }
|
[[ ! -e "$root/headplane-data" && ! -e "$root/headplane-secrets" ]] || { fail 'Unregistered or interrupted Headplane preparation; inspect before recovery'; return 1; }
|
||||||
[[ -z $(ss -H -lnt "sport = :$port") ]] || { fail 'Headplane port occupied'; return 1; }
|
listeners=$(ss -H -lnt "sport = :$port") || { fail 'Cannot inspect Headplane port'; return 1; }
|
||||||
|
[[ -z "$listeners" ]] || { fail 'Headplane port occupied'; return 1; }
|
||||||
mkdir -m 0700 "$root/headplane-data" "$root/headplane-secrets"
|
mkdir -m 0700 "$root/headplane-data" "$root/headplane-secrets"
|
||||||
openssl rand -hex 16 | tr -d '\r\n' > "$root/headplane-secrets/cookie"
|
openssl rand -hex 16 | tr -d '\r\n' > "$root/headplane-secrets/cookie"
|
||||||
chmod 0600 "$root/headplane-secrets/cookie"
|
chmod 0600 "$root/headplane-secrets/cookie"
|
||||||
@@ -30,10 +31,13 @@ prepare() {
|
|||||||
printf 'Headplane directories and protected cookie prepared.\n'
|
printf 'Headplane directories and protected cookie prepared.\n'
|
||||||
}
|
}
|
||||||
issue_key() {
|
issue_key() {
|
||||||
local root=$1 project=$2 container=$3 actual output="$1/headplane-secrets/admin-api-key"
|
local root=$1 project=$2 container actual output="$1/headplane-secrets/admin-api-key"
|
||||||
[[ -f "$root/headplane-prepared" && -d "$root/headplane-secrets" ]] || { fail 'Run prepare first'; return 1; }
|
[[ -f "$root/headplane-prepared" && -d "$root/headplane-secrets" ]] || { fail 'Run prepare first'; return 1; }
|
||||||
actual=$(docker inspect --format '{{index .Config.Labels "com.docker.compose.project"}}/{{index .Config.Labels "com.docker.compose.service"}}/{{.State.Health.Status}}' "$container")
|
container=$(docker ps --filter "label=com.docker.compose.project=$project" --filter label=com.docker.compose.service=headscale --format '{{.ID}}') || return 1
|
||||||
[[ "$actual" == "$project/headscale/healthy" ]] || { fail 'Container project, service or health differs'; return 1; }
|
[[ "$container" =~ ^[a-f0-9]{12,64}$ ]] || { fail 'Exactly one running Headscale container required'; return 1; }
|
||||||
|
actual=$(docker inspect --format '{{index .Config.Labels "com.docker.compose.project"}}/{{index .Config.Labels "com.docker.compose.service"}}/{{.State.Health.Status}}{{range .Mounts}}{{if eq .Destination "/var/lib/headscale"}}/{{.Source}}{{end}}{{end}}' "$container") || return 1
|
||||||
|
[[ "$actual" == "$project/headscale/healthy/$root/data" ]] || { fail 'Container project, data directory or health differs'; return 1; }
|
||||||
|
[[ ! -e "$output.pending" ]] || { fail 'Interrupted key issuance; inspect original operation'; return 1; }
|
||||||
if [[ -e "$output" ]]; then
|
if [[ -e "$output" ]]; then
|
||||||
[[ -f "$output" && -s "$output" && $(stat -c '%u:%a:%h' "$output") == 0:600:1 ]] || { fail 'Existing credential incomplete or unsafe; inspect original operation'; return 1; }
|
[[ -f "$output" && -s "$output" && $(stat -c '%u:%a:%h' "$output") == 0:600:1 ]] || { fail 'Existing credential incomplete or unsafe; inspect original operation'; return 1; }
|
||||||
printf 'Existing login key preserved; verify expiration separately.\n'
|
printf 'Existing login key preserved; verify expiration separately.\n'
|
||||||
@@ -41,19 +45,23 @@ issue_key() {
|
|||||||
fi
|
fi
|
||||||
# Reserve first: interruption leaves an empty file, never silently issues another key.
|
# Reserve first: interruption leaves an empty file, never silently issues another key.
|
||||||
(set -o noclobber; : > "$output")
|
(set -o noclobber; : > "$output")
|
||||||
docker exec "$container" headscale apikeys create --expiration 90d > "$output"
|
if ! docker exec "$container" headscale apikeys create --expiration 90d > "$output.pending"; then
|
||||||
[[ -s "$output" ]] || { fail 'Key creation result missing; inspect original operation'; return 1; }
|
rm -f -- "$output.pending"
|
||||||
chmod 0600 "$output"
|
fail 'Key creation failed; reserved empty file prevents duplicate issuance'; return 1
|
||||||
|
fi
|
||||||
|
[[ -s "$output.pending" ]] || { fail 'Key creation result missing; inspect original operation'; return 1; }
|
||||||
|
chmod 0600 "$output.pending"
|
||||||
|
mv -T -- "$output.pending" "$output"
|
||||||
printf '90-day login key written to protected file; retrieve over the verified management connection.\n'
|
printf '90-day login key written to protected file; retrieve over the verified management connection.\n'
|
||||||
}
|
}
|
||||||
main() {
|
main() {
|
||||||
local mode='' name='' root='' port='' project='' container='' command binding path
|
local mode='' name='' root='' port='' project='' command binding path
|
||||||
while (($#)); do
|
while (($#)); do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--help|-h) echo 'Usage: headplane-prepare.sh --mode prepare|key --hostname NAME --root /srv/platform/headscale --port PORT --project STACK_PROJECT [--container HEADSCALE_CONTAINER]'; return 0;;
|
--help|-h) echo 'Usage: headplane-prepare.sh --mode prepare|key --hostname NAME --root ABSOLUTE_SERVICE_PATH --port PORT --project STACK_PROJECT'; return 0;;
|
||||||
--mode|--hostname|--root|--port|--project|--container)
|
--mode|--hostname|--root|--port|--project)
|
||||||
(($# >= 2)) || { fail 'Missing value'; return 1; }
|
(($# >= 2)) || { fail 'Missing value'; return 1; }
|
||||||
case "$1" in --mode) mode=$2;; --hostname) name=$2;; --root) root=$2;; --port) port=$2;; --project) project=$2;; --container) container=$2;; esac; shift 2;;
|
case "$1" in --mode) mode=$2;; --hostname) name=$2;; --root) root=$2;; --port) port=$2;; --project) project=$2;; esac; shift 2;;
|
||||||
*) fail 'Unknown option'; return 1;;
|
*) fail 'Unknown option'; return 1;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -66,7 +74,7 @@ main() {
|
|||||||
safe_path "$root"
|
safe_path "$root"
|
||||||
[[ -d "$root" && -f "$root/prepared.sha256" ]] || { fail 'Headscale prepared root required'; return 1; }
|
[[ -d "$root" && -f "$root/prepared.sha256" ]] || { fail 'Headscale prepared root required'; return 1; }
|
||||||
[[ $(stat -c '%u:%a' "$root") == 0:700 ]] || { fail 'Service root must be root-owned mode 0700'; return 1; }
|
[[ $(stat -c '%u:%a' "$root") == 0:700 ]] || { fail 'Service root must be root-owned mode 0700'; return 1; }
|
||||||
for path in prepared.sha256 headplane-prepared headplane-data headplane-secrets headplane-secrets/cookie headplane-secrets/admin-api-key headplane-lock; do
|
for path in prepared.sha256 headplane-prepared headplane-data headplane-secrets headplane-secrets/cookie headplane-secrets/admin-api-key headplane-secrets/admin-api-key.pending headplane-lock; do
|
||||||
# File basenames contain dots; inspect every existing component separately.
|
# File basenames contain dots; inspect every existing component separately.
|
||||||
[[ ! -L "$root/$path" ]] || { fail 'Symlink in Headplane files'; return 1; }
|
[[ ! -L "$root/$path" ]] || { fail 'Symlink in Headplane files'; return 1; }
|
||||||
done
|
done
|
||||||
@@ -79,8 +87,7 @@ main() {
|
|||||||
if [[ "$mode" == prepare ]]; then prepare "$root" "$port" "$binding"
|
if [[ "$mode" == prepare ]]; then prepare "$root" "$port" "$binding"
|
||||||
else
|
else
|
||||||
[[ -f "$root/headplane-prepared" && $(cat "$root/headplane-prepared") == "$binding" ]] || { fail 'Headplane binding differs'; return 1; }
|
[[ -f "$root/headplane-prepared" && $(cat "$root/headplane-prepared") == "$binding" ]] || { fail 'Headplane binding differs'; return 1; }
|
||||||
[[ "$container" =~ ^[a-zA-Z0-9][a-zA-Z0-9_.-]+$ ]] || { fail 'Exact Headscale container required'; return 1; }
|
issue_key "$root" "$project"
|
||||||
issue_key "$root" "$project" "$container"
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
if [[ ${BASH_SOURCE[0]:-} == "$0" || ${BASH_SOURCE[0]:-} == '' ]]; then main "$@"; fi
|
if [[ ${BASH_SOURCE[0]:-} == "$0" || ${BASH_SOURCE[0]:-} == '' ]]; then main "$@"; fi
|
||||||
|
|||||||
Reference in New Issue
Block a user