diff --git a/Makefile b/Makefile index 9b7ecef..f694631 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,8 @@ PROJECT ?= $(shell cd infra && pulumi config get gcp:project 2>/dev/null) REGION ?= $(shell cd infra && pulumi config get gcp:region 2>/dev/null || echo us-east1) -ZONE ?= $(shell cd infra && pulumi config get gitea:zone 2>/dev/null || echo $(REGION)-a) +# -b, matching the default in infra/pkg/config: us-east1 has no -a zone. +ZONE ?= $(shell cd infra && pulumi config get gitea:zone 2>/dev/null || echo $(REGION)-b) VM ?= gitea-vm .PHONY: help @@ -19,9 +20,11 @@ bootstrap: ## One-time project setup (run before the first `make up`) fmt: ## Format Go sources cd infra && gofmt -w . +# -o gitea-infra: Pulumi.yaml points the go runtime at this prebuilt binary, so +# Pulumi runs it rather than compiling. Without it preview/up fail outright. .PHONY: check check: ## Build and vet the Pulumi program, and syntax-check the shell scripts - cd infra && go build ./... && go vet ./... + cd infra && go build -o gitea-infra . && go vet ./... bash -n vm/bootstrap.sh scripts/bootstrap.sh @command -v shellcheck >/dev/null && shellcheck -S warning vm/bootstrap.sh scripts/bootstrap.sh || echo "shellcheck not installed -- skipped" @@ -41,12 +44,20 @@ up: check ## Apply the infrastructure # to cb-image@, not to whatever default Cloud Build account this project # happens to have -- and on newer projects the legacy default does not exist. # Without this the images push fine and the rollout step fails. +# --gcs-source-staging-dir: running as cb-image@, the build must be able to read +# the uploaded source. Pulumi grants that on this bucket only; the default +# _cloudbuild bucket is unreadable to it and the build fails at +# "could not resolve source". +# SHORT_SHA: Cloud Build fills it in only for triggered builds. For `builds +# submit` it is empty, and image.yaml's `--tag :$SHORT_SHA` becomes an +# invalid reference that fails the build. .PHONY: build build: ## Build and roll out the container images via Cloud Build gcloud builds submit --config cloudbuild/image.yaml --project $(PROJECT) \ --region=$(REGION) \ --service-account=projects/$(PROJECT)/serviceAccounts/cb-image@$(PROJECT).iam.gserviceaccount.com \ - --substitutions=_REGION=$(REGION),_ZONE=$(ZONE) + --gcs-source-staging-dir=gs://$(PROJECT)-gitea-build-source/source \ + --substitutions=_REGION=$(REGION),_ZONE=$(ZONE),SHORT_SHA=$(shell git rev-parse --short=7 HEAD) .PHONY: rollout rollout: ## Pull the latest :prod images onto the VM right now diff --git a/README.md b/README.md index 161b3bd..acdf077 100644 --- a/README.md +++ b/README.md @@ -47,14 +47,23 @@ printf %s '' | gcloud secrets versions add github-pat --data-file=- --pro # 3. Confirm the Cloud DNS zone is authoritative. DNS-01 cannot work otherwise. dig NS gitea.jasonmross.dev -# 4. Configure and apply. +# 4. The ACME contact address. Kept in Secret Manager, not stack config, so it +# stays out of this public repo; the VM reads it when rendering the Caddyfile. +printf %s 'you@example.com' | gcloud secrets versions add gitea-acme-email --data-file=- --project + +# 5. Configure and apply. Pulumi's GCS backend and Google provider use +# Application Default Credentials, not your gcloud login. +gcloud auth application-default login +# Use the passphrase bootstrap.sh generated. Letting `stack init` prompt for a +# new one encrypts the stack with a key Cloud Build's infra trigger never sees. +export PULUMI_CONFIG_PASSPHRASE=$(gcloud secrets versions access latest \ + --secret=pulumi-config-passphrase --project ) cd infra pulumi login gs://-pulumi-state pulumi stack init prod pulumi config set gcp:project pulumi config set gitea:domain gitea.jasonmross.dev pulumi config set gitea:dnsZone # gcloud dns managed-zones list -pulumi config set gitea:acmeEmail you@example.com pulumi config set gitea:githubOwner pulumi config set gitea:githubAppInstallationId pulumi config set gitea:infraBuildServiceAccount cb-infra@.iam.gserviceaccount.com @@ -62,10 +71,10 @@ pulumi config set gitea:infraBuildServiceAccount cb-infra@.iam.gserv pulumi config set gitea:wafMode DetectionOnly pulumi up -# 5. First image build. Until this runs, the :prod images do not exist. +# 6. First image build. Until this runs, the :prod images do not exist. cd .. && make build -# 6. Create the admin user. +# 7. Create the admin user. make ssh sudo podman exec -u 1000 gitea gitea admin user create \ -c /etc/gitea/app.ini --admin --username --email --random-password @@ -73,7 +82,7 @@ sudo podman exec -u 1000 gitea gitea admin user create \ ### Expected on the first run, not a bug -Between step 4 and step 5 the `:prod` images do not exist yet, so `gitea.service` +Between step 5 and step 6 the `:prod` images do not exist yet, so `gitea.service` and `caddy.service` crash-loop. That is intentional: the units carry `Restart=always` with `StartLimitIntervalSec=0`, so they recover on their own within 30 seconds of the first successful push. Likewise, `app.ini` is not @@ -91,6 +100,11 @@ make backup # on-demand gitea dump to GCS make ssh # shell via IAP ``` +The targets read the project and zone from the Pulumi stack, which needs +Application Default Credentials (`gcloud auth application-default login`). +Without them `pulumi config get` fails quietly and gcloud runs with an empty +`--project=`; pass `PROJECT=` to skip the lookup. + A push to `main` under `image/**` builds, pushes, rolls out, and gates on `/api/healthz`. A push under `infra/**` or `vm/**` runs `pulumi up` and then re-syncs the VM configuration. Anything else does nothing. diff --git a/cloudbuild/image.yaml b/cloudbuild/image.yaml index 19369c3..2cc4bf6 100644 --- a/cloudbuild/image.yaml +++ b/cloudbuild/image.yaml @@ -36,6 +36,9 @@ steps: - id: build-gitea name: gcr.io/cloud-builders/docker + # Both Dockerfiles are written for BuildKit (`# syntax=`, COPY --chmod). This + # builder image defaults to the legacy builder, which rejects --chmod. + env: [DOCKER_BUILDKIT=1] entrypoint: bash args: - -c @@ -52,6 +55,7 @@ steps: - id: build-caddy name: gcr.io/cloud-builders/docker + env: [DOCKER_BUILDKIT=1] entrypoint: bash # xcaddy runs inside the Dockerfile's golang builder stage, so the plain # docker builder is all this step needs -- no Go toolchain out here. diff --git a/cloudbuild/infra.yaml b/cloudbuild/infra.yaml index 1e4470d..5d33152 100644 --- a/cloudbuild/infra.yaml +++ b/cloudbuild/infra.yaml @@ -29,6 +29,10 @@ steps: - -c - | set -euo pipefail + # Pulumi.yaml points the go runtime at a prebuilt binary, so Pulumi + # runs ./gitea-infra rather than compiling the program itself. + go build -o gitea-infra . + # Self-managed GCS backend: no external SaaS dependency, and the state # bucket is versioned so history is recoverable. pulumi login "gs://$PROJECT_ID-pulumi-state" diff --git a/docs/runbook.md b/docs/runbook.md index f248723..976d664 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -58,6 +58,13 @@ curl -vI https://gitea.jasonmross.dev # valid Let's Encrypt cert DNS-01 means renewal does not need inbound port 80 at all. That is testable: temporarily remove the `gitea-allow-web` port 80 rule and force a renewal. +The ACME account and certificates live in the `caddy-data` podman volume, under +`/var/lib/containers` on the **boot** disk, not the data disk. Replacing the +instance therefore re-registers and re-issues on first start. That is fine +occasionally, but Let's Encrypt allows 5 duplicate certificates per week, so +several replacements in a few days can lock issuance out until the window +rolls over. + ### fail2ban — drill it, do not trust the status output ```bash diff --git a/infra/Pulumi.prod.yaml b/infra/Pulumi.prod.yaml index 2005cb3..5effe9a 100644 --- a/infra/Pulumi.prod.yaml +++ b/infra/Pulumi.prod.yaml @@ -4,15 +4,12 @@ # infrastructure metadata. The Gitea application secrets live in Secret Manager # and are never read by this program. config: - gcp:project: CHANGEME-gitea-project-id + gcp:project: gitea-496920 gcp:region: us-east1 - gitea:domain: gitea.jasonmross.dev # The Cloud DNS *resource* name of the existing managed zone, which is not # necessarily the DNS name. `gcloud dns managed-zones list` to find it. - gitea:dnsZone: CHANGEME-managed-zone-name - gitea:acmeEmail: CHANGEME@example.com - + gitea:dnsZone: main # us-east1 has zones b, c and d -- there is no us-east1-a. gitea:zone: us-east1-b # e2-small: 2 shared vCPU, 2 GB RAM. See docs/runbook.md ("Memory on a 2 GB @@ -20,23 +17,20 @@ config: gitea:machineType: e2-small gitea:bootDiskGb: "20" gitea:dataDiskGb: "30" - gitea:appName: Gitea gitea:requireSigninView: "false" gitea:podmanSubnet: 10.89.10.0/24 - # Coraza WAF: On | DetectionOnly | Off. # Start in DetectionOnly, review what it flags (docs/waf.md), then switch to # On. The fail2ban jail that bans on WAF verdicts follows this value. gitea:wafMode: DetectionOnly - # Cloud Build source. The GitHub App installation id comes from the URL of the # app's settings page after you install it on the repository. gitea:githubOwner: JMR-dev gitea:githubRepo: Gitea gitea:githubAppInstallationId: "0" gitea:githubPatSecret: github-pat - # Created by scripts/bootstrap.sh before the first `pulumi up`, because it is # the identity that runs Pulumi and therefore cannot be created by Pulumi. - gitea:infraBuildServiceAccount: CHANGEME@CHANGEME.iam.gserviceaccount.com + gitea:infraBuildServiceAccount: cb-infra@gitea-496920.iam.gserviceaccount.com +encryptionsalt: v1:pIXPmM64Bzc=:v1:0pkb4B2RVM5LFu2v:ZEPaG4RB9ySlpmaHkaBy8v6FQ3paHg== diff --git a/infra/go.mod b/infra/go.mod index 1fb4473..12892c4 100644 --- a/infra/go.mod +++ b/infra/go.mod @@ -1,8 +1,8 @@ module gitea-infra -go 1.25.11 +go 1.26.0 -toolchain go1.26.6 +toolchain go1.26.9 require ( github.com/pulumi/pulumi-gcp/sdk/v9 v9.34.1 @@ -40,14 +40,14 @@ require ( github.com/go-git/gcfg/v2 v2.0.2 // indirect github.com/go-git/go-billy/v6 v6.0.0-alpha.2 // indirect github.com/go-git/go-git/v6 v6.0.0-alpha.5 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.2.5 // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -93,30 +93,30 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/collector/featuregate v1.53.0 // indirect go.opentelemetry.io/collector/pdata v1.53.0 // indirect - go.opentelemetry.io/contrib/bridges/otelslog v0.18.0 // indirect - go.opentelemetry.io/otel v1.44.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 // indirect - go.opentelemetry.io/otel/log v0.19.0 // indirect - go.opentelemetry.io/otel/metric v1.44.0 // indirect - go.opentelemetry.io/otel/sdk v1.43.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect - go.opentelemetry.io/otel/trace v1.44.0 // indirect - go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.opentelemetry.io/contrib/bridges/otelslog v0.20.1 // indirect + go.opentelemetry.io/otel v1.46.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 // indirect + go.opentelemetry.io/otel/log v0.22.0 // indirect + go.opentelemetry.io/otel/metric v1.46.0 // indirect + go.opentelemetry.io/otel/sdk v1.45.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.21.0 // indirect + go.opentelemetry.io/otel/trace v1.46.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.54.0 // indirect - golang.org/x/mod v0.38.0 // indirect - golang.org/x/net v0.57.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.40.0 // indirect - golang.org/x/tools v0.47.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect - google.golang.org/grpc v1.82.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/mod v0.41.0 // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/term v0.46.0 // indirect + golang.org/x/text v0.42.0 // indirect + golang.org/x/tools v0.49.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/frand v1.4.2 // indirect diff --git a/infra/go.sum b/infra/go.sum index 61f1ee6..3ea3e11 100644 --- a/infra/go.sum +++ b/infra/go.sum @@ -76,8 +76,8 @@ github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1/go.mod h1:ECf1MqJlBdYpKggBrO github.com/go-git/go-git/v6 v6.0.0-alpha.5 h1:sE+OlkHgYWNMVmN1s9sR7uyFgsWLtxcNWse/vBYKxRE= github.com/go-git/go-git/v6 v6.0.0-alpha.5/go.mod h1:3IjhiZnM+uBmUrOGSeqrJpsmi4Vd0H2NZO/uK2a7d0s= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= @@ -98,8 +98,8 @@ github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:E github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,9 +128,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -176,7 +175,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435cARxCW6q9gc0S/Yxz7Mkd38pOb0= github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE= @@ -207,8 +205,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= @@ -231,32 +229,32 @@ go.opentelemetry.io/collector/internal/testutil v0.147.0 h1:DFlRxBRp23/sZnpTITK2 go.opentelemetry.io/collector/internal/testutil v0.147.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= go.opentelemetry.io/collector/pdata v1.53.0 h1:DlYDbRwammEZaxDZHINx5v0n8SEOVNniPbi6FRTlVkA= go.opentelemetry.io/collector/pdata v1.53.0/go.mod h1:LRSYGNjKXaUrZEwZv3Yl+8/zV2HmRGKXW62zB2bysms= -go.opentelemetry.io/contrib/bridges/otelslog v0.18.0 h1:hhPGP3zvvy1xWT9RTy970wlniSxFttBIsAK1gvMguJM= -go.opentelemetry.io/contrib/bridges/otelslog v0.18.0/go.mod h1:twJF7inoMza6kxMcF8JOdL3mPmtOZu7GEr34CUNE6Dg= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 h1:mq/Qcf28TWz719lE3/hMB4KkyDuLJIvgJnFGcd0kEUI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0/go.mod h1:yk5LXEYhsL2htyDNJbEq7fWzNEigeEdV5xBF/Y+kAv0= -go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= -go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= -go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= -go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= -go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= -go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.opentelemetry.io/contrib/bridges/otelslog v0.20.1 h1:5sHc4ToTFjfSZCtGAAM6jPunICAmJX73htv372T4ipc= +go.opentelemetry.io/contrib/bridges/otelslog v0.20.1/go.mod h1:oa6kgvyz/3GYW04dohd0++xJIH4xdQY8PAbpeCMaM8M= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 h1:WseeVYf5dJZTsyPiyW5L14k5qsSibqXAMTSiFEDiWr0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0/go.mod h1:SiLZnQS6Qk2eCpvr2CH/XMAOa64TWGXxEZJZCpD2Lmc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI= +go.opentelemetry.io/otel/log v0.22.0 h1:5DBNnfvaJ6CVdkJ+Jle8Tzs50aSSv49TXGj9XRsEYw0= +go.opentelemetry.io/otel/log v0.22.0/go.mod h1:gzOt/R67vF2GniAqWu8Qv0SXy89f71muHcrkz76PCdc= +go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= +go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= +go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= +go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= +go.opentelemetry.io/otel/sdk/log v0.21.0 h1:QsE7XSR0ktQdKmRKGnR+f1ObGF32WG+7MER/P9KgmYc= +go.opentelemetry.io/otel/sdk/log v0.21.0/go.mod h1:m9mApjCoD2/1QuKCAptjv+BrG9WKOvQLVdNx+iBldTo= +go.opentelemetry.io/otel/sdk/log/logtest v0.21.0 h1:X+JBBgKlswCGYsmgL0CnoUUtlE//VB345c84jYAYkdQ= +go.opentelemetry.io/otel/sdk/log/logtest v0.21.0/go.mod h1:HD1575K8e6sIFBBDd5tZB3t9DlMytWXq9FuR+Y4rfjE= +go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= +go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= +go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= +go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.opentelemetry.io/proto/slim/otlp v1.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE= go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI= go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8= @@ -270,31 +268,33 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -305,34 +305,34 @@ golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= +golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc= +google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/infra/main.go b/infra/main.go index b81ea6a..b13f2f1 100644 --- a/infra/main.go +++ b/infra/main.go @@ -63,6 +63,9 @@ func main() { if err := iam.GrantSecrets(ctx, cfg, accounts, secrets.Names); err != nil { return err } + if err := iam.GrantSecretRead(ctx, cfg, accounts, secrets.ACMEEmail); err != nil { + return err + } buckets, err := storage.New(ctx, cfg, vmDir, apis) if err != nil { @@ -71,6 +74,9 @@ func main() { if err := iam.GrantBuckets(ctx, accounts, buckets.Config, buckets.Backup); err != nil { return err } + if err := iam.GrantBuildSource(ctx, accounts, buckets.BuildSource); err != nil { + return err + } // The zone already exists and is delegated; this only adds the A record // and the zone-scoped permission Caddy needs for DNS-01. @@ -97,6 +103,7 @@ func main() { ctx.Export("registry", pulumi.Sprintf("%s/%s/%s", cfg.ARHost(), cfg.Project, registry.RepoID)) ctx.Export("configBucket", buckets.Config.Name) ctx.Export("backupBucket", buckets.Backup.Name) + ctx.Export("buildSourceBucket", buckets.BuildSource.Name) ctx.Export("configHash", pulumi.String(buckets.ConfigHash)) ctx.Export("vmServiceAccount", accounts.VM.Email) ctx.Export("imageServiceAccount", accounts.Image.Email) diff --git a/infra/pkg/compute/compute.go b/infra/pkg/compute/compute.go index d200c16..e5194c7 100644 --- a/infra/pkg/compute/compute.go +++ b/infra/pkg/compute/compute.go @@ -169,7 +169,6 @@ func New( "image-caddy": pulumi.String(imageCaddy), "domain": pulumi.String(cfg.Domain), - "acme-email": pulumi.String(cfg.ACMEEmail), "app-name": pulumi.String(cfg.AppName), "require-signin-view": pulumi.String(strconv.FormatBool(cfg.RequireSigninView)), diff --git a/infra/pkg/config/config.go b/infra/pkg/config/config.go index aa1a95d..865abab 100644 --- a/infra/pkg/config/config.go +++ b/infra/pkg/config/config.go @@ -18,7 +18,6 @@ type Config struct { Domain string DNSZone string - ACMEEmail string AppName string PodmanCIDR string @@ -51,7 +50,6 @@ func Load(ctx *pulumi.Context) (*Config, error) { Zone: c.Get("zone"), Domain: c.Require("domain"), DNSZone: c.Require("dnsZone"), - ACMEEmail: c.Require("acmeEmail"), AppName: c.Get("appName"), PodmanCIDR: c.Get("podmanSubnet"), diff --git a/infra/pkg/dns/dns.go b/infra/pkg/dns/dns.go index e3e0c67..bc7911f 100644 --- a/infra/pkg/dns/dns.go +++ b/infra/pkg/dns/dns.go @@ -8,6 +8,7 @@ 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-gcp/sdk/v9/go/gcp/projects" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "gitea-infra/pkg/config" @@ -60,5 +61,19 @@ func New( return nil, err } + // The zone-scoped grant is not enough on its own: the googleclouddns plugin + // maps the domain to a zone by LISTING the project's managed zones, and a + // list is a project-level permission that no zone binding can confer. Without + // this, presenting the challenge fails with a bare 403. dns.reader adds + // read-only access and nothing else, and every write stays scoped to the zone + // above. + if _, err := projects.NewIAMMember(ctx, "gitea-vm-dns-reader", &projects.IAMMemberArgs{ + Project: pulumi.String(cfg.Project), + Role: pulumi.String("roles/dns.reader"), + Member: pulumi.Sprintf("serviceAccount:%s", vmServiceAccountEmail), + }, pulumi.DependsOn(deps)); err != nil { + return nil, err + } + return &DNS{Zone: zone, Record: rec}, nil } diff --git a/infra/pkg/iam/iam.go b/infra/pkg/iam/iam.go index 73436c1..f58f3c9 100644 --- a/infra/pkg/iam/iam.go +++ b/infra/pkg/iam/iam.go @@ -123,12 +123,7 @@ func GrantRegistry(ctx *pulumi.Context, cfg *config.Config, a *Accounts, repo *a // scripts/bootstrap.sh, not by Pulumi; see package secrets for why. func GrantSecrets(ctx *pulumi.Context, cfg *config.Config, a *Accounts, names []string) error { for _, name := range names { - if _, err := secretmanager.NewSecretIamMember(ctx, "vm-read-"+name, &secretmanager.SecretIamMemberArgs{ - Project: pulumi.String(cfg.Project), - SecretId: pulumi.String(name), - Role: pulumi.String("roles/secretmanager.secretAccessor"), - Member: pulumi.Sprintf("serviceAccount:%s", a.VM.Email), - }); err != nil { + if err := GrantSecretRead(ctx, cfg, a, name); err != nil { return err } // vm/bootstrap.sh's safety net adds a version if one is somehow missing. @@ -144,6 +139,30 @@ func GrantSecrets(ctx *pulumi.Context, cfg *config.Config, a *Accounts, names [] return nil } +// GrantSecretRead gives the VM read-only access to one secret. On its own it is +// for operator-supplied values the VM must never write; GrantSecrets adds +// version-adder on top for the ones it may generate. +func GrantSecretRead(ctx *pulumi.Context, cfg *config.Config, a *Accounts, name string) error { + _, err := secretmanager.NewSecretIamMember(ctx, "vm-read-"+name, &secretmanager.SecretIamMemberArgs{ + Project: pulumi.String(cfg.Project), + SecretId: pulumi.String(name), + Role: pulumi.String("roles/secretmanager.secretAccessor"), + Member: pulumi.Sprintf("serviceAccount:%s", a.VM.Email), + }) + return err +} + +// GrantBuildSource lets cb-image@ read the tarballs `make build` stages, and +// nothing else in storage. See storage.New for why the bucket exists. +func GrantBuildSource(ctx *pulumi.Context, a *Accounts, sourceBucket *storage.Bucket) error { + _, err := storage.NewBucketIAMMember(ctx, "img-build-source-reader", &storage.BucketIAMMemberArgs{ + Bucket: sourceBucket.Name, + Role: pulumi.String("roles/storage.objectViewer"), + Member: pulumi.Sprintf("serviceAccount:%s", a.Image.Email), + }) + return err +} + // GrantBuckets: read-only on config, write-only on backups. The VM can create a // backup but cannot read or delete existing ones, which limits what ransomware // on the box could do to the backup history. diff --git a/infra/pkg/secrets/secrets.go b/infra/pkg/secrets/secrets.go index 2940ee1..4254138 100644 --- a/infra/pkg/secrets/secrets.go +++ b/infra/pkg/secrets/secrets.go @@ -26,3 +26,9 @@ var Names = []string{ "gitea-oauth2-jwt-secret", "gitea-lfs-jwt-secret", } + +// ACMEEmail holds the contact address Caddy registers with Let's Encrypt. It +// is a secret not because it signs anything but to keep it out of this public +// repository and out of instance metadata. Unlike Names it is supplied by the +// operator, never generated, so the VM gets read access only. +const ACMEEmail = "gitea-acme-email" diff --git a/infra/pkg/storage/storage.go b/infra/pkg/storage/storage.go index 6e562d1..ed6671a 100644 --- a/infra/pkg/storage/storage.go +++ b/infra/pkg/storage/storage.go @@ -1,4 +1,4 @@ -// Package storage holds the two buckets and, importantly, uploads the vm/ tree +// Package storage holds the buckets and, importantly, uploads the vm/ tree // as Pulumi-managed objects. // // Uploading the VM configuration through Pulumi (rather than a `gcloud storage @@ -24,6 +24,8 @@ import ( type Buckets struct { Config *storage.Bucket Backup *storage.Bucket + // BuildSource stages the source tarball for `make build`. See New. + BuildSource *storage.Bucket // ConfigHash changes whenever any file under vm/ changes. It is written into // instance metadata so a config change is visible from `describe`, and so // there is something to compare against when debugging drift. @@ -72,12 +74,37 @@ func New(ctx *pulumi.Context, cfg *config.Config, vmDir string, deps []pulumi.Re return nil, err } + // Where `gcloud builds submit` stages its source tarball. Builds run as + // cb-image@, which needs storage.objects.get on that tarball. The default + // staging bucket is _cloudbuild, created by gcloud on the first + // submit -- after `pulumi up`, so there is nothing to bind to in advance -- + // and project-wide objectViewer would also open the backup and state + // buckets. A dedicated bucket keeps the grant exact. Triggered builds fetch + // source through the GitHub connection and never touch it. + buildSourceBucket, err := storage.NewBucket(ctx, "gitea-build-source", &storage.BucketArgs{ + Name: pulumi.Sprintf("%s-gitea-build-source", cfg.Project), + Location: pulumi.String(strings.ToUpper(cfg.Region)), + UniformBucketLevelAccess: pulumi.Bool(true), + PublicAccessPrevention: pulumi.String("enforced"), + // Tarballs are only read once, by the build they were uploaded for. + LifecycleRules: storage.BucketLifecycleRuleArray{ + &storage.BucketLifecycleRuleArgs{ + Action: &storage.BucketLifecycleRuleActionArgs{Type: pulumi.String("Delete")}, + Condition: &storage.BucketLifecycleRuleConditionArgs{Age: pulumi.Int(7)}, + }, + }, + ForceDestroy: pulumi.Bool(true), + }, opts) + if err != nil { + return nil, err + } + hash, err := uploadTree(ctx, configBucket, vmDir) if err != nil { return nil, err } - return &Buckets{Config: configBucket, Backup: backupBucket, ConfigHash: hash}, nil + return &Buckets{Config: configBucket, Backup: backupBucket, BuildSource: buildSourceBucket, ConfigHash: hash}, nil } // uploadTree mirrors vmDir into gs:///vm/ and returns a content hash of diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 36d1b8d..5e563e2 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -99,6 +99,16 @@ if ! has_version github-pat; then echo " printf %s '' | gcloud secrets versions add github-pat --project=${PROJECT} --data-file=-" fi +# The ACME contact address Caddy registers with Let's Encrypt. A secret only to +# keep it out of this public repository and out of instance metadata. Created +# empty: the address is yours to choose, not something to generate. +ensure_secret gitea-acme-email +if ! has_version gitea-acme-email; then + echo " NOTE: secret 'gitea-acme-email' has no value yet. Caddy still issues" + echo " certificates without it, but with no contact address. Set it with:" + echo " printf %s 'you@example.com' | gcloud secrets versions add gitea-acme-email --project=${PROJECT} --data-file=-" +fi + # Gitea's signing secrets. These MUST come from `gitea generate secret`: # INTERNAL_TOKEN is a JWT, and a random string there produces an instance that # starts and then fails every internal API call in a confusing way. @@ -247,7 +257,7 @@ Next: pulumi stack init prod pulumi config set gcp:project ${PROJECT} pulumi config set gitea:infraBuildServiceAccount ${INFRA_SA_EMAIL} - # ...plus domain, dnsZone, acmeEmail, githubOwner, githubAppInstallationId + # ...plus domain, dnsZone, githubOwner, githubAppInstallationId pulumi up 4. make build # or, spelled out: gcloud builds submit --config cloudbuild/image.yaml --project ${PROJECT} \\ diff --git a/vm/bootstrap.sh b/vm/bootstrap.sh index ffdee18..6eb49fd 100755 --- a/vm/bootstrap.sh +++ b/vm/bootstrap.sh @@ -36,7 +36,6 @@ AR_HOST=$(meta ar-host) IMAGE_GITEA=$(meta image-gitea) IMAGE_CADDY=$(meta image-caddy) DOMAIN=$(meta domain) -ACME_EMAIL=$(meta acme-email) APP_NAME=$(meta app-name) PODMAN_SUBNET=$(meta podman-subnet) PODMAN_GATEWAY=$(meta podman-gateway) @@ -57,7 +56,7 @@ case "${WAF_MODE}" in esac : "${DATA_DISK_DEVICE:=/dev/disk/by-id/google-gitea-data}" -export GCP_PROJECT AR_HOST IMAGE_GITEA IMAGE_CADDY DOMAIN ACME_EMAIL APP_NAME +export GCP_PROJECT AR_HOST IMAGE_GITEA IMAGE_CADDY DOMAIN APP_NAME export PODMAN_SUBNET PODMAN_GATEWAY REQUIRE_SIGNIN_VIEW WAF_MODE # --------------------------------------------------------------------------- @@ -199,8 +198,21 @@ setup_nftables() { systemctl disable --now firewalld >/dev/null 2>&1 || true systemctl mask firewalld >/dev/null 2>&1 || true - install -m 0600 "${STATE_DIR}/nftables/gitea.nft" /etc/sysconfig/nftables.conf - nft -c -f /etc/sysconfig/nftables.conf || die "nftables ruleset failed validation" + [[ -n "${PODMAN_SUBNET}" && -n "${PODMAN_GATEWAY}" ]] \ + || die "podman-subnet/podman-gateway metadata missing; cannot render the ruleset" + + # Validate BEFORE installing. A ruleset that fails to parse must never land + # in /etc/sysconfig: nftables.service would fail to load it on the next boot + # and the host would come up with no gitea_filter table at all. + local tmp + tmp=$(mktemp) + envsubst '${PODMAN_SUBNET} ${PODMAN_GATEWAY}' < "${STATE_DIR}/nftables/gitea.nft" > "${tmp}" + if ! nft -c -f "${tmp}"; then + rm -f "${tmp}" + die "nftables ruleset failed validation" + fi + install -m 0600 "${tmp}" /etc/sysconfig/nftables.conf + rm -f "${tmp}" systemctl enable --now nftables systemctl reload nftables @@ -429,6 +441,20 @@ render_all() { rm -f /etc/sysctl.d/90-gitea-caddy.conf fi + # The ACME contact address lives in Secret Manager rather than instance + # metadata, to keep it out of the public repository. Without it Caddy still + # issues certificates, just under an account with no contact address -- far + # better than an empty `email` directive, which fails to parse and leaves + # nothing serving TLS. + local acme_email caddy_email + if acme_email=$(gcloud secrets versions access latest --secret=gitea-acme-email \ + --project="${GCP_PROJECT}" 2>/dev/null) && [[ -n "${acme_email}" ]]; then + caddy_email="email ${acme_email}" + else + warn "secret gitea-acme-email unreadable -- Caddy will register without a contact address" + caddy_email="# no ACME contact address: secret gitea-acme-email was unreadable at render time" + fi + # Trust both the bridge CIDR and loopback so this value stays correct in # either Caddy networking mode. Rootful podman SNATs host-loopback traffic # to the bridge gateway, so the CIDR covers the host-network case too. @@ -444,7 +470,8 @@ render_all() { GITEA_UPSTREAM="${gitea_upstream}" \ CADDY_NETWORK="${caddy_network}" \ CADDY_PUBLISH_PORTS="${caddy_publish}" \ - CADDY_SYSCTL="${caddy_sysctl}" + CADDY_SYSCTL="${caddy_sysctl}" \ + CADDY_EMAIL="${caddy_email}" # app.ini is 0400 owned by uid 1000: it holds SECRET_KEY and INTERNAL_TOKEN, # and the container runs as that uid and must be able to read it. @@ -453,7 +480,7 @@ render_all() { && changed=1 render "${STATE_DIR}/config/Caddyfile.tmpl" /etc/caddy/Caddyfile root:root 0644 \ - '${DOMAIN} ${ACME_EMAIL} ${GITEA_UPSTREAM} ${WAF_MODE}' \ + '${DOMAIN} ${CADDY_EMAIL} ${GITEA_UPSTREAM} ${WAF_MODE}' \ && changed=1 local unit diff --git a/vm/config/Caddyfile.tmpl b/vm/config/Caddyfile.tmpl index 4f8f9a4..8b160fa 100644 --- a/vm/config/Caddyfile.tmpl +++ b/vm/config/Caddyfile.tmpl @@ -5,7 +5,7 @@ # 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 { - email ${ACME_EMAIL} + ${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 @@ -17,7 +17,8 @@ ${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. + # The VM service account holds roles/dns.admin scoped to this zone, plus + # project-level dns.reader so the plugin can list zones to find it. gcp_project {env.GCP_PROJECT} } # Only used for propagation checks. If issuance stalls waiting for diff --git a/vm/nftables/gitea.nft b/vm/nftables/gitea.nft index 2f547fd..0405129 100644 --- a/vm/nftables/gitea.nft +++ b/vm/nftables/gitea.nft @@ -1,6 +1,7 @@ #!/usr/sbin/nft -f # -# Host firewall. Installed by vm/bootstrap.sh as /etc/sysconfig/nftables.conf. +# Host firewall. Rendered by vm/bootstrap.sh into /etc/sysconfig/nftables.conf, +# substituting ${PODMAN_SUBNET} and ${PODMAN_GATEWAY}. # # CRITICAL: this file must NEVER contain `flush ruleset`. The stock nftables # config ships with one, and it would wipe podman/netavark's NAT and forward @@ -37,6 +38,14 @@ table inet gitea_filter { # DHCP renewal from the GCE metadata server. udp sport 67 udp dport 68 accept + # Container DNS. aardvark-dns answers on the bridge gateway, so lookups + # from containers terminate on the host and arrive here, not in forward. + # Netavark accepts them in its own table, but a packet has to survive + # every input-hook chain, and this one's drop policy would discard it + # anyway. Without this rule containers resolve nothing -- Caddy cannot + # reach Let's Encrypt and Gitea cannot reach webhook or mirror hosts. + ip saddr ${PODMAN_SUBNET} ip daddr ${PODMAN_GATEWAY} meta l4proto { tcp, udp } th dport 53 accept comment "container DNS" + # Admin SSH: IAP TCP forwarding range only. There is no other path in -- # the VPC firewall enforces the same restriction as the outer layer. ip saddr 35.235.240.0/20 tcp dport 22 accept comment "IAP SSH"