Files
Lucas WintherandClaude Opus 5.5 792f7cde48 Add the host bootstrap: deploy user, key, docker access, stack root
bootstrap.yml prepares a host as an admin via -u; new-deploy-key.sh makes the
key pair and refuses to write inside the repo. CI deploys refuse the bootstrap
playbook, since the dispatch API ignores the dropdown.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 15:07:31 +02:00

98 lines
3.2 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Generate the Ansible deploy key pair.
#
# scripts/new-deploy-key.sh # ~/.ssh/deploy_key
# scripts/new-deploy-key.sh ~/.ssh/other # somewhere else
# scripts/new-deploy-key.sh --force # replace an existing key
# scripts/new-deploy-key.sh --print-secret # also print the value for CI
#
# Prints the paths and the public key. It does not print the private key unless
# you ask for it: load-ssh-key.sh keeps key material off stdout and out of logs
# and the process list, and this script holds the same line.
#
# The public key belongs in the deploy user's authorized_keys, which
# ansible/playbooks/bootstrap.yml does for you. The private key belongs in the
# CI secret SSH_PRIVATE_KEY_B64. See docs/bootstrap.md and docs/secrets.md.
set -euo pipefail
umask 077
log() { printf 'new-deploy-key: %s\n' "$*" >&2; }
die() { log "$*"; exit 1; }
force=0
print_secret=0
key_path=""
while [ $# -gt 0 ]; do
case "$1" in
--force) force=1 ;;
--print-secret) print_secret=1 ;;
-h|--help) sed -n '3,9p' "$0" | sed 's/^#[[:space:]]\{0,1\}//'; exit 0 ;;
-*) die "unknown option: $1" ;;
*)
[ -z "$key_path" ] || die "only one key path may be given"
key_path="$1"
;;
esac
shift
done
key_path="${key_path:-$HOME/.ssh/deploy_key}"
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
key_dir="$(dirname "$key_path")"
[ -d "$key_dir" ] || die "no such directory: $key_dir"
key_dir="$(cd "$key_dir" && pwd)"
# gitleaks scans the working tree *and* the full history, and a key committed
# once stays leaked after the file is deleted. Refuse before that can happen.
case "$key_dir/" in
"$repo_root"/*) die "refusing to write a private key inside the repository: $repo_root" ;;
esac
if [ -e "$key_path" ] && [ "$force" -ne 1 ]; then
die "$key_path already exists. Replacing it locks out every host that still
trusts the old key, so pass --force if that is what you want."
fi
# An `if`, not `[ ... ] && rm ...`: an AND-list whose test fails returns
# non-zero, which under `set -e` is a trap this repository has been bitten by
# before -- see the same note in .gitea/workflows/deploy.yml.
if [ "$force" -eq 1 ]; then
rm -f "$key_path" "$key_path.pub"
fi
# ed25519, and no passphrase: nothing in CI or in a playbook run can type one,
# and an encrypted key fails later with an authentication error that says
# nothing about the passphrase.
ssh-keygen -t ed25519 -f "$key_path" -N "" -C "ansible-deploy@$(basename "$repo_root")" >/dev/null
printf 'private key %s\n' "$key_path"
printf 'public key %s\n\n' "$key_path.pub"
cat "$key_path.pub"
cat <<INSTRUCTIONS
Next:
1. Put it on the host, and create everything else it needs:
cd ansible
ansible-playbook playbooks/bootstrap.yml -u <admin-user> -k -K \\
-e deploy_pubkey_file=$key_path.pub
2. Store the private key as the CI secret SSH_PRIVATE_KEY_B64 (where, per
platform, is in docs/secrets.md):
base64 -w0 < $key_path
3. Confirm it works before any deploy: the connectivity workflow on GitHub
or Gitea, the ansible:ping job on GitLab.
INSTRUCTIONS
if [ "$print_secret" -eq 1 ]; then
printf '\nSSH_PRIVATE_KEY_B64:\n'
base64 -w0 < "$key_path"
printf '\n'
fi