test: container lab for the two-tier apply
A disposable ubuntu container, a fake private tier and a fake bootstrap endpoint, so the whole documented path — chezmoi init --apply, dotup pick, dotup private, cmp apply, dotsecrets — can run end to end without touching a real machine or a real credential. The fake tier mirrors the real one's structure (seven secrets and one alias) because dotsecrets is copied verbatim and the "8 exports, not 7" assertion depends on that cardinality; its ids are sequential and obviously synthetic. check-verbatim.sh keeps the fake tier's copies of shipped files honest, and snapshot.sh records file modes so a 644 where a 600 belongs is a diff.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# chezmoi configuration for the PRIVATE tier.
|
||||
#
|
||||
# Seven questions, asked once at `chezmoi init` and never again -- promptStringOnce
|
||||
# reads the value already in the config if there is one. Nothing here is fetched
|
||||
# over a network: your own name is not a rotatable secret, and making `apply`
|
||||
# depend on an API call to learn your email address would be absurd.
|
||||
#
|
||||
# There is deliberately no `encryption` key and no [age] section. The one secret
|
||||
# in this repo is an ordinary 600-mode file; see README.md for why that is the
|
||||
# cheaper answer than a per-machine root secret that was already 0 bytes on one
|
||||
# machine in three.
|
||||
#
|
||||
# WHERE THIS FILE LANDS -- read before changing the aliases.
|
||||
# `dotup` runs `chezmoi init --apply --source ~/.local/share/dotfiles-private
|
||||
# -c ~/.config/chezmoi/private.toml <url>`, so this template renders to
|
||||
# private.toml and the public tier keeps chezmoi.toml. The `cmp` alias carries
|
||||
# the same -c. Two sources, two configs, no overlap.
|
||||
#
|
||||
# It was not always so, and the bug is worth remembering. Both tiers rendered
|
||||
# to the DEFAULT config path, so re-running the public installer overwrote this
|
||||
# file and took the seven answers below with it. Nothing failed at the time:
|
||||
# the templates degrade politely when their data is missing -- config.local
|
||||
# emits a comment telling you to re-run init rather than failing the apply --
|
||||
# so the symptom was `git commit` not knowing who you are, days later, with
|
||||
# nothing pointing back at the install that caused it. A loud failure would
|
||||
# have been a smaller bug.
|
||||
|
||||
[data]
|
||||
gitName = {{ promptStringOnce . "gitName" "git user.name" | quote }}
|
||||
gitEmail = {{ promptStringOnce . "gitEmail" "git user.email" | quote }}
|
||||
gitSigningKey = {{ promptStringOnce . "gitSigningKey" "git signing key id (blank for none)" "" | quote }}
|
||||
|
||||
giteaWanSsh = {{ promptStringOnce . "giteaWanSsh" "gitea WAN ssh prefix" "ssh://git@git.example.invalid:222/" | quote }}
|
||||
giteaLanSsh = {{ promptStringOnce . "giteaLanSsh" "gitea LAN ssh prefix" "ssh://git@10.99.99.99:2223/" | quote }}
|
||||
giteaWanWeb = {{ promptStringOnce . "giteaWanWeb" "gitea WAN web url" "https://git.example.invalid/" | quote }}
|
||||
giteaLanWeb = {{ promptStringOnce . "giteaLanWeb" "gitea LAN web url" "http://10.99.99.99:3001/" | quote }}
|
||||
@@ -0,0 +1,43 @@
|
||||
# Fake stand-in for the private tier's bws map. Same STRUCTURE as the real one
|
||||
# -- seven secrets and one alias -- because `dotsecrets` is copied verbatim and
|
||||
# the "8 exports, not 7" assertion depends on that cardinality.
|
||||
#
|
||||
# The ids are sequential and obviously synthetic. They are not secrets in the
|
||||
# real repo either (a UUID fetches nothing without the machine token), but a
|
||||
# fake tree should be unmistakably fake at a glance.
|
||||
[bws]
|
||||
project = "00000000-0000-4000-8000-000000000000"
|
||||
|
||||
[[bws.secrets]]
|
||||
env = "LAB_ALPHA_API_KEY"
|
||||
id = "00000000-0000-4000-8000-000000000001"
|
||||
|
||||
[[bws.secrets]]
|
||||
env = "LAB_BRAVO_API_KEY"
|
||||
id = "00000000-0000-4000-8000-000000000002"
|
||||
|
||||
[[bws.secrets]]
|
||||
env = "LAB_CHARLIE_API_KEY"
|
||||
id = "00000000-0000-4000-8000-000000000003"
|
||||
|
||||
[[bws.secrets]]
|
||||
env = "LAB_DELTA_API_KEY"
|
||||
id = "00000000-0000-4000-8000-000000000004"
|
||||
|
||||
[[bws.secrets]]
|
||||
env = "LAB_ECHO_API_KEY"
|
||||
id = "00000000-0000-4000-8000-000000000005"
|
||||
|
||||
[[bws.secrets]]
|
||||
env = "LAB_FOXTROT_API_KEY"
|
||||
id = "00000000-0000-4000-8000-000000000006"
|
||||
|
||||
[[bws.secrets]]
|
||||
env = "LAB_GOLF_API_KEY"
|
||||
id = "00000000-0000-4000-8000-000000000007"
|
||||
|
||||
# One value under two names, exactly as ZAI_API_KEY aliases Z_AI_API_KEY. This
|
||||
# is why the real machine has 7 secrets and 8 exports.
|
||||
[[bws.aliases]]
|
||||
name = "LAB_GOLF_ALIAS_KEY"
|
||||
from = "LAB_GOLF_API_KEY"
|
||||
@@ -0,0 +1,91 @@
|
||||
# .chezmoiignore -- PRIVATE tier.
|
||||
#
|
||||
# This is gitignore syntax. A `#` in the middle of a line becomes part of the
|
||||
# pattern, silently producing an entry that matches nothing, so every comment
|
||||
# in this file is on its own line. Learned in phase 1; it does not announce
|
||||
# itself.
|
||||
#
|
||||
# Ignoring changes what chezmoi manages. It never removes or modifies a file
|
||||
# on any machine.
|
||||
|
||||
# Repo documentation and the endpoint artifact, not dotfiles. Without these
|
||||
# they land as ~/README.md and ~/bootstrap.env.example.
|
||||
README.md
|
||||
bootstrap.env.example
|
||||
|
||||
# Runbook scripts for the bootstrap endpoint. Operator tooling that is run by
|
||||
# hand a few times a year, not configuration that belongs in a home directory.
|
||||
# They live here rather than in the public tier because they name the endpoint
|
||||
# host and route, and the public repo is cloneable by strangers.
|
||||
ops
|
||||
ops/**
|
||||
|
||||
# chezmoi's own config directory. Never manage the thing that configures the
|
||||
# manager.
|
||||
.config/chezmoi
|
||||
.config/chezmoi/**
|
||||
|
||||
# Nested git checkouts.
|
||||
**/.git
|
||||
**/.git/**
|
||||
|
||||
# Generated at apply time by run_after_50-secrets.sh, never tracked anywhere.
|
||||
# If this line is ever removed, `chezmoi add` on a finished machine sweeps the
|
||||
# seven API keys straight into the repo.
|
||||
.config/zsh/secrets.zsh
|
||||
|
||||
# The per-machine delta that Q5 puts opposite the base settings.json below.
|
||||
# Claude Code writes this one; chezmoi must never fight it.
|
||||
.claude/settings.local.json
|
||||
.claude.json
|
||||
|
||||
# `bws` writes a 600-mode state cache here on its first successful call --
|
||||
# ~/.config/bws/state/<uuid>, about 2 KB. It is derived from the access token
|
||||
# and belongs in no repository. Found by watching what appeared in a throwaway
|
||||
# home directory after the first apply, not by reading the documentation.
|
||||
.config/bws
|
||||
.config/bws/**
|
||||
|
||||
# The bws access token itself. `dotup` writes this from the bootstrap endpoint
|
||||
# before this repo is even cloned, so chezmoi has no business managing it: a
|
||||
# managed copy would overwrite a freshly-rotated token with a stale committed
|
||||
# one on the next apply. It was committed exactly once, in phase 4, by a
|
||||
# `chezmoi add` of the live file. Listing it here makes chezmoi decline the
|
||||
# same `add` rather than accept it silently.
|
||||
.config/bitwarden
|
||||
.config/bitwarden/**
|
||||
|
||||
# ---------------------------------------------------------------- ssh ---
|
||||
# Deny-by-default, then name the three things that travel. Q3 is answered as
|
||||
# "sync public keys, not private": public keys are not secret, private keys
|
||||
# never cross a network and are generated per machine.
|
||||
#
|
||||
# The wildcard is the point. An id_ed25519 generated on this machine tomorrow
|
||||
# matches `.ssh/*` and is matched by no negation below, so `chezmoi add ~/.ssh`
|
||||
# cannot sweep it up. New key files are excluded by default rather than
|
||||
# remembered about.
|
||||
#
|
||||
# ~/.ssh/known_hosts is excluded by the same rule, deliberately: it is a log of
|
||||
# where this machine has been, it rewrites itself constantly, and syncing it
|
||||
# would make `chezmoi status` permanently dirty.
|
||||
#
|
||||
# NOTE THE ABSENCE of `!.ssh/*.pub`. Public keys do travel -- that is what
|
||||
# ~/.ssh/pubkeys/ is for -- but NOT at ~/.ssh/id_ed25519.pub, and the
|
||||
# distinction is not pedantry:
|
||||
#
|
||||
# Every machine generates its own keypair. If this tier also wrote
|
||||
# id_ed25519.pub, that machine would end up with ITS private key sitting
|
||||
# next to SOMEONE ELSE'S public key under the matching name. `ssh-copy-id`
|
||||
# and every agent-only auth path read the .pub, so you would authorise the
|
||||
# wrong machine and watch it appear to work.
|
||||
#
|
||||
# The practical value of syncing public keys is authorized_keys -- a new box
|
||||
# accepting the keys you already have, with nothing to paste. That is carried
|
||||
# in full. ~/.ssh/pubkeys/*.pub is the archive of the keys you own, for pasting
|
||||
# into GitHub and Gitea, parked where ssh will never mistake one for the local
|
||||
# identity.
|
||||
.ssh/*
|
||||
!.ssh/config
|
||||
!.ssh/authorized_keys
|
||||
!.ssh/pubkeys
|
||||
!.ssh/pubkeys/*.pub
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/bin/sh
|
||||
# dotsecrets -- regenerate ~/.config/zsh/secrets.zsh from Bitwarden Secrets Manager.
|
||||
#
|
||||
# Needs no endpoint, no username and no password. The bootstrap exchange already
|
||||
# happened; what it left behind is ~/.config/bitwarden/bws-token, mode 600, and
|
||||
# that token is the whole input to this command. Run it whenever you rotate a
|
||||
# key in bws. `cmp apply` runs it too, via run_after_50-secrets.sh.
|
||||
#
|
||||
# THE ONE INVARIANT: a failed or partial fetch leaves a working secrets.zsh
|
||||
# exactly as it was. Every value is fetched into a 600-mode temp file first,
|
||||
# and that file is renamed over the real one only after all of them have
|
||||
# arrived. Six keys out of seven is a machine that was fine a moment ago and
|
||||
# now silently cannot reach one provider -- worse than a machine that says the
|
||||
# refresh failed and carries on with yesterday's keys.
|
||||
#
|
||||
# The temp file is created in the SAME DIRECTORY as the destination, not in
|
||||
# /tmp. `mv` across filesystems is copy-then-unlink, which has a window where
|
||||
# the destination is half-written; within one filesystem it is rename(2), which
|
||||
# has none. The atomicity this whole script is built around is a property of
|
||||
# rename(2), not of the word "mv".
|
||||
#
|
||||
# NOTHING IS EVER PRINTED. No value reaches stdout, stderr, argv or a log:
|
||||
# - values move from `bws` into a shell variable and from there into a file
|
||||
# through the `printf` BUILTIN, so they never appear in `ps`;
|
||||
# - bws's own stderr is discarded, because an error message is not worth the
|
||||
# risk of it quoting what it was handed;
|
||||
# - every failure message below names the ENV VAR, never the value.
|
||||
#
|
||||
# Generated by chezmoi from the PRIVATE tier. The env-var -> secret-id map is
|
||||
# .chezmoidata/bws.toml; UUIDs are identifiers, not secrets.
|
||||
|
||||
set -u
|
||||
|
||||
PROG=dotsecrets
|
||||
CFG="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
TOKEN_FILE="$CFG/bitwarden/bws-token"
|
||||
OUT="$CFG/zsh/secrets.zsh"
|
||||
TMP=""
|
||||
|
||||
warn() { printf '%s: %s\n' "$PROG" "$*" >&2; }
|
||||
|
||||
cleanup() { [ -n "$TMP" ] && rm -f "$TMP"; return 0; }
|
||||
trap cleanup EXIT
|
||||
trap 'cleanup; exit 130' INT
|
||||
trap 'cleanup; exit 143' TERM HUP
|
||||
|
||||
# Bail out without touching OUT. This is the entire point of the command.
|
||||
abort() {
|
||||
warn "$1"
|
||||
if [ -r "$OUT" ]; then
|
||||
warn "keeping the existing $OUT -- it was NOT modified"
|
||||
else
|
||||
warn "$OUT was not written; the shell starts without those keys"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
# POSIX single-quoting using builtins only, so a value never becomes an
|
||||
# argument to an external command and never becomes a line in `ps` output.
|
||||
# API keys do not contain apostrophes, but a quoting routine that is correct
|
||||
# only for the inputs you happen to have is not a quoting routine.
|
||||
shquote() {
|
||||
_sq_s=$1
|
||||
_sq_o=''
|
||||
while :; do
|
||||
case $_sq_s in
|
||||
*"'"*) ;;
|
||||
*) break ;;
|
||||
esac
|
||||
_sq_o="$_sq_o${_sq_s%%\'*}'\\''"
|
||||
_sq_s=${_sq_s#*\'}
|
||||
done
|
||||
printf "'%s%s'" "$_sq_o" "$_sq_s"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------ preconditions ---
|
||||
|
||||
[ -r "$TOKEN_FILE" ] || abort "no bws token at $TOKEN_FILE (public-only machine?)"
|
||||
|
||||
command -v bws >/dev/null 2>&1 \
|
||||
|| abort "bws is not installed or not on PATH -- https://bitwarden.com/help/secrets-manager-cli/"
|
||||
|
||||
BWS_ACCESS_TOKEN="$(cat "$TOKEN_FILE")"
|
||||
[ -n "$BWS_ACCESS_TOKEN" ] || abort "$TOKEN_FILE is empty"
|
||||
export BWS_ACCESS_TOKEN
|
||||
|
||||
mkdir -p "$CFG/zsh" || abort "cannot create $CFG/zsh"
|
||||
chmod 700 "$CFG/zsh" 2>/dev/null || :
|
||||
|
||||
# ---------------------------------------------------------------- the map ---
|
||||
# `env var name` `bws secret id`, rendered from .chezmoidata/bws.toml so that a
|
||||
# UUID is written down in exactly one place and it is not this script.
|
||||
SECRET_MAP='{{ range .bws.secrets }}
|
||||
{{ .env }} {{ .id }}{{ end }}'
|
||||
|
||||
# Second names for a value fetched once. One secret, two exported names: some
|
||||
# tools spell it Z_AI_API_KEY and some spell it ZAI_API_KEY.
|
||||
ALIAS_MAP='{{ range .bws.aliases }}
|
||||
{{ .name }} {{ .from }}{{ end }}'
|
||||
|
||||
# ------------------------------------------------------------- the fetch ---
|
||||
|
||||
umask 077
|
||||
TMP="$(mktemp "$CFG/zsh/.secrets.zsh.XXXXXXXX")" || abort "cannot create a temp file beside $OUT"
|
||||
chmod 600 "$TMP" || abort "cannot chmod the temp file"
|
||||
|
||||
{
|
||||
printf '# Generated from Bitwarden Secrets Manager. DO NOT EDIT, DO NOT COMMIT.\n'
|
||||
printf '# Regenerate with `dotsecrets`. Mode 600, in no repository.\n'
|
||||
printf '# Last refreshed: %s\n\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
} >"$TMP" || abort "cannot write to the temp file"
|
||||
|
||||
# `set -f` because the maps are split on IFS by `set --`, and an unglobbed
|
||||
# split would let a stray `*` in the source data expand against the cwd.
|
||||
# The loops run in this shell, not in a pipeline subshell, so `count` and the
|
||||
# remembered values survive them -- a `while read` on the right of a pipe is
|
||||
# the classic way to lose exactly the state this script needs.
|
||||
set -f
|
||||
count=0
|
||||
NL="$(printf '\n_')"
|
||||
NL=${NL%_}
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
set -- $SECRET_MAP
|
||||
while [ "$#" -ge 2 ]; do
|
||||
name=$1
|
||||
id=$2
|
||||
shift 2
|
||||
|
||||
# The name becomes part of a variable name below. It comes from a file in
|
||||
# this repo rather than from anywhere a stranger can reach, but a shell
|
||||
# variable name is close enough to code that it gets checked anyway.
|
||||
case $name in
|
||||
[A-Za-z_]*) ;;
|
||||
*) abort "invalid env var name in the secret map: $name" ;;
|
||||
esac
|
||||
case $name in
|
||||
*[!A-Za-z0-9_]*) abort "invalid env var name in the secret map: $name" ;;
|
||||
esac
|
||||
|
||||
# -o env prints `KEY=VALUE`, where KEY is the secret's own name in bws.
|
||||
# Comparing it to the name we asked for is a free integrity check on the
|
||||
# map: an id that points OPENAI_API_KEY at the Groq secret is caught here
|
||||
# rather than six months later as a confusing 401.
|
||||
line="$(bws secret get "$id" -o env 2>/dev/null)" \
|
||||
|| abort "could not fetch $name from bws (no network, or the token is wrong or revoked)"
|
||||
|
||||
# First line only, trimmed with parameter expansion rather than `head` or
|
||||
# `sed`: keeping the value out of every external process's stdin as well
|
||||
# as its argv costs one case statement.
|
||||
case $line in
|
||||
*"$NL"*) line=${line%%"$NL"*} ;;
|
||||
esac
|
||||
|
||||
case $line in
|
||||
"$name"=*) ;;
|
||||
*) abort "bws returned a different secret than $name -- check its id in .chezmoidata/bws.toml" ;;
|
||||
esac
|
||||
|
||||
value=${line#"$name"=}
|
||||
[ -n "$value" ] || abort "bws returned an empty value for $name"
|
||||
|
||||
printf 'export %s=%s\n' "$name" "$(shquote "$value")" >>"$TMP" \
|
||||
|| abort "cannot write to the temp file"
|
||||
|
||||
# Remember it for the alias pass. The value is expanded by the assignment,
|
||||
# not by `eval` -- eval only ever parses the variable NAME.
|
||||
eval "_v_$name=\$value"
|
||||
|
||||
count=$((count + 1))
|
||||
done
|
||||
|
||||
[ "$count" -gt 0 ] || abort ".chezmoidata/bws.toml carries no entries -- nothing to fetch"
|
||||
|
||||
# --------------------------------------------------------------- aliases ---
|
||||
# After the loop, so an alias can only reference a value that has already
|
||||
# arrived intact.
|
||||
alias_count=0
|
||||
# shellcheck disable=SC2086
|
||||
set -- $ALIAS_MAP
|
||||
while [ "$#" -ge 2 ]; do
|
||||
alias_name=$1
|
||||
source_name=$2
|
||||
shift 2
|
||||
alias_count=$((alias_count + 1))
|
||||
|
||||
eval "aliased=\${_v_$source_name:-}"
|
||||
[ -n "$aliased" ] \
|
||||
|| abort "alias $alias_name names $source_name, which is not in the secret map"
|
||||
|
||||
printf '\n# same value, second name expected by some tools\n' >>"$TMP" \
|
||||
|| abort "cannot write to the temp file"
|
||||
printf 'export %s=%s\n' "$alias_name" "$(shquote "$aliased")" >>"$TMP" \
|
||||
|| abort "cannot write to the temp file"
|
||||
done
|
||||
set +f
|
||||
|
||||
# ----------------------------------------------------------------- commit ---
|
||||
# Everything arrived. Only now does the real file change, and it changes in one
|
||||
# rename rather than a truncate followed by a write.
|
||||
chmod 600 "$TMP" || abort "cannot chmod the temp file"
|
||||
mv -f "$TMP" "$OUT" || abort "cannot rename the temp file into place"
|
||||
TMP=""
|
||||
|
||||
# $count is secrets FETCHED; aliases add further exports without another
|
||||
# fetch. Reporting only the first number against a file with more lines than
|
||||
# that reads like a bug in the generator. Say both.
|
||||
if [ "$alias_count" -gt 0 ]; then
|
||||
warn "wrote $OUT ($count secrets + $alias_count alias(es) = $((count + alias_count)) exports, mode 600)"
|
||||
else
|
||||
warn "wrote $OUT ($count secrets, mode 600)"
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,71 @@
|
||||
{{- $name := get . "gitName" -}}
|
||||
{{- $email := get . "gitEmail" -}}
|
||||
{{- $signing := get . "gitSigningKey" -}}
|
||||
{{- $wan := get . "giteaWanSsh" -}}
|
||||
{{- $lan := get . "giteaLanSsh" -}}
|
||||
; ~/.config/git/config.local -- PRIVATE tier.
|
||||
;
|
||||
; The other half of the seam. ~/.gitconfig comes from the public repo, carries
|
||||
; no [user], and ends with `[include] path = ~/.config/git/config.local`. Git
|
||||
; treats a missing include as a no-op, so a public-only machine reads the
|
||||
; public half and stops -- and `git commit` correctly refuses to guess who you
|
||||
; are.
|
||||
;
|
||||
; Everything here is identity, not configuration: it is the answer to "whose
|
||||
; machine is this", which is exactly the question the public tier must not be
|
||||
; able to answer.
|
||||
;
|
||||
; Values come from the [data] prompts in .chezmoi.toml.tmpl, asked once at
|
||||
; `chezmoi init`. Re-answer them with `cmp init` (see README).
|
||||
|
||||
{{ if and $name $email -}}
|
||||
[user]
|
||||
name = {{ $name }}
|
||||
email = {{ $email }}
|
||||
{{- if $signing }}
|
||||
signingkey = {{ $signing }}
|
||||
[commit]
|
||||
gpgsign = true
|
||||
[tag]
|
||||
gpgsign = true
|
||||
{{- end }}
|
||||
{{- else -}}
|
||||
; NO IDENTITY CONFIGURED.
|
||||
;
|
||||
; gitName and/or gitEmail are empty in the chezmoi config, which means either
|
||||
; you pressed enter through the prompts or something overwrote
|
||||
; ~/.config/chezmoi/chezmoi.toml after this tier was initialised. Re-run:
|
||||
;
|
||||
; chezmoi init -S ~/.local/share/dotfiles-private
|
||||
;
|
||||
; Until then git will refuse to commit, which is the correct complaint.
|
||||
{{- end }}
|
||||
|
||||
; Every remote in every repo you own is ssh. This rewrite is what lets a
|
||||
; copy-pasted https:// GitHub URL clone over the key you actually have, which
|
||||
; matters most on a machine built ten minutes ago. It lived in the old
|
||||
; dot_gitconfig; phase 3 removed it from the public tier because it names an
|
||||
; authentication method tied to your keys, not a neutral default.
|
||||
[url "git@github.com:"]
|
||||
insteadOf = https://github.com/
|
||||
|
||||
{{ if $wan -}}
|
||||
; Gitea clone/push shortcuts: git clone gitea:ben/repo.git
|
||||
[url "{{ $wan }}"]
|
||||
insteadOf = gitea:
|
||||
{{ end -}}
|
||||
{{ if $lan -}}
|
||||
[url "{{ $lan }}"]
|
||||
insteadOf = gitea-lan:
|
||||
{{ end }}
|
||||
{{- if and $wan $lan }}
|
||||
[alias]
|
||||
; Configure `origin` to push to BOTH gitea servers at once. Run once inside
|
||||
; a repo whose origin points at either gitea host:
|
||||
;
|
||||
; git dual-gitea
|
||||
;
|
||||
; After this, `git push` writes to WAN + LAN simultaneously. Fetch/pull
|
||||
; continues to use origin's existing fetch URL.
|
||||
dual-gitea = "!f() { url=$(git remote get-url origin) || { echo 'no origin remote' >&2; return 1; }; p=${url#gitea:}; p=${p#gitea-lan:}; p=${p#{{ $wan }}}; p=${p#{{ $lan }}}; if [ \"$p\" = \"$url\" ]; then echo \"origin is not a gitea remote: $url\" >&2; return 1; fi; git config --unset-all remote.origin.pushurl 2>/dev/null; git remote set-url --add --push origin \"gitea:$p\"; git remote set-url --add --push origin \"gitea-lan:$p\"; echo 'Dual-push configured on origin:'; git remote -v; }; f"
|
||||
{{- end }}
|
||||
@@ -0,0 +1,61 @@
|
||||
{{- $wanWeb := get . "giteaWanWeb" -}}
|
||||
{{- $lanWeb := get . "giteaLanWeb" -}}
|
||||
# ~/.config/zsh/local.zsh -- PRIVATE tier. Mode 600.
|
||||
#
|
||||
# Sourced by the public ~/.zshrc, guarded, near the end:
|
||||
#
|
||||
# [[ -r ${XDG_CONFIG_HOME:-$HOME/.config}/zsh/local.zsh ]] && source ...
|
||||
#
|
||||
# Absent on a public-only machine, where the guard makes it a silent no-op.
|
||||
# `_mac` and `_open` from .zshrc are still in scope by the time this runs, so
|
||||
# an alias moved here needs no rewriting.
|
||||
#
|
||||
# What belongs here: anything that names a host you own. Nothing that is a
|
||||
# secret -- those come from ~/.config/zsh/secrets.zsh, written by `dotsecrets`
|
||||
# and carried in no repository at all.
|
||||
#
|
||||
# Mode 600 rather than 644, for the same reason .zshrc and .zshenv are: this is
|
||||
# code your login shell executes, and a group-writable copy of it is arbitrary
|
||||
# code execution for anyone in your primary group.
|
||||
|
||||
# --------------------------------------------------------------- gitea ---
|
||||
# The web UI. The WAN host is a real name behind TLS; the LAN one is a bare
|
||||
# address on a port, reachable only from the house, which is precisely why it
|
||||
# cannot live in the public tier.
|
||||
{{ if $wanWeb }}alias gitea='_open {{ $wanWeb }}'{{ end }}
|
||||
{{ if $lanWeb }}alias gitea-lan='_open {{ $lanWeb }}'{{ end }}
|
||||
|
||||
# The clone/push shortcuts are git-side, not shell-side: `gitea:` and
|
||||
# `gitea-lan:` are url.insteadOf rewrites in ~/.config/git/config.local, so
|
||||
# `git clone gitea:ben/repo.git` works from any shell, not just this one.
|
||||
|
||||
# ------------------------------------------------------------- chezmoi ---
|
||||
# Two instances, one home directory. The public tier lays the base; the private
|
||||
# tier overlays identity on top. Both are ordinary chezmoi invocations with a
|
||||
# different --source, so every subcommand you know still works:
|
||||
#
|
||||
# cm status / cm diff / cm re-add ~/.zshrc public
|
||||
# cmp status / cmp diff / cmp apply private
|
||||
#
|
||||
# Each tier has its own source AND its own config file:
|
||||
#
|
||||
# public `chezmoi init --apply <url>`, no flags -> ~/.local/share/chezmoi
|
||||
# ~/.config/chezmoi/chezmoi.toml
|
||||
# private dotup, --source ... -c .../private.toml -> ~/.local/share/dotfiles-private
|
||||
# ~/.config/chezmoi/private.toml
|
||||
#
|
||||
# The -c is not cosmetic. Both tiers used to render their .chezmoi.toml.tmpl to
|
||||
# the same default path, so re-running the PUBLIC installer overwrote the config
|
||||
# holding this tier's seven promptStringOnce answers -- name, email, signing key,
|
||||
# the gitea addresses -- and they were gone. Silently: the templates degrade
|
||||
# politely when their data is missing, so the first symptom was `git commit` not
|
||||
# knowing who you are, days later and unconnected to the install that caused it.
|
||||
#
|
||||
# -S and -c must stay in step with dotup's PRIV_SRC and PRIV_CFG. If you move one,
|
||||
# move the other, or cmp reads a config that describes a different source tree.
|
||||
alias cm='chezmoi'
|
||||
alias cmp='chezmoi -S ${XDG_DATA_HOME:-$HOME/.local/share}/dotfiles-private -c ${XDG_CONFIG_HOME:-$HOME/.config}/chezmoi/private.toml'
|
||||
|
||||
# Refresh ~/.config/zsh/secrets.zsh from Bitwarden Secrets Manager on demand.
|
||||
# `cmp apply` does the same thing via run_after_50-secrets.sh.
|
||||
# dotsecrets
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBNye8EHJ7ijGBNbmvvY2DqzZ8pd88vlI4OOYcM7ZxBK lab@example.invalid
|
||||
@@ -0,0 +1,57 @@
|
||||
{{- $wan := get . "giteaWanSsh" | trimPrefix "ssh://git@" | trimSuffix "/" -}}
|
||||
{{- $lan := get . "giteaLanSsh" | trimPrefix "ssh://git@" | trimSuffix "/" -}}
|
||||
{{- $wanParts := $wan | splitList ":" -}}
|
||||
{{- $lanParts := $lan | splitList ":" -}}
|
||||
{{- $wanHost := $wanParts | first -}}
|
||||
{{- $lanHost := $lanParts | first -}}
|
||||
{{- $wanPort := ternary ($wanParts | last) "22" (gt (len $wanParts) 1) -}}
|
||||
{{- $lanPort := ternary ($lanParts | last) "22" (gt (len $lanParts) 1) -}}
|
||||
# ~/.ssh/config -- PRIVATE tier. Mode 600.
|
||||
#
|
||||
# HOST ALIASES ONLY. No key material of any kind travels in this repository,
|
||||
# in this tier or any other. Q3 is answered "sync public keys, not private":
|
||||
#
|
||||
# travels ~/.ssh/config, ~/.ssh/authorized_keys, ~/.ssh/pubkeys/*.pub
|
||||
# never id_ed25519, id_rsa, *.pem, anything without a .pub suffix
|
||||
#
|
||||
# The IdentityFile lines below name a key this machine generates for itself:
|
||||
#
|
||||
# ssh-keygen -t ed25519 -C "$(whoami)@$(hostname)"
|
||||
#
|
||||
# then paste ~/.ssh/id_ed25519.pub into GitHub and Gitea once. Ninety seconds
|
||||
# per machine, and a private key never crosses a network. .chezmoiignore denies
|
||||
# everything under .ssh/ by default and re-includes exactly three things, so a
|
||||
# key generated tomorrow cannot be swept in by a careless `chezmoi add ~/.ssh`.
|
||||
|
||||
Host *
|
||||
AddKeysToAgent yes
|
||||
ServerAliveInterval 60
|
||||
ServerAliveCountMax 3
|
||||
HashKnownHosts no
|
||||
|
||||
Host github.com
|
||||
HostName github.com
|
||||
User git
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
IdentitiesOnly yes
|
||||
{{ if $wanHost }}
|
||||
# Gitea over the WAN. A real hostname behind a real certificate; reachable
|
||||
# from any network, which is what makes it the primary for both repos.
|
||||
Host gitea {{ $wanHost }}
|
||||
HostName {{ $wanHost }}
|
||||
Port {{ $wanPort }}
|
||||
User git
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
IdentitiesOnly yes
|
||||
{{ end }}
|
||||
{{- if $lanHost }}
|
||||
# The same Gitea, reached over the LAN on a different port. Faster at home and
|
||||
# the only route if the WAN name is down. A bare address on a private network:
|
||||
# the single most obviously non-public line in this whole system.
|
||||
Host gitea-lan {{ $lanHost }}
|
||||
HostName {{ $lanHost }}
|
||||
Port {{ $lanPort }}
|
||||
User git
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
IdentitiesOnly yes
|
||||
{{ end }}
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBNye8EHJ7ijGBNbmvvY2DqzZ8pd88vlI4OOYcM7ZxBK lab@example.invalid
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/bin/sh
|
||||
# run_after_50-secrets.sh -- PRIVATE tier.
|
||||
#
|
||||
# Regenerate ~/.config/zsh/secrets.zsh at the end of every `cmp apply`.
|
||||
#
|
||||
# The `run_after_` prefix is load-bearing. It guarantees this runs once every
|
||||
# managed file is on disk, which resolves the deadlock the old repo had: the
|
||||
# old .zshrc fetched secrets on line 62 using a token that line 58's file had
|
||||
# not written yet. Ordering by prefix rather than by hope.
|
||||
#
|
||||
# This script is a wrapper and nothing else. The work lives in `dotsecrets`,
|
||||
# which you can also run by hand after rotating a key in bws -- one
|
||||
# implementation, so the scheduled path and the manual path cannot drift apart
|
||||
# and start disagreeing about what a valid secrets.zsh looks like.
|
||||
#
|
||||
# IT ALWAYS EXITS 0. A machine on a train with no signal must still be able to
|
||||
# finish an apply. `dotsecrets` leaves an existing secrets.zsh untouched when
|
||||
# it cannot fetch, so the failure mode here is "yesterday's keys and a warning"
|
||||
# rather than "the apply died half way through".
|
||||
|
||||
set -u
|
||||
{{ if ne .chezmoi.destDir .chezmoi.homeDir }}
|
||||
# Rendered only when this apply is aimed somewhere other than the home
|
||||
# directory -- `chezmoi apply --destination /tmp/whatever`, which is how this
|
||||
# tier gets tested. `dotsecrets` resolves its own paths from $HOME, so running
|
||||
# it here would reach straight past the throwaway destination and rewrite the
|
||||
# real ~/.config/zsh/secrets.zsh. A test that mutates the machine it is
|
||||
# protecting is not a test.
|
||||
printf 'run_after_50-secrets: destination is {{ .chezmoi.destDir }}, not the home directory; skipping\n' >&2
|
||||
exit 0
|
||||
{{ end }}
|
||||
DOTSECRETS="{{ .chezmoi.homeDir }}/.local/bin/dotsecrets"
|
||||
|
||||
if [ ! -x "$DOTSECRETS" ]; then
|
||||
printf 'run_after_50-secrets: %s is missing or not executable; skipping\n' \
|
||||
"$DOTSECRETS" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! "$DOTSECRETS"; then
|
||||
printf 'run_after_50-secrets: refresh failed (see above). The apply itself is fine.\n' >&2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user