Files
Gitea/vm/config/Caddyfile.tmpl
T
JMR-devandClaude Opus 5.5 973c1bbcd3 Keep the ACME contact email in Secret Manager
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>
2026-10-10 04:04:18 -05:00

112 lines
3.7 KiB
Cheetah

# Rendered by vm/bootstrap.sh -> /etc/caddy/Caddyfile
#
# Two compile-time plugins are doing the work here:
#
# googleclouddns -- ACME DNS-01, so issuance and renewal never need inbound 80
# coraza_waf -- OWASP Coraza with the Core Rule Set embedded in the binary
{
${CADDY_EMAIL}
admin 127.0.0.1:2019
# Required by coraza-caddy: Caddy has no built-in ordering for a third-party
# directive, and the WAF must run before anything that could act on the
# request. Applies inside handle blocks too.
order coraza_waf first
}
${DOMAIN} {
tls {
dns googleclouddns {
# Application Default Credentials come from the GCE metadata server.
# The VM service account holds roles/dns.admin scoped to this zone only.
gcp_project {env.GCP_PROJECT}
}
# Only used for propagation checks. If issuance stalls waiting for
# propagation, this is the knob to turn.
resolvers 8.8.8.8 8.8.4.4
}
encode zstd gzip
# Governs the git/LFS branch below. The WAF branch has its own, much smaller,
# SecRequestBodyLimit -- they apply to different routes and are not a mismatch
# to be "fixed".
request_body {
max_size 512MB
}
# ---------------------------------------------------------------------
# Git transport and LFS: deliberately NOT behind the WAF.
#
# Packfiles are binary and trip CRS's SQLi/XSS rules constantly, and
# coraza.conf-recommended's SecRequestBodyLimit would reject any push
# larger than ~12MB outright. Running CRS here does not harden anything;
# it just breaks git. Authentication still applies -- Gitea does that.
# ---------------------------------------------------------------------
@gittransport path_regexp ^/[^/]+/[^/]+/(?:info/refs|git-upload-pack|git-receive-pack|HEAD|objects/.*|info/lfs(?:/.*)?)$
handle @gittransport {
reverse_proxy ${GITEA_UPSTREAM} {
transport http {
read_timeout 900s
write_timeout 900s
}
}
}
# ---------------------------------------------------------------------
# Everything else -- web UI and API -- goes through the WAF.
# ---------------------------------------------------------------------
handle {
coraza_waf {
load_owasp_crs
directives `
Include @coraza.conf-recommended
Include @crs-setup.conf.example
Include @owasp_crs/*.conf
# DetectionOnly logs what it would have blocked without blocking.
# The fail2ban jail is enabled ONLY when this is On -- banning on
# detections that were never blocks would turn a false positive
# into an nftables ban, which is worse than the 403 it avoided.
SecRuleEngine ${WAF_MODE}
# Inspecting responses on a git host costs CPU and catches nothing
# worth catching.
SecResponseBodyAccess Off
# Truncate-and-inspect rather than reject: a large but legitimate
# attachment upload should not 413 because the WAF gave up.
SecRequestBodyLimitAction ProcessPartial
# RelevantOnly, not On. Auditing every request would pour the full
# request volume into journald and then into Cloud Logging.
SecAuditEngine RelevantOnly
# The default relevant-status is ^(?:5|4(?!04)), which audits every
# 401 -- and on a public git host unauthenticated API and web probes
# produce those constantly. That noise would bury the would-be blocks
# this log exists to surface. Narrowed to real WAF refusals and
# server errors; rule-triggered transactions are still audited on
# severity regardless of status, which is what keeps DetectionOnly
# useful.
SecAuditLogRelevantStatus "^(?:5[0-9]{2}|403)$"
SecAuditLog /dev/stdout
SecAuditLogFormat json
SecAuditLogParts ABIJDEFHZ
`
}
reverse_proxy ${GITEA_UPSTREAM} {
transport http {
read_timeout 900s
write_timeout 900s
}
}
}
log {
output stderr
format json
}
}