From a38011fca64623d67f805aca1feb41f6e3a12cb6 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Tue, 12 May 2026 13:30:41 +0200 Subject: [PATCH] feat(metablog): SSH preflight + Gitea key enrolment helper (ref #94) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gitea CLI in 1.22 lacks 'user keys add', so enrolment goes: generate-access-token (--config app.ini) -> POST /api/v1/user/keys via LXC loopback -> delete token via sqlite3 (token list/delete API requires basic auth in 1.22, not token auth). Key finding: Gitea builtin SSH server (START_SSH_SERVER=true) rejects the conventional "git@" username — the connecting username must match the OS user Gitea runs as ("gitea" here). GITEA_SSH_USER var controls this, defaulting to "gitea". Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/lib/gitea-ssh-preflight.sh | 153 +++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100755 scripts/lib/gitea-ssh-preflight.sh diff --git a/scripts/lib/gitea-ssh-preflight.sh b/scripts/lib/gitea-ssh-preflight.sh new file mode 100755 index 00000000..2884f177 --- /dev/null +++ b/scripts/lib/gitea-ssh-preflight.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# scripts/lib/gitea-ssh-preflight.sh +# Verify SSH path to git@gitea.gk2.secubox.in:2222 works (as the MOCHAbin root). +# If not, enrol the MOCHAbin root's id_ed25519.pub into gandalf's Gitea account. +# +# Sourceable: provides ssh_preflight + enrol_mochabin_root_key functions. +# Standalone: bash gitea-ssh-preflight.sh [--check | --enrol] +# +# Enrol path (gitea 1.22 CLI lacks "user keys add"): +# 1. lxc-attach -n gitea -- su gitea -c \ +# "gitea admin user generate-access-token --config --username gandalf --scopes write:user" +# Output format: "Access token was successfully created: " +# 2. POST /api/v1/user/keys via LXC loopback IP (avoids public TLS path) +# 3. Best-effort token cleanup via sqlite3 on the Gitea DB (token list/delete +# API requires basic auth, not token auth, in Gitea 1.22) + +set -euo pipefail + +GITEA_HOST="${GITEA_HOST:-gitea.gk2.secubox.in}" +GITEA_SSH_PORT="${GITEA_SSH_PORT:-2222}" +GITEA_USER="${GITEA_USER:-gandalf}" +# SSH login user for Gitea's builtin SSH server (must match the OS user Gitea runs as). +# This instance runs as "gitea", NOT "git" — using "git" triggers +# "Invalid SSH username git - must use gitea" in the builtin server. +GITEA_SSH_USER="${GITEA_SSH_USER:-gitea}" +LXC_HOST="${LXC_HOST:-192.168.1.200}" +LXC_NAME="${LXC_NAME:-gitea}" +GITEA_APP_INI="${GITEA_APP_INI:-/var/lib/gitea/custom/conf/app.ini}" +GITEA_DB="${GITEA_DB:-/var/lib/gitea/gitea.db}" + +ssh_preflight() { + # Returns 0 if SSH to GITEA_SSH_USER@gitea is authenticated from the MOCHAbin; 1 otherwise. + # Prints the SSH banner/error on stdout. + # NOTE: Gitea's builtin SSH server requires the username to match the OS user + # Gitea runs as (here: "gitea"). The conventional "git@" will be rejected + # unless the Gitea daemon also runs as "git". + local out + out=$(ssh "root@${LXC_HOST}" \ + "ssh -p ${GITEA_SSH_PORT} -o ConnectTimeout=5 -o BatchMode=yes \ + -o StrictHostKeyChecking=accept-new \ + ${GITEA_SSH_USER}@${GITEA_HOST} 2>&1 | head -3" || true) + echo "$out" + if echo "$out" | grep -qE "successfully authenticated|Hi there"; then + return 0 + fi + return 1 +} + +enrol_mochabin_root_key() { + # 1. Fetch the MOCHAbin's root pubkey. + local pubkey + pubkey=$(ssh "root@${LXC_HOST}" \ + 'cat /root/.ssh/id_ed25519.pub 2>/dev/null || cat /root/.ssh/id_rsa.pub 2>/dev/null' \ + || true) + if [[ -z "$pubkey" ]]; then + echo "ERROR: no MOCHAbin root pubkey found" >&2 + return 1 + fi + local title + title="metablog-ingest-$(date +%s)" + + # 2. Generate a one-shot access token via Gitea admin CLI inside the LXC. + # Gitea 1.22 output: "Access token was successfully created: " + local token_raw token + token_raw=$(ssh "root@${LXC_HOST}" \ + "lxc-attach -n ${LXC_NAME} -- \ + su gitea -c 'gitea admin user generate-access-token \ + --config ${GITEA_APP_INI} \ + --username ${GITEA_USER} \ + --scopes write:user \ + --token-name ${title} 2>/dev/null'" 2>&1 || true) + # Extract last whitespace-delimited word on any line containing "Access token" + token=$(echo "$token_raw" | awk '/Access token/ {t=$NF} END{print t}') + if [[ -z "$token" ]]; then + echo "ERROR: could not generate Gitea access token for ${GITEA_USER}" >&2 + echo " gitea output: ${token_raw}" >&2 + echo "Manual fallback: add the key via https://${GITEA_HOST}/user/settings/keys" >&2 + echo "Pubkey to add:" >&2 + echo "$pubkey" >&2 + return 1 + fi + + # 3. Resolve LXC loopback IP (avoids public TLS path for the API call). + local lxc_ip + lxc_ip=$(ssh "root@${LXC_HOST}" "lxc-info -n ${LXC_NAME} -iH" | head -1) + if [[ -z "$lxc_ip" ]]; then + echo "ERROR: could not determine LXC IP for ${LXC_NAME}" >&2 + return 1 + fi + + # 4. POST the SSH key to Gitea via the loopback API. + local lxc_resp key_id + lxc_resp=$(ssh "root@${LXC_HOST}" \ + "curl -sS -X POST \ + -H 'Authorization: token ${token}' \ + -H 'Content-Type: application/json' \ + http://${lxc_ip}:3000/api/v1/user/keys \ + -d '{\"title\":\"${title}\",\"key\":\"${pubkey}\"}'" 2>&1) + if ! echo "$lxc_resp" | grep -q '"id"'; then + echo "ERROR: API call to add SSH key failed:" >&2 + echo "$lxc_resp" >&2 + return 1 + fi + key_id=$(echo "$lxc_resp" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) + echo "Key enrolled (id=${key_id}): ${title}" + + # 5. Best-effort: delete the one-shot token directly from the Gitea SQLite DB. + # (The /api/v1/users/{user}/tokens list+delete endpoint requires basic auth + # in Gitea 1.22, not token auth — so we go via sqlite3 on the LXC.) + local del_rc + del_rc=$(ssh "root@${LXC_HOST}" \ + "lxc-attach -n ${LXC_NAME} -- \ + sqlite3 ${GITEA_DB} \ + \"DELETE FROM access_token WHERE name='${title}'; SELECT changes();\"" \ + 2>/dev/null || echo "0") + if [[ "$del_rc" -ge 1 ]]; then + echo "Token ${title} removed from DB." + else + echo "Note: could not remove one-shot token ${title} — remove manually if needed." + fi + return 0 +} + +if [[ "${1:-}" == "--check" ]]; then + if ssh_preflight; then + echo "OK." + exit 0 + else + echo "Preflight failed — run with --enrol to add the MOCHAbin's key to ${GITEA_USER}." + exit 1 + fi +elif [[ "${1:-}" == "--enrol" ]]; then + if ssh_preflight; then + echo "Already authenticated — no enrolment needed." + exit 0 + fi + if ! enrol_mochabin_root_key; then + exit 1 + fi + echo "Re-running preflight..." + if ssh_preflight; then + echo "OK." + else + echo "Preflight still failing — manual intervention required." + exit 1 + fi +elif [[ "${1:-}" == "" ]]; then + # Sourced mode: just define the functions, do nothing. + : +else + echo "Usage: $(basename "$0") [--check | --enrol]" >&2 + exit 2 +fi