Forgejo on GKE with Cloudflare and Security Hardening
Table of Contents
Section titled “Table of Contents”- Overview
- Final Architecture
- What We Built
- Cost Controls
- Security Controls
- Bootstrap Commands
- Admin Account Setup
- Operational Notes
- What Remains Optional
- Conclusion
Forgejo on GKE with Cloudflare and Security Hardening
Section titled “Forgejo on GKE with Cloudflare and Security Hardening”Overview
Section titled “Overview”This setup turns a small GKE cluster into a self-hosted Git forge running Forgejo, exposed at https://forgejo.cornucopia.build, and fronted by Cloudflare.
The goal was not just to make it work, but to keep it inexpensive and defensible:
- Keep the cluster tiny.
- Keep the public endpoint stable.
- Require HTTPS.
- Restrict the origin to Cloudflare only.
- Require sign-in before browsing Forgejo content.
I also kept the infrastructure split from the earlier GPU experiments so the code-hosting stack can stand on its own.
Final Architecture
Section titled “Final Architecture”The final shape is:
- One zonal GKE cluster.
- One
forgejoDeployment. - One
ingress-nginxcontroller with a reserved static IP. - One Cloudflare DNS
Arecord forforgejo.cornucopia.build. - cert-manager issuing a Let’s Encrypt certificate using Cloudflare DNS-01.
- Cloudflare proxying the hostname in front of the ingress.
The important part is that the origin is not directly exposed to the internet. The ingress controller only accepts traffic from Cloudflare IP ranges.
What We Built
Section titled “What We Built”The deployment includes:
- Forgejo running in its own namespace.
- A small persistent volume for repository and application data.
- An HTTPS ingress using
ingress-nginx. - cert-manager with a Cloudflare DNS-01
ClusterIssuer. - A reserved regional static IP, so the public address does not change every time the controller restarts.
- A site-owner account created from inside the container using the Forgejo CLI.
Cost Controls
Section titled “Cost Controls”The cheapest practical version still has a few unavoidable costs, but each one was trimmed:
- The GKE node stays small.
- Forgejo requests only
100mCPU and256MiRAM, with a modest limit. - The persistent volume is only
10Giin the manifest. - The ingress controller is small and runs as a single replica.
- The public IP is static, so there is no churn in DNS configuration.
- The site uses one load balancer instead of multiple public services.
In other words, this is not free, but it is intentionally lean.
Security Controls
Section titled “Security Controls”These were the hardening steps we kept in place:
- HTTPS is enforced at the edge.
- Cloudflare serves as the public front door.
- The ingress controller only allows Cloudflare source IPs.
- Forgejo registration is disabled.
- Forgejo requires sign-in to view content.
- The site-owner account is created explicitly rather than relying on a permissive bootstrap flow.
- The origin IP is not meant to be a stable public access point for humans.
That gives three layers of defense:
- Cloudflare as the proxy and TLS boundary.
- GKE ingress restricted to Cloudflare source ranges.
- Forgejo itself requiring authentication.
Bootstrap Commands
Section titled “Bootstrap Commands”The cluster credential step is separate from the app install:
gcloud container clusters get-credentials playground \ --zone northamerica-northeast1-c \ --project ckad-gkeInstall the ingress controller:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginxhelm repo update ingress-nginxhelm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx \ --create-namespaceReserve a static IP and bind the controller to it:
gcloud compute addresses create forgejo-ingress-ip \ --region northamerica-northeast1 \ --project ckad-gkeUpdate Cloudflare DNS to the reserved IP:
curl -X PUT "https://api.cloudflare.com/client/v4/zones/<ZONE_ID>/dns_records/<RECORD_ID>" \ -H "Authorization: Bearer <CLOUDFLARE_API_TOKEN>" \ -H "Content-Type: application/json" \ --data '{"type":"A","name":"forgejo.cornucopia.build","content":"34.19.194.216","ttl":120,"proxied":true}'Admin Account Setup
Section titled “Admin Account Setup”Forgejo provides a CLI inside the container image. The binary is forgejo, and the admin user command is:
forgejo admin user create --username <user> --password <password> --email <email> --admin --must-change-password=falseBecause the container runs as root in the kubectl exec context, the command was executed as the non-root git user:
kubectl exec -n forgejo deploy/forgejo -- /bin/bash --noprofile --norc -lc \ "su -s /bin/bash git -c 'forgejo admin user create ...'"The actual account created was a non-reserved site-owner username, not admin, because admin is reserved by Forgejo.
Operational Notes
Section titled “Operational Notes”The current setup is intentionally conservative:
- The site is behind Cloudflare proxying.
- The origin only accepts Cloudflare IP ranges.
- The app does not allow open registration.
- The default view requires sign-in.
- The static IP is reserved, so the DNS record does not need to be changed repeatedly.
The most important maintenance command is checking the live public endpoint:
curl -I https://forgejo.cornucopia.buildAnd if you want to inspect the cluster state:
kubectl get pods -n forgejokubectl get svc -n ingress-nginxkubectl get certificate -n forgejoWhat Remains Optional
Section titled “What Remains Optional”Cloudflare Access was the next hardening step, but it requires Cloudflare Zero Trust write access. The site is already protected without it, but Access would add another identity gate before Forgejo is reached.
If you add it later, the intended model is:
- Create a self-hosted Cloudflare Access application for
forgejo.cornucopia.build. - Require your identity provider or a tightly scoped allow policy.
- Keep the origin locked to Cloudflare only.
That would make the public hostname effectively inaccessible without Cloudflare-authenticated access.
Conclusion
Section titled “Conclusion”The end result is a small, cheap, and reasonably locked-down Forgejo instance:
- GKE hosts the service.
ingress-nginxprovides the public entry point.- Cloudflare handles DNS and proxying.
- cert-manager handles TLS.
- Forgejo requires authentication.
- The origin only trusts Cloudflare.
That is enough for a practical self-hosted code forge without turning the cluster into a security or cost liability.