Compare commits

...
Author SHA1 Message Date
Aleksandr BogomiakovandClaude ff4e87aca0 NG: furumi player pinned to the music node
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 1m19s
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-06 16:18:36 +01:00
Aleksandr BogomiakovandClaude 01561ea2cd NG: furumi database in main PostgreSQL cluster
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-06 16:18:36 +01:00
Aleksandr Bogomiakov 7db427f332 NG: secret stores with lookup by path or id
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 20s
Move the Vaultwarden bridge and the ClusterSecretStores out of external-secrets
into secret-stores (wave 4, after vaultwarden). A store key may be an item UUID
or an item name used as a path (k8s/<namespace>/<what>), matched exactly.
2026-10-06 15:13:09 +01:00
Aleksandr Bogomiakov 41729bf256 NG: enable sso for VW
Keycloak Terraform / Terraform (push) Failing after 39s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 19s
2026-10-06 00:42:40 +01:00
Aleksandr Bogomiakov fe914e2de4 NG: enable sso for VW
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 28s
2026-10-06 00:18:56 +01:00
Aleksandr Bogomiakov 0348329829 NG: enable sso for argocd
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 15s
2026-10-06 00:11:02 +01:00
Aleksandr Bogomiakov e257e3895b NG: added Keycloak
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 10s
2026-10-05 23:29:35 +01:00
Aleksandr Bogomiakov 4fe2a2b5f3 NG: added reloader
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 22s
2026-10-05 19:17:41 +01:00
Aleksandr Bogomiakov 690ff2a19c NG: added http -> https redir
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 17s
2026-10-05 19:00:28 +01:00
Aleksandr Bogomiakov 99e1818572 NG: app-of-apps CORE
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 24s
2026-10-05 18:49:42 +01:00
Aleksandr Bogomiakov cf88753ff5 NG: secret manager and VW
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Failing after 14m33s
2026-10-05 18:40:46 +01:00
Aleksandr Bogomiakov 862a7619e2 Track main instead of k8s-ng in the new cluster's applications
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 22s
2026-10-05 18:31:58 +01:00
Aleksandr Bogomiakov 5a29dcc236 Stop tracking rendered Helm charts in k8s-ng 2026-10-05 18:31:52 +01:00
Aleksandr Bogomiakov bcfbfcd362 Moved PSQL to NG 2026-10-05 18:30:17 +01:00
Aleksandr Bogomiakov 2676ecef5d Moved ArgoCD to NG 2026-10-05 18:13:48 +01:00
Ultradesu b4de55f3c0 Change AmneziaWG port to 5848
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 12s
Check with kubeconform / lint (push) Successful in 10s
Auto-update README / Generate README and Create MR (push) Successful in 11s
2026-09-30 14:00:07 +01:00
Ultradesu de6c9562f1 Update AmneziaWG port to 443, refresh obfuscation params and update fellow image to 1.0.2
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 17s
Check with kubeconform / lint (push) Failing after 7m19s
Auto-update README / Generate README and Create MR (push) Successful in 4m32s
2026-09-30 13:53:38 +01:00
ab 0c333375a5 Fixed AWG scripts
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 5s
Check with kubeconform / lint (push) Successful in 6s
Auto-update README / Generate README and Create MR (push) Successful in 5s
2026-09-17 17:48:53 +03:00
ab 5783547bd3 Fixed AWG scripts
Check with kubeconform / lint (push) Successful in 12s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 9s
Auto-update README / Generate README and Create MR (push) Successful in 6s
2026-09-17 17:38:30 +03:00
ab 29c5b00c16 Added airtrail deployment
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 6s
Check with kubeconform / lint (push) Successful in 7s
Auto-update README / Generate README and Create MR (push) Successful in 9s
2026-09-12 07:18:56 +03:00
ab 76b2090ccc Added airtrail psql user
Check with kubeconform / lint (push) Successful in 9s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 10s
Auto-update README / Generate README and Create MR (push) Successful in 8s
2026-09-12 07:11:22 +03:00
ab bc83f7dd24 Update k8s/apps/pasarguard/deployment.yaml
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 20s
Check with kubeconform / lint (push) Successful in 14s
Auto-update README / Generate README and Create MR (push) Successful in 21s
2026-09-02 08:48:55 +00:00
Aleksandr Bogomiakov 5768a4ff3b fix grafana datasource
Check with kubeconform / lint (push) Successful in 7s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 6s
Auto-update README / Generate README and Create MR (push) Successful in 9s
2026-09-01 23:06:49 +01:00
Aleksandr Bogomiakov 7ce98c4982 fix loki helm chart path
Check with kubeconform / lint (push) Successful in 5s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 6s
Auto-update README / Generate README and Create MR (push) Successful in 12s
2026-09-01 22:35:02 +01:00
Aleksandr Bogomiakov 18f2654ff1 updated prom 84.3.0 -> 88.6.2, loki 6.29.0 -> 18.11.7, promtail 6.16.6 -> 6.17.1
Auto-update README / Generate README and Create MR (push) Successful in 11s
Check with kubeconform / lint (push) Successful in 6s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 6s
2026-09-01 22:30:43 +01:00
Aleksandr Bogomiakov 2fb63dded8 updated longhorn 1.12.0 -> 1.12.1
Check with kubeconform / lint (push) Successful in 6s
Auto-update README / Generate README and Create MR (push) Successful in 8s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 5s
2026-09-01 22:25:34 +01:00
Aleksandr Bogomiakov 46bf1dc41b updated keycloak 7.1.11 -> 7.3.1
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 5s
Check with kubeconform / lint (push) Successful in 5s
Auto-update README / Generate README and Create MR (push) Successful in 8s
2026-09-01 22:23:48 +01:00
Aleksandr Bogomiakov eda408aaea updated external-secrets 1.1.0 -> 2.10.0
Check with kubeconform / lint (push) Successful in 15s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 9s
Auto-update README / Generate README and Create MR (push) Successful in 12s
2026-09-01 22:21:37 +01:00
Aleksandr Bogomiakov 7b9da9236f updated cert-manager 1.20.0 -> 1.21.1
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 20s
2026-09-01 22:19:01 +01:00
Aleksandr Bogomiakov f2a607fda1 Updated k3s v1.36.1+k3s1 -> v1.36.4+k3s1
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 6s
Auto-update README / Generate README and Create MR (push) Successful in 9s
Check with kubeconform / lint (push) Successful in 7s
2026-09-01 22:14:55 +01:00
Aleksandr Bogomiakov d57bb01608 Updated argoCD
Check with kubeconform / lint (push) Successful in 7s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 7s
Auto-update README / Generate README and Create MR (push) Successful in 12s
2026-09-01 22:06:58 +01:00
Aleksandr Bogomiakov 76f77fbc56 Fixed GITEA__webhook__ALLOWED_HOST_LIST -> GITEA__security__ALLOWED_HOST_LIST
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 23s
Check with kubeconform / lint (push) Successful in 10s
Auto-update README / Generate README and Create MR (push) Successful in 8s
2026-09-01 21:51:08 +01:00
Ultradesu e90efbd016 Added proxy
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 6s
Check with kubeconform / lint (push) Successful in 6s
Auto-update README / Generate README and Create MR (push) Successful in 7s
2026-08-14 10:55:52 +01:00
Ultradesu b0378ffb40 Added macos exporter
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 8s
Check with kubeconform / lint (push) Successful in 8s
Auto-update README / Generate README and Create MR (push) Successful in 9s
2026-08-11 11:30:32 +01:00
Ultradesu 5c2e7d0e33 Removed nodeselector for web-petting
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 11s
Check with kubeconform / lint (push) Successful in 11s
Auto-update README / Generate README and Create MR (push) Successful in 30s
2026-08-08 11:59:07 +01:00
Ultradesu b9052a99f1 Fixed matrix secrets
Check with kubeconform / lint (push) Successful in 6s
Auto-update README / Generate README and Create MR (push) Successful in 7s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 5s
2026-08-07 11:17:50 +01:00
Ultradesu d41171955b Moved traefik-ai to kube-system-custom, fixed lan-play ingress
Check with kubeconform / lint (push) Successful in 7s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 7s
Auto-update README / Generate README and Create MR (push) Successful in 9s
2026-08-05 13:44:30 +01:00
Ultradesu f038016d46 Added UK lan-play instance
Keycloak Terraform / Terraform (push) Successful in 26s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 15s
Check with kubeconform / lint (push) Successful in 11s
Auto-update README / Generate README and Create MR (push) Successful in 18s
2026-08-05 12:56:42 +01:00
Aleksandr Bogomiakov 1091d6bd14 Enabled auto sync openbanking
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 10s
Check with kubeconform / lint (push) Successful in 9s
Auto-update README / Generate README and Create MR (push) Successful in 7s
2026-08-01 14:43:48 +01:00
Aleksandr Bogomiakov 9f56045009 Enabled auto sync openbanking
Check with kubeconform / lint (push) Successful in 8s
Auto-update README / Generate README and Create MR (push) Successful in 7s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 6s
2026-08-01 13:30:39 +01:00
Aleksandr Bogomiakov 2583236f6d Enabled auto sync openbanking
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 15s
Check with kubeconform / lint (push) Successful in 10s
Auto-update README / Generate README and Create MR (push) Successful in 8s
2026-08-01 13:24:23 +01:00
Ultradesu 80b792ce8c Fixed gitea runner scheduling. Bump amnezia
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 13s
Check with kubeconform / lint (push) Successful in 10s
Auto-update README / Generate README and Create MR (push) Successful in 8s
2026-07-30 23:16:36 +01:00
Ultradesu e40a4d0c72 Fixed gitea runner k8s auth
Check with kubeconform / lint (push) Successful in 18s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 14s
Auto-update README / Generate README and Create MR (push) Failing after 16m47s
2026-07-30 16:48:37 +01:00
Ultradesu 03d580999a Dropped authentik TF code
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 7s
2026-07-30 16:42:33 +01:00
Ultradesu a28fed03ed TF fmt
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 8s
Keycloak Terraform / Terraform (push) Successful in 27s
2026-07-30 16:39:52 +01:00
115 changed files with 3238 additions and 2079 deletions

No files matched your search

+3
View File
@@ -59,3 +59,6 @@ tags
[._]*.un~
.DS_Store
# Helm charts pulled by kustomize (--enable-helm) when rendering locally
k8s-ng/**/charts/
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Brings the whole core state up on an empty cluster:
# k8s-ng/bootstrap/bootstrap.sh <kube-context>
# Installs Argo CD (the only thing that cannot install itself) and applies the
# root application; Argo CD then pulls every core application in wave order.
set -euo pipefail
ctx=${1:?usage: bootstrap.sh <kube-context>}
here=$(cd "$(dirname "$0")" && pwd)
k="kubectl --context $ctx"
# Render in a scratch copy so kustomize does not leave downloaded charts in the repo.
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
cp -r "$here/../core/argocd" "$tmp/argocd"
kubectl kustomize --enable-helm "$tmp/argocd" > "$tmp/argocd.yaml"
$k create namespace argocd --dry-run=client -o yaml | $k apply -f -
# CRDs must exist before the Argo CD objects that use them: apply twice.
$k apply --server-side --force-conflicts -f "$tmp/argocd.yaml" >/dev/null 2>&1 || true
sleep 8
$k apply --server-side --force-conflicts -f "$tmp/argocd.yaml"
$k -n argocd rollout status deploy --timeout=300s
$k -n argocd rollout status statefulset --timeout=300s
$k apply --server-side --force-conflicts -f "$here/root.yaml"
echo "Core state requested. Watch it with: $k -n argocd get app"
+29
View File
@@ -0,0 +1,29 @@
# Root of the core state. Applying this one object makes Argo CD pull every core
# application in wave order (see k8s-ng/core-state/values.yaml).
# Note: there are deliberately no finalizers; deleting this Application keeps all
# deployed services and data.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: core-state
namespace: argocd
spec:
project: core
destination:
server: https://kubernetes.default.svc
namespace: argocd
source:
repoURL: https://gt.hexor.cy/ab/homelab.git
targetRevision: main
path: k8s-ng/core-state
helm:
# Per-cluster overrides of core-state/values.yaml, for example:
# valuesObject:
# targetRevision: some-branch
valuesObject: {}
syncPolicy:
automated:
selfHeal: true
prune: true
syncOptions:
- ServerSideApply=true
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v2
name: core-state
description: App of apps for the core services of the cluster
version: 0.1.0
@@ -0,0 +1,29 @@
{{- range .Values.apps }}
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: {{ .name }}
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: {{ .wave | quote }}
spec:
project: {{ .project | default $.Values.project }}
destination:
server: {{ $.Values.destinationServer }}
namespace: {{ .namespace }}
source:
repoURL: {{ $.Values.repoURL }}
targetRevision: {{ $.Values.targetRevision }}
path: {{ .path }}
syncPolicy:
automated:
selfHeal: true
prune: {{ if hasKey . "prune" }}{{ .prune }}{{ else }}true{{ end }}
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
{{- if .skipDryRunOnMissingResource }}
- SkipDryRunOnMissingResource=true
{{- end }}
{{- end }}
+65
View File
@@ -0,0 +1,65 @@
# Per-cluster variables. Override them in k8s-ng/bootstrap/root.yaml
# (spec.source.helm.valuesObject) when the state is deployed to another cluster.
repoURL: https://gt.hexor.cy/ab/homelab.git
targetRevision: main
destinationServer: https://kubernetes.default.svc
project: core
# Core applications. Lower waves are synced first and must be Healthy before the
# next wave starts. Every path is a kustomize (or plain manifest) directory.
# wave 0: Argo CD itself and its projects
# wave 1: operators (their CRDs are used by the later waves)
# wave 2: the database cluster and its databases/users
# wave 3: services that need the database and secrets
apps:
- name: argocd
path: k8s-ng/core/argocd
namespace: argocd
wave: 0
- name: cnpg
path: k8s-ng/core/cnpg
namespace: cnpg-system
wave: 1
- name: traefik-config
path: k8s-ng/core/traefik-config
namespace: kube-system
wave: 1
- name: reloader
path: k8s-ng/core/reloader
namespace: reloader
wave: 1
- name: cert-manager
path: k8s-ng/core/cert-manager
namespace: cert-manager
wave: 1
skipDryRunOnMissingResource: true # ClusterIssuers need the cert-manager CRDs
- name: external-secrets
path: k8s-ng/core/external-secrets
namespace: external-secrets
wave: 1
- name: postgresql
path: k8s-ng/core/postgresql
namespace: psql
wave: 2
prune: false # never delete databases because of a manifest change
skipDryRunOnMissingResource: true # CRDs come from the cnpg and external-secrets apps
- name: vaultwarden
path: k8s-ng/core/vaultwarden
namespace: vaultwarden
wave: 3
skipDryRunOnMissingResource: true
- name: keycloak
path: k8s-ng/core/keycloak
namespace: keycloak
wave: 3
skipDryRunOnMissingResource: true # ExternalSecret/Certificate CRDs come from earlier waves
- name: furumi
path: k8s-ng/core/furumi
namespace: furumi
wave: 5 # pinned to the music node; must not hold back the other waves
skipDryRunOnMissingResource: true
- name: secret-stores
path: k8s-ng/core/secret-stores
namespace: external-secrets
wave: 4 # reads the Vaultwarden vault, so it comes after vaultwarden
skipDryRunOnMissingResource: true # ClusterSecretStore CRD comes from the external-secrets app
+66
View File
@@ -0,0 +1,66 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: core
namespace: argocd
spec:
clusterResourceWhitelist:
- group: '*'
kind: '*'
description: Hexor Home Lab Core
destinations:
- namespace: '*'
server: https://kubernetes.default.svc
sourceRepos:
- https://gt.hexor.cy/ab/homelab.git
---
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: apps
namespace: argocd
spec:
clusterResourceWhitelist:
- group: '*'
kind: '*'
description: Hexor Home Lab Apps
destinations:
- namespace: '*'
server: https://kubernetes.default.svc
sourceRepos:
- https://gt.hexor.cy/ab/homelab.git
---
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: games
namespace: argocd
spec:
clusterResourceWhitelist:
- group: '*'
kind: '*'
description: Hexor Home Lab Games
destinations:
- namespace: '*'
server: https://kubernetes.default.svc
sourceRepos:
- https://gt.hexor.cy/ab/homelab.git
---
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: desktop
namespace: argocd
spec:
clusterResourceWhitelist:
- group: '*'
kind: '*'
description: Hexor Home Lab Desktop Apps
destinations:
- namespace: '*'
server: https://kubernetes.default.svc
sourceRepos:
- https://gt.hexor.cy/ab/homelab.git
+26
View File
@@ -0,0 +1,26 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: argocd-dex-server
namespace: argocd
spec:
template:
spec:
containers:
- name: dex-server
readinessProbe:
tcpSocket:
port: 5556
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
livenessProbe:
tcpSocket:
port: 5556
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
+25
View File
@@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd
namespace: argocd
annotations:
cert-manager.io/cluster-issuer: letsencrypt
traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec:
tls:
- hosts:
- ag.btwiusearch.net
secretName: argocd-server-tls
rules:
- host: ag.btwiusearch.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 80
+18
View File
@@ -0,0 +1,18 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ingress.yaml
- appprojects.yaml
helmCharts:
- name: argo-cd
repo: https://argoproj.github.io/argo-helm
version: 10.6.0
releaseName: argocd
namespace: argocd
valuesFile: values.yaml
includeCRDs: true
patches:
- path: dex-probes-patch.yaml
+115
View File
@@ -0,0 +1,115 @@
# Argo CD Helm Chart Values (k8s-ng cluster)
global:
domain: ag.btwiusearch.net
logging:
format: text
level: info
crds:
install: true
keep: true # Recommended to keep true so CRDs are not deleted on uninstall
configs:
params:
server.insecure: "true" # TLS is terminated by the ingress
cm:
create: true
exec.enabled: true
kustomize.buildOptions: --enable-helm
application.instanceLabelKey: argocd.argoproj.io/instance
# Local admin account, kept as a break-glass login next to SSO; the initial
# password is generated by Argo CD into the argocd-initial-admin-secret secret.
# Set to false once SSO is trusted.
admin.enabled: true
statusbadge.enabled: true
timeout.reconciliation: 60s
# Lets sync waves in an app of apps wait for child Applications to be Healthy.
resource.customizations.health.argoproj.io_Application: |
hs = {}
hs.status = "Progressing"
hs.message = ""
if obj.status ~= nil then
if obj.status.health ~= nil then
hs.status = obj.status.health.status
if obj.status.health.message ~= nil then
hs.message = obj.status.health.message
end
end
end
return hs
# SSO through Dex -> Keycloak. The client id/secret come from the oidc-creds
# Secret (labelled app.kubernetes.io/part-of=argocd), created by hand until
# Vaultwarden + External Secrets are the source.
dex.config: |
connectors:
- type: oidc
id: keycloak
name: Keycloak
config:
issuer: https://auth.hexor.cy/auth/realms/hexor
clientID: $oidc-creds:id
clientSecret: $oidc-creds:secret
insecureEnableGroups: true
scopes:
- openid
- profile
- email
- offline_access
getUserInfo: true
rbac:
create: true
policy.default: ""
policy.csv: |
g, game-servers-managers, GameServersManagersRole
# Role permissions
p, GameServersManagersRole, applications, get, games/*, allow
p, GameServersManagersRole, applications, update, games/*, allow
p, GameServersManagersRole, applications, sync, games/*, allow
p, GameServersManagersRole, applications, override, games/*, allow
p, GameServersManagersRole, applications, action/*, games/*, allow
p, GameServersManagersRole, exec, create, games/*, allow
p, GameServersManagersRole, logs, get, games/*, allow
p, GameServersManagersRole, applications, delete, games/*, deny
# Admin policy
g, argocd-admins, role:admin
secret:
createSecret: true
controller:
replicas: 1
# Dex OIDC provider (Keycloak connector configured in configs.cm.dex.config)
dex:
replicas: 1
enabled: true
redis:
enabled: true
redis-ha:
enabled: false
server:
replicas: 1
ingress:
enabled: false
certificate:
enabled: false
certificateSecret:
enabled: false
repoServer:
replicas: 1
livenessProbe:
timeoutSeconds: 10
periodSeconds: 60
applicationSet:
enabled: true
replicas: 1
notifications:
enabled: true
replicas: 1
+38
View File
@@ -0,0 +1,38 @@
# Public certificates through DNS-01. The credential Secrets are created by hand
# (they will move to Vaultwarden + External Secrets):
# kubectl -n cert-manager create secret generic cloudflare-api-token --from-literal=apiToken=<token>
# (Zone:DNS:Edit on the btwiusearch.net zone)
# kubectl -n cert-manager create secret generic aws-api-key-secret \
# --from-literal=accessKey=<id> --from-literal=secretKey=<secret>
# (Route53 access to the hexor.cy zone)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: k8s-master@hexor.cy
privateKeySecretRef:
name: letsencrypt-account-key
solvers:
- selector:
dnsZones:
- btwiusearch.net
dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: apiToken
- selector:
dnsZones:
- hexor.cy
dns01:
route53:
region: eu-north-1
accessKeyIDSecretRef:
name: aws-api-key-secret
key: accessKey
secretAccessKeySecretRef:
name: aws-api-key-secret
key: secretKey
@@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- issuer.yaml
- self-signed.yaml
helmCharts:
- name: cert-manager
repo: https://charts.jetstack.io
version: 1.21.1
releaseName: cert-manager
namespace: cert-manager
valuesFile: values.yaml
includeCRDs: true
+29
View File
@@ -0,0 +1,29 @@
# Private CA for internal services: issuer "hexor-lab".
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: hexor-ca-root
namespace: cert-manager
spec:
isCA: true
duration: 87600h # 10y
commonName: hexor-ca
secretName: hexor-ca-root-secret
issuerRef:
name: selfsigned
kind: ClusterIssuer
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: hexor-lab
spec:
ca:
secretName: hexor-ca-root-secret
+2
View File
@@ -0,0 +1,2 @@
crds:
enabled: true
+11
View File
@@ -0,0 +1,11 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
helmCharts:
- name: cloudnative-pg
repo: https://cloudnative-pg.github.io/charts
version: 0.29.1
releaseName: cnpg
namespace: cnpg-system
valuesFile: values.yaml
includeCRDs: true
+4
View File
@@ -0,0 +1,4 @@
# CloudNativePG operator (chart 0.29.x = operator 1.30.x)
crds:
create: true
@@ -0,0 +1,13 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# The Vaultwarden bridge and the ClusterSecretStores are in ../secret-stores (they need Vaultwarden).
helmCharts:
- name: external-secrets
repo: https://charts.external-secrets.io
version: 2.11.0
releaseName: external-secrets
namespace: external-secrets
valuesFile: values.yaml
includeCRDs: true
+1
View File
@@ -0,0 +1 @@
installCRDs: true
+50
View File
@@ -0,0 +1,50 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: furumi-player
labels:
app: furumi-player
spec:
replicas: 1
strategy:
type: Recreate # the library is a hostPath on a single node
selector:
matchLabels:
app: furumi-player
template:
metadata:
labels:
app: furumi-player
annotations:
reloader.stakater.com/auto: "true"
spec:
# The music library lives on this node's /data (LVM raid, not replicated).
nodeSelector:
kubernetes.io/hostname: music
containers:
- name: furumi-player
image: ultradesu/furumusic:latest
imagePullPolicy: Always
env:
- name: FURU_DATABASE_URL
valueFrom:
secretKeyRef:
name: furumi-creds
key: PG_STRING
ports:
- name: http
containerPort: 8000
volumeMounts:
- name: library
mountPath: /media
- name: inbox
mountPath: /inbox
volumes:
- name: library
hostPath:
path: /data/furumi/library
type: Directory
- name: inbox
hostPath:
path: /data/furumi/inbox
type: Directory
+24
View File
@@ -0,0 +1,24 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: furumi
annotations:
cert-manager.io/cluster-issuer: letsencrypt
traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec:
ingressClassName: traefik
rules:
- host: music.hexor.cy
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: furumi-player
port:
number: 8000
tls:
- secretName: furumi-tls
hosts:
- music.hexor.cy
+8
View File
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- secrets.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
+47
View File
@@ -0,0 +1,47 @@
# Read-only access to the furumi-db Secret in the psql namespace
# (see postgresql/databases/furumi.yaml for the other half).
apiVersion: v1
kind: ServiceAccount
metadata:
name: db-credentials-reader
---
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: psql-credentials
spec:
provider:
kubernetes:
remoteNamespace: psql
server:
caProvider:
type: ConfigMap
name: kube-root-ca.crt
key: ca.crt
auth:
serviceAccount:
name: db-credentials-reader
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: furumi-creds
spec:
refreshInterval: 1h
secretStoreRef:
kind: SecretStore
name: psql-credentials
target:
name: furumi-creds
template:
data:
PG_STRING: "postgresql://{{ .username }}:{{ .password }}@main-rw.psql.svc:5432/furumi"
data:
- secretKey: username
remoteRef:
key: furumi-db
property: username
- secretKey: password
remoteRef:
key: furumi-db
property: password
+16
View File
@@ -0,0 +1,16 @@
---
apiVersion: v1
kind: Service
metadata:
name: furumi-player
labels:
app: furumi-player
spec:
type: ClusterIP
selector:
app: furumi-player
ports:
- name: http
protocol: TCP
port: 8000
targetPort: 8000
+122
View File
@@ -0,0 +1,122 @@
# rsauth2-proxy: forward-auth for services that have no OIDC of their own.
# Needs, besides this file:
# - Secret auth-proxy-creds (AUTH_PROXY_CLIENT_ID, AUTH_PROXY_CLIENT_SECRET,
# AUTH_PROXY_COOKIE_SECRET); the client secret is the one of the Keycloak
# client "rsauth2-proxy". Created by hand until Vaultwarden is the source.
# - ConfigMap auth-proxy-routes, managed by Terraform (terraform/keycloak).
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-proxy
labels:
app: auth-proxy
spec:
replicas: 1
selector:
matchLabels:
app: auth-proxy
template:
metadata:
labels:
app: auth-proxy
spec:
containers:
- name: auth-proxy
image: ultradesu/rsauth2-proxy:latest
ports:
- containerPort: 8080
name: http
protocol: TCP
envFrom:
- secretRef:
name: auth-proxy-creds
env:
- name: AUTH_PROXY_OIDC_ISSUER
value: "https://auth.hexor.cy/auth/realms/hexor"
- name: AUTH_PROXY_COOKIE_DOMAIN
value: ".hexor.cy"
- name: AUTH_PROXY_CALLBACK_URL
value: "https://oauth.hexor.cy/callback"
- name: AUTH_PROXY_ROUTES_FILE
value: "/config/routes.yaml"
- name: AUTH_PROXY_LOG_LEVEL
value: "debug"
volumeMounts:
- name: routes
mountPath: /config
readOnly: true
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 64Mi
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 3
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 30
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
capabilities:
drop:
- ALL
volumes:
- name: routes
configMap:
name: auth-proxy-routes
---
apiVersion: v1
kind: Service
metadata:
name: auth-proxy
labels:
app: auth-proxy
spec:
ports:
- name: http
port: 80
targetPort: 8080
protocol: TCP
selector:
app: auth-proxy
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: auth-proxy
spec:
entryPoints:
- websecure
routes:
- match: Host(`oauth.hexor.cy`)
kind: Rule
services:
- name: auth-proxy
port: 80
tls:
secretName: auth-proxy-tls
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: auth-proxy-tls
spec:
secretName: auth-proxy-tls
issuerRef:
name: letsencrypt
kind: ClusterIssuer
dnsNames:
- oauth.hexor.cy
@@ -0,0 +1,366 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: keycloak-hacker-theme
namespace: keycloak
data:
theme.properties: |
parent=keycloak.v2
import=common/keycloak
styles=css/hacker.css
hacker.css: |
/* ============================================================
HEXOR — Hacker Terminal Theme for Keycloak
============================================================ */
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;600&display=swap');
:root {
--hk-bg: #0a0a0a;
--hk-panel: #0d0d0d;
--hk-border: #00ff4180;
--hk-green: #00ff41;
--hk-green-dim: #00cc33;
--hk-green-glow: rgba(0, 255, 65, 0.15);
--hk-green-glow-strong: rgba(0, 255, 65, 0.35);
--hk-cyan: #00e5ff;
--hk-red: #ff3333;
--hk-text: #b0ffb0;
--hk-text-dim: #5a8a5a;
--hk-input-bg: #050505;
--hk-font: 'Fira Code', 'Cascadia Code', 'JetBrains Mono', monospace;
}
/* -- Force dark always --------------------------------------------------- */
html.login-pf,
html.login-pf.pf-v5-theme-dark {
color-scheme: dark;
}
/* -- Background: scanlines + CRT ---------------------------------------- */
body#keycloak-bg {
font-family: var(--hk-font) !important;
background: var(--hk-bg) !important;
color: var(--hk-text) !important;
min-height: 100vh;
position: relative;
}
body#keycloak-bg::before {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 255, 65, 0.03) 2px,
rgba(0, 255, 65, 0.03) 4px
);
pointer-events: none;
z-index: 9999;
}
/* -- Login container ----------------------------------------------------- */
.pf-v5-c-login {
background: transparent !important;
}
.pf-v5-c-login__container {
max-width: 480px !important;
}
/* -- Header / Brand ------------------------------------------------------ */
#kc-header-wrapper {
font-family: var(--hk-font) !important;
color: var(--hk-green) !important;
text-shadow: 0 0 10px var(--hk-green), 0 0 30px var(--hk-green-glow);
font-size: 1.5rem !important;
letter-spacing: 0.15em;
text-transform: uppercase;
}
#kc-header-wrapper .kc-logo-text span::before {
content: '> ';
opacity: 0.6;
}
/* -- Main card ----------------------------------------------------------- */
.pf-v5-c-login__main {
background: var(--hk-panel) !important;
border: 1px solid var(--hk-border) !important;
border-radius: 0 !important;
box-shadow:
0 0 15px var(--hk-green-glow),
inset 0 0 30px rgba(0, 0, 0, 0.5) !important;
}
/* -- Title --------------------------------------------------------------- */
h1#kc-page-title,
.pf-v5-c-title {
font-family: var(--hk-font) !important;
color: var(--hk-green) !important;
text-shadow: 0 0 8px var(--hk-green-glow);
font-weight: 600 !important;
font-size: 1.25rem !important;
letter-spacing: 0.05em;
}
/* -- Labels -------------------------------------------------------------- */
.pf-v5-c-form__label-text {
font-family: var(--hk-font) !important;
color: var(--hk-green-dim) !important;
font-size: 0.85rem !important;
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* -- Input fields -------------------------------------------------------- */
.pf-v5-c-form-control {
background-color: var(--hk-input-bg) !important;
border: 1px solid var(--hk-border) !important;
border-radius: 0 !important;
color: var(--hk-green) !important;
font-family: var(--hk-font) !important;
font-size: 0.9rem !important;
caret-color: var(--hk-green);
transition: border-color 0.2s, box-shadow 0.2s;
}
.pf-v5-c-form-control:focus-within,
.pf-v5-c-form-control:focus {
border-color: var(--hk-green) !important;
box-shadow: 0 0 8px var(--hk-green-glow), inset 0 0 4px var(--hk-green-glow) !important;
outline: none !important;
}
.pf-v5-c-form-control input {
color: var(--hk-green) !important;
font-family: var(--hk-font) !important;
}
.pf-v5-c-form-control input::placeholder {
color: var(--hk-text-dim) !important;
}
/* -- Primary button (Sign In) -------------------------------------------- */
.pf-v5-c-button.pf-m-primary {
background: transparent !important;
border: 1px solid var(--hk-green) !important;
border-radius: 0 !important;
color: var(--hk-green) !important;
font-family: var(--hk-font) !important;
font-weight: 600 !important;
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.9rem !important;
transition: all 0.2s;
position: relative;
overflow: hidden;
}
.pf-v5-c-button.pf-m-primary:hover {
background: var(--hk-green-glow) !important;
box-shadow: 0 0 15px var(--hk-green-glow-strong), inset 0 0 15px var(--hk-green-glow) !important;
color: #fff !important;
text-shadow: 0 0 5px var(--hk-green);
}
/* -- Secondary button (Try Another Way, Passkey) ------------------------- */
.pf-v5-c-button.pf-m-secondary {
background: transparent !important;
border: 1px solid var(--hk-cyan) !important;
border-radius: 0 !important;
color: var(--hk-cyan) !important;
font-family: var(--hk-font) !important;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.85rem !important;
transition: all 0.2s;
}
.pf-v5-c-button.pf-m-secondary:hover {
background: rgba(0, 229, 255, 0.1) !important;
box-shadow: 0 0 12px rgba(0, 229, 255, 0.3) !important;
color: #fff !important;
}
/* -- Passkey authenticate button ----------------------------------------- */
#authenticateWebAuthnButton {
background: transparent !important;
border: 1px solid var(--hk-cyan) !important;
border-radius: 0 !important;
color: var(--hk-cyan) !important;
font-family: var(--hk-font) !important;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.85rem !important;
padding: 0.75rem 1rem;
display: block;
width: 100%;
text-align: center;
text-decoration: none;
transition: all 0.2s;
margin-top: 1rem;
}
#authenticateWebAuthnButton::before {
content: '\f084 ';
font-family: 'Font Awesome 6 Free', 'FontAwesome';
font-weight: 900;
}
#authenticateWebAuthnButton:hover {
background: rgba(0, 229, 255, 0.1) !important;
box-shadow: 0 0 15px rgba(0, 229, 255, 0.3), inset 0 0 10px rgba(0, 229, 255, 0.1) !important;
color: #fff !important;
text-shadow: 0 0 5px var(--hk-cyan);
}
/* -- Show password button ------------------------------------------------ */
.pf-v5-c-button.pf-m-control {
background: var(--hk-input-bg) !important;
border: 1px solid var(--hk-border) !important;
border-left: none !important;
border-radius: 0 !important;
color: var(--hk-text-dim) !important;
}
.pf-v5-c-button.pf-m-control:hover {
color: var(--hk-green) !important;
}
/* -- Social providers (Google, etc) -------------------------------------- */
.pf-v5-c-login__main-footer {
border-top: 1px solid var(--hk-border) !important;
}
.kc-social-section .pf-v5-c-button.pf-m-secondary {
border-color: var(--hk-text-dim) !important;
color: var(--hk-text) !important;
}
.kc-social-section .pf-v5-c-button.pf-m-secondary:hover {
border-color: var(--hk-green) !important;
color: var(--hk-green) !important;
box-shadow: 0 0 10px var(--hk-green-glow) !important;
}
/* -- Try another way link ------------------------------------------------ */
#try-another-way {
color: var(--hk-cyan) !important;
text-decoration: none !important;
font-family: var(--hk-font) !important;
}
#try-another-way:hover {
text-shadow: 0 0 8px rgba(0, 229, 255, 0.5);
}
/* -- Links --------------------------------------------------------------- */
a {
color: var(--hk-cyan) !important;
text-decoration: none;
transition: text-shadow 0.2s;
}
a:hover {
text-shadow: 0 0 6px rgba(0, 229, 255, 0.4);
}
/* -- Alerts / messages --------------------------------------------------- */
.pf-v5-c-alert {
background: rgba(0, 255, 65, 0.05) !important;
border: 1px solid var(--hk-border) !important;
border-radius: 0 !important;
font-family: var(--hk-font) !important;
}
.pf-v5-c-alert.pf-m-danger {
border-color: var(--hk-red) !important;
background: rgba(255, 51, 51, 0.05) !important;
}
.pf-v5-c-alert__title {
color: var(--hk-text) !important;
font-family: var(--hk-font) !important;
}
/* -- Checkbox (Remember me) ---------------------------------------------- */
.pf-v5-c-check__label {
color: var(--hk-text-dim) !important;
font-family: var(--hk-font) !important;
font-size: 0.8rem !important;
}
/* -- Helper text (Forgot password, etc.) --------------------------------- */
.pf-v5-c-helper-text .pf-v5-c-button.pf-m-link {
color: var(--hk-cyan) !important;
font-family: var(--hk-font) !important;
font-size: 0.8rem !important;
}
/* -- Select auth list (Try another way options) -------------------------- */
.select-auth-container {
background: var(--hk-panel) !important;
font-family: var(--hk-font) !important;
}
.pf-v5-c-data-list__item {
border-color: var(--hk-border) !important;
background: transparent !important;
}
.pf-v5-c-data-list__item:hover {
background: var(--hk-green-glow) !important;
}
.select-auth-box-headline {
color: var(--hk-green) !important;
}
.select-auth-box-desc {
color: var(--hk-text-dim) !important;
}
/* -- Footer -------------------------------------------------------------- */
.pf-v5-c-login__main-footer-band {
background: transparent !important;
border-top: 1px solid var(--hk-border) !important;
}
/* -- Language selector --------------------------------------------------- */
select#login-select-toggle {
background: var(--hk-input-bg) !important;
color: var(--hk-green) !important;
border-color: var(--hk-border) !important;
font-family: var(--hk-font) !important;
}
/* -- Blinking cursor animation for header -------------------------------- */
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
#kc-header-wrapper::after {
content: '_';
animation: blink 1s step-end infinite;
color: var(--hk-green);
}
/* -- Subtle CRT flicker ------------------------------------------------- */
@keyframes flicker {
0% { opacity: 0.97; }
5% { opacity: 0.95; }
10% { opacity: 0.98; }
15% { opacity: 0.96; }
20% { opacity: 0.99; }
100% { opacity: 0.98; }
}
.pf-v5-c-login__main {
animation: flicker 4s infinite;
}
+16
View File
@@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- secrets.yaml
- hacker-theme-configmap.yaml
- auth-proxy.yaml
# ConfigMap auth-proxy-routes is managed by Terraform (terraform/keycloak, kubernetes_config_map_v1).
helmCharts:
- name: keycloakx
repo: https://codecentric.github.io/helm-charts
version: 7.3.1
releaseName: keycloak
namespace: keycloak
valuesFile: values.yaml
+80
View File
@@ -0,0 +1,80 @@
# Read-only access to the keycloak-db Secret in the psql namespace
# (see postgresql/databases/keycloak.yaml for the other half).
apiVersion: v1
kind: ServiceAccount
metadata:
name: db-credentials-reader
---
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: psql-credentials
spec:
provider:
kubernetes:
remoteNamespace: psql
server:
caProvider:
type: ConfigMap
name: kube-root-ca.crt
key: ca.crt
auth:
serviceAccount:
name: db-credentials-reader
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: keycloak-creds
spec:
refreshInterval: 1h
secretStoreRef:
kind: SecretStore
name: psql-credentials
target:
name: keycloak-creds
template:
data:
KC_DB_USERNAME: "{{ .username }}"
KC_DB_PASSWORD: "{{ .password }}"
data:
- secretKey: username
remoteRef:
key: keycloak-db
property: username
- secretKey: password
remoteRef:
key: keycloak-db
property: password
---
# Bootstrap admin: only used when the database has no admin user yet (a fresh
# install). With the restored database the existing admin accounts are kept.
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
metadata:
name: keycloak-bootstrap
spec:
length: 40
digits: 10
symbols: 0
noUpper: false
allowRepeat: true
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: keycloak-bootstrap
spec:
refreshPolicy: CreatedOnce
target:
name: keycloak-bootstrap
template:
data:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: "{{ .password }}"
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
name: keycloak-bootstrap
+75
View File
@@ -0,0 +1,75 @@
replicas: 1
image:
repository: quay.io/keycloak/keycloak
tag: "26.5.6"
command:
- "/opt/keycloak/bin/kc.sh"
- "start"
- "--http-port=8080"
- "--hostname-strict=false"
- "--proxy-headers=xforwarded"
extraEnvFrom: |
- secretRef:
name: keycloak-creds
- secretRef:
name: keycloak-bootstrap
extraEnv: |
- name: KC_HOSTNAME
value: auth.hexor.cy
- name: JAVA_OPTS_APPEND
value: "-Djgroups.dns.query=keycloak-keycloakx-headless.keycloak.svc"
extraVolumes: |
- name: hacker-theme
configMap:
name: keycloak-hacker-theme
extraVolumeMounts: |
- name: hacker-theme
mountPath: /opt/keycloak/themes/hacker/login/theme.properties
subPath: theme.properties
- name: hacker-theme
mountPath: /opt/keycloak/themes/hacker/login/css/hacker.css
subPath: hacker.css
dbchecker:
enabled: true
database:
vendor: postgres
hostname: main-rw.psql.svc
port: 5432
database: keycloak
existingSecret: keycloak-creds
existingSecretKey: KC_DB_PASSWORD
service:
type: ClusterIP
# HTTP is redirected to HTTPS by the cluster-wide Traefik default (core/traefik-config).
ingress:
enabled: true
ingressClassName: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt
rules:
- host: auth.hexor.cy
paths:
- path: /
pathType: Prefix
tls:
- secretName: keycloak-tls
hosts:
- auth.hexor.cy
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "3"
memory: 2Gi
+48
View File
@@ -0,0 +1,48 @@
# Main HA PostgreSQL cluster: one primary and one streaming replica, each on its
# own node with a node-local volume. Replication is asynchronous (nodes sit in
# different regions), so a failover can lose the last few transactions.
#
# Nodes eligible for instances carry the label workload/postgres=true. To move a
# replica: label the new node, unlabel the old one, then recreate the instance
# (kubectl cnpg destroy main <N>); the operator re-clones it from the primary.
#
# Endpoints inside the cluster: main-rw (primary), main-ro (replicas),
# main-r (any instance) in the psql namespace.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: main
namespace: psql
spec:
description: Main HA PostgreSQL cluster
imageName: ghcr.io/cloudnative-pg/postgresql:17
instances: 2
enableSuperuserAccess: false
# Roll updates replica-first and hand over by switchover instead of restarting the primary.
primaryUpdateStrategy: unsupervised
primaryUpdateMethod: switchover
storage:
storageClass: local-path
size: 20Gi # local-path volumes cannot be expanded later
affinity:
enablePodAntiAffinity: true
podAntiAffinityType: required
topologyKey: kubernetes.io/hostname
nodeSelector:
workload/postgres: "true"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
memory: 2Gi
postgresql:
parameters:
max_connections: "200"
shared_buffers: 512MB
effective_cache_size: 1536MB
@@ -0,0 +1,83 @@
# Database "furumi": password, role, database and read access for the app.
# To add another database copy this file and replace the name.
# Generated once; the role and the application both read this Secret.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: furumi-db
namespace: psql
spec:
refreshPolicy: CreatedOnce
target:
name: furumi-db
template:
type: kubernetes.io/basic-auth
metadata:
labels:
cnpg.io/reload: "true"
data:
username: furumi
password: "{{ .password }}"
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
name: db-password
---
apiVersion: postgresql.cnpg.io/v1
kind: DatabaseRole
metadata:
name: main-furumi
namespace: psql
spec:
cluster:
name: main
name: furumi
login: true
passwordSecret:
name: furumi-db
---
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: main-furumi
namespace: psql
spec:
cluster:
name: main
name: furumi
owner: furumi
extensions:
- name: pg_trgm
databaseReclaimPolicy: retain
---
# Lets the furumi namespace read only its own credentials Secret.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: furumi-db-reader
namespace: psql
rules:
- apiGroups: [""]
resources: [secrets]
resourceNames: [furumi-db]
verbs: [get, list, watch]
- apiGroups: [authorization.k8s.io]
resources: [selfsubjectrulesreviews]
verbs: [create]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: furumi-db-reader
namespace: psql
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: furumi-db-reader
subjects:
- kind: ServiceAccount
name: db-credentials-reader
namespace: furumi
@@ -0,0 +1,81 @@
# Database "keycloak": password, role, database and read access for the app.
# To add another database copy this file and replace the name.
# Generated once; the role and the application both read this Secret.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: keycloak-db
namespace: psql
spec:
refreshPolicy: CreatedOnce
target:
name: keycloak-db
template:
type: kubernetes.io/basic-auth
metadata:
labels:
cnpg.io/reload: "true"
data:
username: keycloak
password: "{{ .password }}"
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
name: db-password
---
apiVersion: postgresql.cnpg.io/v1
kind: DatabaseRole
metadata:
name: main-keycloak
namespace: psql
spec:
cluster:
name: main
name: keycloak
login: true
passwordSecret:
name: keycloak-db
---
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: main-keycloak
namespace: psql
spec:
cluster:
name: main
name: keycloak
owner: keycloak
databaseReclaimPolicy: retain
---
# Lets the keycloak namespace read only its own credentials Secret.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: keycloak-db-reader
namespace: psql
rules:
- apiGroups: [""]
resources: [secrets]
resourceNames: [keycloak-db]
verbs: [get, list, watch]
- apiGroups: [authorization.k8s.io]
resources: [selfsubjectrulesreviews]
verbs: [create]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: keycloak-db-reader
namespace: psql
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: keycloak-db-reader
subjects:
- kind: ServiceAccount
name: db-credentials-reader
namespace: keycloak
@@ -0,0 +1,81 @@
# Database "vaultwarden": password, role, database and read access for the app.
# To add another database copy this file and replace the name.
# Generated once; the role and the application both read this Secret.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: vaultwarden-db
namespace: psql
spec:
refreshPolicy: CreatedOnce
target:
name: vaultwarden-db
template:
type: kubernetes.io/basic-auth
metadata:
labels:
cnpg.io/reload: "true"
data:
username: vaultwarden
password: "{{ .password }}"
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
name: db-password
---
apiVersion: postgresql.cnpg.io/v1
kind: DatabaseRole
metadata:
name: main-vaultwarden
namespace: psql
spec:
cluster:
name: main
name: vaultwarden
login: true
passwordSecret:
name: vaultwarden-db
---
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: main-vaultwarden
namespace: psql
spec:
cluster:
name: main
name: vaultwarden
owner: vaultwarden
databaseReclaimPolicy: retain
---
# Lets the vaultwarden namespace read only its own credentials Secret.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: vaultwarden-db-reader
namespace: psql
rules:
- apiGroups: [""]
resources: [secrets]
resourceNames: [vaultwarden-db]
verbs: [get, list, watch]
- apiGroups: [authorization.k8s.io]
resources: [selfsubjectrulesreviews]
verbs: [create]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: vaultwarden-db-reader
namespace: psql
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: vaultwarden-db-reader
subjects:
- kind: ServiceAccount
name: db-credentials-reader
namespace: vaultwarden
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- cluster.yaml
- password-generator.yaml
- databases/vaultwarden.yaml
- databases/keycloak.yaml
- databases/furumi.yaml
@@ -0,0 +1,12 @@
# Random passwords for database roles. Referenced by the ExternalSecrets in databases/.
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
metadata:
name: db-password
namespace: psql
spec:
length: 40
digits: 10
symbols: 0 # the password ends up inside a connection URL
noUpper: false
allowRepeat: true
+10
View File
@@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
helmCharts:
- name: reloader
repo: https://stakater.github.io/stakater-charts
version: 2.2.18
releaseName: reloader
namespace: reloader
valuesFile: values.yaml
+24
View File
@@ -0,0 +1,24 @@
reloader:
watchGlobally: true
autoReloadAll: true
reloadOnCreate: true
reloadOnDelete: false
reloadStrategy: annotations
ignoreConfigMaps: false
ignoreSecrets: false
ignoreJobs: false
ignoreCronJobs: false
enableHA: true
syncAfterRestart: true
logLevel: info
rbac:
enabled: true
deployment:
replicas: 2
resources:
requests:
cpu: 10m
memory: 128Mi
limits:
cpu: 150m
memory: 512Mi
@@ -0,0 +1,26 @@
FROM debian:sid
ENV BW_CLI_VERSION=2025.12.1
RUN apt update && \
apt install -y wget unzip && \
wget https://github.com/bitwarden/clients/releases/download/cli-v${BW_CLI_VERSION}/bw-linux-${BW_CLI_VERSION}.zip && \
unzip bw-linux-${BW_CLI_VERSION}.zip && \
chmod +x bw && \
mv bw /usr/local/bin/bw && \
rm -rfv *.zip && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Build entrypoint script
RUN echo '#!/bin/sh\n\
set -e\n\
bw config server ${BW_HOST}\n\
export BW_SESSION=$(bw login ${BW_USER} --passwordenv BW_PASSWORD --raw)\n\
bw unlock --check\n\
echo "Running \`bw server\` on port 8087"\n\
bw serve --hostname 0.0.0.0\n' > /entrypoint.sh && \
chmod +x /entrypoint.sh
CMD ["/entrypoint.sh"]
@@ -0,0 +1,123 @@
# Bridge between External Secrets and Vaultwarden: `bw serve` logged in as the
# bot account. Needs the Secret bitwarden-cli (created by hand; the bot logs in
# with email + password, so keep password login enabled for it, i.e. no
# SSO_ONLY, or switch the image to API-key login):
# kubectl -n external-secrets create secret generic bitwarden-cli \
# --from-literal=BW_HOST=https://vw.btwiusearch.net \
# --from-literal=BW_USERNAME=<bot email> --from-literal=BW_PASSWORD=<password>
# Image source: Dockerfile-bitwarden-cli in this directory.
apiVersion: apps/v1
kind: Deployment
metadata:
name: bitwarden-cli
labels:
reloader.stakater.com/auto: "true"
app.kubernetes.io/instance: bitwarden-cli
app.kubernetes.io/name: bitwarden-cli
spec:
replicas: 1
strategy:
type: RollingUpdate
selector:
matchLabels:
app.kubernetes.io/name: bitwarden-cli
app.kubernetes.io/instance: bitwarden-cli
template:
metadata:
labels:
app.kubernetes.io/name: bitwarden-cli
app.kubernetes.io/instance: bitwarden-cli
spec:
nodeSelector:
kubernetes.io/arch: amd64
containers:
- name: bitwarden-cli
image: ultradesu/bitwarden-client:2025.12.1
imagePullPolicy: IfNotPresent
resources:
requests:
memory: "128Mi"
cpu: "300m"
limits:
memory: "512Mi"
cpu: "1000m"
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
livenessProbe:
exec:
command:
- wget
- -q
- http://127.0.0.1:8087/sync
- --post-data=''
initialDelaySeconds: 20
failureThreshold: 3
timeoutSeconds: 1
periodSeconds: 120
readinessProbe:
tcpSocket:
port: 8087
initialDelaySeconds: 20
failureThreshold: 3
timeoutSeconds: 1
periodSeconds: 10
startupProbe:
tcpSocket:
port: 8087
initialDelaySeconds: 10
failureThreshold: 30
timeoutSeconds: 1
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: bitwarden-cli
labels:
app.kubernetes.io/instance: bitwarden-cli
app.kubernetes.io/name: bitwarden-cli
spec:
type: ClusterIP
ports:
- port: 8087
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: bitwarden-cli
app.kubernetes.io/instance: bitwarden-cli
---
# Only the External Secrets controller may talk to the bridge: it serves the whole vault.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: external-secret-2-bw-cli
spec:
podSelector:
matchLabels:
app.kubernetes.io/instance: bitwarden-cli
app.kubernetes.io/name: bitwarden-cli
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/instance: external-secrets
app.kubernetes.io/name: external-secrets
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- bitwarden-cli.yaml
- stores.yaml
+47
View File
@@ -0,0 +1,47 @@
# ClusterSecretStores in front of the Vaultwarden vault that belongs to the bot
# account (the vault holds server secrets only, nothing personal).
#
# `key` may be either
# - the item UUID (old style, changes whenever items are imported again), or
# - the item path: the item NAME, which by convention is the full path
# k8s/<namespace>/<what> e.g. k8s/cert-manager/cloudflare-api-token
# Names are looked up by exact match (a name that is a substring of another
# item name, login or URL is fine). Do not use double quotes in names.
#
# vaultwarden-login: `property` is a path inside the item: login.username,
# login.password, notes, fields[0].value, ...
# vaultwarden-fields: `property` is the NAME of a custom field of the item.
#
# data:
# - secretKey: password
# sourceRef:
# storeRef: {name: vaultwarden-login, kind: ClusterSecretStore}
# remoteRef:
# key: k8s/cert-manager/cloudflare-api-token
# property: login.password
#
# The bridge syncs the vault every couple of minutes, so a new item becomes
# visible to ESO after that.
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: vaultwarden-login
spec:
provider:
webhook:
url: 'http://bitwarden-cli.external-secrets.svc:8087/{{ if regexMatch "^[0-9a-fA-F-]{36}$" .remoteRef.key }}object/item/{{ .remoteRef.key }}{{ else }}list/object/items?search={{ .remoteRef.key }}{{ end }}'
headers:
Content-Type: application/json
result:
jsonPath: '{{ if regexMatch "^[0-9a-fA-F-]{36}$" .remoteRef.key }}$.data.{{ .remoteRef.property }}{{ else }}$.data.data[?@.name=="{{ .remoteRef.key }}"].{{ .remoteRef.property }}{{ end }}'
---
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: vaultwarden-fields
spec:
provider:
webhook:
url: 'http://bitwarden-cli.external-secrets.svc:8087/{{ if regexMatch "^[0-9a-fA-F-]{36}$" .remoteRef.key }}object/item/{{ .remoteRef.key }}{{ else }}list/object/items?search={{ .remoteRef.key }}{{ end }}'
result:
jsonPath: '{{ if regexMatch "^[0-9a-fA-F-]{36}$" .remoteRef.key }}$.data.fields[?@.name=="{{ .remoteRef.property }}"].value{{ else }}$.data.data[?@.name=="{{ .remoteRef.key }}"].fields[?@.name=="{{ .remoteRef.property }}"].value{{ end }}'
@@ -0,0 +1,16 @@
# Configuration of the Traefik that ships with k3s.
# Ingresses without a router.entrypoints annotation now use only "websecure", so
# plain HTTP is redirected to HTTPS by default (see http-redirect.yaml).
# To publish an Ingress over plain HTTP add the annotation
# traefik.ingress.kubernetes.io/router.entrypoints: web
# (or "web,websecure" to serve both without a redirect).
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
ports:
websecure:
asDefault: true
@@ -0,0 +1,29 @@
# Catch-all on the "web" entrypoint with the lowest priority: every host that has
# no explicit HTTP router of its own is redirected to HTTPS.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
namespace: kube-system
spec:
redirectScheme:
scheme: https
permanent: true
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: http-redirect
namespace: kube-system
spec:
entryPoints:
- web
routes:
- match: HostRegexp(`.+`)
kind: Rule
priority: 1
middlewares:
- name: redirect-https
services:
- name: noop@internal
kind: TraefikService
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmchartconfig.yaml
- http-redirect.yaml
+77
View File
@@ -0,0 +1,77 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: vaultwarden
labels:
app: vaultwarden
spec:
replicas: 1
strategy:
type: Recreate # the RWO local volume cannot be mounted by two pods
selector:
matchLabels:
app: vaultwarden
template:
metadata:
labels:
app: vaultwarden
spec:
containers:
- name: vaultwarden
image: vaultwarden/server:1.37.3
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 512Mi
# SSO is switched on by the optional Secret vaultwarden-sso, which holds
# SSO_ENABLED=true and SSO_CLIENT_SECRET (the secret of the Keycloak client
# "Vaultwarden"). Without it Vaultwarden starts with SSO off.
envFrom:
- secretRef:
name: vaultwarden-sso
optional: true
env:
- name: DOMAIN
value: https://vw.btwiusearch.net
- name: ORG_GROUPS_ENABLED
value: "true"
- name: SSO_AUTHORITY
value: https://auth.hexor.cy/auth/realms/hexor
- name: SSO_CLIENT_ID
value: Vaultwarden
- name: SSO_SCOPES
value: openid profile email offline_access
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: vaultwarden-db-url
key: DATABASE_URL
- name: ADMIN_TOKEN
valueFrom:
secretKeyRef:
name: admin-token
key: ADMIN_TOKEN
ports:
- name: http
containerPort: 80
protocol: TCP
readinessProbe:
httpGet:
path: /alive
port: http
periodSeconds: 10
livenessProbe:
httpGet:
path: /alive
port: http
initialDelaySeconds: 30
periodSeconds: 30
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: vaultwarden-data
+23
View File
@@ -0,0 +1,23 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vaultwarden
annotations:
cert-manager.io/cluster-issuer: letsencrypt
traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec:
rules:
- host: vw.btwiusearch.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vaultwarden
port:
number: 80
tls:
- secretName: vaultwarden-tls
hosts:
- vw.btwiusearch.net
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- secrets.yaml
- pvc.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
+12
View File
@@ -0,0 +1,12 @@
# Attachments, sends, icon cache and RSA keys live here; the database is in PostgreSQL.
# A local-path volume is bound to one node: the pod stays on the node it first ran on.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vaultwarden-data
spec:
accessModes: [ReadWriteOnce]
storageClassName: local-path
resources:
requests:
storage: 2Gi
+78
View File
@@ -0,0 +1,78 @@
# Read-only access to the vaultwarden-db Secret in the psql namespace
# (see postgresql/databases/vaultwarden.yaml for the other half).
apiVersion: v1
kind: ServiceAccount
metadata:
name: db-credentials-reader
---
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: psql-credentials
spec:
provider:
kubernetes:
remoteNamespace: psql
server:
caProvider:
type: ConfigMap
name: kube-root-ca.crt
key: ca.crt
auth:
serviceAccount:
name: db-credentials-reader
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: vaultwarden-db-url
spec:
refreshInterval: 1h
secretStoreRef:
kind: SecretStore
name: psql-credentials
target:
name: vaultwarden-db-url
template:
data:
DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@main-rw.psql.svc:5432/vaultwarden"
data:
- secretKey: username
remoteRef:
key: vaultwarden-db
property: username
- secretKey: password
remoteRef:
key: vaultwarden-db
property: password
---
# Admin panel token (/admin). Generated once; read it with:
# kubectl -n vaultwarden get secret admin-token -o jsonpath='{.data.ADMIN_TOKEN}' | base64 -d
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
metadata:
name: admin-token
spec:
length: 48
digits: 10
symbols: 0
noUpper: false
allowRepeat: true
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: admin-token
spec:
refreshPolicy: CreatedOnce
target:
name: admin-token
template:
data:
ADMIN_TOKEN: "{{ .password }}"
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
name: admin-token
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: vaultwarden
spec:
selector:
app: vaultwarden
ports:
- protocol: TCP
port: 80
targetPort: http
+20
View File
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: airtrail
namespace: argocd
spec:
project: apps
destination:
namespace: airtrail
server: https://kubernetes.default.svc
source:
repoURL: ssh://git@gt.hexor.cy:30022/ab/homelab.git
targetRevision: HEAD
path: k8s/apps/airtrail
syncPolicy:
automated:
selfHeal: true
prune: true
syncOptions:
- CreateNamespace=true
+66
View File
@@ -0,0 +1,66 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: airtrail
labels:
app: airtrail
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: airtrail
template:
metadata:
labels:
app: airtrail
spec:
securityContext:
fsGroup: 1000
containers:
- name: airtrail
image: johly/airtrail:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 3000
env:
- name: ORIGIN
value: https://at.hexor.cy
- name: DB_URL
valueFrom:
secretKeyRef:
name: airtrail-creds
key: DB_URL
- name: UPLOAD_LOCATION
value: /app/uploads
- name: BODY_SIZE_LIMIT
value: 20M
volumeMounts:
- name: uploads
mountPath: /app/uploads
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
startupProbe:
tcpSocket:
port: http
periodSeconds: 10
failureThreshold: 30
readinessProbe:
tcpSocket:
port: http
periodSeconds: 10
livenessProbe:
tcpSocket:
port: http
periodSeconds: 30
volumes:
- name: uploads
persistentVolumeClaim:
claimName: airtrail-uploads
+25
View File
@@ -0,0 +1,25 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: airtrail-creds
spec:
target:
name: airtrail-creds
deletionPolicy: Delete
template:
type: Opaque
data:
DB_URL: |-
postgresql://airtrail:{{ .airtrail | trim | urlquery | replace "+" "%20" }}@psql.psql.svc:5432/airtrail
data:
- secretKey: airtrail
sourceRef:
storeRef:
name: vaultwarden-login
kind: ClusterSecretStore
remoteRef:
conversionStrategy: Default
decodingStrategy: None
metadataPolicy: None
key: 2a9deb39-ef22-433e-a1be-df1555625e22
property: fields[21].value
+25
View File
@@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: airtrail-tls-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt
traefik.ingress.kubernetes.io/router.middlewares: kube-system-https-redirect@kubernetescrd
acme.cert-manager.io/http01-edit-in-place: "true"
spec:
ingressClassName: traefik
rules:
- host: at.hexor.cy
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: airtrail
port:
number: 3000
tls:
- secretName: airtrail-tls
hosts:
- at.hexor.cy
+10
View File
@@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- app.yaml
- external-secrets.yaml
- storage.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: airtrail
labels:
app: airtrail
spec:
type: ClusterIP
selector:
app: airtrail
ports:
- name: http
protocol: TCP
port: 3000
targetPort: http
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: airtrail-uploads
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 1Gi
+195 -36
View File
@@ -8,9 +8,12 @@ data:
#!/usr/bin/env bash
set -euo pipefail
PORT="${1:-5847}"
PORT="${1:-5848}"
VPN_CIDR="${2:-10.8.0.0/16}"
POLICY_FILE="${3:-/run/amnezia/policy/policy.conf}"
FORWARD_CHAIN="AMNEZIAWG-FORWARD"
POLICY_CHAIN_A="AMNEZIAWG-POLICY-A"
POLICY_CHAIN_B="AMNEZIAWG-POLICY-B"
IPTABLES=(iptables -w 30)
external_interface() {
@@ -49,10 +52,90 @@ data:
done
}
reset_chain() {
local chain="$1"
"${IPTABLES[@]}" -N "${chain}" >/dev/null 2>&1 || true
"${IPTABLES[@]}" -F "${chain}"
ensure_chain() {
"${IPTABLES[@]}" -N "$1" >/dev/null 2>&1 || true
}
valid_ipv4_32() {
local value="$1"
[[ "${value}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/32$ ]] || return 1
local address="${value%/32}"
local octet
IFS=. read -r -a octets <<< "${address}"
[ "${#octets[@]}" -eq 4 ] || return 1
for octet in "${octets[@]}"; do
[[ "${octet}" =~ ^[0-9]+$ ]] || return 1
[ "${octet}" -le 255 ] || return 1
done
}
routed_through_awg0() {
ip -4 route get "${1%/32}" 2>/dev/null | grep -Eq '(^|[[:space:]])dev awg0([[:space:]]|$)'
}
active_policy_chain() {
"${IPTABLES[@]}" -S "${FORWARD_CHAIN}" 2>/dev/null \
| awk '$1 == "-A" && $3 == "-j" && $4 ~ /^AMNEZIAWG-POLICY-[AB]$/ { print $4; exit }'
}
apply_policy() {
local active inactive source destination extra
active="$(active_policy_chain || true)"
if [ "${active}" = "${POLICY_CHAIN_A}" ]; then
inactive="${POLICY_CHAIN_B}"
else
inactive="${POLICY_CHAIN_A}"
fi
ensure_chain "${inactive}"
"${IPTABLES[@]}" -F "${inactive}"
if [ -f "${POLICY_FILE}" ]; then
while read -r source destination extra; do
[ -n "${source:-}" ] || continue
[[ "${source}" == \#* ]] && continue
if [ -n "${extra:-}" ] \
|| ! valid_ipv4_32 "${source}" \
|| ! valid_ipv4_32 "${destination:-}"; then
echo "Skipping malformed policy line: ${source:-} ${destination:-} ${extra:-}" >&2
continue
fi
# A pair whose endpoints are not (yet) routed through awg0 is skipped
# rather than fatal. During interface bring-up the awg0 route may not be
# in place, and a missing ACCEPT simply leaves the pair subject to the
# client-isolation DROP below (fail-closed) instead of aborting the whole
# policy and crash-looping the container.
if ! routed_through_awg0 "${source}" || ! routed_through_awg0 "${destination}"; then
echo "Skipping out-of-tunnel policy pair: ${source} ${destination}" >&2
continue
fi
"${IPTABLES[@]}" -A "${inactive}" \
-i awg0 -o awg0 -s "${source}" -d "${destination}" \
-m comment --comment amneziawg-group-pair -j ACCEPT
done < "${POLICY_FILE}"
fi
# This DROP is deliberately restricted to packets entering and leaving
# awg0. It cannot match Kubernetes, CNI, kubelet, Tailscale, or host traffic.
"${IPTABLES[@]}" -A "${inactive}" \
-i awg0 -o awg0 -s "${VPN_CIDR}" -d "${VPN_CIDR}" \
-m comment --comment amneziawg-client-isolation -j DROP
"${IPTABLES[@]}" -A "${inactive}" \
-i awg0 -o "${EXT_IF}" -s "${VPN_CIDR}" \
-m conntrack --ctstate NEW,ESTABLISHED,RELATED \
-m comment --comment amneziawg-internet-out -j ACCEPT
"${IPTABLES[@]}" -A "${inactive}" \
-i "${EXT_IF}" -o awg0 -d "${VPN_CIDR}" \
-m conntrack --ctstate ESTABLISHED,RELATED \
-m comment --comment amneziawg-internet-return -j ACCEPT
"${IPTABLES[@]}" -A "${inactive}" -j RETURN
# Insert the complete new policy before removing the old jump. There is
# never a window with a partially built or absent active policy.
"${IPTABLES[@]}" -I "${FORWARD_CHAIN}" 1 -j "${inactive}"
if [ -n "${active}" ]; then
"${IPTABLES[@]}" -D "${FORWARD_CHAIN}" -j "${active}"
fi
}
EXT_IF="$(external_interface || true)"
@@ -65,6 +148,7 @@ data:
fi
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv4.conf.awg0.send_redirects=0
# Remove rules written by older revisions. In particular, the unqualified
# tailscale UDP DROP also blocks Flannel VXLAN (UDP/8472).
@@ -79,11 +163,21 @@ data:
"${IPTABLES[@]}" -F AMNEZIAWG-INPUT >/dev/null 2>&1 || true
"${IPTABLES[@]}" -X AMNEZIAWG-INPUT >/dev/null 2>&1 || true
reset_chain "${FORWARD_CHAIN}"
"${IPTABLES[@]}" -A "${FORWARD_CHAIN}" -i awg0 -m comment --comment amneziawg-forward-in -j ACCEPT
"${IPTABLES[@]}" -A "${FORWARD_CHAIN}" -o awg0 -m comment --comment amneziawg-forward-out -j ACCEPT
"${IPTABLES[@]}" -A "${FORWARD_CHAIN}" -j RETURN
ensure_rule -A FORWARD -m comment --comment amneziawg-forward-jump -j "${FORWARD_CHAIN}"
ensure_chain "${FORWARD_CHAIN}"
ensure_chain "${POLICY_CHAIN_A}"
ensure_chain "${POLICY_CHAIN_B}"
if ! "${IPTABLES[@]}" -S "${FORWARD_CHAIN}" 2>/dev/null | grep -q -- '-j RETURN'; then
"${IPTABLES[@]}" -A "${FORWARD_CHAIN}" -j RETURN
fi
apply_policy
delete_rule "${FORWARD_CHAIN}" -i awg0 -m comment --comment amneziawg-forward-in -j ACCEPT
delete_rule "${FORWARD_CHAIN}" -o awg0 -m comment --comment amneziawg-forward-out -j ACCEPT
# The jump is first, but non-awg traffic immediately RETURNs to the original
# host FORWARD chain without an ACCEPT or DROP decision.
delete_rule FORWARD -m comment --comment amneziawg-forward-jump -j "${FORWARD_CHAIN}"
"${IPTABLES[@]}" -I FORWARD 1 \
-m comment --comment amneziawg-forward-jump -j "${FORWARD_CHAIN}"
ensure_rule -A -t nat POSTROUTING -s "${VPN_CIDR}" -o "${EXT_IF}" -m comment --comment amneziawg-masquerade -j MASQUERADE
@@ -93,9 +187,11 @@ data:
#!/usr/bin/env bash
set -euo pipefail
PORT="${1:-5847}"
PORT="${1:-5848}"
VPN_CIDR="${2:-10.8.0.0/16}"
FORWARD_CHAIN="AMNEZIAWG-FORWARD"
POLICY_CHAIN_A="AMNEZIAWG-POLICY-A"
POLICY_CHAIN_B="AMNEZIAWG-POLICY-B"
IPTABLES=(iptables -w 30)
external_interface() {
@@ -139,13 +235,17 @@ data:
"${IPTABLES[@]}" -F AMNEZIAWG-INPUT >/dev/null 2>&1 || true
"${IPTABLES[@]}" -X AMNEZIAWG-INPUT >/dev/null 2>&1 || true
"${IPTABLES[@]}" -F "${FORWARD_CHAIN}" >/dev/null 2>&1 || true
"${IPTABLES[@]}" -F "${POLICY_CHAIN_A}" >/dev/null 2>&1 || true
"${IPTABLES[@]}" -F "${POLICY_CHAIN_B}" >/dev/null 2>&1 || true
"${IPTABLES[@]}" -X "${POLICY_CHAIN_A}" >/dev/null 2>&1 || true
"${IPTABLES[@]}" -X "${POLICY_CHAIN_B}" >/dev/null 2>&1 || true
"${IPTABLES[@]}" -X "${FORWARD_CHAIN}" >/dev/null 2>&1 || true
firewall-check.sh: |
#!/usr/bin/env bash
set -euo pipefail
PORT="${1:-5847}"
PORT="${1:-5848}"
VPN_CIDR="${2:-10.8.0.0/16}"
FORWARD_CHAIN="AMNEZIAWG-FORWARD"
IPTABLES=(iptables -w 5)
@@ -176,11 +276,30 @@ data:
exit 1
fi
# The amneziawg FORWARD jump must be present. firewall-up inserts it at
# position 1, but CNI controllers (e.g. kube-router) legitimately re-assert
# their own jump ahead of it. Requiring the strict first position makes the
# readiness probe flap as the two fight over rule order. Isolation still holds
# as long as the jump is traversed and no earlier rule accepts awg0-to-awg0
# traffic (kube-router's chain returns non-pod traffic without a decision).
"${IPTABLES[@]}" -C FORWARD -m comment --comment amneziawg-forward-jump -j "${FORWARD_CHAIN}"
"${IPTABLES[@]}" -C "${FORWARD_CHAIN}" -i awg0 -m comment --comment amneziawg-forward-in -j ACCEPT
"${IPTABLES[@]}" -C "${FORWARD_CHAIN}" -o awg0 -m comment --comment amneziawg-forward-out -j ACCEPT
ACTIVE_POLICY="$("${IPTABLES[@]}" -S "${FORWARD_CHAIN}" 2>/dev/null \
| awk '$1 == "-A" && $3 == "-j" && $4 ~ /^AMNEZIAWG-POLICY-[AB]$/ { print $4; exit }')"
[ -n "${ACTIVE_POLICY}" ]
"${IPTABLES[@]}" -C "${ACTIVE_POLICY}" \
-i awg0 -o awg0 -s "${VPN_CIDR}" -d "${VPN_CIDR}" \
-m comment --comment amneziawg-client-isolation -j DROP
"${IPTABLES[@]}" -C "${ACTIVE_POLICY}" \
-i awg0 -o "${EXT_IF}" -s "${VPN_CIDR}" \
-m conntrack --ctstate NEW,ESTABLISHED,RELATED \
-m comment --comment amneziawg-internet-out -j ACCEPT
"${IPTABLES[@]}" -C "${ACTIVE_POLICY}" \
-i "${EXT_IF}" -o awg0 -d "${VPN_CIDR}" \
-m conntrack --ctstate ESTABLISHED,RELATED \
-m comment --comment amneziawg-internet-return -j ACCEPT
"${IPTABLES[@]}" -t nat -C POSTROUTING -s "${VPN_CIDR}" -o "${EXT_IF}" -m comment --comment amneziawg-masquerade -j MASQUERADE
[ "$(sysctl -n net.ipv4.conf.awg0.send_redirects)" = "0" ]
awg show awg0 >/dev/null 2>&1
run.sh: |
@@ -189,6 +308,8 @@ data:
SERVER_CONFIG="/etc/amnezia/server/awg0.conf"
CLIENTS_DIR="${AMNEZIAWG_CLIENTS_DIR:-/run/amnezia/clients}"
POLICY_FILE="${AMNEZIAWG_POLICY_FILE:-/run/amnezia/policy/policy.conf}"
CONFIG_GENERATION="${AMNEZIAWG_CONFIG_GENERATION:-/run/amnezia/config-generation}"
RUNTIME_CONFIG="/run/amnezia/awg0.conf"
SYNC_CONFIG="/run/amnezia/awg0.sync.conf"
STATUS_FILE="/run/amnezia/reload-status"
@@ -223,13 +344,12 @@ data:
chmod 0600 "${RUNTIME_CONFIG}"
}
client_config_hash() {
{
for client_config in "${CLIENTS_DIR}"/*; do
[ -f "${client_config}" ] || continue
sha256sum "${client_config}"
done
} | sha256sum | awk '{print $1}'
runtime_config_hash() {
if [ -f "${CONFIG_GENERATION}" ]; then
sha256sum "${CONFIG_GENERATION}" | awk '{print $1}'
else
printf 'missing\n'
fi
}
write_reload_status() {
@@ -250,10 +370,14 @@ data:
}
apply_live_config() {
# Fail closed during a peer/key and policy transition. /dev/null produces
# an Internet-only policy with all awg0-to-awg0 traffic isolated.
/scripts/firewall-up.sh 5848 10.8.0.0/16 /dev/null
render_config
awg-quick strip "${RUNTIME_CONFIG}" > "${SYNC_CONFIG}"
chmod 0600 "${SYNC_CONFIG}"
awg syncconf awg0 "${SYNC_CONFIG}"
/scripts/firewall-up.sh 5848 10.8.0.0/16 "${POLICY_FILE}"
}
watch_client_config() {
@@ -263,7 +387,7 @@ data:
wait "$!" || return 0
local current_hash
current_hash="$(client_config_hash)"
current_hash="$(runtime_config_hash)"
if [ "${current_hash}" = "${last_hash}" ]; then
continue
fi
@@ -283,7 +407,7 @@ data:
trap cleanup EXIT
trap 'exit 0' TERM INT
initial_hash="$(client_config_hash)"
initial_hash="$(runtime_config_hash)"
render_config
cleanup
awg-quick up "${RUNTIME_CONFIG}"
@@ -299,6 +423,8 @@ data:
CLIENT_SECRET_KEY="${AMNEZIAWG_CLIENT_SECRET_KEY:-peers.conf}"
CLIENTS_DIR="${AMNEZIAWG_CLIENTS_DIR:-/run/amnezia/clients}"
PEERS_FILE="${CLIENTS_DIR}/peers.conf"
POLICY_FILE="${AMNEZIAWG_POLICY_FILE:-/run/amnezia/policy/policy.conf}"
CONFIG_GENERATION="${AMNEZIAWG_CONFIG_GENERATION:-/run/amnezia/config-generation}"
SYNC_INTERVAL="${AMNEZIAWG_CLIENT_SECRET_SYNC_INTERVAL:-5}"
NAMESPACE="${POD_NAMESPACE:-$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)}"
@@ -308,33 +434,66 @@ data:
: > "${PEERS_FILE}"
chmod 0600 "${PEERS_FILE}"
fi
mkdir -p "$(dirname "${POLICY_FILE}")"
if [ ! -f "${POLICY_FILE}" ]; then
: > "${POLICY_FILE}"
chmod 0600 "${POLICY_FILE}"
fi
{
sha256sum "${PEERS_FILE}"
sha256sum "${POLICY_FILE}"
} | sha256sum | awk '{print $1}' > "${CONFIG_GENERATION}"
}
sync_once() {
mkdir -p "${CLIENTS_DIR}"
local tmp_file="${PEERS_FILE}.tmp"
local encoded=""
mkdir -p "$(dirname "${POLICY_FILE}")"
local snapshot="${CONFIG_GENERATION}.snapshot"
local peers_tmp="${PEERS_FILE}.tmp"
local policy_tmp="${POLICY_FILE}.tmp"
local peers_encoded policy_encoded generation
if ! encoded="$(kubectl get secret "${CLIENT_SECRET}" -n "${NAMESPACE}" -o "go-template={{ index .data \"${CLIENT_SECRET_KEY}\" }}" 2>/dev/null)"; then
if ! kubectl get secret "${CLIENT_SECRET}" -n "${NAMESPACE}" \
-o "go-template={{ with index .data \"${CLIENT_SECRET_KEY}\" }}{{ . }}{{ end }}{{ \"\n\" }}{{ with index .data \"policy.conf\" }}{{ . }}{{ end }}{{ \"\n\" }}" \
> "${snapshot}" 2>/dev/null; then
echo "WARN: failed to read Secret ${NAMESPACE}/${CLIENT_SECRET}; keeping current peers" >&2
rm -f "${snapshot}"
write_empty_once
return 0
fi
if [ -n "${encoded}" ]; then
printf '%s' "${encoded}" | base64 -d > "${tmp_file}"
else
: > "${tmp_file}"
fi
chmod 0600 "${tmp_file}"
peers_encoded="$(sed -n '1p' "${snapshot}")"
policy_encoded="$(sed -n '2p' "${snapshot}")"
rm -f "${snapshot}"
if [ -f "${PEERS_FILE}" ] && cmp -s "${tmp_file}" "${PEERS_FILE}"; then
rm -f "${tmp_file}"
if [ -n "${peers_encoded}" ]; then
printf '%s' "${peers_encoded}" | base64 -d > "${peers_tmp}"
else
: > "${peers_tmp}"
fi
if [ -n "${policy_encoded}" ]; then
printf '%s' "${policy_encoded}" | base64 -d > "${policy_tmp}"
else
# Missing policy is fail-closed: no client-to-client pairs are allowed.
: > "${policy_tmp}"
fi
chmod 0600 "${peers_tmp}" "${policy_tmp}"
generation="$({
sha256sum "${peers_tmp}"
sha256sum "${policy_tmp}"
} | sha256sum | awk '{print $1}')"
if [ -f "${CONFIG_GENERATION}" ] \
&& [ "$(sed -n '1p' "${CONFIG_GENERATION}")" = "${generation}" ]; then
rm -f "${peers_tmp}" "${policy_tmp}"
return 0
fi
mv "${tmp_file}" "${PEERS_FILE}"
echo "Synced AmneziaWG client peers from Secret ${NAMESPACE}/${CLIENT_SECRET}:${CLIENT_SECRET_KEY}"
mv "${policy_tmp}" "${POLICY_FILE}"
mv "${peers_tmp}" "${PEERS_FILE}"
printf '%s\n' "${generation}" > "${CONFIG_GENERATION}.tmp"
mv "${CONFIG_GENERATION}.tmp" "${CONFIG_GENERATION}"
echo "Synced AmneziaWG peers and policy from Secret ${NAMESPACE}/${CLIENT_SECRET}"
}
if [ "${1:-}" = "once" ]; then
+5 -5
View File
@@ -52,7 +52,7 @@ spec:
fieldRef:
fieldPath: spec.nodeName
- name: PORT
value: "5847"
value: "5848"
command:
- /bin/bash
- -lc
@@ -116,14 +116,14 @@ spec:
- /scripts/run.sh
ports:
- name: awg
containerPort: 5847
containerPort: 5848
protocol: UDP
readinessProbe:
exec:
command:
- /bin/bash
- -lc
- /scripts/firewall-check.sh 5847 10.8.0.0/16
- /scripts/firewall-check.sh 5848 10.8.0.0/16
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 8
@@ -133,7 +133,7 @@ spec:
command:
- /bin/bash
- -lc
- /scripts/firewall-check.sh 5847 10.8.0.0/16 || /scripts/firewall-up.sh 5847 10.8.0.0/16
- /scripts/firewall-check.sh 5848 10.8.0.0/16 || /scripts/firewall-up.sh 5848 10.8.0.0/16
periodSeconds: 5
timeoutSeconds: 15
failureThreshold: 12
@@ -142,7 +142,7 @@ spec:
command:
- /bin/bash
- -lc
- /scripts/firewall-check.sh 5847 10.8.0.0/16 || /scripts/firewall-up.sh 5847 10.8.0.0/16
- /scripts/firewall-check.sh 5848 10.8.0.0/16 || /scripts/firewall-up.sh 5848 10.8.0.0/16
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 15
+14 -14
View File
@@ -16,21 +16,21 @@ spec:
[Interface]
PrivateKey = {{ .server_private_key }}
Address = 10.8.0.1/16
ListenPort = 5847
ListenPort = 5848
MTU = 1376
Jc = 4
Jmin = 64
Jmax = 128
S1 = 15
S2 = 18
S3 = 20
S4 = 23
H1 = 1020325451
H2 = 3288052141
H3 = 1766607858
H4 = 2528465083
PostUp = /scripts/firewall-up.sh 5847 10.8.0.0/16
PostDown = /scripts/firewall-down.sh 5847 10.8.0.0/16
Jc = 5
Jmin = 96
Jmax = 320
S1 = 37
S2 = 47
S3 = 29
S4 = 43
H1 = 1247270921
H2 = 1956534873
H3 = 886504771
H4 = 1623791047
PostUp = /scripts/firewall-up.sh 5848 10.8.0.0/16
PostDown = /scripts/firewall-down.sh 5848 10.8.0.0/16
data:
- secretKey: server_private_key
sourceRef:
+1 -1
View File
@@ -25,7 +25,7 @@ spec:
kubernetes.io/hostname: cy.tail2fe2d.ts.net
containers:
- name: amnezia-fellow
image: ultradesu/amnezia-fellow:latest
image: ultradesu/amnezia-fellow:1.0.2
imagePullPolicy: Always
ports:
- name: http
@@ -0,0 +1,106 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: firefly-data-importer-sync
labels:
app.kubernetes.io/name: firefly-data-importer-sync
spec:
schedule: "17 */6 * * *"
timeZone: Europe/London
concurrencyPolicy: Forbid
startingDeadlineSeconds: 1800
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 1200
template:
metadata:
labels:
app.kubernetes.io/name: firefly-data-importer-sync
spec:
automountServiceAccountToken: false
restartPolicy: Never
nodeSelector:
kubernetes.io/hostname: ai.tail2fe2d.ts.net
tolerations:
- key: workload
operator: Equal
value: ai
effect: NoSchedule
containers:
- name: data-importer-sync
image: fireflyiii/data-importer:version-2.3.4
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
- |-
php artisan importer:import /import/import.json
status=$?
if [ "${status}" -eq 73 ]; then
echo "No new transactions; considering the sync successful."
exit 0
fi
exit "${status}"
env:
- name: FIREFLY_III_URL
value: http://firefly-iii
- name: VANITY_URL
value: http://ff.lan
- name: FIREFLY_III_ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: firefly-data-importer-secrets
key: FIREFLY_III_ACCESS_TOKEN
- name: ENABLE_BANKING_APP_ID
valueFrom:
secretKeyRef:
name: firefly-data-importer-secrets
key: ENABLE_BANKING_APP_ID
- name: ENABLE_BANKING_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: firefly-data-importer-secrets
key: ENABLE_BANKING_PRIVATE_KEY
- name: IMPORT_DIR_ALLOWLIST
value: /import
# Recurring imports intentionally overlap their date range.
# Keep rejecting duplicates, but do not fail the Job for them.
- name: IGNORE_DUPLICATE_ERRORS
value: "true"
- name: TZ
value: Europe/London
- name: APP_ENV
value: production
- name: APP_DEBUG
value: "false"
- name: LOG_LEVEL
value: notice
volumeMounts:
- name: import-config
mountPath: /import
readOnly: true
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
volumes:
- name: import-config
secret:
secretName: firefly-data-importer-secrets
items:
- key: import.json
path: import.json
securityContext:
seccompProfile:
type: RuntimeDefault
@@ -66,6 +66,10 @@ spec:
-----BEGIN PRIVATE KEY-----
{{ .private_key | replace "-----BEGIN PRIVATE KEY-----" "" | replace "-----END PRIVATE KEY-----" "" | replace " " "" | trim }}
-----END PRIVATE KEY-----
FIREFLY_III_ACCESS_TOKEN: |-
{{ .firefly_access_token }}
import.json: |-
{{ .import_config }}
data:
- secretKey: app_id
sourceRef:
@@ -83,3 +87,19 @@ spec:
remoteRef:
key: a1867c81-715c-47cd-978d-14ea5bcedea9
property: fields[4].value
- secretKey: firefly_access_token
sourceRef:
storeRef:
name: vaultwarden-login
kind: ClusterSecretStore
remoteRef:
key: a1867c81-715c-47cd-978d-14ea5bcedea9
property: fields[6].value
- secretKey: import_config
sourceRef:
storeRef:
name: vaultwarden-login
kind: ClusterSecretStore
remoteRef:
key: a1867c81-715c-47cd-978d-14ea5bcedea9
property: fields[5].value
+1 -1
View File
@@ -4,12 +4,12 @@ kind: Kustomization
resources:
- app.yaml
- data-importer.yaml
- data-importer-sync.yaml
- data-importer-ingress.yaml
- external-secrets.yaml
- postgres.yaml
- storage.yaml
- storage-prep.yaml
- traefik-ai.yaml
- network-policy.yaml
helmCharts:
+3
View File
@@ -22,6 +22,9 @@ spec:
- podSelector:
matchLabels:
app.kubernetes.io/name: firefly-data-importer
- podSelector:
matchLabels:
app.kubernetes.io/name: firefly-data-importer-sync
# hostNetwork traffic is seen as node traffic rather than Pod traffic.
- ipBlock:
cidr: 192.168.1.117/32
+1
View File
@@ -8,3 +8,4 @@ resources:
- ingress.yaml
- deployment.yaml
- servicemonitor.yaml
- socks-proxy.yaml
+106
View File
@@ -0,0 +1,106 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: furumi-socks-proxy
labels:
app: furumi-socks-proxy
spec:
replicas: 1
selector:
matchLabels:
app: furumi-socks-proxy
template:
metadata:
labels:
app: furumi-socks-proxy
spec:
nodeSelector:
kubernetes.io/hostname: ai.tail2fe2d.ts.net
tolerations:
- key: workload
operator: Equal
value: ai
effect: NoSchedule
containers:
- name: socks-proxy
image: serjs/go-socks5-proxy:v0.0.4
imagePullPolicy: IfNotPresent
env:
- name: REQUIRE_AUTH
value: "true"
- name: PROXY_USER
value: furumi
- name: PROXY_PASSWORD
value: furumi
- name: PROXY_PORT
value: "1080"
ports:
- name: socks5
containerPort: 1080
protocol: TCP
readinessProbe:
tcpSocket:
port: socks5
initialDelaySeconds: 2
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
tcpSocket:
port: socks5
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 2
failureThreshold: 3
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 200m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
securityContext:
seccompProfile:
type: RuntimeDefault
---
apiVersion: v1
kind: Service
metadata:
name: furumi-socks-proxy
labels:
app: furumi-socks-proxy
spec:
type: ClusterIP
selector:
app: furumi-socks-proxy
ports:
- name: socks5
protocol: TCP
port: 1080
targetPort: socks5
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: furumi-socks-proxy-ingress
spec:
podSelector:
matchLabels:
app: furumi-socks-proxy
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: furumi-player
ports:
- protocol: TCP
port: 1080
+2 -1
View File
@@ -48,7 +48,7 @@ spec:
value: "true"
- name: GITEA__service__CAPTCHA_TYPE
value: "cfturnstile"
- name: GITEA__webhook__ALLOWED_HOST_LIST
- name: GITEA__security__ALLOWED_HOST_LIST
value: "*"
envFrom:
- secretRef:
@@ -79,6 +79,7 @@ spec:
labels:
app: gitea-runner
spec:
serviceAccountName: gitea-runner
dnsConfig:
options:
- name: ndots
+2
View File
@@ -5,6 +5,8 @@ resources:
- app.yaml
- external-secrets.yaml
- deployment.yaml
- runner-rbac.yaml
- runner-rebalance.yaml
- user-unban-cronjob.yaml
- service.yaml
- ingress.yaml
+52
View File
@@ -0,0 +1,52 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitea-runner
namespace: gitea
---
apiVersion: v1
kind: Secret
metadata:
name: gitea-runner-kubernetes-token
namespace: gitea
annotations:
kubernetes.io/service-account.name: gitea-runner
type: kubernetes.io/service-account-token
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: auth-proxy-routes-manager
namespace: auth-proxy
rules:
- apiGroups:
- ""
resources:
- configmaps
resourceNames:
- auth-proxy-routes
verbs:
- get
- update
- patch
- apiGroups:
- ""
resources:
- configmaps
verbs:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: gitea-runner-auth-proxy-routes
namespace: auth-proxy
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: auth-proxy-routes-manager
subjects:
- kind: ServiceAccount
name: gitea-runner
namespace: gitea
+127
View File
@@ -0,0 +1,127 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitea-runner-rebalancer
namespace: gitea
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: gitea-runner-rebalancer
namespace: gitea
rules:
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- list
- delete
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: gitea-runner-rebalancer
namespace: gitea
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: gitea-runner-rebalancer
subjects:
- kind: ServiceAccount
name: gitea-runner-rebalancer
namespace: gitea
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: gitea-runner-rebalancer
rules:
- apiGroups:
- ""
resources:
- nodes
resourceNames:
- ai.tail2fe2d.ts.net
verbs:
- get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: gitea-runner-rebalancer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: gitea-runner-rebalancer
subjects:
- kind: ServiceAccount
name: gitea-runner-rebalancer
namespace: gitea
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: gitea-runner-rebalancer
namespace: gitea
spec:
schedule: "*/2 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 3
jobTemplate:
spec:
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
app: gitea-runner-rebalancer
spec:
serviceAccountName: gitea-runner-rebalancer
restartPolicy: Never
containers:
- name: rebalance
image: bitnami/kubectl:latest
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -ec
- |
target_node="ai.tail2fe2d.ts.net"
ready="$(
kubectl get node "${target_node}" \
-o jsonpath='{range .status.conditions[?(@.type=="Ready")]}{.status}{end}'
)"
if [ "${ready}" != "True" ]; then
echo "${target_node} is not Ready; keeping the runner on its current node"
exit 0
fi
runner_pod="$(
kubectl -n gitea get pods \
-l app=gitea-runner \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}'
)"
runner_node="$(
kubectl -n gitea get pods \
-l app=gitea-runner \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].spec.nodeName}'
)"
if [ -z "${runner_pod}" ] || [ -z "${runner_node}" ]; then
echo "No running Gitea runner found"
exit 0
fi
if [ "${runner_node}" = "${target_node}" ]; then
echo "${runner_pod} already runs on ${target_node}"
exit 0
fi
echo "Moving ${runner_pod} from ${runner_node} to preferred node ${target_node}"
kubectl -n gitea delete pod "${runner_pod}" --wait=false
+20
View File
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: lan-play
namespace: argocd
spec:
project: apps
destination:
namespace: lan-play
server: https://kubernetes.default.svc
source:
repoURL: ssh://git@gt.hexor.cy:30022/ab/homelab.git
targetRevision: HEAD
path: k8s/apps/lan-play
syncPolicy:
automated:
selfHeal: true
prune: true
syncOptions:
- CreateNamespace=true
+63
View File
@@ -0,0 +1,63 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: lan-play
labels:
app.kubernetes.io/name: lan-play
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: lan-play
template:
metadata:
labels:
app.kubernetes.io/name: lan-play
spec:
nodeSelector:
kubernetes.io/hostname: ai.tail2fe2d.ts.net
tolerations:
- key: workload
operator: Equal
value: ai
effect: NoSchedule
containers:
- name: lan-play
image: ultradesu/lan-play:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 8080
protocol: TCP
volumeMounts:
- name: media
mountPath: /media
readinessProbe:
tcpSocket:
port: http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: 20
periodSeconds: 30
resources:
requests:
cpu: "2"
memory: 2Gi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
volumes:
- name: media
hostPath:
path: /k8s/lan-play
type: DirectoryOrCreate
securityContext:
seccompProfile:
type: RuntimeDefault
+63
View File
@@ -0,0 +1,63 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: lan-play-auth
spec:
forwardAuth:
address: http://auth-proxy.auth-proxy.svc:80/auth
trustForwardHeader: true
authResponseHeaders:
- X-Auth-Request-User
- X-Auth-Request-Email
- X-Auth-Request-Groups
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: lan-play-public
spec:
entryPoints:
- websecure
routes:
- match: Host(`lp.hexor.cy`)
kind: Rule
middlewares:
- name: lan-play-auth
services:
- name: lan-play
port: 80
tls:
secretName: lan-play-tls
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: lan-play-tls
spec:
secretName: lan-play-tls
issuerRef:
name: letsencrypt
kind: ClusterIssuer
dnsNames:
- lp.hexor.cy
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: lan-play-local
labels:
firefly.hexor.cy/private-ai: "true"
annotations:
kubernetes.io/ingress.class: traefik-ai
spec:
rules:
- host: video.lan
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: lan-play
port:
number: 80
+8
View File
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- app.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: lan-play
labels:
app.kubernetes.io/name: lan-play
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: lan-play
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
+1 -1
View File
@@ -70,6 +70,7 @@ spec:
action: suggest
template: "{{ `{{ user.email }}` }}"
set_email_verification: always
on_conflict: replace
data:
- secretKey: oauth_client_id
sourceRef:
@@ -93,4 +94,3 @@ spec:
metadataPolicy: None
key: ca76867f-49f3-4a30-9ef3-b05af35ee49a
property: fields[1].value
on_conflict: replace
+1 -1
View File
@@ -34,7 +34,7 @@ spec:
mountPath: /templates/subscription
containers:
- name: pasarguard-web
image: pasarguard/panel:v5.0.3
image: pasarguard/panel:v5.3.0
imagePullPolicy: Always
envFrom:
- secretRef:
+1 -1
View File
@@ -18,7 +18,7 @@ spec:
spec:
nodeSelector:
kubernetes.io/os: linux
kubernetes.io/hostname: master.tail2fe2d.ts.net
# kubernetes.io/hostname: master.tail2fe2d.ts.net
volumes:
- name: data
persistentVolumeClaim:
+1 -1
View File
@@ -10,7 +10,7 @@ resources:
helmCharts:
- name: argo-cd
repo: https://argoproj.github.io/argo-helm
version: 10.2.1
version: 10.6.0
releaseName: argocd
namespace: argocd
valuesFile: values.yaml
+1 -1
View File
@@ -10,7 +10,7 @@ resources:
helmCharts:
- name: cert-manager
repo: https://charts.jetstack.io
version: 1.20.0
version: 1.21.1
releaseName: cert-manager
namespace: cert-manager
valuesFile: values.yaml
+1 -1
View File
@@ -7,7 +7,7 @@ resources:
helmCharts:
- name: external-secrets
repo: https://charts.external-secrets.io
version: 1.1.0
version: 2.10.0
releaseName: external-secrets
namespace: external-secrets
valuesFile: values.yaml
+1 -1
View File
@@ -9,7 +9,7 @@ resources:
helmCharts:
- name: keycloakx
repo: https://codecentric.github.io/helm-charts
version: 7.1.11
version: 7.3.1
releaseName: keycloak
namespace: keycloak
valuesFile: values.yaml
@@ -7,6 +7,7 @@ resources:
- coredns-internal-resolve.yaml
- https-middleware.yaml
- node-external-ip-labeler.yaml
- traefik-ai.yaml
helmCharts:
- name: csi-driver-nfs
@@ -9,11 +9,13 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: traefik-ai
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: traefik-ai
namespace: firefly-iii
rules:
- apiGroups:
- ""
@@ -52,6 +54,7 @@ apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: traefik-ai
namespace: firefly-iii
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
@@ -59,11 +62,65 @@ roleRef:
subjects:
- kind: ServiceAccount
name: traefik-ai
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: traefik-ai
namespace: lan-play
rules:
- apiGroups:
- ""
resources:
- services
- secrets
- endpoints
verbs:
- get
- list
- watch
- apiGroups:
- discovery.k8s.io
resources:
- endpointslices
verbs:
- get
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
- ingresses/status
verbs:
- update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: traefik-ai
namespace: lan-play
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: traefik-ai
subjects:
- kind: ServiceAccount
name: traefik-ai
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: firefly-traefik-ai
name: traefik-ai
rules:
- apiGroups:
- ""
@@ -85,20 +142,21 @@ rules:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: firefly-traefik-ai
name: traefik-ai
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: firefly-traefik-ai
name: traefik-ai
subjects:
- kind: ServiceAccount
name: traefik-ai
namespace: firefly-iii
namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: traefik-ai
namespace: kube-system
labels:
app.kubernetes.io/name: traefik-ai
spec:
@@ -130,7 +188,7 @@ spec:
args:
- --entrypoints.web.address=192.168.1.117:80
- --providers.kubernetesingress=true
- --providers.kubernetesingress.namespaces=firefly-iii
- --providers.kubernetesingress.namespaces=firefly-iii,lan-play
- --providers.kubernetesingress.ingressclass=traefik-ai
- --providers.kubernetesingress.labelselector=firefly.hexor.cy/private-ai=true
- --providers.kubernetescrd=false
+1 -1
View File
@@ -7,7 +7,7 @@ kind: Kustomization
helmCharts:
- name: longhorn
repo: https://charts.longhorn.io
version: 1.12.0
version: 1.12.1
releaseName: longhorn
namespace: longhorn-system
valuesFile: values.yaml
+13
View File
@@ -144,6 +144,8 @@ spec:
{{ .amnezia_fellow }}
USER_web_petting: |-
{{ .web_petting }}
USER_airtrail: |-
{{ .airtrail }}
data:
- secretKey: authentik
sourceRef:
@@ -365,3 +367,14 @@ spec:
metadataPolicy: None
key: 2a9deb39-ef22-433e-a1be-df1555625e22
property: fields[20].value
- secretKey: airtrail
sourceRef:
storeRef:
name: vaultwarden-login
kind: ClusterSecretStore
remoteRef:
conversionStrategy: Default
decodingStrategy: None
metadataPolicy: None
key: 2a9deb39-ef22-433e-a1be-df1555625e22
property: fields[21].value
+4 -4
View File
@@ -14,15 +14,15 @@ resources:
helmCharts:
- name: kube-prometheus-stack
repo: https://prometheus-community.github.io/helm-charts
version: 84.3.0
version: 88.6.2
releaseName: prometheus
namespace: prometheus
valuesFile: prom-values.yaml
includeCRDs: true
- name: loki
repo: https://grafana.github.io/helm-charts
version: 6.29.0
repo: https://grafana-community.github.io/helm-charts
version: 18.11.7
releaseName: loki
namespace: prometheus
valuesFile: loki-values.yaml
@@ -30,7 +30,7 @@ helmCharts:
- name: promtail
repo: https://grafana.github.io/helm-charts
version: 6.16.6
version: 6.17.1
releaseName: promtail
namespace: prometheus
valuesFile: promtail-values.yaml
+9
View File
@@ -75,6 +75,10 @@ prometheus:
static_configs:
- targets: ['prom-a2s-exporter.counter-strike.svc:9841']
labels: {instance: master}
- job_name: macos_prom_exporter
static_configs:
- targets: ['macbook-pro.tail2fe2d.ts.net:9100']
labels: {instance: ultrabook-pro}
retention: "380d"
retentionSize: "0"
@@ -91,6 +95,11 @@ prometheus:
grafana:
enabled: true
sidecar:
datasources:
alertmanager:
uid: ff9iga3xqregwc
serviceAccount:
create: true
+2 -2
View File
@@ -18,7 +18,7 @@ spec:
serviceAccountName: system-upgrade
upgrade:
image: rancher/k3s-upgrade
version: v1.36.1+k3s1
version: v1.36.4+k3s1
---
# Agent plan
apiVersion: upgrade.cattle.io/v1
@@ -43,4 +43,4 @@ spec:
serviceAccountName: system-upgrade
upgrade:
image: rancher/k3s-upgrade
version: v1.36.1+k3s1
version: v1.36.4+k3s1
-44
View File
@@ -1,44 +0,0 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/goauthentik/authentik" {
version = "2025.12.1"
constraints = ">= 2023.10.0, 2025.12.1"
hashes = [
"h1:p9AGeRqK50wTHEIp7z7O4MUP83cs+lt7wPajZ9m9TB8=",
"zh:0e856d3b13614bc32346a236a8e84ba55ecd17238c2008d4b3e71aa8cb49f515",
"zh:2dcc44cd499c18ebbc4f763eff97a7b725763c8ac8fbb5d69c935413ccdc4962",
"zh:434100fc75ec7cd6b64cc9497e8273e79325fa8d285e9fd9d341c1a67421643b",
"zh:483484f66d2e8ce6fa4bfd91e824ceebf07d10acb5df5f366397c55227c4ae91",
"zh:596743a6f1c77a6f103b06ef8d932fe8f2376793b92478853dc84571d17c429f",
"zh:5ed2d5eb7db13229baaf042c725d5c64b58ffdcc641370175e0a88900af94bf1",
"zh:8aecd4cf782c82bee01098f72fe4ffff83707516007b32a01c7fcb19a9260338",
"zh:928c05ecac309287ff7d73ed6e478350fe3003557658ae5dc2be817a4268dba7",
"zh:9b9fd36dfb3e75da8b4478485272505ae9a3c67b10db173e1d2d76cfe2b637b8",
"zh:ab7cd8c61ab67a045854e32f0be1940a92746770dbf3c17bbe923e0259c4f897",
"zh:bb1360ec19a4fc1095d0ef1b7b6c5c3c1a91daac7cd1957d43a4cdbb7356a2e3",
"zh:d2186f4063aa1a547b52a53745d472e43f5343bc1674f2bbb91421c61b0fab50",
"zh:d74bbb67a77951b18ffd7b2863954e70ac03450ad2023cc305c66a5ff25d8d18",
"zh:f5970569ea0a479bbfbf2d452f5962e1c9bd472b82756db822d0e951363daa25",
]
}
provider "registry.terraform.io/hashicorp/random" {
version = "3.8.1"
constraints = ">= 3.5.0"
hashes = [
"h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=",
"zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4",
"zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae",
"zh:229665ddf060aa0ed315597908483eee5b818a17d09b6417a0f52fd9405c4f57",
"zh:2469d2e48f28076254a2a3fc327f184914566d9e40c5780b8d96ebf7205f8bc0",
"zh:37d7eb334d9561f335e748280f5535a384a88675af9a9eac439d4cfd663bcb66",
"zh:741101426a2f2c52dee37122f0f4a2f2d6af6d852cb1db634480a86398fa3511",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:a902473f08ef8df62cfe6116bd6c157070a93f66622384300de235a533e9d4a9",
"zh:b85c511a23e57a2147355932b3b6dce2a11e856b941165793a0c3d7578d94d05",
"zh:c5172226d18eaac95b1daac80172287b69d4ce32750c82ad77fa0768be4ea4b8",
"zh:dab4434dba34aad569b0bc243c2d3f3ff86dd7740def373f2a49816bd2ff819b",
"zh:f49fd62aa8c5525a5c17abd51e27ca5e213881d58882fd42fec4a545b53c9699",
]
}
-81
View File
@@ -1,81 +0,0 @@
# Authentik Terraform Configuration
Root Terraform configuration for managing Authentik SSO — applications (OAuth2/OIDC, Proxy, SAML), groups, outposts, flows, certificates, and property mappings.
State is stored in Terraform Cloud (organization `ultradesu`, workspace `Authentik`).
## Structure
```
.
├── main.tf # Resources: groups, outposts, policy bindings, module calls
├── variables.tf # Input variable definitions
├── outputs.tf # Outputs (app details, groups, flows, wiki data)
├── providers.tf # Authentik provider (goauthentik/authentik 2025.12.1)
├── state.tf # Terraform Cloud backend
├── terraform.tfvars # General settings: authentik_url, outposts, flows, tags
├── oauth2-apps.auto.tfvars # OAuth2/OIDC application definitions
├── proxy-apps.auto.tfvars # Proxy application definitions
├── groups.auto.tfvars # Group definitions
└── modules/
├── oauth-provider/ # OAuth2/OIDC provider + application
├── proxy-provider/ # Proxy provider + application
└── saml-provider/ # SAML provider + application
```
## Usage
```bash
# Set the API token
export TF_VAR_authentik_token="..."
terraform init
terraform plan
terraform apply
```
All `*.auto.tfvars` files are loaded automatically — no `-var-file` flags needed.
## Adding applications
OAuth2/OIDC — add to `oauth2-apps.auto.tfvars`:
```hcl
oauth_applications = {
"my-app" = {
name = "My App"
slug = "my-app"
group = "Tools"
redirect_uris = ["https://my-app.example.com/callback"]
create_group = true
access_groups = ["admins"]
}
}
```
Proxy — add to `proxy-apps.auto.tfvars`:
```hcl
proxy_applications = {
"my-proxy" = {
name = "My Proxy"
slug = "my-proxy"
group = "Tools"
external_host = "https://my-proxy.example.com"
internal_host = "http://my-service.namespace.svc:80"
outpost = "kubernetes-outpost"
create_group = true
access_groups = ["admins"]
}
}
```
## CI/CD
Managed via Gitea Actions (`.gitea/workflows/authentik-apps.yaml`). Runs `terraform apply` on push to `main` when files in `terraform/authentik/` change. Also generates a wiki page with the applications list.
## Requirements
- Terraform >= 1.0
- goauthentik/authentik provider 2025.12.1
- Authentik API token with admin permissions
-10
View File
@@ -1,10 +0,0 @@
groups = {
"admins" = {
name = "Administrators"
is_superuser = true
attributes = {
notes = "Managed by Terraform"
}
}
}
-361
View File
@@ -1,361 +0,0 @@
data "authentik_flow" "default_authorization_flow" {
slug = var.default_authorization_flow
}
data "authentik_flow" "default_authentication_flow" {
slug = var.default_authentication_flow
}
data "authentik_flow" "default_invalidation_flow" {
slug = var.default_invalidation_flow
}
# Root groups (without parent)
resource "authentik_group" "root_groups" {
for_each = {
for k, v in var.groups : k => v
if v.parent == null
}
name = each.value.name
is_superuser = each.value.is_superuser
attributes = jsonencode(each.value.attributes)
}
# Child groups (with parent)
resource "authentik_group" "child_groups" {
for_each = {
for k, v in var.groups : k => v
if v.parent != null
}
name = each.value.name
is_superuser = each.value.is_superuser
parents = authentik_group.root_groups[each.value.parent].id
attributes = jsonencode(each.value.attributes)
depends_on = [authentik_group.root_groups]
}
# Auto-created groups for proxy applications
resource "authentik_group" "proxy_app_groups" {
for_each = {
for k, v in var.proxy_applications : k => v
if v.create_group == true
}
name = "TF-${each.value.name} Users"
is_superuser = false
attributes = jsonencode({
notes = "Auto-created for ${each.value.name} application"
app_slug = each.value.slug
})
}
# Auto-created groups for OAuth applications
resource "authentik_group" "oauth_app_groups" {
for_each = {
for k, v in var.oauth_applications : k => v
if v.create_group == true
}
name = "TF-${each.value.name} Users"
is_superuser = false
attributes = jsonencode({
notes = "Auto-created for ${each.value.name} application"
app_slug = each.value.slug
})
}
resource "authentik_certificate_key_pair" "certificates" {
for_each = var.certificates
name = each.value.name
certificate_data = each.value.certificate_data
key_data = each.value.key_data
}
data "authentik_service_connection_kubernetes" "local_k8s" {
name = "Local Kubernetes Cluster"
}
resource "authentik_flow" "flows" {
for_each = var.flows
name = each.value.name
title = each.value.title
slug = each.value.slug
designation = each.value.designation
policy_engine_mode = each.value.policy_engine_mode
compatibility_mode = each.value.compatibility_mode
layout = each.value.layout
denied_action = each.value.denied_action
}
resource "authentik_property_mapping_provider_scope" "oidc_mappings" {
for_each = {
for k, v in var.property_mappings : k => v
if v.oidc_scope != null
}
name = each.value.name
scope_name = each.value.oidc_scope
expression = each.value.expression
}
resource "authentik_property_mapping_provider_saml" "saml_mappings" {
for_each = {
for k, v in var.property_mappings : k => v
if v.saml_name != null
}
name = each.value.name
saml_name = each.value.saml_name
expression = each.value.expression
}
module "oauth_applications" {
source = "./modules/oauth-provider"
for_each = var.oauth_applications
name = each.value.name
app_name = each.value.name
app_slug = each.value.slug
app_group = each.value.group
client_id = each.value.client_id
authorization_flow = try(authentik_flow.flows[each.value.authorization_flow].id, data.authentik_flow.default_authorization_flow.id)
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
redirect_uris = each.value.redirect_uris
client_type = each.value.client_type
include_claims_in_id_token = each.value.include_claims_in_id_token
access_code_validity = each.value.access_code_validity
access_token_validity = each.value.access_token_validity
refresh_token_validity = each.value.refresh_token_validity
property_mappings = each.value.property_mappings
signing_key = each.value.signing_key
policy_engine_mode = each.value.policy_engine_mode
meta_description = each.value.meta_description
meta_launch_url = each.value.meta_launch_url
meta_icon = each.value.meta_icon
scope_mappings = each.value.scope_mappings
# Access control - only pass explicitly defined groups
access_groups = [
for group_key in each.value.access_groups :
try(
authentik_group.root_groups[group_key].id,
authentik_group.child_groups[group_key].id
)
]
}
module "proxy_applications" {
source = "./modules/proxy-provider"
for_each = var.proxy_applications
name = each.value.name
app_name = each.value.name
app_slug = each.value.slug
app_group = each.value.group
external_host = each.value.external_host
internal_host = each.value.internal_host
internal_host_ssl_validation = each.value.internal_host_ssl_validation
authorization_flow = try(authentik_flow.flows[each.value.authorization_flow].id, data.authentik_flow.default_authorization_flow.id)
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
mode = each.value.mode
intercept_header_auth = each.value.intercept_header_auth
basic_auth_enabled = each.value.basic_auth_enabled
basic_auth_user_attribute = each.value.basic_auth_username_attribute
basic_auth_password_attribute = each.value.basic_auth_password_attribute
cookie_domain = each.value.cookie_domain
skip_path_regex = each.value.skip_path_regex
policy_engine_mode = each.value.policy_engine_mode
meta_description = each.value.meta_description
meta_launch_url = each.value.meta_launch_url
meta_icon = each.value.meta_icon
# Access control - only pass explicitly defined groups
access_groups = [
for group_key in each.value.access_groups :
try(
authentik_group.root_groups[group_key].id,
authentik_group.child_groups[group_key].id
)
]
}
# Binding auto-created groups to their applications
resource "authentik_policy_binding" "auto_group_bindings" {
for_each = {
for k, v in var.proxy_applications : k => v
if v.create_group == true
}
target = module.proxy_applications[each.key].application_uuid
group = authentik_group.proxy_app_groups[each.key].id
order = 100
depends_on = [
module.proxy_applications,
authentik_group.proxy_app_groups
]
}
# Binding auto-created groups to their OAuth applications
resource "authentik_policy_binding" "oauth_auto_group_bindings" {
for_each = {
for k, v in var.oauth_applications : k => v
if v.create_group == true
}
target = module.oauth_applications[each.key].application_uuid
group = authentik_group.oauth_app_groups[each.key].id
order = 100
depends_on = [
module.oauth_applications,
authentik_group.oauth_app_groups
]
}
module "saml_applications" {
source = "./modules/saml-provider"
for_each = var.saml_applications
name = each.value.name
app_name = each.value.name
app_slug = each.value.slug
app_group = each.value.group
authorization_flow = try(authentik_flow.flows[each.value.authorization_flow].id, data.authentik_flow.default_authorization_flow.id)
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
acs_url = each.value.acs_url
issuer = each.value.issuer
audience = each.value.audience
sp_binding = each.value.sp_binding
signing_key = each.value.signing_key
property_mappings = [for pm in each.value.property_mappings : authentik_property_mapping_provider_saml.saml_mappings[pm].id]
name_id_mapping = each.value.name_id_mapping != null ? authentik_property_mapping_provider_saml.saml_mappings[each.value.name_id_mapping].id : null
assertion_valid_not_before = each.value.assertion_valid_not_before
assertion_valid_not_on_or_after = each.value.assertion_valid_not_on_or_after
session_valid_not_on_or_after = each.value.session_valid_not_on_or_after
policy_engine_mode = each.value.policy_engine_mode
meta_description = each.value.meta_description
meta_launch_url = each.value.meta_launch_url
meta_icon = each.value.meta_icon
}
locals {
oauth_outpost_assignments = {
for app_key, app in var.oauth_applications : app_key => app.outpost
if app.outpost != null
}
proxy_outpost_assignments = {
for app_key, app in var.proxy_applications : app_key => app.outpost
if app.outpost != null
}
outpost_providers = {
for outpost_key, outpost in var.outposts : outpost_key => concat(
[for app_key, app_outpost in local.oauth_outpost_assignments :
module.oauth_applications[app_key].provider_id if app_outpost == outpost_key],
[for app_key, app_outpost in local.proxy_outpost_assignments :
module.proxy_applications[app_key].provider_id if app_outpost == outpost_key]
)
}
}
resource "authentik_outpost" "outposts" {
for_each = {
for k, v in var.outposts : k => v
if length(lookup(local.outpost_providers, k, [])) > 0
}
name = each.value.name
type = "proxy"
protocol_providers = local.outpost_providers[each.key]
service_connection = data.authentik_service_connection_kubernetes.local_k8s.id
config = jsonencode({
log_level = "info"
docker_labels = null
authentik_host = var.authentik_url
docker_network = null
container_image = null
docker_map_ports = true
refresh_interval = "minutes=5"
kubernetes_replicas = 1
kubernetes_namespace = "authentik"
authentik_host_browser = ""
object_naming_template = "ak-outpost-%(name)s"
authentik_host_insecure = false
kubernetes_json_patches = {
deployment = [
{
op = "add"
path = "/spec/template/spec/containers/0/env/-"
value = {
name = "AUTHENTIK_POSTGRESQL__HOST"
value = "psql.psql.svc"
}
},
{
op = "add"
path = "/spec/template/spec/containers/0/env/-"
value = {
name = "AUTHENTIK_POSTGRESQL__PORT"
value = "5432"
}
},
{
op = "add"
path = "/spec/template/spec/containers/0/env/-"
value = {
name = "AUTHENTIK_POSTGRESQL__NAME"
value = "authentik"
}
},
{
op = "add"
path = "/spec/template/spec/containers/0/env/-"
value = {
name = "AUTHENTIK_POSTGRESQL__USER"
valueFrom = {
secretKeyRef = {
name = "authentik-creds"
key = "AUTHENTIK_POSTGRESQL__USER"
}
}
}
},
{
op = "add"
path = "/spec/template/spec/containers/0/env/-"
value = {
name = "AUTHENTIK_POSTGRESQL__PASSWORD"
valueFrom = {
secretKeyRef = {
name = "authentik-creds"
key = "AUTHENTIK_POSTGRESQL__PASSWORD"
}
}
}
}
]
}
kubernetes_service_type = "ClusterIP"
kubernetes_image_pull_secrets = []
kubernetes_ingress_class_name = null
kubernetes_disabled_components = []
kubernetes_ingress_annotations = {}
kubernetes_ingress_secret_name = "idm-tls"
})
depends_on = [
module.oauth_applications,
module.proxy_applications
]
}
@@ -1,103 +0,0 @@
terraform {
required_providers {
authentik = {
source = "goauthentik/authentik"
version = ">= 2023.10.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.5.0"
}
}
}
# Get all available scope mappings
data "authentik_property_mapping_provider_scope" "all_scopes" {
managed_list = [
"goauthentik.io/providers/oauth2/scope-email",
"goauthentik.io/providers/oauth2/scope-openid",
"goauthentik.io/providers/oauth2/scope-profile"
]
}
# Filter scope mappings based on requested scopes
locals {
scope_name_mapping = {
"openid" = "goauthentik.io/providers/oauth2/scope-openid"
"profile" = "goauthentik.io/providers/oauth2/scope-profile"
"email" = "goauthentik.io/providers/oauth2/scope-email"
}
selected_scope_ids = [
for scope in var.scope_mappings :
data.authentik_property_mapping_provider_scope.all_scopes.ids[index(data.authentik_property_mapping_provider_scope.all_scopes.managed_list, local.scope_name_mapping[scope])]
if contains(keys(local.scope_name_mapping), scope)
]
}
resource "random_password" "client_secret" {
count = var.client_secret == null ? 1 : 0
length = 40
special = true
}
resource "authentik_provider_oauth2" "provider" {
name = var.name
client_id = var.client_id != null ? var.client_id : random_id.client_id[0].hex
client_secret = var.client_secret != null ? var.client_secret : random_password.client_secret[0].result
client_type = var.client_type
authorization_flow = var.authorization_flow
invalidation_flow = var.invalidation_flow
include_claims_in_id_token = var.include_claims_in_id_token
access_code_validity = var.access_code_validity
access_token_validity = var.access_token_validity
refresh_token_validity = var.refresh_token_validity
signing_key = var.signing_key
allowed_redirect_uris = [
for uri in var.redirect_uris : {
matching_mode = "strict"
url = uri
}
]
property_mappings = length(var.property_mappings) > 0 ? var.property_mappings : local.selected_scope_ids
}
resource "random_id" "client_id" {
count = var.client_id == null ? 1 : 0
byte_length = 20
}
resource "authentik_application" "app" {
name = var.app_name
slug = var.app_slug
protocol_provider = authentik_provider_oauth2.provider.id
group = var.app_group
policy_engine_mode = var.policy_engine_mode
meta_description = var.meta_description
meta_launch_url = var.meta_launch_url
meta_icon = var.meta_icon
}
resource "authentik_policy_binding" "app_access" {
for_each = var.access_policies
target = authentik_application.app.id
policy = each.value.policy_id
order = each.value.order
enabled = lookup(each.value, "enabled", true)
timeout = lookup(each.value, "timeout", 30)
negate = lookup(each.value, "negate", false)
failure_result = lookup(each.value, "failure_result", true)
}
# Binding groups to the application
resource "authentik_policy_binding" "group_bindings" {
for_each = { for idx, group_id in var.access_groups : idx => group_id }
target = authentik_application.app.uuid
group = each.value
order = 10 + each.key
}
@@ -1,30 +0,0 @@
output "provider_id" {
description = "ID of the OAuth2 provider"
value = authentik_provider_oauth2.provider.id
}
output "application_id" {
description = "ID of the application"
value = authentik_application.app.id
}
output "application_uuid" {
description = "UUID of the application"
value = authentik_application.app.uuid
}
output "client_id" {
description = "OAuth2 Client ID"
value = authentik_provider_oauth2.provider.client_id
}
output "client_secret" {
description = "OAuth2 Client Secret"
value = authentik_provider_oauth2.provider.client_secret
sensitive = true
}
output "application_slug" {
description = "Application slug"
value = authentik_application.app.slug
}
@@ -1,150 +0,0 @@
variable "name" {
description = "Name of the OAuth2 provider"
type = string
}
variable "app_name" {
description = "Name of the application"
type = string
}
variable "app_slug" {
description = "Slug of the application"
type = string
}
variable "app_group" {
description = "Group for the application"
type = string
default = ""
}
variable "client_id" {
description = "OAuth2 Client ID"
type = string
default = null
}
variable "client_secret" {
description = "OAuth2 Client Secret"
type = string
default = null
sensitive = true
}
variable "client_type" {
description = "OAuth2 Client type (confidential or public)"
type = string
default = "confidential"
validation {
condition = contains(["confidential", "public"], var.client_type)
error_message = "Client type must be either 'confidential' or 'public'."
}
}
variable "authorization_flow" {
description = "Authorization flow UUID"
type = string
}
variable "invalidation_flow" {
description = "Invalidation flow UUID"
type = string
}
variable "redirect_uris" {
description = "List of allowed redirect URIs"
type = list(string)
default = []
}
variable "access_code_validity" {
description = "Access code validity duration"
type = string
default = "minutes=1"
}
variable "access_token_validity" {
description = "Access token validity duration"
type = string
default = "minutes=5"
}
variable "refresh_token_validity" {
description = "Refresh token validity duration"
type = string
default = "days=30"
}
variable "include_claims_in_id_token" {
description = "Include claims in ID token"
type = bool
default = true
}
variable "signing_key" {
description = "Signing key UUID"
type = string
default = null
}
variable "property_mappings" {
description = "List of property mapping UUIDs"
type = list(string)
default = []
}
variable "policy_engine_mode" {
description = "Policy engine mode"
type = string
default = "all"
validation {
condition = contains(["all", "any"], var.policy_engine_mode)
error_message = "Policy engine mode must be either 'all' or 'any'."
}
}
variable "meta_description" {
description = "Application meta description"
type = string
default = ""
}
variable "meta_launch_url" {
description = "Application launch URL"
type = string
default = ""
}
variable "meta_icon" {
description = "Application icon URL"
type = string
default = ""
}
variable "access_policies" {
description = "Access policies for the application"
type = map(object({
policy_id = string
order = number
enabled = optional(bool, true)
timeout = optional(number, 30)
negate = optional(bool, false)
failure_result = optional(bool, true)
}))
default = {}
}
variable "access_groups" {
description = "List of group IDs that have access to the application"
type = list(string)
default = []
}
variable "scope_mappings" {
description = "List of scope mappings for the OAuth provider"
type = list(string)
default = ["openid", "profile", "email"]
}
@@ -1,58 +0,0 @@
terraform {
required_providers {
authentik = {
source = "goauthentik/authentik"
version = ">= 2023.10.0"
}
}
}
resource "authentik_provider_proxy" "provider" {
name = var.name
external_host = var.external_host
internal_host = var.internal_host
internal_host_ssl_validation = var.internal_host_ssl_validation
authorization_flow = var.authorization_flow
invalidation_flow = var.invalidation_flow
mode = var.mode
cookie_domain = var.cookie_domain
skip_path_regex = var.skip_path_regex
intercept_header_auth = var.intercept_header_auth
basic_auth_enabled = var.basic_auth_enabled
basic_auth_password_attribute = var.basic_auth_password_attribute
property_mappings = var.property_mappings
}
resource "authentik_application" "app" {
name = var.app_name
slug = var.app_slug
protocol_provider = authentik_provider_proxy.provider.id
group = var.app_group
policy_engine_mode = var.policy_engine_mode
meta_description = var.meta_description
meta_launch_url = var.meta_launch_url
meta_icon = var.meta_icon
}
resource "authentik_policy_binding" "app_access" {
for_each = var.access_policies
target = authentik_application.app.id
policy = each.value.policy_id
order = each.value.order
enabled = lookup(each.value, "enabled", true)
timeout = lookup(each.value, "timeout", 30)
negate = lookup(each.value, "negate", false)
failure_result = lookup(each.value, "failure_result", true)
}
# Binding groups to the application
resource "authentik_policy_binding" "group_bindings" {
for_each = { for idx, group_id in var.access_groups : idx => group_id }
target = authentik_application.app.uuid
group = each.value
order = 10 + each.key
}
@@ -1,35 +0,0 @@
output "provider_id" {
description = "ID of the Proxy provider"
value = authentik_provider_proxy.provider.id
}
output "application_id" {
description = "ID of the application"
value = authentik_application.app.id
}
output "application_uuid" {
description = "UUID of the application"
value = authentik_application.app.uuid
}
output "application_slug" {
description = "Application slug"
value = authentik_application.app.slug
}
output "launch_url" {
description = "Application launch URL"
value = authentik_application.app.meta_launch_url
}
output "external_host" {
description = "External host URL"
value = authentik_provider_proxy.provider.external_host
}
output "internal_host" {
description = "Internal host URL"
value = authentik_provider_proxy.provider.internal_host
}
@@ -1,151 +0,0 @@
variable "name" {
description = "Name of the Proxy provider"
type = string
}
variable "app_name" {
description = "Name of the application"
type = string
}
variable "app_slug" {
description = "Slug of the application"
type = string
}
variable "app_group" {
description = "Group for the application"
type = string
default = ""
}
variable "external_host" {
description = "External hostname for the proxy"
type = string
}
variable "internal_host" {
description = "Internal hostname for the proxy"
type = string
default = ""
}
variable "internal_host_ssl_validation" {
description = "Enable SSL validation for internal host"
type = bool
default = true
}
variable "authorization_flow" {
description = "Authorization flow UUID"
type = string
}
variable "invalidation_flow" {
description = "Invalidation flow UUID"
type = string
}
variable "mode" {
description = "Proxy mode (proxy, forward_single, forward_domain)"
type = string
default = "proxy"
validation {
condition = contains(["proxy", "forward_single", "forward_domain"], var.mode)
error_message = "Mode must be one of: proxy, forward_single, forward_domain."
}
}
variable "cookie_domain" {
description = "Cookie domain for the proxy"
type = string
default = ""
}
variable "skip_path_regex" {
description = "Regular expression for paths to skip authentication"
type = string
default = ""
}
variable "intercept_header_auth" {
description = "Intercept header authentication"
type = bool
default = false
}
variable "basic_auth_enabled" {
description = "Enable basic authentication"
type = bool
default = false
}
variable "basic_auth_password_attribute" {
description = "Attribute for basic auth password"
type = string
default = ""
}
variable "basic_auth_user_attribute" {
description = "Attribute for basic auth username"
type = string
default = ""
}
variable "property_mappings" {
description = "List of property mapping UUIDs"
type = list(string)
default = []
}
variable "policy_engine_mode" {
description = "Policy engine mode"
type = string
default = "all"
validation {
condition = contains(["all", "any"], var.policy_engine_mode)
error_message = "Policy engine mode must be either 'all' or 'any'."
}
}
variable "meta_description" {
description = "Application meta description"
type = string
default = ""
}
variable "meta_launch_url" {
description = "Application launch URL"
type = string
default = ""
}
variable "meta_icon" {
description = "Application icon URL"
type = string
default = ""
}
variable "access_policies" {
description = "Access policies for the application"
type = map(object({
policy_id = string
order = number
enabled = optional(bool, true)
timeout = optional(number, 30)
negate = optional(bool, false)
failure_result = optional(bool, true)
}))
default = {}
}
variable "access_groups" {
description = "List of group IDs that have access to the application"
type = list(string)
default = []
}
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more