With DNS resolution fixed, issuance failed at the challenge:
presenting for challenge: adding temporary record for zone
"gitea.jasonmross.dev.": googleapi: Error 403: Forbidden
Testing with the VM service account's own token: managedZones/main and
its rrsets return 200, but managedZones (list) returns 403. The
googleclouddns plugin resolves the domain to a zone by listing the
project's managed zones, and listing is a project-level permission that
the zone-scoped dns.admin binding cannot grant.
Grant roles/dns.reader on the project. It adds read access only, in a
project that holds this single zone; every write stays zone-scoped. A
custom role with just dns.managedZones.list was the alternative, but
managing it would need iam.roleAdmin on the cb-infra Pulumi runner,
which widens a far more powerful identity to narrow a read-only one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first `make build` failed before any step ran:
INVALID_ARGUMENT: could not resolve source: cb-image@... does not
have storage.objects.get access to ... gitea-496920_cloudbuild/source/...
`gcloud builds submit` uploads the source tarball to <project>_cloudbuild
and the build, running as the user-specified cb-image@, must read it
back. Nothing grants that. Binding on that bucket is not an option: gcloud
creates it on the first submit, after `pulumi up` has already run.
Project-wide objectViewer would also open the backup, config and state
buckets.
Pulumi now owns <project>-gitea-build-source, readable by cb-image@ and
nothing else, with a 7-day delete rule since each tarball is read once.
`make build` stages there via --gcs-source-staging-dir. Triggered builds
fetch source through the GitHub connection and are unaffected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fills in the values scripts/bootstrap.sh and the existing project
provide: the project id, the Cloud DNS zone resource name (main, holding
gitea.jasonmross.dev), and the cb-infra Pulumi runner account.
encryptionsalt is from `pulumi stack init` with the passphrase already in
Secret Manager (pulumi-config-passphrase), so Cloud Build's infra trigger
can open the stack with the same key.
githubAppInstallationId stays "0" for now: GitHubConfigured() is then
false and the Cloud Build triggers are skipped until the GitHub App is
installed.
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>
Eight open Dependabot alerts, all transitive through the Pulumi SDK:
grpc < 1.82.2 / <= 1.83.0 (#3, #4 high; #5 medium) -> v1.83.2
otel/sdk, otlptrace, otlptracegrpc <= 1.44.0 (#6-8) -> v1.45.0
otel/sdk/log, otlplog/otlploggrpc < 0.21.0 (#9-10) -> v0.21.0
This supersedes Dependabot PR #1, which bumps grpc only and predates the
otel alerts.
otel/log v0.21 changed its API, so the otelslog bridge has to move with
it: v0.18.0 no longer compiles against it. v0.20.1 is the release built
for that otel line.
govulncheck then reported ten reachable standard-library and x/net
vulnerabilities disclosed since the last pin (net/http HTTP/2 and CONNECT
handling, net/textproto, crypto/tls ECH, os on Windows), all fixed in
go1.26.9 and golang.org/x/net v0.60.0. Raising the toolchain floor is the
same remedy as before; x/net v0.60.0 requires go 1.26, which lifts the
go directive from 1.25.11 to 1.26.0.
The Pulumi SDK and pulumi-gcp direct dependencies are deliberately left
where they are, so this does not also change provider behaviour ahead of
the first deploy.
govulncheck now reports no reachable vulnerabilities. GO-2026-5932
(x/crypto/openpgp, no fix available, not called) remains, as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two open Dependabot alerts, both against github.com/go-git/go-git/v6, a
transitive dependency of github.com/pulumi/pulumi/sdk/v3 by way of
common/workspace:
CVE-2026-71556 (high, 7.1) worktree operations may follow symlinks,
allowing writes outside the worktree
CVE-2026-71557 (medium, 6.3) malicious reference names may resolve
outside the reference storage
Both are fixed in v6.0.0-alpha.5. The Pulumi SDK is already at its latest
release (v3.258.0) and still requires alpha.4, so bumping the direct
dependency does not help; an explicit minimum-version entry is the
remedy. go-billy comes along as a consequence.
Also pins toolchain go1.26.6. govulncheck reported six reachable
standard-library vulnerabilities from building on go1.26.4 -- ASN.1, TLS,
net/url, net/http and os -- all fixed in 1.26.5/1.26.6. Dependabot does
not track the Go standard library, so nothing alerted on these, but they
were reachable from pulumi.Run and dns.LookupManagedZone. Pinning the
floor in go.mod means every build gets a patched stdlib rather than
whatever the build host happens to have.
govulncheck now reports no vulnerabilities.
Not addressed, deliberately: GO-2026-5932 flags golang.org/x/crypto/openpgp
as unmaintained and unsafe by design. It has no fixed version, and our
code does not call it. Upgrading x/crypto to v0.55.0 was tried and does
not clear it, so that change was dropped to keep this diff minimal.
Co-Authored-By: Claude Opus 5 (1M context) <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>