Files
JMR-devandClaude Opus 5 c0382d5d31 Gitea on GCE: podman quadlets, Pulumi, Cloud Build
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>
2026-08-18 21:45:33 -05:00

45 lines
1.3 KiB
Go

// Package project enables the Google APIs the rest of the stack depends on.
package project
import (
"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/projects"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
// Services are enabled explicitly so a fresh project works without anyone
// clicking through the console first.
var services = []string{
"compute.googleapis.com",
"dns.googleapis.com",
"artifactregistry.googleapis.com",
"cloudbuild.googleapis.com",
"secretmanager.googleapis.com",
"iap.googleapis.com",
"storage.googleapis.com",
"logging.googleapis.com",
"monitoring.googleapis.com",
"cloudscheduler.googleapis.com",
"iamcredentials.googleapis.com",
"oslogin.googleapis.com",
}
// EnableAPIs returns resources every other package should depend on, so nothing
// races an API that is still turning on.
func EnableAPIs(ctx *pulumi.Context) ([]pulumi.Resource, error) {
var out []pulumi.Resource
for _, s := range services {
svc, err := projects.NewService(ctx, "api-"+s, &projects.ServiceArgs{
Service: pulumi.String(s),
// Tearing down the stack should not disable APIs that other things
// in the project may be using.
DisableOnDestroy: pulumi.Bool(false),
DisableDependentServices: pulumi.Bool(false),
})
if err != nil {
return nil, err
}
out = append(out, svc)
}
return out, nil
}