Compare commits

...
Author SHA1 Message Date
Aleksandr Bogomiakov 7db427f332 NG: secret stores with lookup by path or id
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 20s
Move the Vaultwarden bridge and the ClusterSecretStores out of external-secrets
into secret-stores (wave 4, after vaultwarden). A store key may be an item UUID
or an item name used as a path (k8s/<namespace>/<what>), matched exactly.
2026-10-06 15:13:09 +01:00
Aleksandr Bogomiakov 41729bf256 NG: enable sso for VW
Keycloak Terraform / Terraform (push) Failing after 39s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 19s
2026-10-06 00:42:40 +01:00
Aleksandr Bogomiakov fe914e2de4 NG: enable sso for VW
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 28s
2026-10-06 00:18:56 +01:00
Aleksandr Bogomiakov 0348329829 NG: enable sso for argocd
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 15s
2026-10-06 00:11:02 +01:00
Aleksandr Bogomiakov e257e3895b NG: added Keycloak
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 10s
2026-10-05 23:29:35 +01:00
Aleksandr Bogomiakov 4fe2a2b5f3 NG: added reloader
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 22s
2026-10-05 19:17:41 +01:00
Aleksandr Bogomiakov 690ff2a19c NG: added http -> https redir
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 17s
2026-10-05 19:00:28 +01:00
Aleksandr Bogomiakov 99e1818572 NG: app-of-apps CORE
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 24s
2026-10-05 18:49:42 +01:00
Aleksandr Bogomiakov cf88753ff5 NG: secret manager and VW
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Failing after 14m33s
2026-10-05 18:40:46 +01:00
Aleksandr Bogomiakov 862a7619e2 Track main instead of k8s-ng in the new cluster's applications
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 22s
2026-10-05 18:31:58 +01:00
Aleksandr Bogomiakov 5a29dcc236 Stop tracking rendered Helm charts in k8s-ng 2026-10-05 18:31:52 +01:00
Aleksandr Bogomiakov bcfbfcd362 Moved PSQL to NG 2026-10-05 18:30:17 +01:00
Aleksandr Bogomiakov 2676ecef5d Moved ArgoCD to NG 2026-10-05 18:13:48 +01:00
Ultradesu b4de55f3c0 Change AmneziaWG port to 5848
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 12s
Check with kubeconform / lint (push) Successful in 10s
Auto-update README / Generate README and Create MR (push) Successful in 11s
2026-09-30 14:00:07 +01:00
Ultradesu de6c9562f1 Update AmneziaWG port to 443, refresh obfuscation params and update fellow image to 1.0.2
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 17s
Check with kubeconform / lint (push) Failing after 7m19s
Auto-update README / Generate README and Create MR (push) Successful in 4m32s
2026-09-30 13:53:38 +01:00
ab 0c333375a5 Fixed AWG scripts
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 5s
Check with kubeconform / lint (push) Successful in 6s
Auto-update README / Generate README and Create MR (push) Successful in 5s
2026-09-17 17:48:53 +03:00
ab 5783547bd3 Fixed AWG scripts
Check with kubeconform / lint (push) Successful in 12s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 9s
Auto-update README / Generate README and Create MR (push) Successful in 6s
2026-09-17 17:38:30 +03:00
ab 29c5b00c16 Added airtrail deployment
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 6s
Check with kubeconform / lint (push) Successful in 7s
Auto-update README / Generate README and Create MR (push) Successful in 9s
2026-09-12 07:18:56 +03:00
ab 76b2090ccc Added airtrail psql user
Check with kubeconform / lint (push) Successful in 9s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 10s
Auto-update README / Generate README and Create MR (push) Successful in 8s
2026-09-12 07:11:22 +03:00
ab bc83f7dd24 Update k8s/apps/pasarguard/deployment.yaml
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 20s
Check with kubeconform / lint (push) Successful in 14s
Auto-update README / Generate README and Create MR (push) Successful in 21s
2026-09-02 08:48:55 +00:00
Aleksandr Bogomiakov 5768a4ff3b fix grafana datasource
Check with kubeconform / lint (push) Successful in 7s
Update Kubernetes Services Wiki / Generate and Update K8s Wiki (push) Successful in 6s
Auto-update README / Generate README and Create MR (push) Successful in 9s
2026-09-01 23:06:49 +01:00
63 changed files with 2154 additions and 40 deletions

No files matched your search

+3
View File
@@ -59,3 +59,6 @@ tags
[._]*.un~
.DS_Store
# Helm charts pulled by kustomize (--enable-helm) when rendering locally
k8s-ng/**/charts/
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Brings the whole core state up on an empty cluster:
# k8s-ng/bootstrap/bootstrap.sh <kube-context>
# Installs Argo CD (the only thing that cannot install itself) and applies the
# root application; Argo CD then pulls every core application in wave order.
set -euo pipefail
ctx=${1:?usage: bootstrap.sh <kube-context>}
here=$(cd "$(dirname "$0")" && pwd)
k="kubectl --context $ctx"
# Render in a scratch copy so kustomize does not leave downloaded charts in the repo.
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
cp -r "$here/../core/argocd" "$tmp/argocd"
kubectl kustomize --enable-helm "$tmp/argocd" > "$tmp/argocd.yaml"
$k create namespace argocd --dry-run=client -o yaml | $k apply -f -
# CRDs must exist before the Argo CD objects that use them: apply twice.
$k apply --server-side --force-conflicts -f "$tmp/argocd.yaml" >/dev/null 2>&1 || true
sleep 8
$k apply --server-side --force-conflicts -f "$tmp/argocd.yaml"
$k -n argocd rollout status deploy --timeout=300s
$k -n argocd rollout status statefulset --timeout=300s
$k apply --server-side --force-conflicts -f "$here/root.yaml"
echo "Core state requested. Watch it with: $k -n argocd get app"
+29
View File
@@ -0,0 +1,29 @@
# Root of the core state. Applying this one object makes Argo CD pull every core
# application in wave order (see k8s-ng/core-state/values.yaml).
# Note: there are deliberately no finalizers; deleting this Application keeps all
# deployed services and data.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: core-state
namespace: argocd
spec:
project: core
destination:
server: https://kubernetes.default.svc
namespace: argocd
source:
repoURL: https://gt.hexor.cy/ab/homelab.git
targetRevision: main
path: k8s-ng/core-state
helm:
# Per-cluster overrides of core-state/values.yaml, for example:
# valuesObject:
# targetRevision: some-branch
valuesObject: {}
syncPolicy:
automated:
selfHeal: true
prune: true
syncOptions:
- ServerSideApply=true
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v2
name: core-state
description: App of apps for the core services of the cluster
version: 0.1.0
@@ -0,0 +1,29 @@
{{- range .Values.apps }}
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: {{ .name }}
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: {{ .wave | quote }}
spec:
project: {{ .project | default $.Values.project }}
destination:
server: {{ $.Values.destinationServer }}
namespace: {{ .namespace }}
source:
repoURL: {{ $.Values.repoURL }}
targetRevision: {{ $.Values.targetRevision }}
path: {{ .path }}
syncPolicy:
automated:
selfHeal: true
prune: {{ if hasKey . "prune" }}{{ .prune }}{{ else }}true{{ end }}
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
{{- if .skipDryRunOnMissingResource }}
- SkipDryRunOnMissingResource=true
{{- end }}
{{- end }}
+60
View File
@@ -0,0 +1,60 @@
# 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: secret-stores
path: k8s-ng/core/secret-stores
namespace: external-secrets
wave: 4 # reads the Vaultwarden vault, so it comes after vaultwarden
skipDryRunOnMissingResource: true # ClusterSecretStore CRD comes from the external-secrets app
+66
View File
@@ -0,0 +1,66 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: core
namespace: argocd
spec:
clusterResourceWhitelist:
- group: '*'
kind: '*'
description: Hexor Home Lab Core
destinations:
- namespace: '*'
server: https://kubernetes.default.svc
sourceRepos:
- https://gt.hexor.cy/ab/homelab.git
---
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: apps
namespace: argocd
spec:
clusterResourceWhitelist:
- group: '*'
kind: '*'
description: Hexor Home Lab Apps
destinations:
- namespace: '*'
server: https://kubernetes.default.svc
sourceRepos:
- https://gt.hexor.cy/ab/homelab.git
---
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: games
namespace: argocd
spec:
clusterResourceWhitelist:
- group: '*'
kind: '*'
description: Hexor Home Lab Games
destinations:
- namespace: '*'
server: https://kubernetes.default.svc
sourceRepos:
- https://gt.hexor.cy/ab/homelab.git
---
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: desktop
namespace: argocd
spec:
clusterResourceWhitelist:
- group: '*'
kind: '*'
description: Hexor Home Lab Desktop Apps
destinations:
- namespace: '*'
server: https://kubernetes.default.svc
sourceRepos:
- https://gt.hexor.cy/ab/homelab.git
+26
View File
@@ -0,0 +1,26 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: argocd-dex-server
namespace: argocd
spec:
template:
spec:
containers:
- name: dex-server
readinessProbe:
tcpSocket:
port: 5556
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
livenessProbe:
tcpSocket:
port: 5556
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
+25
View File
@@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd
namespace: argocd
annotations:
cert-manager.io/cluster-issuer: letsencrypt
traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec:
tls:
- hosts:
- ag.btwiusearch.net
secretName: argocd-server-tls
rules:
- host: ag.btwiusearch.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 80
+18
View File
@@ -0,0 +1,18 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ingress.yaml
- appprojects.yaml
helmCharts:
- name: argo-cd
repo: https://argoproj.github.io/argo-helm
version: 10.6.0
releaseName: argocd
namespace: argocd
valuesFile: values.yaml
includeCRDs: true
patches:
- path: dex-probes-patch.yaml
+115
View File
@@ -0,0 +1,115 @@
# Argo CD Helm Chart Values (k8s-ng cluster)
global:
domain: ag.btwiusearch.net
logging:
format: text
level: info
crds:
install: true
keep: true # Recommended to keep true so CRDs are not deleted on uninstall
configs:
params:
server.insecure: "true" # TLS is terminated by the ingress
cm:
create: true
exec.enabled: true
kustomize.buildOptions: --enable-helm
application.instanceLabelKey: argocd.argoproj.io/instance
# Local admin account, kept as a break-glass login next to SSO; the initial
# password is generated by Argo CD into the argocd-initial-admin-secret secret.
# Set to false once SSO is trusted.
admin.enabled: true
statusbadge.enabled: true
timeout.reconciliation: 60s
# Lets sync waves in an app of apps wait for child Applications to be Healthy.
resource.customizations.health.argoproj.io_Application: |
hs = {}
hs.status = "Progressing"
hs.message = ""
if obj.status ~= nil then
if obj.status.health ~= nil then
hs.status = obj.status.health.status
if obj.status.health.message ~= nil then
hs.message = obj.status.health.message
end
end
end
return hs
# SSO through Dex -> Keycloak. The client id/secret come from the oidc-creds
# Secret (labelled app.kubernetes.io/part-of=argocd), created by hand until
# Vaultwarden + External Secrets are the source.
dex.config: |
connectors:
- type: oidc
id: keycloak
name: Keycloak
config:
issuer: https://auth.hexor.cy/auth/realms/hexor
clientID: $oidc-creds:id
clientSecret: $oidc-creds:secret
insecureEnableGroups: true
scopes:
- openid
- profile
- email
- offline_access
getUserInfo: true
rbac:
create: true
policy.default: ""
policy.csv: |
g, game-servers-managers, GameServersManagersRole
# Role permissions
p, GameServersManagersRole, applications, get, games/*, allow
p, GameServersManagersRole, applications, update, games/*, allow
p, GameServersManagersRole, applications, sync, games/*, allow
p, GameServersManagersRole, applications, override, games/*, allow
p, GameServersManagersRole, applications, action/*, games/*, allow
p, GameServersManagersRole, exec, create, games/*, allow
p, GameServersManagersRole, logs, get, games/*, allow
p, GameServersManagersRole, applications, delete, games/*, deny
# Admin policy
g, argocd-admins, role:admin
secret:
createSecret: true
controller:
replicas: 1
# Dex OIDC provider (Keycloak connector configured in configs.cm.dex.config)
dex:
replicas: 1
enabled: true
redis:
enabled: true
redis-ha:
enabled: false
server:
replicas: 1
ingress:
enabled: false
certificate:
enabled: false
certificateSecret:
enabled: false
repoServer:
replicas: 1
livenessProbe:
timeoutSeconds: 10
periodSeconds: 60
applicationSet:
enabled: true
replicas: 1
notifications:
enabled: true
replicas: 1
+38
View File
@@ -0,0 +1,38 @@
# Public certificates through DNS-01. The credential Secrets are created by hand
# (they will move to Vaultwarden + External Secrets):
# kubectl -n cert-manager create secret generic cloudflare-api-token --from-literal=apiToken=<token>
# (Zone:DNS:Edit on the btwiusearch.net zone)
# kubectl -n cert-manager create secret generic aws-api-key-secret \
# --from-literal=accessKey=<id> --from-literal=secretKey=<secret>
# (Route53 access to the hexor.cy zone)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: k8s-master@hexor.cy
privateKeySecretRef:
name: letsencrypt-account-key
solvers:
- selector:
dnsZones:
- btwiusearch.net
dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: apiToken
- selector:
dnsZones:
- hexor.cy
dns01:
route53:
region: eu-north-1
accessKeyIDSecretRef:
name: aws-api-key-secret
key: accessKey
secretAccessKeySecretRef:
name: aws-api-key-secret
key: secretKey
@@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- issuer.yaml
- self-signed.yaml
helmCharts:
- name: cert-manager
repo: https://charts.jetstack.io
version: 1.21.1
releaseName: cert-manager
namespace: cert-manager
valuesFile: values.yaml
includeCRDs: true
+29
View File
@@ -0,0 +1,29 @@
# Private CA for internal services: issuer "hexor-lab".
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: hexor-ca-root
namespace: cert-manager
spec:
isCA: true
duration: 87600h # 10y
commonName: hexor-ca
secretName: hexor-ca-root-secret
issuerRef:
name: selfsigned
kind: ClusterIssuer
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: hexor-lab
spec:
ca:
secretName: hexor-ca-root-secret
+2
View File
@@ -0,0 +1,2 @@
crds:
enabled: true
+11
View File
@@ -0,0 +1,11 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
helmCharts:
- name: cloudnative-pg
repo: https://cloudnative-pg.github.io/charts
version: 0.29.1
releaseName: cnpg
namespace: cnpg-system
valuesFile: values.yaml
includeCRDs: true
+4
View File
@@ -0,0 +1,4 @@
# CloudNativePG operator (chart 0.29.x = operator 1.30.x)
crds:
create: true
@@ -0,0 +1,13 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# The Vaultwarden bridge and the ClusterSecretStores are in ../secret-stores (they need Vaultwarden).
helmCharts:
- name: external-secrets
repo: https://charts.external-secrets.io
version: 2.11.0
releaseName: external-secrets
namespace: external-secrets
valuesFile: values.yaml
includeCRDs: true
+1
View File
@@ -0,0 +1 @@
installCRDs: true
+122
View File
@@ -0,0 +1,122 @@
# rsauth2-proxy: forward-auth for services that have no OIDC of their own.
# Needs, besides this file:
# - Secret auth-proxy-creds (AUTH_PROXY_CLIENT_ID, AUTH_PROXY_CLIENT_SECRET,
# AUTH_PROXY_COOKIE_SECRET); the client secret is the one of the Keycloak
# client "rsauth2-proxy". Created by hand until Vaultwarden is the source.
# - ConfigMap auth-proxy-routes, managed by Terraform (terraform/keycloak).
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-proxy
labels:
app: auth-proxy
spec:
replicas: 1
selector:
matchLabels:
app: auth-proxy
template:
metadata:
labels:
app: auth-proxy
spec:
containers:
- name: auth-proxy
image: ultradesu/rsauth2-proxy:latest
ports:
- containerPort: 8080
name: http
protocol: TCP
envFrom:
- secretRef:
name: auth-proxy-creds
env:
- name: AUTH_PROXY_OIDC_ISSUER
value: "https://auth.hexor.cy/auth/realms/hexor"
- name: AUTH_PROXY_COOKIE_DOMAIN
value: ".hexor.cy"
- name: AUTH_PROXY_CALLBACK_URL
value: "https://oauth.hexor.cy/callback"
- name: AUTH_PROXY_ROUTES_FILE
value: "/config/routes.yaml"
- name: AUTH_PROXY_LOG_LEVEL
value: "debug"
volumeMounts:
- name: routes
mountPath: /config
readOnly: true
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 64Mi
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 3
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 30
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
capabilities:
drop:
- ALL
volumes:
- name: routes
configMap:
name: auth-proxy-routes
---
apiVersion: v1
kind: Service
metadata:
name: auth-proxy
labels:
app: auth-proxy
spec:
ports:
- name: http
port: 80
targetPort: 8080
protocol: TCP
selector:
app: auth-proxy
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: auth-proxy
spec:
entryPoints:
- websecure
routes:
- match: Host(`oauth.hexor.cy`)
kind: Rule
services:
- name: auth-proxy
port: 80
tls:
secretName: auth-proxy-tls
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: auth-proxy-tls
spec:
secretName: auth-proxy-tls
issuerRef:
name: letsencrypt
kind: ClusterIssuer
dnsNames:
- oauth.hexor.cy
@@ -0,0 +1,366 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: keycloak-hacker-theme
namespace: keycloak
data:
theme.properties: |
parent=keycloak.v2
import=common/keycloak
styles=css/hacker.css
hacker.css: |
/* ============================================================
HEXOR — Hacker Terminal Theme for Keycloak
============================================================ */
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;600&display=swap');
:root {
--hk-bg: #0a0a0a;
--hk-panel: #0d0d0d;
--hk-border: #00ff4180;
--hk-green: #00ff41;
--hk-green-dim: #00cc33;
--hk-green-glow: rgba(0, 255, 65, 0.15);
--hk-green-glow-strong: rgba(0, 255, 65, 0.35);
--hk-cyan: #00e5ff;
--hk-red: #ff3333;
--hk-text: #b0ffb0;
--hk-text-dim: #5a8a5a;
--hk-input-bg: #050505;
--hk-font: 'Fira Code', 'Cascadia Code', 'JetBrains Mono', monospace;
}
/* -- Force dark always --------------------------------------------------- */
html.login-pf,
html.login-pf.pf-v5-theme-dark {
color-scheme: dark;
}
/* -- Background: scanlines + CRT ---------------------------------------- */
body#keycloak-bg {
font-family: var(--hk-font) !important;
background: var(--hk-bg) !important;
color: var(--hk-text) !important;
min-height: 100vh;
position: relative;
}
body#keycloak-bg::before {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 255, 65, 0.03) 2px,
rgba(0, 255, 65, 0.03) 4px
);
pointer-events: none;
z-index: 9999;
}
/* -- Login container ----------------------------------------------------- */
.pf-v5-c-login {
background: transparent !important;
}
.pf-v5-c-login__container {
max-width: 480px !important;
}
/* -- Header / Brand ------------------------------------------------------ */
#kc-header-wrapper {
font-family: var(--hk-font) !important;
color: var(--hk-green) !important;
text-shadow: 0 0 10px var(--hk-green), 0 0 30px var(--hk-green-glow);
font-size: 1.5rem !important;
letter-spacing: 0.15em;
text-transform: uppercase;
}
#kc-header-wrapper .kc-logo-text span::before {
content: '> ';
opacity: 0.6;
}
/* -- Main card ----------------------------------------------------------- */
.pf-v5-c-login__main {
background: var(--hk-panel) !important;
border: 1px solid var(--hk-border) !important;
border-radius: 0 !important;
box-shadow:
0 0 15px var(--hk-green-glow),
inset 0 0 30px rgba(0, 0, 0, 0.5) !important;
}
/* -- Title --------------------------------------------------------------- */
h1#kc-page-title,
.pf-v5-c-title {
font-family: var(--hk-font) !important;
color: var(--hk-green) !important;
text-shadow: 0 0 8px var(--hk-green-glow);
font-weight: 600 !important;
font-size: 1.25rem !important;
letter-spacing: 0.05em;
}
/* -- Labels -------------------------------------------------------------- */
.pf-v5-c-form__label-text {
font-family: var(--hk-font) !important;
color: var(--hk-green-dim) !important;
font-size: 0.85rem !important;
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* -- Input fields -------------------------------------------------------- */
.pf-v5-c-form-control {
background-color: var(--hk-input-bg) !important;
border: 1px solid var(--hk-border) !important;
border-radius: 0 !important;
color: var(--hk-green) !important;
font-family: var(--hk-font) !important;
font-size: 0.9rem !important;
caret-color: var(--hk-green);
transition: border-color 0.2s, box-shadow 0.2s;
}
.pf-v5-c-form-control:focus-within,
.pf-v5-c-form-control:focus {
border-color: var(--hk-green) !important;
box-shadow: 0 0 8px var(--hk-green-glow), inset 0 0 4px var(--hk-green-glow) !important;
outline: none !important;
}
.pf-v5-c-form-control input {
color: var(--hk-green) !important;
font-family: var(--hk-font) !important;
}
.pf-v5-c-form-control input::placeholder {
color: var(--hk-text-dim) !important;
}
/* -- Primary button (Sign In) -------------------------------------------- */
.pf-v5-c-button.pf-m-primary {
background: transparent !important;
border: 1px solid var(--hk-green) !important;
border-radius: 0 !important;
color: var(--hk-green) !important;
font-family: var(--hk-font) !important;
font-weight: 600 !important;
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.9rem !important;
transition: all 0.2s;
position: relative;
overflow: hidden;
}
.pf-v5-c-button.pf-m-primary:hover {
background: var(--hk-green-glow) !important;
box-shadow: 0 0 15px var(--hk-green-glow-strong), inset 0 0 15px var(--hk-green-glow) !important;
color: #fff !important;
text-shadow: 0 0 5px var(--hk-green);
}
/* -- Secondary button (Try Another Way, Passkey) ------------------------- */
.pf-v5-c-button.pf-m-secondary {
background: transparent !important;
border: 1px solid var(--hk-cyan) !important;
border-radius: 0 !important;
color: var(--hk-cyan) !important;
font-family: var(--hk-font) !important;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.85rem !important;
transition: all 0.2s;
}
.pf-v5-c-button.pf-m-secondary:hover {
background: rgba(0, 229, 255, 0.1) !important;
box-shadow: 0 0 12px rgba(0, 229, 255, 0.3) !important;
color: #fff !important;
}
/* -- Passkey authenticate button ----------------------------------------- */
#authenticateWebAuthnButton {
background: transparent !important;
border: 1px solid var(--hk-cyan) !important;
border-radius: 0 !important;
color: var(--hk-cyan) !important;
font-family: var(--hk-font) !important;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.85rem !important;
padding: 0.75rem 1rem;
display: block;
width: 100%;
text-align: center;
text-decoration: none;
transition: all 0.2s;
margin-top: 1rem;
}
#authenticateWebAuthnButton::before {
content: '\f084 ';
font-family: 'Font Awesome 6 Free', 'FontAwesome';
font-weight: 900;
}
#authenticateWebAuthnButton:hover {
background: rgba(0, 229, 255, 0.1) !important;
box-shadow: 0 0 15px rgba(0, 229, 255, 0.3), inset 0 0 10px rgba(0, 229, 255, 0.1) !important;
color: #fff !important;
text-shadow: 0 0 5px var(--hk-cyan);
}
/* -- Show password button ------------------------------------------------ */
.pf-v5-c-button.pf-m-control {
background: var(--hk-input-bg) !important;
border: 1px solid var(--hk-border) !important;
border-left: none !important;
border-radius: 0 !important;
color: var(--hk-text-dim) !important;
}
.pf-v5-c-button.pf-m-control:hover {
color: var(--hk-green) !important;
}
/* -- Social providers (Google, etc) -------------------------------------- */
.pf-v5-c-login__main-footer {
border-top: 1px solid var(--hk-border) !important;
}
.kc-social-section .pf-v5-c-button.pf-m-secondary {
border-color: var(--hk-text-dim) !important;
color: var(--hk-text) !important;
}
.kc-social-section .pf-v5-c-button.pf-m-secondary:hover {
border-color: var(--hk-green) !important;
color: var(--hk-green) !important;
box-shadow: 0 0 10px var(--hk-green-glow) !important;
}
/* -- Try another way link ------------------------------------------------ */
#try-another-way {
color: var(--hk-cyan) !important;
text-decoration: none !important;
font-family: var(--hk-font) !important;
}
#try-another-way:hover {
text-shadow: 0 0 8px rgba(0, 229, 255, 0.5);
}
/* -- Links --------------------------------------------------------------- */
a {
color: var(--hk-cyan) !important;
text-decoration: none;
transition: text-shadow 0.2s;
}
a:hover {
text-shadow: 0 0 6px rgba(0, 229, 255, 0.4);
}
/* -- Alerts / messages --------------------------------------------------- */
.pf-v5-c-alert {
background: rgba(0, 255, 65, 0.05) !important;
border: 1px solid var(--hk-border) !important;
border-radius: 0 !important;
font-family: var(--hk-font) !important;
}
.pf-v5-c-alert.pf-m-danger {
border-color: var(--hk-red) !important;
background: rgba(255, 51, 51, 0.05) !important;
}
.pf-v5-c-alert__title {
color: var(--hk-text) !important;
font-family: var(--hk-font) !important;
}
/* -- Checkbox (Remember me) ---------------------------------------------- */
.pf-v5-c-check__label {
color: var(--hk-text-dim) !important;
font-family: var(--hk-font) !important;
font-size: 0.8rem !important;
}
/* -- Helper text (Forgot password, etc.) --------------------------------- */
.pf-v5-c-helper-text .pf-v5-c-button.pf-m-link {
color: var(--hk-cyan) !important;
font-family: var(--hk-font) !important;
font-size: 0.8rem !important;
}
/* -- Select auth list (Try another way options) -------------------------- */
.select-auth-container {
background: var(--hk-panel) !important;
font-family: var(--hk-font) !important;
}
.pf-v5-c-data-list__item {
border-color: var(--hk-border) !important;
background: transparent !important;
}
.pf-v5-c-data-list__item:hover {
background: var(--hk-green-glow) !important;
}
.select-auth-box-headline {
color: var(--hk-green) !important;
}
.select-auth-box-desc {
color: var(--hk-text-dim) !important;
}
/* -- Footer -------------------------------------------------------------- */
.pf-v5-c-login__main-footer-band {
background: transparent !important;
border-top: 1px solid var(--hk-border) !important;
}
/* -- Language selector --------------------------------------------------- */
select#login-select-toggle {
background: var(--hk-input-bg) !important;
color: var(--hk-green) !important;
border-color: var(--hk-border) !important;
font-family: var(--hk-font) !important;
}
/* -- Blinking cursor animation for header -------------------------------- */
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
#kc-header-wrapper::after {
content: '_';
animation: blink 1s step-end infinite;
color: var(--hk-green);
}
/* -- Subtle CRT flicker ------------------------------------------------- */
@keyframes flicker {
0% { opacity: 0.97; }
5% { opacity: 0.95; }
10% { opacity: 0.98; }
15% { opacity: 0.96; }
20% { opacity: 0.99; }
100% { opacity: 0.98; }
}
.pf-v5-c-login__main {
animation: flicker 4s infinite;
}
+16
View File
@@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- secrets.yaml
- hacker-theme-configmap.yaml
- auth-proxy.yaml
# ConfigMap auth-proxy-routes is managed by Terraform (terraform/keycloak, kubernetes_config_map_v1).
helmCharts:
- name: keycloakx
repo: https://codecentric.github.io/helm-charts
version: 7.3.1
releaseName: keycloak
namespace: keycloak
valuesFile: values.yaml
+80
View File
@@ -0,0 +1,80 @@
# Read-only access to the keycloak-db Secret in the psql namespace
# (see postgresql/databases/keycloak.yaml for the other half).
apiVersion: v1
kind: ServiceAccount
metadata:
name: db-credentials-reader
---
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: psql-credentials
spec:
provider:
kubernetes:
remoteNamespace: psql
server:
caProvider:
type: ConfigMap
name: kube-root-ca.crt
key: ca.crt
auth:
serviceAccount:
name: db-credentials-reader
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: keycloak-creds
spec:
refreshInterval: 1h
secretStoreRef:
kind: SecretStore
name: psql-credentials
target:
name: keycloak-creds
template:
data:
KC_DB_USERNAME: "{{ .username }}"
KC_DB_PASSWORD: "{{ .password }}"
data:
- secretKey: username
remoteRef:
key: keycloak-db
property: username
- secretKey: password
remoteRef:
key: keycloak-db
property: password
---
# Bootstrap admin: only used when the database has no admin user yet (a fresh
# install). With the restored database the existing admin accounts are kept.
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
metadata:
name: keycloak-bootstrap
spec:
length: 40
digits: 10
symbols: 0
noUpper: false
allowRepeat: true
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: keycloak-bootstrap
spec:
refreshPolicy: CreatedOnce
target:
name: keycloak-bootstrap
template:
data:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: "{{ .password }}"
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
name: keycloak-bootstrap
+75
View File
@@ -0,0 +1,75 @@
replicas: 1
image:
repository: quay.io/keycloak/keycloak
tag: "26.5.6"
command:
- "/opt/keycloak/bin/kc.sh"
- "start"
- "--http-port=8080"
- "--hostname-strict=false"
- "--proxy-headers=xforwarded"
extraEnvFrom: |
- secretRef:
name: keycloak-creds
- secretRef:
name: keycloak-bootstrap
extraEnv: |
- name: KC_HOSTNAME
value: auth.hexor.cy
- name: JAVA_OPTS_APPEND
value: "-Djgroups.dns.query=keycloak-keycloakx-headless.keycloak.svc"
extraVolumes: |
- name: hacker-theme
configMap:
name: keycloak-hacker-theme
extraVolumeMounts: |
- name: hacker-theme
mountPath: /opt/keycloak/themes/hacker/login/theme.properties
subPath: theme.properties
- name: hacker-theme
mountPath: /opt/keycloak/themes/hacker/login/css/hacker.css
subPath: hacker.css
dbchecker:
enabled: true
database:
vendor: postgres
hostname: main-rw.psql.svc
port: 5432
database: keycloak
existingSecret: keycloak-creds
existingSecretKey: KC_DB_PASSWORD
service:
type: ClusterIP
# HTTP is redirected to HTTPS by the cluster-wide Traefik default (core/traefik-config).
ingress:
enabled: true
ingressClassName: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt
rules:
- host: auth.hexor.cy
paths:
- path: /
pathType: Prefix
tls:
- secretName: keycloak-tls
hosts:
- auth.hexor.cy
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "3"
memory: 2Gi
+48
View File
@@ -0,0 +1,48 @@
# Main HA PostgreSQL cluster: one primary and one streaming replica, each on its
# own node with a node-local volume. Replication is asynchronous (nodes sit in
# different regions), so a failover can lose the last few transactions.
#
# Nodes eligible for instances carry the label workload/postgres=true. To move a
# replica: label the new node, unlabel the old one, then recreate the instance
# (kubectl cnpg destroy main <N>); the operator re-clones it from the primary.
#
# Endpoints inside the cluster: main-rw (primary), main-ro (replicas),
# main-r (any instance) in the psql namespace.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: main
namespace: psql
spec:
description: Main HA PostgreSQL cluster
imageName: ghcr.io/cloudnative-pg/postgresql:17
instances: 2
enableSuperuserAccess: false
# Roll updates replica-first and hand over by switchover instead of restarting the primary.
primaryUpdateStrategy: unsupervised
primaryUpdateMethod: switchover
storage:
storageClass: local-path
size: 20Gi # local-path volumes cannot be expanded later
affinity:
enablePodAntiAffinity: true
podAntiAffinityType: required
topologyKey: kubernetes.io/hostname
nodeSelector:
workload/postgres: "true"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
memory: 2Gi
postgresql:
parameters:
max_connections: "200"
shared_buffers: 512MB
effective_cache_size: 1536MB
@@ -0,0 +1,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,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- cluster.yaml
- password-generator.yaml
- databases/vaultwarden.yaml
- databases/keycloak.yaml
@@ -0,0 +1,12 @@
# Random passwords for database roles. Referenced by the ExternalSecrets in databases/.
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
metadata:
name: db-password
namespace: psql
spec:
length: 40
digits: 10
symbols: 0 # the password ends up inside a connection URL
noUpper: false
allowRepeat: true
+10
View File
@@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
helmCharts:
- name: reloader
repo: https://stakater.github.io/stakater-charts
version: 2.2.18
releaseName: reloader
namespace: reloader
valuesFile: values.yaml
+24
View File
@@ -0,0 +1,24 @@
reloader:
watchGlobally: true
autoReloadAll: true
reloadOnCreate: true
reloadOnDelete: false
reloadStrategy: annotations
ignoreConfigMaps: false
ignoreSecrets: false
ignoreJobs: false
ignoreCronJobs: false
enableHA: true
syncAfterRestart: true
logLevel: info
rbac:
enabled: true
deployment:
replicas: 2
resources:
requests:
cpu: 10m
memory: 128Mi
limits:
cpu: 150m
memory: 512Mi
@@ -0,0 +1,26 @@
FROM debian:sid
ENV BW_CLI_VERSION=2025.12.1
RUN apt update && \
apt install -y wget unzip && \
wget https://github.com/bitwarden/clients/releases/download/cli-v${BW_CLI_VERSION}/bw-linux-${BW_CLI_VERSION}.zip && \
unzip bw-linux-${BW_CLI_VERSION}.zip && \
chmod +x bw && \
mv bw /usr/local/bin/bw && \
rm -rfv *.zip && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Build entrypoint script
RUN echo '#!/bin/sh\n\
set -e\n\
bw config server ${BW_HOST}\n\
export BW_SESSION=$(bw login ${BW_USER} --passwordenv BW_PASSWORD --raw)\n\
bw unlock --check\n\
echo "Running \`bw server\` on port 8087"\n\
bw serve --hostname 0.0.0.0\n' > /entrypoint.sh && \
chmod +x /entrypoint.sh
CMD ["/entrypoint.sh"]
@@ -0,0 +1,123 @@
# Bridge between External Secrets and Vaultwarden: `bw serve` logged in as the
# bot account. Needs the Secret bitwarden-cli (created by hand; the bot logs in
# with email + password, so keep password login enabled for it, i.e. no
# SSO_ONLY, or switch the image to API-key login):
# kubectl -n external-secrets create secret generic bitwarden-cli \
# --from-literal=BW_HOST=https://vw.btwiusearch.net \
# --from-literal=BW_USERNAME=<bot email> --from-literal=BW_PASSWORD=<password>
# Image source: Dockerfile-bitwarden-cli in this directory.
apiVersion: apps/v1
kind: Deployment
metadata:
name: bitwarden-cli
labels:
reloader.stakater.com/auto: "true"
app.kubernetes.io/instance: bitwarden-cli
app.kubernetes.io/name: bitwarden-cli
spec:
replicas: 1
strategy:
type: RollingUpdate
selector:
matchLabels:
app.kubernetes.io/name: bitwarden-cli
app.kubernetes.io/instance: bitwarden-cli
template:
metadata:
labels:
app.kubernetes.io/name: bitwarden-cli
app.kubernetes.io/instance: bitwarden-cli
spec:
nodeSelector:
kubernetes.io/arch: amd64
containers:
- name: bitwarden-cli
image: ultradesu/bitwarden-client:2025.12.1
imagePullPolicy: IfNotPresent
resources:
requests:
memory: "128Mi"
cpu: "300m"
limits:
memory: "512Mi"
cpu: "1000m"
env:
- name: BW_HOST
valueFrom:
secretKeyRef:
name: bitwarden-cli
key: BW_HOST
- name: BW_USER
valueFrom:
secretKeyRef:
name: bitwarden-cli
key: BW_USERNAME
- name: BW_PASSWORD
valueFrom:
secretKeyRef:
name: bitwarden-cli
key: BW_PASSWORD
ports:
- name: http
containerPort: 8087
protocol: TCP
livenessProbe:
exec:
command:
- wget
- -q
- http://127.0.0.1:8087/sync
- --post-data=''
initialDelaySeconds: 20
failureThreshold: 3
timeoutSeconds: 1
periodSeconds: 120
readinessProbe:
tcpSocket:
port: 8087
initialDelaySeconds: 20
failureThreshold: 3
timeoutSeconds: 1
periodSeconds: 10
startupProbe:
tcpSocket:
port: 8087
initialDelaySeconds: 10
failureThreshold: 30
timeoutSeconds: 1
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: bitwarden-cli
labels:
app.kubernetes.io/instance: bitwarden-cli
app.kubernetes.io/name: bitwarden-cli
spec:
type: ClusterIP
ports:
- port: 8087
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: bitwarden-cli
app.kubernetes.io/instance: bitwarden-cli
---
# Only the External Secrets controller may talk to the bridge: it serves the whole vault.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: external-secret-2-bw-cli
spec:
podSelector:
matchLabels:
app.kubernetes.io/instance: bitwarden-cli
app.kubernetes.io/name: bitwarden-cli
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/instance: external-secrets
app.kubernetes.io/name: external-secrets
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- bitwarden-cli.yaml
- stores.yaml
+47
View File
@@ -0,0 +1,47 @@
# ClusterSecretStores in front of the Vaultwarden vault that belongs to the bot
# account (the vault holds server secrets only, nothing personal).
#
# `key` may be either
# - the item UUID (old style, changes whenever items are imported again), or
# - the item path: the item NAME, which by convention is the full path
# k8s/<namespace>/<what> e.g. k8s/cert-manager/cloudflare-api-token
# Names are looked up by exact match (a name that is a substring of another
# item name, login or URL is fine). Do not use double quotes in names.
#
# vaultwarden-login: `property` is a path inside the item: login.username,
# login.password, notes, fields[0].value, ...
# vaultwarden-fields: `property` is the NAME of a custom field of the item.
#
# data:
# - secretKey: password
# sourceRef:
# storeRef: {name: vaultwarden-login, kind: ClusterSecretStore}
# remoteRef:
# key: k8s/cert-manager/cloudflare-api-token
# property: login.password
#
# The bridge syncs the vault every couple of minutes, so a new item becomes
# visible to ESO after that.
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: vaultwarden-login
spec:
provider:
webhook:
url: 'http://bitwarden-cli.external-secrets.svc:8087/{{ if regexMatch "^[0-9a-fA-F-]{36}$" .remoteRef.key }}object/item/{{ .remoteRef.key }}{{ else }}list/object/items?search={{ .remoteRef.key }}{{ end }}'
headers:
Content-Type: application/json
result:
jsonPath: '{{ if regexMatch "^[0-9a-fA-F-]{36}$" .remoteRef.key }}$.data.{{ .remoteRef.property }}{{ else }}$.data.data[?@.name=="{{ .remoteRef.key }}"].{{ .remoteRef.property }}{{ end }}'
---
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: vaultwarden-fields
spec:
provider:
webhook:
url: 'http://bitwarden-cli.external-secrets.svc:8087/{{ if regexMatch "^[0-9a-fA-F-]{36}$" .remoteRef.key }}object/item/{{ .remoteRef.key }}{{ else }}list/object/items?search={{ .remoteRef.key }}{{ end }}'
result:
jsonPath: '{{ if regexMatch "^[0-9a-fA-F-]{36}$" .remoteRef.key }}$.data.fields[?@.name=="{{ .remoteRef.property }}"].value{{ else }}$.data.data[?@.name=="{{ .remoteRef.key }}"].fields[?@.name=="{{ .remoteRef.property }}"].value{{ end }}'
@@ -0,0 +1,16 @@
# Configuration of the Traefik that ships with k3s.
# Ingresses without a router.entrypoints annotation now use only "websecure", so
# plain HTTP is redirected to HTTPS by default (see http-redirect.yaml).
# To publish an Ingress over plain HTTP add the annotation
# traefik.ingress.kubernetes.io/router.entrypoints: web
# (or "web,websecure" to serve both without a redirect).
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
ports:
websecure:
asDefault: true
@@ -0,0 +1,29 @@
# Catch-all on the "web" entrypoint with the lowest priority: every host that has
# no explicit HTTP router of its own is redirected to HTTPS.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
namespace: kube-system
spec:
redirectScheme:
scheme: https
permanent: true
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: http-redirect
namespace: kube-system
spec:
entryPoints:
- web
routes:
- match: HostRegexp(`.+`)
kind: Rule
priority: 1
middlewares:
- name: redirect-https
services:
- name: noop@internal
kind: TraefikService
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmchartconfig.yaml
- http-redirect.yaml
+77
View File
@@ -0,0 +1,77 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: vaultwarden
labels:
app: vaultwarden
spec:
replicas: 1
strategy:
type: Recreate # the RWO local volume cannot be mounted by two pods
selector:
matchLabels:
app: vaultwarden
template:
metadata:
labels:
app: vaultwarden
spec:
containers:
- name: vaultwarden
image: vaultwarden/server:1.37.3
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 512Mi
# SSO is switched on by the optional Secret vaultwarden-sso, which holds
# SSO_ENABLED=true and SSO_CLIENT_SECRET (the secret of the Keycloak client
# "Vaultwarden"). Without it Vaultwarden starts with SSO off.
envFrom:
- secretRef:
name: vaultwarden-sso
optional: true
env:
- name: DOMAIN
value: https://vw.btwiusearch.net
- name: ORG_GROUPS_ENABLED
value: "true"
- name: SSO_AUTHORITY
value: https://auth.hexor.cy/auth/realms/hexor
- name: SSO_CLIENT_ID
value: Vaultwarden
- name: SSO_SCOPES
value: openid profile email offline_access
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: vaultwarden-db-url
key: DATABASE_URL
- name: ADMIN_TOKEN
valueFrom:
secretKeyRef:
name: admin-token
key: ADMIN_TOKEN
ports:
- name: http
containerPort: 80
protocol: TCP
readinessProbe:
httpGet:
path: /alive
port: http
periodSeconds: 10
livenessProbe:
httpGet:
path: /alive
port: http
initialDelaySeconds: 30
periodSeconds: 30
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: vaultwarden-data
+23
View File
@@ -0,0 +1,23 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vaultwarden
annotations:
cert-manager.io/cluster-issuer: letsencrypt
traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec:
rules:
- host: vw.btwiusearch.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vaultwarden
port:
number: 80
tls:
- secretName: vaultwarden-tls
hosts:
- vw.btwiusearch.net
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- secrets.yaml
- pvc.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
+12
View File
@@ -0,0 +1,12 @@
# Attachments, sends, icon cache and RSA keys live here; the database is in PostgreSQL.
# A local-path volume is bound to one node: the pod stays on the node it first ran on.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vaultwarden-data
spec:
accessModes: [ReadWriteOnce]
storageClassName: local-path
resources:
requests:
storage: 2Gi
+78
View File
@@ -0,0 +1,78 @@
# Read-only access to the vaultwarden-db Secret in the psql namespace
# (see postgresql/databases/vaultwarden.yaml for the other half).
apiVersion: v1
kind: ServiceAccount
metadata:
name: db-credentials-reader
---
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: psql-credentials
spec:
provider:
kubernetes:
remoteNamespace: psql
server:
caProvider:
type: ConfigMap
name: kube-root-ca.crt
key: ca.crt
auth:
serviceAccount:
name: db-credentials-reader
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: vaultwarden-db-url
spec:
refreshInterval: 1h
secretStoreRef:
kind: SecretStore
name: psql-credentials
target:
name: vaultwarden-db-url
template:
data:
DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@main-rw.psql.svc:5432/vaultwarden"
data:
- secretKey: username
remoteRef:
key: vaultwarden-db
property: username
- secretKey: password
remoteRef:
key: vaultwarden-db
property: password
---
# Admin panel token (/admin). Generated once; read it with:
# kubectl -n vaultwarden get secret admin-token -o jsonpath='{.data.ADMIN_TOKEN}' | base64 -d
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
metadata:
name: admin-token
spec:
length: 48
digits: 10
symbols: 0
noUpper: false
allowRepeat: true
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: admin-token
spec:
refreshPolicy: CreatedOnce
target:
name: admin-token
template:
data:
ADMIN_TOKEN: "{{ .password }}"
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
name: admin-token
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: vaultwarden
spec:
selector:
app: vaultwarden
ports:
- protocol: TCP
port: 80
targetPort: http
+20
View File
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: airtrail
namespace: argocd
spec:
project: apps
destination:
namespace: airtrail
server: https://kubernetes.default.svc
source:
repoURL: ssh://git@gt.hexor.cy:30022/ab/homelab.git
targetRevision: HEAD
path: k8s/apps/airtrail
syncPolicy:
automated:
selfHeal: true
prune: true
syncOptions:
- CreateNamespace=true
+66
View File
@@ -0,0 +1,66 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: airtrail
labels:
app: airtrail
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: airtrail
template:
metadata:
labels:
app: airtrail
spec:
securityContext:
fsGroup: 1000
containers:
- name: airtrail
image: johly/airtrail:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 3000
env:
- name: ORIGIN
value: https://at.hexor.cy
- name: DB_URL
valueFrom:
secretKeyRef:
name: airtrail-creds
key: DB_URL
- name: UPLOAD_LOCATION
value: /app/uploads
- name: BODY_SIZE_LIMIT
value: 20M
volumeMounts:
- name: uploads
mountPath: /app/uploads
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
startupProbe:
tcpSocket:
port: http
periodSeconds: 10
failureThreshold: 30
readinessProbe:
tcpSocket:
port: http
periodSeconds: 10
livenessProbe:
tcpSocket:
port: http
periodSeconds: 30
volumes:
- name: uploads
persistentVolumeClaim:
claimName: airtrail-uploads
+25
View File
@@ -0,0 +1,25 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: airtrail-creds
spec:
target:
name: airtrail-creds
deletionPolicy: Delete
template:
type: Opaque
data:
DB_URL: |-
postgresql://airtrail:{{ .airtrail | trim | urlquery | replace "+" "%20" }}@psql.psql.svc:5432/airtrail
data:
- secretKey: airtrail
sourceRef:
storeRef:
name: vaultwarden-login
kind: ClusterSecretStore
remoteRef:
conversionStrategy: Default
decodingStrategy: None
metadataPolicy: None
key: 2a9deb39-ef22-433e-a1be-df1555625e22
property: fields[21].value
+25
View File
@@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: airtrail-tls-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt
traefik.ingress.kubernetes.io/router.middlewares: kube-system-https-redirect@kubernetescrd
acme.cert-manager.io/http01-edit-in-place: "true"
spec:
ingressClassName: traefik
rules:
- host: at.hexor.cy
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: airtrail
port:
number: 3000
tls:
- secretName: airtrail-tls
hosts:
- at.hexor.cy
+10
View File
@@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- app.yaml
- external-secrets.yaml
- storage.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: airtrail
labels:
app: airtrail
spec:
type: ClusterIP
selector:
app: airtrail
ports:
- name: http
protocol: TCP
port: 3000
targetPort: http
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: airtrail-uploads
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 1Gi
+24 -14
View File
@@ -8,7 +8,7 @@ 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"
@@ -96,12 +96,18 @@ data:
[[ "${source}" == \#* ]] && continue
if [ -n "${extra:-}" ] \
|| ! valid_ipv4_32 "${source}" \
|| ! valid_ipv4_32 "${destination:-}" \
|| ! routed_through_awg0 "${source}" \
|| ! routed_through_awg0 "${destination}"; then
echo "Invalid or out-of-tunnel policy line: ${source:-} ${destination:-} ${extra:-}" >&2
"${IPTABLES[@]}" -F "${inactive}"
return 1
|| ! 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}" \
@@ -181,7 +187,7 @@ data:
#!/usr/bin/env bash
set -euo pipefail
PORT="${1:-5847}"
PORT="${1:-5848}"
VPN_CIDR="${2:-10.8.0.0/16}"
FORWARD_CHAIN="AMNEZIAWG-FORWARD"
POLICY_CHAIN_A="AMNEZIAWG-POLICY-A"
@@ -239,7 +245,7 @@ data:
#!/usr/bin/env bash
set -euo pipefail
PORT="${1:-5847}"
PORT="${1:-5848}"
VPN_CIDR="${2:-10.8.0.0/16}"
FORWARD_CHAIN="AMNEZIAWG-FORWARD"
IPTABLES=(iptables -w 5)
@@ -270,9 +276,13 @@ data:
exit 1
fi
FIRST_FORWARD_RULE="$("${IPTABLES[@]}" -S FORWARD | grep '^-A FORWARD ' | sed -n '1p')"
[[ "${FIRST_FORWARD_RULE}" == *"--comment amneziawg-forward-jump"* ]]
[[ "${FIRST_FORWARD_RULE}" == *"-j ${FORWARD_CHAIN}"* ]]
# 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}" ]
@@ -362,12 +372,12 @@ 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 5847 10.8.0.0/16 /dev/null
/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 5847 10.8.0.0/16 "${POLICY_FILE}"
/scripts/firewall-up.sh 5848 10.8.0.0/16 "${POLICY_FILE}"
}
watch_client_config() {
+5 -5
View File
@@ -52,7 +52,7 @@ spec:
fieldRef:
fieldPath: spec.nodeName
- name: PORT
value: "5847"
value: "5848"
command:
- /bin/bash
- -lc
@@ -116,14 +116,14 @@ spec:
- /scripts/run.sh
ports:
- name: awg
containerPort: 5847
containerPort: 5848
protocol: UDP
readinessProbe:
exec:
command:
- /bin/bash
- -lc
- /scripts/firewall-check.sh 5847 10.8.0.0/16
- /scripts/firewall-check.sh 5848 10.8.0.0/16
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 8
@@ -133,7 +133,7 @@ spec:
command:
- /bin/bash
- -lc
- /scripts/firewall-check.sh 5847 10.8.0.0/16 || /scripts/firewall-up.sh 5847 10.8.0.0/16
- /scripts/firewall-check.sh 5848 10.8.0.0/16 || /scripts/firewall-up.sh 5848 10.8.0.0/16
periodSeconds: 5
timeoutSeconds: 15
failureThreshold: 12
@@ -142,7 +142,7 @@ spec:
command:
- /bin/bash
- -lc
- /scripts/firewall-check.sh 5847 10.8.0.0/16 || /scripts/firewall-up.sh 5847 10.8.0.0/16
- /scripts/firewall-check.sh 5848 10.8.0.0/16 || /scripts/firewall-up.sh 5848 10.8.0.0/16
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 15
+14 -14
View File
@@ -16,21 +16,21 @@ spec:
[Interface]
PrivateKey = {{ .server_private_key }}
Address = 10.8.0.1/16
ListenPort = 5847
ListenPort = 5848
MTU = 1376
Jc = 4
Jmin = 64
Jmax = 128
S1 = 15
S2 = 18
S3 = 20
S4 = 23
H1 = 1020325451
H2 = 3288052141
H3 = 1766607858
H4 = 2528465083
PostUp = /scripts/firewall-up.sh 5847 10.8.0.0/16
PostDown = /scripts/firewall-down.sh 5847 10.8.0.0/16
Jc = 5
Jmin = 96
Jmax = 320
S1 = 37
S2 = 47
S3 = 29
S4 = 43
H1 = 1247270921
H2 = 1956534873
H3 = 886504771
H4 = 1623791047
PostUp = /scripts/firewall-up.sh 5848 10.8.0.0/16
PostDown = /scripts/firewall-down.sh 5848 10.8.0.0/16
data:
- secretKey: server_private_key
sourceRef:
+1 -1
View File
@@ -25,7 +25,7 @@ spec:
kubernetes.io/hostname: cy.tail2fe2d.ts.net
containers:
- name: amnezia-fellow
image: ultradesu/amnezia-fellow:latest
image: ultradesu/amnezia-fellow:1.0.2
imagePullPolicy: Always
ports:
- name: http
+1 -1
View File
@@ -34,7 +34,7 @@ spec:
mountPath: /templates/subscription
containers:
- name: pasarguard-web
image: pasarguard/panel:v5.0.3
image: pasarguard/panel:v5.3.0
imagePullPolicy: Always
envFrom:
- secretRef:
+13
View File
@@ -144,6 +144,8 @@ spec:
{{ .amnezia_fellow }}
USER_web_petting: |-
{{ .web_petting }}
USER_airtrail: |-
{{ .airtrail }}
data:
- secretKey: authentik
sourceRef:
@@ -365,3 +367,14 @@ spec:
metadataPolicy: None
key: 2a9deb39-ef22-433e-a1be-df1555625e22
property: fields[20].value
- secretKey: airtrail
sourceRef:
storeRef:
name: vaultwarden-login
kind: ClusterSecretStore
remoteRef:
conversionStrategy: Default
decodingStrategy: None
metadataPolicy: None
key: 2a9deb39-ef22-433e-a1be-df1555625e22
property: fields[21].value
+5
View File
@@ -95,6 +95,11 @@ prometheus:
grafana:
enabled: true
sidecar:
datasources:
alertmanager:
uid: ff9iga3xqregwc
serviceAccount:
create: true
+2
View File
@@ -7,6 +7,7 @@ provider "registry.terraform.io/hashicorp/kubernetes" {
hashes = [
"h1:G9QqKNpcztBRqrywtlNylFJSpGzDfRFtO8hcWLdkvRY=",
"h1:iDIPyL9K9FXc8ievR5vRmXsgT1/I9Vq9CFDKm1Zo8bQ=",
"h1:oodIAuFMikXNmEtil5MQgP4dfSctUBYQiGJfjbsF3NY=",
"zh:0215c5c60be62028c09a2f22458e89cda3ef5830a632299f1d401eb3538874b0",
"zh:09ebb9f442431e278a310a9423f32caf467cb4b3cad3fe59573ca71fa7b14e20",
"zh:0c4e5912f83bb35846ae0a9ae54fc320706ee61894cd21cc6b4181b1c5a2fa5c",
@@ -27,6 +28,7 @@ provider "registry.terraform.io/keycloak/keycloak" {
constraints = ">= 5.0.0"
hashes = [
"h1:3DuKdVeOxwULh7l6bvJKWZvsgSZo92rtnrdvyp+X2Lc=",
"h1:VSlrHkPQm4mczl+5ttuj6jAh2eVvupaoeymEn9Qi5vo=",
"h1:reFEBcuBZf2SVqDa1YIaNLuIW72uEVsOu5hHhQgfHi0=",
"zh:19be4505b17e4818db121a82917cb6723019cf379cfb82b720eaa2886f15bede",
"zh:2bd1565ed22db6a9fb50d60626e22c277f3b034a71f65e6c0011e42f56cad2bb",
+2 -1
View File
@@ -243,6 +243,7 @@ resource "keycloak_openid_client" "oauth2_app" {
valid_redirect_uris = each.value.redirect_uris
valid_post_logout_redirect_uris = each.value.post_logout_redirect_uris
web_origins = each.value.web_origins
access_token_lifespan = each.value.access_token_lifespan
}
resource "keycloak_openid_group_membership_protocol_mapper" "oauth2_app_groups" {
@@ -292,7 +293,7 @@ resource "keycloak_group" "oauth2_app" {
resource "kubernetes_config_map_v1" "auth_proxy_routes" {
metadata {
name = "auth-proxy-routes"
namespace = "auth-proxy"
namespace = "keycloak"
}
data = {
+2 -1
View File
@@ -19,5 +19,6 @@ provider "keycloak" {
}
provider "kubernetes" {
config_path = "~/.kube/config"
config_path = "~/.kube/config"
config_context = "hexor-lab"
}
+11 -3
View File
@@ -37,11 +37,19 @@ oauth2_applications = {
post_logout_redirect_uris = ["https://gt.hexor.cy/*"]
}
ArgoCD = {
redirect_uris = ["https://ag.hexor.cy/api/dex/callback"]
web_origins = ["https://ag.hexor.cy"]
post_logout_redirect_uris = ["https://ag.hexor.cy/*"]
redirect_uris = ["https://ag.hexor.cy/api/dex/callback", "https://ag.btwiusearch.net/api/dex/callback"]
web_origins = ["https://ag.hexor.cy", "https://ag.btwiusearch.net"]
post_logout_redirect_uris = ["https://ag.hexor.cy/*", "https://ag.btwiusearch.net/*"]
extra_optional_scopes = ["offline_access"]
}
Vaultwarden = {
redirect_uris = ["https://vw.btwiusearch.net/identity/connect/oidc-signin"]
web_origins = ["https://vw.btwiusearch.net"]
post_logout_redirect_uris = ["https://vw.btwiusearch.net/*"]
extra_optional_scopes = ["offline_access"]
# Bitwarden clients treat tokens shorter than ~10 minutes as expired.
access_token_lifespan = "900"
}
PGAdmin = {
redirect_uris = ["https://pg.hexor.cy/oauth2/authorize"]
web_origins = ["https://pg.hexor.cy"]
+1
View File
@@ -51,6 +51,7 @@ variable "oauth2_applications" {
extra_default_scopes = optional(list(string), [])
extra_optional_scopes = optional(list(string), [])
allowed_groups = optional(list(string), [])
access_token_lifespan = optional(string) # seconds; null keeps the realm default
}))
default = {}
}