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}"
mkdir -p \
"${cache_root}/cargo" \
"${cache_root}/cargo-target/${{ matrix.cache-id }}" \
"${cache_root}/rustup" \
"${cache_root}/sccache" \
"${cache_root}/homebrew" \
"${cache_root}/apple/PackageCache" \
"${cache_root}/apple/SourcePackages" \
"${cache_root}/apple/DerivedData/${{ matrix.cache-id }}"
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 "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_SOURCE_PACKAGES=${cache_root}/apple/SourcePackages" >> "${GITHUB_ENV}"
echo "APPLE_DERIVED_DATA=${cache_root}/apple/DerivedData/${{ matrix.cache-id }}" >> "${GITHUB_ENV}"

View file

@ -33,9 +33,10 @@ jobs:
run: |
set -euo pipefail
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 "SCCACHE_DIR=${cache_root}/sccache" >> "${GITHUB_ENV}"
echo "CARGO_TARGET_DIR=${cache_root}/cargo-target/build-rust" >> "${GITHUB_ENV}"
- name: Test
shell: bash

1
.gitignore vendored
View file

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

View file

@ -74,16 +74,17 @@ CARGO_PATH="$(dirname $PROTOC):$CARGO_PATH"
# Run cargo without the various environment variables set by Xcode.
# Those variables can confuse cargo and the build scripts it runs.
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
EXTRA_ENV+=("${VAR_NAME}=${!VAR_NAME}")
fi
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}"
# Use `lipo` to merge the architectures together into BUILT_PRODUCTS_DIR
/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"

View file

@ -5,7 +5,12 @@ SECRETS := forgejo/admin-password \
forgejo/agent-ssh-key \
forgejo/nsc-token \
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)
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)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${SCRIPT_DIR}/_burrow-secrets.sh"
usage() {
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
/var/lib/burrow/intake with the ownership expected by the NixOS services.
Legacy path only: the current forge runtime consumes agenix secrets directly.
Options:
--host <user@host> SSH target (default: root@git.burrow.net)
--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
(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
(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
-h, --help Show this help text
EOF
}
HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}"
SSH_KEY="${BURROW_FORGE_SSH_KEY:-${REPO_ROOT}/intake/agent_at_burrow_net_ed25519}"
PASSWORD_FILE="${BURROW_FORGE_PASSWORD_FILE:-${REPO_ROOT}/intake/forgejo_pass_contact_at_burrow_net.txt}"
AGENT_KEY_FILE="${BURROW_FORGE_AGENT_KEY_FILE:-${REPO_ROOT}/intake/agent_at_burrow_net_ed25519}"
SSH_KEY="${BURROW_FORGE_SSH_KEY:-}"
PASSWORD_FILE="${BURROW_FORGE_PASSWORD_FILE:-}"
AGENT_KEY_FILE="${BURROW_FORGE_AGENT_KEY_FILE:-}"
KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}"
VERIFY=1
cleanup() {
burrow_cleanup_secret_tmpfiles
}
trap cleanup EXIT
while [[ $# -gt 0 ]]; do
case "$1" in
--host)
@ -67,12 +75,29 @@ done
mkdir -p "$(dirname "${KNOWN_HOSTS_FILE}")"
for path in "${SSH_KEY}" "${PASSWORD_FILE}" "${AGENT_KEY_FILE}"; do
if [[ ! -s "${path}" ]]; then
echo "required file missing or empty: ${path}" >&2
exit 1
fi
done
SSH_KEY="$(
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"
)"
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=(
-i "${SSH_KEY}"

View file

@ -3,6 +3,8 @@ 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() {
cat <<'EOF'
@ -12,17 +14,22 @@ Run a post-boot verification pass against the Burrow forge host.
Options:
--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
-h, --help Show this help text
EOF
}
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}"
EXPECT_NSC=0
cleanup() {
burrow_cleanup_secret_tmpfiles
}
trap cleanup EXIT
while [[ $# -gt 0 ]]; do
case "$1" in
--host)
@ -51,10 +58,17 @@ done
mkdir -p "$(dirname "${KNOWN_HOSTS_FILE}")"
if [[ ! -f "${SSH_KEY}" ]]; then
echo "forge SSH key not found: ${SSH_KEY}" >&2
SSH_KEY="$(
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
fi
}
ssh \
-i "${SSH_KEY}" \

View file

@ -1,6 +1,11 @@
#!/usr/bin/env bash
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() {
cat <<'EOF'
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
--ipv4 <address> IPv4 address for the A record
--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
default: auto
--proxied <true|false> Whether to proxy through Cloudflare
@ -25,10 +30,15 @@ EOF
ZONE_NAME=""
RECORD_NAME=""
IPV4=""
TOKEN_FILE="intake/cloudflare-token.txt"
TOKEN_FILE="${CLOUDFLARE_TOKEN_FILE:-}"
TTL_VALUE="auto"
PROXIED="false"
cleanup() {
burrow_cleanup_secret_tmpfiles
}
trap cleanup EXIT
while [[ $# -gt 0 ]]; do
case "$1" in
--zone)
@ -71,11 +81,16 @@ if [[ -z "${ZONE_NAME}" || -z "${RECORD_NAME}" || -z "${IPV4}" ]]; then
usage >&2
exit 2
fi
if [[ ! -f "${TOKEN_FILE}" ]]; then
echo "Cloudflare token file not found: ${TOKEN_FILE}" >&2
TOKEN_FILE="$(
burrow_resolve_secret_file \
"${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
fi
}
if [[ ! "${IPV4}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
echo "Invalid IPv4 address: ${IPV4}" >&2

View file

@ -5,6 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=Scripts/_burrow-flake.sh
source "${SCRIPT_DIR}/_burrow-flake.sh"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${SCRIPT_DIR}/_burrow-secrets.sh"
usage() {
cat <<'EOF'
@ -18,7 +20,7 @@ Defaults:
Environment:
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
}
@ -28,6 +30,7 @@ ALLOW_DIRTY=0
BURROW_FLAKE_TMPDIRS=()
cleanup() {
burrow_cleanup_secret_tmpfiles
burrow_cleanup_flake_tmpdirs
}
trap cleanup EXIT
@ -71,21 +74,17 @@ if [[ ${ALLOW_DIRTY} -ne 1 ]] && [[ -n "$(git status --short)" ]]; then
fi
FORGE_HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}"
FORGE_SSH_KEY="${BURROW_FORGE_SSH_KEY:-}"
if [[ -z "${FORGE_SSH_KEY}" ]]; then
if [[ -f "${REPO_ROOT}/intake/agent_at_burrow_net_ed25519" ]]; then
FORGE_SSH_KEY="${REPO_ROOT}/intake/agent_at_burrow_net_ed25519"
else
FORGE_SSH_KEY="${HOME}/.ssh/agent_at_burrow_net_ed25519"
fi
fi
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
FORGE_SSH_KEY="$(
burrow_resolve_secret_file \
"${REPO_ROOT}" \
"${BURROW_FORGE_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 "Unable to resolve the forge SSH key." >&2
exit 1
fi
}
FORGE_KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}"
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
source "${SCRIPT_DIR}/_burrow-flake.sh"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${SCRIPT_DIR}/_burrow-secrets.sh"
DEFAULT_CONFIG="burrow-forge"
DEFAULT_FLAKE="."
DEFAULT_LOCATION="hel1"
DEFAULT_ARCHITECTURE="x86"
DEFAULT_TOKEN_FILE="${REPO_ROOT}/intake/hetzner-api-token.txt"
DEFAULT_TOKEN_FILE=""
CONFIG="${HCLOUD_IMAGE_CONFIG:-${DEFAULT_CONFIG}}"
FLAKE="${HCLOUD_IMAGE_FLAKE:-${DEFAULT_FLAKE}}"
@ -30,6 +32,13 @@ NIX_BUILD_FLAGS=()
BURROW_FLAKE_TMPDIRS=()
LOCAL_STORE_DIR=""
cleanup() {
burrow_cleanup_secret_tmpfiles
burrow_cleanup_flake_tmpdirs
}
trap cleanup EXIT
usage() {
cat <<'EOF'
Usage: Scripts/hcloud-upload-nixos-image.sh [options]
@ -42,7 +51,7 @@ Options:
--location <code> Hetzner location for the temporary upload server (default: hel1)
--architecture <x86|arm> CPU architecture of the image (default: x86)
--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
--output-hash <hash> Stable hash label for --artifact-path uploads
--builder-spec <string> Complete builders string passed to nix build
@ -125,6 +134,17 @@ while [[ $# -gt 0 ]]; do
esac
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() {
burrow_cleanup_flake_tmpdirs
if [[ -n "${LOCAL_STORE_DIR}" && -d "${LOCAL_STORE_DIR}" ]]; then

View file

@ -2,6 +2,9 @@
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() {
cat <<'EOF'
@ -31,7 +34,7 @@ Options:
-h, --help Show this help text.
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
}
@ -43,10 +46,15 @@ IMAGE="ubuntu-24.04"
CONFIG="burrow-forge"
FLAKE="."
UPLOAD_LOCATION=""
TOKEN_FILE="${HCLOUD_TOKEN_FILE:-intake/hetzner-api-token.txt}"
TOKEN_FILE="${HCLOUD_TOKEN_FILE:-}"
YES=0
SSH_KEYS=("contact@burrow.net" "agent@burrow.net")
cleanup() {
burrow_cleanup_secret_tmpfiles
}
trap cleanup EXIT
if [[ $# -gt 0 ]]; then
case "$1" in
show|create|delete|recreate|build-image|create-from-image|recreate-from-image)
@ -110,10 +118,16 @@ while [[ $# -gt 0 ]]; do
esac
done
if [[ ! -f "${TOKEN_FILE}" ]]; then
echo "Hetzner API token file not found: ${TOKEN_FILE}" >&2
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
fi
}
if [[ -z "${UPLOAD_LOCATION}" ]]; then
UPLOAD_LOCATION="${LOCATION}"

View file

@ -6,11 +6,13 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=Scripts/_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}"
FLAKE="${HCLOUD_IMAGE_FLAKE:-.}"
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_MACHINE_TYPE="${NSC_MACHINE_TYPE:-linux/amd64:32x64}"
NSC_BUILDER_DURATION="${NSC_BUILDER_DURATION:-4h}"
@ -26,6 +28,13 @@ EXTRA_LABELS=()
BURROW_FLAKE_TMPDIRS=()
BUILDER_ID=""
cleanup() {
burrow_cleanup_secret_tmpfiles
burrow_cleanup_flake_tmpdirs
}
trap cleanup EXIT
usage() {
cat <<'EOF'
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)
--flake <path> Flake path to build from (default: .)
--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)
--ssh-host <host> Namespace SSH endpoint (default: ssh.ord2.namespace.so)
--duration <ttl> Namespace builder lifetime (default: 4h)
@ -126,6 +135,17 @@ while [[ $# -gt 0 ]]; do
esac
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() {
if [[ -n "${BUILDER_ID}" && -n "${NSC_BIN}" ]]; then
"${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
source "${SCRIPT_DIR}/_burrow-flake.sh"
# shellcheck source=Scripts/_burrow-secrets.sh
source "${SCRIPT_DIR}/_burrow-secrets.sh"
usage() {
cat <<'EOF'
Usage: Scripts/provision-forgejo-nsc.sh [options]
Generate Burrow forgejo-nsc runtime inputs in intake/ and optionally refresh the
Namespace token from the currently logged-in namespace account.
Generate Burrow forgejo-nsc runtime inputs and refresh the authoritative
`secrets/forgejo/*.age` files, optionally refreshing the Namespace token from
the currently logged-in namespace account.
Options:
--host <user@host> SSH target used to mint the Forgejo PAT.
Default: root@git.burrow.net
--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.
--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)
--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-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.
EOF
}
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:-}"
KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}"
REFRESH_TOKEN=1
@ -39,8 +43,12 @@ CONTACT_USER="${FORGEJO_CONTACT_USER:-contact}"
SCOPE_OWNER="${FORGEJO_SCOPE_OWNER:-hackclub}"
SCOPE_NAME="${FORGEJO_SCOPE_NAME:-burrow}"
BURROW_FLAKE_TMPDIRS=()
WRITE_INTAKE=0
TMP_DIR=""
cleanup() {
[[ -n "${TMP_DIR}" ]] && rm -rf "${TMP_DIR}" >/dev/null 2>&1 || true
burrow_cleanup_secret_tmpfiles
burrow_cleanup_flake_tmpdirs
}
trap cleanup EXIT
@ -79,6 +87,10 @@ while [[ $# -gt 0 ]]; do
SCOPE_NAME="${2:?missing value for --scope-name}"
shift 2
;;
--write-intake)
WRITE_INTAKE=1
shift
;;
-h|--help)
usage
exit 0
@ -97,13 +109,15 @@ burrow_require_cmd nix
burrow_require_cmd ssh
burrow_require_cmd python3
if [[ ! -f "${SSH_KEY}" ]]; then
echo "forge SSH key not found: ${SSH_KEY}" >&2
exit 1
fi
mkdir -p "${REPO_ROOT}/intake"
chmod 700 "${REPO_ROOT}/intake"
SSH_KEY="$(
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"
)"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/burrow-forgejo-nsc.XXXXXX")"
flake_ref="$(burrow_prepare_flake_ref "${REPO_ROOT}")"
if [[ -z "${NSC_BIN}" ]]; then
@ -128,13 +142,16 @@ if [[ ! -x "${NSC_BIN}" ]]; then
exit 1
fi
token_file="${REPO_ROOT}/intake/forgejo_nsc_token.txt"
dispatcher_out="${REPO_ROOT}/intake/forgejo_nsc_dispatcher.yaml"
autoscaler_out="${REPO_ROOT}/intake/forgejo_nsc_autoscaler.yaml"
token_file="${TMP_DIR}/forgejo_nsc_token.txt"
dispatcher_out="${TMP_DIR}/forgejo_nsc_dispatcher.yaml"
autoscaler_out="${TMP_DIR}/forgejo_nsc_autoscaler.yaml"
dispatcher_src="${REPO_ROOT}/services/forgejo-nsc/deploy/dispatcher.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
raw_token_file="$(mktemp)"
trap 'rm -f "${raw_token_file}"; cleanup' EXIT
@ -155,7 +172,13 @@ Path(os.environ["TOKEN_FILE"]).write_text(
PY
rm -f "${raw_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'
import json
import os
@ -271,6 +294,24 @@ PY
chmod 600 "${dispatcher_out}" "${autoscaler_out}"
echo "Rendered intake/forgejo_nsc_token.txt, intake/forgejo_nsc_dispatcher.yaml, and intake/forgejo_nsc_autoscaler.yaml."
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}" "${token_secret}" "${token_file}"
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}."

View file

@ -5,12 +5,12 @@ usage() {
cat <<'EOF'
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.
Options:
--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.
--no-restart Copy files only.
-h, --help Show this help text.
@ -19,12 +19,21 @@ EOF
SCRIPT_DIR="$(cd "$(dirname "$0")" && 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}"
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}"
ROTATE_PAT=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
case "$1" in
@ -68,18 +77,41 @@ burrow_require_cmd() {
burrow_require_cmd ssh
burrow_require_cmd scp
if [[ ! -f "${SSH_KEY}" ]]; then
echo "forge SSH key not found: ${SSH_KEY}" >&2
exit 1
fi
SSH_KEY="$(
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"
)"
if [[ "${ROTATE_PAT}" -eq 1 ]]; then
"${SCRIPT_DIR}/provision-forgejo-nsc.sh" --host "${HOST}" --ssh-key "${SSH_KEY}"
fi
token_file="${REPO_ROOT}/intake/forgejo_nsc_token.txt"
dispatcher_file="${REPO_ROOT}/intake/forgejo_nsc_dispatcher.yaml"
autoscaler_file="${REPO_ROOT}/intake/forgejo_nsc_autoscaler.yaml"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/burrow-nsc-sync.XXXXXX")"
token_file="$(
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
if [[ ! -s "${path}" ]]; then
@ -96,12 +128,12 @@ ssh_opts=(
)
remote_tmp="$(ssh "${ssh_opts[@]}" "${HOST}" "mktemp -d")"
cleanup() {
cleanup_remote() {
if [[ -n "${remote_tmp:-}" ]]; then
ssh "${ssh_opts[@]}" "${HOST}" "rm -rf '${remote_tmp}'" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
trap 'cleanup_remote; cleanup' EXIT
scp "${ssh_opts[@]}" \
"${token_file}" \

View file

@ -3,17 +3,22 @@
set -euo pipefail
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() {
cat <<'EOF'
Usage:
Tools/forwardemail-custom-s3.sh \
--domain burrow.net \
--api-token-file intake/forwardemail_api_token.txt \
--api-token-file secrets/forwardemail/api-token.age \
--s3-endpoint https://<endpoint> \
--s3-region <region> \
--s3-bucket <bucket> \
--s3-access-key-file intake/hetzner-s3-user.txt \
--s3-secret-key-file intake/hetzner-s3-secret.txt
--s3-access-key-file secrets/forwardemail/hetzner-s3-user.age \
--s3-secret-key-file secrets/forwardemail/hetzner-s3-secret.age
Options:
--domain <domain> Forward Email domain to update.
@ -54,13 +59,18 @@ read_secret() {
printf '%s' "$value"
}
cleanup() {
burrow_cleanup_secret_tmpfiles
}
trap cleanup EXIT
domain=""
api_token_file=""
api_token_file="${FORWARDEMAIL_API_TOKEN_FILE:-}"
s3_endpoint=""
s3_region=""
s3_bucket=""
s3_access_key_file=""
s3_secret_key_file=""
s3_access_key_file="${FORWARDEMAIL_S3_ACCESS_KEY_FILE:-}"
s3_secret_key_file="${FORWARDEMAIL_S3_SECRET_KEY_FILE:-}"
test_only=false
while [[ $# -gt 0 ]]; do
@ -108,16 +118,38 @@ while [[ $# -gt 0 ]]; do
done
[[ -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_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_access_key_file" || "$test_only" == true ]] || fail "--s3-access-key-file is required unless --test-only is set"
[[ -n "$s3_secret_key_file" || "$test_only" == true ]] || fail "--s3-secret-key-file is required unless --test-only is set"
api_token_file="$(
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"
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
require_file "$s3_access_key_file"
require_file "$s3_secret_key_file"

View file

@ -6,6 +6,7 @@ import argparse
import datetime as dt
import hashlib
import hmac
import subprocess
import sys
import textwrap
from pathlib import Path
@ -13,11 +14,38 @@ from urllib.parse import urlencode, urlparse
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:
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:
raise SystemExit(f"error: empty secret file: {path}")
raise SystemExit(f"error: empty secret file: {file_path}")
return value
@ -212,12 +240,12 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--region", default="hel1", help="S3 region.")
parser.add_argument(
"--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.",
)
parser.add_argument(
"--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.",
)
parser.add_argument(

View file

@ -26,11 +26,14 @@ Forward Email also documents these operational constraints:
## Burrow Secret Layout
Present in `intake/` today:
Authoritative secrets now live in:
- `intake/forwardemail_api_token.txt`
- `intake/hetzner-s3-user.txt`
- `intake/hetzner-s3-secret.txt`
- `secrets/forwardemail/api-token.age`
- `secrets/forwardemail/hetzner-s3-user.age`
- `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 object storage region: `hel1`
- Hetzner bucket used for Forward Email backups: `burrow`
@ -69,12 +72,12 @@ Example:
```sh
Tools/forwardemail-custom-s3.sh \
--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-region hel1 \
--s3-bucket burrow \
--s3-access-key-file intake/hetzner-s3-user.txt \
--s3-secret-key-file intake/hetzner-s3-secret.txt
--s3-access-key-file secrets/forwardemail/hetzner-s3-user.age \
--s3-secret-key-file secrets/forwardemail/hetzner-s3-secret.age
```
Retest an existing domain configuration without rewriting it:
@ -82,7 +85,7 @@ Retest an existing domain configuration without rewriting it:
```sh
Tools/forwardemail-custom-s3.sh \
--domain burrow.net \
--api-token-file intake/forwardemail_api_token.txt \
--api-token-file secrets/forwardemail/api-token.age \
--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`.
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>`.
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.
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`.
@ -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://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 `/`
- 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.
- 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`.

View file

@ -9,11 +9,19 @@ For the Forgejo Namespace Cloud runtime:
- `secrets/forgejo/nsc-token.age`
- `secrets/forgejo/nsc-dispatcher-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:
- `make secret name=forgejo/nsc-token`
- `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
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";
forge = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAlkGo4lwpwIIZ0J01KjTuJuf/U/wGgy4/aKwPIUzutL root@burrow-forge";
operatorSecrets = [ contact agent ];
forgeAutomation = [ contact agent forge ];
in {
"secrets/forgejo/admin-password.age".publicKeys = forgeAutomation;
@ -10,4 +11,9 @@ in {
"secrets/forgejo/nsc-token.age".publicKeys = forgeAutomation;
"secrets/forgejo/nsc-dispatcher-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,
generate a Namespace token from the logged-in namespace account, and render
bootstrap artifacts into `intake/forgejo_nsc_{dispatcher,autoscaler}.yaml` plus
`intake/forgejo_nsc_token.txt`. The token file is emitted as JSON with a
`bearer_token` field so both the Compute API path and the `nsc` CLI fallback can
consume the same secret material.
generate a Namespace token from the logged-in Namespace account, and refresh
`secrets/forgejo/{nsc-token,nsc-dispatcher-config,nsc-autoscaler-config}.age`.
The token file is emitted as JSON with a `bearer_token` field so both the
Compute API path and the `nsc` CLI fallback can consume the same secret
material. Use `--write-intake` only when you explicitly need local plaintext
debug copies.
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-autoscaler-config.age`
After refreshing the intake files, re-encrypt them into `secrets/forgejo/*.age`
and deploy the forge host so `config.age.secrets.*` updates the live paths for
`services.burrow.forge`, `services.burrow.forgeRunner`, and
`services.burrow.forgejoNsc`.
After refreshing the encrypted secrets, deploy the forge host so
`config.age.secrets.*` updates the live paths for `services.burrow.forge`,
`services.burrow.forgeRunner`, and `services.burrow.forgejoNsc`.
Run it next to the dispatcher:

View file

@ -602,6 +602,18 @@ if ! mkdir -p "/Users/runner/.cache/act" 2>/dev/null; then
fi
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
echo "curl is required" >&2
@ -622,14 +634,18 @@ export PATH="${PWD}/bin:${PATH}"
runner_version="v12.6.4"
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_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}"
mkdir -p "${runner_src_dir}"
curl -fsSL "${runner_src_url}" -o "${runner_src_tgz}"
tar -xzf "${runner_src_tgz}" -C "${runner_src_dir}" --strip-components=1
if [[ ! -f "${runner_src_tgz_path}" ]]; then
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)"
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
go_tgz="${toolchain}.darwin-arm64.tar.gz"
go_url="https://go.dev/dl/${go_tgz}"
curl -fsSL "${go_url}" -o "${go_tgz}"
tar -xzf "${go_tgz}"
go_tgz_path="${cache_root}/downloads/${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 PATH="${GOROOT}/bin:${PATH}"
fi
export GOPATH="${PWD}/.gopath"
export GOMODCACHE="${PWD}/.gomodcache"
export GOCACHE="${PWD}/.gocache"
mkdir -p "${GOPATH}" "${GOMODCACHE}" "${GOCACHE}"
(cd "${runner_src_dir}" && go build -o "${workdir}/bin/forgejo-runner" .)
chmod +x "${workdir}/bin/forgejo-runner"
(cd "${runner_src_dir}" && go build -o "${runner_bin_cache}" .)
chmod +x "${runner_bin_cache}"
fi
ln -sf "${runner_bin_cache}" "${workdir}/bin/forgejo-runner"
cat > runner.yaml <<'EOF'
log:
level: info