#!/bin/bash # ============================================================================================== # ============================= Gitea SSH Setup ================================================ # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Generates an SSH keypair for Gitea authentication and registers the public # key with Gitea via the API. Run after the Gitea container is up — typically # called during onboard setup or when re-keying a server. # # Idempotent: skips key generation if key already exists and skips registration # if the same public key is already registered in Gitea. Use --force to # regenerate and re-register. # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # 1. Resolve Gitea API endpoint # Container running locally → use container IP + GITEA_HTTP_PORT # Container not local → fall back to GITEA_DOMAIN # # 2. Validate GITEA_API_TOKEN is set # Token needed for API calls — create once in Gitea UI (see CONFIGURATION) # # 3. Generate SSH keypair (ed25519) at GITEA_SSH_KEY # Skipped if key already exists — use --force to regenerate # # 4. Check if public key already registered in Gitea # Compares fingerprints — skips POST if already registered # # 5. Register public key via POST /api/v1/user/keys # Title: "unraid-{host_id}" (e.g. unraid-host1) # # 6. Test SSH connection to Gitea # ssh -i GITEA_SSH_KEY -p SSH_PORT git@ # Success confirms key is accepted and auth works end-to-end # # 7. Write ~/.ssh/config Host entry for gitea # Skipped if entry already exists — use --force to overwrite # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # Root Required # ssh-keygen and ~/.ssh/ operations require consistent permissions. # # Single Instance Lock # acquire_lock prevents concurrent runs. # # Token Validated Before Any API Call # If HOST*_GITEA_API_TOKEN is empty, exits with a clear message pointing to # Gitea Settings → Applications → Generate Token (scope: write:user). # # Key Never Clobbered Without --force # Existing key at GITEA_SSH_KEY is left untouched unless --force is passed. # Prevents accidentally invalidating working auth. # # Duplicate Registration Safe # Checks existing keys before POST — will not create duplicate entries in Gitea. # # ============================================================================================== # CONFIGURATION # ============================================================================================== # # host*.conf # # HOST*_GITEA_API_TOKEN # Personal access token for the Gitea user who owns the repository. # Create in Gitea: Settings → Applications → Generate Token → scope: write:user # Aliased by detect_hosts() → GITEA_API_TOKEN. # # master.conf # # GITEA_CONTAINER Container name for local IP detection (default: "Gitea") # GITEA_DOMAIN Domain fallback if container is remote or not running # GITEA_SSH_KEY Path for the SSH keypair (default: /root/.ssh/unraid_gitea) # SSH_PORT Gitea SSH port (default: 221) # GITEA_HTTP_PORT Gitea API port (default: 3000) # # ============================================================================================== # STATE FILES # ============================================================================================== # # ~/.ssh/config Updated with Host entry for gitea on successful setup # GITEA_SSH_KEY Private key (permissions: 600) # GITEA_SSH_KEY.pub Public key — registered with Gitea # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # gitea_ssh_setup.sh # Generate key if missing, register with Gitea, test connection. # # gitea_ssh_setup.sh --force # Regenerate key and re-register even if already set up. # # gitea_ssh_setup.sh --status # Show key state, fingerprint, registration status, SSH test. No changes. # # gitea_ssh_setup.sh --dry-run # Show what would be done without generating, registering, or writing files. # # gitea_ssh_setup.sh --log # Verbose output for each step. # # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPTS_ROOT="$SCRIPT_DIR/.." source "$SCRIPTS_ROOT/load_config.sh" # ── Parse --force before parse_args ─────────────────────────────────────────────────────────── FORCE=false FILTERED_ARGS=() for arg in "$@"; do case "$arg" in --force) FORCE=true ;; *) FILTERED_ARGS+=("$arg") ;; esac done parse_args "${FILTERED_ARGS[@]}" # ============================================================================================== # ━━━ Setup ━━━ # ============================================================================================== if [[ "$EUID" -ne 0 ]]; then error "Must be run as root" exit 1 fi validate_unraid_cmd \ "/usr/local/emhttp/plugins/dynamix/scripts/notify" \ "" "" \ "unRAID notify script" || warn "unRAID notify script not found — native notifications disabled" acquire_lock detect_hosts [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made" [[ "$FORCE" == true ]] && warn "FORCE mode — existing key and registration will be replaced" log "Identity: $MY_ID ($LOCAL_SERVER_NAME)" log "SSH key: $GITEA_SSH_KEY" log "SSH port: $SSH_PORT" # ============================================================================================== # ━━━ Resolve Gitea API Endpoint ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_GEAR Gitea API Endpoint ━━━" GITEA_HOST="" GITEA_API="" # Try local container first CONTAINER_IP=$(docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \ "$GITEA_CONTAINER" 2>/dev/null | awk 'NF{print $1; exit}') if [[ -n "$CONTAINER_IP" ]]; then GITEA_HOST="$CONTAINER_IP" GITEA_API="http://${CONTAINER_IP}:${GITEA_HTTP_PORT}/api/v1" log "Gitea container running locally: $CONTAINER_IP:$GITEA_HTTP_PORT" elif [[ -n "$GITEA_DOMAIN" ]]; then GITEA_HOST="$GITEA_DOMAIN" GITEA_API="https://${GITEA_DOMAIN}/api/v1" warn "Gitea container not running locally — using domain: $GITEA_DOMAIN" else error "Cannot resolve Gitea API endpoint" error "Gitea container ($GITEA_CONTAINER) is not running and GITEA_DOMAIN is not set in master.conf" error "Start the Gitea container first, or set GITEA_DOMAIN" exit 1 fi log "API endpoint: $GITEA_API" # ============================================================================================== # ━━━ Validate API Token ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_SHIELD API Token ━━━" # Alias: detect_hosts() maps HOST*_GITEA_API_TOKEN → GITEA_API_TOKEN if [[ -z "${GITEA_API_TOKEN:-}" ]]; then error "HOST${MY_ID#HOST}_GITEA_API_TOKEN is not set in host${MY_ID#HOST}.conf" error "" error "Create a token in Gitea:" error " 1. Log into Gitea" error " 2. Top-right profile → Settings → Applications" error " 3. Generate New Token → name: unraid-setup → scope: write:user" error " 4. Copy the token (shown once) → paste into HOST${MY_ID#HOST}_GITEA_API_TOKEN in host${MY_ID#HOST}.conf" exit 1 fi # Test token against the API HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: token $GITEA_API_TOKEN" \ "$GITEA_API/user" 2>/dev/null) if [[ "$HTTP_CODE" == "200" ]]; then log "API token valid ✅" elif [[ "$HTTP_CODE" == "401" ]]; then error "API token rejected (HTTP 401) — token may be expired or have wrong scope" error "Regenerate the token in Gitea: Settings → Applications → Generate Token → scope: write:user" exit 1 else error "Gitea API not reachable (HTTP $HTTP_CODE) at $GITEA_API" error "Check that the Gitea container is healthy and the port is correct (GITEA_HTTP_PORT=$GITEA_HTTP_PORT)" exit 1 fi # ── Status mode exits here after full state display ─────────────────────────────────────────── if [[ "$SHOW_STATUS" == true ]]; then echo "" echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_GEAR API endpoint: $GITEA_API" echo "" if [[ -f "${GITEA_SSH_KEY}.pub" ]]; then FINGERPRINT=$(ssh-keygen -lf "${GITEA_SSH_KEY}.pub" 2>/dev/null | awk '{print $2}') echo " $ICON_GEAR Key file: ${GITEA_SSH_KEY}.pub" echo " $ICON_GEAR Fingerprint: $FINGERPRINT" # Check registration REGISTERED_KEYS=$(curl -s \ -H "Authorization: token $GITEA_API_TOKEN" \ "$GITEA_API/user/keys" 2>/dev/null) LOCAL_PUB=$(awk '{print $1" "$2}' "${GITEA_SSH_KEY}.pub") if echo "$REGISTERED_KEYS" | grep -qF "$LOCAL_PUB"; then echo " $ICON_GEAR Registered: YES ✅" else echo " $ICON_GEAR Registered: NO — run without --status to register" fi else echo " $ICON_GEAR Key file: NOT FOUND at $GITEA_SSH_KEY" fi # SSH connection test echo "" SSH_OUTPUT=$(ssh -i "$GITEA_SSH_KEY" -p "$SSH_PORT" \ -o StrictHostKeyChecking=no -o ConnectTimeout=5 \ git@"$GITEA_HOST" 2>&1 || true) if echo "$SSH_OUTPUT" | grep -q "successfully authenticated"; then echo " $ICON_DONE SSH test: connected ✅" echo " $ICON_INFO $(echo "$SSH_OUTPUT" | grep "successfully authenticated")" else echo " $ICON_ERROR SSH test: failed" echo " $ICON_INFO $SSH_OUTPUT" fi echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi START=$(date +%s) SETUP_SUCCESS=true # ============================================================================================== # ━━━ Generate SSH Keypair ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_GEAR SSH Keypair ━━━" KEY_TITLE="unraid-$(echo "$MY_ID" | tr '[:upper:]' '[:lower:]')" KEY_COMMENT="$KEY_TITLE@$(hostname)" if [[ -f "$GITEA_SSH_KEY" ]] && [[ "$FORCE" == false ]]; then FINGERPRINT=$(ssh-keygen -lf "${GITEA_SSH_KEY}.pub" 2>/dev/null | awk '{print $2}') log "Key already exists — skipping generation (use --force to regenerate)" log " Path: $GITEA_SSH_KEY" log " Fingerprint: $FINGERPRINT" else if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would generate ed25519 key at $GITEA_SSH_KEY" warn "DRY RUN — key comment: $KEY_COMMENT" else mkdir -p "$(dirname "$GITEA_SSH_KEY")" chmod 700 "$(dirname "$GITEA_SSH_KEY")" # Remove existing if --force [[ "$FORCE" == true ]] && rm -f "$GITEA_SSH_KEY" "${GITEA_SSH_KEY}.pub" if ssh-keygen -t ed25519 -f "$GITEA_SSH_KEY" -C "$KEY_COMMENT" -N "" -q; then chmod 600 "$GITEA_SSH_KEY" chmod 644 "${GITEA_SSH_KEY}.pub" FINGERPRINT=$(ssh-keygen -lf "${GITEA_SSH_KEY}.pub" 2>/dev/null | awk '{print $2}') warn "SSH key generated ✅" log " Path: $GITEA_SSH_KEY" log " Fingerprint: $FINGERPRINT" else error "Failed to generate SSH key" SETUP_SUCCESS=false fi fi fi # ============================================================================================== # ━━━ Register Public Key with Gitea ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_LINK Register Key with Gitea ━━━" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would POST public key to $GITEA_API/user/keys" warn "DRY RUN — key title: $KEY_TITLE" elif [[ "$SETUP_SUCCESS" == true ]]; then LOCAL_PUB=$(cat "${GITEA_SSH_KEY}.pub") LOCAL_PUB_SHORT=$(awk '{print $1" "$2}' "${GITEA_SSH_KEY}.pub") # Check if already registered REGISTERED_KEYS=$(curl -s \ -H "Authorization: token $GITEA_API_TOKEN" \ "$GITEA_API/user/keys" 2>/dev/null) if [[ "$FORCE" == false ]] && echo "$REGISTERED_KEYS" | grep -qF "$LOCAL_PUB_SHORT"; then log "Public key already registered in Gitea — skipping (use --force to re-register)" else # If --force, delete the existing key with this title first if [[ "$FORCE" == true ]]; then EXISTING_ID=$(echo "$REGISTERED_KEYS" | \ python3 -c "import json,sys; keys=json.load(sys.stdin); [print(k['id']) for k in keys if k.get('title','')=='$KEY_TITLE']" 2>/dev/null) if [[ -n "$EXISTING_ID" ]]; then curl -s -X DELETE \ -H "Authorization: token $GITEA_API_TOKEN" \ "$GITEA_API/user/keys/$EXISTING_ID" >/dev/null 2>&1 log "Removed existing key '$KEY_TITLE' (ID: $EXISTING_ID)" fi fi RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ -H "Authorization: token $GITEA_API_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"key\":$(echo "$LOCAL_PUB" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().strip()))'),\"read_only\":false,\"title\":\"$KEY_TITLE\"}" \ "$GITEA_API/user/keys" 2>/dev/null) HTTP_CODE=$(echo "$RESPONSE" | tail -1) BODY=$(echo "$RESPONSE" | head -n -1) if [[ "$HTTP_CODE" == "201" ]]; then KEY_ID=$(echo "$BODY" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])" 2>/dev/null) warn "Public key registered in Gitea ✅ (ID: $KEY_ID, title: $KEY_TITLE)" elif [[ "$HTTP_CODE" == "422" ]]; then warn "Key already registered under a different title — skipping" log "Response: $BODY" else error "Failed to register key (HTTP $HTTP_CODE)" log "Response: $BODY" SETUP_SUCCESS=false fi fi fi # ============================================================================================== # ━━━ SSH Connection Test ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_GEAR SSH Connection Test ━━━" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would test: ssh -i $GITEA_SSH_KEY -p $SSH_PORT git@$GITEA_HOST" else SSH_OUTPUT=$(ssh -i "$GITEA_SSH_KEY" -p "$SSH_PORT" \ -o StrictHostKeyChecking=no \ -o ConnectTimeout=10 \ git@"$GITEA_HOST" 2>&1 || true) if echo "$SSH_OUTPUT" | grep -q "successfully authenticated"; then AUTHED_AS=$(echo "$SSH_OUTPUT" | grep "successfully authenticated" | sed 's/.*authenticated as //') warn "SSH connection successful ✅ — authenticated as $AUTHED_AS" else error "SSH connection test failed" log "Output: $SSH_OUTPUT" warn "Key may not yet be active — Gitea sometimes takes a moment to pick up new keys" warn "Run --status in 30 seconds to recheck" SETUP_SUCCESS=false fi fi # ============================================================================================== # ━━━ SSH Config Entry ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_GEAR SSH Config Entry ━━━" SSH_CONFIG="$HOME/.ssh/config" ENTRY_MARKER="Host gitea-${MY_ID,,}" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would add Host entry to $SSH_CONFIG:" warn " Host gitea-${MY_ID,,}" warn " HostName $GITEA_HOST" warn " Port $SSH_PORT" warn " User git" warn " IdentityFile $GITEA_SSH_KEY" elif grep -qF "$ENTRY_MARKER" "$SSH_CONFIG" 2>/dev/null && [[ "$FORCE" == false ]]; then log "SSH config entry already exists — skipping (use --force to update)" else if [[ "$FORCE" == true ]]; then # Remove old entry block if present if grep -qF "$ENTRY_MARKER" "$SSH_CONFIG" 2>/dev/null; then python3 - "$SSH_CONFIG" "$ENTRY_MARKER" <<'PYEOF' import sys path, marker = sys.argv[1], sys.argv[2] with open(path) as f: lines = f.readlines() out, skip = [], False for line in lines: if line.strip() == marker: skip = True elif skip and line.startswith('Host '): skip = False if not skip: out.append(line) with open(path, 'w') as f: f.writelines(out) PYEOF log "Removed old SSH config entry" fi fi mkdir -p "$(dirname "$SSH_CONFIG")" { echo "" echo "Host gitea-${MY_ID,,}" echo " HostName $GITEA_HOST" echo " Port $SSH_PORT" echo " User git" echo " IdentityFile $GITEA_SSH_KEY" } >> "$SSH_CONFIG" chmod 600 "$SSH_CONFIG" log "SSH config entry added: Host gitea-${MY_ID,,} ✅" fi END=$(date +%s) # ============================================================================================== # ━━━ Summary ━━━ # ============================================================================================== echo "" echo "━━━━━ $ICON_SUMMARY GITEA SSH SETUP SUMMARY ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_GEAR Key: $GITEA_SSH_KEY" echo "$ICON_LINK Gitea: $GITEA_HOST:$SSH_PORT" echo "$ICON_GEAR Key title: $KEY_TITLE" echo "$ICON_TIME Duration: $(format_duration $(( END - START )))" echo "" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — no changes made" elif [[ "$SETUP_SUCCESS" == true ]]; then echo "$ICON_DONE Status: done ✅" echo "" echo "Git remote push:" echo " GIT_SSH_COMMAND='ssh -i $GITEA_SSH_KEY -p $SSH_PORT' git push" else echo "$ICON_ERROR Status: SETUP HAD ERRORS — check output above" notify "Gitea SSH setup errors on $(hostname) ($MY_ID)" \ "Gitea SSH Setup" "warning" exit 1 fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"