Files
Gitea/infra/pkg/dns/dns.go
T
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

65 lines
2.1 KiB
Go

// Package dns manages records in the PRE-EXISTING Cloud DNS zone.
//
// The zone itself is deliberately not a Pulumi resource: it already exists and
// is delegated, so importing it would put a `pulumi destroy` one keystroke away
// from deleting live DNS.
package dns
import (
"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/compute"
"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/dns"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"gitea-infra/pkg/config"
)
type DNS struct {
Zone *dns.LookupManagedZoneResult
Record *dns.RecordSet
}
// New looks up the existing managed zone, points an A record at the static
// address, and grants the VM service account permission to write ACME challenge
// records -- scoped to this one zone rather than the whole project.
func New(
ctx *pulumi.Context,
cfg *config.Config,
addr *compute.Address,
vmServiceAccountEmail pulumi.StringOutput,
deps []pulumi.Resource,
) (*DNS, error) {
zone, err := dns.LookupManagedZone(ctx, &dns.LookupManagedZoneArgs{
Name: cfg.DNSZone,
Project: &cfg.Project,
}, pulumi.DependsOn(deps))
if err != nil {
return nil, err
}
rec, err := dns.NewRecordSet(ctx, "gitea-a", &dns.RecordSetArgs{
Name: pulumi.String(cfg.FQDN()),
ManagedZone: pulumi.String(zone.Name),
Type: pulumi.String("A"),
// Short enough that a VM rebuild behind a new address is not a long
// outage, long enough not to hammer the resolvers.
Ttl: pulumi.Int(300),
Rrdatas: pulumi.StringArray{addr.Address},
}, pulumi.DependsOn(deps))
if err != nil {
return nil, err
}
// Caddy writes and deletes _acme-challenge TXT records here for DNS-01.
// Zone-scoped rather than project-wide: a compromised VM should not be able
// to repoint unrelated domains.
if _, err := dns.NewDnsManagedZoneIamMember(ctx, "gitea-vm-dns-admin", &dns.DnsManagedZoneIamMemberArgs{
ManagedZone: pulumi.String(zone.Name),
Role: pulumi.String("roles/dns.admin"),
Member: pulumi.Sprintf("serviceAccount:%s", vmServiceAccountEmail),
}, pulumi.DependsOn(deps)); err != nil {
return nil, err
}
return &DNS{Zone: zone, Record: rec}, nil
}