Initial commit: k3s homelab infrastructure

Helm charts for media stack (Plex, Sonarr, Radarr, etc.), dashboards
(Glance, Homepage), paperless-ngx, mealie, traefik ingress, MetalLB,
and utilities. Includes SOPS-encrypted secrets and bootstrap script.
This commit is contained in:
Alvin Wang 2026-04-19 19:22:22 -04:00
commit e07a5e1dfa
44 changed files with 2029 additions and 0 deletions

6
.sops.yaml Normal file
View File

@ -0,0 +1,6 @@
creation_rules:
- path_regex: secrets/.*\.yaml$
age: >-
age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te
# Replace the above with your actual age public key from:
# grep 'public key' /etc/sops/age/keys.txt

6
AGENTS.md Normal file
View File

@ -0,0 +1,6 @@
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
dogbox Ready control-plane 3h31m v1.34.6+k3s1 10.0.1.2 <none> Fedora Linux 40 (Server Edition) 6.9.6-200.fc40.x86_64 containerd://2.2.2-bd1.34
mac-worker Ready <none> 3h13m v1.34.6+k3s1 192.168.139.12 <none> Ubuntu 25.10 6.17.8-orbstack-00308-g8f9c941121b1 containerd://2.2.2-bd1.34
The mac-worker is running inside orbstack linux VM if that matters.

152
README.md Normal file
View File

@ -0,0 +1,152 @@
# Homelab — k3s Cluster
2-node k3s cluster (1 manager, 1 worker) running a self-hosted homelab stack.
## Architecture
```
Manager Node Worker Node
┌──────────────┐ ┌──────────────┐
│ k3s server │────────────│ k3s agent │
│ Traefik │ │ │
│ Longhorn │ │ Longhorn │
└──────┬───────┘ └──────┬───────┘
│ │
└───────── NFS ─────────────┘
/dogstore
```
**Storage strategy:**
- `/dogstore` (NFS) — mounted on both nodes, used via hostPath for large media/data volumes
- Longhorn — replicated block storage for small config/state volumes
**Ingress:** k3s built-in Traefik with Let's Encrypt via Cloudflare DNS challenge.
**Secrets:** SOPS + age encryption. Secrets live in `secrets/secrets.enc.yaml`, encrypted at rest.
## Services
| Chart | Namespace | Services |
| -------------- | ----------- | -------------------------------------------------------------- |
| traefik-config | kube-system | Traefik HelmChartConfig overlay |
| media | media | Plex, Sonarr, Radarr, Bazarr, Prowlarr, qBittorrent, unpackerr |
| paperless | paperless | Paperless-ngx, Redis, PostgreSQL |
| mealie | apps | Mealie |
| dashboards | apps | Homepage, Glance |
| utils | apps | Zerobyte, Seerr |
## Prerequisites
- Two Linux machines with NFS `/dogstore` mounted on both
- `curl`, `helm`, `kubectl`, `sops`, `age` installed
## Bootstrap
### 1. Install k3s server (manager node)
```bash
./scripts/bootstrap.sh server
```
This prints the worker join command at the end.
### 2. Install k3s agent (worker node)
```bash
K3S_URL="https://<manager-ip>:6443" K3S_TOKEN="<token>" ./scripts/bootstrap.sh agent
```
### 3. Install Longhorn
```bash
./scripts/bootstrap.sh longhorn
```
### 4. Set up SOPS encryption
Generate an age keypair (run on each node):
```bash
./scripts/bootstrap.sh sops-keygen
```
Copy the public key into `.sops.yaml`, replacing the placeholder. Then encrypt your secrets:
```bash
# Edit secrets/secrets.enc.yaml — replace REPLACE_WITH_* placeholders with real values
sops -e -i secrets/secrets.enc.yaml
```
### 5. Apply secrets
```bash
./scripts/bootstrap.sh apply-secrets
```
### 6. Deploy all charts
```bash
./scripts/bootstrap.sh deploy
```
Or deploy individually:
```bash
kubectl create namespace media
helm upgrade --install media charts/media -n media
kubectl create namespace paperless
helm upgrade --install paperless charts/paperless -n paperless
kubectl create namespace apps
helm upgrade --install mealie charts/mealie -n apps
helm upgrade --install dashboards charts/dashboards -n apps
helm upgrade --install utils charts/utils -n apps
# Traefik config goes in kube-system (managed by k3s)
helm upgrade --install traefik-config charts/traefik-config -n kube-system
```
## Verifying
```bash
# Check all pods
kubectl get pods -A
# Check ingress routes
kubectl get ingress -A
# Test a specific service
curl -I https://mealie.ratboo.me
```
## Secret Rotation
1. Decrypt: `sops secrets/secrets.enc.yaml` (opens in `$EDITOR`)
2. Change the values
3. Save and close (SOPS re-encrypts automatically)
4. Apply: `./scripts/bootstrap.sh apply-secrets`
5. Restart affected pods: `kubectl rollout restart deployment/<name> -n <namespace>`
## Repo Structure
```
homelab/
├── README.md
├── .sops.yaml
├── scripts/
│ └── bootstrap.sh
├── charts/
│ ├── traefik-config/
│ ├── media/
│ ├── paperless/
│ ├── mealie/
│ ├── dashboards/
│ └── utils/
└── secrets/
└── secrets.enc.yaml
```

View File

@ -0,0 +1,5 @@
apiVersion: v2
name: dashboards
description: Homepage and Glance dashboard services
version: 0.1.0
type: application

View File

@ -0,0 +1,15 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: glance
annotations:
kubernetes.io/ingress.class: traefik-internal
spec:
entryPoints:
- web
routes:
- match: PathPrefix(`/`)
kind: Rule
services:
- name: glance
port: 8082

View File

@ -0,0 +1,57 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: glance
labels:
app: glance
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: glance
template:
metadata:
labels:
app: glance
spec:
nodeSelector:
node-role.kubernetes.io/control-plane: "true"
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
containers:
- name: glance
image: {{ .Values.glance.image }}
ports:
- containerPort: 8080
env:
- name: DOMAIN
value: {{ .Values.domain }}
- name: HOST_IP
value: {{ .Values.hostIp | quote }}
- name: ADGUARD_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.glance.secretName }}
key: ADGUARD_PASSWORD
volumeMounts:
- name: config
mountPath: /app/config
volumes:
- name: config
hostPath:
path: /dogstore/service-data/glance/config
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: glance
spec:
selector:
app: glance
ports:
- port: 8082
targetPort: 8080

View File

@ -0,0 +1,50 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: homepage
labels:
app: homepage
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: homepage
template:
metadata:
labels:
app: homepage
spec:
nodeSelector:
node-role.kubernetes.io/control-plane: "true"
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
containers:
- name: homepage
image: {{ .Values.homepage.image }}
ports:
- containerPort: 3000
env:
- name: HOMEPAGE_ALLOWED_HOSTS
value: "*"
volumeMounts:
- name: config
mountPath: /app/config
volumes:
- name: config
hostPath:
path: /dogstore/service-data/homepage/config
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: homepage
spec:
selector:
app: homepage
ports:
- port: 3000
targetPort: 3000

View File

@ -0,0 +1,10 @@
domain: ratboo.me
hostIp: "10.0.1.2"
homepage:
image: ghcr.io/gethomepage/homepage:latest
glance:
image: glanceapp/glance
secretName: glance-secrets

5
charts/mealie/Chart.yaml Normal file
View File

@ -0,0 +1,5 @@
apiVersion: v2
name: mealie
description: Mealie recipe manager
version: 0.1.0
type: application

View File

@ -0,0 +1,101 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mealie-data
labels:
app: mealie
spec:
accessModes: [ReadWriteOnce]
storageClassName: {{ .Values.storageClass }}
resources:
requests:
storage: {{ .Values.storageSize }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mealie
labels:
app: mealie
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: mealie
template:
metadata:
labels:
app: mealie
spec:
containers:
- name: mealie
image: {{ .Values.image }}
ports:
- containerPort: 9000
resources:
limits:
memory: {{ .Values.resources.limits.memory }}
env:
- name: PUID
value: {{ .Values.puid | quote }}
- name: PGID
value: {{ .Values.pgid | quote }}
- name: TZ
value: {{ .Values.tz | quote }}
- name: MAX_WORKERS
value: "1"
- name: WEB_CONCURRENCY
value: "1"
- name: ALLOW_SIGNUP
value: "false"
- name: BASE_URL
value: https://mealie.{{ .Values.domain }}
- name: OPENAI_BASE_URL
value: {{ .Values.ai.baseUrl }}
- name: OPENAI_MODEL
value: {{ .Values.ai.model }}
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.secretName }}
key: OPENAI_API_KEY
volumeMounts:
- name: data
mountPath: /app/data
volumes:
- name: data
persistentVolumeClaim:
claimName: mealie-data
---
apiVersion: v1
kind: Service
metadata:
name: mealie
spec:
selector:
app: mealie
ports:
- port: 9000
targetPort: 9000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mealie
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls.certresolver: {{ .Values.certResolver }}
spec:
rules:
- host: mealie.{{ .Values.domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mealie
port:
number: 9000

19
charts/mealie/values.yaml Normal file
View File

@ -0,0 +1,19 @@
domain: ratboo.me
certResolver: myresolver
tz: America/Los_Angeles
puid: "1000"
pgid: "1000"
image: ghcr.io/mealie-recipes/mealie:v3.14.0
secretName: mealie-secrets
storageClass: longhorn
storageSize: 5Gi
resources:
limits:
memory: 1000Mi
ai:
baseUrl: https://generativelanguage.googleapis.com/v1beta
model: gemini-2.0-flash

5
charts/media/Chart.yaml Normal file
View File

@ -0,0 +1,5 @@
apiVersion: v2
name: media
description: Media stack -- Plex, Sonarr, Radarr, Bazarr, Prowlarr, qBittorrent, unpackerr
version: 0.1.0
type: application

View File

@ -0,0 +1,13 @@
{{- define "media.labels" -}}
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/part-of: media
{{- end -}}
{{- define "media.commonEnv" -}}
- name: PUID
value: {{ .Values.puid | quote }}
- name: PGID
value: {{ .Values.pgid | quote }}
- name: TZ
value: {{ .Values.tz | quote }}
{{- end -}}

View File

@ -0,0 +1,72 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: bazarr-config
labels:
app: bazarr
{{- include "media.labels" . | nindent 4 }}
spec:
accessModes: [ReadWriteOnce]
storageClassName: {{ .Values.storageClass }}
resources:
requests:
storage: {{ .Values.bazarr.configSize }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bazarr
labels:
app: bazarr
{{- include "media.labels" . | nindent 4 }}
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: bazarr
template:
metadata:
labels:
app: bazarr
spec:
containers:
- name: bazarr
image: {{ .Values.bazarr.image }}
ports:
- containerPort: 6767
env:
{{- include "media.commonEnv" . | nindent 12 }}
volumeMounts:
- name: config
mountPath: /config
- name: movies
mountPath: /movies
- name: tv
mountPath: /tv
volumes:
- name: config
persistentVolumeClaim:
claimName: bazarr-config
- name: movies
hostPath:
path: {{ .Values.dogstore }}/sonarr/data/radarr-library
type: DirectoryOrCreate
- name: tv
hostPath:
path: {{ .Values.dogstore }}/sonarr/data/library
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: bazarr
labels:
app: bazarr
spec:
selector:
app: bazarr
ports:
- port: 6767
targetPort: 6767

View File

@ -0,0 +1,108 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: plex-config
labels:
app: plex
{{- include "media.labels" . | nindent 4 }}
spec:
accessModes: [ReadWriteOnce]
storageClassName: {{ .Values.storageClass }}
resources:
requests:
storage: {{ .Values.plex.configSize }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: plex
labels:
app: plex
{{- include "media.labels" . | nindent 4 }}
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: plex
template:
metadata:
labels:
app: plex
spec:
containers:
- name: plex
image: {{ .Values.plex.image }}
ports:
- containerPort: 32400
protocol: TCP
env:
{{- include "media.commonEnv" . | nindent 12 }}
- name: PLEX_CLAIM
valueFrom:
secretKeyRef:
name: {{ .Values.secretName }}
key: PLEX_CLAIM
- name: ADVERTISE_IP
value: {{ .Values.plex.advertiseIp | quote }}
- name: VERSION
value: docker
- name: NVIDIA_VISIBLE_DEVICES
value: all
- name: NVIDIA_DRIVER_CAPABILITIES
value: compute,video,utility
volumeMounts:
- name: config
mountPath: /config
- name: transcode
mountPath: /transcode
- name: data
mountPath: /data
volumes:
- name: config
persistentVolumeClaim:
claimName: plex-config
- name: transcode
hostPath:
path: {{ .Values.dogstore }}/plex/transcode
type: DirectoryOrCreate
- name: data
hostPath:
path: {{ .Values.dogstore }}
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: plex
labels:
app: plex
spec:
selector:
app: plex
ports:
- name: web
port: 32400
targetPort: 32400
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: plex
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls.certresolver: {{ .Values.certResolver }}
spec:
rules:
- host: plex.{{ .Values.domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: plex
port:
number: 32400

View File

@ -0,0 +1,60 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: prowlarr-config
labels:
app: prowlarr
{{- include "media.labels" . | nindent 4 }}
spec:
accessModes: [ReadWriteOnce]
storageClassName: {{ .Values.storageClass }}
resources:
requests:
storage: {{ .Values.prowlarr.configSize }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: prowlarr
labels:
app: prowlarr
{{- include "media.labels" . | nindent 4 }}
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: prowlarr
template:
metadata:
labels:
app: prowlarr
spec:
containers:
- name: prowlarr
image: {{ .Values.prowlarr.image }}
ports:
- containerPort: 9696
env:
{{- include "media.commonEnv" . | nindent 12 }}
volumeMounts:
- name: config
mountPath: /config
volumes:
- name: config
persistentVolumeClaim:
claimName: prowlarr-config
---
apiVersion: v1
kind: Service
metadata:
name: prowlarr
labels:
app: prowlarr
spec:
selector:
app: prowlarr
ports:
- port: 9696
targetPort: 9696

View File

@ -0,0 +1,82 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: qbittorrent-config
labels:
app: qbittorrent
{{- include "media.labels" . | nindent 4 }}
spec:
accessModes: [ReadWriteOnce]
storageClassName: {{ .Values.storageClass }}
resources:
requests:
storage: {{ .Values.qbittorrent.configSize }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: qbittorrent
labels:
app: qbittorrent
{{- include "media.labels" . | nindent 4 }}
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: qbittorrent
template:
metadata:
labels:
app: qbittorrent
spec:
containers:
- name: qbittorrent
image: {{ .Values.qbittorrent.image }}
ports:
- containerPort: {{ .Values.qbittorrent.webuiPort }}
protocol: TCP
- containerPort: 34034
protocol: TCP
- containerPort: 34034
protocol: UDP
env:
{{- include "media.commonEnv" . | nindent 12 }}
- name: WEBUI_PORTS
value: "{{ .Values.qbittorrent.webuiPort }}/tcp"
volumeMounts:
- name: config
mountPath: /config
- name: data
mountPath: /data
volumes:
- name: config
persistentVolumeClaim:
claimName: qbittorrent-config
- name: data
hostPath:
path: {{ .Values.dogstore }}/sonarr/data
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: qbittorrent
labels:
app: qbittorrent
spec:
selector:
app: qbittorrent
ports:
- name: webui
port: {{ .Values.qbittorrent.webuiPort }}
targetPort: {{ .Values.qbittorrent.webuiPort }}
- name: bt-tcp
port: 34034
targetPort: 34034
protocol: TCP
- name: bt-udp
port: 34034
targetPort: 34034
protocol: UDP

View File

@ -0,0 +1,86 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: radarr-config
labels:
app: radarr
{{- include "media.labels" . | nindent 4 }}
spec:
accessModes: [ReadWriteOnce]
storageClassName: {{ .Values.storageClass }}
resources:
requests:
storage: {{ .Values.radarr.configSize }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: radarr
labels:
app: radarr
{{- include "media.labels" . | nindent 4 }}
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: radarr
template:
metadata:
labels:
app: radarr
spec:
containers:
- name: radarr
image: {{ .Values.radarr.image }}
ports:
- containerPort: 7878
env:
{{- include "media.commonEnv" . | nindent 12 }}
volumeMounts:
- name: config
mountPath: /config
- name: data
mountPath: /data
volumes:
- name: config
persistentVolumeClaim:
claimName: radarr-config
- name: data
hostPath:
path: {{ .Values.dogstore }}/sonarr/data
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: radarr
labels:
app: radarr
spec:
selector:
app: radarr
ports:
- port: 7878
targetPort: 7878
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: radarr
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls.certresolver: {{ .Values.certResolver }}
spec:
rules:
- host: radarr.{{ .Values.domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: radarr
port:
number: 7878

View File

@ -0,0 +1,86 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sonarr-config
labels:
app: sonarr
{{- include "media.labels" . | nindent 4 }}
spec:
accessModes: [ReadWriteOnce]
storageClassName: {{ .Values.storageClass }}
resources:
requests:
storage: {{ .Values.sonarr.configSize }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sonarr
labels:
app: sonarr
{{- include "media.labels" . | nindent 4 }}
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: sonarr
template:
metadata:
labels:
app: sonarr
spec:
containers:
- name: sonarr
image: {{ .Values.sonarr.image }}
ports:
- containerPort: 8989
env:
{{- include "media.commonEnv" . | nindent 12 }}
volumeMounts:
- name: config
mountPath: /config
- name: data
mountPath: /data
volumes:
- name: config
persistentVolumeClaim:
claimName: sonarr-config
- name: data
hostPath:
path: {{ .Values.dogstore }}/sonarr/data
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: sonarr
labels:
app: sonarr
spec:
selector:
app: sonarr
ports:
- port: 8989
targetPort: 8989
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: sonarr
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls.certresolver: {{ .Values.certResolver }}
spec:
rules:
- host: sonarr.{{ .Values.domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: sonarr
port:
number: 8989

View File

@ -0,0 +1,93 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: unpackerr
labels:
app: unpackerr
{{- include "media.labels" . | nindent 4 }}
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: unpackerr
template:
metadata:
labels:
app: unpackerr
spec:
containers:
- name: unpackerr
image: {{ .Values.unpackerr.image }}
env:
- name: UN_DEBUG
value: "false"
- name: UN_LOG_FILE
value: /logs/log.txt
- name: UN_LOG_FILES
value: "10"
- name: UN_LOG_FILE_MB
value: "10"
- name: UN_INTERVAL
value: 1m
- name: UN_START_DELAY
value: 1m
- name: UN_RETRY_DELAY
value: 5m
- name: UN_MAX_RETRIES
value: "3"
- name: UN_PARALLEL
value: "1"
- name: UN_FILE_MODE
value: "0644"
- name: UN_DIR_MODE
value: "0755"
- name: UN_SONARR_0_URL
value: http://sonarr:8989
- name: UN_SONARR_0_API_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.secretName }}
key: SONARR_API_KEY
- name: UN_SONARR_0_PATHS_0
value: /data/library
- name: UN_SONARR_0_PROTOCOLS
value: torrent
- name: UN_SONARR_0_TIMEOUT
value: 10s
- name: UN_SONARR_0_DELETE_ORIG
value: "false"
- name: UN_SONARR_0_DELETE_DELAY
value: 5m
- name: UN_RADARR_0_URL
value: http://radarr:7878
- name: UN_RADARR_0_API_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.secretName }}
key: RADARR_API_KEY
- name: UN_RADARR_0_PATHS_0
value: /data/radarr-library
- name: UN_RADARR_0_PROTOCOLS
value: torrent
- name: UN_RADARR_0_TIMEOUT
value: 10s
- name: UN_RADARR_0_DELETE_ORIG
value: "false"
- name: UN_RADARR_0_DELETE_DELAY
value: 5m
volumeMounts:
- name: data
mountPath: /data
- name: logs
mountPath: /logs
volumes:
- name: data
hostPath:
path: {{ .Values.dogstore }}/sonarr/data
type: DirectoryOrCreate
- name: logs
hostPath:
path: {{ .Values.dogstore }}/logs/unpackerr
type: DirectoryOrCreate

41
charts/media/values.yaml Normal file
View File

@ -0,0 +1,41 @@
domain: ratboo.me
certResolver: myresolver
tz: America/Los_Angeles
puid: "1000"
pgid: "1000"
dogstore: /dogstore
secretName: media-secrets
storageClass: longhorn
configStorageSize: 2Gi
plex:
image: plexinc/pms-docker:latest
advertiseIp: "https://plex.ratboo.me:443"
configSize: 20Gi
sonarr:
image: ghcr.io/hotio/sonarr:latest
configSize: 2Gi
radarr:
image: ghcr.io/hotio/radarr:latest
configSize: 2Gi
bazarr:
image: lscr.io/linuxserver/bazarr:latest
configSize: 1Gi
prowlarr:
image: ghcr.io/hotio/prowlarr:latest
configSize: 1Gi
qbittorrent:
image: ghcr.io/hotio/qbittorrent:latest
configSize: 1Gi
webuiPort: 9191
unpackerr:
image: golift/unpackerr

View File

@ -0,0 +1,6 @@
dependencies:
- name: metallb
repository: https://metallb.github.io/metallb
version: 0.15.3
digest: sha256:e449dd234fcbd51b21a6e63f8725dc609ab762d41db733ea7302ec44a1fb52f0
generated: "2026-04-19T18:30:19.754244737-04:00"

View File

@ -0,0 +1,9 @@
apiVersion: v2
name: metallb
description: MetalLB load balancer for internal services
version: 0.1.0
type: application
dependencies:
- name: metallb
version: "0.15.3"
repository: "https://metallb.github.io/metallb"

Binary file not shown.

View File

@ -0,0 +1,19 @@
{{- if .Values.pool.enabled }}
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: {{ .Values.pool.name }}
spec:
addresses:
{{- range .Values.pool.addresses }}
- {{ . }}
{{- end }}
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: {{ .Values.pool.name }}
spec:
ipAddressPools:
- {{ .Values.pool.name }}
{{- end }}

View File

@ -0,0 +1,13 @@
metallb:
controller:
nodeSelector:
node-role.kubernetes.io/control-plane: "true"
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
pool:
enabled: false
name: internal
addresses:
- "10.0.1.250-10.0.1.250"

View File

@ -0,0 +1,5 @@
apiVersion: v2
name: paperless
description: Paperless-ngx with Redis broker and PostgreSQL database
version: 0.1.0
type: application

View File

@ -0,0 +1,64 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: paperless-postgres-data
labels:
app: paperless-postgres
spec:
accessModes: [ReadWriteOnce]
storageClassName: {{ .Values.storageClass }}
resources:
requests:
storage: {{ .Values.postgres.storageSize }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: paperless-postgres
labels:
app: paperless-postgres
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: paperless-postgres
template:
metadata:
labels:
app: paperless-postgres
spec:
containers:
- name: postgres
image: {{ .Values.postgres.image }}
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: {{ .Values.postgres.database }}
- name: POSTGRES_USER
value: {{ .Values.postgres.user }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.secretName }}
key: POSTGRES_PASSWORD
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: paperless-postgres-data
---
apiVersion: v1
kind: Service
metadata:
name: paperless-postgres
spec:
selector:
app: paperless-postgres
ports:
- port: 5432
targetPort: 5432

View File

@ -0,0 +1,54 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: paperless-redis-data
labels:
app: paperless-redis
spec:
accessModes: [ReadWriteOnce]
storageClassName: {{ .Values.storageClass }}
resources:
requests:
storage: {{ .Values.redis.storageSize }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: paperless-redis
labels:
app: paperless-redis
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: paperless-redis
template:
metadata:
labels:
app: paperless-redis
spec:
containers:
- name: redis
image: {{ .Values.redis.image }}
ports:
- containerPort: 6379
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: paperless-redis-data
---
apiVersion: v1
kind: Service
metadata:
name: paperless-redis
spec:
selector:
app: paperless-redis
ports:
- port: 6379
targetPort: 6379

View File

@ -0,0 +1,113 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: paperless-webserver
labels:
app: paperless-webserver
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: paperless-webserver
template:
metadata:
labels:
app: paperless-webserver
spec:
securityContext:
runAsUser: {{ int .Values.puid }}
runAsGroup: {{ int .Values.pgid }}
containers:
- name: paperless
image: {{ .Values.webserver.image }}
ports:
- containerPort: 8000
env:
- name: PAPERLESS_REDIS
value: redis://paperless-redis:6379
- name: PAPERLESS_DBHOST
value: paperless-postgres
- name: PAPERLESS_DBPASS
valueFrom:
secretKeyRef:
name: {{ .Values.secretName }}
key: PAPERLESS_DB_PASS
- name: PAPERLESS_CSRF_TRUSTED_ORIGINS
value: {{ .Values.webserver.csrfTrustedOrigins | quote }}
- name: USERMAP_UID
value: {{ .Values.puid | quote }}
- name: USERMAP_GID
value: {{ .Values.pgid | quote }}
livenessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 10
readinessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 20
periodSeconds: 15
timeoutSeconds: 3
volumeMounts:
- name: data
mountPath: /usr/src/paperless/data
- name: media
mountPath: /usr/src/paperless/media
- name: export
mountPath: /usr/src/paperless/export
- name: consume
mountPath: /usr/src/paperless/consume
volumes:
- name: data
hostPath:
path: {{ .Values.dogstore }}/paperless/data
type: DirectoryOrCreate
- name: media
hostPath:
path: {{ .Values.dogstore }}/paperless/media
type: DirectoryOrCreate
- name: export
hostPath:
path: {{ .Values.dogstore }}/paperless/export
type: DirectoryOrCreate
- name: consume
hostPath:
path: {{ .Values.dogstore }}/paperless/consume
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: paperless-webserver
spec:
selector:
app: paperless-webserver
ports:
- port: 8000
targetPort: 8000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: paperless
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls.certresolver: {{ .Values.certResolver }}
spec:
rules:
- host: paperless.{{ .Values.domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: paperless-webserver
port:
number: 8000

View File

@ -0,0 +1,24 @@
domain: ratboo.me
certResolver: myresolver
tz: America/Los_Angeles
puid: "1000"
pgid: "1000"
dogstore: /dogstore
secretName: paperless-secrets
storageClass: longhorn
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
csrfTrustedOrigins: "https://paperless.ratboo.me"
redis:
image: docker.io/library/redis:7
storageSize: 1Gi
postgres:
image: docker.io/library/postgres:15
storageSize: 5Gi
database: paperless
user: paperless

View File

@ -0,0 +1,5 @@
apiVersion: v2
name: traefik-config
description: Traefik ingress configuration for k3s (HelmChartConfig overlay + Cloudflare DNS challenge)
version: 0.1.0
type: application

View File

@ -0,0 +1,41 @@
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
service:
spec:
loadBalancerClass: io.k3s.klipper
logs:
general:
level: WARN
ports:
web:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
tls:
certResolver: {{ .Values.certResolver }}
certResolvers:
{{ .Values.certResolver }}:
acme:
email: {{ .Values.acmeEmail }}
storage: {{ .Values.letsencrypt.storagePath }}
caServer: {{ .Values.acmeServer }}
dnsChallenge:
provider: cloudflare
env:
- name: CF_DNS_API_TOKEN
valueFrom:
secretKeyRef:
name: {{ .Values.cloudflare.secretName }}
key: {{ .Values.cloudflare.secretKey }}
persistence:
enabled: true
storageClass: longhorn
size: 128Mi

View File

@ -0,0 +1,9 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
namespace: kube-system
spec:
redirectScheme:
scheme: https
permanent: true

View File

@ -0,0 +1,12 @@
domain: ratboo.me
acmeEmail: alvin7@gmail.com
certResolver: myresolver
acmeServer: https://acme-v02.api.letsencrypt.org/directory
cloudflare:
secretName: cloudflare-api-token
secretKey: CF_DNS_API_TOKEN
letsencrypt:
storagePath: /data/acme.json

View File

@ -0,0 +1,5 @@
apiVersion: v2
name: traefik-internal
description: Internal Traefik instance for LAN-only services
version: 0.1.0
type: application

View File

@ -0,0 +1,91 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: traefik-internal
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: traefik-internal
rules:
- apiGroups: [""]
resources: [services, endpoints, secrets, nodes]
verbs: [get, list, watch]
- apiGroups: [discovery.k8s.io]
resources: [endpointslices]
verbs: [get, list, watch]
- apiGroups: [traefik.io]
resources: [ingressroutes, ingressroutetcps, ingressrouteudps, middlewares, middlewaretcps, tlsoptions, tlsstores, traefikservices, serverstransports, serverstransporttcps]
verbs: [get, list, watch]
- apiGroups: [traefik.io]
resources: [ingressroutes/status, ingressroutetcps/status, ingressrouteudps/status]
verbs: [update]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: traefik-internal
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: traefik-internal
subjects:
- kind: ServiceAccount
name: traefik-internal
namespace: {{ .Release.Namespace }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: traefik-internal
labels:
app: traefik-internal
spec:
replicas: 1
selector:
matchLabels:
app: traefik-internal
template:
metadata:
labels:
app: traefik-internal
spec:
serviceAccountName: traefik-internal
nodeSelector:
node-role.kubernetes.io/control-plane: "true"
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
containers:
- name: traefik
image: {{ .Values.image }}
args:
- --entrypoints.web.address=:{{ .Values.port }}
- --providers.kubernetescrd
- --providers.kubernetescrd.ingressClass={{ .Values.ingressClass }}
- --api.insecure=true
- --log.level=WARN
ports:
- name: web
containerPort: {{ .Values.port }}
- name: dashboard
containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: traefik-internal
annotations:
metallb.io/address-pool: internal
spec:
type: LoadBalancer
loadBalancerIP: {{ .Values.loadBalancerIP }}
selector:
app: traefik-internal
ports:
- name: web
port: {{ .Values.port }}
targetPort: {{ .Values.port }}
- name: dashboard
port: 9095
targetPort: 8080

View File

@ -0,0 +1,4 @@
image: traefik:v3.3
port: 8082
ingressClass: traefik-internal
loadBalancerIP: "10.0.1.250"

5
charts/utils/Chart.yaml Normal file
View File

@ -0,0 +1,5 @@
apiVersion: v2
name: utils
description: Utility services -- Zerobyte backup and Seerr media requests
version: 0.1.0
type: application

View File

@ -0,0 +1,91 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: seerr-config
labels:
app: seerr
spec:
accessModes: [ReadWriteOnce]
storageClassName: {{ .Values.storageClass }}
resources:
requests:
storage: {{ .Values.seerr.storageSize }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: seerr
labels:
app: seerr
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: seerr
template:
metadata:
labels:
app: seerr
spec:
containers:
- name: seerr
image: {{ .Values.seerr.image }}
ports:
- containerPort: 5055
env:
- name: TZ
value: {{ .Values.tz | quote }}
readinessProbe:
httpGet:
path: /api/v1/settings/public
port: 5055
initialDelaySeconds: 20
periodSeconds: 15
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /api/v1/settings/public
port: 5055
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 3
volumeMounts:
- name: config
mountPath: /app/config
volumes:
- name: config
persistentVolumeClaim:
claimName: seerr-config
---
apiVersion: v1
kind: Service
metadata:
name: seerr
spec:
selector:
app: seerr
ports:
- port: 5055
targetPort: 5055
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: seerr
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls.certresolver: {{ .Values.certResolver }}
spec:
rules:
- host: watch.{{ .Values.domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: seerr
port:
number: 5055

View File

@ -0,0 +1,71 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: zerobyte-data
labels:
app: zerobyte
spec:
accessModes: [ReadWriteOnce]
storageClassName: {{ .Values.storageClass }}
resources:
requests:
storage: {{ .Values.zerobyte.storageSize }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: zerobyte
labels:
app: zerobyte
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: zerobyte
template:
metadata:
labels:
app: zerobyte
spec:
containers:
- name: zerobyte
image: {{ .Values.zerobyte.image }}
ports:
- containerPort: 4096
env:
- name: TZ
value: {{ .Values.tz | quote }}
- name: BASE_URL
value: http://{{ .Values.hostIp }}:4096
- name: APP_SECRET
valueFrom:
secretKeyRef:
name: {{ .Values.zerobyte.secretName }}
key: APP_SECRET
volumeMounts:
- name: data
mountPath: /var/lib/zerobyte
- name: localtime
mountPath: /etc/localtime
readOnly: true
volumes:
- name: data
persistentVolumeClaim:
claimName: zerobyte-data
- name: localtime
hostPath:
path: /etc/localtime
type: File
---
apiVersion: v1
kind: Service
metadata:
name: zerobyte
spec:
selector:
app: zerobyte
ports:
- port: 4096
targetPort: 4096

14
charts/utils/values.yaml Normal file
View File

@ -0,0 +1,14 @@
domain: ratboo.me
certResolver: myresolver
tz: America/Los_Angeles
hostIp: "10.0.1.2"
storageClass: longhorn
zerobyte:
image: ghcr.io/nicotsx/zerobyte:v0.33
storageSize: 1Gi
secretName: zerobyte-secrets
seerr:
image: ghcr.io/seerr-team/seerr:latest
storageSize: 1Gi

144
scripts/bootstrap.sh Executable file
View File

@ -0,0 +1,144 @@
#!/usr/bin/env bash
set -euo pipefail
# ─── Configuration ───────────────────────────────────────────────────────────
K3S_VERSION="v1.31.4+k3s1"
LONGHORN_VERSION="1.7.2"
LONGHORN_REPO="https://charts.longhorn.io"
# ─── Helper ──────────────────────────────────────────────────────────────────
info() { printf '\033[1;34m[INFO]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; }
error() { printf '\033[1;31m[ERROR]\033[0m %s\n' "$*" >&2; exit 1; }
usage() {
cat <<EOF
Usage: $(basename "$0") <command>
Commands:
server Install k3s server (manager node)
agent Install k3s agent (worker node)
longhorn Install Longhorn via Helm
sops-keygen Generate an age keypair for SOPS
apply-secrets Decrypt and apply SOPS secrets to the cluster
deploy Helm-install all charts
all Run: server → longhorn → sops-keygen → apply-secrets → deploy
EOF
}
# ─── k3s server ──────────────────────────────────────────────────────────────
cmd_server() {
info "Installing k3s server ${K3S_VERSION}"
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="${K3S_VERSION}" sh -s - server \
--write-kubeconfig-mode 644
info "Waiting for node to be Ready …"
until kubectl get node "$(hostname)" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null | grep -q True; do
sleep 2
done
info "Server node is Ready."
local token
token=$(cat /var/lib/rancher/k3s/server/node-token)
local ip
ip=$(hostname -I | awk '{print $1}')
info "Worker join command:"
echo " curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=\"${K3S_VERSION}\" K3S_URL=\"https://${ip}:6443\" K3S_TOKEN=\"${token}\" sh -"
}
# ─── k3s agent ───────────────────────────────────────────────────────────────
cmd_agent() {
if [[ -z "${K3S_URL:-}" || -z "${K3S_TOKEN:-}" ]]; then
error "Set K3S_URL and K3S_TOKEN environment variables first.\n K3S_URL=https://<manager-ip>:6443 K3S_TOKEN=<token> $0 agent"
fi
info "Installing k3s agent ${K3S_VERSION}"
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="${K3S_VERSION}" K3S_URL="${K3S_URL}" K3S_TOKEN="${K3S_TOKEN}" sh -
info "Agent installed. It will appear as a node shortly."
}
# ─── Longhorn ────────────────────────────────────────────────────────────────
cmd_longhorn() {
info "Installing Longhorn ${LONGHORN_VERSION} via Helm …"
helm repo add longhorn "${LONGHORN_REPO}" 2>/dev/null || true
helm repo update longhorn
kubectl create namespace longhorn-system 2>/dev/null || true
helm upgrade --install longhorn longhorn/longhorn \
--namespace longhorn-system \
--version "${LONGHORN_VERSION}" \
--set defaultSettings.defaultReplicaCount=2 \
--wait
info "Longhorn installed."
}
# ─── SOPS / age ─────────────────────────────────────────────────────────────
cmd_sops_keygen() {
local keydir="/etc/sops/age"
local keyfile="${keydir}/keys.txt"
if [[ -f "${keyfile}" ]]; then
warn "Age key already exists at ${keyfile} — skipping generation."
else
info "Generating age keypair …"
sudo mkdir -p "${keydir}"
age-keygen -o "${keyfile}" 2>&1
sudo chmod 600 "${keyfile}"
info "Key written to ${keyfile}"
fi
info "Public key (put this in .sops.yaml):"
grep 'public key' "${keyfile}" | awk '{print $NF}'
}
cmd_apply_secrets() {
local secrets_dir
secrets_dir="$(cd "$(dirname "$0")/.." && pwd)/secrets"
info "Decrypting and applying secrets …"
local decrypted
decrypted="$(sops -d "${secrets_dir}/secrets.enc.yaml")"
local ns
for ns in $(printf '%s\n' "${decrypted}" | grep '^\s*namespace:' | awk '{print $2}' | sort -u); do
kubectl create namespace "${ns}" 2>/dev/null || true
done
printf '%s\n' "${decrypted}" | kubectl apply -f -
info "Secrets applied."
}
# ─── Deploy all charts ──────────────────────────────────────────────────────
cmd_deploy() {
local charts_dir
charts_dir="$(cd "$(dirname "$0")/.." && pwd)/charts"
local -a chart_order=(traefik-config media paperless mealie dashboards utils)
local -A chart_ns=(
[traefik-config]=kube-system
[media]=media
[paperless]=paperless
[mealie]=apps
[dashboards]=apps
[utils]=apps
)
for chart in "${chart_order[@]}"; do
local ns="${chart_ns[$chart]}"
info "Deploying ${chart} → namespace ${ns}"
kubectl create namespace "${ns}" 2>/dev/null || true
helm upgrade --install "${chart}" "${charts_dir}/${chart}" \
--namespace "${ns}" \
--wait --timeout 5m
done
info "All charts deployed."
}
# ─── Main ────────────────────────────────────────────────────────────────────
[[ $# -lt 1 ]] && { usage; exit 1; }
case "$1" in
server) cmd_server ;;
agent) cmd_agent ;;
longhorn) cmd_longhorn ;;
sops-keygen) cmd_sops_keygen ;;
apply-secrets) cmd_apply_secrets ;;
deploy) cmd_deploy ;;
all) cmd_server; cmd_longhorn; cmd_sops_keygen; cmd_apply_secrets; cmd_deploy ;;
*) usage; exit 1 ;;
esac

158
secrets/secrets.enc.yaml Normal file
View File

@ -0,0 +1,158 @@
#ENC[AES256_GCM,data:1wyYmxw5bF4pkfLGKuBx7V0KqV9MOj+WEcNkTLX2o8cjN7k/NJg2YhPkDKbxQhalm5RccC3vCCgmaeJzjExt,iv:v/VlwHMwJSHfevf2sN8Sq9dMUdah3cNWitmWR6OUJm8=,tag:Q/M3rkS+NLlHCT0Mnu1TLA==,type:comment]
#ENC[AES256_GCM,data:ZL4tevfMjoN4RMiNz1zQHAiZ3cpcfPKFwirJWPwf7qWf7fd8jWGr1Fr/KuFiIzSkny3n9nC3Vwg=,iv:YKPVLd4zxA7wMFx0jd917W4pHSRtcXP93rcBz/0jyso=,tag:G3Hi0oXtz+/9tE4zRYPUMA==,type:comment]
#ENC[AES256_GCM,data:IQpAXI8AERzfGJSWcnGnBKustu2wkhf7CtehJLk2qhOj9R5X8QoIb7AEfhy0ONi8EpZSfKIQ5AbYom+ZjB9vUiDKeCmtae1RpQVXHVu7jmh9OI212+8VkUyi3PDMN3wH7NIQ1zX8h5PxYFUuKvWgx4hHCYESwgfkW49fSTfwxeeWjSbyNOM=,iv:69fsTOQ5LJ3s4KRwaOb676dAOaujpWx3Ah+q8mlzzf0=,tag:b/nxflYxTCaiInYsb7Szpw==,type:comment]
#ENC[AES256_GCM,data:Rd8TnilJ/5Ak7egTQsvVeXgCx7Gx3EmYMptVWqhtb6hVJ5I0uHNdMlBe1YGCPfic8F0fYfzBlgJaNz1ycfooQfF/lYfBxGx9EARSGznEsg==,iv:0SRR76k5qnbu4j+dMuUf2kLMeZAkerBmej0gR8/Wpfg=,tag:HMaPbx1ExnzRoiq6DlxYIw==,type:comment]
#
#ENC[AES256_GCM,data:y3lu8sygTZJ+TSJOyrFXShJGBu5gHYLT4A==,iv:xmugUAyc116U4nESzKAYcvz76BBGwHLK7HE61gnS2gE=,tag:1LTSMRi+Aat4hk2N7c+2gw==,type:comment]
#ENC[AES256_GCM,data:/ny7h4o0XSF1kROKuEUD9xrZJYOJ7mTZGIgTtIWHdclmdd9vYTLNl2+Hb9kHzCCWUgPXRg6pOww=,iv:47xT9Fct8sJrfIf8qRGD1xUTEkgdGf+KC9hWQ3XJG44=,tag:yix6VVO+g10cLbUwLWdTeA==,type:comment]
#ENC[AES256_GCM,data:du0a4InEiMTw4mg5EqVPpANM3j0FNpDSle2na/7Q0vM1Z4SE52Jvvz3ZIV99b1pY30QlFdnTGO/4grVJd7xiUYC5wCUNY/uGNYGX2HzOWZjByEb5p9VCOPS2Czl/pPGM9+9d08mn2u1Ww4Wt6a/hrFOUWQddjTNz7HwG66xT/YVTnCN++mTnkCbgQKIjV1YhpSQKWA5jNfgSqSVKwbEA60JvESavT51HSZnrAGKWIepaJc7S14jTp+tUdqA=,iv:SZKsksvhtXCcKByjV85stfmt/DGHSsSh+PTGuCfe874=,tag:IQWtYVrLFx8EfPtKb1tK9g==,type:comment]
apiVersion: ENC[AES256_GCM,data:3lY=,iv:MBpXRZ3rElkVBxAlvxmzORMCq0G87jB3Ik11tWMfz64=,tag:oPPwCxOiK3ePCWRhtHmN2w==,type:str]
kind: ENC[AES256_GCM,data:8+Toxlr5,iv:DTaS/GEKGLtZsHQBYTGaQZYSp9mr/A5Vbuqi7uq46rU=,tag:1YvMaF1/eLNkZv0F3Du0zA==,type:str]
metadata:
name: ENC[AES256_GCM,data:RgUAwk6jY7DHv+CbTtyXx5nzEQQ=,iv:FeAx14K1od0ehMnbcpnD4FW1bpEF/3M+dq8NGoJwTJA=,tag:SziXyyeAgShOgsandPQD7A==,type:str]
namespace: ENC[AES256_GCM,data:IgxkBPTfQ8rEWzA=,iv:56aP5rBeH9aupBi338/9r4fXZ1ova8ubO7N8elr2/qM=,tag:vfFTXJrQ2J5vln/rBGl/og==,type:str]
type: ENC[AES256_GCM,data:Efo/5l3N,iv:moQ775VrJkfQFO3YPM3ZT77i0IzcWy/G7b5E4/XOyJY=,tag:zfEMZgXgO+qMQIvPrLeMsA==,type:str]
stringData:
CF_DNS_API_TOKEN: ENC[AES256_GCM,data:74wQ12RQ9M7BckVfPjbRUnEbeaFB0aOPIhZHrFo=,iv:SIS0dJBpXkcJLjqEGnM1mtD1CB76MN2rZObyG8bCVe4=,tag:Rac0lPa5J2JSkdKcVc1PtQ==,type:str]
sops:
age:
- recipient: age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWFcrTlYrOW1Tb0xGUmVS
Q3VPb3VPMS9hRWQ4aDQzRGtFTXp4SU90YWlrCng5NkN3TUFEUGIrWkRCK1NMeVND
Z2RwV0JKVnRTMWUvWlpDRzhBQWtsNVkKLS0tIHZ2NkZaVTJSaE1vTjVVMXhzTmYz
eGZTZ0VSUElFZVpqWlVISjNYdnA4UFUK/uOyj7CKU0XLHHdPNKByO2c56JWQfhk5
oauimeYrkNE+06dhXgVcJiQH+HcB33tB9u3YS9LxFYs3R98zKAHG6g==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-19T21:05:51Z"
mac: ENC[AES256_GCM,data:GQx4YuPIIfZxRpWtHCa8pCtidtmdYoIdMsK0dQJO42XT9KR5lbwNaP3v4GoaDoeM+L7iAn2OprpE54KkwIwRfb3NAjTeUvXO+J/Yi4ZJnLtuTOlAZrC8YvjmiZ6DaL8pvpRz7VUCfzNOoyrbjSJ2Qv/PWAUENcDEU7yOHNv7RBA=,iv:FqYd2F4vBqSCeKPsrWY/a8RePgkU2aP9cadB6nMyWaQ=,tag:FC75r3bMOQDZZwO0qJi4xg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.2
---
#ENC[AES256_GCM,data:ibx/1sx8LotfzeKT/rRsedIOoYD+F6aXbrA3QEqjfSrFAXWchGiLX9ddAY0q0DOGi7tFcty1rwWUj1Iiy+vd88zZDRhLSVgs0hIdqqPaHVubOWA7gAmxvZdb1faLU4cOAe7gL9bUdaBlGzQKat6miytnWBXL8qidoFl/Ps1wNNDXRBpe+TsQfVAp5T5jirq35s7GTeXUbQO3bNkKx/ZNHmV7ASctKeRv5TLuAJIwyU6UIGSSnx6Sdr43mZlm6iJjdoT6jQxeH7yB1Gut7g==,iv:wAtBwQNTI56JLHISsA1W00M3X+v6R6vEkNK1SVULsTg=,tag:Jk/4OdnXyV6/5VwFcXpX/A==,type:comment]
apiVersion: ENC[AES256_GCM,data:jNM=,iv:dt+kXGEjK6yS09KRE5I3uAXNLGN05RbO0GyjNTHYHtk=,tag:7z93Yh4m2SpZLHdxVIr1hw==,type:str]
kind: ENC[AES256_GCM,data:ORSVG3Tn,iv:gqhsNppUalHTDcKa7q/P3TR5t4VZC1gLc2MgY8V6xEw=,tag:vr764iOVWPJujZd4rxRYKA==,type:str]
metadata:
name: ENC[AES256_GCM,data:1ngffZeDVnrLbux2pg==,iv:OXYS/A5PBvmIVw5qDu+Um7J/JhM8shVfP3KDIXNBCU0=,tag:wfmayiCBy22O+WIPBC3PPQ==,type:str]
namespace: ENC[AES256_GCM,data:xCM+W3I=,iv:0KLfoYx8Q+NVsm84KZX3tClbWdwuTpI6/pD7HQMK3Fk=,tag:hSFwqHfnHWOPvesUhWyo+Q==,type:str]
type: ENC[AES256_GCM,data:K3TlGbY8,iv:ZKm+PyQ4IAP0K1ymcMvWPOEUgUGzH93UkmH+rheJk7A=,tag:8tMEVI895tOOt8/tjiKRuw==,type:str]
stringData:
PLEX_CLAIM: ENC[AES256_GCM,data:A+dwGU//qfy6sBoww5c36xs2WcwdbQY=,iv:S6wOadxaC4ITyZN/7u5Lcu8AeBFdQLRqRlCHH3oSj28=,tag:hlNKdZGzURuFgcSrUrZyJA==,type:str]
SONARR_API_KEY: ENC[AES256_GCM,data:VvU91ZNxv6tauBEyK9j8THxT8zw0mTLEmTTSRgYDEek=,iv:DZr5JL9T4f7XwQ03jkwVKfWA/xyAZlo4f/BKAzIIsQo=,tag:mCh8ExJvuoltcMlzAk6jpw==,type:str]
RADARR_API_KEY: ENC[AES256_GCM,data:UJr87uk6V8sYT3kIp8dzevE8xA55QN4Aop6nP3uMBbc=,iv:Z0NicMtVOBxEwXtcBjqfMZKuTrPjqi3H1vIYu6llHHo=,tag:Xz88jgAwwK/ekh+8udlCjQ==,type:str]
sops:
age:
- recipient: age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWFcrTlYrOW1Tb0xGUmVS
Q3VPb3VPMS9hRWQ4aDQzRGtFTXp4SU90YWlrCng5NkN3TUFEUGIrWkRCK1NMeVND
Z2RwV0JKVnRTMWUvWlpDRzhBQWtsNVkKLS0tIHZ2NkZaVTJSaE1vTjVVMXhzTmYz
eGZTZ0VSUElFZVpqWlVISjNYdnA4UFUK/uOyj7CKU0XLHHdPNKByO2c56JWQfhk5
oauimeYrkNE+06dhXgVcJiQH+HcB33tB9u3YS9LxFYs3R98zKAHG6g==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-19T21:05:51Z"
mac: ENC[AES256_GCM,data:GQx4YuPIIfZxRpWtHCa8pCtidtmdYoIdMsK0dQJO42XT9KR5lbwNaP3v4GoaDoeM+L7iAn2OprpE54KkwIwRfb3NAjTeUvXO+J/Yi4ZJnLtuTOlAZrC8YvjmiZ6DaL8pvpRz7VUCfzNOoyrbjSJ2Qv/PWAUENcDEU7yOHNv7RBA=,iv:FqYd2F4vBqSCeKPsrWY/a8RePgkU2aP9cadB6nMyWaQ=,tag:FC75r3bMOQDZZwO0qJi4xg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.2
---
#ENC[AES256_GCM,data:rmGnwFT8rj+qcuyiuf4VdY5KJP5m4bMt9a1znHpQ+uYyrfzNoAH0eqn6yHMB3xAwFJtFOkzEdd0E1PaL5C30UbqtpwDNyzD0QYipIwDkQBGwRnkXXI8xw1ftE1zHcFWnPmnSH70hCJa+/Xu425pIyJeVlJhUqoOGkG5/gV0DQoVMnJXFXnh5a64Ggv7UzB92o9/iyUFMgi/7j4ePNQCc7Um1VePXc5mkRl8cQBbvwAgyMZo+IxDJ8hpS2HgbRGey5/RAxaDvRbwkyRU=,iv:Cl77wg8+J3D6QrIgbjRS/lzbJLf2qGkK/AtnIOoKG+E=,tag:m4TfMvHm5W612rrJJ+ytkA==,type:comment]
apiVersion: ENC[AES256_GCM,data:fyA=,iv:lkH+XfaHDNUNE3+oUW5lkA3ev0AEQZGe2y/J5H5G4AI=,tag:QCGr4Ia7Ea+CRWWoWSFaug==,type:str]
kind: ENC[AES256_GCM,data:GS9uiEv6,iv:9VAdvjoF7thUVtJRpyaDnBOVlZ9so1p5f4iaw1WxJ0w=,tag:gDCWZfn4zCdyRdMup9Vs3Q==,type:str]
metadata:
name: ENC[AES256_GCM,data:XLV37KdISRoLUp4yo44=,iv:Hb2AQVYhhu5erfg+41edOfB4cT6O6e2k1ytEixVaZDk=,tag:2qP4C7sxSdkdnsTVRhsXcg==,type:str]
namespace: ENC[AES256_GCM,data:S/sYVQ==,iv:SuUw6GPbT7YF66+O2w8al89NcJBr3oi8C12nZIbNWWc=,tag:NBVJrXnAhVJqx325MQnOTQ==,type:str]
type: ENC[AES256_GCM,data:suP66pVq,iv:RrLuX+MOixEaR3iw4EMBCqkT03TW3xvmzdXsTf8kl2c=,tag:Id8xGXmFcmN+gYyZsohp3g==,type:str]
stringData:
OPENAI_API_KEY: ENC[AES256_GCM,data:1QtOURL+C0DcB4AQhvmQP2PqeNQm2VQWTr/alY4FQzbhCnpxSFLS,iv:hy+a7cAcUNYr1XkKK1JoG8imjh5kT7B2tr5FWfHgAds=,tag:72zQFszAQpPEzZ0v24V5yg==,type:str]
sops:
age:
- recipient: age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWFcrTlYrOW1Tb0xGUmVS
Q3VPb3VPMS9hRWQ4aDQzRGtFTXp4SU90YWlrCng5NkN3TUFEUGIrWkRCK1NMeVND
Z2RwV0JKVnRTMWUvWlpDRzhBQWtsNVkKLS0tIHZ2NkZaVTJSaE1vTjVVMXhzTmYz
eGZTZ0VSUElFZVpqWlVISjNYdnA4UFUK/uOyj7CKU0XLHHdPNKByO2c56JWQfhk5
oauimeYrkNE+06dhXgVcJiQH+HcB33tB9u3YS9LxFYs3R98zKAHG6g==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-19T21:05:51Z"
mac: ENC[AES256_GCM,data:GQx4YuPIIfZxRpWtHCa8pCtidtmdYoIdMsK0dQJO42XT9KR5lbwNaP3v4GoaDoeM+L7iAn2OprpE54KkwIwRfb3NAjTeUvXO+J/Yi4ZJnLtuTOlAZrC8YvjmiZ6DaL8pvpRz7VUCfzNOoyrbjSJ2Qv/PWAUENcDEU7yOHNv7RBA=,iv:FqYd2F4vBqSCeKPsrWY/a8RePgkU2aP9cadB6nMyWaQ=,tag:FC75r3bMOQDZZwO0qJi4xg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.2
---
#ENC[AES256_GCM,data:3I2e5Uv2dvWtDWXnVh0AULaS9z2Pcd/iUHjvycTsfxhDpzEJfc0a0dISctuLpELEyBv1PtYemFZ/XeTqGJswTF4vvzM7UL6scPezIm674aOtjVqPj7/C3nQgfnuY1qgVY7duawWZeaozf8hBKef1JJ1qR1T3Mati3YBIaw3fp1qjYyKo47/F19UT/qtL6mlu8CE3/zIVbzZGsKwfqhYf6oLg2I7aNlvJ0/yWz8RSjwmtWj8WzUD1wZh+DnFRiTlbE+kLdgw=,iv:i+4OhA58TOv1pzPInqu9qi9zunYzpNbvHrELXVsXis8=,tag:XJyBP8nci2u5X7THGox3CQ==,type:comment]
apiVersion: ENC[AES256_GCM,data:RdI=,iv:YlI7VI5Tk6f99ZhiJEI/LaGgmYejt6/8k0wo+n8G19E=,tag:WY473Ft6vXo0NkpGlnGq/w==,type:str]
kind: ENC[AES256_GCM,data:5fh5Zy1D,iv:1qApXn+j2LFNs1fzrH8j6M84espcQz4cHwquEmsHDSU=,tag:MnWyue/omxF6Emrpsz9eEw==,type:str]
metadata:
name: ENC[AES256_GCM,data:ZYLasrZQ1Bu0jcLbRQjmjCo=,iv:O0uBw2j+X0CPaUkYQvnTVRgl9nNZBDF058/hZB/WFwY=,tag:xuwCgyi5VT9enQ3HlYzW1g==,type:str]
namespace: ENC[AES256_GCM,data:Xr9+LDzWK7z+,iv:6iXdpkcUho23jU2BZLHKkxONTtQIYkZem9NVlyB/Ltk=,tag:3wV3EMtmqXr0yTUcfbqxCA==,type:str]
type: ENC[AES256_GCM,data:+5bc8NF8,iv:JpVUvDJZdk++m1K5pyuPTF1p17X62Mlwv6GKK9Hqoz8=,tag:Kdihg3qXXHD0ieypkcvWSg==,type:str]
stringData:
PAPERLESS_DB_PASS: ENC[AES256_GCM,data:5/oF170q34nO,iv:y09u1KOZaIoUNtDog0sEbkj5gKD8C3JQyvNDSy0ElkI=,tag:WvIMb0Z0l7XXC6wB59ZTtg==,type:str]
sops:
age:
- recipient: age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWFcrTlYrOW1Tb0xGUmVS
Q3VPb3VPMS9hRWQ4aDQzRGtFTXp4SU90YWlrCng5NkN3TUFEUGIrWkRCK1NMeVND
Z2RwV0JKVnRTMWUvWlpDRzhBQWtsNVkKLS0tIHZ2NkZaVTJSaE1vTjVVMXhzTmYz
eGZTZ0VSUElFZVpqWlVISjNYdnA4UFUK/uOyj7CKU0XLHHdPNKByO2c56JWQfhk5
oauimeYrkNE+06dhXgVcJiQH+HcB33tB9u3YS9LxFYs3R98zKAHG6g==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-19T21:05:51Z"
mac: ENC[AES256_GCM,data:GQx4YuPIIfZxRpWtHCa8pCtidtmdYoIdMsK0dQJO42XT9KR5lbwNaP3v4GoaDoeM+L7iAn2OprpE54KkwIwRfb3NAjTeUvXO+J/Yi4ZJnLtuTOlAZrC8YvjmiZ6DaL8pvpRz7VUCfzNOoyrbjSJ2Qv/PWAUENcDEU7yOHNv7RBA=,iv:FqYd2F4vBqSCeKPsrWY/a8RePgkU2aP9cadB6nMyWaQ=,tag:FC75r3bMOQDZZwO0qJi4xg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.2
---
#ENC[AES256_GCM,data:txDOIoubztyTgbgUC4MMNxNIwDkjtF/m+H5YAZ6hdaHTCBjVZSVKhHjYJzLFPeHE2EL02SEgJaXSkyF0bWwWw/QfR/yCkiGPdZ0fAFDKtJJBtITkM4O5rwRCTEXsQXleWK5NM6M55s5ElWIlBE//uLt2+hPlDQv26EOwwAa9I7kW2bUPDGvYNJVc7mJSfjMQb6aOx1MzrA+PUryeBVdK0xdmBmiQaZyGC0MexEjBwAycVnCM,iv:u4rMeGWy1J779Z7JNFvWTsxDREbTWAS+bXFxQhPTyGA=,tag:mVoS7lSVAHcqPcQ6iqhjvA==,type:comment]
apiVersion: ENC[AES256_GCM,data:JGU=,iv:vr1Lkm9BBG2u8Ay0PGAIMTYUUKhMHIho3mVP1lY6it0=,tag:12JH+n4nUsYqmiy1+CnwfA==,type:str]
kind: ENC[AES256_GCM,data:VLdoeXnN,iv:eH+/KpTMtpclAxFnjX5mXxkF73HUMJlskNSS/iW+g+U=,tag:NE/Xdc9qsfFmXAyZ4y+3HQ==,type:str]
metadata:
name: ENC[AES256_GCM,data:dKvmrGqwCK+KvfdBxcs=,iv:3B71qX/mVAJGMcKlxTrnfub3rp/o7PJ+mquxo+V6svY=,tag:gnvbDBvZexdbOHkQQNUInw==,type:str]
namespace: ENC[AES256_GCM,data:93lKXQ==,iv:TQq2ZC7l1uQBc0FNRg6sQRfLuLIokQHpgAzRTcFmGsE=,tag:Mv+ih+YMDfYIihWNgnztow==,type:str]
type: ENC[AES256_GCM,data:Ep25FUNF,iv:ZCzL99uhG0SYRXlu9j3GmgWtIxcfe6C9lEkP5EFr6SY=,tag:ANEtn7vXJisqoUlX7rEBAQ==,type:str]
stringData:
ADGUARD_PASSWORD: ENC[AES256_GCM,data:Keh2GHhvfSyp9Q==,iv:bJ2CdmjqMZUSVw2T1jerqT1gkiP6k+aL9VyGCVJ10wI=,tag:cfR6jRn6NyrZ3/2WM5SdKg==,type:str]
sops:
age:
- recipient: age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWFcrTlYrOW1Tb0xGUmVS
Q3VPb3VPMS9hRWQ4aDQzRGtFTXp4SU90YWlrCng5NkN3TUFEUGIrWkRCK1NMeVND
Z2RwV0JKVnRTMWUvWlpDRzhBQWtsNVkKLS0tIHZ2NkZaVTJSaE1vTjVVMXhzTmYz
eGZTZ0VSUElFZVpqWlVISjNYdnA4UFUK/uOyj7CKU0XLHHdPNKByO2c56JWQfhk5
oauimeYrkNE+06dhXgVcJiQH+HcB33tB9u3YS9LxFYs3R98zKAHG6g==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-19T21:05:51Z"
mac: ENC[AES256_GCM,data:GQx4YuPIIfZxRpWtHCa8pCtidtmdYoIdMsK0dQJO42XT9KR5lbwNaP3v4GoaDoeM+L7iAn2OprpE54KkwIwRfb3NAjTeUvXO+J/Yi4ZJnLtuTOlAZrC8YvjmiZ6DaL8pvpRz7VUCfzNOoyrbjSJ2Qv/PWAUENcDEU7yOHNv7RBA=,iv:FqYd2F4vBqSCeKPsrWY/a8RePgkU2aP9cadB6nMyWaQ=,tag:FC75r3bMOQDZZwO0qJi4xg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.2
---
#ENC[AES256_GCM,data:CXcPJbTR4tVVRGK6l0NBowzab2DHaufhxX++4dQ9/S3Q9V0Me/kA0xBoR4PlfFRVG6QgsIeEK1ysx3d5qOy6/VcS9HFibUv8lTx0X8EonLNgsc2Qc4V526eJ3tByTyqRw780YsTVZFHZ+ofJBNPAcB4Rls1G8214E/kfyWxKWdHB7Joy83SQeWgL6On2oEdeWnpyp+E7FTqk2SXR+/YLVnNbixeirjpRCWlcnPXxhoSlRzcxUmAWTPT8zWicoUo5AiSh2C/DNw==,iv:lZ1ffJ4ixyhY829s2EiJlcF18fK1RDSUZ1F1Lw+YG14=,tag:dfn5m/iYHvzO7BE+ZygMMw==,type:comment]
apiVersion: ENC[AES256_GCM,data:F6o=,iv:8McPTAtRKlG0wpF1DUXRrhkzNuoD97Vu4OFyI8Opy28=,tag:DgEjMs6yXKFEv6Uu8A8WDg==,type:str]
kind: ENC[AES256_GCM,data:eVSn4ODQ,iv:iDOb7kRnWbW1CYKILAZwbtlhbAqwi/I+YXFbHsmz2KI=,tag:dcWY/PdP2eMRv1HxmfyHoQ==,type:str]
metadata:
name: ENC[AES256_GCM,data:JffgDJUP/W+tue33DF91Ug==,iv:pXq1HZ2azFQSYP1LKYl94q8f1srLOH7GJw1cW6p+yrI=,tag:Dh6BCtR0gxqnb+c20RrKLA==,type:str]
namespace: ENC[AES256_GCM,data:O6kz8Q==,iv:ZMv7m+YLaIChgNTM4Riopt2VUNg5HwUwdLR6bRA1Nf0=,tag:undk4ODEabPJbQKoa1He7A==,type:str]
type: ENC[AES256_GCM,data:YMUJyMI2,iv:o++4jFOch8C8g5iKCzot/AcHnERRO/Yqn/uHuCAIFEI=,tag:ReJgAAajctyGo7xYr2Yc8w==,type:str]
stringData:
ZEROBYTE_APP_SECRET: ENC[AES256_GCM,data:cqkj4VGIbbV9pvkZf+GHC9WWB07I/4pyu3ML7Ld/CiTaJB04WzsTwTi2bzxrfZopJc76gvXe1d55Kkm34fQ=,iv:w7WgywKZfMLcgexyJUvhpXnIhc+Xns1RDJXhHx/vBKc=,tag:X/Xx9PSs69gGY9LIkstB5g==,type:str]
sops:
age:
- recipient: age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWFcrTlYrOW1Tb0xGUmVS
Q3VPb3VPMS9hRWQ4aDQzRGtFTXp4SU90YWlrCng5NkN3TUFEUGIrWkRCK1NMeVND
Z2RwV0JKVnRTMWUvWlpDRzhBQWtsNVkKLS0tIHZ2NkZaVTJSaE1vTjVVMXhzTmYz
eGZTZ0VSUElFZVpqWlVISjNYdnA4UFUK/uOyj7CKU0XLHHdPNKByO2c56JWQfhk5
oauimeYrkNE+06dhXgVcJiQH+HcB33tB9u3YS9LxFYs3R98zKAHG6g==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-19T21:05:51Z"
mac: ENC[AES256_GCM,data:GQx4YuPIIfZxRpWtHCa8pCtidtmdYoIdMsK0dQJO42XT9KR5lbwNaP3v4GoaDoeM+L7iAn2OprpE54KkwIwRfb3NAjTeUvXO+J/Yi4ZJnLtuTOlAZrC8YvjmiZ6DaL8pvpRz7VUCfzNOoyrbjSJ2Qv/PWAUENcDEU7yOHNv7RBA=,iv:FqYd2F4vBqSCeKPsrWY/a8RePgkU2aP9cadB6nMyWaQ=,tag:FC75r3bMOQDZZwO0qJi4xg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.2