Compare commits

...

15 Commits

Author SHA1 Message Date
15c597406a small changes 2026-07-17 21:13:08 -03:00
2d29bddfcd adding ansible 2026-07-17 21:12:47 -03:00
7c2086009a adding bitwarden ESO and secrets 2026-07-16 20:44:15 -03:00
656f9053b4 adding deployments and small adjustments 2026-07-16 13:00:48 -03:00
ce8270290d fix(eso): bw-serve startup actually authenticates + re-logins every cycle
The bitwarden-cli Deployment's startup script never set BW_SESSION when
running without API-key creds (empty password branch / bw unlock --check
only), so bw serve started unauthenticated and every ExternalSecret across
all namespaces failed with HTTP 400 / 'Not found'. Wrap login in a function,
export the session, warm the cache once, and re-login every 5m in the
background loop so the session token never lapses.
2026-07-14 06:59:33 -03:00
d742121719 updating docs 2026-07-10 23:22:28 -03:00
e8f1185412 fixing spec issues 2026-07-03 14:00:14 -03:00
f4044715e4 editing openwebui 2026-07-03 13:55:43 -03:00
ffa4059b07 adding secrets folder 2026-07-03 13:39:53 -03:00
735d00eaf0 updating grafana 2026-07-03 13:39:16 -03:00
1c6ffc6fad adding new applications 2026-07-03 13:38:16 -03:00
b5e1276977 updating openwebui 2026-07-03 13:31:59 -03:00
df4cf4ba77 moving things around 2026-07-03 13:27:31 -03:00
5ff3589fc9 fix(notepad): override entrypoint to skip chown on NFS-mounted _tmp
The notepad container's entrypoint runs chown -R www-data:www-data
on /var/www/html/_tmp, which fails because the NFS export on nebula
uses all_squash with anonuid=65534. Override the command to skip
chown and just create the dir + copy .htaccess before starting apache.
2026-06-28 15:22:10 -03:00
63b768ebf4 removing 2nd AdGuard 2026-05-26 20:16:02 -03:00
42 changed files with 1536 additions and 455 deletions

2
.gitignore vendored
View File

@@ -11,3 +11,5 @@ cronjobs
*.crt
*.key
chacal/

View File

@@ -23,10 +23,13 @@ The repository name references my local TLD, `.haven` ;)
- Uptimekuma
- Vaultwarden
- OpenWebUI
- Paperless
- cloud
- Cloudreve
- Slink
- dns
- AdGuardHome
- AdGuardHome-2 (2nd instance)
- AdGuard-Sync
- AdGuardSync
- infra
- [Haven Notify](https://git.ivanch.me/ivanch/server-scripts/src/branch/main/haven-notify)
- Beszel

View File

@@ -1,3 +1,11 @@
# 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
```

4
ansible/.ansible-lint Normal file
View File

@@ -0,0 +1,4 @@
---
profile: basic
exclude_paths:
- collections/

13
ansible/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Encrypted secrets
group_vars/all/vault.yml
# Installed collections
collections/
# Ansible retry files
*.retry
# Local controller environment and test caches
.venv/
__pycache__/
.pytest_cache/

9
ansible/ansible.cfg Normal file
View File

@@ -0,0 +1,9 @@
[defaults]
inventory = inventory.yml
host_key_checking = True
retry_files_enabled = False
stdout_callback = default
result_format = yaml
collections_path = ./collections
roles_path = ./roles
interpreter_python = auto_silent

49
ansible/inventory.yml Normal file
View File

@@ -0,0 +1,49 @@
---
# Proxmox connection: local API access (the controller reaches the node directly).
# This is the "proxmox" group — not an SSH target, just connection params.
all:
children:
proxmox_nodes:
hosts:
otherside:
ansible_host: otherside.haven
ansible_connection: local # we call the Proxmox API, not SSH
astrid:
ansible_host: astrid.haven
ansible_connection: local # we call the Proxmox API, not SSH
k3s_cluster:
children:
grp_k3s_server: # group name == group_vars/grp_k3s_server.yml
hosts:
iris:
ansible_host: iris.haven
ansible_user: root
ansible_python_interpreter: /usr/bin/python3
grp_k3s_agent: # group name == group_vars/grp_k3s_agent.yml
hosts:
vega:
ansible_host: vega.haven
ansible_user: root
ansible_python_interpreter: /usr/bin/python3
nebula:
ansible_host: nebula.haven
ansible_user: root
ansible_python_interpreter: /usr/bin/python3
nexus:
ansible_host: nexus.haven
ansible_user: root
ansible_python_interpreter: /usr/bin/python3
lxcs:
children:
grp_docker_build: # group name == group_vars/grp_docker_build.yml
hosts:
docker_build:
ansible_host: docker-build.haven
ansible_user: root
ansible_python_interpreter: /usr/bin/python3
grp_redis: # group name == group_vars/grp_redis.yml
hosts:
redis:
ansible_host: redis.haven
ansible_user: root
ansible_python_interpreter: /usr/bin/python3

View File

@@ -0,0 +1,69 @@
## Updates k3s in the entire cluster
- name: Update k3s Control Plane
hosts: grp_k3s_server
serial: 1
gather_facts: false
become: true
tasks:
- name: Update k3s on control plane node
ansible.builtin.shell: >
sh <(curl -sfL https://get.k3s.io)
args:
executable: /bin/bash
register: k3s_update
- name: Print update output
ansible.builtin.debug:
var: k3s_update
- name: Update k3s Agents
hosts: grp_k3s_agent
serial: 1
gather_facts: false
become: true
tasks:
- name: Cordon agent node
ansible.builtin.command: kubectl cordon {{ inventory_hostname }}
delegate_to: "{{ groups['grp_k3s_server'][0] }}"
environment:
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
ignore_errors: true
- name: Drain agent node
ansible.builtin.command: >
kubectl drain {{ inventory_hostname }}
--ignore-daemonsets
--delete-emptydir-data
--force
--timeout=60s
delegate_to: "{{ groups['grp_k3s_server'][0] }}"
environment:
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
ignore_errors: true
- name: Kill k3s-agent process
ansible.builtin.shell: k3s-killall.sh
ignore_errors: true
- name: Update k3s on agent node
ansible.builtin.shell: >
sh <(curl -sfL https://get.k3s.io)
args:
executable: /bin/bash
register: k3s_update
- name: Restart k3s-agent service
ansible.builtin.systemd:
name: k3s-agent
state: restarted
- name: Print update output
ansible.builtin.debug:
var: k3s_update
- name: Uncordon agent node
ansible.builtin.command: kubectl uncordon {{ inventory_hostname }}
delegate_to: "{{ groups['grp_k3s_server'][0] }}"
environment:
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
ignore_errors: true

View File

@@ -0,0 +1,2 @@
-r requirements-python.txt
ansible-lint==26.6.0

View File

@@ -0,0 +1,3 @@
ansible-core==2.21.2
proxmoxer==2.3.0
requests==2.34.2

6
ansible/requirements.yml Normal file
View File

@@ -0,0 +1,6 @@
---
collections:
- name: community.proxmox
version: "2.0.0"
- name: community.general
version: "13.2.0"

178
cloud/cloudreve.yaml Normal file
View File

@@ -0,0 +1,178 @@
# 2) Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudreve
namespace: cloud
spec:
replicas: 1
selector:
matchLabels:
app: cloudreve
template:
metadata:
labels:
app: cloudreve
spec:
containers:
- name: cloudreve
image: cloudreve/cloudreve:v4
imagePullPolicy: Always
ports:
- containerPort: 5212
name: http
- containerPort: 6888
name: slave-tcp
protocol: TCP
- containerPort: 6888
name: slave-udp
protocol: UDP
env:
- name: CR_CONF_Database.Type
value: "postgres"
- name: CR_CONF_Database.Host
value: "postgresql.haven"
- name: CR_CONF_Database.Port
value: "5432"
- name: CR_CONF_Database.User
valueFrom:
secretKeyRef:
name: cloudreve-secret
key: DB_USER
- name: CR_CONF_Database.Name
valueFrom:
secretKeyRef:
name: cloudreve-secret
key: DB_NAME
- name: CR_CONF_Database.Password
valueFrom:
secretKeyRef:
name: cloudreve-secret
key: DB_PASSWORD
- name: CR_CONF_Database.SSLMode
value: "disable"
- name: CR_CONF_Redis.Server
value: ""
- name: CR_CONF_Redis.Password
valueFrom:
secretKeyRef:
name: cloudreve-secret
key: REDIS_PASSWORD
volumeMounts:
- name: cloudreve-data
mountPath: /cloudreve/data
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "1Gi"
volumes:
- name: cloudreve-data
nfs:
server: 192.168.15.99
path: /export/Storage/Cloud
---
# 3) Service (HTTP)
apiVersion: v1
kind: Service
metadata:
name: cloudreve
namespace: cloud
spec:
type: ClusterIP
selector:
app: cloudreve
ports:
- port: 5212
targetPort: 5212
name: http
---
# 4) Service (Slave node / WebDAV — optional)
apiVersion: v1
kind: Service
metadata:
name: cloudreve-slave
namespace: cloud
spec:
type: ClusterIP
selector:
app: cloudreve
ports:
- port: 6888
targetPort: 6888
name: slave-tcp
protocol: TCP
- port: 6888
targetPort: 6888
name: slave-udp
protocol: UDP
---
# 5) PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cloudreve-data
namespace: cloud
annotations:
nfs.io/storage-path: "cloudreve-data"
spec:
storageClassName: "nfs-client"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
limits:
storage: 50Gi
---
# 6) Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cloudreve
namespace: cloud
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
spec:
ingressClassName: nginx
rules:
- host: cloud.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: cloudreve
port:
number: 5212
---
# 7) Ingress Public
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cloudreve-public
namespace: cloud
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
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:
paths:
- path: /
pathType: Prefix
backend:
service:
name: cloudreve
port:
number: 5212

136
cloud/slink.yaml Normal file
View File

@@ -0,0 +1,136 @@
# 1) Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: slink
namespace: cloud
spec:
replicas: 1
selector:
matchLabels:
app: slink
template:
metadata:
labels:
app: slink
spec:
containers:
- name: slink
image: anirdev/slink:latest
imagePullPolicy: Always
securityContext:
runAsUser: 1000
runAsGroup: 1000
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: ORIGIN
value: "http://slink.haven"
- name: TZ
value: "America/Sao_Paulo"
- name: USER_APPROVAL_REQUIRED
value: "true"
- name: USER_PASSWORD_MIN_LENGTH
value: "8"
- name: USER_PASSWORD_REQUIREMENTS
value: "15"
- name: ADMIN_USERNAME
valueFrom:
secretKeyRef:
name: slink-secret
key: ADMIN_USERNAME
- name: ADMIN_EMAIL
valueFrom:
secretKeyRef:
name: slink-secret
key: ADMIN_EMAIL
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: slink-secret
key: ADMIN_PASSWORD
- name: IMAGE_MAX_SIZE
value: "50M"
- name: IMAGE_STRIP_EXIF_METADATA
value: "true"
- name: IMAGE_COMPRESSION_QUALITY
value: "90"
- name: STORAGE_PROVIDER
value: "local"
ports:
- containerPort: 3000
name: slink-port
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "500m"
memory: "512Mi"
volumeMounts:
- name: slink-data
mountPath: /app/var/data
- name: slink-data
mountPath: /app/slink/images
volumes:
- name: slink-data
persistentVolumeClaim:
claimName: slink-data
---
# 2) Service
apiVersion: v1
kind: Service
metadata:
name: slink
namespace: cloud
spec:
type: ClusterIP
selector:
app: slink
ports:
- port: 3000
targetPort: slink-port
---
# 3) PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: slink-data
namespace: cloud
annotations:
nfs.io/storage-path: "slink-data"
spec:
storageClassName: "nfs-client"
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
limits:
storage: 15Gi
---
# 4) Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: slink
namespace: cloud
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
spec:
ingressClassName: nginx
rules:
- host: slink.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: slink
port:
number: 3000

View File

@@ -26,6 +26,13 @@ spec:
secretKeyRef:
name: password
key: password
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"
---
apiVersion: apps/v1
kind: Deployment
@@ -76,6 +83,13 @@ spec:
volumeMounts:
- name: archivebox-data
mountPath: /data
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "2Gi"
cpu: "3000m"
volumes:
- name: archivebox-data
persistentVolumeClaim:

81
default/haven-notify.yaml Normal file
View File

@@ -0,0 +1,81 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: haven-notify
namespace: default
labels:
app: haven-notify
spec:
replicas: 1
selector:
matchLabels:
app: haven-notify
template:
metadata:
labels:
app: haven-notify
spec:
containers:
- name: haven-notify
image: git.ivanch.me/ivanch/haven-notify:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: WEBHOOK_URL
valueFrom:
secretKeyRef:
name: discord-webhook
key: HAVEN_WEBHOOK_URL
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /live
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
name: haven-notify
namespace: default
spec:
selector:
app: haven-notify
ports:
- protocol: TCP
port: 8080
targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: haven-notify
namespace: default
spec:
ingressClassName: nginx
rules:
- host: notify.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: haven-notify
port:
number: 8080

View File

@@ -102,7 +102,7 @@ spec:
labels:
app.kubernetes.io/name: homepage
annotations:
configmap.reloader/checksum: "{{ include (print $.Template.BasePath \"/app/config/services.yaml\") . | sha256sum }}"
configmap.reloader/checksum: '{{ include (print $.Template.BasePath "/app/config/services.yaml") . | sha256sum }}'
spec:
serviceAccountName: homepage
automountServiceAccountToken: true
@@ -138,6 +138,13 @@ spec:
- name: homepage-config
mountPath: /app/public/images
subPath: images
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: homepage-config
persistentVolumeClaim:
@@ -167,7 +174,6 @@ metadata:
namespace: default
labels:
app.kubernetes.io/name: homepage
annotations:
spec:
type: ClusterIP
ports:
@@ -185,12 +191,6 @@ metadata:
namespace: default
labels:
app.kubernetes.io/name: homepage
# annotations:
# gethomepage.dev/description: Dynamically Detected Homepage
# gethomepage.dev/enabled: "true"
# gethomepage.dev/group: Cluster Management
# gethomepage.dev/icon: homepage.png
# gethomepage.dev/name: Homepage
spec:
ingressClassName: nginx
rules:

View File

@@ -26,6 +26,13 @@ spec:
port: 80
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"
---
apiVersion: v1
kind: Service

View File

@@ -19,8 +19,25 @@ 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
- >-
mkdir -p /var/www/html/_tmp &&
cp -n /var/www/html/notes.htaccess /var/www/html/_tmp/.htaccess 2>/dev/null;
exec docker-php-entrypoint apache2-foreground
ports:
- containerPort: 80
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
volumeMounts:
- name: notepad-data
mountPath: /var/www/html/_tmp
@@ -69,7 +86,7 @@ metadata:
spec:
ingressClassName: nginx
rules:
- host: notepad.lab
- host: notepad.haven
http:
paths:
- path: /

View File

@@ -14,12 +14,34 @@ spec:
labels:
app: openwebui
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- iris
containers:
- name: openwebui
image: ghcr.io/open-webui/open-webui:main-slim
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: TZ
value: "America/Sao_Paulo"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: openwebui-secret
key: DATABASE_URL
resources:
requests:
cpu: "250m"

131
default/paperless.yaml Normal file
View File

@@ -0,0 +1,131 @@
---
# 1) Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: paperless
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: paperless
template:
metadata:
labels:
app: paperless
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- iris
containers:
- name: paperless
image: ghcr.io/paperless-ngx/paperless-ngx:latest
imagePullPolicy: Always
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: PAPERLESS_URL
value: "http://paperless.haven"
- name: PAPERLESS_TIME_ZONE
value: "America/Sao_Paulo"
- name: PAPERLESS_OCR_LANGUAGE
value: "por"
- name: PAPERLESS_OCR_LANGUAGES
value: "por"
- name: PAPERLESS_OCR_USER_ARGS
value: '{"invalidate_digital_signatures": true}'
- name: PAPERLESS_DBHOST
value: postgresql.haven
- name: PAPERLESS_REDIS
value: "redis://redis.haven:6379"
- name: PAPERLESS_PORT
value: "8000"
ports:
- containerPort: 8000
name: paperless-port
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "4000m"
memory: "1Gi"
volumeMounts:
- name: paperless-data
subPath: data
mountPath: /usr/src/paperless/data
- name: paperless-data
subPath: media
mountPath: /usr/src/paperless/media
- name: paperless-data
subPath: export
mountPath: /usr/src/paperless/export
- name: paperless-data
subPath: consume
mountPath: /usr/src/paperless/consume
volumes:
- name: paperless-data
persistentVolumeClaim:
claimName: paperless-data
---
# 2) Service
apiVersion: v1
kind: Service
metadata:
name: paperless
namespace: default
spec:
type: ClusterIP
selector:
app: paperless
ports:
- port: 8000
targetPort: paperless-port
---
# 3) PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: paperless-data
namespace: default
annotations:
nfs.io/storage-path: "paperless-data"
spec:
storageClassName: "nfs-client"
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
limits:
storage: 15Gi
---
# 4) Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: paperless
namespace: default
spec:
ingressClassName: nginx
rules:
- host: paperless.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: paperless
port:
number: 8000

View File

@@ -30,6 +30,31 @@ spec:
ports:
- containerPort: 80
name: vault-port
# /alive requires no authentication and verifies the Vaultwarden process
startupProbe:
httpGet:
path: /alive
port: vault-port
scheme: HTTP
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
readinessProbe:
httpGet:
path: /alive
port: vault-port
scheme: HTTP
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
livenessProbe:
httpGet:
path: /alive
port: vault-port
scheme: HTTP
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 6
volumeMounts:
- name: vaultwarden-data
mountPath: /data

View File

@@ -14,6 +14,10 @@ data:
- "ubuntu-amd64:docker://docker.gitea.com/runner-images:ubuntu-latest"
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
- "ubuntu-slim:docker://docker.gitea.com/runner-images:ubuntu-latest-slim"
- "runner-full:docker://git.ivanch.me/ivanch/runner-images:full"
- "runner-slim:docker://git.ivanch.me/ivanch/runner-images:slim"
- "runner-full-amd64:docker://git.ivanch.me/ivanch/runner-images:full"
- "runner-slim-amd64:docker://git.ivanch.me/ivanch/runner-images:slim"
---
# --- ConfigMap for the ARM64 Runner ---
apiVersion: v1
@@ -28,6 +32,8 @@ data:
timeout: 1h
labels:
- "ubuntu-arm64:docker://docker.gitea.com/runner-images:ubuntu-latest"
- "runner-full-arm64:docker://git.ivanch.me/ivanch/runner-images:full"
- "runner-slim-arm64:docker://git.ivanch.me/ivanch/runner-images:slim"
---
# PersistentVolumeClaim for AMD64
apiVersion: v1

View File

@@ -1,6 +0,0 @@
## Setup AdGuard Sync credentials
```bash
kubectl create secret generic adguardhome-password \
--from-literal=password='your_adguardhome_password' \
--from-literal=username='your_adguardhome_username' -n dns
```

View File

@@ -40,7 +40,7 @@ spec:
name: web-port
env:
- name: CRON
value: "*/2 * * * *"
value: "0 * * * *"
- name: RUN_ON_START
value: "true"
- name: LOG_LEVEL

View File

@@ -1,145 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: adguardhome2-pvc
namespace: dns
annotations:
nfs.io/storage-path: "adguardhome2-config"
spec:
storageClassName: "nfs-client"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: adguardhome2
namespace: dns
spec:
strategy:
type: Recreate
replicas: 1
selector:
matchLabels:
app: adguardhome2
template:
metadata:
labels:
app: adguardhome2
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- iris
containers:
- name: adguardhome2
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: 256Mi
volumeMounts:
- name: adguardhome2-storage
mountPath: /opt/adguardhome/work
- name: adguardhome2-storage
mountPath: /opt/adguardhome/conf
volumes:
- name: adguardhome2-storage
persistentVolumeClaim:
claimName: adguardhome2-pvc
---
apiVersion: v1
kind: Service
metadata:
name: adguardhome2-svc
namespace: dns
spec:
type: LoadBalancer
selector:
app: adguardhome2
loadBalancerIP: 192.168.15.201
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: adguard2-install-svc
namespace: dns
spec:
type: ClusterIP
selector:
app: adguardhome2
ports:
- name: install
port: 3000
targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: adguardhome2-ingress
namespace: dns
spec:
ingressClassName: nginx
rules:
- host: adguard2.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: adguardhome2-svc
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: adguardhome2-install-ingress
namespace: dns
spec:
ingressClassName: nginx
rules:
- host: install.adguard2.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: adguard2-install-svc
port:
number: 3000

View File

@@ -1,44 +0,0 @@
# docker-node: iris.haven
# port: 4100
# Service
apiVersion: v1
kind: Service
metadata:
name: paperless-service
namespace: docker-ingress
spec:
ports:
- port: 80
targetPort: 4200
---
# Endpoints
apiVersion: v1
kind: Endpoints
metadata:
name: paperless-service
namespace: docker-ingress
subsets:
- addresses:
- ip: 192.168.20.100
ports:
- port: 4200
---
# Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: paperless-ingress
namespace: docker-ingress
spec:
rules:
- host: paperless.haven
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: paperless-service
port:
number: 80

View File

@@ -15,6 +15,16 @@ spec:
labels:
app: code-config
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- iris
containers:
- name: code-config
image: lscr.io/linuxserver/code-server:latest

View File

@@ -0,0 +1,121 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: bitwarden-cli
namespace: infra
labels:
app.kubernetes.io/name: bitwarden-cli
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: bitwarden-cli
template:
metadata:
labels:
app.kubernetes.io/name: bitwarden-cli
spec:
containers:
- name: bitwarden-cli
image: ghcr.io/charlesthomas/bitwarden-cli:2026.3.0
imagePullPolicy: IfNotPresent
# Override the baked entrypoint so --disable-origin-protection is
# actually passed (it was commented out in the image's entrypoint.sh,
# which made bw serve reject all cross-pod requests -> connection refused).
command: ["/bin/bash", "-lc"]
args:
- |
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.
login() {
if [ -n "$BW_CLIENTID" ] && [ -n "$BW_CLIENTSECRET" ]; then
echo "Using apikey to log in"
BW_SESSION=$(bw login --apikey --raw) || return 1
else
echo "Using password to log in"
BW_SESSION=$(bw login "${BW_USER}" --passwordenv BW_PASSWORD --raw) || return 1
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).
echo "Starting periodic bw login+sync loop (every 5m)"
(
while true; do
sleep 300
login || true
echo "[$(date -u +%FT%TZ)] bw sync"
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:
- name: BW_HOST
valueFrom:
secretKeyRef:
name: bitwarden-cli
key: BW_HOST
- name: BW_USER
valueFrom:
secretKeyRef:
name: bitwarden-cli
key: BW_USERNAME
- name: BW_PASSWORD
valueFrom:
secretKeyRef:
name: bitwarden-cli
key: BW_PASSWORD
ports:
- name: http
containerPort: 8087
protocol: TCP
startupProbe:
tcpSocket: { port: 8087 }
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
readinessProbe:
tcpSocket: { port: 8087 }
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
livenessProbe:
tcpSocket: { port: 8087 }
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 6
resources:
limits:
cpu: 400m
memory: 512Mi
requests:
cpu: 50m
memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
name: bitwarden-cli
namespace: infra
labels:
app.kubernetes.io/name: bitwarden-cli
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: bitwarden-cli
ports:
- name: http
port: 8087
targetPort: http
protocol: TCP

View File

@@ -0,0 +1,21 @@
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

View File

@@ -0,0 +1,20 @@
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata: { name: bitwarden-login }
spec:
provider:
webhook:
url: "http://bitwarden-cli.infra.svc:8087/object/item/{{ .remoteRef.key }}"
result: { jsonPath: "$.data.login.{{ .remoteRef.property }}" }
---
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata: { name: bitwarden-fields }
spec:
provider:
webhook:
url: "http://bitwarden-cli.infra.svc:8087/object/item/{{ .remoteRef.key }}"
result:
{
jsonPath: '$.data.fields[?@.name=="{{ .remoteRef.property }}"].value',
}

View File

@@ -75,6 +75,9 @@ spec:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 2000m
memory: 1Gi
volumeMounts:
- name: wg-easy-volume
mountPath: /etc/wireguard

View File

@@ -48,6 +48,9 @@ spec:
requests:
cpu: 250m
memory: 750Mi
limits:
memory: 1Gi
cpu: 500m
volumeMounts:
- mountPath: /var/lib/grafana
name: grafana-pv
@@ -103,3 +106,22 @@ spec:
name: grafana
port:
number: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: monitoring
name: grafana-public
spec:
ingressClassName: nginx
rules:
- host: grafanah.ivanch.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grafana
port:
number: 3000

24
secrets/adguard.yaml Normal file
View File

@@ -0,0 +1,24 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: adguardhome-password, namespace: dns }
spec:
refreshInterval: 1h
target:
name: adguardhome-password
deletionPolicy: Retain
template:
type: Opaque
data:
username: "{{ .username }}"
password: "{{ .password }}"
data:
- secretKey: username
remoteRef:
{ key: 5bc7e63f-18bb-482c-bc3e-740e03b26334, property: username }
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }
- secretKey: password
remoteRef:
{ key: 5bc7e63f-18bb-482c-bc3e-740e03b26334, property: password }
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }

36
secrets/cloudreve.yaml Normal file
View File

@@ -0,0 +1,36 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: cloudreve, namespace: cloud }
spec:
refreshInterval: 1h
target:
name: cloudreve-secret
deletionPolicy: Retain
template:
type: Opaque
data:
DB_USER: "{{ .DB_USER }}"
DB_PASSWORD: "{{ .DB_PASSWORD }}"
DB_NAME: "{{ .DB_NAME }}"
REDIS_PASSWORD: "{{ .REDIS_PASSWORD }}"
data:
- secretKey: DB_USER
remoteRef:
{ key: 8c25cac9-e9e8-4970-bdf4-31f9aee19276, property: username }
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }
- secretKey: DB_PASSWORD
remoteRef:
{ key: 8c25cac9-e9e8-4970-bdf4-31f9aee19276, property: password }
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }
- secretKey: DB_NAME
remoteRef:
{ key: 8c25cac9-e9e8-4970-bdf4-31f9aee19276, property: DB_NAME }
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }
- secretKey: REDIS_PASSWORD
remoteRef:
{ key: 8c25cac9-e9e8-4970-bdf4-31f9aee19276, property: REDIS_PASSWORD }
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }

21
secrets/gitea-runner.yaml Normal file
View File

@@ -0,0 +1,21 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: gitea-runner, namespace: dev }
spec:
refreshInterval: 1h
target:
name: gitea-runner-token
deletionPolicy: Retain
template:
type: Opaque
data:
REGISTRATION_TOKEN: "{{ .REGISTRATION_TOKEN }}"
data:
- secretKey: REGISTRATION_TOKEN
remoteRef:
{
key: 4e286657-b5de-4354-be2e-d1649b0fd527,
property: REGISTRATION_TOKEN,
}
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }

27
secrets/openwebui.yaml Normal file
View File

@@ -0,0 +1,27 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: openwebui, namespace: default }
spec:
refreshInterval: 1h
target:
name: openwebui-secret
deletionPolicy: Retain
template:
type: Opaque
data:
DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@postgresql.haven:5432/{{ .dbname }}"
data:
- secretKey: username
remoteRef:
{ key: e80ff314-7445-4b61-b5c5-69285ea72586, property: username }
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }
- secretKey: password
remoteRef:
{ key: e80ff314-7445-4b61-b5c5-69285ea72586, property: password }
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }
- secretKey: dbname
remoteRef: { key: e80ff314-7445-4b61-b5c5-69285ea72586, property: dbname }
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }

39
secrets/paperless.yaml Normal file
View File

@@ -0,0 +1,39 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: paperless, namespace: default }
spec:
refreshInterval: 1h
target:
name: paperless-secret
deletionPolicy: Retain
template:
type: Opaque
data:
PAPERLESS_DBNAME: "{{ .PAPERLESS_DBNAME }}"
PAPERLESS_DBUSER: "{{ .PAPERLESS_DBUSER }}"
PAPERLESS_DBPASSWORD: "{{ .PAPERLESS_DBPASSWORD }}"
data:
- secretKey: PAPERLESS_DBNAME
remoteRef:
{
key: 05426e55-fe04-4c16-8697-8a258928257a,
property: PAPERLESS_DBNAME,
}
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }
- secretKey: PAPERLESS_DBUSER
remoteRef:
{
key: 05426e55-fe04-4c16-8697-8a258928257a,
property: PAPERLESS_DBUSER,
}
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }
- secretKey: PAPERLESS_DBPASSWORD
remoteRef:
{
key: 05426e55-fe04-4c16-8697-8a258928257a,
property: PAPERLESS_DBPASSWORD,
}
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }

48
secrets/radarr.yaml Normal file
View File

@@ -0,0 +1,48 @@
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
secrets/recommender.yaml Normal file
View File

@@ -0,0 +1,27 @@
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 } }

30
secrets/slink.yaml Normal file
View File

@@ -0,0 +1,30 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: slink, namespace: cloud }
spec:
refreshInterval: 1h
target:
name: slink-secret
deletionPolicy: Retain
template:
type: Opaque
data:
ADMIN_USERNAME: "{{ .ADMIN_USERNAME }}"
ADMIN_PASSWORD: "{{ .ADMIN_PASSWORD }}"
ADMIN_EMAIL: "{{ .ADMIN_EMAIL }}"
data:
- secretKey: ADMIN_USERNAME
remoteRef:
{ key: 79f97033-6bc3-40cd-a28c-6060b8bd3a63, property: username }
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }
- secretKey: ADMIN_PASSWORD
remoteRef:
{ key: 79f97033-6bc3-40cd-a28c-6060b8bd3a63, property: password }
sourceRef:
{ storeRef: { name: bitwarden-login, kind: ClusterSecretStore } }
- secretKey: ADMIN_EMAIL
remoteRef:
{ key: 79f97033-6bc3-40cd-a28c-6060b8bd3a63, property: ADMIN_EMAIL }
sourceRef:
{ storeRef: { name: bitwarden-fields, kind: ClusterSecretStore } }

42
secrets/sonarr.yaml Normal file
View File

@@ -0,0 +1,42 @@
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 } }