Initial commit

This commit is contained in:
Scooby Husky
2026-03-09 20:21:35 -05:00
commit aacb8eebbe
314 changed files with 21766 additions and 0 deletions
@@ -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
@@ -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"
@@ -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"
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
@@ -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
@@ -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
+177
View File
@@ -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
@@ -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"
@@ -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
+30
View File
@@ -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
@@ -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"
}
@@ -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."
@@ -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: {}
@@ -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"
@@ -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:
- {}
@@ -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
@@ -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"
+120
View File
@@ -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
+38
View File
@@ -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
@@ -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
@@ -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
@@ -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"
}
@@ -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
@@ -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
+132
View File
@@ -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"
@@ -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
@@ -0,0 +1,6 @@
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy-gateway
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
+21
View File
@@ -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
@@ -0,0 +1,8 @@
---
apiVersion: v1
kind: Secret
metadata:
name: external-dns-unifi-secret
namespace: external-dns
stringData:
api-key: 3Qha5hupHsCkkFyQb8z-T2kh-BasIGSH
+69
View File
@@ -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
@@ -0,0 +1 @@
installCRDs: true
@@ -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
File diff suppressed because it is too large Load Diff
@@ -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
@@ -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"
@@ -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
@@ -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}
@@ -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
+141
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
profile: ambient
resources:
requests:
cpu: 50m
memory: 100Mi
@@ -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"
+54
View File
@@ -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)%"
@@ -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
@@ -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"
@@ -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
@@ -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"
@@ -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
@@ -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
@@ -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
@@ -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"
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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*
@@ -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
@@ -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: {}
@@ -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
@@ -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"
@@ -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"
@@ -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
@@ -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
@@ -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-*
@@ -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
@@ -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
@@ -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: "?*"
@@ -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
@@ -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"
@@ -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: "?*"
@@ -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
@@ -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: "*.*/*"
+128
View File
@@ -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
@@ -0,0 +1,6 @@
apiService:
create: true
args:
- --kubelet-insecure-tls
- --kubelet-preferred-address-types=InternalIP
@@ -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
@@ -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
@@ -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
+38
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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

Some files were not shown because too many files have changed in this diff Show More