Update telephony-web image to v20260324d, resolve merge conflicts

This commit is contained in:
Andrew M. Stoltz
2026-03-24 15:55:52 -05:00
parent 42d2894ed1
commit f3fde15002
14 changed files with 3333 additions and 3420 deletions

View File

@@ -1,3 +1,3 @@
# bluejay-infra
# bluejay-infra
Infrastructure manifests for ArgoCD

View File

@@ -1,150 +1,150 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: asterisk
namespace: telephony
labels:
app: asterisk
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: asterisk
template:
metadata:
labels:
app: asterisk
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
securityContext:
fsGroup: 0
initContainers:
- name: install-sounds
image: busybox:latest
command:
- sh
- -c
- |
mkdir -p /sounds/en &&
wget -qO- http://downloads.asterisk.org/pub/telephony/sounds/asterisk-core-sounds-en-ulaw-current.tar.gz | tar xz -C /sounds/en/ &&
wget -qO- http://downloads.asterisk.org/pub/telephony/sounds/asterisk-extra-sounds-en-ulaw-current.tar.gz | tar xz -C /sounds/en/ &&
echo "Sound files installed: $(find /sounds/en -type f | wc -l) files"
volumeMounts:
- name: sounds
mountPath: /sounds/en
containers:
- name: asterisk
image: localhost/andrius/asterisk:latest
imagePullPolicy: Never
ports:
- name: sip-udp
containerPort: 5060
protocol: UDP
- name: sip-tcp
containerPort: 5060
protocol: TCP
- name: ari
containerPort: 8088
protocol: TCP
volumeMounts:
- name: config-modules
mountPath: /etc/asterisk/modules.conf
subPath: modules.conf
- name: config-http
mountPath: /etc/asterisk/http.conf
subPath: http.conf
- name: config-ari
mountPath: /etc/asterisk/ari.conf
subPath: ari.conf
- name: config-manager
mountPath: /etc/asterisk/manager.conf
subPath: manager.conf
- name: config-pjsip
mountPath: /etc/asterisk/pjsip.conf
subPath: pjsip.conf
- name: config-extensions
mountPath: /etc/asterisk/extensions.conf
subPath: extensions.conf
- name: config-rtp
mountPath: /etc/asterisk/rtp.conf
subPath: rtp.conf
- name: asterisk-data
mountPath: /var/spool/asterisk
- name: asterisk-logs
mountPath: /var/log/asterisk
- name: sounds
mountPath: /var/lib/asterisk/sounds/en
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
livenessProbe:
tcpSocket:
port: 8088
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /ari/asterisk/info
port: 8088
httpHeaders:
- name: Authorization
value: "Basic Zmxvd2VyY29yZTpibHVlamF5LWFzdGVyaXNrLWFyaQ=="
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: config-modules
configMap:
name: asterisk-config
items:
- key: modules.conf
path: modules.conf
- name: config-http
configMap:
name: asterisk-config
items:
- key: http.conf
path: http.conf
- name: config-ari
configMap:
name: asterisk-config
items:
- key: ari.conf
path: ari.conf
- name: config-manager
configMap:
name: asterisk-config
items:
- key: manager.conf
path: manager.conf
- name: config-pjsip
configMap:
name: asterisk-config
items:
- key: pjsip.conf
path: pjsip.conf
- name: config-extensions
configMap:
name: asterisk-config
items:
- key: extensions.conf
path: extensions.conf
- name: config-rtp
configMap:
name: asterisk-config
items:
- key: rtp.conf
path: rtp.conf
- name: asterisk-data
persistentVolumeClaim:
claimName: asterisk-data
- name: asterisk-logs
emptyDir: {}
- name: sounds
emptyDir: {}
apiVersion: apps/v1
kind: Deployment
metadata:
name: asterisk
namespace: telephony
labels:
app: asterisk
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: asterisk
template:
metadata:
labels:
app: asterisk
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
securityContext:
fsGroup: 0
initContainers:
- name: install-sounds
image: busybox:latest
command:
- sh
- -c
- |
mkdir -p /sounds/en &&
wget -qO- http://downloads.asterisk.org/pub/telephony/sounds/asterisk-core-sounds-en-ulaw-current.tar.gz | tar xz -C /sounds/en/ &&
wget -qO- http://downloads.asterisk.org/pub/telephony/sounds/asterisk-extra-sounds-en-ulaw-current.tar.gz | tar xz -C /sounds/en/ &&
echo "Sound files installed: $(find /sounds/en -type f | wc -l) files"
volumeMounts:
- name: sounds
mountPath: /sounds/en
containers:
- name: asterisk
image: localhost/andrius/asterisk:latest
imagePullPolicy: Never
ports:
- name: sip-udp
containerPort: 5060
protocol: UDP
- name: sip-tcp
containerPort: 5060
protocol: TCP
- name: ari
containerPort: 8088
protocol: TCP
volumeMounts:
- name: config-modules
mountPath: /etc/asterisk/modules.conf
subPath: modules.conf
- name: config-http
mountPath: /etc/asterisk/http.conf
subPath: http.conf
- name: config-ari
mountPath: /etc/asterisk/ari.conf
subPath: ari.conf
- name: config-manager
mountPath: /etc/asterisk/manager.conf
subPath: manager.conf
- name: config-pjsip
mountPath: /etc/asterisk/pjsip.conf
subPath: pjsip.conf
- name: config-extensions
mountPath: /etc/asterisk/extensions.conf
subPath: extensions.conf
- name: config-rtp
mountPath: /etc/asterisk/rtp.conf
subPath: rtp.conf
- name: asterisk-data
mountPath: /var/spool/asterisk
- name: asterisk-logs
mountPath: /var/log/asterisk
- name: sounds
mountPath: /var/lib/asterisk/sounds/en
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
livenessProbe:
tcpSocket:
port: 8088
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /ari/asterisk/info
port: 8088
httpHeaders:
- name: Authorization
value: "Basic Zmxvd2VyY29yZTpibHVlamF5LWFzdGVyaXNrLWFyaQ=="
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: config-modules
configMap:
name: asterisk-config
items:
- key: modules.conf
path: modules.conf
- name: config-http
configMap:
name: asterisk-config
items:
- key: http.conf
path: http.conf
- name: config-ari
configMap:
name: asterisk-config
items:
- key: ari.conf
path: ari.conf
- name: config-manager
configMap:
name: asterisk-config
items:
- key: manager.conf
path: manager.conf
- name: config-pjsip
configMap:
name: asterisk-config
items:
- key: pjsip.conf
path: pjsip.conf
- name: config-extensions
configMap:
name: asterisk-config
items:
- key: extensions.conf
path: extensions.conf
- name: config-rtp
configMap:
name: asterisk-config
items:
- key: rtp.conf
path: rtp.conf
- name: asterisk-data
persistentVolumeClaim:
claimName: asterisk-data
- name: asterisk-logs
emptyDir: {}
- name: sounds
emptyDir: {}

View File

@@ -1,40 +1,40 @@
apiVersion: v1
kind: Service
metadata:
name: asterisk-sip
namespace: telephony
labels:
app: asterisk
annotations:
metallb.universe.tf/loadBalancerIPs: "10.0.56.207"
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: asterisk
ports:
- name: sip-udp
port: 5060
targetPort: 5060
protocol: UDP
- name: sip-tcp
port: 5060
targetPort: 5060
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: asterisk-ari
namespace: telephony
labels:
app: asterisk
spec:
type: ClusterIP
selector:
app: asterisk
ports:
- name: ari
port: 8088
targetPort: 8088
protocol: TCP
apiVersion: v1
kind: Service
metadata:
name: asterisk-sip
namespace: telephony
labels:
app: asterisk
annotations:
metallb.universe.tf/loadBalancerIPs: "10.0.56.207"
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: asterisk
ports:
- name: sip-udp
port: 5060
targetPort: 5060
protocol: UDP
- name: sip-tcp
port: 5060
targetPort: 5060
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: asterisk-ari
namespace: telephony
labels:
app: asterisk
spec:
type: ClusterIP
selector:
app: asterisk
ports:
- name: ari
port: 8088
targetPort: 8088
protocol: TCP

View File

@@ -1,320 +1,320 @@
# FlowerCore Landing Page
# Blue Jay Lab branded landing page - PUBLIC facing
# ArgoCD managed - BlueJay Lab
---
apiVersion: v1
kind: Namespace
metadata:
name: fc-system
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# Landing page HTML (public-safe - no internal LAN references)
apiVersion: v1
kind: ConfigMap
metadata:
name: fc-landing-html
namespace: fc-system
data:
index.html: |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FlowerCore</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #0a1628 0%, #1a2744 50%, #0d1f3c 100%);
color: #e0e8f0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.hero {
text-align: center;
padding: 3rem;
max-width: 800px;
}
.logo {
font-size: 5rem;
margin-bottom: 1.5rem;
filter: drop-shadow(0 0 20px rgba(74, 158, 255, 0.3));
}
h1 {
font-size: 3rem;
background: linear-gradient(135deg, #4a9eff, #7ab3ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 1.3rem;
color: #7ab3ff;
font-weight: 300;
margin-bottom: 1rem;
}
.description {
font-size: 1rem;
color: #8aa8c4;
line-height: 1.6;
margin-bottom: 3rem;
max-width: 600px;
}
.services {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
width: 100%;
max-width: 700px;
padding: 0 1rem;
}
.service {
background: rgba(74, 158, 255, 0.08);
border: 1px solid rgba(74, 158, 255, 0.2);
border-radius: 8px;
padding: 1.2rem;
text-decoration: none;
color: inherit;
transition: all 0.2s;
}
.service:hover {
background: rgba(74, 158, 255, 0.15);
border-color: rgba(74, 158, 255, 0.5);
transform: translateY(-2px);
}
.service h3 { color: #4a9eff; font-size: 0.95rem; margin-bottom: 0.3rem; }
.service p { color: #8aa8c4; font-size: 0.8rem; }
.status-bar {
display: flex;
gap: 2rem;
margin-top: 2rem;
padding: 1rem 2rem;
background: rgba(74, 158, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(74, 158, 255, 0.1);
}
.status-item { text-align: center; }
.status-item .value { color: #4a9eff; font-size: 1.5rem; font-weight: 700; }
.status-item .label { color: #6a8ca4; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1px; }
.footer {
margin-top: 3rem;
color: #4a6580;
font-size: 0.8rem;
}
.footer a { color: #4a6580; text-decoration: none; }
.footer a:hover { color: #7ab3ff; }
</style>
</head>
<body>
<div class="hero">
<div class="logo">&#x1F33B;</div>
<h1>FlowerCore</h1>
<p class="subtitle">Blue Jay Lab</p>
<p class="description">
Multi-tenant service management platform built on .NET 10,
Kubernetes, and GitOps. Digital signage, telephony IVR,
MySQL/PHP hosting, and infrastructure automation.
</p>
</div>
<div class="services">
<a class="service" href="https://gitea.flowercore.io">
<h3>Source</h3>
<p>Gitea repositories</p>
</a>
<a class="service" href="https://webmail.flowercore.io">
<h3>Mail</h3>
<p>Webmail access</p>
</a>
<a class="service" href="https://element.flowercore.io">
<h3>Chat</h3>
<p>Matrix messaging</p>
</a>
<a class="service" href="https://github.com/FlowerCoreIO">
<h3>GitHub</h3>
<p>Open source</p>
</a>
</div>
<div class="status-bar">
<div class="status-item">
<div class="value">17</div>
<div class="label">Services</div>
</div>
<div class="status-item">
<div class="value">13</div>
<div class="label">VLANs</div>
</div>
<div class="status-item">
<div class="value">12k+</div>
<div class="label">Tests</div>
</div>
</div>
<p class="footer">
FlowerCore &middot; Bare-metal RKE2 &middot; ArgoCD managed
&middot; <a href="mailto:admin@flowercore.io">Contact</a>
</p>
</body>
</html>
---
# nginx configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: fc-landing-nginx-conf
namespace: fc-system
data:
default.conf: |
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
location /healthz {
access_log off;
return 200 "ok";
add_header Content-Type text/plain;
}
}
---
# Landing Page Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: fc-landing
namespace: fc-system
labels:
app: fc-landing
spec:
replicas: 1
selector:
matchLabels:
app: fc-landing
template:
metadata:
labels:
app: fc-landing
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
name: http
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
- name: html
mountPath: /usr/share/nginx/html
resources:
requests:
memory: 16Mi
cpu: 5m
limits:
memory: 64Mi
cpu: 50m
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 3
periodSeconds: 5
volumes:
- name: nginx-conf
configMap:
name: fc-landing-nginx-conf
- name: html
configMap:
name: fc-landing-html
---
apiVersion: v1
kind: Service
metadata:
name: fc-landing
namespace: fc-system
spec:
selector:
app: fc-landing
ports:
- port: 80
targetPort: 80
name: http
---
# Internal IngressRoute (LAN access)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: fc-landing
namespace: fc-system
spec:
entryPoints:
- websecure
routes:
- match: Host(`flowercore.iamworkin.lan`)
kind: Rule
services:
- name: fc-landing
port: 80
tls: {}
---
# Public IngressRoute (flowercore.io with Cloudflare origin cert)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: fc-landing-public
namespace: fc-system
spec:
entryPoints:
- websecure
routes:
- match: Host(`flowercore.io`) || Host(`www.flowercore.io`)
kind: Rule
services:
- name: fc-landing
port: 80
tls:
secretName: cf-origin-flowercore-io
---
# HTTP to HTTPS redirect for public domain
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: fc-landing-public-http
namespace: fc-system
spec:
entryPoints:
- web
routes:
- match: Host(`flowercore.io`) || Host(`www.flowercore.io`)
kind: Rule
services:
- name: fc-landing
port: 80
middlewares:
- name: redirect-https
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
namespace: fc-system
spec:
redirectScheme:
scheme: https
permanent: true
# FlowerCore Landing Page
# Blue Jay Lab branded landing page - PUBLIC facing
# ArgoCD managed - BlueJay Lab
---
apiVersion: v1
kind: Namespace
metadata:
name: fc-system
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# Landing page HTML (public-safe - no internal LAN references)
apiVersion: v1
kind: ConfigMap
metadata:
name: fc-landing-html
namespace: fc-system
data:
index.html: |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FlowerCore</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #0a1628 0%, #1a2744 50%, #0d1f3c 100%);
color: #e0e8f0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.hero {
text-align: center;
padding: 3rem;
max-width: 800px;
}
.logo {
font-size: 5rem;
margin-bottom: 1.5rem;
filter: drop-shadow(0 0 20px rgba(74, 158, 255, 0.3));
}
h1 {
font-size: 3rem;
background: linear-gradient(135deg, #4a9eff, #7ab3ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 1.3rem;
color: #7ab3ff;
font-weight: 300;
margin-bottom: 1rem;
}
.description {
font-size: 1rem;
color: #8aa8c4;
line-height: 1.6;
margin-bottom: 3rem;
max-width: 600px;
}
.services {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
width: 100%;
max-width: 700px;
padding: 0 1rem;
}
.service {
background: rgba(74, 158, 255, 0.08);
border: 1px solid rgba(74, 158, 255, 0.2);
border-radius: 8px;
padding: 1.2rem;
text-decoration: none;
color: inherit;
transition: all 0.2s;
}
.service:hover {
background: rgba(74, 158, 255, 0.15);
border-color: rgba(74, 158, 255, 0.5);
transform: translateY(-2px);
}
.service h3 { color: #4a9eff; font-size: 0.95rem; margin-bottom: 0.3rem; }
.service p { color: #8aa8c4; font-size: 0.8rem; }
.status-bar {
display: flex;
gap: 2rem;
margin-top: 2rem;
padding: 1rem 2rem;
background: rgba(74, 158, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(74, 158, 255, 0.1);
}
.status-item { text-align: center; }
.status-item .value { color: #4a9eff; font-size: 1.5rem; font-weight: 700; }
.status-item .label { color: #6a8ca4; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1px; }
.footer {
margin-top: 3rem;
color: #4a6580;
font-size: 0.8rem;
}
.footer a { color: #4a6580; text-decoration: none; }
.footer a:hover { color: #7ab3ff; }
</style>
</head>
<body>
<div class="hero">
<div class="logo">&#x1F33B;</div>
<h1>FlowerCore</h1>
<p class="subtitle">Blue Jay Lab</p>
<p class="description">
Multi-tenant service management platform built on .NET 10,
Kubernetes, and GitOps. Digital signage, telephony IVR,
MySQL/PHP hosting, and infrastructure automation.
</p>
</div>
<div class="services">
<a class="service" href="https://gitea.flowercore.io">
<h3>Source</h3>
<p>Gitea repositories</p>
</a>
<a class="service" href="https://webmail.flowercore.io">
<h3>Mail</h3>
<p>Webmail access</p>
</a>
<a class="service" href="https://element.flowercore.io">
<h3>Chat</h3>
<p>Matrix messaging</p>
</a>
<a class="service" href="https://github.com/FlowerCoreIO">
<h3>GitHub</h3>
<p>Open source</p>
</a>
</div>
<div class="status-bar">
<div class="status-item">
<div class="value">17</div>
<div class="label">Services</div>
</div>
<div class="status-item">
<div class="value">13</div>
<div class="label">VLANs</div>
</div>
<div class="status-item">
<div class="value">12k+</div>
<div class="label">Tests</div>
</div>
</div>
<p class="footer">
FlowerCore &middot; Bare-metal RKE2 &middot; ArgoCD managed
&middot; <a href="mailto:admin@flowercore.io">Contact</a>
</p>
</body>
</html>
---
# nginx configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: fc-landing-nginx-conf
namespace: fc-system
data:
default.conf: |
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
location /healthz {
access_log off;
return 200 "ok";
add_header Content-Type text/plain;
}
}
---
# Landing Page Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: fc-landing
namespace: fc-system
labels:
app: fc-landing
spec:
replicas: 1
selector:
matchLabels:
app: fc-landing
template:
metadata:
labels:
app: fc-landing
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
name: http
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
- name: html
mountPath: /usr/share/nginx/html
resources:
requests:
memory: 16Mi
cpu: 5m
limits:
memory: 64Mi
cpu: 50m
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 3
periodSeconds: 5
volumes:
- name: nginx-conf
configMap:
name: fc-landing-nginx-conf
- name: html
configMap:
name: fc-landing-html
---
apiVersion: v1
kind: Service
metadata:
name: fc-landing
namespace: fc-system
spec:
selector:
app: fc-landing
ports:
- port: 80
targetPort: 80
name: http
---
# Internal IngressRoute (LAN access)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: fc-landing
namespace: fc-system
spec:
entryPoints:
- websecure
routes:
- match: Host(`flowercore.iamworkin.lan`)
kind: Rule
services:
- name: fc-landing
port: 80
tls: {}
---
# Public IngressRoute (flowercore.io with Cloudflare origin cert)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: fc-landing-public
namespace: fc-system
spec:
entryPoints:
- websecure
routes:
- match: Host(`flowercore.io`) || Host(`www.flowercore.io`)
kind: Rule
services:
- name: fc-landing
port: 80
tls:
secretName: cf-origin-flowercore-io
---
# HTTP to HTTPS redirect for public domain
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: fc-landing-public-http
namespace: fc-system
spec:
entryPoints:
- web
routes:
- match: Host(`flowercore.io`) || Host(`www.flowercore.io`)
kind: Rule
services:
- name: fc-landing
port: 80
middlewares:
- name: redirect-https
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
namespace: fc-system
spec:
redirectScheme:
scheme: https
permanent: true

View File

@@ -1,20 +1,20 @@
# Gitea Public IngressRoute
# Routes gitea.flowercore.io to internal Gitea service via Cloudflare origin cert
# ArgoCD managed - BlueJay Lab
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: gitea-public
namespace: gitea
spec:
entryPoints:
- websecure
routes:
- match: Host(`gitea.flowercore.io`)
kind: Rule
services:
- name: gitea-http
port: 3000
tls:
secretName: cf-origin-flowercore-io
# Gitea Public IngressRoute
# Routes gitea.flowercore.io to internal Gitea service via Cloudflare origin cert
# ArgoCD managed - BlueJay Lab
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: gitea-public
namespace: gitea
spec:
entryPoints:
- websecure
routes:
- match: Host(`gitea.flowercore.io`)
kind: Rule
services:
- name: gitea-http
port: 3000
tls:
secretName: cf-origin-flowercore-io

View File

@@ -1,426 +1,344 @@
# Apache Guacamole - Blue Jay Remote Access
# FlowerCore Infrastructure Gateway
# MySQL 8 + guacd + guacamole web (Blue Jay branded)
# ArgoCD managed - BlueJay Lab
# ALL credentials sourced from 1Password via OnePasswordItem CRD (guacamole-credentials)
# Custom image: fc-guacamole:bluejay (Blue Jay branding + 1Password vault extension)
---
apiVersion: v1
kind: Namespace
metadata:
name: guacamole
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# MySQL 8 StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: guac-mysql
namespace: guacamole
labels:
app: guac-mysql
spec:
serviceName: guac-mysql
replicas: 1
selector:
matchLabels:
app: guac-mysql
template:
metadata:
labels:
app: guac-mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Root-Password
- name: MYSQL_DATABASE
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Name
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-User
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Password
volumeMounts:
- name: guac-mysql-data
mountPath: /var/lib/mysql
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
cpu: 500m
livenessProbe:
exec:
command:
- mysqladmin
- ping
- -h
- localhost
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
exec:
command:
- mysqladmin
- ping
- -h
- localhost
initialDelaySeconds: 30
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: guac-mysql-data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
name: guac-mysql
namespace: guacamole
spec:
selector:
app: guac-mysql
ports:
- port: 3306
targetPort: 3306
name: mysql
clusterIP: None
---
# DB schema init Job
apiVersion: batch/v1
kind: Job
metadata:
name: guacamole-initdb
namespace: guacamole
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
ttlSecondsAfterFinished: 300
template:
spec:
restartPolicy: OnFailure
initContainers:
- name: wait-for-mysql
image: mysql:8.0
command:
- sh
- -c
- |
until mysqladmin ping -h guac-mysql --silent; do
echo "Waiting for MySQL..."
sleep 5
done
containers:
- name: initdb
image: guacamole/guacamole:latest
command:
- sh
- -c
- |
/opt/guacamole/bin/initdb.sh --mysql > /tmp/initdb.sql
mysql -h guac-mysql -u root -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE" < /tmp/initdb.sql || true
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Root-Password
- name: MYSQL_DATABASE
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Name
---
# guacd (Guacamole daemon)
apiVersion: apps/v1
kind: Deployment
metadata:
name: guacd
namespace: guacamole
labels:
app: guacd
spec:
replicas: 1
selector:
matchLabels:
app: guacd
template:
metadata:
labels:
app: guacd
spec:
containers:
serviceAccountName: guacd-exec
- name: guacd
image: guacamole/guacd:latest
ports:
- containerPort: 4822
name: guacd
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 512Mi
cpu: 500m
livenessProbe:
tcpSocket:
port: 4822
initialDelaySeconds: 15
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: guacd
namespace: guacamole
spec:
selector:
app: guacd
ports:
- port: 4822
targetPort: 4822
name: guacd
---
# Guacamole Properties ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: guacamole-properties
namespace: guacamole
labels:
app: guacamole
data:
guacamole.properties: |
# Blue Jay Remote Access — Guacamole Configuration
# MySQL/guacd settings provided via env vars — do NOT duplicate here
# 1Password Vault Integration
1password-connect-url: http://onepassword-connect.onepassword-system.svc.cluster.local:8080
1password-connect-token: placeholder-configure-via-secret
1password-vault-id: qaphopopkryhbg353ukzhhuqoq
# Extension Priority
extension-priority: mysql, ban, bluejay, 1password-vault, *
# Ban (brute force)
ban-max-invalid-attempts: 5
ban-address-duration: 300000
ban-max-addresses: 1000
# TOTP
totp-issuer: Blue Jay Remote Access
totp-digits: 6
totp-period: 30
totp-mode: sha256
# Session Recording
recording-search-path: /var/lib/guacamole/recordings
# Logging
log-level: info
# API Token Expiry
api-session-timeout: 60
---
# Guacamole Web Application — Blue Jay branded
apiVersion: apps/v1
kind: Deployment
metadata:
name: guacamole
namespace: guacamole
labels:
app: guacamole
spec:
replicas: 1
selector:
matchLabels:
app: guacamole
template:
metadata:
labels:
app: guacamole
spec:
containers:
- name: guacamole
image: localhost/fc-guacamole:bluejay
imagePullPolicy: Never
ports:
- containerPort: 8080
name: http
env:
- name: GUACD_HOSTNAME
value: guacd
- name: GUACD_PORT
value: "4822"
- name: MYSQL_HOSTNAME
value: guac-mysql
- name: MYSQL_PORT
value: "3306"
- name: MYSQL_DATABASE
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Name
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-User
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Password
volumeMounts:
- name: guac-properties
mountPath: /etc/guacamole/guacamole.properties
subPath: guacamole.properties
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
cpu: 500m
livenessProbe:
httpGet:
path: /guacamole/
port: 8080
initialDelaySeconds: 120
periodSeconds: 10
readinessProbe:
httpGet:
path: /guacamole/
port: 8080
initialDelaySeconds: 60
periodSeconds: 5
volumes:
- name: guac-properties
configMap:
name: guacamole-properties
---
apiVersion: v1
kind: Service
metadata:
name: guacamole
namespace: guacamole
spec:
selector:
app: guacamole
ports:
- port: 8080
targetPort: 8080
name: http
---
# Traefik addPrefix middleware
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: guac-add-prefix
namespace: guacamole
spec:
addPrefix:
prefix: /guacamole
---
# TLS Certificate via cert-manager
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: guacamole-tls
namespace: guacamole
spec:
secretName: guacamole-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- guac.iamworkin.lan
---
# Traefik IngressRoute
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: guacamole
namespace: guacamole
spec:
entryPoints:
- websecure
routes:
- match: Host(`guac.iamworkin.lan`)
kind: Rule
middlewares:
- name: guac-add-prefix
services:
- name: guacamole
port: 8080
tls:
secretName: guacamole-tls
---
# 1Password secret sync
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: guacamole-credentials
namespace: guacamole
spec:
itemPath: vaults/IAmWorkin/items/Guacamole
---
# RBAC for guacd K8s exec protocol
apiVersion: v1
kind: ServiceAccount
metadata:
name: guacd-exec
namespace: guacamole
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: guacd-pod-exec
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: guacd-pod-exec
subjects:
- kind: ServiceAccount
name: guacd-exec
namespace: guacamole
roleRef:
kind: ClusterRole
name: guacd-pod-exec
apiGroup: rbac.authorization.k8s.io
# Apache Guacamole - Remote Desktop Gateway
# MySQL 8 + guacd + guacamole web
# ArgoCD managed - BlueJay Lab
# ALL credentials sourced from 1Password via OnePasswordItem CRD (guacamole-credentials)
# Fields: username, password, DB-User, DB-Password, DB-Root-Password, DB-Name, URL
---
apiVersion: v1
kind: Namespace
metadata:
name: guacamole
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# MySQL 8 StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: guac-mysql
namespace: guacamole
labels:
app: guac-mysql
spec:
serviceName: guac-mysql
replicas: 1
selector:
matchLabels:
app: guac-mysql
template:
metadata:
labels:
app: guac-mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Root-Password
- name: MYSQL_DATABASE
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Name
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-User
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Password
volumeMounts:
- name: guac-mysql-data
mountPath: /var/lib/mysql
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
cpu: 500m
livenessProbe:
exec:
command:
- mysqladmin
- ping
- -h
- localhost
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
exec:
command:
- mysqladmin
- ping
- -h
- localhost
initialDelaySeconds: 30
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: guac-mysql-data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
name: guac-mysql
namespace: guacamole
spec:
selector:
app: guac-mysql
ports:
- port: 3306
targetPort: 3306
name: mysql
clusterIP: None
---
# DB schema init Job
# Generates the MySQL schema and pipes it into the database
apiVersion: batch/v1
kind: Job
metadata:
name: guacamole-initdb
namespace: guacamole
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
ttlSecondsAfterFinished: 300
template:
spec:
restartPolicy: OnFailure
initContainers:
- name: wait-for-mysql
image: mysql:8.0
command:
- sh
- -c
- |
until mysqladmin ping -h guac-mysql --silent; do
echo "Waiting for MySQL..."
sleep 5
done
containers:
- name: initdb
image: guacamole/guacamole:latest
command:
- sh
- -c
- |
# Generate schema SQL
/opt/guacamole/bin/initdb.sh --mysql > /tmp/initdb.sql
# Apply schema (ignore errors if tables already exist)
mysql -h guac-mysql -u root -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE" < /tmp/initdb.sql || true
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Root-Password
- name: MYSQL_DATABASE
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Name
---
# guacd (Guacamole daemon)
apiVersion: apps/v1
kind: Deployment
metadata:
name: guacd
namespace: guacamole
labels:
app: guacd
spec:
replicas: 1
selector:
matchLabels:
app: guacd
template:
metadata:
labels:
app: guacd
spec:
containers:
- name: guacd
image: guacamole/guacd:latest
ports:
- containerPort: 4822
name: guacd
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 512Mi
cpu: 500m
livenessProbe:
tcpSocket:
port: 4822
initialDelaySeconds: 15
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: guacd
namespace: guacamole
spec:
selector:
app: guacd
ports:
- port: 4822
targetPort: 4822
name: guacd
---
# Guacamole Web Application
apiVersion: apps/v1
kind: Deployment
metadata:
name: guacamole
namespace: guacamole
labels:
app: guacamole
spec:
replicas: 1
selector:
matchLabels:
app: guacamole
template:
metadata:
labels:
app: guacamole
spec:
containers:
- name: guacamole
image: guacamole/guacamole:latest
ports:
- containerPort: 8080
name: http
env:
- name: GUACD_HOSTNAME
value: guacd
- name: GUACD_PORT
value: "4822"
- name: MYSQL_HOSTNAME
value: guac-mysql
- name: MYSQL_PORT
value: "3306"
- name: MYSQL_DATABASE
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Name
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-User
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: guacamole-credentials
key: DB-Password
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
cpu: 500m
livenessProbe:
httpGet:
path: /guacamole/
port: 8080
initialDelaySeconds: 120
periodSeconds: 10
readinessProbe:
httpGet:
path: /guacamole/
port: 8080
initialDelaySeconds: 60
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: guacamole
namespace: guacamole
spec:
selector:
app: guacamole
ports:
- port: 8080
targetPort: 8080
name: http
---
# Traefik addPrefix middleware
# External URL guac.iamworkin.lan/ gets prefix /guacamole added
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: guac-add-prefix
namespace: guacamole
spec:
addPrefix:
prefix: /guacamole
---
# TLS Certificate via cert-manager
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: guacamole-tls
namespace: guacamole
spec:
secretName: guacamole-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- guac.iamworkin.lan
---
# Traefik IngressRoute
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: guacamole
namespace: guacamole
spec:
entryPoints:
- websecure
routes:
- match: Host(`guac.iamworkin.lan`)
kind: Rule
middlewares:
- name: guac-add-prefix
services:
- name: guacamole
port: 8080
tls:
secretName: guacamole-tls
---
# 1Password secret sync — creates guacamole-credentials K8s Secret
# Fields: username, password, DB-User, DB-Password, DB-Root-Password, DB-Name, URL
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: guacamole-credentials
namespace: guacamole
spec:
itemPath: vaults/IAmWorkin/items/Guacamole

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,259 +1,259 @@
# docker-mailserver - Postfix + Dovecot + rspamd
# ArgoCD managed - BlueJay Lab
# Credentials: 1Password → OnePasswordItem CRD → K8s Secret
---
apiVersion: v1
kind: Namespace
metadata:
name: mail
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# 1Password → K8s Secret sync for mail credentials
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: mail-credentials
namespace: mail
spec:
itemPath: "vaults/IAmWorkin/items/Mail Postmaster"
---
# Mail data PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mail-data
namespace: mail
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 5Gi
---
# Mail state PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mail-state
namespace: mail
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
---
# docker-mailserver Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: mailserver
namespace: mail
labels:
app: mailserver
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: mailserver
template:
metadata:
labels:
app: mailserver
spec:
hostname: mail
initContainers:
- name: inject-accounts
image: busybox:1.36
command:
- sh
- -c
- |
ADMIN_EMAIL=$(cat /credentials/Admin-Email)
ADMIN_HASH=$(cat /credentials/Admin-Hash)
NOREPLY_EMAIL=$(cat /credentials/Noreply-Email)
NOREPLY_HASH=$(cat /credentials/Noreply-Hash)
echo "${ADMIN_EMAIL}|${ADMIN_HASH}" > /accounts/postfix-accounts.cf
echo "${NOREPLY_EMAIL}|${NOREPLY_HASH}" >> /accounts/postfix-accounts.cf
volumeMounts:
- name: mail-credentials
mountPath: /credentials
readOnly: true
- name: mail-accounts-generated
mountPath: /accounts
containers:
- name: mailserver
image: docker.io/mailserver/docker-mailserver:latest
ports:
- containerPort: 25
name: smtp
- containerPort: 465
name: smtps
- containerPort: 587
name: submission
- containerPort: 143
name: imap
- containerPort: 993
name: imaps
env:
- name: ENABLE_SPAMASSASSIN
value: "1"
- name: ENABLE_CLAMAV
value: "0"
- name: ENABLE_RSPAMD
value: "1"
- name: TZ
value: America/Chicago
- name: POSTMASTER_ADDRESS
value: postmaster@iamwork.in
- name: OVERRIDE_HOSTNAME
value: mail.iamwork.in
- name: ENABLE_FAIL2BAN
value: "0"
- name: ENABLE_POSTGREY
value: "0"
- name: ONE_DIR
value: "1"
- name: PERMIT_DOCKER
value: network
- name: SSL_TYPE
value: manual
- name: SSL_CERT_PATH
value: /etc/ssl/mail/tls.crt
- name: SSL_KEY_PATH
value: /etc/ssl/mail/tls.key
- name: ACCOUNT_PROVISIONER
value: FILE
volumeMounts:
- name: mail-data
mountPath: /var/mail
- name: mail-state
mountPath: /var/mail-state
- name: mail-tls
mountPath: /etc/ssl/mail
readOnly: true
- name: mail-accounts-generated
mountPath: /tmp/docker-mailserver/postfix-accounts.cf
subPath: postfix-accounts.cf
readOnly: true
resources:
requests:
memory: 512Mi
cpu: 200m
limits:
memory: 2Gi
cpu: "1"
securityContext:
capabilities:
add:
- NET_ADMIN
- SYS_PTRACE
volumes:
- name: mail-data
persistentVolumeClaim:
claimName: mail-data
- name: mail-state
persistentVolumeClaim:
claimName: mail-state
- name: mail-tls
secret:
secretName: mail-tls
- name: mail-credentials
secret:
secretName: mail-credentials
- name: mail-accounts-generated
emptyDir: {}
---
# SMTP LoadBalancer Service (external)
apiVersion: v1
kind: Service
metadata:
name: mail-smtp
namespace: mail
annotations:
metallb.universe.tf/loadBalancerIPs: 10.0.56.202
spec:
type: LoadBalancer
selector:
app: mailserver
ports:
- port: 25
targetPort: 25
name: smtp
protocol: TCP
- port: 465
targetPort: 465
name: smtps
protocol: TCP
- port: 587
targetPort: 587
name: submission
protocol: TCP
---
# IMAP ClusterIP Service (internal)
apiVersion: v1
kind: Service
metadata:
name: mail-imap
namespace: mail
spec:
selector:
app: mailserver
ports:
- port: 143
targetPort: 143
name: imap
- port: 993
targetPort: 993
name: imaps
---
# TLS Certificate via cert-manager
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: mail-tls
namespace: mail
spec:
secretName: mail-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- mail.iamworkin.lan
---
# Traefik IngressRoute - Webmail placeholder
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: mail-webmail
namespace: mail
spec:
entryPoints:
- websecure
routes:
- match: Host(`mail.iamworkin.lan`)
kind: Rule
services:
- name: mail-imap
port: 993
tls:
secretName: mail-tls
---
# Public IngressRoute - Webmail (flowercore.io with Cloudflare origin cert)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: mail-webmail-public
namespace: mail
spec:
entryPoints:
- websecure
routes:
- match: Host(`webmail.flowercore.io`)
kind: Rule
services:
- name: mail-webmail
port: 8080
tls:
secretName: cf-origin-flowercore-io
# docker-mailserver - Postfix + Dovecot + rspamd
# ArgoCD managed - BlueJay Lab
# Credentials: 1Password → OnePasswordItem CRD → K8s Secret
---
apiVersion: v1
kind: Namespace
metadata:
name: mail
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# 1Password → K8s Secret sync for mail credentials
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: mail-credentials
namespace: mail
spec:
itemPath: "vaults/IAmWorkin/items/Mail Postmaster"
---
# Mail data PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mail-data
namespace: mail
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 5Gi
---
# Mail state PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mail-state
namespace: mail
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
---
# docker-mailserver Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: mailserver
namespace: mail
labels:
app: mailserver
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: mailserver
template:
metadata:
labels:
app: mailserver
spec:
hostname: mail
initContainers:
- name: inject-accounts
image: busybox:1.36
command:
- sh
- -c
- |
ADMIN_EMAIL=$(cat /credentials/Admin-Email)
ADMIN_HASH=$(cat /credentials/Admin-Hash)
NOREPLY_EMAIL=$(cat /credentials/Noreply-Email)
NOREPLY_HASH=$(cat /credentials/Noreply-Hash)
echo "${ADMIN_EMAIL}|${ADMIN_HASH}" > /accounts/postfix-accounts.cf
echo "${NOREPLY_EMAIL}|${NOREPLY_HASH}" >> /accounts/postfix-accounts.cf
volumeMounts:
- name: mail-credentials
mountPath: /credentials
readOnly: true
- name: mail-accounts-generated
mountPath: /accounts
containers:
- name: mailserver
image: docker.io/mailserver/docker-mailserver:latest
ports:
- containerPort: 25
name: smtp
- containerPort: 465
name: smtps
- containerPort: 587
name: submission
- containerPort: 143
name: imap
- containerPort: 993
name: imaps
env:
- name: ENABLE_SPAMASSASSIN
value: "1"
- name: ENABLE_CLAMAV
value: "0"
- name: ENABLE_RSPAMD
value: "1"
- name: TZ
value: America/Chicago
- name: POSTMASTER_ADDRESS
value: postmaster@iamwork.in
- name: OVERRIDE_HOSTNAME
value: mail.iamwork.in
- name: ENABLE_FAIL2BAN
value: "0"
- name: ENABLE_POSTGREY
value: "0"
- name: ONE_DIR
value: "1"
- name: PERMIT_DOCKER
value: network
- name: SSL_TYPE
value: manual
- name: SSL_CERT_PATH
value: /etc/ssl/mail/tls.crt
- name: SSL_KEY_PATH
value: /etc/ssl/mail/tls.key
- name: ACCOUNT_PROVISIONER
value: FILE
volumeMounts:
- name: mail-data
mountPath: /var/mail
- name: mail-state
mountPath: /var/mail-state
- name: mail-tls
mountPath: /etc/ssl/mail
readOnly: true
- name: mail-accounts-generated
mountPath: /tmp/docker-mailserver/postfix-accounts.cf
subPath: postfix-accounts.cf
readOnly: true
resources:
requests:
memory: 512Mi
cpu: 200m
limits:
memory: 2Gi
cpu: "1"
securityContext:
capabilities:
add:
- NET_ADMIN
- SYS_PTRACE
volumes:
- name: mail-data
persistentVolumeClaim:
claimName: mail-data
- name: mail-state
persistentVolumeClaim:
claimName: mail-state
- name: mail-tls
secret:
secretName: mail-tls
- name: mail-credentials
secret:
secretName: mail-credentials
- name: mail-accounts-generated
emptyDir: {}
---
# SMTP LoadBalancer Service (external)
apiVersion: v1
kind: Service
metadata:
name: mail-smtp
namespace: mail
annotations:
metallb.universe.tf/loadBalancerIPs: 10.0.56.202
spec:
type: LoadBalancer
selector:
app: mailserver
ports:
- port: 25
targetPort: 25
name: smtp
protocol: TCP
- port: 465
targetPort: 465
name: smtps
protocol: TCP
- port: 587
targetPort: 587
name: submission
protocol: TCP
---
# IMAP ClusterIP Service (internal)
apiVersion: v1
kind: Service
metadata:
name: mail-imap
namespace: mail
spec:
selector:
app: mailserver
ports:
- port: 143
targetPort: 143
name: imap
- port: 993
targetPort: 993
name: imaps
---
# TLS Certificate via cert-manager
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: mail-tls
namespace: mail
spec:
secretName: mail-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- mail.iamworkin.lan
---
# Traefik IngressRoute - Webmail placeholder
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: mail-webmail
namespace: mail
spec:
entryPoints:
- websecure
routes:
- match: Host(`mail.iamworkin.lan`)
kind: Rule
services:
- name: mail-imap
port: 993
tls:
secretName: mail-tls
---
# Public IngressRoute - Webmail (flowercore.io with Cloudflare origin cert)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: mail-webmail-public
namespace: mail
spec:
entryPoints:
- websecure
routes:
- match: Host(`webmail.flowercore.io`)
kind: Rule
services:
- name: mail-webmail
port: 8080
tls:
secretName: cf-origin-flowercore-io

View File

@@ -1,495 +1,495 @@
# Matrix Synapse + Element Web
# PostgreSQL 16 + Synapse homeserver + Element Web client
# ArgoCD managed - BlueJay Lab
# DB credentials sourced from 1Password via OnePasswordItem CRD (matrix-credentials)
# Synapse homeserver.yaml DB password injected at runtime via init container
---
apiVersion: v1
kind: Namespace
metadata:
name: matrix
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# Synapse homeserver.yaml template ConfigMap
# DB password placeholder __DB_PASSWORD__ is replaced at pod startup by init container
apiVersion: v1
kind: ConfigMap
metadata:
name: synapse-config
namespace: matrix
data:
homeserver.yaml.template: |
server_name: "iamworkin.lan"
pid_file: /data/homeserver.pid
public_baseurl: "https://matrix.iamworkin.lan/"
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
bind_addresses: ["0.0.0.0"]
resources:
- names: [client, federation]
compress: false
database:
name: psycopg2
args:
user: __DB_USER__
password: __DB_PASSWORD__
database: synapse
host: matrix-postgres
port: 5432
cp_min: 5
cp_max: 10
log_config: "/config/log.config"
media_store_path: /data/media_store
registration_shared_secret: "a208f2e4b260f6b7d6ff4566df49c56c8b73fa20b911ce4e617b791ee7868adc"
report_stats: false
macaroon_secret_key: "9964f398e8b48a91469ad419d293c06db4562f49df8cc6e129fb3a801fd9052d"
form_secret: "7b0a9dbaf9ee94450e0b3271c408dfc4d313a55843ce4eec2ac1bb0315ffeb76"
signing_key_path: "/data/signing.key"
trusted_key_servers:
- server_name: "matrix.org"
enable_registration: false
suppress_key_server_warning: true
log.config: |
version: 1
formatters:
precise:
format: "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s"
handlers:
console:
class: logging.StreamHandler
formatter: precise
loggers:
synapse.storage.SQL:
level: WARNING
root:
level: WARNING
handlers: [console]
disable_existing_loggers: false
---
# PostgreSQL 16 StatefulSet
# Credentials from 1Password-synced matrix-credentials secret
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: matrix-postgres
namespace: matrix
labels:
app: matrix-postgres
spec:
serviceName: matrix-postgres
replicas: 1
selector:
matchLabels:
app: matrix-postgres
template:
metadata:
labels:
app: matrix-postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: matrix-credentials
key: DB-User
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: matrix-credentials
key: DB-Password
- name: POSTGRES_DB
value: synapse
- name: POSTGRES_INITDB_ARGS
value: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumeMounts:
- name: matrix-postgres-data
mountPath: /var/lib/postgresql/data
subPath: pgdata
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
cpu: 500m
livenessProbe:
exec:
command: ["pg_isready", "-U", "synapse"]
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command: ["pg_isready", "-U", "synapse"]
initialDelaySeconds: 5
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: matrix-postgres-data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
name: matrix-postgres
namespace: matrix
spec:
selector:
app: matrix-postgres
ports:
- port: 5432
targetPort: 5432
name: postgres
clusterIP: None
---
# Synapse Data PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: synapse-data
namespace: matrix
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 2Gi
---
# Synapse Deployment
# Init container injects DB credentials from 1Password secret into homeserver.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: synapse
namespace: matrix
labels:
app: synapse
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: synapse
template:
metadata:
labels:
app: synapse
spec:
initContainers:
- name: generate-signing-key
image: matrixdotorg/synapse:latest
securityContext:
runAsUser: 0
command: ["sh", "-c"]
args:
- |
if [ \! -f /data/signing.key ]; then
python -m synapse.app.homeserver --generate-keys --config-path /config-template/homeserver.yaml.template 2>/dev/null || true
# If key generation fails with template, create a minimal config for key gen
if [ \! -f /data/signing.key ]; then
echo server_name: iamworkin.lan > /tmp/minimal.yaml
echo signing_key_path: /data/signing.key >> /tmp/minimal.yaml
python -c "from signedjson.key import generate_signing_key, write_signing_keys; import sys; key = generate_signing_key(a_auto); write_signing_keys(open(/data/signing.key,w), [key])" 2>/dev/null || true
fi
fi
chown 991:991 /data/signing.key 2>/dev/null || true
chmod 644 /data/signing.key 2>/dev/null || true
mkdir -p /data/media_store
chown -R 991:991 /data 2>/dev/null || true
volumeMounts:
- name: synapse-data
mountPath: /data
- name: synapse-config-template
mountPath: /config-template
- name: inject-credentials
image: busybox:latest
command: ["sh", "-c"]
args:
- |
# Copy template and substitute DB credentials from 1Password secret
cp /config-template/log.config /config/log.config
sed -e "s/__DB_PASSWORD__/${DB_PASSWORD}/g" \
-e "s/__DB_USER__/${DB_USER}/g" \
/config-template/homeserver.yaml.template > /config/homeserver.yaml
echo "Credentials injected into homeserver.yaml"
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: matrix-credentials
key: DB-Password
- name: DB_USER
valueFrom:
secretKeyRef:
name: matrix-credentials
key: DB-User
volumeMounts:
- name: synapse-config-template
mountPath: /config-template
- name: synapse-config-rendered
mountPath: /config
containers:
- name: synapse
image: matrixdotorg/synapse:latest
ports:
- containerPort: 8008
name: http
env:
- name: SYNAPSE_CONFIG_DIR
value: /config
- name: SYNAPSE_CONFIG_PATH
value: /config/homeserver.yaml
volumeMounts:
- name: synapse-data
mountPath: /data
- name: synapse-config-rendered
mountPath: /config
resources:
requests:
memory: 512Mi
cpu: 200m
limits:
memory: 2Gi
cpu: "1"
livenessProbe:
httpGet:
path: /health
port: 8008
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8008
initialDelaySeconds: 30
periodSeconds: 5
volumes:
- name: synapse-data
persistentVolumeClaim:
claimName: synapse-data
- name: synapse-config-template
configMap:
name: synapse-config
- name: synapse-config-rendered
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: synapse
namespace: matrix
spec:
selector:
app: synapse
ports:
- port: 8008
targetPort: 8008
name: http
---
# Element Web ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: element-web-config
namespace: matrix
data:
config.json: |
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.iamworkin.lan",
"server_name": "iamworkin.lan"
}
},
"brand": "BlueJay Chat",
"disable_guests": true,
"disable_3pid_login": true
}
---
# Element Web Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: element-web
namespace: matrix
labels:
app: element-web
spec:
replicas: 1
selector:
matchLabels:
app: element-web
template:
metadata:
labels:
app: element-web
spec:
enableServiceLinks: false
containers:
- name: element-web
image: vectorim/element-web:latest
ports:
- containerPort: 80
name: http
volumeMounts:
- name: element-config
mountPath: /app/config.json
subPath: config.json
resources:
requests:
memory: 32Mi
cpu: 10m
limits:
memory: 128Mi
cpu: 100m
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: element-config
configMap:
name: element-web-config
---
apiVersion: v1
kind: Service
metadata:
name: element-web
namespace: matrix
spec:
selector:
app: element-web
ports:
- port: 80
targetPort: 80
name: http
---
# TLS Certificates via cert-manager
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: matrix-tls
namespace: matrix
spec:
secretName: matrix-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- matrix.iamworkin.lan
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: element-tls
namespace: matrix
spec:
secretName: element-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- element.iamworkin.lan
---
# Traefik IngressRoute - Synapse
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: synapse
namespace: matrix
spec:
entryPoints:
- websecure
routes:
- match: Host(`matrix.iamworkin.lan`)
kind: Rule
services:
- name: synapse
port: 8008
tls:
secretName: matrix-tls
---
# Traefik IngressRoute - Element Web
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: element-web
namespace: matrix
spec:
entryPoints:
- websecure
routes:
- match: Host(`element.iamworkin.lan`)
kind: Rule
services:
- name: element-web
port: 80
tls:
secretName: element-tls
---
# 1Password secret sync — creates matrix-credentials K8s Secret
# Fields: DB-User, DB-Password, Registration-Secret, username, password, URL
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: matrix-credentials
namespace: matrix
spec:
itemPath: vaults/IAmWorkin/items/Matrix Synapse
---
# Public IngressRoute - Element Web (flowercore.io with Cloudflare origin cert)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: element-public
namespace: matrix
spec:
entryPoints:
- websecure
routes:
- match: Host(`element.flowercore.io`)
kind: Rule
services:
- name: element-web
port: 80
tls:
secretName: cf-origin-flowercore-io
---
# Public IngressRoute - Synapse (flowercore.io with Cloudflare origin cert)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: synapse-public
namespace: matrix
spec:
entryPoints:
- websecure
routes:
- match: Host(`matrix.flowercore.io`)
kind: Rule
services:
- name: synapse
port: 8008
tls:
secretName: cf-origin-flowercore-io
# Matrix Synapse + Element Web
# PostgreSQL 16 + Synapse homeserver + Element Web client
# ArgoCD managed - BlueJay Lab
# DB credentials sourced from 1Password via OnePasswordItem CRD (matrix-credentials)
# Synapse homeserver.yaml DB password injected at runtime via init container
---
apiVersion: v1
kind: Namespace
metadata:
name: matrix
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# Synapse homeserver.yaml template ConfigMap
# DB password placeholder __DB_PASSWORD__ is replaced at pod startup by init container
apiVersion: v1
kind: ConfigMap
metadata:
name: synapse-config
namespace: matrix
data:
homeserver.yaml.template: |
server_name: "iamworkin.lan"
pid_file: /data/homeserver.pid
public_baseurl: "https://matrix.iamworkin.lan/"
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
bind_addresses: ["0.0.0.0"]
resources:
- names: [client, federation]
compress: false
database:
name: psycopg2
args:
user: __DB_USER__
password: __DB_PASSWORD__
database: synapse
host: matrix-postgres
port: 5432
cp_min: 5
cp_max: 10
log_config: "/config/log.config"
media_store_path: /data/media_store
registration_shared_secret: "a208f2e4b260f6b7d6ff4566df49c56c8b73fa20b911ce4e617b791ee7868adc"
report_stats: false
macaroon_secret_key: "9964f398e8b48a91469ad419d293c06db4562f49df8cc6e129fb3a801fd9052d"
form_secret: "7b0a9dbaf9ee94450e0b3271c408dfc4d313a55843ce4eec2ac1bb0315ffeb76"
signing_key_path: "/data/signing.key"
trusted_key_servers:
- server_name: "matrix.org"
enable_registration: false
suppress_key_server_warning: true
log.config: |
version: 1
formatters:
precise:
format: "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s"
handlers:
console:
class: logging.StreamHandler
formatter: precise
loggers:
synapse.storage.SQL:
level: WARNING
root:
level: WARNING
handlers: [console]
disable_existing_loggers: false
---
# PostgreSQL 16 StatefulSet
# Credentials from 1Password-synced matrix-credentials secret
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: matrix-postgres
namespace: matrix
labels:
app: matrix-postgres
spec:
serviceName: matrix-postgres
replicas: 1
selector:
matchLabels:
app: matrix-postgres
template:
metadata:
labels:
app: matrix-postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: matrix-credentials
key: DB-User
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: matrix-credentials
key: DB-Password
- name: POSTGRES_DB
value: synapse
- name: POSTGRES_INITDB_ARGS
value: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumeMounts:
- name: matrix-postgres-data
mountPath: /var/lib/postgresql/data
subPath: pgdata
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
cpu: 500m
livenessProbe:
exec:
command: ["pg_isready", "-U", "synapse"]
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command: ["pg_isready", "-U", "synapse"]
initialDelaySeconds: 5
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: matrix-postgres-data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
name: matrix-postgres
namespace: matrix
spec:
selector:
app: matrix-postgres
ports:
- port: 5432
targetPort: 5432
name: postgres
clusterIP: None
---
# Synapse Data PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: synapse-data
namespace: matrix
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 2Gi
---
# Synapse Deployment
# Init container injects DB credentials from 1Password secret into homeserver.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: synapse
namespace: matrix
labels:
app: synapse
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: synapse
template:
metadata:
labels:
app: synapse
spec:
initContainers:
- name: generate-signing-key
image: matrixdotorg/synapse:latest
securityContext:
runAsUser: 0
command: ["sh", "-c"]
args:
- |
if [ \! -f /data/signing.key ]; then
python -m synapse.app.homeserver --generate-keys --config-path /config-template/homeserver.yaml.template 2>/dev/null || true
# If key generation fails with template, create a minimal config for key gen
if [ \! -f /data/signing.key ]; then
echo server_name: iamworkin.lan > /tmp/minimal.yaml
echo signing_key_path: /data/signing.key >> /tmp/minimal.yaml
python -c "from signedjson.key import generate_signing_key, write_signing_keys; import sys; key = generate_signing_key(a_auto); write_signing_keys(open(/data/signing.key,w), [key])" 2>/dev/null || true
fi
fi
chown 991:991 /data/signing.key 2>/dev/null || true
chmod 644 /data/signing.key 2>/dev/null || true
mkdir -p /data/media_store
chown -R 991:991 /data 2>/dev/null || true
volumeMounts:
- name: synapse-data
mountPath: /data
- name: synapse-config-template
mountPath: /config-template
- name: inject-credentials
image: busybox:latest
command: ["sh", "-c"]
args:
- |
# Copy template and substitute DB credentials from 1Password secret
cp /config-template/log.config /config/log.config
sed -e "s/__DB_PASSWORD__/${DB_PASSWORD}/g" \
-e "s/__DB_USER__/${DB_USER}/g" \
/config-template/homeserver.yaml.template > /config/homeserver.yaml
echo "Credentials injected into homeserver.yaml"
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: matrix-credentials
key: DB-Password
- name: DB_USER
valueFrom:
secretKeyRef:
name: matrix-credentials
key: DB-User
volumeMounts:
- name: synapse-config-template
mountPath: /config-template
- name: synapse-config-rendered
mountPath: /config
containers:
- name: synapse
image: matrixdotorg/synapse:latest
ports:
- containerPort: 8008
name: http
env:
- name: SYNAPSE_CONFIG_DIR
value: /config
- name: SYNAPSE_CONFIG_PATH
value: /config/homeserver.yaml
volumeMounts:
- name: synapse-data
mountPath: /data
- name: synapse-config-rendered
mountPath: /config
resources:
requests:
memory: 512Mi
cpu: 200m
limits:
memory: 2Gi
cpu: "1"
livenessProbe:
httpGet:
path: /health
port: 8008
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8008
initialDelaySeconds: 30
periodSeconds: 5
volumes:
- name: synapse-data
persistentVolumeClaim:
claimName: synapse-data
- name: synapse-config-template
configMap:
name: synapse-config
- name: synapse-config-rendered
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: synapse
namespace: matrix
spec:
selector:
app: synapse
ports:
- port: 8008
targetPort: 8008
name: http
---
# Element Web ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: element-web-config
namespace: matrix
data:
config.json: |
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.iamworkin.lan",
"server_name": "iamworkin.lan"
}
},
"brand": "BlueJay Chat",
"disable_guests": true,
"disable_3pid_login": true
}
---
# Element Web Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: element-web
namespace: matrix
labels:
app: element-web
spec:
replicas: 1
selector:
matchLabels:
app: element-web
template:
metadata:
labels:
app: element-web
spec:
enableServiceLinks: false
containers:
- name: element-web
image: vectorim/element-web:latest
ports:
- containerPort: 80
name: http
volumeMounts:
- name: element-config
mountPath: /app/config.json
subPath: config.json
resources:
requests:
memory: 32Mi
cpu: 10m
limits:
memory: 128Mi
cpu: 100m
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: element-config
configMap:
name: element-web-config
---
apiVersion: v1
kind: Service
metadata:
name: element-web
namespace: matrix
spec:
selector:
app: element-web
ports:
- port: 80
targetPort: 80
name: http
---
# TLS Certificates via cert-manager
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: matrix-tls
namespace: matrix
spec:
secretName: matrix-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- matrix.iamworkin.lan
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: element-tls
namespace: matrix
spec:
secretName: element-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- element.iamworkin.lan
---
# Traefik IngressRoute - Synapse
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: synapse
namespace: matrix
spec:
entryPoints:
- websecure
routes:
- match: Host(`matrix.iamworkin.lan`)
kind: Rule
services:
- name: synapse
port: 8008
tls:
secretName: matrix-tls
---
# Traefik IngressRoute - Element Web
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: element-web
namespace: matrix
spec:
entryPoints:
- websecure
routes:
- match: Host(`element.iamworkin.lan`)
kind: Rule
services:
- name: element-web
port: 80
tls:
secretName: element-tls
---
# 1Password secret sync — creates matrix-credentials K8s Secret
# Fields: DB-User, DB-Password, Registration-Secret, username, password, URL
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: matrix-credentials
namespace: matrix
spec:
itemPath: vaults/IAmWorkin/items/Matrix Synapse
---
# Public IngressRoute - Element Web (flowercore.io with Cloudflare origin cert)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: element-public
namespace: matrix
spec:
entryPoints:
- websecure
routes:
- match: Host(`element.flowercore.io`)
kind: Rule
services:
- name: element-web
port: 80
tls:
secretName: cf-origin-flowercore-io
---
# Public IngressRoute - Synapse (flowercore.io with Cloudflare origin cert)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: synapse-public
namespace: matrix
spec:
entryPoints:
- websecure
routes:
- match: Host(`matrix.flowercore.io`)
kind: Rule
services:
- name: synapse
port: 8008
tls:
secretName: cf-origin-flowercore-io

View File

@@ -1,257 +1,257 @@
# NOC Services - Traefik IngressRoutes for noc1 services
# Proxies internal .iamworkin.lan hostnames to noc1 (10.0.56.10) via
# headless Service + manual Endpoints (standard K8s external proxy pattern)
# ArgoCD managed - BlueJay Lab
---
apiVersion: v1
kind: Namespace
metadata:
name: noc-proxy
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# ============================================================
# BasicAuth - shared across all NOC proxy IngressRoutes
# ============================================================
apiVersion: v1
kind: Secret
metadata:
name: noc-proxy-auth
namespace: noc-proxy
type: Opaque
data:
users: YWRtaW46JDJiJDEwJEZjdlVFNWNpNkxvNi5rZ1k5L3hJV2V5M2tvM3VVY1U5YXJaSlQ4N29ZREtCSi5lNkoucXJD
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: noc-proxy-auth
namespace: noc-proxy
spec:
basicAuth:
secret: noc-proxy-auth
---
# ============================================================
# Grafana - noc1:3000
# ============================================================
apiVersion: v1
kind: Service
metadata:
name: grafana-external
namespace: noc-proxy
spec:
ports:
- port: 3000
targetPort: 3000
name: http
clusterIP: None
---
apiVersion: v1
kind: Endpoints
metadata:
name: grafana-external
namespace: noc-proxy
subsets:
- addresses:
- ip: 10.0.56.10
ports:
- port: 3000
name: http
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: grafana-tls
namespace: noc-proxy
spec:
secretName: grafana-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- grafana.iamworkin.lan
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: grafana
namespace: noc-proxy
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`grafana.iamworkin.lan`)
middlewares:
- name: noc-proxy-auth
services:
- name: grafana-external
port: 3000
tls:
secretName: grafana-tls
---
# ============================================================
# Prometheus - noc1:9091
# ============================================================
apiVersion: v1
kind: Service
metadata:
name: prometheus-external
namespace: noc-proxy
spec:
ports:
- port: 9091
targetPort: 9091
name: http
clusterIP: None
---
apiVersion: v1
kind: Endpoints
metadata:
name: prometheus-external
namespace: noc-proxy
subsets:
- addresses:
- ip: 10.0.56.10
ports:
- port: 9091
name: http
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: prometheus-tls
namespace: noc-proxy
spec:
secretName: prometheus-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- prometheus.iamworkin.lan
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: prometheus
namespace: noc-proxy
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`prometheus.iamworkin.lan`)
middlewares:
- name: noc-proxy-auth
services:
- name: prometheus-external
port: 9091
tls:
secretName: prometheus-tls
---
# ============================================================
# Cockpit - noc1:9090
# ============================================================
apiVersion: v1
kind: Service
metadata:
name: cockpit-external
namespace: noc-proxy
spec:
ports:
- port: 9090
targetPort: 9090
name: https
clusterIP: None
---
apiVersion: v1
kind: Endpoints
metadata:
name: cockpit-external
namespace: noc-proxy
subsets:
- addresses:
- ip: 10.0.56.10
ports:
- port: 9090
name: https
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: cockpit-tls
namespace: noc-proxy
spec:
secretName: cockpit-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- cockpit.iamworkin.lan
---
# Cockpit uses self-signed HTTPS on 9090, so we need a ServersTransport
# to skip backend TLS verification
apiVersion: traefik.io/v1alpha1
kind: ServersTransport
metadata:
name: cockpit-transport
namespace: noc-proxy
spec:
insecureSkipVerify: true
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: cockpit
namespace: noc-proxy
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`cockpit.iamworkin.lan`)
middlewares:
- name: noc-proxy-auth
services:
- name: cockpit-external
port: 9090
serversTransport: cockpit-transport
tls:
secretName: cockpit-tls
---
# NetworkPolicy: allow Traefik ingress, allow egress to noc1
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: noc-proxy-netpol
namespace: noc-proxy
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik-system
egress:
- to:
- ipBlock:
cidr: 10.0.56.10/32
ports:
- port: 3000
protocol: TCP
- port: 9090
protocol: TCP
- port: 9091
protocol: TCP
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
# NOC Services - Traefik IngressRoutes for noc1 services
# Proxies internal .iamworkin.lan hostnames to noc1 (10.0.56.10) via
# headless Service + manual Endpoints (standard K8s external proxy pattern)
# ArgoCD managed - BlueJay Lab
---
apiVersion: v1
kind: Namespace
metadata:
name: noc-proxy
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# ============================================================
# BasicAuth - shared across all NOC proxy IngressRoutes
# ============================================================
apiVersion: v1
kind: Secret
metadata:
name: noc-proxy-auth
namespace: noc-proxy
type: Opaque
data:
users: YWRtaW46JDJiJDEwJEZjdlVFNWNpNkxvNi5rZ1k5L3hJV2V5M2tvM3VVY1U5YXJaSlQ4N29ZREtCSi5lNkoucXJD
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: noc-proxy-auth
namespace: noc-proxy
spec:
basicAuth:
secret: noc-proxy-auth
---
# ============================================================
# Grafana - noc1:3000
# ============================================================
apiVersion: v1
kind: Service
metadata:
name: grafana-external
namespace: noc-proxy
spec:
ports:
- port: 3000
targetPort: 3000
name: http
clusterIP: None
---
apiVersion: v1
kind: Endpoints
metadata:
name: grafana-external
namespace: noc-proxy
subsets:
- addresses:
- ip: 10.0.56.10
ports:
- port: 3000
name: http
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: grafana-tls
namespace: noc-proxy
spec:
secretName: grafana-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- grafana.iamworkin.lan
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: grafana
namespace: noc-proxy
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`grafana.iamworkin.lan`)
middlewares:
- name: noc-proxy-auth
services:
- name: grafana-external
port: 3000
tls:
secretName: grafana-tls
---
# ============================================================
# Prometheus - noc1:9091
# ============================================================
apiVersion: v1
kind: Service
metadata:
name: prometheus-external
namespace: noc-proxy
spec:
ports:
- port: 9091
targetPort: 9091
name: http
clusterIP: None
---
apiVersion: v1
kind: Endpoints
metadata:
name: prometheus-external
namespace: noc-proxy
subsets:
- addresses:
- ip: 10.0.56.10
ports:
- port: 9091
name: http
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: prometheus-tls
namespace: noc-proxy
spec:
secretName: prometheus-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- prometheus.iamworkin.lan
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: prometheus
namespace: noc-proxy
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`prometheus.iamworkin.lan`)
middlewares:
- name: noc-proxy-auth
services:
- name: prometheus-external
port: 9091
tls:
secretName: prometheus-tls
---
# ============================================================
# Cockpit - noc1:9090
# ============================================================
apiVersion: v1
kind: Service
metadata:
name: cockpit-external
namespace: noc-proxy
spec:
ports:
- port: 9090
targetPort: 9090
name: https
clusterIP: None
---
apiVersion: v1
kind: Endpoints
metadata:
name: cockpit-external
namespace: noc-proxy
subsets:
- addresses:
- ip: 10.0.56.10
ports:
- port: 9090
name: https
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: cockpit-tls
namespace: noc-proxy
spec:
secretName: cockpit-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- cockpit.iamworkin.lan
---
# Cockpit uses self-signed HTTPS on 9090, so we need a ServersTransport
# to skip backend TLS verification
apiVersion: traefik.io/v1alpha1
kind: ServersTransport
metadata:
name: cockpit-transport
namespace: noc-proxy
spec:
insecureSkipVerify: true
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: cockpit
namespace: noc-proxy
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`cockpit.iamworkin.lan`)
middlewares:
- name: noc-proxy-auth
services:
- name: cockpit-external
port: 9090
serversTransport: cockpit-transport
tls:
secretName: cockpit-tls
---
# NetworkPolicy: allow Traefik ingress, allow egress to noc1
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: noc-proxy-netpol
namespace: noc-proxy
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik-system
egress:
- to:
- ipBlock:
cidr: 10.0.56.10/32
ports:
- port: 3000
protocol: TCP
- port: 9090
protocol: TCP
- port: 9091
protocol: TCP
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP

View File

@@ -1,145 +1,145 @@
# TeamSpeak 3 Server
# ArgoCD managed - BlueJay Lab
---
apiVersion: v1
kind: Namespace
metadata:
name: teamspeak
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# 1Password secret sync - TeamSpeak credentials
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: teamspeak-credentials
namespace: teamspeak
spec:
itemPath: "vaults/IAmWorkin/items/TeamSpeak 3"
---
# TeamSpeak data PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: teamspeak-data
namespace: teamspeak
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
---
# TeamSpeak license key
apiVersion: v1
kind: Secret
metadata:
name: teamspeak-license
namespace: teamspeak
type: Opaque
data:
licensekey.dat: Y29tcGFueSBuYW1lIDogQW5kcmV3IFN0b2x0egphZGRyZXNzICAgICAgOiA2NjI1IDE2Mm5kIHQKemlwY29kZSAgICAgIDogNTUwNjgKY2l0eSAgICAgICAgIDogTGFrZXZpbGxlCmNvdW50cnkgICAgICA6IFVuaXRlZCBTdGF0ZXMgb2YgQW1lcmljYQpwaG9uZSAgICAgICAgOiA5NTI5OTk2NDExCmZheCAgICAgICAgICA6IApzYWxlcyBjb250YWN0OiBBbmRyZXcgU3RvbHR6IChhc3RvbHR6QGlhbXdvcmsuaW4pCnRlY2ggY29udGFjdCA6IEFuZHJldyBTdG9sdHogKGFzdG9sdHpAaWFtd29yay5pbikKCnRzIHZlcnNpb24gICA6IDMKdHlwZSAgICAgICAgIDogQWN0aXZhdGlvbiBMaWNlbnNlCnN0YXJ0IGRhdGUgICA6IFN1biBNYXIgIDggMDA6MDA6MDAgMjAyNgplbmQgZGF0ZSAgICAgOiBNb24gTWFyICA4IDAwOjAwOjAwIDIwMjcKbWF4LiB2aXJ0dWFsIHNlcnZlcnM6IDEKbWF4LiBzbG90cyAgIDogMzIKZGVzY3JpcHRpb24gIDogVGVhbVNwZWFrIDMgQUwKCgo9PWtleTI9PQpDb0VDQ3NnQkFRQ3ZiSEZUUURZL3RlclBlaWxycC9FQ1U5eENINVUzeEM5MmxZVE5hWS8wS1FBSkZ1ZUFhemJzZ0FBQUFDVlVaV0Z0VTNCbFlXc2dVM2x6ZEdWdGN5QkhiV0pJQUFCTnozMW0vRVdKU2w4QjFtcjJ2anZvdjRIL2s2UE9KVGJLb2NzVnoxRTNCUUFZd1c3S1B0R0k4QUFBQUNSVVpXRnRVM0JsWVdzZ2MzbHpkR1Z0Y3lCSGJXSklBQUFXSlVXQkErRjQzTERsMzFxS0NpOWw4WWdpQmhyOXlIWW1rbTFvWVVTMlhnSVl5cFVBR3F2SWdBWUFBQUFBUVc1a2NtVjNJRk4wYjJ4MGVnQVNJTmp2OStDVDQwTTUvQWdlY0lLcW1Gb2hyZnAzd3dSbGVSL25Ia3kvdHRKcEdDQWdBU29PVkdWaGJWTndaV0ZySURNZ1FVd1N0UUVCQUs5c2NWTkFOaisxNnM5NktXdW44UUpUM0VJZmxUZkVMM2FWaE0xcGovUXBBQWtXNTRCck51eUFBQUFBSlZSbFlXMVRjR1ZoYXlCVGVYTjBaVzF6SUVkdFlrZ0FBRTNQZldiOFJZbEtYd0hXYXZhK08raS9nZitUbzg0bE5zcWh5eFhQVVRjRkFCakJic28rMFlqd0FBQUFKRlJsWVcxVGNHVmhheUJ6ZVhOMFpXMXpJRWR0WWtnQUFGTTFQT2YxY0VzclhjckFtMU9wS1RnN2cyaHlVNlROY093TFVJaXJpbnc4QlJqQmJzbyswWWp3R2tBRFpWRFRLVndEQTdqYWVFK0pqRFQ1WUFJc1hScWxpdlpTeTR1aUJSYlB0RUZ3d0VnVXR2RHg4TEJaQ29zOEhPTDI0bllBalZ0UFRCdHJnRWF0NHVBSgo9PWtleTI9PQoKPT1rZXk9PQpWRk16VEdsalpXNXpaV1l3WkFJd1JCL1VCTWQ0MnRnQmg4TkY0dnN4K2lsTmpFZHQzazdxbXZPamRYZUVtUWFjQ0J5S0tnelZ4T2MwMDlYanIrU09BakFjR2oxV1ZwTGpiNGxGYk1DaEpsU1FXZW5zYTJhNmJXK2JoN2lCdzF1Zk5WaXFnTk41YThpN1VFS29sNmhsSmExVFViQkVpTng2T1NjbHBBOENhNDhjVk5zS050N0wvandDaVhuZFBkV3BuUW5CdUlYeXFLUXkxM2ZsNDJWUWo0Rk90V2kzTHRoYkNpODlWWEQwYXRpNTNjQlRTWHN4QXdMUndzZFBqeWFobmFsNStXaiswUFdYOE4ySlEzMFZmalFQVnAyMFk2dmc0K29lcm1vV291QUc5RDFjQzRrQVJoQnlVRmU5VnUvM2VBTVRiNUlHbnllY1k0QUF0RjJmdTR1aG5NYUFYQ3Q3UWNHN1JSVEFLQnNjSVhyVlVLM1NnS3ZLR1p6T2RGeU0=
---
# TeamSpeak 3 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: teamspeak
namespace: teamspeak
labels:
app: teamspeak
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: teamspeak
template:
metadata:
labels:
app: teamspeak
spec:
initContainers:
- name: copy-license
image: busybox:latest
command: ['sh', '-c', 'cp /license/licensekey.dat /data/licensekey.dat']
volumeMounts:
- name: teamspeak-data
mountPath: /data
- name: license
mountPath: /license
readOnly: true
containers:
- name: teamspeak
image: teamspeak:latest
ports:
- containerPort: 9987
name: voice
protocol: UDP
- containerPort: 30033
name: filetransfer
protocol: TCP
- containerPort: 10011
name: serverquery
protocol: TCP
env:
- name: TS3SERVER_LICENSE
value: accept
- name: TS3SERVER_SERVERADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: teamspeak-credentials
key: ServerQuery-Password
volumeMounts:
- name: teamspeak-data
mountPath: /var/ts3server
resources:
requests:
memory: 128Mi
cpu: 50m
limits:
memory: 512Mi
cpu: 500m
readinessProbe:
tcpSocket:
port: 10011
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 10011
initialDelaySeconds: 60
periodSeconds: 15
volumes:
- name: teamspeak-data
persistentVolumeClaim:
claimName: teamspeak-data
- name: license
secret:
secretName: teamspeak-license
---
# TeamSpeak LoadBalancer Service
apiVersion: v1
kind: Service
metadata:
name: teamspeak
namespace: teamspeak
annotations:
metallb.universe.tf/loadBalancerIPs: 10.0.56.205
spec:
type: LoadBalancer
selector:
app: teamspeak
ports:
- port: 9987
targetPort: 9987
name: voice
protocol: UDP
- port: 30033
targetPort: 30033
name: filetransfer
protocol: TCP
- port: 10011
targetPort: 10011
name: serverquery
protocol: TCP
# TeamSpeak 3 Server
# ArgoCD managed - BlueJay Lab
---
apiVersion: v1
kind: Namespace
metadata:
name: teamspeak
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# 1Password secret sync - TeamSpeak credentials
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: teamspeak-credentials
namespace: teamspeak
spec:
itemPath: "vaults/IAmWorkin/items/TeamSpeak 3"
---
# TeamSpeak data PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: teamspeak-data
namespace: teamspeak
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
---
# TeamSpeak license key
apiVersion: v1
kind: Secret
metadata:
name: teamspeak-license
namespace: teamspeak
type: Opaque
data:
licensekey.dat: Y29tcGFueSBuYW1lIDogQW5kcmV3IFN0b2x0egphZGRyZXNzICAgICAgOiA2NjI1IDE2Mm5kIHQKemlwY29kZSAgICAgIDogNTUwNjgKY2l0eSAgICAgICAgIDogTGFrZXZpbGxlCmNvdW50cnkgICAgICA6IFVuaXRlZCBTdGF0ZXMgb2YgQW1lcmljYQpwaG9uZSAgICAgICAgOiA5NTI5OTk2NDExCmZheCAgICAgICAgICA6IApzYWxlcyBjb250YWN0OiBBbmRyZXcgU3RvbHR6IChhc3RvbHR6QGlhbXdvcmsuaW4pCnRlY2ggY29udGFjdCA6IEFuZHJldyBTdG9sdHogKGFzdG9sdHpAaWFtd29yay5pbikKCnRzIHZlcnNpb24gICA6IDMKdHlwZSAgICAgICAgIDogQWN0aXZhdGlvbiBMaWNlbnNlCnN0YXJ0IGRhdGUgICA6IFN1biBNYXIgIDggMDA6MDA6MDAgMjAyNgplbmQgZGF0ZSAgICAgOiBNb24gTWFyICA4IDAwOjAwOjAwIDIwMjcKbWF4LiB2aXJ0dWFsIHNlcnZlcnM6IDEKbWF4LiBzbG90cyAgIDogMzIKZGVzY3JpcHRpb24gIDogVGVhbVNwZWFrIDMgQUwKCgo9PWtleTI9PQpDb0VDQ3NnQkFRQ3ZiSEZUUURZL3RlclBlaWxycC9FQ1U5eENINVUzeEM5MmxZVE5hWS8wS1FBSkZ1ZUFhemJzZ0FBQUFDVlVaV0Z0VTNCbFlXc2dVM2x6ZEdWdGN5QkhiV0pJQUFCTnozMW0vRVdKU2w4QjFtcjJ2anZvdjRIL2s2UE9KVGJLb2NzVnoxRTNCUUFZd1c3S1B0R0k4QUFBQUNSVVpXRnRVM0JsWVdzZ2MzbHpkR1Z0Y3lCSGJXSklBQUFXSlVXQkErRjQzTERsMzFxS0NpOWw4WWdpQmhyOXlIWW1rbTFvWVVTMlhnSVl5cFVBR3F2SWdBWUFBQUFBUVc1a2NtVjNJRk4wYjJ4MGVnQVNJTmp2OStDVDQwTTUvQWdlY0lLcW1Gb2hyZnAzd3dSbGVSL25Ia3kvdHRKcEdDQWdBU29PVkdWaGJWTndaV0ZySURNZ1FVd1N0UUVCQUs5c2NWTkFOaisxNnM5NktXdW44UUpUM0VJZmxUZkVMM2FWaE0xcGovUXBBQWtXNTRCck51eUFBQUFBSlZSbFlXMVRjR1ZoYXlCVGVYTjBaVzF6SUVkdFlrZ0FBRTNQZldiOFJZbEtYd0hXYXZhK08raS9nZitUbzg0bE5zcWh5eFhQVVRjRkFCakJic28rMFlqd0FBQUFKRlJsWVcxVGNHVmhheUJ6ZVhOMFpXMXpJRWR0WWtnQUFGTTFQT2YxY0VzclhjckFtMU9wS1RnN2cyaHlVNlROY093TFVJaXJpbnc4QlJqQmJzbyswWWp3R2tBRFpWRFRLVndEQTdqYWVFK0pqRFQ1WUFJc1hScWxpdlpTeTR1aUJSYlB0RUZ3d0VnVXR2RHg4TEJaQ29zOEhPTDI0bllBalZ0UFRCdHJnRWF0NHVBSgo9PWtleTI9PQoKPT1rZXk9PQpWRk16VEdsalpXNXpaV1l3WkFJd1JCL1VCTWQ0MnRnQmg4TkY0dnN4K2lsTmpFZHQzazdxbXZPamRYZUVtUWFjQ0J5S0tnelZ4T2MwMDlYanIrU09BakFjR2oxV1ZwTGpiNGxGYk1DaEpsU1FXZW5zYTJhNmJXK2JoN2lCdzF1Zk5WaXFnTk41YThpN1VFS29sNmhsSmExVFViQkVpTng2T1NjbHBBOENhNDhjVk5zS050N0wvandDaVhuZFBkV3BuUW5CdUlYeXFLUXkxM2ZsNDJWUWo0Rk90V2kzTHRoYkNpODlWWEQwYXRpNTNjQlRTWHN4QXdMUndzZFBqeWFobmFsNStXaiswUFdYOE4ySlEzMFZmalFQVnAyMFk2dmc0K29lcm1vV291QUc5RDFjQzRrQVJoQnlVRmU5VnUvM2VBTVRiNUlHbnllY1k0QUF0RjJmdTR1aG5NYUFYQ3Q3UWNHN1JSVEFLQnNjSVhyVlVLM1NnS3ZLR1p6T2RGeU0=
---
# TeamSpeak 3 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: teamspeak
namespace: teamspeak
labels:
app: teamspeak
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: teamspeak
template:
metadata:
labels:
app: teamspeak
spec:
initContainers:
- name: copy-license
image: busybox:latest
command: ['sh', '-c', 'cp /license/licensekey.dat /data/licensekey.dat']
volumeMounts:
- name: teamspeak-data
mountPath: /data
- name: license
mountPath: /license
readOnly: true
containers:
- name: teamspeak
image: teamspeak:latest
ports:
- containerPort: 9987
name: voice
protocol: UDP
- containerPort: 30033
name: filetransfer
protocol: TCP
- containerPort: 10011
name: serverquery
protocol: TCP
env:
- name: TS3SERVER_LICENSE
value: accept
- name: TS3SERVER_SERVERADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: teamspeak-credentials
key: ServerQuery-Password
volumeMounts:
- name: teamspeak-data
mountPath: /var/ts3server
resources:
requests:
memory: 128Mi
cpu: 50m
limits:
memory: 512Mi
cpu: 500m
readinessProbe:
tcpSocket:
port: 10011
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 10011
initialDelaySeconds: 60
periodSeconds: 15
volumes:
- name: teamspeak-data
persistentVolumeClaim:
claimName: teamspeak-data
- name: license
secret:
secretName: teamspeak-license
---
# TeamSpeak LoadBalancer Service
apiVersion: v1
kind: Service
metadata:
name: teamspeak
namespace: teamspeak
annotations:
metallb.universe.tf/loadBalancerIPs: 10.0.56.205
spec:
type: LoadBalancer
selector:
app: teamspeak
ports:
- port: 9987
targetPort: 9987
name: voice
protocol: UDP
- port: 30033
targetPort: 30033
name: filetransfer
protocol: TCP
- port: 10011
targetPort: 10011
name: serverquery
protocol: TCP

View File

@@ -1,127 +1,127 @@
# Twilio Voice Bridge - Traefik ingress to edge1
# ArgoCD managed - BlueJay Lab
# Routes voice.bluejay.dev (TwiML) and voice-ws.bluejay.dev (WebSocket)
# to edge1 Pi5 at 10.0.57.15 (PROD VLAN)
---
apiVersion: v1
kind: Namespace
metadata:
name: voice
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# Cloudflare origin cert for *.bluejay.dev
apiVersion: v1
kind: Secret
metadata:
name: cf-origin-bluejay-dev
namespace: voice
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVvakNDQTRxZ0F3SUJBZ0lVTkxnemZ4UVRzMElyWWRaZUZKUGN5TjRyNmFjd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZc3hDekFKQmdOVkJBWVRBbFZUTVJrd0Z3WURWUVFLRXhCRGJHOTFaRVpzWVhKbExDQkpibU11TVRRdwpNZ1lEVlFRTEV5dERiRzkxWkVac1lYSmxJRTl5YVdkcGJpQlRVMHdnUTJWeWRHbG1hV05oZEdVZ1FYVjBhRzl5CmFYUjVNUll3RkFZRFZRUUhFdzFUWVc0Z1JuSmhibU5wYzJOdk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGgKTUI0WERUSTJNRE14TURBMU1USXdNRm9YRFRReE1ETXdOakExTVRJd01Gb3dZakVaTUJjR0ExVUVDaE1RUTJ4dgpkV1JHYkdGeVpTd2dTVzVqTGpFZE1Cc0dBMVVFQ3hNVVEyeHZkV1JHYkdGeVpTQlBjbWxuYVc0Z1EwRXhKakFrCkJnTlZCQU1USFVOc2IzVmtSbXhoY21VZ1QzSnBaMmx1SUVObGNuUnBabWxqWVhSbE1JSUJJakFOQmdrcWhraUcKOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXlPODBJQ3dPV0RWTEpQNm9RenI3aXVENmtWMGNWZ01VbUx5VApKVnVYUlhZSEY3M2ZrM2pPbytCQVE1M2pmbERHUFVYc0UvNlV6VDRoUDVYTlVLaUNXaitvYy84eE1BSWsxcWwrClZnbFI1MDBUQ0FtZDliazNZVkxiZjBSejVMMUQ0WGJmOEVzamhOUVV2Z3Y0dTZoQzdnRmdrVGplc1dIZjg0K04KNERETDdmTjFQZHR4RVBiVWZrbGN1MUZSdXdlMk9QNkFEMlJvdkphNWZwODRHcVY2TDAzdjY2RjFtMnBST1VmRwpFdWpRNG4zSms2cUx5NHZTTENzOGJlOGRBRW5QcDgyZ2NRZk9mUVlIS2JTUWhiQnMwK01vK3lkTHpHSzFRdklRCnVPcDZRT1BtM0lac09Eb0VCdG5kMTh2amx6Y1JSdE94cjNzaFovdmNWY0o0YUJNZDVRSURBUUFCbzRJQkpEQ0MKQVNBd0RnWURWUjBQQVFIL0JBUURBZ1dnTUIwR0ExVWRKUVFXTUJRR0NDc0dBUVVGQndNQ0JnZ3JCZ0VGQlFjRApBVEFNQmdOVkhSTUJBZjhFQWpBQU1CMEdBMVVkRGdRV0JCVHgyYmFCUGlvWjZUd2U3THhnaTViUS82cUFkakFmCkJnTlZIU01FR0RBV2dCUWs2Rk5YWFh3MFFJZXA2NVRidXVFV2VQd3BwREJBQmdnckJnRUZCUWNCQVFRME1ESXcKTUFZSUt3WUJCUVVITUFHR0pHaDBkSEE2THk5dlkzTndMbU5zYjNWa1pteGhjbVV1WTI5dEwyOXlhV2RwYmw5agpZVEFsQmdOVkhSRUVIakFjZ2cwcUxtSnNkV1ZxWVhrdVpHVjJnZ3RpYkhWbGFtRjVMbVJsZGpBNEJnTlZIUjhFCk1UQXZNQzJnSzZBcGhpZG9kSFJ3T2k4dlkzSnNMbU5zYjNWa1pteGhjbVV1WTI5dEwyOXlhV2RwYmw5allTNWoKY213d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFDS0ZVRXhYbFB5KzlPRytJc1VWN1NXS09Udkk0b2JrUkd6bwpOckhudWZ3dGtxa0dzUGErUU5LbUk1UVNaYTVMS1YybWZJdWsrNE12U1FvZklmbUFUb1JEWEdQK041aWxEbWRSCk5rS2ttSUpZL242UzM3MGdZN0JQMjFwNjJKUnZkVUU5ZmV5RU1iMUdNbGNINjN6MUQxMzZZOWlvQ1FnYWNFZVUKOFZSVWFPZkJvby9sVzlYbXA1ZDZzcTBic2tybUhRN1ZSTjUxZCtsL0RvY2lkU2xZcHQxbXljSUN3c1F4U0dpMApYN1pMTXhHdCtDVG9jcFRFbkdrQ2t0NnhrKzVJUElXaHYvVnZuTnlQNUwxM0ZRN1d4QzFsaUIwcVdKMkEwcWpoCkR2cmxPNUpsclNWWEtNc0hwSWRFN3pJZ29JUUc2cnU1N1V4UjJyeko0d0VrSXcrd1ZyMD0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRREk3elFnTEE1WU5Vc2sKL3FoRE92dUs0UHFSWFJ4V0F4U1l2Sk1sVzVkRmRnY1h2ZCtUZU02ajRFQkRuZU4rVU1ZOVJld1QvcFROUGlFLwpsYzFRcUlKYVA2aHovekV3QWlUV3FYNVdDVkhuVFJNSUNaMzF1VGRoVXR0L1JIUGt2VVBoZHQvd1N5T0UxQlMrCkMvaTdxRUx1QVdDUk9ONnhZZC96ajQzZ01NdnQ4M1U5MjNFUTl0UitTVnk3VVZHN0I3WTQvb0FQWkdpOGxybCsKbnpnYXBYb3ZUZS9yb1hXYmFsRTVSOFlTNk5EaWZjbVRxb3ZMaTlJc0t6eHQ3eDBBU2MrbnphQnhCODU5QmdjcAp0SkNGc0d6VDR5ajdKMHZNWXJWQzhoQzQ2bnBBNCtiY2htdzRPZ1FHMmQzWHkrT1hOeEZHMDdHdmV5Rm4rOXhWCnduaG9FeDNsQWdNQkFBRUNnZ0VBRFpWeU9peVFTYkZNbzdNZGovSDRXR0t1UGM2RUlHSno3WUZ1Rnl2eWRZMHQKbkpMRy94Sy9NWC96Q0Q4dnhuWFNlUWoxbFVKMEw4M2Y5SXI5aHRMbGdSRmxvM1hnanVUT05iN2VuaFZpTnBkVQp6b25MNW5VL2c3SlV5VzFJd25GekdkWnQvREl3TkFZY1l0NnZVWXhsL2U0VTU2eG5EYW5XdUlIL2J1VU5uRWZtCjZNcnlIblhseDA0TzZzOElLSCtXNUxDam1ma1Jac0VveE9Damt6T2hucmdCTk4xSWxWSWhhMDhLOXBxRk5qM0wKaWd4MVNYZDhqaHNZUHJxaDNkak0rVUNKSitKMURuMjhaUmoyZUtqWDMvZmFDbGFpOEFzUVBtQTJScjZYZ3kzeApaR090dzRPL2Nxb1hnOUFDU05NanRNK3VtaXM3QzNncWp6VUZ4MWwvNFFLQmdRRFNvSGVDY1dXSUJQV3grR01hCnBSMENHejQ0eDNPRUhnNWYza2RvK3BDK0I3RnYxREV5bktqUUY4VFBYaWJXVGZVL1pBRG5DVmIvU1Z2L1QyNnkKT2NlaW44UGRqdXkraE05ZUxjWXU4QVZiK0VKOHFMUUsrZE5QZm1nYXZJMWR2V3U2Tm8wSTNSSzZWUkpiQXEyWgpLcjdsVEloMjdZMDRMajlaaUIxU01YZ0J0UUtCZ1FEME9EbEVCNHRHa1l2dTVLMkFoaDBmdnlIaUVoUTd5dWUzCmZXdG1VYnRxV0ZlM1R5UG9JTWJWMTM3MkNIdlh6MFNEMXRwYzJxa3hxN3c4d1BYM3RJZGk5NWxMbVMzZm8zWFoKdTNTYVo4enozTlAwR3JXK1Y0c3hHb1VnbWgwL2lzOXJoaWxXZGF5bU5SWEQ5U0MyUU5IdTU5NDY2UjFkczNnbgpiZlJlUkw4SmNRS0JnREptVjNLTk0rQmlYM0Jnb1VaRThEWUswczYvV3pMb0JrU0dhY3dDK1JPZnY2T2t3TWo5Cmw1K0RzSUoyWXhDd3d0aVNVMnoxWFMzbEhmQnZ6MnN5VEVUcnVmQ1FQTEl5RVhUVnV6Q01HcHd4UWFlV3JzNVoKaldqZU5JY0JTMHA5QXdRaC9ZbDdiUG5OVllFVm1QaW5zOW9tZ0JrRkt0K2dvV1FKSUFzRTcxUnBBb0dCQUxNSgphTW43c2RuNUo1bnA0VnhBZGFkcGFvQ2VlbURmUG9KaEd0UTNCT3RRZW5Xek9nS1p6TXJHSVpoaTNjOTNicVlzClk0Y0E4bHFzcU9IdElDVUpIdHVwNHFMdVdCZ0VjSWcvaVpzTWo4OFRTL3MvZlk5ZUJIZnFGa0N4V3RIVGhINHkKSzZucnVMZGNZV2w0RWhRcWJ2enkxUk5oQkp0Rno4Y3dMNTdRVFRDeEFvR0FjZFFkbzk3bzZmS0dQa3kvREpnNApoTGlLbHVkTjF3bmJaTHFVM0UwNzBwVDhJQkx3TFNpTUNpYXRXWWtScHFpdUQyMSsya1p4SE1NdnNoZWJXQmFmCmVqWU9jcHVvQ3VWdWc1K1dlbmc0cmUxSTl2c2czUE5WYkdrNW1xNmZDbndsbGFnMkEvSVBVRFFOaUpCVGM1WUQKc25udzhVTjNBTFBMSTlPVVd1eXJUckk9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
---
# Service (no selector) pointing to external edge1
apiVersion: v1
kind: Service
metadata:
name: voice-bridge
namespace: voice
spec:
ports:
- name: twiml
port: 8766
targetPort: 8766
protocol: TCP
- name: websocket
port: 8765
targetPort: 8765
protocol: TCP
---
# Manual Endpoints for edge1 (outside K8s)
apiVersion: v1
kind: Endpoints
metadata:
name: voice-bridge
namespace: voice
subsets:
- addresses:
- ip: 10.0.57.15
ports:
- name: twiml
port: 8766
protocol: TCP
- name: websocket
port: 8765
protocol: TCP
---
# TwiML webhook: voice.bluejay.dev -> edge1:8766
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: voice-twiml
namespace: voice
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`voice.bluejay.dev`)
services:
- name: voice-bridge
port: 8766
tls:
secretName: cf-origin-bluejay-dev
---
# WebSocket media stream: voice-ws.bluejay.dev -> edge1:8765
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: voice-ws
namespace: voice
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`voice-ws.bluejay.dev`)
services:
- name: voice-bridge
port: 8765
tls:
secretName: cf-origin-bluejay-dev
---
# NetworkPolicy: allow Traefik ingress only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: voice-netpol
namespace: voice
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik-system
egress:
- to:
- ipBlock:
cidr: 10.0.57.15/32
ports:
- port: 8765
protocol: TCP
- port: 8766
protocol: TCP
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
# Twilio Voice Bridge - Traefik ingress to edge1
# ArgoCD managed - BlueJay Lab
# Routes voice.bluejay.dev (TwiML) and voice-ws.bluejay.dev (WebSocket)
# to edge1 Pi5 at 10.0.57.15 (PROD VLAN)
---
apiVersion: v1
kind: Namespace
metadata:
name: voice
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# Cloudflare origin cert for *.bluejay.dev
apiVersion: v1
kind: Secret
metadata:
name: cf-origin-bluejay-dev
namespace: voice
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVvakNDQTRxZ0F3SUJBZ0lVTkxnemZ4UVRzMElyWWRaZUZKUGN5TjRyNmFjd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZc3hDekFKQmdOVkJBWVRBbFZUTVJrd0Z3WURWUVFLRXhCRGJHOTFaRVpzWVhKbExDQkpibU11TVRRdwpNZ1lEVlFRTEV5dERiRzkxWkVac1lYSmxJRTl5YVdkcGJpQlRVMHdnUTJWeWRHbG1hV05oZEdVZ1FYVjBhRzl5CmFYUjVNUll3RkFZRFZRUUhFdzFUWVc0Z1JuSmhibU5wYzJOdk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGgKTUI0WERUSTJNRE14TURBMU1USXdNRm9YRFRReE1ETXdOakExTVRJd01Gb3dZakVaTUJjR0ExVUVDaE1RUTJ4dgpkV1JHYkdGeVpTd2dTVzVqTGpFZE1Cc0dBMVVFQ3hNVVEyeHZkV1JHYkdGeVpTQlBjbWxuYVc0Z1EwRXhKakFrCkJnTlZCQU1USFVOc2IzVmtSbXhoY21VZ1QzSnBaMmx1SUVObGNuUnBabWxqWVhSbE1JSUJJakFOQmdrcWhraUcKOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXlPODBJQ3dPV0RWTEpQNm9RenI3aXVENmtWMGNWZ01VbUx5VApKVnVYUlhZSEY3M2ZrM2pPbytCQVE1M2pmbERHUFVYc0UvNlV6VDRoUDVYTlVLaUNXaitvYy84eE1BSWsxcWwrClZnbFI1MDBUQ0FtZDliazNZVkxiZjBSejVMMUQ0WGJmOEVzamhOUVV2Z3Y0dTZoQzdnRmdrVGplc1dIZjg0K04KNERETDdmTjFQZHR4RVBiVWZrbGN1MUZSdXdlMk9QNkFEMlJvdkphNWZwODRHcVY2TDAzdjY2RjFtMnBST1VmRwpFdWpRNG4zSms2cUx5NHZTTENzOGJlOGRBRW5QcDgyZ2NRZk9mUVlIS2JTUWhiQnMwK01vK3lkTHpHSzFRdklRCnVPcDZRT1BtM0lac09Eb0VCdG5kMTh2amx6Y1JSdE94cjNzaFovdmNWY0o0YUJNZDVRSURBUUFCbzRJQkpEQ0MKQVNBd0RnWURWUjBQQVFIL0JBUURBZ1dnTUIwR0ExVWRKUVFXTUJRR0NDc0dBUVVGQndNQ0JnZ3JCZ0VGQlFjRApBVEFNQmdOVkhSTUJBZjhFQWpBQU1CMEdBMVVkRGdRV0JCVHgyYmFCUGlvWjZUd2U3THhnaTViUS82cUFkakFmCkJnTlZIU01FR0RBV2dCUWs2Rk5YWFh3MFFJZXA2NVRidXVFV2VQd3BwREJBQmdnckJnRUZCUWNCQVFRME1ESXcKTUFZSUt3WUJCUVVITUFHR0pHaDBkSEE2THk5dlkzTndMbU5zYjNWa1pteGhjbVV1WTI5dEwyOXlhV2RwYmw5agpZVEFsQmdOVkhSRUVIakFjZ2cwcUxtSnNkV1ZxWVhrdVpHVjJnZ3RpYkhWbGFtRjVMbVJsZGpBNEJnTlZIUjhFCk1UQXZNQzJnSzZBcGhpZG9kSFJ3T2k4dlkzSnNMbU5zYjNWa1pteGhjbVV1WTI5dEwyOXlhV2RwYmw5allTNWoKY213d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFDS0ZVRXhYbFB5KzlPRytJc1VWN1NXS09Udkk0b2JrUkd6bwpOckhudWZ3dGtxa0dzUGErUU5LbUk1UVNaYTVMS1YybWZJdWsrNE12U1FvZklmbUFUb1JEWEdQK041aWxEbWRSCk5rS2ttSUpZL242UzM3MGdZN0JQMjFwNjJKUnZkVUU5ZmV5RU1iMUdNbGNINjN6MUQxMzZZOWlvQ1FnYWNFZVUKOFZSVWFPZkJvby9sVzlYbXA1ZDZzcTBic2tybUhRN1ZSTjUxZCtsL0RvY2lkU2xZcHQxbXljSUN3c1F4U0dpMApYN1pMTXhHdCtDVG9jcFRFbkdrQ2t0NnhrKzVJUElXaHYvVnZuTnlQNUwxM0ZRN1d4QzFsaUIwcVdKMkEwcWpoCkR2cmxPNUpsclNWWEtNc0hwSWRFN3pJZ29JUUc2cnU1N1V4UjJyeko0d0VrSXcrd1ZyMD0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRREk3elFnTEE1WU5Vc2sKL3FoRE92dUs0UHFSWFJ4V0F4U1l2Sk1sVzVkRmRnY1h2ZCtUZU02ajRFQkRuZU4rVU1ZOVJld1QvcFROUGlFLwpsYzFRcUlKYVA2aHovekV3QWlUV3FYNVdDVkhuVFJNSUNaMzF1VGRoVXR0L1JIUGt2VVBoZHQvd1N5T0UxQlMrCkMvaTdxRUx1QVdDUk9ONnhZZC96ajQzZ01NdnQ4M1U5MjNFUTl0UitTVnk3VVZHN0I3WTQvb0FQWkdpOGxybCsKbnpnYXBYb3ZUZS9yb1hXYmFsRTVSOFlTNk5EaWZjbVRxb3ZMaTlJc0t6eHQ3eDBBU2MrbnphQnhCODU5QmdjcAp0SkNGc0d6VDR5ajdKMHZNWXJWQzhoQzQ2bnBBNCtiY2htdzRPZ1FHMmQzWHkrT1hOeEZHMDdHdmV5Rm4rOXhWCnduaG9FeDNsQWdNQkFBRUNnZ0VBRFpWeU9peVFTYkZNbzdNZGovSDRXR0t1UGM2RUlHSno3WUZ1Rnl2eWRZMHQKbkpMRy94Sy9NWC96Q0Q4dnhuWFNlUWoxbFVKMEw4M2Y5SXI5aHRMbGdSRmxvM1hnanVUT05iN2VuaFZpTnBkVQp6b25MNW5VL2c3SlV5VzFJd25GekdkWnQvREl3TkFZY1l0NnZVWXhsL2U0VTU2eG5EYW5XdUlIL2J1VU5uRWZtCjZNcnlIblhseDA0TzZzOElLSCtXNUxDam1ma1Jac0VveE9Damt6T2hucmdCTk4xSWxWSWhhMDhLOXBxRk5qM0wKaWd4MVNYZDhqaHNZUHJxaDNkak0rVUNKSitKMURuMjhaUmoyZUtqWDMvZmFDbGFpOEFzUVBtQTJScjZYZ3kzeApaR090dzRPL2Nxb1hnOUFDU05NanRNK3VtaXM3QzNncWp6VUZ4MWwvNFFLQmdRRFNvSGVDY1dXSUJQV3grR01hCnBSMENHejQ0eDNPRUhnNWYza2RvK3BDK0I3RnYxREV5bktqUUY4VFBYaWJXVGZVL1pBRG5DVmIvU1Z2L1QyNnkKT2NlaW44UGRqdXkraE05ZUxjWXU4QVZiK0VKOHFMUUsrZE5QZm1nYXZJMWR2V3U2Tm8wSTNSSzZWUkpiQXEyWgpLcjdsVEloMjdZMDRMajlaaUIxU01YZ0J0UUtCZ1FEME9EbEVCNHRHa1l2dTVLMkFoaDBmdnlIaUVoUTd5dWUzCmZXdG1VYnRxV0ZlM1R5UG9JTWJWMTM3MkNIdlh6MFNEMXRwYzJxa3hxN3c4d1BYM3RJZGk5NWxMbVMzZm8zWFoKdTNTYVo4enozTlAwR3JXK1Y0c3hHb1VnbWgwL2lzOXJoaWxXZGF5bU5SWEQ5U0MyUU5IdTU5NDY2UjFkczNnbgpiZlJlUkw4SmNRS0JnREptVjNLTk0rQmlYM0Jnb1VaRThEWUswczYvV3pMb0JrU0dhY3dDK1JPZnY2T2t3TWo5Cmw1K0RzSUoyWXhDd3d0aVNVMnoxWFMzbEhmQnZ6MnN5VEVUcnVmQ1FQTEl5RVhUVnV6Q01HcHd4UWFlV3JzNVoKaldqZU5JY0JTMHA5QXdRaC9ZbDdiUG5OVllFVm1QaW5zOW9tZ0JrRkt0K2dvV1FKSUFzRTcxUnBBb0dCQUxNSgphTW43c2RuNUo1bnA0VnhBZGFkcGFvQ2VlbURmUG9KaEd0UTNCT3RRZW5Xek9nS1p6TXJHSVpoaTNjOTNicVlzClk0Y0E4bHFzcU9IdElDVUpIdHVwNHFMdVdCZ0VjSWcvaVpzTWo4OFRTL3MvZlk5ZUJIZnFGa0N4V3RIVGhINHkKSzZucnVMZGNZV2w0RWhRcWJ2enkxUk5oQkp0Rno4Y3dMNTdRVFRDeEFvR0FjZFFkbzk3bzZmS0dQa3kvREpnNApoTGlLbHVkTjF3bmJaTHFVM0UwNzBwVDhJQkx3TFNpTUNpYXRXWWtScHFpdUQyMSsya1p4SE1NdnNoZWJXQmFmCmVqWU9jcHVvQ3VWdWc1K1dlbmc0cmUxSTl2c2czUE5WYkdrNW1xNmZDbndsbGFnMkEvSVBVRFFOaUpCVGM1WUQKc25udzhVTjNBTFBMSTlPVVd1eXJUckk9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
---
# Service (no selector) pointing to external edge1
apiVersion: v1
kind: Service
metadata:
name: voice-bridge
namespace: voice
spec:
ports:
- name: twiml
port: 8766
targetPort: 8766
protocol: TCP
- name: websocket
port: 8765
targetPort: 8765
protocol: TCP
---
# Manual Endpoints for edge1 (outside K8s)
apiVersion: v1
kind: Endpoints
metadata:
name: voice-bridge
namespace: voice
subsets:
- addresses:
- ip: 10.0.57.15
ports:
- name: twiml
port: 8766
protocol: TCP
- name: websocket
port: 8765
protocol: TCP
---
# TwiML webhook: voice.bluejay.dev -> edge1:8766
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: voice-twiml
namespace: voice
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`voice.bluejay.dev`)
services:
- name: voice-bridge
port: 8766
tls:
secretName: cf-origin-bluejay-dev
---
# WebSocket media stream: voice-ws.bluejay.dev -> edge1:8765
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: voice-ws
namespace: voice
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`voice-ws.bluejay.dev`)
services:
- name: voice-bridge
port: 8765
tls:
secretName: cf-origin-bluejay-dev
---
# NetworkPolicy: allow Traefik ingress only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: voice-netpol
namespace: voice
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik-system
egress:
- to:
- ipBlock:
cidr: 10.0.57.15/32
ports:
- port: 8765
protocol: TCP
- port: 8766
protocol: TCP
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP

View File

@@ -1,360 +1,360 @@
# Zabbix 7.2 Monitoring Stack
# PostgreSQL 16 + Zabbix Server + Zabbix Web (nginx)
# ArgoCD managed - BlueJay Lab
# Credentials sourced from 1Password via OnePasswordItem CRD (zabbix-credentials)
---
apiVersion: v1
kind: Namespace
metadata:
name: zabbix
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# PostgreSQL 16 StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zabbix-postgres
namespace: zabbix
labels:
app: zabbix-postgres
spec:
serviceName: zabbix-postgres
replicas: 1
selector:
matchLabels:
app: zabbix-postgres
template:
metadata:
labels:
app: zabbix-postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
args:
- "-c"
- "shared_buffers=256MB"
- "-c"
- "effective_cache_size=512MB"
- "-c"
- "work_mem=16MB"
- "-c"
- "maintenance_work_mem=128MB"
- "-c"
- "random_page_cost=1.1"
- "-c"
- "effective_io_concurrency=200"
- "-c"
- "max_connections=50"
- "-c"
- "checkpoint_completion_target=0.9"
- "-c"
- "wal_buffers=8MB"
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: DB-User
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: DB-Password
- name: POSTGRES_DB
value: zabbix
volumeMounts:
- name: zabbix-postgres-data
mountPath: /var/lib/postgresql/data
subPath: pgdata
resources:
requests:
memory: 512Mi
cpu: 200m
limits:
memory: 1Gi
cpu: "1"
livenessProbe:
exec:
command:
- pg_isready
- -U
- zabbix
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- zabbix
initialDelaySeconds: 5
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: zabbix-postgres-data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: zabbix-postgres
namespace: zabbix
spec:
selector:
app: zabbix-postgres
ports:
- port: 5432
targetPort: 5432
name: postgres
clusterIP: None
---
# Zabbix Server
apiVersion: apps/v1
kind: Deployment
metadata:
name: zabbix-server
namespace: zabbix
labels:
app: zabbix-server
spec:
replicas: 1
selector:
matchLabels:
app: zabbix-server
template:
metadata:
labels:
app: zabbix-server
spec:
containers:
- name: zabbix-server
image: zabbix/zabbix-server-pgsql:7.2-alpine-latest
ports:
- containerPort: 10051
name: trapper
env:
- name: DB_SERVER_HOST
value: zabbix-postgres
- name: DB_SERVER_PORT
value: "5432"
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: DB-User
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: DB-Password
- name: POSTGRES_DB
value: zabbix
- name: ZBX_CACHESIZE
value: "64M"
- name: ZBX_VALUECACHESIZE
value: "64M"
- name: ZBX_HISTORYCACHESIZE
value: "32M"
- name: ZBX_TRENDCACHESIZE
value: "8M"
- name: ZBX_STARTPOLLERS
value: "10"
- name: ZBX_STARTPOLLERSUNREACHABLE
value: "3"
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
cpu: "1"
livenessProbe:
tcpSocket:
port: 10051
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 10051
initialDelaySeconds: 30
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: zabbix-server
namespace: zabbix
spec:
selector:
app: zabbix-server
ports:
- port: 10051
targetPort: 10051
name: trapper
---
apiVersion: v1
kind: Service
metadata:
name: zabbix-trapper
namespace: zabbix
annotations:
metallb.universe.tf/loadBalancerIPs: 10.0.56.203
spec:
type: LoadBalancer
selector:
app: zabbix-server
ports:
- port: 10051
targetPort: 10051
name: trapper
protocol: TCP
---
# Zabbix Web (nginx + PostgreSQL)
apiVersion: apps/v1
kind: Deployment
metadata:
name: zabbix-web
namespace: zabbix
labels:
app: zabbix-web
spec:
replicas: 1
selector:
matchLabels:
app: zabbix-web
template:
metadata:
labels:
app: zabbix-web
spec:
containers:
- name: zabbix-web
image: zabbix/zabbix-web-nginx-pgsql:7.2-alpine-latest
ports:
- containerPort: 8080
name: http
env:
- name: ZBX_SERVER_HOST
value: zabbix-server
- name: ZBX_SERVER_NAME
value: "BlueJay NOC"
- name: PHP_TZ
value: America/Chicago
- name: DB_SERVER_HOST
value: zabbix-postgres
- name: DB_SERVER_PORT
value: "5432"
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: DB-User
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: DB-Password
- name: POSTGRES_DB
value: zabbix
- name: ZBX_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: password
- name: ZBX_MEMORYLIMIT
value: "256M"
- name: PHP_FPM_PM_MAX_CHILDREN
value: "10"
- name: PHP_FPM_PM_START_SERVERS
value: "3"
- name: PHP_FPM_PM_MIN_SPARE_SERVERS
value: "2"
- name: PHP_FPM_PM_MAX_SPARE_SERVERS
value: "5"
- name: PHP_FPM_PM_MAX_REQUESTS
value: "500"
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 768Mi
cpu: 500m
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 60
timeoutSeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: zabbix-web
namespace: zabbix
spec:
selector:
app: zabbix-web
ports:
- port: 8080
targetPort: 8080
name: http
---
# TLS Certificate via cert-manager
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: zabbix-tls
namespace: zabbix
spec:
secretName: zabbix-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- zabbix.iamworkin.lan
---
# Traefik IngressRoute
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: zabbix-web
namespace: zabbix
spec:
entryPoints:
- websecure
routes:
- match: Host(`zabbix.iamworkin.lan`)
kind: Rule
services:
- name: zabbix-web
port: 8080
tls:
secretName: zabbix-tls
---
# 1Password secret sync — creates zabbix-credentials K8s Secret
# Fields: DB-User, DB-Password, username, password, URL
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: zabbix-credentials
namespace: zabbix
spec:
itemPath: vaults/IAmWorkin/items/Zabbix Admin
# Zabbix 7.2 Monitoring Stack
# PostgreSQL 16 + Zabbix Server + Zabbix Web (nginx)
# ArgoCD managed - BlueJay Lab
# Credentials sourced from 1Password via OnePasswordItem CRD (zabbix-credentials)
---
apiVersion: v1
kind: Namespace
metadata:
name: zabbix
labels:
app.kubernetes.io/part-of: bluejay-infra
---
# PostgreSQL 16 StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zabbix-postgres
namespace: zabbix
labels:
app: zabbix-postgres
spec:
serviceName: zabbix-postgres
replicas: 1
selector:
matchLabels:
app: zabbix-postgres
template:
metadata:
labels:
app: zabbix-postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
args:
- "-c"
- "shared_buffers=256MB"
- "-c"
- "effective_cache_size=512MB"
- "-c"
- "work_mem=16MB"
- "-c"
- "maintenance_work_mem=128MB"
- "-c"
- "random_page_cost=1.1"
- "-c"
- "effective_io_concurrency=200"
- "-c"
- "max_connections=50"
- "-c"
- "checkpoint_completion_target=0.9"
- "-c"
- "wal_buffers=8MB"
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: DB-User
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: DB-Password
- name: POSTGRES_DB
value: zabbix
volumeMounts:
- name: zabbix-postgres-data
mountPath: /var/lib/postgresql/data
subPath: pgdata
resources:
requests:
memory: 512Mi
cpu: 200m
limits:
memory: 1Gi
cpu: "1"
livenessProbe:
exec:
command:
- pg_isready
- -U
- zabbix
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- zabbix
initialDelaySeconds: 5
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: zabbix-postgres-data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: zabbix-postgres
namespace: zabbix
spec:
selector:
app: zabbix-postgres
ports:
- port: 5432
targetPort: 5432
name: postgres
clusterIP: None
---
# Zabbix Server
apiVersion: apps/v1
kind: Deployment
metadata:
name: zabbix-server
namespace: zabbix
labels:
app: zabbix-server
spec:
replicas: 1
selector:
matchLabels:
app: zabbix-server
template:
metadata:
labels:
app: zabbix-server
spec:
containers:
- name: zabbix-server
image: zabbix/zabbix-server-pgsql:7.2-alpine-latest
ports:
- containerPort: 10051
name: trapper
env:
- name: DB_SERVER_HOST
value: zabbix-postgres
- name: DB_SERVER_PORT
value: "5432"
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: DB-User
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: DB-Password
- name: POSTGRES_DB
value: zabbix
- name: ZBX_CACHESIZE
value: "64M"
- name: ZBX_VALUECACHESIZE
value: "64M"
- name: ZBX_HISTORYCACHESIZE
value: "32M"
- name: ZBX_TRENDCACHESIZE
value: "8M"
- name: ZBX_STARTPOLLERS
value: "10"
- name: ZBX_STARTPOLLERSUNREACHABLE
value: "3"
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
cpu: "1"
livenessProbe:
tcpSocket:
port: 10051
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 10051
initialDelaySeconds: 30
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: zabbix-server
namespace: zabbix
spec:
selector:
app: zabbix-server
ports:
- port: 10051
targetPort: 10051
name: trapper
---
apiVersion: v1
kind: Service
metadata:
name: zabbix-trapper
namespace: zabbix
annotations:
metallb.universe.tf/loadBalancerIPs: 10.0.56.203
spec:
type: LoadBalancer
selector:
app: zabbix-server
ports:
- port: 10051
targetPort: 10051
name: trapper
protocol: TCP
---
# Zabbix Web (nginx + PostgreSQL)
apiVersion: apps/v1
kind: Deployment
metadata:
name: zabbix-web
namespace: zabbix
labels:
app: zabbix-web
spec:
replicas: 1
selector:
matchLabels:
app: zabbix-web
template:
metadata:
labels:
app: zabbix-web
spec:
containers:
- name: zabbix-web
image: zabbix/zabbix-web-nginx-pgsql:7.2-alpine-latest
ports:
- containerPort: 8080
name: http
env:
- name: ZBX_SERVER_HOST
value: zabbix-server
- name: ZBX_SERVER_NAME
value: "BlueJay NOC"
- name: PHP_TZ
value: America/Chicago
- name: DB_SERVER_HOST
value: zabbix-postgres
- name: DB_SERVER_PORT
value: "5432"
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: DB-User
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: DB-Password
- name: POSTGRES_DB
value: zabbix
- name: ZBX_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: zabbix-credentials
key: password
- name: ZBX_MEMORYLIMIT
value: "256M"
- name: PHP_FPM_PM_MAX_CHILDREN
value: "10"
- name: PHP_FPM_PM_START_SERVERS
value: "3"
- name: PHP_FPM_PM_MIN_SPARE_SERVERS
value: "2"
- name: PHP_FPM_PM_MAX_SPARE_SERVERS
value: "5"
- name: PHP_FPM_PM_MAX_REQUESTS
value: "500"
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 768Mi
cpu: 500m
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 60
timeoutSeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: zabbix-web
namespace: zabbix
spec:
selector:
app: zabbix-web
ports:
- port: 8080
targetPort: 8080
name: http
---
# TLS Certificate via cert-manager
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: zabbix-tls
namespace: zabbix
spec:
secretName: zabbix-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- zabbix.iamworkin.lan
---
# Traefik IngressRoute
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: zabbix-web
namespace: zabbix
spec:
entryPoints:
- websecure
routes:
- match: Host(`zabbix.iamworkin.lan`)
kind: Rule
services:
- name: zabbix-web
port: 8080
tls:
secretName: zabbix-tls
---
# 1Password secret sync — creates zabbix-credentials K8s Secret
# Fields: DB-User, DB-Password, username, password, URL
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: zabbix-credentials
namespace: zabbix
spec:
itemPath: vaults/IAmWorkin/items/Zabbix Admin