diff --git a/apps/api/k8s/base/ingress.yaml b/apps/api/k8s/base/ingress.yaml index 9ea1967..1251ada 100644 --- a/apps/api/k8s/base/ingress.yaml +++ b/apps/api/k8s/base/ingress.yaml @@ -7,7 +7,7 @@ metadata: spec: ingressClassName: traefik rules: - - host: api.homelab.local + - host: api.coreworlds.io http: paths: - path: / @@ -19,5 +19,5 @@ spec: number: 80 tls: - hosts: - - api.homelab.local + - api.coreworlds.io secretName: api-tls diff --git a/apps/api/k8s/overlays/preview/kustomization.yaml b/apps/api/k8s/overlays/preview/kustomization.yaml index eba5668..911f21b 100644 --- a/apps/api/k8s/overlays/preview/kustomization.yaml +++ b/apps/api/k8s/overlays/preview/kustomization.yaml @@ -19,7 +19,7 @@ patches: patch: | - op: replace path: /spec/rules/0/host - value: api-preview.homelab.local + value: api-preview.coreworlds.io - op: replace path: /spec/tls/0/hosts/0 - value: api-preview.homelab.local + value: api-preview.coreworlds.io diff --git a/apps/web/k8s/base/ingress.yaml b/apps/web/k8s/base/ingress.yaml index c265316..773d757 100644 --- a/apps/web/k8s/base/ingress.yaml +++ b/apps/web/k8s/base/ingress.yaml @@ -7,7 +7,7 @@ metadata: spec: ingressClassName: traefik rules: - - host: homelab.local + - host: coreworlds.io http: paths: - path: / @@ -19,5 +19,5 @@ spec: number: 80 tls: - hosts: - - homelab.local + - coreworlds.io secretName: web-tls diff --git a/apps/web/k8s/overlays/preview/kustomization.yaml b/apps/web/k8s/overlays/preview/kustomization.yaml index 94cc819..b4c33fc 100644 --- a/apps/web/k8s/overlays/preview/kustomization.yaml +++ b/apps/web/k8s/overlays/preview/kustomization.yaml @@ -19,7 +19,7 @@ patches: patch: | - op: replace path: /spec/rules/0/host - value: preview.homelab.local + value: preview.coreworlds.io - op: replace path: /spec/tls/0/hosts/0 - value: preview.homelab.local + value: preview.coreworlds.io diff --git a/infra/ansible/inventory/group_vars/servers.yaml b/infra/ansible/inventory/group_vars/servers.yaml index 7f68a9e..f49db59 100644 --- a/infra/ansible/inventory/group_vars/servers.yaml +++ b/infra/ansible/inventory/group_vars/servers.yaml @@ -6,7 +6,7 @@ k3s_server_args: >- --disable=servicelb --write-kubeconfig-mode=644 --tls-san={{ ansible_host }} - --tls-san=k3s.homelab.local + --tls-san=k3s.coreworlds.io --kube-apiserver-arg=audit-log-maxage=30 --kube-apiserver-arg=audit-log-maxbackup=10 --kube-apiserver-arg=audit-log-maxsize=100 diff --git a/infra/kubernetes/platform/cert-manager-config/cloudflare-api-token-sealed.yaml b/infra/kubernetes/platform/cert-manager-config/cloudflare-api-token-sealed.yaml new file mode 100644 index 0000000..0bf0f6f --- /dev/null +++ b/infra/kubernetes/platform/cert-manager-config/cloudflare-api-token-sealed.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + name: cloudflare-api-token + namespace: cert-manager +spec: + encryptedData: + api-token: AgBhOS+L5bWj4YIxV+u6waATX0iT9EAOCoA2FNDcJfqcH4nICK7zUakh7KaWjrLIYqlNHOFU66a5MZiuNs9IGtDwNSRXBXsuYauJoI2NripYchZ4InswNdzN3CjE9kgOwShOjCAcJz+emogXJq5lSFGr9pIYeyaZ2NIxKR6kcqBmNB1lTBeAOB5c8h/xh/eKq66HEd2gcaXqYgpe+w9I90f1WjASqV92oj6CNT4XhExAmThia7olA5n0o9ogJjdIb9LND/AL/Hf6USGpdfpu7Dr4XfNG1JzyksSiZVIXDXFlO7N6fyWrhOFWCZFCzdIvVhnt8niqCgSY2LutodqMjkfUM7WXjflznqf1x7b8jMb/O6RIYTGkOs8Dno7FrrYXJXOVoumbbd4eBvRUunfsa3qIE6LYSDmb9b8qZx03XKoS4hvcNSXh9VPJYCCK1uRHH6sbAcRwyLalAkuysAtOqH7ZIhAo5GsBwSaQjNVQIxFnDbKtOpQIcS3+nnJcPMcMa56OAAxFDG16J+U16PI/KJzhO4ubBWNt60yALhTtWizygVCPSxtLOU78VbMmAdZwdkc9daOy2USVDSxgJd0EhTunoQYcZUuJqjzCTlfflGP9XJ7WgGItXhsnQTJZVk7Np4s/9KGZtnJVTYpFp94Jp3yPxDXcgJX+6I+YSkqjvq3iYNlVrF4NDyEUE8C00zDE+6zbybsth1t6xXsMWxEU3g1rLtBxYIppBuI0INCLTUCuCzKUSC9oQvXo9mAVXQjgJ4I1za01lA== + template: + metadata: + name: cloudflare-api-token + namespace: cert-manager diff --git a/infra/kubernetes/platform/cert-manager-config/clusterissuer-letsencrypt.yaml b/infra/kubernetes/platform/cert-manager-config/clusterissuer-letsencrypt.yaml index a2b59f9..842c04d 100644 --- a/infra/kubernetes/platform/cert-manager-config/clusterissuer-letsencrypt.yaml +++ b/infra/kubernetes/platform/cert-manager-config/clusterissuer-letsencrypt.yaml @@ -9,7 +9,7 @@ metadata: spec: acme: server: https://acme-staging-v02.api.letsencrypt.org/directory - email: admin@homelab.local + email: julia.eloise@pm.me privateKeySecretRef: name: letsencrypt-staging-key solvers: @@ -26,7 +26,7 @@ metadata: spec: acme: server: https://acme-v02.api.letsencrypt.org/directory - email: admin@homelab.local + email: julia.eloise@pm.me privateKeySecretRef: name: letsencrypt-production-key solvers: diff --git a/infra/kubernetes/platform/cert-manager-config/kustomization.yaml b/infra/kubernetes/platform/cert-manager-config/kustomization.yaml index b4aac00..5f3c768 100644 --- a/infra/kubernetes/platform/cert-manager-config/kustomization.yaml +++ b/infra/kubernetes/platform/cert-manager-config/kustomization.yaml @@ -2,3 +2,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - clusterissuer-letsencrypt.yaml + - cloudflare-api-token-sealed.yaml diff --git a/infra/kubernetes/platform/traefik/helmchartconfig.yaml b/infra/kubernetes/platform/traefik/helmchartconfig.yaml index 02b26b6..539bb65 100644 --- a/infra/kubernetes/platform/traefik/helmchartconfig.yaml +++ b/infra/kubernetes/platform/traefik/helmchartconfig.yaml @@ -22,5 +22,3 @@ spec: metrics: prometheus: entryPoint: metrics - additionalArguments: - - "--entrypoints.websecure.http.tls.certresolver=letsencrypt" diff --git a/infra/kubernetes/platform/traefik/ingressroute-argocd.yaml b/infra/kubernetes/platform/traefik/ingressroute-argocd.yaml new file mode 100644 index 0000000..60873bb --- /dev/null +++ b/infra/kubernetes/platform/traefik/ingressroute-argocd.yaml @@ -0,0 +1,22 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: argocd + namespace: platform + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production +spec: + entryPoints: + - websecure + routes: + - match: Host(`argocd.coreworlds.io`) + kind: Rule + middlewares: + - name: internal-only + namespace: platform + services: + - name: argocd-server + namespace: argocd + port: 80 + tls: + secretName: argocd-tls diff --git a/infra/kubernetes/platform/traefik/ingressroute-grafana.yaml b/infra/kubernetes/platform/traefik/ingressroute-grafana.yaml new file mode 100644 index 0000000..4d70948 --- /dev/null +++ b/infra/kubernetes/platform/traefik/ingressroute-grafana.yaml @@ -0,0 +1,22 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: grafana + namespace: platform + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production +spec: + entryPoints: + - websecure + routes: + - match: Host(`grafana.coreworlds.io`) + kind: Rule + middlewares: + - name: internal-only + namespace: platform + services: + - name: kube-prometheus-stack-grafana + namespace: observability + port: 80 + tls: + secretName: grafana-tls diff --git a/infra/kubernetes/platform/traefik/ingressroute-longhorn.yaml b/infra/kubernetes/platform/traefik/ingressroute-longhorn.yaml new file mode 100644 index 0000000..51711ba --- /dev/null +++ b/infra/kubernetes/platform/traefik/ingressroute-longhorn.yaml @@ -0,0 +1,22 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: longhorn + namespace: platform + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production +spec: + entryPoints: + - websecure + routes: + - match: Host(`longhorn.coreworlds.io`) + kind: Rule + middlewares: + - name: internal-only + namespace: platform + services: + - name: longhorn-frontend + namespace: longhorn-system + port: 80 + tls: + secretName: longhorn-tls diff --git a/infra/kubernetes/platform/traefik/kustomization.yaml b/infra/kubernetes/platform/traefik/kustomization.yaml index 7b00c1d..1d1aede 100644 --- a/infra/kubernetes/platform/traefik/kustomization.yaml +++ b/infra/kubernetes/platform/traefik/kustomization.yaml @@ -3,3 +3,7 @@ kind: Kustomization resources: - helmchartconfig.yaml - middleware-default-headers.yaml + - middleware-internal-only.yaml + - ingressroute-argocd.yaml + - ingressroute-grafana.yaml + - ingressroute-longhorn.yaml diff --git a/infra/kubernetes/platform/traefik/middleware-internal-only.yaml b/infra/kubernetes/platform/traefik/middleware-internal-only.yaml new file mode 100644 index 0000000..f0d049a --- /dev/null +++ b/infra/kubernetes/platform/traefik/middleware-internal-only.yaml @@ -0,0 +1,11 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: internal-only + namespace: platform +spec: + ipAllowList: + sourceRange: + - 192.168.1.0/24 + - 10.42.0.0/16 + - 10.43.0.0/16 diff --git a/infra/ubiquiti/README.md b/infra/ubiquiti/README.md index 1fee0f6..915b74f 100644 --- a/infra/ubiquiti/README.md +++ b/infra/ubiquiti/README.md @@ -10,64 +10,79 @@ Internet ▼ UCG (Ubiquiti Cloud Gateway) │ - ├── VLAN 1 - Management (10.0.1.0/24) - ├── VLAN 10 - Servers (10.0.10.0/24) - ├── VLAN 20 - IoT (10.0.20.0/24) - └── VLAN 99 - Guest (10.0.99.0/24) + └── LAN (192.168.1.0/24) + │ + └── catherby (192.168.1.50) — k3s single-node cluster ``` -## VLAN Configuration +## Node -| VLAN ID | Name | Subnet | Purpose | -|---------|------------|-----------------|------------------------| -| 1 | Management | 10.0.1.0/24 | Network devices, admin | -| 10 | Servers | 10.0.10.0/24 | k3s cluster nodes | -| 20 | IoT | 10.0.20.0/24 | IoT devices | -| 99 | Guest | 10.0.99.0/24 | Guest WiFi | - -## DHCP Reservations (VLAN 10 — Servers) - -| Hostname | IP Address | MAC Address | Role | -|----------|-------------|-------------------|-------------| -| nuc01 | 10.0.10.11 | XX:XX:XX:XX:XX:01 | k3s server | -| nuc02 | 10.0.10.12 | XX:XX:XX:XX:XX:02 | k3s agent | -| nuc03 | 10.0.10.13 | XX:XX:XX:XX:XX:03 | k3s agent | +| Hostname | IP Address | Role | +|-----------|---------------|-------------------| +| catherby | 192.168.1.50 | k3s server (all) | ## Port Forwarding Rules -| Name | External Port | Internal IP | Internal Port | Protocol | -|------------|---------------|--------------|---------------|----------| -| HTTP | 80 | 10.0.10.11 | 80 | TCP | -| HTTPS | 443 | 10.0.10.11 | 443 | TCP | -| k3s API | 6443 | 10.0.10.11 | 6443 | TCP | +| Name | External Port | Internal IP | Internal Port | Protocol | +|-------|---------------|----------------|---------------|----------| +| HTTP | 80 | 192.168.1.50 | 80 | TCP | +| HTTPS | 443 | 192.168.1.50 | 443 | TCP | -> **Note**: HTTP/HTTPS traffic routes to nuc01 where Traefik runs as the ingress controller. -> k3s API port is only forwarded if external kubectl access is needed. +> **Note**: HTTP/HTTPS traffic routes to catherby where Traefik runs as the ingress controller. +> Do NOT forward port 6443 (k3s API) — kubectl access is LAN-only or via VPN. + +## Split-Horizon DNS + +Same hostnames (`*.coreworlds.io`) resolve differently depending on the client's network: + +| Source | Resolution | Path | +|----------|-------------------------------------|--------------------------------------------| +| Internet | Cloudflare DNS → public IP → UCG | WAN:443 → port-forward → 192.168.1.50:443 | +| LAN | UCG local DNS → 192.168.1.50 | Direct to 192.168.1.50:443 (no hairpin) | + +### Why split-horizon? + +Without it, LAN clients hitting the public IP would require hairpin NAT (traffic leaves the network and comes back in). UCG local DNS overrides avoid this — LAN clients resolve directly to the node IP. + +## UCG Local DNS Configuration + +Add these entries in **Settings → Networks → DNS**: + +| Hostname | IP Address | +|----------------------|----------------| +| `coreworlds.io` | 192.168.1.50 | +| `*.coreworlds.io` | 192.168.1.50 | + +> If the UCG doesn't support wildcard DNS entries, add individual records: +> `api.coreworlds.io`, `argocd.coreworlds.io`, `grafana.coreworlds.io`, `longhorn.coreworlds.io`, +> `preview.coreworlds.io`, `api-preview.coreworlds.io` + +## Cloudflare DNS (External) + +Managed in Cloudflare dashboard for `coreworlds.io`: + +| Type | Name | Value | Proxy | +|-------|-------------------|-------------------|-------| +| A | `coreworlds.io` | (public static IP) | Off | +| CNAME | `*` | `coreworlds.io` | Off | + +> Cloudflare proxy must be **off** (DNS-only / grey cloud) so that cert-manager DNS-01 challenges work and Traefik sees real client IPs for the internal-only middleware. ## Firewall Rules -### Inter-VLAN Rules -- **Servers → Internet**: Allow all outbound -- **Servers → Management**: Allow (for UCG API access) -- **IoT → Servers**: Deny (isolate IoT from cluster) -- **Guest → ***: Allow Internet only, block all local - ### Inbound Rules - Allow established/related connections -- Allow HTTP (80) and HTTPS (443) to VLAN 10 +- Allow HTTP (80) and HTTPS (443) to 192.168.1.50 - Drop all other inbound -## DNS Configuration - -- **Internal DNS**: Use UCG as DNS server for VLAN 10 -- **External DNS**: Cloudflare (1.1.1.1, 1.0.0.1) -- **Local DNS entries**: Add `*.homelab.local` → 10.0.10.11 for internal access - ## Setup Steps -1. **Create VLANs** in UniFi Network → Settings → Networks -2. **Assign ports** on the switch to VLAN 10 for NUC connections -3. **Create DHCP reservations** for each NUC (Settings → Networks → VLAN 10) -4. **Add port forwarding rules** (Settings → Firewall & Security → Port Forwarding) -5. **Configure firewall rules** (Settings → Firewall & Security → Firewall Rules) -6. **Set local DNS** entries for *.homelab.local +1. **Add port forwarding rules** (Settings → Firewall & Security → Port Forwarding) + - WAN 80 → 192.168.1.50:80 + - WAN 443 → 192.168.1.50:443 +2. **Configure local DNS** (Settings → Networks → DNS) + - `*.coreworlds.io` → 192.168.1.50 +3. **Configure Cloudflare DNS** (Cloudflare dashboard) + - A record: `coreworlds.io` → public IP + - CNAME: `*` → `coreworlds.io` +4. **Configure firewall rules** (Settings → Firewall & Security → Firewall Rules) diff --git a/infra/ubiquiti/network-diagram.md b/infra/ubiquiti/network-diagram.md index 0cda9b4..83557d4 100644 --- a/infra/ubiquiti/network-diagram.md +++ b/infra/ubiquiti/network-diagram.md @@ -1,37 +1,66 @@ # Network Diagram -``` - ┌──────────────┐ - │ Internet │ - └──────┬───────┘ - │ - ┌──────┴───────┐ - │ UCG │ - │ 10.0.1.1 │ - └──────┬───────┘ - │ - ┌────────────┼────────────┐ - │ │ │ - ┌──────┴───┐ ┌─────┴────┐ ┌────┴──────┐ - │ VLAN 10 │ │ VLAN 20 │ │ VLAN 99 │ - │ Servers │ │ IoT │ │ Guest │ - └──────┬───┘ └──────────┘ └───────────┘ - │ - ┌────────────┼────────────┐ - │ │ │ - ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ - │ nuc01 │ │ nuc02 │ │ nuc03 │ - │ .10.11 │ │ .10.12 │ │ .10.13 │ - │ server │ │ agent │ │ agent │ - └─────────┘ └─────────┘ └─────────┘ +## External Traffic Flow - Services on k3s cluster: - ┌─────────────────────────────────────┐ - │ Traefik (Ingress) ← :80/:443 │ - │ ArgoCD ← :8080 │ - │ Grafana ← :3001 │ - │ PostgreSQL (CNPG) ← :5432 │ - │ Valkey ← :6379 │ - │ Longhorn UI ← :8000 │ - └─────────────────────────────────────┘ +``` + ┌──────────────┐ + │ Internet │ + └──────┬───────┘ + │ + ┌────────┴────────┐ + │ Cloudflare DNS │ + │ coreworlds.io │ + │ → public IP │ + └────────┬────────┘ + │ + ┌──────┴───────┐ + │ UCG │ + │ WAN :443 │ + └──────┬───────┘ + │ port-forward + ┌──────┴───────┐ + │ catherby │ + │ 192.168.1.50 │ + │ Traefik │ + └──────┬───────┘ + │ + ┌─────────────┼─────────────┐ + │ │ │ + coreworlds.io api.coreworlds.io ... + (web app) (api server) +``` + +## LAN Traffic Flow (Split-Horizon DNS) + +``` + ┌──────────────┐ + │ LAN Client │ + └──────┬───────┘ + │ DNS query: argocd.coreworlds.io + ┌──────┴───────┐ + │ UCG DNS │ + │ *.coreworlds │ + │ → 192.168.1 │ + │ .50 │ + └──────┬───────┘ + │ direct (no hairpin NAT) + ┌──────┴───────┐ + │ catherby │ + │ 192.168.1.50 │ + │ Traefik │ + └──────────────┘ +``` + +## Service Routing + +``` +Traefik (192.168.1.50:443) + │ + ├── coreworlds.io → web (public) + ├── api.coreworlds.io → api (public) + ├── preview.coreworlds.io → web (public, preview ns) + ├── api-preview.coreworlds.io → api (public, preview ns) + ├── argocd.coreworlds.io → argocd (LAN only — internal-only middleware) + ├── grafana.coreworlds.io → grafana (LAN only — internal-only middleware) + └── longhorn.coreworlds.io → longhorn (LAN only — internal-only middleware) ``` diff --git a/scripts/seal-secret.sh b/scripts/seal-secret.sh index fc41e97..72c5137 100755 --- a/scripts/seal-secret.sh +++ b/scripts/seal-secret.sh @@ -34,7 +34,7 @@ kubectl create secret generic "$SECRET_NAME" \ | kubeseal \ --format yaml \ --controller-namespace kube-system \ - --controller-name sealed-secrets \ + --controller-name sealed-secrets-helm \ > "${SECRET_NAME}-sealed.yaml" echo "Sealed secret written to ${SECRET_NAME}-sealed.yaml" diff --git a/scripts/ucg-dns-setup.sh b/scripts/ucg-dns-setup.sh new file mode 100755 index 0000000..fe75a13 --- /dev/null +++ b/scripts/ucg-dns-setup.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail + +UCG_HOST="${UCG_HOST:-192.168.1.1}" +BASE_URL="https://${UCG_HOST}" + +TARGET_IP="${1:-192.168.1.50}" +DOMAIN="${2:-coreworlds.io}" + +RECORDS=( + "$DOMAIN" + "*.$DOMAIN" +) + +# --- Auth --- + +if [[ -z "${UCG_API_KEY:-}" ]]; then + echo "Error: UCG_API_KEY is not set" + echo "Create an API key in UniFi OS → Settings → API Keys" + exit 1 +fi + +AUTH_HEADER="X-API-Key: ${UCG_API_KEY}" + +# --- Fetch existing records --- + +echo "Fetching existing static DNS entries..." +EXISTING=$(curl -sk -X GET "${BASE_URL}/proxy/network/v2/api/site/default/static-dns" \ + -H "$AUTH_HEADER") + +# --- Create records --- + +for RECORD in "${RECORDS[@]}"; do + # Skip if record already exists + if echo "$EXISTING" | grep -q "\"key\":\"${RECORD}\""; then + echo " [skip] ${RECORD} → ${TARGET_IP} (already exists)" + continue + fi + + echo " [create] ${RECORD} → ${TARGET_IP}" + HTTP_CODE=$(curl -sk -X POST "${BASE_URL}/proxy/network/v2/api/site/default/static-dns" \ + -H "$AUTH_HEADER" \ + -H "Content-Type: application/json" \ + -d "{\"key\":\"${RECORD}\",\"value\":\"${TARGET_IP}\",\"record_type\":\"A\",\"enabled\":true}" \ + -o /dev/null \ + -w '%{http_code}') + + if [[ "$HTTP_CODE" == "200" || "$HTTP_CODE" == "201" ]]; then + echo " ✓ created" + else + echo " ✗ failed (HTTP ${HTTP_CODE})" + fi +done + +# --- Verify --- + +echo "" +echo "Current static DNS entries:" +curl -sk -X GET "${BASE_URL}/proxy/network/v2/api/site/default/static-dns" \ + -H "$AUTH_HEADER" | python3 -m json.tool 2>/dev/null || echo "(could not pretty-print response)" + +echo "" +echo "Done. Test with: dig @${UCG_HOST} ${DOMAIN}"