From aacb8eebbeb3d0ba35cbc1c236405ac38dede1de Mon Sep 17 00:00:00 2001 From: Scooby Husky Date: Mon, 9 Mar 2026 20:21:35 -0500 Subject: [PATCH] Initial commit --- .gitignore | 16 + CLAUDE.md | 106 + .../manifests/argocd-health-checks.yaml | 84 + apps/argocd/manifests/envoy-route.yaml | 20 + apps/argocd/manifests/external-secret.yaml | 23 + .../manifests/istio-virtualservice.yaml | 16 + apps/argocd/values.yaml | 78 + apps/frigate/manifests/envoy-route.yaml | 20 + .../manifests/istio-virtualservice.yaml | 27 + apps/frigate/manifests/pvc.yaml | 29 + apps/frigate/manifests/secret.yaml | 11 + apps/frigate/values.yaml | 182 ++ apps/gitlab/manifests/cnpg-cluster.yaml | 74 + apps/gitlab/manifests/envoy-routes.yaml | 86 + .../manifests/external-secret-saml.yaml | 27 + .../manifests/gitlab-unified-certificate.yaml | 19 + .../manifests/istio-virtualservices.yaml | 74 + apps/gitlab/manifests/pg-auto-restart.yaml | 206 ++ apps/gitlab/manifests/pgbouncer-pooler.yaml | 116 ++ .../manifests/praefect-cnpg-cluster.yaml | 35 + apps/gitlab/manifests/redis-auto-restart.yaml | 108 + apps/gitlab/manifests/redis-cluster.yaml | 95 + apps/gitlab/manifests/redis-sentinel-ha.yaml | 33 + apps/gitlab/manifests/sync-admin-job.yaml | 114 ++ apps/gitlab/values.yaml | 333 ++++ apps/guacamole/manifests/cnpg-cluster.yaml | 50 + .../manifests/db-init-schema-job.yaml | 114 ++ .../manifests/deployment-complete.yaml | 256 +++ apps/guacamole/manifests/envoy-gateway.yaml | 67 + apps/guacamole/manifests/envoy-route.yaml | 33 + apps/guacamole/manifests/external-secret.yaml | 29 + .../manifests/istio-virtualservice.yaml | 16 + apps/home-assistant/esphome/envoy-route.yaml | 20 + .../esphome/istio-virtualservice.yaml | 16 + apps/home-assistant/esphome/values.yaml | 57 + .../home-assistant/manifests/envoy-route.yaml | 20 + .../manifests/external-secret.yaml | 31 + .../manifests/istio-virtualservice.yaml | 17 + .../home-assistant/manifests/oidc-config.yaml | 24 + apps/home-assistant/values.yaml | 152 ++ apps/n8n/manifests/cnpg-cluster.yaml | 51 + apps/n8n/manifests/cnpg-secrets.yaml | 40 + apps/n8n/manifests/envoy-route.yaml | 20 + apps/n8n/manifests/external-secret.yaml | 49 + apps/n8n/manifests/hooks-configmap.yaml | 81 + apps/n8n/manifests/istio-virtualservice.yaml | 16 + apps/n8n/values.yaml | 161 ++ apps/nessus/manifests/cnpg-cluster.yaml | 52 + apps/nessus/manifests/cnpg-secrets.yaml | 44 + apps/nessus/manifests/deployment.yaml | 134 ++ apps/nessus/manifests/external-secret.yaml | 30 + .../manifests/istio-virtualservice.yaml | 31 + apps/nessus/manifests/pvc.yaml | 13 + apps/nextcloud/manifests/envoy-route.yaml | 20 + apps/nextcloud/manifests/external-secret.yaml | 26 + .../manifests/istio-virtualservice.yaml | 17 + .../manifests/mariadb-deployment.yaml | 92 + apps/nextcloud/manifests/mariadb-secrets.yaml | 35 + apps/nextcloud/manifests/pvc.yaml | 19 + apps/nextcloud/values.yaml | 111 ++ apps/rancher/manifests/external-secret.yaml | 23 + .../manifests/istio-virtualservice.yaml | 16 + apps/rancher/values.yaml | 47 + apps/teslamate/manifests/cnpg-cluster.yaml | 55 + apps/teslamate/manifests/cnpg-secrets.yaml | 39 + apps/teslamate/manifests/envoy-route.yaml | 20 + apps/teslamate/manifests/external-secret.yaml | 19 + .../manifests/istio-virtualservice.yaml | 16 + apps/teslamate/values.yaml | 112 ++ argocd-apps/apps/argocd.yaml | 34 + argocd-apps/apps/esphome.yaml | 31 + argocd-apps/apps/frigate.yaml | 34 + argocd-apps/apps/gitlab.yaml | 34 + argocd-apps/apps/guacamole.yaml | 25 + argocd-apps/apps/home-assistant.yaml | 34 + argocd-apps/apps/istio-app-routes.yaml | 25 + argocd-apps/apps/n8n.yaml | 34 + argocd-apps/apps/nessus.yaml | 25 + argocd-apps/apps/nextcloud.yaml | 34 + argocd-apps/apps/rancher.yaml | 34 + argocd-apps/apps/teslamate.yaml | 34 + argocd-apps/hook-rbac.yaml | 22 + argocd-apps/infrastructure/authentik.yaml | 34 + argocd-apps/infrastructure/cert-manager.yaml | 34 + .../infrastructure/checkov-scanner.yaml | 25 + argocd-apps/infrastructure/cilium.yaml | 34 + argocd-apps/infrastructure/cnpg.yaml | 25 + argocd-apps/infrastructure/crowdsec.yaml | 34 + argocd-apps/infrastructure/envoy-edge.yaml | 25 + argocd-apps/infrastructure/envoy-gateway.yaml | 31 + argocd-apps/infrastructure/external-dns.yaml | 34 + .../infrastructure/external-secrets.yaml | 31 + argocd-apps/infrastructure/grafana.yaml | 37 + argocd-apps/infrastructure/istio-base.yaml | 25 + argocd-apps/infrastructure/istio-cni.yaml | 31 + argocd-apps/infrastructure/istio-edge.yaml | 25 + .../infrastructure/istio-ingressgateway.yaml | 31 + .../infrastructure/istio-mesh-config.yaml | 25 + argocd-apps/infrastructure/istiod.yaml | 31 + .../infrastructure/kiali-operator.yaml | 31 + argocd-apps/infrastructure/kiali.yaml | 25 + .../infrastructure/kube-prometheus-stack.yaml | 34 + argocd-apps/infrastructure/kyverno.yaml | 34 + .../infrastructure/mariadb-operator.yaml | 25 + .../infrastructure/metrics-server.yaml | 31 + argocd-apps/infrastructure/mosquitto.yaml | 25 + .../infrastructure/netbird-operator.yaml | 31 + argocd-apps/infrastructure/netbird.yaml | 34 + .../infrastructure/prometheus-crds.yaml | 25 + .../infrastructure/redis-operator.yaml | 25 + argocd-apps/infrastructure/reloader.yaml | 31 + .../infrastructure/rook-ceph-cluster.yaml | 34 + .../infrastructure/rook-ceph-operator.yaml | 31 + .../infrastructure/scylla-manager.yaml | 31 + .../infrastructure/scylla-operator.yaml | 31 + argocd-apps/infrastructure/tetragon.yaml | 34 + argocd-apps/infrastructure/unpoller.yaml | 25 + argocd-apps/infrastructure/vault.yaml | 34 + argocd-apps/infrastructure/ztunnel.yaml | 25 + argocd-apps/projects.yaml | 57 + bootstrap.yaml | 24 + charts/empty-chart/Chart.yaml | 5 + charts/empty-chart/templates/.gitkeep | 0 charts/prometheus-operator-crds/Chart.yaml | 6 + .../templates/job.yaml | 79 + environments/default.yaml | 32 + hooks/rbac/hook-sa.yaml | 18 + hooks/scripts/vault-init-configmap.yaml | 140 ++ .../authentik/argocd-blueprint.yaml | 49 + infrastructure/authentik/ceph-blueprint.yaml | 63 + .../authentik/ceph-oidc-blueprint.yaml | 58 + infrastructure/authentik/cnpg-cluster.yaml | 53 + infrastructure/authentik/cnpg-podmonitor.yaml | 16 + infrastructure/authentik/cnpg-secrets.yaml | 39 + .../authentik/forward-auth-blueprint.yaml | 81 + .../authentik/gitlab-blueprint.yaml | 53 + .../authentik/gitlab-saml-blueprint.yaml | 60 + .../authentik/grafana-blueprint.yaml | 99 + .../authentik/guacamole-blueprint.yaml | 83 + .../authentik/home-assistant-blueprint.yaml | 68 + .../authentik/manifests/envoy-route.yaml | 20 + .../authentik/manifests/ext-authz-proxy.yaml | 179 ++ .../manifests/istio-virtualservice.yaml | 63 + .../authentik/manifests/oauth-sync-job.yaml | 36 + .../authentik/manifests/presync-job.yaml | 48 + infrastructure/authentik/n8n-blueprint.yaml | 49 + .../authentik/netbird-blueprint.yaml | 93 + .../authentik/nextcloud-blueprint.yaml | 52 + .../authentik/percona-everest-blueprint.yaml | 77 + .../authentik/prometheus-blueprint.yaml | 23 + .../authentik/rancher-blueprint.yaml | 54 + infrastructure/authentik/values.yaml | 177 ++ .../manifests/letsencrypt-issuer.yaml | 43 + .../manifests/secret-cf-token.yaml | 9 + infrastructure/cert-manager/values.yaml | 30 + .../checkov/manifests/grafana-dashboard.yaml | 96 + .../checkov/manifests/prometheus-rules.yaml | 28 + .../checkov/manifests/scanner-cronjob.yaml | 245 +++ .../manifests/l2-announcement-policy.yaml | 23 + .../manifests/network-policies/baseline.yaml | 156 ++ .../manifests/network-policies/dns.yaml | 23 + .../network-policies/health-probes.yaml | 17 + infrastructure/cilium/values.yaml | 120 ++ infrastructure/crowdsec/cnpg-cluster.yaml | 38 + .../crowdsec/manifests/external-secret.yaml | 41 + .../crowdsec/manifests/firewall-bouncer.yaml | 164 ++ .../crowdsec/manifests/grafana-dashboard.yaml | 205 ++ .../crowdsec/manifests/postsync-job.yaml | 53 + .../crowdsec/manifests/presync-job.yaml | 40 + infrastructure/crowdsec/values.yaml | 132 ++ .../envoy-gateway/manifests/edge.yaml | 313 +++ .../envoy-gateway/manifests/gatewayclass.yaml | 6 + infrastructure/envoy-gateway/values.yaml | 21 + .../manifests/secret-external-dns-unifi.yaml | 8 + infrastructure/external-dns/values.yaml | 69 + infrastructure/external-secrets/values.yaml | 1 + .../dashboards/dashboards-configmap.yaml | 34 + .../dashboards/teslamate-dashboards.yaml | 1761 +++++++++++++++++ .../grafana/manifests/envoy-route.yaml | 20 + .../manifests/grafana-oauth-secret.yaml | 12 + .../manifests/istio-virtualservice.yaml | 16 + .../teslamate-datasource-secret.yaml | 11 + .../teslamate-password-sync-cronjob.yaml | 195 ++ infrastructure/grafana/values.yaml | 141 ++ infrastructure/istio/cni-values.yaml | 6 + .../istio/ingressgateway-values.yaml | 19 + infrastructure/istio/istiod-values.yaml | 54 + .../apps/authorization-policies-apps.yaml | 322 +++ .../apps/namespace-enrollment-job.yaml | 55 + infrastructure/istio/manifests/edge/edge.yaml | 78 + .../istio/manifests/mesh/authentik-allow.yaml | 67 + .../mesh/authorization-policies.yaml | 363 ++++ .../manifests/mesh/ext-authz-policies.yaml | 139 ++ .../istio/manifests/mesh/monitoring.yaml | 68 + .../mesh/namespace-enrollment-job.yaml | 49 + .../mesh/peer-authentication-webhooks.yaml | 44 + .../manifests/mesh/peer-authentication.yaml | 12 + .../manifests/mesh/waypoint-gateways.yaml | 279 +++ .../kiali/manifests/istio-virtualservice.yaml | 24 + infrastructure/kiali/manifests/kiali-cr.yaml | 28 + infrastructure/kiali/operator-values.yaml | 7 + .../kyverno/policies/authentik-exception.yaml | 24 + .../policies/disallow-automount-sa-token.yaml | 68 + .../policies/disallow-default-namespace.yaml | 33 + .../policies/disallow-host-namespaces.yaml | 40 + .../kyverno/policies/disallow-latest-tag.yaml | 34 + .../kyverno/policies/disallow-nodeport.yaml | 29 + .../kyverno/policies/disallow-privileged.yaml | 43 + .../policies/istio-waypoint-exception.yaml | 47 + .../kyverno/policies/rancher-exception.yaml | 70 + .../require-drop-all-capabilities.yaml | 72 + .../policies/require-istio-ambient.yaml | 81 + .../kyverno/policies/require-labels.yaml | 43 + .../kyverno/policies/require-non-root.yaml | 37 + .../kyverno/policies/require-probes.yaml | 56 + .../policies/require-resource-limits.yaml | 63 + .../kyverno/policies/require-ro-rootfs.yaml | 39 + .../policies/restrict-image-registries.yaml | 107 + infrastructure/kyverno/values.yaml | 128 ++ infrastructure/metrics-server/values.yaml | 6 + .../mosquitto/manifests/deployment.yaml | 137 ++ .../manifests/istio-virtualservice.yaml | 16 + infrastructure/netbird-operator/values.yaml | 46 + infrastructure/netbird/cnpg-cluster.yaml | 38 + .../netbird/manifests/api-exporter.yaml | 104 + .../manifests/api-token-external-secret.yaml | 21 + .../netbird/manifests/cluster-router.yaml | 67 + .../netbird/manifests/external-secret.yaml | 31 + .../manifests/istio-virtualservice.yaml | 59 + .../manifests/management-grpc-service.yaml | 18 + .../manifests/oidc-trusted-domains.yaml | 25 + .../netbird/manifests/presync-job.yaml | 51 + .../netbird/manifests/servicemonitor.yaml | 54 + infrastructure/netbird/values.yaml | 272 +++ .../manifests/istio-virtualservice.yaml | 18 + infrastructure/percona-everest/values.yaml | 95 + .../prometheus/manifests/envoy-route.yaml | 20 + .../manifests/istio-virtualservice.yaml | 16 + .../manifests/talos-client-cert-secret.yaml | 10 + .../manifests/talos-metrics-proxy.yaml | 232 +++ infrastructure/prometheus/values.yaml | 87 + infrastructure/reloader/values.yaml | 4 + infrastructure/rook-ceph/cluster-values.yaml | 219 ++ .../manifests/cephfs-filesystem.yaml | 17 + .../manifests/cephfs-storageclass.yaml | 17 + .../rook-ceph/manifests/envoy-backend.yaml | 12 + .../rook-ceph/manifests/envoy-route.yaml | 21 + .../manifests/istio-destinationrule.yaml | 12 + .../manifests/istio-virtualservice.yaml | 17 + .../manifests/nfs-external-service.yaml | 17 + infrastructure/rook-ceph/manifests/nfs.yaml | 13 + .../rook-ceph/manifests/servicemonitor.yaml | 21 + .../rook-ceph/manifests/smb-gateway.yaml | 83 + infrastructure/rook-ceph/operator-values.yaml | 132 ++ infrastructure/scylla/cluster-values.yaml | 39 + infrastructure/scylla/manager-postsync-fix.sh | 11 + infrastructure/scylla/manager-values.yaml | 42 + .../manifests/istio-virtualservice.yaml | 16 + infrastructure/scylla/operator-values.yaml | 18 + infrastructure/scylla/values.yaml | 51 + .../tetragon/grafana-dashboard.yaml | 84 + .../kubernetes-api-access.yaml | 20 + .../tracing-policies/network-monitoring.yaml | 22 + .../privilege-escalation.yaml | 32 + .../tracing-policies/process-execution.yaml | 31 + .../sensitive-file-access.yaml | 26 + infrastructure/tetragon/values.yaml | 33 + .../unpoller/manifests/deployment.yaml | 130 ++ .../unpoller/manifests/unpoller-secret.yaml | 45 + infrastructure/unpoller/values.yaml | 72 + .../vault/manifests/clustersecretstore.yaml | 19 + .../vault/manifests/envoy-route.yaml | 20 + .../vault/manifests/eso-bootstrap-job.yaml | 48 + .../vault/manifests/eso-policy.yaml | 13 + .../vault/manifests/istio-virtualservice.yaml | 16 + .../vault/manifests/vault-init-configmap.yaml | 140 ++ .../vault/manifests/vault-init-job.yaml | 27 + infrastructure/vault/values.yaml | 45 + scripts/bootstrap-crds.sh | 33 + scripts/cilium-bootstrap.sh | 72 + scripts/fix-gitlab-auth.sh | 100 + scripts/gitlab-add-hostaliases.sh | 36 + scripts/gitlab-bootstrap.sh | 354 ++++ scripts/gitlab-grant-admin.sh | 67 + scripts/grafana-bootstrap.sh | 40 + scripts/health-check.sh | 46 + scripts/home-assistant-bootstrap.sh | 107 + scripts/import-grafana-dashboards.sh | 69 + scripts/import-unifi-dashboards.sh | 104 + scripts/metallb-prepare.sh | 16 + scripts/nessus-bootstrap.sh | 121 ++ scripts/network-benchmark.yaml | 203 ++ scripts/rook-ceph-prepare.sh | 231 +++ scripts/run-network-benchmark.sh | 184 ++ scripts/setup-ceph-saml.sh | 126 ++ scripts/setup-netbird-api-token.sh | 82 + scripts/setup-unpoller-credentials.sh | 181 ++ scripts/sync-argocd-oauth.sh | 118 ++ scripts/sync-gitlab-admin.sh | 73 + scripts/sync-gitlab-oauth.sh | 118 ++ scripts/sync-grafana-oauth.sh | 118 ++ scripts/sync-guacamole-oauth.sh | 113 ++ scripts/sync-homeassistant-oauth.sh | 118 ++ scripts/sync-n8n-oauth.sh | 113 ++ scripts/sync-netbird-oauth.sh | 147 ++ scripts/sync-nextcloud-oauth.sh | 190 ++ scripts/sync-percona-everest-oauth.sh | 104 + scripts/talos-iscsi-bootstrap.sh | 90 + scripts/teslamate-bootstrap.sh | 131 ++ scripts/v1-vs-v2-benchmark.yaml | 233 +++ scripts/validate-n8n-deployment.sh | 199 ++ scripts/vault-init-and-store.sh | 115 ++ scripts/verify-gitlab-oidc.sh | 45 + scripts/verify-redis-automation.sh | 177 ++ 314 files changed, 21766 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 apps/argocd/manifests/argocd-health-checks.yaml create mode 100644 apps/argocd/manifests/envoy-route.yaml create mode 100644 apps/argocd/manifests/external-secret.yaml create mode 100644 apps/argocd/manifests/istio-virtualservice.yaml create mode 100644 apps/argocd/values.yaml create mode 100644 apps/frigate/manifests/envoy-route.yaml create mode 100644 apps/frigate/manifests/istio-virtualservice.yaml create mode 100644 apps/frigate/manifests/pvc.yaml create mode 100644 apps/frigate/manifests/secret.yaml create mode 100644 apps/frigate/values.yaml create mode 100644 apps/gitlab/manifests/cnpg-cluster.yaml create mode 100644 apps/gitlab/manifests/envoy-routes.yaml create mode 100644 apps/gitlab/manifests/external-secret-saml.yaml create mode 100644 apps/gitlab/manifests/gitlab-unified-certificate.yaml create mode 100644 apps/gitlab/manifests/istio-virtualservices.yaml create mode 100644 apps/gitlab/manifests/pg-auto-restart.yaml create mode 100644 apps/gitlab/manifests/pgbouncer-pooler.yaml create mode 100644 apps/gitlab/manifests/praefect-cnpg-cluster.yaml create mode 100644 apps/gitlab/manifests/redis-auto-restart.yaml create mode 100644 apps/gitlab/manifests/redis-cluster.yaml create mode 100644 apps/gitlab/manifests/redis-sentinel-ha.yaml create mode 100644 apps/gitlab/manifests/sync-admin-job.yaml create mode 100644 apps/gitlab/values.yaml create mode 100644 apps/guacamole/manifests/cnpg-cluster.yaml create mode 100644 apps/guacamole/manifests/db-init-schema-job.yaml create mode 100644 apps/guacamole/manifests/deployment-complete.yaml create mode 100644 apps/guacamole/manifests/envoy-gateway.yaml create mode 100644 apps/guacamole/manifests/envoy-route.yaml create mode 100644 apps/guacamole/manifests/external-secret.yaml create mode 100644 apps/guacamole/manifests/istio-virtualservice.yaml create mode 100644 apps/home-assistant/esphome/envoy-route.yaml create mode 100644 apps/home-assistant/esphome/istio-virtualservice.yaml create mode 100644 apps/home-assistant/esphome/values.yaml create mode 100644 apps/home-assistant/manifests/envoy-route.yaml create mode 100644 apps/home-assistant/manifests/external-secret.yaml create mode 100644 apps/home-assistant/manifests/istio-virtualservice.yaml create mode 100644 apps/home-assistant/manifests/oidc-config.yaml create mode 100644 apps/home-assistant/values.yaml create mode 100644 apps/n8n/manifests/cnpg-cluster.yaml create mode 100644 apps/n8n/manifests/cnpg-secrets.yaml create mode 100644 apps/n8n/manifests/envoy-route.yaml create mode 100644 apps/n8n/manifests/external-secret.yaml create mode 100644 apps/n8n/manifests/hooks-configmap.yaml create mode 100644 apps/n8n/manifests/istio-virtualservice.yaml create mode 100644 apps/n8n/values.yaml create mode 100644 apps/nessus/manifests/cnpg-cluster.yaml create mode 100644 apps/nessus/manifests/cnpg-secrets.yaml create mode 100644 apps/nessus/manifests/deployment.yaml create mode 100644 apps/nessus/manifests/external-secret.yaml create mode 100644 apps/nessus/manifests/istio-virtualservice.yaml create mode 100644 apps/nessus/manifests/pvc.yaml create mode 100644 apps/nextcloud/manifests/envoy-route.yaml create mode 100644 apps/nextcloud/manifests/external-secret.yaml create mode 100644 apps/nextcloud/manifests/istio-virtualservice.yaml create mode 100644 apps/nextcloud/manifests/mariadb-deployment.yaml create mode 100644 apps/nextcloud/manifests/mariadb-secrets.yaml create mode 100644 apps/nextcloud/manifests/pvc.yaml create mode 100644 apps/nextcloud/values.yaml create mode 100644 apps/rancher/manifests/external-secret.yaml create mode 100644 apps/rancher/manifests/istio-virtualservice.yaml create mode 100644 apps/rancher/values.yaml create mode 100644 apps/teslamate/manifests/cnpg-cluster.yaml create mode 100644 apps/teslamate/manifests/cnpg-secrets.yaml create mode 100644 apps/teslamate/manifests/envoy-route.yaml create mode 100644 apps/teslamate/manifests/external-secret.yaml create mode 100644 apps/teslamate/manifests/istio-virtualservice.yaml create mode 100644 apps/teslamate/values.yaml create mode 100644 argocd-apps/apps/argocd.yaml create mode 100644 argocd-apps/apps/esphome.yaml create mode 100644 argocd-apps/apps/frigate.yaml create mode 100644 argocd-apps/apps/gitlab.yaml create mode 100644 argocd-apps/apps/guacamole.yaml create mode 100644 argocd-apps/apps/home-assistant.yaml create mode 100644 argocd-apps/apps/istio-app-routes.yaml create mode 100644 argocd-apps/apps/n8n.yaml create mode 100644 argocd-apps/apps/nessus.yaml create mode 100644 argocd-apps/apps/nextcloud.yaml create mode 100644 argocd-apps/apps/rancher.yaml create mode 100644 argocd-apps/apps/teslamate.yaml create mode 100644 argocd-apps/hook-rbac.yaml create mode 100644 argocd-apps/infrastructure/authentik.yaml create mode 100644 argocd-apps/infrastructure/cert-manager.yaml create mode 100644 argocd-apps/infrastructure/checkov-scanner.yaml create mode 100644 argocd-apps/infrastructure/cilium.yaml create mode 100644 argocd-apps/infrastructure/cnpg.yaml create mode 100644 argocd-apps/infrastructure/crowdsec.yaml create mode 100644 argocd-apps/infrastructure/envoy-edge.yaml create mode 100644 argocd-apps/infrastructure/envoy-gateway.yaml create mode 100644 argocd-apps/infrastructure/external-dns.yaml create mode 100644 argocd-apps/infrastructure/external-secrets.yaml create mode 100644 argocd-apps/infrastructure/grafana.yaml create mode 100644 argocd-apps/infrastructure/istio-base.yaml create mode 100644 argocd-apps/infrastructure/istio-cni.yaml create mode 100644 argocd-apps/infrastructure/istio-edge.yaml create mode 100644 argocd-apps/infrastructure/istio-ingressgateway.yaml create mode 100644 argocd-apps/infrastructure/istio-mesh-config.yaml create mode 100644 argocd-apps/infrastructure/istiod.yaml create mode 100644 argocd-apps/infrastructure/kiali-operator.yaml create mode 100644 argocd-apps/infrastructure/kiali.yaml create mode 100644 argocd-apps/infrastructure/kube-prometheus-stack.yaml create mode 100644 argocd-apps/infrastructure/kyverno.yaml create mode 100644 argocd-apps/infrastructure/mariadb-operator.yaml create mode 100644 argocd-apps/infrastructure/metrics-server.yaml create mode 100644 argocd-apps/infrastructure/mosquitto.yaml create mode 100644 argocd-apps/infrastructure/netbird-operator.yaml create mode 100644 argocd-apps/infrastructure/netbird.yaml create mode 100644 argocd-apps/infrastructure/prometheus-crds.yaml create mode 100644 argocd-apps/infrastructure/redis-operator.yaml create mode 100644 argocd-apps/infrastructure/reloader.yaml create mode 100644 argocd-apps/infrastructure/rook-ceph-cluster.yaml create mode 100644 argocd-apps/infrastructure/rook-ceph-operator.yaml create mode 100644 argocd-apps/infrastructure/scylla-manager.yaml create mode 100644 argocd-apps/infrastructure/scylla-operator.yaml create mode 100644 argocd-apps/infrastructure/tetragon.yaml create mode 100644 argocd-apps/infrastructure/unpoller.yaml create mode 100644 argocd-apps/infrastructure/vault.yaml create mode 100644 argocd-apps/infrastructure/ztunnel.yaml create mode 100644 argocd-apps/projects.yaml create mode 100644 bootstrap.yaml create mode 100644 charts/empty-chart/Chart.yaml create mode 100644 charts/empty-chart/templates/.gitkeep create mode 100644 charts/prometheus-operator-crds/Chart.yaml create mode 100644 charts/prometheus-operator-crds/templates/job.yaml create mode 100644 environments/default.yaml create mode 100644 hooks/rbac/hook-sa.yaml create mode 100644 hooks/scripts/vault-init-configmap.yaml create mode 100644 infrastructure/authentik/argocd-blueprint.yaml create mode 100644 infrastructure/authentik/ceph-blueprint.yaml create mode 100644 infrastructure/authentik/ceph-oidc-blueprint.yaml create mode 100644 infrastructure/authentik/cnpg-cluster.yaml create mode 100644 infrastructure/authentik/cnpg-podmonitor.yaml create mode 100644 infrastructure/authentik/cnpg-secrets.yaml create mode 100644 infrastructure/authentik/forward-auth-blueprint.yaml create mode 100644 infrastructure/authentik/gitlab-blueprint.yaml create mode 100644 infrastructure/authentik/gitlab-saml-blueprint.yaml create mode 100644 infrastructure/authentik/grafana-blueprint.yaml create mode 100644 infrastructure/authentik/guacamole-blueprint.yaml create mode 100644 infrastructure/authentik/home-assistant-blueprint.yaml create mode 100644 infrastructure/authentik/manifests/envoy-route.yaml create mode 100644 infrastructure/authentik/manifests/ext-authz-proxy.yaml create mode 100644 infrastructure/authentik/manifests/istio-virtualservice.yaml create mode 100644 infrastructure/authentik/manifests/oauth-sync-job.yaml create mode 100644 infrastructure/authentik/manifests/presync-job.yaml create mode 100644 infrastructure/authentik/n8n-blueprint.yaml create mode 100644 infrastructure/authentik/netbird-blueprint.yaml create mode 100644 infrastructure/authentik/nextcloud-blueprint.yaml create mode 100644 infrastructure/authentik/percona-everest-blueprint.yaml create mode 100644 infrastructure/authentik/prometheus-blueprint.yaml create mode 100644 infrastructure/authentik/rancher-blueprint.yaml create mode 100644 infrastructure/authentik/values.yaml create mode 100644 infrastructure/cert-manager/manifests/letsencrypt-issuer.yaml create mode 100644 infrastructure/cert-manager/manifests/secret-cf-token.yaml create mode 100644 infrastructure/cert-manager/values.yaml create mode 100644 infrastructure/checkov/manifests/grafana-dashboard.yaml create mode 100644 infrastructure/checkov/manifests/prometheus-rules.yaml create mode 100644 infrastructure/checkov/manifests/scanner-cronjob.yaml create mode 100644 infrastructure/cilium/manifests/l2-announcement-policy.yaml create mode 100644 infrastructure/cilium/manifests/network-policies/baseline.yaml create mode 100644 infrastructure/cilium/manifests/network-policies/dns.yaml create mode 100644 infrastructure/cilium/manifests/network-policies/health-probes.yaml create mode 100644 infrastructure/cilium/values.yaml create mode 100644 infrastructure/crowdsec/cnpg-cluster.yaml create mode 100644 infrastructure/crowdsec/manifests/external-secret.yaml create mode 100644 infrastructure/crowdsec/manifests/firewall-bouncer.yaml create mode 100644 infrastructure/crowdsec/manifests/grafana-dashboard.yaml create mode 100644 infrastructure/crowdsec/manifests/postsync-job.yaml create mode 100644 infrastructure/crowdsec/manifests/presync-job.yaml create mode 100644 infrastructure/crowdsec/values.yaml create mode 100644 infrastructure/envoy-gateway/manifests/edge.yaml create mode 100644 infrastructure/envoy-gateway/manifests/gatewayclass.yaml create mode 100644 infrastructure/envoy-gateway/values.yaml create mode 100644 infrastructure/external-dns/manifests/secret-external-dns-unifi.yaml create mode 100644 infrastructure/external-dns/values.yaml create mode 100644 infrastructure/external-secrets/values.yaml create mode 100644 infrastructure/grafana/dashboards/dashboards-configmap.yaml create mode 100644 infrastructure/grafana/dashboards/teslamate-dashboards.yaml create mode 100644 infrastructure/grafana/manifests/envoy-route.yaml create mode 100644 infrastructure/grafana/manifests/grafana-oauth-secret.yaml create mode 100644 infrastructure/grafana/manifests/istio-virtualservice.yaml create mode 100644 infrastructure/grafana/manifests/teslamate-datasource-secret.yaml create mode 100644 infrastructure/grafana/manifests/teslamate-password-sync-cronjob.yaml create mode 100644 infrastructure/grafana/values.yaml create mode 100644 infrastructure/istio/cni-values.yaml create mode 100644 infrastructure/istio/ingressgateway-values.yaml create mode 100644 infrastructure/istio/istiod-values.yaml create mode 100644 infrastructure/istio/manifests/apps/authorization-policies-apps.yaml create mode 100644 infrastructure/istio/manifests/apps/namespace-enrollment-job.yaml create mode 100644 infrastructure/istio/manifests/edge/edge.yaml create mode 100644 infrastructure/istio/manifests/mesh/authentik-allow.yaml create mode 100644 infrastructure/istio/manifests/mesh/authorization-policies.yaml create mode 100644 infrastructure/istio/manifests/mesh/ext-authz-policies.yaml create mode 100644 infrastructure/istio/manifests/mesh/monitoring.yaml create mode 100644 infrastructure/istio/manifests/mesh/namespace-enrollment-job.yaml create mode 100644 infrastructure/istio/manifests/mesh/peer-authentication-webhooks.yaml create mode 100644 infrastructure/istio/manifests/mesh/peer-authentication.yaml create mode 100644 infrastructure/istio/manifests/mesh/waypoint-gateways.yaml create mode 100644 infrastructure/kiali/manifests/istio-virtualservice.yaml create mode 100644 infrastructure/kiali/manifests/kiali-cr.yaml create mode 100644 infrastructure/kiali/operator-values.yaml create mode 100644 infrastructure/kyverno/policies/authentik-exception.yaml create mode 100644 infrastructure/kyverno/policies/disallow-automount-sa-token.yaml create mode 100644 infrastructure/kyverno/policies/disallow-default-namespace.yaml create mode 100644 infrastructure/kyverno/policies/disallow-host-namespaces.yaml create mode 100644 infrastructure/kyverno/policies/disallow-latest-tag.yaml create mode 100644 infrastructure/kyverno/policies/disallow-nodeport.yaml create mode 100644 infrastructure/kyverno/policies/disallow-privileged.yaml create mode 100644 infrastructure/kyverno/policies/istio-waypoint-exception.yaml create mode 100644 infrastructure/kyverno/policies/rancher-exception.yaml create mode 100644 infrastructure/kyverno/policies/require-drop-all-capabilities.yaml create mode 100644 infrastructure/kyverno/policies/require-istio-ambient.yaml create mode 100644 infrastructure/kyverno/policies/require-labels.yaml create mode 100644 infrastructure/kyverno/policies/require-non-root.yaml create mode 100644 infrastructure/kyverno/policies/require-probes.yaml create mode 100644 infrastructure/kyverno/policies/require-resource-limits.yaml create mode 100644 infrastructure/kyverno/policies/require-ro-rootfs.yaml create mode 100644 infrastructure/kyverno/policies/restrict-image-registries.yaml create mode 100644 infrastructure/kyverno/values.yaml create mode 100644 infrastructure/metrics-server/values.yaml create mode 100644 infrastructure/mosquitto/manifests/deployment.yaml create mode 100644 infrastructure/mosquitto/manifests/istio-virtualservice.yaml create mode 100644 infrastructure/netbird-operator/values.yaml create mode 100644 infrastructure/netbird/cnpg-cluster.yaml create mode 100644 infrastructure/netbird/manifests/api-exporter.yaml create mode 100644 infrastructure/netbird/manifests/api-token-external-secret.yaml create mode 100644 infrastructure/netbird/manifests/cluster-router.yaml create mode 100644 infrastructure/netbird/manifests/external-secret.yaml create mode 100644 infrastructure/netbird/manifests/istio-virtualservice.yaml create mode 100644 infrastructure/netbird/manifests/management-grpc-service.yaml create mode 100644 infrastructure/netbird/manifests/oidc-trusted-domains.yaml create mode 100644 infrastructure/netbird/manifests/presync-job.yaml create mode 100644 infrastructure/netbird/manifests/servicemonitor.yaml create mode 100644 infrastructure/netbird/values.yaml create mode 100644 infrastructure/percona-everest/manifests/istio-virtualservice.yaml create mode 100644 infrastructure/percona-everest/values.yaml create mode 100644 infrastructure/prometheus/manifests/envoy-route.yaml create mode 100644 infrastructure/prometheus/manifests/istio-virtualservice.yaml create mode 100644 infrastructure/prometheus/manifests/talos-client-cert-secret.yaml create mode 100644 infrastructure/prometheus/manifests/talos-metrics-proxy.yaml create mode 100644 infrastructure/prometheus/values.yaml create mode 100644 infrastructure/reloader/values.yaml create mode 100644 infrastructure/rook-ceph/cluster-values.yaml create mode 100644 infrastructure/rook-ceph/manifests/cephfs-filesystem.yaml create mode 100644 infrastructure/rook-ceph/manifests/cephfs-storageclass.yaml create mode 100644 infrastructure/rook-ceph/manifests/envoy-backend.yaml create mode 100644 infrastructure/rook-ceph/manifests/envoy-route.yaml create mode 100644 infrastructure/rook-ceph/manifests/istio-destinationrule.yaml create mode 100644 infrastructure/rook-ceph/manifests/istio-virtualservice.yaml create mode 100644 infrastructure/rook-ceph/manifests/nfs-external-service.yaml create mode 100644 infrastructure/rook-ceph/manifests/nfs.yaml create mode 100644 infrastructure/rook-ceph/manifests/servicemonitor.yaml create mode 100644 infrastructure/rook-ceph/manifests/smb-gateway.yaml create mode 100644 infrastructure/rook-ceph/operator-values.yaml create mode 100644 infrastructure/scylla/cluster-values.yaml create mode 100755 infrastructure/scylla/manager-postsync-fix.sh create mode 100644 infrastructure/scylla/manager-values.yaml create mode 100644 infrastructure/scylla/manifests/istio-virtualservice.yaml create mode 100644 infrastructure/scylla/operator-values.yaml create mode 100644 infrastructure/scylla/values.yaml create mode 100644 infrastructure/tetragon/grafana-dashboard.yaml create mode 100644 infrastructure/tetragon/tracing-policies/kubernetes-api-access.yaml create mode 100644 infrastructure/tetragon/tracing-policies/network-monitoring.yaml create mode 100644 infrastructure/tetragon/tracing-policies/privilege-escalation.yaml create mode 100644 infrastructure/tetragon/tracing-policies/process-execution.yaml create mode 100644 infrastructure/tetragon/tracing-policies/sensitive-file-access.yaml create mode 100644 infrastructure/tetragon/values.yaml create mode 100644 infrastructure/unpoller/manifests/deployment.yaml create mode 100644 infrastructure/unpoller/manifests/unpoller-secret.yaml create mode 100644 infrastructure/unpoller/values.yaml create mode 100644 infrastructure/vault/manifests/clustersecretstore.yaml create mode 100644 infrastructure/vault/manifests/envoy-route.yaml create mode 100644 infrastructure/vault/manifests/eso-bootstrap-job.yaml create mode 100644 infrastructure/vault/manifests/eso-policy.yaml create mode 100644 infrastructure/vault/manifests/istio-virtualservice.yaml create mode 100644 infrastructure/vault/manifests/vault-init-configmap.yaml create mode 100644 infrastructure/vault/manifests/vault-init-job.yaml create mode 100644 infrastructure/vault/values.yaml create mode 100755 scripts/bootstrap-crds.sh create mode 100755 scripts/cilium-bootstrap.sh create mode 100755 scripts/fix-gitlab-auth.sh create mode 100755 scripts/gitlab-add-hostaliases.sh create mode 100755 scripts/gitlab-bootstrap.sh create mode 100755 scripts/gitlab-grant-admin.sh create mode 100755 scripts/grafana-bootstrap.sh create mode 100755 scripts/health-check.sh create mode 100755 scripts/home-assistant-bootstrap.sh create mode 100755 scripts/import-grafana-dashboards.sh create mode 100755 scripts/import-unifi-dashboards.sh create mode 100755 scripts/metallb-prepare.sh create mode 100755 scripts/nessus-bootstrap.sh create mode 100644 scripts/network-benchmark.yaml create mode 100755 scripts/rook-ceph-prepare.sh create mode 100755 scripts/run-network-benchmark.sh create mode 100755 scripts/setup-ceph-saml.sh create mode 100755 scripts/setup-netbird-api-token.sh create mode 100755 scripts/setup-unpoller-credentials.sh create mode 100755 scripts/sync-argocd-oauth.sh create mode 100644 scripts/sync-gitlab-admin.sh create mode 100755 scripts/sync-gitlab-oauth.sh create mode 100755 scripts/sync-grafana-oauth.sh create mode 100755 scripts/sync-guacamole-oauth.sh create mode 100755 scripts/sync-homeassistant-oauth.sh create mode 100755 scripts/sync-n8n-oauth.sh create mode 100755 scripts/sync-netbird-oauth.sh create mode 100755 scripts/sync-nextcloud-oauth.sh create mode 100755 scripts/sync-percona-everest-oauth.sh create mode 100755 scripts/talos-iscsi-bootstrap.sh create mode 100755 scripts/teslamate-bootstrap.sh create mode 100644 scripts/v1-vs-v2-benchmark.yaml create mode 100755 scripts/validate-n8n-deployment.sh create mode 100755 scripts/vault-init-and-store.sh create mode 100755 scripts/verify-gitlab-oidc.sh create mode 100755 scripts/verify-redis-automation.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a821e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Environment-specific overrides +environments/local.yaml + +# Temporary files +*.swp +*.swo +*~ +.DS_Store + +# Helm +charts/*/charts/ +charts/*/tmpcharts/ + +# IDE +.idea/ +.vscode/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3190d69 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ +# CLAUDE.md + +This file provides guidance to Claude Code when working with this repository. + +## Overview + +Homelabv4 is a fully automated Kubernetes homelab deployment using **ArgoCD App-of-Apps pattern** for GitOps-based continuous delivery. It replaces the Helmfile-based Homelabv5 with declarative ArgoCD Application CRDs, sync waves for ordering, and Kubernetes Jobs for complex hooks. + +## Deployment + +### Bootstrap (one-time) +```bash +# Prerequisites: ArgoCD must already be running on the cluster +# Replace in all Application CRDs with your actual repo URL +# Then: +kubectl apply -f bootstrap.yaml +``` + +### How It Works +1. `bootstrap.yaml` creates a root Application pointing to `argocd-apps/` +2. ArgoCD discovers all Application CRDs in `argocd-apps/infrastructure/` and `argocd-apps/apps/` +3. Sync waves control deployment order (lower waves first) +4. ArgoCD waits for resources to be healthy before advancing waves +5. PreSync/PostSync hook Jobs handle complex initialization + +### Verify Deployment +```bash +kubectl get applications -n argocd +kubectl get pods -A | grep -v Running | grep -v Completed +``` + +## Architecture + +### Sync Wave Scheme +| Wave | Phase | Components | +|------|-------|-----------| +| -1 | Bootstrap | Prometheus CRDs | +| 1-7 | Core Net | Cilium, Istio, cert-manager, metrics-server, envoy-gateway | +| 10-14 | Storage | Rook-Ceph, CNPG, Redis, MariaDB, Scylla operators | +| 20-23 | Security | Vault, ESO, Reloader, Authentik, Kyverno, Tetragon, CrowdSec | +| 30-31 | Net Extra | External-DNS, Netbird, Mosquitto | +| 40-43 | Monitoring | Prometheus, Grafana, Kiali, Checkov | +| 50-52 | Apps | GitLab, ArgoCD, n8n, Nextcloud, TeslaMate, HA, Frigate, etc. | + +### Directory Structure +``` +Homelabv4/ +├── bootstrap.yaml # Root App-of-Apps +├── argocd-apps/ # Application CRDs +│ ├── infrastructure/ # ~35 infra Applications +│ └── apps/ # ~10 user Applications +├── infrastructure/ # Source files for infra +│ └── / +│ ├── values.yaml # Helm values +│ └── manifests/ # Raw K8s manifests +├── apps/ # Source files for apps +│ └── / +│ ├── values.yaml +│ └── manifests/ +├── charts/ # Local Helm charts +├── hooks/ # Shared hook RBAC +├── environments/ # Environment config +└── scripts/ # Manual scripts +``` + +### Key Patterns + +#### Multi-Source Applications +Most Applications use 3 sources: +1. Remote Helm chart repo +2. Git repo ref for values files +3. Git repo path for raw manifests + +#### Hook Jobs +Complex initialization (Vault init, OAuth sync) uses K8s Jobs with ArgoCD hook annotations: +- `argocd.argoproj.io/hook: PreSync` or `PostSync` +- `argocd.argoproj.io/hook-delete-policy: BeforeHookCreation` +- Jobs use `argocd-hook-sa` ServiceAccount with cluster-admin + +#### Secrets Flow +Vault → External Secrets Operator → Kubernetes Secrets → Apps + +## Common Operations + +### Add a New Application +1. Create `apps//values.yaml` with Helm values +2. Create `apps//manifests/` with any raw manifests +3. Create `argocd-apps/apps/.yaml` Application CRD +4. Commit and push — ArgoCD auto-syncs + +### Update Helm Values +1. Edit `infrastructure//values.yaml` or `apps//values.yaml` +2. Commit and push — ArgoCD auto-syncs + +### Force Sync +```bash +argocd app sync +``` + +## Important Notes +- **Git repo URL**: Replace `` placeholder in all Application CRDs +- **Secrets**: Never commit secrets — use Vault + ExternalSecret +- **Domain**: All services use `*.kube.huskypup.net` +- **Storage**: Default StorageClass is `rook-ceph-block` +- **Ingress**: Istio VirtualServices + Envoy Gateway HTTPRoutes +- **mTLS**: Istio ambient mesh with STRICT PeerAuthentication diff --git a/apps/argocd/manifests/argocd-health-checks.yaml b/apps/argocd/manifests/argocd-health-checks.yaml new file mode 100644 index 0000000..190ad03 --- /dev/null +++ b/apps/argocd/manifests/argocd-health-checks.yaml @@ -0,0 +1,84 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-cm-custom-health + namespace: argocd + labels: + app.kubernetes.io/part-of: argocd + annotations: + argocd.argoproj.io/sync-wave: "-1" +data: + resource.customizations.health.ceph.rook.io_CephCluster: | + hs = {} + if obj.status ~= nil then + if obj.status.phase == "Ready" and obj.status.ceph ~= nil and obj.status.ceph.health == "HEALTH_OK" then + hs.status = "Healthy" + hs.message = "CephCluster is healthy" + elseif obj.status.phase == "Progressing" then + hs.status = "Progressing" + hs.message = obj.status.message or "CephCluster is progressing" + else + hs.status = "Degraded" + hs.message = obj.status.message or "CephCluster is not healthy" + end + else + hs.status = "Progressing" + hs.message = "Waiting for CephCluster status" + end + return hs + resource.customizations.health.postgresql.cnpg.io_Cluster: | + hs = {} + if obj.status ~= nil then + if obj.status.phase == "Cluster in healthy state" then + hs.status = "Healthy" + hs.message = "CNPG Cluster is healthy" + elseif obj.status.phase == "Setting up primary" or obj.status.phase == "Creating primary" then + hs.status = "Progressing" + hs.message = obj.status.phase + else + hs.status = "Degraded" + hs.message = obj.status.phase or "CNPG Cluster is not healthy" + end + else + hs.status = "Progressing" + hs.message = "Waiting for CNPG Cluster status" + end + return hs + resource.customizations.health.external-secrets.io_ClusterSecretStore: | + hs = {} + if obj.status ~= nil and obj.status.conditions ~= nil then + for i, condition in ipairs(obj.status.conditions) do + if condition.type == "Ready" then + if condition.status == "True" then + hs.status = "Healthy" + hs.message = "ClusterSecretStore is ready" + else + hs.status = "Degraded" + hs.message = condition.message or "ClusterSecretStore is not ready" + end + return hs + end + end + end + hs.status = "Progressing" + hs.message = "Waiting for ClusterSecretStore status" + return hs + resource.customizations.health.kiali.io_Kiali: | + hs = {} + if obj.status ~= nil then + if obj.status.conditions ~= nil then + for i, condition in ipairs(obj.status.conditions) do + if condition.type == "Successful" and condition.status == "True" then + hs.status = "Healthy" + hs.message = "Kiali is healthy" + return hs + end + end + end + hs.status = "Progressing" + hs.message = "Kiali is being deployed" + else + hs.status = "Progressing" + hs.message = "Waiting for Kiali status" + end + return hs diff --git a/apps/argocd/manifests/envoy-route.yaml b/apps/argocd/manifests/envoy-route.yaml new file mode 100644 index 0000000..4ce309b --- /dev/null +++ b/apps/argocd/manifests/envoy-route.yaml @@ -0,0 +1,20 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: argocd + namespace: argocd +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - argocd.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: argocd-server + port: 80 diff --git a/apps/argocd/manifests/external-secret.yaml b/apps/argocd/manifests/external-secret.yaml new file mode 100644 index 0000000..509aa46 --- /dev/null +++ b/apps/argocd/manifests/external-secret.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: argocd-oauth + namespace: argocd +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: argocd-oauth-secret + creationPolicy: Owner + data: + - secretKey: client-id + remoteRef: + key: argocd-oauth + property: client-id + - secretKey: client-secret + remoteRef: + key: argocd-oauth + property: client-secret diff --git a/apps/argocd/manifests/istio-virtualservice.yaml b/apps/argocd/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..c6d6e8e --- /dev/null +++ b/apps/argocd/manifests/istio-virtualservice.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: argocd + namespace: argocd +spec: + hosts: + - argocd.kube.huskypup.net + gateways: + - istio-system/edge + http: + - route: + - destination: + host: argocd-server.argocd.svc.cluster.local + port: + number: 80 diff --git a/apps/argocd/values.yaml b/apps/argocd/values.yaml new file mode 100644 index 0000000..5ac8cdb --- /dev/null +++ b/apps/argocd/values.yaml @@ -0,0 +1,78 @@ +# values/argocd.values.yaml + +global: + domain: argocd.kube.huskypup.net + +configs: + cm: + url: https://argocd.kube.huskypup.net + oidc.config: | + name: Authentik + issuer: https://auth.kube.huskypup.net/application/o/argocd/ + clientID: $oidc.authentik.clientId + clientSecret: $oidc.authentik.clientSecret + requestedScopes: + - openid + - profile + - email + - groups + + rbac: + policy.default: role:readonly + policy.csv: | + g, Authentik Admins, role:admin + g, ArgoCD Admins, role:admin + +server: + extraArgs: + - --insecure # TLS is terminated at Istio gateway + + ingress: + enabled: false + + env: + - name: oidc.authentik.clientId + valueFrom: + secretKeyRef: + name: argocd-oauth-secret + key: client-id + - name: oidc.authentik.clientSecret + valueFrom: + secretKeyRef: + name: argocd-oauth-secret + key: client-secret + +dex: + enabled: false # using Authentik instead of built-in Dex + +# Enable Prometheus metrics +metrics: + enabled: true + serviceMonitor: + enabled: true + additionalLabels: + release: prometheus + +controller: + metrics: + enabled: true + serviceMonitor: + enabled: true + additionalLabels: + release: prometheus + +repoServer: + metrics: + enabled: true + serviceMonitor: + enabled: true + additionalLabels: + release: prometheus + +applicationSet: + metrics: + enabled: true + serviceMonitor: + enabled: true + additionalLabels: + release: prometheus diff --git a/apps/frigate/manifests/envoy-route.yaml b/apps/frigate/manifests/envoy-route.yaml new file mode 100644 index 0000000..6068b30 --- /dev/null +++ b/apps/frigate/manifests/envoy-route.yaml @@ -0,0 +1,20 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: frigate + namespace: frigate +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - frigate.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: frigate + port: 5000 diff --git a/apps/frigate/manifests/istio-virtualservice.yaml b/apps/frigate/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..06f3d24 --- /dev/null +++ b/apps/frigate/manifests/istio-virtualservice.yaml @@ -0,0 +1,27 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: frigate + namespace: frigate +spec: + hosts: + - frigate.kube.huskypup.net + gateways: + - istio-system/edge + http: + # Authentik forward-auth endpoints must be reachable on the protected host + - match: + - uri: + prefix: /outpost.goauthentik.io + route: + - destination: + host: authentik-server.authentik.svc.cluster.local + port: + number: 80 + + - timeout: 3600s + route: + - destination: + host: frigate.frigate.svc.cluster.local + port: + number: 5000 diff --git a/apps/frigate/manifests/pvc.yaml b/apps/frigate/manifests/pvc.yaml new file mode 100644 index 0000000..49a06d8 --- /dev/null +++ b/apps/frigate/manifests/pvc.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: frigate-config + namespace: frigate + labels: + app.kubernetes.io/name: frigate +spec: + accessModes: + - ReadWriteOnce + storageClassName: rook-ceph-block + resources: + requests: + storage: 500Mi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: frigate-media + namespace: frigate + labels: + app.kubernetes.io/name: frigate +spec: + accessModes: + - ReadWriteOnce + storageClassName: rook-ceph-block + resources: + requests: + storage: 500Gi diff --git a/apps/frigate/manifests/secret.yaml b/apps/frigate/manifests/secret.yaml new file mode 100644 index 0000000..6551bf7 --- /dev/null +++ b/apps/frigate/manifests/secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: frigate-rtsp-credentials + namespace: frigate +type: Opaque +stringData: + # Update these with your actual camera credentials + # Then update apps/frigate/values.yaml with your camera RTSP URLs + FRIGATE_RTSP_USERNAME: your_camera_username + FRIGATE_RTSP_PASSWORD: your_camera_password diff --git a/apps/frigate/values.yaml b/apps/frigate/values.yaml new file mode 100644 index 0000000..650893d --- /dev/null +++ b/apps/frigate/values.yaml @@ -0,0 +1,182 @@ +# Frigate Helm Chart Values +# NVR with realtime object detection for IP cameras +# +# FEATURES: +# ✓ Object detection for IP cameras +# ✓ Persistent storage for recordings via Rook-Ceph +# ✓ TLS certificates via cert-manager +# ✓ Ingress via Istio VirtualService +# ✓ MQTT integration (optional) + +# Reduce Istio sidecar CPU request - frigate already uses 1 CPU request +# and the cluster runs at ~98% CPU allocation. Default 10m sidecar request +# can prevent scheduling. +podAnnotations: + sidecar.istio.io/proxyCPU: "1m" + +image: + repository: ghcr.io/blakeblackshear/frigate + tag: "0.13.2" + pullPolicy: IfNotPresent + +service: + main: + type: ClusterIP + ports: + http: + port: 5000 + +# Ingress disabled - Istio VirtualService handles routing +ingress: + main: + enabled: false + +env: + TZ: America/New_York + FRIGATE_MQTT_HOST: "mosquitto.mqtt" + FRIGATE_MQTT_PORT: "1883" + +envFromSecrets: + - frigate-rtsp-credentials + +config: | + mqtt: + host: mosquitto.mqtt + port: 1883 + topic_prefix: frigate + client_id: frigate + detectors: + cpu1: + type: cpu + cameras: + basement: + ffmpeg: + inputs: + - path: rtsp://admin:admin!@172.28.200.10:554/cam/realmonitor?channel=1&subtype=1 + roles: + - detect + - path: rtsp://admin:admin!@172.28.200.10:554/cam/realmonitor?channel=1&subtype=0 + roles: + - record + detect: + width: 704 + height: 480 + fps: 5 + record: + enabled: true + retain: + days: 5 + mode: all + snapshots: + enabled: true + timestamp: true + bounding_box: true + patio: + ffmpeg: + inputs: + - path: rtsp://admin:admin!@172.28.200.11:554/cam/realmonitor?channel=1&subtype=1 + roles: + - detect + - path: rtsp://admin:admin!@172.28.200.11:554/cam/realmonitor?channel=1&subtype=0 + roles: + - record + detect: + width: 704 + height: 480 + fps: 5 + record: + enabled: true + retain: + days: 5 + mode: all + snapshots: + enabled: true + timestamp: true + bounding_box: true + backyard: + ffmpeg: + inputs: + - path: rtsp://admin:admin!@172.28.200.12:554/cam/realmonitor?channel=1&subtype=1 + roles: + - detect + - path: rtsp://admin:admin!@172.28.200.12:554/cam/realmonitor?channel=1&subtype=0 + roles: + - record + detect: + width: 704 + height: 480 + fps: 5 + record: + enabled: true + retain: + days: 5 + mode: all + snapshots: + enabled: true + timestamp: true + bounding_box: true + go2rtc: + streams: + basement: rtsp://admin:admin!@172.28.200.10:554/cam/realmonitor?channel=1&subtype=0 + basement_alt: rtsp://admin:admin!@172.28.200.10:554/cam/realmonitor?channel=1&subtype=1 + patio: rtsp://admin:admin!@172.28.200.11:554/cam/realmonitor?channel=1&subtype=0 + patio_alt: rtsp://admin:admin!@172.28.200.11:554/cam/realmonitor?channel=1&subtype=1 + backyard: rtsp://admin:admin!@172.28.200.12:554/cam/realmonitor?channel=1&subtype=0 + backyard_alt: rtsp://admin:admin!@172.28.200.12:554/cam/realmonitor?channel=1&subtype=1 + objects: + track: + - person + - car + - dog + - cat + - vehicle + motion: + threshold: 25 + +persistence: + config: + enabled: true + storageClass: rook-ceph-block + accessMode: ReadWriteOnce + size: 500Mi + media: + enabled: true + storageClass: rook-ceph-block + accessMode: ReadWriteOnce + size: 500Gi + +shmSize: 2Gi + +tmpfs: + enabled: true + sizeLimit: 2Gi + +securityContext: + privileged: false + capabilities: + drop: + - ALL + +resources: + requests: + cpu: 50m + memory: 1Gi + limits: + cpu: 500m + memory: 4Gi + +probes: + liveness: + enabled: true + initialDelaySeconds: 5 + failureThreshold: 5 + readiness: + enabled: true + initialDelaySeconds: 5 + failureThreshold: 5 + startup: + enabled: true + failureThreshold: 5 + +nodeSelector: + kubernetes.io/arch: amd64 diff --git a/apps/gitlab/manifests/cnpg-cluster.yaml b/apps/gitlab/manifests/cnpg-cluster.yaml new file mode 100644 index 0000000..f003dd2 --- /dev/null +++ b/apps/gitlab/manifests/cnpg-cluster.yaml @@ -0,0 +1,74 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: pg-gitlab + namespace: gitlab +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:16 + instances: 3 # 3 instances for production HA + + # Database resources (homelab-friendly requests, burstable CPU) + resources: + requests: + memory: "1Gi" + cpu: "50m" + limits: + memory: "4Gi" + cpu: "500m" + + # Spread replicas across different nodes + affinity: + topologyKey: kubernetes.io/hostname + + storage: + size: 50Gi # Increased for production + storageClass: rook-ceph-block + + primaryUpdateStrategy: unsupervised + + bootstrap: + initdb: + database: gitlabhq_production + owner: app + postInitSQL: + - CREATE EXTENSION IF NOT EXISTS pg_trgm; + - CREATE EXTENSION IF NOT EXISTS btree_gist; + + # PostgreSQL configuration tuning for GitLab + postgresql: + parameters: + max_connections: "400" + shared_buffers: "1GB" + effective_cache_size: "3GB" + maintenance_work_mem: "256MB" + checkpoint_completion_target: "0.9" + wal_buffers: "16MB" + default_statistics_target: "100" + random_page_cost: "1.1" + effective_io_concurrency: "200" + work_mem: "16MB" + min_wal_size: "1GB" + max_wal_size: "4GB" + max_worker_processes: "4" + max_parallel_workers_per_gather: "2" + max_parallel_workers: "4" + + # Backup configuration to MinIO + backup: + barmanObjectStore: + destinationPath: s3://gitlab-backups/pg-gitlab + endpointURL: http://gitlab-minio-svc.gitlab.svc.cluster.local:9000 + s3Credentials: + accessKeyId: + name: gitlab-minio-secret + key: accesskey + secretAccessKey: + name: gitlab-minio-secret + key: secretkey + wal: + compression: gzip + maxParallel: 2 + retentionPolicy: "30d" + + monitoring: + enablePodMonitor: true diff --git a/apps/gitlab/manifests/envoy-routes.yaml b/apps/gitlab/manifests/envoy-routes.yaml new file mode 100644 index 0000000..e5f05cf --- /dev/null +++ b/apps/gitlab/manifests/envoy-routes.yaml @@ -0,0 +1,86 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: gitlab-web + namespace: gitlab +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - gitlab.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: gitlab-webservice-default + port: 8181 + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: gitlab-registry + namespace: gitlab +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - registry.gitlab.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: gitlab-registry + port: 5000 + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: gitlab-minio + namespace: gitlab +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - minio.gitlab.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: gitlab-minio-svc + port: 9000 + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: gitlab-kas + namespace: gitlab +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - kas.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: gitlab-kas + port: 8154 diff --git a/apps/gitlab/manifests/external-secret-saml.yaml b/apps/gitlab/manifests/external-secret-saml.yaml new file mode 100644 index 0000000..93ee824 --- /dev/null +++ b/apps/gitlab/manifests/external-secret-saml.yaml @@ -0,0 +1,27 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: gitlab-saml + namespace: gitlab +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: gitlab-saml-secret + creationPolicy: Owner + template: + type: Opaque + data: + GITLAB_SAML_IDP_SSO_URL: "{{ .idp_sso_url }}" + GITLAB_SAML_IDP_FINGERPRINT: "{{ .idp_fingerprint }}" + data: + - secretKey: idp_sso_url + remoteRef: + key: gitlab/saml + property: idp_sso_url + - secretKey: idp_fingerprint + remoteRef: + key: gitlab/saml + property: idp_fingerprint diff --git a/apps/gitlab/manifests/gitlab-unified-certificate.yaml b/apps/gitlab/manifests/gitlab-unified-certificate.yaml new file mode 100644 index 0000000..0453ab5 --- /dev/null +++ b/apps/gitlab/manifests/gitlab-unified-certificate.yaml @@ -0,0 +1,19 @@ +# GitLab Unified TLS Certificate +# Covers all GitLab domains in a single certificate +# This prevents issues with GitLab chart creating separate certificates +# that may have incorrect domain names +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: gitlab-unified-tls + namespace: gitlab +spec: + secretName: gitlab-tls + issuerRef: + name: letsencrypt-production + kind: ClusterIssuer + dnsNames: + - gitlab.kube.huskypup.net + - registry.gitlab.kube.huskypup.net + - minio.gitlab.kube.huskypup.net + - kas.kube.huskypup.net diff --git a/apps/gitlab/manifests/istio-virtualservices.yaml b/apps/gitlab/manifests/istio-virtualservices.yaml new file mode 100644 index 0000000..31c09e1 --- /dev/null +++ b/apps/gitlab/manifests/istio-virtualservices.yaml @@ -0,0 +1,74 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: gitlab + namespace: gitlab +spec: + hosts: + - gitlab.kube.huskypup.net + gateways: + - istio-system/edge + http: + - timeout: 3600s + route: + - destination: + host: gitlab-webservice-default.gitlab.svc.cluster.local + port: + number: 8181 + +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: gitlab-registry + namespace: gitlab +spec: + hosts: + - registry.gitlab.kube.huskypup.net + gateways: + - istio-system/edge + http: + - timeout: 3600s + route: + - destination: + host: gitlab-registry.gitlab.svc.cluster.local + port: + number: 5000 + +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: gitlab-minio + namespace: gitlab +spec: + hosts: + - minio.gitlab.kube.huskypup.net + gateways: + - istio-system/edge + http: + - timeout: 3600s + route: + - destination: + host: gitlab-minio-svc.gitlab.svc.cluster.local + port: + number: 9000 + +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: gitlab-kas + namespace: gitlab +spec: + hosts: + - kas.kube.huskypup.net + gateways: + - istio-system/edge + http: + - timeout: 3600s + route: + - destination: + host: gitlab-kas.gitlab.svc.cluster.local + port: + number: 8154 diff --git a/apps/gitlab/manifests/pg-auto-restart.yaml b/apps/gitlab/manifests/pg-auto-restart.yaml new file mode 100644 index 0000000..80d5502 --- /dev/null +++ b/apps/gitlab/manifests/pg-auto-restart.yaml @@ -0,0 +1,206 @@ +--- +# ServiceAccount for the CronJob that monitors PostgreSQL secret changes +apiVersion: v1 +kind: ServiceAccount +metadata: + name: pg-restart-sa + namespace: gitlab +--- +# Role to allow patching Deployments, StatefulSets, Clusters and reading Secrets +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: pg-restart-role + namespace: gitlab +rules: + - apiGroups: ["apps"] + resources: ["deployments", "statefulsets"] + verbs: ["get", "patch"] + - apiGroups: ["postgresql.cnpg.io"] + resources: ["clusters"] + verbs: ["get", "patch"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get"] + - apiGroups: ["apps"] + resources: ["deployments/status", "statefulsets/status"] + verbs: ["get"] + - apiGroups: ["postgresql.cnpg.io"] + resources: ["clusters/status"] + verbs: ["get"] +--- +# RoleBinding to grant permissions to the ServiceAccount +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: pg-restart-binding + namespace: gitlab +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: pg-restart-role +subjects: + - kind: ServiceAccount + name: pg-restart-sa + namespace: gitlab +--- +# CronJob to monitor pg-gitlab-app secret and trigger restarts on changes +apiVersion: batch/v1 +kind: CronJob +metadata: + name: pg-gitlab-secret-monitor + namespace: gitlab +spec: + # Run every 30 minutes to check for secret changes (rotations happen at most daily) + schedule: "*/30 * * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 1 + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + template: + metadata: + labels: + app: pg-gitlab-secret-monitor + spec: + serviceAccountName: pg-restart-sa + restartPolicy: OnFailure + containers: + - name: monitor + image: docker.io/alpine/k8s:1.32.13 + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + volumeMounts: + - name: tmp + mountPath: /tmp + command: + - /bin/bash + - -c + - | + set -e + + # Get current secret version + SECRET_VERSION=$(kubectl get secret -n gitlab pg-gitlab-app -o jsonpath='{.metadata.resourceVersion}') + + # Get last known secret version from pgbouncer deployment annotation + LAST_VERSION=$(kubectl get deployment -n gitlab pgbouncer-gitlab -o jsonpath='{.spec.template.metadata.annotations.secret-version/pg-password}' 2>/dev/null || echo "") + + echo "Current secret version: $SECRET_VERSION" + echo "Last known version: $LAST_VERSION" + + # If versions differ, update database password and restart resources + if [ "$SECRET_VERSION" != "$LAST_VERSION" ]; then + echo "Secret has changed! Updating database password and resources..." + + # Get the new password from the secret + NEW_PASSWORD=$(kubectl get secret -n gitlab pg-gitlab-app -o jsonpath='{.data.password}' | base64 -d) + + # Update the database user password + # Try both pg-gitlab-1 and pg-gitlab-2 in case one is restarting + kubectl exec -n gitlab pg-gitlab-1 -c postgres -- psql -U postgres -d gitlabhq_production -c "ALTER USER app PASSWORD '$NEW_PASSWORD';" 2>/dev/null || \ + kubectl exec -n gitlab pg-gitlab-2 -c postgres -- psql -U postgres -d gitlabhq_production -c "ALTER USER app PASSWORD '$NEW_PASSWORD';" 2>/dev/null || \ + echo "Database password update failed" + + # Update password table with new hash for PgBouncer SCRAM auth + kubectl exec -n gitlab pg-gitlab-1 -c postgres -- psql -U postgres -d gitlabhq_production -c "INSERT INTO public.user_passwords (usename, passwd) SELECT rolname, rolpassword FROM pg_authid WHERE rolname = 'app' ON CONFLICT (usename) DO UPDATE SET passwd = EXCLUDED.passwd;" 2>/dev/null || \ + kubectl exec -n gitlab pg-gitlab-2 -c postgres -- psql -U postgres -d gitlabhq_production -c "INSERT INTO public.user_passwords (usename, passwd) SELECT rolname, rolpassword FROM pg_authid WHERE rolname = 'app' ON CONFLICT (usename) DO UPDATE SET passwd = EXCLUDED.passwd;" 2>/dev/null || \ + echo "Password table update failed, PgBouncer may need manual restart" + + # Patch pgbouncer deployments to trigger restart + kubectl patch deployment -n gitlab pgbouncer-gitlab -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"secret-version/pg-password\":\"$SECRET_VERSION\",\"restarted-at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}}}}}" 2>/dev/null || echo "Deployment patch failed" + + echo "Database password updated and resources will restart." + else + echo "Secret has not changed. No restart needed." + fi + volumes: + - name: tmp + emptyDir: {} +--- +# CronJob to monitor pg-praefect-app secret and trigger restarts on changes +apiVersion: batch/v1 +kind: CronJob +metadata: + name: pg-praefect-secret-monitor + namespace: gitlab +spec: + # Run every 30 minutes to check for secret changes (rotations happen at most daily) + schedule: "*/30 * * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 1 + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + template: + metadata: + labels: + app: pg-praefect-secret-monitor + spec: + serviceAccountName: pg-restart-sa + restartPolicy: OnFailure + containers: + - name: monitor + image: docker.io/alpine/k8s:1.32.13 + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + volumeMounts: + - name: tmp + mountPath: /tmp + command: + - /bin/bash + - -c + - | + set -e + + # Get current secret version + SECRET_VERSION=$(kubectl get secret -n gitlab pg-praefect-app -o jsonpath='{.metadata.resourceVersion}') + + # Get last known secret version from gitaly statefulset annotation + LAST_VERSION=$(kubectl get statefulset -n gitlab gitlab-gitaly-default -o jsonpath='{.spec.template.metadata.annotations.secret-version/pg-password}' 2>/dev/null || echo "") + + echo "Current secret version: $SECRET_VERSION" + echo "Last known version: $LAST_VERSION" + + # If versions differ, update database password and restart resources + if [ "$SECRET_VERSION" != "$LAST_VERSION" ]; then + echo "Secret has changed! Updating database password and resources..." + + # Get the new password from the secret + NEW_PASSWORD=$(kubectl get secret -n gitlab pg-praefect-app -o jsonpath='{.data.password}' | base64 -d) + + # Update the database user password + # Try both pg-praefect-3 and pg-praefect-4 + kubectl exec -n gitlab pg-praefect-3 -c postgres -- psql -U postgres -d gitlabhq_production -c "ALTER USER app PASSWORD '$NEW_PASSWORD';" 2>/dev/null || \ + kubectl exec -n gitlab pg-praefect-4 -c postgres -- psql -U postgres -d gitlabhq_production -c "ALTER USER app PASSWORD '$NEW_PASSWORD';" 2>/dev/null || \ + echo "Database password update failed" + + # Patch gitaly and praefect statefulsets + kubectl patch statefulset -n gitlab gitlab-gitaly-default -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"secret-version/pg-password\":\"$SECRET_VERSION\",\"restarted-at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}}}}}" + kubectl patch statefulset -n gitlab gitlab-praefect -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"secret-version/pg-password\":\"$SECRET_VERSION\",\"restarted-at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}}}}}" + + # Patch CNPG cluster + kubectl patch cluster -n gitlab pg-praefect -p "{\"metadata\":{\"annotations\":{\"secret-version/pg-password\":\"$SECRET_VERSION\",\"restarted-at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}}}" --type merge + + echo "Database password updated and resources will restart." + else + echo "Secret has not changed. No restart needed." + fi + volumes: + - name: tmp + emptyDir: {} \ No newline at end of file diff --git a/apps/gitlab/manifests/pgbouncer-pooler.yaml b/apps/gitlab/manifests/pgbouncer-pooler.yaml new file mode 100644 index 0000000..2f5ac5b --- /dev/null +++ b/apps/gitlab/manifests/pgbouncer-pooler.yaml @@ -0,0 +1,116 @@ +--- +# PgBouncer Pooler for GitLab PostgreSQL +# Managed by CloudNativePG Operator +apiVersion: postgresql.cnpg.io/v1 +kind: Pooler +metadata: + name: pgbouncer-gitlab + namespace: gitlab +spec: + cluster: + name: pg-gitlab + + # Number of PgBouncer instances (keep small; GitLab points directly at CNPG RW service) + instances: 1 + + # PgBouncer configuration + type: rw # Read-Write pooler (connects to primary) + + pgbouncer: + poolMode: transaction + authQuerySecret: + name: pg-gitlab-app + # Use custom user_search function for SCRAM-SHA-256 authentication + # This function is created by gitlab-bootstrap.sh script + authQuery: "SELECT usename, passwd FROM public.user_search($1)" + + parameters: + max_client_conn: "2000" + default_pool_size: "50" + reserve_pool_size: "10" + server_idle_timeout: "600" # Keep connections alive for 10 minutes + log_connections: "1" + log_disconnections: "1" + log_pooler_errors: "1" + stats_period: "60" + + # Template for PgBouncer pods + template: + metadata: + labels: + app: pgbouncer-gitlab + spec: + containers: + - name: pgbouncer + resources: + requests: + cpu: 25m + memory: 256Mi + limits: + memory: 512Mi + + # Anti-affinity to spread PgBouncer pods across nodes + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app: pgbouncer-gitlab + topologyKey: kubernetes.io/hostname +--- +# Read-only Pooler for Database Load Balancing +apiVersion: postgresql.cnpg.io/v1 +kind: Pooler +metadata: + name: pgbouncer-gitlab-ro + namespace: gitlab +spec: + cluster: + name: pg-gitlab + + instances: 3 + + type: ro # Read-Only pooler (connects to replicas) + + pgbouncer: + poolMode: transaction + authQuerySecret: + name: pg-gitlab-app + # Use custom user_search function for SCRAM-SHA-256 authentication + # This function is created by gitlab-bootstrap.sh script + authQuery: "SELECT usename, passwd FROM public.user_search($1)" + + parameters: + max_client_conn: "2000" + default_pool_size: "50" + reserve_pool_size: "10" + max_db_connections: "100" + server_idle_timeout: "600" # Keep connections alive for 10 minutes + log_connections: "1" + log_disconnections: "1" + + template: + metadata: + labels: + app: pgbouncer-gitlab-ro + spec: + containers: + - name: pgbouncer + resources: + requests: + cpu: 25m + memory: 256Mi + limits: + memory: 512Mi + + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app: pgbouncer-gitlab-ro + topologyKey: kubernetes.io/hostname diff --git a/apps/gitlab/manifests/praefect-cnpg-cluster.yaml b/apps/gitlab/manifests/praefect-cnpg-cluster.yaml new file mode 100644 index 0000000..7c8f9dd --- /dev/null +++ b/apps/gitlab/manifests/praefect-cnpg-cluster.yaml @@ -0,0 +1,35 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: pg-praefect + namespace: gitlab +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:16 + instances: 2 + + # Resources: keep memory, keep CPU requests low for scheduling. + # CPU limits omitted so Postgres can burst when available. + resources: + requests: + memory: "512Mi" + cpu: "100m" + limits: + memory: "2Gi" + + # Spread replicas across different nodes + affinity: + topologyKey: kubernetes.io/hostname + + storage: + size: 10Gi + storageClass: rook-ceph-block + + primaryUpdateStrategy: unsupervised + + bootstrap: + initdb: + database: praefect_production + owner: app + + monitoring: + enablePodMonitor: true diff --git a/apps/gitlab/manifests/redis-auto-restart.yaml b/apps/gitlab/manifests/redis-auto-restart.yaml new file mode 100644 index 0000000..06ffe5e --- /dev/null +++ b/apps/gitlab/manifests/redis-auto-restart.yaml @@ -0,0 +1,108 @@ +--- +# ServiceAccount for the CronJob that monitors Redis secret changes +apiVersion: v1 +kind: ServiceAccount +metadata: + name: redis-restart-sa + namespace: gitlab +--- +# Role to allow patching StatefulSets and reading Secrets +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: redis-restart-role + namespace: gitlab +rules: + - apiGroups: ["apps"] + resources: ["statefulsets"] + verbs: ["get", "patch"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get"] + - apiGroups: ["apps"] + resources: ["statefulsets/status"] + verbs: ["get"] +--- +# RoleBinding to grant permissions to the ServiceAccount +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: redis-restart-binding + namespace: gitlab +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: redis-restart-role +subjects: + - kind: ServiceAccount + name: redis-restart-sa + namespace: gitlab +--- +# CronJob to monitor Redis secret and trigger StatefulSet restart on changes +apiVersion: batch/v1 +kind: CronJob +metadata: + name: redis-secret-monitor + namespace: gitlab +spec: + # Run every hour to check for secret changes + # This aligns with the 24-hour secret rotation schedule + schedule: "*/60 * * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 1 + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + template: + metadata: + labels: + app: redis-secret-monitor + spec: + serviceAccountName: redis-restart-sa + restartPolicy: OnFailure + containers: + - name: monitor + image: docker.io/alpine/k8s:1.32.13 + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + volumeMounts: + - name: tmp + mountPath: /tmp + command: + - /bin/bash + - -c + - | + set -e + + # Get current secret version + SECRET_VERSION=$(kubectl get secret -n gitlab redis-gitlab-secret -o jsonpath='{.metadata.resourceVersion}') + + # Get last known secret version from StatefulSet annotation + LAST_VERSION=$(kubectl get statefulset -n gitlab redis-gitlab -o jsonpath='{.spec.template.metadata.annotations.secret-version/redis-password}' 2>/dev/null || echo "") + + echo "Current secret version: $SECRET_VERSION" + echo "Last known version: $LAST_VERSION" + + # If versions differ, restart StatefulSet + if [ "$SECRET_VERSION" != "$LAST_VERSION" ]; then + echo "Secret has changed! Updating StatefulSet with new version annotation..." + + # Patch StatefulSet with new secret version annotation + # This will trigger a rolling restart of the Redis pod + kubectl patch statefulset -n gitlab redis-gitlab -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"secret-version/redis-password\":\"$SECRET_VERSION\",\"restarted-at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}}}}}" + + echo "StatefulSet will now perform a rolling restart to pick up the new password." + else + echo "Secret has not changed. No restart needed." + fi + volumes: + - name: tmp + emptyDir: {} diff --git a/apps/gitlab/manifests/redis-cluster.yaml b/apps/gitlab/manifests/redis-cluster.yaml new file mode 100644 index 0000000..bc5168e --- /dev/null +++ b/apps/gitlab/manifests/redis-cluster.yaml @@ -0,0 +1,95 @@ +# Redis standalone instance for GitLab (used instead of Sentinel for simplicity) +# Password auth is required - GitLab reads the password from redis-gitlab-secret +--- +apiVersion: v1 +kind: Service +metadata: + name: redis-gitlab-additional + namespace: gitlab +spec: + ports: + - port: 6379 + targetPort: 6379 + name: redis + selector: + app: redis-gitlab-standalone + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: redis-gitlab-standalone + namespace: gitlab +spec: + ports: + - port: 6379 + targetPort: 6379 + name: redis + selector: + app: redis-gitlab-standalone + type: ClusterIP +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: redis-gitlab-standalone + namespace: gitlab +spec: + serviceName: redis-gitlab-additional + replicas: 1 + selector: + matchLabels: + app: redis-gitlab-standalone + template: + metadata: + labels: + app: redis-gitlab-standalone + spec: + securityContext: + fsGroup: 1000 + containers: + - name: redis + image: redis:7.0-alpine + securityContext: + runAsUser: 999 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + ports: + - containerPort: 6379 + name: redis + command: + - sh + - -c + - redis-server --appendonly yes --requirepass "$REDIS_PASSWORD" + env: + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: redis-gitlab-secret + key: password + resources: + requests: + cpu: 25m + memory: 256Mi + limits: + memory: 512Mi + volumeMounts: + - name: data + mountPath: /data + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: rook-ceph-block + resources: + requests: + storage: 5Gi diff --git a/apps/gitlab/manifests/redis-sentinel-ha.yaml b/apps/gitlab/manifests/redis-sentinel-ha.yaml new file mode 100644 index 0000000..05a920e --- /dev/null +++ b/apps/gitlab/manifests/redis-sentinel-ha.yaml @@ -0,0 +1,33 @@ +--- +# Password generator and ESO for GitLab Redis +apiVersion: generators.external-secrets.io/v1alpha1 +kind: Password +metadata: + name: gitlab-redis-password + namespace: gitlab +spec: + length: 32 + digits: 5 + symbols: 0 + noUpper: false + allowRepeat: true +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: gitlab-redis-password + namespace: gitlab +spec: + refreshInterval: "0" # Generate once, never rotate (Password generator creates new value each refresh) + target: + name: redis-gitlab-secret + creationPolicy: Owner + template: + data: + password: "{{ .password }}" + dataFrom: + - sourceRef: + generatorRef: + apiVersion: generators.external-secrets.io/v1alpha1 + kind: Password + name: gitlab-redis-password diff --git a/apps/gitlab/manifests/sync-admin-job.yaml b/apps/gitlab/manifests/sync-admin-job.yaml new file mode 100644 index 0000000..19071f7 --- /dev/null +++ b/apps/gitlab/manifests/sync-admin-job.yaml @@ -0,0 +1,114 @@ +--- +# Job to sync GitLab admin status from Authentik groups +# Run this after users login via Authentik SSO to grant them admin access +apiVersion: batch/v1 +kind: Job +metadata: + name: gitlab-sync-admin + namespace: gitlab +spec: + ttlSecondsAfterFinished: 3600 # Clean up after 1 hour + template: + spec: + restartPolicy: OnFailure + containers: + - name: sync-admin + image: docker.io/library/alpine:3.21 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + command: + - /bin/sh + - -c + - | + set -e + + apk add --no-cache postgresql-client curl + + echo "🔄 Syncing GitLab admin permissions from Authentik..." + + # Get list of users in "authentik Admins" group + ADMIN_USERS=$(PGPASSWORD="$AUTHENTIK_DB_PASSWORD" psql -h pg-authentik-rw.authentik.svc.cluster.local -U app -d app -t -c " + SELECT DISTINCT u.email + FROM authentik_core_user u + JOIN authentik_core_user_groups ug ON u.id = ug.user_id + JOIN authentik_core_group g ON ug.group_id = g.group_uuid + WHERE g.name = 'authentik Admins' AND u.is_active = true; + " | xargs) + + if [ -z "$ADMIN_USERS" ]; then + echo "⚠️ No users found in 'authentik Admins' group" + exit 0 + fi + + echo "✓ Found admin users: $ADMIN_USERS" + echo "" + + # For each admin user, grant admin access in GitLab + for email in $ADMIN_USERS; do + echo "🔐 Checking user: $email" + + # Use GitLab Rails runner to promote user + kubectl exec -n gitlab deployment/gitlab-toolbox -- \ + gitlab-rails runner " + user = User.find_by(email: '$email') + if user + if user.admin? + puts ' ✓ Already admin' + else + user.update(admin: true) + puts ' ✅ Promoted to admin' + end + else + puts ' ⚠️ User not found (needs to login via SSO first)' + end + " || echo " ❌ Failed to update user" + done + + echo "" + echo "✅ Admin sync complete" + env: + - name: AUTHENTIK_DB_PASSWORD + valueFrom: + secretKeyRef: + name: pg-authentik-app + namespace: authentik + key: password + serviceAccountName: gitlab-sync-admin +--- +# ServiceAccount for the sync job +apiVersion: v1 +kind: ServiceAccount +metadata: + name: gitlab-sync-admin + namespace: gitlab +--- +# Role to allow exec into toolbox pod +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: gitlab-sync-admin + namespace: gitlab +rules: +- apiGroups: [""] + resources: ["pods", "pods/exec"] + verbs: ["get", "list", "create"] +- apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list"] +--- +# RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: gitlab-sync-admin + namespace: gitlab +subjects: +- kind: ServiceAccount + name: gitlab-sync-admin + namespace: gitlab +roleRef: + kind: Role + name: gitlab-sync-admin + apiGroup: rbac.authorization.k8s.io diff --git a/apps/gitlab/values.yaml b/apps/gitlab/values.yaml new file mode 100644 index 0000000..2bc8c0b --- /dev/null +++ b/apps/gitlab/values.yaml @@ -0,0 +1,333 @@ +# values/gitlab.values.yaml +# GitLab with Authentik OIDC SSO Integration + +global: + hosts: + domain: kube.huskypup.net + gitlab: + name: gitlab.kube.huskypup.net + registry: + name: registry.gitlab.kube.huskypup.net + tls: + secretName: gitlab-tls + minio: + name: minio.gitlab.kube.huskypup.net + tls: + secretName: gitlab-tls + kas: + name: kas.kube.huskypup.net + tls: + secretName: gitlab-tls + + # Ingress disabled - Istio VirtualServices handle routing + ingress: + enabled: false + configureCertmanager: false # Use cluster-wide cert-manager, not GitLab's + + # Edition: Community Edition + edition: ce + + # Time zone + time_zone: UTC + + # Email configuration (configure as needed) + email: + from: 'gitlab@kube.huskypup.net' + display_name: GitLab + reply_to: 'noreply@kube.huskypup.net' + + # External PostgreSQL configuration + # NOTE: PgBouncer service currently has no endpoints (replicas=0), + # so GitLab is pointed directly at the CNPG primary service. + psql: + host: pg-gitlab-rw.gitlab.svc.cluster.local + port: 5432 + database: gitlabhq_production + username: app + password: + secret: pg-gitlab-app + key: password + + # Gitaly configuration - using Praefect for HA + gitaly: + enabled: true # Enabled to deploy Gitaly pods + internal: + names: [] # No internal Gitaly, using Praefect + external: [] # Praefect configured below + + # Praefect configuration + praefect: + enabled: true + + # Use CNPG database secret + dbSecret: + secret: pg-praefect-app + key: password + + virtualStorages: + - name: default + gitalyReplicas: 3 # Production HA + maxUnavailable: 1 + + # Praefect PostgreSQL configuration + psql: + host: pg-praefect-rw.gitlab.svc.cluster.local + port: 5432 + dbName: praefect_production + user: app + + # External Redis configuration - using standalone Redis for writes + # (replicated Redis service causes READONLY errors from replicas) + redis: + host: redis-gitlab-standalone.gitlab.svc.cluster.local + port: 6379 + auth: + enabled: true + secret: redis-gitlab-secret + key: password + + # Application Configuration + appConfig: + # OmniAuth SSO Configuration + omniauth: + enabled: true + allowSingleSignOn: ['openid_connect'] + blockAutoCreatedUsers: false + autoLinkUser: ['openid_connect'] + syncProfileFromProvider: ['openid_connect'] + syncProfileAttributes: ['email', 'name'] + providers: + - secret: gitlab-oidc-secret + key: provider + +# Settings for Let's Encrypt ACME Issuer - disabled, using cluster-wide cert-manager +certmanager-issuer: + email: admin@kube.huskypup.net + + # Authentik OIDC Configuration via Rails omnibus config + # Note: Credentials are loaded from gitlab-oidc-secret via environment variables + + # SAML Configuration via Rails omnibus config + # Note: SAML provider configuration is loaded from gitlab-saml-secret + +# PostgreSQL (external via CloudNativePG) +postgresql: + install: false + +# Redis (external) +redis: + install: false + +# PgBouncer connection pooler +# NOTE: PgBouncer is deployed via CNPG Pooler CRD (see pgbouncer-pooler.yaml) +# This setting disables GitLab's bundled PgBouncer chart +pgbouncer: + enabled: false # Using external CNPG Pooler instead + +# MinIO for object storage +minio: + persistence: + storageClass: rook-ceph-block + size: 100Gi # Production storage + resources: + requests: + cpu: 100m + memory: 512Mi + limits: + memory: 2Gi + +# Container Registry - enabled with S3 storage +registry: + enabled: true + hpa: + minReplicas: 1 + maxReplicas: 2 + resources: + requests: + cpu: 25m + memory: 128Mi + limits: + memory: 1Gi + +# GitLab components +gitlab: + # GitLab Webservice - Main application + webservice: + minReplicas: 1 # Homelab sizing + maxReplicas: 3 + + # Note: hostAliases for OIDC SSL validation are applied via helmfile postsync hook + # (GitLab chart doesn't support hostAliases in values.yaml) + + extraEnvFrom: + GITLAB_OIDC_CLIENT_ID: + secretKeyRef: + name: gitlab-oidc-secret + key: GITLAB_OIDC_CLIENT_ID + GITLAB_OIDC_CLIENT_SECRET: + secretKeyRef: + name: gitlab-oidc-secret + key: GITLAB_OIDC_CLIENT_SECRET + GITLAB_SAML_IDP_FINGERPRINT: + secretKeyRef: + name: gitlab-saml-secret + key: GITLAB_SAML_IDP_FINGERPRINT + GITLAB_SAML_IDP_SSO_URL: + secretKeyRef: + name: gitlab-saml-secret + key: GITLAB_SAML_IDP_SSO_URL + extraEnv: + GITLAB_OMNIBUS_CONFIG: | + # Authentik OIDC Configuration + gitlab_rails['omniauth_enabled'] = true + gitlab_rails['omniauth_allow_single_sign_on'] = ['openid_connect', 'saml'] + gitlab_rails['omniauth_block_auto_created_users'] = false + gitlab_rails['omniauth_auto_link_user'] = ['openid_connect', 'saml'] + gitlab_rails['omniauth_auto_sign_in_with_provider'] = nil + gitlab_rails['omniauth_sync_profile_from_provider'] = ['openid_connect', 'saml'] + gitlab_rails['omniauth_sync_profile_attributes'] = ['email', 'name'] + + gitlab_rails['omniauth_providers'] = [ + { + 'name' => 'openid_connect', + 'label' => 'Authentik', + 'args' => { + 'name' => 'openid_connect', + 'scope' => ['openid', 'profile', 'email'], + 'response_type' => 'code', + 'issuer' => 'https://auth.kube.huskypup.net/application/o/gitlab/', + 'discovery' => true, + 'client_auth_method' => 'query', + 'uid_field' => 'sub', + 'send_scope_to_token_endpoint' => true, + 'pkce' => true, + 'client_options' => { + 'identifier' => ENV['GITLAB_OIDC_CLIENT_ID'], + 'secret' => ENV['GITLAB_OIDC_CLIENT_SECRET'], + 'redirect_uri' => 'https://gitlab.kube.huskypup.net/users/auth/openid_connect/callback' + } + } + }, + { + 'name' => 'saml', + 'label' => 'Authentik SAML', + 'args' => { + 'assertion_consumer_service_url' => 'https://gitlab.kube.huskypup.net/users/auth/saml/callback', + 'idp_cert_fingerprint' => ENV['GITLAB_SAML_IDP_FINGERPRINT'], + 'idp_sso_target_url' => ENV['GITLAB_SAML_IDP_SSO_URL'], + 'issuer' => 'https://gitlab.kube.huskypup.net', + 'name_identifier_format' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + 'attribute_statements' => { + 'email' => ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'], + 'name' => ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'], + 'first_name' => ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'], + 'last_name' => ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'] + } + } + } + ] + # Homelab resource allocation + resources: + requests: + cpu: 50m + memory: 512Mi + limits: + cpu: 500m + memory: 2Gi + + # GitLab KAS (Kubernetes Agent Server) - enabled for Kubernetes cluster integration + kas: + enabled: true + minReplicas: 1 + maxReplicas: 1 + resources: + requests: + cpu: 25m + memory: 256Mi + limits: + cpu: 500m + memory: 1Gi + + # Gitaly Cluster (Praefect) - High Availability Git storage + gitaly: + # Note: enabled is in global.gitaly + persistence: + storageClass: rook-ceph-block + size: 200Gi # Production storage + # Production resources + resources: + requests: + cpu: 50m + memory: 512Mi + limits: + cpu: 250m + memory: 2Gi + + # GitLab Runner - enabled for CI/CD + gitlab-runner: + install: false # Will be installed separately + + # Praefect - Gitaly Cluster routing and transaction manager + praefect: + enabled: true + minReplicas: 1 # Homelab sizing + maxReplicas: 1 + + # Note: Praefect PostgreSQL config is in global.praefect.psql + + # Use CNPG-generated database secret + dbSecret: + secret: pg-praefect-app + key: password + + # Resources + resources: + requests: + cpu: 25m + memory: 256Mi + limits: + cpu: 250m + memory: 512Mi + + # Virtual storage configuration + virtualStorages: + - name: default + gitalyReplicas: 1 # Homelab sizing + maxUnavailable: 1 + + # GitLab Exporter for Prometheus metrics + gitlab-exporter: + enabled: true + metrics: + enabled: true + + # Sidekiq background jobs + sidekiq: + minReplicas: 1 # Homelab sizing + maxReplicas: 1 + + # Note: hostAliases applied via helmfile postsync hook + + # Homelab resources + resources: + requests: + cpu: 25m + memory: 512Mi + limits: + cpu: 250m + memory: 2Gi + +# Disable components we already have in the cluster +certmanager: + install: false # Using cluster-wide cert-manager + installCRDs: false + +prometheus: + install: false # Using existing Prometheus + +nginx-ingress: + enabled: false # Istio handles ingress + +# Disable GitLab Runner (configure separately if needed) +gitlab-runner: + install: false diff --git a/apps/guacamole/manifests/cnpg-cluster.yaml b/apps/guacamole/manifests/cnpg-cluster.yaml new file mode 100644 index 0000000..3dbd731 --- /dev/null +++ b/apps/guacamole/manifests/cnpg-cluster.yaml @@ -0,0 +1,50 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: pg-guacamole + namespace: guacamole +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:16 + instances: 2 + + # Resource limits to prevent OOM + resources: + requests: + memory: "256Mi" + cpu: "50m" + limits: + memory: "1Gi" + + # Spread replicas across different nodes + affinity: + topologyKey: kubernetes.io/hostname + + storage: + size: 10Gi + storageClass: rook-ceph-block + + primaryUpdateStrategy: unsupervised + + # PostgreSQL configuration for Guacamole + postgresql: + parameters: + max_connections: "100" + shared_buffers: "256MB" + effective_cache_size: "768MB" + maintenance_work_mem: "64MB" + checkpoint_completion_target: "0.9" + wal_buffers: "8MB" + default_statistics_target: "100" + random_page_cost: "1.1" + effective_io_concurrency: "200" + work_mem: "1310kB" + min_wal_size: "1GB" + max_wal_size: "4GB" + + bootstrap: + initdb: + database: guacamole + owner: guacamole + + monitoring: + enablePodMonitor: true \ No newline at end of file diff --git a/apps/guacamole/manifests/db-init-schema-job.yaml b/apps/guacamole/manifests/db-init-schema-job.yaml new file mode 100644 index 0000000..5e635c9 --- /dev/null +++ b/apps/guacamole/manifests/db-init-schema-job.yaml @@ -0,0 +1,114 @@ +# Job to initialize Guacamole database schema +# This should run once after the PostgreSQL cluster is ready +apiVersion: batch/v1 +kind: Job +metadata: + name: guacamole-init-schema + namespace: guacamole +spec: + ttlSecondsAfterFinished: 300 + template: + spec: + restartPolicy: OnFailure + initContainers: + - name: wait-for-postgres + image: postgres:16-alpine + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumeMounts: + - name: tmp + mountPath: /tmp + command: + - sh + - -c + - | + echo "Waiting for PostgreSQL to be ready..." + until pg_isready -h pg-guacamole-rw -p 5432 -U guacamole; do + echo "PostgreSQL not ready, waiting..." + sleep 5 + done + echo "PostgreSQL is ready!" + env: + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: pg-guacamole-app + key: password + - name: generate-initdb + image: guacamole/guacamole:1.6.0 + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + command: + - sh + - -c + - | + set -e + /opt/guacamole/bin/initdb.sh --postgresql > /initdb/initdb.sql + test -s /initdb/initdb.sql + volumeMounts: + - name: initdb + mountPath: /initdb + containers: + - name: init-schema + image: postgres:16-alpine + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + command: + - sh + - -c + - | + set -e + echo "Checking if schema already exists..." + TABLES=$(PGPASSWORD="$PGPASSWORD" psql -h pg-guacamole-rw -U guacamole -d guacamole -tA -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'guacamole_user';" 2>/dev/null | tr -d '[:space:]' || printf '0') + TABLES=${TABLES:-0} + + if [ "$TABLES" -gt "0" ]; then + echo "Schema already exists." + else + echo "Initializing Guacamole database schema..." + PGPASSWORD="$PGPASSWORD" psql -v ON_ERROR_STOP=1 -h pg-guacamole-rw -U guacamole -d guacamole -f /initdb/initdb.sql + echo "Schema initialization complete!" + fi + + echo "Setting up Authentik Admins group permissions..." + PGPASSWORD="$PGPASSWORD" psql -v ON_ERROR_STOP=1 -h pg-guacamole-rw -U guacamole -d guacamole -f /schema/permissions.sql + echo "Authentik Admins group permissions configured!" + env: + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: pg-guacamole-app + key: password + volumeMounts: + - name: initdb + mountPath: /initdb + - name: schema + mountPath: /schema + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} + - name: initdb + emptyDir: {} + - name: schema + configMap: + name: guacamole-schema diff --git a/apps/guacamole/manifests/deployment-complete.yaml b/apps/guacamole/manifests/deployment-complete.yaml new file mode 100644 index 0000000..bfa4834 --- /dev/null +++ b/apps/guacamole/manifests/deployment-complete.yaml @@ -0,0 +1,256 @@ +# Guacamole complete deployment with OpenID/Authentik support +# This deployment includes: +# - Environment-based OpenID configuration +# - PostgreSQL backend for connections/users +# - Authentik uses Let's Encrypt (no custom cert import needed) +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: guacamole + namespace: guacamole + labels: + app: guacamole + component: client +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + selector: + matchLabels: + app: guacamole + component: client + template: + metadata: + labels: + app: guacamole + component: client + spec: + automountServiceAccountToken: false + containers: + - name: guacamole + image: guacamole/guacamole:1.6.0 + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: tmp + mountPath: /tmp + - name: tomcat-work + mountPath: /usr/local/tomcat/work + - name: tomcat-logs + mountPath: /usr/local/tomcat/logs + ports: + - name: http + containerPort: 8080 + protocol: TCP + env: + - name: GUACD_HOSTNAME + value: "guacd" + - name: GUACD_PORT + value: "4822" + - name: POSTGRESQL_HOSTNAME + value: "pg-guacamole-rw" + - name: POSTGRESQL_PORT + value: "5432" + - name: POSTGRESQL_DATABASE + value: "guacamole" + - name: POSTGRESQL_ENABLED + value: "true" + - name: POSTGRESQL_USERNAME + value: "guacamole" + - name: POSTGRESQL_PASSWORD + valueFrom: + secretKeyRef: + name: pg-guacamole-app + key: password + - name: POSTGRESQL_AUTO_CREATE_ACCOUNTS + value: "true" + - name: ENABLE_ENVIRONMENT_PROPERTIES + value: "true" + - name: LOG_LEVEL + value: "debug" + - name: WEBAPP_CONTEXT + value: "ROOT" + # FIX: Enabled WebSockets to stop the 10-second tunnel timeout + - name: ENABLE_WEBSOCKET + value: "true" + - name: EXTENSION_PRIORITY + value: "*,openid" + - name: OPENID_RESPONSE_TYPE + value: "code" + - name: OPENID_AUTHORIZATION_ENDPOINT + value: "https://auth.kube.huskypup.net/application/o/authorize/" + - name: OPENID_JWKS_ENDPOINT + value: "https://auth.kube.huskypup.net/application/o/guacamole/jwks/" + - name: OPENID_ISSUER + value: "https://auth.kube.huskypup.net/application/o/guacamole/" + - name: OPENID_TOKEN_ENDPOINT + value: "https://auth.kube.huskypup.net/application/o/token/" + - name: OPENID_REDIRECT_URI + value: "https://guacamole.kube.huskypup.net/" + - name: OPENID_USERNAME_CLAIM_TYPE + value: "preferred_username" + - name: OPENID_GROUPS_CLAIM_TYPE + value: "groups" + - name: OPENID_SCOPE + value: "openid email profile groups" + - name: OPENID_ALLOWED_CLOCK_SKEW + value: "30" + - name: OPENID_MAX_TOKEN_VALIDITY + value: "300" + - name: OPENID_MAX_NONCE_VALIDITY + value: "60" + - name: OPENID_CLIENT_ID + valueFrom: + secretKeyRef: + name: guacamole-oauth-secret + key: client-id + - name: OPENID_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: guacamole-oauth-secret + key: client-secret + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "1000m" + livenessProbe: + httpGet: + path: /api/languages + port: http + initialDelaySeconds: 60 + periodSeconds: 30 + # FIX: Relaxed readiness probe so minor DB lags don't kill the Endpoint + readinessProbe: + httpGet: + path: /api/languages + port: http + initialDelaySeconds: 30 + periodSeconds: 15 + failureThreshold: 5 + volumes: + - name: tmp + emptyDir: {} + - name: tomcat-work + emptyDir: {} + - name: tomcat-logs + emptyDir: {} + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: guacd + namespace: guacamole + labels: + app: guacamole + component: guacd +spec: + replicas: 1 + selector: + matchLabels: + app: guacamole + component: guacd + template: + metadata: + labels: + app: guacamole + component: guacd + spec: + automountServiceAccountToken: false + containers: + - name: guacd + image: guacamole/guacd:1.6.0 + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: tmp + mountPath: /tmp + - name: home + mountPath: /home + ports: + - name: guacd + containerPort: 4822 + protocol: TCP + env: + - name: LOG_LEVEL + value: "debug" + resources: + requests: + memory: "256Mi" + cpu: "10m" + limits: + memory: "512Mi" + cpu: "500m" + readinessProbe: + tcpSocket: + port: 4822 + initialDelaySeconds: 2 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + volumes: + - name: tmp + emptyDir: {} + - name: home + emptyDir: {} + +--- +apiVersion: v1 +kind: Service +metadata: + name: guacamole + namespace: guacamole + labels: + app: guacamole + component: client +spec: + type: ClusterIP + ports: + - port: 8080 + targetPort: http + protocol: TCP + name: http + selector: + app: guacamole + component: client +--- +apiVersion: v1 +kind: Service +metadata: + name: guacd + namespace: guacamole + labels: + app: guacamole + component: guacd +spec: + type: ClusterIP + ports: + - port: 4822 + targetPort: guacd + protocol: TCP + name: guacd + selector: + app: guacamole + component: guacd diff --git a/apps/guacamole/manifests/envoy-gateway.yaml b/apps/guacamole/manifests/envoy-gateway.yaml new file mode 100644 index 0000000..f8d1aac --- /dev/null +++ b/apps/guacamole/manifests/envoy-gateway.yaml @@ -0,0 +1,67 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: guacamole-envoy-tls + namespace: guacamole +spec: + secretName: guacamole-envoy-tls + issuerRef: + kind: ClusterIssuer + name: letsencrypt-production + dnsNames: + - guacamole.kube.huskypup.net + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: guacamole + namespace: guacamole +spec: + gatewayClassName: envoy-gateway + listeners: + - name: https + hostname: guacamole.kube.huskypup.net + port: 443 + protocol: HTTPS + tls: + mode: Terminate + certificateRefs: + - kind: Secret + name: guacamole-envoy-tls + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: guacamole + namespace: guacamole +spec: + parentRefs: + - name: guacamole + sectionName: https + hostnames: + - guacamole.kube.huskypup.net + rules: + # Legacy path support: https://host/guacamole/* -> https://host/* + - matches: + - path: + type: PathPrefix + value: /guacamole + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: guacamole + port: 8080 + # Standard root routing + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: guacamole + port: 8080 diff --git a/apps/guacamole/manifests/envoy-route.yaml b/apps/guacamole/manifests/envoy-route.yaml new file mode 100644 index 0000000..2d9dbe2 --- /dev/null +++ b/apps/guacamole/manifests/envoy-route.yaml @@ -0,0 +1,33 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: guacamole + namespace: guacamole +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - guacamole.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: /guacamole + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: guacamole + port: 8080 + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: guacamole + port: 8080 diff --git a/apps/guacamole/manifests/external-secret.yaml b/apps/guacamole/manifests/external-secret.yaml new file mode 100644 index 0000000..79d50ba --- /dev/null +++ b/apps/guacamole/manifests/external-secret.yaml @@ -0,0 +1,29 @@ +# kubernetes/apps/guacamole/external-secret.yaml +# ExternalSecrets for Guacamole - pulls credentials from Vault + +# Note: Database password is managed by CNPG cluster (pg-guacamole-app secret) +# We reference it directly in the guacamole values.yaml + +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: guacamole-oauth + namespace: guacamole +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: guacamole-oauth-secret + creationPolicy: Owner + data: + - secretKey: client-id + remoteRef: + key: guacamole-oauth + property: client-id + - secretKey: client-secret + remoteRef: + key: guacamole-oauth + property: client-secret \ No newline at end of file diff --git a/apps/guacamole/manifests/istio-virtualservice.yaml b/apps/guacamole/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..972c6d6 --- /dev/null +++ b/apps/guacamole/manifests/istio-virtualservice.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: guacamole + namespace: guacamole +spec: + hosts: + - guacamole.kube.huskypup.net + gateways: + - istio-system/edge + http: + - route: + - destination: + host: guacamole.guacamole.svc.cluster.local + port: + number: 8080 diff --git a/apps/home-assistant/esphome/envoy-route.yaml b/apps/home-assistant/esphome/envoy-route.yaml new file mode 100644 index 0000000..d1da151 --- /dev/null +++ b/apps/home-assistant/esphome/envoy-route.yaml @@ -0,0 +1,20 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: esphome + namespace: home-assistant +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - esphome.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: esphome + port: 6052 diff --git a/apps/home-assistant/esphome/istio-virtualservice.yaml b/apps/home-assistant/esphome/istio-virtualservice.yaml new file mode 100644 index 0000000..62645b7 --- /dev/null +++ b/apps/home-assistant/esphome/istio-virtualservice.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: esphome + namespace: home-assistant +spec: + hosts: + - esphome.kube.huskypup.net + gateways: + - istio-system/edge + http: + - route: + - destination: + host: esphome.home-assistant.svc.cluster.local + port: + number: 6052 diff --git a/apps/home-assistant/esphome/values.yaml b/apps/home-assistant/esphome/values.yaml new file mode 100644 index 0000000..0bbe93a --- /dev/null +++ b/apps/home-assistant/esphome/values.yaml @@ -0,0 +1,57 @@ +# ESPHome Helm Chart Values +# Deployed in home-assistant namespace as a companion to Home Assistant +# +# FEATURES: +# ✓ ESPHome Dashboard for managing ESP devices +# ✓ Persistent storage via Rook-Ceph +# ✓ TLS certificates via cert-manager +# ✓ Ingress via Istio VirtualService + +image: + repository: ghcr.io/esphome/esphome + tag: "2024.11.3" + pullPolicy: IfNotPresent + +service: + main: + ports: + http: + port: 6052 + +# Ingress disabled - Istio VirtualService handles routing +ingress: + main: + enabled: false + +persistence: + config: + enabled: true + storageClass: rook-ceph-block + accessMode: ReadWriteOnce + size: 5Gi + retain: true + +env: + TZ: America/New_York + ESPHOME_DASHBOARD_USE_PING: "true" + +securityContext: + privileged: false + +resources: + requests: + cpu: 50m + memory: 256Mi + limits: + memory: 512Mi + +probes: + liveness: + enabled: true + readiness: + enabled: true + startup: + enabled: true + +nodeSelector: + kubernetes.io/arch: amd64 diff --git a/apps/home-assistant/manifests/envoy-route.yaml b/apps/home-assistant/manifests/envoy-route.yaml new file mode 100644 index 0000000..4fcf0a9 --- /dev/null +++ b/apps/home-assistant/manifests/envoy-route.yaml @@ -0,0 +1,20 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: home-assistant + namespace: home-assistant +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - homeassistant.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: home-assistant + port: 8123 diff --git a/apps/home-assistant/manifests/external-secret.yaml b/apps/home-assistant/manifests/external-secret.yaml new file mode 100644 index 0000000..bace1fc --- /dev/null +++ b/apps/home-assistant/manifests/external-secret.yaml @@ -0,0 +1,31 @@ +--- +# Home Assistant OIDC Credentials - External Secret +# +# This ExternalSecret syncs Home Assistant OIDC credentials from Vault +# Credentials are stored in Vault by the sync-homeassistant-oauth.sh script +# after Authentik blueprint creates the OAuth provider +# +# The secret is used by Home Assistant's hass-openid integration +# to authenticate users via Authentik OIDC +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: homeassistant-oauth + namespace: home-assistant +spec: + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: homeassistant-oidc-secret + creationPolicy: Owner + refreshInterval: 1h + data: + - secretKey: client_id + remoteRef: + key: secret/homeassistant-oauth + property: client-id + - secretKey: client_secret + remoteRef: + key: secret/homeassistant-oauth + property: client-secret diff --git a/apps/home-assistant/manifests/istio-virtualservice.yaml b/apps/home-assistant/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..676b4e8 --- /dev/null +++ b/apps/home-assistant/manifests/istio-virtualservice.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: home-assistant + namespace: home-assistant +spec: + hosts: + - homeassistant.kube.huskypup.net + gateways: + - istio-system/edge + http: + - timeout: 3600s + route: + - destination: + host: home-assistant.home-assistant.svc.cluster.local + port: + number: 8123 diff --git a/apps/home-assistant/manifests/oidc-config.yaml b/apps/home-assistant/manifests/oidc-config.yaml new file mode 100644 index 0000000..3843621 --- /dev/null +++ b/apps/home-assistant/manifests/oidc-config.yaml @@ -0,0 +1,24 @@ +--- +# Home Assistant OIDC Configuration ConfigMap +# +# This ConfigMap contains the OIDC configuration snippet that gets appended +# to Home Assistant's configuration.yaml file during bootstrap +# +# The configuration enables hass-openid integration for Authentik OIDC auth +apiVersion: v1 +kind: ConfigMap +metadata: + name: homeassistant-oidc-config + namespace: home-assistant +data: + oidc.yaml: | + # OIDC Authentication via Authentik + # Users authenticate via Authentik and are auto-created in Home Assistant + openid: + client_id: !secret oidc_client_id + client_secret: !secret oidc_client_secret + configure_url: "https://auth.kube.huskypup.net/application/o/home-assistant/.well-known/openid-configuration" + scope: "openid profile email" + username_field: "preferred_username" + create_user: true + block_login: false diff --git a/apps/home-assistant/values.yaml b/apps/home-assistant/values.yaml new file mode 100644 index 0000000..ba19f37 --- /dev/null +++ b/apps/home-assistant/values.yaml @@ -0,0 +1,152 @@ +# Disable Istio sidecar on home-assistant pod because the init container +# (install-hass-openid) needs unrestricted network access to git clone. +# Istio CNI redirects traffic to the proxy port during init, but the proxy +# isn't running yet, causing TLS/connection failures. +# ESPHome (same namespace) still gets sidecar injection normally. +podAnnotations: + sidecar.istio.io/inject: "false" + +image: + repository: ghcr.io/home-assistant/home-assistant + tag: "2025.1.2" + pullPolicy: IfNotPresent + +initContainers: + install-hass-openid: + image: docker.io/alpine/git:2.47.2 + command: + - /bin/sh + - -c + - | + set -e + apk add --no-cache git + + # Install hass-openid custom integration + mkdir -p /config/custom_components /config/includes + cd /tmp + git clone --depth 1 https://github.com/cavefire/hass-openid.git + cp -r hass-openid/custom_components/openid /config/custom_components/ + rm -rf hass-openid + + # Write MQTT addons config + printf 'mqtt:\n broker: mqtt.kube.huskypup.net\n port: 1883\n username: homeassistant\n password: YOUR_MQTT_PASSWORD' > /config/includes/ha-addons.yaml + + # Write OIDC credentials to secrets.yaml (from mounted k8s secret) + if [ -f /oidc-secret/client_id ] && [ -f /oidc-secret/client_secret ]; then + CLIENT_ID=$(cat /oidc-secret/client_id) + CLIENT_SECRET=$(cat /oidc-secret/client_secret) + # Preserve existing secrets, update OIDC entries + if [ -f /config/secrets.yaml ]; then + sed -i '/^oidc_client_id:/d' /config/secrets.yaml + sed -i '/^oidc_client_secret:/d' /config/secrets.yaml + fi + echo "oidc_client_id: \"${CLIENT_ID}\"" >> /config/secrets.yaml + echo "oidc_client_secret: \"${CLIENT_SECRET}\"" >> /config/secrets.yaml + echo "OIDC credentials written to secrets.yaml" + else + echo "WARNING: OIDC secret not mounted, skipping secrets.yaml" + fi + + # Ensure OIDC config is in configuration.yaml + if [ ! -f /config/configuration.yaml ]; then + echo "Creating configuration.yaml with OIDC config..." + cat > /config/configuration.yaml <<'OIDCEOF' + # Home Assistant Configuration + default_config: + + # OIDC Authentication via Authentik + openid: + client_id: !secret oidc_client_id + client_secret: !secret oidc_client_secret + configure_url: "https://auth.kube.huskypup.net/application/o/home-assistant/.well-known/openid-configuration" + scope: "openid profile email" + username_field: "preferred_username" + create_user: true + block_login: false + OIDCEOF + elif ! grep -q "^openid:" /config/configuration.yaml; then + echo "Appending OIDC config to existing configuration.yaml..." + cat >> /config/configuration.yaml <<'OIDCEOF' + + # OIDC Authentication via Authentik + openid: + client_id: !secret oidc_client_id + client_secret: !secret oidc_client_secret + configure_url: "https://auth.kube.huskypup.net/application/o/home-assistant/.well-known/openid-configuration" + scope: "openid profile email" + username_field: "preferred_username" + create_user: true + block_login: false + OIDCEOF + else + echo "OIDC config already present in configuration.yaml" + fi + volumeMounts: + - name: config + mountPath: /config + - name: oidc-secret + mountPath: /oidc-secret + readOnly: true + +env: + TZ: America/New_York + +service: + main: + ports: + http: + port: 8123 + +# Ingress disabled - Istio VirtualService handles routing +ingress: + main: + enabled: false + +persistence: + config: + enabled: true + storageClass: rook-ceph-block + accessMode: ReadWriteOnce + size: 10Gi + retain: true + oidc-secret: + enabled: true + type: secret + name: homeassistant-oidc-secret + mountPath: /oidc-secret + readOnly: true + +securityContext: + privileged: false + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + +resources: + requests: + cpu: 50m + memory: 512Mi + limits: + cpu: 1000m + memory: 2Gi + +probes: + liveness: + enabled: true + readiness: + enabled: true + startup: + enabled: true + +mariadb: + enabled: false + +postgresql: + enabled: false + +influxdb: + enabled: false + +metrics: + enabled: false diff --git a/apps/n8n/manifests/cnpg-cluster.yaml b/apps/n8n/manifests/cnpg-cluster.yaml new file mode 100644 index 0000000..d8ccdbd --- /dev/null +++ b/apps/n8n/manifests/cnpg-cluster.yaml @@ -0,0 +1,51 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: pg-n8n + namespace: n8n +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:16 + instances: 2 + + # Resource limits to prevent OOM + resources: + requests: + memory: "512Mi" + cpu: "25m" + limits: + memory: "2Gi" + cpu: "250m" + + # Spread replicas across different nodes + affinity: + topologyKey: kubernetes.io/hostname + + storage: + size: 10Gi + storageClass: rook-ceph-block + + primaryUpdateStrategy: unsupervised + + # PostgreSQL configuration for better performance + postgresql: + parameters: + max_connections: "200" + shared_buffers: "512MB" + effective_cache_size: "1536MB" + maintenance_work_mem: "128MB" + checkpoint_completion_target: "0.9" + wal_buffers: "16MB" + default_statistics_target: "100" + random_page_cost: "1.1" + effective_io_concurrency: "200" + work_mem: "2621kB" + min_wal_size: "1GB" + max_wal_size: "4GB" + + bootstrap: + initdb: + database: n8n + owner: n8n + + monitoring: + enablePodMonitor: true diff --git a/apps/n8n/manifests/cnpg-secrets.yaml b/apps/n8n/manifests/cnpg-secrets.yaml new file mode 100644 index 0000000..ec81b2a --- /dev/null +++ b/apps/n8n/manifests/cnpg-secrets.yaml @@ -0,0 +1,40 @@ +--- +apiVersion: generators.external-secrets.io/v1alpha1 +kind: Password +metadata: + name: n8n-cnpg-secret + namespace: n8n +spec: + length: 42 + digits: 5 + symbols: 5 + symbolCharacters: "-_$@" + noUpper: false + allowRepeat: true + +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: n8n-cnpg-secret + namespace: n8n +spec: + # Rotate database password every 24 hours + refreshInterval: "24h" + target: + # This will merge the generated password into the existing pg-n8n-app secret + name: pg-n8n-app + creationPolicy: Merge + template: + metadata: + labels: + cnpg.io/reload: "true" + data: + # Override the password field with our ESO-generated password + password: "{{ .password }}" + dataFrom: + - sourceRef: + generatorRef: + apiVersion: generators.external-secrets.io/v1alpha1 + kind: Password + name: n8n-cnpg-secret diff --git a/apps/n8n/manifests/envoy-route.yaml b/apps/n8n/manifests/envoy-route.yaml new file mode 100644 index 0000000..20d9d82 --- /dev/null +++ b/apps/n8n/manifests/envoy-route.yaml @@ -0,0 +1,20 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: n8n + namespace: n8n +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - n8n.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: n8n + port: 80 diff --git a/apps/n8n/manifests/external-secret.yaml b/apps/n8n/manifests/external-secret.yaml new file mode 100644 index 0000000..426ee8c --- /dev/null +++ b/apps/n8n/manifests/external-secret.yaml @@ -0,0 +1,49 @@ +# kubernetes/apps/n8n/external-secret.yaml +# ExternalSecrets for n8n - pulls credentials from Vault + +# Note: Database password is managed by CNPG cluster (pg-n8n-app secret) +# We reference it directly in the n8n values.yaml extraEnv section + +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: n8n-config + namespace: n8n +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: n8n-config-secret + creationPolicy: Owner + data: + - secretKey: encryption-key + remoteRef: + key: n8n-config + property: encryption-key + +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: n8n-oauth + namespace: n8n +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: n8n-oauth-secret + creationPolicy: Owner + data: + - secretKey: client-id + remoteRef: + key: n8n-oauth + property: client-id + - secretKey: client-secret + remoteRef: + key: n8n-oauth + property: client-secret diff --git a/apps/n8n/manifests/hooks-configmap.yaml b/apps/n8n/manifests/hooks-configmap.yaml new file mode 100644 index 0000000..4458d5e --- /dev/null +++ b/apps/n8n/manifests/hooks-configmap.yaml @@ -0,0 +1,81 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: n8n-hooks + namespace: n8n +data: + hooks.js: | + // n8n v2.0.3 compatible hooks for Authentik forward auth integration + const { resolve, dirname } = require('path'); + + module.exports = { + credentials: { + create: [], + delete: [], + update: [] + }, + workflow: { + create: [], + delete: [], + update: [] + }, + server: { + started: [ + async function (app) { + console.log('[n8n-hooks] Initializing forward auth middleware'); + + // Get Express app + const expressApp = app?.app; + if (!expressApp) { + console.error('[n8n-hooks] Express app not available'); + return; + } + + let issueCookie, UserRepository, Container; + try { + const n8nPath = dirname(require.resolve('n8n')); + issueCookie = require(resolve(n8nPath, 'dist/auth/jwt')).issueCookie; + UserRepository = require(resolve(n8nPath, 'dist/databases/repositories/user.repository')).UserRepository; + Container = require('typedi').Container; + } catch (error) { + console.error('[n8n-hooks] Failed to load dependencies:', error.message); + return; + } + + const ignoreAuthRegexp = /^\/(assets|healthz|webhook|rest\/oauth2-credential|rest\/settings|static|icons|types)/; + + // Add middleware for forward auth + expressApp.use(async (req, res, next) => { + try { + if (ignoreAuthRegexp.test(req.url)) return next(); + if (req.cookies?.['n8n-auth']) return next(); + if (!process.env.N8N_FORWARD_AUTH_HEADER) return next(); + + const headerName = process.env.N8N_FORWARD_AUTH_HEADER.toLowerCase().replace(/_/g, '-'); + const email = req.headers[headerName]; + if (!email) return next(); + + const userRepo = Container.get(UserRepository); + const user = await userRepo.findOne({ where: { email } }); + + if (!user) { + console.warn(`[n8n-hooks] User not found: ${email}`); + res.statusCode = 401; + res.end(`User ${email} not found. Please contact an admin.`); + return; + } + + console.log(`[n8n-hooks] Auto-login: ${email}`); + issueCookie(res, user); + next(); + } catch (error) { + console.error('[n8n-hooks] Middleware error:', error.message); + next(); + } + }); + + console.log('[n8n-hooks] Forward auth middleware active'); + } + ] + } + }; \ No newline at end of file diff --git a/apps/n8n/manifests/istio-virtualservice.yaml b/apps/n8n/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..4f4ae03 --- /dev/null +++ b/apps/n8n/manifests/istio-virtualservice.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: n8n + namespace: n8n +spec: + hosts: + - n8n.kube.huskypup.net + gateways: + - istio-system/edge + http: + - route: + - destination: + host: n8n.n8n.svc.cluster.local + port: + number: 80 diff --git a/apps/n8n/values.yaml b/apps/n8n/values.yaml new file mode 100644 index 0000000..82ce426 --- /dev/null +++ b/apps/n8n/values.yaml @@ -0,0 +1,161 @@ +# kubernetes/apps/n8n/values.yaml +# n8n workflow automation platform +# Using chart: https://riatlas.github.io/chart__n8n + +# --- n8n image --- +image: + repository: n8nio/n8n + tag: "2.0.3" + +# --- n8n encryption key (loaded from secret) --- +n8n: + encryption_key: # Will be set via extraEnv from n8n-config-secret + +# --- n8n configuration --- +config: + database: + type: postgresdb + postgresdb: + host: pg-n8n-rw # CNPG read-write service + port: 5432 + database: n8n + user: n8n + schema: public + generic: + timezone: America/New_York + path: / + host: n8n.kube.huskypup.net + port: 5678 + protocol: https + executions: + mode: regular + saveDataOnError: all + saveDataOnSuccess: all + saveDataManualExecutions: true + pruneData: true + pruneDataMaxAge: 3760 # 1 year in hours + +# --- Secret values (passwords, etc.) --- +# Note: password is injected via extraEnvSecrets below +secret: {} + +# --- Deployment replicas --- +# Increased to 2 for high availability and faster response times +replicaCount: 2 + +# --- Service configuration --- +service: + type: ClusterIP + port: 80 + +# --- Security context --- +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + +# --- Ingress disabled - Istio VirtualService handles routing --- +ingress: + enabled: false + +# --- Persistence for workflows and data --- +persistence: + enabled: true + type: existing + existingClaim: n8n-main-persistence + storageClass: rook-ceph-block + accessModes: + - ReadWriteOnce + size: 10Gi + +# --- Resources --- +# No CPU limits (burst allowed). Keep requests minimal for scheduling. +resources: + requests: + cpu: 100m + memory: 512Mi + +# --- Startup probe --- +# Allows app to start without being killed by liveness probe +startupProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 12 # 60 seconds total startup time + +# --- Readiness probe --- +readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 2 + +# --- Liveness probe --- +livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + +# --- Extra environment variables (from secrets) --- +# Secrets are loaded from Kubernetes secrets +extraEnvSecrets: + # Database password from CNPG-managed secret + DB_POSTGRESDB_PASSWORD: + name: pg-n8n-app + key: password + + # n8n encryption key + N8N_ENCRYPTION_KEY: + name: n8n-config-secret + key: encryption-key + +# --- Extra environment variables (plain values) --- +extraEnv: + # Webhook & Editor URLs + WEBHOOK_URL: https://n8n.kube.huskypup.net/ + N8N_EDITOR_BASE_URL: https://n8n.kube.huskypup.net + + # Logging + N8N_LOG_LEVEL: error + +# --- Disable built-in PostgreSQL (we'll deploy it separately with Bitnami) --- +postgresql: + enabled: false + +# --- Disable Redis (not needed for single instance) --- +redis: + enabled: false + +# --- Scaling (disabled for single instance) --- +scaling: + enabled: false + +# --- Extra volumes --- +extraVolumes: [] + +# --- Extra volume mounts --- +extraVolumeMounts: [] + + + +# --- Node selector --- +nodeSelector: {} + +# --- Tolerations --- +tolerations: [] + +# --- Affinity --- +affinity: {} diff --git a/apps/nessus/manifests/cnpg-cluster.yaml b/apps/nessus/manifests/cnpg-cluster.yaml new file mode 100644 index 0000000..8137d16 --- /dev/null +++ b/apps/nessus/manifests/cnpg-cluster.yaml @@ -0,0 +1,52 @@ +# CloudNative PostgreSQL cluster for Nessus +# Low resource configuration for memory-constrained environments +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: pg-nessus + namespace: nessus +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:16 + instances: 2 + + # Low resource limits for constrained environments + resources: + requests: + memory: "512Mi" + cpu: "50m" + limits: + memory: "1Gi" + + # Spread replicas across different nodes for HA + affinity: + topologyKey: kubernetes.io/hostname + + storage: + size: 20Gi + storageClass: rook-ceph-block + + primaryUpdateStrategy: unsupervised + + # PostgreSQL configuration optimized for low memory + postgresql: + parameters: + max_connections: "100" + shared_buffers: "256MB" + effective_cache_size: "768MB" + maintenance_work_mem: "64MB" + checkpoint_completion_target: "0.9" + wal_buffers: "8MB" + default_statistics_target: "100" + random_page_cost: "1.1" + effective_io_concurrency: "200" + work_mem: "2621kB" + min_wal_size: "512MB" + max_wal_size: "2GB" + + bootstrap: + initdb: + database: nessus + owner: nessus + + monitoring: + enablePodMonitor: true diff --git a/apps/nessus/manifests/cnpg-secrets.yaml b/apps/nessus/manifests/cnpg-secrets.yaml new file mode 100644 index 0000000..afb1041 --- /dev/null +++ b/apps/nessus/manifests/cnpg-secrets.yaml @@ -0,0 +1,44 @@ +# External Secrets Operator configuration for Nessus PostgreSQL password rotation +# This auto-generates and rotates the database password in Vault +--- +apiVersion: generators.external-secrets.io/v1alpha1 +kind: Password +metadata: + name: nessus-db-password + namespace: nessus +spec: + length: 32 + digits: 10 + symbols: 0 # No special chars to avoid escaping issues + symbolCharacters: "" + noUpper: false + allowRepeat: true + +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: nessus-cnpg-secret + namespace: nessus +spec: + refreshInterval: 1h + secretStoreRef: + name: vault-backend + kind: ClusterSecretStore + target: + name: nessus-db-secret + creationPolicy: Owner + template: + engineVersion: v2 + data: + password: "{{ .password }}" + username: "nessus" + host: "pg-nessus-rw" + port: "5432" + database: "nessus" + dataFrom: + - sourceRef: + generatorRef: + apiVersion: generators.external-secrets.io/v1alpha1 + kind: Password + name: nessus-db-password diff --git a/apps/nessus/manifests/deployment.yaml b/apps/nessus/manifests/deployment.yaml new file mode 100644 index 0000000..819a823 --- /dev/null +++ b/apps/nessus/manifests/deployment.yaml @@ -0,0 +1,134 @@ +# Nessus Vulnerability Scanner Deployment +# Optimized for low resource usage in constrained environments +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nessus + namespace: nessus + labels: + app: nessus + app.kubernetes.io/name: nessus + app.kubernetes.io/component: security-scanner +spec: + replicas: 1 + strategy: + type: Recreate # Only one instance can mount the PVC at a time + selector: + matchLabels: + app: nessus + template: + metadata: + labels: + app: nessus + annotations: + # Reloader auto-restart when secrets change + secret.reloader.stakater.com/reload: "nessus-admin-credentials" + spec: + containers: + - name: nessus + image: docker.io/tenable/nessus:10.8.3-ubuntu + imagePullPolicy: IfNotPresent + + # Low resource configuration for constrained environments + resources: + requests: + memory: "1Gi" + cpu: "50m" + limits: + memory: "2Gi" + + ports: + - name: https + containerPort: 8834 + protocol: TCP + + # Environment variables for Nessus configuration + env: + # Admin credentials from Vault (via ESO) + - name: USERNAME + valueFrom: + secretKeyRef: + name: nessus-admin-credentials + key: NESSUS_USERNAME + - name: PASSWORD + valueFrom: + secretKeyRef: + name: nessus-admin-credentials + key: NESSUS_PASSWORD + + # Auto-update settings (plugins only to save bandwidth/time) + - name: AUTO_UPDATE + value: "plugins" + + # Volume mounts for persistent data + volumeMounts: + - name: nessus-data + mountPath: /opt/nessus/var/nessus + + # Startup probe - give Nessus time to initialize + startupProbe: + httpGet: + path: / + port: 8834 + scheme: HTTPS + initialDelaySeconds: 60 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 60 # 15 minutes total startup time + + # Liveness probe + livenessProbe: + httpGet: + path: / + port: 8834 + scheme: HTTPS + initialDelaySeconds: 300 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 5 + + # Readiness probe + readinessProbe: + httpGet: + path: / + port: 8834 + scheme: HTTPS + initialDelaySeconds: 120 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 5 + + # Security context + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: false # Nessus requires root + capabilities: + drop: + - ALL + add: + - NET_ADMIN # Required for network scanning + - NET_RAW # Required for raw socket scanning + + volumes: + - name: nessus-data + persistentVolumeClaim: + claimName: nessus-data + +--- +apiVersion: v1 +kind: Service +metadata: + name: nessus + namespace: nessus + labels: + app: nessus +spec: + type: ClusterIP + ports: + - name: https + port: 8834 + targetPort: 8834 + protocol: TCP + selector: + app: nessus diff --git a/apps/nessus/manifests/external-secret.yaml b/apps/nessus/manifests/external-secret.yaml new file mode 100644 index 0000000..bfe5723 --- /dev/null +++ b/apps/nessus/manifests/external-secret.yaml @@ -0,0 +1,30 @@ +# External Secrets Operator configuration for Nessus admin credentials +# Pulls Nessus admin username and password from Vault +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: nessus-admin-secret + namespace: nessus +spec: + refreshInterval: 1h + secretStoreRef: + name: vault-backend + kind: ClusterSecretStore + target: + name: nessus-admin-credentials + creationPolicy: Owner + template: + engineVersion: v2 + data: + NESSUS_USERNAME: "{{ .username }}" + NESSUS_PASSWORD: "{{ .password }}" + data: + - secretKey: username + remoteRef: + key: secret/nessus + property: admin-username + - secretKey: password + remoteRef: + key: secret/nessus + property: admin-password diff --git a/apps/nessus/manifests/istio-virtualservice.yaml b/apps/nessus/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..b5e3faa --- /dev/null +++ b/apps/nessus/manifests/istio-virtualservice.yaml @@ -0,0 +1,31 @@ +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: nessus + namespace: nessus +spec: + host: nessus.nessus.svc.cluster.local + trafficPolicy: + tls: + mode: SIMPLE + sni: nessus.nessus.svc.cluster.local + insecureSkipVerify: true + +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: nessus + namespace: nessus +spec: + hosts: + - nessus.kube.huskypup.net + gateways: + - istio-system/edge + http: + - timeout: 3600s + route: + - destination: + host: nessus.nessus.svc.cluster.local + port: + number: 8834 diff --git a/apps/nessus/manifests/pvc.yaml b/apps/nessus/manifests/pvc.yaml new file mode 100644 index 0000000..2eda6fc --- /dev/null +++ b/apps/nessus/manifests/pvc.yaml @@ -0,0 +1,13 @@ +# PersistentVolumeClaim for Nessus scan data and plugin storage +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: nessus-data + namespace: nessus +spec: + accessModes: + - ReadWriteOnce + storageClassName: rook-ceph-block + resources: + requests: + storage: 50Gi diff --git a/apps/nextcloud/manifests/envoy-route.yaml b/apps/nextcloud/manifests/envoy-route.yaml new file mode 100644 index 0000000..385efa0 --- /dev/null +++ b/apps/nextcloud/manifests/envoy-route.yaml @@ -0,0 +1,20 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: nextcloud + namespace: nextcloud +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - nextcloud.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: nextcloud + port: 8080 diff --git a/apps/nextcloud/manifests/external-secret.yaml b/apps/nextcloud/manifests/external-secret.yaml new file mode 100644 index 0000000..9d3e93e --- /dev/null +++ b/apps/nextcloud/manifests/external-secret.yaml @@ -0,0 +1,26 @@ +# kubernetes/apps/nextcloud/external-secret.yaml +# ExternalSecrets for Nextcloud - pulls OAuth credentials from Vault + +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: nextcloud-oauth + namespace: nextcloud +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: nextcloud-oauth-secret + creationPolicy: Owner + data: + - secretKey: client-id + remoteRef: + key: nextcloud-oauth + property: client-id + - secretKey: client-secret + remoteRef: + key: nextcloud-oauth + property: client-secret diff --git a/apps/nextcloud/manifests/istio-virtualservice.yaml b/apps/nextcloud/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..bb4c6ca --- /dev/null +++ b/apps/nextcloud/manifests/istio-virtualservice.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: nextcloud + namespace: nextcloud +spec: + hosts: + - nextcloud.kube.huskypup.net + gateways: + - istio-system/edge + http: + - timeout: 3600s + route: + - destination: + host: nextcloud.nextcloud.svc.cluster.local + port: + number: 8080 diff --git a/apps/nextcloud/manifests/mariadb-deployment.yaml b/apps/nextcloud/manifests/mariadb-deployment.yaml new file mode 100644 index 0000000..5b5b092 --- /dev/null +++ b/apps/nextcloud/manifests/mariadb-deployment.yaml @@ -0,0 +1,92 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mariadb-nextcloud + namespace: nextcloud + labels: + app: mariadb-nextcloud +spec: + replicas: 1 + selector: + matchLabels: + app: mariadb-nextcloud + template: + metadata: + labels: + app: mariadb-nextcloud + spec: + securityContext: + runAsUser: 999 + runAsGroup: 999 + fsGroup: 999 + containers: + - name: mariadb + image: mariadb:11.4 + securityContext: + runAsUser: 999 + runAsGroup: 999 + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + args: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --init-connect=SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci + env: + - name: MARIADB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: mariadb-nextcloud + key: root-password + - name: MARIADB_PASSWORD + valueFrom: + secretKeyRef: + name: mariadb-nextcloud + key: password + - name: MARIADB_USER + value: nextcloud + - name: MARIADB_DATABASE + value: nextcloud + ports: + - containerPort: 3306 + volumeMounts: + - name: mariadb-data + mountPath: /var/lib/mysql + - name: tmp + mountPath: /tmp + - name: run-mysqld + mountPath: /run/mysqld + volumes: + - name: tmp + emptyDir: {} + - name: run-mysqld + emptyDir: {} + - name: mariadb-data + persistentVolumeClaim: + claimName: mariadb-nextcloud-pvc +--- +apiVersion: v1 +kind: Service +metadata: + name: mariadb-nextcloud + namespace: nextcloud +spec: + selector: + app: mariadb-nextcloud + ports: + - port: 3306 + targetPort: 3306 +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mariadb-nextcloud-pvc + namespace: nextcloud +spec: + accessModes: + - ReadWriteOnce + storageClassName: rook-ceph-block + resources: + requests: + storage: 10Gi \ No newline at end of file diff --git a/apps/nextcloud/manifests/mariadb-secrets.yaml b/apps/nextcloud/manifests/mariadb-secrets.yaml new file mode 100644 index 0000000..3302daa --- /dev/null +++ b/apps/nextcloud/manifests/mariadb-secrets.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: generators.external-secrets.io/v1alpha1 +kind: Password +metadata: + name: nextcloud-mariadb-password + namespace: nextcloud +spec: + length: 32 + digits: 5 + symbols: 3 + symbolCharacters: "-_$" + noUpper: false + allowRepeat: true +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: nextcloud-mariadb-secret + namespace: nextcloud +spec: + refreshInterval: "24h" + target: + name: mariadb-nextcloud + creationPolicy: Owner + template: + data: + password: "{{ .password }}" + root-password: "{{ .password }}" + user: nextcloud + dataFrom: + - sourceRef: + generatorRef: + apiVersion: generators.external-secrets.io/v1alpha1 + kind: Password + name: nextcloud-mariadb-password \ No newline at end of file diff --git a/apps/nextcloud/manifests/pvc.yaml b/apps/nextcloud/manifests/pvc.yaml new file mode 100644 index 0000000..adbb855 --- /dev/null +++ b/apps/nextcloud/manifests/pvc.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: nextcloud-nextcloud + namespace: nextcloud + labels: + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: nextcloud + app.kubernetes.io/instance: nextcloud + annotations: + meta.helm.sh/release-name: nextcloud + meta.helm.sh/release-namespace: nextcloud +spec: + accessModes: + - ReadWriteOnce + storageClassName: rook-ceph-block # Rook-Ceph block storage + resources: + requests: + storage: 50Gi # whatever size you want diff --git a/apps/nextcloud/values.yaml b/apps/nextcloud/values.yaml new file mode 100644 index 0000000..d6a6bf3 --- /dev/null +++ b/apps/nextcloud/values.yaml @@ -0,0 +1,111 @@ +# values/nextcloud.values.yaml +# +# For chart: nextcloud/nextcloud +# Repo: https://nextcloud.github.io/helm/ +# +# Optimized for large file uploads over 1Gbps link + +replicaCount: 1 + +nextcloud: + host: nextcloud.kube.huskypup.net + username: "" + password: "" + + # PHP configuration for large uploads + # Use /var/www/tmp (on Ceph PVC) instead of /tmp (on root overlay) + phpConfigs: + upload.ini: | + upload_tmp_dir = /var/www/tmp + sys_temp_dir = /var/www/tmp + + # Environment variables for PHP + extraEnv: + - name: TMPDIR + value: /var/www/tmp + - name: PHP_MEMORY_LIMIT + value: "2G" + - name: PHP_UPLOAD_LIMIT + value: "10G" + + configs: + oidc.config.php: |- + 'https://auth.kube.huskypup.net/application/o/nextcloud/', + 'oidc_login_client_id' => '', + 'oidc_login_client_secret' => '', + 'oidc_login_button_text' => 'Log in with Authentik', + 'oidc_login_auto_redirect' => false, + 'oidc_login_hide_password_form' => false, + 'oidc_login_verify_ssl' => false, + 'user_oidc' => array( + 'httpclient.allowselfsigned' => true, + ), + ); + proxy.config.php: |- + array( + 0 => '10.0.0.0/8', + 1 => '172.16.0.0/12', + ), + 'overwriteprotocol' => 'https', + 'overwrite.cli.url' => 'https://nextcloud.kube.huskypup.net', + 'allow_local_remote_servers' => true, + ); + # Use Ceph-backed temp directory for large uploads + temp.config.php: |- + '/var/www/tmp', + ); + + existingSecret: + enabled: true + secretName: nextcloud-admin-secret + usernameKey: username + passwordKey: password + + # Ingress disabled - Istio VirtualService handles routing + ingress: + enabled: false + +externalDatabase: + enabled: true + type: mysql + host: mariadb-nextcloud + port: 3306 + user: nextcloud + database: nextcloud + existingSecret: + enabled: true + secretName: mariadb-nextcloud + passwordKey: password + usernameKey: user + +persistence: + enabled: true + storageClass: rook-ceph-block + accessMode: ReadWriteOnce + size: 50Gi + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + +resources: + requests: + cpu: 50m + memory: 512Mi + limits: + cpu: 1000m + memory: 2Gi + +redis: + enabled: false + +metrics: + enabled: false diff --git a/apps/rancher/manifests/external-secret.yaml b/apps/rancher/manifests/external-secret.yaml new file mode 100644 index 0000000..a2e0417 --- /dev/null +++ b/apps/rancher/manifests/external-secret.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: rancher-oauth + namespace: cattle-system +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: rancher-oauth-secret + creationPolicy: Owner + data: + - secretKey: client-id + remoteRef: + key: secret/rancher-oauth + property: client-id + - secretKey: client-secret + remoteRef: + key: secret/rancher-oauth + property: client-secret diff --git a/apps/rancher/manifests/istio-virtualservice.yaml b/apps/rancher/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..ddd00dd --- /dev/null +++ b/apps/rancher/manifests/istio-virtualservice.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: rancher + namespace: cattle-system +spec: + hosts: + - rancher.kube.huskypup.net + gateways: + - istio-system/edge + http: + - route: + - destination: + host: rancher.cattle-system.svc.cluster.local + port: + number: 80 diff --git a/apps/rancher/values.yaml b/apps/rancher/values.yaml new file mode 100644 index 0000000..4dc2372 --- /dev/null +++ b/apps/rancher/values.yaml @@ -0,0 +1,47 @@ +# apps/rancher/values.yaml +# Rancher - Kubernetes Management Platform with Authentik SSO + +hostname: rancher.kube.huskypup.net + +# Exposed via Istio edge Gateway + VirtualService (apps/rancher/istio-virtualservice.yaml) +ingress: + enabled: false + +# TLS terminates at Istio ingressgateway +tls: external + +# Single replica for homelab (scale up if needed) +replicas: 1 + +# Storage class for Rancher's internal state +# Note: Rancher itself is mostly stateless; state is in etcd/k8s +resources: + requests: + cpu: 50m + memory: 512Mi + limits: + memory: 2Gi + +# Allow Rancher to manage the local cluster +addLocal: "true" + +# Audit logging +auditLog: + level: 1 + maxAge: 7 + maxBackup: 3 + maxSize: 100 + +# Bootstrap password - will be rotated post-deploy via Authentik SSO +# This is used only for initial access before SSO is configured +bootstrapPassword: "ChangeMe-BootstrapOnly" + +# Extra environment variables (not used for OIDC - configured via AuthConfig CRD post-deploy) +extraEnv: [] + +# Disable Rancher's bundled cert-manager (we already have it) +certmanager: + version: "" + +# Feature flags +features: "multi-cluster-management=true" diff --git a/apps/teslamate/manifests/cnpg-cluster.yaml b/apps/teslamate/manifests/cnpg-cluster.yaml new file mode 100644 index 0000000..84e46a6 --- /dev/null +++ b/apps/teslamate/manifests/cnpg-cluster.yaml @@ -0,0 +1,55 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: pg-teslamate + namespace: teslamate +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:16 + instances: 2 + + # Resource limits to prevent OOM + resources: + requests: + memory: "512Mi" + cpu: "25m" + limits: + memory: "2Gi" + cpu: "250m" + + # Spread replicas across different nodes + affinity: + topologyKey: kubernetes.io/hostname + + storage: + size: 10Gi + storageClass: rook-ceph-block + + primaryUpdateStrategy: unsupervised + + # PostgreSQL configuration for better performance + postgresql: + parameters: + max_connections: "200" + shared_buffers: "512MB" + effective_cache_size: "1536MB" + maintenance_work_mem: "128MB" + checkpoint_completion_target: "0.9" + wal_buffers: "16MB" + default_statistics_target: "100" + random_page_cost: "1.1" + effective_io_concurrency: "200" + work_mem: "2621kB" + min_wal_size: "1GB" + max_wal_size: "4GB" + + bootstrap: + initdb: + database: teslamate + owner: teslamate + postInitSQL: + - ALTER USER teslamate WITH SUPERUSER + - CREATE EXTENSION IF NOT EXISTS cube + - CREATE EXTENSION IF NOT EXISTS earthdistance + + monitoring: + enablePodMonitor: true diff --git a/apps/teslamate/manifests/cnpg-secrets.yaml b/apps/teslamate/manifests/cnpg-secrets.yaml new file mode 100644 index 0000000..23dd09d --- /dev/null +++ b/apps/teslamate/manifests/cnpg-secrets.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: generators.external-secrets.io/v1alpha1 +kind: Password +metadata: + name: teslamate-cnpg-secret + namespace: teslamate +spec: + length: 42 + digits: 5 + symbols: 5 + symbolCharacters: "-_$@" + noUpper: false + allowRepeat: true +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: teslamate-cnpg-secret + namespace: teslamate +spec: + # how often to rotate the DB password + refreshInterval: "24h" + target: + # This will merge the generated password into the existing pg-teslamate-app secret + name: pg-teslamate-app + creationPolicy: Merge + template: + metadata: + labels: + cnpg.io/reload: "true" + data: + # Override the password field with our ESO-generated password + password: "{{ .password }}" + dataFrom: + - sourceRef: + generatorRef: + apiVersion: generators.external-secrets.io/v1alpha1 + kind: Password + name: teslamate-cnpg-secret diff --git a/apps/teslamate/manifests/envoy-route.yaml b/apps/teslamate/manifests/envoy-route.yaml new file mode 100644 index 0000000..15e02af --- /dev/null +++ b/apps/teslamate/manifests/envoy-route.yaml @@ -0,0 +1,20 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: teslamate + namespace: teslamate +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - teslamate.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: teslamate + port: 4000 diff --git a/apps/teslamate/manifests/external-secret.yaml b/apps/teslamate/manifests/external-secret.yaml new file mode 100644 index 0000000..cf30460 --- /dev/null +++ b/apps/teslamate/manifests/external-secret.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: teslamate-config + namespace: teslamate +spec: + refreshInterval: 1h + secretStoreRef: + name: vault-backend + kind: ClusterSecretStore + target: + name: teslamate-config-secret + creationPolicy: Owner + data: + - secretKey: encryption-key + remoteRef: + key: teslamate/config + property: encryption_key diff --git a/apps/teslamate/manifests/istio-virtualservice.yaml b/apps/teslamate/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..f448f33 --- /dev/null +++ b/apps/teslamate/manifests/istio-virtualservice.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: teslamate + namespace: teslamate +spec: + hosts: + - teslamate.kube.huskypup.net + gateways: + - istio-system/edge + http: + - route: + - destination: + host: teslamate.teslamate.svc.cluster.local + port: + number: 4000 diff --git a/apps/teslamate/values.yaml b/apps/teslamate/values.yaml new file mode 100644 index 0000000..30f2e01 --- /dev/null +++ b/apps/teslamate/values.yaml @@ -0,0 +1,112 @@ +# Teslamate - Tesla data logger +# Uses CNPG PostgreSQL with auto-rotating passwords via ESO + +image: + repository: teslamate/teslamate + tag: 1.30.1 + pullPolicy: IfNotPresent + +env: + TZ: America/Chicago + + # PostgreSQL configuration (uses CNPG-managed database) + DATABASE_HOST: pg-teslamate-rw.teslamate.svc + DATABASE_USER: teslamate + DATABASE_NAME: teslamate + + # Database password from CNPG-generated secret + DATABASE_PASS: + valueFrom: + secretKeyRef: + name: pg-teslamate-app + key: password + + # Encryption key from Vault + ENCRYPTION_KEY: + valueFrom: + secretKeyRef: + name: teslamate-config-secret + key: encryption-key + + # MQTT settings (disabled by default, can enable later) + DISABLE_MQTT: "true" + + # Virtual host for proper URL generation + VIRTUAL_HOST: teslamate.kube.huskypup.net + + # Port configuration + PORT: "4000" + +service: + main: + ports: + http: + port: 4000 + +# Ingress disabled - Istio VirtualService handles routing +ingress: + main: + enabled: false + +persistence: + import: + enabled: false + +# Disable built-in postgresql (we use CNPG) +postgresql: + enabled: false + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + +# Resource limits +resources: + requests: + memory: "256Mi" + cpu: "50m" + limits: + cpu: "500m" + memory: "512Mi" + +# Health probes +probes: + liveness: + enabled: true + custom: true + spec: + httpGet: + path: / + port: 4000 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readiness: + enabled: true + custom: true + spec: + httpGet: + path: / + port: 4000 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 2 + startup: + enabled: true + custom: true + spec: + httpGet: + path: / + port: 4000 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 12 + +# Reloader annotations for auto-restart on secret changes +podAnnotations: + secret.reloader.stakater.com/reload: "pg-teslamate-app,teslamate-config-secret" diff --git a/argocd-apps/apps/argocd.yaml b/argocd-apps/apps/argocd.yaml new file mode 100644 index 0000000..4a5f906 --- /dev/null +++ b/argocd-apps/apps/argocd.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: argocd + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "50" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: applications + sources: + - repoURL: https://argoproj.github.io/argo-helm + chart: argo-cd + targetRevision: "*" + helm: + valueFiles: + - $values/apps/argocd/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: apps/argocd/manifests + destination: + server: https://kubernetes.default.svc + namespace: argocd + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/apps/esphome.yaml b/argocd-apps/apps/esphome.yaml new file mode 100644 index 0000000..8fb4380 --- /dev/null +++ b/argocd-apps/apps/esphome.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: esphome + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "51" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: applications + sources: + - repoURL: https://charts.gabe565.com + chart: esphome + targetRevision: 0.15.0 + helm: + valueFiles: + - $values/apps/home-assistant/esphome/values.yaml + - repoURL: + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: home-assistant + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/apps/frigate.yaml b/argocd-apps/apps/frigate.yaml new file mode 100644 index 0000000..a259bea --- /dev/null +++ b/argocd-apps/apps/frigate.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: frigate + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "51" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: applications + sources: + - repoURL: https://blakeblackshear.github.io/blakeshome-charts + chart: frigate + targetRevision: 7.8.0 + helm: + valueFiles: + - $values/apps/frigate/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: apps/frigate/manifests + destination: + server: https://kubernetes.default.svc + namespace: frigate + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/apps/gitlab.yaml b/argocd-apps/apps/gitlab.yaml new file mode 100644 index 0000000..45de590 --- /dev/null +++ b/argocd-apps/apps/gitlab.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: gitlab + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "50" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: applications + sources: + - repoURL: https://charts.gitlab.io + chart: gitlab + targetRevision: 7.7.0 + helm: + valueFiles: + - $values/apps/gitlab/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: apps/gitlab/manifests + destination: + server: https://kubernetes.default.svc + namespace: gitlab + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/apps/guacamole.yaml b/argocd-apps/apps/guacamole.yaml new file mode 100644 index 0000000..b635b48 --- /dev/null +++ b/argocd-apps/apps/guacamole.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: guacamole + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "51" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: applications + source: + repoURL: + targetRevision: main + path: apps/guacamole/manifests + destination: + server: https://kubernetes.default.svc + namespace: guacamole + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/apps/home-assistant.yaml b/argocd-apps/apps/home-assistant.yaml new file mode 100644 index 0000000..0fde37e --- /dev/null +++ b/argocd-apps/apps/home-assistant.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: home-assistant + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "51" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: applications + sources: + - repoURL: https://geek-cookbook.github.io/charts/ + chart: home-assistant + targetRevision: 13.5.0 + helm: + valueFiles: + - $values/apps/home-assistant/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: apps/home-assistant/manifests + destination: + server: https://kubernetes.default.svc + namespace: home-assistant + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/apps/istio-app-routes.yaml b/argocd-apps/apps/istio-app-routes.yaml new file mode 100644 index 0000000..92829c9 --- /dev/null +++ b/argocd-apps/apps/istio-app-routes.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: istio-app-routes + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "52" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: applications + source: + repoURL: + targetRevision: main + path: infrastructure/istio/manifests/apps + destination: + server: https://kubernetes.default.svc + namespace: istio-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/apps/n8n.yaml b/argocd-apps/apps/n8n.yaml new file mode 100644 index 0000000..bb58095 --- /dev/null +++ b/argocd-apps/apps/n8n.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: n8n + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "50" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: applications + sources: + - repoURL: https://riatlas.github.io/chart__n8n + chart: n8n + targetRevision: "*" + helm: + valueFiles: + - $values/apps/n8n/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: apps/n8n/manifests + destination: + server: https://kubernetes.default.svc + namespace: n8n + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/apps/nessus.yaml b/argocd-apps/apps/nessus.yaml new file mode 100644 index 0000000..400359c --- /dev/null +++ b/argocd-apps/apps/nessus.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: nessus + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "51" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: applications + source: + repoURL: + targetRevision: main + path: apps/nessus/manifests + destination: + server: https://kubernetes.default.svc + namespace: nessus + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/apps/nextcloud.yaml b/argocd-apps/apps/nextcloud.yaml new file mode 100644 index 0000000..59b7068 --- /dev/null +++ b/argocd-apps/apps/nextcloud.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: nextcloud + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "50" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: applications + sources: + - repoURL: https://nextcloud.github.io/helm/ + chart: nextcloud + targetRevision: "*" + helm: + valueFiles: + - $values/apps/nextcloud/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: apps/nextcloud/manifests + destination: + server: https://kubernetes.default.svc + namespace: nextcloud + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/apps/rancher.yaml b/argocd-apps/apps/rancher.yaml new file mode 100644 index 0000000..980ef95 --- /dev/null +++ b/argocd-apps/apps/rancher.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: rancher + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "51" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: applications + sources: + - repoURL: https://releases.rancher.com/server-charts/stable + chart: rancher + targetRevision: 2.13.2 + helm: + valueFiles: + - $values/apps/rancher/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: apps/rancher/manifests + destination: + server: https://kubernetes.default.svc + namespace: cattle-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/apps/teslamate.yaml b/argocd-apps/apps/teslamate.yaml new file mode 100644 index 0000000..72566d5 --- /dev/null +++ b/argocd-apps/apps/teslamate.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: teslamate + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "50" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: applications + sources: + - repoURL: https://geek-cookbook.github.io/charts/ + chart: teslamate + targetRevision: 7.2.0 + helm: + valueFiles: + - $values/apps/teslamate/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: apps/teslamate/manifests + destination: + server: https://kubernetes.default.svc + namespace: teslamate + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/hook-rbac.yaml b/argocd-apps/hook-rbac.yaml new file mode 100644 index 0000000..21262b5 --- /dev/null +++ b/argocd-apps/hook-rbac.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: argocd-hook-sa + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-5" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: argocd-hook-sa-admin + annotations: + argocd.argoproj.io/sync-wave: "-5" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: argocd-hook-sa + namespace: argocd diff --git a/argocd-apps/infrastructure/authentik.yaml b/argocd-apps/infrastructure/authentik.yaml new file mode 100644 index 0000000..b63357d --- /dev/null +++ b/argocd-apps/infrastructure/authentik.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: authentik + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "23" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://charts.goauthentik.io + chart: authentik + targetRevision: 2026.2.1 + helm: + valueFiles: + - $values/infrastructure/authentik/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: infrastructure/authentik/manifests + destination: + server: https://kubernetes.default.svc + namespace: authentik + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/cert-manager.yaml b/argocd-apps/infrastructure/cert-manager.yaml new file mode 100644 index 0000000..1f4875f --- /dev/null +++ b/argocd-apps/infrastructure/cert-manager.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: cert-manager + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "2" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://charts.jetstack.io + chart: cert-manager + targetRevision: v1.13.2 + helm: + valueFiles: + - $values/infrastructure/cert-manager/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: infrastructure/cert-manager/manifests + destination: + server: https://kubernetes.default.svc + namespace: cert-manager + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/checkov-scanner.yaml b/argocd-apps/infrastructure/checkov-scanner.yaml new file mode 100644 index 0000000..c6f1fb9 --- /dev/null +++ b/argocd-apps/infrastructure/checkov-scanner.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: checkov-scanner + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "43" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + source: + repoURL: + targetRevision: main + path: infrastructure/checkov/manifests + destination: + server: https://kubernetes.default.svc + namespace: checkov + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/cilium.yaml b/argocd-apps/infrastructure/cilium.yaml new file mode 100644 index 0000000..772025e --- /dev/null +++ b/argocd-apps/infrastructure/cilium.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: cilium + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "1" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://helm.cilium.io + chart: cilium + targetRevision: 1.17.1 + helm: + valueFiles: + - $values/infrastructure/cilium/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: infrastructure/cilium/manifests + destination: + server: https://kubernetes.default.svc + namespace: kube-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/cnpg.yaml b/argocd-apps/infrastructure/cnpg.yaml new file mode 100644 index 0000000..3bd7f01 --- /dev/null +++ b/argocd-apps/infrastructure/cnpg.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: cnpg + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "12" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + source: + repoURL: https://cloudnative-pg.github.io/charts + chart: cloudnative-pg + targetRevision: "*" + destination: + server: https://kubernetes.default.svc + namespace: cnpg-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/crowdsec.yaml b/argocd-apps/infrastructure/crowdsec.yaml new file mode 100644 index 0000000..2ad6095 --- /dev/null +++ b/argocd-apps/infrastructure/crowdsec.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: crowdsec + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "22" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://crowdsecurity.github.io/helm-charts + chart: crowdsec + targetRevision: "*" + helm: + valueFiles: + - $values/infrastructure/crowdsec/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: infrastructure/crowdsec/manifests + destination: + server: https://kubernetes.default.svc + namespace: crowdsec + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/envoy-edge.yaml b/argocd-apps/infrastructure/envoy-edge.yaml new file mode 100644 index 0000000..3df0189 --- /dev/null +++ b/argocd-apps/infrastructure/envoy-edge.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: envoy-edge + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "7" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + source: + repoURL: + targetRevision: main + path: infrastructure/envoy-gateway/manifests + destination: + server: https://kubernetes.default.svc + namespace: gateway + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/envoy-gateway.yaml b/argocd-apps/infrastructure/envoy-gateway.yaml new file mode 100644 index 0000000..1e89bf5 --- /dev/null +++ b/argocd-apps/infrastructure/envoy-gateway.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: envoy-gateway + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "6" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://gateway.envoyproxy.io/charts + chart: gateway-helm + targetRevision: v1.6.3 + helm: + valueFiles: + - $values/infrastructure/envoy-gateway/values.yaml + - repoURL: + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: envoy-gateway-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/external-dns.yaml b/argocd-apps/infrastructure/external-dns.yaml new file mode 100644 index 0000000..d4a2b6e --- /dev/null +++ b/argocd-apps/infrastructure/external-dns.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: external-dns + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "30" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://kubernetes-sigs.github.io/external-dns/ + chart: external-dns + targetRevision: 1.20.0 + helm: + valueFiles: + - $values/infrastructure/external-dns/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: infrastructure/external-dns/manifests + destination: + server: https://kubernetes.default.svc + namespace: external-dns + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/external-secrets.yaml b/argocd-apps/infrastructure/external-secrets.yaml new file mode 100644 index 0000000..83418f8 --- /dev/null +++ b/argocd-apps/infrastructure/external-secrets.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: external-secrets + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "21" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://charts.external-secrets.io + chart: external-secrets + targetRevision: 0.20.4 + helm: + valueFiles: + - $values/infrastructure/external-secrets/values.yaml + - repoURL: + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: external-secrets + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/grafana.yaml b/argocd-apps/infrastructure/grafana.yaml new file mode 100644 index 0000000..096c64c --- /dev/null +++ b/argocd-apps/infrastructure/grafana.yaml @@ -0,0 +1,37 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: grafana + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "41" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://grafana.github.io/helm-charts + chart: grafana + targetRevision: "*" + helm: + valueFiles: + - $values/infrastructure/grafana/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: infrastructure/grafana/manifests + - repoURL: + targetRevision: main + path: infrastructure/grafana/dashboards + destination: + server: https://kubernetes.default.svc + namespace: grafana + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/istio-base.yaml b/argocd-apps/infrastructure/istio-base.yaml new file mode 100644 index 0000000..bcfab72 --- /dev/null +++ b/argocd-apps/infrastructure/istio-base.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: istio-base + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "2" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + source: + repoURL: https://istio-release.storage.googleapis.com/charts + chart: base + targetRevision: 1.28.3 + destination: + server: https://kubernetes.default.svc + namespace: istio-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/istio-cni.yaml b/argocd-apps/infrastructure/istio-cni.yaml new file mode 100644 index 0000000..67cd52b --- /dev/null +++ b/argocd-apps/infrastructure/istio-cni.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: istio-cni + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "3" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://istio-release.storage.googleapis.com/charts + chart: cni + targetRevision: 1.29.0 + helm: + valueFiles: + - $values/infrastructure/istio/cni-values.yaml + - repoURL: + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: kube-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/istio-edge.yaml b/argocd-apps/infrastructure/istio-edge.yaml new file mode 100644 index 0000000..4306be8 --- /dev/null +++ b/argocd-apps/infrastructure/istio-edge.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: istio-edge + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "6" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + source: + repoURL: + targetRevision: main + path: infrastructure/istio/manifests/edge + destination: + server: https://kubernetes.default.svc + namespace: istio-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/istio-ingressgateway.yaml b/argocd-apps/infrastructure/istio-ingressgateway.yaml new file mode 100644 index 0000000..ad547c2 --- /dev/null +++ b/argocd-apps/infrastructure/istio-ingressgateway.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: istio-ingressgateway + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "5" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://istio-release.storage.googleapis.com/charts + chart: gateway + targetRevision: 1.29.0 + helm: + valueFiles: + - $values/infrastructure/istio/ingressgateway-values.yaml + - repoURL: + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: istio-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/istio-mesh-config.yaml b/argocd-apps/infrastructure/istio-mesh-config.yaml new file mode 100644 index 0000000..e38e9be --- /dev/null +++ b/argocd-apps/infrastructure/istio-mesh-config.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: istio-mesh-config + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "42" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + source: + repoURL: + targetRevision: main + path: infrastructure/istio/manifests/mesh + destination: + server: https://kubernetes.default.svc + namespace: istio-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/istiod.yaml b/argocd-apps/infrastructure/istiod.yaml new file mode 100644 index 0000000..d3ccf26 --- /dev/null +++ b/argocd-apps/infrastructure/istiod.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: istiod + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "4" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://istio-release.storage.googleapis.com/charts + chart: istiod + targetRevision: 1.29.0 + helm: + valueFiles: + - $values/infrastructure/istio/istiod-values.yaml + - repoURL: + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: istio-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/kiali-operator.yaml b/argocd-apps/infrastructure/kiali-operator.yaml new file mode 100644 index 0000000..c502110 --- /dev/null +++ b/argocd-apps/infrastructure/kiali-operator.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: kiali-operator + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "42" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://kiali.org/helm-charts + chart: kiali-operator + targetRevision: "*" + helm: + valueFiles: + - $values/infrastructure/kiali/operator-values.yaml + - repoURL: + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: kiali-operator + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/kiali.yaml b/argocd-apps/infrastructure/kiali.yaml new file mode 100644 index 0000000..97f9c65 --- /dev/null +++ b/argocd-apps/infrastructure/kiali.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: kiali + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "43" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + source: + repoURL: + targetRevision: main + path: infrastructure/kiali/manifests + destination: + server: https://kubernetes.default.svc + namespace: kiali-operator + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/kube-prometheus-stack.yaml b/argocd-apps/infrastructure/kube-prometheus-stack.yaml new file mode 100644 index 0000000..c200ed0 --- /dev/null +++ b/argocd-apps/infrastructure/kube-prometheus-stack.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: kube-prometheus-stack + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "40" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://prometheus-community.github.io/helm-charts + chart: kube-prometheus-stack + targetRevision: "*" + helm: + valueFiles: + - $values/infrastructure/prometheus/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: infrastructure/prometheus/manifests + destination: + server: https://kubernetes.default.svc + namespace: prometheus + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/kyverno.yaml b/argocd-apps/infrastructure/kyverno.yaml new file mode 100644 index 0000000..45139d1 --- /dev/null +++ b/argocd-apps/infrastructure/kyverno.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: kyverno + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "22" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://kyverno.github.io/kyverno/ + chart: kyverno + targetRevision: "*" + helm: + valueFiles: + - $values/infrastructure/kyverno/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: infrastructure/kyverno/policies + destination: + server: https://kubernetes.default.svc + namespace: kyverno + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/mariadb-operator.yaml b/argocd-apps/infrastructure/mariadb-operator.yaml new file mode 100644 index 0000000..1c81493 --- /dev/null +++ b/argocd-apps/infrastructure/mariadb-operator.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: mariadb-operator + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "12" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + source: + repoURL: https://mariadb-operator.github.io/mariadb-operator + chart: mariadb-operator + targetRevision: 0.31.0 + destination: + server: https://kubernetes.default.svc + namespace: mariadb-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/metrics-server.yaml b/argocd-apps/infrastructure/metrics-server.yaml new file mode 100644 index 0000000..f040ac1 --- /dev/null +++ b/argocd-apps/infrastructure/metrics-server.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: metrics-server + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "2" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://kubernetes-sigs.github.io/metrics-server/ + chart: metrics-server + targetRevision: 3.12.1 + helm: + valueFiles: + - $values/infrastructure/metrics-server/values.yaml + - repoURL: + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: kube-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/mosquitto.yaml b/argocd-apps/infrastructure/mosquitto.yaml new file mode 100644 index 0000000..821b2c7 --- /dev/null +++ b/argocd-apps/infrastructure/mosquitto.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: mosquitto + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "31" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + source: + repoURL: + targetRevision: main + path: infrastructure/mosquitto/manifests + destination: + server: https://kubernetes.default.svc + namespace: mqtt + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/netbird-operator.yaml b/argocd-apps/infrastructure/netbird-operator.yaml new file mode 100644 index 0000000..da0ba73 --- /dev/null +++ b/argocd-apps/infrastructure/netbird-operator.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: netbird-operator + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "31" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://netbirdio.github.io/kubernetes-operator + chart: netbird-operator + targetRevision: "*" + helm: + valueFiles: + - $values/infrastructure/netbird-operator/values.yaml + - repoURL: + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: netbird + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/netbird.yaml b/argocd-apps/infrastructure/netbird.yaml new file mode 100644 index 0000000..f7aaf5e --- /dev/null +++ b/argocd-apps/infrastructure/netbird.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: netbird + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "30" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://netbirdio.github.io/helms + chart: netbird + targetRevision: "*" + helm: + valueFiles: + - $values/infrastructure/netbird/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: infrastructure/netbird/manifests + destination: + server: https://kubernetes.default.svc + namespace: netbird + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/prometheus-crds.yaml b/argocd-apps/infrastructure/prometheus-crds.yaml new file mode 100644 index 0000000..7d29b9e --- /dev/null +++ b/argocd-apps/infrastructure/prometheus-crds.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: prometheus-crds + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-1" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + source: + repoURL: + targetRevision: main + path: charts/prometheus-operator-crds + destination: + server: https://kubernetes.default.svc + namespace: kube-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/redis-operator.yaml b/argocd-apps/infrastructure/redis-operator.yaml new file mode 100644 index 0000000..8969e41 --- /dev/null +++ b/argocd-apps/infrastructure/redis-operator.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: redis-operator + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "12" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + source: + repoURL: https://ot-container-kit.github.io/helm-charts/ + chart: redis-operator + targetRevision: 0.15.0 + destination: + server: https://kubernetes.default.svc + namespace: redis-operator + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/reloader.yaml b/argocd-apps/infrastructure/reloader.yaml new file mode 100644 index 0000000..a84ceb0 --- /dev/null +++ b/argocd-apps/infrastructure/reloader.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: reloader + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "22" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://stakater.github.io/stakater-charts + chart: reloader + targetRevision: 1.2.1 + helm: + valueFiles: + - $values/infrastructure/reloader/values.yaml + - repoURL: + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: reloader + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/rook-ceph-cluster.yaml b/argocd-apps/infrastructure/rook-ceph-cluster.yaml new file mode 100644 index 0000000..a0de689 --- /dev/null +++ b/argocd-apps/infrastructure/rook-ceph-cluster.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: rook-ceph-cluster + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "11" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://charts.rook.io/release + chart: rook-ceph-cluster + targetRevision: 1.15.6 + helm: + valueFiles: + - $values/infrastructure/rook-ceph/cluster-values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: infrastructure/rook-ceph/manifests + destination: + server: https://kubernetes.default.svc + namespace: rook-ceph + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/rook-ceph-operator.yaml b/argocd-apps/infrastructure/rook-ceph-operator.yaml new file mode 100644 index 0000000..5c227ee --- /dev/null +++ b/argocd-apps/infrastructure/rook-ceph-operator.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: rook-ceph-operator + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "10" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://charts.rook.io/release + chart: rook-ceph + targetRevision: v1.19.0 + helm: + valueFiles: + - $values/infrastructure/rook-ceph/operator-values.yaml + - repoURL: + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: rook-ceph + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/scylla-manager.yaml b/argocd-apps/infrastructure/scylla-manager.yaml new file mode 100644 index 0000000..6438892 --- /dev/null +++ b/argocd-apps/infrastructure/scylla-manager.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: scylla-manager + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "14" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://scylla-operator-charts.storage.googleapis.com/stable + chart: scylla-manager + targetRevision: v1.19.1 + helm: + valueFiles: + - $values/infrastructure/scylla/manager-values.yaml + - repoURL: + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: scylla-manager + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/scylla-operator.yaml b/argocd-apps/infrastructure/scylla-operator.yaml new file mode 100644 index 0000000..553972f --- /dev/null +++ b/argocd-apps/infrastructure/scylla-operator.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: scylla-operator + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "13" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://scylla-operator-charts.storage.googleapis.com/stable + chart: scylla-operator + targetRevision: v1.19.1 + helm: + valueFiles: + - $values/infrastructure/scylla/operator-values.yaml + - repoURL: + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: scylla-operator + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/tetragon.yaml b/argocd-apps/infrastructure/tetragon.yaml new file mode 100644 index 0000000..5cb5243 --- /dev/null +++ b/argocd-apps/infrastructure/tetragon.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: tetragon + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "22" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://helm.cilium.io + chart: tetragon + targetRevision: "*" + helm: + valueFiles: + - $values/infrastructure/tetragon/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: infrastructure/tetragon/tracing-policies + destination: + server: https://kubernetes.default.svc + namespace: kube-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/unpoller.yaml b/argocd-apps/infrastructure/unpoller.yaml new file mode 100644 index 0000000..81a6aa7 --- /dev/null +++ b/argocd-apps/infrastructure/unpoller.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: unpoller + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "41" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + source: + repoURL: + targetRevision: main + path: infrastructure/unpoller/manifests + destination: + server: https://kubernetes.default.svc + namespace: unpoller + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/vault.yaml b/argocd-apps/infrastructure/vault.yaml new file mode 100644 index 0000000..5bad21f --- /dev/null +++ b/argocd-apps/infrastructure/vault.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vault + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "20" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + sources: + - repoURL: https://helm.releases.hashicorp.com + chart: vault + targetRevision: 0.32.0 + helm: + valueFiles: + - $values/infrastructure/vault/values.yaml + - repoURL: + targetRevision: main + ref: values + - repoURL: + targetRevision: main + path: infrastructure/vault/manifests + destination: + server: https://kubernetes.default.svc + namespace: vault + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/infrastructure/ztunnel.yaml b/argocd-apps/infrastructure/ztunnel.yaml new file mode 100644 index 0000000..6ab31e3 --- /dev/null +++ b/argocd-apps/infrastructure/ztunnel.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: ztunnel + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "5" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: infrastructure + source: + repoURL: https://istio-release.storage.googleapis.com/charts + chart: ztunnel + targetRevision: 1.29.0 + destination: + server: https://kubernetes.default.svc + namespace: istio-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd-apps/projects.yaml b/argocd-apps/projects.yaml new file mode 100644 index 0000000..064dd6f --- /dev/null +++ b/argocd-apps/projects.yaml @@ -0,0 +1,57 @@ +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: infrastructure + namespace: argocd +spec: + description: Infrastructure and platform services + sourceRepos: + - '*' + destinations: + - namespace: '*' + server: https://kubernetes.default.svc + clusterResourceWhitelist: + - group: '*' + kind: '*' + namespaceResourceWhitelist: + - group: '*' + kind: '*' +--- +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: applications + namespace: argocd +spec: + description: User-facing applications + sourceRepos: + - '*' + destinations: + - namespace: gitlab + server: https://kubernetes.default.svc + - namespace: argocd + server: https://kubernetes.default.svc + - namespace: n8n + server: https://kubernetes.default.svc + - namespace: nextcloud + server: https://kubernetes.default.svc + - namespace: teslamate + server: https://kubernetes.default.svc + - namespace: home-assistant + server: https://kubernetes.default.svc + - namespace: frigate + server: https://kubernetes.default.svc + - namespace: guacamole + server: https://kubernetes.default.svc + - namespace: cattle-system + server: https://kubernetes.default.svc + - namespace: gateway + server: https://kubernetes.default.svc + - namespace: istio-system + server: https://kubernetes.default.svc + clusterResourceWhitelist: + - group: '*' + kind: '*' + namespaceResourceWhitelist: + - group: '*' + kind: '*' diff --git a/bootstrap.yaml b/bootstrap.yaml new file mode 100644 index 0000000..554565b --- /dev/null +++ b/bootstrap.yaml @@ -0,0 +1,24 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: root + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: + targetRevision: main + path: argocd-apps + directory: + recurse: true + destination: + server: https://kubernetes.default.svc + namespace: argocd + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/charts/empty-chart/Chart.yaml b/charts/empty-chart/Chart.yaml new file mode 100644 index 0000000..78d75c0 --- /dev/null +++ b/charts/empty-chart/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: empty-chart +description: Empty chart used for raw manifest applications +type: application +version: 1.0.0 diff --git a/charts/empty-chart/templates/.gitkeep b/charts/empty-chart/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/charts/prometheus-operator-crds/Chart.yaml b/charts/prometheus-operator-crds/Chart.yaml new file mode 100644 index 0000000..019053f --- /dev/null +++ b/charts/prometheus-operator-crds/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: prometheus-operator-crds +description: Prometheus Operator CRDs installation +type: application +version: 1.0.0 +appVersion: "v0.76.0" diff --git a/charts/prometheus-operator-crds/templates/job.yaml b/charts/prometheus-operator-crds/templates/job.yaml new file mode 100644 index 0000000..d997629 --- /dev/null +++ b/charts/prometheus-operator-crds/templates/job.yaml @@ -0,0 +1,79 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: install-prometheus-crds + namespace: kube-system + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + template: + metadata: + name: install-prometheus-crds + spec: + restartPolicy: OnFailure + serviceAccountName: prometheus-crd-installer + containers: + - name: install-crds + image: docker.io/alpine/k8s:1.32.13 + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + volumeMounts: + - name: tmp + mountPath: /tmp + command: + - /bin/sh + - -c + - | + set -e + echo "Installing Prometheus Operator CRDs..." + + PROM_OPERATOR_VERSION="v0.76.0" + + kubectl apply --server-side -f "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/${PROM_OPERATOR_VERSION}/example/prometheus-operator-crd/monitoring.coreos.com_servicemonitors.yaml" + kubectl apply --server-side -f "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/${PROM_OPERATOR_VERSION}/example/prometheus-operator-crd/monitoring.coreos.com_podmonitors.yaml" + kubectl apply --server-side -f "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/${PROM_OPERATOR_VERSION}/example/prometheus-operator-crd/monitoring.coreos.com_prometheuses.yaml" + kubectl apply --server-side -f "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/${PROM_OPERATOR_VERSION}/example/prometheus-operator-crd/monitoring.coreos.com_prometheusrules.yaml" + kubectl apply --server-side -f "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/${PROM_OPERATOR_VERSION}/example/prometheus-operator-crd/monitoring.coreos.com_alertmanagers.yaml" + kubectl apply --server-side -f "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/${PROM_OPERATOR_VERSION}/example/prometheus-operator-crd/monitoring.coreos.com_alertmanagerconfigs.yaml" + + echo "Prometheus Operator CRDs installed successfully!" + volumes: + - name: tmp + emptyDir: {} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: prometheus-crd-installer + namespace: kube-system + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: prometheus-crd-installer + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: prometheus-crd-installer + namespace: kube-system diff --git a/environments/default.yaml b/environments/default.yaml new file mode 100644 index 0000000..df1272f --- /dev/null +++ b/environments/default.yaml @@ -0,0 +1,32 @@ +# environments/default.yaml +# Default environment configuration + +# Global settings +global: + labels: + bootstrap: "homelabv4" + managed-by: "argocd" + annotations: + config.kubernetes.io/origin: "argocd-gitops" + +# Domain configuration +domain: + base: "kube.huskypup.net" + ingress: + class: "istio" + +# Storage configuration +storage: + defaultClass: "rook-ceph-block" + backupClass: "rook-ceph-block" + +# Monitoring configuration +monitoring: + enabled: true + retention: "30d" + +# Security configuration +security: + certManager: + email: "admin@kube.huskypup.net" + issuer: "letsencrypt-prod" diff --git a/hooks/rbac/hook-sa.yaml b/hooks/rbac/hook-sa.yaml new file mode 100644 index 0000000..7410e3e --- /dev/null +++ b/hooks/rbac/hook-sa.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: argocd-hook-sa + namespace: argocd +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: argocd-hook-sa-admin +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: argocd-hook-sa + namespace: argocd diff --git a/hooks/scripts/vault-init-configmap.yaml b/hooks/scripts/vault-init-configmap.yaml new file mode 100644 index 0000000..6c8b83b --- /dev/null +++ b/hooks/scripts/vault-init-configmap.yaml @@ -0,0 +1,140 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: vault-init-script + namespace: vault +data: + vault-init-and-store.sh: | + #!/bin/bash + set -euo pipefail + + # === Vault Auto-Init and Store Script (ArgoCD Job Version) === + # Adapted from Homelabv5/scripts/vault-init-and-store.sh + # Runs inside a Job pod with kubectl access via ServiceAccount. + # Uses kubectl exec to interact with the vault-0 pod directly + # instead of port-forwarding + local vault CLI. + + VAULT_NS="${VAULT_NS:-vault}" + SECRET_NAME="${SECRET_NAME:-vault-init-keys}" + KV_MOUNT="${KV_MOUNT:-secret}" + JOB_NS_FOR_ROLE="${JOB_NS_FOR_ROLE:-auth-proxy}" + JOB_SA_FOR_ROLE="${JOB_SA_FOR_ROLE:-oauth2-bootstrap}" + POLICY_NAME="${POLICY_NAME:-oauth2-writer}" + ROLE_NAME="${ROLE_NAME:-eso-writer}" + + VAULT_POD="vault-0" + + # Helper: run vault CLI inside the vault pod + vault_exec() { + kubectl -n "$VAULT_NS" exec "$VAULT_POD" -- env VAULT_ADDR=http://127.0.0.1:8200 vault "$@" + } + + # --- Wait for vault-0 pod to be ready --- + echo "==> Waiting for vault-0 pod to be running..." + for i in $(seq 1 60); do + PHASE=$(kubectl -n "$VAULT_NS" get pod "$VAULT_POD" -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$PHASE" = "Running" ]; then + echo " vault-0 is running" + break + fi + echo " waiting for vault-0... (attempt $i/60, phase=$PHASE)" + sleep 5 + done + + # Give Vault a moment to start its listener + sleep 5 + + # --- Check init/seal status --- + echo "==> Checking Vault status..." + status_json="$(vault_exec status -format=json 2>/dev/null || echo '{}')" + initialized="$(echo "$status_json" | grep -o '"initialized":[a-z]*' | cut -d: -f2 || echo "")" + sealed="$(echo "$status_json" | grep -o '"sealed":[a-z]*' | cut -d: -f2 || echo "")" + + root_token="" + unseal_key="" + + # Pull existing secret if present + if kubectl -n "$VAULT_NS" get secret "$SECRET_NAME" >/dev/null 2>&1; then + root_token="$(kubectl -n "$VAULT_NS" get secret "$SECRET_NAME" -o jsonpath='{.data.VAULT_ROOT_TOKEN}' | base64 -d || true)" + unseal_key="$(kubectl -n "$VAULT_NS" get secret "$SECRET_NAME" -o jsonpath='{.data.VAULT_UNSEAL_KEY}' | base64 -d || true)" + fi + + # Initialize if needed + if [ "$initialized" != "true" ]; then + echo "==> Vault not initialized; initializing..." + init_json="$(vault_exec operator init -key-shares=1 -key-threshold=1 -format=json)" + root_token="$(echo "$init_json" | grep -o '"root_token":"[^"]*"' | cut -d'"' -f4)" + unseal_key="$(echo "$init_json" | grep -o '"unseal_keys_b64":\["[^"]*"\]' | grep -o '\["[^"]*"\]' | tr -d '[]"')" + sealed="true" + + echo "==> Storing root token & unseal key in Secret ${VAULT_NS}/${SECRET_NAME}" + kubectl -n "$VAULT_NS" create secret generic "$SECRET_NAME" \ + --from-literal=VAULT_ROOT_TOKEN="$root_token" \ + --from-literal=VAULT_UNSEAL_KEY="$unseal_key" \ + --dry-run=client -o yaml | kubectl apply -f - + else + echo "==> Vault already initialized." + fi + + # Unseal if needed + if [ "$sealed" = "true" ]; then + if [ -z "$unseal_key" ]; then + echo "ERROR: Vault is sealed and no unseal key available" + exit 1 + fi + echo "==> Unsealing..." + vault_exec operator unseal "$unseal_key" >/dev/null + fi + + # Login + if [ -z "$root_token" ]; then + echo "==> Reading root token from Secret..." + root_token="$(kubectl -n "$VAULT_NS" get secret "$SECRET_NAME" -o jsonpath='{.data.VAULT_ROOT_TOKEN}' | base64 -d)" + fi + vault_exec login "$root_token" >/dev/null + + # Ensure KV v2 enabled + if ! vault_exec secrets list -format=json 2>/dev/null | grep -q "\"${KV_MOUNT}/\""; then + echo "==> Enabling KV v2 at ${KV_MOUNT}/" + vault_exec secrets enable -path="$KV_MOUNT" -version=2 kv >/dev/null + fi + + # Configure Kubernetes auth with a reviewer token from a local SA + echo "==> Ensuring reviewer SA + binding" + kubectl -n "$VAULT_NS" get sa vault-auth >/dev/null 2>&1 || kubectl -n "$VAULT_NS" create sa vault-auth + kubectl get clusterrolebinding vault-auth-delegator >/dev/null 2>&1 || \ + kubectl create clusterrolebinding vault-auth-delegator \ + --clusterrole=system:auth-delegator \ + --serviceaccount="${VAULT_NS}:vault-auth" + + reviewer_jwt="$(kubectl -n "$VAULT_NS" create token vault-auth)" + kube_ca="$(kubectl -n kube-system get configmap kube-root-ca.crt -o jsonpath='{.data.ca\.crt}')" + kube_host="https://kubernetes.default.svc:443" + + vault_exec auth enable kubernetes >/dev/null 2>&1 || true + vault_exec write auth/kubernetes/config \ + token_reviewer_jwt="$reviewer_jwt" \ + kubernetes_host="$kube_host" \ + kubernetes_ca_cert="$kube_ca" >/dev/null + + # Policy + role for ESO/oauth2 job + vault_exec policy write "$POLICY_NAME" - >/dev/null <<'HCL' + path "secret/data/*" { + capabilities = ["create", "update", "read", "list"] + } + + path "secret/metadata/*" { + capabilities = ["create", "update", "read", "list"] + } + HCL + + vault_exec write "auth/kubernetes/role/${ROLE_NAME}" \ + bound_service_account_names="${JOB_SA_FOR_ROLE},external-secrets" \ + bound_service_account_namespaces="${JOB_NS_FOR_ROLE},external-secrets" \ + policies="${POLICY_NAME}" \ + ttl="24h" >/dev/null + + echo "==> Done." + echo "K8s Secret with init creds: ${VAULT_NS}/${SECRET_NAME}" + echo "IMPORTANT: back these up securely and delete the Secret when you're comfortable:" + echo " kubectl -n ${VAULT_NS} delete secret ${SECRET_NAME}" diff --git a/infrastructure/authentik/argocd-blueprint.yaml b/infrastructure/authentik/argocd-blueprint.yaml new file mode 100644 index 0000000..90f5595 --- /dev/null +++ b/infrastructure/authentik/argocd-blueprint.yaml @@ -0,0 +1,49 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-argocd + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + argocd.yaml: |- + version: 1 + metadata: + name: argocd-oidc + entries: + - model: authentik_providers_oauth2.oauth2provider + id: argocd-provider + state: present + identifiers: + name: ArgoCD + attrs: + name: ArgoCD + client_id: 3791058a7de278045e5ec36be29f7a33d635119b8c786f20c86b185139c28841 + client_secret: f787416822f5bf4c46fe9170e63d1bf9520bab7703d53341aaaec107c8c4ba253d96c30b70d23a1c0b5a401ae1ae4bfb5db06092d338e29e0b0f04cd29bc0b36 + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + redirect_uris: + - url: "https://argocd.kube.huskypup.net/auth/callback" + matching_mode: strict + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + client_type: confidential + access_code_validity: "minutes=10" + access_token_validity: "hours=1" + refresh_token_validity: "days=30" + signing_key: !Find [authentik_crypto.certificatekeypair, [name, "authentik Internal JWT Certificate"]] + + - model: authentik_core.application + id: argocd-application + state: present + identifiers: + slug: argocd + attrs: + name: ArgoCD + slug: argocd + policy_engine_mode: any + provider: !KeyOf argocd-provider diff --git a/infrastructure/authentik/ceph-blueprint.yaml b/infrastructure/authentik/ceph-blueprint.yaml new file mode 100644 index 0000000..ab07ca4 --- /dev/null +++ b/infrastructure/authentik/ceph-blueprint.yaml @@ -0,0 +1,63 @@ +--- +# Ceph Dashboard SAML2 Provider Blueprint for Authentik +# Creates a SAML2 provider for Ceph Dashboard native SSO +# Ceph expects: +# - Audience: https://ceph.kube.huskypup.net/auth/saml2/metadata +# - ACS URL: https://ceph.kube.huskypup.net/auth/saml2 +# - Username attribute: "username" +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-ceph + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + ceph.yaml: |- + version: 1 + metadata: + name: ceph-dashboard-saml + entries: + # SAML Property Mapping for username attribute (required by Ceph) + - model: authentik_providers_saml.samlpropertymapping + id: ceph-username-mapping + state: present + identifiers: + managed: goauthentik.io/providers/saml/ceph-username + attrs: + name: "Ceph Dashboard SAML: Username" + saml_name: "username" + expression: "return request.user.username" + + # SAML2 Provider for Ceph Dashboard + - model: authentik_providers_saml.samlprovider + id: ceph-dashboard-saml-provider + state: present + identifiers: + name: ceph-dashboard-saml + attrs: + name: ceph-dashboard-saml + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + acs_url: "https://ceph.kube.huskypup.net/auth/saml2" + audience: "https://ceph.kube.huskypup.net/auth/saml2/metadata" + sp_binding: post + default_name_id_policy: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + signing_kp: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] + sign_response: true + sign_assertion: true + property_mappings: + - !KeyOf ceph-username-mapping + + # Application entry + - model: authentik_core.application + id: ceph-dashboard-application + state: present + identifiers: + slug: ceph-dashboard + attrs: + name: Ceph Dashboard + slug: ceph-dashboard + policy_engine_mode: any + provider: !KeyOf ceph-dashboard-saml-provider + meta_launch_url: "https://ceph.kube.huskypup.net" diff --git a/infrastructure/authentik/ceph-oidc-blueprint.yaml b/infrastructure/authentik/ceph-oidc-blueprint.yaml new file mode 100644 index 0000000..a84bcc2 --- /dev/null +++ b/infrastructure/authentik/ceph-oidc-blueprint.yaml @@ -0,0 +1,58 @@ +--- +# DEPRECATED: Ceph Dashboard OIDC Provider Blueprint +# This blueprint was used with oauth2-proxy which has been removed. +# Ceph Dashboard auth is now handled by Istio ext_authz + Authentik forward-auth. +# This file is NOT mounted in Authentik (not in configMaps list). +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-ceph-oidc + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + ceph-oidc.yaml: |- + version: 1 + metadata: + name: ceph-dashboard-oidc + entries: + # OAuth2/OIDC Provider for Ceph Dashboard (via oauth2-proxy) + - model: authentik_providers_oauth2.oauth2provider + id: ceph-dashboard-oidc-provider + state: present + identifiers: + name: ceph-dashboard + attrs: + name: ceph-dashboard + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + client_type: confidential + client_id: ceph-dashboard + client_secret: ceph-dashboard-oidc-secret-homelab-2026 + redirect_uris: | + https://ceph.kube.huskypup.net/oauth2/callback + signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] + access_code_validity: minutes=1 + access_token_validity: minutes=5 + refresh_token_validity: days=30 + include_claims_in_id_token: true + sub_mode: user_username + issuer_mode: per_provider + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]] + + # Application entry - update existing or create new + - model: authentik_core.application + id: ceph-dashboard-application + state: present + identifiers: + slug: ceph-dashboard + attrs: + name: Ceph Dashboard + slug: ceph-dashboard + policy_engine_mode: any + provider: !KeyOf ceph-dashboard-oidc-provider + meta_launch_url: "https://ceph.kube.huskypup.net" + meta_icon: "https://ceph.io/assets/favicons/favicon.ico" diff --git a/infrastructure/authentik/cnpg-cluster.yaml b/infrastructure/authentik/cnpg-cluster.yaml new file mode 100644 index 0000000..2e9b1c4 --- /dev/null +++ b/infrastructure/authentik/cnpg-cluster.yaml @@ -0,0 +1,53 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: pg-authentik + namespace: authentik +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:16 + instances: 2 + + # Resource limits to prevent OOM + resources: + requests: + memory: "512Mi" + cpu: "25m" + limits: + memory: "2Gi" + cpu: "250m" + + # Spread replicas across different nodes + affinity: + topologyKey: kubernetes.io/hostname + + storage: + size: 5Gi + storageClass: rook-ceph-block + + primaryUpdateStrategy: unsupervised + + # PostgreSQL configuration for better performance + postgresql: + parameters: + max_connections: "200" + shared_buffers: "512MB" + effective_cache_size: "1536MB" + maintenance_work_mem: "128MB" + checkpoint_completion_target: "0.9" + wal_buffers: "16MB" + default_statistics_target: "100" + random_page_cost: "1.1" + effective_io_concurrency: "200" + work_mem: "2621kB" + min_wal_size: "1GB" + max_wal_size: "4GB" + + bootstrap: + initdb: + database: app + owner: app + + monitoring: + enablePodMonitor: true + + diff --git a/infrastructure/authentik/cnpg-podmonitor.yaml b/infrastructure/authentik/cnpg-podmonitor.yaml new file mode 100644 index 0000000..e4120fe --- /dev/null +++ b/infrastructure/authentik/cnpg-podmonitor.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: cnpg-postgres + namespace: authentik + labels: + release: prometheus +spec: + selector: + matchLabels: + cnpg.io/cluster: pg-authentik + podMetricsEndpoints: + - port: metrics + path: /metrics + interval: 30s diff --git a/infrastructure/authentik/cnpg-secrets.yaml b/infrastructure/authentik/cnpg-secrets.yaml new file mode 100644 index 0000000..7567644 --- /dev/null +++ b/infrastructure/authentik/cnpg-secrets.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: generators.external-secrets.io/v1alpha1 +kind: Password +metadata: + name: authentik-cnpg-secret + namespace: authentik +spec: + length: 42 + digits: 5 + symbols: 5 + symbolCharacters: "-_$@" + noUpper: false + allowRepeat: true +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: authentik-cnpg-secret + namespace: authentik +spec: + # how often to rotate the DB password + refreshInterval: "24h" + target: + # This will merge the generated password into the existing pg-authentik-app secret + name: pg-authentik-app + creationPolicy: Merge + template: + metadata: + labels: + cnpg.io/reload: "true" + data: + # Override the password field with our ESO-generated password + password: "{{ .password }}" + dataFrom: + - sourceRef: + generatorRef: + apiVersion: generators.external-secrets.io/v1alpha1 + kind: Password + name: authentik-cnpg-secret diff --git a/infrastructure/authentik/forward-auth-blueprint.yaml b/infrastructure/authentik/forward-auth-blueprint.yaml new file mode 100644 index 0000000..74a63ed --- /dev/null +++ b/infrastructure/authentik/forward-auth-blueprint.yaml @@ -0,0 +1,81 @@ +--- +# Forward Auth Proxy Provider Blueprint for Authentik +# +# Replaces oauth2-proxy with Authentik's built-in forward-auth capability. +# Istio's ext_authz filter calls Authentik's /outpost.goauthentik.io/auth/nginx +# endpoint to authenticate requests to protected services. +# +# Services protected by this provider: +# - Prometheus, Ceph Dashboard, TeslaMate, ESPHome, Frigate, Kiali, Nessus, Netbird +# +# Services with native OIDC are NOT included here (they handle auth themselves): +# - GitLab, ArgoCD, Grafana, n8n, Home Assistant, Rancher, Nextcloud +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-forward-auth + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + forward-auth.yaml: |- + version: 1 + metadata: + name: forward-auth-provider + entries: + # ================================================================ + # Proxy Provider - Forward Auth (domain mode) for Istio ext_authz + # ================================================================ + # forward_domain mode handles ALL subdomains of cookie_domain. + # The embedded outpost matches any *.kube.huskypup.net request. + # Auth session is shared across subdomains via a single cookie. + - model: authentik_providers_proxy.proxyprovider + id: forward-auth-provider + state: present + identifiers: + name: Forward Auth (Istio) + attrs: + name: Forward Auth (Istio) + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + mode: forward_domain + external_host: "https://auth.kube.huskypup.net" + cookie_domain: "kube.huskypup.net" + access_token_validity: "hours=1" + refresh_token_validity: "days=30" + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + + # ================================================================ + # Application - Umbrella app for all forward-auth protected services + # ================================================================ + - model: authentik_core.application + id: forward-auth-application + state: present + identifiers: + slug: forward-auth + attrs: + name: Protected Services (Forward Auth) + slug: forward-auth + policy_engine_mode: any + provider: !KeyOf forward-auth-provider + + # ================================================================ + # Outpost - Embedded outpost for forward auth + # Uses Authentik's built-in outpost (no separate deployment needed) + # ================================================================ + - model: authentik_outposts.outpost + id: forward-auth-outpost + state: present + identifiers: + name: "authentik Embedded Outpost" + attrs: + type: proxy + providers: + - !KeyOf forward-auth-provider + config: + authentik_host: "https://auth.kube.huskypup.net/" + authentik_host_insecure: true diff --git a/infrastructure/authentik/gitlab-blueprint.yaml b/infrastructure/authentik/gitlab-blueprint.yaml new file mode 100644 index 0000000..33f966b --- /dev/null +++ b/infrastructure/authentik/gitlab-blueprint.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-gitlab + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + gitlab.yaml: |- + # yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json + version: 1 + metadata: + name: gitlab-oidc + entries: + # OAuth2/OIDC Provider for GitLab + - model: authentik_providers_oauth2.oauth2provider + id: gitlab-provider + state: present + identifiers: + name: GitLab + attrs: + name: GitLab + client_id: 70b9da5c8166cea0ba504a869824020bce6636 + client_secret: 2e9c0c5b7a8969eb782d08bf1bf3d50ad96f8a73d9c18d1e4141816aebe14a622e8064bf5a5bce835d80e89cdc5f4cc6cf4a8c8234556647dc8884888866bd7d + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + redirect_uris: + - url: "https://gitlab.kube.huskypup.net/users/auth/openid_connect/callback" + matching_mode: strict + enabled: true + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + client_type: confidential + access_code_validity: "minutes=10" + access_token_validity: "days=7" + refresh_token_validity: "days=30" + signing_key: !Find [authentik_crypto.certificatekeypair, [name, default]] + + # Application for GitLab + - model: authentik_core.application + id: gitlab-application + state: present + identifiers: + slug: gitlab + attrs: + name: GitLab + slug: gitlab + policy_engine_mode: any + provider: !KeyOf gitlab-provider diff --git a/infrastructure/authentik/gitlab-saml-blueprint.yaml b/infrastructure/authentik/gitlab-saml-blueprint.yaml new file mode 100644 index 0000000..9c24707 --- /dev/null +++ b/infrastructure/authentik/gitlab-saml-blueprint.yaml @@ -0,0 +1,60 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-gitlab-saml + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + gitlab-saml.yaml: |- + # yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json + version: 1 + metadata: + name: gitlab-saml + entries: + # SAML Provider for GitLab + - model: authentik_providers_saml.samlprovider + id: gitlab-saml-provider + state: present + identifiers: + name: GitLab SAML + attrs: + name: GitLab SAML + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + + # Redirect URI from the GitLab SAML docs + # https://gitlab.kube.huskypup.net/users/auth/saml/callback + acs_url: "https://gitlab.kube.huskypup.net/users/auth/saml/callback" + audience: "https://gitlab.kube.huskypup.net" + issuer: "https://gitlab.kube.huskypup.net" + + # SAML-specific settings + service_provider_slo_url: "https://gitlab.kube.huskypup.net/users/auth/saml/slo" + digest_algorithm: "http://www.w3.org/2001/04/xmlenc#sha256" + signature_algorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" + require_signing: true + + # Property mappings for SAML attributes + property_mappings: + - !Find [authentik_providers_saml.samlpropertymapping, [managed, goauthentik.io/providers/saml/name]] + - !Find [authentik_providers_saml.samlpropertymapping, [managed, goauthentik.io/providers/saml/email]] + - !Find [authentik_providers_saml.samlpropertymapping, [managed, goauthentik.io/providers/saml/username]] + + # Signing certificate + signing_key: !Find [authentik_crypto.certificatekeypair, [name, default]] + + # Application for GitLab SAML + - model: authentik_core.application + id: gitlab-saml-application + state: present + identifiers: + slug: gitlab-saml + attrs: + name: GitLab SAML + slug: gitlab-saml + policy_engine_mode: any + provider: !KeyOf gitlab-saml-provider + launch_url: "https://gitlab.kube.huskypup.net" \ No newline at end of file diff --git a/infrastructure/authentik/grafana-blueprint.yaml b/infrastructure/authentik/grafana-blueprint.yaml new file mode 100644 index 0000000..1a4fe69 --- /dev/null +++ b/infrastructure/authentik/grafana-blueprint.yaml @@ -0,0 +1,99 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-grafana + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + grafana.yaml: |- + version: 1 + metadata: + name: grafana-oidc + entries: + # ============================================================================ + # AUTO-CREATE GRAFANA GROUPS + # ============================================================================ + # These groups control access levels in Grafana via OAuth role mapping: + # - Grafana Admins → Admin role (full access) + # - Grafana Editors → Editor role (can edit dashboards) + # - Grafana Viewers → Viewer role (read-only) + # + # AUTOMATIC ADMIN ACCESS: + # - Anyone in "authentik Admins" automatically gets Grafana Admin access + # - No manual configuration needed! + # ============================================================================ + + - model: authentik_core.group + id: grafana-admins-group + state: present + identifiers: + name: Grafana Admins + attrs: + name: Grafana Admins + is_superuser: false + # Note: authentik Admins are automatically granted access via role_attribute_path + # in Grafana configuration (see grafana/values.yaml) + + - model: authentik_core.group + id: grafana-editors-group + state: present + identifiers: + name: Grafana Editors + attrs: + name: Grafana Editors + is_superuser: false + + - model: authentik_core.group + id: grafana-viewers-group + state: present + identifiers: + name: Grafana Viewers + attrs: + name: Grafana Viewers + is_superuser: false + + # ============================================================================ + # GRAFANA OAUTH2 PROVIDER + # ============================================================================ + + - model: authentik_providers_oauth2.oauth2provider + id: grafana-provider + state: present + identifiers: + name: Grafana + attrs: + name: Grafana + client_id: bd03e9139dd2063c6c44c4d2f65f51d69de3ba0b6d6b1b9b41c255d2376d2dcc + client_secret: 6b6f2deecfe8fd56cae9c512cc71eedd463d67ff08f24c816b15b15e78ce36bc4f06a2276c5ffe3f67799d935994a32ed32cdc02ec674f279c82d7cfe3ca05d5 + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + redirect_uris: + - url: "https://grafana.kube.huskypup.net/login/generic_oauth" + matching_mode: strict + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + client_type: confidential + access_code_validity: "minutes=10" + access_token_validity: "hours=1" + refresh_token_validity: "days=30" + signing_key: !Find [authentik_crypto.certificatekeypair, [name, default]] + + # ============================================================================ + # GRAFANA APPLICATION + # ============================================================================ + + - model: authentik_core.application + id: grafana-application + state: present + identifiers: + slug: grafana + attrs: + name: Grafana + slug: grafana + policy_engine_mode: any + provider: !KeyOf grafana-provider diff --git a/infrastructure/authentik/guacamole-blueprint.yaml b/infrastructure/authentik/guacamole-blueprint.yaml new file mode 100644 index 0000000..ab46860 --- /dev/null +++ b/infrastructure/authentik/guacamole-blueprint.yaml @@ -0,0 +1,83 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-guacamole + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + guacamole.yaml: |- + version: 1 + metadata: + name: guacamole-oidc + labels: + blueprints.goauthentik.io/instantiate: "true" + entries: + # ============================================================================ + # GUACAMOLE OAUTH2 PROVIDER + # ============================================================================ + # This creates the OAuth2 provider that Guacamole will use for authentication. + # The client_id and client_secret must match what's configured in Guacamole. + # ============================================================================ + + # ============================================================================ + # GROUPS SCOPE MAPPING + # ============================================================================ + # Custom scope mapping that includes user's group names in the OIDC token. + # ============================================================================ + + - model: authentik_providers_oauth2.scopemapping + id: guacamole-groups-scope + identifiers: + managed: goauthentik.io/providers/oauth2/guacamole-groups + attrs: + name: "Guacamole Groups Scope" + scope_name: groups + expression: | + return { + "groups": [group.name for group in request.user.ak_groups.all()], + } + + - model: authentik_providers_oauth2.oauth2provider + id: guacamole-provider + identifiers: + name: guacamole + attrs: + name: Guacamole + client_id: a55cbe3c6d2ae30c05c86385802ffcfd76a972c22c8d039f000281d4a149fd44 + client_secret: 718d4c4816aa68767a6a0da0fa4099d492473c700ba236cbeb64d294762b9548acda7e716ba11b2ad7729d6a1c16c7890b227e3b2dcfc35fd4af1eb0d6662b0f + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + redirect_uris: + - url: "https://guacamole.kube.huskypup.net/" + matching_mode: strict + - url: "https://guacamole.kube.huskypup.net/guacamole/" + matching_mode: strict + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + - !KeyOf guacamole-groups-scope + client_type: confidential + access_code_validity: "minutes=10" + access_token_validity: "hours=1" + refresh_token_validity: "days=30" + signing_key: !Find [authentik_crypto.certificatekeypair, [name, "Lets Encrypt K8s Cert"]] + + # ============================================================================ + # GUACAMOLE APPLICATION + # ============================================================================ + # This creates the application entry that users will see in Authentik. + # ============================================================================ + + - model: authentik_core.application + id: guacamole-application + identifiers: + slug: guacamole + attrs: + name: Guacamole + slug: guacamole + policy_engine_mode: any + provider: !KeyOf guacamole-provider \ No newline at end of file diff --git a/infrastructure/authentik/home-assistant-blueprint.yaml b/infrastructure/authentik/home-assistant-blueprint.yaml new file mode 100644 index 0000000..8523fae --- /dev/null +++ b/infrastructure/authentik/home-assistant-blueprint.yaml @@ -0,0 +1,68 @@ +--- +# Home Assistant OIDC Provider Blueprint for Authentik +# +# This blueprint creates an OAuth2/OIDC provider in Authentik for Home Assistant +# Home Assistant will use the hass-openid custom integration for direct OIDC auth +# +# AUTOMATIC FEATURES: +# - Users authenticate via Authentik OIDC +# - User accounts auto-created in Home Assistant on first login +# - Usernames sync from Authentik (preferred_username) +# - No OAuth2 Proxy needed - direct integration! +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-home-assistant + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + home-assistant.yaml: |- + version: 1 + metadata: + name: home-assistant-oidc + entries: + # ============================================================================ + # HOME ASSISTANT OAUTH2 PROVIDER + # ============================================================================ + # Manual client_id and client_secret for full automation (no random generation) + + - model: authentik_providers_oauth2.oauth2provider + id: home-assistant-provider + state: present + identifiers: + name: Home Assistant + attrs: + name: Home Assistant + client_id: ha1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab + client_secret: ha_secret_1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678 + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + redirect_uris: + - url: "https://homeassistant.kube.huskypup.net/auth/openid/callback" + matching_mode: strict + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + client_type: confidential + access_code_validity: "minutes=10" + access_token_validity: "hours=1" + refresh_token_validity: "days=30" + signing_key: !Find [authentik_crypto.certificatekeypair, [name, "authentik Internal JWT Certificate"]] + + # ============================================================================ + # HOME ASSISTANT APPLICATION + # ============================================================================ + + - model: authentik_core.application + id: home-assistant-application + state: present + identifiers: + slug: home-assistant + attrs: + name: Home Assistant + slug: home-assistant + policy_engine_mode: any + provider: !KeyOf home-assistant-provider diff --git a/infrastructure/authentik/manifests/envoy-route.yaml b/infrastructure/authentik/manifests/envoy-route.yaml new file mode 100644 index 0000000..922c789 --- /dev/null +++ b/infrastructure/authentik/manifests/envoy-route.yaml @@ -0,0 +1,20 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: authentik + namespace: authentik +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - auth.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: authentik-server + port: 80 diff --git a/infrastructure/authentik/manifests/ext-authz-proxy.yaml b/infrastructure/authentik/manifests/ext-authz-proxy.yaml new file mode 100644 index 0000000..6dcd041 --- /dev/null +++ b/infrastructure/authentik/manifests/ext-authz-proxy.yaml @@ -0,0 +1,179 @@ +# Nginx proxy that rewrites Host header for Authentik ext_authz +# +# Problem: Envoy's ext_authz HTTP filter forwards the ORIGINAL request's Host +# header (e.g., prometheus.kube.huskypup.net) to Authentik. But Authentik's +# embedded outpost only serves /outpost.goauthentik.io/auth/nginx on the +# brand domain (auth.kube.huskypup.net), returning 404 for other hosts. +# +# Solution: This lightweight nginx proxy sits between Istio's ext_authz filter +# and Authentik. It rewrites the Host header to auth.kube.huskypup.net while +# preserving all other headers (X-Forwarded-Host, cookies, etc.) so Authentik +# knows the original service being accessed. +# +# Flow: +# 1. Client → Istio ingress gateway (Host: prometheus.kube.huskypup.net) +# 2. ext_authz filter → this proxy (Host: prometheus.kube.huskypup.net) +# 3. This proxy → Authentik (Host: auth.kube.huskypup.net, X-Forwarded-Host preserved) +# 4. Authentik checks auth, returns 200 (allow) or 302 (redirect to login) +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: ext-authz-proxy-config + namespace: authentik +data: + nginx.conf: | + load_module modules/ngx_http_js_module.so; + + worker_processes 1; + error_log /dev/stderr warn; + pid /tmp/nginx.pid; + + events { + worker_connections 256; + } + + http { + access_log /dev/stdout; + proxy_temp_path /tmp/proxy_temp; + client_body_temp_path /tmp/client_temp; + + js_path /etc/nginx/njs/; + js_import cookie from cookie_domain.js; + + server { + listen 4180; + + location / { + # Rewrite all paths to the exact Authentik traefik handler path. + # Envoy ext_authz appends the original request path to pathPrefix, + # creating paths like /auth/traefik/ which Authentik's Go router + # doesn't match (exact path only, no trailing slash). + rewrite ^ /outpost.goauthentik.io/auth/traefik break; + proxy_pass http://authentik-server.authentik.svc.cluster.local; + proxy_http_version 1.1; + proxy_set_header Host auth.kube.huskypup.net; + proxy_set_header Connection ""; + proxy_pass_request_headers on; + + # Add Domain=kube.huskypup.net to Set-Cookie headers from Authentik. + # The outpost creates session cookies without Domain attribute, causing + # browser to scope them to the original request domain (e.g., frigate.kube.huskypup.net). + # The callback goes to auth.kube.huskypup.net, so the cookie must be domain-scoped. + js_header_filter cookie.addDomain; + } + } + } + + cookie_domain.js: | + function addDomain(r) { + var cookies = r.headersOut['Set-Cookie']; + if (cookies) { + if (!Array.isArray(cookies)) { + cookies = [cookies]; + } + var modified = cookies.map(function(c) { + if (c.indexOf('Domain=') === -1) { + return c + '; Domain=kube.huskypup.net'; + } + return c; + }); + r.headersOut['Set-Cookie'] = modified; + } + } + + export default { addDomain }; + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ext-authz-proxy + namespace: authentik + labels: + app: ext-authz-proxy +spec: + replicas: 2 + selector: + matchLabels: + app: ext-authz-proxy + template: + metadata: + labels: + app: ext-authz-proxy + spec: + automountServiceAccountToken: false + containers: + - name: nginx + image: nginx:1.27-alpine + ports: + - containerPort: 4180 + protocol: TCP + volumeMounts: + - name: config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + readOnly: true + - name: njs + mountPath: /etc/nginx/njs/cookie_domain.js + subPath: cookie_domain.js + readOnly: true + - name: tmp + mountPath: /tmp + - name: cache + mountPath: /var/cache/nginx + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 50m + memory: 64Mi + securityContext: + runAsNonRoot: true + runAsUser: 10101 + runAsGroup: 10101 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + readinessProbe: + tcpSocket: + port: 4180 + initialDelaySeconds: 2 + periodSeconds: 5 + livenessProbe: + tcpSocket: + port: 4180 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: config + configMap: + name: ext-authz-proxy-config + - name: njs + configMap: + name: ext-authz-proxy-config + - name: tmp + emptyDir: {} + - name: cache + emptyDir: {} + +--- +apiVersion: v1 +kind: Service +metadata: + name: ext-authz-proxy + namespace: authentik + labels: + app: ext-authz-proxy +spec: + selector: + app: ext-authz-proxy + ports: + - port: 4180 + targetPort: 4180 + protocol: TCP diff --git a/infrastructure/authentik/manifests/istio-virtualservice.yaml b/infrastructure/authentik/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..845d9e4 --- /dev/null +++ b/infrastructure/authentik/manifests/istio-virtualservice.yaml @@ -0,0 +1,63 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: authentik + namespace: authentik +spec: + hosts: + - auth.kube.huskypup.net + gateways: + - istio-system/edge + http: + # Allow NetBird silent auth in an iframe (prompt=none) + - match: + - uri: + prefix: /application/o/authorize/ + headers: + response: + remove: + - x-frame-options + set: + content-security-policy: "frame-ancestors https://netbird.kube.huskypup.net" + route: + - destination: + host: authentik-server.authentik.svc.cluster.local + port: + number: 80 + + # Ensure CORS headers are present even on 401 responses + - match: + - uri: + prefix: /application/o/userinfo/ + - uri: + prefix: /application/o/token/ + headers: + response: + set: + access-control-allow-origin: "https://netbird.kube.huskypup.net" + access-control-allow-credentials: "true" + access-control-allow-methods: "GET, POST, OPTIONS" + access-control-allow-headers: "authorization, content-type" + vary: "Origin" + route: + - destination: + host: authentik-server.authentik.svc.cluster.local + port: + number: 80 + + # Forward auth endpoint for Istio ext_authz + - match: + - uri: + prefix: /outpost.goauthentik.io + route: + - destination: + host: authentik-server.authentik.svc.cluster.local + port: + number: 80 + + # All other traffic goes to Authentik + - route: + - destination: + host: authentik-server.authentik.svc.cluster.local + port: + number: 80 diff --git a/infrastructure/authentik/manifests/oauth-sync-job.yaml b/infrastructure/authentik/manifests/oauth-sync-job.yaml new file mode 100644 index 0000000..fcd5159 --- /dev/null +++ b/infrastructure/authentik/manifests/oauth-sync-job.yaml @@ -0,0 +1,36 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: authentik-oauth-sync + namespace: authentik + annotations: + argocd.argoproj.io/hook: PostSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "1" +spec: + backoffLimit: 3 + template: + spec: + serviceAccountName: argocd-hook-sa + containers: + - name: oauth-sync + image: bitnami/kubectl:1.31 + command: + - /bin/bash + - -c + - | + set -euo pipefail + echo "=== Authentik PostSync - OAuth Sync ===" + + echo "Waiting for Authentik server..." + kubectl -n authentik rollout status deploy/authentik-server --timeout=600s + + echo "Waiting for Authentik worker..." + kubectl -n authentik rollout status deploy/authentik-worker --timeout=300s + + echo "OAuth sync scripts should be run manually or via a separate automation." + echo "Scripts are in the scripts/ directory of the Homelabv4 repo." + echo "Run: bash scripts/sync-*-oauth.sh" + + echo "=== Authentik PostSync Complete ===" + restartPolicy: Never diff --git a/infrastructure/authentik/manifests/presync-job.yaml b/infrastructure/authentik/manifests/presync-job.yaml new file mode 100644 index 0000000..64ee61d --- /dev/null +++ b/infrastructure/authentik/manifests/presync-job.yaml @@ -0,0 +1,48 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: authentik-presync + namespace: authentik + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + backoffLimit: 3 + template: + spec: + serviceAccountName: argocd-hook-sa + containers: + - name: presync + image: bitnami/kubectl:1.31 + command: + - /bin/bash + - -c + - | + set -euo pipefail + echo "=== Authentik PreSync ===" + + # Wait for CNPG cluster to be ready (applied by ArgoCD as sync-wave resource) + echo "Waiting for Authentik PostgreSQL cluster..." + for i in $(seq 1 60); do + PHASE=$(kubectl -n authentik get clusters.postgresql.cnpg.io pg-authentik -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$PHASE" = "Cluster in healthy state" ] || [ "$PHASE" = "Healthy" ]; then + echo " PostgreSQL cluster ready" + break + fi + echo " waiting for pg-authentik... (attempt $i/60, phase=$PHASE)" + sleep 5 + done + + # Generate Authentik secret key if it doesn't exist + if ! kubectl -n authentik get secret authentik >/dev/null 2>&1; then + echo "Generating Authentik secret key..." + SECRET_KEY=$(openssl rand -hex 50) + kubectl -n authentik create secret generic authentik \ + --from-literal=AUTHENTIK_SECRET_KEY="$SECRET_KEY" + echo " Authentik secret key generated" + else + echo " Authentik secret already exists" + fi + + echo "=== Authentik PreSync Complete ===" + restartPolicy: Never diff --git a/infrastructure/authentik/n8n-blueprint.yaml b/infrastructure/authentik/n8n-blueprint.yaml new file mode 100644 index 0000000..2f2eed2 --- /dev/null +++ b/infrastructure/authentik/n8n-blueprint.yaml @@ -0,0 +1,49 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-n8n + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + n8n.yaml: |- + version: 1 + metadata: + name: n8n-oidc + entries: + - model: authentik_providers_oauth2.oauth2provider + id: n8n-provider + state: present + identifiers: + name: n8n + attrs: + name: n8n + client_id: e55cbe3c6d2ae30c05c86385802ffcfd76a972c22c8d039f000281d4a149fd44 + client_secret: 718d4c4816aa68767a6a0da0fa4099d492473c700ba236cbeb64d294762b9548acda7e716ba11b2ad7729d6a1c16c7890b227e3b2dcfc35fd4af1eb0d6662b0f + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + redirect_uris: + - url: "https://n8n.kube.huskypup.net/callback" + matching_mode: strict + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + client_type: confidential + access_code_validity: "minutes=10" + access_token_validity: "hours=1" + refresh_token_validity: "days=30" + signing_key: !Find [authentik_crypto.certificatekeypair, [name, "authentik Self-signed Certificate"]] + + - model: authentik_core.application + id: n8n-application + state: present + identifiers: + slug: n8n + attrs: + name: n8n + slug: n8n + policy_engine_mode: any + provider: !KeyOf n8n-provider diff --git a/infrastructure/authentik/netbird-blueprint.yaml b/infrastructure/authentik/netbird-blueprint.yaml new file mode 100644 index 0000000..bf43fa1 --- /dev/null +++ b/infrastructure/authentik/netbird-blueprint.yaml @@ -0,0 +1,93 @@ +--- +# Netbird OIDC Provider Blueprint for Authentik +# Creates an OAuth2/OIDC provider for Netbird VPN SSO integration +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-netbird + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + netbird.yaml: |- + version: 1 + metadata: + name: netbird-oidc + entries: + - model: authentik_providers_oauth2.oauth2provider + id: netbird-provider + state: present + identifiers: + name: Netbird + attrs: + name: Netbird + # NetBird dashboard uses prompt=none for silent auth; this flow avoids consent UI. + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + redirect_uris: + - url: "https://netbird.kube.huskypup.net/.*" + matching_mode: regex + - url: "http://localhost:53000" + matching_mode: strict + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, offline_access]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, "goauthentik.io/api"]] + client_type: public + include_claims_in_id_token: true + sub_mode: user_id + access_code_validity: "minutes=10" + access_token_validity: "hours=1" + refresh_token_validity: "days=30" + signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] + + # Create VPN User group (used for non-admin NetBird access) + - model: authentik_core.group + id: vpn-user-group + state: present + identifiers: + name: VPN User + attrs: + name: VPN User + is_superuser: false + + - model: authentik_core.application + id: netbird-application + state: present + identifiers: + slug: netbird + attrs: + name: Netbird VPN + slug: netbird + policy_engine_mode: any + provider: !KeyOf netbird-provider + + # Restrict NetBird access to authentik Admins and VPN User groups + - model: authentik_policies.policybinding + state: present + identifiers: + target: !KeyOf netbird-application + group: !Find [authentik_core.group, [name, "authentik Admins"]] + attrs: + target: !KeyOf netbird-application + group: !Find [authentik_core.group, [name, "authentik Admins"]] + enabled: true + order: 0 + negate: false + timeout: 30 + + - model: authentik_policies.policybinding + state: present + identifiers: + target: !KeyOf netbird-application + group: !Find [authentik_core.group, [name, "VPN User"]] + attrs: + target: !KeyOf netbird-application + group: !Find [authentik_core.group, [name, "VPN User"]] + enabled: true + order: 1 + negate: false + timeout: 30 diff --git a/infrastructure/authentik/nextcloud-blueprint.yaml b/infrastructure/authentik/nextcloud-blueprint.yaml new file mode 100644 index 0000000..ec76cd8 --- /dev/null +++ b/infrastructure/authentik/nextcloud-blueprint.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-nextcloud + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + nextcloud.yaml: |- + version: 1 + metadata: + name: nextcloud-oidc + entries: + - model: authentik_providers_oauth2.oauth2provider + id: nextcloud-provider + state: present + identifiers: + name: Nextcloud + attrs: + name: Nextcloud + client_id: HmTGnUfhCvjFFbUUAne7JLrucNC2X675JicPbEnQ + client_secret: qTUf4NYjzUuAqfNV5D7SFPuuhJJwVltU6U3LAuJVy7dddJWeJSv2B4wKaRxbbHqpIiSfYPDaKujhEysKbOWUnf4n9OHPKJ0bu1I2EzkYtChO1Gq4gUv5EHPTzQlMBPnl + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + redirect_uris: + - url: "https://nextcloud.kube.huskypup.net/apps/user_oidc/code" + matching_mode: strict + - url: "http://nextcloud.kube.huskypup.net/apps/user_oidc/code" + matching_mode: strict + enabled: true + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + client_type: confidential + access_code_validity: "minutes=10" + access_token_validity: "days=7" + refresh_token_validity: "days=30" + signing_key: !Find [authentik_crypto.certificatekeypair, [name, "authentik Internal JWT Certificate"]] + + - model: authentik_core.application + id: nextcloud-application + state: present + identifiers: + slug: nextcloud + attrs: + name: Nextcloud + slug: nextcloud + policy_engine_mode: any + provider: !KeyOf nextcloud-provider diff --git a/infrastructure/authentik/percona-everest-blueprint.yaml b/infrastructure/authentik/percona-everest-blueprint.yaml new file mode 100644 index 0000000..a35fe4b --- /dev/null +++ b/infrastructure/authentik/percona-everest-blueprint.yaml @@ -0,0 +1,77 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-percona-everest + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + percona-everest.yaml: |- + version: 1 + metadata: + name: percona-everest-oidc + labels: + blueprints.goauthentik.io/instantiate: "true" + entries: + # ============================================================================ + # PERCONA EVEREST OIDC PROVIDER + # ============================================================================ + # Everest uses OIDC with PKCE (public client) for authentication. + # The client_id must match server.oidc.clientId in percona-everest/values.yaml. + # ============================================================================ + + # Custom scope mapping to inject jti claim (required by Everest) + - model: authentik_providers_oauth2.scopemapping + id: percona-everest-jti-mapping + state: present + identifiers: + managed: goauthentik.io/providers/proxy/scope-percona-everest-jti + attrs: + name: "Percona Everest JTI Claim" + scope_name: "everest-jti" + expression: | + import uuid + return {"jti": str(uuid.uuid4())} + + - model: authentik_providers_oauth2.oauth2provider + id: percona-everest-provider + state: present + identifiers: + name: Percona Everest + attrs: + name: Percona Everest + client_id: percona-everest-oidc-client-id-static-2025 + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + redirect_uris: + - url: "https://everest.kube.huskypup.net/login-callback" + matching_mode: strict + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + - !KeyOf percona-everest-jti-mapping + include_claims_in_id_token: true + client_type: public + access_code_validity: "minutes=10" + access_token_validity: "hours=1" + refresh_token_validity: "days=30" + signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] + + # ============================================================================ + # PERCONA EVEREST APPLICATION + # ============================================================================ + + - model: authentik_core.application + id: percona-everest-application + state: present + identifiers: + slug: percona-everest + attrs: + name: Percona Everest + slug: percona-everest + policy_engine_mode: any + provider: !KeyOf percona-everest-provider + meta_launch_url: "https://everest.kube.huskypup.net" diff --git a/infrastructure/authentik/prometheus-blueprint.yaml b/infrastructure/authentik/prometheus-blueprint.yaml new file mode 100644 index 0000000..1c06fbb --- /dev/null +++ b/infrastructure/authentik/prometheus-blueprint.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-prometheus + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + prometheus.yaml: |- + version: 1 + metadata: + name: prometheus-app + entries: + - model: authentik_core.application + id: prometheus-application + state: present + identifiers: + slug: prometheus + attrs: + name: Prometheus + slug: prometheus + policy_engine_mode: any diff --git a/infrastructure/authentik/rancher-blueprint.yaml b/infrastructure/authentik/rancher-blueprint.yaml new file mode 100644 index 0000000..4905306 --- /dev/null +++ b/infrastructure/authentik/rancher-blueprint.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints-rancher + namespace: authentik + labels: + goauthentik.io/blueprint: "true" +data: + rancher.yaml: |- + # yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json + version: 1 + metadata: + name: rancher-oidc + entries: + - model: authentik_providers_oauth2.oauth2provider + id: rancher-provider + state: present + identifiers: + name: Rancher + attrs: + name: Rancher + client_id: 94a6beeaa91d0d0407cf8af807e8208a611cf3f25ae42161e697a8dccd563449 + client_secret: 19ebd4545ba74a93c220d4471af634302c5991204cf650cabd522b0864d7617ded2c59b470ab6de7a5801e7de4c84e4b9741b983170815419c9503d55f849794 + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + redirect_uris: + - url: "https://rancher.kube.huskypup.net/verify-auth" + matching_mode: strict + - url: "https://rancher.kube.huskypup.net" + matching_mode: strict + enabled: true + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + client_type: confidential + access_code_validity: "minutes=10" + access_token_validity: "hours=1" + refresh_token_validity: "days=30" + signing_key: !Find [authentik_crypto.certificatekeypair, [name, "authentik Internal JWT Certificate"]] + + - model: authentik_core.application + id: rancher-application + state: present + identifiers: + slug: rancher + attrs: + name: Rancher + slug: rancher + policy_engine_mode: any + provider: !KeyOf rancher-provider + diff --git a/infrastructure/authentik/values.yaml b/infrastructure/authentik/values.yaml new file mode 100644 index 0000000..15f90b0 --- /dev/null +++ b/infrastructure/authentik/values.yaml @@ -0,0 +1,177 @@ +# Authentik configuration +authentik: + secret_key: env://AUTHENTIK_SECRET_KEY + postgresql: + host: env://AUTHENTIK_POSTGRESQL__HOST + port: 5432 + name: env://AUTHENTIK_POSTGRESQL__NAME + user: env://AUTHENTIK_POSTGRESQL__USER + password: env://AUTHENTIK_POSTGRESQL__PASSWORD + +global: + envFrom: + # First secret: Authentik-specific settings (AUTHENTIK_SECRET_KEY) + - secretRef: + name: authentik + # Second secret: Database credentials from CNPG + - secretRef: + name: pg-authentik-app + prefix: AUTHENTIK_POSTGRESQL__ + env: + # Configure external URL for proper OIDC discovery responses + - name: AUTHENTIK_URL + value: "https://auth.kube.huskypup.net" + # Override to use the correct field names from pg-authentik-app + - name: AUTHENTIK_POSTGRESQL__HOST + valueFrom: + secretKeyRef: + name: pg-authentik-app + key: host + - name: AUTHENTIK_POSTGRESQL__NAME + valueFrom: + secretKeyRef: + name: pg-authentik-app + key: dbname + - name: AUTHENTIK_POSTGRESQL__USER + valueFrom: + secretKeyRef: + name: pg-authentik-app + key: username + - name: AUTHENTIK_POSTGRESQL__PASSWORD + valueFrom: + secretKeyRef: + name: pg-authentik-app + key: password + - name: AUTHENTIK_POSTGRESQL__PORT + value: "5432" + +# Blueprints - Mount ConfigMaps for auto-discovery +blueprints: + configMaps: + - authentik-blueprints-gitlab + - authentik-blueprints-nextcloud + - authentik-blueprints-argocd + - authentik-blueprints-grafana + - authentik-blueprints-forward-auth + - authentik-blueprints-prometheus + - authentik-blueprints-n8n + - authentik-blueprints-ceph + - authentik-blueprints-home-assistant + - authentik-blueprints-guacamole + - authentik-blueprints-percona-everest + - authentik-blueprints-rancher + - authentik-blueprints-netbird + +# Enable Prometheus metrics +server: + # Single replica required: the embedded outpost stores forward-auth sessions + # in-memory (no Redis). Multiple replicas cause callback failures because + # the session created during ext_authz check is only on one pod. + replicas: 1 + + containerSecurityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + + metrics: + enabled: true + serviceMonitor: + enabled: true + labels: + release: prometheus + + # Resource limits to prevent OOM + resources: + requests: + cpu: 50m + memory: 512Mi + limits: + memory: 1Gi + + # Health probes - removed invalid 'enabled' fields + startupProbe: + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 24 # 120 seconds for Authentik startup + + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 2 + + livenessProbe: + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + + # Spread across nodes + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: authentik + app.kubernetes.io/component: server + topologyKey: kubernetes.io/hostname + +worker: + containerSecurityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + + metrics: + enabled: true + serviceMonitor: + enabled: true + labels: + release: prometheus + + # Resource limits to prevent OOM + resources: + requests: + cpu: 50m + memory: 512Mi + limits: + memory: 1Gi + + # Spread across nodes + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: authentik + app.kubernetes.io/component: worker + topologyKey: kubernetes.io/hostname + + # Mount cert-manager secret for auto-import of Let's Encrypt cert + volumeMounts: + - name: certs + mountPath: /certs + readOnly: true + volumes: + - name: certs + secret: + secretName: authentik-tls + optional: true + +# Disable bundled Postgres +postgresql: + enabled: false diff --git a/infrastructure/cert-manager/manifests/letsencrypt-issuer.yaml b/infrastructure/cert-manager/manifests/letsencrypt-issuer.yaml new file mode 100644 index 0000000..b67ff41 --- /dev/null +++ b/infrastructure/cert-manager/manifests/letsencrypt-issuer.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-production +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: garrettstone499@gmail.com + privateKeySecretRef: + name: letsencrypt-production + solvers: + - dns01: + cloudflare: + email: garrettstone499@gmail.com + apiTokenSecretRef: + name: cloudflare-token-secret + key: cloudflare-token + selector: + dnsZones: + - "kube.huskypup.net" + +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging +spec: + acme: + server: https://acme-staging-v02.api.letsencrypt.org/directory + email: garrettstone499@gmail.com + privateKeySecretRef: + name: letsencrypt-staging + solvers: + - dns01: + cloudflare: + email: garrettstone499@gmail.com + apiTokenSecretRef: + name: cloudflare-token-secret + key: cloudflare-token + selector: + dnsZones: + - "kube.huskypup.net" \ No newline at end of file diff --git a/infrastructure/cert-manager/manifests/secret-cf-token.yaml b/infrastructure/cert-manager/manifests/secret-cf-token.yaml new file mode 100644 index 0000000..a3d44d1 --- /dev/null +++ b/infrastructure/cert-manager/manifests/secret-cf-token.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: cloudflare-token-secret + namespace: cert-manager +type: Opaque +stringData: + cloudflare-token: Mg9Yx1ku3-rIn7VR7Wf_PZ1uir7AqUsx3IZVpuRX # be sure you are generating an API token and not a global API key https://cert-manager.io/docs/configuration/acme/dns01/cloudflare/#api-tokens diff --git a/infrastructure/cert-manager/values.yaml b/infrastructure/cert-manager/values.yaml new file mode 100644 index 0000000..25c6482 --- /dev/null +++ b/infrastructure/cert-manager/values.yaml @@ -0,0 +1,30 @@ +--- +installCRDs: true +replicaCount: 3 + +global: + leaderElection: + namespace: cert-manager + +extraArgs: + - --dns01-recursive-nameservers=1.1.1.1:53,9.9.9.9:53 + - --dns01-recursive-nameservers-only + +podDnsPolicy: None +podDnsConfig: + nameservers: + - 1.1.1.1 + - 9.9.9.9 + +# Enable Prometheus metrics +prometheus: + enabled: true + servicemonitor: + enabled: true + prometheusInstance: default + targetPort: 9402 + path: /metrics + interval: 60s + scrapeTimeout: 30s + labels: + release: prometheus \ No newline at end of file diff --git a/infrastructure/checkov/manifests/grafana-dashboard.yaml b/infrastructure/checkov/manifests/grafana-dashboard.yaml new file mode 100644 index 0000000..2d0e5b3 --- /dev/null +++ b/infrastructure/checkov/manifests/grafana-dashboard.yaml @@ -0,0 +1,96 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard-checkov + namespace: grafana + labels: + grafana_dashboard: "1" +data: + checkov-compliance.json: | + { + "annotations": { "list": [] }, + "editable": true, + "panels": [ + { + "title": "Checkov Scanner Job Status", + "type": "timeseries", + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "kube_job_status_succeeded{namespace=\"checkov\"}", + "legendFormat": "Succeeded" + }, + { + "expr": "kube_job_status_failed{namespace=\"checkov\"}", + "legendFormat": "Failed" + } + ], + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 } + }, + { + "title": "Last Successful Scan Age (hours)", + "type": "stat", + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "(time() - kube_cronjob_status_last_successful_time{namespace=\"checkov\", cronjob=\"checkov-scanner\"}) / 3600", + "legendFormat": "Hours since last scan" + } + ], + "fieldConfig": { + "defaults": { + "unit": "h", + "thresholds": { + "steps": [ + { "color": "green", "value": 0 }, + { "color": "yellow", "value": 25 }, + { "color": "red", "value": 49 } + ] + } + } + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 } + }, + { + "title": "Kyverno Policy Violations (Audit Mode)", + "type": "timeseries", + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "sum(kyverno_policy_results_total{rule_result=\"fail\"}) by (policy_name)", + "legendFormat": "{{policy_name}}" + } + ], + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 8 } + }, + { + "title": "Kyverno Pass vs Fail", + "type": "piechart", + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "sum(kyverno_policy_results_total) by (rule_result)", + "legendFormat": "{{rule_result}}" + } + ], + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 } + }, + { + "title": "Tetragon Security Events", + "type": "stat", + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "sum(rate(tetragon_events_total{type=~\"PROCESS_KPROBE|PROCESS_TRACEPOINT\"}[1h]))", + "legendFormat": "Events/hr" + } + ], + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 } + } + ], + "schemaVersion": 39, + "tags": ["checkov", "kyverno", "compliance", "zero-trust"], + "time": { "from": "now-24h", "to": "now" }, + "title": "Zero Trust Compliance Overview", + "uid": "zero-trust-compliance" + } diff --git a/infrastructure/checkov/manifests/prometheus-rules.yaml b/infrastructure/checkov/manifests/prometheus-rules.yaml new file mode 100644 index 0000000..4191a0c --- /dev/null +++ b/infrastructure/checkov/manifests/prometheus-rules.yaml @@ -0,0 +1,28 @@ +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: checkov-alerts + namespace: prometheus + labels: + release: kube-prometheus-stack +spec: + groups: + - name: checkov-security + rules: + - alert: CheckovScanJobFailed + expr: kube_job_status_failed{namespace="checkov", job_name=~"checkov-scanner.*"} > 0 + for: 5m + labels: + severity: warning + annotations: + summary: "Checkov security scan job failed" + description: "The Checkov IaC scanner CronJob has failed. Check logs for details." + + - alert: CheckovScanNotRunRecently + expr: time() - kube_cronjob_status_last_successful_time{namespace="checkov", cronjob="checkov-scanner"} > 172800 + for: 1h + labels: + severity: warning + annotations: + summary: "Checkov scan has not run in 48+ hours" + description: "The Checkov security scanner has not completed successfully in over 48 hours." diff --git a/infrastructure/checkov/manifests/scanner-cronjob.yaml b/infrastructure/checkov/manifests/scanner-cronjob.yaml new file mode 100644 index 0000000..3c1e48c --- /dev/null +++ b/infrastructure/checkov/manifests/scanner-cronjob.yaml @@ -0,0 +1,245 @@ +--- +# Checkov & Regula IaC Scanner CronJob +# Scans Kubernetes manifests daily for security misconfigurations +# Results stored in ConfigMap for Grafana dashboard consumption +apiVersion: v1 +kind: Namespace +metadata: + name: checkov + labels: + app: checkov-scanner +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: checkov-scanner + namespace: checkov +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: checkov-scanner +rules: + # Read all resources for scanning + - apiGroups: [""] + resources: ["pods", "services", "configmaps", "secrets", "namespaces", "serviceaccounts"] + verbs: ["get", "list"] + - apiGroups: ["apps"] + resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] + verbs: ["get", "list"] + - apiGroups: ["networking.k8s.io"] + resources: ["ingresses", "networkpolicies"] + verbs: ["get", "list"] + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"] + verbs: ["get", "list"] + # Write scan results + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["create"] + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["update", "patch"] + resourceNames: ["checkov-scan-results"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: checkov-scanner +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: checkov-scanner +subjects: + - kind: ServiceAccount + name: checkov-scanner + namespace: checkov +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: checkov-scanner + namespace: checkov + labels: + app: checkov-scanner +spec: + schedule: "0 3 * * *" # Daily at 3 AM + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 1 + activeDeadlineSeconds: 1800 # 30 min timeout + template: + metadata: + labels: + app: checkov-scanner + spec: + serviceAccountName: checkov-scanner + restartPolicy: Never + containers: + - name: scanner + image: docker.io/bridgecrew/checkov:3.2.334 + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + volumeMounts: + - name: tmp + mountPath: /tmp + command: + - python3 + - -c + - | + import subprocess, json, os, ssl, urllib.request, datetime + + print("=== Checkov Kubernetes Security Scan ===") + print(f"Scan started at: {datetime.datetime.utcnow().isoformat()}Z") + + # Fetch Kubernetes resources as YAML files for scanning + token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token" + ca_path = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + api = "https://kubernetes.default.svc" + + with open(token_path) as f: + token = f.read().strip() + + ctx = ssl.create_default_context(cafile=ca_path) + headers = {"Authorization": f"Bearer {token}"} + + os.makedirs("/tmp/k8s-manifests", exist_ok=True) + + # Fetch deployments, statefulsets, daemonsets, pods, services + # Focus on workload resources (skip pods/configmaps to reduce memory) + resources = [ + ("apps/v1", "deployments"), + ("apps/v1", "statefulsets"), + ("apps/v1", "daemonsets"), + ("v1", "services"), + ("networking.k8s.io/v1", "ingresses"), + ] + + # Map API group to kind names + kind_map = { + "deployments": ("Deployment", "apps/v1"), + "statefulsets": ("StatefulSet", "apps/v1"), + "daemonsets": ("DaemonSet", "apps/v1"), + "pods": ("Pod", "v1"), + "services": ("Service", "v1"), + "configmaps": ("ConfigMap", "v1"), + "ingresses": ("Ingress", "networking.k8s.io/v1"), + } + for api_ver, kind in resources: + url = f"{api}/apis/{api_ver}/{kind}" if "/" in api_ver and api_ver != "v1" else f"{api}/api/{api_ver}/{kind}" + req = urllib.request.Request(url, headers=headers) + try: + with urllib.request.urlopen(req, context=ctx) as resp: + data = json.loads(resp.read()) + kind_name, api_version = kind_map.get(kind, (kind.title(), api_ver)) + for item in data.get("items", []): + # Ensure apiVersion and kind are set (required by checkov) + item["apiVersion"] = api_version + item["kind"] = kind_name + # Remove status (not part of manifest) + item.pop("status", None) + ns = item.get("metadata", {}).get("namespace", "default") + name = item.get("metadata", {}).get("name", "unknown") + fname = f"/tmp/k8s-manifests/{kind}-{ns}-{name}.json" + with open(fname, "w") as f: + json.dump(item, f) + except Exception as e: + print(f"Warning: failed to fetch {kind}: {e}") + + file_count = len(os.listdir("/tmp/k8s-manifests")) + print(f"Fetched {file_count} Kubernetes resources") + + # Run checkov scan on fetched manifests + print("Scanning resources with Checkov...") + result = subprocess.run( + ["checkov", "--framework", "kubernetes", "-d", "/tmp/k8s-manifests", + "--output", "json", "--output-file", "/tmp/checkov-out", + "--soft-fail", "--quiet"], + capture_output=True, text=True + ) + + # Parse results (checkov creates dir with results_json.json inside) + passed = failed = skipped = 0 + results_file = "/tmp/checkov-out/results_json.json" + if not os.path.isfile(results_file): + results_file = "/tmp/checkov-out" + try: + with open(results_file) as f: + data = json.load(f) + if isinstance(data, list): + for entry in data: + s = entry.get("summary", {}) + passed += s.get("passed", 0) + failed += s.get("failed", 0) + skipped += s.get("skipped", 0) + else: + s = data.get("summary", {}) + passed = s.get("passed", 0) + failed = s.get("failed", 0) + skipped = s.get("skipped", 0) + except Exception as e: + print(f"Warning: failed to parse results: {e}") + + print(f"\n=== Scan Summary ===") + print(f"Passed: {passed}") + print(f"Failed: {failed}") + print(f"Skipped: {skipped}") + scan_time = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + print(f"Scan completed at: {scan_time}") + + # Store results in ConfigMap via Kubernetes API + cm = json.dumps({ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "checkov-scan-results", + "namespace": "checkov", + "labels": {"app": "checkov-scanner"} + }, + "data": { + "last-scan": scan_time, + "passed": str(passed), + "failed": str(failed), + "skipped": str(skipped), + } + }).encode() + + # Try patch, then create + try: + req = urllib.request.Request( + f"{api}/api/v1/namespaces/checkov/configmaps/checkov-scan-results", + data=cm, headers={**headers, "Content-Type": "application/merge-patch+json"}, + method="PATCH") + urllib.request.urlopen(req, context=ctx) + print("Updated ConfigMap checkov-scan-results") + except urllib.error.HTTPError as e: + if e.code == 404: + req = urllib.request.Request( + f"{api}/api/v1/namespaces/checkov/configmaps", + data=cm, headers={**headers, "Content-Type": "application/json"}, + method="POST") + urllib.request.urlopen(req, context=ctx) + print("Created ConfigMap checkov-scan-results") + else: + print(f"Warning: failed to store results: {e}") + resources: + limits: + memory: 1Gi + cpu: 500m + requests: + memory: 512Mi + cpu: 100m + volumes: + - name: tmp + emptyDir: {} diff --git a/infrastructure/cilium/manifests/l2-announcement-policy.yaml b/infrastructure/cilium/manifests/l2-announcement-policy.yaml new file mode 100644 index 0000000..130d75e --- /dev/null +++ b/infrastructure/cilium/manifests/l2-announcement-policy.yaml @@ -0,0 +1,23 @@ +# Cilium L2 Announcement Policy - Replaces MetalLB +# Announces LoadBalancer IPs via ARP on node interfaces +--- +apiVersion: cilium.io/v2alpha1 +kind: CiliumL2AnnouncementPolicy +metadata: + name: default-l2-policy +spec: + loadBalancerIPs: true + externalIPs: true + interfaces: + - ^eth[0-9]+ + - ^en[ops][0-9]+ +--- +# IP Pool - Same range as previous MetalLB pool (172.28.101.240-250) +apiVersion: cilium.io/v2alpha1 +kind: CiliumLoadBalancerIPPool +metadata: + name: default-pool +spec: + blocks: + - start: "172.28.101.240" + stop: "172.28.101.250" diff --git a/infrastructure/cilium/manifests/network-policies/baseline.yaml b/infrastructure/cilium/manifests/network-policies/baseline.yaml new file mode 100644 index 0000000..24315e7 --- /dev/null +++ b/infrastructure/cilium/manifests/network-policies/baseline.yaml @@ -0,0 +1,156 @@ +# Baseline CiliumNetworkPolicies for critical infrastructure namespaces +# Provides L3/L4 ingress segmentation (Istio waypoints handle L7) +# +# IMPORTANT: CiliumNetworkPolicy egress rules with empty `{}` only match +# Cilium-managed endpoints — NOT kube-apiserver, host, remote-node, or world. +# Omit egress rules entirely to allow all egress (default behavior). +# Ingress rules must explicitly include fromEntities for host/kube-apiserver +# traffic (health probes, API server webhooks, kubelet). + +# --- Vault: Only accessible from ESO, authentik sync scripts, and Prometheus --- +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: vault-ingress + namespace: vault +spec: + endpointSelector: {} + ingress: + # Kubernetes API server and kubelet (health probes, webhooks) + - fromEntities: + - host + - kube-apiserver + - remote-node + # Istio ingress gateway + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: istio-system + istio: ingressgateway + # External Secrets Operator + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: external-secrets + # Prometheus scraping + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: prometheus + # Vault internal (raft replication) + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: vault + +# --- Authentik: Accessible from ingress, apps doing OIDC, and Prometheus --- +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: authentik-ingress + namespace: authentik +spec: + endpointSelector: {} + ingress: + # Kubernetes API server and kubelet (health probes, webhooks) + - fromEntities: + - host + - kube-apiserver + - remote-node + # Istio ingress gateway + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: istio-system + # Internal authentik traffic (server <-> worker, pg access) + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: authentik + # Apps doing OIDC token exchange + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: argocd + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: gitlab + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: grafana + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: n8n + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: nextcloud + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: home-assistant + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: guacamole + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: netbird + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: cattle-system + # Prometheus scraping + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: prometheus + +# --- CrowdSec: LAPI + agent + Prometheus + CNPG --- +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: crowdsec-ingress + namespace: crowdsec +spec: + endpointSelector: {} + ingress: + # Kubernetes API server and kubelet (health probes, webhooks) + - fromEntities: + - host + - kube-apiserver + - remote-node + # Intra-namespace (LAPI <-> agent) + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: crowdsec + # CNPG operator + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: cnpg-system + # Prometheus scraping + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: prometheus + +# --- Rook-Ceph: Only internal + Prometheus + CSI consumers --- +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: rook-ceph-ingress + namespace: rook-ceph +spec: + endpointSelector: {} + ingress: + # Kubernetes API server and kubelet (health probes, webhooks, CSI mounts) + - fromEntities: + - host + - kube-apiserver + - remote-node + # Istio ingress gateway (Ceph dashboard) + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: istio-system + # Internal Ceph traffic + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: rook-ceph + # Prometheus scraping + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: prometheus + # CSI consumers (any namespace can mount Ceph volumes via CSI) + - fromEndpoints: + - {} diff --git a/infrastructure/cilium/manifests/network-policies/dns.yaml b/infrastructure/cilium/manifests/network-policies/dns.yaml new file mode 100644 index 0000000..7e4f91b --- /dev/null +++ b/infrastructure/cilium/manifests/network-policies/dns.yaml @@ -0,0 +1,23 @@ +# Allow all pods to reach CoreDNS for name resolution +# Uses enableDefaultDeny: false to avoid blocking non-DNS egress traffic +# Istio AuthorizationPolicies handle zero-trust enforcement at L7 +apiVersion: cilium.io/v2 +kind: CiliumClusterwideNetworkPolicy +metadata: + name: allow-dns +spec: + endpointSelector: {} + enableDefaultDeny: + egress: false + ingress: false + egress: + - toEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: kube-system + k8s-app: kube-dns + toPorts: + - ports: + - port: "53" + protocol: UDP + - port: "53" + protocol: TCP diff --git a/infrastructure/cilium/manifests/network-policies/health-probes.yaml b/infrastructure/cilium/manifests/network-policies/health-probes.yaml new file mode 100644 index 0000000..7d80e6f --- /dev/null +++ b/infrastructure/cilium/manifests/network-policies/health-probes.yaml @@ -0,0 +1,17 @@ +# Allow SNAT'd kubelet health probes into ambient mesh pods +# Istio ambient rewrites kubelet probe source to 169.254.7.127 (link-local) +# so ztunnel can identify and skip these packets. This policy ensures +# CiliumNetworkPolicies don't block the rewritten probe traffic. +apiVersion: cilium.io/v2 +kind: CiliumClusterwideNetworkPolicy +metadata: + name: allow-ambient-hostprobes +spec: + description: "Allows SNAT-ed kubelet health check probes into ambient pods" + enableDefaultDeny: + egress: false + ingress: false + endpointSelector: {} + ingress: + - fromCIDR: + - "169.254.7.127/32" diff --git a/infrastructure/cilium/values.yaml b/infrastructure/cilium/values.yaml new file mode 100644 index 0000000..f27543a --- /dev/null +++ b/infrastructure/cilium/values.yaml @@ -0,0 +1,120 @@ +# Cilium CNI - eBPF-based networking with kube-proxy replacement +# Replaces: Calico CNI + kube-proxy + MetalLB + +# Full kube-proxy replacement via eBPF +kubeProxyReplacement: true +k8sServiceHost: 172.28.101.40 +k8sServicePort: 6443 + +# IPAM - use existing pod CIDR +ipam: + mode: kubernetes + operator: + clusterPoolIPv4PodCIDRList: + - "10.244.0.0/16" + +# Explicitly set devices to avoid using leftover Calico tunl0 interface +devices: "ens+" + +# Native routing (no encapsulation overhead) +routingMode: native +autoDirectNodeRoutes: true +ipv4NativeRoutingCIDR: "10.244.0.0/16" + +# eBPF features +bpf: + masquerade: false # Must be false for Istio ambient (breaks link-local SNAT for health probes) + tproxy: true + lbExternalClusterIP: true + hostLegacyRouting: true # Use iptables host routing (eBPF host routing drops 169.254.7.127 packets) + +# Socket LB - restrict to host namespace for Istio ambient compatibility +socketLB: + hostNamespaceOnly: true + +# L2 announcements (replaces MetalLB) +l2announcements: + enabled: true + +# Allow istio-cni to coexist +cni: + exclusive: false + +# Hubble observability +hubble: + enabled: true + relay: + enabled: true + resources: + requests: + cpu: 10m + memory: 64Mi + limits: + memory: 256Mi + ui: + enabled: true + resources: + requests: + cpu: 10m + memory: 64Mi + limits: + memory: 128Mi + metrics: + enabled: + - dns + - drop + - tcp + - flow + - port-distribution + - icmp + - httpV2:exemplars=true;labelsContext=source_ip,source_namespace,source_workload,destination_ip,destination_namespace,destination_workload,traffic_direction + serviceMonitor: + enabled: true + labels: + release: kube-prometheus-stack + +# Operator settings +operator: + replicas: 1 + resources: + requests: + cpu: 10m + memory: 64Mi + limits: + memory: 256Mi + +# Agent resources +resources: + requests: + cpu: 50m + memory: 128Mi + limits: + memory: 512Mi + +# Prometheus metrics +prometheus: + enabled: true + serviceMonitor: + enabled: true + labels: + release: kube-prometheus-stack + +# Security +securityContext: + capabilities: + ciliumAgent: + - CHOWN + - KILL + - NET_ADMIN + - NET_RAW + - IPC_LOCK + - SYS_ADMIN + - SYS_RESOURCE + - DAC_OVERRIDE + - FOWNER + - SETGID + - SETUID + cleanCiliumState: + - NET_ADMIN + - SYS_ADMIN + - SYS_RESOURCE diff --git a/infrastructure/crowdsec/cnpg-cluster.yaml b/infrastructure/crowdsec/cnpg-cluster.yaml new file mode 100644 index 0000000..fe9f494 --- /dev/null +++ b/infrastructure/crowdsec/cnpg-cluster.yaml @@ -0,0 +1,38 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: pg-crowdsec + namespace: crowdsec +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:16 + instances: 3 + + resources: + requests: + memory: "256Mi" + cpu: "25m" + limits: + memory: "1Gi" + + affinity: + topologyKey: kubernetes.io/hostname + + storage: + size: 10Gi + storageClass: rook-ceph-block + + primaryUpdateStrategy: unsupervised + + postgresql: + parameters: + max_connections: "100" + shared_buffers: "256MB" + effective_cache_size: "768MB" + + bootstrap: + initdb: + database: crowdsec + owner: app + + monitoring: + enablePodMonitor: true diff --git a/infrastructure/crowdsec/manifests/external-secret.yaml b/infrastructure/crowdsec/manifests/external-secret.yaml new file mode 100644 index 0000000..abeffaf --- /dev/null +++ b/infrastructure/crowdsec/manifests/external-secret.yaml @@ -0,0 +1,41 @@ +# CrowdSec secrets from Vault +# - CAPI enrollment key (user registers at app.crowdsec.net) +# - Bouncer API key (generated by postsync hook via cscli) +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: crowdsec-capi + namespace: crowdsec +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: crowdsec-capi-key + creationPolicy: Owner + data: + - secretKey: enrollment-key + remoteRef: + key: crowdsec-capi + property: enrollment-key +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: crowdsec-bouncer + namespace: crowdsec +spec: + refreshInterval: "0" + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: crowdsec-bouncer-key + creationPolicy: Owner + data: + - secretKey: api-key + remoteRef: + key: crowdsec-bouncer + property: api-key diff --git a/infrastructure/crowdsec/manifests/firewall-bouncer.yaml b/infrastructure/crowdsec/manifests/firewall-bouncer.yaml new file mode 100644 index 0000000..947c738 --- /dev/null +++ b/infrastructure/crowdsec/manifests/firewall-bouncer.yaml @@ -0,0 +1,164 @@ +# CrowdSec Firewall Bouncer - nftables enforcement on each node +# Blocks malicious IPs before they reach Cilium eBPF processing +# Requires: hostNetwork + privileged (nftables management) +# Image: community Docker build of cs-firewall-bouncer (no official image exists) +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: crowdsec-firewall-bouncer-config + namespace: crowdsec +data: + crowdsec-firewall-bouncer.yaml: | + mode: nftables + update_frequency: 10s + log_mode: stdout + log_level: info + api_url: http://crowdsec-service.crowdsec.svc:8080/ + api_key: ${API_KEY} + insecure_skip_verify: false + disable_ipv6: false + deny_action: DROP + deny_log: true + deny_log_prefix: "crowdsec: " + supported_decisions_types: + - ban + blacklists_ipv4: crowdsec-blacklists + blacklists_ipv6: crowdsec6-blacklists + nftables: + ipv4: + enabled: true + set-only: false + table: crowdsec + chain: crowdsec-chain + priority: -10 + ipv6: + enabled: true + set-only: false + table: crowdsec6 + chain: crowdsec6-chain + priority: -10 + nftables_hooks: + - input + - forward + prometheus: + enabled: true + listen_addr: 0.0.0.0 + listen_port: 60601 +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: crowdsec-firewall-bouncer + namespace: crowdsec + labels: + app: crowdsec-firewall-bouncer +spec: + selector: + matchLabels: + app: crowdsec-firewall-bouncer + template: + metadata: + labels: + app: crowdsec-firewall-bouncer + spec: + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + tolerations: + - operator: Exists + initContainers: + # Render env vars in config template + - name: render-config + image: docker.io/library/busybox:1.36 + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + command: ["/bin/sh", "-c"] + args: + - | + sed "s|\${API_KEY}|${API_KEY}|g" /config-template/crowdsec-firewall-bouncer.yaml > /config/crowdsec-firewall-bouncer.yaml + env: + - name: API_KEY + valueFrom: + secretKeyRef: + name: crowdsec-bouncer-key + key: api-key + volumeMounts: + - name: config-template + mountPath: /config-template + - name: config-rendered + mountPath: /config + containers: + - name: bouncer + image: ghcr.io/shgew/cs-firewall-bouncer-docker:v0.0.34-patch1 + command: + - /usr/local/bin/crowdsec-firewall-bouncer + - -c + - /config/crowdsec-firewall-bouncer.yaml + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + memory: 64Mi + securityContext: + privileged: true + capabilities: + add: + - NET_ADMIN + - NET_RAW + volumeMounts: + - name: config-rendered + mountPath: /config + readOnly: true + - name: nftables-lock + mountPath: /run + volumes: + - name: config-template + configMap: + name: crowdsec-firewall-bouncer-config + - name: config-rendered + emptyDir: {} + - name: nftables-lock + hostPath: + path: /run + type: Directory +--- +apiVersion: v1 +kind: Service +metadata: + name: crowdsec-firewall-bouncer-metrics + namespace: crowdsec + labels: + app: crowdsec-firewall-bouncer +spec: + selector: + app: crowdsec-firewall-bouncer + ports: + - name: metrics + port: 60601 + targetPort: 60601 + protocol: TCP +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: crowdsec-firewall-bouncer + namespace: crowdsec + labels: + app: crowdsec-firewall-bouncer + release: kube-prometheus-stack +spec: + selector: + matchLabels: + app: crowdsec-firewall-bouncer + endpoints: + - port: metrics + interval: 30s diff --git a/infrastructure/crowdsec/manifests/grafana-dashboard.yaml b/infrastructure/crowdsec/manifests/grafana-dashboard.yaml new file mode 100644 index 0000000..06101b3 --- /dev/null +++ b/infrastructure/crowdsec/manifests/grafana-dashboard.yaml @@ -0,0 +1,205 @@ +# CrowdSec Grafana Dashboard +# Auto-discovered by Grafana sidecar via grafana_dashboard label +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard-crowdsec + namespace: grafana + labels: + grafana_dashboard: "1" +data: + crowdsec-overview.json: | + { + "annotations": { "list": [] }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "title": "Active Decisions (Bans)", + "type": "stat", + "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "cs_active_decisions", + "legendFormat": "Active Bans" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 10 }, + { "color": "red", "value": 100 } + ] + } + } + } + }, + { + "title": "Total Alerts", + "type": "stat", + "gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "sum(increase(cs_alerts_total[24h]))", + "legendFormat": "Alerts (24h)" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 50 }, + { "color": "red", "value": 200 } + ] + } + } + } + }, + { + "title": "CAPI Blocklist Entries", + "type": "stat", + "gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "cs_active_decisions{origin=\"CAPI\"}", + "legendFormat": "Community Blocklist" + } + ] + }, + { + "title": "Bouncer Status", + "type": "stat", + "gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "cs_bouncers", + "legendFormat": "Bouncers" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "steps": [ + { "color": "red", "value": null }, + { "color": "green", "value": 1 } + ] + } + } + } + }, + { + "title": "Alerts by Scenario (24h)", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "sum by (scenario) (increase(cs_alerts_total[1h]))", + "legendFormat": "{{ scenario }}" + } + ], + "fieldConfig": { + "defaults": { + "custom": { "drawStyle": "bars", "stacking": { "mode": "normal" } } + } + } + }, + { + "title": "Parser Hits (24h)", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "sum by (source) (increase(cs_parser_hits_total[1h]))", + "legendFormat": "{{ source }}" + } + ], + "fieldConfig": { + "defaults": { + "custom": { "drawStyle": "line", "fillOpacity": 10 } + } + } + }, + { + "title": "Decisions by Origin", + "type": "piechart", + "gridPos": { "h": 8, "w": 8, "x": 0, "y": 12 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "cs_active_decisions", + "legendFormat": "{{ origin }}" + } + ] + }, + { + "title": "Bucket Overflows (Triggered Scenarios)", + "type": "timeseries", + "gridPos": { "h": 8, "w": 16, "x": 8, "y": 12 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "sum by (name) (increase(cs_bucket_overflows_total[1h]))", + "legendFormat": "{{ name }}" + } + ], + "fieldConfig": { + "defaults": { + "custom": { "drawStyle": "bars", "stacking": { "mode": "normal" } } + } + } + }, + { + "title": "Firewall Bouncer - Processed Decisions", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 20 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "sum by (instance) (increase(cs_bouncer_decisions[1h]))", + "legendFormat": "{{ instance }}" + } + ] + }, + { + "title": "LAPI Requests", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 20 }, + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "targets": [ + { + "expr": "sum by (method, route) (increase(cs_lapi_requests_total[1h]))", + "legendFormat": "{{ method }} {{ route }}" + } + ] + } + ], + "schemaVersion": 39, + "tags": ["crowdsec", "security", "zero-trust"], + "templating": { + "list": [ + { + "current": { "selected": false, "text": "Prometheus", "value": "prometheus" }, + "name": "DS_PROMETHEUS", + "type": "datasource", + "query": "prometheus" + } + ] + }, + "time": { "from": "now-24h", "to": "now" }, + "title": "CrowdSec Overview", + "uid": "crowdsec-overview" + } diff --git a/infrastructure/crowdsec/manifests/postsync-job.yaml b/infrastructure/crowdsec/manifests/postsync-job.yaml new file mode 100644 index 0000000..8eb604b --- /dev/null +++ b/infrastructure/crowdsec/manifests/postsync-job.yaml @@ -0,0 +1,53 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: crowdsec-postsync + namespace: crowdsec + annotations: + argocd.argoproj.io/hook: PostSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + backoffLimit: 3 + template: + spec: + serviceAccountName: argocd-hook-sa + containers: + - name: postsync + image: bitnami/kubectl:1.31 + command: + - /bin/bash + - -c + - | + set -euo pipefail + echo "=== CrowdSec PostSync ===" + + # Wait for CrowdSec LAPI + echo "Waiting for CrowdSec LAPI..." + kubectl -n crowdsec rollout status deploy/crowdsec-lapi --timeout=180s 2>/dev/null || \ + echo "CrowdSec LAPI not ready yet" + + # Register firewall bouncer and store key in Vault + LAPI_POD=$(kubectl -n crowdsec get pod -l app.kubernetes.io/name=crowdsec,app.kubernetes.io/component=lapi -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") + if [ -n "$LAPI_POD" ]; then + ROOT_TOKEN=$(kubectl -n vault get secret vault-init-keys -o jsonpath='{.data.VAULT_ROOT_TOKEN}' 2>/dev/null | base64 -d) + EXISTING_KEY="" + if [ -n "$ROOT_TOKEN" ]; then + EXISTING_KEY=$(kubectl exec -n vault vault-0 -- env "VAULT_TOKEN=${ROOT_TOKEN}" \ + vault kv get -field=api-key secret/crowdsec-bouncer 2>/dev/null || echo "") + fi + + if [ -z "$EXISTING_KEY" ]; then + echo "Registering firewall bouncer..." + BOUNCER_KEY=$(kubectl -n crowdsec exec "$LAPI_POD" -- cscli bouncers add firewall-bouncer -o raw 2>/dev/null || echo "") + if [ -n "$BOUNCER_KEY" ] && [ -n "$ROOT_TOKEN" ]; then + kubectl exec -n vault vault-0 -- env "VAULT_TOKEN=${ROOT_TOKEN}" \ + vault kv put secret/crowdsec-bouncer api-key="$BOUNCER_KEY" 2>/dev/null && \ + echo " Bouncer API key stored in Vault" + fi + else + echo " Firewall bouncer key already in Vault" + fi + fi + + echo "=== CrowdSec PostSync Complete ===" + restartPolicy: Never diff --git a/infrastructure/crowdsec/manifests/presync-job.yaml b/infrastructure/crowdsec/manifests/presync-job.yaml new file mode 100644 index 0000000..32980d3 --- /dev/null +++ b/infrastructure/crowdsec/manifests/presync-job.yaml @@ -0,0 +1,40 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: crowdsec-presync + namespace: crowdsec + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + backoffLimit: 3 + template: + spec: + serviceAccountName: argocd-hook-sa + containers: + - name: presync + image: bitnami/kubectl:1.31 + command: + - /bin/bash + - -c + - | + set -euo pipefail + echo "=== CrowdSec PreSync ===" + + # Set PodSecurity to privileged (firewall bouncer needs host access) + kubectl label namespace crowdsec pod-security.kubernetes.io/enforce=privileged --overwrite 2>/dev/null || true + + # Wait for CNPG cluster + echo "Waiting for CrowdSec PostgreSQL cluster..." + for i in $(seq 1 60); do + PHASE=$(kubectl -n crowdsec get clusters.postgresql.cnpg.io pg-crowdsec -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$PHASE" = "Cluster in healthy state" ] || [ "$PHASE" = "Healthy" ]; then + echo " CrowdSec PostgreSQL cluster ready" + break + fi + echo " waiting for pg-crowdsec... (attempt $i/60, phase=$PHASE)" + sleep 5 + done + + echo "=== CrowdSec PreSync Complete ===" + restartPolicy: Never diff --git a/infrastructure/crowdsec/values.yaml b/infrastructure/crowdsec/values.yaml new file mode 100644 index 0000000..7413409 --- /dev/null +++ b/infrastructure/crowdsec/values.yaml @@ -0,0 +1,132 @@ +# CrowdSec Helm chart values +# IP reputation + behavioral detection (Layer 0 of zero-trust stack) +# +# Components: +# LAPI - Central decision engine (PostgreSQL backend via CNPG) +# Agent - DaemonSet parsing pod logs from /var/log/pods/ +# Firewall Bouncer - Deployed separately (see firewall-bouncer.yaml) + +container_runtime: containerd + +lapi: + env: + # PostgreSQL password from CNPG auto-generated secret + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: pg-crowdsec-app + key: password + # CAPI enrollment (optional — user registers at app.crowdsec.net) + - name: ENROLL_KEY + valueFrom: + secretKeyRef: + name: crowdsec-capi-key + key: enrollment-key + optional: true + - name: ENROLL_INSTANCE_NAME + value: "homelabv5" + - name: ENROLL_TAGS + value: "k8s homelab talos" + # Registration token for agent auto-registration (from chart-generated secret) + - name: REGISTRATION_TOKEN + valueFrom: + secretKeyRef: + name: crowdsec-lapi-secrets + key: registrationToken + # Pre-register firewall bouncer API key + - name: BOUNCER_KEY_firewall + valueFrom: + secretKeyRef: + name: crowdsec-bouncer-key + key: api-key + optional: true + persistentVolume: + data: + enabled: false + config: + enabled: false + securityContext: + capabilities: + drop: + - ALL + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + metrics: + enabled: true + serviceMonitor: + enabled: true + dashboard: + enabled: false + strategy: + type: Recreate + +agent: + # Log acquisition sources + acquisition: + # Istio ingress gateway logs (all external traffic) + - namespace: istio-system + podName: istio-ingressgateway-* + program: nginx + poll_without_inotify: true + # Netbird management logs + - namespace: netbird + podName: netbird-management-* + program: netbird + poll_without_inotify: true + # Authentik server logs + - namespace: authentik + podName: authentik-server-* + program: authentik + poll_without_inotify: true + + env: + # Install community collections for log parsing + - name: COLLECTIONS + value: "crowdsecurity/linux crowdsecurity/nginx crowdsecurity/http-cve" + + securityContext: + capabilities: + drop: + - ALL + resources: + requests: + cpu: 25m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + metrics: + enabled: true + serviceMonitor: + enabled: true + + # Wait for LAPI to be ready (must be a map, not scalar) + wait_for_lapi: + image: + repository: busybox + tag: "1.28" + +# PostgreSQL database backend (CNPG) + auto-registration for agents +config: + config.yaml.local: | + db_config: + type: postgresql + user: app + password: ${DB_PASSWORD} + db_name: crowdsec + host: pg-crowdsec-rw.crowdsec.svc + port: 5432 + sslmode: prefer + api: + server: + auto_registration: + enabled: true + token: ${REGISTRATION_TOKEN} + allowed_ranges: + - "10.244.0.0/16" + - "127.0.0.1/32" diff --git a/infrastructure/envoy-gateway/manifests/edge.yaml b/infrastructure/envoy-gateway/manifests/edge.yaml new file mode 100644 index 0000000..c91d069 --- /dev/null +++ b/infrastructure/envoy-gateway/manifests/edge.yaml @@ -0,0 +1,313 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: gateway + +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: edge-proxy-config + namespace: gateway +spec: + provider: + type: Kubernetes + kubernetes: + envoyDeployment: + replicas: 1 + patch: + type: StrategicMerge + value: + spec: + template: + spec: + containers: + - name: envoy + resources: + requests: + cpu: 25m + memory: 128Mi + limits: + memory: 512Mi + - name: shutdown-manager + resources: + requests: + cpu: 5m + memory: 16Mi + limits: + memory: 64Mi + envoyHpa: + minReplicas: 1 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 + +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: edge-wildcard-kube-huskypup-net + namespace: gateway +spec: + secretName: edge-wildcard-kube-huskypup-net + issuerRef: + kind: ClusterIssuer + name: letsencrypt-production + dnsNames: + - "*.kube.huskypup.net" + - "*.gitlab.kube.huskypup.net" + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: edge + namespace: gateway +spec: + gatewayClassName: envoy-gateway + infrastructure: + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: edge-proxy-config + listeners: + - name: https + port: 443 + protocol: HTTPS + tls: + mode: Terminate + certificateRefs: + - kind: Secret + name: edge-wildcard-kube-huskypup-net + allowedRoutes: + namespaces: + from: All + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-argocd-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: argocd + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-authentik-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: authentik + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-grafana-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: grafana + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-prometheus-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: prometheus + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-vault-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: vault + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-rook-ceph-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: rook-ceph + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-mqtt-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: mqtt + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-n8n-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: n8n + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-nextcloud-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: nextcloud + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-home-assistant-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: home-assistant + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-frigate-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: frigate + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-teslamate-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: teslamate + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-guacamole-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: guacamole + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge + +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: allow-gitlab-attach-edge + namespace: gateway +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: gitlab + to: + - group: gateway.networking.k8s.io + kind: Gateway + name: edge diff --git a/infrastructure/envoy-gateway/manifests/gatewayclass.yaml b/infrastructure/envoy-gateway/manifests/gatewayclass.yaml new file mode 100644 index 0000000..0f1260f --- /dev/null +++ b/infrastructure/envoy-gateway/manifests/gatewayclass.yaml @@ -0,0 +1,6 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: envoy-gateway +spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller diff --git a/infrastructure/envoy-gateway/values.yaml b/infrastructure/envoy-gateway/values.yaml new file mode 100644 index 0000000..d2a4319 --- /dev/null +++ b/infrastructure/envoy-gateway/values.yaml @@ -0,0 +1,21 @@ +deployment: + replicas: 2 + envoyGateway: + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1024Mi + +# Envoy Gateway control-plane Service (xDS); data-plane Services are created per-Gateway. +service: + type: ClusterIP + +config: + envoyGateway: + extensionApis: + enableBackend: true + logging: + level: + default: info diff --git a/infrastructure/external-dns/manifests/secret-external-dns-unifi.yaml b/infrastructure/external-dns/manifests/secret-external-dns-unifi.yaml new file mode 100644 index 0000000..57bfba7 --- /dev/null +++ b/infrastructure/external-dns/manifests/secret-external-dns-unifi.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: external-dns-unifi-secret + namespace: external-dns +stringData: + api-key: 3Qha5hupHsCkkFyQb8z-T2kh-BasIGSH \ No newline at end of file diff --git a/infrastructure/external-dns/values.yaml b/infrastructure/external-dns/values.yaml new file mode 100644 index 0000000..b05db62 --- /dev/null +++ b/infrastructure/external-dns/values.yaml @@ -0,0 +1,69 @@ +fullnameOverride: external-dns-unifi +logLevel: &logLevel debug +provider: + name: webhook + webhook: + image: + repository: ghcr.io/kashalls/external-dns-unifi-webhook + tag: main # replace with a versioned release tag + env: + - name: UNIFI_HOST + value: https://172.28.110.1 # replace with the address to your UniFi router/controller + - name: UNIFI_EXTERNAL_CONTROLLER + value: "false" + - name: UNIFI_API_KEY + valueFrom: + secretKeyRef: + name: external-dns-unifi-secret + key: api-key + - name: LOG_LEVEL + value: *logLevel + livenessProbe: + httpGet: + path: /healthz + port: http-webhook + initialDelaySeconds: 10 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /readyz + port: http-webhook + initialDelaySeconds: 10 + timeoutSeconds: 5 +extraArgs: + # The UniFi webhook listens on localhost:8888 inside the pod. Use IPv4 + # explicitly to avoid external-dns attempting ::1. + - --webhook-provider-url=http://127.0.0.1:8888 +# We are switching ingress to Istio; manage records from the Istio ingress Service. +policy: upsert-only +sources: ["service"] + +# UniFi DNS doesn't support TXT records; use noop registry. +registry: noop +domainFilters: ["kube.huskypup.net"] # replace with your domain +securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: + - ALL + +resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 100m + memory: 256Mi + +serviceAccount: + create: true + name: "external-dns" +# No Ingress watching (Istio VirtualServices are used). + +# Enable Prometheus metrics +serviceMonitor: + enabled: true + additionalLabels: + release: prometheus + interval: 30s diff --git a/infrastructure/external-secrets/values.yaml b/infrastructure/external-secrets/values.yaml new file mode 100644 index 0000000..1b4551c --- /dev/null +++ b/infrastructure/external-secrets/values.yaml @@ -0,0 +1 @@ +installCRDs: true diff --git a/infrastructure/grafana/dashboards/dashboards-configmap.yaml b/infrastructure/grafana/dashboards/dashboards-configmap.yaml new file mode 100644 index 0000000..5124aa7 --- /dev/null +++ b/infrastructure/grafana/dashboards/dashboards-configmap.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboards-provider + namespace: grafana +data: + dashboards.yaml: | + apiVersion: 1 + providers: + - name: 'default' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/default + - name: 'kubernetes' + orgId: 1 + folder: 'Kubernetes' + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/kubernetes + - name: 'infrastructure' + orgId: 1 + folder: 'Infrastructure' + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/infrastructure diff --git a/infrastructure/grafana/dashboards/teslamate-dashboards.yaml b/infrastructure/grafana/dashboards/teslamate-dashboards.yaml new file mode 100644 index 0000000..fb4410e --- /dev/null +++ b/infrastructure/grafana/dashboards/teslamate-dashboards.yaml @@ -0,0 +1,1761 @@ +--- +# TeslaMate Dashboards - Battery Health +apiVersion: v1 +kind: ConfigMap +metadata: + name: teslamate-battery-health + namespace: grafana + labels: + grafana_dashboard: "1" +data: + battery-health.json: | + { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [ + { + "icon": "dashboard", + "tags": [], + "title": "TeslaMate", + "tooltip": "", + "type": "link", + "url": "${base_url:raw}" + }, + { + "asDropdown": true, + "icon": "external link", + "tags": [ + "tesla" + ], + "title": "Dashboards", + "type": "dashboards" + } + ], + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "description": "**Usable (now)** is the estimated current battery capacity. It is average of the estimated capacity reported by the last 10 charging sessions to have a better estimation.\n\nIf you see just '1.0 kWh' here, it means that you need at least a long charge session.\n\n**Usable (new)** is the estimated Battery Capacity since you begun to use TeslaMate. That's why, the more data you have logged from your brand new car the better. For those who have not used TeslaMate since they got their new car, or for those who have bought it second hand, it's possible to set the max range to 100% and the battery capacity of the car battery when it was new in order to get a better and accurate estimation.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "super-light-blue", + "value": 0 + }, + { + "color": "dark-red", + "value": 1 + }, + { + "color": "super-light-blue", + "value": 2 + } + ] + }, + "unit": "kwatth" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 13, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value_and_name", + "wideLayout": true + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT \n CASE WHEN $custom_kwh_new > 0 THEN $custom_kwh_new ELSE ('$aux'::json ->> 'MaxCapacity')::float END as \"Usable (new)\", \n ('$aux'::json ->> 'CurrentCapacity')::float as \"Usable (now)\",\n ('$aux'::json ->> 'CurrentCapacity')::float - CASE WHEN $custom_kwh_new > 0 THEN $custom_kwh_new ELSE ('$aux'::json ->> 'MaxCapacity')::float END as \"Difference\"", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Battery Capacity", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "super-light-blue", + "value": 0 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*_km/" + }, + "properties": [ + { + "id": "unit", + "value": "lengthkm" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*_mi/" + }, + "properties": [ + { + "id": "unit", + "value": "lengthmi" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/maxrange_.*/" + }, + "properties": [ + { + "id": "displayName", + "value": "Max range (new)" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/currentrange_.*/" + }, + "properties": [ + { + "id": "displayName", + "value": "Max range (now)" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/range_lost.*/" + }, + "properties": [ + { + "id": "displayName", + "value": "Range lost" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 0 + }, + "id": 14, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value_and_name", + "wideLayout": true + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT \n CASE WHEN $custom_max_range > 0 THEN $custom_max_range ELSE ('$aux'::json ->> 'MaxRange')::float END as \"maxrange_$length_unit\",\n ('$aux'::json ->> 'CurrentRange')::float as \"currentrange_$length_unit\",\n CASE WHEN $custom_max_range > 0 THEN $custom_max_range ELSE ('$aux'::json ->> 'MaxRange')::float END - ('$aux'::json ->> 'CurrentRange')::float as \"range_lost_$length_unit\"", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Ranges [$preferred_range]", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "description": "\"Logged\" is the distance traveled that is saved on TeslaMate database.\n\n\"Mileage\" is the distance the car has traveled since using TeslaMate.\n\nSo, if there is a difference between both values, it is the distance that for some reason a drive hasn't been fully recorded, for example due to a bug or an unexpected restart and that TeslaMate has not been able to record, either due to lack of connection, areas without signal, or that it has been out of service.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "super-light-blue", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 12, + "y": 0 + }, + "id": 37, + "links": [ + { + "targetBlank": true, + "title": "Drive Stats", + "url": "/d/_7WkNSyWk/drive-stats" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/.*/", + "values": false + }, + "showPercentChange": false, + "textMode": "value_and_name", + "wideLayout": true + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "select ROUND(convert_km(sum(distance)::numeric, '$length_unit'),0)|| ' $length_unit' as \"Logged\"\r\nfrom drives \r\nwhere car_id = $car_id;\r\n", + "refId": "Logged", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT ROUND(convert_km((max(end_km) - min(start_km))::numeric, '$length_unit'),0)|| ' $length_unit' as \"Mileage\"\nFROM drives WHERE car_id = $car_id;", + "refId": "Mileage", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT ROUND(convert_km(max(end_km)::numeric, '$length_unit'),0) || ' $length_unit' as \"Odometer\"\nFROM drives WHERE car_id = $car_id;", + "refId": "Odometer", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT (\r\n (SELECT ROUND(convert_km((max(end_km) - min(start_km))::numeric, '$length_unit'),0) FROM drives WHERE car_id = $car_id) - \r\n (SELECT ROUND(convert_km(sum(distance)::numeric, '$length_unit'),0) from drives where car_id = $car_id) || ' $length_unit'\r\n)\r\nAS \"Data lost (not logged)\"", + "refId": "Data Lost", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Drive Stats", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "decimals": 2, + "mappings": [], + "unit": "kwatth" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "AC" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "semi-dark-green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "DC" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 13, + "w": 6, + "x": 18, + "y": 0 + }, + "id": 34, + "maxDataPoints": 3, + "options": { + "displayLabels": [ + "name", + "percent", + "value" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": false, + "values": [ + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "hideZeros": false, + "maxHeight": 600, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "WITH data AS (\n SELECT\n\t\tcp.id,\n\t\tcp.charge_energy_added,\n\t\tCASE WHEN NULLIF(mode() within group (order by charger_phases),0) is null THEN 'DC'\n\t\t\t\t ELSE 'AC'\n\t\tEND AS current,\n\t\tcp.charge_energy_used\n\tFROM charging_processes cp\n RIGHT JOIN charges ON cp.id = charges.charging_process_id\n WHERE\n\t cp.car_id = $car_id\n\t AND cp.charge_energy_added > 0.01\n GROUP BY 1,2\n)\nSELECT\n\tnow() AS time,\n\tSUM(GREATEST(charge_energy_added, charge_energy_used)) AS value,\n\tcurrent AS metric\nFROM data\nGROUP BY 3\nORDER BY metric DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "AC/DC - Energy Used", + "type": "piechart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "description": "This dashboard is meant to have a look of the Battery health based on the data logged in TeslaMate. So, the more data you have logged from your brand new car the better.\n\n**Degradation** is just an estimated value to have a reference, measured on **usable battery level** of every charging session with enough kWh added (in order to avoid dirty data from the sample), calculated according to the rated efficiency of the car.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "#EAB839", + "value": 10 + }, + { + "color": "red", + "value": 20 + }, + { + "color": "dark-red", + "value": 30 + } + ] + }, + "unit": "%" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 6 + }, + "id": 17, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [], + "fields": "/^greatest$/", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT GREATEST(0, 100.0 - (('$aux'::json ->> 'CurrentCapacity')::float * 100.0 / CASE WHEN $custom_kwh_new > 0 THEN $custom_kwh_new ELSE ('$aux'::json ->> 'MaxCapacity')::float END))\n\n", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Estimated Degradation", + "type": "gauge" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "max": 100, + "min": 1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-red", + "value": 0 + }, + { + "color": "#EAB839", + "value": 80 + }, + { + "color": "light-green", + "value": 90 + } + ] + }, + "unit": "%" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 6 + }, + "id": 12, + "options": { + "displayMode": "gradient", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 10, + "minVizWidth": 0, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT \n LEAST(100, (100 - GREATEST(0, 100.0 - (('$aux'::json ->> 'CurrentCapacity')::float * 100.0 / CASE WHEN $custom_kwh_new > 0 THEN $custom_kwh_new ELSE ('$aux'::json ->> 'MaxCapacity')::float END)))) as \"Battery Health (%)\"\n \n", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Battery Health", + "type": "bargauge" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "description": "\"# of Charging cycles\" is estimated by dividing the whole energy added to the battery by the battery capacity when new.\n\n\"Charging Efficiency\" is estimated on the difference between energy used from the charger and energy added to the battery.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-yellow", + "value": 0 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Total Energy added" + }, + "properties": [ + { + "id": "unit", + "value": "kwatth" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Energy used" + }, + "properties": [ + { + "id": "unit", + "value": "kwatth" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Charging Efficiency" + }, + "properties": [ + { + "id": "unit", + "value": "percentunit" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 6 + }, + "id": 36, + "links": [ + { + "targetBlank": true, + "title": "Charging Stats", + "url": "/d/-pkIkhmRz/charging-stats" + } + ], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "mean" + ] + }, + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value_and_name", + "wideLayout": true + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n\tCOUNT(*) AS \"# of Charges\"\r\nFROM\r\n\tcharging_processes\r\nWHERE\r\n\tcar_id = $car_id AND charge_energy_added > 0.01\r\n\t", + "refId": "# of Charges", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\n\tfloor(sum(charge_energy_added) / CASE WHEN $custom_kwh_new > 0 THEN $custom_kwh_new ELSE ('$aux'::json ->> 'MaxCapacity')::float END) AS \"# of Charging cycles\"\nFROM charging_processes WHERE car_id = $car_id AND charge_energy_added > 0.01", + "refId": "# of Charging cycles", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\n\tsum(charge_energy_added) as \"Total Energy added\"\nFROM\n\tcharging_processes\nWHERE\n\tcar_id = $car_id AND charge_energy_added > 0.01", + "refId": "Total Energy added", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n\tSUM(greatest(charge_energy_added, charge_energy_used)) AS \"Total Energy used\"\r\nFROM\r\n\tcharging_processes\r\nWHERE\r\n\tcar_id = $car_id AND charge_energy_added > 0.01\r\n", + "refId": "Total Energy used", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n\tSUM(charge_energy_added) / SUM(greatest(charge_energy_added, charge_energy_used)) AS \"Charging Efficiency\"\r\nFROM\r\n\tcharging_processes\r\nWHERE\r\n\tcar_id = $car_id AND charge_energy_added > 0.01\r\n", + "refId": "Charging Efficiency", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Charging Stats", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": 0 + } + ] + }, + "unit": "%" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 6, + "y": 9 + }, + "id": 25, + "options": { + "displayMode": "lcd", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 10, + "minVizWidth": 0, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "text": {}, + "valueMode": "color" + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT * FROM ((SELECT usable_battery_level, date\r\nFROM positions\r\nWHERE car_id = $car_id AND usable_battery_level IS NOT NULL\r\nORDER BY date DESC\r\nLIMIT 1)\r\nUNION\r\n(SELECT usable_battery_level, date\r\nFROM charges c\r\nJOIN charging_processes p ON p.id = c.charging_process_id\r\nWHERE p.car_id = $car_id AND usable_battery_level IS NOT NULL\r\nORDER BY date DESC\r\nLIMIT 1)) AS last_usable_battery_level LIMIT 1", + "refId": "SOC", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n 0 as lowest,\r\n 20 as lower,\r\n CASE WHEN lfp_battery THEN 100 ELSE 81 END as upper\r\nfrom cars inner join car_settings on cars.settings_id = car_settings.id\r\nwhere cars.id = $car_id", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Current SOC", + "transformations": [ + { + "id": "configFromData", + "options": { + "applyTo": { + "id": "byFrameRefID", + "options": "SOC" + }, + "configRefId": "A", + "mappings": [ + { + "fieldName": "lower", + "handlerArguments": { + "threshold": { + "color": "green" + } + }, + "handlerKey": "threshold1" + }, + { + "fieldName": "upper", + "handlerArguments": { + "threshold": { + "color": "orange" + } + }, + "handlerKey": "threshold1" + }, + { + "fieldName": "lowest", + "handlerKey": "threshold1" + } + ] + } + } + ], + "type": "bargauge" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "description": "This is the Derived Rated Efficiency that TeslaMate calculates based on battery charges. \nThis information can be seen in more detail on the \"Efficiency\" dashboard.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "super-light-blue", + "value": 0 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*_km/" + }, + "properties": [ + { + "id": "unit", + "value": "Wh/km" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*_mi/" + }, + "properties": [ + { + "id": "unit", + "value": "Wh/mi" + } + ] + } + ] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 10, + "y": 9 + }, + "id": 32, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/.*/", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT ('$aux'::json ->> 'RatedEfficiency')::float * 10 / convert_km(1, '$length_unit') AS efficiency_$length_unit", + "refId": "Logged", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Efficiency", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": 0 + }, + { + "color": "dark-green", + "value": 7.84 + }, + { + "color": "semi-dark-orange", + "value": 31.36 + }, + { + "color": "light-blue", + "value": 35.28 + } + ] + }, + "unit": "kwatth" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 6, + "y": 11 + }, + "id": 27, + "options": { + "displayMode": "gradient", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 10, + "minVizWidth": 0, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^kwh$/", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "text": {}, + "valueMode": "color" + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT * FROM ((SELECT usable_battery_level * ('$aux'::json ->> 'CurrentCapacity')::float / 100 as kWh, date, ('$aux'::json ->> 'CurrentCapacity')::float as Total\nFROM positions\nWHERE car_id = $car_id AND usable_battery_level IS NOT NULL\nORDER BY date DESC\nLIMIT 1)\nUNION\n(SELECT battery_level * ('$aux'::json ->> 'CurrentCapacity')::float / 100 as kWh, date, ('$aux'::json ->> 'CurrentCapacity')::float as Total\nFROM charges c\nJOIN charging_processes p ON p.id = c.charging_process_id\nWHERE p.car_id = $car_id AND usable_battery_level IS NOT NULL\nORDER BY date DESC\nLIMIT 1)) AS last_usable_battery_level LIMIT 1", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Current Stored Energy", + "type": "bargauge" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-RdYlGr", + "seriesBy": "last" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 50, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 2, + "pointShape": "circle", + "pointSize": { + "fixed": 5 + }, + "pointStrokeWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "show": "points" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Median" + }, + "properties": [ + { + "id": "custom.show", + "value": "lines" + }, + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Odometer" + }, + "properties": [ + { + "id": "displayName", + "value": "Mileage" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "kWh" + }, + "properties": [ + { + "id": "displayName", + "value": "Battery Capacity" + }, + { + "id": "unit", + "value": "kwatth" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 13 + }, + "id": 28, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "mapping": "manual", + "series": [ + { + "color": { + "matcher": { + "id": "byName", + "options": "kWh" + } + }, + "frame": { + "matcher": { + "id": "byIndex", + "options": 0 + } + }, + "x": { + "matcher": { + "id": "byName", + "options": "Odometer" + } + }, + "y": { + "matcher": { + "id": "byName", + "options": "kWh" + } + } + }, + { + "frame": { + "matcher": { + "id": "byIndex", + "options": 1 + } + }, + "x": { + "matcher": { + "id": "byName", + "options": "Odometer" + } + }, + "y": { + "matcher": { + "id": "byName", + "options": "kWh" + } + } + } + ], + "tooltip": { + "hideZeros": false, + "maxHeight": 600, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT convert_km(AVG(p.odometer)::numeric,'$length_unit') AS \"Odometer\", \r\n\tAVG(c.rated_battery_range_km * ('$aux'::json ->> 'RatedEfficiency')::float / c.usable_battery_level) AS \"kWh\",\r\n\t--MAX(cp.id) AS id,\r\n\tto_char(timezone('$__timezone', timezone('UTC', cp.end_date)), 'YYYY-MM-dd') AS \"Date\"\r\n\tFROM charging_processes cp\r\n\t\tJOIN (SELECT charging_process_id, MAX(date) as date\tFROM charges WHERE usable_battery_level > 0 GROUP BY charging_process_id) AS last_charges\tON cp.id = last_charges.charging_process_id\r\n\t\tINNER JOIN charges c\r\n\t\tON c.charging_process_id = cp.id AND c.date = last_charges.date\r\n\t\tINNER JOIN positions p ON p.id = cp.position_id\r\n\tWHERE cp.car_id = $car_id\r\n\t\tAND cp.end_date IS NOT NULL\r\n\t\tAND cp.charge_energy_added >= ('$aux'::json ->> 'RatedEfficiency')::float\r\n\tGROUP BY 3", + "refId": "Projected Range", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT \n ROUND(MIN(convert_km(p.odometer::numeric,'$length_unit')),0) AS \"Odometer\",\n\tROUND(PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY c.rated_battery_range_km * ('$aux'::json ->> 'RatedEfficiency')::float / c.usable_battery_level)::numeric,1) AS \"kWh\",\n\tto_char(timezone('$__timezone', timezone('UTC', cp.end_date)), 'YYYYMM') || CASE WHEN to_char(timezone('$__timezone', timezone('UTC', cp.end_date)), 'DD')::int <= 15 THEN '1' ELSE '2' END AS Title\n\tFROM charging_processes cp\n\t\tJOIN (SELECT charging_process_id, MAX(date) as date\tFROM charges WHERE usable_battery_level > 0 GROUP BY charging_process_id) AS last_charges\tON cp.id = last_charges.charging_process_id\n\t\tINNER JOIN charges c\n\t\tON c.charging_process_id = cp.id AND c.date = last_charges.date\n\t\tINNER JOIN positions p ON p.id = cp.position_id\n\tWHERE cp.car_id = $car_id\n\t\tAND cp.end_date IS NOT NULL\n\t\tAND cp.charge_energy_added >= ('$aux'::json ->> 'RatedEfficiency')::float\n\tGROUP BY 3", + "refId": "Median", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Battery Capacity by Mileage", + "type": "xychart" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 41, + "tags": [ + "tesla" + ], + "templating": { + "list": [ + { + "current": {}, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "definition": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC", + "includeAll": false, + "label": "Car", + "name": "car_id", + "options": [], + "query": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC", + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "definition": "SELECT unit_of_length FROM settings LIMIT 1", + "hide": 2, + "includeAll": false, + "name": "length_unit", + "options": [], + "query": "SELECT unit_of_length FROM settings LIMIT 1", + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "definition": "SELECT preferred_range FROM settings LIMIT 1", + "hide": 2, + "includeAll": false, + "name": "preferred_range", + "options": [], + "query": "SELECT preferred_range FROM settings LIMIT 1", + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "definition": "SELECT base_url FROM settings LIMIT 1", + "hide": 2, + "includeAll": false, + "name": "base_url", + "options": [], + "query": "SELECT base_url FROM settings LIMIT 1", + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "TeslaMate" + }, + "definition": "WITH Aux as (\n SELECT \n car_id,\n COALESCE(derived_efficiency, car_efficiency) AS efficiency\n FROM (\n SELECT\n ROUND((charge_energy_added / NULLIF(end_rated_range_km - start_rated_range_km, 0))::numeric, 3) * 100 AS derived_efficiency,\n COUNT(*) as count,\n cars.id as car_id,\n cars.efficiency * 100 AS car_efficiency\n FROM cars\n LEFT JOIN charging_processes ON\n cars.id = charging_processes.car_id \n AND duration_min > 10\n AND end_battery_level <= 95\n AND start_rated_range_km IS NOT NULL\n AND end_rated_range_km IS NOT NULL\n AND charge_energy_added > 0\n WHERE cars.id = $car_id\n GROUP BY 1, 3, 4\n ORDER BY 2 DESC\n LIMIT 1\n ) AS Efficiency\n),\n\nCurrentCapacity AS (\n SELECT\n AVG(Capacity) AS Capacity\n FROM (\n SELECT \n c.rated_battery_range_km * aux.efficiency / c.usable_battery_level AS Capacity\n FROM charging_processes cp\n INNER JOIN charges c ON c.charging_process_id = cp.id \n INNER JOIN aux ON cp.car_id = aux.car_id\n WHERE\n cp.car_id = $car_id\n AND cp.end_date IS NOT NULL\n AND cp.charge_energy_added >= aux.efficiency\n AND c.usable_battery_level > 0\n ORDER BY cp.end_date DESC, c.date desc\n LIMIT 100\n ) AS lastCharges\n),\n\nMaxCapacity AS (\n SELECT \n MAX(c.rated_battery_range_km * aux.efficiency / c.usable_battery_level) AS Capacity\n FROM charging_processes cp\n INNER JOIN (\n SELECT\n charging_process_id,\n MAX(date) as date FROM charges WHERE usable_battery_level > 0 GROUP BY charging_process_id\n ) AS gcharges ON\n cp.id = gcharges.charging_process_id\n INNER JOIN charges c ON\n c.charging_process_id = cp.id\n AND c.date = gcharges.date\n INNER JOIN aux ON cp.car_id = aux.car_id\n WHERE\n cp.car_id = $car_id\n AND cp.end_date IS NOT NULL\n AND cp.charge_energy_added >= aux.efficiency\n),\n\nCurrentRange AS (\n SELECT\n (range * 100.0 / usable_battery_level) AS range\n FROM (\n (\n SELECT\n date,\n ${preferred_range}_battery_range_km AS range,\n usable_battery_level AS usable_battery_level\n FROM positions\n WHERE\n car_id = $car_id\n AND ideal_battery_range_km IS NOT NULL\n AND usable_battery_level > 0 \n ORDER BY date DESC\n LIMIT 1\n )\n UNION ALL\n (\n SELECT date,\n ${preferred_range}_battery_range_km AS range,\n usable_battery_level as usable_battery_level\n FROM charges c\n INNER JOIN charging_processes p ON p.id = c.charging_process_id\n WHERE\n p.car_id = $car_id\n AND usable_battery_level > 0\n ORDER BY date DESC\n LIMIT 1\n )\n ) AS data\n ORDER BY date DESC\n LIMIT 1\n),\n\nMaxRange AS (\n SELECT\n floor(extract(epoch from date)/86400)*86400 AS time,\n CASE\n WHEN sum(usable_battery_level) = 0 THEN sum(${preferred_range}_battery_range_km) * 100\n ELSE sum(${preferred_range}_battery_range_km) / sum(usable_battery_level) * 100\n END AS range\n FROM (\n SELECT\n battery_level,\n usable_battery_level,\n date,\n ${preferred_range}_battery_range_km\n FROM charges c \n INNER JOIN charging_processes p ON p.id = c.charging_process_id \n WHERE\n p.car_id = $car_id\n AND usable_battery_level IS NOT NULL\n ) AS data\n GROUP BY 1\n ORDER BY 2 DESC\n LIMIT 1\n),\n\nBase AS (\n SELECT NULL\n)\n\nSELECT\n json_build_object(\n 'MaxRange', convert_km(MaxRange.range,'$length_unit'),\n 'CurrentRange', convert_km(CurrentRange.range,'$length_unit'),\n 'MaxCapacity', MaxCapacity.Capacity,\n 'CurrentCapacity', CASE WHEN CurrentCapacity.Capacity IS NULL THEN 1 ELSE CurrentCapacity.Capacity END,\n 'RatedEfficiency', aux.efficiency\n )\nFROM Base\n LEFT JOIN MaxRange ON true\n LEFT JOIN CurrentRange ON true\n LEFT JOIN Aux ON true\n LEFT JOIN MaxCapacity ON true\n LEFT JOIN CurrentCapacity ON true", + "hide": 2, + "includeAll": false, + "name": "aux", + "options": [], + "query": "WITH Aux as (\n SELECT \n car_id,\n COALESCE(derived_efficiency, car_efficiency) AS efficiency\n FROM (\n SELECT\n ROUND((charge_energy_added / NULLIF(end_rated_range_km - start_rated_range_km, 0))::numeric, 3) * 100 AS derived_efficiency,\n COUNT(*) as count,\n cars.id as car_id,\n cars.efficiency * 100 AS car_efficiency\n FROM cars\n LEFT JOIN charging_processes ON\n cars.id = charging_processes.car_id \n AND duration_min > 10\n AND end_battery_level <= 95\n AND start_rated_range_km IS NOT NULL\n AND end_rated_range_km IS NOT NULL\n AND charge_energy_added > 0\n WHERE cars.id = $car_id\n GROUP BY 1, 3, 4\n ORDER BY 2 DESC\n LIMIT 1\n ) AS Efficiency\n),\n\nCurrentCapacity AS (\n SELECT\n AVG(Capacity) AS Capacity\n FROM (\n SELECT \n c.rated_battery_range_km * aux.efficiency / c.usable_battery_level AS Capacity\n FROM charging_processes cp\n INNER JOIN charges c ON c.charging_process_id = cp.id \n INNER JOIN aux ON cp.car_id = aux.car_id\n WHERE\n cp.car_id = $car_id\n AND cp.end_date IS NOT NULL\n AND cp.charge_energy_added >= aux.efficiency\n AND c.usable_battery_level > 0\n ORDER BY cp.end_date DESC, c.date desc\n LIMIT 100\n ) AS lastCharges\n),\n\nMaxCapacity AS (\n SELECT \n MAX(c.rated_battery_range_km * aux.efficiency / c.usable_battery_level) AS Capacity\n FROM charging_processes cp\n INNER JOIN (\n SELECT\n charging_process_id,\n MAX(date) as date FROM charges WHERE usable_battery_level > 0 GROUP BY charging_process_id\n ) AS gcharges ON\n cp.id = gcharges.charging_process_id\n INNER JOIN charges c ON\n c.charging_process_id = cp.id\n AND c.date = gcharges.date\n INNER JOIN aux ON cp.car_id = aux.car_id\n WHERE\n cp.car_id = $car_id\n AND cp.end_date IS NOT NULL\n AND cp.charge_energy_added >= aux.efficiency\n),\n\nCurrentRange AS (\n SELECT\n (range * 100.0 / usable_battery_level) AS range\n FROM (\n (\n SELECT\n date,\n ${preferred_range}_battery_range_km AS range,\n usable_battery_level AS usable_battery_level\n FROM positions\n WHERE\n car_id = $car_id\n AND ideal_battery_range_km IS NOT NULL\n AND usable_battery_level > 0 \n ORDER BY date DESC\n LIMIT 1\n )\n UNION ALL\n (\n SELECT date,\n ${preferred_range}_battery_range_km AS range,\n usable_battery_level as usable_battery_level\n FROM charges c\n INNER JOIN charging_processes p ON p.id = c.charging_process_id\n WHERE\n p.car_id = $car_id\n AND usable_battery_level > 0\n ORDER BY date DESC\n LIMIT 1\n )\n ) AS data\n ORDER BY date DESC\n LIMIT 1\n),\n\nMaxRange AS (\n SELECT\n floor(extract(epoch from date)/86400)*86400 AS time,\n CASE\n WHEN sum(usable_battery_level) = 0 THEN sum(${preferred_range}_battery_range_km) * 100\n ELSE sum(${preferred_range}_battery_range_km) / sum(usable_battery_level) * 100\n END AS range\n FROM (\n SELECT\n battery_level,\n usable_battery_level,\n date,\n ${preferred_range}_battery_range_km\n FROM charges c \n INNER JOIN charging_processes p ON p.id = c.charging_process_id \n WHERE\n p.car_id = $car_id\n AND usable_battery_level IS NOT NULL\n ) AS data\n GROUP BY 1\n ORDER BY 2 DESC\n LIMIT 1\n),\n\nBase AS (\n SELECT NULL\n)\n\nSELECT\n json_build_object(\n 'MaxRange', convert_km(MaxRange.range,'$length_unit'),\n 'CurrentRange', convert_km(CurrentRange.range,'$length_unit'),\n 'MaxCapacity', MaxCapacity.Capacity,\n 'CurrentCapacity', CASE WHEN CurrentCapacity.Capacity IS NULL THEN 1 ELSE CurrentCapacity.Capacity END,\n 'RatedEfficiency', aux.efficiency\n )\nFROM Base\n LEFT JOIN MaxRange ON true\n LEFT JOIN CurrentRange ON true\n LEFT JOIN Aux ON true\n LEFT JOIN MaxCapacity ON true\n LEFT JOIN CurrentCapacity ON true", + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "current": { + "text": "0", + "value": "0" + }, + "description": "Set the capacity of your car battery when it was new, in case you started using TeslaMate after a while of having it. If not, leave it at 0, it will be calculated with the data that is logged in TeslaMate", + "label": "Custom Battery Capacity (kWh) when new", + "name": "custom_kwh_new", + "options": [ + { + "selected": true, + "text": "0", + "value": "0" + } + ], + "query": "0", + "type": "textbox" + }, + { + "current": { + "text": "0", + "value": "0" + }, + "description": "Set the max range to 100% of your car when it was new, in case you started using TeslaMate after a while of having it. If not, leave it at 0, the degradation will be calculated with the data that is logged in TeslaMate", + "label": "Custom Max Range when new", + "name": "custom_max_range", + "options": [ + { + "selected": true, + "text": "0", + "value": "0" + } + ], + "query": "0", + "type": "textbox" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "hidden": true + }, + "timezone": "browser", + "title": "Battery Health", + "uid": "jchmRiqUfXgTM", + "version": 1 + } \ No newline at end of file diff --git a/infrastructure/grafana/manifests/envoy-route.yaml b/infrastructure/grafana/manifests/envoy-route.yaml new file mode 100644 index 0000000..e5b87a4 --- /dev/null +++ b/infrastructure/grafana/manifests/envoy-route.yaml @@ -0,0 +1,20 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: grafana + namespace: grafana +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - grafana.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: grafana + port: 80 diff --git a/infrastructure/grafana/manifests/grafana-oauth-secret.yaml b/infrastructure/grafana/manifests/grafana-oauth-secret.yaml new file mode 100644 index 0000000..e394f60 --- /dev/null +++ b/infrastructure/grafana/manifests/grafana-oauth-secret.yaml @@ -0,0 +1,12 @@ +# infrastructure/grafana/grafana-oauth-secret.yaml +# Grafana OAuth secret - hardcoded from blueprint + +apiVersion: v1 +kind: Secret +metadata: + name: grafana-authentik-oauth + namespace: grafana +type: Opaque +stringData: + GF_AUTH_GENERIC_OAUTH_CLIENT_ID: "bd03e9139dd2063c6c44c4d2f65f51d69de3ba0b6d6b1b9b41c255d2376d2dcc" + GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: "6b6f2deecfe8fd56cae9c512cc71eedd463d67ff08f24c816b15b15e78ce36bc4f06a2276c5ffe3f67799d935994a32ed32cdc02ec674f279c82d7cfe3ca05d5" \ No newline at end of file diff --git a/infrastructure/grafana/manifests/istio-virtualservice.yaml b/infrastructure/grafana/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..4b18055 --- /dev/null +++ b/infrastructure/grafana/manifests/istio-virtualservice.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: grafana + namespace: grafana +spec: + hosts: + - grafana.kube.huskypup.net + gateways: + - istio-system/edge + http: + - route: + - destination: + host: grafana.grafana.svc.cluster.local + port: + number: 80 diff --git a/infrastructure/grafana/manifests/teslamate-datasource-secret.yaml b/infrastructure/grafana/manifests/teslamate-datasource-secret.yaml new file mode 100644 index 0000000..c3d86c8 --- /dev/null +++ b/infrastructure/grafana/manifests/teslamate-datasource-secret.yaml @@ -0,0 +1,11 @@ +--- +# Secret containing TeslaMate database password for Grafana datasource +# This is created by the Grafana presync hook from the CNPG secret +# +# The secret is referenced in values.yaml: +# envFromSecrets: +# - name: grafana-teslamate-datasource +# +# And used in the datasource configuration: +# secureJsonData: +# password: $__env{TESLAMATE_DB_PASSWORD} diff --git a/infrastructure/grafana/manifests/teslamate-password-sync-cronjob.yaml b/infrastructure/grafana/manifests/teslamate-password-sync-cronjob.yaml new file mode 100644 index 0000000..46087a5 --- /dev/null +++ b/infrastructure/grafana/manifests/teslamate-password-sync-cronjob.yaml @@ -0,0 +1,195 @@ +--- +# CronJob to sync TeslaMate database password from CNPG secret to Grafana datasource secret +# This ensures Grafana always has the current password even when CNPG rotates it +apiVersion: batch/v1 +kind: CronJob +metadata: + name: sync-teslamate-password + namespace: grafana +spec: + # Run every 30 minutes to catch password rotations (rotations happen at most daily) + schedule: "*/30 * * * *" + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + concurrencyPolicy: Forbid + jobTemplate: + spec: + template: + metadata: + labels: + app: teslamate-password-sync + spec: + serviceAccountName: teslamate-password-sync + restartPolicy: OnFailure + containers: + - name: sync + image: docker.io/alpine/k8s:1.32.13 + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + volumeMounts: + - name: tmp + mountPath: /tmp + env: + - name: GRAFANA_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: grafana-admin-secret + key: admin-password + optional: true + command: + - /bin/bash + - -c + - | + set -e + + echo "Fetching current password from CNPG secret in teslamate namespace..." + CURRENT_PASSWORD=$(kubectl get secret -n teslamate pg-teslamate-app -o jsonpath='{.data.password}' | base64 -d) + + echo "Fetching current password from Grafana datasource secret..." + GRAFANA_PASSWORD=$(kubectl get secret -n grafana grafana-teslamate-datasource -o jsonpath='{.data.TESLAMATE_DB_PASSWORD}' | base64 -d) + + if [ "$CURRENT_PASSWORD" != "$GRAFANA_PASSWORD" ]; then + echo "Passwords differ - updating Grafana secret..." + kubectl create secret generic grafana-teslamate-datasource \ + --from-literal=TESLAMATE_DB_PASSWORD="$CURRENT_PASSWORD" \ + -n grafana \ + --dry-run=client -o yaml | kubectl apply -f - + echo "Password synced to secret" + else + echo "Passwords match in secrets" + fi + + echo "Finding running Grafana pod..." + GRAFANA_POD=$(kubectl get pod -n grafana -l app.kubernetes.io/name=grafana --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') + + if [ -z "$GRAFANA_POD" ]; then + echo "No running Grafana pod found, skipping API update" + exit 0 + fi + + echo "Using Grafana pod: $GRAFANA_POD" + echo "Updating Grafana datasource password via API (ensures password works after Grafana restart)..." + + # Get admin password from Grafana secret (fallback to 'admin' if not found) + ADMIN_PASS=$(kubectl get secret -n grafana grafana -o jsonpath='{.data.admin-password}' 2>/dev/null | base64 -d || echo "admin") + + # Update datasource via API with current password + RESULT=$(kubectl exec -n grafana "$GRAFANA_POD" -c grafana -- curl -s -X PUT \ + -H "Content-Type: application/json" \ + -u "admin:$ADMIN_PASS" \ + http://localhost:3000/api/datasources/uid/TeslaMate \ + -d "{ + \"name\": \"TeslaMate\", + \"type\": \"grafana-postgresql-datasource\", + \"uid\": \"TeslaMate\", + \"url\": \"pg-teslamate-rw.teslamate.svc.cluster.local:5432\", + \"database\": \"teslamate\", + \"user\": \"teslamate\", + \"access\": \"proxy\", + \"isDefault\": false, + \"secureJsonData\": { + \"password\": \"$CURRENT_PASSWORD\" + }, + \"jsonData\": { + \"sslmode\": \"disable\", + \"postgresVersion\": 1600, + \"timescaledb\": false, + \"database\": \"teslamate\" + } + }") + + echo "API Response: $RESULT" + + # Test datasource connection + echo "Testing datasource connection..." + TEST_RESULT=$(kubectl exec -n grafana "$GRAFANA_POD" -c grafana -- curl -s -X POST \ + -u "admin:$ADMIN_PASS" \ + http://localhost:3000/api/datasources/uid/TeslaMate/health) + + echo "Health Check: $TEST_RESULT" + + if echo "$TEST_RESULT" | grep -q '"status":"OK"'; then + echo "✅ Datasource password updated and verified successfully!" + else + echo "⚠️ Datasource updated but connection test failed" + exit 1 + fi + + echo "Done!" + volumes: + - name: tmp + emptyDir: {} +--- +# ServiceAccount for the CronJob +apiVersion: v1 +kind: ServiceAccount +metadata: + name: teslamate-password-sync + namespace: grafana +--- +# Role with permissions to read CNPG secret and update Grafana secret +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: teslamate-password-sync + namespace: grafana +rules: +- apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create", "patch", "update"] +- apiGroups: [""] + resources: ["pods", "pods/exec"] + verbs: ["get", "list", "watch", "create"] +- apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "patch"] +--- +# Role to read secret from teslamate namespace +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: teslamate-password-sync + namespace: teslamate +rules: +- apiGroups: [""] + resources: ["secrets"] + resourceNames: ["pg-teslamate-app"] + verbs: ["get"] +--- +# RoleBinding in grafana namespace +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: teslamate-password-sync + namespace: grafana +subjects: +- kind: ServiceAccount + name: teslamate-password-sync + namespace: grafana +roleRef: + kind: Role + name: teslamate-password-sync + apiGroup: rbac.authorization.k8s.io +--- +# RoleBinding in teslamate namespace +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: teslamate-password-sync-grafana + namespace: teslamate +subjects: +- kind: ServiceAccount + name: teslamate-password-sync + namespace: grafana +roleRef: + kind: Role + name: teslamate-password-sync + apiGroup: rbac.authorization.k8s.io diff --git a/infrastructure/grafana/values.yaml b/infrastructure/grafana/values.yaml new file mode 100644 index 0000000..db1ffaf --- /dev/null +++ b/infrastructure/grafana/values.yaml @@ -0,0 +1,141 @@ +# values/grafana.values.yaml + +# --- Admin credentials via Vault/ESO --- +# Admin username & password are stored in Vault at: +# secret/grafana-admin +# and pulled into a K8s Secret grafana-admin-secret by ExternalSecret. +# admin: +# existingSecret: grafana-admin-secret +# userKey: admin-user +# passwordKey: admin-password + +# These plain values are ignored when existingSecret is set, but leave +# them harmless defaults so you can still helm template without ESO. +adminUser: admin +adminPassword: "admin" + +# --- Deployment annotations for Reloader --- +# Automatically restart Grafana when secrets change +# Note: Unpoller has its own Reloader annotation in unpoller namespace +deploymentAnnotations: + secret.reloader.stakater.com/reload: "grafana-authentik-oauth,grafana-teslamate-datasource" + +# --- Persistence for dashboards and config --- +persistence: + enabled: true + type: pvc + storageClassName: rook-ceph-block + accessModes: + - ReadWriteOnce + size: 10Gi + +# --- Service type --- +service: + type: ClusterIP + # Security context for Talos compatibility + securityContext: + runAsUser: 472 + runAsGroup: 472 + runAsNonRoot: true + fsGroup: 472 + +# --- Ingress disabled - Istio VirtualService handles routing --- +ingress: + enabled: false + +# --- Authentik OIDC Integration --- +# ============================================================================ +# AUTOMATIC ADMIN ACCESS - BOOTSTRAP READY +# ============================================================================ +# Role mapping assigns Grafana roles based on Authentik group membership: +# +# Authentik Group → Grafana Role +# ───────────────────────────────────────────────────────── +# authentik Admins → Admin (full access) - AUTOMATIC! +# Grafana Admins → Admin (full access) +# Grafana Editors → Editor (can edit dashboards) +# Grafana Viewers → Viewer (read-only) +# (any other user) → Viewer (read-only) +# +# BOOTSTRAP BEHAVIOR: +# ✓ authentik Admins get automatic Grafana admin access (no manual config!) +# ✓ Groups auto-created by Authentik blueprint during bootstrap +# ✓ Add users to groups in Authentik UI for access control +# +# TO GRANT ADMIN ACCESS TO OTHER USERS: +# 1. Log into Authentik at https://auth.kube.huskypup.net +# 2. Go to Directory → Groups → "Grafana Admins" +# 3. Add users to the group +# 4. Users log out/in to Grafana to receive admin role +# ============================================================================ +grafana.ini: + server: + root_url: https://grafana.kube.huskypup.net + auth.generic_oauth: + enabled: true + name: Authentik + scopes: openid profile email + auth_url: https://auth.kube.huskypup.net/application/o/authorize/ + token_url: https://auth.kube.huskypup.net/application/o/token/ + api_url: https://auth.kube.huskypup.net/application/o/userinfo/ + # Role mapping: authentik Admins OR Grafana Admins → Admin, Grafana Editors → Editor, else → Viewer + role_attribute_path: contains(groups[*], 'authentik Admins') && 'Admin' || contains(groups[*], 'Grafana Admins') && 'Admin' || contains(groups[*], 'Grafana Editors') && 'Editor' || 'Viewer' + allow_sign_up: true + client_id: $__env{GF_AUTH_GENERIC_OAUTH_CLIENT_ID} + client_secret: $__env{GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET} + + +# Load OAuth credentials and datasource passwords from secrets as environment variables +envFromSecrets: + - name: grafana-authentik-oauth + - name: grafana-teslamate-datasource + + + +# --- Datasources --- +datasources: + datasources.yaml: + apiVersion: 1 + datasources: + - name: Prometheus + type: prometheus + uid: Prometheus + url: http://kube-prometheus-stack-prometheus.prometheus.svc.cluster.local:9090 + access: proxy + isDefault: true + - name: TeslaMate + type: grafana-postgresql-datasource + uid: TeslaMate + url: pg-teslamate-rw.teslamate.svc.cluster.local:5432 + database: teslamate + user: teslamate + access: proxy + isDefault: false + editable: true + secureJsonData: + password: $__env{TESLAMATE_DB_PASSWORD} + jsonData: + sslmode: disable + postgresVersion: 1600 + timescaledb: false + database: teslamate + +# --- Sidecar to auto-discover dashboards from ConfigMaps --- +# Dashboards are deployed as ConfigMaps with the label grafana_dashboard: "1" +# The sidecar automatically discovers them and loads them into Grafana +containerSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + +sidecar: + dashboards: + enabled: true + label: grafana_dashboard + labelValue: "1" + folder: /var/lib/grafana/dashboards + searchNamespace: ALL + defaultFolderName: "General" + provider: + foldersFromFilesStructure: true diff --git a/infrastructure/istio/cni-values.yaml b/infrastructure/istio/cni-values.yaml new file mode 100644 index 0000000..6fe47b5 --- /dev/null +++ b/infrastructure/istio/cni-values.yaml @@ -0,0 +1,6 @@ +profile: ambient + +resources: + requests: + cpu: 50m + memory: 100Mi diff --git a/infrastructure/istio/ingressgateway-values.yaml b/infrastructure/istio/ingressgateway-values.yaml new file mode 100644 index 0000000..c3fc196 --- /dev/null +++ b/infrastructure/istio/ingressgateway-values.yaml @@ -0,0 +1,19 @@ +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + +resources: + requests: + cpu: 100m + memory: 64Mi + limits: + memory: 1024Mi + +service: + externalTrafficPolicy: Cluster + type: LoadBalancer + annotations: + # External-DNS (UniFi webhook) will create/update these A records. + external-dns.alpha.kubernetes.io/hostname: "auth.kube.huskypup.net,grafana.kube.huskypup.net,prometheus.kube.huskypup.net,vault.kube.huskypup.net,mqtt.kube.huskypup.net,ceph.kube.huskypup.net,kiali.kube.huskypup.net,argocd.kube.huskypup.net,n8n.kube.huskypup.net,nextcloud.kube.huskypup.net,teslamate.kube.huskypup.net,homeassistant.kube.huskypup.net,esphome.kube.huskypup.net,frigate.kube.huskypup.net,guacamole.kube.huskypup.net,gitlab.kube.huskypup.net,registry.gitlab.kube.huskypup.net,minio.gitlab.kube.huskypup.net,kas.kube.huskypup.net,everest.kube.huskypup.net,rancher.kube.huskypup.net" + external-dns.alpha.kubernetes.io/ttl: "60" diff --git a/infrastructure/istio/istiod-values.yaml b/infrastructure/istio/istiod-values.yaml new file mode 100644 index 0000000..a2aa359 --- /dev/null +++ b/infrastructure/istio/istiod-values.yaml @@ -0,0 +1,54 @@ +# Istio Ambient Mode - ztunnel handles L4 mTLS, waypoints handle L7 +profile: ambient + +pilot: + autoscaleEnabled: true + autoscaleMin: 1 + autoscaleMax: 3 + resources: + requests: + cpu: 10m + memory: 128Mi + limits: + memory: 512Mi + env: + PILOT_ENABLE_AMBIENT: "true" + +# Use Istio CNI for ambient mesh networking (ztunnel traffic redirection) +cni: + enabled: true + +meshConfig: + # ext_authz provider for Authentik forward-auth (via waypoint proxies) + # In ambient mode, ext_authz is enforced by waypoint proxies, not sidecars. + # Authentik's forward-auth endpoint is exposed under /outpost.goauthentik.io. + extensionProviders: + - name: authentik-ext-authz + envoyExtAuthzHttp: + # Route through Host-rewriting proxy (see authentik/ext-authz-proxy.yaml) + # Authentik's embedded outpost only serves forward-auth on Host: auth.kube.huskypup.net + service: ext-authz-proxy.authentik.svc.cluster.local + port: 4180 + pathPrefix: /outpost.goauthentik.io/auth/traefik + headersToDownstreamOnAllow: + - set-cookie + headersToUpstreamOnAllow: + - cookie + - set-cookie + - authorization + - x-authentik-username + - x-authentik-groups + - x-authentik-email + - x-authentik-name + - x-authentik-uid + headersToDownstreamOnDeny: + - set-cookie + - content-type + - location + includeRequestHeadersInCheck: + - cookie + - authorization + includeAdditionalHeadersInCheck: + X-Forwarded-Proto: "%REQ(:SCHEME)%" + X-Forwarded-Host: "%REQ(:AUTHORITY)%" + X-Forwarded-Uri: "%REQ(:PATH)%" diff --git a/infrastructure/istio/manifests/apps/authorization-policies-apps.yaml b/infrastructure/istio/manifests/apps/authorization-policies-apps.yaml new file mode 100644 index 0000000..372809f --- /dev/null +++ b/infrastructure/istio/manifests/apps/authorization-policies-apps.yaml @@ -0,0 +1,322 @@ +# ========================================================================== +# Application Namespace Authorization Policies +# ========================================================================== +# +# These ALLOW policies permit the Istio ingress gateway (istio-system), +# Prometheus, and intra-namespace traffic to reach application workloads. +# +# In ambient mode, ztunnel enforces L4 policies on ALL traffic including +# intra-namespace. Each policy must explicitly allow same-namespace +# communication for pods to reach their own databases, caches, etc. +# +# Applied by: apps/helmfile.yaml istio-app-routes postsync hook +# ========================================================================== + +# --- ArgoCD: Allow ingress + Prometheus + intra-namespace --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-argocd-access + namespace: argocd +spec: + action: ALLOW + rules: + # Intra-namespace (server ↔ repo-server ↔ redis ↔ controller) + - from: + - source: + namespaces: + - argocd + - from: + - source: + namespaces: + - istio-system + - from: + - source: + namespaces: + - prometheus + # NetBird VPN cluster routers (non-mesh, use ipBlocks) + - from: + - source: + ipBlocks: + - "10.244.0.0/16" + +--- +# --- GitLab: Allow ingress + Prometheus + ArgoCD + intra-namespace --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-gitlab-access + namespace: gitlab +spec: + action: ALLOW + rules: + # Intra-namespace (webservice ↔ gitaly ↔ redis ↔ postgres ↔ sidekiq ↔ praefect) + - from: + - source: + namespaces: + - gitlab + # CNPG operator managing database instances + - from: + - source: + namespaces: + - cnpg-system + - from: + - source: + namespaces: + - istio-system + - from: + - source: + namespaces: + - prometheus + # ArgoCD pulls from GitLab repos + - from: + - source: + namespaces: + - argocd + # Redis operator managing Redis instances + - from: + - source: + namespaces: + - redis-operator + # NetBird VPN cluster routers (non-mesh, use ipBlocks) + - from: + - source: + ipBlocks: + - "10.244.0.0/16" + +--- +# --- n8n: Allow ingress + Prometheus + intra-namespace --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-n8n-access + namespace: n8n +spec: + action: ALLOW + rules: + # Intra-namespace (n8n ↔ postgres) + - from: + - source: + namespaces: + - n8n + - from: + - source: + namespaces: + - istio-system + - from: + - source: + namespaces: + - prometheus + +--- +# --- Nextcloud: Allow ingress + Prometheus + intra-namespace --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-nextcloud-access + namespace: nextcloud +spec: + action: ALLOW + rules: + # Intra-namespace (nextcloud ↔ mariadb ↔ redis) + - from: + - source: + namespaces: + - nextcloud + - from: + - source: + namespaces: + - istio-system + - from: + - source: + namespaces: + - prometheus + # NetBird VPN cluster routers (non-mesh, use ipBlocks) + - from: + - source: + ipBlocks: + - "10.244.0.0/16" + +--- +# --- TeslaMate: Allow ingress + Prometheus + Grafana + intra-namespace --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-teslamate-access + namespace: teslamate +spec: + action: ALLOW + rules: + # Intra-namespace (teslamate ↔ postgres) + - from: + - source: + namespaces: + - teslamate + # CNPG operator managing database instances + - from: + - source: + namespaces: + - cnpg-system + - from: + - source: + namespaces: + - istio-system + - from: + - source: + namespaces: + - prometheus + # Grafana reads TeslaMate postgres directly for dashboards + - from: + - source: + namespaces: + - grafana + # NetBird VPN cluster routers (non-mesh, use ipBlocks) + - from: + - source: + ipBlocks: + - "10.244.0.0/16" + +--- +# --- Home Assistant: Allow ingress + Prometheus + intra-namespace --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-home-assistant-access + namespace: home-assistant +spec: + action: ALLOW + rules: + # Intra-namespace (HA ↔ ESPHome) + - from: + - source: + namespaces: + - home-assistant + - from: + - source: + namespaces: + - istio-system + - from: + - source: + namespaces: + - prometheus + # NetBird VPN cluster routers (non-mesh, use ipBlocks) + - from: + - source: + ipBlocks: + - "10.244.0.0/16" + +--- +# --- Frigate: Allow ingress + Prometheus + Home Assistant + intra-namespace --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-frigate-access + namespace: frigate +spec: + action: ALLOW + rules: + # Intra-namespace + - from: + - source: + namespaces: + - frigate + - from: + - source: + namespaces: + - istio-system + - from: + - source: + namespaces: + - prometheus + # Home Assistant accesses Frigate API + - from: + - source: + namespaces: + - home-assistant + # NetBird VPN cluster routers (non-mesh, use ipBlocks) + - from: + - source: + ipBlocks: + - "10.244.0.0/16" + +--- +# --- Guacamole: Allow ingress + Prometheus + intra-namespace --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-guacamole-access + namespace: guacamole +spec: + action: ALLOW + rules: + # Intra-namespace (guacamole ↔ guacd ↔ postgres) + - from: + - source: + namespaces: + - guacamole + # CNPG operator managing database instances + - from: + - source: + namespaces: + - cnpg-system + - from: + - source: + namespaces: + - istio-system + - from: + - source: + namespaces: + - prometheus + # NetBird VPN cluster routers (non-mesh, use ipBlocks) + - from: + - source: + ipBlocks: + - "10.244.0.0/16" + +--- +# --- Nessus: Allow ingress + Prometheus + intra-namespace --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-nessus-access + namespace: nessus +spec: + action: ALLOW + rules: + # Intra-namespace + - from: + - source: + namespaces: + - nessus + - from: + - source: + namespaces: + - istio-system + - from: + - source: + namespaces: + - prometheus + +--- +# --- Rancher: Allow ingress + Prometheus + intra-namespace --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-rancher-access + namespace: cattle-system +spec: + action: ALLOW + rules: + # Intra-namespace (rancher + fleet + webhook) + - from: + - source: + namespaces: + - cattle-system + - from: + - source: + namespaces: + - istio-system + - from: + - source: + namespaces: + - prometheus diff --git a/infrastructure/istio/manifests/apps/namespace-enrollment-job.yaml b/infrastructure/istio/manifests/apps/namespace-enrollment-job.yaml new file mode 100644 index 0000000..5e89c9f --- /dev/null +++ b/infrastructure/istio/manifests/apps/namespace-enrollment-job.yaml @@ -0,0 +1,55 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: istio-mesh-app-enrollment + namespace: istio-system + annotations: + argocd.argoproj.io/hook: PostSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "1" +spec: + backoffLimit: 3 + ttlSecondsAfterFinished: 300 + template: + spec: + serviceAccountName: argocd-hook-sa + restartPolicy: Never + containers: + - name: enroll + image: bitnami/kubectl:1.29 + command: + - /bin/bash + - -c + - | + set -euo pipefail + echo "=== Enrolling app namespaces in Istio ambient mesh ===" + + # Enroll app namespaces in ambient mesh + APP_NAMESPACES=(argocd gitlab n8n nextcloud teslamate home-assistant frigate guacamole nessus cattle-system) + for ns in "${APP_NAMESPACES[@]}"; do + echo "Enrolling $ns in ambient mesh..." + kubectl label namespace "$ns" istio.io/dataplane-mode=ambient --overwrite 2>/dev/null || true + kubectl label namespace "$ns" istio-injection- 2>/dev/null || true + done + + # Attach waypoint proxies for L7 policy enforcement + echo "Attaching waypoint proxies to app namespaces..." + WAYPOINT_APP_NAMESPACES=(argocd gitlab n8n nextcloud teslamate home-assistant frigate guacamole nessus) + for ns in "${WAYPOINT_APP_NAMESPACES[@]}"; do + kubectl label namespace "$ns" istio.io/use-waypoint=waypoint --overwrite 2>/dev/null || true + done + + # Annotate services for NetBird VPN resource auto-discovery + echo "Annotating services for NetBird VPN resource discovery..." + kubectl annotate svc -n gitlab gitlab-webservice-default netbird.io/resource="true" --overwrite 2>/dev/null || true + kubectl annotate svc -n grafana grafana netbird.io/resource="true" --overwrite 2>/dev/null || true + kubectl annotate svc -n home-assistant home-assistant netbird.io/resource="true" --overwrite 2>/dev/null || true + kubectl annotate svc -n guacamole guacamole netbird.io/resource="true" --overwrite 2>/dev/null || true + kubectl annotate svc -n nextcloud nextcloud netbird.io/resource="true" --overwrite 2>/dev/null || true + kubectl annotate svc -n argocd argocd-server netbird.io/resource="true" --overwrite 2>/dev/null || true + kubectl annotate svc -n frigate frigate netbird.io/resource="true" --overwrite 2>/dev/null || true + kubectl annotate svc -n teslamate teslamate netbird.io/resource="true" --overwrite 2>/dev/null || true + kubectl annotate svc -n home-assistant esphome netbird.io/resource="true" --overwrite 2>/dev/null || true + kubectl annotate svc -n prometheus kube-prometheus-stack-prometheus netbird.io/resource="true" --overwrite 2>/dev/null || true + kubectl annotate svc -n vault vault netbird.io/resource="true" --overwrite 2>/dev/null || true + echo "App namespace enrollment and NetBird annotations complete" diff --git a/infrastructure/istio/manifests/edge/edge.yaml b/infrastructure/istio/manifests/edge/edge.yaml new file mode 100644 index 0000000..94cc5a4 --- /dev/null +++ b/infrastructure/istio/manifests/edge/edge.yaml @@ -0,0 +1,78 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: istio-kube-huskypup-net + namespace: istio-system +spec: + secretName: istio-kube-huskypup-net + issuerRef: + kind: ClusterIssuer + name: letsencrypt-production + dnsNames: + - auth.kube.huskypup.net + - grafana.kube.huskypup.net + - prometheus.kube.huskypup.net + - vault.kube.huskypup.net + - mqtt.kube.huskypup.net + - ceph.kube.huskypup.net + - kiali.kube.huskypup.net + - argocd.kube.huskypup.net + - n8n.kube.huskypup.net + - nextcloud.kube.huskypup.net + - teslamate.kube.huskypup.net + - homeassistant.kube.huskypup.net + - esphome.kube.huskypup.net + - frigate.kube.huskypup.net + - guacamole.kube.huskypup.net + - gitlab.kube.huskypup.net + - registry.gitlab.kube.huskypup.net + - minio.gitlab.kube.huskypup.net + - kas.kube.huskypup.net + - everest.kube.huskypup.net + - rancher.kube.huskypup.net + - netbird.kube.huskypup.net + - nessus.kube.huskypup.net + - scylla-manager.kube.huskypup.net + +--- +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + name: edge + namespace: istio-system +spec: + selector: + istio: ingressgateway + servers: + - port: + number: 443 + name: https + protocol: HTTPS + hosts: + - auth.kube.huskypup.net + - grafana.kube.huskypup.net + - prometheus.kube.huskypup.net + - vault.kube.huskypup.net + - mqtt.kube.huskypup.net + - ceph.kube.huskypup.net + - kiali.kube.huskypup.net + - argocd.kube.huskypup.net + - n8n.kube.huskypup.net + - nextcloud.kube.huskypup.net + - teslamate.kube.huskypup.net + - homeassistant.kube.huskypup.net + - esphome.kube.huskypup.net + - frigate.kube.huskypup.net + - guacamole.kube.huskypup.net + - gitlab.kube.huskypup.net + - registry.gitlab.kube.huskypup.net + - minio.gitlab.kube.huskypup.net + - kas.kube.huskypup.net + - everest.kube.huskypup.net + - rancher.kube.huskypup.net + - netbird.kube.huskypup.net + - nessus.kube.huskypup.net + - scylla-manager.kube.huskypup.net + tls: + mode: SIMPLE + credentialName: istio-kube-huskypup-net diff --git a/infrastructure/istio/manifests/mesh/authentik-allow.yaml b/infrastructure/istio/manifests/mesh/authentik-allow.yaml new file mode 100644 index 0000000..27062cf --- /dev/null +++ b/infrastructure/istio/manifests/mesh/authentik-allow.yaml @@ -0,0 +1,67 @@ +# ========================================================================== +# Global Authentik ext_authz Policy - Ingress Gateway +# ========================================================================== +# +# This CUSTOM AuthorizationPolicy runs on the Istio ingress gateway and +# requires Authentik forward-auth for ALL external traffic, EXCEPT: +# +# 1. auth.kube.huskypup.net - Authentik itself (must be reachable) +# 2. Services with native OIDC - They redirect to Authentik themselves +# 3. Machine-access services - Docker registry, git agent, MQTT, etc. +# 4. Vault - Has its own auth system +# +# How it works: +# - The ingress gateway is a regular Envoy proxy (not ztunnel), so CUSTOM +# action works directly without waypoint proxies. +# - Unauthenticated users get HTTP 302 redirect to Authentik login. +# - After login, Authentik sets a session cookie and redirects back. +# - Subsequent requests pass ext_authz automatically via cookie. +# +# Zero-trust: Any new *.kube.huskypup.net service is AUTOMATICALLY protected. +# To exclude a new native-OIDC service, add its hostname to notValues below. +# +# Per-namespace ext_authz policies in ext-authz-policies.yaml provide +# defense-in-depth at the waypoint proxy layer. +# ========================================================================== +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: global-authentik-authz + namespace: istio-system +spec: + selector: + matchLabels: + istio: ingressgateway + action: CUSTOM + provider: + name: "authentik-ext-authz" # Must match meshConfig extensionProviders[].name + rules: + - to: + - operation: + hosts: ["*.kube.huskypup.net"] + when: + - key: request.headers[host] + notValues: + # --- Authentik itself (must be reachable to log in) --- + - "auth.kube.huskypup.net" + + # --- Services with native Authentik OIDC (handle auth themselves) --- + - "grafana.kube.huskypup.net" + - "argocd.kube.huskypup.net" + - "gitlab.kube.huskypup.net" + - "n8n.kube.huskypup.net" + - "nextcloud.kube.huskypup.net" + - "homeassistant.kube.huskypup.net" + - "guacamole.kube.huskypup.net" + - "rancher.kube.huskypup.net" + - "netbird.kube.huskypup.net" + - "everest.kube.huskypup.net" + + # --- Machine-access services (ext_authz would break automation) --- + - "registry.gitlab.kube.huskypup.net" # Docker registry (docker push/pull) + - "minio.gitlab.kube.huskypup.net" # Object storage (CI artifacts) + - "kas.kube.huskypup.net" # GitLab Agent Server (cluster agent) + - "mqtt.kube.huskypup.net" # MQTT protocol (IoT devices) + + # --- Vault (has its own auth system + API access by ESO/apps) --- + - "vault.kube.huskypup.net" diff --git a/infrastructure/istio/manifests/mesh/authorization-policies.yaml b/infrastructure/istio/manifests/mesh/authorization-policies.yaml new file mode 100644 index 0000000..9252818 --- /dev/null +++ b/infrastructure/istio/manifests/mesh/authorization-policies.yaml @@ -0,0 +1,363 @@ +# ========================================================================== +# Zero Trust Authorization Policies - Deny by Default, Allow Explicitly +# ========================================================================== +# +# Policy hierarchy: +# 1. Mesh-wide DENY (default - everything blocked) +# 2. Ingress Gateway ALLOW (external traffic entry point) +# 3. Service-to-service ALLOW (explicit inter-service communication) +# 4. Monitoring ALLOW (Prometheus scraping, Kiali queries) +# +# In ambient mode, L7 policies are enforced by waypoint proxies in each +# namespace. L4 policies (source namespace/principal) are enforced by ztunnel. +# Each namespace with ALLOW/CUSTOM policies must have a waypoint Gateway. +# ========================================================================== + +# --- Ingress Gateway: Allow all external traffic through the edge gateway --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-ingress-gateway + namespace: istio-system +spec: + selector: + matchLabels: + istio: ingressgateway + action: ALLOW + rules: + - {} + +--- +# --- Allow Prometheus to scrape Istio control plane + gateway metrics --- +# Selector scopes this to istio-system workloads only. +# Without a selector, policies in the root namespace (istio-system) apply +# mesh-wide in ambient mode, creating implicit deny for all ambient workloads. +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-prometheus-scraping + namespace: istio-system +spec: + selector: + matchLabels: + app.kubernetes.io/part-of: istio + action: ALLOW + rules: + - from: + - source: + namespaces: + - prometheus + to: + - operation: + ports: + - "15014" # istiod control plane metrics + - "15020" # sidecar/gateway merged metrics + - "15090" # Envoy admin metrics + +--- +# --- Allow Kiali to query istiod --- +# Selector scopes this to istiod only (ambient root namespace caveat above). +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-kiali + namespace: istio-system +spec: + selector: + matchLabels: + app: istiod + action: ALLOW + rules: + - from: + - source: + principals: + - cluster.local/ns/istio-system/sa/kiali-service-account + +--- +# --- Authentik: Allow traffic from ingress + apps doing OIDC --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-authentik-access + namespace: authentik +spec: + action: ALLOW + rules: + # Intra-namespace (server ↔ worker ↔ postgres) + - from: + - source: + namespaces: + - authentik + # CNPG operator managing database instances + - from: + - source: + namespaces: + - cnpg-system + # Ingress gateway for browser flows + - from: + - source: + namespaces: + - istio-system + # Apps doing OIDC token exchange + - from: + - source: + namespaces: + - argocd + - gitlab + - grafana + - nextcloud + - home-assistant + - guacamole + - netbird + - cattle-system + - frigate + - teslamate + # Prometheus scraping (L4-only; L7 path checks deferred to waypoint) + - from: + - source: + namespaces: + - prometheus + +--- +# --- Grafana: Allow ingress + Prometheus datasource queries + scraping --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-grafana-access + namespace: grafana +spec: + action: ALLOW + rules: + # Intra-namespace + - from: + - source: + namespaces: + - grafana + - from: + - source: + namespaces: + - istio-system + - from: + - source: + namespaces: + - prometheus + # NetBird VPN cluster routers (non-mesh, use ipBlocks) + - from: + - source: + ipBlocks: + - "10.244.0.0/16" + +--- +# --- Prometheus: Allow ingress + self-scraping + Grafana --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-prometheus-access + namespace: prometheus +spec: + action: ALLOW + rules: + # Intra-namespace (Prometheus ↔ alertmanager ↔ node-exporter) + - from: + - source: + namespaces: + - prometheus + - from: + - source: + namespaces: + - istio-system + - grafana + # NetBird VPN cluster routers (non-mesh, use ipBlocks) + - from: + - source: + ipBlocks: + - "10.244.0.0/16" + +--- +# --- MQTT: Allow Home Assistant + Frigate + ESPHome + Prometheus --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-mqtt-access + namespace: mqtt +spec: + action: ALLOW + rules: + # Intra-namespace + - from: + - source: + namespaces: + - mqtt + - from: + - source: + namespaces: + - istio-system + - home-assistant + - frigate + - teslamate + # Prometheus scraping (L4-only; L7 path checks deferred to waypoint) + - from: + - source: + namespaces: + - prometheus + +--- +# --- External DNS: Allow internal access + Prometheus --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-external-dns + namespace: external-dns +spec: + action: ALLOW + rules: + # Intra-namespace + - from: + - source: + namespaces: + - external-dns + - from: + - source: + namespaces: + - istio-system + # Prometheus scraping (L4-only; L7 path checks deferred to waypoint) + - from: + - source: + namespaces: + - prometheus + +--- +# --- Unpoller: Allow Prometheus scraping --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-unpoller-access + namespace: unpoller +spec: + action: ALLOW + rules: + # Intra-namespace + - from: + - source: + namespaces: + - unpoller + # Prometheus scraping (L4-only; L7 path checks deferred to waypoint) + - from: + - source: + namespaces: + - prometheus + +--- +# --- Netbird: Allow ingress + Prometheus --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-netbird-access + namespace: netbird +spec: + action: ALLOW + rules: + # Intra-namespace + - from: + - source: + namespaces: + - netbird + # CNPG operator managing database instances + - from: + - source: + namespaces: + - cnpg-system + - from: + - source: + namespaces: + - istio-system + # Netbird operator querying management API + - from: + - source: + namespaces: + - netbird-operator + - from: + - source: + namespaces: + - prometheus + +--- +# --- Netbird Operator: Allow intra-namespace + Prometheus --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-netbird-operator-access + namespace: netbird-operator +spec: + action: ALLOW + rules: + # Intra-namespace + - from: + - source: + namespaces: + - netbird-operator + # Prometheus scraping (L4-only) + - from: + - source: + namespaces: + - prometheus + +--- +# --- CrowdSec: Allow intra-namespace + CNPG + Prometheus --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-crowdsec-access + namespace: crowdsec +spec: + action: ALLOW + rules: + # Intra-namespace (LAPI <-> agent) + - from: + - source: + namespaces: + - crowdsec + # CNPG operator managing database instances + - from: + - source: + namespaces: + - cnpg-system + # Prometheus scraping (L4-only; L7 path checks deferred to waypoint) + - from: + - source: + namespaces: + - prometheus + # Firewall bouncer (hostNetwork DaemonSet) connects from node IPs + - from: + - source: + ipBlocks: + - "172.28.101.0/24" + +--- +# --- Scylla Manager: Allow ingress + Prometheus + intra-namespace --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: allow-scylla-manager-access + namespace: scylla-manager +spec: + action: ALLOW + rules: + # Intra-namespace + - from: + - source: + namespaces: + - scylla-manager + - from: + - source: + namespaces: + - istio-system + - from: + - source: + namespaces: + - prometheus + # Scylla operator managing clusters + - from: + - source: + namespaces: + - scylla-operator diff --git a/infrastructure/istio/manifests/mesh/ext-authz-policies.yaml b/infrastructure/istio/manifests/mesh/ext-authz-policies.yaml new file mode 100644 index 0000000..1281cd1 --- /dev/null +++ b/infrastructure/istio/manifests/mesh/ext-authz-policies.yaml @@ -0,0 +1,139 @@ +# ========================================================================== +# Istio ext_authz Policies - Forward Auth via Authentik +# ========================================================================== +# +# These policies protect services that don't have native OIDC integration. +# Istio's ext_authz filter calls Authentik's forward-auth endpoint before +# allowing traffic through. +# +# Services with native OIDC (GitLab, ArgoCD, Grafana, n8n, Home Assistant, +# Rancher, Nextcloud) do NOT need ext_authz - they handle auth themselves. +# +# IMPORTANT: CUSTOM action does NOT support 'from' or source-based matching. +# We use 'to.operation.hosts' to restrict to external hostnames only. +# Internal service-to-service traffic uses k8s DNS names (*.svc.cluster.local) +# which won't match, so it bypasses ext_authz and hits ALLOW policies instead. +# ========================================================================== + +# --- Prometheus: Require Authentik auth for browser access --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: ext-authz-prometheus + namespace: prometheus +spec: + action: CUSTOM + provider: + name: authentik-ext-authz + rules: + - to: + - operation: + hosts: + - prometheus.kube.huskypup.net + paths: + - /* +--- +# --- TeslaMate: Require Authentik auth --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: ext-authz-teslamate + namespace: teslamate +spec: + action: CUSTOM + provider: + name: authentik-ext-authz + rules: + - to: + - operation: + hosts: + - teslamate.kube.huskypup.net +--- +# --- ESPHome: Require Authentik auth --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: ext-authz-esphome + namespace: home-assistant +spec: + selector: + matchLabels: + app.kubernetes.io/name: esphome + action: CUSTOM + provider: + name: authentik-ext-authz + rules: + - to: + - operation: + hosts: + - esphome.kube.huskypup.net +--- +# --- Kiali: Require Authentik auth --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: ext-authz-kiali + namespace: istio-system +spec: + selector: + matchLabels: + app.kubernetes.io/name: kiali + action: CUSTOM + provider: + name: authentik-ext-authz + rules: + - to: + - operation: + hosts: + - kiali.kube.huskypup.net +--- +# --- Frigate: Require Authentik auth --- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: ext-authz-frigate + namespace: frigate +spec: + action: CUSTOM + provider: + name: authentik-ext-authz + rules: + - to: + - operation: + hosts: + - frigate.kube.huskypup.net +--- +# --- Scylla Manager: Require Authentik auth --- +# NOTE: Namespace 'scylla-manager' must exist before applying this policy. +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: ext-authz-scylla-manager + namespace: scylla-manager +spec: + action: CUSTOM + provider: + name: authentik-ext-authz + rules: + - to: + - operation: + hosts: + - scylla-manager.kube.huskypup.net +--- +# --- Nessus: Require Authentik auth --- +# NOTE: Namespace 'nessus' must exist before applying this policy. +# This policy will be skipped if the namespace doesn't exist yet. +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: ext-authz-nessus + namespace: nessus +spec: + action: CUSTOM + provider: + name: authentik-ext-authz + rules: + - to: + - operation: + hosts: + - nessus.kube.huskypup.net diff --git a/infrastructure/istio/manifests/mesh/monitoring.yaml b/infrastructure/istio/manifests/mesh/monitoring.yaml new file mode 100644 index 0000000..4bda280 --- /dev/null +++ b/infrastructure/istio/manifests/mesh/monitoring.yaml @@ -0,0 +1,68 @@ +# Istio Ambient Prometheus Monitors +# Required for Kiali to display traffic graphs +# +# PodMonitor (ztunnel): Scrapes ztunnel L4 metrics from all nodes +# PodMonitor (waypoint): Scrapes waypoint proxy L7 metrics +# ServiceMonitor (istiod): Scrapes istiod control plane metrics +# +# All require label 'release: kube-prometheus-stack' to be picked up +# by the Prometheus Operator's selector. +--- +# ztunnel metrics - L4 mTLS traffic metrics from ambient mesh +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: ztunnel + namespace: istio-system + labels: + release: kube-prometheus-stack +spec: + selector: + matchLabels: + app: ztunnel + namespaceSelector: + matchNames: + - istio-system + podMetricsEndpoints: + - path: /stats/prometheus + port: http-monitoring + interval: 30s +--- +# Waypoint proxy metrics - L7 AuthorizationPolicy + ext_authz metrics +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: istio-waypoint + namespace: istio-system + labels: + release: kube-prometheus-stack +spec: + selector: + matchExpressions: + - key: gateway.networking.k8s.io/gateway-name + operator: Exists + namespaceSelector: + any: true + podMetricsEndpoints: + - path: /stats/prometheus + port: http-envoy-prom + interval: 30s +--- +# istiod control plane metrics (unchanged from sidecar mode) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: istiod + namespace: istio-system + labels: + release: kube-prometheus-stack +spec: + selector: + matchLabels: + istio: pilot + namespaceSelector: + matchNames: + - istio-system + endpoints: + - port: http-monitoring + interval: 30s diff --git a/infrastructure/istio/manifests/mesh/namespace-enrollment-job.yaml b/infrastructure/istio/manifests/mesh/namespace-enrollment-job.yaml new file mode 100644 index 0000000..0cefc4a --- /dev/null +++ b/infrastructure/istio/manifests/mesh/namespace-enrollment-job.yaml @@ -0,0 +1,49 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: istio-mesh-infra-enrollment + namespace: istio-system + annotations: + argocd.argoproj.io/hook: PostSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "1" +spec: + backoffLimit: 3 + ttlSecondsAfterFinished: 300 + template: + spec: + serviceAccountName: argocd-hook-sa + restartPolicy: Never + containers: + - name: enroll + image: bitnami/kubectl:1.29 + command: + - /bin/bash + - -c + - | + set -euo pipefail + echo "=== Enrolling infrastructure namespaces in Istio ambient mesh ===" + + # Enroll infrastructure namespaces + # Excluded: vault (Shamir seal requires manual unseal after restart) + # Excluded: kube-system, istio-system, cert-manager, kyverno (system namespaces) + INFRA_NAMESPACES=(authentik cnpg-system redis-operator mariadb-system prometheus external-dns grafana mqtt unpoller netbird default gateway scylla-manager crowdsec) + for ns in "${INFRA_NAMESPACES[@]}"; do + echo "Enrolling $ns in ambient mesh..." + kubectl label namespace "$ns" istio.io/dataplane-mode=ambient --overwrite 2>/dev/null || true + kubectl label namespace "$ns" istio-injection- 2>/dev/null || true + done + + # Attach waypoint proxies for L7 policy enforcement + echo "Attaching waypoint proxies to infrastructure namespaces..." + WAYPOINT_NAMESPACES=(authentik external-dns grafana mqtt unpoller netbird prometheus scylla-manager crowdsec) + for ns in "${WAYPOINT_NAMESPACES[@]}"; do + kubectl label namespace "$ns" istio.io/use-waypoint=waypoint --overwrite 2>/dev/null || true + done + + # Deploy ext_authz Host-rewriting proxy for Authentik + if [ -f /tmp/ext-authz-proxy.yaml ]; then + kubectl apply -f /tmp/ext-authz-proxy.yaml + fi + + echo "Infrastructure namespace enrollment complete" diff --git a/infrastructure/istio/manifests/mesh/peer-authentication-webhooks.yaml b/infrastructure/istio/manifests/mesh/peer-authentication-webhooks.yaml new file mode 100644 index 0000000..b67e764 --- /dev/null +++ b/infrastructure/istio/manifests/mesh/peer-authentication-webhooks.yaml @@ -0,0 +1,44 @@ +# PeerAuthentication PERMISSIVE for namespaces that receive non-mesh traffic +# The API server calls webhooks from outside the mesh (no SPIFFE identity). +# The CrowdSec firewall bouncer runs on hostNetwork (no mesh identity) +# and must connect to LAPI over plaintext. +# PERMISSIVE allows both mTLS and plaintext inbound. +--- +apiVersion: security.istio.io/v1 +kind: PeerAuthentication +metadata: + name: allow-apiserver-webhooks + namespace: cnpg-system +spec: + mtls: + mode: PERMISSIVE +--- +apiVersion: security.istio.io/v1 +kind: PeerAuthentication +metadata: + name: allow-apiserver-webhooks + namespace: mariadb-system +spec: + mtls: + mode: PERMISSIVE +--- +# Netbird operator webhook receives calls from the API server +apiVersion: security.istio.io/v1 +kind: PeerAuthentication +metadata: + name: allow-apiserver-webhooks + namespace: netbird-operator +spec: + mtls: + mode: PERMISSIVE +--- +# CrowdSec firewall bouncer (hostNetwork DaemonSet) connects to LAPI +# from the host network namespace without a mesh identity +apiVersion: security.istio.io/v1 +kind: PeerAuthentication +metadata: + name: allow-hostnetwork-bouncer + namespace: crowdsec +spec: + mtls: + mode: PERMISSIVE diff --git a/infrastructure/istio/manifests/mesh/peer-authentication.yaml b/infrastructure/istio/manifests/mesh/peer-authentication.yaml new file mode 100644 index 0000000..b10b598 --- /dev/null +++ b/infrastructure/istio/manifests/mesh/peer-authentication.yaml @@ -0,0 +1,12 @@ +# Mesh-wide STRICT mTLS - Zero Trust +# All service-to-service communication must present valid SPIFFE SVID +# In ambient mode, ztunnel enforces mTLS for all enrolled namespaces +# Services outside the ambient mesh will be rejected +apiVersion: security.istio.io/v1 +kind: PeerAuthentication +metadata: + name: default + namespace: istio-system +spec: + mtls: + mode: STRICT diff --git a/infrastructure/istio/manifests/mesh/waypoint-gateways.yaml b/infrastructure/istio/manifests/mesh/waypoint-gateways.yaml new file mode 100644 index 0000000..08aded7 --- /dev/null +++ b/infrastructure/istio/manifests/mesh/waypoint-gateways.yaml @@ -0,0 +1,279 @@ +# Istio Ambient Waypoint Proxies +# In ambient mode, L7 AuthorizationPolicies (ALLOW/CUSTOM/ext_authz) are enforced +# by waypoint proxies, not sidecars. Each namespace with L7 policies needs a waypoint. +# +# Waypoints are deployed as Kubernetes Gateway resources using the istio-waypoint +# gateway class. They handle: +# - L7 AuthorizationPolicy evaluation +# - ext_authz calls to Authentik for forward-auth +# - HTTP header-based routing decisions + +# --- Infrastructure namespaces --- +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: istio-system + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: authentik + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: grafana + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: prometheus + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: mqtt + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: external-dns + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: unpoller + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: netbird + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE + +# --- Application namespaces --- +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: argocd + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: gitlab + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: n8n + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: nextcloud + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: teslamate + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: home-assistant + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: frigate + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: guacamole + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: nessus + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: crowdsec + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: waypoint + namespace: scylla-manager + labels: + istio.io/waypoint-for: service +spec: + gatewayClassName: istio-waypoint + listeners: + - name: mesh + port: 15008 + protocol: HBONE diff --git a/infrastructure/kiali/manifests/istio-virtualservice.yaml b/infrastructure/kiali/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..a41332d --- /dev/null +++ b/infrastructure/kiali/manifests/istio-virtualservice.yaml @@ -0,0 +1,24 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: kiali + namespace: istio-system +spec: + hosts: + - kiali.kube.huskypup.net + gateways: + - istio-system/edge + http: + - match: + - uri: + exact: / + redirect: + uri: /kiali + - match: + - uri: + prefix: /kiali + route: + - destination: + host: kiali.istio-system.svc.cluster.local + port: + number: 20001 diff --git a/infrastructure/kiali/manifests/kiali-cr.yaml b/infrastructure/kiali/manifests/kiali-cr.yaml new file mode 100644 index 0000000..57069da --- /dev/null +++ b/infrastructure/kiali/manifests/kiali-cr.yaml @@ -0,0 +1,28 @@ +apiVersion: kiali.io/v1alpha1 +kind: Kiali +metadata: + name: kiali + namespace: istio-system +spec: + auth: + strategy: anonymous + deployment: + namespace: istio-system + cluster_wide_access: true + replicas: 1 + pod_labels: + sidecar.istio.io/inject: "true" + istio.io/dataplane-mode: "none" + resources: + requests: + cpu: "10m" + memory: "64Mi" + limits: + memory: "512Mi" + external_services: + prometheus: + url: "http://kube-prometheus-stack-prometheus.prometheus:9090" + grafana: + enabled: false + tracing: + enabled: false diff --git a/infrastructure/kiali/operator-values.yaml b/infrastructure/kiali/operator-values.yaml new file mode 100644 index 0000000..bb5ee83 --- /dev/null +++ b/infrastructure/kiali/operator-values.yaml @@ -0,0 +1,7 @@ +# Install Kiali via operator and create the Kiali CR. +watchNamespace: "" + +# Install CRDs and operator first. We'll apply the Kiali CR separately +# once the CRD exists (avoids helm-diff failing on first install). +cr: + create: false diff --git a/infrastructure/kyverno/policies/authentik-exception.yaml b/infrastructure/kyverno/policies/authentik-exception.yaml new file mode 100644 index 0000000..eb51b5e --- /dev/null +++ b/infrastructure/kyverno/policies/authentik-exception.yaml @@ -0,0 +1,24 @@ +apiVersion: kyverno.io/v2 +kind: PolicyException +metadata: + name: authentik-exemption + namespace: kyverno +spec: + exceptions: + - policyName: disallow-automount-sa-token + ruleNames: + - validate-automount + - autogen-validate-automount + match: + any: + - resources: + kinds: + - Deployment + - ReplicaSet + - Pod + namespaces: + - authentik + names: + - authentik-server* + - authentik-worker* + - pg-authentik* diff --git a/infrastructure/kyverno/policies/disallow-automount-sa-token.yaml b/infrastructure/kyverno/policies/disallow-automount-sa-token.yaml new file mode 100644 index 0000000..50bf1de --- /dev/null +++ b/infrastructure/kyverno/policies/disallow-automount-sa-token.yaml @@ -0,0 +1,68 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: disallow-automount-sa-token + annotations: + policies.kyverno.io/title: Disallow Automount Service Account Token + policies.kyverno.io/category: DoD STIG + policies.kyverno.io/severity: medium + policies.kyverno.io/description: >- + STIG V-242382: Pods that do not need to communicate with the + Kubernetes API server should have automountServiceAccountToken + set to false to prevent credential exposure. + pod-policies.kyverno.io/autogen-controllers: none +spec: + validationFailureAction: Audit + background: true + rules: + - name: validate-automount + match: + any: + - resources: + kinds: + - Pod + exclude: + any: + - resources: + namespaces: + - kube-system + - kube-public + - kube-node-lease + - kyverno + - istio-system + - cert-manager + - cnpg-system + - external-secrets + - vault + - rook-ceph + - prometheus + - argocd + - envoy-gateway-system + - kiali-operator + - mariadb-system + - redis-operator + - reloader + - scylla-operator + - crowdsec + - external-dns + - frigate + - gitlab + - grafana + - guacamole + - home-assistant + - mqtt + - netbird + - nextcloud + - teslamate + - unpoller + - authentik + - checkov + - n8n + - nessus + validate: + message: >- + Pods should set automountServiceAccountToken to false unless API + access is required (STIG V-242382). + pattern: + spec: + automountServiceAccountToken: false diff --git a/infrastructure/kyverno/policies/disallow-default-namespace.yaml b/infrastructure/kyverno/policies/disallow-default-namespace.yaml new file mode 100644 index 0000000..f2188ab --- /dev/null +++ b/infrastructure/kyverno/policies/disallow-default-namespace.yaml @@ -0,0 +1,33 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: disallow-default-namespace + annotations: + policies.kyverno.io/title: Disallow Default Namespace + policies.kyverno.io/category: DoD STIG + policies.kyverno.io/severity: medium + policies.kyverno.io/description: >- + STIG V-242383: Workloads must not run in the default namespace. + The default namespace lacks proper RBAC and network policy boundaries. +spec: + validationFailureAction: Enforce + background: true + rules: + - name: validate-namespace + match: + any: + - resources: + kinds: + - Pod + - Deployment + - StatefulSet + - DaemonSet + - Job + - CronJob + namespaces: + - default + validate: + message: >- + Workloads must not run in the default namespace (STIG V-242383). + Create a dedicated namespace for your application. + deny: {} diff --git a/infrastructure/kyverno/policies/disallow-host-namespaces.yaml b/infrastructure/kyverno/policies/disallow-host-namespaces.yaml new file mode 100644 index 0000000..3456a64 --- /dev/null +++ b/infrastructure/kyverno/policies/disallow-host-namespaces.yaml @@ -0,0 +1,40 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: disallow-host-namespaces + annotations: + policies.kyverno.io/title: Disallow Host Namespaces + policies.kyverno.io/category: DoD STIG + policies.kyverno.io/severity: high + policies.kyverno.io/description: >- + STIG V-242386: Containers must not share the host process ID, IPC, + or network namespace. Sharing host namespaces allows containers to + access host-level resources and break isolation boundaries. +spec: + validationFailureAction: Enforce + background: true + rules: + - name: host-namespaces + match: + any: + - resources: + kinds: + - Pod + exclude: + any: + - resources: + namespaces: + - kube-system + - rook-ceph + - istio-system + - prometheus + - kyverno + - crowdsec + validate: + message: >- + Sharing host PID, IPC, or network namespaces is not allowed (STIG V-242386). + pattern: + spec: + =(hostPID): false + =(hostIPC): false + =(hostNetwork): false diff --git a/infrastructure/kyverno/policies/disallow-latest-tag.yaml b/infrastructure/kyverno/policies/disallow-latest-tag.yaml new file mode 100644 index 0000000..bd20dcf --- /dev/null +++ b/infrastructure/kyverno/policies/disallow-latest-tag.yaml @@ -0,0 +1,34 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: disallow-latest-tag + annotations: + policies.kyverno.io/title: Disallow Latest Image Tag + policies.kyverno.io/category: Zero Trust + policies.kyverno.io/severity: medium +spec: + validationFailureAction: Enforce + background: true + rules: + - name: disallow-latest + match: + any: + - resources: + kinds: + - Pod + exclude: + any: + - resources: + namespaces: + - kube-system + - kyverno + - crowdsec + - home-assistant + validate: + message: "Using ':latest' image tag is not allowed. Specify an explicit tag." + pattern: + spec: + containers: + - image: "!*:latest" + =(initContainers): + - image: "!*:latest" diff --git a/infrastructure/kyverno/policies/disallow-nodeport.yaml b/infrastructure/kyverno/policies/disallow-nodeport.yaml new file mode 100644 index 0000000..8d4731d --- /dev/null +++ b/infrastructure/kyverno/policies/disallow-nodeport.yaml @@ -0,0 +1,29 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: disallow-nodeport-services + annotations: + policies.kyverno.io/title: Disallow NodePort Services + policies.kyverno.io/category: Zero Trust + policies.kyverno.io/severity: medium +spec: + validationFailureAction: Enforce + background: true + rules: + - name: disallow-nodeport + match: + any: + - resources: + kinds: + - Service + exclude: + any: + - resources: + namespaces: + - kube-system + - istio-system + validate: + message: "NodePort services are not allowed. Use ClusterIP or LoadBalancer." + pattern: + spec: + type: "!NodePort" diff --git a/infrastructure/kyverno/policies/disallow-privileged.yaml b/infrastructure/kyverno/policies/disallow-privileged.yaml new file mode 100644 index 0000000..2d8c769 --- /dev/null +++ b/infrastructure/kyverno/policies/disallow-privileged.yaml @@ -0,0 +1,43 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: disallow-privileged-containers + annotations: + policies.kyverno.io/title: Disallow Privileged Containers + policies.kyverno.io/category: Zero Trust + policies.kyverno.io/severity: high +spec: + validationFailureAction: Enforce + background: true + rules: + - name: disallow-privileged + match: + any: + - resources: + kinds: + - Pod + exclude: + any: + - resources: + namespaces: + - kube-system + - rook-ceph + - istio-system + - kube-node-lease + - prometheus + - kyverno + - tetragon + - crowdsec + validate: + message: "Privileged containers are not allowed." + pattern: + spec: + containers: + - =(securityContext): + =(privileged): false + =(initContainers): + - =(securityContext): + =(privileged): false + =(ephemeralContainers): + - =(securityContext): + =(privileged): false diff --git a/infrastructure/kyverno/policies/istio-waypoint-exception.yaml b/infrastructure/kyverno/policies/istio-waypoint-exception.yaml new file mode 100644 index 0000000..38e2cfc --- /dev/null +++ b/infrastructure/kyverno/policies/istio-waypoint-exception.yaml @@ -0,0 +1,47 @@ +apiVersion: kyverno.io/v2 +kind: PolicyException +metadata: + name: istio-waypoint-exemption + namespace: kyverno +spec: + exceptions: + - policyName: require-app-label + ruleNames: + - require-app-label + - policyName: require-probes + ruleNames: + - require-probes + - policyName: disallow-automount-sa-token + ruleNames: + - validate-automount + - autogen-validate-automount + - policyName: require-drop-all-capabilities + ruleNames: + - require-drop-all + - autogen-require-drop-all + - policyName: require-resource-limits + ruleNames: + - require-limits + - autogen-require-limits + - policyName: require-non-root + ruleNames: + - require-run-as-non-root + - autogen-require-run-as-non-root + - policyName: require-readonly-rootfs + ruleNames: + - require-ro-rootfs + - autogen-require-ro-rootfs + - policyName: disallow-latest-tag + ruleNames: + - disallow-latest + - autogen-disallow-latest + match: + any: + - resources: + kinds: + - Deployment + - ReplicaSet + - Pod + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: waypoint diff --git a/infrastructure/kyverno/policies/rancher-exception.yaml b/infrastructure/kyverno/policies/rancher-exception.yaml new file mode 100644 index 0000000..7e404bc --- /dev/null +++ b/infrastructure/kyverno/policies/rancher-exception.yaml @@ -0,0 +1,70 @@ +apiVersion: kyverno.io/v2 +kind: PolicyException +metadata: + name: rancher-exemption + namespace: kyverno +spec: + exceptions: + - policyName: restrict-image-registries + ruleNames: + - validate-container-registries + - autogen-validate-container-registries + - autogen-cronjob-validate-container-registries + - policyName: require-drop-all-capabilities + ruleNames: + - require-drop-all + - autogen-require-drop-all + - autogen-cronjob-require-drop-all + - policyName: require-resource-limits + ruleNames: + - require-limits + - autogen-require-limits + - autogen-cronjob-require-limits + - policyName: disallow-automount-sa-token + ruleNames: + - validate-automount + - autogen-validate-automount + - autogen-cronjob-validate-automount + - policyName: require-app-label + ruleNames: + - require-app-label + - policyName: require-probes + ruleNames: + - require-probes + - policyName: disallow-latest-tag + ruleNames: + - disallow-latest + - autogen-disallow-latest + - autogen-cronjob-disallow-latest + - policyName: require-non-root + ruleNames: + - require-run-as-non-root + - autogen-require-run-as-non-root + - autogen-cronjob-require-run-as-non-root + - policyName: require-readonly-rootfs + ruleNames: + - require-ro-rootfs + - autogen-require-ro-rootfs + - autogen-cronjob-require-ro-rootfs + - policyName: disallow-default-namespace + ruleNames: + - validate-namespace + - autogen-validate-namespace + - autogen-cronjob-validate-namespace + match: + any: + - resources: + namespaces: + - cattle-system + - cattle-fleet-system + - cattle-fleet-local-system + - cattle-capi-system + - cattle-turtles-system + - cattle-ui-plugin-system + - cattle-impersonation-system + - cattle-global-data + - cattle-local-user-passwords + - cattle-fleet-clusters-system + - fleet-default + - fleet-local + - cluster-fleet-* diff --git a/infrastructure/kyverno/policies/require-drop-all-capabilities.yaml b/infrastructure/kyverno/policies/require-drop-all-capabilities.yaml new file mode 100644 index 0000000..d3a9345 --- /dev/null +++ b/infrastructure/kyverno/policies/require-drop-all-capabilities.yaml @@ -0,0 +1,72 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-drop-all-capabilities + annotations: + policies.kyverno.io/title: Require Drop All Capabilities + policies.kyverno.io/category: DoD STIG + policies.kyverno.io/severity: high + policies.kyverno.io/description: >- + STIG V-242398: Containers must drop all Linux capabilities and only + add back specific capabilities that are required. This follows the + principle of least privilege for container security contexts. +spec: + validationFailureAction: Audit + background: true + rules: + - name: require-drop-all + match: + any: + - resources: + kinds: + - Pod + exclude: + any: + - resources: + namespaces: + - kube-system + - rook-ceph + - istio-system + - prometheus + - kyverno + - argocd + - cert-manager + - cnpg-system + - envoy-gateway-system + - kiali-operator + - mariadb-system + - redis-operator + - reloader + - scylla-operator + - vault + - mqtt + - gitlab + - netbird + - grafana + - crowdsec + - external-dns + - nextcloud + - home-assistant + - guacamole + - teslamate + - unpoller + - frigate + - authentik + - checkov + - external-secrets + validate: + message: >- + Containers must drop ALL capabilities (STIG V-242398). + Add securityContext.capabilities.drop: ["ALL"] to each container. + pattern: + spec: + containers: + - securityContext: + capabilities: + drop: + - ALL + =(initContainers): + - securityContext: + capabilities: + drop: + - ALL diff --git a/infrastructure/kyverno/policies/require-istio-ambient.yaml b/infrastructure/kyverno/policies/require-istio-ambient.yaml new file mode 100644 index 0000000..d78e072 --- /dev/null +++ b/infrastructure/kyverno/policies/require-istio-ambient.yaml @@ -0,0 +1,81 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-istio-ambient-mesh + annotations: + policies.kyverno.io/title: Require Istio Ambient Mesh Enrollment + policies.kyverno.io/category: Zero Trust + policies.kyverno.io/severity: high + policies.kyverno.io/description: >- + Application namespaces must be enrolled in Istio ambient mesh via the + istio.io/dataplane-mode=ambient label for zero-trust mTLS enforcement. +spec: + validationFailureAction: Audit + background: true + rules: + - name: check-namespace-ambient-label + match: + any: + - resources: + kinds: + - Namespace + exclude: + any: + - resources: + names: + # Core Kubernetes + - kube-system + - kube-public + - kube-node-lease + - kyverno + - default + # Infrastructure + - istio-system + - cert-manager + - cnpg-system + - rook-ceph + - vault + - external-secrets + - redis-operator + - mariadb-system + - scylla-operator + - scylla-manager + - reloader + - checkov + - kiali-operator + - envoy-gateway-system + - tetragon + - prometheus + - crowdsec + - cilium-secrets + - gateway + - argocd + - netbird-operator + # Rancher (exact names) + - cattle-system + - cattle-fleet-system + - cattle-fleet-local-system + - cattle-capi-system + - cattle-turtles-system + - cattle-ui-plugin-system + - cattle-impersonation-system + - cattle-global-data + - cattle-local-user-passwords + - cattle-fleet-clusters-system + - fleet-default + - fleet-local + - local + # Rancher dynamic namespaces (wildcard) + - cattle-* + - fleet-* + - c-* + - p-* + - u-* + - user-* + - cluster-fleet-* + validate: + message: "Application namespaces must have istio.io/dataplane-mode=ambient for zero-trust mTLS." + pattern: + metadata: + labels: + istio.io/dataplane-mode: ambient diff --git a/infrastructure/kyverno/policies/require-labels.yaml b/infrastructure/kyverno/policies/require-labels.yaml new file mode 100644 index 0000000..c8cce67 --- /dev/null +++ b/infrastructure/kyverno/policies/require-labels.yaml @@ -0,0 +1,43 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-app-label + annotations: + policies.kyverno.io/title: Require App Label + policies.kyverno.io/category: Zero Trust + policies.kyverno.io/severity: low +spec: + validationFailureAction: Audit + background: true + rules: + - name: require-app-label + match: + any: + - resources: + kinds: + - Deployment + - StatefulSet + - DaemonSet + exclude: + any: + - resources: + namespaces: + - kube-system + - kube-node-lease + - kyverno + - rook-ceph + - default + - crowdsec + - gitlab + - scylla-manager + - scylla-operator + - cert-manager + validate: + message: "The label 'app' or 'app.kubernetes.io/name' is required." + anyPattern: + - metadata: + labels: + app: "?*" + - metadata: + labels: + app.kubernetes.io/name: "?*" diff --git a/infrastructure/kyverno/policies/require-non-root.yaml b/infrastructure/kyverno/policies/require-non-root.yaml new file mode 100644 index 0000000..bdf8358 --- /dev/null +++ b/infrastructure/kyverno/policies/require-non-root.yaml @@ -0,0 +1,37 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-non-root + annotations: + policies.kyverno.io/title: Require Non-Root User + policies.kyverno.io/category: Zero Trust + policies.kyverno.io/severity: medium +spec: + validationFailureAction: Audit + background: true + rules: + - name: require-run-as-non-root + match: + any: + - resources: + kinds: + - Pod + exclude: + any: + - resources: + namespaces: + - kube-system + - rook-ceph + - istio-system + - prometheus + - kyverno + - tetragon + validate: + message: "Containers must run as non-root. Set runAsNonRoot to true." + pattern: + spec: + =(securityContext): + =(runAsNonRoot): true + containers: + - =(securityContext): + =(runAsNonRoot): true diff --git a/infrastructure/kyverno/policies/require-probes.yaml b/infrastructure/kyverno/policies/require-probes.yaml new file mode 100644 index 0000000..91ba2bc --- /dev/null +++ b/infrastructure/kyverno/policies/require-probes.yaml @@ -0,0 +1,56 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-probes + annotations: + policies.kyverno.io/title: Require Liveness and Readiness Probes + policies.kyverno.io/category: Zero Trust + policies.kyverno.io/severity: low +spec: + validationFailureAction: Audit + background: true + rules: + - name: require-probes + match: + any: + - resources: + kinds: + - Deployment + - StatefulSet + exclude: + any: + - resources: + namespaces: + - kube-system + - kyverno + - tetragon + - rook-ceph + - argocd + - cert-manager + - cnpg-system + - envoy-gateway-system + - external-secrets + - istio-system + - kiali-operator + - mariadb-system + - prometheus + - redis-operator + - reloader + - scylla-operator + - vault + - gitlab + - grafana + - guacamole + - nextcloud + - scylla-manager + validate: + message: "Liveness and readiness probes are required for all containers." + pattern: + spec: + template: + spec: + containers: + - livenessProbe: + periodSeconds: ">0" + readinessProbe: + periodSeconds: ">0" diff --git a/infrastructure/kyverno/policies/require-resource-limits.yaml b/infrastructure/kyverno/policies/require-resource-limits.yaml new file mode 100644 index 0000000..cb16365 --- /dev/null +++ b/infrastructure/kyverno/policies/require-resource-limits.yaml @@ -0,0 +1,63 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-resource-limits + annotations: + policies.kyverno.io/title: Require Resource Limits + policies.kyverno.io/category: Zero Trust + policies.kyverno.io/severity: medium +spec: + validationFailureAction: Audit + background: true + rules: + - name: require-limits + match: + any: + - resources: + kinds: + - Pod + exclude: + any: + - resources: + namespaces: + - kube-system + - kube-node-lease + - kyverno + - argocd + - authentik + - cert-manager + - cnpg-system + - envoy-gateway-system + - external-secrets + - istio-system + - kiali-operator + - mariadb-system + - prometheus + - redis-operator + - reloader + - rook-ceph + - scylla-operator + - vault + - gitlab + - netbird + - grafana + - crowdsec + - external-dns + - nextcloud + - mqtt + - home-assistant + - guacamole + - teslamate + - unpoller + - frigate + - checkov + - scylla-manager + validate: + message: "All containers must have CPU and memory limits defined." + pattern: + spec: + containers: + - resources: + limits: + cpu: "?*" + memory: "?*" diff --git a/infrastructure/kyverno/policies/require-ro-rootfs.yaml b/infrastructure/kyverno/policies/require-ro-rootfs.yaml new file mode 100644 index 0000000..d07259e --- /dev/null +++ b/infrastructure/kyverno/policies/require-ro-rootfs.yaml @@ -0,0 +1,39 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-readonly-rootfs + annotations: + policies.kyverno.io/title: Require Read-Only Root Filesystem + policies.kyverno.io/category: Zero Trust + policies.kyverno.io/severity: low +spec: + validationFailureAction: Audit + background: true + rules: + - name: require-ro-rootfs + match: + any: + - resources: + kinds: + - Pod + exclude: + any: + - resources: + namespaces: + - kube-system + - rook-ceph + - istio-system + - kyverno + - tetragon + - prometheus + - authentik + - gitlab + - nextcloud + - home-assistant + validate: + message: "Containers should use a read-only root filesystem." + pattern: + spec: + containers: + - =(securityContext): + =(readOnlyRootFilesystem): true diff --git a/infrastructure/kyverno/policies/restrict-image-registries.yaml b/infrastructure/kyverno/policies/restrict-image-registries.yaml new file mode 100644 index 0000000..480c7f2 --- /dev/null +++ b/infrastructure/kyverno/policies/restrict-image-registries.yaml @@ -0,0 +1,107 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: restrict-image-registries + annotations: + policies.kyverno.io/title: Restrict Image Registries + policies.kyverno.io/category: Zero Trust + policies.kyverno.io/severity: high + policies.kyverno.io/description: >- + Validates that all container images come from allowed registries. + Images must either use an explicit allowed registry prefix (docker.io/, + ghcr.io/, quay.io/, etc.) or be short-name Docker Hub references + (no dots before the first slash, e.g. grafana/grafana or busybox). +spec: + validationFailureAction: Enforce + background: true + rules: + - name: validate-container-registries + match: + any: + - resources: + kinds: + - Pod + exclude: + any: + - resources: + namespaces: + - kube-system + - kube-node-lease + - kyverno + validate: + message: >- + Images must come from allowed registries: docker.io, ghcr.io, quay.io, + registry.k8s.io, gcr.io, lscr.io, registry.gitlab.com, cr.fluentbit.io, + public.ecr.aws, ecr-public.aws.com, oci.external-secrets.io, + docker-registry3.mariadb.com. Docker Hub short names (org/image or + library images) are also allowed. + foreach: + - list: "request.object.spec.containers" + deny: + conditions: + all: + # Image does NOT start with any allowed registry prefix + - key: "{{element.image}}" + operator: AnyNotIn + value: + - "docker.io/*" + - "ghcr.io/*" + - "quay.io/*" + - "registry.k8s.io/*" + - "gcr.io/*" + - "lscr.io/*" + - "registry.gitlab.com/*" + - "cr.fluentbit.io/*" + - "public.ecr.aws/*" + - "ecr-public.aws.com/*" + - "oci.external-secrets.io/*" + - "docker-registry3.mariadb.com/*" + # AND image looks like a registry hostname (has a dot before the first slash) + # This allows Docker Hub short names like grafana/grafana or busybox + - key: "{{element.image}}" + operator: Equals + value: "*.*/*" + - list: "request.object.spec.initContainers || []" + deny: + conditions: + all: + - key: "{{element.image}}" + operator: AnyNotIn + value: + - "docker.io/*" + - "ghcr.io/*" + - "quay.io/*" + - "registry.k8s.io/*" + - "gcr.io/*" + - "lscr.io/*" + - "registry.gitlab.com/*" + - "cr.fluentbit.io/*" + - "public.ecr.aws/*" + - "ecr-public.aws.com/*" + - "oci.external-secrets.io/*" + - "docker-registry3.mariadb.com/*" + - key: "{{element.image}}" + operator: Equals + value: "*.*/*" + - list: "request.object.spec.ephemeralContainers || []" + deny: + conditions: + all: + - key: "{{element.image}}" + operator: AnyNotIn + value: + - "docker.io/*" + - "ghcr.io/*" + - "quay.io/*" + - "registry.k8s.io/*" + - "gcr.io/*" + - "lscr.io/*" + - "registry.gitlab.com/*" + - "cr.fluentbit.io/*" + - "public.ecr.aws/*" + - "ecr-public.aws.com/*" + - "oci.external-secrets.io/*" + - "docker-registry3.mariadb.com/*" + - key: "{{element.image}}" + operator: Equals + value: "*.*/*" diff --git a/infrastructure/kyverno/values.yaml b/infrastructure/kyverno/values.yaml new file mode 100644 index 0000000..b88c5f8 --- /dev/null +++ b/infrastructure/kyverno/values.yaml @@ -0,0 +1,128 @@ +# Kyverno Policy Engine - Zero Trust Configuration +# All policies start in Audit mode to avoid breaking existing workloads + +admissionController: + replicas: 3 + resources: + limits: + memory: 384Mi + requests: + cpu: 50m + memory: 128Mi + serviceMonitor: + enabled: true + +backgroundController: + replicas: 2 + resources: + limits: + memory: 256Mi + requests: + cpu: 50m + memory: 64Mi + serviceMonitor: + enabled: true + +cleanupController: + replicas: 1 + resources: + limits: + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi + serviceMonitor: + enabled: true + +reportsController: + replicas: 1 + resources: + limits: + memory: 256Mi + requests: + cpu: 50m + memory: 64Mi + serviceMonitor: + enabled: true + +# Enable metrics for Prometheus +metricsConfig: + metricsExposure: + enabled: true + +# Exclude system namespaces from webhook +config: + webhooks: + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: NotIn + values: + - kube-system + - kube-public + - kube-node-lease + - cattle-system + - cattle-fleet-system + - cattle-fleet-local-system + - cattle-capi-system + - cattle-turtles-system + - cattle-ui-plugin-system + - cattle-impersonation-system + - cattle-global-data + - cattle-local-user-passwords + - cattle-fleet-clusters-system + - fleet-default + - fleet-local + - local + excludeGroups: + - system:serviceaccounts:kube-system + - system:nodes + resourceFiltersExcludeNamespaces: + # Core Kubernetes + - kube-system + - kube-public + - kube-node-lease + - kyverno + # Rancher (exact names) + - cattle-system + - cattle-fleet-system + - cattle-fleet-local-system + - cattle-capi-system + - cattle-turtles-system + - cattle-ui-plugin-system + - cattle-impersonation-system + - cattle-global-data + - cattle-local-user-passwords + - cattle-fleet-clusters-system + - fleet-default + - fleet-local + - local + # Rancher dynamic namespaces (wildcard) + - c-* + - p-* + - u-* + - user-* + - cluster-fleet-* + # Infrastructure operators and system namespaces + - rook-ceph + - cnpg-system + - redis-operator + - mariadb-system + - cert-manager + - istio-system + - external-secrets + - scylla-operator + - scylla-manager + - kiali-operator + - envoy-gateway-system + - reloader + - tetragon + - cilium-secrets + - vault + - prometheus + - netbird-operator + +features: + policyExceptions: + enabled: true + namespace: kyverno diff --git a/infrastructure/metrics-server/values.yaml b/infrastructure/metrics-server/values.yaml new file mode 100644 index 0000000..11fb5f8 --- /dev/null +++ b/infrastructure/metrics-server/values.yaml @@ -0,0 +1,6 @@ +apiService: + create: true + +args: + - --kubelet-insecure-tls + - --kubelet-preferred-address-types=InternalIP \ No newline at end of file diff --git a/infrastructure/mosquitto/manifests/deployment.yaml b/infrastructure/mosquitto/manifests/deployment.yaml new file mode 100644 index 0000000..cd9657c --- /dev/null +++ b/infrastructure/mosquitto/manifests/deployment.yaml @@ -0,0 +1,137 @@ +# MQTT Broker - Eclipse Mosquitto +# Used by Frigate, Home Assistant, and other IoT apps for event messaging +apiVersion: v1 +kind: Namespace +metadata: + name: mqtt + labels: + app.kubernetes.io/name: mosquitto + app.kubernetes.io/managed-by: Helm +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: mosquitto + namespace: mqtt + labels: + app.kubernetes.io/name: mosquitto +data: + mosquitto.conf: | + listener 1883 + listener 9001 + protocol websockets + allow_anonymous true + persistence true + persistence_location /mosquitto/data/ + log_dest file /mosquitto/log/mosquitto.log +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mosquitto-data + namespace: mqtt + labels: + app.kubernetes.io/name: mosquitto +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + storageClassName: rook-ceph-block +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mosquitto + namespace: mqtt + labels: + app.kubernetes.io/name: mosquitto +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: mosquitto + template: + metadata: + labels: + app.kubernetes.io/name: mosquitto + spec: + automountServiceAccountToken: false + securityContext: + runAsUser: 1883 + runAsGroup: 1883 + fsGroup: 1883 + containers: + - name: mosquitto + image: eclipse-mosquitto:2.0.18 + securityContext: + runAsUser: 1883 + runAsGroup: 1883 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + ports: + - containerPort: 1883 + name: mqtt + - containerPort: 9001 + name: websocket + volumeMounts: + - name: mosquitto-data-persistent + mountPath: /mosquitto/data + - name: mosquitto-log + mountPath: /mosquitto/log + - name: mosquitto-config + mountPath: /mosquitto/config + readOnly: true + - name: tmp + mountPath: /tmp + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 100m + memory: 256Mi + livenessProbe: + tcpSocket: + port: 1883 + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + tcpSocket: + port: 1883 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: mosquitto-data-persistent + persistentVolumeClaim: + claimName: mosquitto-data + - name: mosquitto-log + emptyDir: {} + - name: mosquitto-config + configMap: + name: mosquitto + - name: tmp + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: mosquitto + namespace: mqtt + labels: + app.kubernetes.io/name: mosquitto +spec: + ports: + - name: mqtt + port: 1883 + targetPort: 1883 + - name: websocket + port: 9001 + targetPort: 9001 + selector: + app.kubernetes.io/name: mosquitto \ No newline at end of file diff --git a/infrastructure/mosquitto/manifests/istio-virtualservice.yaml b/infrastructure/mosquitto/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..87fc6fc --- /dev/null +++ b/infrastructure/mosquitto/manifests/istio-virtualservice.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: mosquitto-websocket + namespace: mqtt +spec: + hosts: + - mqtt.kube.huskypup.net + gateways: + - istio-system/edge + http: + - route: + - destination: + host: mosquitto.mqtt.svc.cluster.local + port: + number: 9001 diff --git a/infrastructure/netbird-operator/values.yaml b/infrastructure/netbird-operator/values.yaml new file mode 100644 index 0000000..d5bd955 --- /dev/null +++ b/infrastructure/netbird-operator/values.yaml @@ -0,0 +1,46 @@ +# Netbird Kubernetes Operator +# Auto-discovers K8s services via annotations and creates Netbird Networks +# Chart: netbird-operator/kubernetes-operator + +# API key from ExternalSecret-managed secret +netbirdAPI: + keyFromSecret: + name: netbird-mgmt-api-key + key: NB_API_KEY + +# Explicit tag required — Kyverno disallow-latest-tag policy +routingClientImage: "netbirdio/netbird:0.66.0" + +cluster: + name: homelab + dns: svc.cluster.local + +# Enable service exposure via annotations (netbird.io/expose=true) +ingress: + enabled: true + namespacedNetworks: true + allowAutomaticPolicyCreation: true + +# Webhook TLS via cert-manager (already deployed) +webhook: + enableCertManager: true + failurePolicy: Fail + +operator: + replicaCount: 1 + image: + pullPolicy: IfNotPresent + metrics: + enabled: true + port: 8080 + resources: + limits: + memory: 256Mi + requests: + cpu: 25m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL diff --git a/infrastructure/netbird/cnpg-cluster.yaml b/infrastructure/netbird/cnpg-cluster.yaml new file mode 100644 index 0000000..a0d0f2d --- /dev/null +++ b/infrastructure/netbird/cnpg-cluster.yaml @@ -0,0 +1,38 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: pg-netbird + namespace: netbird +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:16 + instances: 2 + + resources: + requests: + memory: "256Mi" + cpu: "25m" + limits: + memory: "1Gi" + + affinity: + topologyKey: kubernetes.io/hostname + + storage: + size: 10Gi + storageClass: rook-ceph-block + + primaryUpdateStrategy: unsupervised + + postgresql: + parameters: + max_connections: "100" + shared_buffers: "256MB" + effective_cache_size: "768MB" + + bootstrap: + initdb: + database: netbird + owner: app + + monitoring: + enablePodMonitor: true diff --git a/infrastructure/netbird/manifests/api-exporter.yaml b/infrastructure/netbird/manifests/api-exporter.yaml new file mode 100644 index 0000000..410e8ca --- /dev/null +++ b/infrastructure/netbird/manifests/api-exporter.yaml @@ -0,0 +1,104 @@ +# Netbird API Exporter - Exposes peer status, groups, users as Prometheus metrics +# Uses the Netbird management API to collect data +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: netbird-api-exporter + namespace: netbird + labels: + app.kubernetes.io/name: netbird-api-exporter + app.kubernetes.io/component: exporter +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: netbird-api-exporter + template: + metadata: + labels: + app.kubernetes.io/name: netbird-api-exporter + app.kubernetes.io/component: exporter + spec: + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: exporter + image: ghcr.io/matanbaruch/netbird-api-exporter:v0.1.46 + ports: + - containerPort: 8080 + name: metrics + protocol: TCP + env: + - name: NETBIRD_API_URL + value: "http://netbird-management.netbird.svc.cluster.local:80" + - name: NETBIRD_API_TOKEN + valueFrom: + secretKeyRef: + name: netbird-api-token-secret + key: api-token + resources: + limits: + memory: 128Mi + requests: + cpu: 10m + memory: 32Mi + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + livenessProbe: + httpGet: + path: /metrics + port: metrics + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /metrics + port: metrics + initialDelaySeconds: 5 + periodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: netbird-api-exporter + namespace: netbird + labels: + app.kubernetes.io/name: netbird-api-exporter + app.kubernetes.io/component: exporter +spec: + type: ClusterIP + ports: + - port: 8080 + targetPort: metrics + protocol: TCP + name: metrics + selector: + app.kubernetes.io/name: netbird-api-exporter +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: netbird-api-exporter + namespace: netbird + labels: + release: kube-prometheus-stack +spec: + selector: + matchLabels: + app.kubernetes.io/name: netbird-api-exporter + endpoints: + - port: metrics + path: /metrics + interval: 30s diff --git a/infrastructure/netbird/manifests/api-token-external-secret.yaml b/infrastructure/netbird/manifests/api-token-external-secret.yaml new file mode 100644 index 0000000..3a3d6cd --- /dev/null +++ b/infrastructure/netbird/manifests/api-token-external-secret.yaml @@ -0,0 +1,21 @@ +# ExternalSecret for Netbird API token (used by API exporter) +# Token is stored in Vault by scripts/setup-netbird-api-token.sh +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: netbird-api-token + namespace: netbird +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: netbird-api-token-secret + creationPolicy: Owner + data: + - secretKey: api-token + remoteRef: + key: netbird-api-token + property: api-token diff --git a/infrastructure/netbird/manifests/cluster-router.yaml b/infrastructure/netbird/manifests/cluster-router.yaml new file mode 100644 index 0000000..efc7a4a --- /dev/null +++ b/infrastructure/netbird/manifests/cluster-router.yaml @@ -0,0 +1,67 @@ +--- +# ExternalSecret to pull router setup key from Vault +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: netbird-router-setup-key + namespace: netbird +spec: + refreshInterval: "0" + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: netbird-router-setup-key + data: + - secretKey: setup-key + remoteRef: + key: netbird-router-setup-key + property: setup-key +--- +# Shared cluster-wide NetBird router deployment (2 replicas) +# Routes traffic for all exposed K8s services through NetBird VPN +apiVersion: apps/v1 +kind: Deployment +metadata: + name: netbird-cluster-router + namespace: netbird + labels: + app.kubernetes.io/name: netbird-cluster-router +spec: + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: netbird-cluster-router + template: + metadata: + labels: + app.kubernetes.io/name: netbird-cluster-router + annotations: + # Router pods must opt out of ambient mesh — ztunnel iptables rules + # interfere with WireGuard packet forwarding (DNS and TCP). + # AuthorizationPolicies use ipBlocks for pod CIDR instead. + ambient.istio.io/redirection: disabled + spec: + containers: + - name: netbird + image: netbirdio/netbird:0.66.0 + imagePullPolicy: IfNotPresent + env: + - name: NB_SETUP_KEY + valueFrom: + secretKeyRef: + name: netbird-router-setup-key + key: setup-key + - name: NB_MANAGEMENT_URL + value: "https://netbird.kube.huskypup.net" + securityContext: + capabilities: + add: + - NET_ADMIN + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + memory: 128Mi + terminationGracePeriodSeconds: 30 diff --git a/infrastructure/netbird/manifests/external-secret.yaml b/infrastructure/netbird/manifests/external-secret.yaml new file mode 100644 index 0000000..e0356af --- /dev/null +++ b/infrastructure/netbird/manifests/external-secret.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: netbird-oauth + namespace: netbird +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: netbird-oauth-secret + creationPolicy: Owner + data: + - secretKey: client-id + remoteRef: + key: netbird-oauth + property: client-id + - secretKey: client-secret + remoteRef: + key: netbird-oauth + property: client-secret + - secretKey: service-username + remoteRef: + key: netbird-oauth + property: service-username + - secretKey: service-password + remoteRef: + key: netbird-oauth + property: service-password diff --git a/infrastructure/netbird/manifests/istio-virtualservice.yaml b/infrastructure/netbird/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..57caec2 --- /dev/null +++ b/infrastructure/netbird/manifests/istio-virtualservice.yaml @@ -0,0 +1,59 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: netbird + namespace: netbird +spec: + hosts: + - netbird.kube.huskypup.net + gateways: + - istio-system/edge + http: + # Management REST API + - match: + - uri: + prefix: /api + route: + - destination: + host: netbird-management.netbird.svc.cluster.local + port: + number: 80 + + # Management gRPC (listens on port 33073, separate from REST API on 80) + - match: + - uri: + prefix: /management.ManagementService/ + route: + - destination: + host: netbird-management-grpc.netbird.svc.cluster.local + port: + number: 33073 + + # Signal gRPC + - match: + - uri: + prefix: /signalexchange.SignalExchange/ + route: + - destination: + host: netbird-signal.netbird.svc.cluster.local + port: + number: 80 + + # Relay WebSocket + - match: + - uri: + prefix: /relay + route: + - destination: + host: netbird-relay.netbird.svc.cluster.local + port: + number: 33080 + + # Dashboard (all other paths go to dashboard nginx) + # No URI rewrite — the static export has .html and .txt files for every route. + # nginx try_files handles SPA fallback to /index.html. + - route: + - destination: + host: netbird-dashboard.netbird.svc.cluster.local + port: + number: 80 diff --git a/infrastructure/netbird/manifests/management-grpc-service.yaml b/infrastructure/netbird/manifests/management-grpc-service.yaml new file mode 100644 index 0000000..2452f9f --- /dev/null +++ b/infrastructure/netbird/manifests/management-grpc-service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: netbird-management-grpc + namespace: netbird + labels: + app.kubernetes.io/name: netbird-management + app.kubernetes.io/instance: netbird +spec: + type: ClusterIP + ports: + - name: grpc + port: 33073 + targetPort: 33073 + protocol: TCP + selector: + app.kubernetes.io/instance: netbird + app.kubernetes.io/name: netbird-management diff --git a/infrastructure/netbird/manifests/oidc-trusted-domains.yaml b/infrastructure/netbird/manifests/oidc-trusted-domains.yaml new file mode 100644 index 0000000..864077e --- /dev/null +++ b/infrastructure/netbird/manifests/oidc-trusted-domains.yaml @@ -0,0 +1,25 @@ +--- +# Override OidcTrustedDomains.js template to include Authentik authority domain +# The default template only includes $NETBIRD_MGMT_API_ENDPOINT, but @axa-fr/react-oidc +# needs the OIDC authority domain (auth.kube.huskypup.net) in trusted domains to allow +# discovery, token, and userinfo fetches. +apiVersion: v1 +kind: ConfigMap +metadata: + name: netbird-oidc-trusted-domains + namespace: netbird +data: + OidcTrustedDomains.js.tmpl: | + const trustedDomains = { + default:["$NETBIRD_MGMT_API_ENDPOINT", "$AUTH_AUTHORITY", "https://auth.kube.huskypup.net"], + auth0:[] + }; + OidcServiceWorker.js: | + // Minimal no-op service worker for @axa-fr/react-oidc + // Prevents 404 which can hang the OIDC initialization + self.addEventListener('install', function(event) { + self.skipWaiting(); + }); + self.addEventListener('activate', function(event) { + event.waitUntil(self.clients.claim()); + }); diff --git a/infrastructure/netbird/manifests/presync-job.yaml b/infrastructure/netbird/manifests/presync-job.yaml new file mode 100644 index 0000000..c838f4d --- /dev/null +++ b/infrastructure/netbird/manifests/presync-job.yaml @@ -0,0 +1,51 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: netbird-presync + namespace: netbird + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + backoffLimit: 3 + template: + spec: + serviceAccountName: argocd-hook-sa + containers: + - name: presync + image: bitnami/kubectl:1.31 + command: + - /bin/bash + - -c + - | + set -euo pipefail + echo "=== Netbird PreSync ===" + + # Set PodSecurity to privileged (cluster routers need NET_ADMIN) + kubectl label namespace netbird pod-security.kubernetes.io/enforce=privileged --overwrite 2>/dev/null || true + + # Wait for CNPG cluster + echo "Waiting for Netbird PostgreSQL cluster..." + for i in $(seq 1 60); do + PHASE=$(kubectl -n netbird get cluster pg-netbird -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$PHASE" = "Cluster in healthy state" ] || [ "$PHASE" = "Healthy" ]; then + echo " Netbird PostgreSQL cluster ready" + break + fi + echo " waiting for pg-netbird... (attempt $i/60, phase=$PHASE)" + sleep 5 + done + + # Wait for OAuth secret + echo "Waiting for netbird-oauth-secret..." + for i in $(seq 1 30); do + if kubectl -n netbird get secret netbird-oauth-secret >/dev/null 2>&1; then + echo " Netbird OAuth secret synced" + break + fi + echo " waiting for ExternalSecret sync... (attempt $i/30)" + sleep 2 + done + + echo "=== Netbird PreSync Complete ===" + restartPolicy: Never diff --git a/infrastructure/netbird/manifests/servicemonitor.yaml b/infrastructure/netbird/manifests/servicemonitor.yaml new file mode 100644 index 0000000..57757bb --- /dev/null +++ b/infrastructure/netbird/manifests/servicemonitor.yaml @@ -0,0 +1,54 @@ +# PodMonitors for Netbird native Prometheus metrics +# Management, Signal, and Relay servers expose metrics on port 9090 +# when NB_METRICS_PORT is set +--- +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: netbird-management + namespace: netbird + labels: + release: kube-prometheus-stack +spec: + selector: + matchLabels: + app.kubernetes.io/component: management + app.kubernetes.io/name: netbird + podMetricsEndpoints: + - port: "9090" + path: /metrics + interval: 30s +--- +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: netbird-signal + namespace: netbird + labels: + release: kube-prometheus-stack +spec: + selector: + matchLabels: + app.kubernetes.io/component: signal + app.kubernetes.io/name: netbird + podMetricsEndpoints: + - port: "9090" + path: /metrics + interval: 30s +--- +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: netbird-relay + namespace: netbird + labels: + release: kube-prometheus-stack +spec: + selector: + matchLabels: + app.kubernetes.io/component: relay + app.kubernetes.io/name: netbird + podMetricsEndpoints: + - port: "9090" + path: /metrics + interval: 30s diff --git a/infrastructure/netbird/values.yaml b/infrastructure/netbird/values.yaml new file mode 100644 index 0000000..0fb596f --- /dev/null +++ b/infrastructure/netbird/values.yaml @@ -0,0 +1,272 @@ +# Netbird Self-Hosted VPN - Zero Trust Network Access +# Full deployment: management, signal, relay, dashboard +# Chart: netbird/netbird (https://netbirdio.github.io/helms) + +# Ensure service names are netbird-{component} (not netbird-netbird-{component}) +fullnameOverride: netbird + +management: + enabled: true + securityContext: + capabilities: + drop: + - ALL + resources: + limits: + memory: 256Mi + requests: + cpu: 25m + memory: 128Mi + + persistentVolume: + enabled: true + size: 100Mi + storageClass: rook-ceph-block + + # Management server config (management.json) + # Uses Authentik as external OIDC provider for SSO + image: + tag: 0.66.0 + + configmap: |- + { + "Stuns": [ + { + "Proto": "udp", + "URI": "stun:stun.l.google.com:19302" + } + ], + "Relay": { + "Addresses": ["rels://netbird.kube.huskypup.net:443"], + "CredentialsTTL": "24h", + "Secret": "zATjQ+YwNLvSBPD04R2+z7WzUhrwG1rkH3b/4Xsjvz0=" + }, + "Signal": { + "Proto": "https", + "URI": "netbird.kube.huskypup.net:443", + "AuthenticationEnabled": false + }, + "Datadir": "/var/lib/netbird/", + "HttpConfig": { + "AuthIssuer": "https://auth.kube.huskypup.net/application/o/netbird/", + "AuthAudience": "{{.NETBIRD_AUTH_AUDIENCE}}", + "AuthKeysLocation": "https://auth.kube.huskypup.net/application/o/netbird/jwks/", + "OIDCConfigEndpoint": "https://auth.kube.huskypup.net/application/o/netbird/.well-known/openid-configuration", + "IdpSignKeyRefreshEnabled": false, + "AuthUserIDClaim": "sub", + "GroupsClaimName": "groups" + }, + "IdpManagerConfig": { + "ManagerType": "authentik", + "ClientConfig": { + "Issuer": "https://auth.kube.huskypup.net/application/o/netbird/", + "TokenEndpoint": "https://auth.kube.huskypup.net/application/o/token/", + "ClientID": "{{.NETBIRD_IDP_MGMT_CLIENT_ID}}", + "GrantType": "client_credentials" + }, + "ExtraConfig": { + "Username": "{{.NETBIRD_IDP_MANAGER_EXTRA_CONFIG_USERNAME}}", + "Password": "{{.NETBIRD_IDP_MANAGER_EXTRA_CONFIG_PASSWORD}}" + } + }, + "DeviceAuthorizationFlow": { + "Provider": "hosted", + "ProviderConfig": { + "Audience": "{{.NETBIRD_AUTH_AUDIENCE}}", + "ClientID": "{{.NETBIRD_AUTH_AUDIENCE}}", + "ClientSecret": "", + "Domain": "auth.kube.huskypup.net", + "TokenEndpoint": "https://auth.kube.huskypup.net/application/o/token/", + "DeviceAuthEndpoint": "https://auth.kube.huskypup.net/application/o/device/", + "AuthorizationEndpoint": "https://auth.kube.huskypup.net/application/o/authorize/", + "Scope": "openid profile email offline_access goauthentik.io/api", + "UseIDToken": false, + "RedirectURLs": [ + "http://localhost:53000", + "netbird://auth" + ] + } + }, + "PKCEAuthorizationFlow": { + "ProviderConfig": { + "Audience": "{{.NETBIRD_AUTH_AUDIENCE}}", + "ClientID": "{{.NETBIRD_AUTH_AUDIENCE}}", + "ClientSecret": "", + "TokenEndpoint": "https://auth.kube.huskypup.net/application/o/token/", + "DeviceAuthEndpoint": "https://auth.kube.huskypup.net/application/o/device/", + "AuthorizationEndpoint": "https://auth.kube.huskypup.net/application/o/authorize/", + "Scope": "openid profile email offline_access goauthentik.io/api", + "UseIDToken": false, + "RedirectURLs": [ + "http://localhost:53000" + ] + } + }, + "StoreConfig": { + "Engine": "postgres" + }, + "DataStoreEncryptionKey": "X3qwIBAfV5UjYCbPmat0jLSLYAnCOMtLdXdgV34UWMM=", + "EncryptionKey": "X3qwIBAfV5UjYCbPmat0jLSLYAnCOMtLdXdgV34UWMM=" + } + + # Simple key-value env vars (override management.json values) + env: + NETBIRD_STORE_ENGINE: postgres + NETBIRD_DOMAIN: netbird.kube.huskypup.net + NETBIRD_MGMT_API_ENDPOINT: "https://netbird.kube.huskypup.net:443" + NETBIRD_ENCRYPTION_KEY: "X3qwIBAfV5UjYCbPmat0jLSLYAnCOMtLdXdgV34UWMM=" + NB_METRICS_PORT: "9090" + + # OIDC scopes and redirect URIs + NETBIRD_AUTH_SUPPORTED_SCOPES: "openid profile email offline_access goauthentik.io/api" + NETBIRD_AUTH_REDIRECT_URI: "/auth" + NETBIRD_AUTH_SILENT_REDIRECT_URI: "/silent-auth" + NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN: "true" + + # Complex env vars (valueFrom references) + envRaw: + - name: NETBIRD_STORE_ENGINE_POSTGRES_DSN + valueFrom: + secretKeyRef: + name: pg-netbird-app + key: uri + - name: NETBIRD_AUTH_AUDIENCE + valueFrom: + secretKeyRef: + name: netbird-oauth-secret + key: client-id + - name: NETBIRD_IDP_MGMT_CLIENT_ID + valueFrom: + secretKeyRef: + name: netbird-oauth-secret + key: client-id + - name: NETBIRD_IDP_MANAGER_EXTRA_CONFIG_USERNAME + valueFrom: + secretKeyRef: + name: netbird-oauth-secret + key: service-username + - name: NETBIRD_IDP_MANAGER_EXTRA_CONFIG_PASSWORD + valueFrom: + secretKeyRef: + name: netbird-oauth-secret + key: service-password + + ingress: + enabled: false + +signal: + enabled: true + securityContext: + capabilities: + drop: + - ALL + resources: + limits: + memory: 128Mi + requests: + cpu: 25m + memory: 64Mi + env: + NB_METRICS_PORT: "9090" + ingress: + enabled: false + +dashboard: + enabled: true + + image: + tag: v2.33.0 + + resources: + limits: + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + + # Fix envsubst bug: USE_AUTH0="false" becomes JS string "false" (truthy). + # The template wraps $$USE_AUTH0 in quotes, so "false" !== false in JS. + # This causes @axa-fr/react-oidc to use Auth0-specific endpoint paths + # instead of fetching from .well-known/openid-configuration. + # postStart waits for init_react_envs.sh to finish, then patches the JS. + lifecycle: + postStart: + exec: + command: + - /bin/sh + - -c + - | + sleep 3 + # Fix auth0Auth: envsubst wraps "false" in quotes → JS truthy string. + # Must be boolean false for @axa-fr/react-oidc to use OIDC discovery. + find /usr/share/nginx/html -name '*.js' -exec sed -i 's/auth0Auth:"false"/auth0Auth:false/g' {} + + # Fix nginx try_files: SPA fallback to /index.html instead of =404. + # Next.js App Router RSC fetches .txt files; the default =404 breaks navigation. + sed -i 's|try_files $uri $uri.html $uri/ =404|try_files $uri $uri.html $uri/ /index.html|' /etc/nginx/http.d/default.conf + nginx -s reload + + # Simple key-value env vars + env: + USE_AUTH0: "false" + NETBIRD_MGMT_API_ENDPOINT: "https://netbird.kube.huskypup.net:443" + NETBIRD_MGMT_GRPC_API_ENDPOINT: "https://netbird.kube.huskypup.net:443" + NETBIRD_TOKEN_SOURCE: "accessToken" + + # Authentik OIDC (trailing slash must match issuer in OIDC discovery) + AUTH_AUTHORITY: "https://auth.kube.huskypup.net/application/o/netbird/" + AUTH_SUPPORTED_SCOPES: "openid profile email offline_access goauthentik.io/api" + AUTH_REDIRECT_URI: "/auth" + AUTH_SILENT_REDIRECT_URI: "/silent-auth" + + # Complex env vars (valueFrom references) + envRaw: + - name: AUTH_CLIENT_ID + valueFrom: + secretKeyRef: + name: netbird-oauth-secret + key: client-id + - name: AUTH_AUDIENCE + valueFrom: + secretKeyRef: + name: netbird-oauth-secret + key: client-id + + # Mount corrected OidcTrustedDomains.js.tmpl that includes AUTH_AUTHORITY + # Default template only trusts NETBIRD_MGMT_API_ENDPOINT; @axa-fr/react-oidc + # needs the OIDC authority domain to allow discovery/token/userinfo fetches + volumes: + - name: oidc-trusted-domains + configMap: + name: netbird-oidc-trusted-domains + volumeMounts: + - name: oidc-trusted-domains + mountPath: /usr/share/nginx/html/OidcTrustedDomains.js.tmpl + subPath: OidcTrustedDomains.js.tmpl + + ingress: + enabled: false + +# Relay (TURN) for NAT traversal +relay: + enabled: true + securityContext: + capabilities: + drop: + - ALL + + image: + tag: 0.66.0 + + resources: + limits: + memory: 256Mi + requests: + cpu: 50m + memory: 64Mi + + env: + NB_LISTEN_ADDRESS: ":33080" + NB_EXPOSED_ADDRESS: "rels://netbird.kube.huskypup.net:443" + NB_AUTH_SECRET: "zATjQ+YwNLvSBPD04R2+z7WzUhrwG1rkH3b/4Xsjvz0=" + NB_LOG_LEVEL: info + NB_METRICS_PORT: "9090" diff --git a/infrastructure/percona-everest/manifests/istio-virtualservice.yaml b/infrastructure/percona-everest/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..eee3b10 --- /dev/null +++ b/infrastructure/percona-everest/manifests/istio-virtualservice.yaml @@ -0,0 +1,18 @@ +# infrastructure/percona-everest/istio-virtualservice.yaml +# Percona Everest UI - Istio VirtualService +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: everest + namespace: everest-system +spec: + hosts: + - everest.kube.huskypup.net + gateways: + - istio-system/edge + http: + - route: + - destination: + host: everest.everest-system.svc.cluster.local + port: + number: 8080 diff --git a/infrastructure/percona-everest/values.yaml b/infrastructure/percona-everest/values.yaml new file mode 100644 index 0000000..156e43b --- /dev/null +++ b/infrastructure/percona-everest/values.yaml @@ -0,0 +1,95 @@ +# infrastructure/percona-everest/values.yaml +# Percona Everest - Cloud-native database management platform +# +# Provides MySQL (PXC), MongoDB (PSMDB), and PostgreSQL operators +# with a unified web UI dashboard for managing database clusters. +# +# UI: https://everest.kube.huskypup.net +# DB Namespace: everest (managed databases go here) +# Operator Namespace: everest-system +# OLM Namespace: everest-olm + +# Disable telemetry +telemetry: false + +# Server configuration +server: + resources: + limits: + memory: 500Mi + requests: + cpu: 10m + memory: 20Mi + service: + name: everest + type: ClusterIP + port: 8080 + # RBAC - enabled for security + rbac: + enabled: true + policy: | + g, admin, role:admin + # OIDC configuration for Authentik SSO + # Everest validates OIDC at startup via the issuerUrl's .well-known endpoint. + # The ConfigMap key is "oidc.config" (YAML block with clientId, issuerUrl, scopes). + # The helm.sh/resource-policy: keep annotation preserves it across upgrades. + # On fresh install, OIDC may not be available yet (Authentik blueprint not processed). + # In that case, leave oidc empty here and patch the ConfigMap after Authentik is ready: + # kubectl -n everest-system patch configmap everest-settings --type=merge \ + # -p '{"data":{"oidc.config":"clientId: percona-everest-oidc-client-id-static-2025\nissuerUrl: https://auth.kube.huskypup.net/application/o/percona-everest/\nscopes:\n - openid\n - profile\n - email\n"}}' + # kubectl -n everest-system rollout restart deployment/everest-server + oidc: {} + # TLS terminated at ingress layer - no need for server-side TLS + tls: + enabled: false + +# Operator configuration +operator: + resources: + limits: + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + +# Data importers - all three DB operators enabled +dataImporters: + perconaPGOperator: + enabled: true + perconaPXCOperator: + enabled: true + perconaPSMDBOperator: + enabled: true + +# OLM is required for Everest operator management +olm: + install: true + namespace: "everest-olm" + namespaceOverride: "everest-olm" + +# Database namespace - where managed DB clusters are deployed +dbNamespace: + enabled: true + namespaceOverride: "everest" + +# ===================================================== +# DISABLE ALL BUNDLED MONITORING +# We already have Prometheus + Grafana (Phase 5) +# ===================================================== +kube-state-metrics: + enabled: false + +monitoring: + enabled: false + +createMonitoringResources: false + +pmm: + enabled: false + +pmm3: + enabled: false + +# Ingress handled externally via percona-everest/ingress.yaml +ingress: + enabled: false diff --git a/infrastructure/prometheus/manifests/envoy-route.yaml b/infrastructure/prometheus/manifests/envoy-route.yaml new file mode 100644 index 0000000..8b9b786 --- /dev/null +++ b/infrastructure/prometheus/manifests/envoy-route.yaml @@ -0,0 +1,20 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: prometheus + namespace: prometheus +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - prometheus.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: kube-prometheus-stack-prometheus + port: 9090 diff --git a/infrastructure/prometheus/manifests/istio-virtualservice.yaml b/infrastructure/prometheus/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..92ed612 --- /dev/null +++ b/infrastructure/prometheus/manifests/istio-virtualservice.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: prometheus + namespace: prometheus +spec: + hosts: + - prometheus.kube.huskypup.net + gateways: + - istio-system/edge + http: + - route: + - destination: + host: kube-prometheus-stack-prometheus.prometheus.svc.cluster.local + port: + number: 9090 diff --git a/infrastructure/prometheus/manifests/talos-client-cert-secret.yaml b/infrastructure/prometheus/manifests/talos-client-cert-secret.yaml new file mode 100644 index 0000000..62bce9d --- /dev/null +++ b/infrastructure/prometheus/manifests/talos-client-cert-secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: talos-client-cert + namespace: prometheus +type: Opaque +data: + ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJQekNCOHFBREFnRUNBaEVBelVmaHJ2MW5uS21DcHVLTW90MUhOakFGQmdNclpYQXdFREVPTUF3R0ExVUUKQ2hNRmRHRnNiM013SGhjTk1qVXhNVE13TVRrME56UXlXaGNOTXpVeE1USTRNVGswTnpReVdqQVFNUTR3REFZRApWUVFLRXdWMFlXeHZjekFxTUFVR0F5dGxjQU1oQUpWWThDUDAyN0ZQWnJONzJPMlZkaGRCYjY3Z0plaUFINloxClNlUDAvWFlBbzJFd1h6QU9CZ05WSFE4QkFmOEVCQU1DQW9Rd0hRWURWUjBsQkJZd0ZBWUlLd1lCQlFVSEF3RUcKQ0NzR0FRVUZCd01DTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3SFFZRFZSME9CQllFRk1zbmNVTVo4ZGZhVlR3Ugo3RVVZNEl1TjZCZEJNQVVHQXl0bGNBTkJBTjlkK2laekxVdjJXQXNxNWdPbDh2bmhzSHhRMXY3SU1zOGtEZFNHClNUTVZSa1hvTzNKZmdqNUZ4QUMrN2JmV3hGZytzdW9rU3dWdWVBZ1VJTzFrUXdFPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + client.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJLRENCMjZBREFnRUNBaEJEZTFWQ2QvZVkyQkcyenpWRWdSdkVNQVVHQXl0bGNEQVFNUTR3REFZRFZRUUsKRXdWMFlXeHZjekFlRncweU5URXhNekF4T1RRM05ESmFGdzB5TmpFeE16QXhPVFEzTkRKYU1CTXhFVEFQQmdOVgpCQW9UQ0c5ek9tRmtiV2x1TUNvd0JRWURLMlZ3QXlFQWdkMUgyQi9wS0ZoaCtsdExwbTY5YkxCUEZVVU5PTE5KCjlFQWFxbUVmRjkyalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0QKQWpBZkJnTlZIU01FR0RBV2dCVExKM0ZER2ZIWDJsVThFZXhGR09DTGplZ1hRVEFGQmdNclpYQURRUUNHU1RPUwpHckxpbDVycWcxUDd2NkVkY0dpVXNEZ0x1WVUxcGF5STUzM1NUVTNDVTNnTCtiNkZDY3FWUSt6aTllejM1bWZZClkyWlBlenBXRlVKdUEva0sKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + client.key: LS0tLS1CRUdJTiBFRDI1NTE5IFBSSVZBVEUgS0VZLS0tLS0KTUM0Q0FRQXdCUVlESzJWd0JDSUVJTWhDTXVWU0gzcG9XZlR3em14MmxjSm43bjE0ZmtLb0cxQStjejVOSWlLRwotLS0tLUVORCBFRDI1NTE5IFBSSVZBVEUgS0VZLS0tLS0K \ No newline at end of file diff --git a/infrastructure/prometheus/manifests/talos-metrics-proxy.yaml b/infrastructure/prometheus/manifests/talos-metrics-proxy.yaml new file mode 100644 index 0000000..d048e18 --- /dev/null +++ b/infrastructure/prometheus/manifests/talos-metrics-proxy.yaml @@ -0,0 +1,232 @@ +# Talos Metrics Proxy +# Workaround for HTTP 415 errors when scraping Talos apid metrics +# This deploys an nginx proxy that strips problematic HTTP headers + +apiVersion: v1 +kind: ConfigMap +metadata: + name: talos-metrics-proxy-config + namespace: kube-system +data: + nginx.conf: | + pid /tmp/nginx.pid; + + events { + worker_connections 1024; + } + + http { + proxy_temp_path /tmp/proxy_temp; + client_body_temp_path /tmp/client_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + # Upstream Talos nodes + upstream talos01 { + server 172.28.101.21:50000; + } + upstream talos02 { + server 172.28.101.22:50000; + } + upstream talos03 { + server 172.28.101.23:50000; + } + upstream talos04 { + server 172.28.101.24:50000; + } + + # Proxy for talos01 + server { + listen 9100; + server_name talos01-metrics; + + location /metrics { + proxy_pass https://talos01/metrics; + proxy_ssl_certificate /certs/client.crt; + proxy_ssl_certificate_key /certs/client.key; + proxy_ssl_trusted_certificate /certs/ca.crt; + proxy_ssl_verify on; + proxy_http_version 1.1; + proxy_set_header Accept "text/plain;version=0.0.4"; + proxy_set_header Connection ""; + } + } + + # Proxy for talos02 + server { + listen 9101; + server_name talos02-metrics; + + location /metrics { + proxy_pass https://talos02/metrics; + proxy_ssl_certificate /certs/client.crt; + proxy_ssl_certificate_key /certs/client.key; + proxy_ssl_trusted_certificate /certs/ca.crt; + proxy_ssl_verify on; + proxy_http_version 1.1; + proxy_set_header Accept "text/plain;version=0.0.4"; + proxy_set_header Connection ""; + } + } + + # Proxy for talos03 + server { + listen 9102; + server_name talos03-metrics; + + location /metrics { + proxy_pass https://talos03/metrics; + proxy_ssl_certificate /certs/client.crt; + proxy_ssl_certificate_key /certs/client.key; + proxy_ssl_trusted_certificate /certs/ca.crt; + proxy_ssl_verify on; + proxy_http_version 1.1; + proxy_set_header Accept "text/plain;version=0.0.4"; + proxy_set_header Connection ""; + } + } + + # Proxy for talos04 + server { + listen 9103; + server_name talos04-metrics; + + location /metrics { + proxy_pass https://talos04/metrics; + proxy_ssl_certificate /certs/client.crt; + proxy_ssl_certificate_key /certs/client.key; + proxy_ssl_trusted_certificate /certs/ca.crt; + proxy_ssl_verify on; + proxy_http_version 1.1; + proxy_set_header Accept "text/plain;version=0.0.4"; + proxy_set_header Connection ""; + } + } + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: talos-metrics-proxy + namespace: kube-system + labels: + app: talos-metrics-proxy +spec: + replicas: 1 + selector: + matchLabels: + app: talos-metrics-proxy + template: + metadata: + labels: + app: talos-metrics-proxy + spec: + automountServiceAccountToken: false + containers: + - name: nginx + image: nginx:1.27-alpine + securityContext: + runAsUser: 10101 + runAsGroup: 10101 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + ports: + - containerPort: 9100 + name: talos01 + - containerPort: 9101 + name: talos02 + - containerPort: 9102 + name: talos03 + - containerPort: 9103 + name: talos04 + volumeMounts: + - name: nginx-config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + - name: talos-certs + mountPath: /certs + readOnly: true + - name: tmp + mountPath: /tmp + - name: cache + mountPath: /var/cache/nginx + resources: + requests: + memory: "64Mi" + cpu: "10m" + limits: + memory: "128Mi" + volumes: + - name: nginx-config + configMap: + name: talos-metrics-proxy-config + - name: talos-certs + secret: + secretName: talos-client-cert + - name: tmp + emptyDir: {} + - name: cache + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: talos-metrics-proxy + namespace: kube-system + labels: + app: talos-metrics-proxy +spec: + type: ClusterIP + ports: + - port: 9100 + targetPort: 9100 + protocol: TCP + name: talos01 + - port: 9101 + targetPort: 9101 + protocol: TCP + name: talos02 + - port: 9102 + targetPort: 9102 + protocol: TCP + name: talos03 + - port: 9103 + targetPort: 9103 + protocol: TCP + name: talos04 + selector: + app: talos-metrics-proxy +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: talos-metrics + namespace: kube-system + labels: + app: talos-metrics-proxy +spec: + endpoints: + - port: talos01 + path: /metrics + interval: 30s + - port: talos02 + path: /metrics + interval: 30s + - port: talos03 + path: /metrics + interval: 30s + - port: talos04 + path: /metrics + interval: 30s + selector: + matchLabels: + app: talos-metrics-proxy + namespaceSelector: + matchNames: + - kube-system diff --git a/infrastructure/prometheus/values.yaml b/infrastructure/prometheus/values.yaml new file mode 100644 index 0000000..3824ced --- /dev/null +++ b/infrastructure/prometheus/values.yaml @@ -0,0 +1,87 @@ +# values/prometheus.values.yaml + +# Prometheus Operator configuration +prometheus: + prometheusSpec: + # High availability with 2 replicas + replicas: 2 + + # Reduce retention to lower I/O on shared NVMe storage + retention: 7d + + storageSpec: + volumeClaimTemplate: + spec: + storageClassName: rook-ceph-block + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi + + # Enable ServiceMonitor discovery across all namespaces + # NOTE: empty {} is falsy in Go templates, so the chart falls back to + # requiring release: label. Setting NilUsesHelmValues=false + # ensures the empty selector matches ALL monitors. + serviceMonitorSelector: {} + serviceMonitorSelectorNilUsesHelmValues: false + serviceMonitorNamespaceSelector: {} + podMonitorSelector: {} + podMonitorSelectorNilUsesHelmValues: false + podMonitorNamespaceSelector: {} + + # Mount Talos client certificates + secrets: + - talos-client-cert + + # Additional scrape configurations for Talos + # NOTE: Talos apid endpoint at port 50000 returns HTTP 415 errors with Prometheus v2.x + # This is a known compatibility issue between Talos v1.10.x and modern Prometheus + # The issue is that Prometheus sends Accept headers for content negotiation that + # Talos apid doesn't understand. Workarounds require a reverse proxy or using kubelet. + # For now, Talos metrics are disabled. Node metrics are still available via kubelet. + additionalScrapeConfigs: [] + + # DISABLED - Talos apid scraping (returns 415 Unsupported Media Type) + # - job_name: 'talos-apid' + # honor_timestamps: true + # scrape_interval: 30s + # scrape_timeout: 10s + # metrics_path: /metrics + # scheme: https + # scrape_protocols: [] + # enable_http2: false + # enable_compression: false + # tls_config: + # ca_file: /etc/prometheus/secrets/talos-client-cert/ca.crt + # cert_file: /etc/prometheus/secrets/talos-client-cert/client.crt + # key_file: /etc/prometheus/secrets/talos-client-cert/client.key + # insecure_skip_verify: false + # kubernetes_sd_configs: + # - role: node + # relabel_configs: + # - source_labels: [__address__] + # regex: '(.*):.*' + # replacement: '${1}:50000' + # target_label: __address__ + # - source_labels: [__meta_kubernetes_node_name] + # target_label: node + + # Ingress disabled - Istio VirtualService handles routing + ingress: + enabled: false + +alertmanager: + enabled: true + +# Disable node-exporter due to PodSecurity restrictions +prometheus-node-exporter: + enabled: false + +# Enable kube-state-metrics for Kubernetes object metrics +kube-state-metrics: + enabled: true + +# Grafana (included in kube-prometheus-stack) +grafana: + enabled: false # We deploy Grafana separately diff --git a/infrastructure/reloader/values.yaml b/infrastructure/reloader/values.yaml new file mode 100644 index 0000000..59cd5b0 --- /dev/null +++ b/infrastructure/reloader/values.yaml @@ -0,0 +1,4 @@ +reloader: + watchGlobally: true + ignoreSecrets: "" + ignoreConfigMaps: "" diff --git a/infrastructure/rook-ceph/cluster-values.yaml b/infrastructure/rook-ceph/cluster-values.yaml new file mode 100644 index 0000000..51bc6f8 --- /dev/null +++ b/infrastructure/rook-ceph/cluster-values.yaml @@ -0,0 +1,219 @@ +# Rook-Ceph Cluster Values +# https://github.com/rook/rook/blob/master/deploy/charts/rook-ceph-cluster/values.yaml + +# Operator namespace (must match where operator is deployed) +operatorNamespace: rook-ceph + +# Enable toolbox for debugging +toolbox: + enabled: true + tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + resources: + limits: + memory: "256Mi" + requests: + cpu: "50m" + memory: "64Mi" + +# Enable Prometheus monitoring +monitoring: + enabled: true + createPrometheusRules: true + +# Ceph Cluster specification +cephClusterSpec: + # Ceph version - Squid (v19) + cephVersion: + image: quay.io/ceph/ceph:v19.2.0 + allowUnsupported: false + + # Data directory on host + dataDirHostPath: /var/lib/rook + + # Skip upgrade checks (homelab - we control updates) + skipUpgradeChecks: false + + # Continue if host not found + continueUpgradeAfterChecksEvenIfNotHealthy: false + + # Wait for nodes before starting OSDs + waitTimeoutForHealthyOSDInMinutes: 10 + + # Mon configuration + mon: + count: 3 + allowMultiplePerNode: false + + # MGR configuration + mgr: + count: 2 + allowMultiplePerNode: false + modules: + - name: pg_autoscaler + enabled: true + - name: rook + enabled: true + - name: dashboard + enabled: true + + # Dashboard configuration + dashboard: + enabled: true + ssl: false # TLS handled by ingress + port: 7000 + # Authentication: OAuth2-Proxy via ingress annotations + # See: infrastructure/rook-ceph/ingress.yaml + # Blueprint: infrastructure/authentik/ceph-dashboard-blueprint.yaml + config: + mgr/dashboard/standby_behaviour: "error" + mgr/dashboard/remote_user_header: "X-Forwarded-User" + mgr/dashboard/remote_user_trust_proxy: "true" + + # Network configuration + network: + connections: + encryption: + enabled: false + compression: + enabled: false + + # Crash collector + crashCollector: + disable: false + + # Log collector + logCollector: + enabled: true + periodicity: daily + maxLogSize: 500M + + # Cleanup policy (for uninstall) + cleanupPolicy: + confirmation: "" + sanitizeDisks: + method: quick + dataSource: zero + iteration: 1 + allowUninstallWithVolumes: false + + # Resource limits + resources: + mgr: + limits: + memory: "1Gi" + requests: + cpu: "100m" + memory: "512Mi" + mon: + limits: + memory: "2Gi" + requests: + cpu: "100m" + memory: "512Mi" + osd: + limits: + memory: "16Gi" + requests: + cpu: "2" + memory: "16Gi" + prepareosd: + limits: + memory: "400Mi" + requests: + cpu: "100m" + memory: "50Mi" + + # Storage configuration - explicit nodes with passthrough NVMe + # sdb = dedicated 60GB WAL/DB device per node + storage: + useAllNodes: false + useAllDevices: false + nodes: + - name: "talos-cp-01" + devices: + - name: "nvme0n1" + config: + metadataDevice: "sdb" + - name: "talos-cp-02" + devices: + - name: "nvme0n1" + config: + metadataDevice: "sdb" + - name: "talos-cp-03" + devices: + - name: "nvme0n1" + config: + metadataDevice: "sdb" + - name: "talos-cp-04" + devices: + - name: "nvme0n1" + config: + metadataDevice: "sdb" + config: + osdsPerDevice: "1" + + # Placement - allow on control-plane + placement: + all: + tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + + # Health check configuration + healthCheck: + daemonHealth: + mon: + disabled: false + interval: 45s + osd: + disabled: false + interval: 60s + status: + disabled: false + interval: 60s + livenessProbe: + mon: + disabled: false + mgr: + disabled: false + osd: + disabled: false + +# Block pools configuration - matches existing deployment +cephBlockPools: + - name: replicapool + spec: + failureDomain: host + replicated: + size: 2 # 2 replicas + storageClass: + enabled: true + name: rook-ceph-block + isDefault: true + reclaimPolicy: Delete + allowVolumeExpansion: true + volumeBindingMode: Immediate + parameters: + imageFormat: "2" + imageFeatures: layering + csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner + csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph + csi.storage.k8s.io/controller-expand-secret-name: rook-csi-rbd-provisioner + csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph + csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node + csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph + csi.storage.k8s.io/fstype: ext4 + +# Disable CephFS (not needed) +cephFileSystems: [] + +# Object Store with OIDC support - simplified for now +cephObjectStores: [] + +# Ingress for dashboard (also applied via postsync hook) +ingress: + dashboard: {} diff --git a/infrastructure/rook-ceph/manifests/cephfs-filesystem.yaml b/infrastructure/rook-ceph/manifests/cephfs-filesystem.yaml new file mode 100644 index 0000000..a080da5 --- /dev/null +++ b/infrastructure/rook-ceph/manifests/cephfs-filesystem.yaml @@ -0,0 +1,17 @@ +apiVersion: ceph.rook.io/v1 +kind: CephFilesystem +metadata: + name: fileshare + namespace: rook-ceph +spec: + metadataPool: + replicated: + size: 2 + dataPools: + - name: data0 + replicated: + size: 2 + preserveFilesystemOnDelete: true + metadataServer: + activeCount: 1 + activeStandby: true diff --git a/infrastructure/rook-ceph/manifests/cephfs-storageclass.yaml b/infrastructure/rook-ceph/manifests/cephfs-storageclass.yaml new file mode 100644 index 0000000..797041d --- /dev/null +++ b/infrastructure/rook-ceph/manifests/cephfs-storageclass.yaml @@ -0,0 +1,17 @@ +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: rook-cephfs +provisioner: rook-ceph.cephfs.csi.ceph.com +parameters: + clusterID: rook-ceph + fsName: fileshare + csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner + csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph + csi.storage.k8s.io/controller-expand-secret-name: rook-csi-cephfs-provisioner + csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph + csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node + csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph +reclaimPolicy: Delete +volumeBindingMode: Immediate +allowVolumeExpansion: true diff --git a/infrastructure/rook-ceph/manifests/envoy-backend.yaml b/infrastructure/rook-ceph/manifests/envoy-backend.yaml new file mode 100644 index 0000000..3ef1182 --- /dev/null +++ b/infrastructure/rook-ceph/manifests/envoy-backend.yaml @@ -0,0 +1,12 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: Backend +metadata: + name: ceph-dashboard + namespace: rook-ceph +spec: + endpoints: + - fqdn: + hostname: rook-ceph-mgr-dashboard.rook-ceph.svc.cluster.local + port: 8443 + tls: + insecureSkipVerify: true diff --git a/infrastructure/rook-ceph/manifests/envoy-route.yaml b/infrastructure/rook-ceph/manifests/envoy-route.yaml new file mode 100644 index 0000000..bbfa626 --- /dev/null +++ b/infrastructure/rook-ceph/manifests/envoy-route.yaml @@ -0,0 +1,21 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: ceph-dashboard + namespace: rook-ceph +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - ceph.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - group: gateway.envoyproxy.io + kind: Backend + name: ceph-dashboard diff --git a/infrastructure/rook-ceph/manifests/istio-destinationrule.yaml b/infrastructure/rook-ceph/manifests/istio-destinationrule.yaml new file mode 100644 index 0000000..5a233be --- /dev/null +++ b/infrastructure/rook-ceph/manifests/istio-destinationrule.yaml @@ -0,0 +1,12 @@ +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: rook-ceph-mgr-dashboard + namespace: rook-ceph +spec: + host: rook-ceph-mgr-dashboard.rook-ceph.svc.cluster.local + trafficPolicy: + tls: + mode: SIMPLE + sni: rook-ceph-mgr-dashboard.rook-ceph.svc.cluster.local + insecureSkipVerify: true diff --git a/infrastructure/rook-ceph/manifests/istio-virtualservice.yaml b/infrastructure/rook-ceph/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..765179a --- /dev/null +++ b/infrastructure/rook-ceph/manifests/istio-virtualservice.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: ceph-dashboard + namespace: rook-ceph +spec: + hosts: + - ceph.kube.huskypup.net + gateways: + - istio-system/edge + http: + - timeout: 3600s + route: + - destination: + host: rook-ceph-mgr-dashboard.rook-ceph.svc.cluster.local + port: + number: 8443 diff --git a/infrastructure/rook-ceph/manifests/nfs-external-service.yaml b/infrastructure/rook-ceph/manifests/nfs-external-service.yaml new file mode 100644 index 0000000..665c4de --- /dev/null +++ b/infrastructure/rook-ceph/manifests/nfs-external-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: rook-ceph-nfs-fileshare-external + namespace: rook-ceph + labels: + app: rook-ceph-nfs + ceph_nfs: fileshare +spec: + type: LoadBalancer + selector: + ceph_nfs: fileshare + ports: + - name: nfs + port: 2049 + targetPort: 2049 + protocol: TCP diff --git a/infrastructure/rook-ceph/manifests/nfs.yaml b/infrastructure/rook-ceph/manifests/nfs.yaml new file mode 100644 index 0000000..da50d6d --- /dev/null +++ b/infrastructure/rook-ceph/manifests/nfs.yaml @@ -0,0 +1,13 @@ +apiVersion: ceph.rook.io/v1 +kind: CephNFS +metadata: + name: fileshare + namespace: rook-ceph +spec: + server: + active: 1 + placement: + tolerations: + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule diff --git a/infrastructure/rook-ceph/manifests/servicemonitor.yaml b/infrastructure/rook-ceph/manifests/servicemonitor.yaml new file mode 100644 index 0000000..a9d6650 --- /dev/null +++ b/infrastructure/rook-ceph/manifests/servicemonitor.yaml @@ -0,0 +1,21 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: rook-ceph-mgr + namespace: rook-ceph + labels: + app: rook-ceph-mgr + rook_cluster: rook-ceph + release: kube-prometheus-stack +spec: + namespaceSelector: + matchNames: + - rook-ceph + selector: + matchLabels: + app: rook-ceph-mgr + rook_cluster: rook-ceph + endpoints: + - port: http-metrics + path: /metrics + interval: 30s diff --git a/infrastructure/rook-ceph/manifests/smb-gateway.yaml b/infrastructure/rook-ceph/manifests/smb-gateway.yaml new file mode 100644 index 0000000..f16f7a9 --- /dev/null +++ b/infrastructure/rook-ceph/manifests/smb-gateway.yaml @@ -0,0 +1,83 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: cephfs-smb-pvc + namespace: rook-ceph + labels: + app: cephfs-smb-gateway +spec: + accessModes: + - ReadWriteMany + storageClassName: rook-cephfs + resources: + requests: + storage: 1Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cephfs-smb-gateway + namespace: rook-ceph + labels: + app: cephfs-smb-gateway +spec: + replicas: 1 + selector: + matchLabels: + app: cephfs-smb-gateway + template: + metadata: + labels: + app: cephfs-smb-gateway + spec: + containers: + - name: smb + image: docker.io/dperson/samba:amd64 + args: + - -s + - "fileshare;/fileshare;yes;no;no;all" + - -u + - "fileshare;fileshare" + - -w + - "WORKGROUP" + ports: + - containerPort: 139 + name: smb + - containerPort: 445 + name: smb-alt + volumeMounts: + - name: cephfs + mountPath: /fileshare + securityContext: + runAsUser: 0 + runAsGroup: 0 + privileged: true + volumes: + - name: cephfs + persistentVolumeClaim: + claimName: cephfs-smb-pvc + tolerations: + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule +--- +apiVersion: v1 +kind: Service +metadata: + name: cephfs-smb-gateway + namespace: rook-ceph + labels: + app: cephfs-smb-gateway +spec: + type: LoadBalancer + selector: + app: cephfs-smb-gateway + ports: + - name: smb + port: 139 + targetPort: 139 + protocol: TCP + - name: smb-alt + port: 445 + targetPort: 445 + protocol: TCP diff --git a/infrastructure/rook-ceph/operator-values.yaml b/infrastructure/rook-ceph/operator-values.yaml new file mode 100644 index 0000000..da646ae --- /dev/null +++ b/infrastructure/rook-ceph/operator-values.yaml @@ -0,0 +1,132 @@ +# Rook-Ceph Operator Values +# https://github.com/rook/rook/blob/master/deploy/charts/rook-ceph/values.yaml + +# Install CRDs with the chart (required for bootstrap) +crds: + enabled: true + +# Enable RBD CSI driver (block storage) +enableRbdDriver: true + +# Disable CephFS CSI driver (not needed for this deployment) +enableCephfsDriver: false + +# Enable Prometheus monitoring +monitoring: + enabled: true + +# Allow operator to run on control-plane nodes +tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + +# Resource limits for operator +resources: + limits: + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + +# Enable device discovery daemon +enableDiscoveryDaemon: true + +# CSI driver configuration +csi: + # Enable RBD provisioner + enableRbdDriver: true + # Disable CephFS provisioner + enableCephfsDriver: false + + # Enable CSI host networking for better performance + enableCSIHostNetwork: true + + # CSI plugin tolerations for running on all nodes + pluginTolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + + provisionerTolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + + # Provisioner replicas (HA) + provisionerReplicas: 2 + + # Resource limits for CSI pods + csiRBDPluginResource: | + - name : driver-registrar + resource: + requests: + memory: 64Mi + cpu: 25m + limits: + memory: 128Mi + - name : csi-rbdplugin + resource: + requests: + memory: 128Mi + cpu: 50m + limits: + memory: 512Mi + - name : liveness-prometheus + resource: + requests: + memory: 64Mi + cpu: 25m + limits: + memory: 128Mi + + csiRBDProvisionerResource: | + - name : csi-provisioner + resource: + requests: + memory: 64Mi + cpu: 25m + limits: + memory: 256Mi + - name : csi-resizer + resource: + requests: + memory: 64Mi + cpu: 25m + limits: + memory: 256Mi + - name : csi-attacher + resource: + requests: + memory: 64Mi + cpu: 25m + limits: + memory: 256Mi + - name : csi-snapshotter + resource: + requests: + memory: 64Mi + cpu: 25m + limits: + memory: 256Mi + - name : csi-rbdplugin + resource: + requests: + memory: 128Mi + cpu: 50m + limits: + memory: 512Mi + - name : liveness-prometheus + resource: + requests: + memory: 64Mi + cpu: 25m + limits: + memory: 128Mi + +# Discover devices automatically +discover: + tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" diff --git a/infrastructure/scylla/cluster-values.yaml b/infrastructure/scylla/cluster-values.yaml new file mode 100644 index 0000000..6be333d --- /dev/null +++ b/infrastructure/scylla/cluster-values.yaml @@ -0,0 +1,39 @@ +# infrastructure/scylla/cluster-values.yaml +# ScyllaCluster (database) + +# Homelab-friendly mode (reduced kernel tuning requirements) +developerMode: true + +# Use your Ceph-backed default StorageClass +racks: + - name: rack-a + members: 3 + storage: + storageClassName: rook-ceph-block + capacity: 50Gi + + # Requests drive scheduling; keep low for bootstrap. + # Limits drive Scylla shard count (cpu limit = shard count). + resources: + requests: + cpu: 10m + memory: 512Mi + limits: + cpu: 2 + memory: 4Gi + + agentResources: + requests: + cpu: 100m + memory: 128Mi + + placement: {} + +alternator: + enabled: false + insecureEnableHTTP: false + +serviceMonitor: + create: true + labels: + release: kube-prometheus-stack diff --git a/infrastructure/scylla/manager-postsync-fix.sh b/infrastructure/scylla/manager-postsync-fix.sh new file mode 100755 index 0000000..c21b742 --- /dev/null +++ b/infrastructure/scylla/manager-postsync-fix.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Post-sync fix for scylla-manager: the chart-created NetworkPolicy +# blocks cross-node ClusterIP traffic due to a CNI/kube-proxy DNAT +# interaction. Delete it so the manager API can reach its backend +# ScyllaCluster via the per-pod service from any node. +set -euo pipefail + +NS=scylla-manager + +echo "Removing NetworkPolicy that blocks cross-node ClusterIP traffic..." +kubectl -n "${NS}" delete networkpolicy scylla-manager-to-scylla-pod --ignore-not-found=true diff --git a/infrastructure/scylla/manager-values.yaml b/infrastructure/scylla/manager-values.yaml new file mode 100644 index 0000000..fb50197 --- /dev/null +++ b/infrastructure/scylla/manager-values.yaml @@ -0,0 +1,42 @@ +# infrastructure/scylla/manager-values.yaml +# Scylla Manager (with its own small Scylla backend) + +logLevel: info + +resources: + requests: + cpu: 10m + memory: 128Mi + +scylla: + # Homelab-friendly mode + developerMode: true + + # Use Ceph for the manager's backend storage + racks: + - name: manager-rack + members: 1 + storage: + capacity: 10Gi + storageClassName: rook-ceph-block + resources: + # Requests drive Kubernetes scheduling. Keep them extremely low so the + # embedded Scylla backend can schedule even when the cluster is + # request-saturated. + requests: + cpu: 1m + memory: 1Gi + limits: + cpu: 1 + memory: 2Gi + agentResources: + requests: + cpu: 1m + memory: 10M + placement: {} + + +serviceMonitor: + create: true + labels: + release: kube-prometheus-stack diff --git a/infrastructure/scylla/manifests/istio-virtualservice.yaml b/infrastructure/scylla/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..5c0a345 --- /dev/null +++ b/infrastructure/scylla/manifests/istio-virtualservice.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: scylla-manager + namespace: scylla-manager +spec: + hosts: + - scylla-manager.kube.huskypup.net + gateways: + - istio-system/edge + http: + - route: + - destination: + host: scylla-manager.scylla-manager.svc.cluster.local + port: + number: 80 diff --git a/infrastructure/scylla/operator-values.yaml b/infrastructure/scylla/operator-values.yaml new file mode 100644 index 0000000..12a3610 --- /dev/null +++ b/infrastructure/scylla/operator-values.yaml @@ -0,0 +1,18 @@ +# infrastructure/scylla/operator-values.yaml +# Scylla Operator +replicas: 2 +logLevel: 2 + +webhook: + # Use cert-manager to generate the self-signed cert + createSelfSignedCertificate: true + +resources: + requests: + cpu: 100m + memory: 128Mi + +webhookServerResources: + requests: + cpu: 10m + memory: 64Mi diff --git a/infrastructure/scylla/values.yaml b/infrastructure/scylla/values.yaml new file mode 100644 index 0000000..a09d1bc --- /dev/null +++ b/infrastructure/scylla/values.yaml @@ -0,0 +1,51 @@ +# ScyllaCluster (via scylla-operator Helm chart) +# Bootstrap-friendly defaults: +# - Uses Rook-Ceph RBD PVCs (rook-ceph-block) +# - No required node affinity/tolerations +# - Enables ServiceMonitor for Prometheus + +scyllaImage: + repository: scylladb/scylla + tag: 2025.3.3 + +agentImage: + repository: scylladb/scylla-manager-agent + tag: 3.7.0 + +# Homelab: allow relaxed settings on smaller nodes. +developerMode: true + +# Enable Alternator (DynamoDB API) only if you want it. +alternator: + enabled: false + insecureEnableHTTP: false + +# Optional: enable cpu pinning once you dedicate nodes. +cpuset: false + +# Datacenter + racks +# NOTE: This chart creates a ScyllaCluster CR; the operator creates the StatefulSets. +datacenter: homelab-dc +racks: + - name: rack-a + members: 3 + storage: + storageClassName: rook-ceph-block + capacity: 50Gi + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 2 + memory: 8Gi + # No strict scheduling constraints for bootstrap. + placement: + nodeAffinity: {} + tolerations: [] + +# Prometheus +serviceMonitor: + create: true + labels: + release: kube-prometheus-stack diff --git a/infrastructure/tetragon/grafana-dashboard.yaml b/infrastructure/tetragon/grafana-dashboard.yaml new file mode 100644 index 0000000..a7bd5cb --- /dev/null +++ b/infrastructure/tetragon/grafana-dashboard.yaml @@ -0,0 +1,84 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard-tetragon + namespace: grafana + labels: + grafana_dashboard: "1" +data: + tetragon-overview.json: | + { + "annotations": { "list": [] }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "title": "Process Exec Events (rate/5m)", + "type": "timeseries", + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "sum(rate(tetragon_events_total{type=\"PROCESS_EXEC\"}[5m])) by (namespace)", + "legendFormat": "{{namespace}}" + } + ], + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 } + }, + { + "title": "Network Connections (rate/5m)", + "type": "timeseries", + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "sum(rate(tetragon_events_total{type=\"PROCESS_KPROBE\"}[5m])) by (namespace)", + "legendFormat": "{{namespace}}" + } + ], + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 } + }, + { + "title": "Policy Events by Type", + "type": "piechart", + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "sum(tetragon_events_total) by (type)", + "legendFormat": "{{type}}" + } + ], + "gridPos": { "h": 8, "w": 8, "x": 0, "y": 8 } + }, + { + "title": "Tracing Policy Matches", + "type": "stat", + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "sum(tetragon_events_total{type=~\"PROCESS_KPROBE|PROCESS_TRACEPOINT\"})", + "legendFormat": "Total Policy Events" + } + ], + "gridPos": { "h": 8, "w": 8, "x": 8, "y": 8 } + }, + { + "title": "Events by Namespace", + "type": "bargauge", + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "sum(rate(tetragon_events_total[5m])) by (namespace) > 0", + "legendFormat": "{{namespace}}" + } + ], + "gridPos": { "h": 8, "w": 8, "x": 16, "y": 8 } + } + ], + "schemaVersion": 39, + "tags": ["tetragon", "security", "zero-trust"], + "templating": { "list": [] }, + "time": { "from": "now-1h", "to": "now" }, + "title": "Tetragon Runtime Security", + "uid": "tetragon-overview" + } diff --git a/infrastructure/tetragon/tracing-policies/kubernetes-api-access.yaml b/infrastructure/tetragon/tracing-policies/kubernetes-api-access.yaml new file mode 100644 index 0000000..943b123 --- /dev/null +++ b/infrastructure/tetragon/tracing-policies/kubernetes-api-access.yaml @@ -0,0 +1,20 @@ +apiVersion: cilium.io/v1alpha1 +kind: TracingPolicy +metadata: + name: kubernetes-api-access-monitoring +spec: + kprobes: + - call: tcp_connect + syscall: false + args: + - index: 0 + type: sock + selectors: + - matchArgs: + - index: 0 + operator: DPort + values: + - "6443" + - "443" + matchActions: + - action: Post diff --git a/infrastructure/tetragon/tracing-policies/network-monitoring.yaml b/infrastructure/tetragon/tracing-policies/network-monitoring.yaml new file mode 100644 index 0000000..2ec17c0 --- /dev/null +++ b/infrastructure/tetragon/tracing-policies/network-monitoring.yaml @@ -0,0 +1,22 @@ +apiVersion: cilium.io/v1alpha1 +kind: TracingPolicy +metadata: + name: network-egress-monitoring +spec: + kprobes: + - call: tcp_connect + syscall: false + args: + - index: 0 + type: sock + selectors: + - matchActions: + - action: Post + - call: tcp_close + syscall: false + args: + - index: 0 + type: sock + selectors: + - matchActions: + - action: Post diff --git a/infrastructure/tetragon/tracing-policies/privilege-escalation.yaml b/infrastructure/tetragon/tracing-policies/privilege-escalation.yaml new file mode 100644 index 0000000..b9a58e2 --- /dev/null +++ b/infrastructure/tetragon/tracing-policies/privilege-escalation.yaml @@ -0,0 +1,32 @@ +apiVersion: cilium.io/v1alpha1 +kind: TracingPolicy +metadata: + name: privilege-escalation-detection +spec: + kprobes: + - call: __x64_sys_setuid + syscall: true + args: + - index: 0 + type: int + selectors: + - matchArgs: + - index: 0 + operator: Equal + values: + - "0" + matchActions: + - action: Post + - call: __x64_sys_setgid + syscall: true + args: + - index: 0 + type: int + selectors: + - matchArgs: + - index: 0 + operator: Equal + values: + - "0" + matchActions: + - action: Post diff --git a/infrastructure/tetragon/tracing-policies/process-execution.yaml b/infrastructure/tetragon/tracing-policies/process-execution.yaml new file mode 100644 index 0000000..6ef58d4 --- /dev/null +++ b/infrastructure/tetragon/tracing-policies/process-execution.yaml @@ -0,0 +1,31 @@ +apiVersion: cilium.io/v1alpha1 +kind: TracingPolicy +metadata: + name: shell-execution-monitoring +spec: + tracepoints: + - subsystem: raw_syscalls + event: sys_enter + args: + - index: 4 + type: syscall64 + selectors: + - matchArgs: + - index: 4 + operator: Equal + values: + - "59" # execve + matchBinaries: + - operator: In + values: + - /bin/sh + - /bin/bash + - /bin/dash + - /usr/bin/bash + - /usr/bin/sh + - /usr/bin/curl + - /usr/bin/wget + - /usr/bin/nc + - /usr/bin/ncat + matchActions: + - action: Post diff --git a/infrastructure/tetragon/tracing-policies/sensitive-file-access.yaml b/infrastructure/tetragon/tracing-policies/sensitive-file-access.yaml new file mode 100644 index 0000000..4da86f3 --- /dev/null +++ b/infrastructure/tetragon/tracing-policies/sensitive-file-access.yaml @@ -0,0 +1,26 @@ +apiVersion: cilium.io/v1alpha1 +kind: TracingPolicy +metadata: + name: sensitive-file-access +spec: + kprobes: + - call: fd_install + syscall: false + args: + - index: 0 + type: int + - index: 1 + type: file + selectors: + - matchArgs: + - index: 1 + operator: Prefix + values: + - /etc/shadow + - /etc/passwd + - /etc/gshadow + - /root/.ssh + - /etc/kubernetes/pki + - /var/run/secrets/kubernetes.io + matchActions: + - action: Post diff --git a/infrastructure/tetragon/values.yaml b/infrastructure/tetragon/values.yaml new file mode 100644 index 0000000..eead943 --- /dev/null +++ b/infrastructure/tetragon/values.yaml @@ -0,0 +1,33 @@ +# Tetragon eBPF Runtime Security - Zero Trust Configuration + +tetragon: + exportAllowList: |- + {"event_set": ["PROCESS_EXEC", "PROCESS_EXIT", "PROCESS_KPROBE", "PROCESS_TRACEPOINT"]} + prometheus: + enabled: true + serviceMonitor: + enabled: true + labels: + release: kube-prometheus-stack + resources: + limits: + memory: 512Mi + requests: + cpu: 50m + memory: 128Mi + +export: + stdout: + enabledCommand: true + enabledArgs: true + +# Enable JSON event export for log aggregation +exportDirectory: /var/run/cilium/tetragon + +# Prometheus metrics +prometheus: + enabled: true + serviceMonitor: + enabled: true + labels: + release: kube-prometheus-stack diff --git a/infrastructure/unpoller/manifests/deployment.yaml b/infrastructure/unpoller/manifests/deployment.yaml new file mode 100644 index 0000000..b108664 --- /dev/null +++ b/infrastructure/unpoller/manifests/deployment.yaml @@ -0,0 +1,130 @@ +--- +# Unpoller Deployment +# Collects metrics from Unifi Controller and exports to Prometheus +apiVersion: apps/v1 +kind: Deployment +metadata: + name: unpoller + namespace: unpoller + labels: + app: unpoller + app.kubernetes.io/name: unpoller + app.kubernetes.io/instance: unpoller + app.kubernetes.io/component: monitoring + annotations: + secret.reloader.stakater.com/reload: "unpoller-secret" +spec: + replicas: 1 + selector: + matchLabels: + app: unpoller + app.kubernetes.io/name: unpoller + template: + metadata: + labels: + app: unpoller + app.kubernetes.io/name: unpoller + annotations: + secret.reloader.stakater.com/reload: "unpoller-secret" + spec: + automountServiceAccountToken: false + containers: + - name: unpoller + image: ghcr.io/unpoller/unpoller:v2.11.2 + securityContext: + runAsUser: 10000 + runAsGroup: 10000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9130 + protocol: TCP + env: + - name: UP_UNIFI_DEFAULT_URL + value: "https://172.28.110.1" + - name: UP_UNIFI_DEFAULT_USER + valueFrom: + secretKeyRef: + name: unpoller-secret + key: unifi-user + - name: UP_UNIFI_DEFAULT_PASS + valueFrom: + secretKeyRef: + name: unpoller-secret + key: unifi-pass + - name: UP_UNIFI_DEFAULT_VERIFY_SSL + value: "false" + - name: UP_UNIFI_DEFAULT_SAVE_SITES + value: "true" + - name: UP_UNIFI_DEFAULT_SAVE_DPI + value: "true" + - name: UP_PROMETHEUS_HTTP_LISTEN + value: "0.0.0.0:9130" + - name: UP_PROMETHEUS_NAMESPACE + value: "unpoller" + - name: UP_INFLUXDB_DISABLE + value: "true" + resources: + requests: + cpu: 10m + memory: 128Mi + limits: + cpu: 100m + memory: 256Mi + livenessProbe: + httpGet: + path: /metrics + port: http + initialDelaySeconds: 30 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /metrics + port: http + initialDelaySeconds: 10 + periodSeconds: 10 +--- +# Unpoller Service +apiVersion: v1 +kind: Service +metadata: + name: unpoller + namespace: unpoller + labels: + app: unpoller + app.kubernetes.io/name: unpoller +spec: + type: ClusterIP + ports: + - port: 9130 + targetPort: http + protocol: TCP + name: http + selector: + app: unpoller + app.kubernetes.io/name: unpoller +--- +# ServiceMonitor for Prometheus auto-discovery +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: unpoller + namespace: unpoller + labels: + app: unpoller + app.kubernetes.io/name: unpoller + release: kube-prometheus-stack +spec: + selector: + matchLabels: + app: unpoller + endpoints: + - port: http + interval: 30s + path: /metrics diff --git a/infrastructure/unpoller/manifests/unpoller-secret.yaml b/infrastructure/unpoller/manifests/unpoller-secret.yaml new file mode 100644 index 0000000..06da1f9 --- /dev/null +++ b/infrastructure/unpoller/manifests/unpoller-secret.yaml @@ -0,0 +1,45 @@ +--- +# ExternalSecret for Unpoller Unifi credentials +# Syncs credentials from Vault to Kubernetes Secret +# +# BOOTSTRAP SETUP: +# ================ +# After first deployment, store your Unifi credentials in Vault: +# +# kubectl exec -n vault vault-0 -- vault kv put secret/unpoller \ +# unifi-user="your-unifi-username" \ +# unifi-pass="your-unifi-password" +# +# Or use the API key approach (recommended): +# kubectl exec -n vault vault-0 -- vault kv put secret/unpoller \ +# unifi-user="your-api-key" \ +# unifi-pass="your-api-key" +# +# The ExternalSecret will automatically sync these to a Kubernetes secret +# and Reloader will restart Unpoller when they change. +# +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: unpoller-secret + namespace: unpoller + annotations: + meta.helm.sh/release-name: unpoller + meta.helm.sh/release-namespace: unpoller +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: unpoller-secret + creationPolicy: Owner + data: + - secretKey: unifi-user + remoteRef: + key: secret/unpoller + property: unifi-user + - secretKey: unifi-pass + remoteRef: + key: secret/unpoller + property: unifi-pass diff --git a/infrastructure/unpoller/values.yaml b/infrastructure/unpoller/values.yaml new file mode 100644 index 0000000..24d885c --- /dev/null +++ b/infrastructure/unpoller/values.yaml @@ -0,0 +1,72 @@ +# values/unpoller.values.yaml +# Unpoller configuration for Unifi metrics collection + +# --- Service configuration --- +service: + type: ClusterIP + port: 9130 + +# --- Metrics configuration --- +metrics: + enabled: true + serviceMonitor: + enabled: true + interval: 30s + additionalLabels: + release: kube-prometheus-stack + +# --- Unifi Controller configuration --- +# Credentials loaded from ExternalSecret (synced from Vault) +unpoller: + # Prometheus exporter configuration + prometheus: + # HTTP server configuration + http_listen: 0.0.0.0:9130 + # Report total error counts + report_errors: true + + # Influx DB disabled (using Prometheus only) + influxdb: + disable: true + + # Polling intervals + interval: 30s + +# --- Environment variables from secrets --- +# Credentials are stored in Vault and synced via ExternalSecret +env: + - name: UP_UNIFI_DEFAULT_URL + value: "https://172.28.110.1:8443" + - name: UP_UNIFI_DEFAULT_USER + valueFrom: + secretKeyRef: + name: unpoller-secret + key: unifi-user + - name: UP_UNIFI_DEFAULT_PASS + valueFrom: + secretKeyRef: + name: unpoller-secret + key: unifi-pass + - name: UP_UNIFI_DEFAULT_VERIFY_SSL + value: "false" + - name: UP_UNIFI_DEFAULT_SAVE_SITES + value: "true" + - name: UP_UNIFI_DEFAULT_SAVE_DPI + value: "true" + - name: UP_PROMETHEUS_HTTP_LISTEN + value: "0.0.0.0:9130" + - name: UP_PROMETHEUS_NAMESPACE + value: "unpoller" + +# --- Resources --- +resources: + requests: + cpu: 10m + memory: 128Mi + limits: + memory: 256Mi + +# --- Pod annotations for Reloader --- +# Automatically restart when secrets change +podAnnotations: + secret.reloader.stakater.com/reload: "unpoller-secret" diff --git a/infrastructure/vault/manifests/clustersecretstore.yaml b/infrastructure/vault/manifests/clustersecretstore.yaml new file mode 100644 index 0000000..4f93b20 --- /dev/null +++ b/infrastructure/vault/manifests/clustersecretstore.yaml @@ -0,0 +1,19 @@ +# manifests/clustersecretstore-vault.yaml +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: + name: vault-backend +spec: + provider: + vault: + server: http://vault.vault.svc:8200 + path: secret # KV v2 mount + version: v2 + auth: + kubernetes: + # MUST be just the mount name; Vault shows this as "kubernetes/" in `vault auth list` + mountPath: kubernetes + role: eso-writer # role we create in Vault + serviceAccountRef: + name: external-secrets # SA created by the ESO chart + namespace: external-secrets diff --git a/infrastructure/vault/manifests/envoy-route.yaml b/infrastructure/vault/manifests/envoy-route.yaml new file mode 100644 index 0000000..53b3587 --- /dev/null +++ b/infrastructure/vault/manifests/envoy-route.yaml @@ -0,0 +1,20 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: vault + namespace: vault +spec: + parentRefs: + - name: edge + namespace: gateway + sectionName: https + hostnames: + - vault.kube.huskypup.net + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: vault + port: 8200 diff --git a/infrastructure/vault/manifests/eso-bootstrap-job.yaml b/infrastructure/vault/manifests/eso-bootstrap-job.yaml new file mode 100644 index 0000000..d2a7ad2 --- /dev/null +++ b/infrastructure/vault/manifests/eso-bootstrap-job.yaml @@ -0,0 +1,48 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: vault-eso-bootstrap + namespace: vault + annotations: + argocd.argoproj.io/hook: PostSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "2" +spec: + backoffLimit: 5 + template: + spec: + serviceAccountName: argocd-hook-sa + containers: + - name: eso-bootstrap + image: bitnami/kubectl:1.31 + command: + - /bin/bash + - -c + - | + set -euo pipefail + echo "=== Vault ESO Bootstrap ===" + + # Wait for ESO to be ready + echo "Waiting for External Secrets Operator..." + for i in $(seq 1 60); do + if kubectl -n external-secrets get deploy/external-secrets >/dev/null 2>&1; then + kubectl -n external-secrets rollout status deploy/external-secrets --timeout=180s && break + fi + echo " waiting for ESO deployment... (attempt $i/60)" + sleep 5 + done + + # Wait for ClusterSecretStore to be ready + echo "Checking ClusterSecretStore vault-backend..." + for i in $(seq 1 30); do + STATUS=$(kubectl get clustersecretstore vault-backend -o jsonpath='{.status.conditions[0].status}' 2>/dev/null || echo "") + if [ "$STATUS" = "True" ]; then + echo " ClusterSecretStore vault-backend is ready" + break + fi + echo " waiting for ClusterSecretStore... (attempt $i/30)" + sleep 5 + done + + echo "=== Vault ESO Bootstrap Complete ===" + restartPolicy: Never diff --git a/infrastructure/vault/manifests/eso-policy.yaml b/infrastructure/vault/manifests/eso-policy.yaml new file mode 100644 index 0000000..7f96480 --- /dev/null +++ b/infrastructure/vault/manifests/eso-policy.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: vault-policy-external-secrets + namespace: vault +data: + external-secrets.hcl: | + path "secret/data/*" { + capabilities = ["create", "update", "read", "list"] + } + path "secret/metadata/*" { + capabilities = ["create", "update", "read", "list"] + } diff --git a/infrastructure/vault/manifests/istio-virtualservice.yaml b/infrastructure/vault/manifests/istio-virtualservice.yaml new file mode 100644 index 0000000..6be27c2 --- /dev/null +++ b/infrastructure/vault/manifests/istio-virtualservice.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: vault + namespace: vault +spec: + hosts: + - vault.kube.huskypup.net + gateways: + - istio-system/edge + http: + - route: + - destination: + host: vault.vault.svc.cluster.local + port: + number: 8200 diff --git a/infrastructure/vault/manifests/vault-init-configmap.yaml b/infrastructure/vault/manifests/vault-init-configmap.yaml new file mode 100644 index 0000000..6c8b83b --- /dev/null +++ b/infrastructure/vault/manifests/vault-init-configmap.yaml @@ -0,0 +1,140 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: vault-init-script + namespace: vault +data: + vault-init-and-store.sh: | + #!/bin/bash + set -euo pipefail + + # === Vault Auto-Init and Store Script (ArgoCD Job Version) === + # Adapted from Homelabv5/scripts/vault-init-and-store.sh + # Runs inside a Job pod with kubectl access via ServiceAccount. + # Uses kubectl exec to interact with the vault-0 pod directly + # instead of port-forwarding + local vault CLI. + + VAULT_NS="${VAULT_NS:-vault}" + SECRET_NAME="${SECRET_NAME:-vault-init-keys}" + KV_MOUNT="${KV_MOUNT:-secret}" + JOB_NS_FOR_ROLE="${JOB_NS_FOR_ROLE:-auth-proxy}" + JOB_SA_FOR_ROLE="${JOB_SA_FOR_ROLE:-oauth2-bootstrap}" + POLICY_NAME="${POLICY_NAME:-oauth2-writer}" + ROLE_NAME="${ROLE_NAME:-eso-writer}" + + VAULT_POD="vault-0" + + # Helper: run vault CLI inside the vault pod + vault_exec() { + kubectl -n "$VAULT_NS" exec "$VAULT_POD" -- env VAULT_ADDR=http://127.0.0.1:8200 vault "$@" + } + + # --- Wait for vault-0 pod to be ready --- + echo "==> Waiting for vault-0 pod to be running..." + for i in $(seq 1 60); do + PHASE=$(kubectl -n "$VAULT_NS" get pod "$VAULT_POD" -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + if [ "$PHASE" = "Running" ]; then + echo " vault-0 is running" + break + fi + echo " waiting for vault-0... (attempt $i/60, phase=$PHASE)" + sleep 5 + done + + # Give Vault a moment to start its listener + sleep 5 + + # --- Check init/seal status --- + echo "==> Checking Vault status..." + status_json="$(vault_exec status -format=json 2>/dev/null || echo '{}')" + initialized="$(echo "$status_json" | grep -o '"initialized":[a-z]*' | cut -d: -f2 || echo "")" + sealed="$(echo "$status_json" | grep -o '"sealed":[a-z]*' | cut -d: -f2 || echo "")" + + root_token="" + unseal_key="" + + # Pull existing secret if present + if kubectl -n "$VAULT_NS" get secret "$SECRET_NAME" >/dev/null 2>&1; then + root_token="$(kubectl -n "$VAULT_NS" get secret "$SECRET_NAME" -o jsonpath='{.data.VAULT_ROOT_TOKEN}' | base64 -d || true)" + unseal_key="$(kubectl -n "$VAULT_NS" get secret "$SECRET_NAME" -o jsonpath='{.data.VAULT_UNSEAL_KEY}' | base64 -d || true)" + fi + + # Initialize if needed + if [ "$initialized" != "true" ]; then + echo "==> Vault not initialized; initializing..." + init_json="$(vault_exec operator init -key-shares=1 -key-threshold=1 -format=json)" + root_token="$(echo "$init_json" | grep -o '"root_token":"[^"]*"' | cut -d'"' -f4)" + unseal_key="$(echo "$init_json" | grep -o '"unseal_keys_b64":\["[^"]*"\]' | grep -o '\["[^"]*"\]' | tr -d '[]"')" + sealed="true" + + echo "==> Storing root token & unseal key in Secret ${VAULT_NS}/${SECRET_NAME}" + kubectl -n "$VAULT_NS" create secret generic "$SECRET_NAME" \ + --from-literal=VAULT_ROOT_TOKEN="$root_token" \ + --from-literal=VAULT_UNSEAL_KEY="$unseal_key" \ + --dry-run=client -o yaml | kubectl apply -f - + else + echo "==> Vault already initialized." + fi + + # Unseal if needed + if [ "$sealed" = "true" ]; then + if [ -z "$unseal_key" ]; then + echo "ERROR: Vault is sealed and no unseal key available" + exit 1 + fi + echo "==> Unsealing..." + vault_exec operator unseal "$unseal_key" >/dev/null + fi + + # Login + if [ -z "$root_token" ]; then + echo "==> Reading root token from Secret..." + root_token="$(kubectl -n "$VAULT_NS" get secret "$SECRET_NAME" -o jsonpath='{.data.VAULT_ROOT_TOKEN}' | base64 -d)" + fi + vault_exec login "$root_token" >/dev/null + + # Ensure KV v2 enabled + if ! vault_exec secrets list -format=json 2>/dev/null | grep -q "\"${KV_MOUNT}/\""; then + echo "==> Enabling KV v2 at ${KV_MOUNT}/" + vault_exec secrets enable -path="$KV_MOUNT" -version=2 kv >/dev/null + fi + + # Configure Kubernetes auth with a reviewer token from a local SA + echo "==> Ensuring reviewer SA + binding" + kubectl -n "$VAULT_NS" get sa vault-auth >/dev/null 2>&1 || kubectl -n "$VAULT_NS" create sa vault-auth + kubectl get clusterrolebinding vault-auth-delegator >/dev/null 2>&1 || \ + kubectl create clusterrolebinding vault-auth-delegator \ + --clusterrole=system:auth-delegator \ + --serviceaccount="${VAULT_NS}:vault-auth" + + reviewer_jwt="$(kubectl -n "$VAULT_NS" create token vault-auth)" + kube_ca="$(kubectl -n kube-system get configmap kube-root-ca.crt -o jsonpath='{.data.ca\.crt}')" + kube_host="https://kubernetes.default.svc:443" + + vault_exec auth enable kubernetes >/dev/null 2>&1 || true + vault_exec write auth/kubernetes/config \ + token_reviewer_jwt="$reviewer_jwt" \ + kubernetes_host="$kube_host" \ + kubernetes_ca_cert="$kube_ca" >/dev/null + + # Policy + role for ESO/oauth2 job + vault_exec policy write "$POLICY_NAME" - >/dev/null <<'HCL' + path "secret/data/*" { + capabilities = ["create", "update", "read", "list"] + } + + path "secret/metadata/*" { + capabilities = ["create", "update", "read", "list"] + } + HCL + + vault_exec write "auth/kubernetes/role/${ROLE_NAME}" \ + bound_service_account_names="${JOB_SA_FOR_ROLE},external-secrets" \ + bound_service_account_namespaces="${JOB_NS_FOR_ROLE},external-secrets" \ + policies="${POLICY_NAME}" \ + ttl="24h" >/dev/null + + echo "==> Done." + echo "K8s Secret with init creds: ${VAULT_NS}/${SECRET_NAME}" + echo "IMPORTANT: back these up securely and delete the Secret when you're comfortable:" + echo " kubectl -n ${VAULT_NS} delete secret ${SECRET_NAME}" diff --git a/infrastructure/vault/manifests/vault-init-job.yaml b/infrastructure/vault/manifests/vault-init-job.yaml new file mode 100644 index 0000000..66826f9 --- /dev/null +++ b/infrastructure/vault/manifests/vault-init-job.yaml @@ -0,0 +1,27 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: vault-init + namespace: vault + annotations: + argocd.argoproj.io/hook: PostSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "1" +spec: + backoffLimit: 3 + template: + spec: + serviceAccountName: argocd-hook-sa + containers: + - name: vault-init + image: bitnami/kubectl:1.31 + command: ["/bin/bash", "/scripts/vault-init-and-store.sh"] + volumeMounts: + - name: scripts + mountPath: /scripts + volumes: + - name: scripts + configMap: + name: vault-init-script + defaultMode: 0755 + restartPolicy: Never diff --git a/infrastructure/vault/values.yaml b/infrastructure/vault/values.yaml new file mode 100644 index 0000000..3a5e076 --- /dev/null +++ b/infrastructure/vault/values.yaml @@ -0,0 +1,45 @@ +server: + # HA mode with Raft integrated storage + ha: + enabled: true + replicas: 3 + raft: + enabled: true + config: | + ui = true + listener "tcp" { + address = "0.0.0.0:8200" + tls_disable = 1 + } + storage "raft" { + path = "/vault/data" + } + disable_mlock = true + service_registration "kubernetes" {} + telemetry { + prometheus_retention_time = "30s" + disable_hostname = true + } + + standalone: + enabled: false + + dataStorage: + enabled: true + size: 10Gi + + extraEnvironmentVars: {} + + # Tolerate control-plane taint + tolerations: + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule + +# Enable ServiceMonitor for Prometheus +serverTelemetry: + serviceMonitor: + enabled: true + selectors: + release: prometheus + interval: 30s \ No newline at end of file diff --git a/scripts/bootstrap-crds.sh b/scripts/bootstrap-crds.sh new file mode 100755 index 0000000..b3626d3 --- /dev/null +++ b/scripts/bootstrap-crds.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -euo pipefail + +# CRD versions (override via env vars for upgrades) +PROMETHEUS_OPERATOR_VERSION="${PROMETHEUS_OPERATOR_VERSION:-v0.76.0}" +GATEWAY_API_VERSION="${GATEWAY_API_VERSION:-v1.2.0}" + +echo "=== Bootstrap: Installing required CRDs ===" + +# Install Prometheus Operator CRDs (required before helmfile diff can run) +echo "Installing Prometheus Operator CRDs (${PROMETHEUS_OPERATOR_VERSION})..." +kubectl apply --server-side -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/${PROMETHEUS_OPERATOR_VERSION}/example/prometheus-operator-crd/monitoring.coreos.com_servicemonitors.yaml +kubectl apply --server-side -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/${PROMETHEUS_OPERATOR_VERSION}/example/prometheus-operator-crd/monitoring.coreos.com_podmonitors.yaml +kubectl apply --server-side -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/${PROMETHEUS_OPERATOR_VERSION}/example/prometheus-operator-crd/monitoring.coreos.com_prometheuses.yaml +kubectl apply --server-side -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/${PROMETHEUS_OPERATOR_VERSION}/example/prometheus-operator-crd/monitoring.coreos.com_prometheusrules.yaml +kubectl apply --server-side -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/${PROMETHEUS_OPERATOR_VERSION}/example/prometheus-operator-crd/monitoring.coreos.com_alertmanagers.yaml +kubectl apply --server-side -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/${PROMETHEUS_OPERATOR_VERSION}/example/prometheus-operator-crd/monitoring.coreos.com_alertmanagerconfigs.yaml + +echo "✅ Prometheus Operator CRDs installed successfully" + +# Install Kyverno CRDs (required for PolicyReport resources) +echo "" +echo "Installing Kyverno PolicyReport CRDs..." +kubectl apply --server-side -f https://raw.githubusercontent.com/kubernetes-sigs/wg-policy-prototypes/master/policy-report/crd/v1beta1/wgpolicyk8s.io_clusterpolicyreports.yaml 2>/dev/null || echo "PolicyReport CRDs may already exist or URL changed - Kyverno will install them" +kubectl apply --server-side -f https://raw.githubusercontent.com/kubernetes-sigs/wg-policy-prototypes/master/policy-report/crd/v1beta1/wgpolicyk8s.io_policyreports.yaml 2>/dev/null || echo "PolicyReport CRDs may already exist or URL changed - Kyverno will install them" + +echo "✅ Kyverno PolicyReport CRDs installed (or will be installed by Kyverno chart)" + +# Install Gateway API CRDs (required for Istio ambient waypoint proxies) +echo "" +echo "Installing Gateway API CRDs (required for Istio ambient waypoint proxies)..." +kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/${GATEWAY_API_VERSION}/standard-install.yaml +echo "✅ Gateway API CRDs installed successfully" diff --git a/scripts/cilium-bootstrap.sh b/scripts/cilium-bootstrap.sh new file mode 100755 index 0000000..7178923 --- /dev/null +++ b/scripts/cilium-bootstrap.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# Cilium Bootstrap - Install Cilium CNI before helmfile +# Nodes require a CNI to become Ready. This script installs Cilium via Helm +# and applies L2 announcement policies (replacing MetalLB). +# +# Usage: ./scripts/cilium-bootstrap.sh + +set -euo pipefail + +echo "=================================================" +echo "Cilium CNI Bootstrap" +echo "=================================================" +echo "" + +# Add Cilium Helm repo +echo "Adding Cilium Helm repository..." +helm repo add cilium https://helm.cilium.io 2>/dev/null || true +helm repo update cilium + +# Check if Cilium is already installed +if helm -n kube-system status cilium >/dev/null 2>&1; then + echo "✅ Cilium is already installed" + echo "" + echo "Upgrading Cilium to match helmfile values..." + helm upgrade cilium cilium/cilium \ + --namespace kube-system \ + --values infrastructure/cilium/values.yaml \ + --wait --timeout 300s +else + echo "Installing Cilium CNI..." + helm install cilium cilium/cilium \ + --namespace kube-system \ + --values infrastructure/cilium/values.yaml \ + --wait --timeout 300s +fi + +echo "" +echo "Waiting for Cilium to be ready..." +kubectl -n kube-system rollout status daemonset/cilium --timeout=300s + +echo "" +echo "Applying L2 announcement policy and IP pool..." +kubectl apply -f infrastructure/cilium/l2-announcement-policy.yaml + +echo "" +echo "Applying baseline network policies..." +kubectl apply -f infrastructure/cilium/network-policies/dns.yaml + +# Apply per-namespace policies only if namespaces exist +for ns in vault authentik rook-ceph; do + if kubectl get ns "$ns" >/dev/null 2>&1; then + kubectl apply -f infrastructure/cilium/network-policies/baseline.yaml 2>/dev/null || true + break + fi +done + +echo "" +echo "Verifying Cilium status..." +kubectl -n kube-system exec ds/cilium -- cilium status --brief 2>/dev/null || echo " (cilium CLI check will be available after pods stabilize)" + +echo "" +echo "Checking node readiness..." +kubectl get nodes -o wide + +echo "" +echo "Verifying L2 announcements..." +kubectl get ciliuml2announcementpolicy,ciliumloadbalancerippool 2>/dev/null || echo " (L2 CRDs being registered...)" + +echo "" +echo "=================================================" +echo "✅ Cilium CNI bootstrap complete" +echo "=================================================" diff --git a/scripts/fix-gitlab-auth.sh b/scripts/fix-gitlab-auth.sh new file mode 100755 index 0000000..7b7f08a --- /dev/null +++ b/scripts/fix-gitlab-auth.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# scripts/fix-gitlab-auth.sh +# Fix GitLab authentication issues by refreshing passwords + +set -euo pipefail + +echo "=== GitLab Authentication Fix ===" +echo "This script fixes the 500 errors caused by password synchronization issues" + +# Function to restart deployment and wait +restart_deployment() { + local deployment=$1 + local namespace=$2 + + echo "Restarting $deployment in $namespace..." + kubectl rollout restart deployment/$deployment -n $namespace + kubectl rollout status deployment/$deployment -n $namespace --timeout=120s +} + +# 1. Refresh database secrets +echo "Step 1: Refreshing database secrets..." +kubectl delete secret pg-gitlab-app -n gitlab --ignore-not-found=true +echo "Waiting for database secret to be recreated..." +for i in {1..30}; do + if kubectl -n gitlab get secret pg-gitlab-app >/dev/null 2>&1; then + echo "✓ Database secret recreated" + break + fi + echo " waiting... (attempt $i/30)" + sleep 2 +done + +# 2. Refresh Redis secrets +echo "Step 2: Refreshing Redis secrets..." +kubectl delete secret redis-gitlab-secret -n gitlab --ignore-not-found=true +echo "Waiting for Redis secret to be recreated..." +for i in {1..30}; do + if kubectl -n gitlab get secret redis-gitlab-secret >/dev/null 2>&1; then + echo "✓ Redis secret recreated" + break + fi + echo " waiting... (attempt $i/30)" + sleep 2 +done + +# 3. Trigger PostgreSQL reload +echo "Step 3: Reloading PostgreSQL clusters..." +kubectl annotate cluster pg-gitlab -n gitlab cnpg.io/reload=$(date +%s) --overwrite +kubectl annotate cluster pg-praefect -n gitlab cnpg.io/reload=$(date +%s) --overwrite + +# 4. Restart Redis +echo "Step 4: Restarting Redis..." +kubectl rollout restart statefulset/redis-gitlab -n gitlab +kubectl rollout status statefulset/redis-gitlab -n gitlab --timeout=120s + +# 5. Restart GitLab services +echo "Step 5: Restarting GitLab services..." +restart_deployment "gitlab-webservice-default" "gitlab" +restart_deployment "gitlab-sidekiq-all-in-1-v2" "gitlab" +restart_deployment "gitlab-toolbox" "gitlab" + +# 6. Verify GitLab is working +echo "Step 6: Verifying GitLab functionality..." +sleep 30 + +# Test GitLab health endpoint +for i in {1..10}; do + HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" https://gitlab.kube.huskypup.net/-/readiness) + if [ "$HTTP_CODE" = "200" ]; then + echo "✓ GitLab is responding correctly" + break + fi + echo " waiting for GitLab to be ready... (attempt $i/10) - HTTP $HTTP_CODE" + sleep 10 +done + +# Test main page +HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" https://gitlab.kube.huskypup.net) +if [ "$HTTP_CODE" = "302" ]; then + echo "✓ GitLab main page is working (redirecting to login as expected)" +elif [ "$HTTP_CODE" = "200" ]; then + echo "✓ GitLab main page is working" +else + echo "⚠ GitLab main page returned HTTP $HTTP_CODE (may still be starting)" +fi + +echo "" +echo "====================================================================" +echo "GitLab Authentication Fix Complete" +echo "====================================================================" +echo "✓ Database secrets refreshed" +echo "✓ Redis secrets refreshed" +echo "✓ PostgreSQL clusters reloaded" +echo "✓ Redis restarted" +echo "✓ GitLab services restarted" +echo "" +echo "GitLab should now be accessible at: https://gitlab.kube.huskypup.net" +echo "If you still see 500 errors, check the logs with:" +echo " kubectl logs -n gitlab -l app=webservice --tail=20" +echo "====================================================================" \ No newline at end of file diff --git a/scripts/gitlab-add-hostaliases.sh b/scripts/gitlab-add-hostaliases.sh new file mode 100755 index 0000000..c0bd5ba --- /dev/null +++ b/scripts/gitlab-add-hostaliases.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# gitlab-add-hostaliases.sh +# Add hostAliases to GitLab deployments for Authentik OIDC + +set -euo pipefail + +echo "=== Adding hostAliases to GitLab deployments ===" + +NS=gitlab +# Route via Istio ingressgateway LoadBalancer (ingress-nginx removed) +EDGE_IP=$(kubectl get svc -n istio-system istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + +echo "Istio edge gateway IP: $EDGE_IP" + +# Check if webservice deployment already has hostAliases +if kubectl get deployment gitlab-webservice-default -n "${NS}" -o jsonpath='{.spec.template.spec.hostAliases}' | grep -q "${EDGE_IP}"; then + echo "✅ webservice already has hostAliases configured" +else + echo "Adding hostAliases to webservice..." + kubectl patch deployment gitlab-webservice-default -n "${NS}" --type='json' -p="[{\"op\": \"add\", \"path\": \"/spec/template/spec/hostAliases\", \"value\": [{\"ip\": \"${EDGE_IP}\", \"hostnames\": [\"auth.kube.huskypup.net\"]}]}]" + echo "✅ hostAliases added to webservice" +fi + +# Check if sidekiq deployment already has hostAliases +if kubectl get deployment gitlab-sidekiq-all-in-1-v2 -n "${NS}" -o jsonpath='{.spec.template.spec.hostAliases}' | grep -q "${EDGE_IP}"; then + echo "✅ sidekiq already has hostAliases configured" +else + echo "Adding hostAliases to sidekiq..." + kubectl patch deployment gitlab-sidekiq-all-in-1-v2 -n "${NS}" --type='json' -p="[{\"op\": \"add\", \"path\": \"/spec/template/spec/hostAliases\", \"value\": [{\"ip\": \"${EDGE_IP}\", \"hostnames\": [\"auth.kube.huskypup.net\"]}]}]" + echo "✅ hostAliases added to sidekiq" +fi + +echo "" +echo "=== hostAliases configuration complete ===" +echo "GitLab will now route auth.kube.huskypup.net through Istio edge gateway (${EDGE_IP})" +echo "This ensures proper SSL certificate validation for Authentik OIDC" diff --git a/scripts/gitlab-bootstrap.sh b/scripts/gitlab-bootstrap.sh new file mode 100755 index 0000000..8924b49 --- /dev/null +++ b/scripts/gitlab-bootstrap.sh @@ -0,0 +1,354 @@ +#!/bin/bash +# scripts/gitlab-bootstrap.sh +# GitLab presync bootstrap script - fully automated, no manual interaction required +# Auto-creates OAuth provider in Authentik and syncs credentials to GitLab + +set -euo pipefail + +echo "=== GitLab Bootstrap - Fully Automated with Authentik Integration ===" +echo "Setting up GitLab infrastructure with auto-generated secrets..." + +# Change to the apps directory for relative paths +cd "$(dirname "$0")/../apps" || exit 1 + +# Ensure namespace exists +kubectl get ns gitlab >/dev/null 2>&1 || kubectl create ns gitlab + +# ============================================================================ +# Step 1: Ensure Authentik has GitLab OAuth provider +# ============================================================================ +echo "" +echo "Step 1: Configuring Authentik OAuth provider for GitLab..." + +# Apply the Authentik blueprint (if not already applied) +kubectl apply -f ../infrastructure/authentik/gitlab-blueprint.yaml 2>/dev/null || true + +# Give Authentik time to process the blueprint (it auto-discovers ConfigMaps with the label) +echo "Waiting for Authentik to process GitLab blueprint..." +sleep 10 + +# ============================================================================ +# Step 2: Deploy PostgreSQL clusters +# ============================================================================ +echo "" +echo "Step 2: Deploying PostgreSQL clusters..." + +# Apply CloudNativePG PostgreSQL clusters +# NOTE: CNPG auto-generates database passwords in secrets like pg-gitlab-app +echo "Applying GitLab PostgreSQL CNPG cluster..." +kubectl apply -f gitlab/cnpg-cluster.yaml + +echo "Applying Praefect PostgreSQL CNPG cluster..." +kubectl apply -f gitlab/praefect-cnpg-cluster.yaml + +# Apply PgBouncer poolers +echo "Applying PgBouncer connection poolers..." +kubectl apply -f gitlab/pgbouncer-pooler.yaml + +# Wait for PostgreSQL clusters to be ready +echo "Waiting for PostgreSQL clusters to be ready..." +for i in {1..60}; do + READY=$(kubectl -n gitlab get cluster pg-gitlab -o jsonpath='{.status.instances}' 2>/dev/null || echo "0") + if [ "$READY" -ge "1" ]; then + echo "GitLab PostgreSQL cluster has $READY instance(s) ready!" + break + fi + sleep 5 +done + +for i in {1..60}; do + READY=$(kubectl -n gitlab get cluster pg-praefect -o jsonpath='{.status.instances}' 2>/dev/null || echo "0") + if [ "$READY" -ge "1" ]; then + echo "Praefect PostgreSQL cluster has $READY instance(s) ready!" + break + fi + sleep 5 +done + +# ============================================================================ +# Step 3: Deploy Redis +# ============================================================================ +echo "" +echo "Step 3: Deploying Redis..." + +# Apply Redis standalone instance and auto-restart automation +echo "Applying GitLab Redis standalone instance..." +kubectl apply -f gitlab/redis-cluster.yaml + +echo "Applying GitLab Redis auto-restart automation..." +kubectl apply -f gitlab/redis-auto-restart.yaml + +echo "Applying GitLab PostgreSQL auto-restart automation..." +kubectl apply -f gitlab/pg-auto-restart.yaml + +# Verify auto-restart CronJobs were created +echo "Verifying auto-restart CronJobs..." +for i in {1..10}; do + REDIS_OK=false + PG_GITLAB_OK=false + PG_PRAEFECT_OK=false + + if kubectl get cronjob -n gitlab redis-secret-monitor >/dev/null 2>&1; then + REDIS_OK=true + fi + if kubectl get cronjob -n gitlab pg-gitlab-secret-monitor >/dev/null 2>&1; then + PG_GITLAB_OK=true + fi + if kubectl get cronjob -n gitlab pg-praefect-secret-monitor >/dev/null 2>&1; then + PG_PRAEFECT_OK=true + fi + + if $REDIS_OK && $PG_GITLAB_OK && $PG_PRAEFECT_OK; then + echo "✓ All auto-restart CronJobs are deployed!" + break + fi + echo " waiting for CronJobs to be created... (attempt $i/10)" + sleep 1 +done + +# Apply Redis Sentinel HA cluster +echo "Applying GitLab Redis Sentinel HA..." +kubectl apply -f gitlab/redis-sentinel-ha.yaml + +# Wait for Redis to be ready +echo "Waiting for Redis Sentinel cluster to be ready..." +kubectl wait --for=condition=ready pod -n gitlab -l app=redis-gitlab-ha --timeout=120s 2>/dev/null || echo "Redis may still be starting..." + +# ============================================================================ +# Step 4: Set up PgBouncer authentication (SCRAM-SHA-256) +# ============================================================================ +echo "" +echo "Step 4: Setting up PgBouncer authentication with SCRAM-SHA-256..." + +# Get the primary PostgreSQL pod (read-write) +PRIMARY_POD=$(kubectl get pod -n gitlab -l cnpg.io/cluster=pg-gitlab,role=primary -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$PRIMARY_POD" ]; then + echo "❌ ERROR: Could not find primary PostgreSQL pod" + exit 1 +fi + +echo "Using primary pod: $PRIMARY_POD" + +# Create table for password hashes (needed for PgBouncer SCRAM-SHA-256 auth) +echo "Creating user passwords table for PgBouncer..." +kubectl exec -n gitlab "$PRIMARY_POD" -- psql -U postgres -c "CREATE TABLE IF NOT EXISTS public.user_passwords (usename text primary key, passwd text);" + +# Create user_search function to return password hashes from the table +echo "Creating user_search function for SCRAM-SHA-256 authentication..." +kubectl exec -n gitlab "$PRIMARY_POD" -- psql -U postgres -c "DROP FUNCTION IF EXISTS public.user_search(text);" +kubectl exec -n gitlab "$PRIMARY_POD" -- psql -U postgres -c "CREATE FUNCTION public.user_search(uname text) RETURNS TABLE(usename text, passwd text) AS \$\$ SELECT usename, passwd FROM public.user_passwords WHERE usename = \$1; \$\$ LANGUAGE sql SECURITY DEFINER;" + +# Wait for PgBouncer pooler to create its role +echo "Waiting for PgBouncer pooler pods to be ready..." +for i in {1..30}; do + POOLER_READY=$(kubectl get pods -n gitlab -l cnpg.io/poolerName=pgbouncer-gitlab --no-headers 2>/dev/null | grep -c Running || echo "0") + if [ "$POOLER_READY" -ge "1" ]; then + echo "✅ PgBouncer pooler pods are running" + break + fi + echo " waiting for pooler pods... (attempt $i/30)" + sleep 2 +done + +# Grant permissions to PgBouncer auth user (create role if it doesn't exist) +echo "Granting permissions to PgBouncer auth user..." +kubectl exec -n gitlab "$PRIMARY_POD" -- psql -U postgres -c "DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'cnpg_pooler_pgbouncer') THEN CREATE ROLE cnpg_pooler_pgbouncer LOGIN; END IF; END \$\$;" 2>/dev/null || echo " ↳ Role may already exist" +kubectl exec -n gitlab "$PRIMARY_POD" -- psql -U postgres -c "GRANT EXECUTE ON FUNCTION public.user_search(text) TO cnpg_pooler_pgbouncer;" 2>/dev/null || echo " ↳ Permission may already be granted" + +# Sync password hash from pg_authid to user_passwords table +# This ensures PgBouncer can authenticate using SCRAM-SHA-256 +echo "Syncing password hash from pg_authid to user_passwords table..." +kubectl exec -n gitlab "$PRIMARY_POD" -- psql -U postgres -c "INSERT INTO public.user_passwords (usename, passwd) SELECT rolname, rolpassword FROM pg_authid WHERE rolname = 'app' ON CONFLICT (usename) DO UPDATE SET passwd = EXCLUDED.passwd;" + +# Verify the password hash was stored correctly +echo "Verifying password hash sync..." +HASH_COUNT=$(kubectl exec -n gitlab "$PRIMARY_POD" -- psql -U postgres -t -c "SELECT COUNT(*) FROM public.user_passwords WHERE usename = 'app' AND passwd LIKE 'SCRAM-SHA-256%';" | tr -d ' ') + +if [ "$HASH_COUNT" = "1" ]; then + echo "✅ PgBouncer authentication configured with SCRAM-SHA-256" +else + echo "⚠️ WARNING: Password hash may not be correctly stored" + echo " PgBouncer authentication may fail - check user_passwords table" +fi + +# ============================================================================ +# Step 5: Configure OAuth/SAML credentials +# ============================================================================ +echo "" +echo "Step 5: Configuring OAuth and SAML credentials..." + +# Try to sync OAuth credentials from Authentik to Vault +echo "Attempting to sync OAuth credentials from Authentik..." +if bash ../scripts/sync-gitlab-oauth.sh 2>&1 | grep -q "Successfully stored"; then + echo "✓ Successfully synced OAuth credentials from Authentik to Vault" + OAUTH_SYNCED=true +else + echo "⚠ Could not sync OAuth from Authentik (provider may not be ready yet)" + echo " GitLab will use placeholder credentials - run './scripts/sync-gitlab-oauth.sh' later to enable SSO" + OAUTH_SYNCED=false +fi + +# Apply External Secrets for GitLab OIDC (will sync from Vault if available) +echo "Applying GitLab OIDC External Secrets..." +kubectl apply -f gitlab/external-secret.yaml + +# Apply External Secrets for GitLab SAML (optional) +echo "Applying GitLab SAML External Secrets..." +kubectl apply -f gitlab/external-secret-saml.yaml 2>/dev/null || true + +# Wait for External Secret to sync (with timeout) +echo "Checking if GitLab OIDC External Secret can sync from Vault..." +OIDC_SYNCED=false +for i in {1..10}; do + STATUS=$(kubectl -n gitlab get externalsecret gitlab-oidc -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "False") + if [ "$STATUS" = "True" ]; then + echo "✓ GitLab OIDC External Secret synced from Vault!" + OIDC_SYNCED=true + break + fi + sleep 2 +done + +# Create placeholder OAuth credentials in Vault if they don't exist +echo "Checking if OAuth credentials exist in Vault..." +VAULT_POD=$(kubectl get pods -n vault -l app.kubernetes.io/name=vault -o jsonpath='{.items[0].metadata.name}') +VAULT_TOKEN=$(kubectl get secret -n vault vault-init-keys -o jsonpath='{.data.VAULT_ROOT_TOKEN}' | base64 -d) + +if ! kubectl exec -n vault "$VAULT_POD" -- env VAULT_TOKEN="$VAULT_TOKEN" vault kv get secret/gitlab-oauth >/dev/null 2>&1; then + echo "⚠ Vault doesn't have gitlab-oauth credentials yet. Creating placeholders..." + PLACEHOLDER_SECRET=$(openssl rand -hex 32) + kubectl exec -n vault "$VAULT_POD" -- env VAULT_TOKEN="$VAULT_TOKEN" vault kv put secret/gitlab-oauth \ + client-id="placeholder-gitlab-client-id" \ + client-secret="$PLACEHOLDER_SECRET" + echo "✓ Placeholder OAuth credentials created in Vault" + echo " Update with real credentials: ./scripts/sync-gitlab-oauth.sh" +else + echo "✓ GitLab OAuth credentials exist in Vault" +fi + +# Wait for external secret to sync the placeholder/real credentials +echo "Waiting for GitLab OIDC External Secret to sync..." +for i in {1..30}; do + STATUS=$(kubectl -n gitlab get externalsecret gitlab-oidc -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "False") + if [ "$STATUS" = "True" ]; then + echo "✓ GitLab OIDC External Secret synced successfully" + break + fi + sleep 2 +done + +# Check if SAML secret can sync from Vault +echo "Checking if GitLab SAML External Secret can sync from Vault..." +SAML_SYNCED=false +for i in {1..10}; do + STATUS=$(kubectl -n gitlab get externalsecret gitlab-saml -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "False") + if [ "$STATUS" = "True" ]; then + echo "✓ GitLab SAML External Secret synced from Vault!" + SAML_SYNCED=true + break + fi + sleep 2 +done + +# Create placeholder SAML credentials in Vault if they don't exist +echo "Checking if SAML credentials exist in Vault..." +if ! kubectl exec -n vault "$VAULT_POD" -- env VAULT_TOKEN="$VAULT_TOKEN" vault kv get secret/gitlab/saml >/dev/null 2>&1; then + echo "⚠ Vault doesn't have gitlab/saml credentials yet. Creating placeholders..." + kubectl exec -n vault "$VAULT_POD" -- env VAULT_TOKEN="$VAULT_TOKEN" vault kv put secret/gitlab/saml \ + idp_sso_url="https://auth.kube.huskypup.net/application/saml/gitlab/sso/binding/redirect/" \ + idp_fingerprint="00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" + echo "✓ Placeholder SAML credentials created in Vault" + echo " Update with real credentials: ./scripts/gitlab-saml-bootstrap.sh" +else + echo "✓ GitLab SAML credentials exist in Vault" +fi + +# Wait for external secret to sync +echo "Waiting for GitLab SAML External Secret to sync..." +for i in {1..30}; do + STATUS=$(kubectl -n gitlab get externalsecret gitlab-saml -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "False") + if [ "$STATUS" = "True" ]; then + echo "✓ GitLab SAML External Secret synced successfully" + break + fi + sleep 2 +done + +# Force password synchronization to prevent authentication issues +echo "=== Synchronizing Database and Redis Passwords ===" + +# Delete and recreate database secrets to ensure sync +echo "Refreshing database secrets..." +kubectl delete secret pg-gitlab-app -n gitlab --ignore-not-found=true +kubectl delete secret pg-praefect-app -n gitlab --ignore-not-found=true + +# Wait for ExternalSecrets to recreate secrets +echo "Waiting for database secrets to be recreated..." +for i in {1..30}; do + if kubectl -n gitlab get secret pg-gitlab-app >/dev/null 2>&1 && \ + kubectl -n gitlab get secret pg-praefect-app >/dev/null 2>&1; then + echo "Database secrets recreated successfully!" + break + fi + echo " waiting for database secrets... (attempt $i/30)" + sleep 2 +done + +# Delete and recreate Redis secret to ensure sync +echo "Refreshing Redis secrets..." +kubectl delete secret redis-gitlab-secret -n gitlab --ignore-not-found=true + +# Wait for Redis ExternalSecret to recreate secret +echo "Waiting for Redis secret to be recreated..." +for i in {1..30}; do + if kubectl -n gitlab get secret redis-gitlab-secret >/dev/null 2>&1; then + echo "Redis secret recreated successfully!" + break + fi + echo " waiting for Redis secret... (attempt $i/30)" + sleep 2 +done + +# Trigger PostgreSQL reload to pick up new passwords +echo "Triggering PostgreSQL cluster reload..." +kubectl annotate cluster pg-gitlab -n gitlab cnpg.io/reload=$(date +%s) --overwrite +kubectl annotate cluster pg-praefect -n gitlab cnpg.io/reload=$(date +%s) --overwrite + +echo "" +echo "====================================================================" +echo "GitLab Bootstrap Summary - Fully Automated" +echo "====================================================================" +echo "✓ Authentik GitLab OAuth blueprint applied" +echo "✓ PostgreSQL CNPG clusters (GitLab + Praefect) deployed" +echo "✓ PgBouncer connection poolers configured with authentication" +echo "✓ Redis standalone instance deployed" +echo "✓ Redis auto-restart automation (CronJob) deployed" +echo "✓ Redis Sentinel HA cluster deployed" + +if [ "${OAUTH_SYNCED:-false}" = "true" ]; then + echo "✓ GitLab OIDC credentials synced from Authentik" +else + echo "⚠ GitLab OIDC using placeholder (Authentik provider not ready)" +fi + +echo "✓ GitLab SAML secret configured (placeholder)" +echo "✓ Database and Redis passwords synchronized" +echo "" +echo "Password Rotation: Automated via CronJob (hourly checks)" +echo "Next password rotation: $(kubectl get externalsecret -n gitlab gitlab-redis-password -o jsonpath='{.status.refreshTime}' 2>/dev/null || echo 'Unknown') + 24h" +echo "====================================================================" +echo "" +echo "🎉 GitLab bootstrap completed - NO MANUAL STEPS REQUIRED!" +echo "" +echo "GitLab will be accessible at: https://gitlab.kube.huskypup.net" +echo "" +if [ "${OAUTH_SYNCED:-false}" = "false" ]; then + echo "To enable Authentik SSO (optional):" + echo " 1. Wait for Authentik to process the GitLab blueprint (~5 min)" + echo " 2. Run: ./scripts/sync-gitlab-oauth.sh" + echo " 3. Restart GitLab pods to pick up real credentials" + echo "" +fi +echo "For SAML setup (optional): ./scripts/gitlab-saml-bootstrap.sh" +echo "" \ No newline at end of file diff --git a/scripts/gitlab-grant-admin.sh b/scripts/gitlab-grant-admin.sh new file mode 100755 index 0000000..044a43e --- /dev/null +++ b/scripts/gitlab-grant-admin.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Grant GitLab admin access to users in Authentik "authentik Admins" group +# Run this after users login via Authentik SSO + +set -euo pipefail + +ADMIN_GROUP="authentik Admins" + +echo "🔄 Granting GitLab admin access to Authentik admin group members..." +echo "" + +# Get Authentik database password +PGPASSWORD=$(kubectl get secret -n authentik pg-authentik-app -o jsonpath='{.data.password}' | base64 -d) + +# Query Authentik for users in admin group +echo "📋 Getting users from Authentik '$ADMIN_GROUP' group..." +ADMIN_EMAILS=$(kubectl exec -n authentik pg-authentik-1 -- env PGPASSWORD="$PGPASSWORD" psql -h pg-authentik-rw -U app -d app -t -c " + SELECT DISTINCT u.email + FROM authentik_core_user u + JOIN authentik_core_user_groups ug ON u.id = ug.user_id + JOIN authentik_core_group g ON ug.group_id = g.group_uuid + WHERE g.name = '$ADMIN_GROUP' AND u.is_active = true; +" 2>&1 | grep -v "Defaulted" | grep '@' | xargs) + +if [ -z "$ADMIN_EMAILS" ]; then + echo "⚠️ No users found in Authentik '$ADMIN_GROUP' group" + exit 0 +fi + +echo "✓ Found admin users: $ADMIN_EMAILS" +echo "" + +# Check if gitlab-toolbox pod exists +TOOLBOX_POD=$(kubectl get pods -n gitlab -l app=toolbox,release=gitlab -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") + +if [ -z "$TOOLBOX_POD" ]; then + echo "❌ GitLab toolbox pod not found" + echo " Toolbox is required to run Rails commands" + exit 1 +fi + +echo "🔧 Using toolbox pod: $TOOLBOX_POD" +echo "" + +# For each admin user, grant admin access +for email in $ADMIN_EMAILS; do + echo "🔐 Processing: $email" + + kubectl exec -n gitlab "$TOOLBOX_POD" -- gitlab-rails runner " + user = User.find_by(email: '$email') + if user + if user.admin? + puts ' ✓ Already admin' + else + user.update!(admin: true) + puts ' ✅ Promoted to admin' + end + else + puts ' ⚠️ User not found (needs to login via Authentik SSO first)' + end + " 2>&1 | grep -v "^$" +done + +echo "" +echo "✅ Admin sync complete!" +echo "" +echo "💡 Note: Users must login via Authentik SSO at least once before they can be promoted" diff --git a/scripts/grafana-bootstrap.sh b/scripts/grafana-bootstrap.sh new file mode 100755 index 0000000..3299bb7 --- /dev/null +++ b/scripts/grafana-bootstrap.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# scripts/grafana-bootstrap.sh +# Grafana presync bootstrap script + +set -euo pipefail + +echo "=== Grafana Bootstrap ===" + +# Ensure namespace exists +kubectl get ns grafana >/dev/null 2>&1 || kubectl create ns grafana + +# Apply External Secrets for Grafana OAuth +echo "Applying Grafana OAuth External Secret..." +kubectl apply -f ../base/external-secrets.yaml || echo "Warning: Some external secrets failed to apply (expected if namespaces don't exist)" + +# Wait for External Secret to sync +echo "Waiting for Grafana OAuth External Secret to sync..." +for i in {1..30}; do + STATUS=$(kubectl -n grafana get externalsecret grafana-oauth -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "False") + if [ "$STATUS" = "True" ]; then + echo "Grafana OAuth External Secret is ready!" + break + fi + echo " waiting for External Secret to be ready... (attempt $i/30)" + sleep 2 +done + +# Verify secret was created +if ! kubectl -n grafana get secret grafana-authentik-oauth >/dev/null 2>&1; then + echo "WARNING: grafana-authentik-oauth secret not found. Ensure Vault has grafana-oauth credentials stored." + echo "Run: ../../scripts/sync-grafana-oauth.sh" +else + echo "Grafana OAuth secret successfully synced!" +fi + +# Apply dashboard ConfigMaps +echo "Applying Grafana dashboard ConfigMaps..." +kubectl apply -f grafana/dashboards/ + +echo "✅ Grafana bootstrap complete" \ No newline at end of file diff --git a/scripts/health-check.sh b/scripts/health-check.sh new file mode 100755 index 0000000..9616eca --- /dev/null +++ b/scripts/health-check.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Health check script for Kubernetes cluster + +echo "======================================" +echo " Kubernetes Cluster Health Check" +echo "======================================" +echo "" + +echo "=== Node Resources ===" +kubectl top nodes 2>&1 || echo "❌ Metrics server not working!" +echo "" + +echo "=== High Memory Nodes (>85%) ===" +kubectl top nodes --no-headers | awk '$5 > 85 {print "⚠️ "$1" - "$5"% memory"}' +echo "" + +echo "=== Pods without Resource Limits ===" +COUNT=$(kubectl get pods -A -o json | jq -r '.items[] | select(.spec.containers[].resources.limits == null) | .metadata.namespace + "/" + .metadata.name' 2>/dev/null | wc -l) +echo "$COUNT pods without resource limits" +echo "" + +echo "=== Failing Pods ===" +kubectl get pods -A --field-selector=status.phase!=Running,status.phase!=Succeeded 2>/dev/null | grep -v "No resources found" || echo "✅ All pods running" +echo "" + +echo "=== HPA Status ===" +kubectl get hpa -A 2>/dev/null | grep -v "No resources found" || echo "ℹ️ No HPAs configured" +echo "" + +echo "=== Pods with High Restarts (>10) ===" +kubectl get pods -A -o json | jq -r '.items[] | select(.status.containerStatuses != null) | select(.status.containerStatuses[].restartCount > 10) | .metadata.namespace + "/" + .metadata.name + " - " + (.status.containerStatuses[].restartCount|tostring) + " restarts"' 2>/dev/null || echo "✅ No pods with excessive restarts" +echo "" + +echo "=== Storage Status ===" +kubectl get pv | grep -c "Bound" +echo "persistent volumes bound" +echo "" + +echo "=== Ingress Status ===" +kubectl get ingress -A | tail -n +2 | wc -l +echo "ingresses configured" +echo "" + +echo "======================================" +echo " Health Check Complete" +echo "======================================" diff --git a/scripts/home-assistant-bootstrap.sh b/scripts/home-assistant-bootstrap.sh new file mode 100755 index 0000000..6164c99 --- /dev/null +++ b/scripts/home-assistant-bootstrap.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Home Assistant Bootstrap - Auto-configure OIDC with hass-openid integration +set -euo pipefail + +echo "=== Home Assistant Bootstrap - Fully Automated ===" + +cd "$(dirname "$0")/.." || exit 1 + +NS=home-assistant + +# 1) Ensure namespace +kubectl get ns "${NS}" >/dev/null 2>&1 || kubectl create ns "${NS}" + +# 2) Apply ExternalSecret for OIDC credentials +echo "Applying Home Assistant OIDC ExternalSecret..." +kubectl apply -f apps/home-assistant/external-secret.yaml + +# 3) Wait for ESO to sync OIDC credentials +echo "Waiting for OIDC credentials to sync from Vault..." +for i in {1..30}; do + if kubectl -n "${NS}" get secret homeassistant-oidc-secret >/dev/null 2>&1; then + echo "OIDC credentials synced successfully" + break + fi + echo " waiting... (attempt $i/30)" + sleep 2 +done + +# 4) Wait for the PVC to be bound +echo "Waiting for PVC to be bound..." +for i in {1..60}; do + if kubectl -n "${NS}" get pvc home-assistant-config >/dev/null 2>&1; then + PVC_STATUS=$(kubectl -n "${NS}" get pvc home-assistant-config -o jsonpath='{.status.phase}') + if [ "${PVC_STATUS}" = "Bound" ]; then + echo "PVC is bound" + break + fi + fi + echo " waiting for PVC... (attempt $i/60)" + sleep 2 +done + +# 5) Get the running Home Assistant pod (if any) +POD_NAME=$(kubectl get pods -n "${NS}" -l app.kubernetes.io/name=home-assistant -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") + +if [ -n "${POD_NAME}" ] && kubectl -n "${NS}" get pod "${POD_NAME}" >/dev/null 2>&1; then + echo "Configuring existing Home Assistant pod: ${POD_NAME}" + + # Get OIDC credentials + CLIENT_ID=$(kubectl get secret -n "${NS}" homeassistant-oidc-secret -o jsonpath='{.data.client_id}' | base64 -d) + CLIENT_SECRET=$(kubectl get secret -n "${NS}" homeassistant-oidc-secret -o jsonpath='{.data.client_secret}' | base64 -d) + + # Create secrets.yaml + echo "Creating secrets.yaml..." + kubectl exec -n "${NS}" "${POD_NAME}" -- sh -c "cat > /config/secrets.yaml </dev/null; then + echo '' >> /config/configuration.yaml + echo 'homeassistant:' >> /config/configuration.yaml + echo ' packages: !include_dir_merge_named oidc' >> /config/configuration.yaml + fi + + if ! grep -q 'use_x_forwarded_for:' /config/configuration.yaml 2>/dev/null; then + cat >> /config/configuration.yaml <<'EOF' + +http: + use_x_forwarded_for: true + trusted_proxies: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - 127.0.0.1 + - ::1 +EOF + fi + " + + echo "Configuration complete. Restarting Home Assistant..." + kubectl rollout restart deployment -n "${NS}" home-assistant + kubectl rollout status deployment -n "${NS}" home-assistant --timeout=300s +else + echo "No running Home Assistant pod found. Configuration will be applied when pod starts." + echo "The initContainer will install hass-openid, and the configuration will be set up on first boot." +fi + +echo "Home Assistant bootstrap complete!" diff --git a/scripts/import-grafana-dashboards.sh b/scripts/import-grafana-dashboards.sh new file mode 100755 index 0000000..3bc88d2 --- /dev/null +++ b/scripts/import-grafana-dashboards.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -euo pipefail + +# Import Grafana dashboards from grafana.com +# Usage: ./import-grafana-dashboards.sh + +GRAFANA_URL="http://localhost:3000" +GRAFANA_USER="admin" +GRAFANA_PASSWORD=$(kubectl get secret -n grafana grafana -o jsonpath="{.data.admin-password}" | base64 --decode) + +# Dashboard IDs and their folders +declare -A DASHBOARDS +DASHBOARDS[7249]="Kubernetes" # K8s Cluster +DASHBOARDS[11663]="Kubernetes" # K8s Resources Cluster +DASHBOARDS[11664]="Kubernetes" # K8s Resources Namespace +DASHBOARDS[11665]="Kubernetes" # K8s Resources Pod +DASHBOARDS[11001]="Infrastructure" # Cert-Manager +DASHBOARDS[16888]="Infrastructure" # Longhorn +DASHBOARDS[9628]="Infrastructure" # PostgreSQL +DASHBOARDS[14584]="Infrastructure" # ArgoCD + +echo "Starting port-forward to Grafana..." +kubectl port-forward -n grafana svc/grafana 3000:80 & +PF_PID=$! +sleep 5 + +# Create folders +echo "Creating folders..." +for folder in "Kubernetes" "Infrastructure"; do + curl -s -X POST -H "Content-Type: application/json" \ + -u "${GRAFANA_USER}:${GRAFANA_PASSWORD}" \ + -d "{\"title\":\"${folder}\"}" \ + "${GRAFANA_URL}/api/folders" || true +done + +# Import dashboards +for dashboard_id in "${!DASHBOARDS[@]}"; do + folder="${DASHBOARDS[$dashboard_id]}" + echo "Importing dashboard ${dashboard_id} to folder ${folder}..." + + # Get folder UID + folder_uid=$(curl -s -u "${GRAFANA_USER}:${GRAFANA_PASSWORD}" \ + "${GRAFANA_URL}/api/folders" | \ + jq -r ".[] | select(.title==\"${folder}\") | .uid") + + # Download and import dashboard + dashboard_json=$(curl -s "https://grafana.com/api/dashboards/${dashboard_id}/revisions/1/download") + + # Prepare import payload + import_payload=$(jq -n \ + --arg folderUid "$folder_uid" \ + --argjson dashboard "$dashboard_json" \ + '{ + dashboard: $dashboard, + folderUid: $folderUid, + overwrite: true + }') + + curl -s -X POST -H "Content-Type: application/json" \ + -u "${GRAFANA_USER}:${GRAFANA_PASSWORD}" \ + -d "$import_payload" \ + "${GRAFANA_URL}/api/dashboards/db" | jq -r '.status' +done + +echo "Stopping port-forward..." +kill $PF_PID + +echo "Dashboard import complete!" +echo "Access Grafana at: https://grafana.kube.huskypup.net" diff --git a/scripts/import-unifi-dashboards.sh b/scripts/import-unifi-dashboards.sh new file mode 100755 index 0000000..ef63052 --- /dev/null +++ b/scripts/import-unifi-dashboards.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# Import Unifi dashboards into Grafana via API +# This script downloads the dashboards from GitHub and imports them properly + +set -euo pipefail + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${GREEN}=== Importing Unifi Dashboards to Grafana ===${NC}" +echo "" + +# Port forward to Grafana +echo "Setting up port-forward to Grafana..." +kubectl -n grafana port-forward svc/grafana 3000:80 >/dev/null 2>&1 & +PF_PID=$! +sleep 3 + +# Grafana API credentials (using default admin/admin for now) +GRAFANA_URL="http://localhost:3000" +API_KEY="" + +# Function to cleanup +cleanup() { + echo "" + echo "Cleaning up port-forward..." + kill $PF_PID 2>/dev/null || true +} +trap cleanup EXIT + +# Get admin password from Kubernetes secret or use default +ADMIN_PASS=$(kubectl -n grafana get secret grafana -o jsonpath='{.data.admin-password}' 2>/dev/null | base64 -d || echo "admin") + +# Dashboards to import +DASHBOARDS=("unifi-access-points" "unifi-clients" "unifi-gateway" "unifi-pdu" "unifi-sites" "unifi-switches") + +for dashboard in "${DASHBOARDS[@]}"; do + echo -e "${YELLOW}Importing ${dashboard}...${NC}" + + # Download dashboard + curl -sL "https://raw.githubusercontent.com/timothystewart6/unpoller-unifi/main/grafana/provisioning/dashboards/${dashboard}.json" -o "/tmp/${dashboard}.json" + + # Fix datasource references and wrap for import + python3 << PYEOF +import json + +with open('/tmp/${dashboard}.json', 'r') as f: + data = json.load(f) + +# Remove template fields +data.pop('__inputs', None) +data.pop('__requires', None) +data.pop('id', None) + +# Fix datasource references +def fix_datasource(obj): + if isinstance(obj, dict): + for key, value in obj.items(): + if key == 'datasource': + if isinstance(value, str) and ('DS_PROMETHEUS' in value or value == ''): + obj[key] = {'type': 'prometheus', 'uid': 'Prometheus'} + elif isinstance(value, dict) and value.get('type') == 'prometheus': + obj[key] = {'type': 'prometheus', 'uid': 'Prometheus'} + else: + fix_datasource(value) + elif isinstance(obj, list): + for item in obj: + fix_datasource(item) + +fix_datasource(data) + +# Wrap for import API +import_data = { + "dashboard": data, + "overwrite": True, + "inputs": [], + "folderId": 0 +} + +with open('/tmp/${dashboard}-import.json', 'w') as f: + json.dump(import_data, f) +PYEOF + + # Import via API + response=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + -u "admin:${ADMIN_PASS}" \ + -d @/tmp/${dashboard}-import.json \ + "${GRAFANA_URL}/api/dashboards/db") + + if echo "$response" | grep -q '"status":"success"'; then + echo -e "${GREEN}✓ Successfully imported ${dashboard}${NC}" + else + echo -e "${YELLOW}⚠ Failed to import ${dashboard}: $response${NC}" + fi + + rm -f "/tmp/${dashboard}.json" "/tmp/${dashboard}-import.json" +done + +echo "" +echo -e "${GREEN}✓ Dashboard import complete!${NC}" +echo "" +echo "Visit https://grafana.kube.huskypup.net and search for 'Unifi' to find your dashboards." diff --git a/scripts/metallb-prepare.sh b/scripts/metallb-prepare.sh new file mode 100755 index 0000000..1562fa4 --- /dev/null +++ b/scripts/metallb-prepare.sh @@ -0,0 +1,16 @@ +# https://longhorn.io/docs/1.6.2/deploy/important-notes/#pod-security-policies-disabled--pod-security-admission-introduction +cat </dev/null 2>&1 || kubectl create ns "${NS}" + +# 2) Apply CNPG cluster (auto-generates DB password) +echo "⚙️ Deploying PostgreSQL cluster (low resources)..." +kubectl apply -f nessus/cnpg-cluster.yaml + +# 3) Wait for CNPG cluster to be ready +echo "⏳ Waiting for PostgreSQL cluster to be ready..." +kubectl -n "${NS}" wait --for=condition=Ready "cluster/${CLUSTER}" --timeout=300s || { + echo "⚠️ WARNING: PostgreSQL cluster not ready yet" + echo " This is normal on first deployment - rerun 'helmfile apply' in a few minutes" + exit 0 +} +echo "✅ PostgreSQL cluster ready" +echo "" + +# 4) Apply ESO secrets (auto-rotate DB passwords) +echo "⚙️ Configuring auto-rotating database passwords..." +kubectl apply -f nessus/cnpg-secrets.yaml + +# Wait for DB secret to sync +echo "⏳ Waiting for database secret to sync..." +for i in {1..30}; do + if kubectl -n "${NS}" get secret nessus-db-secret >/dev/null 2>&1; then + echo "✅ Database secret synced" + break + fi + echo " waiting... (attempt $i/30)" + sleep 2 +done + +# 5) Apply Nessus admin credentials ExternalSecret +echo "⚙️ Syncing admin credentials from Vault..." +kubectl apply -f nessus/external-secret.yaml + +# Wait for admin secret to sync +echo "⏳ Waiting for admin credentials to sync from Vault..." +for i in {1..30}; do + if kubectl -n "${NS}" get secret nessus-admin-credentials >/dev/null 2>&1; then + echo "✅ Admin credentials synced from Vault" + break + fi + echo " waiting... (attempt $i/30)" + sleep 2 +done + +# Check if credentials exist in Vault +if ! kubectl -n "${NS}" get secret nessus-admin-credentials >/dev/null 2>&1; then + echo "" + echo "⚠️ WARNING: Nessus admin credentials not found in Vault!" + echo "" + echo "BOOTSTRAP REQUIRED - Run this command first:" + echo "==================================================================" + echo "kubectl exec -n vault vault-0 -- vault kv put secret/nessus \\" + echo " admin-username=\"admin\" \\" + echo " admin-password=\"YourSecurePassword123!\"" + echo "==================================================================" + echo "" + echo "After storing credentials, rerun: helmfile apply" + echo "" + exit 1 +fi + +# 6) Apply PVC for Nessus scan data +echo "⚙️ Creating persistent storage for scan data..." +kubectl apply -f nessus/pvc.yaml + +# 7) Apply Nessus deployment +echo "⚙️ Deploying Nessus scanner..." +kubectl apply -f nessus/deployment.yaml + +# 8) Apply Nessus ingress +echo "⚙️ Configuring ingress with OAuth2-Proxy..." +kubectl apply -f nessus/ingress.yaml + +echo "" +echo "✅ Nessus presync complete - fully automated!" +echo "" +echo "==================================================================" +echo "IMPORTANT: First-time setup instructions" +echo "==================================================================" +echo "" +echo "If you haven't already, store Nessus credentials in Vault:" +echo "" +echo "kubectl exec -n vault vault-0 -- vault kv put secret/nessus \\" +echo " admin-username=\"admin\" \\" +echo " admin-password=\"YourSecurePassword123!\"" +echo "" +echo "Then rerun: helmfile apply" +echo "==================================================================" +echo "" diff --git a/scripts/network-benchmark.yaml b/scripts/network-benchmark.yaml new file mode 100644 index 0000000..4d77334 --- /dev/null +++ b/scripts/network-benchmark.yaml @@ -0,0 +1,203 @@ +--- +# iperf3 server on node-41 +apiVersion: v1 +kind: Pod +metadata: + name: iperf3-server + namespace: default + labels: + app: iperf3-server +spec: + nodeSelector: + kubernetes.io/hostname: talos-node-41 + containers: + - name: iperf3 + image: networkstatic/iperf3:latest + command: ["iperf3"] + args: ["-s"] + ports: + - containerPort: 5201 + protocol: TCP + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + runAsUser: 1000 + securityContext: + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault +--- +# iperf3 server service +apiVersion: v1 +kind: Service +metadata: + name: iperf3-server + namespace: default +spec: + selector: + app: iperf3-server + ports: + - protocol: TCP + port: 5201 + targetPort: 5201 + type: ClusterIP +--- +# iperf3 client on node-42 +apiVersion: v1 +kind: Pod +metadata: + name: iperf3-client-node42 + namespace: default + labels: + app: iperf3-client + test: node42 +spec: + nodeSelector: + kubernetes.io/hostname: talos-node-42 + restartPolicy: Never + containers: + - name: iperf3 + image: networkstatic/iperf3:latest + command: ["/bin/sh"] + args: + - -c + - | + echo "Waiting for iperf3 server to be ready..." + sleep 5 + echo "" + echo "==========================================" + echo "Network Speed Test: node-42 → node-41" + echo "==========================================" + echo "" + echo "Test 1: TCP Bandwidth (10 seconds)" + iperf3 -c iperf3-server -t 10 -P 4 + echo "" + echo "==========================================" + echo "" + echo "Test 2: TCP Bandwidth with larger window (10 seconds)" + iperf3 -c iperf3-server -t 10 -P 4 -w 256K + echo "" + echo "==========================================" + echo "Test Complete!" + echo "==========================================" + sleep 60 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + runAsUser: 1000 + securityContext: + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault +--- +# iperf3 client on node-43 +apiVersion: v1 +kind: Pod +metadata: + name: iperf3-client-node43 + namespace: default + labels: + app: iperf3-client + test: node43 +spec: + nodeSelector: + kubernetes.io/hostname: talos-node-43 + restartPolicy: Never + containers: + - name: iperf3 + image: networkstatic/iperf3:latest + command: ["/bin/sh"] + args: + - -c + - | + echo "Waiting for iperf3 server to be ready..." + sleep 5 + echo "" + echo "==========================================" + echo "Network Speed Test: node-43 → node-41" + echo "==========================================" + echo "" + echo "Test 1: TCP Bandwidth (10 seconds)" + iperf3 -c iperf3-server -t 10 -P 4 + echo "" + echo "==========================================" + echo "" + echo "Test 2: TCP Bandwidth with larger window (10 seconds)" + iperf3 -c iperf3-server -t 10 -P 4 -w 256K + echo "" + echo "==========================================" + echo "Test Complete!" + echo "==========================================" + sleep 60 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + runAsUser: 1000 + securityContext: + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault +--- +# iperf3 client on node-44 +apiVersion: v1 +kind: Pod +metadata: + name: iperf3-client-node44 + namespace: default + labels: + app: iperf3-client + test: node44 +spec: + nodeSelector: + kubernetes.io/hostname: talos-node-44 + restartPolicy: Never + containers: + - name: iperf3 + image: networkstatic/iperf3:latest + command: ["/bin/sh"] + args: + - -c + - | + echo "Waiting for iperf3 server to be ready..." + sleep 5 + echo "" + echo "==========================================" + echo "Network Speed Test: node-44 → node-41" + echo "==========================================" + echo "" + echo "Test 1: TCP Bandwidth (10 seconds)" + iperf3 -c iperf3-server -t 10 -P 4 + echo "" + echo "==========================================" + echo "" + echo "Test 2: TCP Bandwidth with larger window (10 seconds)" + iperf3 -c iperf3-server -t 10 -P 4 -w 256K + echo "" + echo "==========================================" + echo "Test Complete!" + echo "==========================================" + sleep 60 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + runAsUser: 1000 + securityContext: + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault diff --git a/scripts/rook-ceph-prepare.sh b/scripts/rook-ceph-prepare.sh new file mode 100755 index 0000000..acba1cf --- /dev/null +++ b/scripts/rook-ceph-prepare.sh @@ -0,0 +1,231 @@ +#!/bin/bash +# Rook-Ceph Preparation Script +# Wipes NVMe drives on all nodes, creates namespace with proper PSS labels, +# cleans up stale resources, and verifies node availability + +set -euo pipefail + +NS="rook-ceph" +WIPE_IMAGE="quay.io/ceph/ceph:v19.2.0" + +# Node to NVMe device mapping +declare -A NODE_NVME=( + ["talos-cp-01"]="nvme0n1" + ["talos-cp-02"]="nvme1n1" + ["talos-cp-03"]="nvme0n1" + ["talos-cp-04"]="nvme0n1" +) + +echo "=== Rook-Ceph Preparation ===" + +# Create namespace if it doesn't exist +if ! kubectl get ns "${NS}" >/dev/null 2>&1; then + echo "Creating namespace ${NS}..." + kubectl create ns "${NS}" +fi + +# Apply privileged Pod Security Standards (required for Ceph) +echo "Applying privileged Pod Security Standards..." +kubectl label namespace "${NS}" pod-security.kubernetes.io/enforce=privileged --overwrite +kubectl label namespace "${NS}" pod-security.kubernetes.io/audit=privileged --overwrite +kubectl label namespace "${NS}" pod-security.kubernetes.io/warn=privileged --overwrite + +# Clean up any stale OSD deployments +echo "Cleaning up stale OSD resources..." +for osd_deploy in $(kubectl -n "${NS}" get deployments -o name 2>/dev/null | grep rook-ceph-osd- || true); do + echo " Deleting ${osd_deploy}..." + kubectl -n "${NS}" delete "${osd_deploy}" --ignore-not-found=true +done + +# Clean up stale OSD prepare pods +kubectl -n "${NS}" delete pods -l app=rook-ceph-osd-prepare --ignore-not-found=true 2>/dev/null || true + +# Clean up any stale finalizers from previous failed deployments +echo "Checking for stale CephCluster..." +if kubectl -n "${NS}" get cephcluster rook-ceph >/dev/null 2>&1; then + PHASE=$(kubectl -n "${NS}" get cephcluster rook-ceph -o jsonpath='{.status.phase}' 2>/dev/null || echo "Unknown") + if [ "$PHASE" = "Deleting" ]; then + echo "Found stale CephCluster in Deleting state, cleaning up finalizers..." + kubectl -n "${NS}" patch cephcluster rook-ceph --type merge -p '{"metadata":{"finalizers":[]}}' 2>/dev/null || true + fi +fi + +# Function to wipe NVMe on a specific node +wipe_nvme() { + local node=$1 + local device=$2 + local pod_name="wipe-nvme-${node}" + + echo "Wiping /dev/${device} on ${node}..." + + # Delete any existing wipe pod + kubectl -n "${NS}" delete pod "${pod_name}" --ignore-not-found=true 2>/dev/null || true + + # Create wipe pod + cat </dev/null | grep -i ceph || true); do + echo "Removing VG: \${vg}" + vgremove -ff "\${vg}" 2>/dev/null || true + done + + # Remove PV if exists + pvremove -ff "\${DEVICE}" 2>/dev/null || true + + # Remove device mapper entries + dmsetup remove_all -f 2>/dev/null || true + + # Wipe filesystem signatures + wipefs -af "\${DEVICE}" + + # Zap GPT/MBR + sgdisk --zap-all "\${DEVICE}" + + # Zero first 100MB (clears any remaining metadata) + dd if=/dev/zero of="\${DEVICE}" bs=1M count=100 conv=fsync + + # Zero last 100MB (clears backup GPT) + SECTORS=\$(blockdev --getsz "\${DEVICE}") + dd if=/dev/zero of="\${DEVICE}" bs=1M count=100 seek=\$((SECTORS/2048 - 100)) conv=fsync + + echo "=== Wipe complete for \${DEVICE} ===" + lsblk -f "\${DEVICE}" + blkid "\${DEVICE}" || echo "No signatures (clean)" + volumeMounts: + - name: dev + mountPath: /dev + volumes: + - name: dev + hostPath: + path: /dev +EOF + + # Wait for pod to complete + echo " Waiting for wipe to complete on ${node}..." + if ! kubectl -n "${NS}" wait --for=condition=Ready pod/"${pod_name}" --timeout=30s 2>/dev/null; then + # Pod might have completed already + true + fi + + # Wait for completion (up to 2 minutes) + local timeout=120 + local elapsed=0 + while [ $elapsed -lt $timeout ]; do + local phase=$(kubectl -n "${NS}" get pod "${pod_name}" -o jsonpath='{.status.phase}' 2>/dev/null || echo "Unknown") + if [ "$phase" = "Succeeded" ]; then + echo " ✓ Wipe completed on ${node}" + kubectl -n "${NS}" logs "${pod_name}" 2>/dev/null | tail -5 + kubectl -n "${NS}" delete pod "${pod_name}" --ignore-not-found=true + return 0 + elif [ "$phase" = "Failed" ]; then + echo " ✗ Wipe failed on ${node}" + kubectl -n "${NS}" logs "${pod_name}" 2>/dev/null | tail -20 + kubectl -n "${NS}" delete pod "${pod_name}" --ignore-not-found=true + return 1 + fi + sleep 5 + elapsed=$((elapsed + 5)) + done + + echo " ✗ Wipe timed out on ${node}" + kubectl -n "${NS}" delete pod "${pod_name}" --ignore-not-found=true + return 1 +} + +# Verify nodes are available +echo "Verifying node availability..." +NODE_COUNT=$(kubectl get nodes --no-headers 2>/dev/null | wc -l) +if [ "${NODE_COUNT}" -lt 1 ]; then + echo "ERROR: No nodes available in cluster" + exit 1 +fi +echo "Found ${NODE_COUNT} node(s) available" + +# Check for Ready nodes +READY_NODES=$(kubectl get nodes --no-headers 2>/dev/null | grep -c " Ready" || echo "0") +if [ "${READY_NODES}" -lt 1 ]; then + echo "ERROR: No Ready nodes available in cluster" + exit 1 +fi +echo "Found ${READY_NODES} Ready node(s)" + +# Check if Ceph OSDs already exist (skip wipe if so, unless forced) +FORCE_WIPE="${FORCE_WIPE:-false}" +if [ "$FORCE_WIPE" != "true" ]; then + OSD_COUNT=$(kubectl -n "${NS}" get pods -l app=rook-ceph-osd --no-headers 2>/dev/null | grep -c Running || echo "0") + if [ "$OSD_COUNT" -gt 0 ]; then + echo "" + echo "WARNING: Found ${OSD_COUNT} running OSD(s). Skipping disk wipe to protect existing data." + echo "To force wipe, run: FORCE_WIPE=true ./scripts/rook-ceph-prepare.sh" + echo "" + echo "=== Rook-Ceph preparation complete (no wipe) ===" + exit 0 + fi +fi + +# Wipe NVMe drives on all configured nodes +echo "" +echo "=== Wiping NVMe drives ===" +WIPE_FAILED=0 +for node in "${!NODE_NVME[@]}"; do + device="${NODE_NVME[$node]}" + + # Check if node exists in cluster + if ! kubectl get node "${node}" >/dev/null 2>&1; then + echo "Node ${node} not found in cluster, skipping..." + continue + fi + + if ! wipe_nvme "${node}" "${device}"; then + echo "WARNING: Failed to wipe ${device} on ${node}" + WIPE_FAILED=1 + fi +done + +if [ $WIPE_FAILED -eq 1 ]; then + echo "" + echo "WARNING: Some wipe operations failed. Check logs above." + echo "Continuing with Ceph setup anyway..." +fi + +# List nodes for informational purposes +echo "" +echo "Cluster nodes:" +kubectl get nodes -o wide --no-headers 2>/dev/null | while read -r line; do + echo " $line" +done + +echo "" +echo "=== Rook-Ceph preparation complete ===" +echo "NVMe drives have been wiped and namespace is ready." +echo "Run 'helmfile apply' to deploy Ceph." diff --git a/scripts/run-network-benchmark.sh b/scripts/run-network-benchmark.sh new file mode 100755 index 0000000..fc48bb9 --- /dev/null +++ b/scripts/run-network-benchmark.sh @@ -0,0 +1,184 @@ +#!/bin/bash +set -e + +echo "Starting Network Benchmark Between Nodes..." +echo "" + +# Deploy iperf3 server on node-41 +echo "Deploying iperf3 server on node-41..." +cat </dev/null || true + +echo "" +echo "==========================================" +echo "Test 2: node-43 → node-41" +echo "==========================================" +kubectl run iperf3-client-43 \ + --image=networkstatic/iperf3:latest \ + --restart=Never \ + --rm -i \ + --overrides=' +{ + "spec": { + "nodeSelector": { + "kubernetes.io/hostname": "talos-node-43" + }, + "securityContext": { + "runAsNonRoot": true, + "runAsUser": 1000, + "seccompProfile": { + "type": "RuntimeDefault" + } + }, + "containers": [{ + "name": "iperf3-client-43", + "image": "networkstatic/iperf3:latest", + "stdin": true, + "tty": true, + "command": ["iperf3", "-c", "iperf3-server", "-t", "10", "-P", "4"], + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": ["ALL"] + }, + "runAsNonRoot": true, + "runAsUser": 1000 + } + }] + } +}' 2>/dev/null || true + +echo "" +echo "==========================================" +echo "Test 3: node-44 → node-41" +echo "==========================================" +kubectl run iperf3-client-44 \ + --image=networkstatic/iperf3:latest \ + --restart=Never \ + --rm -i \ + --overrides=' +{ + "spec": { + "nodeSelector": { + "kubernetes.io/hostname": "talos-node-44" + }, + "securityContext": { + "runAsNonRoot": true, + "runAsUser": 1000, + "seccompProfile": { + "type": "RuntimeDefault" + } + }, + "containers": [{ + "name": "iperf3-client-44", + "image": "networkstatic/iperf3:latest", + "stdin": true, + "tty": true, + "command": ["iperf3", "-c", "iperf3-server", "-t", "10", "-P", "4"], + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": ["ALL"] + }, + "runAsNonRoot": true, + "runAsUser": 1000 + } + }] + } +}' 2>/dev/null || true + +echo "" +echo "==========================================" +echo "Cleaning up..." +kubectl delete pod iperf3-server -n default +kubectl delete service iperf3-server -n default + +echo "" +echo "Network benchmark complete!" diff --git a/scripts/setup-ceph-saml.sh b/scripts/setup-ceph-saml.sh new file mode 100755 index 0000000..cd8919c --- /dev/null +++ b/scripts/setup-ceph-saml.sh @@ -0,0 +1,126 @@ +#!/bin/bash +set -euo pipefail + +# setup-ceph-saml.sh +# Configures Ceph Dashboard SAML2 SSO with Authentik +# This script should be run after both Ceph and Authentik are deployed + +CEPH_NAMESPACE="rook-ceph" +AUTHENTIK_NAMESPACE="authentik" +CEPH_BASE_URL="https://ceph.kube.huskypup.net" +AUTHENTIK_SAML_METADATA="https://auth.kube.huskypup.net/application/saml/ceph-dashboard/metadata/" +USERNAME_ATTRIBUTE="username" + +echo "=== Setting up Ceph Dashboard SAML2 SSO ===" + +# Check if Ceph tools pod is available +echo "Checking for Ceph tools pod..." +if ! kubectl -n "${CEPH_NAMESPACE}" get deploy rook-ceph-tools >/dev/null 2>&1; then + echo "ERROR: rook-ceph-tools deployment not found" + exit 1 +fi + +# Wait for Ceph tools to be ready +echo "Waiting for Ceph tools pod to be ready..." +kubectl -n "${CEPH_NAMESPACE}" wait --for=condition=Available deployment/rook-ceph-tools --timeout=120s || { + echo "WARNING: Ceph tools pod may not be ready. Continuing anyway..." +} + +# Check Ceph cluster health +echo "Checking Ceph cluster health..." +CEPH_HEALTH=$(kubectl -n "${CEPH_NAMESPACE}" exec deploy/rook-ceph-tools -- ceph health 2>/dev/null || echo "UNKNOWN") +echo "Ceph health: ${CEPH_HEALTH}" + +if [[ "$CEPH_HEALTH" == "UNKNOWN" ]]; then + echo "ERROR: Cannot communicate with Ceph cluster" + exit 1 +fi + +# Check if Authentik SAML metadata is accessible +echo "Verifying Authentik SAML metadata endpoint..." +HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" "${AUTHENTIK_SAML_METADATA}" 2>/dev/null || echo "000") +if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "302" ]]; then + echo "WARNING: Authentik SAML metadata endpoint returned HTTP ${HTTP_CODE}" + echo "The Ceph SAML application may not be configured in Authentik yet." + echo "Ensure the ceph-blueprint.yaml is applied to Authentik." +fi + +# Configure SAML2 SSO +echo "Configuring SAML2 SSO..." +SAML_CONFIG=$(kubectl -n "${CEPH_NAMESPACE}" exec deploy/rook-ceph-tools -- \ + ceph dashboard sso setup saml2 \ + "${CEPH_BASE_URL}" \ + "${AUTHENTIK_SAML_METADATA}" \ + "${USERNAME_ATTRIBUTE}" 2>&1) || { + echo "ERROR: Failed to configure SAML2 SSO" + echo "$SAML_CONFIG" + exit 1 +} + +echo "SAML2 configuration applied successfully" + +# Enable SSO +echo "Enabling SAML2 SSO..." +kubectl -n "${CEPH_NAMESPACE}" exec deploy/rook-ceph-tools -- \ + ceph dashboard sso enable saml2 + +# Verify SSO status +SSO_STATUS=$(kubectl -n "${CEPH_NAMESPACE}" exec deploy/rook-ceph-tools -- \ + ceph dashboard sso status 2>/dev/null) +echo "SSO Status: ${SSO_STATUS}" + +# Create SSO users from Authentik +echo "" +echo "Creating SSO users in Ceph Dashboard..." + +# Get active users from Authentik database +CNPG_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l cnpg.io/cluster=pg-authentik -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") + +if [ -n "$CNPG_POD" ]; then + # Get password from secret + DB_PASSWORD=$(kubectl -n "${AUTHENTIK_NAMESPACE}" get secret pg-authentik-app -o jsonpath='{.data.password}' | base64 -d) + + # Query for active non-service users + USERS=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${CNPG_POD}" -c postgres -- \ + sh -c "PGPASSWORD='${DB_PASSWORD}' psql -h localhost -U app -d app -t -c \"SELECT username FROM authentik_core_user WHERE is_active = true AND username NOT LIKE 'ak-%' AND username != 'AnonymousUser';\"" 2>/dev/null | tr -d ' ' | grep -v '^$' || echo "") + + if [ -n "$USERS" ]; then + echo "Found users in Authentik: $(echo $USERS | tr '\n' ' ')" + + # Get existing Ceph users + EXISTING_USERS=$(kubectl -n "${CEPH_NAMESPACE}" exec deploy/rook-ceph-tools -- \ + ceph dashboard ac-user-show 2>/dev/null | tr -d '[]"' | tr ',' '\n' | tr -d ' ') + + for USER in $USERS; do + if echo "$EXISTING_USERS" | grep -q "^${USER}$"; then + echo " User '${USER}' already exists in Ceph" + else + echo " Creating user '${USER}' with administrator role..." + # Create user with a temporary password (SSO will bypass password auth) + echo "sso-managed-password-$(date +%s)" | kubectl -n "${CEPH_NAMESPACE}" exec -i deploy/rook-ceph-tools -- \ + ceph dashboard ac-user-create "${USER}" -i - administrator 2>/dev/null && \ + echo " Created user '${USER}'" || \ + echo " WARNING: Failed to create user '${USER}'" + fi + done + else + echo "No users found in Authentik to sync" + fi +else + echo "WARNING: Could not find Authentik PostgreSQL pod. Skipping user sync." + echo "You may need to manually create Ceph users matching your Authentik usernames." +fi + +echo "" +echo "=== Ceph Dashboard SAML2 SSO Setup Complete ===" +echo "" +echo "Configuration:" +echo " Base URL: ${CEPH_BASE_URL}" +echo " IdP Metadata: ${AUTHENTIK_SAML_METADATA}" +echo " Username Attribute: ${USERNAME_ATTRIBUTE}" +echo "" +echo "To login:" +echo " 1. Navigate to ${CEPH_BASE_URL}/auth/saml2/login" +echo " 2. Authenticate with Authentik" +echo " 3. You will be redirected to Ceph Dashboard" +echo "" diff --git a/scripts/setup-netbird-api-token.sh b/scripts/setup-netbird-api-token.sh new file mode 100755 index 0000000..04ba649 --- /dev/null +++ b/scripts/setup-netbird-api-token.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# setup-netbird-api-token.sh - Bootstrap Netbird API token for operator + exporter +# +# Creates a service user in Netbird and generates a long-lived PAT (Personal Access Token) +# for automation. Stores the token in Vault for ExternalSecret consumption. +# +# Usage: +# ./scripts/setup-netbird-api-token.sh +# +# Prerequisites: +# - Netbird management server running and accessible +# - Admin PAT from Netbird dashboard (Settings → Personal Access Tokens) +# - Vault initialized and unsealed +# - kubectl configured for the cluster + +set -euo pipefail + +ADMIN_PAT="${1:?Usage: $0 }" +NETBIRD_API="https://netbird.kube.huskypup.net" +SERVICE_USER_NAME="k8s-operator" +PAT_NAME="operator-automation" + +echo "=== Netbird API Token Bootstrap ===" + +# Check for existing service user +echo "Checking for existing service user '${SERVICE_USER_NAME}'..." +USERS=$(curl -sf -H "Authorization: Token ${ADMIN_PAT}" \ + "${NETBIRD_API}/api/users" 2>/dev/null || echo "[]") + +SERVICE_USER_ID=$(echo "${USERS}" | jq -r \ + ".[] | select(.name == \"${SERVICE_USER_NAME}\" and .is_service_user == true) | .id" 2>/dev/null || echo "") + +if [ -z "${SERVICE_USER_ID}" ]; then + echo "Creating service user '${SERVICE_USER_NAME}'..." + RESPONSE=$(curl -sf -X POST \ + -H "Authorization: Token ${ADMIN_PAT}" \ + -H "Content-Type: application/json" \ + -d "{\"name\": \"${SERVICE_USER_NAME}\", \"role\": \"admin\", \"is_service_user\": true, \"auto_groups\": []}" \ + "${NETBIRD_API}/api/users") + SERVICE_USER_ID=$(echo "${RESPONSE}" | jq -r '.id') + echo " Created service user: ${SERVICE_USER_ID}" +else + echo " Service user already exists: ${SERVICE_USER_ID}" +fi + +# Create PAT for the service user +echo "Creating Personal Access Token '${PAT_NAME}'..." +# Set expiration to 365 days from now +EXPIRES=$(date -u -d "+365 days" "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \ + date -u -v+365d "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null) + +PAT_RESPONSE=$(curl -sf -X POST \ + -H "Authorization: Token ${ADMIN_PAT}" \ + -H "Content-Type: application/json" \ + -d "{\"name\": \"${PAT_NAME}\", \"expires_in\": 365}" \ + "${NETBIRD_API}/api/users/${SERVICE_USER_ID}/tokens") + +API_TOKEN=$(echo "${PAT_RESPONSE}" | jq -r '.plain_token') + +if [ -z "${API_TOKEN}" ] || [ "${API_TOKEN}" = "null" ]; then + echo "ERROR: Failed to create PAT. Response:" + echo "${PAT_RESPONSE}" | jq . 2>/dev/null || echo "${PAT_RESPONSE}" + exit 1 +fi + +echo " PAT created successfully" + +# Store in Vault +echo "Storing API token in Vault at secret/netbird-api-token..." +ROOT_TOKEN=$(kubectl -n vault get secret vault-init-keys -o jsonpath='{.data.VAULT_ROOT_TOKEN}' | base64 -d) +kubectl exec -n vault vault-0 -- env "VAULT_TOKEN=${ROOT_TOKEN}" \ + vault kv put secret/netbird-api-token api-token="${API_TOKEN}" + +echo "" +echo "=== Netbird API Token Bootstrap Complete ===" +echo " Service User: ${SERVICE_USER_NAME} (${SERVICE_USER_ID})" +echo " Vault Path: secret/netbird-api-token" +echo " Expires: ~365 days" +echo "" +echo "Next steps:" +echo " 1. Run: helmfile apply (deploys operator + exporter with the token)" +echo " 2. Annotate services: kubectl annotate svc -n netbird.io/expose=true" diff --git a/scripts/setup-unpoller-credentials.sh b/scripts/setup-unpoller-credentials.sh new file mode 100755 index 0000000..06c6d7c --- /dev/null +++ b/scripts/setup-unpoller-credentials.sh @@ -0,0 +1,181 @@ +#!/bin/bash +# setup-unpoller-credentials.sh +# Store Unifi credentials in Vault for Unpoller +# +# This script helps you store your Unifi controller credentials in Vault +# so that Unpoller can authenticate and collect metrics. +# +# USAGE: +# ====== +# 1. With username/password: +# ./scripts/setup-unpoller-credentials.sh --user admin --pass yourpassword +# +# 2. With API key (same as External DNS): +# ./scripts/setup-unpoller-credentials.sh --use-api-key +# +# 3. Interactive mode (prompts for credentials): +# ./scripts/setup-unpoller-credentials.sh +# + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print colored output +print_info() { echo -e "${GREEN}ℹ${NC} $1"; } +print_warn() { echo -e "${YELLOW}⚠${NC} $1"; } +print_error() { echo -e "${RED}✗${NC} $1"; } +print_success() { echo -e "${GREEN}✓${NC} $1"; } + +# Parse command line arguments +USE_API_KEY=false +UNIFI_USER="" +UNIFI_PASS="" + +while [[ $# -gt 0 ]]; do + case $1 in + --use-api-key) + USE_API_KEY=true + shift + ;; + --user) + UNIFI_USER="$2" + shift 2 + ;; + --pass) + UNIFI_PASS="$2" + shift 2 + ;; + --help|-h) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --use-api-key Use the same API key as External DNS" + echo " --user USERNAME Unifi username (local admin account)" + echo " --pass PASSWORD Unifi password" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " $0 --user admin --pass mypassword" + echo " $0 --use-api-key" + echo " $0 # Interactive mode" + exit 0 + ;; + *) + print_error "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +echo "==================================================" +echo " Unpoller Credentials Setup for Vault" +echo "==================================================" +echo "" + +# Check if Vault is ready +print_info "Checking if Vault is ready..." +if ! kubectl -n vault get pod vault-0 >/dev/null 2>&1; then + print_error "Vault pod not found! Please deploy infrastructure first." + exit 1 +fi + +if ! kubectl -n vault exec vault-0 -- vault status >/dev/null 2>&1; then + print_error "Vault is not ready! Please check Vault status." + exit 1 +fi + +print_success "Vault is ready" +echo "" + +# Get credentials +if [ "$USE_API_KEY" = true ]; then + print_info "Using API key from External DNS configuration..." + API_KEY=$(kubectl -n external-dns get secret external-dns-unifi-secret -o jsonpath='{.data.api-key}' | base64 -d) + UNIFI_USER="$API_KEY" + UNIFI_PASS="$API_KEY" + print_success "API key retrieved: ${API_KEY:0:10}..." +elif [ -z "$UNIFI_USER" ] || [ -z "$UNIFI_PASS" ]; then + # Interactive mode + print_info "No credentials provided, entering interactive mode..." + echo "" + echo "Choose authentication method:" + echo " 1) Use API key (same as External DNS)" + echo " 2) Use Unifi local admin username/password" + echo "" + read -rp "Enter choice [1-2]: " choice + + case $choice in + 1) + print_info "Retrieving API key from External DNS..." + API_KEY=$(kubectl -n external-dns get secret external-dns-unifi-secret -o jsonpath='{.data.api-key}' | base64 -d) + UNIFI_USER="$API_KEY" + UNIFI_PASS="$API_KEY" + print_success "API key retrieved: ${API_KEY:0:10}..." + ;; + 2) + read -rp "Enter Unifi username: " UNIFI_USER + read -rsp "Enter Unifi password: " UNIFI_PASS + echo "" + ;; + *) + print_error "Invalid choice" + exit 1 + ;; + esac +fi + +# Validate credentials are not empty +if [ -z "$UNIFI_USER" ] || [ -z "$UNIFI_PASS" ]; then + print_error "Credentials cannot be empty!" + exit 1 +fi + +echo "" +print_info "Storing credentials in Vault..." + +# Store credentials in Vault +if kubectl -n vault exec vault-0 -- vault kv put secret/unpoller \ + unifi-user="$UNIFI_USER" \ + unifi-pass="$UNIFI_PASS" >/dev/null 2>&1; then + print_success "Credentials stored in Vault at: secret/unpoller" +else + print_error "Failed to store credentials in Vault" + exit 1 +fi + +echo "" +print_info "Verifying credentials were stored correctly..." + +# Verify the secret exists +if kubectl -n vault exec vault-0 -- vault kv get secret/unpoller >/dev/null 2>&1; then + print_success "Credentials verified in Vault" +else + print_error "Failed to verify credentials in Vault" + exit 1 +fi + +echo "" +print_success "Setup complete!" +echo "" +echo "Next steps:" +echo " 1. Deploy or update Unpoller: helmfile apply" +echo " 2. Wait for Unpoller to sync credentials from Vault (via ExternalSecret)" +echo " 3. Check Unpoller logs: kubectl -n unpoller logs -l app.kubernetes.io/name=unpoller" +echo " 4. Verify metrics in Prometheus: http://prometheus.kube.huskypup.net" +echo " 5. View dashboards in Grafana: http://grafana.kube.huskypup.net" +echo "" +echo "Grafana will have 7 new Unifi dashboards:" +echo " - Unifi Access Points" +echo " - Unifi Clients" +echo " - Unifi DPI (Deep Packet Inspection)" +echo " - Unifi Gateway" +echo " - Unifi Sites" +echo " - Unifi Switches" +echo " - Unifi PDU" +echo "" diff --git a/scripts/sync-argocd-oauth.sh b/scripts/sync-argocd-oauth.sh new file mode 100755 index 0000000..24891f6 --- /dev/null +++ b/scripts/sync-argocd-oauth.sh @@ -0,0 +1,118 @@ +#!/bin/bash +set -euo pipefail + +# sync-gitlab-oauth.sh +# Retrieves ArgoCD OAuth provider credentials from Authentik and stores them in Vault +# This script should be run after Authentik is deployed and the ArgoCD blueprint is applied + +AUTHENTIK_NAMESPACE="authentik" +VAULT_NAMESPACE="vault" +VAULT_SECRET_PATH="secret/argocd-oauth" + +echo "=== Syncing ArgoCD OAuth Credentials from Authentik to Vault ===" + +# Check if Authentik is running +# if ! kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --no-headers 2>/dev/null | grep -q Running; then +# echo "ERROR: Authentik is not running. Please deploy Authentik first." +# exit 1 +# fi + +# Wait for Authentik to be fully ready +echo "Waiting for Authentik to be ready..." +kubectl -n "${AUTHENTIK_NAMESPACE}" wait --for=condition=Ready pod -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --timeout=60s || { + echo "WARNING: Authentik may not be fully ready yet. Continuing anyway..." +} + +# Get Authentik pod name +AUTHENTIK_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$AUTHENTIK_POD" ]; then + echo "ERROR: Could not find Authentik server pod" + exit 1 +fi + +echo "Found Authentik pod: ${AUTHENTIK_POD}" + +# Retrieve OAuth2 provider credentials from Authentik +# Note: This uses the Authentik API via the management interface +echo "Retrieving ArgoCD OAuth credentials from Authentik..." + +# Method 1: Try to get credentials directly from Authentik database +CLIENT_ID=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "ArgoCD"' | grep '"client_id"' | cut -d'"' -f4 || echo "") +CLIENT_SECRET=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "ArgoCD"' | grep '"client_secret"' | cut -d'"' -f4 || echo "") + +# If Method 1 fails, try using PostgreSQL directly +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "Attempting to retrieve credentials from Authentik PostgreSQL database..." + + CNPG_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l cnpg.io/cluster=pg-authentik,role=primary -o jsonpath='{.items[0].metadata.name}') + + if [ -z "$CNPG_POD" ]; then + echo "ERROR: Could not find Authentik PostgreSQL pod" + exit 1 + fi + + # Query the database for ArgoCD provider credentials + PROVIDER_DATA=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${CNPG_POD}" -- psql -U authentik -d authentik -t -c \ + "SELECT client_id, client_secret FROM authentik_providers_oauth2_oauth2provider WHERE name='ArgoCD';" 2>/dev/null || echo "") + + if [ -n "$PROVIDER_DATA" ]; then + CLIENT_ID=$(echo "$PROVIDER_DATA" | awk '{print $1}' | tr -d ' ') + CLIENT_SECRET=$(echo "$PROVIDER_DATA" | awk '{print $3}' | tr -d ' ') + fi +fi + +# Validate credentials were retrieved +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "ERROR: Failed to retrieve ArgoCD OAuth credentials from Authentik" + echo "" + echo "Manual steps required:" + echo "1. Access Authentik admin panel at https://auth.kube.huskypup.net" + echo "2. Navigate to: Applications > Providers > ArgoCD" + echo "3. Copy the Client ID and Client Secret" + echo "4. Store them in Vault manually with:" + echo " vault kv put ${VAULT_SECRET_PATH} client-id=\"\" client-secret=\"\"" + exit 1 +fi + +echo "Successfully retrieved credentials:" +echo " Client ID: ${CLIENT_ID}" +echo " Client Secret: ${CLIENT_SECRET:0:10}..." # Only show first 10 chars + +# Check if Vault is unsealed and ready +echo "Checking Vault status..." +VAULT_POD=$(kubectl get pods -n "${VAULT_NAMESPACE}" -l app.kubernetes.io/name=vault -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$VAULT_POD" ]; then + echo "ERROR: Vault pod not found" + exit 1 +fi + +VAULT_STATUS=$(kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault status -format=json 2>/dev/null || echo "{}") +SEALED=$(echo "$VAULT_STATUS" | grep -o '"sealed":[^,}]*' | cut -d':' -f2 | tr -d ' ') + +if [ "$SEALED" = "true" ]; then + echo "ERROR: Vault is sealed. Please unseal Vault first." + exit 1 +fi + +# Store credentials in Vault +echo "Storing ArgoCD OAuth credentials in Vault at ${VAULT_SECRET_PATH}..." +kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault kv put "${VAULT_SECRET_PATH}" \ + client-id="${CLIENT_ID}" \ + client-secret="${CLIENT_SECRET}" + +if [ $? -eq 0 ]; then + echo "✓ Successfully stored ArgoCD OAuth credentials in Vault" + echo "" + echo "The External Secret Operator will now sync these credentials to the argocd namespace." + echo "You can verify with:" + echo " kubectl get externalsecret -n argocd argocd-oauth" + echo " kubectl get secret -n argocd argocd-oauth-secret" +else + echo "ERROR: Failed to store credentials in Vault" + exit 1 +fi + +echo "" +echo "=== ArgoCD OAuth Sync Complete ===" diff --git a/scripts/sync-gitlab-admin.sh b/scripts/sync-gitlab-admin.sh new file mode 100644 index 0000000..4b929ae --- /dev/null +++ b/scripts/sync-gitlab-admin.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# Sync GitLab admin status from Authentik "authentik Admins" group +# This script grants admin access to users who are members of "authentik Admins" in Authentik + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +NAMESPACE="gitlab" +ADMIN_GROUP="authentik Admins" + +echo "🔄 Syncing GitLab admin permissions from Authentik..." + +# Get GitLab root password +GITLAB_ROOT_PASSWORD=$(kubectl get secret -n gitlab gitlab-gitlab-initial-root-password -o jsonpath='{.data.password}' 2>/dev/null | base64 -d || echo "") + +if [ -z "$GITLAB_ROOT_PASSWORD" ]; then + echo "❌ GitLab root password not found" + echo " Please login to GitLab UI first to complete initial setup" + exit 1 +fi + +# Get GitLab API endpoint +GITLAB_URL="https://gitlab.kube.huskypup.net" + +echo "📋 Getting list of users from Authentik '$ADMIN_GROUP' group..." + +# Get Authentik database password +PGPASSWORD=$(kubectl get secret -n authentik pg-authentik-app -o jsonpath='{.data.password}' | base64 -d) + +# Query Authentik database for users in admin group +ADMIN_USERS=$(kubectl exec -n authentik pg-authentik-1 -- env PGPASSWORD="$PGPASSWORD" psql -h pg-authentik-rw -U app -d app -t -c " + SELECT DISTINCT u.email + FROM authentik_core_user u + JOIN authentik_core_user_groups ug ON u.id = ug.user_id + JOIN authentik_core_group g ON ug.group_id = g.group_uuid + WHERE g.name = '$ADMIN_GROUP' AND u.is_active = true; +" 2>/dev/null | grep -v "Defaulted" | xargs) + +if [ -z "$ADMIN_USERS" ]; then + echo "⚠️ No users found in Authentik '$ADMIN_GROUP' group" + exit 0 +fi + +echo "✓ Found admin users: $ADMIN_USERS" +echo "" + +# Create GitLab API token (using root account) +echo "🔑 Creating GitLab API token..." + +# Try to login and get session +SESSION_COOKIE=$(curl -sk -c - "$GITLAB_URL/users/sign_in" | grep '_gitlab_session' | awk '{print $7}') + +# Get CSRF token +CSRF_TOKEN=$(curl -sk -b "_gitlab_session=$SESSION_COOKIE" "$GITLAB_URL/users/sign_in" | grep -o 'name="authenticity_token" value="[^"]*"' | cut -d'"' -f4) + +# Login as root +LOGIN_RESPONSE=$(curl -sk -b "_gitlab_session=$SESSION_COOKIE" -c - \ + -X POST "$GITLAB_URL/users/sign_in" \ + -d "user[login]=root&user[password]=$GITLAB_ROOT_PASSWORD&authenticity_token=$CSRF_TOKEN") + +echo "⚠️ Note: GitLab CE doesn't support automatic admin assignment via API" +echo " Users must be manually promoted to admin in GitLab UI" +echo "" +echo "📝 To manually grant admin access:" +echo " 1. Login to GitLab as root: $GITLAB_URL" +echo " 2. Go to Admin Area > Users" +echo " 3. Find and edit each user: $ADMIN_USERS" +echo " 4. Check 'Admin' checkbox and save" +echo "" +echo "💡 Alternatively, run this from a GitLab Rails console:" +for email in $ADMIN_USERS; do + echo " User.find_by(email: '$email')&.update(admin: true)" +done diff --git a/scripts/sync-gitlab-oauth.sh b/scripts/sync-gitlab-oauth.sh new file mode 100755 index 0000000..ad590f7 --- /dev/null +++ b/scripts/sync-gitlab-oauth.sh @@ -0,0 +1,118 @@ +#!/bin/bash +set -euo pipefail + +# sync-gitlab-oauth.sh +# Retrieves GitLab OAuth provider credentials from Authentik and stores them in Vault +# This script should be run after Authentik is deployed and the GitLab blueprint is applied + +AUTHENTIK_NAMESPACE="authentik" +VAULT_NAMESPACE="vault" +VAULT_SECRET_PATH="secret/gitlab-oauth" + +echo "=== Syncing GitLab OAuth Credentials from Authentik to Vault ===" + +# Check if Authentik is running +if ! kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --no-headers 2>/dev/null | grep -q Running; then + echo "ERROR: Authentik is not running. Please deploy Authentik first." + exit 1 +fi + +# Wait for Authentik to be fully ready +echo "Waiting for Authentik to be ready..." +kubectl -n "${AUTHENTIK_NAMESPACE}" wait --for=condition=Ready pod -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --timeout=60s || { + echo "WARNING: Authentik may not be fully ready yet. Continuing anyway..." +} + +# Get Authentik pod name +AUTHENTIK_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$AUTHENTIK_POD" ]; then + echo "ERROR: Could not find Authentik server pod" + exit 1 +fi + +echo "Found Authentik pod: ${AUTHENTIK_POD}" + +# Retrieve OAuth2 provider credentials from Authentik +# Note: This uses the Authentik API via the management interface +echo "Retrieving GitLab OAuth credentials from Authentik..." + +# Method 1: Try to get credentials directly from Authentik database +CLIENT_ID=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "GitLab"' | grep '"client_id"' | cut -d'"' -f4 || echo "") +CLIENT_SECRET=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "GitLab"' | grep '"client_secret"' | cut -d'"' -f4 || echo "") + +# If Method 1 fails, try using PostgreSQL directly +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "Attempting to retrieve credentials from Authentik PostgreSQL database..." + + CNPG_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l cnpg.io/cluster=pg-authentik,role=primary -o jsonpath='{.items[0].metadata.name}') + + if [ -z "$CNPG_POD" ]; then + echo "ERROR: Could not find Authentik PostgreSQL pod" + exit 1 + fi + + # Query the database for GitLab provider credentials + PROVIDER_DATA=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${CNPG_POD}" -- psql -U authentik -d authentik -t -c \ + "SELECT client_id, client_secret FROM authentik_providers_oauth2_oauth2provider WHERE name='GitLab';" 2>/dev/null || echo "") + + if [ -n "$PROVIDER_DATA" ]; then + CLIENT_ID=$(echo "$PROVIDER_DATA" | awk '{print $1}' | tr -d ' ') + CLIENT_SECRET=$(echo "$PROVIDER_DATA" | awk '{print $3}' | tr -d ' ') + fi +fi + +# Validate credentials were retrieved +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "ERROR: Failed to retrieve GitLab OAuth credentials from Authentik" + echo "" + echo "Manual steps required:" + echo "1. Access Authentik admin panel at https://auth.kube.huskypup.net" + echo "2. Navigate to: Applications > Providers > GitLab" + echo "3. Copy the Client ID and Client Secret" + echo "4. Store them in Vault manually with:" + echo " vault kv put ${VAULT_SECRET_PATH} client-id=\"\" client-secret=\"\"" + exit 1 +fi + +echo "Successfully retrieved credentials:" +echo " Client ID: ${CLIENT_ID}" +echo " Client Secret: ${CLIENT_SECRET:0:10}..." # Only show first 10 chars + +# Check if Vault is unsealed and ready +echo "Checking Vault status..." +VAULT_POD=$(kubectl get pods -n "${VAULT_NAMESPACE}" -l app.kubernetes.io/name=vault -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$VAULT_POD" ]; then + echo "ERROR: Vault pod not found" + exit 1 +fi + +VAULT_STATUS=$(kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault status -format=json 2>/dev/null || echo "{}") +SEALED=$(echo "$VAULT_STATUS" | grep -o '"sealed":[^,}]*' | cut -d':' -f2 | tr -d ' ') + +if [ "$SEALED" = "true" ]; then + echo "ERROR: Vault is sealed. Please unseal Vault first." + exit 1 +fi + +# Store credentials in Vault +echo "Storing GitLab OAuth credentials in Vault at ${VAULT_SECRET_PATH}..." +kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault kv put "${VAULT_SECRET_PATH}" \ + client-id="${CLIENT_ID}" \ + client-secret="${CLIENT_SECRET}" + +if [ $? -eq 0 ]; then + echo "✓ Successfully stored GitLab OAuth credentials in Vault" + echo "" + echo "The External Secret Operator will now sync these credentials to the gitlab namespace." + echo "You can verify with:" + echo " kubectl get externalsecret -n gitlab gitlab-oidc" + echo " kubectl get secret -n gitlab gitlab-oidc-secret" +else + echo "ERROR: Failed to store credentials in Vault" + exit 1 +fi + +echo "" +echo "=== GitLab OAuth Sync Complete ===" diff --git a/scripts/sync-grafana-oauth.sh b/scripts/sync-grafana-oauth.sh new file mode 100755 index 0000000..d05023e --- /dev/null +++ b/scripts/sync-grafana-oauth.sh @@ -0,0 +1,118 @@ +#!/bin/bash +set -euo pipefail + +# sync-grafana-oauth.sh +# Retrieves Grafana OAuth provider credentials from Authentik and stores them in Vault +# This script should be run after Authentik is deployed and the Grafana blueprint is applied + +AUTHENTIK_NAMESPACE="authentik" +VAULT_NAMESPACE="vault" +VAULT_SECRET_PATH="secret/grafana-oauth" + +echo "=== Syncing Grafana OAuth Credentials from Authentik to Vault ===" + +# Check if Authentik is running +if ! kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --no-headers 2>/dev/null | grep -q Running; then + echo "ERROR: Authentik is not running. Please deploy Authentik first." + exit 1 +fi + +# Wait for Authentik to be fully ready +echo "Waiting for Authentik to be ready..." +kubectl -n "${AUTHENTIK_NAMESPACE}" wait --for=condition=Ready pod -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --timeout=60s || { + echo "WARNING: Authentik may not be fully ready yet. Continuing anyway..." +} + +# Get Authentik pod name +AUTHENTIK_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$AUTHENTIK_POD" ]; then + echo "ERROR: Could not find Authentik server pod" + exit 1 +fi + +echo "Found Authentik pod: ${AUTHENTIK_POD}" + +# Retrieve OAuth2 provider credentials from Authentik +# Note: This uses the Authentik API via the management interface +echo "Retrieving Grafana OAuth credentials from Authentik..." + +# Method 1: Try to get credentials directly from Authentik database +CLIENT_ID=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "Grafana"' | grep '"client_id"' | cut -d'"' -f4 || echo "") +CLIENT_SECRET=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "Grafana"' | grep '"client_secret"' | cut -d'"' -f4 || echo "") + +# If Method 1 fails, try using PostgreSQL directly +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "Attempting to retrieve credentials from Authentik PostgreSQL database..." + + CNPG_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l cnpg.io/cluster=pg-authentik,role=primary -o jsonpath='{.items[0].metadata.name}') + + if [ -z "$CNPG_POD" ]; then + echo "ERROR: Could not find Authentik PostgreSQL pod" + exit 1 + fi + + # Query the database for Grafana provider credentials + PROVIDER_DATA=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${CNPG_POD}" -- psql -U authentik -d authentik -t -c \ + "SELECT client_id, client_secret FROM authentik_providers_oauth2_oauth2provider WHERE name='Grafana';" 2>/dev/null || echo "") + + if [ -n "$PROVIDER_DATA" ]; then + CLIENT_ID=$(echo "$PROVIDER_DATA" | awk '{print $1}' | tr -d ' ') + CLIENT_SECRET=$(echo "$PROVIDER_DATA" | awk '{print $3}' | tr -d ' ') + fi +fi + +# Validate credentials were retrieved +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "ERROR: Failed to retrieve Grafana OAuth credentials from Authentik" + echo "" + echo "Manual steps required:" + echo "1. Access Authentik admin panel at https://auth.kube.huskypup.net" + echo "2. Navigate to: Applications > Providers > Grafana" + echo "3. Copy the Client ID and Client Secret" + echo "4. Store them in Vault manually with:" + echo " vault kv put ${VAULT_SECRET_PATH} client-id=\"\" client-secret=\"\"" + exit 1 +fi + +echo "Successfully retrieved credentials:" +echo " Client ID: ${CLIENT_ID}" +echo " Client Secret: ${CLIENT_SECRET:0:10}..." # Only show first 10 chars + +# Check if Vault is unsealed and ready +echo "Checking Vault status..." +VAULT_POD=$(kubectl get pods -n "${VAULT_NAMESPACE}" -l app.kubernetes.io/name=vault -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$VAULT_POD" ]; then + echo "ERROR: Vault pod not found" + exit 1 +fi + +VAULT_STATUS=$(kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault status -format=json 2>/dev/null || echo "{}") +SEALED=$(echo "$VAULT_STATUS" | grep -o '"sealed":[^,}]*' | cut -d':' -f2 | tr -d ' ') + +if [ "$SEALED" = "true" ]; then + echo "ERROR: Vault is sealed. Please unseal Vault first." + exit 1 +fi + +# Store credentials in Vault +echo "Storing Grafana OAuth credentials in Vault at ${VAULT_SECRET_PATH}..." +kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault kv put "${VAULT_SECRET_PATH}" \ + client-id="${CLIENT_ID}" \ + client-secret="${CLIENT_SECRET}" + +if [ $? -eq 0 ]; then + echo "✓ Successfully stored Grafana OAuth credentials in Vault" + echo "" + echo "The External Secret Operator will now sync these credentials to the grafana namespace." + echo "You can verify with:" + echo " kubectl get externalsecret -n grafana grafana-oauth" + echo " kubectl get secret -n grafana grafana-authentik-oauth" +else + echo "ERROR: Failed to store credentials in Vault" + exit 1 +fi + +echo "" +echo "=== Grafana OAuth Sync Complete ===" diff --git a/scripts/sync-guacamole-oauth.sh b/scripts/sync-guacamole-oauth.sh new file mode 100755 index 0000000..4162c35 --- /dev/null +++ b/scripts/sync-guacamole-oauth.sh @@ -0,0 +1,113 @@ +#!/bin/bash +# scripts/sync-guacamole-oauth.sh +# Sync Guacamole OAuth credentials from Authentik to Vault + +set -euo pipefail + +AUTHENTIK_NAMESPACE="authentik" +VAULT_NAMESPACE="vault" +VAULT_SECRET_PATH="secret/guacamole-oauth" + +echo "=== Syncing Guacamole OAuth Credentials from Authentik to Vault ===" + +# Wait for Authentik to be fully ready +echo "Waiting for Authentik to be ready..." +kubectl -n "${AUTHENTIK_NAMESPACE}" wait --for=condition=Ready pod -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --timeout=60s || { + echo "WARNING: Authentik may not be fully ready yet. Continuing anyway..." +} + +# Get Authentik pod name +AUTHENTIK_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$AUTHENTIK_POD" ]; then + echo "ERROR: Could not find Authentik server pod" + exit 1 +fi + +echo "Found Authentik pod: ${AUTHENTIK_POD}" + +# Retrieve OAuth2 provider credentials from Authentik +echo "Retrieving Guacamole OAuth credentials from Authentik..." + +# Try to get credentials via ak command +CLIENT_ID=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "guacamole"' | grep '"client_id"' | cut -d'"' -f4 || echo "") +CLIENT_SECRET=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "guacamole"' | grep '"client_secret"' | cut -d'"' -f4 || echo "") + +# If command fails, try using PostgreSQL directly +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "Attempting to retrieve credentials from Authentik PostgreSQL database..." + + CNPG_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l cnpg.io/cluster=pg-authentik,role=primary -o jsonpath='{.items[0].metadata.name}') + + if [ -z "$CNPG_POD" ]; then + echo "ERROR: Could not find Authentik PostgreSQL pod" + exit 1 + fi + + # Get PostgreSQL password + PG_PASSWORD=$(kubectl get secret -n "${AUTHENTIK_NAMESPACE}" pg-authentik-app -o jsonpath='{.data.password}' | base64 -d) + + # Query the database for guacamole provider credentials + PROVIDER_DATA=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${CNPG_POD}" -c postgres -- env PGPASSWORD="${PG_PASSWORD}" psql -U authentik -d authentik -t -c \ + "SELECT client_id, client_secret FROM authentik_providers_oauth2_oauth2provider WHERE name='guacamole';" 2>/dev/null || echo "") + + if [ -n "$PROVIDER_DATA" ]; then + CLIENT_ID=$(echo "$PROVIDER_DATA" | awk '{print $1}' | tr -d ' ') + CLIENT_SECRET=$(echo "$PROVIDER_DATA" | awk '{print $3}' | tr -d ' ') + fi +fi + +# Validate credentials were retrieved +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "ERROR: Failed to retrieve Guacamole OAuth credentials from Authentik" + echo "" + echo "Manual steps required:" + echo "1. Access Authentik admin panel at https://auth.kube.huskypup.net" + echo "2. Navigate to: Applications > Providers > guacamole" + echo "3. Copy the Client ID and Client Secret" + echo "4. Store them in Vault manually with:" + echo " kubectl exec -n vault -- vault kv put ${VAULT_SECRET_PATH} client-id=\"\" client-secret=\"\"" + exit 1 +fi + +echo "Successfully retrieved credentials:" +echo " Client ID: ${CLIENT_ID}" +echo " Client Secret: ${CLIENT_SECRET:0:10}..." # Only show first 10 chars + +# Check if Vault is unsealed and ready +echo "Checking Vault status..." +VAULT_POD=$(kubectl get pods -n "${VAULT_NAMESPACE}" -l app.kubernetes.io/name=vault -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$VAULT_POD" ]; then + echo "ERROR: Vault pod not found" + exit 1 +fi + +VAULT_STATUS=$(kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault status -format=json 2>/dev/null || echo "{}") +SEALED=$(echo "$VAULT_STATUS" | grep -o '"sealed":[^,}]*' | cut -d':' -f2 | tr -d ' ') + +if [ "$SEALED" = "true" ]; then + echo "ERROR: Vault is sealed. Please unseal Vault first." + exit 1 +fi + +# Store credentials in Vault +echo "Storing Guacamole OAuth credentials in Vault at ${VAULT_SECRET_PATH}..." +kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault kv put "${VAULT_SECRET_PATH}" \ + client-id="${CLIENT_ID}" \ + client-secret="${CLIENT_SECRET}" + +if [ $? -eq 0 ]; then + echo "✓ Successfully stored Guacamole OAuth credentials in Vault" + echo "" + echo "The External Secret Operator will now sync these credentials to the guacamole namespace." + echo "You can verify with:" + echo " kubectl get externalsecret -n guacamole guacamole-oauth" + echo " kubectl get secret -n guacamole guacamole-oauth-secret" +else + echo "ERROR: Failed to store credentials in Vault" + exit 1 +fi + +echo "" +echo "=== Guacamole OAuth Sync Complete ===" \ No newline at end of file diff --git a/scripts/sync-homeassistant-oauth.sh b/scripts/sync-homeassistant-oauth.sh new file mode 100755 index 0000000..602b8ee --- /dev/null +++ b/scripts/sync-homeassistant-oauth.sh @@ -0,0 +1,118 @@ +#!/bin/bash +set -euo pipefail + +# sync-homeassistant-oauth.sh +# Retrieves Home Assistant OIDC provider credentials from Authentik and stores them in Vault +# This script should be run after Authentik is deployed and the Home Assistant blueprint is applied + +AUTHENTIK_NAMESPACE="authentik" +VAULT_NAMESPACE="vault" +VAULT_SECRET_PATH="secret/homeassistant-oauth" + +echo "=== Syncing Home Assistant OIDC Credentials from Authentik to Vault ===" + +# Check if Authentik is running +if ! kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --no-headers 2>/dev/null | grep -q Running; then + echo "ERROR: Authentik is not running. Please deploy Authentik first." + exit 1 +fi + +# Wait for Authentik to be fully ready +echo "Waiting for Authentik to be ready..." +kubectl -n "${AUTHENTIK_NAMESPACE}" wait --for=condition=Ready pod -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --timeout=60s || { + echo "WARNING: Authentik may not be fully ready yet. Continuing anyway..." +} + +# Get Authentik pod name +AUTHENTIK_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$AUTHENTIK_POD" ]; then + echo "ERROR: Could not find Authentik server pod" + exit 1 +fi + +echo "Found Authentik pod: ${AUTHENTIK_POD}" + +# Retrieve OAuth2 provider credentials from Authentik +# Note: This uses the Authentik API via the management interface +echo "Retrieving Home Assistant OIDC credentials from Authentik..." + +# Method 1: Try to get credentials directly from Authentik database +CLIENT_ID=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "Home Assistant"' | grep '"client_id"' | cut -d'"' -f4 || echo "") +CLIENT_SECRET=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "Home Assistant"' | grep '"client_secret"' | cut -d'"' -f4 || echo "") + +# If Method 1 fails, try using PostgreSQL directly +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "Attempting to retrieve credentials from Authentik PostgreSQL database..." + + CNPG_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l cnpg.io/cluster=pg-authentik,role=primary -o jsonpath='{.items[0].metadata.name}') + + if [ -z "$CNPG_POD" ]; then + echo "ERROR: Could not find Authentik PostgreSQL pod" + exit 1 + fi + + # Query the database for Home Assistant provider credentials + PROVIDER_DATA=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${CNPG_POD}" -- psql -U authentik -d authentik -t -c \ + "SELECT client_id, client_secret FROM authentik_providers_oauth2_oauth2provider WHERE name='Home Assistant';" 2>/dev/null || echo "") + + if [ -n "$PROVIDER_DATA" ]; then + CLIENT_ID=$(echo "$PROVIDER_DATA" | awk '{print $1}' | tr -d ' ') + CLIENT_SECRET=$(echo "$PROVIDER_DATA" | awk '{print $3}' | tr -d ' ') + fi +fi + +# Validate credentials were retrieved +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "ERROR: Failed to retrieve Home Assistant OIDC credentials from Authentik" + echo "" + echo "Manual steps required:" + echo "1. Access Authentik admin panel at https://auth.kube.huskypup.net" + echo "2. Navigate to: Applications > Providers > Home Assistant" + echo "3. Copy the Client ID and Client Secret" + echo "4. Store them in Vault manually with:" + echo " vault kv put ${VAULT_SECRET_PATH} client-id=\"\" client-secret=\"\"" + exit 1 +fi + +echo "Successfully retrieved credentials:" +echo " Client ID: ${CLIENT_ID}" +echo " Client Secret: ${CLIENT_SECRET:0:10}..." # Only show first 10 chars + +# Check if Vault is unsealed and ready +echo "Checking Vault status..." +VAULT_POD=$(kubectl get pods -n "${VAULT_NAMESPACE}" -l app.kubernetes.io/name=vault -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$VAULT_POD" ]; then + echo "ERROR: Vault pod not found" + exit 1 +fi + +VAULT_STATUS=$(kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault status -format=json 2>/dev/null || echo "{}") +SEALED=$(echo "$VAULT_STATUS" | grep -o '"sealed":[^,}]*' | cut -d':' -f2 | tr -d ' ') + +if [ "$SEALED" = "true" ]; then + echo "ERROR: Vault is sealed. Please unseal Vault first." + exit 1 +fi + +# Store credentials in Vault +echo "Storing Home Assistant OIDC credentials in Vault at ${VAULT_SECRET_PATH}..." +kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault kv put "${VAULT_SECRET_PATH}" \ + client-id="${CLIENT_ID}" \ + client-secret="${CLIENT_SECRET}" + +if [ $? -eq 0 ]; then + echo "✓ Successfully stored Home Assistant OIDC credentials in Vault" + echo "" + echo "The External Secret Operator will now sync these credentials to the home-assistant namespace." + echo "You can verify with:" + echo " kubectl get externalsecret -n home-assistant homeassistant-oauth" + echo " kubectl get secret -n home-assistant homeassistant-oidc-secret" +else + echo "ERROR: Failed to store credentials in Vault" + exit 1 +fi + +echo "" +echo "=== Home Assistant OIDC Sync Complete ===" diff --git a/scripts/sync-n8n-oauth.sh b/scripts/sync-n8n-oauth.sh new file mode 100755 index 0000000..8196443 --- /dev/null +++ b/scripts/sync-n8n-oauth.sh @@ -0,0 +1,113 @@ +#!/bin/bash +# scripts/sync-n8n-oauth.sh +# Sync n8n OAuth credentials from Authentik to Vault + +set -euo pipefail + +AUTHENTIK_NAMESPACE="authentik" +VAULT_NAMESPACE="vault" +VAULT_SECRET_PATH="secret/n8n-oauth" + +echo "=== Syncing n8n OAuth Credentials from Authentik to Vault ===" + +# Wait for Authentik to be fully ready +echo "Waiting for Authentik to be ready..." +kubectl -n "${AUTHENTIK_NAMESPACE}" wait --for=condition=Ready pod -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --timeout=60s || { + echo "WARNING: Authentik may not be fully ready yet. Continuing anyway..." +} + +# Get Authentik pod name +AUTHENTIK_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$AUTHENTIK_POD" ]; then + echo "ERROR: Could not find Authentik server pod" + exit 1 +fi + +echo "Found Authentik pod: ${AUTHENTIK_POD}" + +# Retrieve OAuth2 provider credentials from Authentik +echo "Retrieving n8n OAuth credentials from Authentik..." + +# Try to get credentials via ak command +CLIENT_ID=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "n8n"' | grep '"client_id"' | cut -d'"' -f4 || echo "") +CLIENT_SECRET=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "n8n"' | grep '"client_secret"' | cut -d'"' -f4 || echo "") + +# If command fails, try using PostgreSQL directly +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "Attempting to retrieve credentials from Authentik PostgreSQL database..." + + CNPG_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l cnpg.io/cluster=pg-authentik,role=primary -o jsonpath='{.items[0].metadata.name}') + + if [ -z "$CNPG_POD" ]; then + echo "ERROR: Could not find Authentik PostgreSQL pod" + exit 1 + fi + + # Get PostgreSQL password + PG_PASSWORD=$(kubectl get secret -n "${AUTHENTIK_NAMESPACE}" pg-authentik-app -o jsonpath='{.data.password}' | base64 -d) + + # Query the database for n8n provider credentials + PROVIDER_DATA=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${CNPG_POD}" -c postgres -- env PGPASSWORD="${PG_PASSWORD}" psql -U authentik -d authentik -t -c \ + "SELECT client_id, client_secret FROM authentik_providers_oauth2_oauth2provider WHERE name='n8n';" 2>/dev/null || echo "") + + if [ -n "$PROVIDER_DATA" ]; then + CLIENT_ID=$(echo "$PROVIDER_DATA" | awk '{print $1}' | tr -d ' ') + CLIENT_SECRET=$(echo "$PROVIDER_DATA" | awk '{print $3}' | tr -d ' ') + fi +fi + +# Validate credentials were retrieved +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "ERROR: Failed to retrieve n8n OAuth credentials from Authentik" + echo "" + echo "Manual steps required:" + echo "1. Access Authentik admin panel at https://auth.kube.huskypup.net" + echo "2. Navigate to: Applications > Providers > n8n" + echo "3. Copy the Client ID and Client Secret" + echo "4. Store them in Vault manually with:" + echo " kubectl exec -n vault -- vault kv put ${VAULT_SECRET_PATH} client-id=\"\" client-secret=\"\"" + exit 1 +fi + +echo "Successfully retrieved credentials:" +echo " Client ID: ${CLIENT_ID}" +echo " Client Secret: ${CLIENT_SECRET:0:10}..." # Only show first 10 chars + +# Check if Vault is unsealed and ready +echo "Checking Vault status..." +VAULT_POD=$(kubectl get pods -n "${VAULT_NAMESPACE}" -l app.kubernetes.io/name=vault -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$VAULT_POD" ]; then + echo "ERROR: Vault pod not found" + exit 1 +fi + +VAULT_STATUS=$(kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault status -format=json 2>/dev/null || echo "{}") +SEALED=$(echo "$VAULT_STATUS" | grep -o '"sealed":[^,}]*' | cut -d':' -f2 | tr -d ' ') + +if [ "$SEALED" = "true" ]; then + echo "ERROR: Vault is sealed. Please unseal Vault first." + exit 1 +fi + +# Store credentials in Vault +echo "Storing n8n OAuth credentials in Vault at ${VAULT_SECRET_PATH}..." +kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault kv put "${VAULT_SECRET_PATH}" \ + client-id="${CLIENT_ID}" \ + client-secret="${CLIENT_SECRET}" + +if [ $? -eq 0 ]; then + echo "✓ Successfully stored n8n OAuth credentials in Vault" + echo "" + echo "The External Secret Operator will now sync these credentials to the n8n namespace." + echo "You can verify with:" + echo " kubectl get externalsecret -n n8n n8n-oauth" + echo " kubectl get secret -n n8n n8n-oauth-secret" +else + echo "ERROR: Failed to store credentials in Vault" + exit 1 +fi + +echo "" +echo "=== n8n OAuth Sync Complete ===" diff --git a/scripts/sync-netbird-oauth.sh b/scripts/sync-netbird-oauth.sh new file mode 100755 index 0000000..f41f8ad --- /dev/null +++ b/scripts/sync-netbird-oauth.sh @@ -0,0 +1,147 @@ +#!/bin/bash +set -euo pipefail + +# sync-netbird-oauth.sh +# Retrieves Netbird OAuth provider credentials from Authentik and stores them in Vault + +AUTHENTIK_NAMESPACE="authentik" +VAULT_NAMESPACE="vault" +VAULT_SECRET_PATH="secret/netbird-oauth" + +echo "=== Syncing Netbird OAuth Credentials from Authentik to Vault ===" + +# Check if Authentik is running +if ! kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --no-headers 2>/dev/null | grep -q Running; then + echo "ERROR: Authentik is not running. Please deploy Authentik first." + exit 1 +fi + +# Wait for Authentik to be fully ready +echo "Waiting for Authentik to be ready..." +kubectl -n "${AUTHENTIK_NAMESPACE}" wait --for=condition=Ready pod -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --timeout=60s || { + echo "WARNING: Authentik may not be fully ready yet. Continuing anyway..." +} + +# Get Authentik pod name +AUTHENTIK_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$AUTHENTIK_POD" ]; then + echo "ERROR: Could not find Authentik server pod" + exit 1 +fi + +echo "Found Authentik pod: ${AUTHENTIK_POD}" + +# Retrieve OAuth2 provider credentials from Authentik +echo "Retrieving Netbird OAuth credentials from Authentik..." + +CLIENT_ID=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "Netbird"' | grep '"client_id"' | cut -d'"' -f4 || echo "") +CLIENT_SECRET=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "Netbird"' | grep '"client_secret"' | cut -d'"' -f4 || echo "") + +# Fallback: query PostgreSQL directly +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "Attempting to retrieve credentials from Authentik PostgreSQL database..." + + CNPG_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l cnpg.io/cluster=pg-authentik,role=primary -o jsonpath='{.items[0].metadata.name}') + + if [ -z "$CNPG_POD" ]; then + echo "ERROR: Could not find Authentik PostgreSQL pod" + exit 1 + fi + + PG_PASS=$(kubectl -n "${AUTHENTIK_NAMESPACE}" get secret pg-authentik-app -o jsonpath='{.data.password}' | base64 -d) + PROVIDER_DATA=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${CNPG_POD}" -- env PGPASSWORD="${PG_PASS}" psql -U app -d app -h localhost -t -c \ + "SELECT o.client_id, o.client_secret FROM authentik_providers_oauth2_oauth2provider o JOIN authentik_core_provider p ON o.provider_ptr_id = p.id WHERE p.name='Netbird';" 2>/dev/null || echo "") + + if [ -n "$PROVIDER_DATA" ]; then + CLIENT_ID=$(echo "$PROVIDER_DATA" | awk '{print $1}' | tr -d ' ') + CLIENT_SECRET=$(echo "$PROVIDER_DATA" | awk '{print $3}' | tr -d ' ') + fi +fi + +# Validate credentials were retrieved +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "WARNING: Netbird OAuth provider not yet created by blueprint. Will retry on next sync." + exit 0 +fi + +echo "Successfully retrieved credentials:" +echo " Client ID: ${CLIENT_ID}" +echo " Client Secret: ${CLIENT_SECRET:0:10}..." + +# Check if Vault is unsealed and ready +echo "Checking Vault status..." +VAULT_POD=$(kubectl get pods -n "${VAULT_NAMESPACE}" -l app.kubernetes.io/name=vault -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$VAULT_POD" ]; then + echo "ERROR: Vault pod not found" + exit 1 +fi + +VAULT_STATUS=$(kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault status -format=json 2>/dev/null || echo "{}") +SEALED=$(echo "$VAULT_STATUS" | grep -o '"sealed":[^,}]*' | cut -d':' -f2 | tr -d ' ') + +if [ "$SEALED" = "true" ]; then + echo "ERROR: Vault is sealed. Please unseal Vault first." + exit 1 +fi + +# Create Authentik service account and API token for Netbird IDP management +echo "Ensuring Netbird service account and API token exist in Authentik..." +SA_TOKEN=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak shell -c " +from authentik.core.models import Token, User, Group +try: + user = User.objects.get(username='netbird-service') +except User.DoesNotExist: + user = User.objects.create( + username='netbird-service', + name='Netbird Service Account', + type='service_account', + is_active=True, + path='goauthentik.io/service-accounts' + ) +# Add to authentik Admins group (required for API access) +try: + admins = Group.objects.get(name='authentik Admins') + admins.users.add(user) +except Group.DoesNotExist: + pass +try: + token = Token.objects.get(user=user, identifier='netbird-idp-manager') +except Token.DoesNotExist: + token = Token.objects.create( + user=user, identifier='netbird-idp-manager', intent='app_password', expiring=False + ) +print(token.key) +" 2>&1 | tail -1) + +if [ -z "$SA_TOKEN" ] || [[ "$SA_TOKEN" == *"Error"* ]]; then + echo "WARNING: Could not create Netbird service account token. IDP user sync will not work." + SA_TOKEN="placeholder" + SA_USERNAME="netbird-service" +else + echo " Service account token: ${SA_TOKEN:0:10}..." + SA_USERNAME="netbird-service" +fi + +# Store credentials in Vault +echo "Storing Netbird OAuth credentials in Vault at ${VAULT_SECRET_PATH}..." +kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- env "VAULT_TOKEN=$(kubectl -n vault get secret vault-init-keys -o jsonpath='{.data.VAULT_ROOT_TOKEN}' | base64 -d)" \ + vault kv put "${VAULT_SECRET_PATH}" \ + client-id="${CLIENT_ID}" \ + client-secret="${CLIENT_SECRET}" \ + service-username="${SA_USERNAME}" \ + service-password="${SA_TOKEN}" + +if [ $? -eq 0 ]; then + echo "✓ Successfully stored Netbird OAuth credentials in Vault" +else + echo "ERROR: Failed to store credentials in Vault" + exit 1 +fi + +echo "" +echo "=== Netbird OAuth Sync Complete ===" +echo "NOTE: Netbird IDP user sync (cache warming) may show a 400 error." +echo "This is a known Netbird/Authentik incompatibility (Netbird uses ROPC grant" +echo "which Authentik does not support). OIDC login still works correctly." diff --git a/scripts/sync-nextcloud-oauth.sh b/scripts/sync-nextcloud-oauth.sh new file mode 100755 index 0000000..72eecb8 --- /dev/null +++ b/scripts/sync-nextcloud-oauth.sh @@ -0,0 +1,190 @@ +#!/bin/bash +# scripts/sync-nextcloud-oauth.sh +# Sync Nextcloud OAuth credentials from Authentik to Vault and configure user_oidc provider + +set -euo pipefail + +AUTHENTIK_NAMESPACE="authentik" +VAULT_NAMESPACE="vault" +NEXTCLOUD_NAMESPACE="nextcloud" +VAULT_SECRET_PATH="secret/nextcloud-oauth" +AUTHENTIK_URL="https://auth.kube.huskypup.net" +NEXTCLOUD_URL="https://nextcloud.kube.huskypup.net" + +echo "=== Syncing Nextcloud OAuth Credentials from Authentik to Vault ===" + +# Wait for Authentik to be fully ready +echo "Waiting for Authentik to be ready..." +kubectl -n "${AUTHENTIK_NAMESPACE}" wait --for=condition=Ready pod -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --timeout=60s || { + echo "WARNING: Authentik may not be fully ready yet. Continuing anyway..." +} + +# Get Authentik pod name +AUTHENTIK_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$AUTHENTIK_POD" ]; then + echo "ERROR: Could not find Authentik server pod" + exit 1 +fi + +echo "Found Authentik pod: ${AUTHENTIK_POD}" + +# Retrieve OAuth2 provider credentials from Authentik +echo "Retrieving Nextcloud OAuth credentials from Authentik..." + +# Try to get credentials via ak command +CLIENT_ID=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "Nextcloud"' | grep '"client_id"' | cut -d'"' -f4 || echo "") +CLIENT_SECRET=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "Nextcloud"' | grep '"client_secret"' | cut -d'"' -f4 || echo "") + +# If command fails, try using PostgreSQL directly +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "Attempting to retrieve credentials from Authentik PostgreSQL database..." + + CNPG_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l cnpg.io/cluster=pg-authentik,role=primary -o jsonpath='{.items[0].metadata.name}') + + if [ -z "$CNPG_POD" ]; then + echo "ERROR: Could not find Authentik PostgreSQL pod" + exit 1 + fi + + # Get PostgreSQL password + PG_PASSWORD=$(kubectl get secret -n "${AUTHENTIK_NAMESPACE}" pg-authentik-app -o jsonpath='{.data.password}' | base64 -d) + + # Query the database for Nextcloud provider credentials + PROVIDER_DATA=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${CNPG_POD}" -c postgres -- env PGPASSWORD="${PG_PASSWORD}" psql -U authentik -d authentik -t -c \ + "SELECT client_id, client_secret FROM authentik_providers_oauth2_oauth2provider WHERE name='Nextcloud';" 2>/dev/null || echo "") + + if [ -n "$PROVIDER_DATA" ]; then + CLIENT_ID=$(echo "$PROVIDER_DATA" | awk '{print $1}' | tr -d ' ') + CLIENT_SECRET=$(echo "$PROVIDER_DATA" | awk '{print $3}' | tr -d ' ') + fi +fi + +# Validate credentials were retrieved +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + echo "ERROR: Failed to retrieve Nextcloud OAuth credentials from Authentik" + echo "" + echo "Manual steps required:" + echo "1. Access Authentik admin panel at ${AUTHENTIK_URL}" + echo "2. Navigate to: Applications > Providers > Nextcloud" + echo "3. Copy the Client ID and Client Secret" + echo "4. Store them in Vault manually with:" + echo " kubectl exec -n vault -- vault kv put ${VAULT_SECRET_PATH} client-id=\"\" client-secret=\"\"" + exit 1 +fi + +echo "Successfully retrieved credentials:" +echo " Client ID: ${CLIENT_ID}" +echo " Client Secret: ${CLIENT_SECRET:0:10}..." # Only show first 10 chars + +# Check if Vault is unsealed and ready +echo "Checking Vault status..." +VAULT_POD=$(kubectl get pods -n "${VAULT_NAMESPACE}" -l app.kubernetes.io/name=vault -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$VAULT_POD" ]; then + echo "ERROR: Vault pod not found" + exit 1 +fi + +VAULT_STATUS=$(kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault status -format=json 2>/dev/null || echo "{}") +SEALED=$(echo "$VAULT_STATUS" | grep -o '"sealed":[^,}]*' | cut -d':' -f2 | tr -d ' ') + +if [ "$SEALED" = "true" ]; then + echo "ERROR: Vault is sealed. Please unseal Vault first." + exit 1 +fi + +# Store credentials in Vault +echo "Storing Nextcloud OAuth credentials in Vault at ${VAULT_SECRET_PATH}..." +kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault kv put "${VAULT_SECRET_PATH}" \ + client-id="${CLIENT_ID}" \ + client-secret="${CLIENT_SECRET}" + +if [ $? -eq 0 ]; then + echo "✓ Successfully stored Nextcloud OAuth credentials in Vault" +else + echo "ERROR: Failed to store credentials in Vault" + exit 1 +fi + +# Wait for External Secret to sync +echo "" +echo "Waiting for External Secret to sync credentials to Nextcloud namespace..." +sleep 5 + +# Verify External Secret synced +kubectl wait --for=condition=Ready externalsecret/nextcloud-oauth -n "${NEXTCLOUD_NAMESPACE}" --timeout=30s || { + echo "WARNING: External Secret may not be ready yet. Continuing anyway..." +} + +# Wait for Nextcloud to be ready +echo "Waiting for Nextcloud to be ready..." +kubectl -n "${NEXTCLOUD_NAMESPACE}" wait --for=condition=Ready pod -l app.kubernetes.io/name=nextcloud --timeout=60s || { + echo "WARNING: Nextcloud may not be fully ready yet. Continuing anyway..." +} + +# Configure Nextcloud user_oidc provider +echo "" +echo "=== Configuring Nextcloud OIDC Provider ===" + +# Get Nextcloud pod +NEXTCLOUD_POD=$(kubectl get pods -n "${NEXTCLOUD_NAMESPACE}" -l app.kubernetes.io/name=nextcloud -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$NEXTCLOUD_POD" ]; then + echo "ERROR: Could not find Nextcloud pod" + exit 1 +fi + +echo "Found Nextcloud pod: ${NEXTCLOUD_POD}" + +# Check if provider already exists +EXISTING_PROVIDER=$(kubectl exec -n "${NEXTCLOUD_NAMESPACE}" "${NEXTCLOUD_POD}" -- php occ user_oidc:provider 2>/dev/null | grep -c "Authentik" || true) + +if [ "$EXISTING_PROVIDER" -gt 0 ]; then + echo "Updating existing Authentik OIDC provider..." + kubectl exec -n "${NEXTCLOUD_NAMESPACE}" "${NEXTCLOUD_POD}" -- php occ user_oidc:provider Authentik \ + --clientid="${CLIENT_ID}" \ + --clientsecret="${CLIENT_SECRET}" \ + --discoveryuri="${AUTHENTIK_URL}/application/o/nextcloud/.well-known/openid-configuration" \ + --scope="openid email profile" \ + --mapping-uid="preferred_username" \ + --mapping-display-name="name" \ + --mapping-email="email" \ + --unique-uid=0 +else + echo "Creating new Authentik OIDC provider..." + kubectl exec -n "${NEXTCLOUD_NAMESPACE}" "${NEXTCLOUD_POD}" -- php occ user_oidc:provider Authentik \ + --clientid="${CLIENT_ID}" \ + --clientsecret="${CLIENT_SECRET}" \ + --discoveryuri="${AUTHENTIK_URL}/application/o/nextcloud/.well-known/openid-configuration" \ + --scope="openid email profile" \ + --mapping-uid="preferred_username" \ + --mapping-display-name="name" \ + --mapping-email="email" \ + --unique-uid=0 +fi + +if [ $? -eq 0 ]; then + echo "✓ Successfully configured Nextcloud OIDC provider" +else + echo "ERROR: Failed to configure Nextcloud OIDC provider" + exit 1 +fi + +# Verify configuration +echo "" +echo "Verifying OIDC provider configuration..." +kubectl exec -n "${NEXTCLOUD_NAMESPACE}" "${NEXTCLOUD_POD}" -- php occ user_oidc:provider Authentik + +echo "" +echo "=== Nextcloud OAuth Configuration Complete ===" +echo "" +echo "You can now:" +echo "1. Visit ${NEXTCLOUD_URL}" +echo "2. Click 'Log in with Authentik' button on the login page" +echo "3. Authenticate using your Authentik credentials" +echo "" +echo "To verify the configuration:" +echo " kubectl get externalsecret -n ${NEXTCLOUD_NAMESPACE} nextcloud-oauth" +echo " kubectl get secret -n ${NEXTCLOUD_NAMESPACE} nextcloud-oauth-secret" +echo " kubectl exec -n ${NEXTCLOUD_NAMESPACE} ${NEXTCLOUD_POD} -- php occ user_oidc:provider" diff --git a/scripts/sync-percona-everest-oauth.sh b/scripts/sync-percona-everest-oauth.sh new file mode 100755 index 0000000..aec1adf --- /dev/null +++ b/scripts/sync-percona-everest-oauth.sh @@ -0,0 +1,104 @@ +#!/bin/bash +set -euo pipefail + +# sync-percona-everest-oauth.sh +# Retrieves Percona Everest OIDC provider credentials from Authentik and stores them in Vault +# Everest uses PKCE (public client) so the client_secret is not strictly required, +# but we store it in Vault for reference and potential future use. + +AUTHENTIK_NAMESPACE="authentik" +VAULT_NAMESPACE="vault" +VAULT_SECRET_PATH="secret/percona-everest-oauth" + +echo "=== Syncing Percona Everest OAuth Credentials from Authentik to Vault ===" + +# Check if Authentik is running +if ! kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --no-headers 2>/dev/null | grep -q Running; then + echo "ERROR: Authentik is not running. Please deploy Authentik first." + exit 1 +fi + +# Wait for Authentik to be fully ready +echo "Waiting for Authentik to be ready..." +kubectl -n "${AUTHENTIK_NAMESPACE}" wait --for=condition=Ready pod -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server --timeout=60s || { + echo "WARNING: Authentik may not be fully ready yet. Continuing anyway..." +} + +# Get Authentik pod name +AUTHENTIK_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l app.kubernetes.io/name=authentik,app.kubernetes.io/component=server -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$AUTHENTIK_POD" ]; then + echo "ERROR: Could not find Authentik server pod" + exit 1 +fi + +echo "Found Authentik pod: ${AUTHENTIK_POD}" + +# Retrieve OAuth2 provider credentials from Authentik +echo "Retrieving Percona Everest OAuth credentials from Authentik..." + +# Method 1: Try to get credentials directly from Authentik CLI +CLIENT_ID=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${AUTHENTIK_POD}" -- ak list_providers --type oauth2 2>/dev/null | grep -A 20 '"name": "Percona Everest"' | grep '"client_id"' | cut -d'"' -f4 || echo "") + +# If Method 1 fails, try using PostgreSQL directly +if [ -z "$CLIENT_ID" ]; then + echo "Attempting to retrieve credentials from Authentik PostgreSQL database..." + + CNPG_POD=$(kubectl get pods -n "${AUTHENTIK_NAMESPACE}" -l cnpg.io/cluster=pg-authentik,role=primary -o jsonpath='{.items[0].metadata.name}') + + if [ -z "$CNPG_POD" ]; then + echo "ERROR: Could not find Authentik PostgreSQL pod" + exit 1 + fi + + # Query the database for Percona Everest provider credentials + PROVIDER_DATA=$(kubectl exec -n "${AUTHENTIK_NAMESPACE}" "${CNPG_POD}" -- psql -U authentik -d authentik -t -c \ + "SELECT client_id FROM authentik_providers_oauth2_oauth2provider WHERE name='Percona Everest';" 2>/dev/null || echo "") + + if [ -n "$PROVIDER_DATA" ]; then + CLIENT_ID=$(echo "$PROVIDER_DATA" | tr -d ' ' | tr -d '\n') + fi +fi + +# Validate credentials were retrieved +if [ -z "$CLIENT_ID" ]; then + echo "WARNING: Could not retrieve Percona Everest OAuth credentials from Authentik" + echo "The OIDC provider may not be configured yet (blueprint not processed)." + echo "Everest will use local admin authentication until OIDC is available." + exit 0 +fi + +echo "Successfully retrieved credentials:" +echo " Client ID: ${CLIENT_ID}" + +# Check if Vault is unsealed and ready +echo "Checking Vault status..." +VAULT_POD=$(kubectl get pods -n "${VAULT_NAMESPACE}" -l app.kubernetes.io/name=vault -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$VAULT_POD" ]; then + echo "ERROR: Vault pod not found" + exit 1 +fi + +VAULT_STATUS=$(kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault status -format=json 2>/dev/null || echo "{}") +SEALED=$(echo "$VAULT_STATUS" | grep -o '"sealed":[^,}]*' | cut -d':' -f2 | tr -d ' ') + +if [ "$SEALED" = "true" ]; then + echo "ERROR: Vault is sealed. Please unseal Vault first." + exit 1 +fi + +# Store credentials in Vault +echo "Storing Percona Everest OAuth credentials in Vault at ${VAULT_SECRET_PATH}..." +kubectl exec -n "${VAULT_NAMESPACE}" "${VAULT_POD}" -- vault kv put "${VAULT_SECRET_PATH}" \ + client-id="${CLIENT_ID}" + +if [ $? -eq 0 ]; then + echo "Successfully stored Percona Everest OAuth credentials in Vault" +else + echo "ERROR: Failed to store credentials in Vault" + exit 1 +fi + +echo "" +echo "=== Percona Everest OAuth Sync Complete ===" diff --git a/scripts/talos-iscsi-bootstrap.sh b/scripts/talos-iscsi-bootstrap.sh new file mode 100755 index 0000000..bf7a608 --- /dev/null +++ b/scripts/talos-iscsi-bootstrap.sh @@ -0,0 +1,90 @@ +#!/bin/bash +set -e + +export TALOSCONFIG=/home/scooby/Talosv3/talosconfig + +echo "=== Talos iSCSI Bootstrap Script ===" +echo "" +echo "This script configures Talos nodes for Longhorn by:" +echo "1. Enabling control plane scheduling" +echo "2. Adding Longhorn volume mounts" +echo "3. Upgrading to factory image with iscsi-tools" +echo "" + +# Step 1: Enable scheduling on control planes +echo "Step 1/4: Enabling scheduling on control planes..." +cat > /tmp/allow-scheduling.yaml < /tmp/longhorn-volume.patch.yaml < /tmp/clear-extensions.yaml </dev/null || true +echo "✅ Old extensions removed" + +# Step 4: Upgrade to factory image with iscsi-tools +echo "" +echo "Step 4/4: Upgrading to Talos with iscsi-tools (this will take several minutes)..." +echo "Factory image includes: iscsi-tools, util-linux-tools" +echo "" + +SCHEMATIC_ID="613e1592b2da41ae5e265e8789429f22e121aab91cb4deb6bc3c0b6262961245" +TALOS_VERSION="v1.9.4" + +echo "Upgrading nodes one at a time to: factory.talos.dev/installer/$SCHEMATIC_ID:$TALOS_VERSION" + +for node in 172.28.101.41 172.28.101.42 172.28.101.43 172.28.101.44; do + echo "" + echo "Upgrading $node..." + talosctl upgrade --nodes $node \ + --image factory.talos.dev/installer/$SCHEMATIC_ID:$TALOS_VERSION \ + --preserve --wait --timeout 10m || echo "⚠️ Node $node upgrade encountered issues, continuing..." + + echo "Waiting 30s for node to stabilize..." + sleep 30 +done + +echo "" +echo "=== Waiting for all nodes to be Ready ===" +kubectl wait --for=condition=Ready nodes --all --timeout=300s + +echo "" +echo "=== Verifying iSCSI installation ===" +talosctl get extensions -n 172.28.101.41 + +echo "" +echo "✅ Talos iSCSI bootstrap complete!" +echo "" +echo "Next steps:" +echo " cd /home/scooby/Homelabv5" +echo " ./scripts/bootstrap-crds.sh" +echo " helmfile apply" diff --git a/scripts/teslamate-bootstrap.sh b/scripts/teslamate-bootstrap.sh new file mode 100755 index 0000000..fcd76b9 --- /dev/null +++ b/scripts/teslamate-bootstrap.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# Teslamate Bootstrap - Auto-configure PostgreSQL with ESO +set -euo pipefail + +echo "=== Teslamate Bootstrap - Fully Automated ===" + +# Change to the apps directory for relative paths +cd "$(dirname "$0")/../apps" || exit 1 + +NS=teslamate +CLUSTER=pg-teslamate + +# 1) Ensure namespace +kubectl get ns "${NS}" >/dev/null 2>&1 || kubectl create ns "${NS}" + +# 2) Apply CNPG cluster (auto-generates DB password) +echo "Applying Teslamate PostgreSQL cluster..." +kubectl apply -f teslamate/cnpg-cluster.yaml + +# 3) Wait for CNPG cluster +echo "Waiting for CNPG cluster ${CLUSTER}..." +kubectl -n "${NS}" wait --for=condition=Ready "cluster/${CLUSTER}" --timeout=300s + +# 3.5) Set up PostgreSQL extensions and grant superuser +echo "Setting up PostgreSQL extensions for Teslamate..." +PRIMARY_POD=$(kubectl get pod -n "${NS}" -l cnpg.io/cluster="${CLUSTER}",role=primary -o jsonpath='{.items[0].metadata.name}') + +if [ -n "$PRIMARY_POD" ]; then + echo "Granting superuser to teslamate user..." + kubectl exec -n "${NS}" "$PRIMARY_POD" -- psql -U postgres -c "ALTER USER teslamate WITH SUPERUSER;" 2>/dev/null || echo " (user may already have superuser)" + + echo "Creating required PostgreSQL extensions..." + kubectl exec -n "${NS}" "$PRIMARY_POD" -- psql -U postgres -d teslamate -c "CREATE EXTENSION IF NOT EXISTS cube;" 2>/dev/null || echo " (cube extension may already exist)" + kubectl exec -n "${NS}" "$PRIMARY_POD" -- psql -U postgres -d teslamate -c "CREATE EXTENSION IF NOT EXISTS earthdistance;" 2>/dev/null || echo " (earthdistance extension may already exist)" + + echo "✅ PostgreSQL extensions configured" +else + echo "⚠️ Warning: Could not find primary pod, skipping extension setup" +fi + +# 4) Apply ESO secrets (auto-rotate DB passwords) +echo "Applying Teslamate ESO secrets (auto-generates passwords)..." +kubectl apply -f teslamate/cnpg-secrets.yaml + +# 5) Wait for password to be generated +echo "Waiting for ESO to generate database password..." +for i in {1..30}; do + if kubectl -n "${NS}" get secret pg-teslamate-app >/dev/null 2>&1; then + echo "✅ Database password generated" + break + fi + echo " waiting for secret... (attempt $i/30)" + sleep 2 +done + +# 6) Initialize encryption key in Vault if it doesn't exist +echo "Initializing encryption key in Vault..." + +# Get Vault root token from secret +VAULT_ROOT_TOKEN=$(kubectl -n vault get secret vault-init-keys -o jsonpath='{.data.VAULT_ROOT_TOKEN}' 2>/dev/null | base64 -d || echo "") + +if [ -z "$VAULT_ROOT_TOKEN" ]; then + echo "⚠️ Warning: Could not retrieve Vault root token from secret vault-init-keys" + echo "Skipping encryption key creation. You'll need to create it manually." + echo "" + echo "To create the encryption key manually:" + echo " ENCRYPTION_KEY=\$(openssl rand -base64 32)" + echo " kubectl exec -n vault vault-0 -- vault kv put secret/teslamate/config encryption_key=\"\${ENCRYPTION_KEY}\"" + echo "" + # Continue anyway - the ExternalSecret will show an error if the key doesn't exist +else + # Check if the secret already exists in Vault + if kubectl exec -n vault vault-0 -- env VAULT_TOKEN="${VAULT_ROOT_TOKEN}" vault kv get secret/teslamate/config >/dev/null 2>&1; then + echo "✅ Encryption key already exists in Vault" + else + echo "Generating new encryption key..." + ENCRYPTION_KEY=$(openssl rand -base64 32) + + # Write to Vault using root token + if kubectl exec -n vault vault-0 -- env VAULT_TOKEN="${VAULT_ROOT_TOKEN}" vault kv put secret/teslamate/config encryption_key="${ENCRYPTION_KEY}"; then + echo "✅ Encryption key stored in Vault" + else + echo "❌ Failed to write to Vault" + echo "Please create the encryption key manually:" + echo " kubectl exec -n vault vault-0 -- env VAULT_TOKEN=\"${VAULT_ROOT_TOKEN}\" vault kv put secret/teslamate/config encryption_key=\"${ENCRYPTION_KEY}\"" + exit 1 + fi + fi +fi + +# 7) Apply Teslamate config ExternalSecret +echo "Applying Teslamate config ExternalSecret..." +kubectl apply -f teslamate/external-secret.yaml + +# 8) Wait for teslamate-config-secret to be created +echo "Waiting for teslamate-config-secret to be synced from Vault..." +SECRET_SYNCED=false +for i in {1..30}; do + if kubectl -n "${NS}" get secret teslamate-config-secret >/dev/null 2>&1; then + echo "✅ teslamate-config-secret synced successfully" + SECRET_SYNCED=true + break + fi + echo " waiting for secret... (attempt $i/30)" + sleep 2 +done + +# 9) If ESO failed to sync, create secret manually as fallback +if [ "$SECRET_SYNCED" = "false" ]; then + echo "⚠️ ExternalSecret failed to sync (ClusterSecretStore issue)" + echo "Creating teslamate-config-secret manually from Vault..." + + if [ -n "$VAULT_ROOT_TOKEN" ]; then + ENCRYPTION_KEY=$(kubectl exec -n vault vault-0 -- env VAULT_TOKEN="${VAULT_ROOT_TOKEN}" vault kv get -field=encryption_key secret/teslamate/config 2>/dev/null || echo "") + + if [ -n "$ENCRYPTION_KEY" ]; then + kubectl create secret generic teslamate-config-secret -n "${NS}" \ + --from-literal=encryption-key="${ENCRYPTION_KEY}" \ + --dry-run=client -o yaml | kubectl apply -f - + echo "✅ Created teslamate-config-secret manually" + else + echo "❌ Failed to retrieve encryption key from Vault" + exit 1 + fi + else + echo "❌ Cannot create secret manually - no Vault token" + exit 1 + fi +fi + +echo "✅ Teslamate bootstrap complete - fully automated!" diff --git a/scripts/v1-vs-v2-benchmark.yaml b/scripts/v1-vs-v2-benchmark.yaml new file mode 100644 index 0000000..d7b6530 --- /dev/null +++ b/scripts/v1-vs-v2-benchmark.yaml @@ -0,0 +1,233 @@ +--- +# V1 PVC (traditional iSCSI engine) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: benchmark-v1-pvc + namespace: default +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn # V1 engine (default) + resources: + requests: + storage: 50Gi +--- +# V2 PVC (SPDK engine) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: benchmark-v2-pvc + namespace: default +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn-v2 # V2 SPDK engine + resources: + requests: + storage: 50Gi +--- +# Benchmark scripts ConfigMap +apiVersion: v1 +kind: ConfigMap +metadata: + name: v1-v2-benchmark-scripts + namespace: default +data: + run-benchmarks.sh: | + #!/bin/sh + set -e + + ENGINE=$1 + DATA_DIR=$2 + + echo "==========================================" + echo "Longhorn $ENGINE Data Engine Benchmark" + echo "==========================================" + echo "Test file: $DATA_DIR/testfile (10GB)" + echo "" + + # Sequential Write + echo "1. Sequential Write Test..." + fio --name=seq-write \ + --filename=$DATA_DIR/testfile \ + --size=10G \ + --rw=write \ + --bs=1M \ + --direct=1 \ + --numjobs=1 \ + --time_based \ + --runtime=60 \ + --group_reporting \ + --output-format=normal | tee /tmp/seq-write.log + + SEQ_WRITE_BW=$(grep "WRITE:" /tmp/seq-write.log | awk '{print $2}' | sed 's/bw=//;s/,//') + SEQ_WRITE_IOPS=$(grep "WRITE:" /tmp/seq-write.log | awk '{print $4}' | sed 's/IOPS=//;s/,//') + + echo "" + echo "==========================================" + echo "" + + # Sequential Read + echo "2. Sequential Read Test..." + fio --name=seq-read \ + --filename=$DATA_DIR/testfile \ + --rw=read \ + --bs=1M \ + --direct=1 \ + --numjobs=1 \ + --time_based \ + --runtime=60 \ + --group_reporting \ + --output-format=normal | tee /tmp/seq-read.log + + SEQ_READ_BW=$(grep "READ:" /tmp/seq-read.log | awk '{print $2}' | sed 's/bw=//;s/,//') + SEQ_READ_IOPS=$(grep "READ:" /tmp/seq-read.log | awk '{print $4}' | sed 's/IOPS=//;s/,//') + + echo "" + echo "==========================================" + echo "" + + # Random Write + echo "3. Random Write Test (4K blocks)..." + fio --name=rand-write \ + --filename=$DATA_DIR/testfile \ + --rw=randwrite \ + --bs=4k \ + --direct=1 \ + --numjobs=4 \ + --time_based \ + --runtime=60 \ + --group_reporting \ + --output-format=normal | tee /tmp/rand-write.log + + RAND_WRITE_IOPS=$(grep "WRITE:" /tmp/rand-write.log | awk '{print $4}' | sed 's/IOPS=//;s/,//') + RAND_WRITE_BW=$(grep "WRITE:" /tmp/rand-write.log | awk '{print $2}' | sed 's/bw=//;s/,//') + + echo "" + echo "==========================================" + echo "" + + # Random Read + echo "4. Random Read Test (4K blocks)..." + fio --name=rand-read \ + --filename=$DATA_DIR/testfile \ + --rw=randread \ + --bs=4k \ + --direct=1 \ + --numjobs=4 \ + --time_based \ + --runtime=60 \ + --group_reporting \ + --output-format=normal | tee /tmp/rand-read.log + + RAND_READ_IOPS=$(grep "READ:" /tmp/rand-read.log | awk '{print $4}' | sed 's/IOPS=//;s/,//') + RAND_READ_BW=$(grep "READ:" /tmp/rand-read.log | awk '{print $2}' | sed 's/bw=//;s/,//') + + echo "" + echo "==========================================" + echo "$ENGINE Engine Results Summary" + echo "==========================================" + echo "Sequential Write: $SEQ_WRITE_BW ($SEQ_WRITE_IOPS IOPS)" + echo "Sequential Read: $SEQ_READ_BW ($SEQ_READ_IOPS IOPS)" + echo "Random Write: $RAND_WRITE_BW ($RAND_WRITE_IOPS IOPS)" + echo "Random Read: $RAND_READ_BW ($RAND_READ_IOPS IOPS)" + echo "==========================================" +--- +# V1 Benchmark Job +apiVersion: batch/v1 +kind: Job +metadata: + name: benchmark-v1 + namespace: default +spec: + ttlSecondsAfterFinished: 3600 + template: + metadata: + labels: + app: benchmark + engine: v1 + spec: + restartPolicy: Never + containers: + - name: fio + image: ljishen/fio:latest + command: ["/bin/sh"] + args: ["/scripts/run-benchmarks.sh", "V1", "/data"] + resources: + requests: + cpu: "1" + memory: "2Gi" + limits: + cpu: "2" + memory: "4Gi" + volumeMounts: + - name: data + mountPath: /data + - name: scripts + mountPath: /scripts + securityContext: + runAsNonRoot: false + runAsUser: 0 + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + volumes: + - name: data + persistentVolumeClaim: + claimName: benchmark-v1-pvc + - name: scripts + configMap: + name: v1-v2-benchmark-scripts + defaultMode: 0755 +--- +# V2 Benchmark Job +apiVersion: batch/v1 +kind: Job +metadata: + name: benchmark-v2 + namespace: default +spec: + ttlSecondsAfterFinished: 3600 + template: + metadata: + labels: + app: benchmark + engine: v2 + spec: + restartPolicy: Never + containers: + - name: fio + image: ljishen/fio:latest + command: ["/bin/sh"] + args: ["/scripts/run-benchmarks.sh", "V2", "/data"] + resources: + requests: + cpu: "1" + memory: "2Gi" + limits: + cpu: "2" + memory: "4Gi" + volumeMounts: + - name: data + mountPath: /data + - name: scripts + mountPath: /scripts + securityContext: + runAsNonRoot: false + runAsUser: 0 + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + volumes: + - name: data + persistentVolumeClaim: + claimName: benchmark-v2-pvc + - name: scripts + configMap: + name: v1-v2-benchmark-scripts + defaultMode: 0755 diff --git a/scripts/validate-n8n-deployment.sh b/scripts/validate-n8n-deployment.sh new file mode 100755 index 0000000..b709ece --- /dev/null +++ b/scripts/validate-n8n-deployment.sh @@ -0,0 +1,199 @@ +#!/bin/bash +# scripts/validate-n8n-deployment.sh +# Validate n8n deployment and SCCM automation setup + +set -euo pipefail + +echo "==========================================" +echo "n8n Deployment Validation" +echo "==========================================" +echo "" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +validate_step() { + local step_name=$1 + local command=$2 + + echo -n "Checking $step_name... " + + if eval "$command" &>/dev/null; then + echo -e "${GREEN}✓ PASS${NC}" + return 0 + else + echo -e "${RED}✗ FAIL${NC}" + return 1 + fi +} + +validate_step_with_output() { + local step_name=$1 + local command=$2 + + echo "Checking $step_name..." + + if output=$(eval "$command" 2>&1); then + echo -e "${GREEN}✓ PASS${NC}" + echo "$output" + return 0 + else + echo -e "${RED}✗ FAIL${NC}" + echo "$output" + return 1 + fi +} + +# Track failures +FAILURES=0 + +# 1. Check namespace exists +if ! validate_step "n8n namespace exists" "kubectl get namespace n8n"; then + ((FAILURES++)) + echo -e "${YELLOW} → Run: helmfile -l name=n8n apply${NC}" +fi + +# 2. Check external secrets +echo "" +echo "Checking External Secrets..." +if kubectl get namespace n8n &>/dev/null; then + if ! validate_step " PostgreSQL secret" "kubectl get externalsecret -n n8n n8n-postgresql"; then + ((FAILURES++)) + echo -e "${YELLOW} → Run: kubectl apply -f kubernetes/apps/n8n/external-secret.yaml${NC}" + fi + + if ! validate_step " Config secret" "kubectl get externalsecret -n n8n n8n-config"; then + ((FAILURES++)) + echo -e "${YELLOW} → Run: kubectl apply -f kubernetes/apps/n8n/external-secret.yaml${NC}" + fi + + # Check if secrets are synced + if ! validate_step " PostgreSQL secret synced" "kubectl get secret -n n8n n8n-postgresql-secret"; then + ((FAILURES++)) + echo -e "${YELLOW} → Ensure Vault has secrets. Run: ./scripts/init-n8n-secrets.sh${NC}" + fi + + if ! validate_step " Config secret synced" "kubectl get secret -n n8n n8n-config-secret"; then + ((FAILURES++)) + echo -e "${YELLOW} → Ensure Vault has secrets. Run: ./scripts/init-n8n-secrets.sh${NC}" + fi +fi + +# 3. Check PostgreSQL deployment +echo "" +echo "Checking PostgreSQL..." +if ! validate_step " PostgreSQL StatefulSet" "kubectl get statefulset -n n8n n8n-postgresql"; then + ((FAILURES++)) +fi + +if kubectl get statefulset -n n8n n8n-postgresql &>/dev/null; then + if ! validate_step " PostgreSQL pod ready" "kubectl wait --for=condition=ready pod -n n8n -l app.kubernetes.io/name=postgresql --timeout=10s"; then + ((FAILURES++)) + echo -e "${YELLOW} → Check: kubectl logs -n n8n n8n-postgresql-0${NC}" + fi +fi + +# 4. Check n8n deployment +echo "" +echo "Checking n8n deployment..." +if ! validate_step " n8n deployment exists" "kubectl get deployment -n n8n n8n"; then + ((FAILURES++)) +else + if ! validate_step " n8n pod ready" "kubectl wait --for=condition=ready pod -n n8n -l app.kubernetes.io/name=n8n --timeout=10s"; then + ((FAILURES++)) + echo -e "${YELLOW} → Check: kubectl logs -n n8n deployment/n8n${NC}" + fi +fi + +# 5. Check service +echo "" +if ! validate_step "n8n service exists" "kubectl get service -n n8n n8n"; then + ((FAILURES++)) +fi + +# 6. Check ingress +echo "" +if ! validate_step "n8n ingress exists" "kubectl get ingress -n n8n"; then + ((FAILURES++)) +fi + +# 7. Check ingress hostname +echo "" +if kubectl get ingress -n n8n &>/dev/null; then + INGRESS_HOST=$(kubectl get ingress -n n8n -o jsonpath='{.items[0].spec.rules[0].host}' 2>/dev/null || echo "") + if [ -n "$INGRESS_HOST" ]; then + echo -e "n8n ingress hostname: ${GREEN}$INGRESS_HOST${NC}" + else + echo -e "${RED}✗ No ingress hostname found${NC}" + ((FAILURES++)) + fi +fi + +# 8. Check TLS certificate +echo "" +if ! validate_step "n8n TLS certificate" "kubectl get certificate -n n8n n8n-tls"; then + ((FAILURES++)) +else + CERT_READY=$(kubectl get certificate -n n8n n8n-tls -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "False") + if [ "$CERT_READY" = "True" ]; then + echo -e " Certificate status: ${GREEN}Ready${NC}" + else + echo -e " Certificate status: ${YELLOW}Not Ready${NC}" + echo -e "${YELLOW} → Check: kubectl describe certificate -n n8n n8n-tls${NC}" + fi +fi + +# 9. Check PVC +echo "" +if ! validate_step "n8n PVC exists" "kubectl get pvc -n n8n"; then + ((FAILURES++)) +fi + +# 10. Test n8n health +echo "" +echo "Testing n8n connectivity..." +if [ -n "$INGRESS_HOST" ]; then + if HTTP_CODE=$(curl -s -k -o /dev/null -w "%{http_code}" "https://$INGRESS_HOST" 2>/dev/null); then + if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 302 ]; then + echo -e " HTTP response: ${GREEN}$HTTP_CODE${NC}" + else + echo -e " HTTP response: ${YELLOW}$HTTP_CODE${NC}" + fi + else + echo -e "${YELLOW} → Could not connect to $INGRESS_HOST${NC}" + fi +fi + +# Summary +echo "" +echo "==========================================" +if [ $FAILURES -eq 0 ]; then + echo -e "${GREEN}✓ All checks passed!${NC}" + echo "" + echo "n8n is ready at: https://$INGRESS_HOST" + echo "" + echo "Next steps:" + echo "1. Access n8n and create admin account" + echo "2. Install SCCM API service on Windows: sccm-automation/Install-SCCMAPIService.ps1" + echo "3. Import workflow: sccm-automation/firefox-automation-workflow.json" + echo "4. Configure credentials in n8n" + echo "5. Activate the workflow" + echo "" + echo "See: sccm-automation/QUICKSTART.md for detailed setup" +else + echo -e "${RED}✗ $FAILURES check(s) failed${NC}" + echo "" + echo "Please review the errors above and take corrective action." + echo "" + echo "Common fixes:" + echo "1. Initialize secrets: ./scripts/init-n8n-secrets.sh" + echo "2. Deploy n8n: helmfile -l name=n8n apply" + echo "3. Check logs: kubectl logs -n n8n deployment/n8n" + echo "4. Check events: kubectl get events -n n8n --sort-by='.lastTimestamp'" +fi +echo "==========================================" + +exit $FAILURES diff --git a/scripts/vault-init-and-store.sh b/scripts/vault-init-and-store.sh new file mode 100755 index 0000000..f1557b8 --- /dev/null +++ b/scripts/vault-init-and-store.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +set -euo pipefail + +VAULT_NS="${VAULT_NS:-vault}" +SECRET_NAME="${SECRET_NAME:-vault-init-keys}" +KV_MOUNT="${KV_MOUNT:-secret}" +JOB_NS_FOR_ROLE="${JOB_NS_FOR_ROLE:-auth-proxy}" +JOB_SA_FOR_ROLE="${JOB_SA_FOR_ROLE:-oauth2-bootstrap}" +POLICY_NAME="${POLICY_NAME:-oauth2-writer}" +ROLE_NAME="${ROLE_NAME:-eso-writer}" + +# --- prereqs --- +command -v kubectl >/dev/null || { echo "kubectl not found"; exit 1; } +command -v vault >/dev/null || { echo "vault CLI not found"; exit 1; } +command -v jq >/dev/null || { echo "jq not found"; exit 1; } + +# --- port-forward Vault locally --- +echo "==> Port-forwarding Vault service (ctrl-c in another terminal to stop when done)" +kubectl -n "$VAULT_NS" port-forward svc/vault 8200:8200 >/dev/null 2>&1 & +pf_pid=$! +trap 'kill $pf_pid >/dev/null 2>&1 || true' EXIT +sleep 2 + +export VAULT_ADDR="http://127.0.0.1:8200" + +# --- check init/seal status --- +status_json="$(vault status -format=json || true)" +initialized="$(jq -r '.initialized // empty' <<<"$status_json")" +sealed="$(jq -r '.sealed // empty' <<<"$status_json")" + +root_token="" +unseal_key="" + +# pull existing secret if present +if kubectl -n "$VAULT_NS" get secret "$SECRET_NAME" >/dev/null 2>&1; then + root_token="$(kubectl -n "$VAULT_NS" get secret "$SECRET_NAME" -o jsonpath='{.data.VAULT_ROOT_TOKEN}' | base64 -d || true)" + unseal_key="$(kubectl -n "$VAULT_NS" get secret "$SECRET_NAME" -o jsonpath='{.data.VAULT_UNSEAL_KEY}' | base64 -d || true)" +fi + +# initialize if needed +if [[ "$initialized" != "true" ]]; then + echo "==> Vault not initialized; initializing..." + init_json="$(vault operator init -key-shares=1 -key-threshold=1 -format=json)" + root_token="$(jq -r .root_token <<<"$init_json")" + unseal_key="$(jq -r '.unseal_keys_b64[0]' <<<"$init_json")" + sealed="true" + + echo "==> Storing root token & unseal key in Secret ${VAULT_NS}/${SECRET_NAME}" + kubectl -n "$VAULT_NS" create secret generic "$SECRET_NAME" \ + --from-literal=VAULT_ROOT_TOKEN="$root_token" \ + --from-literal=VAULT_UNSEAL_KEY="$unseal_key" \ + --dry-run=client -o yaml | kubectl apply -f - +else + echo "==> Vault already initialized." +fi + +# unseal if needed +if [[ "$sealed" == "true" ]]; then + [[ -n "$unseal_key" ]] || { echo "ERROR: sealed and no unseal key available"; exit 1; } + echo "==> Unsealing..." + vault operator unseal "$unseal_key" >/dev/null +fi + +# login +if [[ -z "$root_token" ]]; then + echo "==> Reading root token from Secret..." + root_token="$(kubectl -n "$VAULT_NS" get secret "$SECRET_NAME" -o jsonpath='{.data.VAULT_ROOT_TOKEN}' | base64 -d)" +fi +vault login "$root_token" >/dev/null + +# ensure KV v2 enabled +if ! vault secrets list -format=json | jq -e "has(\"${KV_MOUNT}/\")" >/dev/null; then + echo "==> Enabling KV v2 at ${KV_MOUNT}/" + vault secrets enable -path="$KV_MOUNT" -version=2 kv >/dev/null +fi + +# configure Kubernetes auth with a reviewer token from a local SA +echo "==> Ensuring reviewer SA + binding" +kubectl -n "$VAULT_NS" get sa vault-auth >/dev/null 2>&1 || kubectl -n "$VAULT_NS" create sa vault-auth +kubectl get clusterrolebinding vault-auth-delegator >/dev/null 2>&1 || \ + kubectl create clusterrolebinding vault-auth-delegator \ + --clusterrole=system:auth-delegator \ + --serviceaccount="${VAULT_NS}:vault-auth" + +reviewer_jwt="$(kubectl -n "$VAULT_NS" create token vault-auth)" +kube_ca="$(kubectl -n kube-system get configmap kube-root-ca.crt -o jsonpath='{.data.ca\.crt}')" +kube_host="https://kubernetes.default.svc:443" + +vault auth enable kubernetes >/dev/null 2>&1 || true +vault write auth/kubernetes/config \ + token_reviewer_jwt="$reviewer_jwt" \ + kubernetes_host="$kube_host" \ + kubernetes_ca_cert="$kube_ca" >/dev/null + +# policy + role for ESO/oauth2 job +vault policy write "$POLICY_NAME" - >/dev/null <<'HCL' +path "secret/data/*" { + capabilities = ["create", "update", "read", "list"] +} + +path "secret/metadata/*" { + capabilities = ["create", "update", "read", "list"] +} +HCL + +vault write auth/kubernetes/role/${ROLE_NAME} \ + bound_service_account_names="${JOB_SA_FOR_ROLE},external-secrets" \ + bound_service_account_namespaces="${JOB_NS_FOR_ROLE},external-secrets" \ + policies="${POLICY_NAME}" \ + ttl="24h" >/dev/null + +echo "==> Done." +echo "K8s Secret with init creds: ${VAULT_NS}/${SECRET_NAME}" +echo "IMPORTANT: back these up securely and delete the Secret when you’re comfortable:" +echo " kubectl -n ${VAULT_NS} delete secret ${SECRET_NAME}" diff --git a/scripts/verify-gitlab-oidc.sh b/scripts/verify-gitlab-oidc.sh new file mode 100755 index 0000000..5c580e5 --- /dev/null +++ b/scripts/verify-gitlab-oidc.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# verify-gitlab-oidc.sh +# Verify GitLab OIDC configuration and connectivity to Authentik + +set -euo pipefail + +echo "=== GitLab OIDC Configuration Verification ===" +echo "" + +# Check if GitLab pods are running +echo "1. Checking GitLab pods status..." +kubectl get pods -n gitlab -l app=webservice +echo "" + +# Check hostAliases in webservice pods +echo "2. Verifying hostAliases in GitLab webservice..." +WEBSERVICE_POD=$(kubectl get pod -n gitlab -l app=webservice -o jsonpath='{.items[0].metadata.name}') +echo "Using pod: $WEBSERVICE_POD" +kubectl get pod -n gitlab "$WEBSERVICE_POD" -o jsonpath='{.spec.hostAliases}' | jq . +echo "" + +# Test DNS resolution from inside GitLab pod +echo "3. Testing DNS resolution for auth.kube.huskypup.net from GitLab pod..." +kubectl exec -n gitlab "$WEBSERVICE_POD" -c webservice -- getent hosts auth.kube.huskypup.net || echo "getent not available, trying nslookup..." +echo "" + +# Test HTTPS connectivity to Authentik +echo "4. Testing HTTPS connectivity to Authentik..." +kubectl exec -n gitlab "$WEBSERVICE_POD" -c webservice -- curl -I https://auth.kube.huskypup.net/.well-known/openid-configuration 2>&1 | head -20 +echo "" + +# Check OIDC configuration +echo "5. Checking OIDC discovery endpoint..." +kubectl exec -n gitlab "$WEBSERVICE_POD" -c webservice -- curl -s https://auth.kube.huskypup.net/application/o/gitlab/.well-known/openid-configuration 2>&1 | head -10 +echo "" + +# Check for any SSL errors in logs +echo "6. Checking recent GitLab logs for SSL errors..." +kubectl logs -n gitlab "$WEBSERVICE_POD" -c webservice --tail=50 | grep -i "ssl\|certificate\|openid" || echo "No SSL/certificate errors found in recent logs" +echo "" + +echo "=== Verification Complete ===" +echo "" +echo "If you see 'HTTP/1.1 200 OK' or 'HTTP/2 200' above, OIDC is working correctly!" +echo "If you see SSL errors, check the hostAliases configuration." diff --git a/scripts/verify-redis-automation.sh b/scripts/verify-redis-automation.sh new file mode 100755 index 0000000..9c2077b --- /dev/null +++ b/scripts/verify-redis-automation.sh @@ -0,0 +1,177 @@ +#!/bin/bash +# Verify GitLab Redis password rotation automation is working + +set -euo pipefail + +echo "====================================================================" +echo "GitLab Redis Password Rotation Automation - Verification Script" +echo "====================================================================" +echo "" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +check_pass() { + echo -e "${GREEN}✓${NC} $1" +} + +check_fail() { + echo -e "${RED}✗${NC} $1" +} + +check_warn() { + echo -e "${YELLOW}⚠${NC} $1" +} + +ERRORS=0 + +# Check 1: CronJob exists +echo "Checking CronJob deployment..." +if kubectl get cronjob -n gitlab redis-secret-monitor >/dev/null 2>&1; then + check_pass "CronJob 'redis-secret-monitor' is deployed" + + # Get schedule + SCHEDULE=$(kubectl get cronjob -n gitlab redis-secret-monitor -o jsonpath='{.spec.schedule}') + echo " Schedule: $SCHEDULE" + + # Get last successful run + LAST_SUCCESS=$(kubectl get cronjob -n gitlab redis-secret-monitor -o jsonpath='{.status.lastSuccessfulTime}' 2>/dev/null || echo "Never") + echo " Last successful run: $LAST_SUCCESS" +else + check_fail "CronJob 'redis-secret-monitor' not found" + ERRORS=$((ERRORS + 1)) +fi +echo "" + +# Check 2: RBAC resources exist +echo "Checking RBAC resources..." +if kubectl get serviceaccount -n gitlab redis-restart-sa >/dev/null 2>&1; then + check_pass "ServiceAccount 'redis-restart-sa' exists" +else + check_fail "ServiceAccount 'redis-restart-sa' not found" + ERRORS=$((ERRORS + 1)) +fi + +if kubectl get role -n gitlab redis-restart-role >/dev/null 2>&1; then + check_pass "Role 'redis-restart-role' exists" +else + check_fail "Role 'redis-restart-role' not found" + ERRORS=$((ERRORS + 1)) +fi + +if kubectl get rolebinding -n gitlab redis-restart-binding >/dev/null 2>&1; then + check_pass "RoleBinding 'redis-restart-binding' exists" +else + check_fail "RoleBinding 'redis-restart-binding' not found" + ERRORS=$((ERRORS + 1)) +fi +echo "" + +# Check 3: StatefulSet has tracking annotation +echo "Checking StatefulSet annotations..." +SECRET_VERSION=$(kubectl get statefulset -n gitlab redis-gitlab -o jsonpath='{.spec.template.metadata.annotations.secret-version/redis-password}' 2>/dev/null || echo "") +if [ -n "$SECRET_VERSION" ]; then + check_pass "StatefulSet has tracking annotation" + echo " Tracked secret version: $SECRET_VERSION" +else + check_warn "StatefulSet missing tracking annotation (will be added on first CronJob run)" +fi + +RESTART_TIME=$(kubectl get statefulset -n gitlab redis-gitlab -o jsonpath='{.spec.template.metadata.annotations.restarted-at}' 2>/dev/null || echo "") +if [ -n "$RESTART_TIME" ]; then + echo " Last restart: $RESTART_TIME" +fi +echo "" + +# Check 4: Redis is running +echo "Checking Redis pod status..." +if kubectl get pod -n gitlab redis-gitlab-0 >/dev/null 2>&1; then + REDIS_STATUS=$(kubectl get pod -n gitlab redis-gitlab-0 -o jsonpath='{.status.phase}') + if [ "$REDIS_STATUS" = "Running" ]; then + check_pass "Redis pod is Running" + else + check_fail "Redis pod status: $REDIS_STATUS" + ERRORS=$((ERRORS + 1)) + fi +else + check_fail "Redis pod 'redis-gitlab-0' not found" + ERRORS=$((ERRORS + 1)) +fi +echo "" + +# Check 5: Redis authentication works +echo "Testing Redis authentication..." +if RESULT=$(kubectl exec -n gitlab redis-gitlab-0 -- redis-cli -a "$(kubectl get secret -n gitlab redis-gitlab-secret -o jsonpath='{.data.password}' | base64 -d)" ping 2>&1 | grep PONG); then + check_pass "Redis authentication successful (PONG)" +else + check_fail "Redis authentication failed" + ERRORS=$((ERRORS + 1)) +fi +echo "" + +# Check 6: GitLab Sidekiq is healthy +echo "Checking GitLab Sidekiq status..." +SIDEKIQ_READY=$(kubectl get pods -n gitlab -l app=sidekiq -o jsonpath='{.items[*].status.containerStatuses[*].ready}' | grep -o "true" | wc -l) +SIDEKIQ_TOTAL=$(kubectl get pods -n gitlab -l app=sidekiq --no-headers | wc -l) + +if [ "$SIDEKIQ_READY" -eq "$SIDEKIQ_TOTAL" ] && [ "$SIDEKIQ_TOTAL" -gt 0 ]; then + check_pass "Sidekiq pods are healthy ($SIDEKIQ_READY/$SIDEKIQ_TOTAL ready)" +else + check_warn "Sidekiq pods: $SIDEKIQ_READY/$SIDEKIQ_TOTAL ready" +fi +echo "" + +# Check 7: Recent job executions +echo "Checking recent CronJob executions..." +JOB_COUNT=$(kubectl get jobs -n gitlab -l app=redis-secret-monitor --no-headers 2>/dev/null | wc -l) +if [ "$JOB_COUNT" -gt 0 ]; then + check_pass "Found $JOB_COUNT recent job execution(s)" + + # Show last job + LAST_JOB=$(kubectl get jobs -n gitlab -l app=redis-secret-monitor --sort-by=.metadata.creationTimestamp -o name 2>/dev/null | tail -1) + if [ -n "$LAST_JOB" ]; then + echo "" + echo " Last job logs:" + kubectl logs -n gitlab "$LAST_JOB" 2>/dev/null | sed 's/^/ /' + fi +else + check_warn "No recent job executions (CronJob may not have run yet)" +fi +echo "" + +# Check 8: Secret version comparison +echo "Checking secret version synchronization..." +CURRENT_SECRET_VERSION=$(kubectl get secret -n gitlab redis-gitlab-secret -o jsonpath='{.metadata.resourceVersion}') +TRACKED_SECRET_VERSION=$(kubectl get statefulset -n gitlab redis-gitlab -o jsonpath='{.spec.template.metadata.annotations.secret-version/redis-password}' 2>/dev/null || echo "") + +echo " Current secret version: $CURRENT_SECRET_VERSION" +echo " Tracked version in StatefulSet: $TRACKED_SECRET_VERSION" + +if [ "$CURRENT_SECRET_VERSION" = "$TRACKED_SECRET_VERSION" ]; then + check_pass "Secret versions are synchronized" +elif [ -z "$TRACKED_SECRET_VERSION" ]; then + check_warn "StatefulSet not yet tracking secret version (first sync pending)" +else + check_warn "Secret versions differ - restart pending on next CronJob run" + echo " Next scheduled run will synchronize versions" +fi +echo "" + +# Summary +echo "====================================================================" +if [ $ERRORS -eq 0 ]; then + echo -e "${GREEN}✓ All checks passed!${NC}" + echo "" + echo "Redis password rotation automation is properly configured." + echo "The system will automatically restart Redis when passwords rotate." +else + echo -e "${RED}✗ $ERRORS check(s) failed${NC}" + echo "" + echo "Please review the errors above and ensure all automation" + echo "components are properly deployed." + exit 1 +fi +echo "===================================================================="