diff --git a/apps/infra/beszel-agent.yaml b/apps/infra/beszel-agent.yaml new file mode 100644 index 0000000..49043c7 --- /dev/null +++ b/apps/infra/beszel-agent.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: beszel-agent + namespace: infra +spec: + selector: + matchLabels: + app: beszel-agent + template: + metadata: + labels: + app: beszel-agent + spec: + hostNetwork: true + containers: + - env: + - name: PORT + value: "45876" + - name: KEY + valueFrom: + secretKeyRef: + name: beszel-key + key: SECRET-KEY + image: henrygd/beszel-agent:0.18.8 + imagePullPolicy: Always + name: beszel-agent + ports: + - containerPort: 45876 + hostPort: 45876 + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "200m" + restartPolicy: Always diff --git a/apps/infra/beszel.yaml b/apps/infra/beszel.yaml new file mode 100644 index 0000000..d14e483 --- /dev/null +++ b/apps/infra/beszel.yaml @@ -0,0 +1,97 @@ +--- +# 1) Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: beszel + namespace: infra +spec: + replicas: 1 + selector: + matchLabels: + app: beszel + template: + metadata: + labels: + app: beszel + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + containers: + - name: beszel + image: ghcr.io/henrygd/beszel/beszel:0.18.7 + imagePullPolicy: Always + ports: + - containerPort: 8090 + name: beszel-port + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + volumeMounts: + - name: beszel-config + mountPath: /beszel_data + volumes: + - name: beszel-config + persistentVolumeClaim: + claimName: beszel-config +--- +# 2) Service +apiVersion: v1 +kind: Service +metadata: + name: beszel + namespace: infra +spec: + type: ClusterIP + selector: + app: beszel + ports: + - port: 80 + targetPort: beszel-port +--- +# 3) PersistentVolumeClaim +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: beszel-config + namespace: infra + annotations: + nfs.io/storage-path: "beszel-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi +--- +# 4) Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: beszel + namespace: infra +spec: + ingressClassName: nginx + rules: + - host: beszel.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: beszel + port: + number: 80 diff --git a/apps/infra/bitwarden-cli.yaml b/apps/infra/bitwarden-cli.yaml new file mode 100644 index 0000000..7bf0332 --- /dev/null +++ b/apps/infra/bitwarden-cli.yaml @@ -0,0 +1,144 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bitwarden-cli + namespace: infra + labels: + app.kubernetes.io/name: bitwarden-cli +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: bitwarden-cli + template: + metadata: + labels: + app.kubernetes.io/name: bitwarden-cli + spec: + nodeSelector: + kubernetes.io/arch: amd64 + containers: + - name: bitwarden-cli + image: ghcr.io/charlesthomas/bitwarden-cli:2026.3.0 + imagePullPolicy: IfNotPresent + # Override the baked entrypoint so --disable-origin-protection is + # actually passed (it was commented out in the image's entrypoint.sh, + # which made bw serve reject all cross-pod requests -> connection refused). + command: ["/bin/bash", "-lc"] + args: + - | + set -e + bw config server "${BW_HOST}" + + # Authenticate. Prefer the API key if client creds are present. + login() { + if [ -n "$BW_CLIENTID" ] && [ -n "$BW_CLIENTSECRET" ]; then + echo "Using apikey to log in" + BW_SESSION=$(bw login --apikey --raw) || return 1 + else + echo "Using password to log in" + BW_SESSION=$(bw login "${BW_USER}" --passwordenv BW_PASSWORD --raw) || return 1 + fi + export BW_SESSION + } + + login + + # Warm the vault cache once at startup so bw serve has data immediately. + bw sync + bw status + + # Keep the session alive + + echo "Starting periodic bw login+sync loop (every 5m)" + ( + while true; do + sleep 300 + login || true + echo "[$(date -u +%FT%TZ)] bw sync" + bw sync >/dev/null 2>&1 || echo "[$(date -u +%FT%TZ)] bw sync failed" + done + ) & + + echo 'Running `bw serve` on port 8087' + bw serve --hostname 0.0.0.0 --disable-origin-protection + 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 + startupProbe: + tcpSocket: { port: 8087 } + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 30 + readinessProbe: + tcpSocket: { port: 8087 } + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + tcpSocket: { port: 8087 } + periodSeconds: 15 + timeoutSeconds: 3 + failureThreshold: 6 + resources: + limits: + cpu: 400m + memory: 512Mi + requests: + cpu: 50m + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: bitwarden-cli + namespace: infra + labels: + app.kubernetes.io/name: bitwarden-cli +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: bitwarden-cli + ports: + - name: http + port: 8087 + targetPort: http + protocol: TCP +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: bw-cli + namespace: infra +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: bitwarden-cli + policyTypes: + - Ingress + ingress: + - from: + # ESO pods in the external-secrets namespace. + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: external-secrets + podSelector: + matchLabels: + app.kubernetes.io/name: external-secrets diff --git a/apps/infra/code-config.yaml b/apps/infra/code-config.yaml new file mode 100644 index 0000000..9627d92 --- /dev/null +++ b/apps/infra/code-config.yaml @@ -0,0 +1,113 @@ +--- +# 1) Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: code-config + namespace: infra +spec: + replicas: 1 + selector: + matchLabels: + app: code-config + template: + metadata: + labels: + app: code-config + spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + preference: + matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - iris + containers: + - name: code-config + image: lscr.io/linuxserver/code-server:latest + imagePullPolicy: Always + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: PROXY_DOMAIN + value: "code-config.haven" + - name: DEFAULT_WORKSPACE + value: "/k8s-config" + resources: + requests: + memory: 512Mi + cpu: 200m + limits: + memory: 1Gi + cpu: 2000m + ports: + - containerPort: 8443 + name: code-port + volumeMounts: + - name: code-config + mountPath: /config + - name: k8s-config + mountPath: /k8s-config + volumes: + - name: code-config + persistentVolumeClaim: + claimName: code-config + - name: k8s-config + nfs: + server: nfs-config.haven + path: /export/config +--- +# 2) Service +apiVersion: v1 +kind: Service +metadata: + name: code-config + namespace: infra +spec: + type: ClusterIP + selector: + app: code-config + ports: + - port: 8443 + targetPort: code-port +--- +# 3) PersistentVolumeClaim +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: code-config + namespace: infra + annotations: + nfs.io/storage-path: "code-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi +--- +# 4) Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: code-config + namespace: infra +spec: + ingressClassName: nginx + rules: + - host: code-config.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: code-config + port: + number: 8443 diff --git a/apps/infra/file-nginx.yaml b/apps/infra/file-nginx.yaml new file mode 100644 index 0000000..bb9cd10 --- /dev/null +++ b/apps/infra/file-nginx.yaml @@ -0,0 +1,129 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: file-nginx + namespace: infra + labels: + app: file-nginx +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + selector: + matchLabels: + app: file-nginx + template: + metadata: + labels: + app: file-nginx + spec: + containers: + - name: nginx + image: nginx:alpine + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + securityContext: + allowPrivilegeEscalation: false + runAsUser: 0 + volumeMounts: + - name: html + mountPath: /usr/share/nginx/data + - name: nginx-conf + mountPath: /etc/nginx/conf.d/default.conf + subPath: default.conf + startupProbe: + httpGet: + path: / + port: 80 + failureThreshold: 30 + periodSeconds: 5 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + volumes: + - name: html + nfs: + server: vega.haven + path: /export/Fast + - name: nginx-conf + configMap: + name: file-nginx-conf + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: file-nginx-conf + namespace: infra +data: + default.conf: | + server { + listen 80; + listen [::]:80; + server_name _; + + root /usr/share/nginx/data/file-nginx; + index index.html; + + autoindex on; + + location / { + try_files $uri $uri/ =404; + } + } +--- +apiVersion: v1 +kind: Service +metadata: + name: file-nginx + namespace: infra +spec: + selector: + app: file-nginx + ports: + - protocol: TCP + port: 80 + targetPort: 80 + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: file-nginx + namespace: infra + annotations: + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/cors-allow-origin: "*" +spec: + ingressClassName: nginx + rules: + - host: file-nginx.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: file-nginx + port: + number: 80 diff --git a/apps/infra/haven-notify.yaml b/apps/infra/haven-notify.yaml new file mode 100644 index 0000000..ab4a11e --- /dev/null +++ b/apps/infra/haven-notify.yaml @@ -0,0 +1,86 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: haven-notify + namespace: infra + labels: + app: haven-notify +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + selector: + matchLabels: + app: haven-notify + template: + metadata: + labels: + app: haven-notify + spec: + containers: + - name: haven-notify + image: git.ivanch.me/ivanch/haven-notify:latest + imagePullPolicy: Always + ports: + - containerPort: 8080 + env: + - name: WEBHOOK_URL + valueFrom: + secretKeyRef: + name: discord-webhook + key: HAVEN_WEBHOOK_URL + readinessProbe: + httpGet: + path: /ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /live + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + +--- +apiVersion: v1 +kind: Service +metadata: + name: haven-notify + namespace: infra +spec: + selector: + app: haven-notify + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: haven-notify + namespace: infra +spec: + ingressClassName: nginx + rules: + - host: notify.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: haven-notify + port: + number: 8080 diff --git a/apps/infra/ingress-nginx-helm.yaml b/apps/infra/ingress-nginx-helm.yaml new file mode 100644 index 0000000..ae5625f --- /dev/null +++ b/apps/infra/ingress-nginx-helm.yaml @@ -0,0 +1,33 @@ +apiVersion: helm.cattle.io/v1 +kind: HelmChart +metadata: + name: ingress-nginx + namespace: kube-system +spec: + repo: https://kubernetes.github.io/ingress-nginx + chart: ingress-nginx + version: 4.x.x + targetNamespace: ingress-nginx + valuesContent: |- + controller: + replicaCount: 2 + ingressClassResource: + name: nginx + enabled: true + default: true + controllerValue: "k8s.io/ingress-nginx" + ingressClass: nginx + service: + type: LoadBalancer + externalTrafficPolicy: Local + annotations: + metallb.io/ip-allocated-from-pool: default-pool + metallb.io/loadBalancerIPs: "192.168.20.204" + loadBalancerIP: 192.168.20.204 + resources: + requests: + cpu: 100m + memory: 90Mi + limits: + cpu: 1000m + memory: 256Mi diff --git a/apps/infra/wg-easy.yaml b/apps/infra/wg-easy.yaml new file mode 100644 index 0000000..628c3fb --- /dev/null +++ b/apps/infra/wg-easy.yaml @@ -0,0 +1,127 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: wg-easy-pvc + namespace: infra + annotations: + nfs.io/storage-path: "wg-easy-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wg-easy + namespace: infra +spec: + strategy: + type: Recreate + replicas: 1 + selector: + matchLabels: + app: wg-easy + template: + metadata: + labels: + app: wg-easy + spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - preference: + matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - nexus + weight: 100 + containers: + - name: wg-easy + image: ghcr.io/wg-easy/wg-easy:latest + imagePullPolicy: Always + ports: + - containerPort: 51820 + protocol: UDP + name: wg-port + - containerPort: 51821 + protocol: TCP + name: web-port + env: + - name: LANG + value: en + - name: WG_HOST + value: vpn.ivanch.me + - name: WG_MTU + value: "1420" + - name: UI_TRAFFIC_STATS + value: "true" + - name: UI_CHART_TYPE + value: "0" + - name: WG_ENABLE_ONE_TIME_LINKS + value: "true" + - name: UI_ENABLE_SORT_CLIENTS + value: "true" + securityContext: + capabilities: + add: + - NET_ADMIN + - SYS_MODULE + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 2000m + memory: 1Gi + volumeMounts: + - name: wg-easy-volume + mountPath: /etc/wireguard + restartPolicy: Always + volumes: + - name: wg-easy-volume + persistentVolumeClaim: + claimName: wg-easy-pvc +--- +apiVersion: v1 +kind: Service +metadata: + name: wg-easy-svc + namespace: infra +spec: + type: LoadBalancer + selector: + app: wg-easy + loadBalancerIP: 192.168.20.203 + ports: + - name: wg-port + port: 51820 + targetPort: 51820 + protocol: UDP + - name: web-port + port: 51821 + targetPort: 51821 + protocol: TCP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: wg-easy-ingress + namespace: infra +spec: + ingressClassName: nginx + rules: + - host: vpn.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: wg-easy-svc + port: + number: 51821 diff --git a/bootstrap/secretstores.yaml b/bootstrap/secretstores.yaml new file mode 100644 index 0000000..40c0d60 --- /dev/null +++ b/bootstrap/secretstores.yaml @@ -0,0 +1,20 @@ +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: { name: bitwarden-login } +spec: + provider: + webhook: + url: "http://bitwarden-cli.infra.svc:8087/object/item/{{ .remoteRef.key }}" + result: { jsonPath: "$.data.login.{{ .remoteRef.property }}" } +--- +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: { name: bitwarden-fields } +spec: + provider: + webhook: + url: "http://bitwarden-cli.infra.svc:8087/object/item/{{ .remoteRef.key }}" + result: + { + jsonPath: '$.data.fields[?@.name=="{{ .remoteRef.property }}"].value', + }