Compare commits

..
7 Commits
Author SHA1 Message Date
ivanch fa8ca940f1 docs following git-ops reestructure 2026-09-02 19:53:41 -03:00
ivanch 7a560f6f97 adding new git-ops structure 2026-09-02 19:53:17 -03:00
ivanch 39bda3c129 removing old project structure 2026-09-02 19:53:02 -03:00
ivanch 3e6f09720e change in secrets 2026-09-02 19:52:19 -03:00
ivanch 1e35db8b4d adding new node 2026-08-23 20:34:43 -03:00
ivanch 76c7cfa39b updating k3s update playbook 2026-08-23 20:34:19 -03:00
ivanch 2c86598ace adding nginx-ingress helm 2026-08-22 19:48:01 -03:00
72 changed files with 1526 additions and 847 deletions
+12 -13
View File
@@ -1,16 +1,15 @@
media/
default/*tt*
default/havenllo*
certs/ca*
lab/*
sandbox/*
!lab/nfs-pod.yaml
rbac
cronjobs
*.crt
*.key
*.pem
*.pfx
chacal/
# Secrets & Environment files
.env
.env.*
*.env
group_vars/**/vault.yml
!group_vars/**/vault.yml.example
*kubeconfig*
# Ansible artifacts
*.retry
+22 -54
View File
@@ -12,57 +12,25 @@ For setup details, see [SETUP.md](SETUP.md).
The repository name references my local TLD, `.haven` ;)
## Namespaces
- default
- ArchiveBox
- Homarr
- Homepage
- It-tools
- Notepad
- Searxng
- Uptimekuma
- Vaultwarden
- OpenWebUI
- Paperless
- cloud
- Cloudreve
- Slink
- dns
- AdGuardHome
- AdGuardSync
- infra
- [Haven Notify](https://git.ivanch.me/ivanch/server-scripts/src/branch/main/haven-notify)
- Beszel
- Beszel Agent (running as a DaemonSet)
- Code Config (VS Code for internal config editing)
- WireGuard Easy
- dev
- Gitea Runner (x64)
- Gitea Runner (arm64)
- monitoring
- Grafana
- Prometheus
- Node Exporter
- Kube State Metrics
- Loki
- Alloy
#### Miscellaneous namespaces
- lab (a playground/sandbox namespace)
- nfs-pod (for testing and accessing NFS mounts)
- metallb-system
- MetalLB components
- cert-manager
- cert-manager components
- docker-ingress
- nginx ingress controller components for Docker-based services
## Todo
- Move ArchiveBox data to its own PVC on the NAS
- Move Uptime Kuma to the infra namespace
- Add links to each application's documentation
- Add links to server scripts
- Move Alloy to the monitoring namespace
- Install Loki, Grafana, and Prometheus via Helm charts
- Configure Loki and Prometheus to use PVCs
## Repository Layout
```
haven/
├── bootstrap/
│ ├── namespaces.yaml # cluster namespaces
│ ├── secretstores.yaml # cluster secret stores for BitWarden ESO
│ ├── address-pool.yaml # cluster IP pool for MetalLB
│ ├── argocd-install/ # Argo CD install manifests + ingress
│ └── root-app.yaml # root Application watching apps/root
├── secrets/
│ ├── adguard.yaml # adguard credentials from BitWarden
│ ├── <app>.yaml # <app> credentials from BitWarden
│ └── ...
├── apps/
│ ├── root/
│ │ ├── kustomization.yaml # points to applicationset.yaml
│ │ └── applicationset.yaml# auto-discovers apps/*/*.yaml
│ └── <namespace>/ # e.g., dev/, default/, infra/, monitoring/
│ ├── <app-1>.yaml # all-in-one manifest per app
│ ├── <app-2>.yaml # ...
| └──── ...
```
-11
View File
@@ -1,11 +0,0 @@
# Helm configuration for Alloy (OSS monitoring collector)
This directory contains Helm configuration for deploying Grafana Alloy, an open-source observability data collector.
## Quick Install
```bash
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
helm install alloy grafana/alloy --namespace alloy -f values.yaml
```
-107
View File
@@ -1,107 +0,0 @@
alloy:
clustering:
enabled: false # Single node deployment
configMap:
create: true
content: |-
discovery.kubernetes "all_pods" {
role = "pod"
selectors {
role = "pod"
field = "spec.nodeName=" + coalesce(env("HOSTNAME"), constants.hostname)
}
}
discovery.relabel "all_pods" {
targets = discovery.kubernetes.all_pods.targets
rule {
source_labels = ["__meta_kubernetes_namespace"]
target_label = "namespace"
}
rule {
source_labels = ["__meta_kubernetes_pod_name"]
target_label = "pod"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_name"]
target_label = "container"
}
rule {
source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
target_label = "app"
}
}
loki.source.kubernetes "all_logs" {
targets = discovery.relabel.all_pods.output
forward_to = [loki.write.main.receiver]
}
discovery.kubernetes "shared_pods" {
role = "pod"
selectors {
role = "pod"
field = "metadata.namespace=chacal"
}
}
discovery.relabel "shared_pods" {
targets = discovery.kubernetes.shared_pods.targets
rule {
source_labels = ["__meta_kubernetes_namespace"]
target_label = "namespace"
}
rule {
source_labels = ["__meta_kubernetes_pod_name"]
target_label = "pod"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_name"]
target_label = "container"
}
rule {
source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
target_label = "app"
}
}
loki.source.kubernetes "shared_logs" {
targets = discovery.relabel.shared_pods.output
forward_to = [loki.write.shared.receiver]
}
loki.write "main" {
endpoint {
url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push"
headers = {
"X-Scope-OrgID" = "main",
}
}
}
loki.write "shared" {
endpoint {
url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push"
headers = {
"X-Scope-OrgID" = "chacal",
}
}
}
mounts:
varlog: true # Mount host /var/log for pod logs
controller:
type: daemonset # Run on every node
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
+4
View File
@@ -33,6 +33,10 @@ all:
ansible_host: nexus.haven
ansible_user: root
ansible_python_interpreter: /usr/bin/python3
polaris:
ansible_host: polaris.haven
ansible_user: root
ansible_python_interpreter: /usr/bin/python3
lxcs:
children:
grp_docker_build: # group name == group_vars/grp_docker_build.yml
+39
View File
@@ -57,6 +57,45 @@
name: k3s-agent
state: restarted
- name: Fetch authoritative CA bundle from control-plane
ansible.builtin.slurp:
src: /var/lib/rancher/k3s/server/tls/{{ item }}
delegate_to: "{{ groups['grp_k3s_server'][0] }}"
register: ca_bundle
loop:
- client-ca.crt
- server-ca.crt
- name: Install server CAs into agent trust store
ansible.builtin.copy:
dest: "/var/lib/rancher/k3s/agent/{{ item.item }}"
content: "{{ item.content | b64decode }}"
owner: root
group: root
mode: "0600"
loop: "{{ ca_bundle.results }}"
loop_control:
label: "{{ item.item }}"
- name: Stop agent and clear stale client certs signed by old CA
ansible.builtin.shell: |
systemctl stop k3s-agent
rm -f /var/lib/rancher/k3s/agent/client-kubelet.crt \
/var/lib/rancher/k3s/agent/client-k3s-controller.crt \
/var/lib/rancher/k3s/agent/client-kube-proxy.crt \
/var/lib/rancher/k3s/agent/serving-kubelet.crt \
/var/lib/rancher/k3s/agent/kubelet.kubeconfig \
/var/lib/rancher/k3s/agent/k3scontroller.kubeconfig \
/var/lib/rancher/k3s/agent/kubeproxy.kubeconfig \
/var/lib/rancher/k3s/agent/client-kubeconfig.yaml
args:
executable: /bin/bash
- name: Restart k3s-agent so it re-issues certs against the server CA
ansible.builtin.systemd:
name: k3s-agent
state: restarted
- name: Print update output
ansible.builtin.debug:
var: k3s_update
+123
View File
@@ -0,0 +1,123 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
chart: true
metadata:
name: alloy
namespace: argocd
finalizers: [resources-finalizer.argocd.argoproj.io]
spec:
project: default
destination:
server: https://kubernetes.default.svc
namespace: alloy
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [CreateNamespace=true, ServerSideApply=true]
source:
repoURL: https://grafana.github.io/helm-charts
chart: alloy
targetRevision: 1.12.1
helm:
valuesObject:
controller:
type: daemonset
alloy:
clustering: { enabled: false }
mounts:
varlog: true
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
configMap:
create: true
content: |-
discovery.kubernetes "all_pods" {
role = "pod"
selectors {
role = "pod"
field = "spec.nodeName=" + coalesce(env("HOSTNAME"), constants.hostname)
}
}
discovery.relabel "all_pods" {
targets = discovery.kubernetes.all_pods.targets
rule {
source_labels = ["__meta_kubernetes_namespace"]
target_label = "namespace"
}
rule {
source_labels = ["__meta_kubernetes_pod_name"]
target_label = "pod"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_name"]
target_label = "container"
}
rule {
source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
target_label = "app"
}
}
loki.source.kubernetes "all_logs" {
targets = discovery.relabel.all_pods.output
forward_to = [loki.write.main.receiver]
}
discovery.kubernetes "shared_pods" {
role = "pod"
selectors {
role = "pod"
field = "metadata.namespace=chacal"
}
}
discovery.relabel "shared_pods" {
targets = discovery.kubernetes.shared_pods.targets
rule {
source_labels = ["__meta_kubernetes_namespace"]
target_label = "namespace"
}
rule {
source_labels = ["__meta_kubernetes_pod_name"]
target_label = "pod"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_name"]
target_label = "container"
}
rule {
source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
target_label = "app"
}
}
loki.source.kubernetes "shared_logs" {
targets = discovery.relabel.shared_pods.output
forward_to = [loki.write.shared.receiver]
}
loki.write "main" {
endpoint {
url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push"
headers = {
"X-Scope-OrgID" = "main",
}
}
}
loki.write "shared" {
endpoint {
url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push"
headers = {
"X-Scope-OrgID" = "chacal",
}
}
}
+99
View File
@@ -0,0 +1,99 @@
# Auth (Authentik + Kyverno SSO annotation policy)
Draft manifests only — nothing applied yet. Convention: any Ingress annotated
with `use-sso-auth: "true"` gets the Authentik forward-auth annotations injected
by the Kyverno ClusterPolicy (`auth/sso-auth-policy.yaml`).
## Deploy order
1. **Authentik** (`auth/authentik.yaml`) — IdP + embedded outpost. Needs a
Postgres user/DB on postgresql.haven first (see below), plus secrets from
Vaultwarden/ESO.
2. **Proxy provider** — create in Authentik UI (or via API) once it's up:
one provider per app you want header-auth'd, mode "forward_auth (single
app)", or a domain-level provider with the embedded outpost.
3. **Kyverno** (`auth/kyverno-helm.yaml`) — policy engine.
4. **SSO policy** (`auth/sso-auth-policy.yaml`) — expands `use-sso-auth: "true"`
into `auth-url` / `auth-signin` / `auth-response-headers` annotations.
5. Annotate ingresses: `kubectl annotate ingress <name> -n <ns> use-sso-auth=true`
and mirror the change in this repo.
## Postgres bootstrap (one-time, on postgresql.haven)
```sql
CREATE USER authentik WITH PASSWORD '<from-vaultwarden>';
CREATE DATABASE authentik OWNER authentik;
```
## Notes
- Outpost URL used everywhere is `https://auth.haven` — add DNS + ingress for
the outpost before annotating anything.
- Apps that should *consume* the injected `X-Authentik-*` headers (Grafana auth
proxy, Paperless remote-user, OpenWebUI trusted headers) need their own env
changes; those are app-side, not covered by the policy.
- API/WebSocket-heavy apps (arr stack, qBittorrent) should NOT get the
annotation on API paths — either skip the annotation or exclude paths.
## Which apps get the annotation (scoping)
Three tiers, applied per ingress — never blanket:
1. **OIDC tier** (no annotation, wire native OIDC in the app instead):
grafana, paperless, affine, openwebui, vaultwarden, beszel, slink.
Proper logout, group mapping, zero API breakage.
2. **Forward-auth tier** (`use-sso-auth: "true"`): browser-only utilities with
no real auth — it-tools, notepad, searxng, homepage, archivebox,
stirlingpdf, file-nginx, code-config, havenllo, own apps (chacal,
mindforge) if wanted.
3. **Hands off** (built-in auth is fine, forward-auth breaks clients):
sonarr, radarr, prowlarr, qbittorrent, adguard, changedetection,
uptimekuma, cloudreve (WebDAV), jellyfin (clients can't do redirects —
only the community SSO plugin route exists).
## Pre-annotation checklist (per new app)
Forward-auth breakage is loud and immediate if you look in the right places.
Before annotating any real hostname:
1. **Ask: what talks to this app that isn't a human in a browser?**
Mobile/desktop apps with own login, API-key consumers, push/webhook
receivers, cronjobs curling through the ingress, WebDAV/RSS → any of
those = hands off (tier 3) or exclude paths. "Just me in a browser" =
safe.
2. **Canary first:** create a scratch ingress for the same service
(`<app>-test.haven`) with the annotation; leave the original untouched
for ~a week. Forgotten integrations keep hitting the original and only
break the canary — instant rollback
(`kubectl annotate ingress <n> -n <ns> use-sso-auth-`).
3. **Grep the docs** (30s): "reverse proxy", "trusted header", "API key",
"webhook", "basic auth". A "running behind a reverse proxy" doc section
is where landmines are documented.
4. **Header-trust hygiene:** apps consuming `X-Authentik-*` must be
reachable ONLY through the ingress (no NodePort/exposed port, otherwise
LAN clients can forge headers straight to the pod). Keep built-in local
login enabled; never set SSO-only mode.
## Files
- `kyverno-helm.yaml` — Kyverno install values
- `sso-auth-policy.yaml` — ClusterPolicy: `use-sso-auth` → nginx auth annotations
- `authentik.yaml` — Authentik server/worker + outpost, Postgres PVC, ingress
## Status / when to actually deploy this (decision from 2026-08-28)
NOT deployed — deliberate. Sole user, low login frequency, everything
reachable via wg-easy, so SSO solves a problem that doesn't exist. The
`*.ivanch.me` public ingresses were reviewed instead: exposure without usage
is pure risk, and the right fix is removing exposure (VPN-only), not adding
an IdP.
Pull the trigger only if one of these becomes true:
1. A second person uses the homelab regularly (SSO value scales with users).
2. Something must be exposed that can't sit behind VPN (third-party
webhooks/callbacks) — then public tier gets `use-sso-auth` + 2FA.
3. Login friction on the OIDC apps (grafana/paperless/affine/openwebui/
vaultwarden/beszel/slink) actually annoys on a weekly basis.
Until then this folder is a shelf-ready draft; cost of keeping it is zero.
+244
View File
@@ -0,0 +1,244 @@
# Authentik — IdP + embedded outpost (proxy provider forward-auth).
# Draft — do NOT apply as-is:
# - secret values are placeholders; create them from Vaultwarden/ESO first
# - requires the authentik user/DB on postgresql.haven (see README.md)
# - requires DNS record for auth.haven -> ingress IP (user manages DNS)
---
apiVersion: v1
kind: Secret
metadata:
name: authentik-secrets
namespace: auth
type: Opaque
stringData:
postgres_password: "CHANGE_ME" # Vaultwarden -> ESO later
secret_key: "CHANGE_ME" # `openssl rand -base64 60`
bootstrap_password: "CHANGE_ME" # initial akadmin password
bootstrap_token: "CHANGE_ME" # outpost token
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: authentik-media
namespace: auth
spec:
accessModes: [ReadWriteOnce]
storageClassName: nfs-fast-client # verified: exists in cluster (nfs-client is also default)
resources:
requests:
storage: 2Gi
---
# Server (web + API)
apiVersion: apps/v1
kind: Deployment
metadata:
name: authentik-server
namespace: auth
spec:
replicas: 1
selector:
matchLabels: { app: authentik, component: server }
template:
metadata:
labels: { app: authentik, component: server }
spec:
containers:
- name: server
image: ghcr.io/goauthentik/server:2026.8
args: [server]
env:
- name: AUTHENTIK_SECRET_KEY
valueFrom:
{ secretKeyRef: { name: authentik-secrets, key: secret_key } }
- name: AUTHENTIK_POSTGRESQL__HOST
value: postgresql.haven
- name: AUTHENTIK_POSTGRESQL__NAME
value: authentik
- name: AUTHENTIK_POSTGRESQL__USER
value: authentik
- name: AUTHENTIK_POSTGRESQL__PASSWORD
valueFrom:
{
secretKeyRef:
{ name: authentik-secrets, key: postgres_password },
}
- name: AUTHENTIK_BOOTSTRAP_PASSWORD
valueFrom:
{
secretKeyRef:
{ name: authentik-secrets, key: bootstrap_password },
}
- name: AUTHENTIK_BOOTSTRAP_TOKEN
valueFrom:
{
secretKeyRef:
{ name: authentik-secrets, key: bootstrap_token },
}
- name: AUTHENTIK_ERROR_REPORTING__ENABLED
value: "false"
ports:
- { containerPort: 9000, name: http }
- { containerPort: 9443, name: https }
readinessProbe:
httpGet: { path: /-/health/ready/, port: 9000 }
initialDelaySeconds: 20
periodSeconds: 10
livenessProbe:
httpGet: { path: /-/health/live/, port: 9000 }
initialDelaySeconds: 40
periodSeconds: 20
resources:
requests: { cpu: 250m, memory: 512Mi }
limits: { memory: 1Gi, cpu: 1000m }
volumeMounts:
- { name: media, mountPath: /media }
volumes:
- name: media
persistentVolumeClaim: { claimName: authentik-media }
---
# Worker (policies, outpost management, scheduled tasks)
apiVersion: apps/v1
kind: Deployment
metadata:
name: authentik-worker
namespace: auth
spec:
replicas: 1
selector:
matchLabels: { app: authentik, component: worker }
template:
metadata:
labels: { app: authentik, component: worker }
spec:
containers:
- name: worker
image: ghcr.io/goauthentik/server:2026.8
args: [worker]
env: # same env as server; kept duplicated for a flat draft manifest
- name: AUTHENTIK_SECRET_KEY
valueFrom:
{ secretKeyRef: { name: authentik-secrets, key: secret_key } }
- name: AUTHENTIK_POSTGRESQL__HOST
value: postgresql.haven
- name: AUTHENTIK_POSTGRESQL__NAME
value: authentik
- name: AUTHENTIK_POSTGRESQL__USER
value: authentik
- name: AUTHENTIK_POSTGRESQL__PASSWORD
valueFrom:
{
secretKeyRef:
{ name: authentik-secrets, key: postgres_password },
}
- name: AUTHENTIK_BOOTSTRAP_PASSWORD
valueFrom:
{
secretKeyRef:
{ name: authentik-secrets, key: bootstrap_password },
}
- name: AUTHENTIK_BOOTSTRAP_TOKEN
valueFrom:
{
secretKeyRef:
{ name: authentik-secrets, key: bootstrap_token },
}
- name: AUTHENTIK_ERROR_REPORTING__ENABLED
value: "false"
resources:
requests: { cpu: 200m, memory: 512Mi }
limits: { memory: 1Gi, cpu: 1000m }
---
apiVersion: v1
kind: Service
metadata:
name: authentik
namespace: auth
spec:
selector: { app: authentik, component: server }
ports:
- { name: http, port: 80, targetPort: 9000 }
---
# Embedded outpost — runs the proxy providers; this is what ingress-nginx
# calls for /auth/nginx on every use-sso-auth ingress.
apiVersion: apps/v1
kind: Deployment
metadata:
name: authentik-outpost
namespace: auth
spec:
replicas: 1
selector:
matchLabels: { app: authentik, component: outpost }
template:
metadata:
labels: { app: authentik, component: outpost }
spec:
containers:
- name: outpost
image: ghcr.io/goauthentik/proxy:2026.8
env:
- name: AUTHENTIK_HOST
value: http://authentik.auth.svc.cluster.local
- name: AUTHENTIK_INSECURE
value: "true"
- name: AUTHENTIK_TOKEN
valueFrom:
{
secretKeyRef:
{ name: authentik-secrets, key: bootstrap_token },
}
ports:
- { containerPort: 9000, name: http }
readinessProbe:
httpGet: { path: /outpost.goauthentik.io/ping, port: 9000 }
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests: { cpu: 200m, memory: 512Mi }
limits: { memory: 1Gi, cpu: 1000m }
---
apiVersion: v1
kind: Service
metadata:
name: authentik-outpost
namespace: auth
spec:
selector: { app: authentik, component: outpost }
ports:
- { name: http, port: 80, targetPort: 9000 }
---
# SSO portal (user-facing login) — auth.haven
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: authentik
namespace: auth
spec:
ingressClassName: traefik
rules:
- host: auth.haven
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: authentik, port: { number: 80 } } }
---
# Outpost route — the /outpost.goauthentik.io path must resolve on the same
# host the protected apps redirect to. Kept as a separate ingress so it can
# also be attached to other hosts later if needed (do NOT give it
# use-sso-auth — that would create an auth loop).
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: authentik-outpost
namespace: auth
spec:
ingressClassName: traefik
rules:
- host: auth.haven
http:
paths:
- path: /outpost.goauthentik.io
pathType: Prefix
backend:
{ service: { name: authentik-outpost, port: { number: 80 } } }
+24
View File
@@ -0,0 +1,24 @@
# Kyverno — policy engine. Install with:
# helm repo add kyverno https://kyverno.github.io/kyverno
# helm upgrade --install kyverno kyverno/kyverno -n kyverno --create-namespace -f auth/kyverno-helm.yaml
# The only consumer of Kyverno here is the use-sso-auth mutation policy;
# if you later drop it, Kyverno can be removed with no other impact.
admissionController:
replicas: 1
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
backgroundController:
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 128Mi
cleanupController:
enabled: false
reportsController:
enabled: false
+39
View File
@@ -0,0 +1,39 @@
# ClusterPolicy: expands the single `use-sso-auth: "true"` Ingress annotation
# into the ingress-nginx forward-auth annotations pointed at the Authentik
# embedded outpost.
#
# Test before applying to real ingresses:
# kubectl apply -f auth/sso-auth-policy.yaml
# kubectl annotate ingress -n default homepage use-sso-auth=true --dry-run=server -o yaml # check annotations get injected
# # remove the test annotation afterwards
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: inject-sso-auth
spec:
validationFailureAction: Audit
background: false
rules:
- name: authentik-forward-auth
match:
any:
- resources:
kinds: [Ingress]
preconditions:
all:
- key: "{{ request.object.metadata.annotations.\"use-sso-auth\" || '' }}"
operator: Equals
value: "true"
mutate:
patchStrategicMerge:
metadata:
annotations:
nginx.ingress.kubernetes.io/auth-url: https://auth.haven/outpost.goauthentik.io/auth/nginx
nginx.ingress.kubernetes.io/auth-signin: https://auth.haven/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
# NOTE: no auth-snippet used — ingress-nginx 1.15 rejects snippet
# annotations by default (allow-snippet-annotations=false, your
# controller ConfigMap is empty so it uses the default).
# ingress-nginx already forwards X-Original-URL to the auth
# backend automatically, which is all Authentik needs.
nginx.ingress.kubernetes.io/auth-response-headers: X-Authentik-Username,X-Authentik-Email,X-Authentik-Groups,X-Authentik-Name,X-Authentik-Metadata
@@ -1,5 +1,4 @@
---
# ============ AFFiNE app ============
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -116,7 +115,6 @@ metadata:
labels:
app: affine
spec:
ingressClassName: nginx
rules:
- host: affine.haven
http:
@@ -1,4 +1,3 @@
# 2) Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -74,7 +73,6 @@ spec:
server: 192.168.15.99
path: /export/Storage/Cloud
---
# 3) Service (HTTP)
apiVersion: v1
kind: Service
metadata:
@@ -90,7 +88,6 @@ spec:
name: http
---
# 4) Service (Slave node / WebDAV — optional)
apiVersion: v1
kind: Service
metadata:
@@ -111,7 +108,6 @@ spec:
protocol: UDP
---
# 5) PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@@ -129,7 +125,6 @@ spec:
limits:
storage: 50Gi
---
# 6) Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
@@ -140,7 +135,6 @@ metadata:
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
spec:
ingressClassName: nginx
rules:
- host: cloud.haven
http:
@@ -153,7 +147,6 @@ spec:
port:
number: 5212
---
# 7) Ingress Public
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
@@ -164,7 +157,6 @@ metadata:
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
spec:
ingressClassName: nginx
rules:
- host: cloud.ivanch.me
http:
@@ -122,7 +122,6 @@ metadata:
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
spec:
ingressClassName: nginx
rules:
- host: slink.haven
http:
+39
View File
@@ -0,0 +1,39 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: renovate
namespace: cronjobs
spec:
schedule: "0 12 * * 0" # every Sunday 12:00
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
containers:
- name: renovate
image: renovate/renovate:44.39.3
args:
- ivanch/haven
env:
- name: LOG_LEVEL
value: debug
- name: RENOVATE_AUTODISCOVER
value: "false"
- name: RENOVATE_PLATFORM
value: "gitea"
- name: RENOVATE_ENDPOINT
value: "https://git.ivanch.me"
- name: RENOVATE_GIT_AUTHOR
value: "Renovate Bot <bot@renovateapp.com>"
- name: RENOVATE_TOKEN
valueFrom:
secretKeyRef:
name: renovate-bot
key: RENOVATE_TOKEN
- name: RENOVATE_GITHUB_COM_TOKEN
valueFrom:
secretKeyRef:
name: renovate-bot
key: RENOVATE_GITHUB_COM_TOKEN
restartPolicy: Never
@@ -65,14 +65,17 @@ spec:
- name: SONIC_PASSWORD
valueFrom:
secretKeyRef:
name: password
name: archivebox-password
key: password
- name: ADMIN_USERNAME
value: "ivanch"
valueFrom:
secretKeyRef:
name: archivebox-password
key: password
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: password
name: archivebox-password
key: password
- name: CSRF_TRUSTED_ORIGINS
value: "archive.haven"
@@ -121,7 +124,6 @@ spec:
port: 8000
targetPort: 8000
---
# 3) PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@@ -145,7 +147,6 @@ metadata:
name: archivebox-ingress
namespace: default
spec:
ingressClassName: nginx
rules:
- host: "archive.haven"
http:
@@ -98,7 +98,6 @@ spec:
restartPolicy: Always
terminationGracePeriodSeconds: 30
---
# 2) Service
apiVersion: v1
kind: Service
metadata:
@@ -116,7 +115,6 @@ spec:
protocol: TCP
targetPort: http
---
# 3) PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@@ -134,7 +132,6 @@ spec:
limits:
storage: 2Gi
---
# 4) Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
@@ -143,7 +140,6 @@ metadata:
labels:
app.kubernetes.io/name: changedetection
spec:
ingressClassName: nginx
rules:
- host: change.haven
http:
@@ -192,7 +192,6 @@ metadata:
labels:
app.kubernetes.io/name: homepage
spec:
ingressClassName: nginx
rules:
- host: "homepage.haven"
http:
@@ -53,7 +53,6 @@ metadata:
name: it-tools-ingress
namespace: default
spec:
ingressClassName: nginx
rules:
- host: "tools.haven"
http:
@@ -1,5 +1,4 @@
---
# 1) Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -19,9 +18,6 @@ spec:
- name: notepad
image: jdreinhardt/minimalist-web-notepad:latest
imagePullPolicy: Always
# The image entrypoint runs `chown -R www-data:www-data` on the NFS-mounted
# _tmp dir, which fails because the NFS export uses all_squash (anonuid=65534).
# Override the entrypoint to skip chown — ownership is handled by the NFS server.
command:
- sh
- -c
@@ -47,7 +43,6 @@ spec:
claimName: notepad-data
---
# 2) Service
apiVersion: v1
kind: Service
metadata:
@@ -61,7 +56,6 @@ spec:
- port: 80
targetPort: 80
---
# 3) PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@@ -77,14 +71,12 @@ spec:
requests:
storage: 1Gi
---
# 4) Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: notepad
namespace: default
spec:
ingressClassName: nginx
rules:
- host: notepad.haven
http:
@@ -1,4 +1,3 @@
# 1) Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -57,7 +56,6 @@ spec:
persistentVolumeClaim:
claimName: openwebui-data
---
# 2) Service
apiVersion: v1
kind: Service
metadata:
@@ -71,7 +69,6 @@ spec:
- port: 8080
targetPort: 8080
---
# 3) PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@@ -89,7 +86,6 @@ spec:
limits:
storage: 10Gi
---
# 4) Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
@@ -98,7 +94,6 @@ metadata:
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
ingressClassName: nginx
rules:
- host: openwebui.haven
http:
@@ -137,7 +137,6 @@ metadata:
name: paperless
namespace: default
spec:
ingressClassName: nginx
rules:
- host: paperless.haven
http:
+130
View File
@@ -0,0 +1,130 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: playwright
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: playwright
strategy:
type: Recreate
template:
metadata:
labels:
app: playwright
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- preference:
matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- iris
weight: 100
containers:
- command:
- npx
- -y
- playwright@1.58.0
- run-server
- --port
- "3000"
- --host
- 0.0.0.0
env:
- name: TZ
value: America/Sao_Paulo
image: mcr.microsoft.com/playwright:v1.62.1-noble
imagePullPolicy: Always
name: playwright
ports:
- containerPort: 3000
protocol: TCP
resources:
limits:
cpu: "4"
memory: 4Gi
requests:
cpu: 500m
memory: 512Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /dev/shm
name: dshm
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
volumes:
- emptyDir:
medium: Memory
name: dshm
status:
availableReplicas: 1
conditions:
- lastTransitionTime: "2026-05-29T15:54:24Z"
lastUpdateTime: "2026-07-22T10:14:35Z"
message: ReplicaSet "playwright-86c74d7c78" has successfully progressed.
reason: NewReplicaSetAvailable
status: "True"
type: Progressing
- lastTransitionTime: "2026-08-28T06:04:14Z"
lastUpdateTime: "2026-08-28T06:04:14Z"
message: Deployment has minimum availability.
reason: MinimumReplicasAvailable
status: "True"
type: Available
observedGeneration: 95
readyReplicas: 1
replicas: 1
terminatingReplicas: 0
updatedReplicas: 1
---
apiVersion: v1
kind: Service
metadata:
name: playwright
namespace: default
spec:
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- port: 3000
protocol: TCP
targetPort: 3000
selector:
app: playwright
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: playwright
namespace: default
spec:
rules:
- host: playwright.haven
http:
paths:
- backend:
service:
name: playwright
port:
number: 3000
path: /
pathType: Prefix
status:
loadBalancer:
ingress:
- ip: 192.168.20.204
@@ -80,7 +80,6 @@ metadata:
name: searxng
namespace: default
spec:
ingressClassName: nginx
rules:
- host: search.haven
http:
@@ -99,7 +99,6 @@ metadata:
labels:
app: stirlingpdf
spec:
ingressClassName: nginx
rules:
- host: stirling.haven
http:
@@ -93,7 +93,6 @@ metadata:
name: uptimekuma
namespace: default
spec:
ingressClassName: nginx
rules:
- host: uptimekuma.haven
http:
@@ -110,7 +110,6 @@ metadata:
cert-manager.io/cluster-issuer: internal-ca
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- vault.haven
@@ -134,7 +133,6 @@ metadata:
name: vaultwarden-public
namespace: default
spec:
ingressClassName: nginx
rules:
- host: vault.ivanch.me
http:
+117
View File
@@ -0,0 +1,117 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: adguardsync-pvc
namespace: dns
annotations:
nfs.io/storage-path: "adguardsync-config"
spec:
storageClassName: "nfs-client"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: adguardsync
namespace: dns
spec:
strategy:
type: Recreate
replicas: 1
selector:
matchLabels:
app: adguardsync
template:
metadata:
labels:
app: adguardsync
spec:
containers:
- name: adguardsync
image: ghcr.io/bakito/adguardhome-sync:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP
name: web-port
env:
- name: CRON
value: "0 * * * *"
- name: RUN_ON_START
value: "true"
- name: LOG_LEVEL
value: "info"
- name: ORIGIN_URL
value: "http://adguard.haven"
- name: ORIGIN_USERNAME
valueFrom:
secretKeyRef:
name: adguardhome-password
key: username
- name: ORIGIN_PASSWORD
valueFrom:
secretKeyRef:
name: adguardhome-password
key: password
- name: REPLICA1_URL
value: "http://adguard2.haven"
- name: REPLICA1_USERNAME
valueFrom:
secretKeyRef:
name: adguardhome-password
key: username
- name: REPLICA1_PASSWORD
valueFrom:
secretKeyRef:
name: adguardhome-password
key: password
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: adguardsync-storage
mountPath: /config
volumes:
- name: adguardsync-storage
persistentVolumeClaim:
claimName: adguardsync-pvc
---
apiVersion: v1
kind: Service
metadata:
name: adguardsync-svc
namespace: dns
spec:
type: ClusterIP
selector:
app: adguardsync
ports:
- name: web
port: 8080
targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: adguardsync-ingress
namespace: dns
spec:
rules:
- host: adguardsync.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: adguardsync-svc
port:
number: 8080
+171
View File
@@ -0,0 +1,171 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: adguardhome-pvc
namespace: dns
annotations:
nfs.io/storage-path: "adguardhome-config"
spec:
storageClassName: "nfs-client"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: adguardhome
namespace: dns
spec:
strategy:
type: Recreate
replicas: 1
selector:
matchLabels:
app: adguardhome
template:
metadata:
labels:
app: adguardhome
spec:
containers:
- name: adguardhome
image: adguard/adguardhome:latest
imagePullPolicy: Always
ports:
- containerPort: 53
protocol: TCP
- containerPort: 53
protocol: UDP
- containerPort: 3000
protocol: TCP
name: install-port
- containerPort: 80
protocol: TCP
name: web-port
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 1000m
memory: 2Gi
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: adguardhome-storage
mountPath: /opt/adguardhome/work
- name: adguardhome-storage
mountPath: /opt/adguardhome/conf
volumes:
- name: adguardhome-storage
persistentVolumeClaim:
claimName: adguardhome-pvc
---
apiVersion: v1
kind: Service
metadata:
name: adguardhome-svc
namespace: dns
spec:
type: LoadBalancer
selector:
app: adguardhome
loadBalancerIP: 192.168.20.200
ports:
- name: dns-tcp
port: 53
targetPort: 53
protocol: TCP
- name: dns-udp
port: 53
targetPort: 53
protocol: UDP
- name: web
port: 80
targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: adguardhome-lan-svc
namespace: dns
spec:
type: LoadBalancer
selector:
app: adguardhome
loadBalancerIP: 192.168.15.200
ports:
- name: dns-tcp
port: 53
targetPort: 53
protocol: TCP
- name: dns-udp
port: 53
targetPort: 53
protocol: UDP
- name: web
port: 80
targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: adguard-install-svc
namespace: dns
spec:
type: ClusterIP
selector:
app: adguardhome
ports:
- name: install
port: 3000
targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: adguardhome-ingress
namespace: dns
spec:
rules:
- host: adguard.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: adguardhome-svc
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: adguardhome-install-ingress
namespace: dns
spec:
rules:
- host: install.adguard.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: adguard-install-svc
port:
number: 3000
@@ -22,7 +22,7 @@ spec:
secretKeyRef:
name: beszel-key
key: SECRET-KEY
image: henrygd/beszel-agent:0.18.7
image: henrygd/beszel-agent:0.18.8
imagePullPolicy: Always
name: beszel-agent
ports:
+1 -2
View File
@@ -26,7 +26,7 @@ spec:
- amd64
containers:
- name: beszel
image: ghcr.io/henrygd/beszel/beszel:0.18.7
image: ghcr.io/henrygd/beszel/beszel:0.18.8
imagePullPolicy: Always
ports:
- containerPort: 8090
@@ -83,7 +83,6 @@ metadata:
name: beszel
namespace: infra
spec:
ingressClassName: nginx
rules:
- host: beszel.haven
http:
@@ -19,7 +19,7 @@ spec:
kubernetes.io/arch: amd64
containers:
- name: bitwarden-cli
image: ghcr.io/charlesthomas/bitwarden-cli:2026.3.0
image: ghcr.io/charlesthomas/bitwarden-cli:2026.7.0
imagePullPolicy: IfNotPresent
# Override the baked entrypoint so --disable-origin-protection is
# actually passed (it was commented out in the image's entrypoint.sh,
@@ -29,9 +29,8 @@ spec:
- |
set -e
bw config server "${BW_HOST}"
# Authenticate. Prefer the API key if client creds are present,
# otherwise fall back to username+password. The session is exported
# so the background loop can re-derive (and refresh) it.
# Authenticate. Prefer the API key if client creds are present.
login() {
if [ -n "$BW_CLIENTID" ] && [ -n "$BW_CLIENTSECRET" ]; then
echo "Using apikey to log in"
@@ -42,15 +41,15 @@ spec:
fi
export BW_SESSION
}
login
# Warm the vault cache once at startup so bw serve has data immediately.
bw sync
bw status
# Keep the session alive: RE-LOGIN every cycle and re-sync so the on-disk
# vault cache stays warm and the session token never lapses. A loop that
# only re-ran `bw sync` with a captured session would eventually fail once
# the session expired (-> stale cache / "Not found" for changed items, or a
# full unauthenticated 400 if the initial login never happened at all).
# Keep the session alive
echo "Starting periodic bw login+sync loop (every 5m)"
(
while true; do
@@ -60,6 +59,7 @@ spec:
bw sync >/dev/null 2>&1 || echo "[$(date -u +%FT%TZ)] bw sync failed"
done
) &
echo 'Running `bw serve` on port 8087'
bw serve --hostname 0.0.0.0 --disable-origin-protection
env:
@@ -121,3 +121,24 @@ spec:
port: 8087
targetPort: http
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: bw-cli
namespace: infra
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: bitwarden-cli
policyTypes:
- Ingress
ingress:
- from:
# ESO pods in the external-secrets namespace.
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: external-secrets
podSelector:
matchLabels:
app.kubernetes.io/name: external-secrets
@@ -99,7 +99,6 @@ metadata:
name: code-config
namespace: infra
spec:
ingressClassName: nginx
rules:
- host: code-config.haven
http:
+26
View File
@@ -0,0 +1,26 @@
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
name: csi-driver-nfs
namespace: infra
spec:
repo: https://kubernetes-csi.github.io/csi-driver-nfs
chart: csi-driver-nfs
version: 4.13.4
targetNamespace: infra
valuesContent: |-
controller:
replicas: 1
logLevel: 5
defaultOnDeletePolicy: retain
storageClasses:
- name: nfs-client
annotations:
storageclass.kubernetes.io/is-default-class: "true"
parameters:
server: nfs-config.haven
share: /export/config
subDir: ${pvc.metadata.namespace}/${pvc.metadata.name}
onDelete: retain
reclaimPolicy: Retain
volumeBindingMode: Immediate
+11
View File
@@ -0,0 +1,11 @@
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
name: external-secrets
namespace: kube-system
spec:
repo: https://charts.external-secrets.io
chart: external-secrets
version: 2.7.0
targetNamespace: external-secrets
createNamespace: true
@@ -115,7 +115,6 @@ metadata:
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "*"
spec:
ingressClassName: nginx
rules:
- host: file-nginx.haven
http:
@@ -72,7 +72,6 @@ metadata:
name: haven-notify
namespace: infra
spec:
ingressClassName: nginx
rules:
- host: notify.haven
http:
+32
View File
@@ -0,0 +1,32 @@
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
name: traefik
namespace: kube-system
spec:
repo: https://traefik.github.io/charts
chart: traefik
version: 41.2.0
targetNamespace: traefik
createNamespace: true
valuesContent: |-
deployment:
replicas: 2
ingressClass:
enabled: true
isDefaultClass: true
name: traefik
service:
annotations:
metallb.io/ip-allocated-from-pool: default-pool
metallb.io/loadBalancerIPs: "192.168.20.204"
spec:
type: LoadBalancer
externalTrafficPolicy: Local
resources:
requests:
cpu: 100m
memory: 90Mi
limits:
cpu: 1000m
memory: 256Mi
@@ -113,7 +113,6 @@ metadata:
name: wg-easy-ingress
namespace: infra
spec:
ingressClassName: nginx
rules:
- host: vpn.haven
http:
+10
View File
@@ -0,0 +1,10 @@
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
name: metallb
namespace: kube-system
spec:
repo: https://metallb.github.io/metallb
chart: metallb
version: 0.15.2
targetNamespace: metallb-system
@@ -94,7 +94,6 @@ metadata:
namespace: monitoring
name: grafana
spec:
ingressClassName: nginx
rules:
- host: grafana.haven
http:
@@ -113,7 +112,6 @@ metadata:
namespace: monitoring
name: grafana-public
spec:
ingressClassName: nginx
rules:
- host: grafanah.ivanch.me
http:
+46
View File
@@ -0,0 +1,46 @@
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: haven-apps
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- git:
repoURL: https://git.ivanch.me/ivanch/haven.git
revision: main
files:
- path: "apps/*/*.yaml"
- path: "apps/root/*.yaml"
exclude: true
template:
metadata:
name: '{{ .path.filename | trimSuffix ".yaml" }}'
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://git.ivanch.me/ivanch/haven.git
targetRevision: main
path: "{{ .path.path }}"
directory:
include: "{{ .path.filename }}"
destination:
server: https://kubernetes.default.svc
namespace: "{{ index .path.segments 1 }}"
syncPolicy:
automated:
prune: true
selfHeal: true
templatePatch: |
{{- if hasKey . "chart" }}
spec:
source:
$patch: replace
{{- toYaml .spec.source | nindent 4 }}
syncPolicy:
syncOptions: [CreateNamespace=true, ServerSideApply=true]
{{- end }}
+5
View File
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- applicationset.yaml
+22
View File
@@ -0,0 +1,22 @@
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default-pool
namespace: metallb-system
spec:
addresses:
- 192.168.15.200/32 # reserved for DNS 1
- 192.168.15.202/32 # reserved for wg-easy
- 192.168.15.203/32 # reserved for k3s ingress
- 192.168.20.200/32 # reserved for DNS 1
# - 192.168.20.201 is reserved for DNS 2
- 192.168.20.202-192.168.20.220
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- default-pool
@@ -0,0 +1,23 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
labels:
app.kubernetes.io/name: argocd-cm
app.kubernetes.io/part-of: argocd
data:
accounts.api: apiKey
accounts.api.enabled: "true"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
labels:
app.kubernetes.io/name: argocd-rbac-cm
app.kubernetes.io/part-of: argocd
data:
policy.csv: |
g, api, role:readonly
+17
View File
@@ -0,0 +1,17 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd-server
namespace: argocd
spec:
rules:
- host: argocd.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 80
@@ -0,0 +1,12 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: argocd
resources:
- https://github.com/argoproj/argo-cd.git/manifests/cluster-install?ref=stable
- ingress.yaml
patches:
- path: server-insecure-patch.yaml
- path: argocd-api-user.yaml
@@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cmd-params-cm
data:
server.insecure: "true"
+141
View File
@@ -0,0 +1,141 @@
---
# ==============================================================================
# Core / System Namespaces
# ==============================================================================
apiVersion: v1
kind: Namespace
metadata:
name: default
---
apiVersion: v1
kind: Namespace
metadata:
name: kube-node-lease
---
apiVersion: v1
kind: Namespace
metadata:
name: kube-public
---
apiVersion: v1
kind: Namespace
metadata:
name: kube-system
---
# ==============================================================================
# Infrastructure, Ingress & GitOps
# ==============================================================================
apiVersion: v1
kind: Namespace
metadata:
name: argocd
---
apiVersion: v1
kind: Namespace
metadata:
name: cert-manager
---
apiVersion: v1
kind: Namespace
metadata:
name: dns
---
apiVersion: v1
kind: Namespace
metadata:
name: docker-ingress
---
apiVersion: v1
kind: Namespace
metadata:
name: external-secrets
labels:
name: external-secrets
---
apiVersion: v1
kind: Namespace
metadata:
name: infra
---
apiVersion: v1
kind: Namespace
metadata:
name: ingress-nginx
labels:
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
---
apiVersion: v1
kind: Namespace
metadata:
name: metallb-system
---
# ==============================================================================
# Observability & Monitoring
# ==============================================================================
apiVersion: v1
kind: Namespace
metadata:
name: alloy
---
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
---
# ==============================================================================
# Storage & Data
# ==============================================================================
apiVersion: v1
kind: Namespace
metadata:
name: garage
labels:
name: garage
---
# ==============================================================================
# Environments & Workloads
# ==============================================================================
apiVersion: v1
kind: Namespace
metadata:
name: chacal
---
apiVersion: v1
kind: Namespace
metadata:
name: cloud
---
apiVersion: v1
kind: Namespace
metadata:
name: cronjobs
---
apiVersion: v1
kind: Namespace
metadata:
name: dev
---
apiVersion: v1
kind: Namespace
metadata:
name: lab
---
apiVersion: v1
kind: Namespace
metadata:
name: media
---
apiVersion: v1
kind: Namespace
metadata:
name: mindforge
---
apiVersion: v1
kind: Namespace
metadata:
name: vpn-session-pods
labels:
pod-security.kubernetes.io/audit: privileged
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/warn: privileged
+20
View File
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://git.ivanch.me/ivanch/haven.git
targetRevision: main
path: apps/root
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
-8
View File
@@ -1,8 +0,0 @@
# internal-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: internal-ca
spec:
ca:
secretName: internal-ca-secret
-6
View File
@@ -1,6 +0,0 @@
## Creating gitea-runner secrets
```bash
kubectl create secret generic gitea-runner-token \
--from-literal=REGISTRATION_TOKEN='your_gitea_runner_token' -n dev
```
-118
View File
@@ -1,118 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: adguardsync-pvc
namespace: dns
annotations:
nfs.io/storage-path: "adguardsync-config"
spec:
storageClassName: "nfs-client"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: adguardsync
namespace: dns
spec:
strategy:
type: Recreate
replicas: 1
selector:
matchLabels:
app: adguardsync
template:
metadata:
labels:
app: adguardsync
spec:
containers:
- name: adguardsync
image: ghcr.io/bakito/adguardhome-sync:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP
name: web-port
env:
- name: CRON
value: "0 * * * *"
- name: RUN_ON_START
value: "true"
- name: LOG_LEVEL
value: "info"
- name: ORIGIN_URL
value: "http://adguard.haven"
- name: ORIGIN_USERNAME
valueFrom:
secretKeyRef:
name: adguardhome-password
key: username
- name: ORIGIN_PASSWORD
valueFrom:
secretKeyRef:
name: adguardhome-password
key: password
- name: REPLICA1_URL
value: "http://adguard2.haven"
- name: REPLICA1_USERNAME
valueFrom:
secretKeyRef:
name: adguardhome-password
key: username
- name: REPLICA1_PASSWORD
valueFrom:
secretKeyRef:
name: adguardhome-password
key: password
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: adguardsync-storage
mountPath: /config
volumes:
- name: adguardsync-storage
persistentVolumeClaim:
claimName: adguardsync-pvc
---
apiVersion: v1
kind: Service
metadata:
name: adguardsync-svc
namespace: dns
spec:
type: ClusterIP
selector:
app: adguardsync
ports:
- name: web
port: 8080
targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: adguardsync-ingress
namespace: dns
spec:
ingressClassName: nginx
rules:
- host: adguardsync.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: adguardsync-svc
port:
number: 8080
-173
View File
@@ -1,173 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: adguardhome-pvc
namespace: dns
annotations:
nfs.io/storage-path: "adguardhome-config"
spec:
storageClassName: "nfs-client"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: adguardhome
namespace: dns
spec:
strategy:
type: Recreate
replicas: 1
selector:
matchLabels:
app: adguardhome
template:
metadata:
labels:
app: adguardhome
spec:
containers:
- name: adguardhome
image: adguard/adguardhome:latest
imagePullPolicy: Always
ports:
- containerPort: 53
protocol: TCP
- containerPort: 53
protocol: UDP
- containerPort: 3000
protocol: TCP
name: install-port
- containerPort: 80
protocol: TCP
name: web-port
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 1000m
memory: 2Gi
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: adguardhome-storage
mountPath: /opt/adguardhome/work
- name: adguardhome-storage
mountPath: /opt/adguardhome/conf
volumes:
- name: adguardhome-storage
persistentVolumeClaim:
claimName: adguardhome-pvc
---
apiVersion: v1
kind: Service
metadata:
name: adguardhome-svc
namespace: dns
spec:
type: LoadBalancer
selector:
app: adguardhome
loadBalancerIP: 192.168.20.200
ports:
- name: dns-tcp
port: 53
targetPort: 53
protocol: TCP
- name: dns-udp
port: 53
targetPort: 53
protocol: UDP
- name: web
port: 80
targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: adguardhome-lan-svc
namespace: dns
spec:
type: LoadBalancer
selector:
app: adguardhome
loadBalancerIP: 192.168.15.200
ports:
- name: dns-tcp
port: 53
targetPort: 53
protocol: TCP
- name: dns-udp
port: 53
targetPort: 53
protocol: UDP
- name: web
port: 80
targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: adguard-install-svc
namespace: dns
spec:
type: ClusterIP
selector:
app: adguardhome
ports:
- name: install
port: 3000
targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: adguardhome-ingress
namespace: dns
spec:
ingressClassName: nginx
rules:
- host: adguard.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: adguardhome-svc
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: adguardhome-install-ingress
namespace: dns
spec:
ingressClassName: nginx
rules:
- host: install.adguard.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: adguard-install-svc
port:
number: 3000
-4
View File
@@ -1,4 +0,0 @@
## Create namespace
```bash
kubectl create namespace docker-ingress
```
-44
View File
@@ -1,44 +0,0 @@
# docker-node: iris.haven
# port: 4100
# Service
apiVersion: v1
kind: Service
metadata:
name: changedetection-service
namespace: docker-ingress
spec:
ports:
- port: 80
targetPort: 4100
---
# Endpoints
apiVersion: v1
kind: Endpoints
metadata:
name: changedetection-service
namespace: docker-ingress
subsets:
- addresses:
- ip: 192.168.20.100
ports:
- port: 4100
---
# Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: changedetection-ingress
namespace: docker-ingress
spec:
rules:
- host: change.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: changedetection-service
port:
number: 80
-47
View File
@@ -1,47 +0,0 @@
# docker-node: iris.haven
# port: 4100
# Service
apiVersion: v1
kind: Service
metadata:
name: dockge-service
namespace: docker-ingress
spec:
ports:
- name: http
protocol: TCP
port: 80
targetPort: 5001
---
apiVersion: v1
kind: Endpoints
metadata:
name: dockge-service
namespace: docker-ingress
subsets:
- addresses:
- ip: 192.168.20.100
ports:
- name: http
protocol: TCP
port: 5001
---
# Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dockge-ingress
namespace: docker-ingress
spec:
rules:
- host: dockge.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dockge-service
port:
number: 80
-3
View File
@@ -1,3 +0,0 @@
```bash
kubectl create secret generic beszel-key --from-literal=SECRET-KEY=<KEY> -n infra
```
-21
View File
@@ -1,21 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: bw-cli
namespace: infra
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: bitwarden-cli
policyTypes:
- Ingress
ingress:
- from:
# ESO pods (app.kubernetes.io/name: external-secrets) in the external-secrets namespace.
# Both podSelector AND namespaceSelector must match for cross-namespace traffic.
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: external-secrets
podSelector:
matchLabels:
app.kubernetes.io/name: external-secrets
-37
View File
@@ -1,37 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nfs-pod
spec:
containers:
- name: nfs-container
image: alpine:latest
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
volumeMounts:
- name: nfs-backup
mountPath: /mnt/Backup
- name: nfs-storage
mountPath: /mnt/Storage
command: ["/bin/sh", "-c", "sleep infinity"]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 800m
memory: 256Mi
securityContext:
privileged: true
volumes:
- name: nfs-backup
nfs:
server: 192.168.15.99
path: /srv/dev-md1/Backup
- name: nfs-storage
nfs:
server: 192.168.15.99
path: /export/Storage
-22
View File
@@ -1,22 +0,0 @@
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default-pool
namespace: metallb-system
spec:
addresses:
- 192.168.15.200/32 # reserved for DNS 1
- 192.168.15.202/32 # reserved for wg-easy
- 192.168.15.203/32 # reserved for k3s ingress
- 192.168.20.200/32 # reserved for DNS 1
# - 192.168.20.201 is reserved for DNS 2
- 192.168.20.202-192.168.20.220
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- default-pool
+24
View File
@@ -0,0 +1,24 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: archivebox-password, namespace: default }
spec:
refreshInterval: 1h
target:
name: archivebox-password
deletionPolicy: Retain
template:
type: Opaque
data:
username: "{{ .username }}"
password: "{{ .password }}"
data:
- secretKey: username
remoteRef:
{ key: 6b4ca371-dcd4-4e31-aebe-8e5ea48b1fb9, property: username }
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }
- secretKey: password
remoteRef:
{ key: 6b4ca371-dcd4-4e31-aebe-8e5ea48b1fb9, property: password }
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }
-48
View File
@@ -1,48 +0,0 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: radarr, namespace: media }
spec:
refreshInterval: 1h
target:
name: radarr-secret
deletionPolicy: Retain
template:
type: Opaque
data:
Radarr__Postgres__User: "{{ .Radarr__Postgres__User }}"
Radarr__Postgres__Password: "{{ .Radarr__Postgres__Password }}"
Radarr__Postgres__Host: "{{ .Radarr__Postgres__Host }}"
Radarr__Postgres__MainDb: "{{ .Radarr__Postgres__MainDb }}"
data:
- secretKey: Radarr__Postgres__User
remoteRef:
{
key: ab723b65-3ec8-469c-b01d-67a6e3049023,
property: username,
}
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }
- secretKey: Radarr__Postgres__Password
remoteRef:
{
key: ab723b65-3ec8-469c-b01d-67a6e3049023,
property: password,
}
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }
- secretKey: Radarr__Postgres__Host
remoteRef:
{
key: ab723b65-3ec8-469c-b01d-67a6e3049023,
property: Radarr__Postgres__Host,
}
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }
- secretKey: Radarr__Postgres__MainDb
remoteRef:
{
key: ab723b65-3ec8-469c-b01d-67a6e3049023,
property: Radarr__Postgres__MainDb,
}
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }
-27
View File
@@ -1,27 +0,0 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: recommender, namespace: media }
spec:
refreshInterval: 1h
target:
name: recommender-secrets
deletionPolicy: Retain
template:
type: Opaque
data:
OPENAI_API_KEY: "{{ .OPENAI_API_KEY }}"
DATABASE_URL: "{{ .DATABASE_URL }}"
data:
- secretKey: OPENAI_API_KEY
remoteRef:
{
key: 5079ef6f-3d1d-4522-b22c-6dd5f1c19acd,
property: OPENAI_API_KEY,
}
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }
- secretKey: DATABASE_URL
remoteRef:
{ key: 5079ef6f-3d1d-4522-b22c-6dd5f1c19acd, property: DATABASE_URL }
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }
+27
View File
@@ -0,0 +1,27 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: renovate-bot, namespace: cronjobs }
spec:
refreshInterval: 1h
target:
name: renovate-bot
deletionPolicy: Retain
template:
type: Opaque
data:
RENOVATE_GITHUB_COM_TOKEN: "{{ .RENOVATE_GITHUB_COM_TOKEN }}"
RENOVATE_TOKEN: "{{ .RENOVATE_TOKEN }}"
data:
- secretKey: RENOVATE_GITHUB_COM_TOKEN
remoteRef:
{
key: 25b1f5c9-0a2b-438e-a4c6-23ea1d58e2de,
property: RENOVATE_GITHUB_COM_TOKEN,
}
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }
- secretKey: RENOVATE_TOKEN
remoteRef:
{ key: 25b1f5c9-0a2b-438e-a4c6-23ea1d58e2de, property: RENOVATE_TOKEN }
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }
-42
View File
@@ -1,42 +0,0 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: sonarr, namespace: media }
spec:
refreshInterval: 1h
target:
name: sonarr-secret
deletionPolicy: Retain
template:
type: Opaque
data:
Sonarr__Postgres__User: "{{ .Sonarr__Postgres__User }}"
Sonarr__Postgres__Password: "{{ .Sonarr__Postgres__Password }}"
Sonarr__Postgres__Host: "{{ .Sonarr__Postgres__Host }}"
Sonarr__Postgres__MainDb: "{{ .Sonarr__Postgres__MainDb }}"
data:
- secretKey: Sonarr__Postgres__User
remoteRef:
{ key: 2c5b6d27-971f-4876-b10d-db400dfde7b2, property: username }
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }
- secretKey: Sonarr__Postgres__Password
remoteRef:
{ key: 2c5b6d27-971f-4876-b10d-db400dfde7b2, property: password }
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }
- secretKey: Sonarr__Postgres__Host
remoteRef:
{
key: 2c5b6d27-971f-4876-b10d-db400dfde7b2,
property: Sonarr__Postgres__Host,
}
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }
- secretKey: Sonarr__Postgres__MainDb
remoteRef:
{
key: 2c5b6d27-971f-4876-b10d-db400dfde7b2,
property: Sonarr__Postgres__MainDb,
}
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }