diff --git a/.gitea/workflows/build-and-deploy.yaml b/.gitea/workflows/build-and-deploy.yaml index 54f6933..e8e101e 100644 --- a/.gitea/workflows/build-and-deploy.yaml +++ b/.gitea/workflows/build-and-deploy.yaml @@ -82,6 +82,7 @@ jobs: - name: Apply Kubernetes manifests run: | kubectl apply -f k8s/namespace.yaml + kubectl apply -f k8s/middleware.yaml kubectl apply -f k8s/deployment.yaml kubectl apply -f k8s/service.yaml kubectl apply -f k8s/ingress.yaml diff --git a/README.md b/README.md index e9cd831..438e2fe 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,61 @@ # signalledger.nl -Signal Ledger news site \ No newline at end of file +Signal Ledger — an independent news publication, a subsidiary of Jopdorp. + +## Architecture Decision Record (ADR) + +### ADR-002: Ingress Controller Migration (nginx → Traefik) + +**Status:** Accepted + +**Context:** +The cluster uses Traefik as its ingress controller. The initial K8s manifests were written with `ingressClassName: nginx` and nginx-specific annotations. This caused a mismatch: the Ingress resource was never picked up by any controller, leaving the site unreachable via the configured domains. + +**Decision:** +Migrate all ingress configuration to Traefik-native resources. + +1. **Ingress class:** Changed from `nginx` to `traefik`. +2. **Annotations:** Replaced nginx-specific `configuration-snippet` with Traefik `router.entrypoints`, `router.tls`, and `router.middlewares` annotations. +3. **Security headers:** Extracted from inline nginx snippets into a dedicated `Middleware` CRD (`k8s/middleware.yaml`). This keeps header policy declarative and reusable. + +**Migration strategy:** In-place update +- The namespace `openclaw-private` already exists. +- The deployment, service, and TLS secret are unchanged. +- We apply the new Ingress and Middleware manifests; Traefik picks them up immediately. +- Rolling back is a single `kubectl apply` of the previous manifest version. + +**Consequences:** +- Positive: Aligns with cluster infrastructure. No extra ingress controller needed. +- Positive: Middleware CRD is cleaner and version-controllable than inline snippets. +- Risk: Traefik middleware syntax errors will cause 404/500 until fixed. Mitigated by validating manifests in CI before deploy. + +## Deployment + +### Prerequisites +- Kubernetes cluster with Traefik and cert-manager installed. +- `registry.claw.jopdorp.nl` push access. +- `KUBECONFIG_BASE64` and `REGISTRY_TOKEN` secrets configured in Gitea. + +### CI/CD Pipeline +Gitea Actions workflow (`.gitea/workflows/build-and-deploy.yaml`): +1. Build and test on every PR/push. +2. Build and push Docker image on merge to `main`. +3. Apply K8s manifests and wait for rollout. + +### Manual Deploy +```bash +kubectl apply -f k8s/namespace.yaml +kubectl apply -f k8s/middleware.yaml +kubectl apply -f k8s/deployment.yaml +kubectl apply -f k8s/service.yaml +kubectl apply -f k8s/ingress.yaml +kubectl rollout status deployment/signalledger -n openclaw-private --timeout=120s +``` + +### Domains +- `signalledger.nl` +- `www.signalledger.nl` + +### Contact +- Email: signalledger@jopdorp.nl +- Owner: Signal Ledger is a subsidiary of Jopdorp. diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml index c9731f9..255923a 100644 --- a/k8s/ingress.yaml +++ b/k8s/ingress.yaml @@ -9,17 +9,11 @@ metadata: app.kubernetes.io/part-of: signalledger annotations: cert-manager.io/cluster-issuer: "letsencrypt-prod" - nginx.ingress.kubernetes.io/ssl-redirect: "true" - nginx.ingress.kubernetes.io/configuration-snippet: | - more_set_headers "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload"; - more_set_headers "X-Frame-Options: DENY"; - more_set_headers "X-Content-Type-Options: nosniff"; - more_set_headers "Referrer-Policy: strict-origin-when-cross-origin"; - more_set_headers "Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://pagead2.googlesyndication.com https://partner.googleadservices.com https://tpc.googlesyndication.com; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; font-src 'self'; connect-src 'self'; frame-src https://googleads.g.doubleclick.net; object-src 'none'; base-uri 'self'; form-action 'self';"; - more_set_headers "X-XSS-Protection: 1; mode=block"; - more_set_headers "Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"; + traefik.ingress.kubernetes.io/router.entrypoints: "websecure" + traefik.ingress.kubernetes.io/router.tls: "true" + traefik.ingress.kubernetes.io/router.middlewares: "openclaw-private-security-headers@kubernetescrd" spec: - ingressClassName: nginx + ingressClassName: traefik tls: - hosts: - signalledger.nl diff --git a/k8s/middleware.yaml b/k8s/middleware.yaml new file mode 100644 index 0000000..b8f1798 --- /dev/null +++ b/k8s/middleware.yaml @@ -0,0 +1,22 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: security-headers + namespace: openclaw-private + labels: + app.kubernetes.io/name: signalledger + app.kubernetes.io/component: middleware + app.kubernetes.io/part-of: signalledger +spec: + headers: + customRequestHeaders: + X-Forwarded-Proto: "https" + customResponseHeaders: + Strict-Transport-Security: "max-age=31536000; includeSubDomains; preload" + X-Frame-Options: "DENY" + X-Content-Type-Options: "nosniff" + Referrer-Policy: "strict-origin-when-cross-origin" + Content-Security-Policy: "default-src 'self'; script-src 'self' 'unsafe-inline' https://pagead2.googlesyndication.com https://partner.googleadservices.com https://tpc.googlesyndication.com; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; font-src 'self'; connect-src 'self'; frame-src https://googleads.g.doubleclick.net; object-src 'none'; base-uri 'self'; form-action 'self';" + X-XSS-Protection: "1; mode=block" + Permissions-Policy: "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" + sslRedirect: true