diff --git a/.sops.yaml b/.sops.yaml
new file mode 100644
index 0000000000..d1164eb3ff
--- /dev/null
+++ b/.sops.yaml
@@ -0,0 +1,12 @@
+---
+creation_rules:
+  - # IMPORTANT: This rule MUST be above the others
+    path_regex: talos/.*\.sops\.ya?ml
+    key_groups:
+      - age:
+          - "age1y0kzuf0tn94a74whazwae4r9qal4snuqfuhl5jacscrpr7up5gts74fe5w"
+  - path_regex: kubernetes/.*\.sops\.ya?ml
+    encrypted_regex: "^(data|stringData)$"
+    key_groups:
+      - age:
+          - "age1y0kzuf0tn94a74whazwae4r9qal4snuqfuhl5jacscrpr7up5gts74fe5w"
diff --git a/Taskfile.yaml b/Taskfile.yaml
index c812b9db01..233f504b48 100644
--- a/Taskfile.yaml
+++ b/Taskfile.yaml
@@ -36,7 +36,6 @@ includes:
     optional: true
 
 tasks:
-
   default: task --list
 
   init:
@@ -58,6 +57,7 @@ tasks:
       - task: .template
       - task: sops:encrypt
       - task: .validate
+      - cmd: bash {{.ROOT_DIR}}/scripts/rebuild-kustomizations.sh
 
   .template:
     internal: true
diff --git a/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/ks.yaml.j2
index 3efe99d811..289b7bba72 100644
--- a/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/ks.yaml.j2
@@ -13,7 +13,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: true
   interval: 30m
   retryInterval: 1m
@@ -36,7 +36,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: true
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/receiver.yaml.j2 b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/receiver.yaml.j2
index cca5931bd5..d178f1c3c3 100644
--- a/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/receiver.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/receiver.yaml.j2
@@ -13,7 +13,7 @@ spec:
   resources:
     - apiVersion: source.toolkit.fluxcd.io/v1
       kind: GitRepository
-      name: home-kubernetes
+      name: k8s-homelab
       namespace: flux-system
     - apiVersion: kustomize.toolkit.fluxcd.io/v1
       kind: Kustomization
diff --git a/bootstrap/templates/kubernetes/apps/flux-system/webhooks/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/ks.yaml.j2
index e80c50b23b..afa7b0e849 100644
--- a/bootstrap/templates/kubernetes/apps/flux-system/webhooks/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/ks.yaml.j2
@@ -13,7 +13,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: true
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/kube-system/cilium/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/cilium/ks.yaml.j2
index 2522f1dfeb..dbcf6bbbdd 100644
--- a/bootstrap/templates/kubernetes/apps/kube-system/cilium/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/kube-system/cilium/ks.yaml.j2
@@ -13,7 +13,7 @@ spec:
   prune: false # never should be deleted
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: true
   interval: 30m
   retryInterval: 1m
@@ -35,7 +35,7 @@ spec:
   prune: false # never should be deleted
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: false
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/kube-system/coredns/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/coredns/ks.yaml.j2
index bf2a537e65..cec4bfef07 100644
--- a/bootstrap/templates/kubernetes/apps/kube-system/coredns/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/kube-system/coredns/ks.yaml.j2
@@ -13,7 +13,7 @@ spec:
   prune: false # never should be deleted
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: false
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml.j2
index adfb4940ad..936bac6450 100644
--- a/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml.j2
@@ -13,7 +13,7 @@ spec:
   prune: false # never should be deleted
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: false
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/kube-system/metrics-server/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/metrics-server/ks.yaml.j2
index 244f53c165..090b2ae731 100644
--- a/bootstrap/templates/kubernetes/apps/kube-system/metrics-server/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/kube-system/metrics-server/ks.yaml.j2
@@ -13,7 +13,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: false
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/kube-system/reloader/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/reloader/ks.yaml.j2
index 9aa4299346..6f9458dc13 100644
--- a/bootstrap/templates/kubernetes/apps/kube-system/reloader/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/kube-system/reloader/ks.yaml.j2
@@ -13,7 +13,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: false
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/kube-system/spegel/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/spegel/ks.yaml.j2
index 83c730b07f..cb9f37ed47 100644
--- a/bootstrap/templates/kubernetes/apps/kube-system/spegel/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/kube-system/spegel/ks.yaml.j2
@@ -13,7 +13,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: false
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/network/cloudflared/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/cloudflared/ks.yaml.j2
index eb8d8da0be..da98a0f784 100644
--- a/bootstrap/templates/kubernetes/apps/network/cloudflared/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/network/cloudflared/ks.yaml.j2
@@ -15,7 +15,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: false
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/network/echo-server/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/echo-server/ks.yaml.j2
index 2984f219c8..73aef89b6d 100644
--- a/bootstrap/templates/kubernetes/apps/network/echo-server/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/network/echo-server/ks.yaml.j2
@@ -13,7 +13,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: false
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/network/external-dns/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/external-dns/ks.yaml.j2
index eaed4b5665..56b8ed00d9 100644
--- a/bootstrap/templates/kubernetes/apps/network/external-dns/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/network/external-dns/ks.yaml.j2
@@ -13,7 +13,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: true
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/network/ingress-nginx/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/ks.yaml.j2
index 99b1abb58b..570f919471 100644
--- a/bootstrap/templates/kubernetes/apps/network/ingress-nginx/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/ks.yaml.j2
@@ -15,7 +15,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: true
   interval: 30m
   retryInterval: 1m
@@ -37,7 +37,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: false
   interval: 30m
   retryInterval: 1m
@@ -59,7 +59,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: false
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/network/k8s-gateway/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/k8s-gateway/ks.yaml.j2
index 06f442555f..2d4c643f2b 100644
--- a/bootstrap/templates/kubernetes/apps/network/k8s-gateway/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/network/k8s-gateway/ks.yaml.j2
@@ -13,7 +13,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: false
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/observability/prometheus-operator-crds/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/observability/prometheus-operator-crds/ks.yaml.j2
index ffbb5dcb5a..19ed2ef9ee 100644
--- a/bootstrap/templates/kubernetes/apps/observability/prometheus-operator-crds/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/observability/prometheus-operator-crds/ks.yaml.j2
@@ -13,7 +13,7 @@ spec:
   prune: false # never should be deleted
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: false
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/apps/openebs-system/openebs/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/openebs-system/openebs/ks.yaml.j2
index 170feca916..531f679edd 100644
--- a/bootstrap/templates/kubernetes/apps/openebs-system/openebs/ks.yaml.j2
+++ b/bootstrap/templates/kubernetes/apps/openebs-system/openebs/ks.yaml.j2
@@ -13,7 +13,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   wait: false
   interval: 30m
   retryInterval: 1m
diff --git a/bootstrap/templates/kubernetes/flux/apps.yaml.j2 b/bootstrap/templates/kubernetes/flux/apps.yaml.j2
index c4ebba9993..408c48bff3 100644
--- a/bootstrap/templates/kubernetes/flux/apps.yaml.j2
+++ b/bootstrap/templates/kubernetes/flux/apps.yaml.j2
@@ -10,7 +10,7 @@ spec:
   prune: true
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   decryption:
     provider: sops
     secretRef:
diff --git a/bootstrap/templates/kubernetes/flux/config/cluster.yaml.j2 b/bootstrap/templates/kubernetes/flux/config/cluster.yaml.j2
index bae21e8311..e5282983b9 100644
--- a/bootstrap/templates/kubernetes/flux/config/cluster.yaml.j2
+++ b/bootstrap/templates/kubernetes/flux/config/cluster.yaml.j2
@@ -2,7 +2,7 @@
 apiVersion: source.toolkit.fluxcd.io/v1
 kind: GitRepository
 metadata:
-  name: home-kubernetes
+  name: k8s-homelab
   namespace: flux-system
 spec:
   interval: 30m
@@ -31,7 +31,7 @@ spec:
   wait: false
   sourceRef:
     kind: GitRepository
-    name: home-kubernetes
+    name: k8s-homelab
   decryption:
     provider: sops
     secretRef:
diff --git a/kubernetes/apps/cert-manager/cert-manager/app/helmrelease.yaml b/kubernetes/apps/cert-manager/cert-manager/app/helmrelease.yaml
new file mode 100644
index 0000000000..b72a878dff
--- /dev/null
+++ b/kubernetes/apps/cert-manager/cert-manager/app/helmrelease.yaml
@@ -0,0 +1,30 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: cert-manager
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: cert-manager
+      version: v1.14.5
+      sourceRef:
+        kind: HelmRepository
+        name: jetstack
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    installCRDs: true
+    dns01RecursiveNameservers: https://1.1.1.1:443/dns-query,https://1.0.0.1:443/dns-query
+    dns01RecursiveNameserversOnly: true
+    prometheus:
+      enabled: true
+      servicemonitor:
+        enabled: true
diff --git a/kubernetes/apps/cert-manager/cert-manager/app/kustomization.yaml b/kubernetes/apps/cert-manager/cert-manager/app/kustomization.yaml
new file mode 100644
index 0000000000..5dd7baca73
--- /dev/null
+++ b/kubernetes/apps/cert-manager/cert-manager/app/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
diff --git a/kubernetes/apps/cert-manager/cert-manager/issuers/issuers.yaml b/kubernetes/apps/cert-manager/cert-manager/issuers/issuers.yaml
new file mode 100644
index 0000000000..036a159ffb
--- /dev/null
+++ b/kubernetes/apps/cert-manager/cert-manager/issuers/issuers.yaml
@@ -0,0 +1,41 @@
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+  name: letsencrypt-production
+spec:
+  acme:
+    server: https://acme-v02.api.letsencrypt.org/directory
+    email: "${SECRET_ACME_EMAIL}"
+    privateKeySecretRef:
+      name: letsencrypt-production
+    solvers:
+      - dns01:
+          cloudflare:
+            apiTokenSecretRef:
+              name: cert-manager-secret
+              key: api-token
+        selector:
+          dnsZones:
+            - "${SECRET_DOMAIN}"
+            - "${SECRET_CH_DOMAIN}"
+---
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+  name: letsencrypt-staging
+spec:
+  acme:
+    server: https://acme-staging-v02.api.letsencrypt.org/directory
+    email: "${SECRET_ACME_EMAIL}"
+    privateKeySecretRef:
+      name: letsencrypt-staging
+    solvers:
+      - dns01:
+          cloudflare:
+            apiTokenSecretRef:
+              name: cert-manager-secret
+              key: api-token
+        selector:
+          dnsZones:
+            - "${SECRET_DOMAIN}"
+            - "${SECRET_CH_DOMAIN}"
diff --git a/kubernetes/apps/cert-manager/cert-manager/issuers/kustomization.yaml b/kubernetes/apps/cert-manager/cert-manager/issuers/kustomization.yaml
new file mode 100644
index 0000000000..17754be63f
--- /dev/null
+++ b/kubernetes/apps/cert-manager/cert-manager/issuers/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./secret.sops.yaml
+  - ./issuers.yaml
diff --git a/kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml b/kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml
new file mode 100644
index 0000000000..7c95d96a59
--- /dev/null
+++ b/kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml
@@ -0,0 +1,26 @@
+apiVersion: v1
+kind: Secret
+metadata:
+    name: cert-manager-secret
+stringData:
+    api-token: ENC[AES256_GCM,data:F7C2CGVxyxT2aMKrmqW8DzRXMRXfj6ccg9z3jNG7hv/TJTjdipcKHg==,iv:ZeCw+YvvEi/OS4Eh11fttWWqmQw2dyia+8acraizYhw=,tag:46W7pIm9DBCG7h0Dsohm8w==,type:str]
+sops:
+    kms: []
+    gcp_kms: []
+    azure_kv: []
+    hc_vault: []
+    age:
+        - recipient: age1y0kzuf0tn94a74whazwae4r9qal4snuqfuhl5jacscrpr7up5gts74fe5w
+          enc: |
+            -----BEGIN AGE ENCRYPTED FILE-----
+            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWSVdkV2xtQjVueHN6anpE
+            WTNTVE16eFpyS1NHYjNSUzZSY0IrT1c5ZWs4Cm5ib0I0VWpYTlp2cyt6L1ErVXBG
+            Wjh3dGIvelErTGtmUmc4YVpRc2dQaGsKLS0tIDFFYmtnelZjcm5nSzZiU2lLSDk3
+            Y25wYWptbXV3aTczYVp6Zjd1dnp3YVUKdLZiofuhJoGueozdKTc5PkSwzPfQLllp
+            eA1ghGvyH2ux+RAobqDwFyD+JXXJ5aPNVTSc1C1dV0WI4QmzAwXi0Q==
+            -----END AGE ENCRYPTED FILE-----
+    lastmodified: "2024-06-06T09:08:40Z"
+    mac: ENC[AES256_GCM,data:M26XTz4ljTD3j9ojYiwPMMoDTWTeryJr5+g7UUvKgelYjVc/Aglb5TplwOfTS561crGGcDo2gRHJ9EsiCRlx0kMh/u7d09FeQLsEy1xrfqueELPczlhXVe1BjryqB34vNfO6QlCjD6QeITzPyRIrCIzjRykYbEQlVgW4ml99TR4=,iv:dqpyvLv7e+aBLvg6B01t0Y6iPxTDk7++z1DLhRr5x9s=,tag:Cq43iF3TWOZtubDb7ZJTSQ==,type:str]
+    pgp: []
+    encrypted_regex: ^(data|stringData)$
+    version: 3.8.1
diff --git a/kubernetes/apps/cert-manager/cert-manager/ks.yaml b/kubernetes/apps/cert-manager/cert-manager/ks.yaml
new file mode 100644
index 0000000000..04f818d638
--- /dev/null
+++ b/kubernetes/apps/cert-manager/cert-manager/ks.yaml
@@ -0,0 +1,42 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app cert-manager
+  namespace: flux-system
+spec:
+  targetNamespace: cert-manager
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/cert-manager/cert-manager/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app cert-manager-issuers
+  namespace: flux-system
+spec:
+  targetNamespace: cert-manager
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  dependsOn:
+    - name: cert-manager
+  path: ./kubernetes/apps/cert-manager/cert-manager/issuers
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/cert-manager/kustomization.yaml b/kubernetes/apps/cert-manager/kustomization.yaml
new file mode 100644
index 0000000000..3ad7d0307c
--- /dev/null
+++ b/kubernetes/apps/cert-manager/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - namespace.yaml
+  - cert-manager/ks.yaml
diff --git a/kubernetes/apps/cert-manager/namespace.yaml b/kubernetes/apps/cert-manager/namespace.yaml
new file mode 100644
index 0000000000..ed788350f1
--- /dev/null
+++ b/kubernetes/apps/cert-manager/namespace.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: cert-manager
+  labels:
+    kustomize.toolkit.fluxcd.io/prune: disabled
diff --git a/kubernetes/apps/flux-system/kustomization.yaml b/kubernetes/apps/flux-system/kustomization.yaml
new file mode 100644
index 0000000000..e6213b1016
--- /dev/null
+++ b/kubernetes/apps/flux-system/kustomization.yaml
@@ -0,0 +1,8 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - namespace.yaml
+  - monitoring/ks.yaml
+  - weave-gitops/ks.yaml
+  - webhooks/ks.yaml
diff --git a/kubernetes/apps/flux-system/monitoring/app/kustomization.yaml b/kubernetes/apps/flux-system/monitoring/app/kustomization.yaml
new file mode 100644
index 0000000000..247c037449
--- /dev/null
+++ b/kubernetes/apps/flux-system/monitoring/app/kustomization.yaml
@@ -0,0 +1,8 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: flux-system
+resources:
+  - ./podmonitor.yaml
+  - ./prometheusrule.yaml
diff --git a/kubernetes/apps/flux-system/monitoring/app/podmonitor.yaml b/kubernetes/apps/flux-system/monitoring/app/podmonitor.yaml
new file mode 100644
index 0000000000..ece785133f
--- /dev/null
+++ b/kubernetes/apps/flux-system/monitoring/app/podmonitor.yaml
@@ -0,0 +1,31 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/monitoring.coreos.com/podmonitor_v1.json
+apiVersion: monitoring.coreos.com/v1
+kind: PodMonitor
+metadata:
+  name: flux-system
+  labels:
+    app.kubernetes.io/part-of: flux
+    app.kubernetes.io/component: monitoring
+spec:
+  namespaceSelector:
+    matchNames:
+      - flux-system
+  selector:
+    matchExpressions:
+      - key: app
+        operator: In
+        values:
+          - helm-controller
+          - source-controller
+          - kustomize-controller
+          - notification-controller
+          - image-automation-controller
+          - image-reflector-controller
+  podMetricsEndpoints:
+    - port: http-prom
+      relabelings:
+        # Ref: https://github.com/prometheus-operator/prometheus-operator/issues/4816
+        - sourceLabels: [__meta_kubernetes_pod_phase]
+          action: keep
+          regex: Running
diff --git a/kubernetes/apps/flux-system/monitoring/app/prometheusrule.yaml b/kubernetes/apps/flux-system/monitoring/app/prometheusrule.yaml
new file mode 100644
index 0000000000..17af8d62f9
--- /dev/null
+++ b/kubernetes/apps/flux-system/monitoring/app/prometheusrule.yaml
@@ -0,0 +1,31 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/monitoring.coreos.com/prometheusrule_v1.json
+apiVersion: monitoring.coreos.com/v1
+kind: PrometheusRule
+metadata:
+  name: flux-rules
+spec:
+  groups:
+    - name: flux.rules
+      rules:
+        - alert: FluxComponentAbsent
+          annotations:
+            summary: Flux component has disappeared from Prometheus target discovery.
+          expr: |
+            absent(up{job=~".*flux-system.*"} == 1)
+          for: 15m
+          labels:
+            severity: critical
+        - alert: FluxReconciliationFailure
+          annotations:
+            summary: >-
+              {{ $labels.kind }} {{ $labels.namespace }}/{{ $labels.name }} reconciliation
+              has been failing for more than 15 minutes.
+          expr: |
+            max(gotk_reconcile_condition{status="False",type="Ready"}) by (namespace, name, kind)
+              +
+            on(namespace, name, kind) (max(gotk_reconcile_condition{status="Deleted"})
+              by (namespace, name, kind)) * 2 == 1
+          for: 15m
+          labels:
+            severity: critical
diff --git a/kubernetes/apps/flux-system/monitoring/ks.yaml b/kubernetes/apps/flux-system/monitoring/ks.yaml
new file mode 100644
index 0000000000..5bc9dfb256
--- /dev/null
+++ b/kubernetes/apps/flux-system/monitoring/ks.yaml
@@ -0,0 +1,22 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app flux-monitoring
+  namespace: flux-system
+spec:
+  targetNamespace: flux-system
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  dependsOn:
+    - name: prometheus-operator-crds
+  path: ./kubernetes/apps/flux-system/monitoring/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/flux-system/namespace.yaml b/kubernetes/apps/flux-system/namespace.yaml
new file mode 100644
index 0000000000..b48db4521b
--- /dev/null
+++ b/kubernetes/apps/flux-system/namespace.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: flux-system
+  labels:
+    kustomize.toolkit.fluxcd.io/prune: disabled
diff --git a/kubernetes/apps/flux-system/weave-gitops/app/externalsecret.yaml b/kubernetes/apps/flux-system/weave-gitops/app/externalsecret.yaml
new file mode 100644
index 0000000000..f8cff792e3
--- /dev/null
+++ b/kubernetes/apps/flux-system/weave-gitops/app/externalsecret.yaml
@@ -0,0 +1,47 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: weave-gitops-cluster-user-auth
+spec:
+  secretStoreRef:
+    kind: ClusterSecretStore
+    name: onepassword
+  target:
+    name: cluster-user-auth
+    template:
+      engineVersion: v2
+      data:
+        username: "{{ .username }}"
+        password: "{{ .password }}"
+  dataFrom:
+    - extract:
+        key: weave-gitops
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: weave-gitops-oidc-auth
+spec:
+  secretStoreRef:
+    kind: ClusterSecretStore
+    name: onepassword
+  target:
+    name: oidc-auth
+    template:
+      engineVersion: v2
+      data:
+        issuerURL: "{{ .issuerURL }}"
+        clientID: "{{ .clientID }}"
+        clientSecret: "{{ .clientSecret }}"
+        redirectURL: "https://gitops.${SECRET_DOMAIN}/oauth2/callback"
+        customScopes: "{{ .customScopes }}"
+        claimGroups: "{{ .claimGroups }}"
+        claimUsername: "{{ .redireclaimUsernamectURL }}"
+  dataFrom:
+    - extract:
+        key: weave-gitops
+    - extract:
+        key: oidc-general
diff --git a/kubernetes/apps/flux-system/weave-gitops/app/helmrelease.yaml b/kubernetes/apps/flux-system/weave-gitops/app/helmrelease.yaml
new file mode 100644
index 0000000000..20641e7485
--- /dev/null
+++ b/kubernetes/apps/flux-system/weave-gitops/app/helmrelease.yaml
@@ -0,0 +1,71 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: weave-gitops
+  namespace: flux-system
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: weave-gitops
+      version: 4.0.36
+      sourceRef:
+        kind: HelmRepository
+        name: weave-gitops
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    envVars:
+      - name: WEAVE_GITOPS_FEATURE_TENANCY
+        value: "true"
+      - name: WEAVE_GITOPS_FEATURE_CLUSTER
+        value: "true"
+      - name: WEAVE_GITOPS_FEATURE_TELEMETRY
+        value: "true"
+      - name: WEAVE_GITOPS_FEATURE_OIDC_BUTTON_LABEL
+        value: "Login with Homelab Account"
+    additionalArgs:
+      - --auth-methods=oidc
+    adminUser:
+      create: true
+      createSecret: false
+      username: admin
+    ingress:
+      enabled: true
+      className: internal
+      annotations:
+        hajimari.io/icon: sawtooth-wave
+      hosts:
+        - host: &host gitops.${SECRET_DOMAIN}
+          paths:
+            - path: /
+              pathType: Prefix
+      tls:
+        - hosts:
+            - *host
+    networkPolicy:
+      create: false
+    metrics:
+      enabled: true
+    logLevel: info
+    rbac:
+      create: true
+      #impersonationResourceNames: ["tdeutsch"]
+      additionalRules:
+        - apiGroups:
+            - "infra.contrib.fluxcd.io"
+          resources:
+            - "terraforms"
+          verbs:
+            - "get"
+            - "list"
+            - "patch"
+    podAnnotations:
+      secret.reloader.stakater.com/reload: auto
diff --git a/kubernetes/apps/flux-system/weave-gitops/app/kustomization.yaml b/kubernetes/apps/flux-system/weave-gitops/app/kustomization.yaml
new file mode 100644
index 0000000000..0c68d859ca
--- /dev/null
+++ b/kubernetes/apps/flux-system/weave-gitops/app/kustomization.yaml
@@ -0,0 +1,10 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: flux-system
+resources:
+  - ./externalsecret.yaml
+  - ./helmrelease.yaml
+  - ./rbac.yaml
+  - ../../../../shared/gatus/internal
diff --git a/kubernetes/apps/flux-system/weave-gitops/app/rbac.yaml b/kubernetes/apps/flux-system/weave-gitops/app/rbac.yaml
new file mode 100644
index 0000000000..639c8be313
--- /dev/null
+++ b/kubernetes/apps/flux-system/weave-gitops/app/rbac.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: wego-admin-oidc
+subjects:
+  - name: applications_weave_gitops
+    apiGroup: rbac.authorization.k8s.io
+    kind: Group
+roleRef:
+  name: wego-admin-cluster-role
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
diff --git a/kubernetes/apps/flux-system/weave-gitops/ks.yaml b/kubernetes/apps/flux-system/weave-gitops/ks.yaml
new file mode 100644
index 0000000000..f343a764d1
--- /dev/null
+++ b/kubernetes/apps/flux-system/weave-gitops/ks.yaml
@@ -0,0 +1,25 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app weave-gitops
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: flux-system
+  path: ./kubernetes/apps/flux-system/weave-gitops/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false # no flux ks dependents
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
+      GATUS_SUBDOMAIN: gitops
diff --git a/kubernetes/apps/flux-system/webhooks/app/github/ingress.yaml b/kubernetes/apps/flux-system/webhooks/app/github/ingress.yaml
new file mode 100644
index 0000000000..e20604f046
--- /dev/null
+++ b/kubernetes/apps/flux-system/webhooks/app/github/ingress.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: flux-webhook
+  annotations:
+    external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+spec:
+  ingressClassName: external
+  rules:
+    - host: "flux-webhook.${SECRET_DOMAIN}"
+      http:
+        paths:
+          - path: /hook/
+            pathType: Prefix
+            backend:
+              service:
+                name: webhook-receiver
+                port:
+                  number: 80
diff --git a/kubernetes/apps/flux-system/webhooks/app/github/kustomization.yaml b/kubernetes/apps/flux-system/webhooks/app/github/kustomization.yaml
new file mode 100644
index 0000000000..786e654a56
--- /dev/null
+++ b/kubernetes/apps/flux-system/webhooks/app/github/kustomization.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./secret.sops.yaml
+  - ./ingress.yaml
+  - ./receiver.yaml
diff --git a/kubernetes/apps/flux-system/webhooks/app/github/receiver.yaml b/kubernetes/apps/flux-system/webhooks/app/github/receiver.yaml
new file mode 100644
index 0000000000..d178f1c3c3
--- /dev/null
+++ b/kubernetes/apps/flux-system/webhooks/app/github/receiver.yaml
@@ -0,0 +1,25 @@
+---
+apiVersion: notification.toolkit.fluxcd.io/v1
+kind: Receiver
+metadata:
+  name: github-receiver
+spec:
+  type: github
+  events:
+    - ping
+    - push
+  secretRef:
+    name: github-webhook-token-secret
+  resources:
+    - apiVersion: source.toolkit.fluxcd.io/v1
+      kind: GitRepository
+      name: k8s-homelab
+      namespace: flux-system
+    - apiVersion: kustomize.toolkit.fluxcd.io/v1
+      kind: Kustomization
+      name: cluster
+      namespace: flux-system
+    - apiVersion: kustomize.toolkit.fluxcd.io/v1
+      kind: Kustomization
+      name: cluster-apps
+      namespace: flux-system
diff --git a/kubernetes/apps/flux-system/webhooks/app/github/secret.sops.yaml b/kubernetes/apps/flux-system/webhooks/app/github/secret.sops.yaml
new file mode 100644
index 0000000000..a2487d3330
--- /dev/null
+++ b/kubernetes/apps/flux-system/webhooks/app/github/secret.sops.yaml
@@ -0,0 +1,26 @@
+apiVersion: v1
+kind: Secret
+metadata:
+    name: github-webhook-token-secret
+stringData:
+    token: ENC[AES256_GCM,data:jX/2BxoBAn+g2qpkqi0AXwGmhWnS3mfbLVbDfqkFKys=,iv:tFLobk2nZSTkVQubJk+RmYa4k05PBPG3RkRxPXvivxQ=,tag:o66TIrLrd44AXL9ESIyhNA==,type:str]
+sops:
+    kms: []
+    gcp_kms: []
+    azure_kv: []
+    hc_vault: []
+    age:
+        - recipient: age1y0kzuf0tn94a74whazwae4r9qal4snuqfuhl5jacscrpr7up5gts74fe5w
+          enc: |
+            -----BEGIN AGE ENCRYPTED FILE-----
+            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqaFN5SUF5WS9JQWppYzcx
+            SitkVnZ5Y0w3ZkIyYk9xUHdOZ2VMWjFFOVRzCkJDekczN0ZJWFpKdXdZbDZRVUU4
+            OWY2UW1YT1I0YXkvWkFOOElzVWkrZmcKLS0tIDhkTk5QNGs4clF2UkRKTE5zNWZS
+            T1p3NWlkUkRLZ2Z5OXlQb1ozcU0xWkUKT0zzBvr354akJnLBW/Bgh4j+KanLHjfJ
+            blq7yWE9pVHmCDaT4LFTktfAkAjP7DiN/ZilN74vY5zs/KSIH8ELtg==
+            -----END AGE ENCRYPTED FILE-----
+    lastmodified: "2024-06-06T09:08:40Z"
+    mac: ENC[AES256_GCM,data:i1/KO5tRck/N2ZCeFrf14h6NWdnUxX70IMHYY8dmz5ZVHF6iugpjucZdDvy3vTF73aMohMUFsxgOkll/iFcHJ674O4yfM/V1Cx84fzrAaGEfPuoya/IKvidYkGcVJ71VWVTKB+QlPpoPfavAPdP+V6XAoZ4ikfQTfJzZ1IfT73Q=,iv:G5aaEuVt3TlSFwjSlwQLhg6w+TekNpspPd0PJsMfpHI=,tag:W4pVeayUmYx8Wi285gVoeg==,type:str]
+    pgp: []
+    encrypted_regex: ^(data|stringData)$
+    version: 3.8.1
diff --git a/kubernetes/apps/flux-system/webhooks/app/kustomization.yaml b/kubernetes/apps/flux-system/webhooks/app/kustomization.yaml
new file mode 100644
index 0000000000..ccd8b3eb8d
--- /dev/null
+++ b/kubernetes/apps/flux-system/webhooks/app/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./github
diff --git a/kubernetes/apps/flux-system/webhooks/ks.yaml b/kubernetes/apps/flux-system/webhooks/ks.yaml
new file mode 100644
index 0000000000..afa7b0e849
--- /dev/null
+++ b/kubernetes/apps/flux-system/webhooks/ks.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app flux-webhooks
+  namespace: flux-system
+spec:
+  targetNamespace: flux-system
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/flux-system/webhooks/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/kube-system/cilium/app/helm-values.yaml b/kubernetes/apps/kube-system/cilium/app/helm-values.yaml
new file mode 100644
index 0000000000..cadf78a6d9
--- /dev/null
+++ b/kubernetes/apps/kube-system/cilium/app/helm-values.yaml
@@ -0,0 +1,57 @@
+---
+autoDirectNodeRoutes: true
+bpf:
+  masquerade: false
+cgroup:
+  automount:
+    enabled: false
+  hostRoot: /sys/fs/cgroup
+cluster:
+  id: 1
+  name: talos-test
+cni:
+  exclusive: false
+containerRuntime:
+  integration: containerd
+# NOTE: devices might need to be set if you have more than one active NIC on your hosts
+# devices: eno+ eth+
+endpointRoutes:
+  enabled: true
+hubble:
+  enabled: false
+ipam:
+  mode: kubernetes
+ipv4NativeRoutingCIDR: 10.69.0.0/16
+k8sServiceHost: 127.0.0.1
+k8sServicePort: 7445
+kubeProxyReplacement: true
+kubeProxyReplacementHealthzBindAddr: 0.0.0.0:10256
+l2announcements:
+  enabled: true
+loadBalancer:
+  algorithm: maglev
+  mode: snat
+localRedirectPolicy: true
+operator:
+  replicas: 1
+  rollOutPods: true
+rollOutCiliumPods: true
+routingMode: native
+securityContext:
+  capabilities:
+    ciliumAgent:
+      - CHOWN
+      - KILL
+      - NET_ADMIN
+      - NET_RAW
+      - IPC_LOCK
+      - SYS_ADMIN
+      - SYS_RESOURCE
+      - DAC_OVERRIDE
+      - FOWNER
+      - SETGID
+      - SETUID
+    cleanCiliumState:
+      - NET_ADMIN
+      - SYS_ADMIN
+      - SYS_RESOURCE
diff --git a/kubernetes/apps/kube-system/cilium/app/helmrelease.yaml b/kubernetes/apps/kube-system/cilium/app/helmrelease.yaml
new file mode 100644
index 0000000000..d2cd26095b
--- /dev/null
+++ b/kubernetes/apps/kube-system/cilium/app/helmrelease.yaml
@@ -0,0 +1,74 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: cilium
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: cilium
+      version: 1.15.5
+      sourceRef:
+        kind: HelmRepository
+        name: cilium
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  valuesFrom:
+    - kind: ConfigMap
+      name: cilium-helm-values
+  values:
+    hubble:
+      enabled: true
+      metrics:
+        enabled:
+          - dns:query
+          - drop
+          - tcp
+          - flow
+          - port-distribution
+          - icmp
+          - http
+        serviceMonitor:
+          enabled: true
+        dashboards:
+          enabled: true
+          annotations:
+            grafana_folder: Cilium
+      relay:
+        enabled: true
+        rollOutPods: true
+        prometheus:
+          serviceMonitor:
+            enabled: true
+      ui:
+        enabled: true
+        rollOutPods: true
+        ingress:
+          enabled: true
+          className: internal
+          hosts: ["hubble.${SECRET_DOMAIN}"]
+    operator:
+      prometheus:
+        enabled: true
+        serviceMonitor:
+          enabled: true
+      dashboards:
+        enabled: true
+        annotations:
+          grafana_folder: Cilium
+    prometheus:
+      enabled: true
+      serviceMonitor:
+        enabled: true
+        trustCRDsExist: true
+    dashboards:
+      enabled: true
+      annotations:
+        grafana_folder: Cilium
diff --git a/kubernetes/apps/kube-system/cilium/app/kustomization.yaml b/kubernetes/apps/kube-system/cilium/app/kustomization.yaml
new file mode 100644
index 0000000000..80f880d7d1
--- /dev/null
+++ b/kubernetes/apps/kube-system/cilium/app/kustomization.yaml
@@ -0,0 +1,12 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
+  - ../../../../shared/gatus/internal
+configMapGenerator:
+  - name: cilium-helm-values
+    files:
+      - values.yaml=./helm-values.yaml
+configurations:
+  - kustomizeconfig.yaml
diff --git a/kubernetes/apps/kube-system/cilium/app/kustomizeconfig.yaml b/kubernetes/apps/kube-system/cilium/app/kustomizeconfig.yaml
new file mode 100644
index 0000000000..58f92ba153
--- /dev/null
+++ b/kubernetes/apps/kube-system/cilium/app/kustomizeconfig.yaml
@@ -0,0 +1,7 @@
+---
+nameReference:
+  - kind: ConfigMap
+    version: v1
+    fieldSpecs:
+      - path: spec/valuesFrom/name
+        kind: HelmRelease
diff --git a/kubernetes/apps/kube-system/cilium/config/cilium-l2.yaml b/kubernetes/apps/kube-system/cilium/config/cilium-l2.yaml
new file mode 100644
index 0000000000..8d8e669226
--- /dev/null
+++ b/kubernetes/apps/kube-system/cilium/config/cilium-l2.yaml
@@ -0,0 +1,24 @@
+---
+# https://docs.cilium.io/en/latest/network/l2-announcements
+apiVersion: cilium.io/v2alpha1
+kind: CiliumL2AnnouncementPolicy
+metadata:
+  name: l2-policy
+spec:
+  loadBalancerIPs: true
+  # NOTE: interfaces might need to be set if you have more than one active NIC on your hosts
+  # interfaces:
+  #   - ^eno[0-9]+
+  #   - ^eth[0-9]+
+  nodeSelector:
+    matchLabels:
+      kubernetes.io/os: linux
+---
+apiVersion: cilium.io/v2alpha1
+kind: CiliumLoadBalancerIPPool
+metadata:
+  name: l2-pool
+spec:
+  allowFirstLastIPs: "Yes"
+  blocks:
+    - cidr: "192.168.13.64/27" # Network range 192.168.13.64 - 192.168.13.95 / Usable range 192.168.13.65 - 192.168.13.94
diff --git a/kubernetes/apps/kube-system/cilium/config/kustomization.yaml b/kubernetes/apps/kube-system/cilium/config/kustomization.yaml
new file mode 100644
index 0000000000..f689965383
--- /dev/null
+++ b/kubernetes/apps/kube-system/cilium/config/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./cilium-l2.yaml
diff --git a/kubernetes/apps/kube-system/cilium/ks.yaml b/kubernetes/apps/kube-system/cilium/ks.yaml
new file mode 100644
index 0000000000..b5191fccb9
--- /dev/null
+++ b/kubernetes/apps/kube-system/cilium/ks.yaml
@@ -0,0 +1,46 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app cilium
+  namespace: flux-system
+spec:
+  targetNamespace: kube-system
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/kube-system/cilium/app
+  prune: false # never should be deleted
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
+      GATUS_SUBDOMAIN: hubble
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app cilium-config
+  namespace: flux-system
+spec:
+  targetNamespace: kube-system
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  dependsOn:
+    - name: cilium
+  path: ./kubernetes/apps/kube-system/cilium/config
+  prune: false # never should be deleted
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/kube-system/coredns/app/helm-values.yaml b/kubernetes/apps/kube-system/coredns/app/helm-values.yaml
new file mode 100644
index 0000000000..22da029869
--- /dev/null
+++ b/kubernetes/apps/kube-system/coredns/app/helm-values.yaml
@@ -0,0 +1,50 @@
+---
+fullnameOverride: coredns
+k8sAppLabelOverride: kube-dns
+serviceAccount:
+  create: true
+service:
+  name: kube-dns
+  clusterIP: "10.96.0.10"
+servers:
+  - zones:
+      - zone: .
+        scheme: dns://
+        use_tcp: true
+    port: 53
+    plugins:
+      - name: errors
+      - name: health
+        configBlock: |-
+          lameduck 5s
+      - name: ready
+      - name: log
+        configBlock: |-
+          class error
+      - name: prometheus
+        parameters: 0.0.0.0:9153
+      - name: kubernetes
+        parameters: cluster.local in-addr.arpa ip6.arpa
+        configBlock: |-
+          pods insecure
+          fallthrough in-addr.arpa ip6.arpa
+      - name: forward
+        parameters: . /etc/resolv.conf
+      - name: cache
+        parameters: 30
+      - name: loop
+      - name: reload
+      - name: loadbalance
+affinity:
+  nodeAffinity:
+    requiredDuringSchedulingIgnoredDuringExecution:
+      nodeSelectorTerms:
+        - matchExpressions:
+            - key: node-role.kubernetes.io/control-plane
+              operator: Exists
+tolerations:
+  - key: CriticalAddonsOnly
+    operator: Exists
+  - key: node-role.kubernetes.io/control-plane
+    operator: Exists
+    effect: NoSchedule
diff --git a/kubernetes/apps/kube-system/coredns/app/helmrelease.yaml b/kubernetes/apps/kube-system/coredns/app/helmrelease.yaml
new file mode 100644
index 0000000000..eb6cd2f9a1
--- /dev/null
+++ b/kubernetes/apps/kube-system/coredns/app/helmrelease.yaml
@@ -0,0 +1,26 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: coredns
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: coredns
+      version: 1.30.0
+      sourceRef:
+        kind: HelmRepository
+        name: coredns
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      strategy: rollback
+      retries: 3
+  valuesFrom:
+    - kind: ConfigMap
+      name: coredns-helm-values
diff --git a/kubernetes/apps/kube-system/coredns/app/kustomization.yaml b/kubernetes/apps/kube-system/coredns/app/kustomization.yaml
new file mode 100644
index 0000000000..691355b567
--- /dev/null
+++ b/kubernetes/apps/kube-system/coredns/app/kustomization.yaml
@@ -0,0 +1,11 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
+configMapGenerator:
+  - name: coredns-helm-values
+    files:
+      - values.yaml=./helm-values.yaml
+configurations:
+  - kustomizeconfig.yaml
diff --git a/kubernetes/apps/kube-system/coredns/app/kustomizeconfig.yaml b/kubernetes/apps/kube-system/coredns/app/kustomizeconfig.yaml
new file mode 100644
index 0000000000..58f92ba153
--- /dev/null
+++ b/kubernetes/apps/kube-system/coredns/app/kustomizeconfig.yaml
@@ -0,0 +1,7 @@
+---
+nameReference:
+  - kind: ConfigMap
+    version: v1
+    fieldSpecs:
+      - path: spec/valuesFrom/name
+        kind: HelmRelease
diff --git a/kubernetes/apps/kube-system/coredns/ks.yaml b/kubernetes/apps/kube-system/coredns/ks.yaml
new file mode 100644
index 0000000000..cec4bfef07
--- /dev/null
+++ b/kubernetes/apps/kube-system/coredns/ks.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app coredns
+  namespace: flux-system
+spec:
+  targetNamespace: kube-system
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/kube-system/coredns/app
+  prune: false # never should be deleted
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/kube-system/kubelet-csr-approver/app/helm-values.yaml b/kubernetes/apps/kube-system/kubelet-csr-approver/app/helm-values.yaml
new file mode 100644
index 0000000000..67bd17346c
--- /dev/null
+++ b/kubernetes/apps/kube-system/kubelet-csr-approver/app/helm-values.yaml
@@ -0,0 +1,3 @@
+---
+providerRegex: ^(talos-test01|talos-test02|talos-test03|talos-test04)$
+bypassDnsResolution: true
diff --git a/kubernetes/apps/kube-system/kubelet-csr-approver/app/helmrelease.yaml b/kubernetes/apps/kube-system/kubelet-csr-approver/app/helmrelease.yaml
new file mode 100644
index 0000000000..0c14465d22
--- /dev/null
+++ b/kubernetes/apps/kube-system/kubelet-csr-approver/app/helmrelease.yaml
@@ -0,0 +1,30 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: kubelet-csr-approver
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: kubelet-csr-approver
+      version: 1.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: postfinance
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  valuesFrom:
+    - kind: ConfigMap
+      name: kubelet-csr-approver-helm-values
+  values:
+    metrics:
+      enable: true
+      serviceMonitor:
+        enabled: true
diff --git a/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomization.yaml b/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomization.yaml
new file mode 100644
index 0000000000..30dddafcba
--- /dev/null
+++ b/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomization.yaml
@@ -0,0 +1,11 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
+configMapGenerator:
+  - name: kubelet-csr-approver-helm-values
+    files:
+      - values.yaml=./helm-values.yaml
+configurations:
+  - kustomizeconfig.yaml
diff --git a/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomizeconfig.yaml b/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomizeconfig.yaml
new file mode 100644
index 0000000000..58f92ba153
--- /dev/null
+++ b/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomizeconfig.yaml
@@ -0,0 +1,7 @@
+---
+nameReference:
+  - kind: ConfigMap
+    version: v1
+    fieldSpecs:
+      - path: spec/valuesFrom/name
+        kind: HelmRelease
diff --git a/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml b/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml
new file mode 100644
index 0000000000..936bac6450
--- /dev/null
+++ b/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app kubelet-csr-approver
+  namespace: flux-system
+spec:
+  targetNamespace: kube-system
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/kube-system/kubelet-csr-approver/app
+  prune: false # never should be deleted
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/kube-system/kubernetes-replicator/app/helmrelease.yaml b/kubernetes/apps/kube-system/kubernetes-replicator/app/helmrelease.yaml
new file mode 100644
index 0000000000..408f7b237d
--- /dev/null
+++ b/kubernetes/apps/kube-system/kubernetes-replicator/app/helmrelease.yaml
@@ -0,0 +1,23 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: &app kubernetes-replicator
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: *app
+      version: 2.9.2
+      sourceRef:
+        kind: HelmRepository
+        name: mittwald-charts
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
diff --git a/kubernetes/apps/kube-system/kubernetes-replicator/app/kustomization.yaml b/kubernetes/apps/kube-system/kubernetes-replicator/app/kustomization.yaml
new file mode 100644
index 0000000000..5dd7baca73
--- /dev/null
+++ b/kubernetes/apps/kube-system/kubernetes-replicator/app/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
diff --git a/kubernetes/apps/kube-system/kubernetes-replicator/ks.yaml b/kubernetes/apps/kube-system/kubernetes-replicator/ks.yaml
new file mode 100644
index 0000000000..56c75fd694
--- /dev/null
+++ b/kubernetes/apps/kube-system/kubernetes-replicator/ks.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app kubernetes-replicator
+  namespace: flux-system
+spec:
+  targetNamespace: kube-system
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/kube-system/kubernetes-replicator/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/kube-system/kustomization.yaml b/kubernetes/apps/kube-system/kustomization.yaml
new file mode 100644
index 0000000000..d3a8064261
--- /dev/null
+++ b/kubernetes/apps/kube-system/kustomization.yaml
@@ -0,0 +1,12 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - namespace.yaml
+  - cilium/ks.yaml
+  - coredns/ks.yaml
+  - kubelet-csr-approver/ks.yaml
+  - kubernetes-replicator/ks.yaml
+  - metrics-server/ks.yaml
+  - reloader/ks.yaml
+  - spegel/ks.yaml
diff --git a/kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml b/kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml
new file mode 100644
index 0000000000..60298df669
--- /dev/null
+++ b/kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml
@@ -0,0 +1,31 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: metrics-server
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: metrics-server
+      version: 3.12.1
+      sourceRef:
+        kind: HelmRepository
+        name: metrics-server
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    args:
+      - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
+      - --kubelet-use-node-status-port
+      - --metric-resolution=15s
+    metrics:
+      enabled: true
+    serviceMonitor:
+      enabled: true
diff --git a/kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml b/kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml
new file mode 100644
index 0000000000..5dd7baca73
--- /dev/null
+++ b/kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
diff --git a/kubernetes/apps/kube-system/metrics-server/ks.yaml b/kubernetes/apps/kube-system/metrics-server/ks.yaml
new file mode 100644
index 0000000000..090b2ae731
--- /dev/null
+++ b/kubernetes/apps/kube-system/metrics-server/ks.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app metrics-server
+  namespace: flux-system
+spec:
+  targetNamespace: kube-system
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/kube-system/metrics-server/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/kube-system/namespace.yaml b/kubernetes/apps/kube-system/namespace.yaml
new file mode 100644
index 0000000000..5eeb2c9183
--- /dev/null
+++ b/kubernetes/apps/kube-system/namespace.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: kube-system
+  labels:
+    kustomize.toolkit.fluxcd.io/prune: disabled
diff --git a/kubernetes/apps/kube-system/reloader/app/helmrelease.yaml b/kubernetes/apps/kube-system/reloader/app/helmrelease.yaml
new file mode 100644
index 0000000000..2b63a3b137
--- /dev/null
+++ b/kubernetes/apps/kube-system/reloader/app/helmrelease.yaml
@@ -0,0 +1,29 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: reloader
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: reloader
+      version: 1.0.101
+      sourceRef:
+        kind: HelmRepository
+        name: stakater
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    fullnameOverride: reloader
+    reloader:
+      readOnlyRootFileSystem: true
+      podMonitor:
+        enabled: true
+        namespace: "{{ .Release.Namespace }}"
diff --git a/kubernetes/apps/kube-system/reloader/app/kustomization.yaml b/kubernetes/apps/kube-system/reloader/app/kustomization.yaml
new file mode 100644
index 0000000000..5dd7baca73
--- /dev/null
+++ b/kubernetes/apps/kube-system/reloader/app/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
diff --git a/kubernetes/apps/kube-system/reloader/ks.yaml b/kubernetes/apps/kube-system/reloader/ks.yaml
new file mode 100644
index 0000000000..6f9458dc13
--- /dev/null
+++ b/kubernetes/apps/kube-system/reloader/ks.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app reloader
+  namespace: flux-system
+spec:
+  targetNamespace: kube-system
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/kube-system/reloader/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/kube-system/spegel/app/helm-values.yaml b/kubernetes/apps/kube-system/spegel/app/helm-values.yaml
new file mode 100644
index 0000000000..a4185ae368
--- /dev/null
+++ b/kubernetes/apps/kube-system/spegel/app/helm-values.yaml
@@ -0,0 +1,7 @@
+---
+spegel:
+  containerdSock: /run/containerd/containerd.sock
+  containerdRegistryConfigPath: /etc/cri/conf.d/hosts
+service:
+  registry:
+    hostPort: 29999
diff --git a/kubernetes/apps/kube-system/spegel/app/helmrelease.yaml b/kubernetes/apps/kube-system/spegel/app/helmrelease.yaml
new file mode 100644
index 0000000000..4200fa89b7
--- /dev/null
+++ b/kubernetes/apps/kube-system/spegel/app/helmrelease.yaml
@@ -0,0 +1,28 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: spegel
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: spegel
+      version: v0.0.22
+      sourceRef:
+        kind: HelmRepository
+        name: spegel
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  valuesFrom:
+    - kind: ConfigMap
+      name: spegel-helm-values
+  values:
+    serviceMonitor:
+      enabled: true
diff --git a/kubernetes/apps/kube-system/spegel/app/kustomization.yaml b/kubernetes/apps/kube-system/spegel/app/kustomization.yaml
new file mode 100644
index 0000000000..1e1aa1d17c
--- /dev/null
+++ b/kubernetes/apps/kube-system/spegel/app/kustomization.yaml
@@ -0,0 +1,11 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
+configMapGenerator:
+  - name: spegel-helm-values
+    files:
+      - values.yaml=./helm-values.yaml
+configurations:
+  - kustomizeconfig.yaml
diff --git a/kubernetes/apps/kube-system/spegel/app/kustomizeconfig.yaml b/kubernetes/apps/kube-system/spegel/app/kustomizeconfig.yaml
new file mode 100644
index 0000000000..58f92ba153
--- /dev/null
+++ b/kubernetes/apps/kube-system/spegel/app/kustomizeconfig.yaml
@@ -0,0 +1,7 @@
+---
+nameReference:
+  - kind: ConfigMap
+    version: v1
+    fieldSpecs:
+      - path: spec/valuesFrom/name
+        kind: HelmRelease
diff --git a/kubernetes/apps/kube-system/spegel/ks.yaml b/kubernetes/apps/kube-system/spegel/ks.yaml
new file mode 100644
index 0000000000..cb9f37ed47
--- /dev/null
+++ b/kubernetes/apps/kube-system/spegel/ks.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app spegel
+  namespace: flux-system
+spec:
+  targetNamespace: kube-system
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/kube-system/spegel/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/media/calibre-web/app/helmrelease.yaml b/kubernetes/apps/media/calibre-web/app/helmrelease.yaml
new file mode 100644
index 0000000000..28644c0c1a
--- /dev/null
+++ b/kubernetes/apps/media/calibre-web/app/helmrelease.yaml
@@ -0,0 +1,107 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: &app calibre-web
+  namespace: media
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    global:
+      nameOverride: *app
+    controllers:
+      app:
+        strategy: Recreate
+        annotations:
+          reloader.stakater.com/auto: "true"
+        containers:
+          main:
+            image:
+              repository: ghcr.io/linuxserver/calibre-web
+              tag: 0.6.20-ls229
+            env:
+              TZ: ${TIMEZONE}
+              PUID: 1000
+              PGID: 1000
+              DOCKER_MODS: linuxserver/mods:universal-calibre
+              CACHE_DIR: /cache
+            resources:
+              requests:
+                cpu: 5m
+                memory: 100Mi
+              limits:
+                memory: 500Mi
+        initContainers:
+          update-volume-permission:
+            image:
+              repository: busybox
+              tag: 1.36.1
+            command: [sh, -c, chown -R 1000:1000 /config]
+            securityContext:
+              runAsUser: 0
+    service:
+      app:
+        controller: app
+        ports:
+          http:
+            port: &port 8083
+    ingress:
+      app:
+        enabled: true
+        className: internal
+        annotations:
+          # nginx.ingress.kubernetes.io/auth-method: GET
+          # nginx.ingress.kubernetes.io/auth-url: https://auth.${SECRET_DOMAIN}/api/verify
+          # nginx.ingress.kubernetes.io/auth-signin: https://auth.${SECRET_DOMAIN}?rm=$request_method
+          # nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
+          # nginx.ingress.kubernetes.io/auth-snippet: |
+          #  proxy_set_header X-Forwarded-Method $request_method;
+          #  proxy_set_header X-Forwarded-Scheme $scheme;
+          hajimari.io/icon: bookshelf
+        hosts:
+          - host: &host books.${SECRET_DOMAIN}
+            paths:
+              - path: /
+                pathType: Prefix
+                service:
+                  identifier: app
+                  port: *port
+        tls:
+          - hosts:
+              - *host
+    persistence:
+      config:
+        enabled: true
+        type: persistentVolumeClaim
+        accessMode: ReadWriteOnce
+        size: 5Gi
+        storageClass: ${MAIN_SC}
+        globalMounts:
+          - path: /config
+      data:
+        enabled: true
+        type: nfs
+        server: 10.20.30.40
+        path: /volume2/data
+        globalMounts:
+          - path: /data
+            readOnly: true
+      cache:
+        type: emptyDir
+        globalMounts:
+          - path: /cache
diff --git a/kubernetes/apps/media/calibre-web/app/kustomization.yaml b/kubernetes/apps/media/calibre-web/app/kustomization.yaml
new file mode 100644
index 0000000000..f6472ee848
--- /dev/null
+++ b/kubernetes/apps/media/calibre-web/app/kustomization.yaml
@@ -0,0 +1,8 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: media
+resources:
+  - ./helmrelease.yaml
+  - ../../../../shared/gatus/internal
diff --git a/kubernetes/apps/media/calibre-web/ks.yaml b/kubernetes/apps/media/calibre-web/ks.yaml
new file mode 100644
index 0000000000..f6706fa070
--- /dev/null
+++ b/kubernetes/apps/media/calibre-web/ks.yaml
@@ -0,0 +1,24 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app calibre-web
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: media
+  path: ./kubernetes/apps/media/calibre-web/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false # no flux ks dependents
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
diff --git a/kubernetes/apps/media/kustomization.yaml b/kubernetes/apps/media/kustomization.yaml
new file mode 100644
index 0000000000..8f22b6e046
--- /dev/null
+++ b/kubernetes/apps/media/kustomization.yaml
@@ -0,0 +1,10 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - namespace.yaml
+  - calibre-web/ks.yaml
+  - mediabox/ks.yaml
+  - plex-exporter/ks.yaml
+  - plex-trakt-sync/ks.yaml
+  - tautulli/ks.yaml
diff --git a/kubernetes/apps/media/mediabox/app/bazarr-ingress.yaml b/kubernetes/apps/media/mediabox/app/bazarr-ingress.yaml
new file mode 100755
index 0000000000..713084a645
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/app/bazarr-ingress.yaml
@@ -0,0 +1,54 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: bazarr
+  annotations:
+    external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+    nginx.ingress.kubernetes.io/auth-method: GET
+    nginx.ingress.kubernetes.io/auth-url: https://auth.${SECRET_DOMAIN}/api/verify
+    nginx.ingress.kubernetes.io/auth-signin: https://auth.${SECRET_DOMAIN}?rm=$request_method
+    nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
+    nginx.ingress.kubernetes.io/auth-snippet: |
+      proxy_set_header X-Forwarded-Method $request_method;
+      proxy_set_header X-Forwarded-Scheme $scheme;
+    hajimari.io/enable: "true"
+    hajimari.io/icon: chart-bar
+spec:
+  ingressClassName: external
+  tls:
+    - secretName: ${SECRET_DOMAIN/./-}-production-tls
+      hosts:
+        - bazarr.${SECRET_DOMAIN}
+  rules:
+    - host: bazarr.${SECRET_DOMAIN}
+      http:
+        paths:
+          - path: /
+            pathType: Prefix
+            backend:
+              service:
+                name: mediabox
+                port:
+                  name: bazarr
+---
+kind: ConfigMap
+metadata:
+  labels:
+    app.kubernetes.io/name: bazarr
+    gatus.io/enabled: "true"
+  name: bazarr-gatus-ep
+apiVersion: v1
+data:
+  config.yaml: |
+    endpoints:
+      - name: "bazarr"
+        group: external-kubernetes
+        url: "https://bazarr.${SECRET_DOMAIN}/"
+        interval: 1m
+        client:
+          dns-resolver: tcp://1.1.1.1:53
+        conditions:
+          - "[STATUS] == 200"
+        alerts:
+          - type: discord
diff --git a/kubernetes/apps/media/mediabox/app/gaps-ingress.yaml b/kubernetes/apps/media/mediabox/app/gaps-ingress.yaml
new file mode 100755
index 0000000000..1bb382e748
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/app/gaps-ingress.yaml
@@ -0,0 +1,55 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: gaps
+  annotations:
+    external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+    nginx.ingress.kubernetes.io/auth-method: GET
+    nginx.ingress.kubernetes.io/auth-url: https://auth.${SECRET_DOMAIN}/api/verify
+    nginx.ingress.kubernetes.io/auth-signin: https://auth.${SECRET_DOMAIN}?rm=$request_method
+    nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
+    nginx.ingress.kubernetes.io/auth-snippet: |
+      proxy_set_header X-Forwarded-Method $request_method;
+      proxy_set_header X-Forwarded-Scheme $scheme;
+    hajimari.io/enable: "true"
+    hajimari.io/icon: filmstrip
+spec:
+  ingressClassName: external
+  tls:
+    - secretName: ${SECRET_DOMAIN/./-}-production-tls
+      hosts:
+        - gaps.${SECRET_DOMAIN}
+  rules:
+    - host: gaps.${SECRET_DOMAIN}
+      http:
+        paths:
+          - path: /
+            pathType: Prefix
+            backend:
+              service:
+                name: mediabox
+                port:
+                  name: gaps
+
+---
+kind: ConfigMap
+metadata:
+  labels:
+    app.kubernetes.io/name: gaps
+    gatus.io/enabled: "true"
+  name: gaps-gatus-ep
+apiVersion: v1
+data:
+  config.yaml: |
+    endpoints:
+      - name: "gaps"
+        group: external-kubernetes
+        url: "https://gaps.${SECRET_DOMAIN}/"
+        interval: 1m
+        client:
+          dns-resolver: tcp://1.1.1.1:53
+        conditions:
+          - "[STATUS] == 200"
+        alerts:
+          - type: discord
diff --git a/kubernetes/apps/media/mediabox/app/kustomization.yaml b/kubernetes/apps/media/mediabox/app/kustomization.yaml
new file mode 100755
index 0000000000..57b6540232
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/app/kustomization.yaml
@@ -0,0 +1,18 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: media
+resources:
+  - bazarr-ingress.yaml
+  - gaps-ingress.yaml
+  - lldap-ingress.yaml
+  - sabnzbd-ingress.yaml
+  - prowlarr-ingress.yaml
+  - radarr-ingress.yaml
+  - service.yaml
+  - sonarr-ingress.yaml
+  - notifiarr-ingress.yaml
+  - radarr-exporter.yaml
+  - sonarr-exporter.yaml
+  - prowlarr-exporter.yaml
diff --git a/kubernetes/apps/media/mediabox/app/lldap-ingress.yaml b/kubernetes/apps/media/mediabox/app/lldap-ingress.yaml
new file mode 100644
index 0000000000..ed51d8bf5d
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/app/lldap-ingress.yaml
@@ -0,0 +1,47 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: lldap
+  annotations:
+    hajimari.io/enable: "true"
+    hajimari.io/icon: account-group
+spec:
+  ingressClassName: internal
+  tls:
+    - secretName: ${SECRET_DOMAIN/./-}-production-tls
+      hosts:
+        - lldap.${SECRET_DOMAIN}
+  rules:
+    - host: lldap.${SECRET_DOMAIN}
+      http:
+        paths:
+          - path: /
+            pathType: Prefix
+            backend:
+              service:
+                name: mediabox
+                port:
+                  name: lldap
+
+---
+kind: ConfigMap
+metadata:
+  labels:
+    app.kubernetes.io/name: lldap
+    gatus.io/enabled: "true"
+  name: lldap-gatus-ep
+apiVersion: v1
+data:
+  config.yaml: |
+    endpoints:
+      - name: "lldap"
+        group: internal-kubernetes
+        url: "https://lldap.${SECRET_DOMAIN}/"
+        interval: 1m
+        client:
+          dns-resolver: tcp://192.168.13.1:53
+        conditions:
+          - "[STATUS] == 200"
+        alerts:
+          - type: discord
diff --git a/kubernetes/apps/media/mediabox/app/notifiarr-ingress.yaml b/kubernetes/apps/media/mediabox/app/notifiarr-ingress.yaml
new file mode 100755
index 0000000000..9135547c59
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/app/notifiarr-ingress.yaml
@@ -0,0 +1,48 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: notifiarr
+  annotations:
+    external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+    hajimari.io/enable: "true"
+    hajimari.io/icon: filmstrip
+spec:
+  ingressClassName: external
+  tls:
+    - secretName: ${SECRET_DOMAIN/./-}-production-tls
+      hosts:
+        - notifiarr.${SECRET_DOMAIN}
+  rules:
+    - host: notifiarr.${SECRET_DOMAIN}
+      http:
+        paths:
+          - path: /
+            pathType: Prefix
+            backend:
+              service:
+                name: mediabox
+                port:
+                  name: notifiarr
+
+---
+kind: ConfigMap
+metadata:
+  labels:
+    app.kubernetes.io/name: notifiarr
+    gatus.io/enabled: "true"
+  name: notifiarr-gatus-ep
+apiVersion: v1
+data:
+  config.yaml: |
+    endpoints:
+      - name: "notifiarr"
+        group: external-kubernetes
+        url: "https://notifiarr.${SECRET_DOMAIN}/"
+        interval: 1m
+        client:
+          dns-resolver: tcp://1.1.1.1:53
+        conditions:
+          - "[STATUS] == 200"
+        alerts:
+          - type: discord
diff --git a/kubernetes/apps/media/mediabox/app/prowlarr-exporter.yaml b/kubernetes/apps/media/mediabox/app/prowlarr-exporter.yaml
new file mode 100644
index 0000000000..6fac16ba15
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/app/prowlarr-exporter.yaml
@@ -0,0 +1,884 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: prowlarr-exporter
+  namespace: media
+  labels:
+    app.kubernetes.io/name: prowlarr-exporter
+    app.kubernetes.io/instance: prowlarr-exporter
+spec:
+  clusterIP: None
+  selector:
+    app.kubernetes.io/name: prowlarr-exporter
+    app.kubernetes.io/instance: prowlarr-exporter
+  ports:
+    - name: monitoring
+      port: 9707
+---
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+  name: prowlarr-exporter
+  namespace: media
+  labels:
+    app.kubernetes.io/name: prowlarr-exporter
+    app.kubernetes.io/instance: prowlarr-exporter
+spec:
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: prowlarr-exporter
+      app.kubernetes.io/instance: prowlarr-exporter
+  endpoints:
+    - port: monitoring
+      interval: 4m
+      scrapeTimeout: 90s
+      path: /metrics
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+  name: prowlarr-exporter
+  namespace: media
+  labels:
+    app.kubernetes.io/name: prowlarr-exporter
+    app.kubernetes.io/instance: prowlarr-exporter
+  annotations:
+    fluxcd.io/ignore: "false"
+    fluxcd.io/automated: "true"
+spec:
+  replicas: 1
+  revisionHistoryLimit: 3
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: prowlarr-exporter
+      app.kubernetes.io/instance: prowlarr-exporter
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/name: prowlarr-exporter
+        app.kubernetes.io/instance: prowlarr-exporter
+      annotations:
+        prometheus.io/scrape: "true"
+        prometheus.io/port: monitoring
+    spec:
+      containers:
+        - name: prowlarr-exporter
+          image: ghcr.io/onedr0p/exportarr:v2.0.1
+          imagePullPolicy: Always
+          args:
+            - prowlarr
+          env:
+            - name: PORT
+              value: "9707"
+            - name: URL
+              value: ${SECRET_PROWLARR_URL}
+            - name: APIKEY
+              value: ${SECRET_PROWLARR_API_KEY}
+            - name: ENABLE_EPISODE_QUALITY_METRICS
+              value: "true"
+            - name: ADDITIONALMETRICS
+              value: "true"
+            - name: UNKNOWNQUEUEITEMS
+              value: "true"
+          ports:
+            - name: monitoring
+              containerPort: 9707
+          livenessProbe:
+            httpGet:
+              path: /healthz
+              port: monitoring
+            failureThreshold: 5
+            periodSeconds: 10
+          readinessProbe:
+            httpGet:
+              path: /healthz
+              port: monitoring
+            failureThreshold: 5
+            periodSeconds: 10
+          resources:
+            requests:
+              cpu: 5m
+              memory: 10Mi
+            limits:
+              cpu: 500m
+              memory: 256Mi
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: prowlarr-exporter-dashboard
+  labels:
+    grafana_dashboard: "1"
+    app: plex-exporter
+    namespace: media
+  annotations:
+    grafana_folder: Apps
+data:
+  prowlarr-exporter-dashboard.json: |-
+    {
+      "annotations": {
+        "list": [
+          {
+            "builtIn": 1,
+            "datasource": "-- Grafana --",
+            "enable": true,
+            "hide": true,
+            "iconColor": "rgba(0, 211, 255, 1)",
+            "name": "Annotations & Alerts",
+            "type": "dashboard"
+          }
+        ]
+      },
+      "editable": true,
+      "gnetId": null,
+      "graphTooltip": 0,
+      "id": 273,
+      "iteration": 1617623061759,
+      "links": [],
+      "panels": [
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "Online",
+                  "to": "",
+                  "type": 1,
+                  "value": "1"
+                },
+                {
+                  "from": "",
+                  "id": 2,
+                  "operator": "",
+                  "text": "Offline",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                },
+                {
+                  "from": "",
+                  "id": 3,
+                  "operator": "",
+                  "text": "Offline",
+                  "to": "",
+                  "type": 1,
+                  "value": "0"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              }
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 0,
+            "y": 0
+          },
+          "id": 2,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["last"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_system_status{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "status",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 7,
+            "y": 0
+          },
+          "id": 5,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_episode_missing_total{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "intervalFactor": 1,
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "missing ep",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 4,
+            "x": 14,
+            "y": 0
+          },
+          "id": 16,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_queue_total{job=\"$instance\"}",
+              "format": "time_series",
+              "hide": false,
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "in queue",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 12,
+            "w": 4,
+            "x": 18,
+            "y": 0
+          },
+          "id": 4,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_system_health_issues{job=\"$instance\"}",
+              "format": "time_series",
+              "hide": false,
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "health issues",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 0,
+            "y": 4
+          },
+          "id": 6,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_series_total{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "intervalFactor": 1,
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "series",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "bytes"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 7,
+            "y": 4
+          },
+          "id": 10,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_series_filesize_bytes{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "intervalFactor": 1,
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "size",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "decimals": 0,
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 4,
+            "x": 14,
+            "y": 4
+          },
+          "id": 8,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["last"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sum(increase(sonarr_episode_downloaded_total{job=\"$instance\"}[30d]))",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "d/l past month",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 0,
+            "y": 8
+          },
+          "id": 17,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_series_monitored_total{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "intervalFactor": 1,
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "monitored",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "decimals": 0,
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 4,
+            "x": 14,
+            "y": 8
+          },
+          "id": 7,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["last"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sum(increase(sonarr_episode_downloaded_total{job=\"$instance\"}[7d]))",
+              "format": "table",
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "d/ past week",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "color": {
+                "mode": "thresholds"
+              },
+              "links": [],
+              "mappings": [],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              }
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 11,
+            "w": 22,
+            "x": 0,
+            "y": 12
+          },
+          "id": 13,
+          "options": {
+            "displayMode": "gradient",
+            "orientation": "horizontal",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "showUnfilled": true,
+            "text": {
+              "titleSize": 10,
+              "valueSize": 10
+            }
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sort_desc(sum(sonarr_episode_quality_total{job=\"$instance\"}) by (quality))",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "legendFormat": "{{quality}}",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "qualities",
+          "type": "bargauge"
+        },
+        {
+          "datasource": "Loki",
+          "fieldConfig": {
+            "defaults": {},
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 11,
+            "w": 22,
+            "x": 0,
+            "y": 23
+          },
+          "id": 15,
+          "options": {
+            "dedupStrategy": "none",
+            "showLabels": false,
+            "showTime": false,
+            "sortOrder": "Descending",
+            "wrapLogMessage": true
+          },
+          "targets": [
+            {
+              "expr": "{app_kubernetes_io_name=\"prowlarr\"} !~ \"Sending HTTP request to http://localhost:8989\"",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "log",
+          "type": "logs"
+        }
+      ],
+      "refresh": "1m",
+      "schemaVersion": 27,
+      "style": "dark",
+      "tags": [],
+      "templating": {
+        "list": [
+          {
+            "allValue": null,
+            "current": {
+              "selected": true,
+              "text": "media/prowlarr",
+              "value": "media/prowlarr"
+            },
+            "datasource": null,
+            "definition": "label_values(sonarr_system_status, job)",
+            "description": null,
+            "error": null,
+            "hide": 0,
+            "includeAll": false,
+            "label": "Instance",
+            "multi": false,
+            "name": "instance",
+            "options": [
+              {
+                "selected": true,
+                "text": "media/prowlarr",
+                "value": "media/prowlarr"
+              }
+            ],
+            "query": {
+              "query": "label_values(sonarr_system_status, job)",
+              "refId": "StandardVariableQuery"
+            },
+            "refresh": 0,
+            "regex": "",
+            "skipUrlSync": false,
+            "sort": 1,
+            "tagValuesQuery": "",
+            "tags": [],
+            "tagsQuery": "",
+            "type": "query",
+            "useTags": false
+          }
+        ]
+      },
+      "time": {
+        "from": "now-2d",
+        "to": "now"
+      },
+      "timepicker": {},
+      "timezone": "",
+      "title": "prowlarr",
+      "uid": "A8iPsdsjZd",
+      "version": 8
+    }
diff --git a/kubernetes/apps/media/mediabox/app/prowlarr-ingress.yaml b/kubernetes/apps/media/mediabox/app/prowlarr-ingress.yaml
new file mode 100755
index 0000000000..6095e489bd
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/app/prowlarr-ingress.yaml
@@ -0,0 +1,54 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: prowlarr
+  annotations:
+    external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+    nginx.ingress.kubernetes.io/auth-method: GET
+    nginx.ingress.kubernetes.io/auth-url: https://auth.${SECRET_DOMAIN}/api/verify
+    nginx.ingress.kubernetes.io/auth-signin: https://auth.${SECRET_DOMAIN}?rm=$request_method
+    nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
+    nginx.ingress.kubernetes.io/auth-snippet: |
+      proxy_set_header X-Forwarded-Method $request_method;
+      proxy_set_header X-Forwarded-Scheme $scheme;
+    hajimari.io/enable: "true"
+    hajimari.io/icon: plex
+spec:
+  ingressClassName: external
+  tls:
+    - secretName: ${SECRET_DOMAIN/./-}-production-tls
+      hosts:
+        - prowlarr.${SECRET_DOMAIN}
+  rules:
+    - host: prowlarr.${SECRET_DOMAIN}
+      http:
+        paths:
+          - path: /
+            pathType: Prefix
+            backend:
+              service:
+                name: mediabox
+                port:
+                  name: prowlarr
+---
+kind: ConfigMap
+metadata:
+  labels:
+    app.kubernetes.io/name: prowlarr
+    gatus.io/enabled: "true"
+  name: prowlarr-gatus-ep
+apiVersion: v1
+data:
+  config.yaml: |
+    endpoints:
+      - name: "prowlarr"
+        group: external-kubernetes
+        url: "https://prowlarr.${SECRET_DOMAIN}/"
+        interval: 1m
+        client:
+          dns-resolver: tcp://1.1.1.1:53
+        conditions:
+          - "[STATUS] == 200"
+        alerts:
+          - type: discord
diff --git a/kubernetes/apps/media/mediabox/app/radarr-exporter.yaml b/kubernetes/apps/media/mediabox/app/radarr-exporter.yaml
new file mode 100644
index 0000000000..1d7424549f
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/app/radarr-exporter.yaml
@@ -0,0 +1,942 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: radarr-exporter
+  namespace: media
+  labels:
+    app.kubernetes.io/name: radarr-exporter
+    app.kubernetes.io/instance: radarr-exporter
+spec:
+  clusterIP: None
+  selector:
+    app.kubernetes.io/name: radarr-exporter
+    app.kubernetes.io/instance: radarr-exporter
+  ports:
+    - name: monitoring
+      port: 9708
+---
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+  name: radarr-exporter
+  namespace: media
+  labels:
+    app.kubernetes.io/name: radarr-exporter
+    app.kubernetes.io/instance: radarr-exporter
+spec:
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: radarr-exporter
+      app.kubernetes.io/instance: radarr-exporter
+  endpoints:
+    - port: monitoring
+      interval: 4m
+      scrapeTimeout: 90s
+      path: /metrics
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+  name: radarr-exporter
+  namespace: media
+  labels:
+    app.kubernetes.io/name: radarr-exporter
+    app.kubernetes.io/instance: radarr-exporter
+spec:
+  replicas: 1
+  revisionHistoryLimit: 3
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: radarr-exporter
+      app.kubernetes.io/instance: radarr-exporter
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/name: radarr-exporter
+        app.kubernetes.io/instance: radarr-exporter
+      annotations:
+        prometheus.io/scrape: "true"
+        prometheus.io/port: monitoring
+    spec:
+      containers:
+        - name: radarr-exporter
+          image: ghcr.io/onedr0p/exportarr:v2.0.1
+          imagePullPolicy: Always
+          args:
+            - radarr
+          env:
+            - name: PORT
+              value: "9708"
+            - name: URL
+              value: ${SECRET_RADARR_URL}
+            - name: APIKEY
+              value: ${SECRET_RADARR_API_KEY}
+            - name: ADDITIONALMETRICS
+              value: "true"
+            - name: UNKNOWNQUEUEITEMS
+              value: "true"
+          ports:
+            - name: monitoring
+              containerPort: 9708
+          livenessProbe:
+            httpGet:
+              path: /healthz
+              port: monitoring
+            failureThreshold: 5
+            periodSeconds: 10
+          readinessProbe:
+            httpGet:
+              path: /healthz
+              port: monitoring
+            failureThreshold: 5
+            periodSeconds: 10
+          resources:
+            requests:
+              cpu: 5m
+              memory: 10Mi
+            limits:
+              cpu: 500m
+              memory: 256Mi
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: radarr-exporter-dashboard
+  labels:
+    grafana_dashboard: "1"
+    app: radarr-exporter
+  namespace: media
+  annotations:
+    grafana_folder: Apps
+data:
+  radarr-exporter-dashboard.json: |-
+    {
+      "annotations": {
+        "list": [
+          {
+            "builtIn": 1,
+            "datasource": "-- Grafana --",
+            "enable": true,
+            "hide": true,
+            "iconColor": "rgba(0, 211, 255, 1)",
+            "name": "Annotations & Alerts",
+            "type": "dashboard"
+          }
+        ]
+      },
+      "editable": true,
+      "gnetId": null,
+      "graphTooltip": 0,
+      "id": 272,
+      "iteration": 1617623062689,
+      "links": [],
+      "panels": [
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "Online",
+                  "to": "",
+                  "type": 1,
+                  "value": "1"
+                },
+                {
+                  "from": "",
+                  "id": 2,
+                  "operator": "",
+                  "text": "Offline",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                },
+                {
+                  "from": "",
+                  "id": 3,
+                  "operator": "",
+                  "text": "Offline",
+                  "to": "",
+                  "type": 1,
+                  "value": "0"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(255, 194, 48)",
+                    "value": null
+                  }
+                ]
+              }
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 0,
+            "y": 0
+          },
+          "id": 2,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["last"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "radarr_system_status{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "status",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(255, 194, 48)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 7,
+            "y": 0
+          },
+          "id": 5,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "radarr_movie_missing_total{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "intervalFactor": 1,
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "missing",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(255, 194, 48)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 4,
+            "x": 14,
+            "y": 0
+          },
+          "id": 16,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "radarr_queue_total{job=\"$instance\"}",
+              "format": "time_series",
+              "hide": false,
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "in queue",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(255, 194, 48)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 12,
+            "w": 4,
+            "x": 18,
+            "y": 0
+          },
+          "id": 4,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "radarr_system_health_issues{job=\"$instance\"}",
+              "format": "time_series",
+              "hide": false,
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "health issues",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(255, 194, 48)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 0,
+            "y": 4
+          },
+          "id": 6,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "radarr_movie_total{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "intervalFactor": 1,
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "movies",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(255, 194, 48)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 7,
+            "y": 4
+          },
+          "id": 3,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["last"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "radarr_movie_wanted_total{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "wanted",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "decimals": 0,
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(255, 194, 48)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 4,
+            "x": 14,
+            "y": 4
+          },
+          "id": 8,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["last"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sum(increase(radarr_movie_downloaded_total{job=\"$instance\"}[30d]))",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "d/l past month",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(255, 194, 48)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 0,
+            "y": 8
+          },
+          "id": 17,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "radarr_movie_monitored_total{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "intervalFactor": 1,
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "monitored",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(255, 194, 48)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "bytes"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 7,
+            "y": 8
+          },
+          "id": 10,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "radarr_movie_filesize_total{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "intervalFactor": 1,
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "size",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "decimals": 0,
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(255, 194, 48)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 4,
+            "x": 14,
+            "y": 8
+          },
+          "id": 7,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["last"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sum(increase(radarr_movie_downloaded_total{job=\"$instance\"}[7d]))",
+              "format": "table",
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "d/ past week",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "color": {
+                "mode": "thresholds"
+              },
+              "links": [],
+              "mappings": [],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(255, 194, 48)",
+                    "value": null
+                  }
+                ]
+              }
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 11,
+            "w": 22,
+            "x": 0,
+            "y": 12
+          },
+          "id": 13,
+          "options": {
+            "displayMode": "gradient",
+            "orientation": "horizontal",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "showUnfilled": true,
+            "text": {
+              "titleSize": 10,
+              "valueSize": 10
+            }
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sort_desc(sum(radarr_movie_quality_total{job=\"$instance\"}) by (quality))",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "legendFormat": "{{quality}}",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "qualities",
+          "type": "bargauge"
+        },
+        {
+          "datasource": "Loki",
+          "fieldConfig": {
+            "defaults": {},
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 11,
+            "w": 22,
+            "x": 0,
+            "y": 23
+          },
+          "id": 15,
+          "options": {
+            "dedupStrategy": "none",
+            "showLabels": false,
+            "showTime": false,
+            "sortOrder": "Descending",
+            "wrapLogMessage": true
+          },
+          "targets": [
+            {
+              "expr": "{app_kubernetes_io_name=\"radarr\"} !~ \"Sending HTTP request to http://localhost:7878\"",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "log",
+          "type": "logs"
+        }
+      ],
+      "refresh": "1m",
+      "schemaVersion": 27,
+      "style": "dark",
+      "tags": [],
+      "templating": {
+        "list": [
+          {
+            "allValue": null,
+            "current": {
+              "selected": false,
+              "text": "media/radarr",
+              "value": "media/radarr"
+            },
+            "datasource": null,
+            "definition": "label_values(radarr_system_status, job)",
+            "description": null,
+            "error": null,
+            "hide": 0,
+            "includeAll": false,
+            "label": "Instance",
+            "multi": false,
+            "name": "instance",
+            "options": [
+              {
+                "selected": true,
+                "text": "media/radarr",
+                "value": "media/radarr"
+              }
+            ],
+            "query": {
+              "query": "label_values(radarr_system_status, job)",
+              "refId": "StandardVariableQuery"
+            },
+            "refresh": 0,
+            "regex": "",
+            "skipUrlSync": false,
+            "sort": 1,
+            "tagValuesQuery": "",
+            "tags": [],
+            "tagsQuery": "",
+            "type": "query",
+            "useTags": false
+          }
+        ]
+      },
+      "time": {
+        "from": "now-2d",
+        "to": "now"
+      },
+      "timepicker": {},
+      "timezone": "",
+      "title": "Radarr",
+      "uid": "A8iPssjZk",
+      "version": 18
+    }
diff --git a/kubernetes/apps/media/mediabox/app/radarr-ingress.yaml b/kubernetes/apps/media/mediabox/app/radarr-ingress.yaml
new file mode 100755
index 0000000000..a1eb70e4d3
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/app/radarr-ingress.yaml
@@ -0,0 +1,54 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: radarr
+  annotations:
+    external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+    nginx.ingress.kubernetes.io/auth-method: GET
+    nginx.ingress.kubernetes.io/auth-url: https://auth.${SECRET_DOMAIN}/api/verify
+    nginx.ingress.kubernetes.io/auth-signin: https://auth.${SECRET_DOMAIN}?rm=$request_method
+    nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
+    nginx.ingress.kubernetes.io/auth-snippet: |
+      proxy_set_header X-Forwarded-Method $request_method;
+      proxy_set_header X-Forwarded-Scheme $scheme;
+    hajimari.io/enable: "true"
+    hajimari.io/icon: filmstrip
+spec:
+  ingressClassName: external
+  tls:
+    - secretName: ${SECRET_DOMAIN/./-}-production-tls
+      hosts:
+        - radarr.${SECRET_DOMAIN}
+  rules:
+    - host: radarr.${SECRET_DOMAIN}
+      http:
+        paths:
+          - path: /
+            pathType: Prefix
+            backend:
+              service:
+                name: mediabox
+                port:
+                  name: radarr
+---
+kind: ConfigMap
+metadata:
+  labels:
+    app.kubernetes.io/name: radarr
+    gatus.io/enabled: "true"
+  name: radarr-gatus-ep
+apiVersion: v1
+data:
+  config.yaml: |
+    endpoints:
+      - name: "radarr"
+        group: external-kubernetes
+        url: "https://radarr.${SECRET_DOMAIN}/"
+        interval: 1m
+        client:
+          dns-resolver: tcp://1.1.1.1:53
+        conditions:
+          - "[STATUS] == 200"
+        alerts:
+          - type: discord
diff --git a/kubernetes/apps/media/mediabox/app/sabnzbd-ingress.yaml b/kubernetes/apps/media/mediabox/app/sabnzbd-ingress.yaml
new file mode 100755
index 0000000000..4a5be01348
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/app/sabnzbd-ingress.yaml
@@ -0,0 +1,54 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: sabnzbd
+  annotations:
+    external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+    nginx.ingress.kubernetes.io/auth-method: GET
+    nginx.ingress.kubernetes.io/auth-url: https://auth.${SECRET_DOMAIN}/api/verify
+    nginx.ingress.kubernetes.io/auth-signin: https://auth.${SECRET_DOMAIN}?rm=$request_method
+    nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
+    nginx.ingress.kubernetes.io/auth-snippet: |
+      proxy_set_header X-Forwarded-Method $request_method;
+      proxy_set_header X-Forwarded-Scheme $scheme;
+    hajimari.io/enable: "true"
+    hajimari.io/icon: cloud-download
+spec:
+  ingressClassName: external
+  tls:
+    - secretName: ${SECRET_DOMAIN/./-}-production-tls
+      hosts:
+        - sabnzbd.${SECRET_DOMAIN}
+  rules:
+    - host: sabnzbd.${SECRET_DOMAIN}
+      http:
+        paths:
+          - path: /
+            pathType: Prefix
+            backend:
+              service:
+                name: mediabox
+                port:
+                  name: sabnzbd
+---
+kind: ConfigMap
+metadata:
+  labels:
+    app.kubernetes.io/name: sabnzbd
+    gatus.io/enabled: "true"
+  name: sabnzbd-gatus-ep
+apiVersion: v1
+data:
+  config.yaml: |
+    endpoints:
+      - name: "sabnzbd"
+        group: external-kubernetes
+        url: "https://sabnzbd.${SECRET_DOMAIN}/"
+        interval: 1m
+        client:
+          dns-resolver: tcp://1.1.1.1:53
+        conditions:
+          - "[STATUS] == 200"
+        alerts:
+          - type: discord
diff --git a/kubernetes/apps/media/mediabox/app/service.yaml b/kubernetes/apps/media/mediabox/app/service.yaml
new file mode 100755
index 0000000000..616954b4ca
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/app/service.yaml
@@ -0,0 +1,42 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: mediabox
+spec:
+  externalName: thiazi.home
+  ports:
+    - name: sonarr
+      port: 8989
+      protocol: TCP
+      targetPort: 8989
+    - name: radarr
+      port: 7878
+      protocol: TCP
+      targetPort: 7878
+    - name: prowlarr
+      port: 9696
+      protocol: TCP
+      targetPort: 9696
+    - name: sabnzbd
+      port: 8080
+      protocol: TCP
+      targetPort: 8080
+    - name: gaps
+      port: 8484
+      protocol: TCP
+      targetPort: 8484
+    - name: bazarr
+      port: 6767
+      protocol: TCP
+      targetPort: 6767
+    - name: notifiarr
+      port: 5454
+      protocol: TCP
+      targetPort: 5454
+    - name: lldap
+      port: 17170
+      protocol: TCP
+      targetPort: 17170
+  sessionAffinity: None
+  type: ExternalName
diff --git a/kubernetes/apps/media/mediabox/app/sonarr-exporter.yaml b/kubernetes/apps/media/mediabox/app/sonarr-exporter.yaml
new file mode 100644
index 0000000000..531484c137
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/app/sonarr-exporter.yaml
@@ -0,0 +1,885 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: sonarr-exporter
+  namespace: monitoring
+  labels:
+    app.kubernetes.io/name: sonarr-exporter
+    app.kubernetes.io/instance: sonarr-exporter
+spec:
+  clusterIP: None
+  selector:
+    app.kubernetes.io/name: sonarr-exporter
+    app.kubernetes.io/instance: sonarr-exporter
+  ports:
+    - name: monitoring
+      port: 9707
+---
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+  name: sonarr-exporter
+  namespace: monitoring
+  labels:
+    app.kubernetes.io/name: sonarr-exporter
+    app.kubernetes.io/instance: sonarr-exporter
+spec:
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: sonarr-exporter
+      app.kubernetes.io/instance: sonarr-exporter
+  endpoints:
+    - port: monitoring
+      interval: 4m
+      scrapeTimeout: 90s
+      path: /metrics
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+  name: sonarr-exporter
+  namespace: monitoring
+  labels:
+    app.kubernetes.io/name: sonarr-exporter
+    app.kubernetes.io/instance: sonarr-exporter
+  annotations:
+    fluxcd.io/ignore: "false"
+    fluxcd.io/automated: "true"
+    fluxcd.io/tag.sonarr-exporter: semver:~v0.1
+spec:
+  replicas: 1
+  revisionHistoryLimit: 3
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: sonarr-exporter
+      app.kubernetes.io/instance: sonarr-exporter
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/name: sonarr-exporter
+        app.kubernetes.io/instance: sonarr-exporter
+      annotations:
+        prometheus.io/scrape: "true"
+        prometheus.io/port: "monitoring"
+    spec:
+      containers:
+        - name: sonarr-exporter
+          image: ghcr.io/onedr0p/exportarr:v1.5.3
+          imagePullPolicy: IfNotPresent
+          args:
+            - sonarr
+          env:
+            - name: PORT
+              value: "9707"
+            - name: URL
+              value: ${SECRET_SONARR_URL}
+            - name: APIKEY
+              value: ${SECRET_SONARR_API_KEY}
+            - name: ENABLE_EPISODE_QUALITY_METRICS
+              value: "true"
+            - name: ADDITIONALMETRICS
+              value: "true"
+            - name: UNKNOWNQUEUEITEMS
+              value: "true"
+          ports:
+            - name: monitoring
+              containerPort: 9707
+          livenessProbe:
+            httpGet:
+              path: /healthz
+              port: monitoring
+            failureThreshold: 5
+            periodSeconds: 10
+          readinessProbe:
+            httpGet:
+              path: /healthz
+              port: monitoring
+            failureThreshold: 5
+            periodSeconds: 10
+          resources:
+            requests:
+              cpu: 100m
+              memory: 64Mi
+            limits:
+              cpu: 500m
+              memory: 256Mi
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: sonarr-exporter-dashboard
+  labels:
+    grafana_dashboard: "1"
+    app: plex-exporter
+    namespace: media
+  annotations:
+    grafana_folder: Apps
+data:
+  sonarr-exporter-dashboard.json: |-
+    {
+      "annotations": {
+        "list": [
+          {
+            "builtIn": 1,
+            "datasource": "-- Grafana --",
+            "enable": true,
+            "hide": true,
+            "iconColor": "rgba(0, 211, 255, 1)",
+            "name": "Annotations & Alerts",
+            "type": "dashboard"
+          }
+        ]
+      },
+      "editable": true,
+      "gnetId": null,
+      "graphTooltip": 0,
+      "id": 273,
+      "iteration": 1617623061759,
+      "links": [],
+      "panels": [
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "Online",
+                  "to": "",
+                  "type": 1,
+                  "value": "1"
+                },
+                {
+                  "from": "",
+                  "id": 2,
+                  "operator": "",
+                  "text": "Offline",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                },
+                {
+                  "from": "",
+                  "id": 3,
+                  "operator": "",
+                  "text": "Offline",
+                  "to": "",
+                  "type": 1,
+                  "value": "0"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              }
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 0,
+            "y": 0
+          },
+          "id": 2,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["last"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_system_status{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "status",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 7,
+            "y": 0
+          },
+          "id": 5,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_episode_missing_total{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "intervalFactor": 1,
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "missing ep",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 4,
+            "x": 14,
+            "y": 0
+          },
+          "id": 16,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_queue_total{job=\"$instance\"}",
+              "format": "time_series",
+              "hide": false,
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "in queue",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 12,
+            "w": 4,
+            "x": 18,
+            "y": 0
+          },
+          "id": 4,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_system_health_issues{job=\"$instance\"}",
+              "format": "time_series",
+              "hide": false,
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "health issues",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 0,
+            "y": 4
+          },
+          "id": 6,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_series_total{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "intervalFactor": 1,
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "series",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "bytes"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 7,
+            "y": 4
+          },
+          "id": 10,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_series_filesize_bytes{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "intervalFactor": 1,
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "size",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "decimals": 0,
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 4,
+            "x": 14,
+            "y": 4
+          },
+          "id": 8,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["last"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sum(increase(sonarr_episode_downloaded_total{job=\"$instance\"}[30d]))",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "d/l past month",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 7,
+            "x": 0,
+            "y": 8
+          },
+          "id": 17,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sonarr_series_monitored_total{job=\"$instance\"}",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "intervalFactor": 1,
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "monitored",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "decimals": 0,
+              "mappings": [
+                {
+                  "from": "",
+                  "id": 1,
+                  "operator": "",
+                  "text": "0",
+                  "to": "",
+                  "type": 1,
+                  "value": "null"
+                }
+              ],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              },
+              "unit": "locale"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 4,
+            "x": 14,
+            "y": 8
+          },
+          "id": 7,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": ["last"],
+              "fields": "",
+              "values": false
+            },
+            "text": {},
+            "textMode": "auto"
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sum(increase(sonarr_episode_downloaded_total{job=\"$instance\"}[7d]))",
+              "format": "table",
+              "instant": true,
+              "interval": "",
+              "legendFormat": "",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "d/ past week",
+          "type": "stat"
+        },
+        {
+          "datasource": "Prometheus",
+          "fieldConfig": {
+            "defaults": {
+              "color": {
+                "mode": "thresholds"
+              },
+              "links": [],
+              "mappings": [],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "rgb(33, 147, 181)",
+                    "value": null
+                  }
+                ]
+              }
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 11,
+            "w": 22,
+            "x": 0,
+            "y": 12
+          },
+          "id": 13,
+          "options": {
+            "displayMode": "gradient",
+            "orientation": "horizontal",
+            "reduceOptions": {
+              "calcs": ["lastNotNull"],
+              "fields": "",
+              "values": false
+            },
+            "showUnfilled": true,
+            "text": {
+              "titleSize": 10,
+              "valueSize": 10
+            }
+          },
+          "pluginVersion": "7.5.2",
+          "targets": [
+            {
+              "expr": "sort_desc(sum(sonarr_episode_quality_total{job=\"$instance\"}) by (quality))",
+              "format": "time_series",
+              "instant": true,
+              "interval": "",
+              "legendFormat": "{{quality}}",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "qualities",
+          "type": "bargauge"
+        },
+        {
+          "datasource": "Loki",
+          "fieldConfig": {
+            "defaults": {},
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 11,
+            "w": 22,
+            "x": 0,
+            "y": 23
+          },
+          "id": 15,
+          "options": {
+            "dedupStrategy": "none",
+            "showLabels": false,
+            "showTime": false,
+            "sortOrder": "Descending",
+            "wrapLogMessage": true
+          },
+          "targets": [
+            {
+              "expr": "{app_kubernetes_io_name=\"sonarr\"} !~ \"Sending HTTP request to http://localhost:8989\"",
+              "refId": "A"
+            }
+          ],
+          "timeFrom": null,
+          "timeShift": null,
+          "title": "log",
+          "type": "logs"
+        }
+      ],
+      "refresh": "1m",
+      "schemaVersion": 27,
+      "style": "dark",
+      "tags": [],
+      "templating": {
+        "list": [
+          {
+            "allValue": null,
+            "current": {
+              "selected": true,
+              "text": "media/sonarr",
+              "value": "media/sonarr"
+            },
+            "datasource": null,
+            "definition": "label_values(sonarr_system_status, job)",
+            "description": null,
+            "error": null,
+            "hide": 0,
+            "includeAll": false,
+            "label": "Instance",
+            "multi": false,
+            "name": "instance",
+            "options": [
+              {
+                "selected": true,
+                "text": "media/sonarr",
+                "value": "media/sonarr"
+              }
+            ],
+            "query": {
+              "query": "label_values(sonarr_system_status, job)",
+              "refId": "StandardVariableQuery"
+            },
+            "refresh": 0,
+            "regex": "",
+            "skipUrlSync": false,
+            "sort": 1,
+            "tagValuesQuery": "",
+            "tags": [],
+            "tagsQuery": "",
+            "type": "query",
+            "useTags": false
+          }
+        ]
+      },
+      "time": {
+        "from": "now-2d",
+        "to": "now"
+      },
+      "timepicker": {},
+      "timezone": "",
+      "title": "Sonarr",
+      "uid": "A8iPssjZd",
+      "version": 8
+    }
diff --git a/kubernetes/apps/media/mediabox/app/sonarr-ingress.yaml b/kubernetes/apps/media/mediabox/app/sonarr-ingress.yaml
new file mode 100755
index 0000000000..8a7bbd7800
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/app/sonarr-ingress.yaml
@@ -0,0 +1,54 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: sonarr
+  annotations:
+    external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+    nginx.ingress.kubernetes.io/auth-method: GET
+    nginx.ingress.kubernetes.io/auth-url: https://auth.${SECRET_DOMAIN}/api/verify
+    nginx.ingress.kubernetes.io/auth-signin: https://auth.${SECRET_DOMAIN}?rm=$request_method
+    nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
+    nginx.ingress.kubernetes.io/auth-snippet: |
+      proxy_set_header X-Forwarded-Method $request_method;
+      proxy_set_header X-Forwarded-Scheme $scheme;
+    hajimari.io/enable: "true"
+    hajimari.io/icon: television-box
+spec:
+  ingressClassName: external
+  tls:
+    - secretName: ${SECRET_DOMAIN/./-}-production-tls
+      hosts:
+        - sonarr.${SECRET_DOMAIN}
+  rules:
+    - host: sonarr.${SECRET_DOMAIN}
+      http:
+        paths:
+          - path: /
+            pathType: Prefix
+            backend:
+              service:
+                name: mediabox
+                port:
+                  name: sonarr
+---
+kind: ConfigMap
+metadata:
+  labels:
+    app.kubernetes.io/name: sonarr
+    gatus.io/enabled: "true"
+  name: sonarr-gatus-ep
+apiVersion: v1
+data:
+  config.yaml: |
+    endpoints:
+      - name: "sonarr"
+        group: external-kubernetes
+        url: "https://sonarr.${SECRET_DOMAIN}/"
+        interval: 1m
+        client:
+          dns-resolver: tcp://1.1.1.1:53
+        conditions:
+          - "[STATUS] == 200"
+        alerts:
+          - type: discord
diff --git a/kubernetes/apps/media/mediabox/ks.yaml b/kubernetes/apps/media/mediabox/ks.yaml
new file mode 100644
index 0000000000..91fa2bfed3
--- /dev/null
+++ b/kubernetes/apps/media/mediabox/ks.yaml
@@ -0,0 +1,21 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app mediabox
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: media
+  path: ./kubernetes/apps/media/mediabox/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/media/namespace.yaml b/kubernetes/apps/media/namespace.yaml
new file mode 100644
index 0000000000..e4b066b2a4
--- /dev/null
+++ b/kubernetes/apps/media/namespace.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: media
+  labels:
+    kustomize.toolkit.fluxcd.io/prune: disabled
diff --git a/kubernetes/apps/media/plex-exporter/app/configmap.yaml b/kubernetes/apps/media/plex-exporter/app/configmap.yaml
new file mode 100755
index 0000000000..a6120bd313
--- /dev/null
+++ b/kubernetes/apps/media/plex-exporter/app/configmap.yaml
@@ -0,0 +1,19 @@
+---
+apiVersion: v1
+data:
+  config.json: |
+    {
+    "exporter": {
+        "port": 9567
+      },
+      "server": {
+        "address": "plex.home",
+        "port": 32400,
+        "token": "${SECRET_PLEXTOKEN}"
+      }
+    }
+kind: ConfigMap
+metadata:
+  labels:
+    app: plex-exporter
+  name: plex-exporter-config
diff --git a/kubernetes/apps/media/plex-exporter/app/deployment.yaml b/kubernetes/apps/media/plex-exporter/app/deployment.yaml
new file mode 100755
index 0000000000..512cdc9e0e
--- /dev/null
+++ b/kubernetes/apps/media/plex-exporter/app/deployment.yaml
@@ -0,0 +1,50 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  annotations:
+    reloader.stakater.com/auto: "true"
+  labels:
+    app: plex-exporter
+  name: plex-exporter
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: plex-exporter
+  template:
+    metadata:
+      labels:
+        app: plex-exporter
+    spec:
+      containers:
+        - image: registry.eighty-three.me/tuxpeople/plex-exporter:nightly
+          command:
+            - /plex_exporter
+          args:
+            - --config
+            - /config.json
+          imagePullPolicy: Always
+          name: plex-exporter
+          ports:
+            - containerPort: 9567
+              protocol: TCP
+          resources:
+            requests:
+              cpu: 10m
+              memory: 50Mi
+            limits:
+              cpu: 500m
+              memory: 500Mi
+          volumeMounts:
+            - mountPath: /config.json
+              name: plex-exporter-config
+              subPath: config.json
+      volumes:
+        - configMap:
+            items:
+              - key: config.json
+                mode: 420
+                path: config.json
+            name: plex-exporter-config
+          name: plex-exporter-config
diff --git a/kubernetes/apps/media/plex-exporter/app/grafana-dashboard.yaml b/kubernetes/apps/media/plex-exporter/app/grafana-dashboard.yaml
new file mode 100755
index 0000000000..737b49d4e7
--- /dev/null
+++ b/kubernetes/apps/media/plex-exporter/app/grafana-dashboard.yaml
@@ -0,0 +1,477 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: plex-exporter-dashboard
+  labels:
+    grafana_dashboard: '1'
+    app: plex-exporter
+    namespace: media
+  annotations:
+    grafana_folder: Apps
+data:
+  plex-exporter-dashboard.json: |-
+    {
+      "annotations": {
+        "list": [
+          {
+            "builtIn": 1,
+            "datasource": "-- Grafana --",
+            "enable": true,
+            "hide": true,
+            "iconColor": "rgba(0, 211, 255, 1)",
+            "name": "Annotations & Alerts",
+            "target": {
+              "limit": 100,
+              "matchAny": false,
+              "tags": [],
+              "type": "dashboard"
+            },
+            "type": "dashboard"
+          }
+        ]
+      },
+      "editable": true,
+      "fiscalYearStartMonth": 0,
+      "gnetId": null,
+      "graphTooltip": 0,
+      "id": null,
+      "links": [],
+      "liveNow": false,
+      "panels": [
+        {
+          "datasource": null,
+          "fieldConfig": {
+            "defaults": {
+              "color": {
+                "mode": "thresholds"
+              },
+              "mappings": [],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "green",
+                    "value": null
+                  },
+                  {
+                    "color": "red",
+                    "value": 80
+                  }
+                ]
+              }
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 6,
+            "y": 0
+          },
+          "id": 12,
+          "options": {
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "fields": "",
+              "values": false
+            },
+            "showThresholdLabels": false,
+            "showThresholdMarkers": true,
+            "text": {}
+          },
+          "pluginVersion": "8.2.1",
+          "targets": [
+            {
+              "exemplar": true,
+              "expr": "sum(plex_transcode_sessions_active_count) by (container)",
+              "interval": "",
+              "legendFormat": "Plex sessions",
+              "refId": "A"
+            },
+            {
+              "exemplar": true,
+              "expr": "sum(plex_transcode_sessions_active_count) by (container)",
+              "hide": false,
+              "interval": "",
+              "legendFormat": "Transcoding sessions",
+              "refId": "B"
+            }
+          ],
+          "title": "Current usage",
+          "type": "gauge"
+        },
+        {
+          "collapsed": false,
+          "datasource": null,
+          "gridPos": {
+            "h": 1,
+            "w": 24,
+            "x": 0,
+            "y": 8
+          },
+          "id": 14,
+          "panels": [],
+          "title": "Sessions history",
+          "type": "row"
+        },
+        {
+          "datasource": null,
+          "fieldConfig": {
+            "defaults": {
+              "color": {
+                "mode": "palette-classic"
+              },
+              "custom": {
+                "axisLabel": "",
+                "axisPlacement": "auto",
+                "barAlignment": 0,
+                "drawStyle": "line",
+                "fillOpacity": 0,
+                "gradientMode": "none",
+                "hideFrom": {
+                  "legend": false,
+                  "tooltip": false,
+                  "viz": false
+                },
+                "lineInterpolation": "linear",
+                "lineWidth": 1,
+                "pointSize": 5,
+                "scaleDistribution": {
+                  "type": "linear"
+                },
+                "showPoints": "auto",
+                "spanNulls": false,
+                "stacking": {
+                  "group": "A",
+                  "mode": "none"
+                },
+                "thresholdsStyle": {
+                  "mode": "off"
+                }
+              },
+              "mappings": [],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "green",
+                    "value": null
+                  },
+                  {
+                    "color": "red",
+                    "value": 80
+                  }
+                ]
+              }
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 0,
+            "y": 9
+          },
+          "id": 8,
+          "options": {
+            "legend": {
+              "calcs": [],
+              "displayMode": "list",
+              "placement": "bottom"
+            },
+            "tooltip": {
+              "mode": "single"
+            }
+          },
+          "targets": [
+            {
+              "exemplar": true,
+              "expr": "sum(plex_transcode_sessions_active_count) by (container)",
+              "interval": "",
+              "legendFormat": "Active plex sessions",
+              "refId": "A"
+            }
+          ],
+          "title": "Active plex sessions",
+          "type": "timeseries"
+        },
+        {
+          "datasource": null,
+          "fieldConfig": {
+            "defaults": {
+              "color": {
+                "mode": "palette-classic"
+              },
+              "custom": {
+                "axisLabel": "",
+                "axisPlacement": "auto",
+                "barAlignment": 0,
+                "drawStyle": "line",
+                "fillOpacity": 0,
+                "gradientMode": "none",
+                "hideFrom": {
+                  "legend": false,
+                  "tooltip": false,
+                  "viz": false
+                },
+                "lineInterpolation": "linear",
+                "lineWidth": 1,
+                "pointSize": 5,
+                "scaleDistribution": {
+                  "type": "linear"
+                },
+                "showPoints": "auto",
+                "spanNulls": false,
+                "stacking": {
+                  "group": "A",
+                  "mode": "none"
+                },
+                "thresholdsStyle": {
+                  "mode": "off"
+                }
+              },
+              "mappings": [],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "green",
+                    "value": null
+                  },
+                  {
+                    "color": "red",
+                    "value": 80
+                  }
+                ]
+              }
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 12,
+            "y": 9
+          },
+          "id": 10,
+          "options": {
+            "legend": {
+              "calcs": [],
+              "displayMode": "list",
+              "placement": "bottom"
+            },
+            "tooltip": {
+              "mode": "single"
+            }
+          },
+          "targets": [
+            {
+              "exemplar": true,
+              "expr": "sum(plex_transcode_sessions_active_count) by (container)",
+              "interval": "",
+              "legendFormat": "Active transcode sessions",
+              "refId": "A"
+            }
+          ],
+          "title": "Active transcode sessions",
+          "type": "timeseries"
+        },
+        {
+          "collapsed": false,
+          "datasource": null,
+          "gridPos": {
+            "h": 1,
+            "w": 24,
+            "x": 0,
+            "y": 17
+          },
+          "id": 6,
+          "panels": [],
+          "title": "Library content",
+          "type": "row"
+        },
+        {
+          "datasource": null,
+          "description": "",
+          "fieldConfig": {
+            "defaults": {
+              "color": {
+                "mode": "palette-classic"
+              },
+              "custom": {
+                "axisLabel": "",
+                "axisPlacement": "auto",
+                "barAlignment": 0,
+                "drawStyle": "line",
+                "fillOpacity": 0,
+                "gradientMode": "none",
+                "hideFrom": {
+                  "legend": false,
+                  "tooltip": false,
+                  "viz": false
+                },
+                "lineInterpolation": "linear",
+                "lineWidth": 1,
+                "pointSize": 5,
+                "scaleDistribution": {
+                  "type": "linear"
+                },
+                "showPoints": "auto",
+                "spanNulls": false,
+                "stacking": {
+                  "group": "A",
+                  "mode": "none"
+                },
+                "thresholdsStyle": {
+                  "mode": "off"
+                }
+              },
+              "mappings": [],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "green",
+                    "value": null
+                  },
+                  {
+                    "color": "red",
+                    "value": 80
+                  }
+                ]
+              }
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 9,
+            "w": 12,
+            "x": 0,
+            "y": 18
+          },
+          "id": 4,
+          "options": {
+            "legend": {
+              "calcs": [],
+              "displayMode": "list",
+              "placement": "bottom"
+            },
+            "tooltip": {
+              "mode": "single"
+            }
+          },
+          "targets": [
+            {
+              "exemplar": true,
+              "expr": "plex_media_server_library_media_count{type=\"show\"}",
+              "interval": "",
+              "legendFormat": "Library \"{{name}}\"",
+              "refId": "A"
+            }
+          ],
+          "title": "Numbers of TV shows",
+          "type": "timeseries"
+        },
+        {
+          "datasource": null,
+          "description": "",
+          "fieldConfig": {
+            "defaults": {
+              "color": {
+                "mode": "palette-classic"
+              },
+              "custom": {
+                "axisLabel": "",
+                "axisPlacement": "auto",
+                "barAlignment": 0,
+                "drawStyle": "line",
+                "fillOpacity": 0,
+                "gradientMode": "none",
+                "hideFrom": {
+                  "legend": false,
+                  "tooltip": false,
+                  "viz": false
+                },
+                "lineInterpolation": "linear",
+                "lineWidth": 1,
+                "pointSize": 5,
+                "scaleDistribution": {
+                  "type": "linear"
+                },
+                "showPoints": "auto",
+                "spanNulls": false,
+                "stacking": {
+                  "group": "A",
+                  "mode": "none"
+                },
+                "thresholdsStyle": {
+                  "mode": "off"
+                }
+              },
+              "mappings": [],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "green",
+                    "value": null
+                  },
+                  {
+                    "color": "red",
+                    "value": 80
+                  }
+                ]
+              }
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 9,
+            "w": 12,
+            "x": 12,
+            "y": 18
+          },
+          "id": 2,
+          "options": {
+            "legend": {
+              "calcs": [],
+              "displayMode": "list",
+              "placement": "bottom"
+            },
+            "tooltip": {
+              "mode": "single"
+            }
+          },
+          "targets": [
+            {
+              "exemplar": true,
+              "expr": "plex_media_server_library_media_count{type=\"movie\"}",
+              "interval": "",
+              "legendFormat": "Library \"{{name}}\"",
+              "refId": "A"
+            }
+          ],
+          "title": "Numbers of movies",
+          "type": "timeseries"
+        }
+      ],
+      "schemaVersion": 31,
+      "style": "dark",
+      "tags": [],
+      "templating": {
+        "list": []
+      },
+      "time": {
+        "from": "now-6h",
+        "to": "now"
+      },
+      "timepicker": {},
+      "timezone": "",
+      "title": "Plex Dashboard",
+      "uid": null,
+      "version": 0
+    }
diff --git a/kubernetes/apps/media/plex-exporter/app/kustomization.yaml b/kubernetes/apps/media/plex-exporter/app/kustomization.yaml
new file mode 100755
index 0000000000..e7fdf10f9c
--- /dev/null
+++ b/kubernetes/apps/media/plex-exporter/app/kustomization.yaml
@@ -0,0 +1,11 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: media
+resources:
+  - configmap.yaml
+  - deployment.yaml
+  - service.yaml
+  - service-monitor.yaml
+  - grafana-dashboard.yaml
diff --git a/kubernetes/apps/media/plex-exporter/app/plex-exporter-grafana-dashboard.json b/kubernetes/apps/media/plex-exporter/app/plex-exporter-grafana-dashboard.json
new file mode 100755
index 0000000000..c7dcb5f107
--- /dev/null
+++ b/kubernetes/apps/media/plex-exporter/app/plex-exporter-grafana-dashboard.json
@@ -0,0 +1,464 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "target": {
+          "limit": 100,
+          "matchAny": false,
+          "tags": [],
+          "type": "dashboard"
+        },
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "fiscalYearStartMonth": 0,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "id": null,
+  "links": [],
+  "liveNow": false,
+  "panels": [
+    {
+      "datasource": null,
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 6,
+        "y": 0
+      },
+      "id": 12,
+      "options": {
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "text": {}
+      },
+      "pluginVersion": "8.2.1",
+      "targets": [
+        {
+          "exemplar": true,
+          "expr": "sum(plex_transcode_sessions_active_count) by (container)",
+          "interval": "",
+          "legendFormat": "Plex sessions",
+          "refId": "A"
+        },
+        {
+          "exemplar": true,
+          "expr": "sum(plex_transcode_sessions_active_count) by (container)",
+          "hide": false,
+          "interval": "",
+          "legendFormat": "Transcoding sessions",
+          "refId": "B"
+        }
+      ],
+      "title": "Current usage",
+      "type": "gauge"
+    },
+    {
+      "collapsed": false,
+      "datasource": null,
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 8
+      },
+      "id": 14,
+      "panels": [],
+      "title": "Sessions history",
+      "type": "row"
+    },
+    {
+      "datasource": null,
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 9
+      },
+      "id": 8,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom"
+        },
+        "tooltip": {
+          "mode": "single"
+        }
+      },
+      "targets": [
+        {
+          "exemplar": true,
+          "expr": "sum(plex_transcode_sessions_active_count) by (container)",
+          "interval": "",
+          "legendFormat": "Active plex sessions",
+          "refId": "A"
+        }
+      ],
+      "title": "Active plex sessions",
+      "type": "timeseries"
+    },
+    {
+      "datasource": null,
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 9
+      },
+      "id": 10,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom"
+        },
+        "tooltip": {
+          "mode": "single"
+        }
+      },
+      "targets": [
+        {
+          "exemplar": true,
+          "expr": "sum(plex_transcode_sessions_active_count) by (container)",
+          "interval": "",
+          "legendFormat": "Active transcode sessions",
+          "refId": "A"
+        }
+      ],
+      "title": "Active transcode sessions",
+      "type": "timeseries"
+    },
+    {
+      "collapsed": false,
+      "datasource": null,
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 17
+      },
+      "id": 6,
+      "panels": [],
+      "title": "Library content",
+      "type": "row"
+    },
+    {
+      "datasource": null,
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 9,
+        "w": 12,
+        "x": 0,
+        "y": 18
+      },
+      "id": 4,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom"
+        },
+        "tooltip": {
+          "mode": "single"
+        }
+      },
+      "targets": [
+        {
+          "exemplar": true,
+          "expr": "plex_media_server_library_media_count{type=\"show\"}",
+          "interval": "",
+          "legendFormat": "Library \"{{name}}\"",
+          "refId": "A"
+        }
+      ],
+      "title": "Numbers of TV shows",
+      "type": "timeseries"
+    },
+    {
+      "datasource": null,
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 9,
+        "w": 12,
+        "x": 12,
+        "y": 18
+      },
+      "id": 2,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom"
+        },
+        "tooltip": {
+          "mode": "single"
+        }
+      },
+      "targets": [
+        {
+          "exemplar": true,
+          "expr": "plex_media_server_library_media_count{type=\"movie\"}",
+          "interval": "",
+          "legendFormat": "Library \"{{name}}\"",
+          "refId": "A"
+        }
+      ],
+      "title": "Numbers of movies",
+      "type": "timeseries"
+    }
+  ],
+  "schemaVersion": 31,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "timepicker": {},
+  "timezone": "",
+  "title": "Plex Dashboard",
+  "uid": null,
+  "version": 0
+}
diff --git a/kubernetes/apps/media/plex-exporter/app/service-monitor.yaml b/kubernetes/apps/media/plex-exporter/app/service-monitor.yaml
new file mode 100755
index 0000000000..aeb22449d7
--- /dev/null
+++ b/kubernetes/apps/media/plex-exporter/app/service-monitor.yaml
@@ -0,0 +1,18 @@
+---
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+  name: prometheus-plex-exporter
+  labels:
+    release: prometheus
+spec:
+  jobLabel: plex-exporter
+  selector:
+    matchExpressions:
+      - {key: app, operator: In, values: [plex-exporter]}
+  namespaceSelector:
+    matchNames:
+      - media
+  endpoints:
+    - port: metrics
+      interval: 30s
diff --git a/kubernetes/apps/media/plex-exporter/app/service.yaml b/kubernetes/apps/media/plex-exporter/app/service.yaml
new file mode 100755
index 0000000000..821916b21f
--- /dev/null
+++ b/kubernetes/apps/media/plex-exporter/app/service.yaml
@@ -0,0 +1,16 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: plex-exporter
+  name: plex-exporter-svc
+spec:
+  ports:
+    - name: metrics
+      port: 9567
+      protocol: TCP
+      targetPort: 9567
+  selector:
+    app: plex-exporter
+  type: ClusterIP
diff --git a/kubernetes/apps/media/plex-exporter/ks.yaml b/kubernetes/apps/media/plex-exporter/ks.yaml
new file mode 100644
index 0000000000..6d6ffab67c
--- /dev/null
+++ b/kubernetes/apps/media/plex-exporter/ks.yaml
@@ -0,0 +1,21 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app plex-exporter
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: media
+  path: ./kubernetes/apps/media/plex-exporter/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/media/plex-trakt-sync/app/config/config.yml b/kubernetes/apps/media/plex-trakt-sync/app/config/config.yml
new file mode 100644
index 0000000000..07de0ad3b0
--- /dev/null
+++ b/kubernetes/apps/media/plex-trakt-sync/app/config/config.yml
@@ -0,0 +1,34 @@
+cache:
+  path: $PTS_CACHE_DIR/trakt_cache
+
+excluded-libraries:
+  - Privat
+
+config:
+  dotenv_override: true
+
+logging:
+  append: false
+  debug: false
+  filename: plextraktsync.log
+
+sync:
+  plex_to_trakt:
+    collection: false
+    ratings: false
+    watched_status: true
+  trakt_to_plex:
+    liked_lists: false
+    ratings: false
+    watched_status: true
+    watchlist: false
+
+watch:
+  add_collection: false
+  remove_collection: false
+  scrobble_threshold: 90
+  username_filter: true
+
+xbmc-providers:
+  movies: imdb
+  shows: tvdb
diff --git a/kubernetes/apps/media/plex-trakt-sync/app/helmrelease.yaml b/kubernetes/apps/media/plex-trakt-sync/app/helmrelease.yaml
new file mode 100644
index 0000000000..23e1974083
--- /dev/null
+++ b/kubernetes/apps/media/plex-trakt-sync/app/helmrelease.yaml
@@ -0,0 +1,124 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: plex-trakt-sync
+  namespace: media
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    controllers:
+      app:
+        strategy: Recreate
+        annotations:
+          reloader.stakater.com/auto: "true"
+
+        pod:
+          enableServiceLinks: false
+          securityContext:
+            runAsUser: 1000
+            runAsGroup: 1000
+            fsGroup: 1000
+            fsGroupChangePolicy: OnRootMismatch
+
+        containers:
+          main:
+            image:
+              repository: ghcr.io/taxel/plextraktsync
+              tag: 0.30.10
+            args:
+              - watch
+            env:
+              PLEX_BASEURL: http://10.20.30.40:32400
+              PLEX_LOCALURL: http://10.20.30.40:32400
+              PLEX_TOKEN: ${SECRET_PLEXTOKEN}
+              PLEX_USERNAME: ${SECRET_ACME_EMAIL}
+              TRAKT_USERNAME: ${SECRET_ACME_EMAIL}
+            probes:
+              liveness:
+                enabled: false
+              readiness:
+                enabled: false
+              startup:
+                enabled: false
+            resources:
+              requests:
+                cpu: 5m
+                memory: 101M
+              limits:
+                memory: 101M
+
+      cronjob:
+        type: cronjob
+        cronjob:
+          concurrencyPolicy: Forbid
+          schedule: "@daily"
+        annotations:
+          reloader.stakater.com/auto: "true"
+
+        pod:
+          enableServiceLinks: false
+          securityContext:
+            runAsUser: 1000
+            runAsGroup: 1000
+            fsGroup: 1000
+            fsGroupChangePolicy: OnRootMismatch
+
+        containers:
+          main:
+            image:
+              repository: ghcr.io/taxel/plextraktsync
+              tag: 0.30.10
+            args:
+              - sync
+            env:
+              PLEX_BASEURL: http://10.20.30.40:32400
+              PLEX_LOCALURL: http://10.20.30.40:32400
+              PLEX_TOKEN: ${SECRET_PLEXTOKEN}
+              PLEX_USERNAME: ${SECRET_ACME_EMAIL}
+              TRAKT_USERNAME: ${SECRET_ACME_EMAIL}
+            probes:
+              liveness:
+                enabled: false
+              readiness:
+                enabled: false
+              startup:
+                enabled: false
+            resources:
+              requests:
+                cpu: 5m
+                memory: 101M
+              limits:
+                memory: 101M
+
+    persistence:
+      config-yaml:
+        type: configMap
+        name: plex-tract-sync-configmap
+        globalMounts:
+          - path: /app/config/config.yml
+            subPath: config.yml
+            readOnly: true
+      config-pv:
+        enabled: true
+        type: persistentVolumeClaim
+        accessMode: ReadWriteOnce
+        size: 5Gi
+        storageClass: ${MAIN_SC}
+        globalMounts:
+          - path: /app/config
diff --git a/kubernetes/apps/media/plex-trakt-sync/app/kustomization.yaml b/kubernetes/apps/media/plex-trakt-sync/app/kustomization.yaml
new file mode 100644
index 0000000000..c93f583aac
--- /dev/null
+++ b/kubernetes/apps/media/plex-trakt-sync/app/kustomization.yaml
@@ -0,0 +1,20 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: media
+resources:
+- ./helmrelease.yaml
+configMapGenerator:
+- files:
+  - ./config/config.yml
+  name: plex-tract-sync-configmap
+generatorOptions:
+  annotations:
+    kustomize.toolkit.fluxcd.io/substitute: disabled
+  disableNameSuffixHash: true
+labels:
+- includeSelectors: true
+  pairs:
+    app.kubernetes.io/instance: plex-trakt-sync
+    app.kubernetes.io/name: plex-trakt-sync
diff --git a/kubernetes/apps/media/plex-trakt-sync/ks.yaml b/kubernetes/apps/media/plex-trakt-sync/ks.yaml
new file mode 100644
index 0000000000..10077d724f
--- /dev/null
+++ b/kubernetes/apps/media/plex-trakt-sync/ks.yaml
@@ -0,0 +1,22 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app plex-trakt-sync
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: media
+  dependsOn:
+  path: ./kubernetes/apps/media/plex-trakt-sync/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/media/podsync/app/config/config.toml b/kubernetes/apps/media/podsync/app/config/config.toml
new file mode 100755
index 0000000000..8c847e85fc
--- /dev/null
+++ b/kubernetes/apps/media/podsync/app/config/config.toml
@@ -0,0 +1,110 @@
+[server]
+port = 8080
+hostname = "https://podsync.${SECRET_DOMAIN}"
+
+[storage]
+  [storage.local]
+    data_dir = "/data"
+
+[database]
+badger = { truncate = true, file_io = true }
+
+[tokens]
+youtube = "${SECRET_YOUTUBE_TOKEN}"
+
+[feeds]
+  [feeds.DevOpsToolkit]
+  url = "https://www.youtube.com/playlist?list=UUfz8x0lVzJpb_dgWm9kPVrw"
+  update_period = "8h"
+  format = "video"
+  quality = "high"
+  page_size = 15
+  clean = { keep_last = 15 }
+
+  [feeds.TechWorldWithNana]
+  url = "https://www.youtube.com/channel/UCdngmbVKX1Tgre699-XLlUA"
+  update_period = "8h"
+  format = "video"
+  quality = "high"
+  page_size = 15
+  clean = { keep_last = 15 }
+
+  [feeds.bares]
+  url = "https://www.youtube.com/channel/UC53bIpnef1pwAx69ERmmOLA"
+  update_period = "8h"
+  format = "video"
+  quality = "high"
+  page_size = 10
+  clean = { keep_last = 10 }
+
+  [feeds.headlock_breaking]
+  url = "https://www.youtube.com/channel/UC4A1tkY5mw9MWYa9SnDi3CQ"
+  filters = { title = "(B|b)(R|r)(E|e)(A|a)(K|k)(I|i)(N|n)(G|g)" }
+  custom = { title = "Headlock Breaking News" }
+  update_period = "6h"
+  format = "audio"
+  quality = "high"
+  page_size = 5
+  clean = { keep_last = 5 }
+
+  [feeds.lffs]
+  url = "https://www.youtube.com/playlist?list=PLDLU7Rp1Fecmx3mjQJU97rn5gfV8NH8AW"
+  update_period = "9h"
+  format = "video"
+  quality = "high"
+  page_size = 5
+  clean = { keep_last = 5 }
+
+  [feeds.mytowatch]
+  url = "https://youtube.com/playlist?list=PL7qBQ0Mi2xAp-alNW5mJ7z4X9Zko9rpWJ"
+  update_period = "7h"
+  format = "video"
+  quality = "high"
+  page_size = 30
+  clean = { keep_last = 30 }
+
+  [feeds.oralsessions]
+  url = "https://www.youtube.com/playlist?list=PLquP20HDxBb19HgiRawKoiTkHtHT473qG"
+  custom = { title = "The Sessions x AEW" }
+  update_period = "12h"
+  format = "video"
+  quality = "high"
+  page_size = 5
+  clean = { keep_last = 5 }
+
+  [feeds.spotfight_others]
+  url = "https://www.youtube.com/channel/UCROVkXXxpp_wisE7YyrTYyQ"
+  filters = { not_title = "(HAUPTKAMPF|Review)" }
+  custom = { title = "Spotfight Alles Andere" }
+  update_period = "12h"
+  format = "video"
+  quality = "high"
+  page_size = 15
+  clean = { keep_last = 5 }
+
+  [feeds.spotfightnews]
+  url = "https://www.youtube.com/channel/UCdsHMu1VHhZ8SlrdUQa457w"
+  update_period = "1h"
+  format = "audio"
+  quality = "high"
+  page_size = 13
+  clean = { keep_last = 13 }
+
+  [feeds.yt_fotografieren]
+  url = "https://www.youtube.com/playlist?list=PL7qBQ0Mi2xApoNGSCObcehoOnw9S2YTpc"
+  update_period = "7h"
+  format = "video"
+  quality = "high"
+  page_size = 30
+  clean = { keep_last = 30 }
+
+  [feeds.yt_kubernetes]
+  url = "https://www.youtube.com/playlist?list=PL7qBQ0Mi2xApcBPl9Ve3RVBD6CgQKaDY4"
+  update_period = "5h"
+  format = "video"
+  quality = "high"
+  page_size = 30
+  clean = { keep_last = 30 }
+
+[downloader]
+self_update = true
diff --git a/kubernetes/apps/media/podsync/app/helmrelease.yaml b/kubernetes/apps/media/podsync/app/helmrelease.yaml
new file mode 100644
index 0000000000..4919d7b1fc
--- /dev/null
+++ b/kubernetes/apps/media/podsync/app/helmrelease.yaml
@@ -0,0 +1,104 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: &app podsync
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    global:
+      nameOverride: *app
+    controllers:
+      app:
+        strategy: Recreate
+        annotations:
+          reloader.stakater.com/auto: "true"
+        containers:
+          main:
+            image:
+              repository: registry.eighty-three.me/tuxpeople/podsync
+              tag: v2.7.0
+            env:
+              TZ: ${TIMEZONE}
+            resources:
+              requests:
+                cpu: 5m
+                memory: 60Mi
+              limits:
+                memory: 500Mi
+            probes:
+              liveness:
+                enabled: true
+                custom: true
+                spec:
+                  httpGet:
+                    path: /
+                    port: &port 8080
+              readiness:
+                enabled: true
+                custom: true
+                spec:
+                  httpGet:
+                    path: /
+                    port: *port
+              startup:
+                enabled: true
+                custom: true
+                spec:
+                  httpGet:
+                    path: /
+                    port: *port
+    # defaultPodOptions:
+    #   securityContext:
+    #     runAsUser: 1026
+    #     runAsGroup: 100
+    service:
+      app:
+        ports:
+          http:
+            port: *port
+    ingress:
+      app:
+        enabled: true
+        ingressClassName: internal
+        annotations:
+          external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+        hosts:
+          - host: &host podsync.${SECRET_DOMAIN}
+            paths:
+              - path: /
+                pathType: Prefix
+                service:
+                  identifier: app
+                  port: *port
+        tls:
+          - hosts:
+              - ${SECRET_DOMAIN/./-}-production-tls
+    persistence:
+      config-toml:
+        type: configMap
+        name: podsync-configmap
+        globalMounts:
+          - path: /app/config.toml
+            subPath: config.toml
+            readOnly: true
+      data:
+        enabled: true
+        type: nfs
+        server: 10.20.30.40
+        path: /volume2/data/media/podcasts
diff --git a/kubernetes/apps/media/podsync/app/kustomization.yaml b/kubernetes/apps/media/podsync/app/kustomization.yaml
new file mode 100644
index 0000000000..2d1c823d7c
--- /dev/null
+++ b/kubernetes/apps/media/podsync/app/kustomization.yaml
@@ -0,0 +1,12 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
+configMapGenerator:
+  - name: podsync-configmap
+    files:
+      - ./config/config.toml
+generatorOptions:
+  disableNameSuffixHash: true
diff --git a/kubernetes/apps/media/podsync/ks.dis b/kubernetes/apps/media/podsync/ks.dis
new file mode 100644
index 0000000000..86af7a447c
--- /dev/null
+++ b/kubernetes/apps/media/podsync/ks.dis
@@ -0,0 +1,21 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app podsync
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: media
+  path: ./kubernetes/apps/media/podsync/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false # no flux ks dependents
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/media/tautulli/app/helmrelease.yaml b/kubernetes/apps/media/tautulli/app/helmrelease.yaml
new file mode 100644
index 0000000000..8e5b0cc537
--- /dev/null
+++ b/kubernetes/apps/media/tautulli/app/helmrelease.yaml
@@ -0,0 +1,106 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: tautulli
+  namespace: media
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    controllers:
+      app:
+        annotations:
+          reloader.stakater.com/auto: "true"
+        containers:
+          main:
+            image:
+              repository: ghcr.io/onedr0p/tautulli
+              tag: 2.13.4@sha256:633a57b2f8634feb67811064ec3fa52f40a70641be927fdfda6f5d91ebbd5d73
+            env:
+              TZ: ${TIMEZONE}
+            probes:
+              liveness: &probes
+                enabled: true
+                custom: true
+                spec:
+                  httpGet:
+                    path: /status
+                    port: &port 8181
+                  initialDelaySeconds: 0
+                  periodSeconds: 10
+                  timeoutSeconds: 1
+                  failureThreshold: 3
+              readiness: *probes
+              startup:
+                enabled: false
+            resources:
+              requests:
+                cpu: 100m
+                memory: 128Mi
+              limits:
+                memory: 512Mi
+          jbops:
+            image:
+              repository: registry.k8s.io/git-sync/git-sync
+              tag: v4.2.3
+            env:
+              GITSYNC_REPO: https://github.com/blacktwin/JBOPS
+              GITSYNC_REF: master
+              GITSYNC_PERIOD: 24h
+              GITSYNC_ROOT: /add-ons
+            resources:
+              requests:
+                cpu: 10m
+                memory: 10M
+              limits:
+                memory: 128M
+        pod:
+          securityContext:
+            runAsUser: 568
+            runAsGroup: 568
+            fsGroup: 568
+            fsGroupChangePolicy: OnRootMismatch
+    service:
+      app:
+        controller: app
+        ports:
+          http:
+            port: *port
+    ingress:
+      app:
+        enabled: true
+        className: internal
+        hosts:
+          - host: &host "{{ .Release.Name }}.${SECRET_DOMAIN}"
+            paths:
+              - path: /
+                service:
+                  identifier: app
+                  port: *port
+        tls:
+          - hosts:
+              - *host
+    persistence:
+      config:
+        enabled: true
+        type: persistentVolumeClaim
+        accessMode: ReadWriteOnce
+        size: 5Gi
+        storageClass: ${MAIN_SC}
+      add-ons:
+        type: emptyDir
diff --git a/kubernetes/apps/media/tautulli/app/kustomization.yaml b/kubernetes/apps/media/tautulli/app/kustomization.yaml
new file mode 100644
index 0000000000..b7f2d4ef75
--- /dev/null
+++ b/kubernetes/apps/media/tautulli/app/kustomization.yaml
@@ -0,0 +1,8 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: media
+resources:
+  - ./helmrelease.yaml
+  - ../../../../shared/gatus/external
diff --git a/kubernetes/apps/media/tautulli/exporter/helmrelease.yaml b/kubernetes/apps/media/tautulli/exporter/helmrelease.yaml
new file mode 100644
index 0000000000..d66caae366
--- /dev/null
+++ b/kubernetes/apps/media/tautulli/exporter/helmrelease.yaml
@@ -0,0 +1,68 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: tautulli-exporter
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    controllers:
+      app:
+        annotations:
+          reloader.stakater.com/auto: "true"
+        containers:
+          main:
+            image:
+              repository: registry.eighty-three.me/tuxpeople/tautulli-exporter
+              tag: 0.1.0
+            env:
+              SERVE_PORT: &port 9487
+              TAUTULLI_URI: ${SECRET_TAUTULLI_URL}
+              TAUTULLI_API_KEY: ${SECRET_TAUTULLI_API_KEY}
+            resources:
+              requests:
+                cpu: 5m
+                memory: 36M
+              limits:
+                memory: 128M
+            securityContext:
+              allowPrivilegeEscalation: false
+              readOnlyRootFilesystem: true
+              capabilities:
+                drop:
+                  - ALL
+        pod:
+          securityContext:
+            runAsUser: 568
+            runAsGroup: 568
+            runAsNonRoot: true
+    service:
+      app:
+        controller: app
+        ports:
+          http:
+            port: *port
+    serviceMonitor:
+      main:
+        serviceName: main
+        endpoints:
+          - port: metrics
+            scheme: http
+            path: /metrics
+            interval: 1m
+            scrapeTimeout: 10s
diff --git a/kubernetes/apps/media/tautulli/exporter/kustomization.yaml b/kubernetes/apps/media/tautulli/exporter/kustomization.yaml
new file mode 100644
index 0000000000..94f7a61744
--- /dev/null
+++ b/kubernetes/apps/media/tautulli/exporter/kustomization.yaml
@@ -0,0 +1,7 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: media
+resources:
+  - ./helmrelease.yaml
diff --git a/kubernetes/apps/media/tautulli/ks.yaml b/kubernetes/apps/media/tautulli/ks.yaml
new file mode 100644
index 0000000000..b1c76bb86b
--- /dev/null
+++ b/kubernetes/apps/media/tautulli/ks.yaml
@@ -0,0 +1,49 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app tautulli
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: media
+  dependsOn:
+  path: ./kubernetes/apps/media/tautulli/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
+
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app tautulli-exporter
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: media
+  dependsOn:
+    - name: tautulli
+  path: ./kubernetes/apps/media/tautulli/exporter
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/network/cloudflared/app/configs/config.yaml b/kubernetes/apps/network/cloudflared/app/configs/config.yaml
new file mode 100644
index 0000000000..05bcef5cff
--- /dev/null
+++ b/kubernetes/apps/network/cloudflared/app/configs/config.yaml
@@ -0,0 +1,10 @@
+---
+originRequest:
+  originServerName: "external.${SECRET_DOMAIN}"
+
+ingress:
+  - hostname: "${SECRET_DOMAIN}"
+    service: https://ingress-nginx-external-controller.network.svc.cluster.local:443
+  - hostname: "*.${SECRET_DOMAIN}"
+    service: https://ingress-nginx-external-controller.network.svc.cluster.local:443
+  - service: http_status:404
diff --git a/kubernetes/apps/network/cloudflared/app/dnsendpoint.yaml b/kubernetes/apps/network/cloudflared/app/dnsendpoint.yaml
new file mode 100644
index 0000000000..43d7d7b295
--- /dev/null
+++ b/kubernetes/apps/network/cloudflared/app/dnsendpoint.yaml
@@ -0,0 +1,10 @@
+---
+apiVersion: externaldns.k8s.io/v1alpha1
+kind: DNSEndpoint
+metadata:
+  name: cloudflared
+spec:
+  endpoints:
+    - dnsName: "external.${SECRET_DOMAIN}"
+      recordType: CNAME
+      targets: ["${SECRET_CLOUDFLARE_TUNNEL_ID}.cfargotunnel.com"]
diff --git a/kubernetes/apps/network/cloudflared/app/helmrelease.yaml b/kubernetes/apps/network/cloudflared/app/helmrelease.yaml
new file mode 100644
index 0000000000..c10a58dd9e
--- /dev/null
+++ b/kubernetes/apps/network/cloudflared/app/helmrelease.yaml
@@ -0,0 +1,109 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: cloudflared
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    controllers:
+      cloudflared:
+        strategy: RollingUpdate
+        annotations:
+          reloader.stakater.com/auto: "true"
+        containers:
+          app:
+            image:
+              repository: docker.io/cloudflare/cloudflared
+              tag: 2024.5.0
+            env:
+              NO_AUTOUPDATE: true
+              TUNNEL_CRED_FILE: /etc/cloudflared/creds/credentials.json
+              TUNNEL_METRICS: 0.0.0.0:8080
+              TUNNEL_ORIGIN_ENABLE_HTTP2: true
+              TUNNEL_TRANSPORT_PROTOCOL: quic
+              TUNNEL_POST_QUANTUM: true
+              TUNNEL_ID:
+                valueFrom:
+                  secretKeyRef:
+                    name: cloudflared-secret
+                    key: TUNNEL_ID
+            args:
+              - tunnel
+              - --config
+              - /etc/cloudflared/config/config.yaml
+              - run
+              - "$(TUNNEL_ID)"
+            probes:
+              liveness: &probes
+                enabled: true
+                custom: true
+                spec:
+                  httpGet:
+                    path: /ready
+                    port: &port 8080
+                  initialDelaySeconds: 0
+                  periodSeconds: 10
+                  timeoutSeconds: 1
+                  failureThreshold: 3
+              readiness: *probes
+            securityContext:
+              allowPrivilegeEscalation: false
+              readOnlyRootFilesystem: true
+              capabilities: { drop: ["ALL"] }
+            resources:
+              requests:
+                cpu: 10m
+              limits:
+                memory: 256Mi
+    defaultPodOptions:
+      securityContext:
+        runAsNonRoot: true
+        runAsUser: 65534
+        runAsGroup: 65534
+        seccompProfile: { type: RuntimeDefault }
+    service:
+      app:
+        controller: cloudflared
+        ports:
+          http:
+            port: *port
+    serviceMonitor:
+      app:
+        serviceName: cloudflared
+        endpoints:
+          - port: http
+            scheme: http
+            path: /metrics
+            interval: 1m
+            scrapeTimeout: 10s
+    persistence:
+      config:
+        type: configMap
+        name: cloudflared-configmap
+        globalMounts:
+          - path: /etc/cloudflared/config/config.yaml
+            subPath: config.yaml
+            readOnly: true
+      creds:
+        type: secret
+        name: cloudflared-secret
+        globalMounts:
+          - path: /etc/cloudflared/creds/credentials.json
+            subPath: credentials.json
+            readOnly: true
diff --git a/kubernetes/apps/network/cloudflared/app/kustomization.yaml b/kubernetes/apps/network/cloudflared/app/kustomization.yaml
new file mode 100644
index 0000000000..891a864adf
--- /dev/null
+++ b/kubernetes/apps/network/cloudflared/app/kustomization.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./dnsendpoint.yaml
+  - ./secret.sops.yaml
+  - ./helmrelease.yaml
+configMapGenerator:
+  - name: cloudflared-configmap
+    files:
+      - ./configs/config.yaml
+generatorOptions:
+  disableNameSuffixHash: true
diff --git a/kubernetes/apps/network/cloudflared/app/secret.sops.yaml b/kubernetes/apps/network/cloudflared/app/secret.sops.yaml
new file mode 100644
index 0000000000..0d0e3b9ae9
--- /dev/null
+++ b/kubernetes/apps/network/cloudflared/app/secret.sops.yaml
@@ -0,0 +1,27 @@
+apiVersion: v1
+kind: Secret
+metadata:
+    name: cloudflared-secret
+stringData:
+    TUNNEL_ID: ENC[AES256_GCM,data:4dguhd3VxFV83/W/lGuYraNlGgE9ev0JJAUknqqn42q2GsH7,iv:xi1ZljYkIebk8OjoZh3R7CrKtoZp5BdF5EwxOCRuaKU=,tag:u/wt3SKaZ/Jx9FkktA2+Ig==,type:str]
+    credentials.json: ENC[AES256_GCM,data:T3Cais2bI6iNyk+6OopjOhbZPLBBHoZBup6FaGMZuDA4KDRUvhunuCeTKyFh59ajl/Zh8LXOSrpqTnOLTjhqxyxp7QOy6g/zAQbDO732BmOYnDlAl3TT2pWbiEpzaSIr/Hy9K6sbpolaSld8SSvV7I/kNSgJGC5OSEzDdBfOymxp46NUD1S4Ut1RxoruehNv9ljKOR10g/NpMcyUhG6LUbCfnDCJL1jvyqd+3gtvCg==,iv:WdHtanSc7C37LqfMfHVUJFR/zxNFbVTRX4GVzcWHkJo=,tag:7tFfI2YjLjhXq+MFrLtlAw==,type:str]
+sops:
+    kms: []
+    gcp_kms: []
+    azure_kv: []
+    hc_vault: []
+    age:
+        - recipient: age1y0kzuf0tn94a74whazwae4r9qal4snuqfuhl5jacscrpr7up5gts74fe5w
+          enc: |
+            -----BEGIN AGE ENCRYPTED FILE-----
+            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4MU9WVW1tTFlac25WcXpT
+            c21uaFl1V2xUZDcrcEltTnNDUmIzVUJlU2hjCmREeVg4aEtPZmZKa2wwTlB0VVF6
+            MXRvVVA3SFdzL0F6RVFXUk5jMVlSYkUKLS0tIFlDL0UxdkgwUWVnbmJyWVczSm54
+            aVNDVWl5dXRWZ1JWdHlJQ2E5ckdXTHMK3IijAW6UUQVpvzJcW82AK0cSMx3A6d59
+            JB3x4/D0zeWbGi3bS42Hb3J3CLs758hbmXl2B2b7+6kHGHhBBMlmnw==
+            -----END AGE ENCRYPTED FILE-----
+    lastmodified: "2024-06-06T09:08:40Z"
+    mac: ENC[AES256_GCM,data:mdw8exiya2PPcn0n14nglurnSu1FZ73K1KU0fGDDMJ+kvxlF23XY+rcDQQHn0WK+gjw6ULOE0H5e4D0bKONknWNAQHRWNSFASjLBIYTXvIx8CcS/0HAnNSNlxEcwcuZ/7pIB1cgLjTCfRTo3WG3lOSUreDqkta813QaNIv9LjiY=,iv:tl8xxZAIUB1kEZpBKyn+b6q+RVTU5TMXBWrfGd7cex0=,tag:csp3ngNow87AzCGf4mC/4Q==,type:str]
+    pgp: []
+    encrypted_regex: ^(data|stringData)$
+    version: 3.8.1
diff --git a/kubernetes/apps/network/cloudflared/ks.yaml b/kubernetes/apps/network/cloudflared/ks.yaml
new file mode 100644
index 0000000000..da98a0f784
--- /dev/null
+++ b/kubernetes/apps/network/cloudflared/ks.yaml
@@ -0,0 +1,22 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app cloudflared
+  namespace: flux-system
+spec:
+  targetNamespace: network
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  dependsOn:
+    - name: external-dns
+  path: ./kubernetes/apps/network/cloudflared/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/network/echo-server/app/helmrelease.yaml b/kubernetes/apps/network/echo-server/app/helmrelease.yaml
new file mode 100644
index 0000000000..1052569dbe
--- /dev/null
+++ b/kubernetes/apps/network/echo-server/app/helmrelease.yaml
@@ -0,0 +1,91 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: echo-server
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    controllers:
+      echo-server:
+        strategy: RollingUpdate
+        containers:
+          app:
+            image:
+              repository: ghcr.io/mendhak/http-https-echo
+              tag: 33
+            env:
+              HTTP_PORT: &port 8080
+              LOG_WITHOUT_NEWLINE: true
+              LOG_IGNORE_PATH: /healthz
+              PROMETHEUS_ENABLED: true
+            probes:
+              liveness: &probes
+                enabled: true
+                custom: true
+                spec:
+                  httpGet:
+                    path: /healthz
+                    port: *port
+                  initialDelaySeconds: 0
+                  periodSeconds: 10
+                  timeoutSeconds: 1
+                  failureThreshold: 3
+              readiness: *probes
+            securityContext:
+              allowPrivilegeEscalation: false
+              readOnlyRootFilesystem: true
+              capabilities: { drop: ["ALL"] }
+            resources:
+              requests:
+                cpu: 10m
+              limits:
+                memory: 64Mi
+    defaultPodOptions:
+      securityContext:
+        runAsNonRoot: true
+        runAsUser: 65534
+        runAsGroup: 65534
+        seccompProfile: { type: RuntimeDefault }
+    service:
+      app:
+        controller: echo-server
+        ports:
+          http:
+            port: *port
+    serviceMonitor:
+      app:
+        serviceName: echo-server
+        endpoints:
+          - port: http
+            scheme: http
+            path: /metrics
+            interval: 1m
+            scrapeTimeout: 10s
+    ingress:
+      app:
+        className: external
+        annotations:
+          external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+        hosts:
+          - host: "{{ .Release.Name }}.${SECRET_DOMAIN}"
+            paths:
+              - path: /
+                service:
+                  identifier: app
+                  port: http
diff --git a/kubernetes/apps/network/echo-server/app/kustomization.yaml b/kubernetes/apps/network/echo-server/app/kustomization.yaml
new file mode 100644
index 0000000000..53bbb6a453
--- /dev/null
+++ b/kubernetes/apps/network/echo-server/app/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
+  - ../../../../shared/gatus/external
diff --git a/kubernetes/apps/network/echo-server/ks.yaml b/kubernetes/apps/network/echo-server/ks.yaml
new file mode 100644
index 0000000000..dd458ba13c
--- /dev/null
+++ b/kubernetes/apps/network/echo-server/ks.yaml
@@ -0,0 +1,23 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app echo-server
+  namespace: flux-system
+spec:
+  targetNamespace: network
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/network/echo-server/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
diff --git a/kubernetes/apps/network/external-dns/app/helmrelease.yaml b/kubernetes/apps/network/external-dns/app/helmrelease.yaml
new file mode 100644
index 0000000000..5528f9cf02
--- /dev/null
+++ b/kubernetes/apps/network/external-dns/app/helmrelease.yaml
@@ -0,0 +1,48 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: &app external-dns
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: external-dns
+      version: 1.14.4
+      sourceRef:
+        kind: HelmRepository
+        name: external-dns
+        namespace: flux-system
+  install:
+    crds: CreateReplace
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    crds: CreateReplace
+    remediation:
+      strategy: rollback
+      retries: 3
+  values:
+    fullnameOverride: *app
+    provider: cloudflare
+    env:
+      - name: CF_API_TOKEN
+        valueFrom:
+          secretKeyRef:
+            name: external-dns-secret
+            key: api-token
+    extraArgs:
+      - --ingress-class=external
+      - --cloudflare-proxied
+      - --crd-source-apiversion=externaldns.k8s.io/v1alpha1
+      - --crd-source-kind=DNSEndpoint
+    policy: sync
+    sources: ["crd", "ingress"]
+    txtPrefix: k8s.
+    txtOwnerId: default
+    domainFilters: ["${SECRET_DOMAIN}", "${SECRET_CH_DOMAIN}"]
+    serviceMonitor:
+      enabled: true
+    podAnnotations:
+      secret.reloader.stakater.com/reload: external-dns-secret
diff --git a/kubernetes/apps/network/external-dns/app/kustomization.yaml b/kubernetes/apps/network/external-dns/app/kustomization.yaml
new file mode 100644
index 0000000000..95bf4747fd
--- /dev/null
+++ b/kubernetes/apps/network/external-dns/app/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./secret.sops.yaml
+  - ./helmrelease.yaml
diff --git a/kubernetes/apps/network/external-dns/app/secret.sops.yaml b/kubernetes/apps/network/external-dns/app/secret.sops.yaml
new file mode 100644
index 0000000000..721282eff2
--- /dev/null
+++ b/kubernetes/apps/network/external-dns/app/secret.sops.yaml
@@ -0,0 +1,26 @@
+apiVersion: v1
+kind: Secret
+metadata:
+    name: external-dns-secret
+stringData:
+    api-token: ENC[AES256_GCM,data:dOW/EpSJzQCQ9r7dyTTWS+ulMvS9yHVsLo3eqoMo1iYkBJDcrfNHIQ==,iv:Vnh9K/P8niziwU4mIizBYttlCwasu0QKWz5kz+vLW2o=,tag:EtSmZvGRGNEEX9es87m6ZQ==,type:str]
+sops:
+    kms: []
+    gcp_kms: []
+    azure_kv: []
+    hc_vault: []
+    age:
+        - recipient: age1y0kzuf0tn94a74whazwae4r9qal4snuqfuhl5jacscrpr7up5gts74fe5w
+          enc: |
+            -----BEGIN AGE ENCRYPTED FILE-----
+            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCU0Z6andZejNHSUZNNzVj
+            QzRjdTZvYUZ3QnZ1VWJkV0hTbXpHQkMrYlhrCmNHaVF3UXRpNGlMejFUbUp6c3Fj
+            WlMrbGZhOGtvVjlRbHFDY1JCMFR0MXcKLS0tIDY4akk4T2UvNitwZjBaQUZVQ25v
+            UzZ3WURLVlhRTzZXUmlQd3dBcnJPVk0KEfG2v02pW0yDEUUKp1jNlQzH9g5EC2wj
+            HAC/ig576UDuId4RTASDV3Lc0Re9neS5FN7zBm6EDI7fADgBYOm5jw==
+            -----END AGE ENCRYPTED FILE-----
+    lastmodified: "2024-06-06T09:08:40Z"
+    mac: ENC[AES256_GCM,data:5g+ctCbMNQbIe4o2t11KQXjShV0rdhTXUyZzp7sL0aYAbRrJSp9Iit9iogbc7ulTxgPpiFeKLrzNqHEeIsadSGAv0JkfHGIujAP+VYGMWFbe+Mq4jXnmZKtJIVmox5bMYveH5g7+fL9mUgxY6HiG4B3zl0w/SrR3npncm6aKHO4=,iv:T0YNXrBKtu+ETIkkQTNyZXzy/3j1priid0yAT4crYOg=,tag:5VvvR36r25DB0YBhKQqaQw==,type:str]
+    pgp: []
+    encrypted_regex: ^(data|stringData)$
+    version: 3.8.1
diff --git a/kubernetes/apps/network/external-dns/ks.yaml b/kubernetes/apps/network/external-dns/ks.yaml
new file mode 100644
index 0000000000..56b8ed00d9
--- /dev/null
+++ b/kubernetes/apps/network/external-dns/ks.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app external-dns
+  namespace: flux-system
+spec:
+  targetNamespace: network
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/network/external-dns/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/network/ingress-nginx/certificates/kustomization.yaml b/kubernetes/apps/network/ingress-nginx/certificates/kustomization.yaml
new file mode 100644
index 0000000000..76a6679e10
--- /dev/null
+++ b/kubernetes/apps/network/ingress-nginx/certificates/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  # - ./staging.yaml
+  - ./production.yaml
diff --git a/kubernetes/apps/network/ingress-nginx/certificates/production.yaml b/kubernetes/apps/network/ingress-nginx/certificates/production.yaml
new file mode 100644
index 0000000000..b3f8a370a2
--- /dev/null
+++ b/kubernetes/apps/network/ingress-nginx/certificates/production.yaml
@@ -0,0 +1,34 @@
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+  name: "${SECRET_DOMAIN/./-}-production"
+spec:
+  secretName: "${SECRET_DOMAIN/./-}-production-tls"
+  issuerRef:
+    name: letsencrypt-production
+    kind: ClusterIssuer
+  secretTemplate:
+    annotations:
+      replicator.v1.mittwald.de/replicate-to: ".*"
+  commonName: "${SECRET_DOMAIN}"
+  dnsNames:
+    - "${SECRET_DOMAIN}"
+    - "*.${SECRET_DOMAIN}"
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+  name: "${SECRET_CH_DOMAIN/./-}-production"
+spec:
+  secretName: "${SECRET_CH_DOMAIN/./-}-production-tls"
+  issuerRef:
+    name: letsencrypt-production
+    kind: ClusterIssuer
+  secretTemplate:
+    annotations:
+      replicator.v1.mittwald.de/replicate-to: ".*"
+  commonName: "${SECRET_CH_DOMAIN}"
+  dnsNames:
+    - "${SECRET_CH_DOMAIN}"
+    - "*.${SECRET_CH_DOMAIN}"
diff --git a/kubernetes/apps/network/ingress-nginx/certificates/staging.yaml b/kubernetes/apps/network/ingress-nginx/certificates/staging.yaml
new file mode 100644
index 0000000000..6883530001
--- /dev/null
+++ b/kubernetes/apps/network/ingress-nginx/certificates/staging.yaml
@@ -0,0 +1,34 @@
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+  name: "${SECRET_DOMAIN/./-}-staging"
+spec:
+  secretName: "${SECRET_DOMAIN/./-}-staging-tls"
+  issuerRef:
+    name: letsencrypt-staging
+    kind: ClusterIssuer
+  secretTemplate:
+    annotations:
+      replicator.v1.mittwald.de/replicate-to: ".*"
+  commonName: "${SECRET_DOMAIN}"
+  dnsNames:
+    - "${SECRET_DOMAIN}"
+    - "*.${SECRET_DOMAIN}"
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+  name: "${SECRET_CH_DOMAIN/./-}-staging"
+spec:
+  secretName: "${SECRET_CH_DOMAIN/./-}-staging-tls"
+  issuerRef:
+    name: letsencrypt-staging
+    kind: ClusterIssuer
+  secretTemplate:
+    annotations:
+      replicator.v1.mittwald.de/replicate-to: ".*"
+  commonName: "${SECRET_CH_DOMAIN}"
+  dnsNames:
+    - "${SECRET_CH_DOMAIN}"
+    - "*.${SECRET_CH_DOMAIN}"
diff --git a/kubernetes/apps/network/ingress-nginx/external/helmrelease.yaml b/kubernetes/apps/network/ingress-nginx/external/helmrelease.yaml
new file mode 100644
index 0000000000..3565765a03
--- /dev/null
+++ b/kubernetes/apps/network/ingress-nginx/external/helmrelease.yaml
@@ -0,0 +1,75 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: ingress-nginx-external
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: ingress-nginx
+      version: 4.10.1
+      sourceRef:
+        kind: HelmRepository
+        name: ingress-nginx
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  dependsOn:
+    - name: cloudflared
+      namespace: network
+  values:
+    fullnameOverride: ingress-nginx-external
+    controller:
+      service:
+        annotations:
+          external-dns.alpha.kubernetes.io/hostname: "external.${SECRET_DOMAIN}"
+          io.cilium/lb-ipam-ips: "192.168.13.64"
+        externalTrafficPolicy: Cluster
+      ingressClassResource:
+        name: external
+        default: false
+        controllerValue: k8s.io/external
+      admissionWebhooks:
+        objectSelector:
+          matchExpressions:
+            - key: ingress-class
+              operator: In
+              values: ["external"]
+      config:
+        client-body-buffer-size: 100M
+        client-body-timeout: 120
+        client-header-timeout: 120
+        enable-brotli: "true"
+        enable-real-ip: "true"
+        hsts-max-age: 31449600
+        keep-alive-requests: 10000
+        keep-alive: 120
+        log-format-escape-json: "true"
+        log-format-upstream: >
+          {"time": "$time_iso8601", "remote_addr": "$proxy_protocol_addr", "x_forwarded_for": "$proxy_add_x_forwarded_for",
+          "request_id": "$req_id", "remote_user": "$remote_user", "bytes_sent": $bytes_sent, "request_time": $request_time,
+          "status": $status, "vhost": "$host", "request_proto": "$server_protocol", "path": "$uri", "request_query": "$args",
+          "request_length": $request_length, "duration": $request_time, "method": "$request_method", "http_referrer": "$http_referer",
+          "http_user_agent": "$http_user_agent"}
+        proxy-body-size: 0
+        proxy-buffer-size: 16k
+        ssl-protocols: TLSv1.3 TLSv1.2
+      metrics:
+        enabled: true
+        serviceMonitor:
+          enabled: true
+          namespaceSelector:
+            any: true
+      extraArgs:
+        default-ssl-certificate: "network/${SECRET_DOMAIN/./-}-production-tls"
+      resources:
+        requests:
+          cpu: 100m
+        limits:
+          memory: 500Mi
diff --git a/kubernetes/apps/network/ingress-nginx/external/kustomization.yaml b/kubernetes/apps/network/ingress-nginx/external/kustomization.yaml
new file mode 100644
index 0000000000..5dd7baca73
--- /dev/null
+++ b/kubernetes/apps/network/ingress-nginx/external/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
diff --git a/kubernetes/apps/network/ingress-nginx/internal/helmrelease.yaml b/kubernetes/apps/network/ingress-nginx/internal/helmrelease.yaml
new file mode 100644
index 0000000000..156319be82
--- /dev/null
+++ b/kubernetes/apps/network/ingress-nginx/internal/helmrelease.yaml
@@ -0,0 +1,72 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: ingress-nginx-internal
+  namespace: network
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: ingress-nginx
+      version: 4.10.1
+      sourceRef:
+        kind: HelmRepository
+        name: ingress-nginx
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    fullnameOverride: ingress-nginx-internal
+    controller:
+      service:
+        annotations:
+          io.cilium/lb-ipam-ips: "192.168.13.66"
+        externalTrafficPolicy: Cluster
+      ingressClassResource:
+        name: internal
+        default: true
+        controllerValue: k8s.io/internal
+      admissionWebhooks:
+        objectSelector:
+          matchExpressions:
+            - key: ingress-class
+              operator: In
+              values: ["internal"]
+      config:
+        client-body-buffer-size: 100M
+        client-body-timeout: 120
+        client-header-timeout: 120
+        enable-brotli: "true"
+        enable-real-ip: "true"
+        hsts-max-age: 31449600
+        keep-alive-requests: 10000
+        keep-alive: 120
+        log-format-escape-json: "true"
+        log-format-upstream: >
+          {"time": "$time_iso8601", "remote_addr": "$proxy_protocol_addr", "x_forwarded_for": "$proxy_add_x_forwarded_for",
+          "request_id": "$req_id", "remote_user": "$remote_user", "bytes_sent": $bytes_sent, "request_time": $request_time,
+          "status": $status, "vhost": "$host", "request_proto": "$server_protocol", "path": "$uri", "request_query": "$args",
+          "request_length": $request_length, "duration": $request_time, "method": "$request_method", "http_referrer": "$http_referer",
+          "http_user_agent": "$http_user_agent"}
+        proxy-body-size: 0
+        proxy-buffer-size: 16k
+        ssl-protocols: TLSv1.3 TLSv1.2
+      metrics:
+        enabled: true
+        serviceMonitor:
+          enabled: true
+          namespaceSelector:
+            any: true
+      extraArgs:
+        default-ssl-certificate: "network/${SECRET_DOMAIN/./-}-production-tls"
+      resources:
+        requests:
+          cpu: 100m
+        limits:
+          memory: 500Mi
diff --git a/kubernetes/apps/network/ingress-nginx/internal/kustomization.yaml b/kubernetes/apps/network/ingress-nginx/internal/kustomization.yaml
new file mode 100644
index 0000000000..5dd7baca73
--- /dev/null
+++ b/kubernetes/apps/network/ingress-nginx/internal/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
diff --git a/kubernetes/apps/network/ingress-nginx/ks.yaml b/kubernetes/apps/network/ingress-nginx/ks.yaml
new file mode 100644
index 0000000000..570f919471
--- /dev/null
+++ b/kubernetes/apps/network/ingress-nginx/ks.yaml
@@ -0,0 +1,66 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app ingress-nginx-certificates
+  namespace: flux-system
+spec:
+  targetNamespace: network
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  dependsOn:
+    - name: cert-manager-issuers
+  path: ./kubernetes/apps/network/ingress-nginx/certificates
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app ingress-nginx-internal
+  namespace: flux-system
+spec:
+  targetNamespace: network
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  dependsOn:
+    - name: ingress-nginx-certificates
+  path: ./kubernetes/apps/network/ingress-nginx/internal
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app ingress-nginx-external
+  namespace: flux-system
+spec:
+  targetNamespace: network
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  dependsOn:
+    - name: ingress-nginx-certificates
+  path: ./kubernetes/apps/network/ingress-nginx/external
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml b/kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml
new file mode 100644
index 0000000000..63b2bc2fe9
--- /dev/null
+++ b/kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml
@@ -0,0 +1,51 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: k8s-gateway
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: k8s-gateway
+      version: 2.4.0
+      sourceRef:
+        kind: HelmRepository
+        name: k8s-gateway
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    fullnameOverride: k8s-gateway
+    domain: "${SECRET_DOMAIN}"
+    fallthrough:
+      enabled: true
+    ttl: 1
+    service:
+      type: LoadBalancer
+      port: 53
+      annotations:
+        io.cilium/lb-ipam-ips: "192.168.13.65"
+      externalTrafficPolicy: Cluster
+    watchedResources: ["Ingress", "Service"]
+    extraZonePlugins:
+      - name: log
+      - name: errors
+      - name: health
+        configBlock: |-
+          lameduck 5s
+      - name: ready
+      - name: prometheus
+        parameters: 0.0.0.0:9153
+      - name: forward
+        parameters: . tls://1.1.1.1 tls://1.0.0.1
+        configBlock: |-
+          tls_servername cloudflare-dns.com
+      - name: loop
+      - name: reload
+      - name: loadbalance
diff --git a/kubernetes/apps/network/k8s-gateway/app/kustomization.yaml b/kubernetes/apps/network/k8s-gateway/app/kustomization.yaml
new file mode 100644
index 0000000000..5dd7baca73
--- /dev/null
+++ b/kubernetes/apps/network/k8s-gateway/app/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
diff --git a/kubernetes/apps/network/k8s-gateway/ks.yaml b/kubernetes/apps/network/k8s-gateway/ks.yaml
new file mode 100644
index 0000000000..2d4c643f2b
--- /dev/null
+++ b/kubernetes/apps/network/k8s-gateway/ks.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app k8s-gateway
+  namespace: flux-system
+spec:
+  targetNamespace: network
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/network/k8s-gateway/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/network/kustomization.yaml b/kubernetes/apps/network/kustomization.yaml
new file mode 100644
index 0000000000..0d57213686
--- /dev/null
+++ b/kubernetes/apps/network/kustomization.yaml
@@ -0,0 +1,10 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - namespace.yaml
+  - cloudflared/ks.yaml
+  - echo-server/ks.yaml
+  - external-dns/ks.yaml
+  - ingress-nginx/ks.yaml
+  - k8s-gateway/ks.yaml
diff --git a/kubernetes/apps/network/namespace.yaml b/kubernetes/apps/network/namespace.yaml
new file mode 100644
index 0000000000..4d78d7b11b
--- /dev/null
+++ b/kubernetes/apps/network/namespace.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: network
+  labels:
+    kustomize.toolkit.fluxcd.io/prune: disabled
diff --git a/kubernetes/apps/observability/alertmanager-discord/app/alertmanager-discord-config.yaml b/kubernetes/apps/observability/alertmanager-discord/app/alertmanager-discord-config.yaml
new file mode 100755
index 0000000000..7a5c2bbd5d
--- /dev/null
+++ b/kubernetes/apps/observability/alertmanager-discord/app/alertmanager-discord-config.yaml
@@ -0,0 +1,23 @@
+---
+apiVersion: monitoring.coreos.com/v1alpha1
+kind: AlertmanagerConfig
+metadata:
+  name: discord
+  namespace: observability
+  labels:
+    alertmanagerConfig: discord
+spec:
+  route:
+    groupBy:
+      - alertname
+    groupInterval: 10s
+    groupWait: 1s
+    repeatInterval: 30s
+    receiver: discord
+    routes:
+      - matchers:
+          - namespace: "*"
+  receivers:
+    - name: discord
+      webhookConfigs:
+        - url: http://alertmanager-discord:9094
diff --git a/kubernetes/apps/observability/alertmanager-discord/app/alertmanager-discord-deployment.yaml b/kubernetes/apps/observability/alertmanager-discord/app/alertmanager-discord-deployment.yaml
new file mode 100755
index 0000000000..174b31bc98
--- /dev/null
+++ b/kubernetes/apps/observability/alertmanager-discord/app/alertmanager-discord-deployment.yaml
@@ -0,0 +1,35 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: alertmanager-discord
+  namespace: observability
+spec:
+  selector:
+    matchLabels:
+      app: alertmanager-discord
+  template:
+    metadata:
+      labels:
+        app: alertmanager-discord
+    spec:
+      containers:
+        - image: registry.eighty-three.me/tuxpeople/alertmanager-discord:rolling
+          imagePullPolicy: Always
+          name: main
+          ports:
+            - containerPort: 9094
+              name: http
+          resources:
+            requests:
+              cpu: 10m
+              memory: 10Mi
+            limits:
+              cpu: 20m
+              memory: 40Mi
+          env:
+            - name: DISCORD_WEBHOOK
+              valueFrom:
+                secretKeyRef:
+                  key: ALERTMANAGER_DISCORD_WEBHOOK
+                  name: alertmanager-discord-webhook-secret
diff --git a/kubernetes/apps/observability/alertmanager-discord/app/alertmanager-discord-service.yaml b/kubernetes/apps/observability/alertmanager-discord/app/alertmanager-discord-service.yaml
new file mode 100755
index 0000000000..4fcf93148e
--- /dev/null
+++ b/kubernetes/apps/observability/alertmanager-discord/app/alertmanager-discord-service.yaml
@@ -0,0 +1,15 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: alertmanager-discord
+  name: alertmanager-discord
+  namespace: observability
+spec:
+  ports:
+    - name: http
+      port: 9094
+      targetPort: http
+  selector:
+    app: alertmanager-discord
diff --git a/kubernetes/apps/observability/alertmanager-discord/app/externalsecret.yaml b/kubernetes/apps/observability/alertmanager-discord/app/externalsecret.yaml
new file mode 100644
index 0000000000..81e0a62d18
--- /dev/null
+++ b/kubernetes/apps/observability/alertmanager-discord/app/externalsecret.yaml
@@ -0,0 +1,19 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: alertmanager-discord-webhook
+spec:
+  secretStoreRef:
+    kind: ClusterSecretStore
+    name: onepassword
+  target:
+    name: alertmanager-discord-webhook-secret
+    template:
+      engineVersion: v2
+      data:
+        ALERTMANAGER_DISCORD_WEBHOOK: "{{ .ALERTMANAGER_DISCORD_WEBHOOK }}"
+  dataFrom:
+    - extract:
+        key: alertmanager-discord-webhook
diff --git a/kubernetes/apps/observability/alertmanager-discord/app/kustomization.yaml b/kubernetes/apps/observability/alertmanager-discord/app/kustomization.yaml
new file mode 100755
index 0000000000..f70265c662
--- /dev/null
+++ b/kubernetes/apps/observability/alertmanager-discord/app/kustomization.yaml
@@ -0,0 +1,10 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: observability
+resources:
+  - externalsecret.yaml
+  # - alertmanager-discord-config.yaml
+  - alertmanager-discord-deployment.yaml
+  - alertmanager-discord-service.yaml
diff --git a/kubernetes/apps/observability/alertmanager-discord/ks.yaml b/kubernetes/apps/observability/alertmanager-discord/ks.yaml
new file mode 100644
index 0000000000..8baca37080
--- /dev/null
+++ b/kubernetes/apps/observability/alertmanager-discord/ks.yaml
@@ -0,0 +1,23 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app alertmanager-discord
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: observability
+  dependsOn:
+    - name: external-secrets-secretstores
+  path: ./kubernetes/apps/observability/alertmanager-discord/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/observability/gatus/app/config/config.yaml b/kubernetes/apps/observability/gatus/app/config/config.yaml
new file mode 100644
index 0000000000..15db62a47d
--- /dev/null
+++ b/kubernetes/apps/observability/gatus/app/config/config.yaml
@@ -0,0 +1,51 @@
+---
+# Note: Gatus vars should be escaped with $${VAR_NAME} to avoid interpolation by Flux
+web:
+  port: $${CUSTOM_WEB_PORT}
+metrics: true
+debug: true
+storage:
+  type: sqlite
+  path: /data/data.db
+ui:
+  title: Status | Gatus
+  header: Status
+connectivity:
+  checker:
+    target: 1.1.1.1:53
+    interval: 1m
+remote:
+  instances:
+    - url: "http://status.home:8088/api/v1/endpoints/statuses"
+      # endpoint-prefix: "nonkubernetes-"
+alerting:
+  discord:
+    webhook-url: $${GATUS_DISCORD_WEBHOOK}
+    default-alert:
+      # description: "health check failed"
+      send-on-resolved: true
+      failure-threshold: 2
+      success-threshold: 2
+endpoints:
+  # - name: status
+  #   group: external
+  #   url: https://status.${SECRET_DOMAIN}
+  #   interval: 1m
+  #   ui:
+  #     hide-hostname: true
+  #     hide-url: true
+  #   client:
+  #     dns-resolver: tcp://1.1.1.1:53
+  #   conditions:
+  #     - "[STATUS] == 200"
+  - name: flux-webhook
+    group: external-kubernetes
+    url: https://flux-webhook.${SECRET_DOMAIN}
+    interval: 1m
+    ui:
+      hide-hostname: true
+      hide-url: true
+    client:
+      dns-resolver: tcp://1.1.1.1:53
+    conditions:
+      - "[STATUS] == 404"
diff --git a/kubernetes/apps/observability/gatus/app/externalsecret.yaml b/kubernetes/apps/observability/gatus/app/externalsecret.yaml
new file mode 100644
index 0000000000..3aee670dfb
--- /dev/null
+++ b/kubernetes/apps/observability/gatus/app/externalsecret.yaml
@@ -0,0 +1,19 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: gatus-discord-webhook
+spec:
+  secretStoreRef:
+    kind: ClusterSecretStore
+    name: onepassword
+  target:
+    name: gatus-discord-webhook-secret
+    template:
+      engineVersion: v2
+      data:
+        GATUS_DISCORD_WEBHOOK: "{{ .WEBHOOK }}"
+  dataFrom:
+    - extract:
+        key: gatus-discord-webhook
diff --git a/kubernetes/apps/observability/gatus/app/helmrelease.yaml b/kubernetes/apps/observability/gatus/app/helmrelease.yaml
new file mode 100644
index 0000000000..257249a6fe
--- /dev/null
+++ b/kubernetes/apps/observability/gatus/app/helmrelease.yaml
@@ -0,0 +1,126 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: gatus
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    controllers:
+      gatus:
+        annotations:
+          reloader.stakater.com/auto: "true"
+        initContainers:
+          init-config:
+            image:
+              repository: ghcr.io/kiwigrid/k8s-sidecar
+              tag: 1.27.2
+            env:
+              FOLDER: /config
+              LABEL: gatus.io/enabled
+              NAMESPACE: ALL
+              RESOURCE: both
+              UNIQUE_FILENAMES: true
+              METHOD: WATCH
+            restartPolicy: Always
+            resources: &resources
+              requests:
+                cpu: 10m
+              limits:
+                memory: 256Mi
+        containers:
+          app:
+            image:
+              repository: ghcr.io/twin/gatus
+              tag: v5.11.0
+            env:
+              TZ: Europe/Zurich
+              GATUS_CONFIG_PATH: /config
+              GATUS_DELAY_START_SECONDS: 5
+              CUSTOM_WEB_PORT: &port 80
+            envFrom:
+              - secretRef:
+                  name: gatus-discord-webhook-secret
+            probes:
+              liveness: &probes
+                enabled: true
+                custom: true
+                spec:
+                  httpGet:
+                    path: /health
+                    port: *port
+                  initialDelaySeconds: 0
+                  periodSeconds: 10
+                  timeoutSeconds: 1
+                  failureThreshold: 3
+              readiness: *probes
+            securityContext:
+              allowPrivilegeEscalation: false
+              readOnlyRootFilesystem: true
+              capabilities: { drop: ["ALL"] }
+            resources: *resources
+    service:
+      app:
+        controller: gatus
+        ports:
+          http:
+            port: *port
+    serviceMonitor:
+      app:
+        serviceName: gatus
+        endpoints:
+          - port: http
+            scheme: http
+            path: /metrics
+            interval: 1m
+            scrapeTimeout: 10s
+    ingress:
+      app:
+        className: external
+        annotations:
+          external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+        hosts:
+          - host: &host "status.${SECRET_DOMAIN}"
+            paths:
+              - path: /
+                service:
+                  identifier: app
+                  port: http
+        tls:
+          - hosts:
+              - *host
+            secretName: ${SECRET_DOMAIN/./-}-production-tls
+    serviceAccount:
+      create: true
+      name: gatus
+    persistence:
+      config:
+        type: emptyDir
+      config-file:
+        type: configMap
+        name: gatus-configmap
+        globalMounts:
+          - path: /config/config.yaml
+            subPath: config.yaml
+            readOnly: true
+      data:
+        type: emptyDir
+    defaultPodOptions:
+      dnsConfig:
+        options:
+          - { name: ndots, value: "1" }
diff --git a/kubernetes/apps/observability/gatus/app/kustomization.yaml b/kubernetes/apps/observability/gatus/app/kustomization.yaml
new file mode 100644
index 0000000000..7b8cf14537
--- /dev/null
+++ b/kubernetes/apps/observability/gatus/app/kustomization.yaml
@@ -0,0 +1,15 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./rbac.yaml
+  - ./helmrelease.yaml
+  - ./externalsecret.yaml
+  - ../../../../shared/gatus/external
+configMapGenerator:
+  - name: gatus-configmap
+    files:
+      - config.yaml=./config/config.yaml
+generatorOptions:
+  disableNameSuffixHash: true
diff --git a/kubernetes/apps/observability/gatus/app/rbac.yaml b/kubernetes/apps/observability/gatus/app/rbac.yaml
new file mode 100644
index 0000000000..0f12c439b4
--- /dev/null
+++ b/kubernetes/apps/observability/gatus/app/rbac.yaml
@@ -0,0 +1,22 @@
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: gatus
+rules:
+  - apiGroups: [""]
+    resources: ["configmaps", "secrets"]
+    verbs: ["get", "watch", "list"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: gatus
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: gatus
+subjects:
+  - kind: ServiceAccount
+    name: gatus
+    namespace: observability
diff --git a/kubernetes/apps/observability/gatus/ks.yaml b/kubernetes/apps/observability/gatus/ks.yaml
new file mode 100644
index 0000000000..8a0e01098b
--- /dev/null
+++ b/kubernetes/apps/observability/gatus/ks.yaml
@@ -0,0 +1,27 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app gatus
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  dependsOn:
+    - name: external-secrets-secretstores
+  targetNamespace: observability
+  path: ./kubernetes/apps/observability/gatus/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
+      GATUS_SUBDOMAIN: status
diff --git a/kubernetes/apps/observability/kube-prometheus-stack/app/helmrelease.yaml b/kubernetes/apps/observability/kube-prometheus-stack/app/helmrelease.yaml
new file mode 100644
index 0000000000..07d3d480ac
--- /dev/null
+++ b/kubernetes/apps/observability/kube-prometheus-stack/app/helmrelease.yaml
@@ -0,0 +1,452 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: kube-prometheus-stack
+  namespace: observability
+spec:
+  interval: 30m
+  timeout: 15m
+  chart:
+    spec:
+      chart: kube-prometheus-stack
+      version: 59.1.0
+      sourceRef:
+        kind: HelmRepository
+        name: prometheus-community
+        namespace: flux-system
+  install:
+    crds: Skip
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    crds: Skip
+    remediation:
+      strategy: rollback
+      retries: 3
+  dependsOn:
+    - name: prometheus-operator-crds
+      namespace: observability
+  values:
+    crds:
+      enabled: false
+    cleanPrometheusOperatorObjectNames: true
+    alertmanager:
+      enabled: true
+      config:
+        global:
+          slack_api_url: ${SECRET_ALERT_MANAGER_DISCORD_WEBHOOK}
+          resolve_timeout: 5m
+        #  smtp_smarthost: smtp.gmail.com:587
+        #  smtp_auth_username: you@gmail.com
+        #  smtp_auth_password: yourapppassword  # https://support.google.com/mail/answer/185833?hl=en-GB
+        #  smtp_auth_identity: you@gmail.com
+        route:
+          group_by:
+            - alertname
+            - job
+          group_wait: 30s
+          group_interval: 5m
+          repeat_interval: 6h
+          receiver: discord
+          routes:
+            - receiver: "null"
+              match:
+                alertname: InfoInhibitor
+            - receiver: "null"
+              match:
+                alertname: CPUThrottlingHigh
+            - receiver: DeadMansSnitch
+              repeat_interval: 30m
+              match:
+                alertname: Watchdog
+            - receiver: discord
+              matchers:
+                - severity = "critical"
+              continue: true
+              # - receiver: discord
+              #   group_wait: 10s
+              #   match_re:
+              #     issue: Portworx*
+              #   continue: true
+        receivers:
+          - name: "null"
+          - name: email
+            email_configs:
+              - send_resolved: true
+                to: ${SECRET_ACME_EMAIL}
+                from: prometheus@tuxpeople.org
+                smarthost: smtp.utils.svc.cluster.local:25
+                require_tls: false
+          - name: DeadMansSnitch
+            webhook_configs:
+              - url: https://nosnch.in/c15491ac44
+                send_resolved: false
+          - name: discord
+            webhook_configs:
+              - send_resolved: true
+                url: http://alertmanager-discord:9094
+                # title: |-
+                #   [{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ if ne .CommonAnnotations.summary ""}}{{ .CommonAnnotations.summary }}{{ else }}{{ .CommonLabels.alertname }}{{ end }}
+                # text: >-
+                #   {{ range .Alerts -}}
+                #     **Alert:** {{ .Annotations.title }}{{ if .Labels.severity }} - `{{ .Labels.severity }}`{{ end }}
+                #   **Description:** {{ if ne .Annotations.description ""}}{{ .Annotations.description }}{{else}}N/A{{ end }}
+                #   **Details:**
+                #     {{ range .Labels.SortedPairs }} • *{{ .Name }}:* `{{ .Value }}`
+                #     {{ end }}
+                #   {{ end }}
+          # - name: discord
+          #   webhook_configs:
+          #     - send_resolved: true
+          #       url: 'http://alertmanager-discord:9094'
+        # Inhibition rules allow to mute a set of alerts given that another alert is firing.
+        # We use this to mute any warning-level notifications if the same alert is already critical.
+        inhibit_rules:
+          - source_matchers:
+              - severity = "critical"
+            target_matchers:
+              - severity = "warning"
+            equal:
+              - alertname
+              - namespace
+      alertmanagerSpec:
+        replicas: 1
+        podAntiAffinity: hard
+        storage:
+          volumeClaimTemplate:
+            spec:
+              storageClassName: ${MAIN_SC}
+              accessModes:
+                - ReadWriteOnce
+              resources:
+                requests:
+                  storage: 1Gi
+        resources:
+          limits:
+            cpu: 500m
+            memory: 400Mi
+          requests:
+            cpu: 5m
+            memory: 50Mi
+            # priorityClassName: high-priority
+      alertmanagerConfigSelector:
+        matchLabels:
+          alertmanagerConfig: homelab
+      ingress:
+        enabled: true
+        pathType: Prefix
+        ingressClassName: internal
+        annotations:
+          external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+          hajimari.io/enable: "true"
+          hajimari.io/appName: Alertmanager
+          hajimari.io/icon: mdi:alert-decagram-outline
+        tls:
+          - secretName: ${SECRET_DOMAIN/./-}-production-tls
+            hosts:
+              - alertmanager.${SECRET_DOMAIN}
+        hosts:
+          - alertmanager.${SECRET_DOMAIN}
+    grafana:
+      enabled: false
+      forceDeployDashboards: true
+      sidecar:
+        dashboards:
+          multicluster:
+            etcd:
+              enabled: true
+    kubeStateMetrics:
+      enabled: true
+    kube-state-metrics:
+      metricLabelsAllowlist:
+        - pods=[*]
+        - deployments=[*]
+        - persistentvolumeclaims=[*]
+      prometheus:
+        monitor:
+          enabled: true
+          relabelings:
+            - action: replace
+              regex: (.*)
+              replacement: $1
+              sourceLabels: [__meta_kubernetes_pod_node_name]
+              targetLabel: kubernetes_node
+    kubelet:
+      enabled: true
+      serviceMonitor:
+        metricRelabelings:
+          # Remove duplicate metrics
+          - sourceLabels: [__name__]
+            regex: (apiserver_audit|apiserver_client|apiserver_delegated|apiserver_envelope|apiserver_storage|apiserver_webhooks|authentication_token|cadvisor_version|container_blkio|container_cpu|container_fs|container_last|container_memory|container_network|container_oom|container_processes|container|csi_operations|disabled_metric|get_token|go|hidden_metric|kubelet_certificate|kubelet_cgroup|kubelet_container|kubelet_containers|kubelet_cpu|kubelet_device|kubelet_graceful|kubelet_http|kubelet_lifecycle|kubelet_managed|kubelet_node|kubelet_pleg|kubelet_pod|kubelet_run|kubelet_running|kubelet_runtime|kubelet_server|kubelet_started|kubelet_volume|kubernetes_build|kubernetes_feature|machine_cpu|machine_memory|machine_nvm|machine_scrape|node_namespace|plugin_manager|prober_probe|process_cpu|process_max|process_open|process_resident|process_start|process_virtual|registered_metric|rest_client|scrape_duration|scrape_samples|scrape_series|storage_operation|volume_manager|volume_operation|workqueue)_(.+)
+            action: keep
+          - sourceLabels: [node]
+            targetLabel: instance
+            action: replace
+    kubeApiServer:
+      enabled: true
+      serviceMonitor:
+        metricRelabelings:
+          # Remove duplicate metrics
+          - sourceLabels: [__name__]
+            regex: (aggregator_openapi|aggregator_unavailable|apiextensions_openapi|apiserver_admission|apiserver_audit|apiserver_cache|apiserver_cel|apiserver_client|apiserver_crd|apiserver_current|apiserver_envelope|apiserver_flowcontrol|apiserver_init|apiserver_kube|apiserver_longrunning|apiserver_request|apiserver_requested|apiserver_response|apiserver_selfrequest|apiserver_storage|apiserver_terminated|apiserver_tls|apiserver_watch|apiserver_webhooks|authenticated_user|authentication|disabled_metric|etcd_bookmark|etcd_lease|etcd_request|field_validation|get_token|go|grpc_client|hidden_metric|kube_apiserver|kubernetes_build|kubernetes_feature|node_authorizer|pod_security|process_cpu|process_max|process_open|process_resident|process_start|process_virtual|registered_metric|rest_client|scrape_duration|scrape_samples|scrape_series|serviceaccount_legacy|serviceaccount_stale|serviceaccount_valid|watch_cache|workqueue)_(.+)
+            action: keep
+          # Remove high cardinality metrics
+          - sourceLabels: [__name__]
+            regex: (apiserver|etcd|rest_client)_request(|_sli|_slo)_duration_seconds_bucket
+            action: drop
+          - sourceLabels: [__name__]
+            regex: (apiserver_response_sizes_bucket|apiserver_watch_events_sizes_bucket)
+            action: drop
+    kubeControllerManager:
+      enabled: true
+      endpoints: &cp
+        - 192.168.8.111
+        - 192.168.8.112
+        - 192.168.8.113
+      serviceMonitor:
+        metricRelabelings:
+          # Remove duplicate metrics
+          - sourceLabels: [__name__]
+            regex: (apiserver_audit|apiserver_client|apiserver_delegated|apiserver_envelope|apiserver_storage|apiserver_webhooks|attachdetach_controller|authenticated_user|authentication|cronjob_controller|disabled_metric|endpoint_slice|ephemeral_volume|garbagecollector_controller|get_token|go|hidden_metric|job_controller|kubernetes_build|kubernetes_feature|leader_election|node_collector|node_ipam|process_cpu|process_max|process_open|process_resident|process_start|process_virtual|pv_collector|registered_metric|replicaset_controller|rest_client|retroactive_storageclass|root_ca|running_managed|scrape_duration|scrape_samples|scrape_series|service_controller|storage_count|storage_operation|ttl_after|volume_operation|workqueue)_(.+)
+            action: keep
+    kubeEtcd:
+      enabled: true
+      endpoints: *cp
+    kubeProxy:
+      enabled: false # Disabled because eBPF
+    kubeScheduler:
+      enabled: true
+      endpoints: *cp
+      serviceMonitor:
+        metricRelabelings:
+          # Remove duplicate metrics
+          - sourceLabels: [__name__]
+            regex: (apiserver_audit|apiserver_client|apiserver_delegated|apiserver_envelope|apiserver_storage|apiserver_webhooks|authenticated_user|authentication|disabled_metric|go|hidden_metric|kubernetes_build|kubernetes_feature|leader_election|process_cpu|process_max|process_open|process_resident|process_start|process_virtual|registered_metric|rest_client|scheduler|scrape_duration|scrape_samples|scrape_series|workqueue)_(.+)
+            action: keep
+    prometheus:
+      extraFlags:
+        - --web.enable-lifecycle
+      ingress:
+        enabled: true
+        ingressClassName: internal
+        annotations:
+          hajimari.io/appName: Prometheus
+          hajimari.io/icon: simple-icons:prometheus
+          nginx.ingress.kubernetes.io/ssl-redirect: "false"
+        pathType: Prefix
+        hosts:
+          - &host prometheus.${SECRET_DOMAIN}
+        tls:
+          - hosts:
+              - *host
+      prometheusSpec:
+        ruleSelectorNilUsesHelmValues: false
+        serviceMonitorSelectorNilUsesHelmValues: false
+        podMonitorSelectorNilUsesHelmValues: false
+        probeSelectorNilUsesHelmValues: false
+        enableAdminAPI: true
+        walCompression: true
+        retentionSize: 8GB
+        storageSpec:
+          volumeClaimTemplate:
+            spec:
+              storageClassName: ${MAIN_SC}
+              resources:
+                requests:
+                  storage: 10Gi
+        # additionalScrapeConfigs:
+        #   - job_name: node-exporter
+        #     scrape_interval: 1m
+        #     scrape_timeout: 30s
+        #     honor_timestamps: true
+        #     # basic_auth:
+        #     #   username: randomuser
+        #     #   password: examplepassword
+        #     static_configs:
+        #       - targets: # k3s-node1
+        #           - 192.168.8.111:9100
+        #         labels:
+        #           node: "k3s-node1"
+        #       - targets: # k3s-node2
+        #           - 192.168.8.112:9100
+        #         labels:
+        #           node: "k3s-node2"
+        #       - targets: # k3s-node3
+        #           - 192.168.8.113:9100
+        #         labels:
+        #           node: "k3s-node3"
+        #       - targets: # k3s-node4
+        #           - 192.168.8.114:9100
+        #         labels:
+        #           node: "k3s-node4"
+        #       - targets: # NAS
+        #           - 10.20.30.40:9100
+        #         labels:
+        #           node: "nas"
+        #       - targets: # laptop2
+        #           - laptop2.home:9100
+        #         labels:
+        #           node: "laptop-work"
+    prometheus-node-exporter:
+      extraArgs:
+        - --collector.filesystem.ignored-mount-points=^/(dev|proc|sys|var/lib/docker/.+|var/lib/kubelet/.+|boot/firmware)($|/)
+      #  # From archive
+    # prometheus-node-exporter:
+    #   fullnameOverride: node-exporter
+    #   prometheus:
+    #     monitor:
+    #       enabled: true
+    #       relabelings:
+    #         - action: replace
+    #           regex: (.*)
+    #           replacement: $1
+    #           sourceLabels:
+    #             - __meta_kubernetes_pod_node_name
+    #           targetLabel: kubernetes_node
+    # prometheusOperator:
+    #   prometheusConfigReloader:
+    #     resources:
+    #       requests:
+    #         cpu: 100m
+    #         memory: 50Mi
+    #       limits:
+    #         cpu: 200m
+    #         memory: 100Mi
+    # prometheus:
+    #   enabled: true
+    #   persistentVolume:
+    #     enabled: true
+    #     size: 10Gi
+    #   thanosService:
+    #     enabled: true
+    #   thanosServiceMonitor:
+    #     enabled: true
+    #   prometheusSpec:
+    #     replicas: 1
+    #     externalLabels:
+    #       cluster: homelab
+    #     thanos:
+    #       image: quay.io/thanos/thanos:v0.31.0
+    #       objectStorageConfig:
+    #         name: thanos-objstore-secret
+    #         key: objstore.yml
+    #     retention: 12h
+    #     retentionSize: 10GB
+    #     podAntiAffinity: hard
+    #     replicaExternalLabelName: __replica__
+    #     scrapeInterval: 1m
+    #     ruleSelectorNilUsesHelmValues: false
+    #     serviceMonitorSelectorNilUsesHelmValues: false
+    #     podMonitorSelectorNilUsesHelmValues: false
+    #     probeSelectorNilUsesHelmValues: false
+    #     enableAdminAPI: true
+    #     walCompression: true
+    #     disableCompaction: true
+    #     storageSpec:
+    #       volumeClaimTemplate:
+    #         spec:
+    #           storageClassName: ${MAIN_SC}
+    #           resources:
+    #             requests:
+    #               storage: 10Gi
+    #     resources:
+    #       requests:
+    #         cpu: 10m
+    #         memory: 2000Mi
+    #       limits:
+    #         memory: 8000Mi
+    #     additionalScrapeConfigs:
+    #       # - job_name: minio
+    #       #   honor_timestamps: true
+    #       #   metrics_path: /minio/v2/metrics/cluster
+    #       #   static_configs:
+    #       #     - targets:
+    #       #         - "minio.domain.com:9000"
+    #       - job_name: octoprint
+    #         scrape_interval: 1m
+    #         metrics_path: /plugin/prometheus_exporter/metrics
+    #         params:
+    #           apikey:
+    #             - ${SECRET_OCTOPRINTAPI}
+    #         static_configs:
+    #           - targets:
+    #               - octopi.home:80
+    #       - job_name: speedtest-exporter
+    #         scrape_interval: 1m
+    #         scrape_timeout: 30s
+    #         static_configs:
+    #           - targets:
+    #               - speedtest-exporter:9090
+    #       - job_name: minio-job
+    #         bearer_token: ${SECRET_MINIO_BEARERTOKEN}
+    #         metrics_path: /minio/v2/metrics/cluster
+    #         scheme: http
+    #         static_configs:
+    #           - targets:
+    #               - minio.lab.tdeutsch.ch:9091
+    #       - job_name: mystrom-exporter
+    #         scrape_interval: 1m
+    #         metrics_path: /device
+    #         honor_labels: true
+    #         static_configs:
+    #           - targets:
+    #               - 10.20.30.33
+    #             labels:
+    #               alias: 3D Drucker
+    #         relabel_configs:
+    #           - source_labels:
+    #               - __address__
+    #             target_label: __param_target
+    #           - target_label: __address__
+    #             replacement: mystrom-3dprinter:9452
+    #       - job_name: prometheus-pushgateway
+    #         scrape_interval: 1m
+    #         scrape_timeout: 30s
+    #         honor_labels: true
+    #         static_configs:
+    #           - targets:
+    #               - prometheus-pushgateway:9091
+    #       - job_name: wireguard-exporter
+    #         scrape_interval: 1m
+    #         scrape_timeout: 30s
+    #         metrics_path: /metrics
+    #         static_configs:
+    #           - targets:
+    #               - 10.20.30.1:9586
+    #       - job_name: node-exporter
+    #         scrape_interval: 1m
+    #         scrape_timeout: 30s
+    #         honor_timestamps: true
+    #         # basic_auth:
+    #         #   username: randomuser
+    #         #   password: examplepassword
+    #         static_configs:
+    #           - targets:  # k3s-node01
+    #               - 192.168.8.111:9100
+    #           - targets:  # k3s-node02
+    #               - 192.168.8.112:9100
+    #           - targets:  # k3s-node03
+    #               - 192.168.8.113:9100
+    #           - targets:  # NAS
+    #               - 10.20.30.40:9100
+    #   ingress:
+    #     enabled: true
+    #     pathType: Prefix
+    #     ingressClassName: internal
+    #     annotations:
+    #       nginx.ingress.kubernetes.io/auth-method: 'GET'
+    nginx.ingress.kubernetes.io/auth-url: "https://auth.${SECRET_DOMAIN}/api/authz/auth-request"
+    nginx.ingress.kubernetes.io/auth-signin: "https://auth.${SECRET_DOMAIN}?rm=$request_method"
+    nginx.ingress.kubernetes.io/auth-response-headers: "Remote-User,Remote-Name,Remote-Groups,Remote-Email"
+    #       hajimari.io/enable: "true"
+    #       hajimari.io/appName: Prometheus
+    #       hajimari.io/icon: mdi:fire
+    #       external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+    #     tls:
+    #       - secretName: ${SECRET_DOMAIN/./-}-production-tls
+    #         hosts:
+    #           - prometheus.${SECRET_DOMAIN}
+    #     hosts:
+    #       - prometheus.${SECRET_DOMAIN}
diff --git a/kubernetes/apps/observability/kube-prometheus-stack/app/kustomization.yaml b/kubernetes/apps/observability/kube-prometheus-stack/app/kustomization.yaml
new file mode 100644
index 0000000000..dd79397fb5
--- /dev/null
+++ b/kubernetes/apps/observability/kube-prometheus-stack/app/kustomization.yaml
@@ -0,0 +1,8 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: observability
+resources:
+  - ./helmrelease.yaml
+  - ../../../../shared/gatus/internal
diff --git a/kubernetes/apps/observability/kube-prometheus-stack/config/kustomization.yaml b/kubernetes/apps/observability/kube-prometheus-stack/config/kustomization.yaml
new file mode 100644
index 0000000000..1321fd7973
--- /dev/null
+++ b/kubernetes/apps/observability/kube-prometheus-stack/config/kustomization.yaml
@@ -0,0 +1,9 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: observability
+resources:
+  - ./prometheusrules.yaml
+  - ./scrapeconfigs.yaml
+  - ../../../../shared/gatus/internal
diff --git a/kubernetes/apps/observability/kube-prometheus-stack/config/prometheusrules.yaml b/kubernetes/apps/observability/kube-prometheus-stack/config/prometheusrules.yaml
new file mode 100644
index 0000000000..ae12afd86f
--- /dev/null
+++ b/kubernetes/apps/observability/kube-prometheus-stack/config/prometheusrules.yaml
@@ -0,0 +1,26 @@
+apiVersion: monitoring.coreos.com/v1
+kind: PrometheusRule
+metadata:
+  name: homebrew-rules
+spec:
+  groups:
+    - name: homebrew
+      rules:
+        - alert: homebrew_outdated_formulas
+          expr: |
+            homebrew_outdated_formulaes > 0
+          for: 5m
+          labels:
+            severity: critical
+          annotations:
+            description: "There are upgradeable Homebrew formulas on {{$labels.instance}}"
+            summary: "Homebrew formulas can be upgraded"
+        - alert: homebrew_outdated_casks
+          expr: |
+            homebrew_outdated_casks > 0
+          for: 5m
+          labels:
+            severity: critical
+          annotations:
+            description: "There are upgradeable Homebrew casks on {{$labels.instance}}"
+            summary: "Homebrew casks can be upgraded"
diff --git a/kubernetes/apps/observability/kube-prometheus-stack/config/scrapeconfigs.yaml b/kubernetes/apps/observability/kube-prometheus-stack/config/scrapeconfigs.yaml
new file mode 100644
index 0000000000..29e3b104d7
--- /dev/null
+++ b/kubernetes/apps/observability/kube-prometheus-stack/config/scrapeconfigs.yaml
@@ -0,0 +1,35 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/monitoring.coreos.com/scrapeconfig_v1alpha1.json
+apiVersion: monitoring.coreos.com/v1alpha1
+kind: ScrapeConfig
+metadata:
+  name: &name weatherstation
+spec:
+  staticConfigs:
+    - targets:
+        - weatherstation.home:80
+  metricsPath: /metrics
+  relabelings:
+    - action: replace
+      targetLabel: job
+      replacement: *name
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/monitoring.coreos.com/scrapeconfig_v1alpha1.json
+apiVersion: monitoring.coreos.com/v1alpha1
+kind: ScrapeConfig
+metadata:
+  name: &name node-exporter
+spec:
+  staticConfigs:
+    - targets:
+        - diskstation:9100
+        - test-cluster-node01:9100
+        - test-cluster-node02:9100
+        - test-cluster-node03:9100
+        - test-cluster-node04:9100
+        - laptop2:9100
+  metricsPath: /metrics
+  relabelings:
+    - action: replace
+      targetLabel: job
+      replacement: *name
diff --git a/kubernetes/apps/observability/kube-prometheus-stack/ks.yaml b/kubernetes/apps/observability/kube-prometheus-stack/ks.yaml
new file mode 100644
index 0000000000..ded6c7dd6a
--- /dev/null
+++ b/kubernetes/apps/observability/kube-prometheus-stack/ks.yaml
@@ -0,0 +1,54 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app kube-prometheus-stack
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: observability
+  dependsOn:
+    - name: alertmanager-discord
+  path: ./kubernetes/apps/observability/kube-prometheus-stack/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
+      GATUS_SUBDOMAIN: prometheus
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app kube-prometheus-config
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: observability
+  dependsOn:
+    - name: kube-prometheus-stack
+  path: ./kubernetes/apps/observability/kube-prometheus-stack/config
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false # no flux ks dependents
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
+      GATUS_SUBDOMAIN: alertmanager
diff --git a/kubernetes/apps/observability/kustomization.yaml b/kubernetes/apps/observability/kustomization.yaml
new file mode 100644
index 0000000000..e499c49c99
--- /dev/null
+++ b/kubernetes/apps/observability/kustomization.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - namespace.yaml
+  - alertmanager-discord/ks.yaml
+  - gatus/ks.yaml
+  - kube-prometheus-stack/ks.yaml
+  - prometheus-operator-crds/ks.yaml
diff --git a/kubernetes/apps/observability/namespace.yaml b/kubernetes/apps/observability/namespace.yaml
new file mode 100644
index 0000000000..ce3a5bd22a
--- /dev/null
+++ b/kubernetes/apps/observability/namespace.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: observability
+  labels:
+    kustomize.toolkit.fluxcd.io/prune: disabled
diff --git a/kubernetes/apps/observability/prometheus-operator-crds/app/helmrelease.yaml b/kubernetes/apps/observability/prometheus-operator-crds/app/helmrelease.yaml
new file mode 100644
index 0000000000..1a8e3442f0
--- /dev/null
+++ b/kubernetes/apps/observability/prometheus-operator-crds/app/helmrelease.yaml
@@ -0,0 +1,22 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: prometheus-operator-crds
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: prometheus-operator-crds
+      version: 12.0.0
+      sourceRef:
+        kind: HelmRepository
+        name: prometheus-community
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
diff --git a/kubernetes/apps/observability/prometheus-operator-crds/app/kustomization.yaml b/kubernetes/apps/observability/prometheus-operator-crds/app/kustomization.yaml
new file mode 100644
index 0000000000..5dd7baca73
--- /dev/null
+++ b/kubernetes/apps/observability/prometheus-operator-crds/app/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
diff --git a/kubernetes/apps/observability/prometheus-operator-crds/ks.yaml b/kubernetes/apps/observability/prometheus-operator-crds/ks.yaml
new file mode 100644
index 0000000000..19ed2ef9ee
--- /dev/null
+++ b/kubernetes/apps/observability/prometheus-operator-crds/ks.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app prometheus-operator-crds
+  namespace: flux-system
+spec:
+  targetNamespace: observability
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/observability/prometheus-operator-crds/app
+  prune: false # never should be deleted
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/openebs-system/kustomization.yaml b/kubernetes/apps/openebs-system/kustomization.yaml
new file mode 100644
index 0000000000..ca05ea8f40
--- /dev/null
+++ b/kubernetes/apps/openebs-system/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - namespace.yaml
+  - openebs/ks.yaml
diff --git a/kubernetes/apps/openebs-system/namespace.yaml b/kubernetes/apps/openebs-system/namespace.yaml
new file mode 100644
index 0000000000..f173c6c9cd
--- /dev/null
+++ b/kubernetes/apps/openebs-system/namespace.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: openebs-system
+  labels:
+    kustomize.toolkit.fluxcd.io/prune: disabled
diff --git a/kubernetes/apps/openebs-system/openebs/app/helmrelease.yaml b/kubernetes/apps/openebs-system/openebs/app/helmrelease.yaml
new file mode 100644
index 0000000000..975bff303f
--- /dev/null
+++ b/kubernetes/apps/openebs-system/openebs/app/helmrelease.yaml
@@ -0,0 +1,45 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: openebs
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: openebs
+      version: 4.0.1
+      sourceRef:
+        kind: HelmRepository
+        name: openebs
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    engines:
+      local:
+        lvm:
+          enabled: false
+        zfs:
+          enabled: false
+      replicated:
+        mayastor:
+          enabled: false
+    openebs-crds:
+      csi:
+        volumeSnapshots:
+          enabled: false
+    localpv-provisioner:
+      localpv:
+        image:
+          registry: quay.io/
+      hostpathClass:
+        enabled: true
+        name: openebs-hostpath
+        isDefaultClass: false
+        basePath: /var/openebs/local
diff --git a/kubernetes/apps/openebs-system/openebs/app/kustomization.yaml b/kubernetes/apps/openebs-system/openebs/app/kustomization.yaml
new file mode 100644
index 0000000000..5dd7baca73
--- /dev/null
+++ b/kubernetes/apps/openebs-system/openebs/app/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
diff --git a/kubernetes/apps/openebs-system/openebs/ks.yaml b/kubernetes/apps/openebs-system/openebs/ks.yaml
new file mode 100644
index 0000000000..531f679edd
--- /dev/null
+++ b/kubernetes/apps/openebs-system/openebs/ks.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app openebs
+  namespace: flux-system
+spec:
+  targetNamespace: openebs-system
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/openebs-system/openebs/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/productivity/kasm/app/ingress.yaml b/kubernetes/apps/productivity/kasm/app/ingress.yaml
new file mode 100755
index 0000000000..63df3e5269
--- /dev/null
+++ b/kubernetes/apps/productivity/kasm/app/ingress.yaml
@@ -0,0 +1,35 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: kasm
+  annotations:
+    external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+    hajimari.io/enable: "true"
+    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
+    nginx.ingress.kubernetes.io/proxy-ssl-verify: "off"
+    # hajimari.io/icon: printer-3d-nozzle
+    # nginx.ingress.kubernetes.io/auth-method: GET
+    # nginx.ingress.kubernetes.io/auth-url: https://auth.${SECRET_DOMAIN}/api/verify
+    # nginx.ingress.kubernetes.io/auth-signin: https://auth.${SECRET_DOMAIN}?rm=$request_method
+    # nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
+    # nginx.ingress.kubernetes.io/auth-snippet: |
+    #  proxy_set_header X-Forwarded-Method $request_method;
+    #  proxy_set_header X-Forwarded-Scheme $scheme;
+spec:
+  ingressClassName: external
+  tls:
+    - secretName: ${SECRET_DOMAIN/./-}-production-tls
+      hosts:
+        - kasm.${SECRET_DOMAIN}
+  rules:
+    - host: kasm.${SECRET_DOMAIN}
+      http:
+        paths:
+          - path: /
+            pathType: Prefix
+            backend:
+              service:
+                name: kasm
+                port:
+                  number: 443
diff --git a/kubernetes/apps/productivity/kasm/app/kustomization.yaml b/kubernetes/apps/productivity/kasm/app/kustomization.yaml
new file mode 100755
index 0000000000..34979c8ad5
--- /dev/null
+++ b/kubernetes/apps/productivity/kasm/app/kustomization.yaml
@@ -0,0 +1,9 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: productivity
+resources:
+  - ingress.yaml
+  - service.yaml
+  - ../../../../shared/gatus/external
diff --git a/kubernetes/apps/productivity/kasm/app/service.yaml b/kubernetes/apps/productivity/kasm/app/service.yaml
new file mode 100755
index 0000000000..b686313d07
--- /dev/null
+++ b/kubernetes/apps/productivity/kasm/app/service.yaml
@@ -0,0 +1,17 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: kasm
+  # annotations:
+  # traefik.ingress.kubernetes.io/service.serversscheme: https
+  # traefik.ingress.kubernetes.io/service.serverstransport: networking-insecureskipverify@kubernetescrd
+spec:
+  externalName: kasm.vm.tdeutsch.ch
+  ports:
+    - name: https
+      port: 443
+      protocol: TCP
+      targetPort: 443
+  sessionAffinity: None
+  type: ExternalName
diff --git a/kubernetes/apps/productivity/kasm/ks.yaml b/kubernetes/apps/productivity/kasm/ks.yaml
new file mode 100644
index 0000000000..5650f2df72
--- /dev/null
+++ b/kubernetes/apps/productivity/kasm/ks.yaml
@@ -0,0 +1,24 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app kasm
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: productivity
+  path: ./kubernetes/apps/productivity/kasm/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
diff --git a/kubernetes/apps/productivity/kustomization.yaml b/kubernetes/apps/productivity/kustomization.yaml
new file mode 100644
index 0000000000..09e9eb089e
--- /dev/null
+++ b/kubernetes/apps/productivity/kustomization.yaml
@@ -0,0 +1,10 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - namespace.yaml
+  - kasm/ks.yaml
+  - linkding/ks.yaml
+  - octoprint/ks.yaml
+  - paperless/ks.yaml
+  - webtrees/ks.yaml
diff --git a/kubernetes/apps/productivity/linkding/app/externalsecret.yaml b/kubernetes/apps/productivity/linkding/app/externalsecret.yaml
new file mode 100644
index 0000000000..8997dc4b46
--- /dev/null
+++ b/kubernetes/apps/productivity/linkding/app/externalsecret.yaml
@@ -0,0 +1,27 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: linkding-secrets
+spec:
+  secretStoreRef:
+    kind: ClusterSecretStore
+    name: onepassword
+  target:
+    name: linkding-secrets
+    template:
+      engineVersion: v2
+      data:
+        litestream-minio-id: "{{ .AWS_ACCESS_KEY_ID }}"
+        litestream-minio-key: "{{ .AWS_SECRET_ACCESS_KEY }}"
+        litestream-minio-endpoint: "http://{{ .BUCKET_HOST }}"
+        litestream-minio-bucket: "litestream"
+        litestream-age-pubkey: "{{ .LITESTREAM_AGE_PUBKEY }}"
+        litestream-age-secret: "{{ .LITESTREAM_AGE_SECRET }}"
+
+  dataFrom:
+    - extract:
+        key: litestream-age
+    - extract:
+        key: minio-lab
diff --git a/kubernetes/apps/productivity/linkding/app/helmrelease.yaml b/kubernetes/apps/productivity/linkding/app/helmrelease.yaml
new file mode 100644
index 0000000000..7e4492f409
--- /dev/null
+++ b/kubernetes/apps/productivity/linkding/app/helmrelease.yaml
@@ -0,0 +1,190 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: &app linkding
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    controllers:
+      app:
+        annotations:
+          reloader.stakater.com/auto: "true"
+        pod:
+          enableServiceLinks: false
+        containers:
+          main:
+            image:
+              repository: sissbruecker/linkding
+              tag: 1.30.0
+            resources:
+              requests:
+                cpu: 12m
+                memory: 64M
+              limits:
+                memory: 256M
+            env:
+              TZ: ${TIMEZONE}
+              LD_SUPERUSER_NAME: tdeutsch
+              LD_ENABLE_AUTH_PROXY: "True"
+              LD_AUTH_PROXY_USERNAME_HEADER: "HTTP_REMOTE_USER"
+              LD_AUTH_PROXY_LOGOUT_URL: "https://auth.eighty-three.me/"
+              # to use a db see https://github.com/bjw-s/home-ops/blob/main/ kubernetes/apps/selfhosted/linkding/app/helmrelease.yaml
+          litestream: &ls
+            image:
+              repository: "docker.io/litestream/litestream"
+              tag: "0.3.13"
+            args: ["replicate"]
+            env: &lsenv
+              LITESTREAM_ACCESS_KEY_ID:
+                valueFrom:
+                  secretKeyRef:
+                    name: "linkding-secrets"
+                    key: "litestream-minio-id"
+              LITESTREAM_SECRET_ACCESS_KEY:
+                valueFrom:
+                  secretKeyRef:
+                    name: "linkding-secrets"
+                    key: "litestream-minio-key"
+              MINIO_ENDPOINT:
+                valueFrom:
+                  secretKeyRef:
+                    name: "linkding-secrets"
+                    key: "litestream-minio-endpoint"
+              MINIO_BUCKET:
+                valueFrom:
+                  secretKeyRef:
+                    name: "linkding-secrets"
+                    key: "litestream-minio-bucket"
+              AGE_PUBKEY:
+                valueFrom:
+                  secretKeyRef:
+                    name: "linkding-secrets"
+                    key: "litestream-age-pubkey"
+            resources:
+              requests:
+                cpu: 10m
+                memory: 128Mi
+              limits:
+                memory: 1024Mi
+        initContainers:
+          01-litestream-restore:
+            <<: *ls
+            args:
+              [
+                "restore",
+                "-if-db-not-exists",
+                "-if-replica-exists",
+                "/etc/linkding/data/db.sqlite3",
+              ]
+            env:
+              <<: *lsenv
+              AGE_SECRET:
+                valueFrom:
+                  secretKeyRef:
+                    name: "linkding-secrets"
+                    key: "litestream-age-secret"
+    service:
+      app:
+        controller: app
+        ports:
+          http:
+            port: &port 9090
+    ingress:
+      app:
+        enabled: true
+        className: external
+        annotations:
+          nginx.ingress.kubernetes.io/auth-method: GET
+          nginx.ingress.kubernetes.io/auth-url: https://auth.${SECRET_DOMAIN}/api/verify
+          nginx.ingress.kubernetes.io/auth-signin: https://auth.${SECRET_DOMAIN}?rm=$request_method
+          nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
+          nginx.ingress.kubernetes.io/auth-snippet: |
+            proxy_set_header X-Forwarded-Method $request_method;
+            proxy_set_header X-Forwarded-Scheme $scheme;
+          external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+          hajimari.io/icon: "mdi:link-variant"
+        hosts:
+          - host: &host "{{ .Release.Name }}.${SECRET_DOMAIN}"
+            paths:
+              - path: /
+                pathType: Prefix
+                service:
+                  identifier: app
+                  port: *port
+        tls:
+          - hosts:
+              - *host
+            secretName: ${SECRET_DOMAIN/./-}-production-tls
+    persistence:
+      data:
+        enabled: true
+        type: persistentVolumeClaim
+        accessMode: ReadWriteOnce
+        size: 5Gi
+        storageClass: ${MAIN_SC}
+        globalMounts:
+          - path: /etc/linkding/data
+      config:
+        enabled: true
+        type: configMap
+        name: "linkding-config"
+        advancedMounts:
+          app:
+            litestream:
+              - &lsmnt
+                subPath: "litestream-replicate"
+                path: "/etc/litestream.yml"
+                readOnly: true
+            01-litestream-restore:
+              - <<: *lsmnt
+                subPath: "litestream-restore"
+    configMaps:
+      config:
+        enabled: true
+        data:
+          litestream-replicate: |
+            dbs:
+              - path: /etc/linkding/data/db.sqlite3
+                replicas:
+                  - name: "minio"
+                    type: "s3"
+                    endpoint: "$${MINIO_ENDPOINT}"
+                    bucket: "$${MINIO_BUCKET}"
+                    path: "linkding"
+                    force-path-style: true
+                    retention: 168h
+                    validation-interval: 24h
+                    age:
+                      recipients:
+                        - $${AGE_PUBKEY}
+          litestream-restore: |
+            dbs:
+              - path: /etc/linkding/data/db.sqlite3
+                replicas:
+                  - name: "minio"
+                    type: "s3"
+                    endpoint: "$${MINIO_ENDPOINT}"
+                    bucket: "$${MINIO_BUCKET}"
+                    path: "linkding"
+                    force-path-style: true
+                    retention: 168h
+                    validation-interval: 24h
+                    age:
+                      identities:
+                        - $${AGE_SECRET}
diff --git a/kubernetes/apps/productivity/linkding/app/kustomization.yaml b/kubernetes/apps/productivity/linkding/app/kustomization.yaml
new file mode 100644
index 0000000000..2993a5f0bd
--- /dev/null
+++ b/kubernetes/apps/productivity/linkding/app/kustomization.yaml
@@ -0,0 +1,8 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
+  - ./externalsecret.yaml
+  - ../../../../shared/gatus/external
diff --git a/kubernetes/apps/productivity/linkding/ks.yaml b/kubernetes/apps/productivity/linkding/ks.yaml
new file mode 100644
index 0000000000..86287a2092
--- /dev/null
+++ b/kubernetes/apps/productivity/linkding/ks.yaml
@@ -0,0 +1,26 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app linkding
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: productivity
+  dependsOn:
+    - name: external-secrets-secretstores
+  path: ./kubernetes/apps/productivity/linkding/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
diff --git a/kubernetes/apps/productivity/namespace.yaml b/kubernetes/apps/productivity/namespace.yaml
new file mode 100644
index 0000000000..92638dea0b
--- /dev/null
+++ b/kubernetes/apps/productivity/namespace.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: productivity
+  labels:
+    kustomize.toolkit.fluxcd.io/prune: disabled
diff --git a/kubernetes/apps/productivity/octoprint/app/ingress.yaml b/kubernetes/apps/productivity/octoprint/app/ingress.yaml
new file mode 100755
index 0000000000..9685b37e80
--- /dev/null
+++ b/kubernetes/apps/productivity/octoprint/app/ingress.yaml
@@ -0,0 +1,33 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: octoprint
+  annotations:
+    external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+    hajimari.io/enable: "true"
+    hajimari.io/icon: printer-3d-nozzle
+    # nginx.ingress.kubernetes.io/auth-method: GET
+    # nginx.ingress.kubernetes.io/auth-url: https://auth.${SECRET_DOMAIN}/api/verify
+    # nginx.ingress.kubernetes.io/auth-signin: https://auth.${SECRET_DOMAIN}?rm=$request_method
+    # nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
+    # nginx.ingress.kubernetes.io/auth-snippet: |
+    #  proxy_set_header X-Forwarded-Method $request_method;
+    #  proxy_set_header X-Forwarded-Scheme $scheme;
+spec:
+  ingressClassName: external
+  tls:
+    - secretName: ${SECRET_DOMAIN/./-}-production-tls
+      hosts:
+        - print.${SECRET_DOMAIN}
+  rules:
+    - host: print.${SECRET_DOMAIN}
+      http:
+        paths:
+          - path: /
+            pathType: Prefix
+            backend:
+              service:
+                name: octoprint
+                port:
+                  number: 80
diff --git a/kubernetes/apps/productivity/octoprint/app/kustomization.yaml b/kubernetes/apps/productivity/octoprint/app/kustomization.yaml
new file mode 100755
index 0000000000..34979c8ad5
--- /dev/null
+++ b/kubernetes/apps/productivity/octoprint/app/kustomization.yaml
@@ -0,0 +1,9 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: productivity
+resources:
+  - ingress.yaml
+  - service.yaml
+  - ../../../../shared/gatus/external
diff --git a/kubernetes/apps/productivity/octoprint/app/service.yaml b/kubernetes/apps/productivity/octoprint/app/service.yaml
new file mode 100755
index 0000000000..5f6664ab9d
--- /dev/null
+++ b/kubernetes/apps/productivity/octoprint/app/service.yaml
@@ -0,0 +1,14 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: octoprint
+spec:
+  externalName: octopi.home
+  ports:
+    - name: http
+      port: 80
+      protocol: TCP
+      targetPort: 80
+  sessionAffinity: None
+  type: ExternalName
diff --git a/kubernetes/apps/productivity/octoprint/ks.yaml b/kubernetes/apps/productivity/octoprint/ks.yaml
new file mode 100644
index 0000000000..cb3fe0e780
--- /dev/null
+++ b/kubernetes/apps/productivity/octoprint/ks.yaml
@@ -0,0 +1,25 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app octoprint
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: productivity
+  path: ./kubernetes/apps/productivity/octoprint/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
+      GATUS_SUBDOMAIN: print
diff --git a/kubernetes/apps/productivity/paperless/app/helmrelease-gotenberg.yaml b/kubernetes/apps/productivity/paperless/app/helmrelease-gotenberg.yaml
new file mode 100644
index 0000000000..c8e6af88de
--- /dev/null
+++ b/kubernetes/apps/productivity/paperless/app/helmrelease-gotenberg.yaml
@@ -0,0 +1,40 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: paperless-gotenberg
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    controllers:
+      app:
+        annotations:
+          reloader.stakater.com/auto: "true"
+        containers:
+          main:
+            image:
+              repository: docker.io/gotenberg/gotenberg
+              tag: 8.5.1
+            env:
+              DISABLE_GOOGLE_CHROME: "1"
+    service:
+      app:
+        controller: app
+        ports:
+          http:
+            port: 3000
diff --git a/kubernetes/apps/productivity/paperless/app/helmrelease-tika.yaml b/kubernetes/apps/productivity/paperless/app/helmrelease-tika.yaml
new file mode 100644
index 0000000000..91a8f0fac0
--- /dev/null
+++ b/kubernetes/apps/productivity/paperless/app/helmrelease-tika.yaml
@@ -0,0 +1,38 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: paperless-tika
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    controllers:
+      app:
+        annotations:
+          reloader.stakater.com/auto: "true"
+        containers:
+          main:
+            image:
+              repository: ghcr.io/paperless-ngx/tika
+              tag: 2.9.1-full
+    service:
+      app:
+        controller: app
+        ports:
+          http:
+            port: 9998
diff --git a/kubernetes/apps/productivity/paperless/app/helmrelease.yaml b/kubernetes/apps/productivity/paperless/app/helmrelease.yaml
new file mode 100644
index 0000000000..bfc35237ec
--- /dev/null
+++ b/kubernetes/apps/productivity/paperless/app/helmrelease.yaml
@@ -0,0 +1,143 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: paperless
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  dependsOn:
+    - name: paperless-gotenberg
+      namespace: productivity
+    - name: paperless-tika
+      namespace: productivity
+  values:
+    controllers:
+      app:
+        annotations:
+          reloader.stakater.com/auto: "true"
+        containers:
+          main:
+            image:
+              repository: ghcr.io/paperless-ngx/paperless-ngx
+              tag: 2.9.0
+            env:
+              PAPERLESS_SECRET_KEY:
+                valueFrom:
+                  secretKeyRef:
+                    name: paperless-secret
+                    key: PAPERLESS_SECRET_KEY
+              PAPERLESS_URL: https://{{ .Release.Name }}.${SECRET_DOMAIN}
+              PAPERLESS_PORT: "8080"
+              PAPERLESS_TIME_ZONE: ${TIMEZONE}
+              PAPERLESS_WEBSERVER_WORKERS: "1"
+              PAPERLESS_TASK_WORKERS: "1"
+              PAPERLESS_TIKA_ENABLED: "1"
+              PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://paperless-gotenberg:3000
+              PAPERLESS_TIKA_ENDPOINT: http://paperless-tika:9998
+              PAPERLESS_FILENAME_FORMAT:
+                "{created_year}/{correspondent}/{created_year}-{created_month}-{created_day}
+                {title}"
+              # Configure Remote User auth
+              PAPERLESS_ENABLE_HTTP_REMOTE_USER: "true"
+              # Configure folders
+              PAPERLESS_CONSUMPTION_DIR: /nfs/consume
+              PAPERLESS_DATA_DIR: /nfs/data
+              PAPERLESS_EXPORT_DIR: /nfs/export
+              PAPERLESS_MEDIA_ROOT: /nfs/media
+              # Configure folder importer
+              PAPERLESS_CONSUMER_POLLING: "60"
+              PAPERLESS_CONSUMER_RECURSIVE: "true"
+              PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS: "true"
+              # Configure OCR
+              PAPERLESS_OCR_LANGUAGES: deu eng
+              PAPERLESS_OCR_LANGUAGE: deu+eng
+              PAPERLESS_OCR_MODE: skip
+              PAPERLESS_OCR_USER_ARGS: '{"invalidate_digital_signatures": true}'
+              #PAPERLESS_OCR_SKIP_ARCHIVE_FILE: with_text
+              # Configure redis integration
+              PAPERLESS_REDIS: redis://paperless-redis:6379
+              # Configure admin user
+              PAPERLESS_ADMIN_USER:
+                valueFrom:
+                  secretKeyRef:
+                    name: paperless-secret
+                    key: PAPERLESS_ADMIN_USER
+              PAPERLESS_ADMIN_PASSWORD:
+                valueFrom:
+                  secretKeyRef:
+                    name: paperless-secret
+                    key: PAPERLESS_ADMIN_PASSWORD
+            resources:
+              requests:
+                cpu: 500m
+                memory: 700M
+              limits:
+                memory: 2000M
+    service:
+      app:
+        controller: app
+        ports:
+          http:
+            port: &port 8080
+    ingress:
+      app:
+        enabled: true
+        className: internal
+        annotations:
+          hajimari.io/icon: material-symbols:scanner-outline
+          nginx.ingress.kubernetes.io/auth-method: GET
+          nginx.ingress.kubernetes.io/auth-url: https://auth.${SECRET_DOMAIN}/api/verify
+          nginx.ingress.kubernetes.io/auth-signin: https://auth.${SECRET_DOMAIN}?rm=$request_method
+          nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
+          nginx.ingress.kubernetes.io/auth-snippet: |
+            proxy_set_header X-Forwarded-Method $request_method;
+            proxy_set_header X-Forwarded-Scheme $scheme;
+        hosts:
+          - host: &host "{{ .Release.Name }}.${SECRET_DOMAIN}"
+            paths:
+              - path: /
+                pathType: Prefix
+                service:
+                  identifier: app
+                  port: http
+        tls:
+          - hosts:
+              - *host
+            secretName: ${SECRET_DOMAIN/./-}-production-tls
+      public:
+        enabled: true
+        className: internal
+        annotations:
+          external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}"
+        hosts:
+          - host: &host2 "documents.${SECRET_CH_DOMAIN}"
+            paths:
+              - path: /share
+                pathType: Prefix
+                service:
+                  identifier: app
+                  port: http
+        tls:
+          - hosts:
+              - *host2
+            secretName: ${SECRET_CH_DOMAIN/./-}-production-tls
+    persistence:
+      nfs:
+        type: nfs
+        server: 10.20.30.40
+        path: /volume2/scanner
diff --git a/kubernetes/apps/productivity/paperless/app/kustomization.yaml b/kubernetes/apps/productivity/paperless/app/kustomization.yaml
new file mode 100644
index 0000000000..2cdfd74e68
--- /dev/null
+++ b/kubernetes/apps/productivity/paperless/app/kustomization.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./secret.sops.yaml
+  - ./helmrelease.yaml
+  - ./helmrelease-gotenberg.yaml
+  - ./helmrelease-tika.yaml
+  - ../../../../shared/gatus/internal
diff --git a/kubernetes/apps/productivity/paperless/app/secret.sops.yaml b/kubernetes/apps/productivity/paperless/app/secret.sops.yaml
new file mode 100644
index 0000000000..96d7b5dd7f
--- /dev/null
+++ b/kubernetes/apps/productivity/paperless/app/secret.sops.yaml
@@ -0,0 +1,28 @@
+apiVersion: v1
+kind: Secret
+metadata:
+    name: paperless-secret
+stringData:
+    PAPERLESS_ADMIN_USER: ENC[AES256_GCM,data:jwQRqR7sCy0=,iv:EiS5flZ7udBl43sMMinuBk19/rt611QxZOameKLxk+0=,tag:svdAj6UPnXYu4EnLnEntQg==,type:str]
+    PAPERLESS_ADMIN_PASSWORD: ENC[AES256_GCM,data:KOjXQVEjwD8G7wTOB7RjTAZIANUawJboywmq8mER/6x/XCYMrLj8Dtrype+H0o76lTQ=,iv:SYFJZ6rwgAtqZ07RA2ccEZJ/1GmevOVprsXUmbdIEKk=,tag:gaU5UYXlCYDuNlXFGrgbzg==,type:str]
+    PAPERLESS_SECRET_KEY: ENC[AES256_GCM,data:hIAQGN3TBy+aQRPZHJ91fIi0/ZlnzmUZJqpdGW7RcSEDEB+DsRWZGw==,iv:6rDWsj8ahc9tQnpjzSzVGPInsGVe323X7HCGg6TbOG8=,tag:46lD4l4Clx7+3WD2dA8QIg==,type:str]
+sops:
+    kms: []
+    gcp_kms: []
+    azure_kv: []
+    hc_vault: []
+    age:
+        - recipient: age1y0kzuf0tn94a74whazwae4r9qal4snuqfuhl5jacscrpr7up5gts74fe5w
+          enc: |
+            -----BEGIN AGE ENCRYPTED FILE-----
+            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPOGRIQmNTZDJaNUpDbWVq
+            bkwvbklvbkRpVE9RQytkeUNWamFsWUFzRzJBCk1qek5ueFBWYUF4NmptRTBqekxr
+            VXZDazhreHB6UnhGNXZzMTZwV0xxWkkKLS0tIERUQkhOUEhDellUWjNZRU9WU1Z0
+            bWZUWDR1SUwvVnJ1SDAydXV6YS9Db3cKEygUxkjdTZjA9y7i0CHSGdfCrgGOXhp3
+            6+67/ce4guTnhNIxux7dOARTg3gjp4lVAbR4SZFkAbEIMOq1JU63aQ==
+            -----END AGE ENCRYPTED FILE-----
+    lastmodified: "2023-12-12T10:36:29Z"
+    mac: ENC[AES256_GCM,data:M+XOPvMJJK0GIRtE7iFhK854sVTgH+QFzkgYma6UyXUeaQhXqyF1T0Ecs/fmBzpjQNrVVKhmgvREc7zOICHGXmPC6qs+WxXuNKNnWpnZhJm/8UdsuDFUW2ew9CahZZKAUDKKL/J56ND/GfKg6bGpMde+j1XuEOAVM4Z2B51j8Ho=,iv:CC523CMBJbMKq1fQ5yTyEYuIxiHOq+DURdHVsuOidlM=,tag:Hiw79sOFXOEgn6pd76pgXA==,type:str]
+    pgp: []
+    encrypted_regex: ^(data|stringData)$
+    version: 3.8.1
diff --git a/kubernetes/apps/productivity/paperless/ks.yaml b/kubernetes/apps/productivity/paperless/ks.yaml
new file mode 100644
index 0000000000..c84434bc08
--- /dev/null
+++ b/kubernetes/apps/productivity/paperless/ks.yaml
@@ -0,0 +1,45 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app paperless-redis
+  namespace: flux-system
+spec:
+  targetNamespace: productivity
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/productivity/paperless/redis
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app paperless
+  namespace: flux-system
+spec:
+  targetNamespace: productivity
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  dependsOn:
+    - name: paperless-redis
+  path: ./kubernetes/apps/productivity/paperless/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
diff --git a/kubernetes/apps/productivity/paperless/redis/helmrelease.yaml b/kubernetes/apps/productivity/paperless/redis/helmrelease.yaml
new file mode 100644
index 0000000000..e29b5c94c8
--- /dev/null
+++ b/kubernetes/apps/productivity/paperless/redis/helmrelease.yaml
@@ -0,0 +1,43 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: paperless-redis
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    controllers:
+      app:
+        containers:
+          main:
+            image:
+              repository: public.ecr.aws/docker/library/redis
+              tag: 7.2.5
+            resources:
+              requests:
+                cpu: 5m
+                memory: 32M
+              limits:
+                memory: 32M
+    service:
+      app:
+        controller: app
+        ports:
+          redis:
+            enabled: true
+            port: 6379
diff --git a/kubernetes/apps/productivity/paperless/redis/kustomization.yaml b/kubernetes/apps/productivity/paperless/redis/kustomization.yaml
new file mode 100644
index 0000000000..5dd7baca73
--- /dev/null
+++ b/kubernetes/apps/productivity/paperless/redis/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
diff --git a/kubernetes/apps/productivity/webtrees/app/helmrelease.yaml b/kubernetes/apps/productivity/webtrees/app/helmrelease.yaml
new file mode 100644
index 0000000000..9f6d2ce2fe
--- /dev/null
+++ b/kubernetes/apps/productivity/webtrees/app/helmrelease.yaml
@@ -0,0 +1,137 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: webtrees
+  namespace: productivity
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  maxHistory: 2
+  install:
+    crds: CreateReplace
+    createNamespace: true
+    remediation:
+      retries: 3
+  upgrade:
+    crds: CreateReplace
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  uninstall:
+    keepHistory: false
+  dependsOn:
+    - name: webtrees-db
+  values:
+    controllers:
+      app:
+        strategy: Recreate
+        initContainers:
+          01-init-modules:
+            image:
+              repository: busybox
+              pullPolicy: Always
+              tag: latest
+            command: [
+                sh,
+                -c,
+                cd /var/www/html/modules_v4/; wget https://github.com/JesseWebDotCom/webtrees-theme-modern/releases/download/0.0.9/webtrees-theme-modern.0.0.9.zip
+                -O /tmp/webtrees-theme-modern.0.0.9.zip; unzip /tmp/webtrees-theme-modern.0.0.9.zip;
+                rm -f /tmp/webtrees-theme-modern.0.0.9.zip,
+              ]
+        containers:
+          main:
+            image:
+              repository: docker.io/dtjs48jkt/webtrees
+              tag: 2.1.20
+            env:
+              DISABLE_SSL: "TRUE"
+              PRETTYURLSl: "TRUE"
+              ENABLE_REMOTE_USER: "TRUE"
+              HEADER_AUTH_VAR: Remote-User
+              PORT: "80"
+              BASE_URL: https://{{ .Release.Name }}.${SECRET_DOMAIN}
+              DB_HOST: webtrees-db-mariadb
+              DB_NAME: webtrees
+              DB_USER: webtrees
+              DB_PASSWORD:
+                valueFrom:
+                  secretKeyRef:
+                    name: mariadb-secret
+                    key: mariadb-password
+              WT_ADMINPW:
+                valueFrom:
+                  secretKeyRef:
+                    name: webtrees-admin-pass
+                    key: password
+            # probes:
+            #   liveness: &probes
+            #     enabled: true
+            #     custom: true
+            #     spec:
+            #       httpGet:
+            #         path: /
+            #         port: *port
+            #       initialDelaySeconds: 0
+            #       periodSeconds: 10
+            #       timeoutSeconds: 1
+            #       failureThreshold: 3
+            #   readiness: *probes
+            resources:
+              requests:
+                cpu: 5m
+                memory: 10Mi
+              limits:
+                memory: 500Mi
+    service:
+      app:
+        controller: app
+        ports:
+          http:
+            port: &port 80
+    ingress:
+      app:
+        annotations:
+          nginx.ingress.kubernetes.io/auth-method: GET
+          nginx.ingress.kubernetes.io/auth-url: https://auth.${SECRET_DOMAIN}/api/verify
+          nginx.ingress.kubernetes.io/auth-signin: https://auth.${SECRET_DOMAIN}?rm=$request_method
+          nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
+          nginx.ingress.kubernetes.io/auth-snippet: |
+            proxy_set_header X-Forwarded-Method $request_method;
+            proxy_set_header X-Forwarded-Scheme $scheme;
+        enabled: true
+        className: internal
+        hosts:
+          - host: &host "{{ .Release.Name }}.${SECRET_DOMAIN}"
+            paths:
+              - path: /
+                pathType: Prefix
+                service:
+                  identifier: app
+                  port: *port
+        tls:
+          - hosts:
+              - *host
+    persistence:
+      data:
+        enabled: true
+        type: persistentVolumeClaim
+        accessMode: ReadWriteOnce
+        size: 5Gi
+        storageClass: ${MAIN_SC}
+        globalMounts:
+          - path: /var/www/html/data
+            subPath: data
+      modules:
+        enabled: true
+        type: emptyDir
+        globalMounts:
+          - path: /var/www/html/modules_v4
+            subPath: modules_v4
diff --git a/kubernetes/apps/productivity/webtrees/app/kustomization.yaml b/kubernetes/apps/productivity/webtrees/app/kustomization.yaml
new file mode 100644
index 0000000000..037443be72
--- /dev/null
+++ b/kubernetes/apps/productivity/webtrees/app/kustomization.yaml
@@ -0,0 +1,9 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: productivity
+resources:
+  - ./helmrelease.yaml
+  - secret.sops.yaml
+  - ../../../../shared/gatus/internal
diff --git a/kubernetes/apps/productivity/webtrees/app/secret.sops.yaml b/kubernetes/apps/productivity/webtrees/app/secret.sops.yaml
new file mode 100644
index 0000000000..121d333195
--- /dev/null
+++ b/kubernetes/apps/productivity/webtrees/app/secret.sops.yaml
@@ -0,0 +1,26 @@
+apiVersion: v1
+kind: Secret
+metadata:
+    name: webtrees-admin-pass
+stringData:
+    password: ENC[AES256_GCM,data:6BRzjNC+WOZpG8s3,iv:UIpZKleh3hLQb9fPtjA8jz+YhD7e/Un7aOCoGQM0vzE=,tag:YJkW1/1Ea78JyF1q6qa66g==,type:str]
+sops:
+    kms: []
+    gcp_kms: []
+    azure_kv: []
+    hc_vault: []
+    age:
+        - recipient: age1y0kzuf0tn94a74whazwae4r9qal4snuqfuhl5jacscrpr7up5gts74fe5w
+          enc: |
+            -----BEGIN AGE ENCRYPTED FILE-----
+            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPOGRIQmNTZDJaNUpDbWVq
+            bkwvbklvbkRpVE9RQytkeUNWamFsWUFzRzJBCk1qek5ueFBWYUF4NmptRTBqekxr
+            VXZDazhreHB6UnhGNXZzMTZwV0xxWkkKLS0tIERUQkhOUEhDellUWjNZRU9WU1Z0
+            bWZUWDR1SUwvVnJ1SDAydXV6YS9Db3cKEygUxkjdTZjA9y7i0CHSGdfCrgGOXhp3
+            6+67/ce4guTnhNIxux7dOARTg3gjp4lVAbR4SZFkAbEIMOq1JU63aQ==
+            -----END AGE ENCRYPTED FILE-----
+    lastmodified: "2023-07-06T12:03:59Z"
+    mac: ENC[AES256_GCM,data:HgqRShiHLjWGG5qDgUQPe9Xu69YItk7i/g1qsYXm0XZcsgel/yu9YhhyxFyGcLOnS4W2c+jvvGfddExtZ2APVgenQfyGjhVCFkP7wkNCODkqR2c2xjnwoiHyyNGvHXazausdd/iILXjxIzUTe1oz8fxIGOocRAfpXt6HMCemRyk=,iv:69nAetjFY58TwI461MW28HbGC0raR9UidRc2+Uc+mxE=,tag:OEPsGnt9267aBPc+Ow+HEA==,type:str]
+    pgp: []
+    encrypted_regex: ^(data|stringData)$
+    version: 3.7.3
diff --git a/kubernetes/apps/productivity/webtrees/db/cronjob.yaml b/kubernetes/apps/productivity/webtrees/db/cronjob.yaml
new file mode 100644
index 0000000000..c6064194ba
--- /dev/null
+++ b/kubernetes/apps/productivity/webtrees/db/cronjob.yaml
@@ -0,0 +1,53 @@
+---
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+  name: webtrees-db-mariadb
+spec:
+  suspend: false
+  schedule: "@daily"
+  concurrencyPolicy: Forbid
+  successfulJobsHistoryLimit: 1
+  failedJobsHistoryLimit: 1
+  jobTemplate:
+    spec:
+      template:
+        spec:
+          containers:
+            - name: alpine
+              image: alpine
+              env:
+                - name: DATABASES
+                  value: webtrees
+                - name: MARIADB_HOST
+                  value: webtrees-db-mariadb
+                - name: MARIADB_USER
+                  value: root
+                - name: MYSQL_PWD
+                  valueFrom:
+                    secretKeyRef:
+                      name: mariadb-secret
+                      key: mariadb-root-password
+              command:
+                - /bin/sh
+              args:
+                - -c
+                - |
+                  set -xe
+                  DATE=$(date +%Y-%m-%d)
+                  apk update
+                  apk add mysql-client py-pip
+                  mkdir -p /var/nfs/webtrees-db/$DATE
+                  cd /var/nfs/webtrees-db
+                  for DATABASE in $${DATABASES}; do
+                    mysqldump -h $MARIADB_HOST -u $MARIADB_USER $DATABASE | gzip > $DATE/$DATABASE.sql.gz
+                  done
+              volumeMounts:
+                - name: nfs-vol
+                  mountPath: /var/nfs
+          restartPolicy: OnFailure
+          volumes:
+            - name: nfs-vol
+              nfs:
+                server: 10.20.30.40
+                path: /volume2/data/backup/kubernetes
diff --git a/kubernetes/apps/productivity/webtrees/db/helmrelease.yaml b/kubernetes/apps/productivity/webtrees/db/helmrelease.yaml
new file mode 100644
index 0000000000..27d0532ea5
--- /dev/null
+++ b/kubernetes/apps/productivity/webtrees/db/helmrelease.yaml
@@ -0,0 +1,38 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: webtrees-db
+  namespace: productivity
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: mariadb
+      version: 18.2.0
+      sourceRef:
+        kind: HelmRepository
+        name: bitnami
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    auth:
+      existingSecret: mariadb-secret
+      database: webtrees
+      username: webtrees
+    primary:
+      extraEnvVars:
+        - name: TZ
+          value: ${TIMEZONE}
+        - name: MYSQL_PASSWORD
+          value: $(MARIADB_ROOT_PASSWORD)
+    volumePermissions:
+      enabled: true
+    global:
+      storageClass: ${MAIN_SC}
diff --git a/kubernetes/apps/productivity/webtrees/db/kustomization.yaml b/kubernetes/apps/productivity/webtrees/db/kustomization.yaml
new file mode 100644
index 0000000000..a20d4398a2
--- /dev/null
+++ b/kubernetes/apps/productivity/webtrees/db/kustomization.yaml
@@ -0,0 +1,9 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: productivity
+resources:
+  - ./helmrelease.yaml
+  - cronjob.yaml
+  - secret.sops.yaml
diff --git a/kubernetes/apps/productivity/webtrees/db/secret.sops.yaml b/kubernetes/apps/productivity/webtrees/db/secret.sops.yaml
new file mode 100644
index 0000000000..b04a00975e
--- /dev/null
+++ b/kubernetes/apps/productivity/webtrees/db/secret.sops.yaml
@@ -0,0 +1,28 @@
+apiVersion: v1
+kind: Secret
+metadata:
+    name: mariadb-secret
+stringData:
+    mariadb-password: ENC[AES256_GCM,data:nNPB9rKCgg4TEt9T8KFk66wKV+bYkgNZSiZ/JB3U,iv:FTmlo2rrVvxsvlaMnwq4Uruu3C67TNBiPzNVmrkuqbU=,tag:PWPeoUwZN56egnCj3uUmmA==,type:str]
+    mariadb-root-password: ENC[AES256_GCM,data:A/wBQqbSJlEHngKw94VmFIqT7fI28q83L+Mwa/mP,iv:D6wGpLfhv2VYiZUa6SM7v/W5PL3mPwrp++DaRAUOJu4=,tag:q/L53cb3liEuLfa/IPS1Sw==,type:str]
+    mariadb-replication-password: ENC[AES256_GCM,data:vq/nUN/L3av+AUsJg1rs/7O7pUtbrLPXGrUS99Sr,iv:YshZjFEDUI2uejrUn+TtBoLmpfdJGJerYwat1H1XDFM=,tag:sO7py4jmlg0L/jPUQMU73g==,type:str]
+sops:
+    kms: []
+    gcp_kms: []
+    azure_kv: []
+    hc_vault: []
+    age:
+        - recipient: age1y0kzuf0tn94a74whazwae4r9qal4snuqfuhl5jacscrpr7up5gts74fe5w
+          enc: |
+            -----BEGIN AGE ENCRYPTED FILE-----
+            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPOGRIQmNTZDJaNUpDbWVq
+            bkwvbklvbkRpVE9RQytkeUNWamFsWUFzRzJBCk1qek5ueFBWYUF4NmptRTBqekxr
+            VXZDazhreHB6UnhGNXZzMTZwV0xxWkkKLS0tIERUQkhOUEhDellUWjNZRU9WU1Z0
+            bWZUWDR1SUwvVnJ1SDAydXV6YS9Db3cKEygUxkjdTZjA9y7i0CHSGdfCrgGOXhp3
+            6+67/ce4guTnhNIxux7dOARTg3gjp4lVAbR4SZFkAbEIMOq1JU63aQ==
+            -----END AGE ENCRYPTED FILE-----
+    lastmodified: "2023-07-06T17:00:15Z"
+    mac: ENC[AES256_GCM,data:H1Q4WDW2gkYUtqEARPCNEG0rNMI+ZZtIAoHYH7KSbi/s0esBFzRzPF6f6L1tU+C+Phhd73IKlmUY7XkkjZIZSsCMZh/X/gMlFIH9T7HZsCV8XhmXWPXwfXN1+HQp2wA6UxVq24BGV0r2ZXJmy5+HSanf90XGyRTPUlgmUE2j59A=,iv:R75EhMLD70DOjAmIBywChsl+bFvkC9Rp7ihUc6H2PAQ=,tag:km4UxZ1ZFwAHiBsH9nbrDg==,type:str]
+    pgp: []
+    encrypted_regex: ^(data|stringData)$
+    version: 3.7.3
diff --git a/kubernetes/apps/productivity/webtrees/ks.yaml b/kubernetes/apps/productivity/webtrees/ks.yaml
new file mode 100644
index 0000000000..409f93cad1
--- /dev/null
+++ b/kubernetes/apps/productivity/webtrees/ks.yaml
@@ -0,0 +1,49 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app webtrees
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: productivity
+  dependsOn:
+    - name: webtrees-db
+  path: ./kubernetes/apps/productivity/webtrees/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false # no flux ks dependents
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
+
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app webtrees-db
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: productivity
+  dependsOn:
+  path: ./kubernetes/apps/productivity/webtrees/db
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/security/external-secrets/ks.yaml b/kubernetes/apps/security/external-secrets/ks.yaml
new file mode 100644
index 0000000000..f955033f31
--- /dev/null
+++ b/kubernetes/apps/security/external-secrets/ks.yaml
@@ -0,0 +1,48 @@
+---
+# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/kustomization-kustomize-v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app external-secrets-operator
+  namespace: flux-system
+spec:
+  targetNamespace: security
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/security/external-secrets/operator
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+---
+# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/kustomization-kustomize-v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app external-secrets-secretstores
+  namespace: flux-system
+spec:
+  targetNamespace: security
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  path: ./kubernetes/apps/security/external-secrets/secretstores
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  dependsOn:
+    - name: external-secrets-operator
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: onepassword-connect
+      GATUS_PATH: /health
diff --git a/kubernetes/apps/security/external-secrets/operator/helmrelease.yaml b/kubernetes/apps/security/external-secrets/operator/helmrelease.yaml
new file mode 100644
index 0000000000..91ed36f8b4
--- /dev/null
+++ b/kubernetes/apps/security/external-secrets/operator/helmrelease.yaml
@@ -0,0 +1,33 @@
+---
+# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/helmrelease-helm-v2beta2.json
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: external-secrets
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: external-secrets
+      version: 0.9.18
+      sourceRef:
+        kind: HelmRepository
+        name: external-secrets
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    installCRDs: true
+    serviceMonitor:
+      enabled: true
+    webhook:
+      serviceMonitor:
+        enabled: true
+    certController:
+      serviceMonitor:
+        enabled: true
diff --git a/kubernetes/apps/security/external-secrets/operator/kustomization.yaml b/kubernetes/apps/security/external-secrets/operator/kustomization.yaml
new file mode 100644
index 0000000000..17cbc72b25
--- /dev/null
+++ b/kubernetes/apps/security/external-secrets/operator/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
diff --git a/kubernetes/apps/security/external-secrets/secretstores/doppler/kustomization.yaml b/kubernetes/apps/security/external-secrets/secretstores/doppler/kustomization.yaml
new file mode 100644
index 0000000000..0508dd04e7
--- /dev/null
+++ b/kubernetes/apps/security/external-secrets/secretstores/doppler/kustomization.yaml
@@ -0,0 +1,7 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./secret.sops.yaml
+  - ./secretstore.yaml
diff --git a/kubernetes/apps/security/external-secrets/secretstores/doppler/secret.sops.yaml b/kubernetes/apps/security/external-secrets/secretstores/doppler/secret.sops.yaml
new file mode 100644
index 0000000000..cf56086003
--- /dev/null
+++ b/kubernetes/apps/security/external-secrets/secretstores/doppler/secret.sops.yaml
@@ -0,0 +1,26 @@
+apiVersion: v1
+stringData:
+    dopplerToken: ENC[AES256_GCM,data:g2iEL/9pvyVXWUkVYJYIEj3nmCZCaP2kMYqRmAfLPKgghoodu5CaaXENdIQIiUQ4GtUxDq08CXfyJs19mQ==,iv:6Y2p6v0szMQSGySlXKWNxf5gPIpPR1mHGhf4IV0JJl0=,tag:nDV7xs8vpmEKrixm/xrxHA==,type:str]
+kind: Secret
+metadata:
+    name: doppler-token-auth-api
+sops:
+    kms: []
+    gcp_kms: []
+    azure_kv: []
+    hc_vault: []
+    age:
+        - recipient: age1y0kzuf0tn94a74whazwae4r9qal4snuqfuhl5jacscrpr7up5gts74fe5w
+          enc: |
+            -----BEGIN AGE ENCRYPTED FILE-----
+            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2ek9hRUJHbk5OSVRtSkND
+            dWNZbG9uVlIzaUR0ekYrcFZScFEvQXo1aEFFCjlrR1lFSVlQWER2VmNYUkFhT21m
+            TXdZSkUrcm1ZUHQ0aEJ4SWwyZTdaWkkKLS0tIENVS09zaHRLYk55Mnl3bjE0eWNU
+            Z0RwQ3I5QXMvZWhzQW9hVW9nTE45YzgKlxbbjqihW8qoqWhrLCtzxfAMpXLRYzH7
+            oq2Lab8rwhppqD28tiGknY61q82x/SaZNphYoXEAZKWEMKYo3lpX3A==
+            -----END AGE ENCRYPTED FILE-----
+    lastmodified: "2023-05-16T17:01:55Z"
+    mac: ENC[AES256_GCM,data:QE7nolYJJMAVdB3ZNaNCx6GyhigrcTlbglmGk857ojP02MS7WNcVpI64RbcG2HkGKjYpQiZ1QPRz5vKooSPQj7NNtzjpr9D6ChksVuEIC5x/8kotv5WmJAdYI7z8OGzJRM/XC8B8rqKOmEck1/zGU95/BYY3uA7hlJZ7/LpwmXA=,iv:mHbJ6vpICHAjPh/snpPjkDzWOp1/qH+rJ3wP8zlVExo=,tag:KHoJmvkufrMnPS3lm67Pmg==,type:str]
+    pgp: []
+    encrypted_regex: ^(data|stringData)$
+    version: 3.7.3
diff --git a/kubernetes/apps/security/external-secrets/secretstores/doppler/secretstore.yaml b/kubernetes/apps/security/external-secrets/secretstores/doppler/secretstore.yaml
new file mode 100644
index 0000000000..794cb30795
--- /dev/null
+++ b/kubernetes/apps/security/external-secrets/secretstores/doppler/secretstore.yaml
@@ -0,0 +1,14 @@
+---
+apiVersion: external-secrets.io/v1beta1
+kind: ClusterSecretStore
+metadata:
+  name: doppler-auth-api
+spec:
+  provider:
+    doppler:
+      auth:
+        secretRef:
+          dopplerToken:
+            name: doppler-token-auth-api
+            namespace: security
+            key: dopplerToken
diff --git a/kubernetes/apps/security/external-secrets/secretstores/kustomization.yaml b/kubernetes/apps/security/external-secrets/secretstores/kustomization.yaml
new file mode 100644
index 0000000000..a41e314e23
--- /dev/null
+++ b/kubernetes/apps/security/external-secrets/secretstores/kustomization.yaml
@@ -0,0 +1,8 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: security
+resources:
+  - ./onepassword
+  - ./doppler
diff --git a/kubernetes/apps/security/external-secrets/secretstores/onepassword/helmrelease.yaml b/kubernetes/apps/security/external-secrets/secretstores/onepassword/helmrelease.yaml
new file mode 100644
index 0000000000..cdf5f3c176
--- /dev/null
+++ b/kubernetes/apps/security/external-secrets/secretstores/onepassword/helmrelease.yaml
@@ -0,0 +1,136 @@
+---
+# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/helmrelease-helm-v2beta2.json
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: onepassword-connect
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    controllers:
+      onepassword-connect:
+        strategy: RollingUpdate
+        annotations:
+          reloader.stakater.com/auto: "true"
+        containers:
+          api:
+            image:
+              repository: docker.io/1password/connect-api
+              tag: 1.7.2
+            env:
+              XDG_DATA_HOME: &configDir /config
+              OP_HTTP_PORT: &apiPort 80
+              OP_BUS_PORT: 11220
+              OP_BUS_PEERS: localhost:11221
+              OP_SESSION:
+                valueFrom:
+                  secretKeyRef:
+                    name: onepassword-connect-secret
+                    key: 1password-credentials.json
+            probes:
+              liveness:
+                enabled: true
+                custom: true
+                spec:
+                  httpGet:
+                    path: /heartbeat
+                    port: *apiPort
+                  initialDelaySeconds: 15
+                  periodSeconds: 30
+                  failureThreshold: 3
+              readiness:
+                enabled: true
+                custom: true
+                spec:
+                  httpGet:
+                    path: /health
+                    port: *apiPort
+                  initialDelaySeconds: 15
+            securityContext: &securityContext
+              allowPrivilegeEscalation: false
+              readOnlyRootFilesystem: true
+              capabilities: { drop: ["ALL"] }
+            resources: &resources
+              requests:
+                cpu: 10m
+              limits:
+                memory: 256M
+          sync:
+            image:
+              repository: docker.io/1password/connect-sync
+              tag: 1.7.2
+            env:
+              XDG_DATA_HOME: *configDir
+              OP_HTTP_PORT: &syncPort 8081
+              OP_BUS_PORT: 11221
+              OP_BUS_PEERS: localhost:11220
+              OP_SESSION:
+                valueFrom:
+                  secretKeyRef:
+                    name: onepassword-connect-secret
+                    key: 1password-credentials.json
+            probes:
+              liveness:
+                enabled: true
+                custom: true
+                spec:
+                  httpGet:
+                    path: /heartbeat
+                    port: *syncPort
+                  initialDelaySeconds: 15
+                  periodSeconds: 30
+                  failureThreshold: 3
+              readiness:
+                enabled: true
+                custom: true
+                spec:
+                  httpGet:
+                    path: /health
+                    port: *syncPort
+                  initialDelaySeconds: 15
+            securityContext: *securityContext
+            resources: *resources
+    defaultPodOptions:
+      securityContext:
+        runAsNonRoot: true
+        runAsUser: 999
+        runAsGroup: 999
+        fsGroup: 999
+        fsGroupChangePolicy: OnRootMismatch
+        seccompProfile: { type: RuntimeDefault }
+    service:
+      app:
+        controller: onepassword-connect
+        ports:
+          http:
+            port: *apiPort
+    ingress:
+      app:
+        className: internal
+        hosts:
+          - host: "{{ .Release.Name }}.${SECRET_DOMAIN}"
+            paths:
+              - path: /
+                service:
+                  identifier: app
+                  port: http
+    persistence:
+      config:
+        type: emptyDir
+        globalMounts:
+          - path: *configDir
diff --git a/kubernetes/apps/security/external-secrets/secretstores/onepassword/kustomization.yaml b/kubernetes/apps/security/external-secrets/secretstores/onepassword/kustomization.yaml
new file mode 100644
index 0000000000..a5c75f9688
--- /dev/null
+++ b/kubernetes/apps/security/external-secrets/secretstores/onepassword/kustomization.yaml
@@ -0,0 +1,9 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
+  - ./secret.sops.yaml
+  - ./secretstore.yaml
+  - ../../../../../shared/gatus/internal
diff --git a/kubernetes/apps/security/external-secrets/secretstores/onepassword/secret.sops.yaml b/kubernetes/apps/security/external-secrets/secretstores/onepassword/secret.sops.yaml
new file mode 100644
index 0000000000..faba096231
--- /dev/null
+++ b/kubernetes/apps/security/external-secrets/secretstores/onepassword/secret.sops.yaml
@@ -0,0 +1,28 @@
+---
+apiVersion: v1
+stringData:
+  token: ENC[AES256_GCM,data:+yrrku0u+7eZfc5shry6J1CYtmNS/17TVqIpTmscy80KauEv+YA8/uu1KnWaQWWv1663FnfI8qXM12PNCSbHd0wLVRUT26YcnkEFx5lo+EbCJAiFXrJyMafWbn6OskRoWdQq7TodOQtjvYXsvzGEJ6IsUZz0JdAcBAohL0DkIiql97F4sGnXXWGRpZhvk6lkQDbWaX+djVAFjAfx2W+N5glzK8ESWX/nSBOnpTfPVosmL3aROA92ER7eQnuZgrz5HnHRuXwEtFIuPf9ykLc75bKxP6xlV6pQV30db5hGc1MISgUiqIIn9PDnMBG2H8Xj67+EsVRMzx5L1U/1yd0Lu8szZxvbR/6YlYyWfmkqG9Js7bYlfJg0xjJZE4sWaqF4TeWlJSmkpSvmL+zJ2/2pCWZTo4Op1ZfGRPaBxz+0kRIMdAbv3HZDBQiuD7mdSNvizbzQ/NYLod5N8aBRawDidLKMqGZZbN70bi2191OxQiJhFyA25trlTcmfZbw0cF6lSfPzS46NcoRl8F35O4jr+/pljbDcEoGuXJaKCxxWCn/e94re8hqLds8dEsqdevo93ahX18hGYLal5qDi0gXTZDgtC+q9mUB3xvgCNAge2DL/+vgZeAL+vYcp+Q2niIh6irWX9t+HAu13T4t18m0azfjgr2YDSyJoxdcj1ZHvt0Oy1m1Wd16ribw2LtcD+zRRreQMRIrEXpY1IN+gx2bMwWNWzku3d9pgAlkXyIx27V2kUNCJOY0kf5qJPyM5f+kaInFGoTgg8F1GLt27IxOW07WbjJXO/MabOs5h1jHF5QwY5//+nFGhdAqyr8KK922bnCBkW18OcxA+bkNH8kkqQbNR,iv:ifaiUGf2JGE26wItSPGtrkenPZQuM5omE8RXyml2qqY=,tag:WZyuZU0K4WJnJXVWL+27fg==,type:str]
+  1password-credentials.json: ENC[AES256_GCM,data:0EpilwcQQ1mbuWBnopgRNT4Ra2ifVW4ddx9tBzWEM3Z5JkukOvToAiHUVDSYMFmn7mcu/E5Af4EXy4sQ80DVNkXv4YFLKsWtVD0Pjsm3dpLolQ8uAxhtuiTTF+KFkRA3s125pV/+KmTJ/A5NiTTeEtMBd8O1m01Sus3xVJS9fAlP/sY6/CBut4NGzdLTSTDNWZOJZfvK9lI9J40GT87JiGnhydHOU3WzwuHjjsm4IavgZC/BEAfvbpIzmEaPCMYYtD2SbwfUpJVh97EeBiNxpd8UO8RUStoUlvFLwqgipYCntfQRNhfFRoT19crYvhp83XRtk1v9H+75gJrS2AXZJ/niIBHZs9sSxpwbgqDJGBz/b7Nh0ShYdK0urJL4a6KT1NhbGCnbCqetb+0fki+XkynoafK15ihhHRqoJ0tNNn1/sSghVnfFUmc7gemXoxDpLWh2OmNNkwvs9eKDJlj4Ow1tXPswoUU6dXLNQfTtfU25SUkI5AQs3juwq+YJRTxTqFqf7AlsiKEMOBaHZnX8sa8FnIgpO5plFfApFmfhyGG6xDQxmEd6e3EUFSRDJvKW9+xy2Jwedl5LRyxwT2BAUZUVDIAU5foy4ktFRY+Ajb2PoqryqymV0R8B3gp86fJGNn0dj3U49BkLYDORvIjVU71NBGodCJxqVVbf1ZsgjD8U0vdvBknY1SpoLqXtCtbwzqMJHdaiza7T80X4IRNjJ1Kwb357GTLU81CE2Du9vmUw1+f6GKzqSEVgThHkdEnSX8LOMl6ZHQPU0k11vsnJ+7JCyNEs/8BEgoeBR9D9Cq8FUJQIndSb+Inj/YOZTBSekkHpqAvgzOmmmz74nC8e+CGJ2aEE5av3tagLLuVoOh2/iJtt1Qsz9YmogLmr9QP5GMBzfWceD8E74tcwWnPjLM6rZB5FIvyIPS3uKtcWZedkXHqYApzx8ebGCG7eMjJKoAzTSqeu1QTbNRv6BUdGWXfwH8sld+UtDFrOWa3KMgrYhisJyfDSPqMh/H6VgrmTxKYPt8NUYNLB+yAXt5jIb2Tb1jJm1cpmsCY+zjH4PBP78ri7YDI0BW9c2ZUSzBDSrBLbezz64u5AsURTM9rRFgvn+kJhtxOvJUm+MlSuioOZkI7NtwdN1xM67ntUrB+S8jz3VL52EK8ORA5mUPBIFAhXuTFqVqFMiuzs1LghNATjPEiigUukheGcx271otKVGn/9isko0hQpY33ZC9FTTWOhXZve/bpLaH8d8esonyca/nAOySO9WCb8/8mWEBv2uCDmngBsVbvN890s5XzK8VhUYLOF7xfRckCOqaBhVE8naKAqs9EcIHn5jaktiVkoGmaxs/7mFvqSVAMf8FbL95sWHw93JseNZmi2Im9vyfKqjwDl4XEi6Xbjhiev8zJOCdijW84UiwA1SUrmOkcBdGtRLi1TCsCQ4xiU51mC+qLnRg9KsfKj+4YlVDh6EAE+RfKwBJ6S75OeTrOTyGdN17f7ClvV20xlIUPJ0nkmIYk956uP9uTJS7m5esaEpgVv2Amr14gkITpsuY0brUba6MnUo68+w2lgV0amhGgR6RR79obKFICEpHn5NhfsvefyNcQTaEnGEy41HBrs0F//5xW4llZih8dEZPFoW12S1pIk1zcTRWHM8t9dJCB/M3UX8up+yKgW/uXgLorXk4fP5c2Zu5elXwkQa+Yey5MJuehtWnoF+eQORN8DCmfXVjb4XA0HyTjIySLh06ZUpRqdpAICPN//ZcV3HS+nxUEYSaHZqphwXaoYznPXDus3F6zAxUQLeE2rjJgPXDTeRUjiorw5WAo+viZlBa4RtD75akx3VAgUh88x6kfeocC+owsG0UfHG/ORO2Ce/u3rTYWO9vyD7pY6HS7jh4HAJP4x/RyVU/KMpUVw/dZx2XcegLN85z5WbsOVZ7Y7RwxU,iv:gtpdjyDGb0AKJ2S1snA1sESeqkesi8dLfIyTxEbw+TU=,tag:1u/yu3VIS4HHaGtqqJzquA==,type:str]
+kind: Secret
+metadata:
+  name: onepassword-connect-secret
+sops:
+  kms: []
+  gcp_kms: []
+  azure_kv: []
+  hc_vault: []
+  age:
+    - recipient: age1y0kzuf0tn94a74whazwae4r9qal4snuqfuhl5jacscrpr7up5gts74fe5w
+      enc: |
+        -----BEGIN AGE ENCRYPTED FILE-----
+        YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJVk5jeUQwT2oyMjlSV0t6
+        NkpDWjlCaUFXdE9FeVl0bzh2M3Nia012TzJVCjlYUjlPMmVPdHdpT3hBTFRTQ0Y4
+        KzFOSzFVQ2V2OEV4ZXdyQnNKYk1VRHcKLS0tIHVrdU51UHN2RWFyeTErQXdjdkwx
+        UzRycXl5SUhMNUx0a1F4VW16VndnaUkKln0lFslzjCU1LXVD86qytL/eSLPVYlGQ
+        eUf9aohlG+anEDqY2h/U3hIGnrrWaNEVteI8eXTNvO6AdnVa07RkSg==
+        -----END AGE ENCRYPTED FILE-----
+  lastmodified: '2023-03-03T20:04:50Z'
+  mac: ENC[AES256_GCM,data:aOBJcLG3gEPinMemD2faHVavdRe1fQf2tTgp7PlnOn1GgNvcFDu3FTbLCARU69l0AePrQrCq63qiNmO7N2k2a0l5sYPWDPvL2trJazwCoO9uNo0Dmw+DENMXszY+N2YuZRXBEiRzWITK9rqCCHiaSBRiJ6BICFkSGyYoPr6Pin4=,iv:se/PlganuFOf42TKInen2hyBTnBvOaJoTVNd3jeQttE=,tag:A3+M6q5+9cnbRSt1FOi9Cg==,type:str]
+  pgp: []
+  encrypted_regex: ^(data|stringData)$
+  version: 3.7.3
diff --git a/kubernetes/apps/security/external-secrets/secretstores/onepassword/secretstore.yaml b/kubernetes/apps/security/external-secrets/secretstores/onepassword/secretstore.yaml
new file mode 100644
index 0000000000..b9550d9fa7
--- /dev/null
+++ b/kubernetes/apps/security/external-secrets/secretstores/onepassword/secretstore.yaml
@@ -0,0 +1,17 @@
+---
+apiVersion: external-secrets.io/v1beta1
+kind: ClusterSecretStore
+metadata:
+  name: onepassword
+spec:
+  provider:
+    onepassword:
+      connectHost: http://onepassword-connect:80
+      vaults:
+        Homelab: 1
+      auth:
+        secretRef:
+          connectTokenSecretRef:
+            name: onepassword-connect-secret
+            namespace: security
+            key: token
diff --git a/kubernetes/apps/security/kustomization.yaml b/kubernetes/apps/security/kustomization.yaml
new file mode 100644
index 0000000000..ae546e25c3
--- /dev/null
+++ b/kubernetes/apps/security/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - namespace.yaml
+  - external-secrets/ks.yaml
diff --git a/kubernetes/apps/security/namespace.yaml b/kubernetes/apps/security/namespace.yaml
new file mode 100644
index 0000000000..397a2359d3
--- /dev/null
+++ b/kubernetes/apps/security/namespace.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: security
+  labels:
+    kustomize.toolkit.fluxcd.io/prune: disabled
diff --git a/kubernetes/apps/storage/kustomization.yaml b/kubernetes/apps/storage/kustomization.yaml
new file mode 100644
index 0000000000..5873c50d8b
--- /dev/null
+++ b/kubernetes/apps/storage/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - namespace.yaml
+  - longhorn/ks.yaml
diff --git a/kubernetes/apps/storage/longhorn/app/helm-release.yaml b/kubernetes/apps/storage/longhorn/app/helm-release.yaml
new file mode 100644
index 0000000000..a50c9d70d4
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/app/helm-release.yaml
@@ -0,0 +1,82 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: &app longhorn
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: longhorn
+      version: 1.6.2
+      sourceRef:
+        kind: HelmRepository
+        name: longhorn
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    persistence:
+      defaultClass: true
+      defaultClassReplicaCount: 2
+      defaultFsType: ext4
+      reclaimPolicy: Delete
+      # recurringJobs:
+      #   enable: true
+      #   jobList:
+      #     '[{"name":"daily", "task":"backup", "cron":"0 6 * * ?", "retain":3,"labels":
+      #     {"interval":"daily"}}]'
+    defaultSettings:
+      backupstorePollInterval: 300
+      backupTarget: nfs://10.20.30.40:/volume2/data/backup/kubernetes/longhorn-backup
+      snapshotDataIntegrity: "fast-check"
+      defaultDataPath: /var/lib/longhorn
+      defaultDataLocality: best-effort
+      replicaAutoBalance: best-effort
+      staleReplicaTimeout: "30"
+      defaultReplicaCount: 2
+      defaultLonghornStaticStorageClass: longhorn
+      createDefaultDiskLabeledNodes: false
+      nodeDownPodDeletionPolicy: delete-both-statefulset-and-deployment-pod
+      concurrentAutomaticEngineUpgradePerNodeLimit: 1
+      storageMinimalAvailablePercentage: 10
+      StorageOverProvisioningPercentage: 110
+      # taintToleration: "node-role.kubernetes.io/master=true:NoSchedule"
+      # upgradeChecker: true
+    ingress:
+      enabled: true
+      ingressClassName: internal
+      host: longhorn.${SECRET_DOMAIN}
+      tlsSecret: ${SECRET_DOMAIN/./-}-production-tls
+      tls: true
+      path: /
+      annotations:
+        hajimari.io/enable: "true"
+        hajimari.io/icon: cow
+        hajimari.io/appName: Longhorn
+        hajimari.io/group: "storage"
+        # hajimari.io/targetBlank: "true"
+        # hajimari.io/info: "Storage"
+    # longhornManager:
+    #   tolerations:
+    #     - key: node-role.kubernetes.io/master
+    #       operator: Equal
+    #       value: "true"
+    #       effect: NoSchedule
+    # longhornDriver:
+    #   tolerations:
+    #     - key: node-role.kubernetes.io/master
+    #       operator: Equal
+    #       value: "true"
+    #       effect: NoSchedule
+    # longhornUI:
+    #   tolerations:
+    #     - key: node-role.kubernetes.io/master
+    #       operator: Equal
+    #       value: "true"
+    #       effect: NoSchedule
diff --git a/kubernetes/apps/storage/longhorn/app/kustomization.yaml b/kubernetes/apps/storage/longhorn/app/kustomization.yaml
new file mode 100755
index 0000000000..bf406d42ee
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/app/kustomization.yaml
@@ -0,0 +1,8 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: storage
+resources:
+  - helm-release.yaml
+  - ../../../../shared/gatus/internal
diff --git a/kubernetes/apps/storage/longhorn/conf/kustomization.yaml b/kubernetes/apps/storage/longhorn/conf/kustomization.yaml
new file mode 100755
index 0000000000..3c9449c26f
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/kustomization.yaml
@@ -0,0 +1,10 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: storage
+resources:
+  - monitoring
+  - other
+  - recurringjobs
+  # - snap-class.yaml
diff --git a/kubernetes/apps/storage/longhorn/conf/monitoring/kustomization.yaml b/kubernetes/apps/storage/longhorn/conf/monitoring/kustomization.yaml
new file mode 100755
index 0000000000..425e1e1c11
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/monitoring/kustomization.yaml
@@ -0,0 +1,8 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: longhorn-system
+resources:
+  - prometheusrule.yaml
+  - servicemonitor.yaml
diff --git a/kubernetes/apps/storage/longhorn/conf/monitoring/prometheusrule.yaml b/kubernetes/apps/storage/longhorn/conf/monitoring/prometheusrule.yaml
new file mode 100755
index 0000000000..5f1296b4b7
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/monitoring/prometheusrule.yaml
@@ -0,0 +1,110 @@
+apiVersion: monitoring.coreos.com/v1
+kind: PrometheusRule
+metadata:
+  name: longhorn-rules
+  namespace: monitoring
+spec:
+  groups:
+    - name: longhorn.rules
+      rules:
+        - alert: LonghornVolumeActualSpaceUsedWarning
+          annotations:
+            description:
+              The actual space used by Longhorn volume {{$labels.volume}} on {{$labels.node}} is at {{$value}}% capacity for
+              more than 5 minutes.
+            summary: The actual used space of Longhorn volume is over 90% of the capacity.
+          expr: (longhorn_volume_actual_size_bytes / longhorn_volume_capacity_bytes) * 100 > 90
+          for: 5m
+          labels:
+            issue: The actual used space of Longhorn volume {{$labels.volume}} on {{$labels.node}} is high.
+            severity: warning
+        - alert: LonghornVolumeStatusCritical
+          annotations:
+            description:
+              Longhorn volume {{$labels.volume}} on {{$labels.node}} is Fault for
+              more than 2 minutes.
+            summary: Longhorn volume {{$labels.volume}} is Fault
+          expr: longhorn_volume_robustness == 3
+          for: 5m
+          labels:
+            issue: Longhorn volume {{$labels.volume}} is Fault.
+            severity: critical
+        - alert: LonghornVolumeStatusWarning
+          annotations:
+            description:
+              Longhorn volume {{$labels.volume}} on {{$labels.node}} is Degraded for
+              more than 5 minutes.
+            summary: Longhorn volume {{$labels.volume}} is Degraded
+          expr: longhorn_volume_robustness == 2
+          for: 5m
+          labels:
+            issue: Longhorn volume {{$labels.volume}} is Degraded.
+            severity: warning
+        - alert: LonghornNodeStorageWarning
+          annotations:
+            description:
+              The used storage of node {{$labels.node}} is at {{$value}}% capacity for
+              more than 5 minutes.
+            summary: The used storage of node is over 70% of the capacity.
+          expr: (longhorn_node_storage_usage_bytes / longhorn_node_storage_capacity_bytes) * 100 > 70
+          for: 5m
+          labels:
+            issue: The used storage of node {{$labels.node}} is high.
+            severity: warning
+        - alert: LonghornDiskStorageWarning
+          annotations:
+            description:
+              The used storage of disk {{$labels.disk}} on node {{$labels.node}} is at {{$value}}% capacity for
+              more than 5 minutes.
+            summary: The used storage of disk is over 70% of the capacity.
+          expr: (longhorn_disk_usage_bytes / longhorn_disk_capacity_bytes) * 100 > 70
+          for: 5m
+          labels:
+            issue: The used storage of disk {{$labels.disk}} on node {{$labels.node}} is high.
+            severity: warning
+        - alert: LonghornNodeDown
+          annotations:
+            description: There are {{$value}} Longhorn nodes which have been offline for more than 5 minutes.
+            summary: Longhorn nodes is offline
+          expr: (avg(longhorn_node_count_total) or on() vector(0)) - (count(longhorn_node_status{condition="ready"} == 1) or on() vector(0)) > 0
+          for: 5m
+          labels:
+            issue: There are {{$value}} Longhorn nodes are offline
+            severity: warning
+        - alert: LonghornIntanceManagerCPUUsageWarning
+          annotations:
+            description:
+              Longhorn instance manager {{$labels.instance_manager}} on {{$labels.node}} has CPU Usage / CPU request is {{$value}}% for
+              more than 5 minutes.
+            summary: Longhorn instance manager {{$labels.instance_manager}} on {{$labels.node}} has CPU Usage / CPU request is over 300%.
+          expr: (longhorn_instance_manager_cpu_usage_millicpu/longhorn_instance_manager_cpu_requests_millicpu) * 100 > 300
+          for: 5m
+          labels:
+            issue: Longhorn instance manager {{$labels.instance_manager}} on {{$labels.node}} consumes 3 times the CPU request.
+            severity: warning
+        - alert: LonghornNodeCPUUsageWarning
+          annotations:
+            description:
+              Longhorn node {{$labels.node}} has CPU Usage / CPU capacity is {{$value}}% for
+              more than 5 minutes.
+            summary: Longhorn node {{$labels.node}} experiences high CPU pressure for more than 5m.
+          expr: (longhorn_node_cpu_usage_millicpu / longhorn_node_cpu_capacity_millicpu) * 100 > 90
+          for: 5m
+          labels:
+            issue: Longhorn node {{$labels.node}} experiences high CPU pressure.
+            severity: warning
+        - alert: LonghornVolumeBackupStuck
+          expr: count by (volume) (longhorn_backup_state < 2)
+          for: 8h
+          labels:
+            severity: warning
+          annotations:
+            description: There are {{$value}} longhorn backups of a volume {{$labels.volume}} stuck for at least 8h.
+            summary: Longhorn backups stuck.
+        - alert: LonghornVolumeBackupError
+          expr: count by (volume) (longhorn_backup_state > 3)
+          labels:
+            severity: warning
+          annotations:
+            description: There are {{$value}} longhorn backups of a volume {{$labels.volume}} which failed to complete.
+            summary: Longhorn backups failed.
diff --git a/kubernetes/apps/storage/longhorn/conf/monitoring/servicemonitor.yaml b/kubernetes/apps/storage/longhorn/conf/monitoring/servicemonitor.yaml
new file mode 100755
index 0000000000..b670f9d735
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/monitoring/servicemonitor.yaml
@@ -0,0 +1,16 @@
+---
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+  name: longhorn-prometheus-servicemonitor
+  labels:
+    name: longhorn-prometheus-servicemonitor
+spec:
+  selector:
+    matchLabels:
+      app: longhorn-manager
+  namespaceSelector:
+    matchNames:
+      - longhorn-system
+  endpoints:
+    - port: manager
diff --git a/kubernetes/apps/storage/longhorn/conf/other/kustomization.yaml b/kubernetes/apps/storage/longhorn/conf/other/kustomization.yaml
new file mode 100755
index 0000000000..df29442071
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/other/kustomization.yaml
@@ -0,0 +1,7 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: longhorn-system
+resources:
+  - systembackup.yaml
diff --git a/kubernetes/apps/storage/longhorn/conf/other/systembackup.yaml b/kubernetes/apps/storage/longhorn/conf/other/systembackup.yaml
new file mode 100644
index 0000000000..7dab0e9b21
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/other/systembackup.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: longhorn.io/v1beta2
+kind: SystemBackup
+metadata:
+  name: system
+spec:
+  volumeBackupPolicy: if-not-present
diff --git a/kubernetes/apps/storage/longhorn/conf/recurringjobs/30min-snapshot.yaml b/kubernetes/apps/storage/longhorn/conf/recurringjobs/30min-snapshot.yaml
new file mode 100755
index 0000000000..e510408deb
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/recurringjobs/30min-snapshot.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: longhorn.io/v1beta1
+kind: RecurringJob
+metadata:
+  name: 30min-snapshot
+spec:
+  name: 30min-snapshot
+  concurrency: 2
+  cron: 0/30 * * * *
+  groups:
+    - normal
+  retain: 4
+  task: snapshot
diff --git a/kubernetes/apps/storage/longhorn/conf/recurringjobs/daily-backup.yaml b/kubernetes/apps/storage/longhorn/conf/recurringjobs/daily-backup.yaml
new file mode 100755
index 0000000000..7361e1bb89
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/recurringjobs/daily-backup.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: longhorn.io/v1beta1
+kind: RecurringJob
+metadata:
+  name: daily-backup
+spec:
+  name: daily-backup
+  concurrency: 2
+  cron: 45 0 * * *
+  groups:
+    - normal
+  retain: 7
+  task: backup
diff --git a/kubernetes/apps/storage/longhorn/conf/recurringjobs/daily-cleanup.yaml b/kubernetes/apps/storage/longhorn/conf/recurringjobs/daily-cleanup.yaml
new file mode 100755
index 0000000000..47656a4a9b
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/recurringjobs/daily-cleanup.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: longhorn.io/v1beta1
+kind: RecurringJob
+metadata:
+  name: daily-cleanup
+spec:
+  name: daily-cleanup
+  concurrency: 1
+  cron: 45 4 * * *
+  groups:
+    - normal
+  retain: 1
+  task: snapshot-cleanup
diff --git a/kubernetes/apps/storage/longhorn/conf/recurringjobs/daily-delete.yaml b/kubernetes/apps/storage/longhorn/conf/recurringjobs/daily-delete.yaml
new file mode 100755
index 0000000000..56598cdc4d
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/recurringjobs/daily-delete.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: longhorn.io/v1beta1
+kind: RecurringJob
+metadata:
+  name: daily-delete
+spec:
+  name: daily-delete
+  concurrency: 1
+  cron: 45 3 * * *
+  groups:
+    - normal
+  retain: 1
+  task: snapshot-delete
diff --git a/kubernetes/apps/storage/longhorn/conf/recurringjobs/daily-trim.yaml b/kubernetes/apps/storage/longhorn/conf/recurringjobs/daily-trim.yaml
new file mode 100755
index 0000000000..994896f318
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/recurringjobs/daily-trim.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: longhorn.io/v1beta1
+kind: RecurringJob
+metadata:
+  name: daily-trim
+spec:
+  name: daily-trim
+  concurrency: 1
+  cron: 45 22 * * *
+  groups:
+    - normal
+  retain: 1
+  task: filesystem-trim
diff --git a/kubernetes/apps/storage/longhorn/conf/recurringjobs/hourly-backup.yaml b/kubernetes/apps/storage/longhorn/conf/recurringjobs/hourly-backup.yaml
new file mode 100755
index 0000000000..ae5af79abe
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/recurringjobs/hourly-backup.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: longhorn.io/v1beta1
+kind: RecurringJob
+metadata:
+  name: hourly-backup
+spec:
+  name: hourly-backup
+  concurrency: 2
+  cron: 15 * * * *
+  groups:
+    - normal
+  retain: 6
+  task: backup
diff --git a/kubernetes/apps/storage/longhorn/conf/recurringjobs/kustomization.yaml b/kubernetes/apps/storage/longhorn/conf/recurringjobs/kustomization.yaml
new file mode 100755
index 0000000000..cb90caf07a
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/recurringjobs/kustomization.yaml
@@ -0,0 +1,12 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: longhorn-system
+resources:
+  - 30min-snapshot.yaml
+  - daily-backup.yaml
+  - daily-cleanup.yaml
+  - daily-delete.yaml
+  - daily-trim.yaml
+  - hourly-backup.yaml
diff --git a/kubernetes/apps/storage/longhorn/conf/snap-class.yaml b/kubernetes/apps/storage/longhorn/conf/snap-class.yaml
new file mode 100755
index 0000000000..ae2f0ae0d0
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/conf/snap-class.yaml
@@ -0,0 +1,9 @@
+---
+kind: VolumeSnapshotClass
+apiVersion: snapshot.storage.k8s.io/v1
+metadata:
+  name: longhorn
+  labels:
+    velero.io/csi-volumesnapshot-class: "true"
+driver: driver.longhorn.io
+deletionPolicy: Retain
diff --git a/kubernetes/apps/storage/longhorn/ks.yaml b/kubernetes/apps/storage/longhorn/ks.yaml
new file mode 100644
index 0000000000..f5796c2397
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/ks.yaml
@@ -0,0 +1,66 @@
+# ---
+# # yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+# apiVersion: kustomize.toolkit.fluxcd.io/v1
+# kind: Kustomization
+# metadata:
+#   name: longhorn-prereq
+#   namespace: flux-system
+# spec:
+#   targetNamespace: storage
+#   path: ./kubernetes/apps/storage/longhorn/prereq
+#   prune: true
+#   sourceRef:
+#     kind: GitRepository
+#     name: k8s-homelab
+#   wait: true  # no flux ks dependents
+#   interval: 30m
+#   retryInterval: 1m
+#   timeout: 5m
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app longhorn
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: storage
+  # dependsOn:
+  #   - name: longhorn-prereq
+  path: ./kubernetes/apps/storage/longhorn/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: true
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
+  postBuild:
+    substitute:
+      APP: *app
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app longhorn-conf
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  dependsOn:
+    - name: longhorn
+  path: ./kubernetes/apps/storage/longhorn/conf
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false # no flux ks dependents
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/apps/storage/longhorn/prereq/kustomization.yaml b/kubernetes/apps/storage/longhorn/prereq/kustomization.yaml
new file mode 100644
index 0000000000..aaae13c21b
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/prereq/kustomization.yaml
@@ -0,0 +1,12 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: storage
+resources:
+- longhorn-iscsi-installation.yaml
+labels:
+- includeSelectors: true
+  pairs:
+    app.kubernetes.io/instance: longhorn
+    app.kubernetes.io/name: longhorn
diff --git a/kubernetes/apps/storage/longhorn/prereq/longhorn-iscsi-installation.yaml b/kubernetes/apps/storage/longhorn/prereq/longhorn-iscsi-installation.yaml
new file mode 100644
index 0000000000..e8b1f4f294
--- /dev/null
+++ b/kubernetes/apps/storage/longhorn/prereq/longhorn-iscsi-installation.yaml
@@ -0,0 +1,40 @@
+---
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  name: longhorn-iscsi-installation
+  labels:
+    app: longhorn-iscsi-installation
+  annotations:
+    command: &cmd sudo apt-get update -q -y && sudo apt-get install -q -y open-iscsi
+      && sudo systemctl -q enable iscsid && sudo systemctl start iscsid && sudo modprobe
+      iscsi_tcp && if [ $? -eq 0 ]; then echo "iscsi install successfully"; else echo
+      "iscsi install failed error code $?"; fi
+spec:
+  selector:
+    matchLabels:
+      app: longhorn-iscsi-installation
+  template:
+    metadata:
+      labels:
+        app: longhorn-iscsi-installation
+    spec:
+      hostNetwork: true
+      hostPID: true
+      initContainers:
+        - name: iscsi-installation
+          command:
+            - nsenter
+            - --mount=/proc/1/ns/mnt
+            - --
+            - bash
+            - -c
+            - *cmd
+          image: alpine:3.20
+          securityContext:
+            privileged: true
+      containers:
+        - name: sleep
+          image: registry.k8s.io/pause:3.10
+  updateStrategy:
+    type: RollingUpdate
diff --git a/kubernetes/apps/storage/namespace.yaml b/kubernetes/apps/storage/namespace.yaml
new file mode 100644
index 0000000000..a8966521ef
--- /dev/null
+++ b/kubernetes/apps/storage/namespace.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: storage
+  labels:
+    kustomize.toolkit.fluxcd.io/prune: disabled
diff --git a/kubernetes/apps/tools/kustomization.yaml b/kubernetes/apps/tools/kustomization.yaml
new file mode 100644
index 0000000000..17b5a675d6
--- /dev/null
+++ b/kubernetes/apps/tools/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - namespace.yaml
+  - smtp-relay/ks.yaml
diff --git a/kubernetes/apps/tools/namespace.yaml b/kubernetes/apps/tools/namespace.yaml
new file mode 100644
index 0000000000..29b15efa3e
--- /dev/null
+++ b/kubernetes/apps/tools/namespace.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: tools
+  labels:
+    kustomize.toolkit.fluxcd.io/prune: disabled
diff --git a/kubernetes/apps/tools/smtp-relay/app/config/maddy.conf b/kubernetes/apps/tools/smtp-relay/app/config/maddy.conf
new file mode 100644
index 0000000000..7004b3c84b
--- /dev/null
+++ b/kubernetes/apps/tools/smtp-relay/app/config/maddy.conf
@@ -0,0 +1,24 @@
+state_dir /cache/state
+runtime_dir /cache/run
+
+openmetrics tcp://0.0.0.0:{env:SMTP_RELAY_METRICS_PORT} { }
+
+tls off
+hostname {env:SMTP_RELAY_HOSTNAME}
+
+smtp tcp://0.0.0.0:{env:SMTP_RELAY_SMTP_PORT} {
+    default_source {
+        deliver_to &remote_queue
+    }
+}
+
+target.queue remote_queue {
+    target &remote_smtp
+}
+
+target.smtp remote_smtp {
+    attempt_starttls yes
+    require_tls yes
+    auth plain {env:SMTP_RELAY_USERNAME} {env:SMTP_RELAY_PASSWORD}
+    targets tls://{env:SMTP_RELAY_SERVER}:{env:SMTP_RELAY_SERVER_PORT}
+}
diff --git a/kubernetes/apps/tools/smtp-relay/app/externalsecret.yaml b/kubernetes/apps/tools/smtp-relay/app/externalsecret.yaml
new file mode 100644
index 0000000000..96eba21c19
--- /dev/null
+++ b/kubernetes/apps/tools/smtp-relay/app/externalsecret.yaml
@@ -0,0 +1,22 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: smtp-relay
+spec:
+  secretStoreRef:
+    kind: ClusterSecretStore
+    name: onepassword
+  target:
+    name: smtp-relay-secret
+    template:
+      engineVersion: v2
+      data:
+        SMTP_RELAY_HOSTNAME: "{{ .SMTP_RELAY_HOSTNAME }}"
+        SMTP_RELAY_SERVER: "{{ .SMTP_RELAY_SERVER }}"
+        SMTP_RELAY_USERNAME: "{{ .SMTP_RELAY_USERNAME }}"
+        SMTP_RELAY_PASSWORD: "{{ .SMTP_RELAY_PASSWORD }}"
+  dataFrom:
+    - extract:
+        key: smtp-relay
diff --git a/kubernetes/apps/tools/smtp-relay/app/helmrelease.yaml b/kubernetes/apps/tools/smtp-relay/app/helmrelease.yaml
new file mode 100644
index 0000000000..b8c9d5854c
--- /dev/null
+++ b/kubernetes/apps/tools/smtp-relay/app/helmrelease.yaml
@@ -0,0 +1,98 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: &app smtp-relay
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: app-template
+      version: 3.2.1
+      sourceRef:
+        kind: HelmRepository
+        name: bjw-s
+        namespace: flux-system
+  install:
+    remediation:
+      retries: 3
+  upgrade:
+    cleanupOnFail: true
+    remediation:
+      retries: 3
+  values:
+    controllers:
+      smtp-relay:
+        replicas: 1
+        annotations:
+          reloader.stakater.com/auto: "true"
+        containers:
+          app:
+            image:
+              repository: ghcr.io/foxcpp/maddy
+              tag: 0.7.1
+            env:
+              SMTP_RELAY_SMTP_PORT: &port 25
+              SMTP_RELAY_METRICS_PORT: &metricsPort 8080
+              SMTP_RELAY_SERVER_PORT: 465
+            envFrom:
+              - secretRef:
+                  name: smtp-relay-secret
+            probes:
+              liveness:
+                enabled: true
+              readiness:
+                enabled: true
+            securityContext:
+              allowPrivilegeEscalation: false
+              readOnlyRootFilesystem: true
+              capabilities: { drop: ["ALL"] }
+            resources:
+              requests:
+                cpu: 10m
+              limits:
+                memory: 128Mi
+    defaultPodOptions:
+      securityContext:
+        runAsNonRoot: true
+        runAsUser: 65534
+        runAsGroup: 65534
+        seccompProfile: { type: RuntimeDefault }
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: DoNotSchedule
+          labelSelector:
+            matchLabels:
+              app.kubernetes.io/name: *app
+    service:
+      app:
+        controller: smtp-relay
+        type: LoadBalancer
+        ports:
+          http:
+            primary: true
+            port: *metricsPort
+          smtp:
+            port: *port
+    serviceMonitor:
+      app:
+        serviceName: smtp-relay
+        endpoints:
+          - port: http
+            scheme: http
+            path: /metrics
+            interval: 1m
+            scrapeTimeout: 10s
+    persistence:
+      config:
+        type: configMap
+        name: smtp-relay-configmap
+        globalMounts:
+          - path: /data/maddy.conf
+            subPath: maddy.conf
+            readOnly: true
+      cache:
+        type: emptyDir
+        globalMounts:
+          - path: /cache
diff --git a/kubernetes/apps/tools/smtp-relay/app/kustomization.yaml b/kubernetes/apps/tools/smtp-relay/app/kustomization.yaml
new file mode 100644
index 0000000000..65f6edd76b
--- /dev/null
+++ b/kubernetes/apps/tools/smtp-relay/app/kustomization.yaml
@@ -0,0 +1,13 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./helmrelease.yaml
+  - ./externalsecret.yaml
+configMapGenerator:
+  - name: smtp-relay-configmap
+    files:
+      - maddy.conf=./config/maddy.conf
+generatorOptions:
+  disableNameSuffixHash: true
diff --git a/kubernetes/apps/tools/smtp-relay/ks.yaml b/kubernetes/apps/tools/smtp-relay/ks.yaml
new file mode 100644
index 0000000000..d951fee5e3
--- /dev/null
+++ b/kubernetes/apps/tools/smtp-relay/ks.yaml
@@ -0,0 +1,24 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: &app smtp-relay
+  namespace: flux-system
+spec:
+  commonMetadata:
+    labels:
+      app.kubernetes.io/name: *app
+  targetNamespace: tools
+  dependsOn:
+    - name: external-secrets-secretstores
+    - name: ingress-nginx-internal
+  path: ./kubernetes/apps/tools/smtp-relay/app
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  wait: false
+  interval: 30m
+  retryInterval: 1m
+  timeout: 5m
diff --git a/kubernetes/bootstrap/flux/kustomization.yaml b/kubernetes/bootstrap/flux/kustomization.yaml
new file mode 100644
index 0000000000..4a669d63ea
--- /dev/null
+++ b/kubernetes/bootstrap/flux/kustomization.yaml
@@ -0,0 +1,61 @@
+# IMPORTANT: This file is not tracked by flux and should never be. Its
+# purpose is to only install the Flux components and CRDs into your cluster.
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - github.com/fluxcd/flux2/manifests/install?ref=v2.3.0
+patches:
+  # Remove the default network policies
+  - patch: |-
+      $patch: delete
+      apiVersion: networking.k8s.io/v1
+      kind: NetworkPolicy
+      metadata:
+        name: not-used
+    target:
+      group: networking.k8s.io
+      kind: NetworkPolicy
+  # Resources renamed to match those installed by oci://ghcr.io/fluxcd/flux-manifests
+  - target:
+      kind: ResourceQuota
+      name: critical-pods
+    patch: |
+      - op: replace
+        path: /metadata/name
+        value: critical-pods-flux-system
+  - target:
+      kind: ClusterRoleBinding
+      name: cluster-reconciler
+    patch: |
+      - op: replace
+        path: /metadata/name
+        value: cluster-reconciler-flux-system
+  - target:
+      kind: ClusterRoleBinding
+      name: crd-controller
+    patch: |
+      - op: replace
+        path: /metadata/name
+        value: crd-controller-flux-system
+  - target:
+      kind: ClusterRole
+      name: crd-controller
+    patch: |
+      - op: replace
+        path: /metadata/name
+        value: crd-controller-flux-system
+  - target:
+      kind: ClusterRole
+      name: flux-edit
+    patch: |
+      - op: replace
+        path: /metadata/name
+        value: flux-edit-flux-system
+  - target:
+      kind: ClusterRole
+      name: flux-view
+    patch: |
+      - op: replace
+        path: /metadata/name
+        value: flux-view-flux-system
diff --git a/kubernetes/bootstrap/helmfile.yaml b/kubernetes/bootstrap/helmfile.yaml
new file mode 100644
index 0000000000..7e743b1e5f
--- /dev/null
+++ b/kubernetes/bootstrap/helmfile.yaml
@@ -0,0 +1,59 @@
+---
+helmDefaults:
+  wait: true
+  waitForJobs: true
+  timeout: 600
+  recreatePods: true
+  force: true
+
+repositories:
+  - name: cilium
+    url: https://helm.cilium.io
+  - name: coredns
+    url: https://coredns.github.io/helm
+  - name: postfinance
+    url: https://postfinance.github.io/kubelet-csr-approver
+
+releases:
+  - name: prometheus-operator-crds
+    namespace: observability
+    chart: oci://ghcr.io/prometheus-community/charts/prometheus-operator-crds
+    version: 12.0.0
+  - name: cilium
+    namespace: kube-system
+    chart: cilium/cilium
+    version: 1.15.5
+    values:
+      - ../apps/kube-system/cilium/app/helm-values.yaml
+    needs:
+      - observability/prometheus-operator-crds
+  - name: coredns
+    namespace: kube-system
+    chart: coredns/coredns
+    version: 1.30.0
+    values:
+      - ../apps/kube-system/coredns/app/helm-values.yaml
+    needs:
+      - observability/prometheus-operator-crds
+      - kube-system/cilium
+  - name: kubelet-csr-approver
+    namespace: kube-system
+    chart: postfinance/kubelet-csr-approver
+    version: 1.2.1
+    values:
+      - ../apps/kube-system/kubelet-csr-approver/app/helm-values.yaml
+    needs:
+      - observability/prometheus-operator-crds
+      - kube-system/cilium
+      - kube-system/coredns
+  - name: spegel
+    namespace: kube-system
+    chart: oci://ghcr.io/spegel-org/helm-charts/spegel
+    version: v0.0.22
+    values:
+      - ../apps/kube-system/spegel/app/helm-values.yaml
+    needs:
+      - observability/prometheus-operator-crds
+      - kube-system/cilium
+      - kube-system/coredns
+      - kube-system/kubelet-csr-approver
diff --git a/kubernetes/bootstrap/talos/clusterconfig/.gitignore b/kubernetes/bootstrap/talos/clusterconfig/.gitignore
new file mode 100644
index 0000000000..abdc20ecb6
--- /dev/null
+++ b/kubernetes/bootstrap/talos/clusterconfig/.gitignore
@@ -0,0 +1,5 @@
+talos-test-talos-test01.yaml
+talos-test-talos-test02.yaml
+talos-test-talos-test03.yaml
+talos-test-talos-test04.yaml
+talosconfig
diff --git a/kubernetes/bootstrap/talos/patches/README.md b/kubernetes/bootstrap/talos/patches/README.md
new file mode 100644
index 0000000000..b968188875
--- /dev/null
+++ b/kubernetes/bootstrap/talos/patches/README.md
@@ -0,0 +1,15 @@
+# Talos Patching
+
+This directory contains Kustomization patches that are added to the talhelper configuration file.
+
+<https://www.talos.dev/v1.7/talos-guides/configuration/patching/>
+
+## Patch Directories
+
+Under this `patches` directory, there are several sub-directories that can contain patches that are added to the talhelper configuration file.
+Each directory is optional and therefore might not created by default.
+
+- `global/`: patches that are applied to both the controller and worker configurations
+- `controller/`: patches that are applied to the controller configurations
+- `worker/`: patches that are applied to the worker configurations
+- `${node-hostname}/`: patches that are applied to the node with the specified name
diff --git a/kubernetes/bootstrap/talos/patches/controller/api-access.yaml b/kubernetes/bootstrap/talos/patches/controller/api-access.yaml
new file mode 100644
index 0000000000..7723284427
--- /dev/null
+++ b/kubernetes/bootstrap/talos/patches/controller/api-access.yaml
@@ -0,0 +1,8 @@
+machine:
+  features:
+    kubernetesTalosAPIAccess:
+      enabled: true
+      allowedRoles:
+        - os:admin
+      allowedKubernetesNamespaces:
+        - system-upgrade
diff --git a/kubernetes/bootstrap/talos/patches/controller/cluster.yaml b/kubernetes/bootstrap/talos/patches/controller/cluster.yaml
new file mode 100644
index 0000000000..aa3a9f2268
--- /dev/null
+++ b/kubernetes/bootstrap/talos/patches/controller/cluster.yaml
@@ -0,0 +1,12 @@
+cluster:
+  allowSchedulingOnControlPlanes: true
+  controllerManager:
+    extraArgs:
+      bind-address: 0.0.0.0
+  coreDNS:
+    disabled: true
+  proxy:
+    disabled: true
+  scheduler:
+    extraArgs:
+      bind-address: 0.0.0.0
diff --git a/kubernetes/bootstrap/talos/patches/controller/disable-admission-controller.yaml b/kubernetes/bootstrap/talos/patches/controller/disable-admission-controller.yaml
new file mode 100644
index 0000000000..e311789f4c
--- /dev/null
+++ b/kubernetes/bootstrap/talos/patches/controller/disable-admission-controller.yaml
@@ -0,0 +1,2 @@
+- op: remove
+  path: /cluster/apiServer/admissionControl
diff --git a/kubernetes/bootstrap/talos/patches/controller/etcd.yaml b/kubernetes/bootstrap/talos/patches/controller/etcd.yaml
new file mode 100644
index 0000000000..9e27e9d6f3
--- /dev/null
+++ b/kubernetes/bootstrap/talos/patches/controller/etcd.yaml
@@ -0,0 +1,6 @@
+cluster:
+  etcd:
+    extraArgs:
+      listen-metrics-urls: http://0.0.0.0:2381
+    advertisedSubnets:
+      - 192.168.13.0/24
diff --git a/kubernetes/bootstrap/talos/patches/global/cluster-discovery.yaml b/kubernetes/bootstrap/talos/patches/global/cluster-discovery.yaml
new file mode 100644
index 0000000000..586a07abb6
--- /dev/null
+++ b/kubernetes/bootstrap/talos/patches/global/cluster-discovery.yaml
@@ -0,0 +1,7 @@
+cluster:
+  discovery:
+    registries:
+      kubernetes:
+        disabled: false
+      service:
+        disabled: false
diff --git a/kubernetes/bootstrap/talos/patches/global/containerd.yaml b/kubernetes/bootstrap/talos/patches/global/containerd.yaml
new file mode 100644
index 0000000000..2952d6b41f
--- /dev/null
+++ b/kubernetes/bootstrap/talos/patches/global/containerd.yaml
@@ -0,0 +1,12 @@
+machine:
+  files:
+    - op: create
+      path: /etc/cri/conf.d/20-customization.part
+      content: |-
+        [plugins."io.containerd.grpc.v1.cri"]
+          enable_unprivileged_ports = true
+          enable_unprivileged_icmp = true
+        [plugins."io.containerd.grpc.v1.cri".containerd]
+          discard_unpacked_layers = false
+        [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
+          discard_unpacked_layers = false
diff --git a/kubernetes/bootstrap/talos/patches/global/disable-search-domain.yaml b/kubernetes/bootstrap/talos/patches/global/disable-search-domain.yaml
new file mode 100644
index 0000000000..8ba647c499
--- /dev/null
+++ b/kubernetes/bootstrap/talos/patches/global/disable-search-domain.yaml
@@ -0,0 +1,3 @@
+machine:
+  network:
+    disableSearchDomain: true
diff --git a/kubernetes/bootstrap/talos/patches/global/hostdns.yaml b/kubernetes/bootstrap/talos/patches/global/hostdns.yaml
new file mode 100644
index 0000000000..36c5e94f7f
--- /dev/null
+++ b/kubernetes/bootstrap/talos/patches/global/hostdns.yaml
@@ -0,0 +1,6 @@
+machine:
+  features:
+    hostDNS:
+      enabled: true
+      resolveMemberNames: true
+      forwardKubeDNSToHost: false
diff --git a/kubernetes/bootstrap/talos/patches/global/kubelet.yaml b/kubernetes/bootstrap/talos/patches/global/kubelet.yaml
new file mode 100644
index 0000000000..d86fc9640d
--- /dev/null
+++ b/kubernetes/bootstrap/talos/patches/global/kubelet.yaml
@@ -0,0 +1,7 @@
+machine:
+  kubelet:
+    extraArgs:
+      rotate-server-certificates: true
+    nodeIP:
+      validSubnets:
+        - 192.168.13.0/24
diff --git a/kubernetes/bootstrap/talos/patches/global/openebs-local.yaml b/kubernetes/bootstrap/talos/patches/global/openebs-local.yaml
new file mode 100644
index 0000000000..e4095d171c
--- /dev/null
+++ b/kubernetes/bootstrap/talos/patches/global/openebs-local.yaml
@@ -0,0 +1,10 @@
+machine:
+  kubelet:
+    extraMounts:
+      - destination: /var/openebs/local
+        type: bind
+        source: /var/openebs/local
+        options:
+          - bind
+          - rshared
+          - rw
diff --git a/kubernetes/bootstrap/talos/patches/global/sysctl.yaml b/kubernetes/bootstrap/talos/patches/global/sysctl.yaml
new file mode 100644
index 0000000000..90361d7bbf
--- /dev/null
+++ b/kubernetes/bootstrap/talos/patches/global/sysctl.yaml
@@ -0,0 +1,7 @@
+machine:
+  sysctls:
+    fs.inotify.max_queued_events: "65536"
+    fs.inotify.max_user_watches: "524288"
+    fs.inotify.max_user_instances: "8192"
+    net.core.rmem_max: "2500000"
+    net.core.wmem_max: "2500000"
diff --git a/kubernetes/bootstrap/talos/talconfig.yaml b/kubernetes/bootstrap/talos/talconfig.yaml
new file mode 100644
index 0000000000..9581d5d12c
--- /dev/null
+++ b/kubernetes/bootstrap/talos/talconfig.yaml
@@ -0,0 +1,114 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/budimanjojo/talhelper/master/pkg/config/schemas/talconfig.json
+---
+# renovate: datasource=docker depName=ghcr.io/siderolabs/installer
+talosVersion: v1.7.4
+# renovate: datasource=docker depName=ghcr.io/siderolabs/kubelet
+kubernetesVersion: v1.30.1
+
+clusterName: "talos-test"
+endpoint: https://192.168.13.10:6443
+clusterPodNets:
+  - "10.69.0.0/16"
+clusterSvcNets:
+  - "10.96.0.0/16"
+additionalApiServerCertSans: &sans
+  - "192.168.13.10"
+  - 127.0.0.1 # KubePrism
+additionalMachineCertSans: *sans
+
+# Disable built-in Flannel to use Cilium
+cniConfig:
+  name: none
+
+nodes:
+  - hostname: "talos-test01"
+    ipAddress: "192.168.13.11"
+    installDisk: "/dev/sda"
+    talosImageURL: factory.talos.dev/installer/a28d86375cf9debe952efbcbe8e2886cf0a174b1f4dd733512600a40334977d7
+    controlPlane: true
+    networkInterfaces:
+      - deviceSelector:
+          hardwareAddr: "b2:46:57:e3:e4:52"
+        dhcp: false
+        addresses:
+          - "192.168.13.11/24"
+        routes:
+          - network: 0.0.0.0/0
+            gateway: "192.168.13.1"
+        mtu: 1500
+        vip:
+          ip: "192.168.13.10"
+  - hostname: "talos-test02"
+    ipAddress: "192.168.13.12"
+    installDisk: "/dev/sda"
+    talosImageURL: factory.talos.dev/installer/a28d86375cf9debe952efbcbe8e2886cf0a174b1f4dd733512600a40334977d7
+    controlPlane: true
+    networkInterfaces:
+      - deviceSelector:
+          hardwareAddr: "5e:a2:56:18:3c:c8"
+        dhcp: false
+        addresses:
+          - "192.168.13.12/24"
+        routes:
+          - network: 0.0.0.0/0
+            gateway: "192.168.13.1"
+        mtu: 1500
+        vip:
+          ip: "192.168.13.10"
+  - hostname: "talos-test03"
+    ipAddress: "192.168.13.13"
+    installDisk: "/dev/sda"
+    talosImageURL: factory.talos.dev/installer/a28d86375cf9debe952efbcbe8e2886cf0a174b1f4dd733512600a40334977d7
+    controlPlane: true
+    networkInterfaces:
+      - deviceSelector:
+          hardwareAddr: "9e:a9:2a:00:ab:4d"
+        dhcp: false
+        addresses:
+          - "192.168.13.13/24"
+        routes:
+          - network: 0.0.0.0/0
+            gateway: "192.168.13.1"
+        mtu: 1500
+        vip:
+          ip: "192.168.13.10"
+  - hostname: "talos-test04"
+    ipAddress: "192.168.13.14"
+    installDisk: "/dev/sda"
+    talosImageURL: factory.talos.dev/installer/a28d86375cf9debe952efbcbe8e2886cf0a174b1f4dd733512600a40334977d7
+    controlPlane: false
+    networkInterfaces:
+      - deviceSelector:
+          hardwareAddr: "e6:e1:01:cb:6c:ee"
+        dhcp: false
+        addresses:
+          - "192.168.13.14/24"
+        routes:
+          - network: 0.0.0.0/0
+            gateway: "192.168.13.1"
+        mtu: 1500
+
+# Global patches
+patches:
+  - # Force nameserver
+    |-
+    machine:
+      network:
+        nameservers:
+          - 192.168.13.1
+  - "@./patches/global/cluster-discovery.yaml"
+  - "@./patches/global/containerd.yaml"
+  - "@./patches/global/disable-search-domain.yaml"
+  - "@./patches/global/hostdns.yaml"
+  - "@./patches/global/kubelet.yaml"
+  - "@./patches/global/openebs-local.yaml"
+  - "@./patches/global/sysctl.yaml"
+
+# Controller patches
+controlPlane:
+  patches:
+    - "@./patches/controller/api-access.yaml"
+    - "@./patches/controller/cluster.yaml"
+    - "@./patches/controller/disable-admission-controller.yaml"
+    - "@./patches/controller/etcd.yaml"
+
diff --git a/kubernetes/bootstrap/talos/talsecret.sops.yaml b/kubernetes/bootstrap/talos/talsecret.sops.yaml
new file mode 100644
index 0000000000..8a3bc9e589
--- /dev/null
+++ b/kubernetes/bootstrap/talos/talsecret.sops.yaml
@@ -0,0 +1,43 @@
+cluster:
+    id: ENC[AES256_GCM,data:Na/opm9Ev3mlCNvTG4qeItC9n7I5M8xx/p9KtaWyZNVEMPBTw/yUClsUYOw=,iv:jOGmQ8YMeKHApST0W5x7T97CFAd78VinW/upkyXw3ME=,tag:QwTw5U4b28082hMJNW6QMQ==,type:str]
+    secret: ENC[AES256_GCM,data:SeEjhn50dsrlduNDateT16Kl/O+Eex+AngkfnFN/NtClTbbJrmQd3p7pth8=,iv:eIxa0hu7KedxClmh2LW8syhZ3qwR5xKjbOEl6fapIm8=,tag:k5wnxYVCP2KkKCmxqKtbvg==,type:str]
+secrets:
+    bootstraptoken: ENC[AES256_GCM,data:L/+LSyWve+OHP5JnxPjNOmG/BQxamaQ=,iv:/g5S9mCl9l5+LQkcNaUGp//N+VIVfrAT+AglAOPlChA=,tag:tlH4x3Z39mwzRHZROYjzsQ==,type:str]
+    secretboxencryptionsecret: ENC[AES256_GCM,data:/KOxdMRBZmIbYUIRQEjD3kZvlBolVKYjxarOS1cyLMciUwzeSr1nUcU40I0=,iv:YyKkEhghk9g/cLWUsS9jkSoPZmoaIuThQbiVIZf25ko=,tag:tEVbUgIR2apZIMDF1Ue1IQ==,type:str]
+trustdinfo:
+    token: ENC[AES256_GCM,data:u49VFg1PpYnXn0DVE6TxQob0tSJ5Zd4=,iv:3orj2CoYXnyTFWIRjOfUKySoh4qTOHPWagEin7JezwQ=,tag:a2qukyIQeO3a8QIOCEA6gQ==,type:str]
+certs:
+    etcd:
+        crt: ENC[AES256_GCM,data:XL0XVUiIOPm9bOD3SW49TbMkpYunheQXcbNOq451kwKlF7Pm+Zvsppcaox7pMXbO3k/y8It+oiusf3ynw2znwKu52kkCKPQGg4qFCilsn8IAsY+Sm7Fs56wCnALN9dGb3ixgnMnPVl1hJWoTK+cMgc+FlTE8lTENf0EKKjKPmNybwrnZnqnQSLxD0qr8/p3XHfgGzXgCWBuWtkwTiZibsu+5XAmd6cKiR6MTv6faH/GaSJ7qPfKBqeenm7rGuaCBrWpu0eN+J4CafADmkrBrM0AOSdbThTCzzUFrA9+vw8RO4G3bEICj+GQuVmkhDhlkxbYwRSqx4VBsCca9wsnzOCZI2lR/zr7TeB9EPtyorj2Dqhqg5fJw6MFeUrdCizPFiQ4wpdgkjHQT2XgWafain5l7qk9WFPN2sgbUJ8vAioP+NA4u3U2WEh+fLUB6NVqt0n8LC8ti0WE95HffMEsnJ21XJzCrxRZBrQwV/vfxbPxtLad8/k2oZVxmAF0baOWNrYbuyVpxefOG9Acp62+KivtybNjO5b2tPpSYfuZvqtk1ufsm6YTKlUrz+ivHM037Yem3/aFxwcfzoW8D4H1FjMPBpNrg31n3Bv6R64zbCirGAWUywp3u4N3k9Qc7W4U8q9OmsGZq3bNTk/NO1OUbwBLjw0ZQJ9vfRglVuWwPAZqW+N6axKwZiYNyfYqKBDNEz4cIT8sNG/fZ6oPYwF8WJlsM5vA5P65UHP1IkDMrLOBcTKsDvF1593iVWiiVidkuBYm+WFvbp0AFc2KcAdHXD1W8Ze7s2soYrnC+Ugck+a97+YsTKQ/VsUZaUMDRFBNi9NZWpIy6D5DVT88gxDj1cgKo4kVq5r7IebNfUG+3J4YYmbE926UGTMgIfdSk7IIs1/k4mjbpii1i/8WIG4w8KqzXlE9g/1kg5kgQ4HjShj1cfAcYKxLEmKyKIkLcDyVVKtb/U8nMEMykvXSEEOAWHxKvvueDpMdsUA7WYGD8lC+F44Ffuo4bTwdvsSvFWRwzG4fjTQ==,iv:OWYyqszvXCsvy/2jovIaMjB51JpLK162t/2IgAicH/M=,tag:I9klN4vprUR+P9TMO9otzw==,type:str]
+        key: ENC[AES256_GCM,data:4SBy6qxkfGUj6yB5wuRArf1Mo6HOYE77cRydsl2IbOEquJijvRFHMEvP1ZDhySCAbUZoXjnVrKfoNKuiUhe7KV9RAsCJJSEnJKYdUUxcjmZKQ8uTyd4KUF4vV1qex4+ugfgkRakn3EXHWg656fwk36fvIFhU/hb4AkdovIjNCEa0dPCg5lHIQ2CnEzJ33/+p2M05pixg2Pfmdq/P8OvqfSBQZ+/mX6X4KQS5Hou/sH9nGJvYSS41PmttQZdDRM5kWWf9TDNFooIZMhgc1KZXRogGiPI5kucGSi9KiVypPy35jqP0TLCDcS/TnMgULBOCIM3hZrXBdtAkgctjV238htvKaL5QeHa0r2ZXXi8naGEoDfS7CSqNGZlACyMJlpzeXXOhDXRROBPkF5P/c9/asA==,iv:EHeUTQQ5nr3otksfx0UevnobGjw8RWhqChujNfzmlgo=,tag:vagapBWZl8dFpgOy0W2MCQ==,type:str]
+    k8s:
+        crt: ENC[AES256_GCM,data:GTNOdJbQjnE1I7pt4rkNrAdadVUXNPUlz5l6zAna4vQR5mpK9obLZCH+iBIYvh9FAwej0UDlLdvcZTmjcz8i2cnKuil1R9QPm4dqJFXAd3j1Mch3oBp8kT/Ay9Q1BFbwgDkApGDq0Rk2mEIQmrg8Oe59IaTMXvMVJZdd1A8ZkDFvaJmGGDhjpOeL6cEBXEYhM3Ur4Am/LtH4UdrO0qSfi17WiSw1/lHCGORFGLOn/hZI2HgdHvX6speuf5Mr4UttKGNbak/4aXESk5stoRrfbvSOMmq3Gvan4WnyTJUhzgINTs5BNslj9bcFm7ddf/R2wMDokoBBqex6zQxSblPISNTgJs+psu6WKzaKivbCvd017lI3F4+lIQPG2VmngkI9m+TP9/bnURcHm/ciCqszAyh6KkqSEeWEmfsxjqttw/ksKCmzp0LrfT9UjNfNrb0YXpVKOYAxMrcdMi7IXBJHPTbhbzhZdYHNojOsF011okIblpPqs00yVaXbt3y5Upgr/WSlzPznn45Ls+J5AhVXLfAbrCujfx+XmTAEh0th3ckULqs8wHa1hbS+A0i+ul+Jm6pUkNaWEMvpYdiNPn1fLozM+KaCj3F3ZDQSJJloZr2JBPlH+Y2NU1ZNw4qVvnqRaJ8O/7OPjO2v+WmKq2tPctBdEFgZuSiQ3j3KVdqmv/N3zdF9ioIf8zxO78Z4paoK4JLLBloyUKt8mnXcZnW5SGUxudMzHeaVDjt8O0YsS4JvVd1ZiT5ht/VnyYh9kWluYU/Peda/pZ69ih8Sn0t5u7C5A12wc8taLCidyIIG6/Dl/0g8TMjlKXtdez5Eu65+Aw/6at/Vk40fdiga7FczlCreQmjxxrPIzn60q/31+9daclFc23tx+824CCx2BX/EYHpI0pQM0gW5gpul/WDzk9E4qwfQN+afU7kcHb656LE2QAyc3YFWgEIRe30OmIzuw9KuL5bQeUkJ/x0mYKdcyi8dLuR9bIIFcg2saIk/+AHYJ0lJvgYAWlQgsYbW7xtchwFfEJ5TxlDI0WjA3ZHZuoiQ5T20RcyBRZxZmg==,iv:VANbOxCIDcCE6GH+I8AG/A+Uf7Mv0BweCZQTO7MgYaU=,tag:lwwOwq4fxKztBCVNPKbHbQ==,type:str]
+        key: ENC[AES256_GCM,data:GA4nnE9z6S9F9tYh8QltqV2Up6bNzu7OSF5PAmStDyQoatvp87RZK50WWKNtzIw/zoP+eQDPgccFduyAWFIDC/L8wZCxT6U8v4MYFpe9Yl/DCdo6Pca5W+gYiaYH0Nno4YA62PfLkr7MAwxUR2MCjJgUbo540dXpy9WX6ZN/fq+5WTNmXq1uSUwT8wTMXvRLwBa4TpKO/xudwMfMZ70o9kUxH2VTGf5pJfxWrc4BaG2gR1apMC8Et+FVQ6EB66iAxDghPwTks9mW8M5LxC9FxevnTUFHA9vM9tENmG6tqugJG+KliRYfB9hi0BgPW5oUB3zII05ACA7n0xm/8th1EazbYVeySfBouPg6tvMGjM8nNhF8VSVqbp6JYcEAcGA7hkoHPfH6W9E3feE/FLn2iw==,iv:FkwkSguzfFjG4khlSCRBMF8SXuf5gH+hiVllN0cIIns=,tag:1wcMfvmy7AhPIWhrJRDwdA==,type:str]
+    k8saggregator:
+        crt: ENC[AES256_GCM,data:KkZnPRD3jxddDyZh+mef9Oeru54s3HetKQ/J4dH+U3+DlxFQWmnJrhrxCt9Xf2k21gVPvSIsoceXZeZ8rhUvjZgOhGcV7CaUodl/17YAR8ArVeNL23dZ2FsfeYKV70FTswgHjhglmTZ4rX7W7E0joYWfu0GuykNOn6Vb3dfqDu5oS5iLpsM/oeL4lTUIPqbPbXp+eakW+EYrh8llB91XGCcLV/8spi+JRZiNHfIS3/wZ1lUklviKsQqXJaq5tXYbqE1psAIg4XDuFJFLzr6kvgSlUJKLpm/2n0ncWhUxaTeGvuMKPBjBmZA4r2J9MZ6rK8plP3T6/H0xqam565faQVV/ycIZfLq7cijBqCGTGDmGb6veil0obilV9x+duh0j6HWrYmSzuIaVqOj20HDEkJ4VOmFZdUTOxcGODf0TE7BoaaGCHiaOKrNq0iK922FiyO7v6IsGFGi6mUmSx2WMg7+UVl8szPXE664QRQIQF2mhTQUk14/L5Br7I4p24sfvj/RijLTzNj+Q6WbGXPW2UEmfWVnCE/mSH553qvFPE+VeUYzvaCQqkv9OK4fDJ/zsNd3o3qsrfgTuDMLYdKXLIZs49Swqv7sJxx430m1AqLMsa/9gUYUaEcacNVhWy9v2Wak0t4xhR48DHEOxXb1YVvoYavE2GYr8593w1So3/EQgvd7DgCP3vf7CkMvaguBvMKnTJFn3wcla/KB9HJazjNn6xx5IYh1SJsnhhTw98ZZVYolq+T9Src2nmr4u7tP4fWeDj3U6xBaFJoD3cFPtkvZRB38kcXzBq3IY7WViWhVnSmSxw3Un8Ij8Omz5AEcxsPLcowDPNm1zm4zMV+VfAwC8Y3xobDsPKVeRYnkHLPWsZfeYYqVzcTnCRPzCgwo0emnN1SEnCL64lEUvtxJgrvZRnjQFuHWb4A5hY1bDEU2W/WiavTmb9A==,iv:lPkiOJvsmC2JUlKlResHut+G/ecZVUi3RybMc1NrEYw=,tag:56GQjrdsIw+5TXAVwUCaRw==,type:str]
+        key: ENC[AES256_GCM,data:9f4gmDeiEgf7ysmlhDHA7w1CHM1xbluu6TwR8MrUSVn5PnvX6SwaQkEGUsCIQWOGN6t3Tl7FZdI6NKphyTRTkFWBnclK7Dsm+lmEU1cmFfCZCsNYptLjwtdMVYjS9120j4n1wq84+ZcUbLADabfAWazFZols11Gc7RiMDlVk7ozc6FO+JyD3NhadBAAtMsPZb/jZlfMQiXBA03VyCVX6VIc5INjzoH19K/LXxCu6UJ3sm6b4+wn5YlcHmde1BFpC1K9A0JDdJJ2Hf7XKm3KWbavZAY6+Hf/eNNHgJQEXOrsR6D4UyWOt2LzrymyK5SN09skzVRl3fSaOMbmYDwnS9z5qwOVTSuLfwxt8xYcV1XVAuIJebS/jBZYuOfQeQjO0wIDnCeGpgogDqraPfYRHXQ==,iv:E4CrC/y3+j/ZM3emOFA1X3e5+Ydz6iASSsmGQVv2jmY=,tag:+tqkSjnieezRcQOdF7QDvg==,type:str]
+    k8sserviceaccount:
+        key: ENC[AES256_GCM,data:YwyRpEz7MvF3mV9ISkiFFBcQdaGyLLiz4wvWM3OU+9TsdRGfNUGKed5SlGYVAB1nQFaT7afcx0WEwdrrP7kpDZwN1PeAzrdllT/1CBMg0LJKjBIK7d2DwtyfufNw9I/dKrOXfQPO3Sc199qQPQXYe4CTWjljOTTPNxMWRDXbnHvHmCjnJJOhzRIUZE4FfsX43L3IMoN4demA6I+DuUAkgOggTQQcjt1Z8RUuduX7lTSh3mVVl0cG0LU2Wb5yorhRcNcfYROOaP603nbHYg1PGgbQbIRvaxqFOEe6vA3HjrBtjxVhJOsLo1NF3+KNj3gWvhKtbFe3CPM3G20O2QjCxhfJO6/19GbuKqaochLi5jjwOO0wV18R8UfgR8vU+0B6PKxUdzRIBIn/kGU8bh38bHfxIPbQKwAu3F66ofO62lEeU3PuWghsUaaesvOx2IHugyqPpPzUzaKUckACF9Xv0MqO2AMYaSC6uHQMDyAUkx2xpThVG6fiOWf6dMhs/Q8FxI1k1umCzTwC27Brk+VEPvhGfRcVKXLLdiPjmT94HeU4FHQv9Tdkx2zb09stxMrK5qvwRj2OvcqWhbhTD+LXUZpGIr2YlZBYPNav+GmwxEaMkewVmfqtzCbpME9n4VlaXgLFuI1efl/OthUOA/7HEg8WyZCqAWIV8rAVSjg68EyWX+PH0VsaP4DZqPJaLgGXcT2KvkVXyF6gxY+YWlRl0pmtq5hArKxEJBPNVE4FN1N/dEFhLxuidfE1MRUBygZFN6DsaxKrDd+z0pezVDz2/YClnbKrnP5HIvRa8+NyZ2q+GFMUoljnHVnzCFARp6VD4o9xi+8GWbDa7jKG1muF2/KKwykZkwkq1Fpvx3Hy+a1uV+UNxNeA1Xdjgp5O5ifFC46ZFzqXf7YUB9n4JuDci6rg/rxjLLCbjUN5QdZjAbb+HXs+zaMGFK8omCw6ACbiMTPZZXHlvJ4wnhtto/8qWyNMaI2lfzB2SQb1jr6nZVEa6U3xJmPljE3dsYId/5cIEzPE+f6mCM1Aa8nkHb3AHtDPRnO3OYVHIrrKPObPSstePumLGS79fcVbN2dTgMXVwT1VVsggWD3K/QOz7XpAVHT/bTErHCCMrPV1PEvUXLahALgKJp97+8QXqEYjkVjL7PM966oCRdlO3PBIvF3SQYLoFEbQUzXEwJexwwXVzpZVXDI+0Kr/pCQUkNk9YAc2/e8m1MQLZ6L+1AHs4nTodyLYg5Cz/kwR9nJnqLRLKkw0lbb8D+VO5ZJXBdPWGdni6+eC+nHSv6ushBJ3oqfKVgIgPEphdrin7OAmkLTbWf0Qja47oXEWqhnuMsjKgHq4u/1hZ+v/9pQfRL2lUs5zOU4bD6IfdobRMf1qp//KJbn2B0LazKoNaojG1w1vzcpgh1gOuwW+L1i31L77+CpvfWRoyW2FDu+a6IhqyR/L5PDsHXqxoz2EXxkuytqeI3gJLgyQlgYvGbjO+WgsYONNe7ye1RTabXBqRzpZIzn4TlR2sq05DnqEaVNhVOssEwAeC/oABuuguD+A8oYLkKRTVkJiDq+cx9IQr8/yisnygT/oPqYDAvpQwa1EF8sXSehAzUl4PSCo9h1d/dub4DLbjwZmlhrGQWhbu8TVKcQ+uqO+MEFRH/FunhFmF0nplVzeQBMWaeHAVqSoKptpEFnhcKbzUxSEvk7nedJWWtZT2q+txCN5Jcu+8XfIwf7OH6+yJ82jWdbX6czjWqLYDyVzbc8s+VihPpmOikj6HKmHbpT16dXO40lfOi0nyw0IRXMZtekKkjxrnP/rPcgINl1il8o6dbwBdr5fRGikOttQ4MD5dNbJQ/FW9XcaeOg4gU6fDMI16QkCY/0Qv4Wq+mmBaC/xmZrrfNw+MPj/HD0tqboNkfyK00/ulMIysxh7G8hZQ1TuhmNQCuOlJBj5eBWtQijJqTi5Qi2c1YZnmp5ocgkQKLpaPAXhepwbdJaLvhx397wgMC91w8hW7FSF/ewOp4WHU479onu1HXXqbLoHTUDK1iM+4zj5AEzRO/typCFHhenilIkAOuVvgYhWtb5cFDl4Q9FstFJUI9V61YNi5SWKgyI2nG71L4/VTwu2nXdU5E0tgbIsvu8mSNxyn2R1eQFnOt438rtx2PgDYnKzrVeFK2aPiJwijbXa4zmCo2Xwa5SxPnwv4jc55kQpdRKV/rXGslzFKFzfPJFea4o6yGAucPBCapuaLER9Gz6Vj+KogQqF/lI07H7QFvlxcDiAJiEKtzd2aaKzY672ZAW6a/SxGQnbE7P2HDp+4MD+wur3fjvjhBTkiARgN6+QUnLrDL82UyqdqWD9b31NqRMX6gzNWDbN9GOAMzMFl5EQpyADBS/iPIYHjnnCDQRDkNpAlMdYpPfI0y5W5S08XTZlmaDtYH3aPkfO/2liQiEv1C6EGTQbfSXhj6sp2yunuSVCC8NSBy6NAs9K0yE1qWxj8hxBJbHfNPBApwqdThUN6KRJAr6iVbLO89K+fDvGbE2NVHx/3eHetcK9+tEiaHIiTxBJDKYcQvpjuQAiswJVvtcde/nDAWg2AbfsAwbNXVv/SA1mO3TL5a+OeqkF+WH3MgJaMkqjsmu6jeIyS5YBFHWN/bawNGO+OYIqUiGoSbCAE5skJlVqTNVXMe0EC034Tz5xaevNAL1lXTOE9iKE8Lg5qCiB8hh2nEaa0Ya8lzn6d7o7rDRGQx1EqzJH8yYYoJ2wFRXWpo49qjwYa8OeQEW8GCxPgNyLbbN674MDUro99HJhcaXMIixzSGsrtsq/ZUUJc/fSor3FVuIbgdnh6i7fPdCKv1ti5o1AdUunDwRo7K0Uv4z0N0BUawwrodrHOeinfwudH2YlvjtmNAePsND9MCbzo3mQkRbjj7MzD95gI5b+Cdp0/9imoTYT3G8UgZQ+3a0NhiCdYirzwkS8bU0xRi7A1klWNQwSXK5RNsI/pWj7/FS9nXwO3VMwuP0QUxsiWOceLItVtsICZcXT042hvzYWcKiELlZtKoGGhSG3EXkn8h37FJUIxV/I/HTamMfnA1IGh/pLEMwDmhoQEPHjkgYMP04igHT7jrrEGk5AVHA+1tyR2iOLiwew9IWVTPDxiPMhjArIFc0tM9cYWDdlOHEOmhrID+gqzeJz6zxvOdxGX4hBhIKQkwGeuPFhLUaCoUD4w8AAOWzM5br6f+Mrs8FQwxpM9bz7ipoU+YvergbEEUTVbLlcUnyJoQlOnvfSEcsF8vlaDcutFoL1Ru/EzIAidhwzis3KzORQFjOezXt/bBiI6UCZ8ELkRLC33st9oMx8C1N/IRBLKik3ilODsW+w+g2whpGR2uOwYgFlQlJhzsFbp8UslDGf5B91SIUIqRErs71vWwTN4H2jJJpIHYLCgd0TqmuNq2ZcdTexxRa9UEdpv/AIePGjCO1d41U60PQUOwVgv3xsQLsK7FO7vEgn2foWBN2tZgw4EwBUTYmjIMGokbLEFWRFNdoc/S6QQ/3VBY1neY8aS6n6X5j6SPgpsaKQ8B+A96DuyegEqCoH+5Bx2ZX8y6BcbwAW6xaFK5qG942QsisyRaPBUhxxn1mNLr1gtHV7By2QZtZV1OA2hdCxDjlv605rIT53dAptLUJbjExdqAcIpTB61eIe93kT0PTcqb0HgHq3yzpsrCKdXvOhIiejvZxS3TYSJ3xd6jbcJAX0BwU8y095BItToBQLY5HTRwNoYZalMF0QAA9s2lpzdchTv/RkGFuzlRTxcURpPzioE/FTZ5LJ/jV75MbrglDFXmvEshUTyRDJaldmsSVX0/0XmiaQ+eZx0ERdI+vIve/79s8qKHnluQnc5Z+z3jfcgsEqC4GnTyKoOmOGedkjOA8dMfptkNBgmtnBE+uX5gEVKZzXl21qj0q5J/nB3UWWIliZConWhq8dEHHM9cqykm06VECiH4XBqE8s7xRS3U5y0yjSzrzB+AtCaNcua2Zplr3nocknh0vRLax+qncLaW1mr+oyWzU2UIN8ot5cdbgHpBey8aVi4rLqNlMlp/gguEvLl6CvCzCvVCmqALUvhMlYgyIBcKC5UymQdvbwuKPdHbRpt9WI42Dd48HUPdQX39OjtTuULl7xZLLwjGQgiXc1OkQhhmjjUVCcWwyFMThsLaCidoFNNecmVfutfaM18Vd7xkcQCnOzVtVkIJtG3AYJdrAvqsKn7uUQviEwoOYsvmrt6IGeiEyD0ho2Aw3SUGXuEPom5wd3ItUB74twIVgugzqW4LWdvZQHQ2NvIfTF1GdfOTg+BDYwDzMOIPkTziHxeejenFwNC7+EYiY2/uORR4JpQhN7U8OtliCm8mENQAjVByV5BdunaSurT8H4/U5SSXTlzS/X08lVjv/jyHDDDBDiQc6ryQO7OZ5KYCXUw9EpMUS4+gUljLtEnal0N5MgCfdA765BnPBWxBnCxrPxMOzoiFpnHhIc8A94oRWuDtQ4coYGgSTSEsCUoy52Bi/ej8RNcH6VNOLV5ZLyreJGGifvdkQdH9eJwrH1sxzRRDWHzguYDr+8rYeevZFxBuikUsS5CAtzigoX+gQs5HeseAiROAbme9lC1SunL6QxKDZAXiIVO6W1JogTmrKlt0yBP2jxNqlHhShusBKs4531v6n+sBKdn3JqY/4AKqIqlU+7BLHBbdkfKWCtAtE6KnctL9wd1NkMtA9u/vu8qTeLMUlcCZJfoAPyzLSjrfMIGStNNljlW7pWQ9KAo82Gl4ZJX2ldVrI5bu3oufuMybJVBCiHM2K9Z4/DinBitSxFUZiztqcUAES+CJmT83CBOvKjb7K7nhJmXH2YpY0+kNfRRVoo4mfwJYr30ceFLsn4u7vcYRrTYrlYJ++mJFvhyAYv8PncpywWNtqk595+vg5EQ+3jcFHyB1atRezxnn9qSiGceONFbZ83XqpxrMBmlg0FgyyNaqA3PQ8zsDywuv3EvdW/kQicRgpRKLgo1dZQvQ7xWOxr9UIv5ul7wWim0Fo8jUFS16Dhk5u2oKukWreG6CHOeUuznatnR5Uuf47OHLhQne0azpWS9u4ufkHC9fyJ/ItJlmXVM9PJwaDClVG7dfln7vHYU1g6V+KiWavY3WIXjdKovzgxnFn2ZEpKHOH920K2r59mLSITLEVBIsNzRZ0Mtkj16XINnRRFC8LSOjosDdSXDQi7DkhlahXkY3nJmX1MCv0d8NqGzz1zvIdNfvtRIvRHe2A+WF1ZFkjLSYigC4oQ1QFYRW5Jsv+xxOTiL6PMXlalYZzeZxuPazek0sRCkHmrKqUAdNHs24TTTZDLYdU6SR31U647dgKfNmtyw6+xc11Vy52j0YEQFM0jPUb5riJBN7CXIYySRWVJGM5R42EWtgW/e8RfimnK9FtfED9qiI3DQYX/iJ8OT7cdlE+OCp6QSqTK9KtJ35NYIXJEEtywafKCnWom5+gEfxZtfBTQr0CNnX74+GqS50nX7zOLM0qkXvL+mJ7J/OTwK0aI/fa43TBc2rMtG4P74YbcCjBiEj73yKk3sSOKUKokhiPo0O0Ue/5XSvsMDTSfmTWhdick6PfqZJ2GsmZULZaDhL3i6IQvIUbgsTIghwdp6lX52ewJzlPxplc7wHrKCSL0KpFiw6XwnUeBf5UkjQ1GCa2ZRrgz4xSLqZl0f7vLBI6E7lGfmQ+Wh7yQAzoR/5nVvcLbvpEhhiBjwOwrfi3vzDEK21S9frCZ5ghyz/ytfmm1QUQwU82umuAnQm8e0K/Nf6+IJXNYU6vjfA==,iv:1XJVixfCQ6e01mePZ/amLTaj9N2uhbiyHnuTZdiGnB0=,tag:nNTr5SX88KRqnOUdvPUoLA==,type:str]
+    os:
+        crt: ENC[AES256_GCM,data:98x+xUdKQ3505hSEcIBEbR/Uf8Z99qb4o9jcfaq1AJ/vjx9yg9wM0CAt2ZQ3etaMC3TUaq0tnxUhiI855EYHbv5MWCUP1+ddmv/3R8+Ub/NDK1gDYU7Z0COsFOSzLf4cn4Ac3uSm9l/Hz7TNQ6vf7m+/CvugS1xM9GAa/no8bqhSWzzcBcOP5npfWd3bRO77ON/LxYyfru7Ix7Hh/+TjVzmYejwhyc9ge6DHQ9IKYyTFli8FmePDTLJgS4YqKV/IZOfeJWBo1i0n8ouPKwtamCSNQ+AU9AN35nsm5cz0s3AykUcOoqWQySWxfH+dTVlat02nxeoXU4H/rlgzj+a5WbZnAgvj09k5/84R8j+PFmP5AyiFMEgaGh4nKUNQMIb5EJ35Y06/+99V2C1LAGmr+PjuJGv1K9oqHjsEsHhhvcFSSzR60BJWgPm67cnHByWYbyCbakAqUQiWb9ThARA858PEFpvae3PL747HUbaqrCV1q+2KAn83myxqAbcqPDbyuCY9Ppgk+0Bz4b2WVL0hB+d7NtmOuNCZkNCMthDgyyqyUakf7wRi8/32QXGmrNEKC+WI3FmH+P0EQk/fvwKrk7WDt8sHZRyWnaFNk4IWju2TzKIAoMyPIX+TKjuY8P/KpmwJQNylZjGfZxWiyj9ejUwpMVlRtOr2wt7Vl1H/ETzFQDhdpsioV4GsWIYj9nFrYz6KJwP8vhnPZ2I8ImFqgEPsa7c+hVyt836WyRUE8YH/1zQPD7GMWnO/R85uXIJ7cUcCs4cDgspii3gZWN98kNSE7JB01IBOmQyMZlmdYiG1Ob6YlLoOrYjsT9I3HkB9HIpMuKoZSoZEqLkEEziFMYhOvoYQSuWLQyphoh8Elpr2gM8i,iv:gZen5SkrGhiM3xgCOskvSXe4xL1AP6cpVTZXwI+APs4=,tag:hNIeTLNlWo+LYBIt4+bSPg==,type:str]
+        key: ENC[AES256_GCM,data:q3+85cURMGPPulRuxMHeIA2BABiqBO4MSEb/fqKU15a+gQNEqECagshxtDa2M0fvx1xgmWtY/oK4AGJIxSIlX6zRnRKS3SCfYIc76mDEM5nUz7ulCAYOINOcEGLbnsRMZwEZb088OgH2fb6nilmjxNWqrcQVh9nEECr7s0r8NeLl1q6qPb5ZDO2oxRTtP2poj8x6dVTarRoqFOP45U2Le03L0TKiRS1BlIi/gncs8jjqSQFj,iv:cJUc2u6l67ZPYkfJHkInRJACCNmmvd9U74CNnxBIU34=,tag:TbQk002LZI+arBf+c0amxg==,type:str]
+sops:
+    kms: []
+    gcp_kms: []
+    azure_kv: []
+    hc_vault: []
+    age:
+        - recipient: age1y0kzuf0tn94a74whazwae4r9qal4snuqfuhl5jacscrpr7up5gts74fe5w
+          enc: |
+            -----BEGIN AGE ENCRYPTED FILE-----
+            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0dmFraWRzbEtQdDdPOUU3
+            YXUzNjFNcTJvdXdUNUc5aTJXb2FmODBUcjBFCmd0ekRyYkh2R2NPc2J5THNmUVlT
+            WUxzTHhlVks0M0FoT21MTkxlMzJlK3MKLS0tIE1VN0JzUERPTGFQemplOUtWR2Ns
+            K2FYWURjUEVHU3dkUmQrVmpJS0QzZUEKgPuwlmCGHiCbx6oBOiic2Y0XIVsGVGPo
+            SHVmdAhQu+kLHxylcT0gqnXdGnexuBJaltKOMgSnsfRMnFI/s8MPUQ==
+            -----END AGE ENCRYPTED FILE-----
+    lastmodified: "2024-06-05T13:32:59Z"
+    mac: ENC[AES256_GCM,data:cAvu5yzKOPdxVeXdCrB8BGO5j2LhoEv0x9fJR8JpqTO3mJ6ijgCLco4FizKWST5CN3LAq9R5CR5u1GPwZqZfux2Bc4PqMmH6ZYntmWTZsLfb9jXZaV85xcaBCCas17wJM2bv9X9SzCIOQxFCX8zG3QX9z+mu3k765loiZ4wvWz8=,iv:LY3zuCnnxywVJY1ggD1DlyM/CMfh2HfopeTXD0xAOlo=,tag:xK1xC03xjuSoaeg8Ai8jFA==,type:str]
+    pgp: []
+    unencrypted_suffix: _unencrypted
+    version: 3.8.1
diff --git a/kubernetes/flux/apps.yaml b/kubernetes/flux/apps.yaml
new file mode 100644
index 0000000000..408c48bff3
--- /dev/null
+++ b/kubernetes/flux/apps.yaml
@@ -0,0 +1,56 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: cluster-apps
+  namespace: flux-system
+spec:
+  interval: 30m
+  path: ./kubernetes/apps
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  decryption:
+    provider: sops
+    secretRef:
+      name: sops-age
+  postBuild:
+    substituteFrom:
+      - kind: ConfigMap
+        name: cluster-settings
+      - kind: Secret
+        name: cluster-secrets
+      - kind: ConfigMap
+        name: cluster-user-settings
+        optional: true
+      - kind: Secret
+        name: cluster-user-secrets
+        optional: true
+  patches:
+    - patch: |-
+        apiVersion: kustomize.toolkit.fluxcd.io/v1
+        kind: Kustomization
+        metadata:
+          name: not-used
+        spec:
+          decryption:
+            provider: sops
+            secretRef:
+              name: sops-age
+          postBuild:
+            substituteFrom:
+              - kind: ConfigMap
+                name: cluster-settings
+              - kind: Secret
+                name: cluster-secrets
+              - kind: ConfigMap
+                name: cluster-user-settings
+                optional: true
+              - kind: Secret
+                name: cluster-user-secrets
+                optional: true
+      target:
+        group: kustomize.toolkit.fluxcd.io
+        kind: Kustomization
+        labelSelector: substitution.flux.home.arpa/disabled notin (true)
diff --git a/kubernetes/flux/config/cluster.yaml b/kubernetes/flux/config/cluster.yaml
new file mode 100644
index 0000000000..e1712665a7
--- /dev/null
+++ b/kubernetes/flux/config/cluster.yaml
@@ -0,0 +1,40 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: GitRepository
+metadata:
+  name: k8s-homelab
+  namespace: flux-system
+spec:
+  interval: 30m
+  url: "https://github.com/tuxpeople/k8s-homelab.git"
+  ref:
+    branch: "main"
+  ignore: |
+    # exclude all
+    /*
+    # include kubernetes directory
+    !/kubernetes
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: cluster
+  namespace: flux-system
+spec:
+  interval: 30m
+  path: ./kubernetes/flux
+  prune: true
+  wait: false
+  sourceRef:
+    kind: GitRepository
+    name: k8s-homelab
+  decryption:
+    provider: sops
+    secretRef:
+      name: sops-age
+  postBuild:
+    substituteFrom:
+      - kind: ConfigMap
+        name: cluster-settings
+      - kind: Secret
+        name: cluster-secrets
diff --git a/kubernetes/flux/config/flux.yaml b/kubernetes/flux/config/flux.yaml
new file mode 100644
index 0000000000..4f9bb975b9
--- /dev/null
+++ b/kubernetes/flux/config/flux.yaml
@@ -0,0 +1,86 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: OCIRepository
+metadata:
+  name: flux-manifests
+  namespace: flux-system
+spec:
+  interval: 10m
+  url: oci://ghcr.io/fluxcd/flux-manifests
+  ref:
+    tag: v2.3.0
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: flux
+  namespace: flux-system
+spec:
+  interval: 10m
+  path: ./
+  prune: true
+  wait: true
+  sourceRef:
+    kind: OCIRepository
+    name: flux-manifests
+  patches:
+    # Remove the network policies
+    - patch: |
+        $patch: delete
+        apiVersion: networking.k8s.io/v1
+        kind: NetworkPolicy
+        metadata:
+          name: not-used
+      target:
+        group: networking.k8s.io
+        kind: NetworkPolicy
+    # Increase the number of reconciliations that can be performed in parallel and bump the resources limits
+    # https://fluxcd.io/flux/cheatsheets/bootstrap/#increase-the-number-of-workers
+    - patch: |
+        - op: add
+          path: /spec/template/spec/containers/0/args/-
+          value: --concurrent=8
+        - op: add
+          path: /spec/template/spec/containers/0/args/-
+          value: --kube-api-qps=500
+        - op: add
+          path: /spec/template/spec/containers/0/args/-
+          value: --kube-api-burst=1000
+        - op: add
+          path: /spec/template/spec/containers/0/args/-
+          value: --requeue-dependency=5s
+      target:
+        kind: Deployment
+        name: (kustomize-controller|helm-controller|source-controller)
+    - patch: |
+        apiVersion: apps/v1
+        kind: Deployment
+        metadata:
+          name: not-used
+        spec:
+          template:
+            spec:
+              containers:
+                - name: manager
+                  resources:
+                    limits:
+                      cpu: 2000m
+                      memory: 2Gi
+      target:
+        kind: Deployment
+        name: (kustomize-controller|helm-controller|source-controller)
+    # Enable Helm near OOM detection
+    # https://fluxcd.io/flux/cheatsheets/bootstrap/#enable-helm-near-oom-detection
+    - patch: |
+        - op: add
+          path: /spec/template/spec/containers/0/args/-
+          value: --feature-gates=OOMWatch=true
+        - op: add
+          path: /spec/template/spec/containers/0/args/-
+          value: --oom-watch-memory-threshold=95
+        - op: add
+          path: /spec/template/spec/containers/0/args/-
+          value: --oom-watch-interval=500ms
+      target:
+        kind: Deployment
+        name: helm-controller
diff --git a/kubernetes/flux/config/kustomization.yaml b/kubernetes/flux/config/kustomization.yaml
new file mode 100644
index 0000000000..ef231746a3
--- /dev/null
+++ b/kubernetes/flux/config/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./flux.yaml
+  - ./cluster.yaml
diff --git a/kubernetes/flux/repositories/git/kustomization.yaml b/kubernetes/flux/repositories/git/kustomization.yaml
new file mode 100644
index 0000000000..fe0f332a96
--- /dev/null
+++ b/kubernetes/flux/repositories/git/kustomization.yaml
@@ -0,0 +1,4 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources: []
diff --git a/kubernetes/flux/repositories/helm/bitnami.yaml b/kubernetes/flux/repositories/helm/bitnami.yaml
new file mode 100644
index 0000000000..1dcdba438b
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/bitnami.yaml
@@ -0,0 +1,11 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1beta2.json
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: bitnami
+  namespace: flux-system
+spec:
+  type: oci
+  interval: 1h
+  url: oci://registry-1.docker.io/bitnamicharts
diff --git a/kubernetes/flux/repositories/helm/bjw-s.yaml b/kubernetes/flux/repositories/helm/bjw-s.yaml
new file mode 100644
index 0000000000..a40b5d7789
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/bjw-s.yaml
@@ -0,0 +1,10 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: bjw-s
+  namespace: flux-system
+spec:
+  type: oci
+  interval: 5m
+  url: oci://ghcr.io/bjw-s/helm
diff --git a/kubernetes/flux/repositories/helm/cilium.yaml b/kubernetes/flux/repositories/helm/cilium.yaml
new file mode 100644
index 0000000000..3aee367887
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/cilium.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: cilium
+  namespace: flux-system
+spec:
+  interval: 1h
+  url: https://helm.cilium.io
diff --git a/kubernetes/flux/repositories/helm/coredns.yaml b/kubernetes/flux/repositories/helm/coredns.yaml
new file mode 100644
index 0000000000..3bdbbafbeb
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/coredns.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: coredns
+  namespace: flux-system
+spec:
+  interval: 1h
+  url: https://coredns.github.io/helm
diff --git a/kubernetes/flux/repositories/helm/external-dns.yaml b/kubernetes/flux/repositories/helm/external-dns.yaml
new file mode 100644
index 0000000000..a445126675
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/external-dns.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: external-dns
+  namespace: flux-system
+spec:
+  interval: 1h
+  url: https://kubernetes-sigs.github.io/external-dns
diff --git a/kubernetes/flux/repositories/helm/external-secrets.yaml b/kubernetes/flux/repositories/helm/external-secrets.yaml
new file mode 100644
index 0000000000..bcf54eb5e4
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/external-secrets.yaml
@@ -0,0 +1,10 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1beta2.json
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: external-secrets
+  namespace: flux-system
+spec:
+  interval: 1h
+  url: https://charts.external-secrets.io
diff --git a/kubernetes/flux/repositories/helm/ingress-nginx.yaml b/kubernetes/flux/repositories/helm/ingress-nginx.yaml
new file mode 100644
index 0000000000..82a0d0fff3
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/ingress-nginx.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: ingress-nginx
+  namespace: flux-system
+spec:
+  interval: 1h
+  url: https://kubernetes.github.io/ingress-nginx
diff --git a/kubernetes/flux/repositories/helm/jetstack.yaml b/kubernetes/flux/repositories/helm/jetstack.yaml
new file mode 100644
index 0000000000..737e06af09
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/jetstack.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: jetstack
+  namespace: flux-system
+spec:
+  interval: 1h
+  url: https://charts.jetstack.io
diff --git a/kubernetes/flux/repositories/helm/k8s-gateway.yaml b/kubernetes/flux/repositories/helm/k8s-gateway.yaml
new file mode 100644
index 0000000000..63a90615e6
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/k8s-gateway.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: k8s-gateway
+  namespace: flux-system
+spec:
+  interval: 1h
+  url: https://ori-edge.github.io/k8s_gateway
diff --git a/kubernetes/flux/repositories/helm/kustomization.yaml b/kubernetes/flux/repositories/helm/kustomization.yaml
new file mode 100644
index 0000000000..9e3829550d
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/kustomization.yaml
@@ -0,0 +1,22 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - bitnami.yaml
+  - bjw-s.yaml
+  - cilium.yaml
+  - coredns.yaml
+  - external-dns.yaml
+  - external-secrets.yaml
+  - ingress-nginx.yaml
+  - jetstack.yaml
+  - k8s-gateway.yaml
+  - longhorn.yaml
+  - metrics-server.yaml
+  - mittwald-charts.yaml
+  - openebs.yaml
+  - postfinance.yaml
+  - prometheus-community.yaml
+  - spegel.yaml
+  - stakater.yaml
+  - weave-gitops.yaml
diff --git a/kubernetes/flux/repositories/helm/longhorn.yaml b/kubernetes/flux/repositories/helm/longhorn.yaml
new file mode 100755
index 0000000000..d9c4116dc4
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/longhorn.yaml
@@ -0,0 +1,10 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1beta2.json
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: longhorn
+  namespace: flux-system
+spec:
+  interval: 1h
+  url: https://charts.longhorn.io
diff --git a/kubernetes/flux/repositories/helm/metrics-server.yaml b/kubernetes/flux/repositories/helm/metrics-server.yaml
new file mode 100644
index 0000000000..27a44828a5
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/metrics-server.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: metrics-server
+  namespace: flux-system
+spec:
+  interval: 1h
+  url: https://kubernetes-sigs.github.io/metrics-server
diff --git a/kubernetes/flux/repositories/helm/mittwald-charts.yaml b/kubernetes/flux/repositories/helm/mittwald-charts.yaml
new file mode 100755
index 0000000000..d501dbe7e1
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/mittwald-charts.yaml
@@ -0,0 +1,10 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1beta2.json
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: mittwald-charts
+  namespace: flux-system
+spec:
+  interval: 1h
+  url: https://helm.mittwald.de
diff --git a/kubernetes/flux/repositories/helm/openebs.yaml b/kubernetes/flux/repositories/helm/openebs.yaml
new file mode 100644
index 0000000000..4f48013ee7
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/openebs.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: openebs
+  namespace: flux-system
+spec:
+  interval: 1h
+  url: https://openebs.github.io/openebs
diff --git a/kubernetes/flux/repositories/helm/postfinance.yaml b/kubernetes/flux/repositories/helm/postfinance.yaml
new file mode 100644
index 0000000000..b14a64d8e7
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/postfinance.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: postfinance
+  namespace: flux-system
+spec:
+  interval: 1h
+  url: https://postfinance.github.io/kubelet-csr-approver
diff --git a/kubernetes/flux/repositories/helm/prometheus-community.yaml b/kubernetes/flux/repositories/helm/prometheus-community.yaml
new file mode 100644
index 0000000000..318a1a5140
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/prometheus-community.yaml
@@ -0,0 +1,10 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: prometheus-community
+  namespace: flux-system
+spec:
+  type: oci
+  interval: 5m
+  url: oci://ghcr.io/prometheus-community/charts
diff --git a/kubernetes/flux/repositories/helm/spegel.yaml b/kubernetes/flux/repositories/helm/spegel.yaml
new file mode 100644
index 0000000000..d9a8b2cd30
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/spegel.yaml
@@ -0,0 +1,10 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: spegel
+  namespace: flux-system
+spec:
+  type: oci
+  interval: 5m
+  url: oci://ghcr.io/spegel-org/helm-charts
diff --git a/kubernetes/flux/repositories/helm/stakater.yaml b/kubernetes/flux/repositories/helm/stakater.yaml
new file mode 100644
index 0000000000..98a3f6455b
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/stakater.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: stakater
+  namespace: flux-system
+spec:
+  interval: 1h
+  url: https://stakater.github.io/stakater-charts
diff --git a/kubernetes/flux/repositories/helm/weave-gitops.yaml b/kubernetes/flux/repositories/helm/weave-gitops.yaml
new file mode 100644
index 0000000000..49362c1d83
--- /dev/null
+++ b/kubernetes/flux/repositories/helm/weave-gitops.yaml
@@ -0,0 +1,11 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1beta2.json
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: weave-gitops
+  namespace: flux-system
+spec:
+  type: oci
+  interval: 1h
+  url: oci://ghcr.io/weaveworks/charts
diff --git a/kubernetes/flux/repositories/kustomization.yaml b/kubernetes/flux/repositories/kustomization.yaml
new file mode 100644
index 0000000000..d158d426ee
--- /dev/null
+++ b/kubernetes/flux/repositories/kustomization.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./git
+  - ./helm
+  - ./oci
diff --git a/kubernetes/flux/repositories/oci/kustomization.yaml b/kubernetes/flux/repositories/oci/kustomization.yaml
new file mode 100644
index 0000000000..fe0f332a96
--- /dev/null
+++ b/kubernetes/flux/repositories/oci/kustomization.yaml
@@ -0,0 +1,4 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources: []
diff --git a/kubernetes/flux/vars/cluster-secrets.sops.yaml b/kubernetes/flux/vars/cluster-secrets.sops.yaml
new file mode 100644
index 0000000000..96acf007d2
--- /dev/null
+++ b/kubernetes/flux/vars/cluster-secrets.sops.yaml
@@ -0,0 +1,34 @@
+apiVersion: v1
+kind: Secret
+metadata:
+    name: cluster-secrets
+    namespace: flux-system
+stringData:
+    SECRET_DOMAIN: ENC[AES256_GCM,data:HD2+IYTT1NYLy8P0r9b0,iv:4Au1eHFDF2Hvf5ulOS5PdfK4HRbvepJfKsrsLXK5pHA=,tag:gK8oz6RbICG8cGC9PkstBQ==,type:str]
+    SECRET_CH_DOMAIN: ENC[AES256_GCM,data:OiXW1ncVmlX4Wa8=,iv:Eoh8KbbtEBuYR8KrTbEKPasWZoX2yPLLi7I9jBK4HDY=,tag:0pDUvuRVT/zx20nnpNSM1w==,type:str]
+    SECRET_ACME_EMAIL: ENC[AES256_GCM,data:sBMQkxk0JKmfd2hyXe+cUIVG56s=,iv:VG1mU8InYp9Rmc7RND4+1irYWW7z/BgkqPSM7UF7mOo=,tag:QNFcui7oWUhyzSC9UpYRkw==,type:str]
+    SECRET_CLOUDFLARE_TUNNEL_ID: ENC[AES256_GCM,data:VJVigXU8YnnD7IlBpVruPk00WTNsAtrfM6HVaqCxZ3K5/jOj,iv:rspNCQYW7oxa44qWidGebFKdmuY6uHjwPT1x/MaLX1k=,tag:sVCnjhEexxd6sq68tougIg==,type:str]
+    #ENC[AES256_GCM,data:ywv/N6Rv,iv:fRH8YMj1C4zISA3roX2p8MH9snV1toX1NBXhQSvNW9c=,tag:8QRAHb2FTh3ArRAJ6sLxow==,type:comment]
+    SECRET_MINIO_BEARERTOKEN: ENC[AES256_GCM,data:03nx9nZQtVqsYW3BTjGqyN9lyHfHQRKcv1GsvoxsEDXPrniq7V8Jstqu+IQMNNkv/dOyxJQ33MmCjIsbTTMort8j/hGoPBJ1YFxNnkz+zxLM9vIcDNk4LzfN+uQaOD8a7WMl2Z+2MOTF6eqeTbs5U/w5PqSpmLqnXqHZZy2rVNUPnveLia+FqmEeEIVCJL5PCoI5ykceyCs0mgz8hVOVImLsJ0ecUSnvj0RaH6hSgTSIptC52TMymLOupjhCo8uF,iv:WsFTXI1h78xThvggo+Fsjj7QM4ylk1IMFfOiAY7PDHI=,tag:yX5B5qX1f3aSLmUV6O0T+w==,type:str]
+    #ENC[AES256_GCM,data:/vTntgGpTGHTG4Q=,iv:ia/tLuhey8RGt/JQ2BAHJY4T3/IZk15I9/9H8qZlIY0=,tag:DxmWxLCOJnO92aQuppRM6A==,type:comment]
+    SECRET_ALERT_MANAGER_DISCORD_WEBHOOK: ENC[AES256_GCM,data:Fc/8+wmOTy5fBx6abKhXmUItKIRNIrRmzl+Qzx/gu+9GX5Ril2U5iPmyiKX7EzaHzq6gyliRj6miW4wlOxSM0GkXwsto0CufEetC7us2N/Pd4RisUCgISrAHlGfYndcOJ3Dx+7Y92/p6WHyOpqiXUlOacQOy0+u30j4=,iv:FOWLuki3FuY4J3dSTpuyW7c0Qwdui0GGK6/nHDPQ11Q=,tag:WGVn2u3R2/ukfBWw5KVD6A==,type:str]
+sops:
+    kms: []
+    gcp_kms: []
+    azure_kv: []
+    hc_vault: []
+    age:
+        - recipient: age1y0kzuf0tn94a74whazwae4r9qal4snuqfuhl5jacscrpr7up5gts74fe5w
+          enc: |
+            -----BEGIN AGE ENCRYPTED FILE-----
+            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmYmhiZ0llMG52ejgxQnk5
+            OTMwanhxQU51dngyTGUxUDVGTWVvN1F6c240ClRDMFR1YmpXc1pBOTVBZGFZay81
+            MnlETWQ5RzdzL1U0ZG50WStNS2tPNmsKLS0tIFJYbHFMSWwzU3dKcmx0MFpCMVNY
+            SWRQZ2p4OUNSeTMyRnVNUWdmR3B2cTQK7LMzvv7DygENOwcwdv95kL9L7ohUzW/B
+            FESqyAt3DdZD/uvxVViM0KJ2Ktfugi3HP0jOCKWNBchps+mn8qdoWQ==
+            -----END AGE ENCRYPTED FILE-----
+    lastmodified: "2024-06-06T10:07:24Z"
+    mac: ENC[AES256_GCM,data:xiP6ZasBmbltTJSHQPmVlaCcQsE2B/tOZDA8wrXs1u59SqcG60z8qKHA85kidOiGuJCaNMBaPO6iow/B8zRUBvbNOIDTLExzR3ldNGFqkHIuFI8O96alHmIvbF5sOhkcS0bvhp6uxfbRRnLuRLIJBuRhs0ClyWQLnj8p/E0ctQ4=,iv:0mNR2gzaPdkYutoJEUNxi/YUVMeQRf3ew77TMCugRZw=,tag:NSSq6Fb9/o/esGKsBVkacw==,type:str]
+    pgp: []
+    encrypted_regex: ^(data|stringData)$
+    version: 3.8.1
diff --git a/kubernetes/flux/vars/cluster-settings.yaml b/kubernetes/flux/vars/cluster-settings.yaml
new file mode 100644
index 0000000000..4c7f910fcd
--- /dev/null
+++ b/kubernetes/flux/vars/cluster-settings.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: cluster-settings
+  namespace: flux-system
+data:
+  TIMEZONE: Europe/Zurich
+  MAIN_SC: longhorn
diff --git a/kubernetes/flux/vars/kustomization.yaml b/kubernetes/flux/vars/kustomization.yaml
new file mode 100644
index 0000000000..8db2fe9119
--- /dev/null
+++ b/kubernetes/flux/vars/kustomization.yaml
@@ -0,0 +1,5 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./cluster-settings.yaml
+  - ./cluster-secrets.sops.yaml
diff --git a/kubernetes/shared/gatus/external/configmap.yaml b/kubernetes/shared/gatus/external/configmap.yaml
new file mode 100644
index 0000000000..eca29bfe1b
--- /dev/null
+++ b/kubernetes/shared/gatus/external/configmap.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: "${APP}-gatus-ep"
+  labels:
+    gatus.io/enabled: "true"
+data:
+  config.yaml: |
+    endpoints:
+      - name: "${APP}"
+        group: external-kubernetes
+        url: "https://${GATUS_SUBDOMAIN:-${APP}}.${GATUS_DOMAIN:-${SECRET_DOMAIN}}${GATUS_PATH:-/}"
+        interval: 1m
+        client:
+          dns-resolver: tcp://1.1.1.1:53
+        conditions:
+          - "[STATUS] == ${GATUS_STATUS:-200}"
+        alerts:
+          - type: discord
diff --git a/kubernetes/shared/gatus/external/kustomization.yaml b/kubernetes/shared/gatus/external/kustomization.yaml
new file mode 100644
index 0000000000..e09060b994
--- /dev/null
+++ b/kubernetes/shared/gatus/external/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./configmap.yaml
diff --git a/kubernetes/shared/gatus/guarded/configmap.yaml b/kubernetes/shared/gatus/guarded/configmap.yaml
new file mode 100644
index 0000000000..1f3707055d
--- /dev/null
+++ b/kubernetes/shared/gatus/guarded/configmap.yaml
@@ -0,0 +1,24 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: "${APP}-gatus-ep"
+  labels:
+    gatus.io/enabled: "true"
+data:
+  config.yaml: |
+    endpoints:
+      - name: "${APP}"
+        group: guarded-kubernetes
+        url: 1.1.1.1
+        interval: 1m
+        ui:
+          hide-hostname: true
+          hide-url: true
+        dns:
+          query-name: "${GATUS_SUBDOMAIN:-${APP}}.${GATUS_DOMAIN:-${SECRET_DOMAIN}}"
+          query-type: A
+        conditions:
+          - "len([BODY]) == 0"
+        alerts:
+          - type: discord
diff --git a/kubernetes/shared/gatus/guarded/kustomization.yaml b/kubernetes/shared/gatus/guarded/kustomization.yaml
new file mode 100644
index 0000000000..e09060b994
--- /dev/null
+++ b/kubernetes/shared/gatus/guarded/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./configmap.yaml
diff --git a/kubernetes/shared/gatus/internal/configmap.yaml b/kubernetes/shared/gatus/internal/configmap.yaml
new file mode 100644
index 0000000000..a1c67aa51d
--- /dev/null
+++ b/kubernetes/shared/gatus/internal/configmap.yaml
@@ -0,0 +1,20 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: "${APP}-gatus-ep"
+  labels:
+    gatus.io/enabled: "true"
+data:
+  config.yaml: |
+    endpoints:
+      - name: "${APP}"
+        group: internal-kubernetes
+        url: "https://${GATUS_SUBDOMAIN:-${APP}}.${GATUS_DOMAIN:-${SECRET_DOMAIN}}${GATUS_PATH:-/}"
+        interval: 1m
+        client:
+          dns-resolver: tcp://192.168.13.1:53
+        conditions:
+          - "[STATUS] == ${GATUS_STATUS:-200}"
+        alerts:
+          - type: discord
diff --git a/kubernetes/shared/gatus/internal/kustomization.yaml b/kubernetes/shared/gatus/internal/kustomization.yaml
new file mode 100644
index 0000000000..e09060b994
--- /dev/null
+++ b/kubernetes/shared/gatus/internal/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - ./configmap.yaml
diff --git a/scripts/rebuild-kustomizations.sh b/scripts/rebuild-kustomizations.sh
new file mode 100755
index 0000000000..bc33f6c5c0
--- /dev/null
+++ b/scripts/rebuild-kustomizations.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+FOLDERS="kubernetes/flux/repositories/oci kubernetes/flux/repositories/helm kubernetes/flux/repositories/git"
+
+_pwd="$(pwd)"
+_basedir="${_pwd}/$(dirname $(which ${0}))"
+
+cd ${_basedir} && cd $(git rev-parse --show-toplevel)
+
+_gitdir="$(git rev-parse --show-toplevel)"
+
+# https://github.com/lyz-code/yamlfix/blob/main/docs/index.md#configure-environment-prefix
+export YAMLFIX_SEQUENCE_STYLE="block_style"
+
+for i in ${FOLDERS}; do
+  cd ${i}
+  f=$(ls -1 | grep -v kustomization.yaml)
+  if [[ ! -z "${f}" ]]; then
+    rm -f kustomization.yaml
+    kustomize create --autodetect
+    #gawk -i inplace 'NR==1{print "# yaml-language-server: $schema=https://json.schemastore.org/kustomization"}1' kustomization.yaml
+    yamlfix kustomization.yaml
+  fi
+  cd ${_gitdir}
+done
+
+cd ${_gitdir}
+
+for d in $(for i in $(find kubernetes -name ks.yaml); do echo $i | rev | cut -d/ -f3- | rev; done | sort -u); do
+  cd ${d}
+  rm -f kustomization.yaml
+  kustomize create --autodetect
+  for k in */ks.yaml; do
+    kustomize edit add resource ${k}
+  done
+  #gawk -i inplace 'NR==1{print "# yaml-language-server: $schema=https://json.schemastore.org/kustomization"}1' kustomization.yaml
+  yamlfix kustomization.yaml
+  cd ${_gitdir}
+done