cilium, argo

This commit is contained in:
2026-05-18 13:20:42 -05:00
parent 6f0c751f63
commit cdc78955b1
18 changed files with 821 additions and 136 deletions

View File

@@ -1,136 +1,180 @@
# fastpass GitOps Bootstrap # fastpass — ArgoCD Bootstrap
This document is the complete procedure to go from a freshly provisioned Talos This document covers the manual steps required to bootstrap ArgoCD on the
cluster to a fully GitOps-managed platform. After completing these steps, all `fastpass` cluster. After these steps, all further changes are made via Git.
future changes are made via git — never `kubectl apply` again.
## Prerequisites **Prerequisites:**
- All 6 Talos nodes `Ready` (`kubectl get nodes`)
- Cilium healthy (`cilium status --wait`)
- IP pools applied (`kubectl get ciliumloadbalancerippool`)
- `city-hall` has `helm`, `kubectl`, and `talosctl` installed
- SSH keypair for ArgoCD → Gitea exists at `/home/wed/.ssh/argocd_gitea`
- Talos cluster running (`space-mountain`, `big-thunder-mountain`, `splash-mountain` CP nodes) ---
- Cilium installed as CNI (done at Talos bootstrap via talhelper)
- `kubectl` configured and pointing at `fastpass`
- `kubeconfig` on `city-hall` at `~/.kube/config`
- 1Password Connect credentials JSON downloaded from your 1Password account
(Developer Tools > Connect Servers > New Server)
- 1Password Connect token generated for the `mk-labs` vault
## Step 1 — Install ArgoCD ## Step 1 — Create the argocd namespace
This is the only `kubectl apply` you will ever run against this cluster.
```bash ```bash
kubectl create namespace argocd kubectl apply -f cluster/argocd/namespace.yaml
kubectl apply -n argocd \
--server-side \
--force-conflicts \
-f https://raw.githubusercontent.com/argoproj/argo-cd/v3.4.2/manifests/install.yaml
# Wait for ArgoCD to be healthy
kubectl wait --for=condition=available deployment/argocd-server \
-n argocd --timeout=120s
``` ```
## Step 2 — Bootstrap the 1Password Connect secret ---
This is the only secret you will ever manage manually. Everything else flows ## Step 2 — Add Gitea to ArgoCD known hosts
through ESO from here.
ArgoCD refuses SSH connections to unknown hosts. Pre-populate the known hosts
configmap before ArgoCD starts:
```bash ```bash
# Create the namespace first ssh-keyscan -t ed25519 gitea.mk-labs.cloud | \
kubectl create namespace onepassword-connect kubectl create configmap argocd-ssh-known-hosts-cm \
--namespace argocd \
# Apply the credentials file (downloaded from 1Password) --from-file=ssh_known_hosts=/dev/stdin \
kubectl create secret generic op-credentials \ --dry-run=client -o yaml | kubectl apply -f -
--from-file=1password-credentials.json=/path/to/1password-credentials.json \
-n onepassword-connect
# Apply the Connect API token
kubectl create secret generic connect-token \
--from-literal=token=<your-connect-token> \
-n onepassword-connect
``` ```
## Step 3 — Apply the App of Apps ---
This is the second and final manual apply. From this point on, ArgoCD manages ## Step 3 — Create the Gitea repo secret
everything, including itself.
This is the only credential that must be manually created. Once ESO is running,
secrets will be managed via 1Password. For now, create it directly:
```bash
kubectl create secret generic gitea-repo \
--namespace argocd \
--from-literal=type=git \
--from-literal=url=git@gitea.mk-labs.cloud:rblundon/homelab.git \
--from-file=sshPrivateKey=/home/wed/.ssh/argocd_gitea
kubectl label secret gitea-repo -n argocd \
argocd.argoproj.io/secret-type=repository
```
Verify:
```bash
kubectl -n argocd get secret gitea-repo
```
---
## Step 4 — Install ArgoCD via Helm
```bash
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
helm upgrade --install argocd argo/argo-cd \
--namespace argocd \
--create-namespace \
--version 7.8.23 \
--values cluster/argocd/values.yaml \
--wait
```
Wait for all ArgoCD pods to be ready:
```bash
kubectl -n argocd get pods --watch
```
---
## Step 5 — Get the initial admin password
```bash
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d; echo
```
Access the UI via port-forward (ingress-nginx isn't installed yet at this point):
```bash
kubectl port-forward svc/argocd-server -n argocd 8080:80
```
Browse to http://localhost:8080 — login with `admin` and the password above.
---
## Step 6 — Apply the app-of-apps
This is the second and final manual `kubectl apply`. It hands control to ArgoCD:
```bash ```bash
kubectl apply -n argocd -f cluster/argocd/apps-of-apps.yaml kubectl apply -n argocd -f cluster/argocd/apps-of-apps.yaml
``` ```
## Step 4 — Watch it go From this point, all changes are made via Git. ArgoCD will begin syncing
`cluster/platform/` and `cluster/applications/` automatically.
Watch the sync:
```bash ```bash
# Watch all Applications come up in wave order kubectl -n argocd get applications --watch
kubectl get applications -n argocd -w
# Or use the ArgoCD UI
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Then open https://localhost:8080
# Default admin password:
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d
``` ```
## Step 2.5 — Vendor Gateway API CRDs ---
Before applying the App of Apps, vendor the Gateway API CRDs into git. ## Step 7 — Approve CSRs
Run from the repo root on city-hall:
After nodes join and kubelet starts requesting certs, approve pending CSRs:
```bash ```bash
curl -L https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml \ kubectl get csr --no-headers | awk '{print $1}' | xargs kubectl certificate approve
-o cluster/platform/gateway-api/manifests/standard-install.yaml
curl -L https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/experimental-install.yaml \
-o cluster/platform/gateway-api/manifests/experimental-install.yaml
git add cluster/platform/gateway-api/manifests/
git commit -m "feat(platform): vendor gateway-api CRDs v1.2.1"
git push
``` ```
## Wave Order Run this periodically until no pending CSRs remain.
| Wave | Application | Depends On | ---
|------|---------------------|---------------------------------|
| 1 | external-secrets | (none) |
| 2 | onepassword-connect | external-secrets |
| 3 | cert-manager | onepassword-connect (CF secret) |
| 3 | gateway-api-crds | (none — CRDs only) |
| 4 | cilium-config | gateway-api-crds (CRDs exist) |
| 5 | ingress-nginx | cilium-config (LB pool) |
| 5 | gateway (resource) | cilium-config (GatewayClass) |
| 6 | external-dns | ingress-nginx (IP to register) |
| 10+ | applications/* | platform complete |
## Upgrading ArgoCD ## Expected platform sync order
```bash Apps sync in wave order via `argocd.argoproj.io/sync-wave` annotations:
kubectl apply -n argocd \
--server-side \
--force-conflicts \
-f https://raw.githubusercontent.com/argoproj/argo-cd/v<NEW_VERSION>/manifests/install.yaml
```
Update the version comment in `cluster/argocd/install.yaml` to match. | Wave | App |
|------|-----|
| 0 | apps-of-apps (root) |
| 1 | cert-manager CRDs |
| 2 | cert-manager, external-secrets |
| 3 | ingress-nginx |
| 4 | external-dns |
| 5+ | application workloads |
---
## Troubleshooting ## Troubleshooting
**ArgoCD can't reach Gitea:**
```bash ```bash
# Check sync status of all apps kubectl -n argocd logs deployment/argocd-repo-server | grep -i "error\|ssh"
kubectl get applications -n argocd
# Check why an app is out of sync
kubectl describe application <name> -n argocd
# Force a sync
kubectl patch application <name> -n argocd \
--type merge \
-p '{"operation": {"initiatedBy": {"username": "admin"}, "sync": {}}}'
# Check ESO secret sync
kubectl get externalsecret -A
kubectl describe externalsecret <name> -n <namespace>
``` ```
Check that the `gitea-repo` secret exists and the known hosts configmap has
`gitea.mk-labs.cloud`.
**App stuck in `Unknown` state:**
```bash
kubectl -n argocd get application <name> -o yaml | grep -A10 "conditions"
```
**self-reference loop (app syncing itself):**
Ensure `apps-of-apps.yaml` uses `include: "*/application.yaml"` with
`recurse: true` — this prevents ArgoCD from picking up its own manifest.
**ArgoCD pruning itself:**
The `argocd` namespace Applications must have `prune: false`. Never enable
prune on any Application that manages the `argocd` namespace.
**Helm valueFiles field:**
The correct field name is `valueFiles` (plural). `valuesFile` and `valuesFiles`
are silently ignored — ArgoCD syncs green but your values aren't applied.
---
## Post-bootstrap
Once ingress-nginx and cert-manager are synced:
- ArgoCD UI available at https://argocd.local.mk-labs.cloud
- Change the admin password: `argocd account update-password`
- Authentik OIDC: uncomment the `oidc.config` block in `values.yaml` once
the ArgoCD provider is configured in Authentik
- Migrate `gitea-repo` secret to ESO once 1Password Connect is running

110
cluster/argocd/values.yaml Normal file
View File

@@ -0,0 +1,110 @@
# ─── ArgoCD Helm values — fastpass cluster ───────────────────────────────────
# Chart: argo/argo-cd
# Version: 7.8.23
#
# Install:
# helm repo add argo https://argoproj.github.io/argo-helm
# helm repo update
# helm upgrade --install argocd argo/argo-cd \
# --namespace argocd \
# --create-namespace \
# --version 7.8.23 \
# --values cluster/argocd/values.yaml \
# --wait
#
# Then apply the root app-of-apps (second and final manual step):
# kubectl apply -n argocd -f cluster/argocd/apps-of-apps.yaml
# ─────────────────────────────────────────────────────────────────────────────
# ─── Global ──────────────────────────────────────────────────────────────────
global:
domain: argocd.local.mk-labs.cloud
# ─── ArgoCD server ───────────────────────────────────────────────────────────
server:
# Run insecure — TLS is terminated at ingress-nginx, not at ArgoCD itself.
# Without this, ArgoCD redirects to HTTPS internally and breaks behind ingress.
extraArgs:
- --insecure
ingress:
enabled: true
ingressClassName: nginx
hostname: argocd.local.mk-labs.cloud
tls: true
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
nginx.ingress.kubernetes.io/ssl-passthrough: "false"
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
# ─── ConfigMap — core ArgoCD settings ────────────────────────────────────────
configs:
params:
# Matches --insecure above — tells the server not to handle TLS
server.insecure: "true"
cm:
# Gitea as the source repo — HTTPS with token auth (set in secret below)
# SSH alternative is in the repositories section if preferred
url: https://argocd.local.mk-labs.cloud
# Authentik OIDC SSO
# Uncomment once Authentik is configured with an ArgoCD OIDC provider
# oidc.config: |
# name: Authentik
# issuer: https://authentik.local.mk-labs.cloud/application/o/argocd/
# clientID: <client-id-from-authentik>
# clientSecret: $oidc.authentik.clientSecret
# requestedScopes:
# - openid
# - profile
# - email
# - groups
# Resource health checks — ignore progressing Cilium CRDs
resource.customizations.health.cilium.io_CiliumLoadBalancerIPPool: |
hs = {}
hs.status = "Healthy"
hs.message = "IP pool ready"
return hs
# RBAC — admin gets full access, SSO groups mapped after Authentik integration
rbac:
policy.default: role:readonly
policy.csv: |
g, argocd-admins, role:admin
# Repository credentials — Gitea SSH
# Private key is injected via ESO once external-secrets is bootstrapped.
# For initial bootstrap, create the secret manually:
# kubectl create secret generic gitea-repo \
# --namespace argocd \
# --from-literal=type=git \
# --from-literal=url=git@gitea.mk-labs.cloud:rblundon/homelab.git \
# --from-file=sshPrivateKey=/path/to/argocd_gitea_ed25519 \
# kubectl label secret gitea-repo -n argocd \
# argocd.argoproj.io/secret-type=repository
repositories:
homelab-repo:
url: git@gitea.mk-labs.cloud:rblundon/homelab.git
name: homelab
type: git
insecure: "false"
# ─── Redis ───────────────────────────────────────────────────────────────────
redis:
enabled: true
# ─── Notifications ───────────────────────────────────────────────────────────
# Disabled for now — wire into n8n via webhook later
notifications:
enabled: false
# ─── ApplicationSet controller ───────────────────────────────────────────────
applicationSet:
enabled: true
# ─── Dex (built-in OIDC) ─────────────────────────────────────────────────────
# Disabled — using Authentik as the OIDC provider
dex:
enabled: false

View File

@@ -59,6 +59,8 @@ Required for provisioning and managing the `fastpass` Talos Kubernetes cluster.
| `talosctl` | Talos cluster CLI | See below | | `talosctl` | Talos cluster CLI | See below |
| `kubectl` | Kubernetes CLI | See below | | `kubectl` | Kubernetes CLI | See below |
| `helm` | Kubernetes package manager | See below | | `helm` | Kubernetes package manager | See below |
| `cilium` | Cilium CNI CLI (status, connectivity tests) | See below |
| `argocd` | ArgoCD CLI (app management, account operations) | See below |
| `age` | Encryption key generation (used by SOPS) | `sudo apt install age -y` | | `age` | Encryption key generation (used by SOPS) | `sudo apt install age -y` |
| `sops` | Secret encryption for `talsecret.sops.yaml` | See below | | `sops` | Secret encryption for `talsecret.sops.yaml` | See below |
@@ -108,6 +110,37 @@ Verify:
helm version helm version
``` ```
#### Cilium CLI
```bash
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
curl -L --remote-name-all \
https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-amd64.tar.gz
tar -xzvf cilium-linux-amd64.tar.gz
sudo mv cilium /usr/local/bin/
rm cilium-linux-amd64.tar.gz
```
Verify:
```bash
cilium version
```
#### ArgoCD CLI
```bash
ARGOCD_VERSION=$(curl -s https://api.github.com/repos/argoproj/argo-cd/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -sSL -o /tmp/argocd \
https://github.com/argoproj/argo-cd/releases/download/${ARGOCD_VERSION}/argocd-linux-amd64
chmod +x /tmp/argocd
sudo mv /tmp/argocd /usr/local/bin/argocd
```
Verify:
```bash
argocd version --client
```
#### SOPS #### SOPS
```bash ```bash
@@ -274,48 +307,67 @@ talosctl --talosconfig clusterconfig/talosconfig \
--nodes 10.1.71.66 kubeconfig ~/.kube/config --nodes 10.1.71.66 kubeconfig ~/.kube/config
``` ```
### 8. Approve Worker CSRs ### 8. Approve CSRs
```bash ```bash
kubectl get csr kubectl get csr --no-headers | awk '{print $1}' | xargs kubectl certificate approve
kubectl certificate approve $(kubectl get csr --no-headers | awk '{print $1}')
``` ```
Run periodically until no pending CSRs remain.
### 9. Install Cilium (CNI) ### 9. Install Cilium (CNI)
Cilium is managed by Helm only — never via ArgoCD.
```bash ```bash
helm repo add cilium https://helm.cilium.io/ helm repo add cilium https://helm.cilium.io/
helm repo update helm repo update
helm install cilium cilium/cilium \ helm upgrade --install cilium cilium/cilium \
--version 1.17.3 \
--namespace kube-system \ --namespace kube-system \
--set ipam.mode=kubernetes \ --values talos/cilium/cilium-values.yaml \
--set routingMode=tunnel \ --wait \
--set tunnelProtocol=vxlan \ --timeout 5m
--set kubeProxyReplacement=true \
--set k8sServiceHost=10.1.71.65 \
--set k8sServicePort=6443 \
--set securityContext.capabilities.ciliumAgent="{CHOWN,KILL,NET_ADMIN,NET_RAW,IPC_LOCK,SYS_ADMIN,SYS_RESOURCE,DAC_OVERRIDE,FOWNER,SETGID,SETUID}" \
--set securityContext.capabilities.cleanCiliumState="{NET_ADMIN,SYS_ADMIN,SYS_RESOURCE}" \
--set cgroup.autoMount.enabled=false \
--set cgroup.hostRoot=/sys/fs/cgroup
``` ```
Note: The explicit `securityContext.capabilities` flags are required for Talos — Talos Apply IP pools:
restricts privileged container capabilities by default. The `cgroup.autoMount.enabled=false`
and `cgroup.hostRoot` flags are required because Talos manages cgroups itself.
### 10. Verify Cluster ```bash
kubectl apply -f talos/cilium/ip-pools.yaml
```
Verify:
```bash
cilium status --wait
kubectl get ciliumloadbalancerippool
kubectl get ciliuml2announcementpolicy
```
**Important:** The `cilium-values.yaml` contains required Talos-specific overrides:
- `securityContext.capabilities` — removes `SYS_MODULE` (blocked by Talos permanently)
- `cgroup.autoMount.enabled: false` — Talos manages cgroups itself
- `l2announcements.interface: ens18` — Proxmox VMs use `ens18`, not `eth0`
- `l2podAnnouncements.interface: ens18` — same reason
### 10. Bootstrap ArgoCD
See `cluster/argocd/BOOTSTRAP.md` for the full ArgoCD bootstrap sequence.
Summary:
1. `kubectl apply -f cluster/argocd/namespace.yaml`
2. Add Gitea SSH host key to ArgoCD known hosts
3. Create `gitea-repo` secret with SSH private key at `~/.ssh/argocd_gitea`
4. `helm upgrade --install argocd argo/argo-cd --values cluster/argocd/values.yaml`
5. `kubectl apply -n argocd -f cluster/argocd/apps-of-apps.yaml`
### 11. Verify Cluster
```bash ```bash
kubectl get nodes kubectl get nodes
kubectl get pods -n kube-system kubectl get pods -A | grep -v Running | grep -v Completed
cilium status --wait
``` ```
All nodes should show `Ready`. Cilium pods should be `Running`. All nodes should show `Ready`. All pods should be `Running` or `Completed`.
### 11. Post-Install HA Setup
See output of `terraform output` for the full HA registration commands.
Register VMs with Proxmox HA and add to node-affinity rules once the
cluster is stable and the ISO is no longer needed for boot.

View File

@@ -0,0 +1,433 @@
# fastpass cluster rebuild runbook
Root cause: Talos 1.13 defaults to `enforce: baseline` for Pod Security admission
control. Cilium requires `NET_ADMIN`, `NET_RAW`, and `SYS_ADMIN` — all blocked
under baseline. Fix: `enforce: privileged` in talconfig.yaml before generating
machine configs. Everything else in this runbook follows from a clean wipe.
All commands run from `city-hall` (10.1.71.38) in the `talos/talhelper/`
directory unless otherwise noted.
---
## Phase 0 — pre-flight
Confirm tools are present and on the right versions:
```bash
talhelper --version # expect 3.1.10
talosctl version --client # expect v1.13.2
kubectl version --client # expect v1.36.x
helm version # expect v3.x
```
Confirm the age key and sops config are in place:
```bash
cat ~/.config/sops/age/keys.txt | head -1 # should show "# created: ..."
cat /opt/git/homelab/.sops.yaml # should reference your age pubkey
```
---
## Phase 1 — wipe and redeploy Talos nodes
### 1.1 Reset all six nodes
Run from `talos/talhelper/`. If the nodes are currently in a broken state,
reset them to wipe the install and reboot into maintenance mode:
```bash
# Control plane nodes
for node in 10.1.71.66 10.1.71.67 10.1.71.68; do
talosctl reset \
--talosconfig clusterconfig/talosconfig \
--nodes $node \
--graceful=false \
--reboot \
--system-labels-to-wipe STATE \
--system-labels-to-wipe EPHEMERAL
done
# Worker nodes
for node in 10.1.71.69 10.1.71.70 10.1.71.71; do
talosctl reset \
--talosconfig clusterconfig/talosconfig \
--nodes $node \
--graceful=false \
--reboot \
--system-labels-to-wipe STATE \
--system-labels-to-wipe EPHEMERAL
done
```
Wait ~2 minutes. Nodes boot into maintenance mode (no OS installed, just the
Talos installer kernel running in RAM). You can verify via Proxmox console —
the node should show the Talos maintenance screen.
If nodes are completely unreachable (talosctl can't connect), boot them from
the Talos ISO via Proxmox and they'll come up in maintenance mode automatically.
### 1.2 Regenerate machine configs
The talconfig.yaml now has `enforce: privileged`. Regenerate everything:
```bash
cd /opt/git/homelab/talos/talhelper
talhelper genconfig
```
This writes fresh configs to `clusterconfig/`. Commit the regenerated configs
before applying:
```bash
cd /opt/git/homelab
git add talos/
git commit -m "fix: set enforce:privileged for Cilium compatibility"
git push
```
### 1.3 Apply machine configs
```bash
cd /opt/git/homelab/talos/talhelper
talhelper gencommand apply --extra-flags="--insecure" | bash
```
Expected output for each node: `Applied configuration without a reboot`
The nodes will install Talos to disk and reboot automatically. Watch progress
on one control plane node:
```bash
talosctl -n 10.1.71.66 --talosconfig clusterconfig/talosconfig dmesg --follow
```
Wait for all six nodes to finish installing and come back up (~3-5 min).
### 1.4 Bootstrap etcd
Run bootstrap exactly once — on the first control plane node only:
```bash
talhelper gencommand bootstrap | bash
```
This initializes etcd on `space-mountain`. The other two control plane nodes
join automatically. Do not run bootstrap again — it will corrupt the cluster.
### 1.5 Get kubeconfig
```bash
talhelper gencommand kubeconfig | bash
# or directly:
talosctl kubeconfig \
--talosconfig clusterconfig/talosconfig \
--nodes 10.1.71.66 \
~/.kube/config
```
### 1.6 Verify all 6 nodes are Ready
```bash
kubectl get nodes -o wide --watch
```
Wait until all six show `Ready`. This takes 3-5 minutes after bootstrap.
Do not proceed to Phase 2 until all six nodes are `Ready`.
Expected output:
```
NAME STATUS ROLES AGE VERSION
big-thunder-mountain Ready control-plane 5m v1.32.3
haunted-mansion Ready <none> 4m v1.32.3
jungle-cruise Ready <none> 4m v1.32.3
peter-pans-flight Ready <none> 4m v1.32.3
space-mountain Ready control-plane 5m v1.32.3
splash-mountain Ready control-plane 5m v1.32.3
```
### 1.7 Approve all pending CSRs
```bash
kubectl get csr --no-headers | grep Pending | awk '{print $1}' | xargs kubectl certificate approve
```
Nodes will show `NotReady` until Cilium is installed — that's expected. The
condition clears after Phase 2.
---
## Phase 2 — install Cilium
Cilium is Helm-only. It is never managed by ArgoCD.
### 2.1 Add the Helm repo
```bash
helm repo add cilium https://helm.cilium.io/
helm repo update
```
### 2.2 Install Cilium
```bash
helm upgrade --install cilium cilium/cilium \
--version 1.17.3 \
--namespace kube-system \
--values /opt/git/homelab/talos/cilium/cilium-values.yaml \
--wait \
--timeout 5m
```
The `--wait` flag blocks until all Cilium pods are Running. If it times out,
check what's happening:
```bash
kubectl -n kube-system get pods -l app.kubernetes.io/name=cilium-agent
kubectl -n kube-system describe pod <cilium-agent-pod>
```
### 2.3 Apply IP pools
```bash
kubectl apply -f /opt/git/homelab/talos/cilium/ip-pools.yaml
```
### 2.4 Verify Cilium health
```bash
# Overall status
cilium status --wait
# All nodes should show cilium-agent as OK
kubectl -n kube-system get pods -l app.kubernetes.io/name=cilium-agent
# Confirm L2 announcement policy is active
kubectl get ciliuml2announcementpolicy
# Confirm IP pools are ready
kubectl get ciliumloadbalancerippool
```
### 2.5 Verify all nodes are Ready
```bash
kubectl get nodes
```
All six must show `Ready` before proceeding. If any are still `NotReady` after
Cilium is healthy, drain and uncordon to force a kubelet re-check:
```bash
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data
kubectl uncordon <node>
```
---
## Phase 3 — bootstrap ArgoCD
ArgoCD is installed manually once. After that, everything else is GitOps.
### 3.1 Create the argocd namespace
```bash
kubectl create namespace argocd
```
### 3.2 Install ArgoCD
```bash
kubectl apply -n argocd \
-f https://raw.githubusercontent.com/argoproj/argo-cd/v3.4.2/manifests/install.yaml
```
Wait for ArgoCD to come up:
```bash
kubectl -n argocd rollout status deployment argocd-server --timeout=3m
```
### 3.3 Apply server config (insecure mode for ingress TLS termination)
ArgoCD needs to run with `--insecure` so ingress-nginx handles TLS. This is in
your repo at `cluster/argocd/argocd-cmd-params-cm.yaml`:
```bash
kubectl apply -f /opt/git/homelab/cluster/argocd/argocd-cmd-params-cm.yaml
kubectl -n argocd rollout restart deployment argocd-server
kubectl -n argocd rollout status deployment argocd-server --timeout=2m
```
The ConfigMap should contain:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cmd-params-cm
namespace: argocd
data:
server.insecure: "true"
```
### 3.4 Bootstrap 1Password Connect secret
This is the only manual `kubectl apply` after ArgoCD install. Everything else
comes from Git.
```bash
# Create the secret from your 1Password Connect credentials file
kubectl create namespace external-secrets
kubectl create secret generic onepassword-connect-secret \
--namespace external-secrets \
--from-file=1password-credentials.json=/path/to/1password-credentials.json \
--from-literal=token=<your-1password-connect-token>
```
Verify:
```bash
kubectl -n external-secrets get secret onepassword-connect-secret
```
### 3.5 Configure ArgoCD to access Gitea via SSH
Add the Gitea SSH key to ArgoCD's known hosts and create the repo secret:
```bash
# Get Gitea's SSH host key
ssh-keyscan -t ed25519 mad-tea-party.local.mk-labs.cloud >> /tmp/gitea-known-hosts
# Create the repo credentials secret
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: gitea-repo-creds
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
type: git
url: git@mad-tea-party.local.mk-labs.cloud:rblundon/homelab.git
sshPrivateKey: |
$(cat ~/.ssh/argocd_gitea_ed25519 | sed 's/^/ /')
EOF
```
Add Gitea to ArgoCD known hosts:
```bash
argocd cert add-ssh mad-tea-party.local.mk-labs.cloud \
--ssh-known-hosts-data "$(ssh-keyscan -t ed25519 mad-tea-party.local.mk-labs.cloud 2>/dev/null)"
```
### 3.6 Apply the root Application
This is the app-of-apps entry point. It points ArgoCD at `cluster/platform/`
and `cluster/applications/`:
```bash
kubectl apply -f /opt/git/homelab/cluster/argocd/application.yaml
```
The argocd Application in that file must have `prune: false` to prevent ArgoCD
from ever pruning its own namespace:
```yaml
# cluster/argocd/application.yaml (key fields)
spec:
project: default
source:
repoURL: git@mad-tea-party.local.mk-labs.cloud:rblundon/homelab.git
targetRevision: main
path: cluster/platform
directory:
exclude: "application.yaml" # prevents self-reference loop
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
prune: false # NEVER prune argocd namespace
selfHeal: true
```
### 3.7 Watch the platform sync
```bash
# Get initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d; echo
# Port-forward to access UI (before ingress is up)
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Or use CLI
argocd login localhost:8080 --username admin --insecure
# Watch sync status
argocd app list
argocd app get platform
```
Platform apps will sync in wave order (sync-wave annotation). Expected order:
1. Wave 0: cert-manager CRDs
2. Wave 1: cert-manager, external-secrets
3. Wave 2: ingress-nginx
4. Wave 3: external-dns
5. Wave 4: application workloads
---
## Phase 4 — verify platform
```bash
# All platform pods running
kubectl get pods -A | grep -v Running | grep -v Completed
# Ingress controller has a LB IP from the pool
kubectl -n ingress-nginx get svc ingress-nginx-controller
# cert-manager is issuing certs
kubectl get clusterissuers
# External secrets operator is syncing
kubectl -n external-secrets get pods
# external-dns is running
kubectl -n external-dns get pods
```
---
## Known gotchas (learned the hard way)
**ArgoCD self-prune:** The `argocd-config` Application must have `prune: false`.
If prune is enabled and ArgoCD syncs itself, it will delete its own resources
and take itself down. Recovery requires manual re-apply.
**Multi-source self-reference loop:** If an Application's source path contains
its own `application.yaml`, ArgoCD enters a sync loop. Fix: add
`directory.exclude: "application.yaml"` to the source spec.
**ArgoCD Helm valueFiles:** The correct field name is `valueFiles` (plural), not
`valuesFile` or `valuesFiles`. Wrong field name causes silent failure — ArgoCD
syncs green but ignores your values.
**Cilium managed by Helm only:** Never add Cilium to ArgoCD. If ArgoCD manages
Cilium and there's a sync conflict, ArgoCD can restart Cilium mid-cluster
operation, breaking all networking. Helm is the correct tool here.
**IP pool API version:** CiliumLoadBalancerIPPool must use `cilium.io/v2alpha1`.
Wrong version creates the object but it never activates — LB IPs are assigned
but not advertised.
**Talos VIP and NIC name:** The VIP (10.1.71.65) is configured per-node in the
`networkInterfaces` section. Each control plane node declares the VIP on `ens18`.
Workers do not declare the VIP. If the NIC name changes on your Proxmox
VMs, check with: `talosctl -n <ip> get addresses --talosconfig clusterconfig/talosconfig`
**Bootstrap once only:** `talhelper gencommand bootstrap` initializes etcd.
Running it a second time on an already-bootstrapped cluster corrupts etcd.
If you accidentally run it twice, you need to wipe and start over.

View File

@@ -44,10 +44,12 @@ tunnelProtocol: vxlan
# Without this, LoadBalancer IPs are assigned but unreachable from outside the cluster. # Without this, LoadBalancer IPs are assigned but unreachable from outside the cluster.
l2announcements: l2announcements:
enabled: true enabled: true
interface: ens18
# Also enable L2 pod announcements so Cilium handles ARP for pod IPs directly # Also enable L2 pod announcements so Cilium handles ARP for pod IPs directly
l2podAnnouncements: l2podAnnouncements:
enabled: true enabled: true
interface: ens18
# ─── IP pools (defined as CRDs post-install, but referenced here for docs) ─── # ─── IP pools (defined as CRDs post-install, but referenced here for docs) ───
# Pool 1: ingress-nginx 10.1.71.80 - 10.1.71.89 # Pool 1: ingress-nginx 10.1.71.80 - 10.1.71.89
@@ -55,6 +57,39 @@ l2podAnnouncements:
# Apply these after Cilium is healthy: # Apply these after Cilium is healthy:
# kubectl apply -f talos/cilium/ip-pools.yaml # kubectl apply -f talos/cilium/ip-pools.yaml
# ─── Talos security context overrides ───────────────────────────────────────
# Talos permanently blocks SYS_MODULE and SYS_BOOT at the OS level regardless
# of pod security settings. Cilium's default caps include SYS_MODULE which
# causes clean-cilium-state to fail with "unable to apply caps".
# These overrides replace the default cap sets with Talos-compatible ones.
securityContext:
capabilities:
ciliumAgent:
- CHOWN
- KILL
- NET_ADMIN
- NET_RAW
- IPC_LOCK
- SYS_ADMIN
- SYS_RESOURCE
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
- PERFMON
- BPF
cleanCiliumState:
- NET_ADMIN
- SYS_ADMIN
- SYS_RESOURCE
- BPF
# Talos mounts cgroups differently — disable auto-mount and point at host root
cgroup:
autoMount:
enabled: false
hostRoot: /sys/fs/cgroup
# ─── Operator ──────────────────────────────────────────────────────────────── # ─── Operator ────────────────────────────────────────────────────────────────
operator: operator:
replicas: 1 # single-replica is fine for homelab; avoids scheduling issues on 3-node CP replicas: 1 # single-replica is fine for homelab; avoids scheduling issues on 3-node CP
@@ -80,7 +115,7 @@ gatewayAPI:
# ─── Security ──────────────────────────────────────────────────────────────── # ─── Security ────────────────────────────────────────────────────────────────
# Restrict host namespace access — pods can't escape to host network by default # Restrict host namespace access — pods can't escape to host network by default
hostNamespaceAccess: hostNamespaceAccess:
enabled: false enabled: true
# Enable network policies (default deny is applied per-namespace by ArgoCD apps) # Enable network policies (default deny is applied per-namespace by ArgoCD apps)
policyEnforcementMode: default policyEnforcementMode: default

View File

@@ -42,4 +42,4 @@ spec:
nodeSelector: nodeSelector:
matchLabels: {} matchLabels: {}
interfaces: interfaces:
- eth0 - ens18

View File

@@ -41,7 +41,6 @@ patches:
usernames: [] usernames: []
runtimeClasses: [] runtimeClasses: []
namespaces: namespaces:
- kube-system
- argocd - argocd
- cert-manager - cert-manager
- ingress-nginx - ingress-nginx
@@ -53,8 +52,20 @@ patches:
servers: servers:
- 10.1.71.21 # sundial — local NTP - 10.1.71.21 # sundial — local NTP
# DNS search domain so short names resolve inside the cluster # Sysctls required for Cilium eBPF on Talos
# bpf_jit_harden: 2 (Talos default) blocks Cilium's eBPF programs
# unprivileged_bpf_disabled: 1 (Talos default) blocks BPF for unprivileged containers
# perf_event_paranoid: 3 (Talos default) too restrictive for Cilium observability
sysctls:
net.core.bpf_jit_harden: "0"
kernel.unprivileged_bpf_disabled: "0"
kernel.perf_event_paranoid: "1"
# DNS — point at world-drive (UDM Pro), which forwards local.mk-labs.cloud
# to monorail and handles external resolution via DoH
network: network:
nameservers:
- 10.1.71.1
extraHostEntries: [] extraHostEntries: []
# Talos manages kubelet — these flags are additive # Talos manages kubelet — these flags are additive
@@ -90,7 +101,7 @@ controlPlane:
# VIP — floats across all three control plane nodes # VIP — floats across all three control plane nodes
# Talos handles VIP natively; no keepalived needed # Talos handles VIP natively; no keepalived needed
interfaces: interfaces:
- interface: eth0 - interface: ens18
vip: vip:
ip: 10.1.71.65 ip: 10.1.71.65
@@ -102,7 +113,7 @@ nodes:
controlPlane: true controlPlane: true
installDisk: /dev/sda installDisk: /dev/sda
networkInterfaces: networkInterfaces:
- interface: eth0 - interface: ens18
addresses: addresses:
- 10.1.71.66/24 - 10.1.71.66/24
routes: routes:
@@ -117,7 +128,7 @@ nodes:
controlPlane: true controlPlane: true
installDisk: /dev/sda installDisk: /dev/sda
networkInterfaces: networkInterfaces:
- interface: eth0 - interface: ens18
addresses: addresses:
- 10.1.71.67/24 - 10.1.71.67/24
routes: routes:
@@ -132,7 +143,7 @@ nodes:
controlPlane: true controlPlane: true
installDisk: /dev/sda installDisk: /dev/sda
networkInterfaces: networkInterfaces:
- interface: eth0 - interface: ens18
addresses: addresses:
- 10.1.71.68/24 - 10.1.71.68/24
routes: routes:
@@ -148,7 +159,7 @@ nodes:
controlPlane: false controlPlane: false
installDisk: /dev/sda installDisk: /dev/sda
networkInterfaces: networkInterfaces:
- interface: eth0 - interface: ens18
addresses: addresses:
- 10.1.71.69/24 - 10.1.71.69/24
routes: routes:
@@ -161,7 +172,7 @@ nodes:
controlPlane: false controlPlane: false
installDisk: /dev/sda installDisk: /dev/sda
networkInterfaces: networkInterfaces:
- interface: eth0 - interface: ens18
addresses: addresses:
- 10.1.71.70/24 - 10.1.71.70/24
routes: routes:
@@ -174,7 +185,7 @@ nodes:
controlPlane: false controlPlane: false
installDisk: /dev/sda installDisk: /dev/sda
networkInterfaces: networkInterfaces:
- interface: eth0 - interface: ens18
addresses: addresses:
- 10.1.71.71/24 - 10.1.71.71/24
routes: routes: