Rotate operator secrets into agenix and deepen caches
Some checks failed
Build Rust / Cargo Test (push) Waiting to run
Build Site / Next.js Build (push) Waiting to run
Build Apple / Build App (iOS Simulator) (push) Failing after 52s
Build Apple / Build App (macOS) (push) Failing after 1m1s

This commit is contained in:
Conrad Kramer 2026-03-19 00:28:18 -07:00
parent 7039bf5aad
commit 03415e579b
28 changed files with 526 additions and 126 deletions

View file

@ -75,14 +75,18 @@ jobs:
cache_root="${NSC_CACHE_PATH:-${HOME}/.cache/burrow}" cache_root="${NSC_CACHE_PATH:-${HOME}/.cache/burrow}"
mkdir -p \ mkdir -p \
"${cache_root}/cargo" \ "${cache_root}/cargo" \
"${cache_root}/cargo-target/${{ matrix.cache-id }}" \
"${cache_root}/rustup" \ "${cache_root}/rustup" \
"${cache_root}/sccache" \ "${cache_root}/sccache" \
"${cache_root}/homebrew" \
"${cache_root}/apple/PackageCache" \ "${cache_root}/apple/PackageCache" \
"${cache_root}/apple/SourcePackages" \ "${cache_root}/apple/SourcePackages" \
"${cache_root}/apple/DerivedData/${{ matrix.cache-id }}" "${cache_root}/apple/DerivedData/${{ matrix.cache-id }}"
echo "CARGO_HOME=${cache_root}/cargo" >> "${GITHUB_ENV}" echo "CARGO_HOME=${cache_root}/cargo" >> "${GITHUB_ENV}"
echo "CARGO_TARGET_DIR=${cache_root}/cargo-target/${{ matrix.cache-id }}" >> "${GITHUB_ENV}"
echo "RUSTUP_HOME=${cache_root}/rustup" >> "${GITHUB_ENV}" echo "RUSTUP_HOME=${cache_root}/rustup" >> "${GITHUB_ENV}"
echo "SCCACHE_DIR=${cache_root}/sccache" >> "${GITHUB_ENV}" echo "SCCACHE_DIR=${cache_root}/sccache" >> "${GITHUB_ENV}"
echo "HOMEBREW_CACHE=${cache_root}/homebrew" >> "${GITHUB_ENV}"
echo "APPLE_PACKAGE_CACHE=${cache_root}/apple/PackageCache" >> "${GITHUB_ENV}" echo "APPLE_PACKAGE_CACHE=${cache_root}/apple/PackageCache" >> "${GITHUB_ENV}"
echo "APPLE_SOURCE_PACKAGES=${cache_root}/apple/SourcePackages" >> "${GITHUB_ENV}" echo "APPLE_SOURCE_PACKAGES=${cache_root}/apple/SourcePackages" >> "${GITHUB_ENV}"
echo "APPLE_DERIVED_DATA=${cache_root}/apple/DerivedData/${{ matrix.cache-id }}" >> "${GITHUB_ENV}" echo "APPLE_DERIVED_DATA=${cache_root}/apple/DerivedData/${{ matrix.cache-id }}" >> "${GITHUB_ENV}"

View file

@ -33,9 +33,10 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
cache_root="${HOME}/.cache/burrow" cache_root="${HOME}/.cache/burrow"
mkdir -p "${cache_root}/cargo" "${cache_root}/sccache" mkdir -p "${cache_root}/cargo" "${cache_root}/sccache" "${cache_root}/cargo-target/build-rust"
echo "CARGO_HOME=${cache_root}/cargo" >> "${GITHUB_ENV}" echo "CARGO_HOME=${cache_root}/cargo" >> "${GITHUB_ENV}"
echo "SCCACHE_DIR=${cache_root}/sccache" >> "${GITHUB_ENV}" echo "SCCACHE_DIR=${cache_root}/sccache" >> "${GITHUB_ENV}"
echo "CARGO_TARGET_DIR=${cache_root}/cargo-target/build-rust" >> "${GITHUB_ENV}"
- name: Test - name: Test
shell: bash shell: bash

1
.gitignore vendored
View file

@ -12,6 +12,7 @@ target/
.idea/ .idea/
tmp/ tmp/
intake/
*.db *.db
*.sqlite3 *.sqlite3

View file

@ -74,16 +74,17 @@ CARGO_PATH="$(dirname $PROTOC):$CARGO_PATH"
# Run cargo without the various environment variables set by Xcode. # Run cargo without the various environment variables set by Xcode.
# Those variables can confuse cargo and the build scripts it runs. # Those variables can confuse cargo and the build scripts it runs.
EXTRA_ENV=() EXTRA_ENV=()
for VAR_NAME in HOME CARGO_HOME RUSTUP_HOME RUSTC_WRAPPER SCCACHE_DIR CARGO_INCREMENTAL; do for VAR_NAME in HOME CARGO_HOME CARGO_TARGET_DIR RUSTUP_HOME RUSTC_WRAPPER SCCACHE_DIR CARGO_INCREMENTAL; do
if [[ -n "${!VAR_NAME:-}" ]]; then if [[ -n "${!VAR_NAME:-}" ]]; then
EXTRA_ENV+=("${VAR_NAME}=${!VAR_NAME}") EXTRA_ENV+=("${VAR_NAME}=${!VAR_NAME}")
fi fi
done done
env -i PATH="$CARGO_PATH" PROTOC="$PROTOC" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" IPHONEOS_DEPLOYMENT_TARGET="$IPHONEOS_DEPLOYMENT_TARGET" MACOSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" "${EXTRA_ENV[@]}" cargo build "${CARGO_ARGS[@]}" EFFECTIVE_CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-${CONFIGURATION_TEMP_DIR}/target}"
env -i PATH="$CARGO_PATH" PROTOC="$PROTOC" CARGO_TARGET_DIR="${EFFECTIVE_CARGO_TARGET_DIR}" IPHONEOS_DEPLOYMENT_TARGET="$IPHONEOS_DEPLOYMENT_TARGET" MACOSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" "${EXTRA_ENV[@]}" cargo build "${CARGO_ARGS[@]}"
mkdir -p "${BUILT_PRODUCTS_DIR}" mkdir -p "${BUILT_PRODUCTS_DIR}"
# Use `lipo` to merge the architectures together into BUILT_PRODUCTS_DIR # Use `lipo` to merge the architectures together into BUILT_PRODUCTS_DIR
/usr/bin/xcrun --sdk $PLATFORM_NAME lipo \ /usr/bin/xcrun --sdk $PLATFORM_NAME lipo \
-create $(printf "${CONFIGURATION_TEMP_DIR}/target/%q/${CARGO_TARGET_SUBDIR}/libburrow.a " "${RUST_TARGETS[@]}") \ -create $(printf "${EFFECTIVE_CARGO_TARGET_DIR}/%q/${CARGO_TARGET_SUBDIR}/libburrow.a " "${RUST_TARGETS[@]}") \
-output "${BUILT_PRODUCTS_DIR}/libburrow.a" -output "${BUILT_PRODUCTS_DIR}/libburrow.a"

View file

@ -5,7 +5,12 @@ SECRETS := forgejo/admin-password \
forgejo/agent-ssh-key \ forgejo/agent-ssh-key \
forgejo/nsc-token \ forgejo/nsc-token \
forgejo/nsc-dispatcher-config \ forgejo/nsc-dispatcher-config \
forgejo/nsc-autoscaler-config forgejo/nsc-autoscaler-config \
cloudflare/api-token \
hetzner/api-token \
forwardemail/api-token \
forwardemail/hetzner-s3-user \
forwardemail/hetzner-s3-secret
tun := $(shell ifconfig -l | sed 's/ /\n/g' | grep utun | tail -n 1) tun := $(shell ifconfig -l | sed 's/ /\n/g' | grep utun | tail -n 1)
cargo_console := env RUST_BACKTRACE=1 RUST_LOG=debug RUSTFLAGS='--cfg tokio_unstable' cargo run --all-features -- cargo_console := env RUST_BACKTRACE=1 RUST_LOG=debug RUSTFLAGS='--cfg tokio_unstable' cargo run --all-features --

View file

@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -euo pipefail
BURROW_SECRET_TMPFILES=()
burrow_cleanup_secret_tmpfiles() {
local path
for path in "${BURROW_SECRET_TMPFILES[@]:-}"; do
[[ -n "${path}" ]] && rm -f "${path}" >/dev/null 2>&1 || true
done
BURROW_SECRET_TMPFILES=()
}
burrow_decrypt_age_secret_to_temp() {
local repo_root="$1"
local secret_path="$2"
local tmp_file
if [[ ! -f "${secret_path}" ]]; then
echo "age secret not found: ${secret_path}" >&2
return 1
fi
tmp_file="$(mktemp "${TMPDIR:-/tmp}/burrow-secret.XXXXXX")"
nix --extra-experimental-features "nix-command flakes" run "${repo_root}#agenix" -- -d "${secret_path}" > "${tmp_file}"
chmod 600 "${tmp_file}"
BURROW_SECRET_TMPFILES+=("${tmp_file}")
printf '%s\n' "${tmp_file}"
}
burrow_resolve_secret_file() {
local repo_root="$1"
local explicit_path="$2"
local intake_path="$3"
local age_path="$4"
local fallback_path="${5:-}"
if [[ -n "${explicit_path}" ]]; then
if [[ ! -s "${explicit_path}" ]]; then
echo "required file missing or empty: ${explicit_path}" >&2
return 1
fi
printf '%s\n' "${explicit_path}"
return 0
fi
if [[ -n "${intake_path}" && -s "${intake_path}" ]]; then
printf '%s\n' "${intake_path}"
return 0
fi
if [[ -n "${age_path}" && -f "${age_path}" ]]; then
burrow_decrypt_age_secret_to_temp "${repo_root}" "${age_path}"
return 0
fi
if [[ -n "${fallback_path}" && -s "${fallback_path}" ]]; then
printf '%s\n' "${fallback_path}"
return 0
fi
return 1
}
burrow_encrypt_secret_from_file() {
local repo_root="$1"
local secret_path="$2"
local source_path="$3"
if [[ ! -s "${source_path}" ]]; then
echo "secret source missing or empty: ${source_path}" >&2
return 1
fi
SECRET_SOURCE_FILE="${source_path}" \
EDITOR="${repo_root}/Scripts/agenix-load-file.sh" \
nix --extra-experimental-features "nix-command flakes" run "${repo_root}#agenix" -- -e "${secret_path}"
}

View file

@ -3,6 +3,8 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${SCRIPT_DIR}/_burrow-secrets.sh"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
@ -10,27 +12,33 @@ Usage: Scripts/bootstrap-forge-intake.sh [options]
Copy the minimum Burrow forge bootstrap secrets onto the target host under Copy the minimum Burrow forge bootstrap secrets onto the target host under
/var/lib/burrow/intake with the ownership expected by the NixOS services. /var/lib/burrow/intake with the ownership expected by the NixOS services.
Legacy path only: the current forge runtime consumes agenix secrets directly.
Options: Options:
--host <user@host> SSH target (default: root@git.burrow.net) --host <user@host> SSH target (default: root@git.burrow.net)
--ssh-key <path> SSH private key used to reach the host --ssh-key <path> SSH private key used to reach the host
(default: intake/agent_at_burrow_net_ed25519) (default: secrets/forgejo/agent-ssh-key.age, then intake/)
--password-file <path> Forgejo admin bootstrap password file --password-file <path> Forgejo admin bootstrap password file
(default: intake/forgejo_pass_contact_at_burrow_net.txt) (default: secrets/forgejo/admin-password.age, then intake/)
--agent-key-file <path> Agent SSH private key copied for runner bootstrap --agent-key-file <path> Agent SSH private key copied for runner bootstrap
(default: intake/agent_at_burrow_net_ed25519) (default: secrets/forgejo/agent-ssh-key.age, then intake/)
--no-verify Skip remote ls/stat verification after install --no-verify Skip remote ls/stat verification after install
-h, --help Show this help text -h, --help Show this help text
EOF EOF
} }
HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}" HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}"
SSH_KEY="${BURROW_FORGE_SSH_KEY:-${REPO_ROOT}/intake/agent_at_burrow_net_ed25519}" SSH_KEY="${BURROW_FORGE_SSH_KEY:-}"
PASSWORD_FILE="${BURROW_FORGE_PASSWORD_FILE:-${REPO_ROOT}/intake/forgejo_pass_contact_at_burrow_net.txt}" PASSWORD_FILE="${BURROW_FORGE_PASSWORD_FILE:-}"
AGENT_KEY_FILE="${BURROW_FORGE_AGENT_KEY_FILE:-${REPO_ROOT}/intake/agent_at_burrow_net_ed25519}" AGENT_KEY_FILE="${BURROW_FORGE_AGENT_KEY_FILE:-}"
KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}" KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}"
VERIFY=1 VERIFY=1
cleanup() {
burrow_cleanup_secret_tmpfiles
}
trap cleanup EXIT
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--host) --host)
@ -67,12 +75,29 @@ done
mkdir -p "$(dirname "${KNOWN_HOSTS_FILE}")" mkdir -p "$(dirname "${KNOWN_HOSTS_FILE}")"
for path in "${SSH_KEY}" "${PASSWORD_FILE}" "${AGENT_KEY_FILE}"; do SSH_KEY="$(
if [[ ! -s "${path}" ]]; then burrow_resolve_secret_file \
echo "required file missing or empty: ${path}" >&2 "${REPO_ROOT}" \
exit 1 "${SSH_KEY}" \
fi "${REPO_ROOT}/intake/agent_at_burrow_net_ed25519" \
done "${REPO_ROOT}/secrets/forgejo/agent-ssh-key.age" \
"${HOME}/.ssh/agent_at_burrow_net_ed25519"
)"
PASSWORD_FILE="$(
burrow_resolve_secret_file \
"${REPO_ROOT}" \
"${PASSWORD_FILE}" \
"${REPO_ROOT}/intake/forgejo_pass_contact_at_burrow_net.txt" \
"${REPO_ROOT}/secrets/forgejo/admin-password.age"
)"
AGENT_KEY_FILE="$(
burrow_resolve_secret_file \
"${REPO_ROOT}" \
"${AGENT_KEY_FILE}" \
"${REPO_ROOT}/intake/agent_at_burrow_net_ed25519" \
"${REPO_ROOT}/secrets/forgejo/agent-ssh-key.age" \
"${HOME}/.ssh/agent_at_burrow_net_ed25519"
)"
ssh_opts=( ssh_opts=(
-i "${SSH_KEY}" -i "${SSH_KEY}"

View file

@ -3,6 +3,8 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${SCRIPT_DIR}/_burrow-secrets.sh"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
@ -12,17 +14,22 @@ Run a post-boot verification pass against the Burrow forge host.
Options: Options:
--host <user@host> SSH target (default: root@git.burrow.net) --host <user@host> SSH target (default: root@git.burrow.net)
--ssh-key <path> SSH private key (default: intake/agent_at_burrow_net_ed25519) --ssh-key <path> SSH private key (default: secrets/forgejo/agent-ssh-key.age, then intake/)
--expect-nsc Fail if forgejo-nsc services are not active --expect-nsc Fail if forgejo-nsc services are not active
-h, --help Show this help text -h, --help Show this help text
EOF EOF
} }
HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}" HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}"
SSH_KEY="${BURROW_FORGE_SSH_KEY:-${REPO_ROOT}/intake/agent_at_burrow_net_ed25519}" SSH_KEY="${BURROW_FORGE_SSH_KEY:-}"
KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}" KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}"
EXPECT_NSC=0 EXPECT_NSC=0
cleanup() {
burrow_cleanup_secret_tmpfiles
}
trap cleanup EXIT
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--host) --host)
@ -51,10 +58,17 @@ done
mkdir -p "$(dirname "${KNOWN_HOSTS_FILE}")" mkdir -p "$(dirname "${KNOWN_HOSTS_FILE}")"
if [[ ! -f "${SSH_KEY}" ]]; then SSH_KEY="$(
echo "forge SSH key not found: ${SSH_KEY}" >&2 burrow_resolve_secret_file \
"${REPO_ROOT}" \
"${SSH_KEY}" \
"${REPO_ROOT}/intake/agent_at_burrow_net_ed25519" \
"${REPO_ROOT}/secrets/forgejo/agent-ssh-key.age" \
"${HOME}/.ssh/agent_at_burrow_net_ed25519"
)" || {
echo "forge SSH key could not be resolved" >&2
exit 1 exit 1
fi }
ssh \ ssh \
-i "${SSH_KEY}" \ -i "${SSH_KEY}" \

View file

@ -1,6 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${SCRIPT_DIR}/_burrow-secrets.sh"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: Scripts/cloudflare-upsert-a-record.sh --zone <zone> --name <fqdn> --ipv4 <address> [options] Usage: Scripts/cloudflare-upsert-a-record.sh --zone <zone> --name <fqdn> --ipv4 <address> [options]
@ -13,7 +18,7 @@ Options:
--name <fqdn> Fully-qualified DNS record name --name <fqdn> Fully-qualified DNS record name
--ipv4 <address> IPv4 address for the A record --ipv4 <address> IPv4 address for the A record
--token-file <path> Cloudflare API token file --token-file <path> Cloudflare API token file
default: intake/cloudflare-token.txt default: secrets/cloudflare/api-token.age, then intake/cloudflare-token.txt
--ttl <seconds|auto> Record TTL, or auto --ttl <seconds|auto> Record TTL, or auto
default: auto default: auto
--proxied <true|false> Whether to proxy through Cloudflare --proxied <true|false> Whether to proxy through Cloudflare
@ -25,10 +30,15 @@ EOF
ZONE_NAME="" ZONE_NAME=""
RECORD_NAME="" RECORD_NAME=""
IPV4="" IPV4=""
TOKEN_FILE="intake/cloudflare-token.txt" TOKEN_FILE="${CLOUDFLARE_TOKEN_FILE:-}"
TTL_VALUE="auto" TTL_VALUE="auto"
PROXIED="false" PROXIED="false"
cleanup() {
burrow_cleanup_secret_tmpfiles
}
trap cleanup EXIT
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--zone) --zone)
@ -71,11 +81,16 @@ if [[ -z "${ZONE_NAME}" || -z "${RECORD_NAME}" || -z "${IPV4}" ]]; then
usage >&2 usage >&2
exit 2 exit 2
fi fi
TOKEN_FILE="$(
if [[ ! -f "${TOKEN_FILE}" ]]; then burrow_resolve_secret_file \
echo "Cloudflare token file not found: ${TOKEN_FILE}" >&2 "${REPO_ROOT}" \
"${TOKEN_FILE}" \
"${REPO_ROOT}/intake/cloudflare-token.txt" \
"${REPO_ROOT}/secrets/cloudflare/api-token.age"
)" || {
echo "Cloudflare token file could not be resolved" >&2
exit 1 exit 1
fi }
if [[ ! "${IPV4}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then if [[ ! "${IPV4}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
echo "Invalid IPv4 address: ${IPV4}" >&2 echo "Invalid IPv4 address: ${IPV4}" >&2

View file

@ -5,6 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=Scripts/_burrow-flake.sh # shellcheck source=Scripts/_burrow-flake.sh
source "${SCRIPT_DIR}/_burrow-flake.sh" source "${SCRIPT_DIR}/_burrow-flake.sh"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${SCRIPT_DIR}/_burrow-secrets.sh"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
@ -18,7 +20,7 @@ Defaults:
Environment: Environment:
BURROW_FORGE_HOST root@git.burrow.net BURROW_FORGE_HOST root@git.burrow.net
BURROW_FORGE_SSH_KEY intake/agent_at_burrow_net_ed25519 BURROW_FORGE_SSH_KEY explicit path, otherwise secrets/forgejo/agent-ssh-key.age
EOF EOF
} }
@ -28,6 +30,7 @@ ALLOW_DIRTY=0
BURROW_FLAKE_TMPDIRS=() BURROW_FLAKE_TMPDIRS=()
cleanup() { cleanup() {
burrow_cleanup_secret_tmpfiles
burrow_cleanup_flake_tmpdirs burrow_cleanup_flake_tmpdirs
} }
trap cleanup EXIT trap cleanup EXIT
@ -71,21 +74,17 @@ if [[ ${ALLOW_DIRTY} -ne 1 ]] && [[ -n "$(git status --short)" ]]; then
fi fi
FORGE_HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}" FORGE_HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}"
FORGE_SSH_KEY="${BURROW_FORGE_SSH_KEY:-}" FORGE_SSH_KEY="$(
burrow_resolve_secret_file \
if [[ -z "${FORGE_SSH_KEY}" ]]; then "${REPO_ROOT}" \
if [[ -f "${REPO_ROOT}/intake/agent_at_burrow_net_ed25519" ]]; then "${BURROW_FORGE_SSH_KEY:-}" \
FORGE_SSH_KEY="${REPO_ROOT}/intake/agent_at_burrow_net_ed25519" "${REPO_ROOT}/intake/agent_at_burrow_net_ed25519" \
else "${REPO_ROOT}/secrets/forgejo/agent-ssh-key.age" \
FORGE_SSH_KEY="${HOME}/.ssh/agent_at_burrow_net_ed25519" "${HOME}/.ssh/agent_at_burrow_net_ed25519"
fi )" || {
fi echo "Unable to resolve the forge SSH key." >&2
if [[ ! -f "${FORGE_SSH_KEY}" ]]; then
echo "Forge SSH key not found at ${FORGE_SSH_KEY}." >&2
echo "Set BURROW_FORGE_SSH_KEY or place the agent key in intake/." >&2
exit 1 exit 1
fi }
FORGE_KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}" FORGE_KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}"
mkdir -p "$(dirname "${FORGE_KNOWN_HOSTS_FILE}")" mkdir -p "$(dirname "${FORGE_KNOWN_HOSTS_FILE}")"

View file

@ -6,12 +6,14 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=Scripts/_burrow-flake.sh # shellcheck source=Scripts/_burrow-flake.sh
source "${SCRIPT_DIR}/_burrow-flake.sh" source "${SCRIPT_DIR}/_burrow-flake.sh"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${SCRIPT_DIR}/_burrow-secrets.sh"
DEFAULT_CONFIG="burrow-forge" DEFAULT_CONFIG="burrow-forge"
DEFAULT_FLAKE="." DEFAULT_FLAKE="."
DEFAULT_LOCATION="hel1" DEFAULT_LOCATION="hel1"
DEFAULT_ARCHITECTURE="x86" DEFAULT_ARCHITECTURE="x86"
DEFAULT_TOKEN_FILE="${REPO_ROOT}/intake/hetzner-api-token.txt" DEFAULT_TOKEN_FILE=""
CONFIG="${HCLOUD_IMAGE_CONFIG:-${DEFAULT_CONFIG}}" CONFIG="${HCLOUD_IMAGE_CONFIG:-${DEFAULT_CONFIG}}"
FLAKE="${HCLOUD_IMAGE_FLAKE:-${DEFAULT_FLAKE}}" FLAKE="${HCLOUD_IMAGE_FLAKE:-${DEFAULT_FLAKE}}"
@ -30,6 +32,13 @@ NIX_BUILD_FLAGS=()
BURROW_FLAKE_TMPDIRS=() BURROW_FLAKE_TMPDIRS=()
LOCAL_STORE_DIR="" LOCAL_STORE_DIR=""
cleanup() {
burrow_cleanup_secret_tmpfiles
burrow_cleanup_flake_tmpdirs
}
trap cleanup EXIT
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: Scripts/hcloud-upload-nixos-image.sh [options] Usage: Scripts/hcloud-upload-nixos-image.sh [options]
@ -42,7 +51,7 @@ Options:
--location <code> Hetzner location for the temporary upload server (default: hel1) --location <code> Hetzner location for the temporary upload server (default: hel1)
--architecture <x86|arm> CPU architecture of the image (default: x86) --architecture <x86|arm> CPU architecture of the image (default: x86)
--server-type <name> Hetzner server type for the temporary upload server --server-type <name> Hetzner server type for the temporary upload server
--token-file <path> Hetzner API token file (default: intake/hetzner-api-token.txt) --token-file <path> Hetzner API token file (default: secrets/hetzner/api-token.age, then intake/hetzner-api-token.txt)
--artifact-path <path> Prebuilt raw image artifact to upload directly --artifact-path <path> Prebuilt raw image artifact to upload directly
--output-hash <hash> Stable hash label for --artifact-path uploads --output-hash <hash> Stable hash label for --artifact-path uploads
--builder-spec <string> Complete builders string passed to nix build --builder-spec <string> Complete builders string passed to nix build
@ -125,6 +134,17 @@ while [[ $# -gt 0 ]]; do
esac esac
done done
TOKEN_FILE="$(
burrow_resolve_secret_file \
"${REPO_ROOT}" \
"${TOKEN_FILE}" \
"${REPO_ROOT}/intake/hetzner-api-token.txt" \
"${REPO_ROOT}/secrets/hetzner/api-token.age"
)" || {
echo "Hetzner API token file could not be resolved" >&2
exit 1
}
cleanup() { cleanup() {
burrow_cleanup_flake_tmpdirs burrow_cleanup_flake_tmpdirs
if [[ -n "${LOCAL_STORE_DIR}" && -d "${LOCAL_STORE_DIR}" ]]; then if [[ -n "${LOCAL_STORE_DIR}" && -d "${LOCAL_STORE_DIR}" ]]; then

View file

@ -2,6 +2,9 @@
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${SCRIPT_DIR}/_burrow-secrets.sh"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
@ -31,7 +34,7 @@ Options:
-h, --help Show this help text. -h, --help Show this help text.
Environment: Environment:
HCLOUD_TOKEN_FILE Defaults to intake/hetzner-api-token.txt HCLOUD_TOKEN_FILE Defaults to secrets/hetzner/api-token.age, then intake/hetzner-api-token.txt
EOF EOF
} }
@ -43,10 +46,15 @@ IMAGE="ubuntu-24.04"
CONFIG="burrow-forge" CONFIG="burrow-forge"
FLAKE="." FLAKE="."
UPLOAD_LOCATION="" UPLOAD_LOCATION=""
TOKEN_FILE="${HCLOUD_TOKEN_FILE:-intake/hetzner-api-token.txt}" TOKEN_FILE="${HCLOUD_TOKEN_FILE:-}"
YES=0 YES=0
SSH_KEYS=("contact@burrow.net" "agent@burrow.net") SSH_KEYS=("contact@burrow.net" "agent@burrow.net")
cleanup() {
burrow_cleanup_secret_tmpfiles
}
trap cleanup EXIT
if [[ $# -gt 0 ]]; then if [[ $# -gt 0 ]]; then
case "$1" in case "$1" in
show|create|delete|recreate|build-image|create-from-image|recreate-from-image) show|create|delete|recreate|build-image|create-from-image|recreate-from-image)
@ -110,10 +118,16 @@ while [[ $# -gt 0 ]]; do
esac esac
done done
if [[ ! -f "${TOKEN_FILE}" ]]; then TOKEN_FILE="$(
echo "Hetzner API token file not found: ${TOKEN_FILE}" >&2 burrow_resolve_secret_file \
"${REPO_ROOT}" \
"${TOKEN_FILE}" \
"${REPO_ROOT}/intake/hetzner-api-token.txt" \
"${REPO_ROOT}/secrets/hetzner/api-token.age"
)" || {
echo "Hetzner API token file could not be resolved" >&2
exit 1 exit 1
fi }
if [[ -z "${UPLOAD_LOCATION}" ]]; then if [[ -z "${UPLOAD_LOCATION}" ]]; then
UPLOAD_LOCATION="${LOCATION}" UPLOAD_LOCATION="${LOCATION}"

View file

@ -6,11 +6,13 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=Scripts/_burrow-flake.sh # shellcheck source=Scripts/_burrow-flake.sh
source "${SCRIPT_DIR}/_burrow-flake.sh" source "${SCRIPT_DIR}/_burrow-flake.sh"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${SCRIPT_DIR}/_burrow-secrets.sh"
CONFIG="${HCLOUD_IMAGE_CONFIG:-burrow-forge}" CONFIG="${HCLOUD_IMAGE_CONFIG:-burrow-forge}"
FLAKE="${HCLOUD_IMAGE_FLAKE:-.}" FLAKE="${HCLOUD_IMAGE_FLAKE:-.}"
LOCATION="${HCLOUD_IMAGE_LOCATION:-hel1}" LOCATION="${HCLOUD_IMAGE_LOCATION:-hel1}"
TOKEN_FILE="${HCLOUD_TOKEN_FILE:-${REPO_ROOT}/intake/hetzner-api-token.txt}" TOKEN_FILE="${HCLOUD_TOKEN_FILE:-}"
NSC_SSH_HOST="${NSC_SSH_HOST:-ssh.ord2.namespace.so}" NSC_SSH_HOST="${NSC_SSH_HOST:-ssh.ord2.namespace.so}"
NSC_MACHINE_TYPE="${NSC_MACHINE_TYPE:-linux/amd64:32x64}" NSC_MACHINE_TYPE="${NSC_MACHINE_TYPE:-linux/amd64:32x64}"
NSC_BUILDER_DURATION="${NSC_BUILDER_DURATION:-4h}" NSC_BUILDER_DURATION="${NSC_BUILDER_DURATION:-4h}"
@ -26,6 +28,13 @@ EXTRA_LABELS=()
BURROW_FLAKE_TMPDIRS=() BURROW_FLAKE_TMPDIRS=()
BUILDER_ID="" BUILDER_ID=""
cleanup() {
burrow_cleanup_secret_tmpfiles
burrow_cleanup_flake_tmpdirs
}
trap cleanup EXIT
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: Scripts/nsc-build-and-upload-image.sh [options] Usage: Scripts/nsc-build-and-upload-image.sh [options]
@ -37,7 +46,7 @@ Options:
--config <name> images.<name>-raw output to build (default: burrow-forge) --config <name> images.<name>-raw output to build (default: burrow-forge)
--flake <path> Flake path to build from (default: .) --flake <path> Flake path to build from (default: .)
--location <code> Hetzner upload location (default: hel1) --location <code> Hetzner upload location (default: hel1)
--token-file <path> Hetzner API token file (default: intake/hetzner-api-token.txt) --token-file <path> Hetzner API token file (default: secrets/hetzner/api-token.age, then intake/hetzner-api-token.txt)
--machine-type <type> Namespace machine type (default: linux/amd64:32x64) --machine-type <type> Namespace machine type (default: linux/amd64:32x64)
--ssh-host <host> Namespace SSH endpoint (default: ssh.ord2.namespace.so) --ssh-host <host> Namespace SSH endpoint (default: ssh.ord2.namespace.so)
--duration <ttl> Namespace builder lifetime (default: 4h) --duration <ttl> Namespace builder lifetime (default: 4h)
@ -126,6 +135,17 @@ while [[ $# -gt 0 ]]; do
esac esac
done done
TOKEN_FILE="$(
burrow_resolve_secret_file \
"${REPO_ROOT}" \
"${TOKEN_FILE}" \
"${REPO_ROOT}/intake/hetzner-api-token.txt" \
"${REPO_ROOT}/secrets/hetzner/api-token.age"
)" || {
echo "Hetzner API token file could not be resolved" >&2
exit 1
}
cleanup() { cleanup() {
if [[ -n "${BUILDER_ID}" && -n "${NSC_BIN}" ]]; then if [[ -n "${BUILDER_ID}" && -n "${NSC_BIN}" ]]; then
"${NSC_BIN}" destroy "${BUILDER_ID}" --force >/dev/null 2>&1 || true "${NSC_BIN}" destroy "${BUILDER_ID}" --force >/dev/null 2>&1 || true

View file

@ -6,31 +6,35 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=Scripts/_burrow-flake.sh # shellcheck source=Scripts/_burrow-flake.sh
source "${SCRIPT_DIR}/_burrow-flake.sh" source "${SCRIPT_DIR}/_burrow-flake.sh"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${SCRIPT_DIR}/_burrow-secrets.sh"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: Scripts/provision-forgejo-nsc.sh [options] Usage: Scripts/provision-forgejo-nsc.sh [options]
Generate Burrow forgejo-nsc runtime inputs in intake/ and optionally refresh the Generate Burrow forgejo-nsc runtime inputs and refresh the authoritative
Namespace token from the currently logged-in namespace account. `secrets/forgejo/*.age` files, optionally refreshing the Namespace token from
the currently logged-in namespace account.
Options: Options:
--host <user@host> SSH target used to mint the Forgejo PAT. --host <user@host> SSH target used to mint the Forgejo PAT.
Default: root@git.burrow.net Default: root@git.burrow.net
--ssh-key <path> SSH private key for the forge host. --ssh-key <path> SSH private key for the forge host.
Default: intake/agent_at_burrow_net_ed25519 Default: secrets/forgejo/agent-ssh-key.age, then intake/
--nsc-bin <path> Override the nsc binary. --nsc-bin <path> Override the nsc binary.
--no-refresh-token Reuse intake/forgejo_nsc_token.txt if it already exists. --no-refresh-token Reuse the existing encrypted Namespace token if it already exists.
--token-name <name> Forgejo PAT name prefix (default: forgejo-nsc) --token-name <name> Forgejo PAT name prefix (default: forgejo-nsc)
--contact-user <name> Forgejo username used for PAT creation (default: contact) --contact-user <name> Forgejo username used for PAT creation (default: contact)
--scope-owner <name> Forgejo org/user owner for the default NSC scope (default: hackclub) --scope-owner <name> Forgejo org/user owner for the default NSC scope (default: hackclub)
--scope-name <name> Forgejo repository name for the default NSC scope (default: burrow) --scope-name <name> Forgejo repository name for the default NSC scope (default: burrow)
--write-intake Also write plaintext runtime inputs to intake/ for local debugging.
-h, --help Show this help text. -h, --help Show this help text.
EOF EOF
} }
HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}" HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}"
SSH_KEY="${BURROW_FORGE_SSH_KEY:-${REPO_ROOT}/intake/agent_at_burrow_net_ed25519}" SSH_KEY="${BURROW_FORGE_SSH_KEY:-}"
NSC_BIN="${NSC_BIN:-}" NSC_BIN="${NSC_BIN:-}"
KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}" KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}"
REFRESH_TOKEN=1 REFRESH_TOKEN=1
@ -39,8 +43,12 @@ CONTACT_USER="${FORGEJO_CONTACT_USER:-contact}"
SCOPE_OWNER="${FORGEJO_SCOPE_OWNER:-hackclub}" SCOPE_OWNER="${FORGEJO_SCOPE_OWNER:-hackclub}"
SCOPE_NAME="${FORGEJO_SCOPE_NAME:-burrow}" SCOPE_NAME="${FORGEJO_SCOPE_NAME:-burrow}"
BURROW_FLAKE_TMPDIRS=() BURROW_FLAKE_TMPDIRS=()
WRITE_INTAKE=0
TMP_DIR=""
cleanup() { cleanup() {
[[ -n "${TMP_DIR}" ]] && rm -rf "${TMP_DIR}" >/dev/null 2>&1 || true
burrow_cleanup_secret_tmpfiles
burrow_cleanup_flake_tmpdirs burrow_cleanup_flake_tmpdirs
} }
trap cleanup EXIT trap cleanup EXIT
@ -79,6 +87,10 @@ while [[ $# -gt 0 ]]; do
SCOPE_NAME="${2:?missing value for --scope-name}" SCOPE_NAME="${2:?missing value for --scope-name}"
shift 2 shift 2
;; ;;
--write-intake)
WRITE_INTAKE=1
shift
;;
-h|--help) -h|--help)
usage usage
exit 0 exit 0
@ -97,13 +109,15 @@ burrow_require_cmd nix
burrow_require_cmd ssh burrow_require_cmd ssh
burrow_require_cmd python3 burrow_require_cmd python3
if [[ ! -f "${SSH_KEY}" ]]; then SSH_KEY="$(
echo "forge SSH key not found: ${SSH_KEY}" >&2 burrow_resolve_secret_file \
exit 1 "${REPO_ROOT}" \
fi "${SSH_KEY}" \
"${REPO_ROOT}/intake/agent_at_burrow_net_ed25519" \
mkdir -p "${REPO_ROOT}/intake" "${REPO_ROOT}/secrets/forgejo/agent-ssh-key.age" \
chmod 700 "${REPO_ROOT}/intake" "${HOME}/.ssh/agent_at_burrow_net_ed25519"
)"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/burrow-forgejo-nsc.XXXXXX")"
flake_ref="$(burrow_prepare_flake_ref "${REPO_ROOT}")" flake_ref="$(burrow_prepare_flake_ref "${REPO_ROOT}")"
if [[ -z "${NSC_BIN}" ]]; then if [[ -z "${NSC_BIN}" ]]; then
@ -128,13 +142,16 @@ if [[ ! -x "${NSC_BIN}" ]]; then
exit 1 exit 1
fi fi
token_file="${REPO_ROOT}/intake/forgejo_nsc_token.txt" token_file="${TMP_DIR}/forgejo_nsc_token.txt"
dispatcher_out="${REPO_ROOT}/intake/forgejo_nsc_dispatcher.yaml" dispatcher_out="${TMP_DIR}/forgejo_nsc_dispatcher.yaml"
autoscaler_out="${REPO_ROOT}/intake/forgejo_nsc_autoscaler.yaml" autoscaler_out="${TMP_DIR}/forgejo_nsc_autoscaler.yaml"
dispatcher_src="${REPO_ROOT}/services/forgejo-nsc/deploy/dispatcher.yaml" dispatcher_src="${REPO_ROOT}/services/forgejo-nsc/deploy/dispatcher.yaml"
autoscaler_src="${REPO_ROOT}/services/forgejo-nsc/deploy/autoscaler.yaml" autoscaler_src="${REPO_ROOT}/services/forgejo-nsc/deploy/autoscaler.yaml"
token_secret="${REPO_ROOT}/secrets/forgejo/nsc-token.age"
dispatcher_secret="${REPO_ROOT}/secrets/forgejo/nsc-dispatcher-config.age"
autoscaler_secret="${REPO_ROOT}/secrets/forgejo/nsc-autoscaler-config.age"
if [[ "${REFRESH_TOKEN}" -eq 1 || ! -s "${token_file}" ]]; then if [[ "${REFRESH_TOKEN}" -eq 1 ]]; then
"${NSC_BIN}" auth check-login --duration 20m >/dev/null "${NSC_BIN}" auth check-login --duration 20m >/dev/null
raw_token_file="$(mktemp)" raw_token_file="$(mktemp)"
trap 'rm -f "${raw_token_file}"; cleanup' EXIT trap 'rm -f "${raw_token_file}"; cleanup' EXIT
@ -155,7 +172,13 @@ Path(os.environ["TOKEN_FILE"]).write_text(
PY PY
rm -f "${raw_token_file}" rm -f "${raw_token_file}"
chmod 600 "${token_file}" chmod 600 "${token_file}"
elif [[ -s "${token_file}" ]]; then elif [[ -f "${token_secret}" ]]; then
burrow_decrypt_age_secret_to_temp "${REPO_ROOT}" "${token_secret}" > "${token_file}"
elif [[ -s "${REPO_ROOT}/intake/forgejo_nsc_token.txt" ]]; then
cp "${REPO_ROOT}/intake/forgejo_nsc_token.txt" "${token_file}"
fi
if [[ -s "${token_file}" ]]; then
TOKEN_FILE="${token_file}" python3 - <<'PY' TOKEN_FILE="${token_file}" python3 - <<'PY'
import json import json
import os import os
@ -271,6 +294,24 @@ PY
chmod 600 "${dispatcher_out}" "${autoscaler_out}" chmod 600 "${dispatcher_out}" "${autoscaler_out}"
echo "Rendered intake/forgejo_nsc_token.txt, intake/forgejo_nsc_dispatcher.yaml, and intake/forgejo_nsc_autoscaler.yaml." burrow_encrypt_secret_from_file "${REPO_ROOT}" "${token_secret}" "${token_file}"
echo "Re-encrypt them into secrets/forgejo/{nsc-token,nsc-dispatcher-config,nsc-autoscaler-config}.age before deploying the forge host." burrow_encrypt_secret_from_file "${REPO_ROOT}" "${dispatcher_secret}" "${dispatcher_out}"
burrow_encrypt_secret_from_file "${REPO_ROOT}" "${autoscaler_secret}" "${autoscaler_out}"
if [[ "${WRITE_INTAKE}" -eq 1 ]]; then
mkdir -p "${REPO_ROOT}/intake"
chmod 700 "${REPO_ROOT}/intake"
cp "${token_file}" "${REPO_ROOT}/intake/forgejo_nsc_token.txt"
cp "${dispatcher_out}" "${REPO_ROOT}/intake/forgejo_nsc_dispatcher.yaml"
cp "${autoscaler_out}" "${REPO_ROOT}/intake/forgejo_nsc_autoscaler.yaml"
chmod 600 \
"${REPO_ROOT}/intake/forgejo_nsc_token.txt" \
"${REPO_ROOT}/intake/forgejo_nsc_dispatcher.yaml" \
"${REPO_ROOT}/intake/forgejo_nsc_autoscaler.yaml"
fi
echo "Updated secrets/forgejo/{nsc-token,nsc-dispatcher-config,nsc-autoscaler-config}.age."
if [[ "${WRITE_INTAKE}" -eq 1 ]]; then
echo "Also refreshed intake/forgejo_nsc_{token,dispatcher,autoscaler} for local debugging."
fi
echo "Minted Forgejo PAT ${token_name} for ${CONTACT_USER} on ${HOST}." echo "Minted Forgejo PAT ${token_name} for ${CONTACT_USER} on ${HOST}."

View file

@ -5,12 +5,12 @@ usage() {
cat <<'EOF' cat <<'EOF'
Usage: Scripts/sync-forgejo-nsc-config.sh [options] Usage: Scripts/sync-forgejo-nsc-config.sh [options]
Copy Burrow forgejo-nsc runtime inputs from intake/ onto the forge host and Copy Burrow forgejo-nsc runtime inputs from age secrets or intake/ onto the forge host and
restart the dispatcher/autoscaler units. restart the dispatcher/autoscaler units.
Options: Options:
--host <user@host> SSH target (default: root@git.burrow.net) --host <user@host> SSH target (default: root@git.burrow.net)
--ssh-key <path> SSH private key (default: intake/agent_at_burrow_net_ed25519) --ssh-key <path> SSH private key (default: secrets/forgejo/agent-ssh-key.age, then intake/)
--rotate-pat Re-render the intake files before syncing. --rotate-pat Re-render the intake files before syncing.
--no-restart Copy files only. --no-restart Copy files only.
-h, --help Show this help text. -h, --help Show this help text.
@ -19,12 +19,21 @@ EOF
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${SCRIPT_DIR}/_burrow-secrets.sh"
HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}" HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}"
SSH_KEY="${BURROW_FORGE_SSH_KEY:-${REPO_ROOT}/intake/agent_at_burrow_net_ed25519}" SSH_KEY="${BURROW_FORGE_SSH_KEY:-}"
KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}" KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}"
ROTATE_PAT=0 ROTATE_PAT=0
NO_RESTART=0 NO_RESTART=0
TMP_DIR=""
cleanup() {
[[ -n "${TMP_DIR}" ]] && rm -rf "${TMP_DIR}" >/dev/null 2>&1 || true
burrow_cleanup_secret_tmpfiles
}
trap cleanup EXIT
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
@ -68,18 +77,41 @@ burrow_require_cmd() {
burrow_require_cmd ssh burrow_require_cmd ssh
burrow_require_cmd scp burrow_require_cmd scp
if [[ ! -f "${SSH_KEY}" ]]; then SSH_KEY="$(
echo "forge SSH key not found: ${SSH_KEY}" >&2 burrow_resolve_secret_file \
exit 1 "${REPO_ROOT}" \
fi "${SSH_KEY}" \
"${REPO_ROOT}/intake/agent_at_burrow_net_ed25519" \
"${REPO_ROOT}/secrets/forgejo/agent-ssh-key.age" \
"${HOME}/.ssh/agent_at_burrow_net_ed25519"
)"
if [[ "${ROTATE_PAT}" -eq 1 ]]; then if [[ "${ROTATE_PAT}" -eq 1 ]]; then
"${SCRIPT_DIR}/provision-forgejo-nsc.sh" --host "${HOST}" --ssh-key "${SSH_KEY}" "${SCRIPT_DIR}/provision-forgejo-nsc.sh" --host "${HOST}" --ssh-key "${SSH_KEY}"
fi fi
token_file="${REPO_ROOT}/intake/forgejo_nsc_token.txt" TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/burrow-nsc-sync.XXXXXX")"
dispatcher_file="${REPO_ROOT}/intake/forgejo_nsc_dispatcher.yaml" token_file="$(
autoscaler_file="${REPO_ROOT}/intake/forgejo_nsc_autoscaler.yaml" burrow_resolve_secret_file \
"${REPO_ROOT}" \
"" \
"${REPO_ROOT}/intake/forgejo_nsc_token.txt" \
"${REPO_ROOT}/secrets/forgejo/nsc-token.age"
)"
dispatcher_file="$(
burrow_resolve_secret_file \
"${REPO_ROOT}" \
"" \
"${REPO_ROOT}/intake/forgejo_nsc_dispatcher.yaml" \
"${REPO_ROOT}/secrets/forgejo/nsc-dispatcher-config.age"
)"
autoscaler_file="$(
burrow_resolve_secret_file \
"${REPO_ROOT}" \
"" \
"${REPO_ROOT}/intake/forgejo_nsc_autoscaler.yaml" \
"${REPO_ROOT}/secrets/forgejo/nsc-autoscaler-config.age"
)"
for path in "${token_file}" "${dispatcher_file}" "${autoscaler_file}"; do for path in "${token_file}" "${dispatcher_file}" "${autoscaler_file}"; do
if [[ ! -s "${path}" ]]; then if [[ ! -s "${path}" ]]; then
@ -96,12 +128,12 @@ ssh_opts=(
) )
remote_tmp="$(ssh "${ssh_opts[@]}" "${HOST}" "mktemp -d")" remote_tmp="$(ssh "${ssh_opts[@]}" "${HOST}" "mktemp -d")"
cleanup() { cleanup_remote() {
if [[ -n "${remote_tmp:-}" ]]; then if [[ -n "${remote_tmp:-}" ]]; then
ssh "${ssh_opts[@]}" "${HOST}" "rm -rf '${remote_tmp}'" >/dev/null 2>&1 || true ssh "${ssh_opts[@]}" "${HOST}" "rm -rf '${remote_tmp}'" >/dev/null 2>&1 || true
fi fi
} }
trap cleanup EXIT trap 'cleanup_remote; cleanup' EXIT
scp "${ssh_opts[@]}" \ scp "${ssh_opts[@]}" \
"${token_file}" \ "${token_file}" \

View file

@ -3,17 +3,22 @@
set -euo pipefail set -euo pipefail
umask 077 umask 077
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${REPO_ROOT}/Scripts/_burrow-secrets.sh"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: Usage:
Tools/forwardemail-custom-s3.sh \ Tools/forwardemail-custom-s3.sh \
--domain burrow.net \ --domain burrow.net \
--api-token-file intake/forwardemail_api_token.txt \ --api-token-file secrets/forwardemail/api-token.age \
--s3-endpoint https://<endpoint> \ --s3-endpoint https://<endpoint> \
--s3-region <region> \ --s3-region <region> \
--s3-bucket <bucket> \ --s3-bucket <bucket> \
--s3-access-key-file intake/hetzner-s3-user.txt \ --s3-access-key-file secrets/forwardemail/hetzner-s3-user.age \
--s3-secret-key-file intake/hetzner-s3-secret.txt --s3-secret-key-file secrets/forwardemail/hetzner-s3-secret.age
Options: Options:
--domain <domain> Forward Email domain to update. --domain <domain> Forward Email domain to update.
@ -54,13 +59,18 @@ read_secret() {
printf '%s' "$value" printf '%s' "$value"
} }
cleanup() {
burrow_cleanup_secret_tmpfiles
}
trap cleanup EXIT
domain="" domain=""
api_token_file="" api_token_file="${FORWARDEMAIL_API_TOKEN_FILE:-}"
s3_endpoint="" s3_endpoint=""
s3_region="" s3_region=""
s3_bucket="" s3_bucket=""
s3_access_key_file="" s3_access_key_file="${FORWARDEMAIL_S3_ACCESS_KEY_FILE:-}"
s3_secret_key_file="" s3_secret_key_file="${FORWARDEMAIL_S3_SECRET_KEY_FILE:-}"
test_only=false test_only=false
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@ -108,16 +118,38 @@ while [[ $# -gt 0 ]]; do
done done
[[ -n "$domain" ]] || fail "--domain is required" [[ -n "$domain" ]] || fail "--domain is required"
[[ -n "$api_token_file" ]] || fail "--api-token-file is required"
[[ -n "$s3_endpoint" || "$test_only" == true ]] || fail "--s3-endpoint is required unless --test-only is set" [[ -n "$s3_endpoint" || "$test_only" == true ]] || fail "--s3-endpoint is required unless --test-only is set"
[[ -n "$s3_region" || "$test_only" == true ]] || fail "--s3-region is required unless --test-only is set" [[ -n "$s3_region" || "$test_only" == true ]] || fail "--s3-region is required unless --test-only is set"
[[ -n "$s3_bucket" || "$test_only" == true ]] || fail "--s3-bucket is required unless --test-only is set" [[ -n "$s3_bucket" || "$test_only" == true ]] || fail "--s3-bucket is required unless --test-only is set"
[[ -n "$s3_access_key_file" || "$test_only" == true ]] || fail "--s3-access-key-file is required unless --test-only is set" api_token_file="$(
[[ -n "$s3_secret_key_file" || "$test_only" == true ]] || fail "--s3-secret-key-file is required unless --test-only is set" burrow_resolve_secret_file \
"${REPO_ROOT}" \
"${api_token_file}" \
"${REPO_ROOT}/intake/forwardemail_api_token.txt" \
"${REPO_ROOT}/secrets/forwardemail/api-token.age"
)" || fail "unable to resolve Forward Email API token file"
require_file "$api_token_file" require_file "$api_token_file"
api_token="$(read_secret "$api_token_file")" api_token="$(read_secret "$api_token_file")"
if [[ "$test_only" != true ]]; then
s3_access_key_file="$(
burrow_resolve_secret_file \
"${REPO_ROOT}" \
"${s3_access_key_file}" \
"${REPO_ROOT}/intake/hetzner-s3-user.txt" \
"${REPO_ROOT}/secrets/forwardemail/hetzner-s3-user.age"
)" || fail "unable to resolve Hetzner S3 access key file"
s3_secret_key_file="$(
burrow_resolve_secret_file \
"${REPO_ROOT}" \
"${s3_secret_key_file}" \
"${REPO_ROOT}/intake/hetzner-s3-secret.txt" \
"${REPO_ROOT}/secrets/forwardemail/hetzner-s3-secret.age"
)" || fail "unable to resolve Hetzner S3 secret key file"
require_file "$s3_access_key_file"
require_file "$s3_secret_key_file"
fi
if [[ "$test_only" == false ]]; then if [[ "$test_only" == false ]]; then
require_file "$s3_access_key_file" require_file "$s3_access_key_file"
require_file "$s3_secret_key_file" require_file "$s3_secret_key_file"

View file

@ -6,6 +6,7 @@ import argparse
import datetime as dt import datetime as dt
import hashlib import hashlib
import hmac import hmac
import subprocess
import sys import sys
import textwrap import textwrap
from pathlib import Path from pathlib import Path
@ -13,11 +14,38 @@ from urllib.parse import urlencode, urlparse
import requests import requests
REPO_ROOT = Path(__file__).resolve().parent.parent
def default_secret_path(age_rel: str, intake_rel: str) -> str:
age_path = REPO_ROOT / age_rel
if age_path.exists():
return str(age_path)
return intake_rel
def read_secret(path: str) -> str: def read_secret(path: str) -> str:
value = Path(path).read_text(encoding="utf-8").strip() file_path = Path(path)
if not file_path.is_absolute():
file_path = REPO_ROOT / file_path
if file_path.suffix == ".age":
value = subprocess.check_output(
[
"nix",
"--extra-experimental-features",
"nix-command flakes",
"run",
f"{REPO_ROOT}#agenix",
"--",
"-d",
str(file_path),
],
text=True,
).strip()
else:
value = file_path.read_text(encoding="utf-8").strip()
if not value: if not value:
raise SystemExit(f"error: empty secret file: {path}") raise SystemExit(f"error: empty secret file: {file_path}")
return value return value
@ -212,12 +240,12 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--region", default="hel1", help="S3 region.") parser.add_argument("--region", default="hel1", help="S3 region.")
parser.add_argument( parser.add_argument(
"--access-key-file", "--access-key-file",
default="intake/hetzner-s3-user.txt", default=default_secret_path("secrets/forwardemail/hetzner-s3-user.age", "intake/hetzner-s3-user.txt"),
help="File containing the S3 access key id.", help="File containing the S3 access key id.",
) )
parser.add_argument( parser.add_argument(
"--secret-key-file", "--secret-key-file",
default="intake/hetzner-s3-secret.txt", default=default_secret_path("secrets/forwardemail/hetzner-s3-secret.age", "intake/hetzner-s3-secret.txt"),
help="File containing the S3 secret key.", help="File containing the S3 secret key.",
) )
parser.add_argument( parser.add_argument(

View file

@ -26,11 +26,14 @@ Forward Email also documents these operational constraints:
## Burrow Secret Layout ## Burrow Secret Layout
Present in `intake/` today: Authoritative secrets now live in:
- `intake/forwardemail_api_token.txt` - `secrets/forwardemail/api-token.age`
- `intake/hetzner-s3-user.txt` - `secrets/forwardemail/hetzner-s3-user.age`
- `intake/hetzner-s3-secret.txt` - `secrets/forwardemail/hetzner-s3-secret.age`
Legacy plaintext `intake/` files may still exist locally for debugging, but the
tooling now prefers the age-encrypted files above.
- Hetzner public S3 endpoint for Forward Email: `https://hel1.your-objectstorage.com` - Hetzner public S3 endpoint for Forward Email: `https://hel1.your-objectstorage.com`
- Hetzner object storage region: `hel1` - Hetzner object storage region: `hel1`
- Hetzner bucket used for Forward Email backups: `burrow` - Hetzner bucket used for Forward Email backups: `burrow`
@ -69,12 +72,12 @@ Example:
```sh ```sh
Tools/forwardemail-custom-s3.sh \ Tools/forwardemail-custom-s3.sh \
--domain burrow.net \ --domain burrow.net \
--api-token-file intake/forwardemail_api_token.txt \ --api-token-file secrets/forwardemail/api-token.age \
--s3-endpoint https://hel1.your-objectstorage.com \ --s3-endpoint https://hel1.your-objectstorage.com \
--s3-region hel1 \ --s3-region hel1 \
--s3-bucket burrow \ --s3-bucket burrow \
--s3-access-key-file intake/hetzner-s3-user.txt \ --s3-access-key-file secrets/forwardemail/hetzner-s3-user.age \
--s3-secret-key-file intake/hetzner-s3-secret.txt --s3-secret-key-file secrets/forwardemail/hetzner-s3-secret.age
``` ```
Retest an existing domain configuration without rewriting it: Retest an existing domain configuration without rewriting it:
@ -82,7 +85,7 @@ Retest an existing domain configuration without rewriting it:
```sh ```sh
Tools/forwardemail-custom-s3.sh \ Tools/forwardemail-custom-s3.sh \
--domain burrow.net \ --domain burrow.net \
--api-token-file intake/forwardemail_api_token.txt \ --api-token-file secrets/forwardemail/api-token.age \
--test-only --test-only
``` ```

View file

@ -29,7 +29,7 @@ Mail hosting is intentionally not part of this NixOS host in the current plan. B
3. Encrypt the Forgejo admin password and agent SSH key into `secrets/forgejo/{admin-password,agent-ssh-key}.age`. 3. Encrypt the Forgejo admin password and agent SSH key into `secrets/forgejo/{admin-password,agent-ssh-key}.age`.
4. Let `burrow-forgejo-bootstrap.service` create or rotate the initial Forgejo admin account from the agenix secret path. 4. Let `burrow-forgejo-bootstrap.service` create or rotate the initial Forgejo admin account from the agenix secret path.
5. Let `burrow-forgejo-runner-bootstrap.service` register the self-hosted Forgejo runner and seed Git identity as `agent <agent@burrow.net>`. 5. Let `burrow-forgejo-runner-bootstrap.service` register the self-hosted Forgejo runner and seed Git identity as `agent <agent@burrow.net>`.
6. Run `Scripts/provision-forgejo-nsc.sh` locally, re-encrypt the resulting NSC token + configs into `secrets/forgejo/*.age`, then deploy with `Scripts/forge-deploy.sh` so agenix updates the live forgejo-nsc runtime paths. 6. Run `Scripts/provision-forgejo-nsc.sh` locally to refresh `secrets/forgejo/{nsc-token,nsc-dispatcher-config,nsc-autoscaler-config}.age`, then deploy with `Scripts/forge-deploy.sh` so agenix updates the live forgejo-nsc runtime paths.
7. Use `Scripts/cloudflare-upsert-a-record.sh` to point `git.burrow.net`, `burrow.net`, and `nsc-autoscaler.burrow.net` at the host with Cloudflare proxying disabled for ACME. 7. Use `Scripts/cloudflare-upsert-a-record.sh` to point `git.burrow.net`, `burrow.net`, and `nsc-autoscaler.burrow.net` at the host with Cloudflare proxying disabled for ACME.
8. Use `Scripts/forge-deploy.sh --allow-dirty` for subsequent remote `nixos-rebuild` runs from the live workspace. 8. Use `Scripts/forge-deploy.sh --allow-dirty` for subsequent remote `nixos-rebuild` runs from the live workspace.
9. Configure Forward Email custom S3 backups for `burrow.net` and `burrow.rs` out-of-band with `Tools/forwardemail-custom-s3.sh`. 9. Configure Forward Email custom S3 backups for `burrow.net` and `burrow.rs` out-of-band with `Tools/forwardemail-custom-s3.sh`.
@ -43,7 +43,7 @@ Mail hosting is intentionally not part of this NixOS host in the current plan. B
- `https://burrow.net` returns the root forge landing response - `https://burrow.net` returns the root forge landing response
- `https://git.burrow.net` returns the live Forgejo front door - `https://git.burrow.net` returns the live Forgejo front door
- `https://nsc-autoscaler.burrow.net` terminates TLS on Caddy and returns the expected application-level `404` for `/` - `https://nsc-autoscaler.burrow.net` terminates TLS on Caddy and returns the expected application-level `404` for `/`
- The Cloudflare token currently in `intake/cloudflare-token.txt` is an account-scoped token: `POST /accounts/<account>/tokens/verify` succeeds, while `POST /user/tokens/verify` returns `Invalid API Token`. - The Cloudflare token now lives in `secrets/cloudflare/api-token.age`; the current token is account-scoped: `POST /accounts/<account>/tokens/verify` succeeds, while `POST /user/tokens/verify` returns `Invalid API Token`.
- `burrow.rs` still resolves publicly to a Vercel `DEPLOYMENT_NOT_FOUND` response. - `burrow.rs` still resolves publicly to a Vercel `DEPLOYMENT_NOT_FOUND` response.
- Both domains publish Forward Email MX/TXT records. - Both domains publish Forward Email MX/TXT records.
- Forward Email custom S3 is live on both domains against the Hetzner `burrow` bucket and the public regional endpoint `https://hel1.your-objectstorage.com`. - Forward Email custom S3 is live on both domains against the Hetzner `burrow` bucket and the public regional endpoint `https://hel1.your-objectstorage.com`.

View file

@ -9,11 +9,19 @@ For the Forgejo Namespace Cloud runtime:
- `secrets/forgejo/nsc-token.age` - `secrets/forgejo/nsc-token.age`
- `secrets/forgejo/nsc-dispatcher-config.age` - `secrets/forgejo/nsc-dispatcher-config.age`
- `secrets/forgejo/nsc-autoscaler-config.age` - `secrets/forgejo/nsc-autoscaler-config.age`
- `secrets/cloudflare/api-token.age`
- `secrets/hetzner/api-token.age`
- `secrets/forwardemail/api-token.age`
- `secrets/forwardemail/hetzner-s3-user.age`
- `secrets/forwardemail/hetzner-s3-secret.age`
Use: Use:
- `make secret name=forgejo/nsc-token` - `make secret name=forgejo/nsc-token`
- `make secret-file name=forgejo/agent-ssh-key file=/path/to/source` - `make secret-file name=forgejo/agent-ssh-key file=/path/to/source`
- `Scripts/provision-forgejo-nsc.sh` to refresh the Forgejo Namespace token and runtime configs in `secrets/forgejo/*.age`
- `make secret-file name=cloudflare/api-token file=/path/to/cloudflare-token.txt`
- `make secret-file name=hetzner/api-token file=/path/to/hetzner-api-token.txt`
The forge host decrypts these files at activation time and feeds the resulting The forge host decrypts these files at activation time and feeds the resulting
paths into `services.burrow.forge`, `services.burrow.forgeRunner`, and paths into `services.burrow.forge`, `services.burrow.forgeRunner`, and

View file

@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 ux4N8Q rX5+bmtxyHNgD+xNdHkB1fKdjUlrX275DaKTIHssYyA
KwbfKHx14QXRKBIGWwJDR8+DONyCdVssh8Ti8mdajyQ
-> ssh-ed25519 IrZmAg SOG/KvURA6PrxVhtZyIbazFGNQZyp0BR4MH+YInHGB4
79pENXhtLwlCQVnqkPEzoFgrXMmTqRsfs4ULluTevWA
--- gDA64KNbgN+eGHsQbIbKvhOg1T/Nqui6I/wy2MK8VWE
û<EFBFBD>[|V{[ƒöŽ’ýö¯'E .Í{CÃǶ Õö{ha

View file

@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 ux4N8Q ICuXuDsZiw1ShfUX9qjq8bCkeNdsbHWnG4e+3ZOC3jg
wswxqzQtf7jumSYB8ZeQzRBpMrBPVsUnWOYsmlDvpSs
-> ssh-ed25519 IrZmAg Xrvp/tXzXrHF1+NxgTZs9nNufyxtTq5NoYT5gaW6p1M
UWGlhZpV19CWMR9abp30vkQwZUMb/ylvInGEBlDdjjE
--- qhAaAECwhmAY4g3/e+Dz9RvL1MBQkHGWyoe1NkdTuqA
ìÑdÍéé?)¼ ñ<3ïŽ6ÜF:a•Ë<E280A2> ųñÖ²Ä

View file

@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 ux4N8Q jwJzvmXUV5rCB6ku7ILLQUDInuQJL2gN+pjmX/ccXWE
q9OSyVhTuzERRRZZOCQzbwAwLOvOFIT/l9MxJ0V3UTo
-> ssh-ed25519 IrZmAg 8IutYG3CnNP9gw5fTFOaXm1Ue4i/cVs1apA88bNs9mo
daaf+6HoE3bmUEKR8/zu9jKTstVFCXqBlBxBdNVpQ90
--- gRGNkWqoh+lZWpDG7yNLd4fjoX2jCyHTWbzImzoFGko
R@+‰fu9ËÏRB±áÎX³2öúæ<C3BA> “[I¤<49>®

View file

@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 ux4N8Q jwyFpeVX18Q/1vnK2A1gwETTTH/QDUmW7vhCA+E/1lc
vtG1Ra+hR0cc/o9oJw7YTWMc2+JmrehzBE5QkIHQMKY
-> ssh-ed25519 IrZmAg KljcDNRlBmn7ElVfXq/E2prFHnRQD2TkQY9Vto+OQUA
T37sFc3xVrhky6e0n4KbsX18/fBqP3VjS/mNbxX6bfI
--- lvSjWGriUCYC14eI2eH9MdO2cB76Pe3gWD7pidw8Qjo
s&¾Ùßxö™<C3B6>*°4}‰<1D>Í”z&¢F¥Å

View file

@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 ux4N8Q pEJA2VJkPC+NzA9yFvBrpXHD8qFMTD9iIHYSkx8P2RI
AGE1QJya77d92ERA1yQYylvZPNAJEQKoCL32BY5XBzo
-> ssh-ed25519 IrZmAg VMpoTBpNG/TAlnbJ2APwc4VMt2CX5rQwlrrihtmojFo
caOwayLgVDGPrjqLLH8hHHQ3Fy2WeRI2tf+R02HFqx0
--- Ey1DYpyA4lnVqPaabNsEuSihl4fvZ2vpSc/IRGZwYBw
¥Uï2Q÷‘âÖã*ð÷m¹¼†<C2BC>F<EFBFBD>ÒÞž|^EVÜ"

View file

@ -3,6 +3,7 @@ let
agent = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEN0+tRJy7Y2DW0uGYHb86N2t02WyU5lDNX6FaxBF/G8 agent@burrow.net"; agent = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEN0+tRJy7Y2DW0uGYHb86N2t02WyU5lDNX6FaxBF/G8 agent@burrow.net";
forge = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAlkGo4lwpwIIZ0J01KjTuJuf/U/wGgy4/aKwPIUzutL root@burrow-forge"; forge = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAlkGo4lwpwIIZ0J01KjTuJuf/U/wGgy4/aKwPIUzutL root@burrow-forge";
operatorSecrets = [ contact agent ];
forgeAutomation = [ contact agent forge ]; forgeAutomation = [ contact agent forge ];
in { in {
"secrets/forgejo/admin-password.age".publicKeys = forgeAutomation; "secrets/forgejo/admin-password.age".publicKeys = forgeAutomation;
@ -10,4 +11,9 @@ in {
"secrets/forgejo/nsc-token.age".publicKeys = forgeAutomation; "secrets/forgejo/nsc-token.age".publicKeys = forgeAutomation;
"secrets/forgejo/nsc-dispatcher-config.age".publicKeys = forgeAutomation; "secrets/forgejo/nsc-dispatcher-config.age".publicKeys = forgeAutomation;
"secrets/forgejo/nsc-autoscaler-config.age".publicKeys = forgeAutomation; "secrets/forgejo/nsc-autoscaler-config.age".publicKeys = forgeAutomation;
"secrets/cloudflare/api-token.age".publicKeys = operatorSecrets;
"secrets/hetzner/api-token.age".publicKeys = operatorSecrets;
"secrets/forwardemail/api-token.age".publicKeys = operatorSecrets;
"secrets/forwardemail/hetzner-s3-user.age".publicKeys = operatorSecrets;
"secrets/forwardemail/hetzner-s3-secret.age".publicKeys = operatorSecrets;
} }

View file

@ -155,11 +155,12 @@ instances:
``` ```
For Burrow, use `Scripts/provision-forgejo-nsc.sh` to mint the Forgejo PAT, For Burrow, use `Scripts/provision-forgejo-nsc.sh` to mint the Forgejo PAT,
generate a Namespace token from the logged-in namespace account, and render generate a Namespace token from the logged-in Namespace account, and refresh
bootstrap artifacts into `intake/forgejo_nsc_{dispatcher,autoscaler}.yaml` plus `secrets/forgejo/{nsc-token,nsc-dispatcher-config,nsc-autoscaler-config}.age`.
`intake/forgejo_nsc_token.txt`. The token file is emitted as JSON with a The token file is emitted as JSON with a `bearer_token` field so both the
`bearer_token` field so both the Compute API path and the `nsc` CLI fallback can Compute API path and the `nsc` CLI fallback can consume the same secret
consume the same secret material. material. Use `--write-intake` only when you explicitly need local plaintext
debug copies.
Long-lived runtime state is now sourced from age-encrypted files: Long-lived runtime state is now sourced from age-encrypted files:
@ -169,10 +170,9 @@ Long-lived runtime state is now sourced from age-encrypted files:
- `secrets/forgejo/nsc-dispatcher-config.age` - `secrets/forgejo/nsc-dispatcher-config.age`
- `secrets/forgejo/nsc-autoscaler-config.age` - `secrets/forgejo/nsc-autoscaler-config.age`
After refreshing the intake files, re-encrypt them into `secrets/forgejo/*.age` After refreshing the encrypted secrets, deploy the forge host so
and deploy the forge host so `config.age.secrets.*` updates the live paths for `config.age.secrets.*` updates the live paths for `services.burrow.forge`,
`services.burrow.forge`, `services.burrow.forgeRunner`, and `services.burrow.forgeRunner`, and `services.burrow.forgejoNsc`.
`services.burrow.forgejoNsc`.
Run it next to the dispatcher: Run it next to the dispatcher:

View file

@ -602,6 +602,18 @@ if ! mkdir -p "/Users/runner/.cache/act" 2>/dev/null; then
fi fi
export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH}" export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH}"
cache_root="${NSC_CACHE_PATH:-$HOME/.cache/burrow}"
mkdir -p \
"${cache_root}/bin" \
"${cache_root}/downloads" \
"${cache_root}/go/path" \
"${cache_root}/go/mod" \
"${cache_root}/go/build" \
"${cache_root}/homebrew"
export HOMEBREW_CACHE="${cache_root}/homebrew"
export GOPATH="${cache_root}/go/path"
export GOMODCACHE="${cache_root}/go/mod"
export GOCACHE="${cache_root}/go/build"
if ! command -v curl >/dev/null 2>&1; then if ! command -v curl >/dev/null 2>&1; then
echo "curl is required" >&2 echo "curl is required" >&2
@ -622,14 +634,18 @@ export PATH="${PWD}/bin:${PATH}"
runner_version="v12.6.4" runner_version="v12.6.4"
runner_src_tgz="forgejo-runner-${runner_version}.tar.gz" runner_src_tgz="forgejo-runner-${runner_version}.tar.gz"
runner_src_tgz_path="${cache_root}/downloads/${runner_src_tgz}"
runner_src_url="https://code.forgejo.org/forgejo/runner/archive/${runner_version}.tar.gz" runner_src_url="https://code.forgejo.org/forgejo/runner/archive/${runner_version}.tar.gz"
runner_src_dir="forgejo-runner-src" runner_src_dir="forgejo-runner-src"
runner_bin_cache="${cache_root}/bin/forgejo-runner-${runner_version}"
if ! command -v forgejo-runner >/dev/null 2>&1; then if [[ ! -x "${runner_bin_cache}" ]]; then
rm -rf "${runner_src_dir}" rm -rf "${runner_src_dir}"
mkdir -p "${runner_src_dir}" mkdir -p "${runner_src_dir}"
curl -fsSL "${runner_src_url}" -o "${runner_src_tgz}" if [[ ! -f "${runner_src_tgz_path}" ]]; then
tar -xzf "${runner_src_tgz}" -C "${runner_src_dir}" --strip-components=1 curl -fsSL "${runner_src_url}" -o "${runner_src_tgz_path}"
fi
tar -xzf "${runner_src_tgz_path}" -C "${runner_src_dir}" --strip-components=1
toolchain="$(grep -E '^toolchain ' "${runner_src_dir}/go.mod" | awk '{print $2}' | head -n 1 || true)" toolchain="$(grep -E '^toolchain ' "${runner_src_dir}/go.mod" | awk '{print $2}' | head -n 1 || true)"
if [ -z "${toolchain}" ]; then if [ -z "${toolchain}" ]; then
@ -639,21 +655,23 @@ if ! command -v forgejo-runner >/dev/null 2>&1; then
if ! command -v go >/dev/null 2>&1; then if ! command -v go >/dev/null 2>&1; then
go_tgz="${toolchain}.darwin-arm64.tar.gz" go_tgz="${toolchain}.darwin-arm64.tar.gz"
go_url="https://go.dev/dl/${go_tgz}" go_url="https://go.dev/dl/${go_tgz}"
curl -fsSL "${go_url}" -o "${go_tgz}" go_tgz_path="${cache_root}/downloads/${go_tgz}"
tar -xzf "${go_tgz}" if [[ ! -f "${go_tgz_path}" ]]; then
curl -fsSL "${go_url}" -o "${go_tgz_path}"
fi
tar -xzf "${go_tgz_path}"
export GOROOT="${PWD}/go" export GOROOT="${PWD}/go"
export PATH="${GOROOT}/bin:${PATH}" export PATH="${GOROOT}/bin:${PATH}"
fi fi
export GOPATH="${PWD}/.gopath"
export GOMODCACHE="${PWD}/.gomodcache"
export GOCACHE="${PWD}/.gocache"
mkdir -p "${GOPATH}" "${GOMODCACHE}" "${GOCACHE}" mkdir -p "${GOPATH}" "${GOMODCACHE}" "${GOCACHE}"
(cd "${runner_src_dir}" && go build -o "${workdir}/bin/forgejo-runner" .) (cd "${runner_src_dir}" && go build -o "${runner_bin_cache}" .)
chmod +x "${workdir}/bin/forgejo-runner" chmod +x "${runner_bin_cache}"
fi fi
ln -sf "${runner_bin_cache}" "${workdir}/bin/forgejo-runner"
cat > runner.yaml <<'EOF' cat > runner.yaml <<'EOF'
log: log:
level: info level: info