11 Commits

Author SHA1 Message Date
renovate[bot] 40c620cd58 Update dependency ExternalSecret to external-secrets.io/v1 2026-07-21 13:17:20 +00:00
Ruben Hensen f13417bc8f Add bolletjestrui app + vic.rubenhensen.nl
Deploys ghcr.io/rubenhensen/bolletjestrui at vic.rubenhensen.nl:
- bolletjestrui/ folder: deployment (SQLite on a PVC, migrate-on-start,
  ORIGIN + raised BODY_SIZE_LIMIT for GPX uploads), service, nginx
  ingress with prod-cluster-issuer TLS, PVC, and a Vault ExternalSecret
  (kv/bolletjestrui: site_password, admin_password, key_secret).
- apps/templates/bolletjestrui-folder.yaml: ArgoCD Application.
- dns: vic A record -> 46.224.26.65 (ingress LB).
- renovate: watch bolletjestrui/ for image tag bumps.

Committed locally, not pushed: needs the Vault secret created and the
ghcr package made public first.
2026-07-21 14:33:31 +02:00
Ruben Hensen a11309263e Update home cluster IP to 62.41.86.27 2026-06-06 09:52:36 +02:00
Ruben Hensen 379c050bb3 Update actual-budget to chart 1.8.9 / app 26.6.0 2026-06-03 11:28:10 +02:00
Ruben Hensen 20128bcd73 Add gitea at git.rubenhensen.nl
Single-pod sqlite install (simplest path for a home git host). HTTPS-only,
no SSH listener for now. Wired into the Hetzner tunnel like the other
rubenhensen.nl services.

First visitor to git.rubenhensen.nl can register; Gitea auto-promotes the
first user to admin.
2026-05-19 23:19:21 +02:00
Ruben Hensen ffe8830b5e Tunnel argocd/ha/longhorn/lingo/blog/serpbear through Hetzner
Refactor node001 nginx to drive both the SNI map and the port-80 vhosts
from a single tunneledHosts list so adding a service is one line.
2026-05-19 23:14:04 +02:00
Ruben Hensen 8d383d4261 Tunnel ynab.rubenhensen.nl through Hetzner 2026-05-19 23:04:02 +02:00
Ruben Hensen 00d46b6bd6 Tunnel vault.rubenhensen.nl through Hetzner 2026-05-19 22:56:52 +02:00
Ruben Hensen cb7cba25d0 Tunnel authentik.rubenhensen.nl through Hetzner
Same pattern as rss — needed so the OIDC redirect from freshrss (and
other relying parties) works for clients outside the home LAN.
2026-05-19 22:50:56 +02:00
Ruben Hensen ff9c2d3cef Tunnel rss.rubenhensen.nl through Hetzner
Point rss to the Hetzner box and reverse-proxy/SNI-passthrough back to
the home cluster so cert-manager keeps owning the certificate. Move
stalwart's HTTPS listener to 127.0.0.1:8443 so nginx can take :443 and
dispatch by SNI.
2026-05-19 22:45:55 +02:00
Ruben Hensen 790d7d6f32 Add WF_ENCRYPTION_KEY to mbgwp wordpress 2026-05-18 22:12:45 +02:00
18 changed files with 438 additions and 62 deletions
+2 -2
View File
@@ -12,14 +12,14 @@ spec:
project: default project: default
source: source:
repoURL: https://community-charts.github.io/helm-charts repoURL: https://community-charts.github.io/helm-charts
targetRevision: 1.8.7 targetRevision: 1.8.9
chart: actualbudget chart: actualbudget
helm: helm:
values: | values: |
# Actual Budget configuration # Actual Budget configuration
image: image:
tag: "26.3.0" tag: "26.6.0"
# Authentication configuration # Authentication configuration
login: login:
+21
View File
@@ -0,0 +1,21 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: bolletjestrui
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/rubenhensen/k8scd.git
targetRevision: HEAD
path: bolletjestrui
destination:
server: https://kubernetes.default.svc
namespace: bolletjestrui
syncPolicy:
syncOptions:
- CreateNamespace=true
automated:
selfHeal: true
+21
View File
@@ -0,0 +1,21 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: gitea
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/rubenhensen/k8scd.git
targetRevision: HEAD
path: gitea
destination:
server: https://kubernetes.default.svc
namespace: gitea
syncPolicy:
syncOptions:
- CreateNamespace=true
automated:
selfHeal: true
+5
View File
@@ -41,6 +41,11 @@ spec:
secretKeyRef: secretKeyRef:
name: mbgwp-mariadb-credentials name: mbgwp-mariadb-credentials
key: password key: password
- name: WF_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: wordpress-credentials
key: wf-encryption-key
# WordPress specific settings # WordPress specific settings
settings: settings:
@@ -0,0 +1,80 @@
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: bolletjestrui
name: bolletjestrui
spec:
replicas: 1
selector:
matchLabels:
app: bolletjestrui
strategy:
type: Recreate
template:
metadata:
labels:
app: bolletjestrui
spec:
containers:
- image: ghcr.io/rubenhensen/bolletjestrui:main-bb54c6a
name: bolletjestrui
ports:
- containerPort: 3000
protocol: TCP
env:
- name: DATABASE_URL
value: /app/data/bolletjestrui.db
- name: ORIGIN
value: https://vic.rubenhensen.nl
- name: PROTOCOL_HEADER
value: x-forwarded-proto
- name: HOST_HEADER
value: x-forwarded-host
# route GPX uploads can be a few MB; raise adapter-node's 512K default
- name: BODY_SIZE_LIMIT
value: "10M"
- name: TZ
value: Europe/Amsterdam
- name: SITE_PASSWORD
valueFrom:
secretKeyRef:
name: bolletjestrui-secrets
key: site_password
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: bolletjestrui-secrets
key: admin_password
- name: KEY_SECRET
valueFrom:
secretKeyRef:
name: bolletjestrui-secrets
key: key_secret
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "512Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /login
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /login
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
volumeMounts:
- mountPath: /app/data
name: data
restartPolicy: Always
volumes:
- name: data
persistentVolumeClaim:
claimName: bolletjestrui-data
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: bolletjestrui
name: bolletjestrui
spec:
ports:
- name: "3000"
port: 3000
targetPort: 3000
selector:
app: bolletjestrui
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
app: bolletjestrui
name: bolletjestrui-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
+25
View File
@@ -0,0 +1,25 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: bolletjestrui-secrets
spec:
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
refreshInterval: 15m
target:
name: bolletjestrui-secrets
creationPolicy: Owner
data:
- secretKey: site_password
remoteRef:
key: kv/bolletjestrui
property: site_password
- secretKey: admin_password
remoteRef:
key: kv/bolletjestrui
property: admin_password
- secretKey: key_secret
remoteRef:
key: kv/bolletjestrui
property: key_secret
+26
View File
@@ -0,0 +1,26 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: bolletjestrui-ingress
annotations:
cert-manager.io/cluster-issuer: prod-cluster-issuer
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
nginx.ingress.kubernetes.io/ssl-passthrough: "false"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
ingressClassName: nginx
rules:
- host: vic.rubenhensen.nl
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: bolletjestrui
port:
number: 3000
tls:
- secretName: letsencrypt-prod
hosts:
- vic.rubenhensen.nl
+20 -12
View File
@@ -10,51 +10,59 @@ records:
- name: "phocaslustrum" - name: "phocaslustrum"
expire: 300 expire: 300
type: A type: A
content: "62.41.87.114" content: "62.41.86.27"
- name: "ynab" - name: "ynab"
expire: 300 expire: 300
type: A type: A
content: "62.41.87.114" content: "46.224.26.65"
- name: "authentik" - name: "authentik"
expire: 300 expire: 300
type: A type: A
content: "62.41.87.114" content: "46.224.26.65"
- name: "ldap" - name: "ldap"
expire: 300 expire: 300
type: A type: A
content: "62.41.87.114" content: "62.41.86.27"
- name: "argocd" - name: "argocd"
expire: 300 expire: 300
type: A type: A
content: "62.41.87.114" content: "46.224.26.65"
- name: "vault" - name: "vault"
expire: 300 expire: 300
type: A type: A
content: "62.41.87.114" content: "46.224.26.65"
- name: "rss" - name: "rss"
expire: 300 expire: 300
type: A type: A
content: "62.41.87.114" content: "46.224.26.65"
- name: "ha" - name: "ha"
expire: 300 expire: 300
type: A type: A
content: "62.41.87.114" content: "46.224.26.65"
- name: "longhorn" - name: "longhorn"
expire: 300 expire: 300
type: A type: A
content: "62.41.87.114" content: "46.224.26.65"
- name: "lingo" - name: "lingo"
expire: 300 expire: 300
type: A type: A
content: "62.41.87.114" content: "46.224.26.65"
- name: "blog" - name: "blog"
expire: 300 expire: 300
type: A type: A
content: "62.41.87.114" content: "46.224.26.65"
- name: "serpbear" - name: "serpbear"
expire: 300 expire: 300
type: A type: A
content: "62.41.87.114" content: "46.224.26.65"
- name: "git"
expire: 300
type: A
content: "46.224.26.65"
- name: "vic"
expire: 300
type: A
content: "46.224.26.65"
- name: "@" - name: "@"
expire: 300 expire: 300
type: MX type: MX
@@ -0,0 +1,10 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
+60
View File
@@ -0,0 +1,60 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea
labels:
app: gitea
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: gitea
template:
metadata:
labels:
app: gitea
spec:
securityContext:
fsGroup: 1000
containers:
- name: gitea
image: gitea/gitea:1
env:
- name: USER_UID
value: "1000"
- name: USER_GID
value: "1000"
- name: TZ
value: Europe/Amsterdam
- name: GITEA__server__ROOT_URL
value: https://git.rubenhensen.nl/
- name: GITEA__server__DOMAIN
value: git.rubenhensen.nl
- name: GITEA__server__PROTOCOL
value: http
- name: GITEA__server__HTTP_PORT
value: "3000"
- name: GITEA__server__DISABLE_SSH
value: "true"
- name: GITEA__database__DB_TYPE
value: sqlite3
- name: GITEA__database__PATH
value: /data/gitea/gitea.db
- name: GITEA__security__INSTALL_LOCK
value: "true"
- name: GITEA__log__MODE
value: console
ports:
- containerPort: 3000
name: http
protocol: TCP
volumeMounts:
- mountPath: /data
name: data
restartPolicy: Always
volumes:
- name: data
persistentVolumeClaim:
claimName: gitea-data
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: gitea
labels:
app: gitea
spec:
selector:
app: gitea
ports:
- name: http
port: 3000
targetPort: 3000
+24
View File
@@ -0,0 +1,24 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gitea-ingress
annotations:
cert-manager.io/cluster-issuer: prod-cluster-issuer
nginx.ingress.kubernetes.io/proxy-body-size: "1g"
spec:
ingressClassName: nginx
rules:
- host: git.rubenhensen.nl
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitea
port:
number: 3000
tls:
- secretName: letsencrypt-prod
hosts:
- git.rubenhensen.nl
+5 -1
View File
@@ -19,4 +19,8 @@ spec:
- secretKey: smtp-password - secretKey: smtp-password
remoteRef: remoteRef:
key: kv/mbgwp key: kv/mbgwp
property: smtp-password property: smtp-password
- secretKey: wf-encryption-key
remoteRef:
key: kv/mbgwp
property: wf-encryption-key
+98 -45
View File
@@ -1,4 +1,43 @@
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
let
# Public IP of the home network where the k8s cluster ingress lives.
# Keep in sync with dns/domains/rubenhensen.nl.yaml.
homeIP = "62.41.86.27";
# Hosts tunneled to the home k8s cluster. Each entry gets:
# * an SNI map entry for TCP passthrough on :443
# * an HTTP vhost on :80 that reverse-proxies to the cluster
# The cluster's nginx-ingress terminates TLS with cert-manager.
tunneledHosts = [
"rss.rubenhensen.nl"
"authentik.rubenhensen.nl"
"vault.rubenhensen.nl"
"ynab.rubenhensen.nl"
"argocd.rubenhensen.nl"
"ha.rubenhensen.nl"
"longhorn.rubenhensen.nl"
"lingo.rubenhensen.nl"
"blog.rubenhensen.nl"
"serpbear.rubenhensen.nl"
"git.rubenhensen.nl"
];
sniMapEntries =
lib.concatMapStringsSep "\n"
(h: " ${h} ${homeIP}:443;")
tunneledHosts;
tunneledVhosts = lib.listToAttrs (map (h: {
name = h;
value = {
listen = [
{ addr = "0.0.0.0"; port = 80; }
{ addr = "[::]"; port = 80; }
];
locations."/".proxyPass = "http://${homeIP}";
};
}) tunneledHosts);
in
{ {
# ────────────────────────────────────────────── # ──────────────────────────────────────────────
# Firewall # Firewall
@@ -35,55 +74,67 @@
"d /var/lib/acme/acme-challenge 0755 acme acme -" "d /var/lib/acme/acme-challenge 0755 acme acme -"
]; ];
# Serve ACME challenges via nginx on port 80 # Serve ACME challenges via nginx on port 80.
# Also reverse-proxy tunneled hosts to the home k8s cluster, and do
# SNI-based TCP passthrough on 443 so the cluster's cert-manager keeps
# owning the TLS certificate for those hosts.
services.nginx = { services.nginx = {
enable = true; enable = true;
virtualHosts."mail.rubenhensen.nl" = { recommendedProxySettings = true;
listen = [
{ addr = "0.0.0.0"; port = 80; } # SNI passthrough on 443:
{ addr = "[::]"; port = 80; } # - mail.rubenhensen.nl (and anything else) → local stalwart on 8443
]; # - tunneled hosts → home cluster ingress on 443
locations."/.well-known/acme-challenge/" = { streamConfig = ''
root = "/var/lib/acme/acme-challenge"; map $ssl_preread_server_name $tunnel_upstream {
${sniMapEntries}
default 127.0.0.1:8443;
}
server {
listen 443;
listen [::]:443;
proxy_pass $tunnel_upstream;
ssl_preread on;
}
'';
# Port 80: tunneled hosts reverse-proxy to the home cluster so its
# nginx-ingress handles HTTP→HTTPS redirects and cert-manager
# HTTP-01 ACME challenges. Mail-related hosts serve ACME challenges
# locally for stalwart's cert and redirect everything else to HTTPS.
virtualHosts = tunneledVhosts // {
"mail.rubenhensen.nl" = {
listen = [
{ addr = "0.0.0.0"; port = 80; }
{ addr = "[::]"; port = 80; }
];
locations."/.well-known/acme-challenge/".root = "/var/lib/acme/acme-challenge";
locations."/".return = "301 https://$host$request_uri";
}; };
locations."/" = { "autoconfig.rubenhensen.nl" = {
return = "301 https://$host$request_uri"; listen = [
{ addr = "0.0.0.0"; port = 80; }
{ addr = "[::]"; port = 80; }
];
locations."/.well-known/acme-challenge/".root = "/var/lib/acme/acme-challenge";
locations."/".return = "301 https://$host$request_uri";
}; };
}; "autodiscover.rubenhensen.nl" = {
virtualHosts."autoconfig.rubenhensen.nl" = { listen = [
listen = [ { addr = "0.0.0.0"; port = 80; }
{ addr = "0.0.0.0"; port = 80; } { addr = "[::]"; port = 80; }
{ addr = "[::]"; port = 80; } ];
]; locations."/.well-known/acme-challenge/".root = "/var/lib/acme/acme-challenge";
locations."/.well-known/acme-challenge/" = { locations."/".return = "301 https://$host$request_uri";
root = "/var/lib/acme/acme-challenge";
}; };
locations."/" = { "rubenhensen.nl" = {
return = "301 https://$host$request_uri"; listen = [
}; { addr = "0.0.0.0"; port = 80; }
}; { addr = "[::]"; port = 80; }
virtualHosts."autodiscover.rubenhensen.nl" = { ];
listen = [ locations."/.well-known/acme-challenge/".root = "/var/lib/acme/acme-challenge";
{ addr = "0.0.0.0"; port = 80; } locations."/".return = "301 https://$host$request_uri";
{ addr = "[::]"; port = 80; }
];
locations."/.well-known/acme-challenge/" = {
root = "/var/lib/acme/acme-challenge";
};
locations."/" = {
return = "301 https://$host$request_uri";
};
};
virtualHosts."rubenhensen.nl" = {
listen = [
{ addr = "0.0.0.0"; port = 80; }
{ addr = "[::]"; port = 80; }
];
locations."/.well-known/acme-challenge/" = {
root = "/var/lib/acme/acme-challenge";
};
locations."/" = {
return = "301 https://$host$request_uri";
}; };
}; };
}; };
@@ -120,7 +171,9 @@
protocol = "managesieve"; protocol = "managesieve";
}; };
https = { https = {
bind = "[::]:443"; # nginx owns the public :443 and does SNI passthrough to here
# for the mail.rubenhensen.nl SNI. Stalwart still terminates TLS.
bind = "127.0.0.1:8443";
protocol = "http"; protocol = "http";
tls.implicit = true; tls.implicit = true;
}; };
+2 -1
View File
@@ -16,7 +16,8 @@
"kubernetes": { "kubernetes": {
"managerFilePatterns": [ "managerFilePatterns": [
"/apps/templates/.+\\.yaml$/", "/apps/templates/.+\\.yaml$/",
"/serpbear/.+\\.yaml$/" "/serpbear/.+\\.yaml$/",
"/bolletjestrui/.+\\.yaml$/"
] ]
}, },
"customManagers": [ "customManagers": [
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: external-secrets.io/v1beta1 apiVersion: external-secrets.io/v1
kind: ExternalSecret kind: ExternalSecret
metadata: metadata:
name: serpbear-secrets name: serpbear-secrets