The Makefile derives PROJECT and ZONE from `pulumi config get`, which
reads the stack from the GCS backend and so needs Application Default
Credentials. Without them the lookup fails silently and every gcloud
command runs with `--project=`. The passphrase is not needed for
plaintext config values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gitea:acmeEmail sat in Pulumi.prod.yaml, which this public repository
publishes, and was then copied into instance metadata. It now lives in a
gitea-acme-email secret instead, read by the VM when it renders the
Caddyfile.
- scripts/bootstrap.sh creates the secret empty and prints how to set
it, the same as github-pat: the address is chosen, not generated.
- Pulumi grants the VM secretAccessor on it and nothing more. It is kept
out of secrets.Names, whose members also get secretVersionAdder and are
mapped to `gitea generate secret` by vm/bootstrap.sh.
- The gitea:acmeEmail config key and the acme-email metadata entry are
gone.
- vm/bootstrap.sh renders the whole `email` directive. If the secret is
unreadable it renders a comment instead and warns: Caddy still issues
certificates under an account with no contact address, whereas an
empty `email` would fail to parse and leave nothing serving TLS. Same
directive-or-comment pattern as CADDY_PUBLISH_PORTS and CADDY_SYSCTL.
README setup gains the secret step, plus two that were missing: ADC
login (Pulumi's GCS backend and provider do not use the gcloud login),
and exporting PULUMI_CONFIG_PASSPHRASE from Secret Manager before
`stack init`. Without the latter, init prompts for a new passphrase and
the stack is encrypted with a key Cloud Build's infra trigger never sees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-hosted Gitea on a single e2-small AlmaLinux 10 VM in us-east1,
serving gitea.jasonmross.dev.
Runtime is podman quadlets (systemd .container/.network/.volume units).
Both images are built on Debian 13: Gitea from a GPG-verified release
binary, and Caddy from an xcaddy build carrying the Google Cloud DNS
provider (ACME DNS-01) and the Coraza WAF with the OWASP CRS embedded.
Infrastructure is a Pulumi program in Go against a GCS state backend.
Cloud Build handles CI: a push trigger for images, one for infra, and a
weekly scheduled rebuild. Everything Cloud Build touches is 2nd gen.
Notable design decisions, each documented where it lives:
- Quadlets track a floating :prod tag. AutoUpdate=registry compares
digests for a tag, so a digest-pinned image silently disables
auto-updates.
- Git transport and LFS bypass the WAF. With the bypass removed, a plain
git push returns 403 -- packfiles trip CRS reliably.
- gitea:wafMode drives both SecRuleEngine and whether the fail2ban jail
acting on WAF verdicts exists. Banning on detections that were never
blocks would turn a tuning false positive into an nftables ban.
- fail2ban bans at the nftables prerouting hook. Published container
ports are DNAT'd and never traverse INPUT, where the stock actions
install their rules.
- The DNS zone, backup bucket, and Gitea signing secrets are not
Pulumi-owned, so pulumi destroy cannot take them with it.
- The podman subnet is pinned because it is what Gitea's
REVERSE_PROXY_TRUSTED_PROXIES names.
Three update layers: dnf5-automatic for the OS, podman-auto-update with
health-gated rollback for containers, and a weekly image rebuild that
gives the second layer something to pull.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>