Establish clean homelab infrastructure baseline
lint / yaml (push) Has been cancelled
lint / ansible (push) Has been cancelled
lint / terraform (push) Has been cancelled

Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
This commit is contained in:
2026-09-09 16:47:20 +00:00
commit 88a02ababa
418 changed files with 50579 additions and 0 deletions
@@ -0,0 +1,62 @@
# Cloudflare tunnel — the PUBLIC ingress for *.ddupan.top.
#
# The tunnel is TOKEN-MANAGED: `TUNNEL_TOKEN` is the whole credential, and the
# routing/ingress rules live server-side, managed by ../cloudflared/terraform
# (`cloudflare_zero_trust_tunnel_cloudflared_config`). There is deliberately no
# local config.yml and no credentials-file here.
#
# A ConfigMap + credentials Secret used to be embedded in this file. Both were
# dead and were removed 2026-07-28, with three independent proofs:
# 1. the config routed idm.ddupan.top -> keycloak, RETIRED 2026-07-10;
# 2. it pointed auth.ddupan.top at authelia:9091, which 502s — Terraform
# corrected that to :80 and auth works, so Terraform's config is the live one;
# 3. the credentials volume mounted subPath `<uuid>.json` while the Secret's
# key was `credentials-file`, so that mount never resolved at all.
# `origin_request.no_tls_verify` is likewise set API-side by Terraform.
#
# The token itself is NOT in this file — see secret.example.yaml.
---
apiVersion: v1
kind: Namespace
metadata:
name: cloudflared
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudflared
namespace: cloudflared
spec:
replicas: 1
selector:
matchLabels:
app: cloudflared
template:
metadata:
labels:
app: cloudflared
spec:
containers:
- name: cloudflared
# NOTE: :latest on the public ingress. Pinning is desirable but a wrong
# pin is worse than an unpinned upgrade here; left as-is deliberately.
image: cloudflare/cloudflared:latest
command:
- cloudflared
- tunnel
- --no-autoupdate
- --loglevel
- info
- --metrics
- 0.0.0.0:2000
# No --config: with TUNNEL_TOKEN set, cloudflared pulls its ingress
# rules from the Cloudflare API. A --config pointing at a file that
# no longer exists would be a hard startup error.
- run
env:
- name: TUNNEL_TOKEN
valueFrom:
secretKeyRef:
name: cloudflared-tunnel
key: token
restartPolicy: Always
@@ -0,0 +1,23 @@
# Template. Copy to secret.yaml, fill in the real token, and apply.
# secret.yaml is gitignored — same convention as ../../platform/cert-manager, ../../apps/netbox
# and ../../apps/smtp-relay.
#
# The token IS the tunnel credential: it embeds the account tag, tunnel ID and
# tunnel secret, so anything holding it can serve traffic for the tunnel. It is
# also all cloudflared needs — the ingress rules come from the Cloudflare API,
# managed by ./terraform.
#
# Where to get it:
# Cloudflare dashboard -> Zero Trust -> Networks -> Tunnels -> <tunnel> ->
# Configure -> the `cloudflared service install <TOKEN>` command contains it.
# Rotating it there requires re-applying this Secret and restarting the
# Deployment; the tunnel UUID does not change.
---
apiVersion: v1
kind: Secret
metadata:
name: cloudflared-tunnel
namespace: cloudflared
type: Opaque
stringData:
token: REPLACE_WITH_CLOUDFLARE_TUNNEL_TOKEN
@@ -0,0 +1,7 @@
# Never commit secrets or state
terraform.tfvars
*.tfstate
*.tfstate.*
.terraform/
.terraform.lock.hcl.bak
crash.log
+19
View File
@@ -0,0 +1,19 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/cloudflare/cloudflare" {
version = "5.21.1"
constraints = "~> 5.0"
hashes = [
"h1:hU72otEs26Wx6tcJD9igX6I/BQtVgeRuaIe3s/hn6bQ=",
"zh:049719425b8be43d9d4f0c208217aca0baa22374f061d7ff92f02563490f649c",
"zh:0a8a3c1b26680b437fe9e7910ca81e532d36f8efacfb14f45690b6a779856993",
"zh:32b61f80892243f7ab8e453fa038c1f3e2aac733ccb98307c2cfe798b2793b32",
"zh:42c27f3cd62979e70716c51f682a3d131d51ad76d86dff83d8cdbfffcebac841",
"zh:4c8cd464f9b6ecde5cd4430bbba4be3b810826105e51ef6328b6a2b69f821443",
"zh:586ea42ef74d6c5bc4c9b89da6b1f8618a19f4e80272fe8d615e7d5b11c491af",
"zh:b09b86c7cac7085e01c9b7a828f09d13c44589d3e3cd42f0b694ca3e4cd3ed0a",
"zh:eac80665e60c701b37a6318f4e405d67f1720f8da5f93135c6256049282d3367",
"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32",
]
}
@@ -0,0 +1,46 @@
# Cloudflare tunnel + DNS as code (Terraform)
The cloudflared tunnel runs **token-managed** (`TUNNEL_TOKEN` in the Deployment), so its
ingress config is authoritative **at Cloudflare's edge**, not the in-cluster `config.yml`
(which is ignored in token mode). This Terraform manages that edge config + DNS declaratively.
- `cloudflare_zero_trust_tunnel_cloudflared_config.main` — the tunnel's full ingress list
(all public hostnames → in-cluster services). First match wins; `http_status:404` stays last.
- `cloudflare_dns_record.auth` — proxied CNAME `auth.ddupan.top → <tunnel>.cfargotunnel.com`.
> The `../cloudflared/cloudflared.yaml` `config.yml` ingress is now cosmetic. Either keep it
> in sync for documentation, or drop `TUNNEL_TOKEN` to make it authoritative instead — but
> we chose Terrraform-over-edge-config so the tunnel stays dashboard/token managed.
## One-time setup
1. **Create a Cloudflare API token** (dash → My Profile → API Tokens) with:
- Account · **Cloudflare Tunnel : Edit**
- Zone · **DNS : Edit** (zone `ddupan.top`)
2. Provide it + the zone id:
```bash
export TF_VAR_cloudflare_api_token='...'
# zone id:
curl -s -H "Authorization: Bearer $TF_VAR_cloudflare_api_token" \
"https://api.cloudflare.com/client/v4/zones?name=ddupan.top" | jq -r '.result[0].id'
export TF_VAR_zone_id='<that id>'
```
3. **Adopt existing resources into state** (they already exist — don't recreate):
```bash
terraform init
# tunnel config singleton:
terraform import cloudflare_zero_trust_tunnel_cloudflared_config.main \
65bf9ede92caa915f992fdf3d1e7b2f1/ff392451-b0b1-45bb-964e-6d9372c3a9e3
# the auth DNS record (get its id from the API, then):
terraform import cloudflare_dns_record.auth $TF_VAR_zone_id/<record_id>
```
## Apply
```bash
terraform plan # should show only auth.ddupan.top being added to ingress
terraform apply
```
State is local (`terraform.tfstate`, gitignored). Move to a remote backend (r2/s3) if this
grows.
@@ -0,0 +1,40 @@
# Remote state in SeaweedFS S3, on the LAN.
#
# WHY remote at all: local state means the only copy lives on this laptop, which is
# also the k3s node, the NFS server and the libvirt host — i.e. the single point of
# failure. It also cannot be locked, so two concurrent applies silently corrupt it.
#
# WHY s3.ad.ddupan.top and NOT obj.ddupan.top: the public name resolves to
# Cloudflare and hairpins through the WAN. On 2026-07-28 that path was blackholed
# for hours by a dead VPN tunnel. State must be reachable when the WAN is not —
# it is what you need DURING an incident. See ../../../apps/seaweedfs/httproute-s3.yaml.
#
# CREDENTIALS are not in this file. Export them before running terraform:
# export AWS_ACCESS_KEY_ID=$(bao kv get -field=... kv/k8s/seaweedfs-s3) # see README
# export AWS_SECRET_ACCESS_KEY=...
# The `terraform` S3 identity is scoped to this bucket only — it deliberately
# cannot create buckets or read anything else in the store.
terraform {
backend "s3" {
bucket = "tfstate"
key = "cloudflared/terraform.tfstate"
endpoints = {
s3 = "https://s3.ad.ddupan.top"
}
# SeaweedFS is not AWS: it has no regions, no IAM, no metadata service and no
# account IDs, so every AWS-specific validation has to be skipped or the
# provider fails before it ever talks to the endpoint.
region = "us-east-1"
use_path_style = true
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
skip_requesting_account_id = true
# Native S3 locking (Terraform >= 1.10; this repo runs 1.15). Writes a
# .tflock object alongside the state — no DynamoDB table needed.
use_lockfile = true
}
}
@@ -0,0 +1,71 @@
# Manages the REMOTE (dashboard) config of the token-managed cloudflared tunnel.
# This is what's actually authoritative when the pod runs with TUNNEL_TOKEN — the
# in-cluster config.yml ingress is ignored in that mode. Terraform is the source of truth.
resource "cloudflare_zero_trust_tunnel_cloudflared_config" "main" {
account_id = var.account_id
tunnel_id = var.tunnel_id
config = {
# First match wins; the http_status:404 catch-all MUST stay last.
ingress = [
{
hostname = "auth.ddupan.top"
# Authelia's k8s Service listens on :80 (targetPort -> container 9091).
# Hitting :9091 on the service yields 502; the service port is 80.
service = "http://authelia.authelia.svc.cluster.local:80"
},
{
hostname = "git.ddupan.top"
service = "http://gitea-http.gitea.svc.cluster.local:3000"
},
{
hostname = "e5renew.ddupan.top"
service = "http://e5renew.e5renew.svc.cluster.local:80"
},
{
hostname = "obj.ddupan.top"
service = "http://seaweedfs-s3.seaweedfs.svc.cluster.local:8333"
},
{
service = "http_status:404"
},
]
origin_request = {
no_tls_verify = true
}
}
}
# Public DNS: proxied CNAME -> the tunnel. (auth was bootstrapped with
# `cloudflared tunnel route dns`; import it into state — see README.)
resource "cloudflare_dns_record" "auth" {
zone_id = var.zone_id
name = "auth.ddupan.top"
type = "CNAME"
content = "${var.tunnel_id}.cfargotunnel.com"
proxied = true
ttl = 1 # 1 = automatic (required when proxied)
}
# DKIM for Microsoft 365 mail sent as *@ddupan.top (via the smtp-relay). CNAMEs point
# at the tenant's DKIM keys; must be DNS-only (unproxied). Enable signing in Exchange
# after these resolve: smtp-relay/scripts/enable-dkim.ps1.
resource "cloudflare_dns_record" "dkim_selector1" {
zone_id = var.zone_id
name = "selector1._domainkey.ddupan.top"
type = "CNAME"
content = "selector1-ddupan-top._domainkey.ddupan1.d-v1.dkim.mail.microsoft"
proxied = false
ttl = 3600
}
resource "cloudflare_dns_record" "dkim_selector2" {
zone_id = var.zone_id
name = "selector2._domainkey.ddupan.top"
type = "CNAME"
content = "selector2-ddupan-top._domainkey.ddupan1.d-v1.dkim.mail.microsoft"
proxied = false
ttl = 3600
}
@@ -0,0 +1,4 @@
# Copy to terraform.tfvars (gitignored) and fill in. Or export TF_VAR_cloudflare_api_token.
cloudflare_api_token = "CHANGE-ME" # Account·Cloudflare Tunnel:Edit + Zone·DNS:Edit (ddupan.top)
zone_id = "CHANGE-ME" # GET /zones?name=ddupan.top -> result[0].id
# account_id / tunnel_id / zone_name have defaults in variables.tf
@@ -0,0 +1,27 @@
variable "cloudflare_api_token" {
type = string
sensitive = true
description = "Cloudflare API token — scopes: Account·Cloudflare Tunnel:Edit, Zone·DNS:Edit (ddupan.top). Provide via TF_VAR_cloudflare_api_token or terraform.tfvars (gitignored)."
}
variable "account_id" {
type = string
default = "65bf9ede92caa915f992fdf3d1e7b2f1"
description = "Cloudflare account tag (from the tunnel credentials)."
}
variable "zone_name" {
type = string
default = "ddupan.top"
}
variable "zone_id" {
type = string
description = "Zone ID for ddupan.top (fetch once: GET /zones?name=ddupan.top)."
}
variable "tunnel_id" {
type = string
default = "ff392451-b0b1-45bb-964e-6d9372c3a9e3"
description = "Existing cloudflared tunnel ID (remotely managed)."
}
@@ -0,0 +1,13 @@
terraform {
required_version = ">= 1.6"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 5.0"
}
}
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}