Compare commits
63
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff4e87aca0 | ||
|
|
01561ea2cd | ||
|
|
7db427f332 | ||
|
|
41729bf256 | ||
|
|
fe914e2de4 | ||
|
|
0348329829 | ||
|
|
e257e3895b | ||
|
|
4fe2a2b5f3 | ||
|
|
690ff2a19c | ||
|
|
99e1818572 | ||
|
|
cf88753ff5 | ||
|
|
862a7619e2 | ||
|
|
5a29dcc236 | ||
|
|
bcfbfcd362 | ||
|
|
2676ecef5d | ||
|
|
b4de55f3c0 | ||
|
|
de6c9562f1 | ||
|
|
0c333375a5 | ||
|
|
5783547bd3 | ||
|
|
29c5b00c16 | ||
|
|
76b2090ccc | ||
|
|
bc83f7dd24 | ||
|
|
5768a4ff3b | ||
|
|
7ce98c4982 | ||
|
|
18f2654ff1 | ||
|
|
2fb63dded8 | ||
|
|
46bf1dc41b | ||
|
|
eda408aaea | ||
|
|
7b9da9236f | ||
|
|
f2a607fda1 | ||
|
|
d57bb01608 | ||
|
|
76f77fbc56 | ||
|
|
e90efbd016 | ||
|
|
b0378ffb40 | ||
|
|
5c2e7d0e33 | ||
|
|
b9052a99f1 | ||
|
|
d41171955b | ||
|
|
f038016d46 | ||
|
|
1091d6bd14 | ||
|
|
9f56045009 | ||
|
|
2583236f6d | ||
|
|
80b792ce8c | ||
|
|
e40a4d0c72 | ||
|
|
03d580999a | ||
|
|
a28fed03ed | ||
|
|
99a69257e6 | ||
|
|
f0f455c7dd | ||
|
|
ede07b72e8 | ||
|
|
4662797118 | ||
|
|
505bfec45a | ||
|
|
a3b5811b83 | ||
|
|
f58f979b3b | ||
|
|
c4a3623e55 | ||
|
|
dd3693095f | ||
|
|
4273e62d68 | ||
|
|
5d8a582e1e | ||
|
|
31714fe357 | ||
|
|
2e22f98eb4 | ||
|
|
69186d807e | ||
|
|
719504a399 | ||
|
|
1059e430a0 | ||
|
|
f3d199415f | ||
|
|
ef0123b5fb |
No files matched your search
@@ -59,3 +59,6 @@ tags
|
||||
[._]*.un~
|
||||
|
||||
.DS_Store
|
||||
|
||||
# Helm charts pulled by kustomize (--enable-helm) when rendering locally
|
||||
k8s-ng/**/charts/
|
||||
Executable
+27
@@ -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"
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
crds:
|
||||
enabled: true
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
installCRDs: true
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- secrets.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: vaultwarden
|
||||
spec:
|
||||
selector:
|
||||
app: vaultwarden
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: http
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: airtrail-uploads
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
@@ -8,14 +8,21 @@ 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() {
|
||||
ip route get 1.1.1.1 | awk '{for (i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}'
|
||||
}
|
||||
|
||||
ensure_insert_rule() {
|
||||
ensure_rule() {
|
||||
local mode="$1"
|
||||
shift
|
||||
local table_args=()
|
||||
if [ "${1:-}" = "-t" ]; then
|
||||
table_args=("$1" "$2")
|
||||
@@ -25,8 +32,8 @@ data:
|
||||
local chain="$1"
|
||||
shift
|
||||
|
||||
if ! iptables "${table_args[@]}" -C "${chain}" "$@" >/dev/null 2>&1; then
|
||||
iptables "${table_args[@]}" -I "${chain}" 1 "$@"
|
||||
if ! "${IPTABLES[@]}" "${table_args[@]}" -C "${chain}" "$@" >/dev/null 2>&1; then
|
||||
"${IPTABLES[@]}" "${table_args[@]}" "${mode}" "${chain}" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -40,23 +47,94 @@ data:
|
||||
local chain="$1"
|
||||
shift
|
||||
|
||||
while iptables "${table_args[@]}" -D "${chain}" "$@" >/dev/null 2>&1; do
|
||||
while "${IPTABLES[@]}" "${table_args[@]}" -D "${chain}" "$@" >/dev/null 2>&1; do
|
||||
true
|
||||
done
|
||||
}
|
||||
|
||||
ensure_append_rule() {
|
||||
local table_args=()
|
||||
if [ "${1:-}" = "-t" ]; then
|
||||
table_args=("$1" "$2")
|
||||
shift 2
|
||||
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
|
||||
|
||||
local chain="$1"
|
||||
shift
|
||||
ensure_chain "${inactive}"
|
||||
"${IPTABLES[@]}" -F "${inactive}"
|
||||
|
||||
if ! iptables "${table_args[@]}" -C "${chain}" "$@" >/dev/null 2>&1; then
|
||||
iptables "${table_args[@]}" -A "${chain}" "$@"
|
||||
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
|
||||
}
|
||||
|
||||
@@ -70,21 +148,51 @@ 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).
|
||||
delete_rule INPUT -i tailscale0 -p udp -m comment --comment amneziawg-block-tailscale -j DROP
|
||||
ensure_insert_rule INPUT -i "${EXT_IF}" -p udp --dport "${PORT}" -m comment --comment amneziawg-allow-external -j ACCEPT
|
||||
ensure_insert_rule INPUT -i tailscale0 -p udp --dport "${PORT}" -m comment --comment amneziawg-block-tailscale -j DROP
|
||||
ensure_append_rule INPUT -i awg0 -m comment --comment amneziawg-awg-input -j ACCEPT
|
||||
ensure_append_rule FORWARD -i awg0 -m comment --comment amneziawg-forward-in -j ACCEPT
|
||||
ensure_append_rule FORWARD -o awg0 -m comment --comment amneziawg-forward-out -j ACCEPT
|
||||
ensure_append_rule -t nat POSTROUTING -s "${VPN_CIDR}" -o "${EXT_IF}" -m comment --comment amneziawg-masquerade -j MASQUERADE
|
||||
delete_rule INPUT -i "${EXT_IF}" -p udp -m comment --comment amneziawg-allow-external -j ACCEPT
|
||||
delete_rule INPUT -i "${EXT_IF}" -p udp --dport "${PORT}" -m comment --comment amneziawg-allow-external -j ACCEPT
|
||||
delete_rule INPUT -i tailscale0 -p udp --dport "${PORT}" -m comment --comment amneziawg-block-tailscale -j DROP
|
||||
delete_rule INPUT -i awg0 -m comment --comment amneziawg-awg-input -j ACCEPT
|
||||
delete_rule FORWARD -i awg0 -m comment --comment amneziawg-forward-in -j ACCEPT
|
||||
delete_rule FORWARD -o awg0 -m comment --comment amneziawg-forward-out -j ACCEPT
|
||||
delete_rule INPUT -m comment --comment amneziawg-input-jump -j AMNEZIAWG-INPUT
|
||||
"${IPTABLES[@]}" -F AMNEZIAWG-INPUT >/dev/null 2>&1 || true
|
||||
"${IPTABLES[@]}" -X AMNEZIAWG-INPUT >/dev/null 2>&1 || true
|
||||
|
||||
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
|
||||
|
||||
/scripts/firewall-check.sh "${PORT}" "${VPN_CIDR}"
|
||||
|
||||
firewall-down.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"
|
||||
POLICY_CHAIN_A="AMNEZIAWG-POLICY-A"
|
||||
POLICY_CHAIN_B="AMNEZIAWG-POLICY-B"
|
||||
IPTABLES=(iptables -w 30)
|
||||
|
||||
external_interface() {
|
||||
ip route get 1.1.1.1 | awk '{for (i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}'
|
||||
@@ -100,7 +208,7 @@ data:
|
||||
local chain="$1"
|
||||
shift
|
||||
|
||||
while iptables "${table_args[@]}" -D "${chain}" "$@" >/dev/null 2>&1; do
|
||||
while "${IPTABLES[@]}" "${table_args[@]}" -D "${chain}" "$@" >/dev/null 2>&1; do
|
||||
true
|
||||
done
|
||||
}
|
||||
@@ -111,6 +219,7 @@ data:
|
||||
fi
|
||||
|
||||
if [ -n "${EXT_IF}" ]; then
|
||||
delete_rule INPUT -i "${EXT_IF}" -p udp -m comment --comment amneziawg-allow-external -j ACCEPT
|
||||
delete_rule INPUT -i "${EXT_IF}" -p udp --dport "${PORT}" -m comment --comment amneziawg-allow-external -j ACCEPT
|
||||
delete_rule -t nat POSTROUTING -s "${VPN_CIDR}" -o "${EXT_IF}" -m comment --comment amneziawg-masquerade -j MASQUERADE
|
||||
fi
|
||||
@@ -120,6 +229,78 @@ data:
|
||||
delete_rule INPUT -i awg0 -m comment --comment amneziawg-awg-input -j ACCEPT
|
||||
delete_rule FORWARD -i awg0 -m comment --comment amneziawg-forward-in -j ACCEPT
|
||||
delete_rule FORWARD -o awg0 -m comment --comment amneziawg-forward-out -j ACCEPT
|
||||
delete_rule INPUT -m comment --comment amneziawg-input-jump -j AMNEZIAWG-INPUT
|
||||
delete_rule FORWARD -m comment --comment amneziawg-forward-jump -j "${FORWARD_CHAIN}"
|
||||
|
||||
"${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:-5848}"
|
||||
VPN_CIDR="${2:-10.8.0.0/16}"
|
||||
FORWARD_CHAIN="AMNEZIAWG-FORWARD"
|
||||
IPTABLES=(iptables -w 5)
|
||||
|
||||
external_interface() {
|
||||
ip route get 1.1.1.1 | awk '{for (i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}'
|
||||
}
|
||||
|
||||
EXT_IF="$(external_interface || true)"
|
||||
if [ -z "${EXT_IF}" ]; then
|
||||
EXT_IF="$(ip route show default | awk '{print $5; exit}')"
|
||||
fi
|
||||
if [ -z "${EXT_IF}" ]; then
|
||||
echo "Unable to detect external interface" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if "${IPTABLES[@]}" -C INPUT -i tailscale0 -p udp -m comment --comment amneziawg-block-tailscale -j DROP >/dev/null 2>&1; then
|
||||
echo "Unsafe broad UDP DROP on tailscale0 is present" >&2
|
||||
exit 1
|
||||
fi
|
||||
if "${IPTABLES[@]}" -C INPUT -i "${EXT_IF}" -p udp -m comment --comment amneziawg-allow-external -j ACCEPT >/dev/null 2>&1; then
|
||||
echo "Unsafe broad UDP ACCEPT on ${EXT_IF} is present" >&2
|
||||
exit 1
|
||||
fi
|
||||
if "${IPTABLES[@]}" -C INPUT -m comment --comment amneziawg-input-jump -j AMNEZIAWG-INPUT >/dev/null 2>&1; then
|
||||
echo "Unexpected AmneziaWG INPUT chain is present" >&2
|
||||
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}"
|
||||
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: |
|
||||
#!/usr/bin/env bash
|
||||
@@ -127,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"
|
||||
@@ -161,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() {
|
||||
@@ -188,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() {
|
||||
@@ -201,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
|
||||
@@ -221,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}"
|
||||
@@ -237,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)}"
|
||||
|
||||
@@ -246,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
|
||||
|
||||
@@ -52,7 +52,7 @@ spec:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
- name: PORT
|
||||
value: "5847"
|
||||
value: "5848"
|
||||
command:
|
||||
- /bin/bash
|
||||
- -lc
|
||||
@@ -116,27 +116,36 @@ spec:
|
||||
- /scripts/run.sh
|
||||
ports:
|
||||
- name: awg
|
||||
containerPort: 5847
|
||||
containerPort: 5848
|
||||
protocol: UDP
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/bash
|
||||
- -lc
|
||||
- awg show awg0 >/dev/null 2>&1
|
||||
- /scripts/firewall-check.sh 5848 10.8.0.0/16
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
timeoutSeconds: 8
|
||||
failureThreshold: 2
|
||||
startupProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/bash
|
||||
- -lc
|
||||
- /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
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/bash
|
||||
- -lc
|
||||
- awg show awg0 >/dev/null 2>&1
|
||||
- /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: 5
|
||||
timeoutSeconds: 15
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,21 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: firefly-iii
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: apps
|
||||
destination:
|
||||
namespace: firefly-iii
|
||||
server: https://kubernetes.default.svc
|
||||
source:
|
||||
repoURL: ssh://git@gt.hexor.cy:30022/ab/homelab.git
|
||||
targetRevision: HEAD
|
||||
path: k8s/apps/firefly-iii
|
||||
syncPolicy:
|
||||
automated:
|
||||
selfHeal: true
|
||||
prune: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: firefly-data-importer-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: firefly-data-importer
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
# OAuth callback is intentionally public. Data Importer validates both the
|
||||
# state parameter and its browser session before exchanging the code.
|
||||
- match: Host(`import.hexor.cy`) && Path(`/eb-callback`)
|
||||
kind: Rule
|
||||
priority: 200
|
||||
services:
|
||||
- name: firefly-data-importer
|
||||
port: 80
|
||||
# Everything else requires a valid auth-proxy session and group.
|
||||
- match: Host(`import.hexor.cy`)
|
||||
kind: Rule
|
||||
priority: 100
|
||||
middlewares:
|
||||
- name: firefly-data-importer-auth
|
||||
services:
|
||||
- name: firefly-data-importer
|
||||
port: 80
|
||||
tls:
|
||||
secretName: firefly-data-importer-tls
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: firefly-data-importer-tls
|
||||
spec:
|
||||
secretName: firefly-data-importer-tls
|
||||
issuerRef:
|
||||
name: letsencrypt
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- import.hexor.cy
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,105 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: firefly-data-importer
|
||||
labels:
|
||||
app.kubernetes.io/name: firefly-data-importer
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: firefly-data-importer
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: firefly-data-importer
|
||||
spec:
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: ai.tail2fe2d.ts.net
|
||||
tolerations:
|
||||
- key: workload
|
||||
operator: Equal
|
||||
value: ai
|
||||
effect: NoSchedule
|
||||
containers:
|
||||
- name: data-importer
|
||||
image: fireflyiii/data-importer:version-2.3.4
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: FIREFLY_III_URL
|
||||
value: http://firefly-iii
|
||||
- name: VANITY_URL
|
||||
value: http://ff.lan
|
||||
- name: TRUSTED_PROXIES
|
||||
value: "**"
|
||||
- name: EXPECT_SECURE_URL
|
||||
value: "false"
|
||||
- name: VERIFY_TLS_SECURITY
|
||||
value: "true"
|
||||
- name: TZ
|
||||
value: Europe/London
|
||||
- name: APP_ENV
|
||||
value: production
|
||||
- name: APP_DEBUG
|
||||
value: "false"
|
||||
- name: LOG_LEVEL
|
||||
value: notice
|
||||
- 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
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 30
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 512Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
securityContext:
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: firefly-data-importer
|
||||
labels:
|
||||
app.kubernetes.io/name: firefly-data-importer
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: firefly-data-importer
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
@@ -0,0 +1,105 @@
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: firefly-secrets
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
target:
|
||||
name: firefly-secrets
|
||||
creationPolicy: Owner
|
||||
deletionPolicy: Delete
|
||||
template:
|
||||
type: Opaque
|
||||
data:
|
||||
# Firefly and CloudNativePG deliberately share this generated Secret.
|
||||
username: firefly
|
||||
password: |-
|
||||
{{ .db_password }}
|
||||
DB_PASSWORD: |-
|
||||
{{ .db_password }}
|
||||
APP_KEY: |-
|
||||
{{ .app_key }}
|
||||
STATIC_CRON_TOKEN: |-
|
||||
{{ .cron_token }}
|
||||
data:
|
||||
- secretKey: db_password
|
||||
sourceRef:
|
||||
storeRef:
|
||||
name: vaultwarden-login
|
||||
kind: ClusterSecretStore
|
||||
remoteRef:
|
||||
key: a1867c81-715c-47cd-978d-14ea5bcedea9
|
||||
property: fields[0].value
|
||||
- secretKey: app_key
|
||||
sourceRef:
|
||||
storeRef:
|
||||
name: vaultwarden-login
|
||||
kind: ClusterSecretStore
|
||||
remoteRef:
|
||||
key: a1867c81-715c-47cd-978d-14ea5bcedea9
|
||||
property: fields[1].value
|
||||
- secretKey: cron_token
|
||||
sourceRef:
|
||||
storeRef:
|
||||
name: vaultwarden-login
|
||||
kind: ClusterSecretStore
|
||||
remoteRef:
|
||||
key: a1867c81-715c-47cd-978d-14ea5bcedea9
|
||||
property: fields[2].value
|
||||
---
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: firefly-data-importer-secrets
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
target:
|
||||
name: firefly-data-importer-secrets
|
||||
creationPolicy: Owner
|
||||
deletionPolicy: Delete
|
||||
template:
|
||||
type: Opaque
|
||||
data:
|
||||
ENABLE_BANKING_APP_ID: |-
|
||||
{{ .app_id }}
|
||||
ENABLE_BANKING_PRIVATE_KEY: |-
|
||||
-----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:
|
||||
storeRef:
|
||||
name: vaultwarden-login
|
||||
kind: ClusterSecretStore
|
||||
remoteRef:
|
||||
key: a1867c81-715c-47cd-978d-14ea5bcedea9
|
||||
property: fields[3].value
|
||||
- secretKey: private_key
|
||||
sourceRef:
|
||||
storeRef:
|
||||
name: vaultwarden-login
|
||||
kind: ClusterSecretStore
|
||||
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
|
||||
@@ -0,0 +1,101 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
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
|
||||
- network-policy.yaml
|
||||
|
||||
helmCharts:
|
||||
- name: firefly-iii
|
||||
repo: https://harish2k01.github.io/helm-charts
|
||||
version: 0.1.2
|
||||
releaseName: firefly-iii
|
||||
namespace: firefly-iii
|
||||
valuesFile: values.yaml
|
||||
includeCRDs: true
|
||||
|
||||
patches:
|
||||
# Chart 0.1.2 requires postgres.enabled=true. Remove its StatefulSet and
|
||||
# make the generated database Service select the CloudNativePG instance.
|
||||
- target:
|
||||
group: apps
|
||||
version: v1
|
||||
kind: StatefulSet
|
||||
name: firefly-iii-postgres
|
||||
patch: |-
|
||||
$patch: delete
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: firefly-iii-postgres
|
||||
- target:
|
||||
version: v1
|
||||
kind: Service
|
||||
name: firefly-iii-postgres
|
||||
patch: |-
|
||||
- op: replace
|
||||
path: /spec/selector
|
||||
value:
|
||||
cnpg.io/cluster: firefly-postgres
|
||||
# The upstream chart does not expose scheduling settings for its CronJob.
|
||||
- target:
|
||||
group: batch
|
||||
version: v1
|
||||
kind: CronJob
|
||||
name: firefly-iii-cron
|
||||
patch: |-
|
||||
- op: add
|
||||
path: /spec/concurrencyPolicy
|
||||
value: Forbid
|
||||
- op: add
|
||||
path: /spec/jobTemplate/spec/backoffLimit
|
||||
value: 1
|
||||
- op: add
|
||||
path: /spec/jobTemplate/spec/activeDeadlineSeconds
|
||||
value: 600
|
||||
- op: add
|
||||
path: /spec/jobTemplate/spec/template/spec/nodeSelector
|
||||
value:
|
||||
kubernetes.io/hostname: ai.tail2fe2d.ts.net
|
||||
- op: add
|
||||
path: /spec/jobTemplate/spec/template/spec/tolerations
|
||||
value:
|
||||
- key: workload
|
||||
operator: Equal
|
||||
value: ai
|
||||
effect: NoSchedule
|
||||
- op: replace
|
||||
path: /spec/jobTemplate/spec/template/spec/containers/0/command
|
||||
value:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- >-
|
||||
curl --fail --silent --show-error
|
||||
--retry 30 --retry-delay 10 --retry-all-errors
|
||||
--connect-timeout 5 --max-time 30
|
||||
"http://firefly-iii:80/api/v1/cron/${STATIC_CRON_TOKEN}"
|
||||
# A second independent filter in addition to ingressClassName.
|
||||
- target:
|
||||
group: networking.k8s.io
|
||||
version: v1
|
||||
kind: Ingress
|
||||
name: firefly-iii
|
||||
patch: |-
|
||||
- op: add
|
||||
path: /metadata/labels/firefly.hexor.cy~1private-ai
|
||||
value: "true"
|
||||
# The shared Traefik has the same controller identifier and can discover
|
||||
# IngressClass objects. The explicit annotation partitions this Ingress
|
||||
# so only the instance configured with ingressclass=traefik-ai accepts it.
|
||||
- op: add
|
||||
path: /metadata/annotations/kubernetes.io~1ingress.class
|
||||
value: traefik-ai
|
||||
- op: remove
|
||||
path: /spec/ingressClassName
|
||||
@@ -0,0 +1,101 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: firefly-ingress
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: firefly-iii
|
||||
app.kubernetes.io/name: firefly-iii
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: traefik-ai
|
||||
# The chart's CronJob uses the same selector labels as the application.
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: firefly-iii
|
||||
app.kubernetes.io/name: firefly-iii
|
||||
- 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
|
||||
- ipBlock:
|
||||
cidr: 100.77.155.120/32
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: firefly-postgres-ingress
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
cnpg.io/cluster: firefly-postgres
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: firefly-iii
|
||||
app.kubernetes.io/name: firefly-iii
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
cnpg.io/cluster: firefly-postgres
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
# CloudNativePG operator health/status traffic to the instance manager.
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: psql
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: cloudnative-pg
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8000
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: firefly-data-importer-ingress
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: firefly-data-importer
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
# Shared Traefik serves the public HTTPS importer.
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: traefik
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
# Keep node traffic allowed for probes and local diagnostics.
|
||||
- from:
|
||||
- ipBlock:
|
||||
cidr: 192.168.1.117/32
|
||||
- ipBlock:
|
||||
cidr: 100.77.155.120/32
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
@@ -0,0 +1,49 @@
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
name: firefly-postgres
|
||||
spec:
|
||||
description: PostgreSQL for Firefly III
|
||||
instances: 1
|
||||
enableSuperuserAccess: false
|
||||
primaryUpdateStrategy: unsupervised
|
||||
bootstrap:
|
||||
initdb:
|
||||
database: firefly
|
||||
owner: firefly
|
||||
secret:
|
||||
name: firefly-secrets
|
||||
dataChecksums: true
|
||||
storage:
|
||||
size: 10Gi
|
||||
storageClass: firefly-local
|
||||
pvcTemplate:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: firefly-local
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: firefly-postgres
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
volumeMode: Filesystem
|
||||
affinity:
|
||||
enablePodAntiAffinity: false
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: ai.tail2fe2d.ts.net
|
||||
tolerations:
|
||||
- key: workload
|
||||
operator: Equal
|
||||
value: ai
|
||||
effect: NoSchedule
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
monitoring:
|
||||
enablePodMonitor: true
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: firefly-storage-prep
|
||||
annotations:
|
||||
argocd.argoproj.io/hook: PreSync
|
||||
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: firefly-storage-prep
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: ai.tail2fe2d.ts.net
|
||||
tolerations:
|
||||
- key: workload
|
||||
operator: Equal
|
||||
value: ai
|
||||
effect: NoSchedule
|
||||
containers:
|
||||
- name: prepare
|
||||
image: busybox:1.37.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- |
|
||||
mkdir -p /host/k8s/firefly-iii/postgres
|
||||
chown 26:26 /host/k8s/firefly-iii/postgres
|
||||
chmod 0700 /host/k8s/firefly-iii/postgres
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
runAsGroup: 0
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- FOWNER
|
||||
volumeMounts:
|
||||
- name: host-root
|
||||
mountPath: /host/k8s/firefly-iii
|
||||
volumes:
|
||||
- name: host-root
|
||||
hostPath:
|
||||
path: /k8s/firefly-iii
|
||||
type: DirectoryOrCreate
|
||||
@@ -0,0 +1,72 @@
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: firefly-local
|
||||
provisioner: kubernetes.io/no-provisioner
|
||||
reclaimPolicy: Retain
|
||||
volumeBindingMode: WaitForFirstConsumer
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: firefly-postgres
|
||||
labels:
|
||||
app.kubernetes.io/name: firefly-postgres
|
||||
spec:
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: firefly-local
|
||||
volumeMode: Filesystem
|
||||
hostPath:
|
||||
path: /k8s/firefly-iii/postgres
|
||||
type: DirectoryOrCreate
|
||||
nodeAffinity:
|
||||
required:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- ai.tail2fe2d.ts.net
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: firefly-uploads
|
||||
labels:
|
||||
app.kubernetes.io/name: firefly-uploads
|
||||
spec:
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: firefly-local
|
||||
volumeMode: Filesystem
|
||||
hostPath:
|
||||
path: /k8s/firefly-iii/uploads
|
||||
type: DirectoryOrCreate
|
||||
nodeAffinity:
|
||||
required:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- ai.tail2fe2d.ts.net
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: firefly-uploads
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: firefly-local
|
||||
volumeName: firefly-uploads
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
@@ -0,0 +1,68 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: fireflyiii/core
|
||||
tag: version-6.6.1
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: traefik-ai
|
||||
hosts:
|
||||
- host: ff.lan
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
|
||||
persistence:
|
||||
upload:
|
||||
enabled: true
|
||||
existingClaim: firefly-uploads
|
||||
|
||||
# Must remain true because chart 0.1.2 validates it. Kustomize removes the
|
||||
# generated StatefulSet and makes this Service select the CloudNativePG Pod.
|
||||
postgres:
|
||||
enabled: true
|
||||
auth:
|
||||
database: firefly
|
||||
username: firefly
|
||||
service:
|
||||
port: 5432
|
||||
|
||||
cronjob:
|
||||
enabled: true
|
||||
schedule: "0 3 * * *"
|
||||
timeZone: Europe/London
|
||||
|
||||
firefly:
|
||||
env:
|
||||
APP_ENV: production
|
||||
APP_DEBUG: "false"
|
||||
APP_URL: http://ff.lan
|
||||
SITE_OWNER: owner@ff.lan
|
||||
TZ: Europe/London
|
||||
DEFAULT_LANGUAGE: en_US
|
||||
TRUSTED_PROXIES: 10.42.0.0/16
|
||||
COOKIE_SECURE: "false"
|
||||
PGSQL_SSL_MODE: prefer
|
||||
secrets:
|
||||
existingSecret: firefly-secrets
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: ai.tail2fe2d.ts.net
|
||||
tolerations:
|
||||
- key: workload
|
||||
operator: Equal
|
||||
value: ai
|
||||
effect: NoSchedule
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 1Gi
|
||||
@@ -8,3 +8,4 @@ resources:
|
||||
- ingress.yaml
|
||||
- deployment.yaml
|
||||
- servicemonitor.yaml
|
||||
- socks-proxy.yaml
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- app.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -17,6 +17,8 @@ spec:
|
||||
labels:
|
||||
app: teamspeak
|
||||
spec:
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: music.tail2fe2d.ts.net
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -10,7 +10,7 @@ resources:
|
||||
helmCharts:
|
||||
- name: argo-cd
|
||||
repo: https://argoproj.github.io/argo-helm
|
||||
version: 9.4.10
|
||||
version: 10.6.0
|
||||
releaseName: argocd
|
||||
namespace: argocd
|
||||
valuesFile: values.yaml
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: IngressClass
|
||||
metadata:
|
||||
name: traefik-ai
|
||||
spec:
|
||||
controller: traefik.io/ingress-controller
|
||||
---
|
||||
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:
|
||||
- ""
|
||||
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: firefly-iii
|
||||
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: 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: traefik-ai
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- nodes
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingressclasses
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: traefik-ai
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: traefik-ai
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: traefik-ai
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: traefik-ai
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app.kubernetes.io/name: traefik-ai
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: traefik-ai
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: traefik-ai
|
||||
spec:
|
||||
serviceAccountName: traefik-ai
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: ai.tail2fe2d.ts.net
|
||||
tolerations:
|
||||
- key: workload
|
||||
operator: Equal
|
||||
value: ai
|
||||
effect: NoSchedule
|
||||
containers:
|
||||
- name: traefik
|
||||
image: rancher/mirrored-library-traefik:3.6.13
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- --entrypoints.web.address=192.168.1.117:80
|
||||
- --providers.kubernetesingress=true
|
||||
- --providers.kubernetesingress.namespaces=firefly-iii,lan-play
|
||||
- --providers.kubernetesingress.ingressclass=traefik-ai
|
||||
- --providers.kubernetesingress.labelselector=firefly.hexor.cy/private-ai=true
|
||||
- --providers.kubernetescrd=false
|
||||
- --api.dashboard=false
|
||||
- --log.level=INFO
|
||||
ports:
|
||||
- name: web
|
||||
containerPort: 80
|
||||
hostPort: 80
|
||||
hostIP: 192.168.1.117
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
host: 192.168.1.117
|
||||
port: web
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
host: 192.168.1.117
|
||||
port: web
|
||||
periodSeconds: 30
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- NET_BIND_SERVICE
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: false
|
||||
runAsUser: 0
|
||||
runAsGroup: 0
|
||||
securityContext:
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
@@ -7,9 +7,9 @@ kind: Kustomization
|
||||
helmCharts:
|
||||
- name: longhorn
|
||||
repo: https://charts.longhorn.io
|
||||
version: 1.12.0
|
||||
version: 1.12.1
|
||||
releaseName: longhorn
|
||||
namespace: longhorn
|
||||
namespace: longhorn-system
|
||||
valuesFile: values.yaml
|
||||
includeCRDs: true
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Generated
-44
@@ -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",
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user