# FlowerCore.Telephony - Blazor Server + REST API + Twilio IVR # ArgoCD managed - BlueJay Lab # Credentials: 1Password → OnePasswordItem CRD → K8s Secret (twilio-credentials) # TTS: Piper on edge1 (10.0.57.17:8500) — endpoint /tts with {"text":"..."} # Public: telephony.flowercore.io via Cloudflare origin cert --- apiVersion: v1 kind: Namespace metadata: name: telephony labels: app.kubernetes.io/part-of: bluejay-infra --- # Cloudflare Origin Certificate for *.flowercore.io + *.iamwork.in (15-year RSA) apiVersion: v1 kind: Secret metadata: name: cf-origin-flowercore-io namespace: telephony type: kubernetes.io/tls data: tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVvRENDQTRpZ0F3SUJBZ0lVSXN4c1NKV1VRL0tqZ09ldk81YnNuVi9rZVE4d0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZc3hDekFKQmdOVkJBWVRBbFZUTVJrd0Z3WURWUVFLRXhCRGJHOTFaRVpzWVhKbExDQkpibU11TVRRdwpNZ1lEVlFRTEV5dERiRzkxWkVac1lYSmxJRTl5YVdkcGJpQlRVMHdnUTJWeWRHbG1hV05oZEdVZ1FYVjBhRzl5CmFYUjVNUll3RkFZRFZRUUhFdzFUWVc0Z1JuSmhibU5wYzJOdk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGgKTUI0WERUSTJNRE14TURFMk16TXdNRm9YRFRReE1ETXdOakUyTXpNd01Gb3dZakVaTUJjR0ExVUVDaE1RUTJ4dgpkV1JHYkdGeVpTd2dTVzVqTGpFZE1Cc0dBMVVFQ3hNVVEyeHZkV1JHYkdGeVpTQlBjbWxuYVc0Z1EwRXhKakFrCkJnTlZCQU1USFVOc2IzVmtSbXhoY21VZ1QzSnBaMmx1SUVObGNuUnBabWxqWVhSbE1JSUJJakFOQmdrcWhraUcKOXcwQkFRRUZBQU9DQVE0QU1JSUJDZ0tDQVFFQXV0QmpkQ0xEdHdMQlZCU0Y1ZU1OMkt3ckIxTmZmRVhRMjlRRAo1aVR0dzJFcEZXNVJJSllkMjNrYUpCMU5jZXpHWlg4a0Q0cGEyWHpFZW1MVEtJNWw0MU11b3FoWjczNVE3U3RWCkVjRFFTT2ZYTkZQdFMwb0hqb0pRdGF2QjM0ZmJNR3l4Mmx0MU9HUzRNMGtLUWpBNWR6OTJQYjNyZ1RKR0JhOW4KeTZtVThncjRuUHRSdklxZ3NxdjRtMFA3dVU1YjE3NzU1Y2JLSDVoMzIxWHVjMDU4Tzl4M2JHQ0NuRUJXWDdqeApjRGhkUEs1Ri9XRjVBQnl5cFhIQ0ZxUUd4M1NVbmtCQ0ZQSmRabnMra3BHVUZWZGhud3B6NjBtNnlJSzQ0eVR4CjZqR3JOTFEyM1dOK2gwU1lCZU5vb2JBWThydkpiVlZEaGJqSVhBTWtFNGQzVll1TlhRSURBUUFCbzRJQklqQ0MKQVI0d0RnWURWUjBQQVFIL0JBUURBZ1dnTUIwR0ExVWRKUVFXTUJRR0NDc0dBUVVGQndNQ0JnZ3JCZ0VGQlFjRApBVEFNQmdOVkhSTUJBZjhFQWpBQU1CMEdBMVVkRGdRV0JCUkt1NkJVUDZ0N2dpbFRPay9FdEdKQ3R6N3dTREFmCkJnTlZIU01FR0RBV2dCUWs2Rk5YWFh3MFFJZXA2NVRidXVFV2VQd3BwREJBQmdnckJnRUZCUWNCQVFRME1ESXcKTUFZSUt3WUJCUVVITUFHR0pHaDBkSEE2THk5dlkzTndMbU5zYjNWa1pteGhjbVV1WTI5dEwyOXlhV2RwYmw5agpZVEFqQmdOVkhSRUVIREFhZ2d3cUxtbGhiWGR2Y21zdWFXNkNDbWxoYlhkdmNtc3VhVzR3T0FZRFZSMGZCREV3Ckx6QXRvQ3VnS1lZbmFIUjBjRG92TDJOeWJDNWpiRzkxWkdac1lYSmxMbU52YlM5dmNtbG5hVzVmWTJFdVkzSnMKTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFDSjMvTGNleE5pb0lWdUxoemhmbTZCeDV2SWk3T25CaHF1WUlDdwplNnArZ0prdE16ZFJQcDV0bk03dllBWmxMajVJOTByWDRuczhJc3dEbzJBN2wwYTRGZVJFclFmRklsZXQzbjIyCjUxVTZYVElCSks5c1FZT0FkU3pJUzV1OUNKSFpBUTF5WmxSd3BBR3RVWnhxL1dpcGFWUTRwNXhrcEJNMVlZSlAKNW1jQ09HcFErSnpORlpQc2daYUJncDBYL1BBZkNJRkkyZld5QWE2elBqRm0rdDVXUXIrZlBaT2VUS2VIbWVzVgo3UlZxUUdEb3Q0eTY1NklEdmdmU2ZLRnFIRW9XNDJVbDBxQ05hMS9keEJld3NIS1VWWE1ETkdiQlNVQjM4TG9YCm1OQ3hJQlVOUjR0TG1CQUxZT3hVMnZhSWRCd0xBc2YrcndnVnVjUGpCUTc2VWMwUQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzYwR04wSXNPM0FzRlUKRklYbDR3M1lyQ3NIYTE5OFJkRGIxQVBtSk8zRFlTa1ZibEVnbGgzYmVSb2tIVTF4N01abGZ5UVBpbHJaZk1SNgpZdE1vam1YalV5NmlxRm52ZmxEdEsxVVJ3TkJJNTljMFUrMUxTZ2VPZ2xDMXE4SGZoOXN3YkxIYVczVTRaTGd6ClNRcENNRGwzUDNZOXZldUJNa1lGcjJmTHFaVHlDdmljKzFHOGlxQ3lxL2liUS91NVRsdlh2dm5seHNvZm1IZmIKVmU1elRudzczSGRzWUlLY1FGWmZ1UEZ3T0YwOHJrWDlZWGtBSExLbGNjSVdwQWJIZEpTZVFFSVU4bDFtZXo2UwprWlFWVjJHZkNuUHJTYnJJZ3JqakpQSHFNYXMwdERiZFkzNkhSSmdGNDJpaHNCanl1OGx0VlVPRnVNaGNBeVFUCmgzZFZpNDFkQWdNQkFBRUNnZ0VBTGlseXZkNmVTcEYvZUxtV2lhTVV4NUxwa2dhWHpITkxCQnNNZUpqcytLL0EKVVdlZ1crTkVUdmlLalZ5QlI5SzRocG1IYldDa2lPUDBBQUwrQnlKQ3lvekNOQmJTSEdRejlwc1R5dzZBV1ZlUwpuYjlVWGx1VmFQRktKTTRqbXNydERuYjVic25WT2lGblErTDdTalkwNlFMUlFybjBvUWp0ZFJldUdBMFlQVU90CkhSYzNsMFg2ZHJqdkJYY2prWTQwWm9ZYkRrelJnU1JWbWVOUGFIbjZPR0NtYUVUMXVyK01qYVZ2ME9lbEdIWncKVzljSEIxaHNxRzUvMWU3V0RQN0l0cjkwTmg4ay81NVhiK3lQUnhsRFd5bWtZMzIvdFBtZzdESTRKV2tRRWt3cgpIZUtwODVTcE5ta1liRnVpVFppeU8zZDZ0aXZHNHhFZW8rSzFVVFU4c1FLQmdRRFRNSEU1RDFYVC9HbGR5VHNsCllrODRVL1N0NXUrK2RIUEt1Wmw2dVB0UGgxV1lrdnFRcmdrL05YanVud2xGN0Y3b2tWOGdPeWxreTYwYTZkcXIKeXZwN1ZJdXYzekVlc2h2NjNWMlpaVkMzcXZYSzFheit3Zmx3NitCZmVuRlY5S2NENHN0dTdwOFRPWmFGN01CUgo3YXZzaXVXbWtqdmM1TlVLRmVDRTY0SnZFUUtCZ1FEaWMrbWlNLzBodDN1ajhuOXgyMDFQZFNqbEpVaUc1NjNNCnRYZlBCdDJRT0NhaVluUFNFdTdXdm5pQWRFL2xrMm91cFRWam9LYmZPbDFyQjd6UzVhc2kxdVdDZDhlUy9UWGIKdU5iRmlNMDB4L3JxalMydCtQbTd4MVhrYTB4TFNSRDNmZ0tSQldSN3pscStkYWZ1WE1qelUxRnh5dTIycGphRgpIMEl3NEpCUmpRS0JnUUNOaWhMb0Rob1V5RCtKNXJzb00vb3FJMEtDWnB0WlJzendHbkg5cVFwdFk2Ti9iVXBYCk92emhpeUh3czAvUXVEbG5uejVrNktHMmR6Y2VLWXN2eGdzWUt6S3ZmV043VWgya2hVWWM3NlVvWTREMkh6MGgKUkxtNzc2cGg4enNRUTdiSHlQRlUrTUpPYlRNdnNOdTRUUlVEcEplRGl0QnFIRWVYeWMrKzVlUjJNUUtCZ0h2UgptVHVoWlpVYitEVEtrVGkyQ20yWnlBU1RBRGNUVW9xTjVyYUNNSDk4MUZNUnRmWjFkN1pmYXhBQmlQWWtSbmkrCnlKUnk4UXM1cEg2ek9tR3VSb2JFTGJYS3ZJcjRmSXhwWXJXYmVXaVV0L09yd2dCUUZHekNMNHEzeUgyWnMvYy8KSlRRYVdMa0JPY2pPR0VaUzRXVjZkeHZiTTJNZE9zNUxLeXdDZmFhNUFvR0FIQUE1eEN0dndOZE4xeExndkZ3RApPK2lyMDl1bXMxOFBzSVpmK1ZrWGtpcHF4MWNUT0hEanpPR01yWXV0M2FFeE00Zjd2ckFHRFMyY2pwZjM0T1JxCit4Y2gwWlNaQ2FDZmlnZG9OelNkcDFLcmo0cnFKdG5ZdS9CNDlDQlVoSDBNaCtSRWswQ0hHOVE4b3FOWFk0V0wKbVVOVTZMYUkwQWtvSzNVb2tWQVJEYXM9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K --- # 1Password → K8s Secret sync for Twilio credentials # Creates secret "twilio-credentials" with fields: AccountSid, AuthToken, DefaultFromNumber apiVersion: onepassword.com/v1 kind: OnePasswordItem metadata: name: twilio-credentials namespace: telephony spec: itemPath: "vaults/IAmWorkin/items/Twilio Account" --- # Application configuration overlay apiVersion: v1 kind: ConfigMap metadata: name: telephony-config namespace: telephony data: appsettings.Production.json: | { "Telephony": { "Provider": "asterisk", "Twilio": { "VoiceUrl": "https://telephony.flowercore.io/api/twilio/webhooks/voice/incoming", "StatusCallbackUrl": "https://telephony.flowercore.io/api/twilio/webhooks/voice/status" }, "Asterisk": { "BaseUrl": "http://10.0.56.12:8088", "Username": "flowercore", "Password": "bluejay-asterisk-ari", "Application": "flowercore-pbx", "ReconnectDelaySeconds": 5, "MaxReconnectDelaySeconds": 60 } }, "Ari": { "BaseUrl": "http://10.0.56.12:8088", "Username": "flowercore", "Password": "bluejay-asterisk-ari", "Application": "flowercore-pbx", "ReconnectDelaySeconds": 5, "MaxReconnectDelaySeconds": 60 }, "Sip": { "Domain": "10.0.56.207", "Port": 5060, "Transport": "udp" }, "Tts": { "PiperUrl": "http://10.0.57.17:8500", "DefaultEngine": "piper", "SampleRate": 8000 }, "DatabaseProvider": "Sqlite", "ConnectionStrings": { "DefaultConnection": "Data Source=/data/telephony.db" }, "Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5100" } } } } --- # Persistent volume for SQLite database apiVersion: v1 kind: PersistentVolumeClaim metadata: name: telephony-data namespace: telephony spec: accessModes: [ReadWriteOnce] resources: requests: storage: 5Gi --- # Telephony web application apiVersion: apps/v1 kind: Deployment metadata: name: telephony-web namespace: telephony labels: app: telephony-web spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: telephony-web template: metadata: annotations: fc.flowercore.io/healthz-anon: "true" fc.flowercore.io/probe-path: "/health" labels: app: telephony-web spec: securityContext: runAsNonRoot: true runAsUser: 1654 runAsGroup: 1654 fsGroup: 1654 nodeSelector: kubernetes.io/hostname: rke2-agent1 initContainers: - name: fix-data-perms image: busybox:latest # Must run as root to chown the hostPath /tmp/tts-audio that may be # root-owned after node reboot. Pod-level runAsNonRoot:true would # otherwise inherit and chown would fail with EPERM (see Notes memory # feedback_hostpath_initcontainer_chown_perms). securityContext: runAsUser: 0 runAsNonRoot: false command: ["sh", "-c", "chown -R 1654:1654 /data && chown 1654:1654 /shared-tts && chmod 0755 /shared-tts"] volumeMounts: - name: telephony-data mountPath: /data - name: shared-tts mountPath: /shared-tts hostNetwork: true dnsPolicy: ClusterFirstWithHostNet affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: asterisk topologyKey: kubernetes.io/hostname containers: - name: telephony-web image: localhost/fc-telephony-web:v202604252156 imagePullPolicy: Never securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: [ALL] ports: - containerPort: 5100 name: http # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. env: - name: Telephony__Twilio__AccountSid valueFrom: secretKeyRef: name: twilio-credentials key: AccountSid optional: true - name: Telephony__Twilio__AuthToken valueFrom: secretKeyRef: name: twilio-credentials key: AuthToken optional: true - name: Telephony__Twilio__DefaultFromNumber valueFrom: secretKeyRef: name: twilio-credentials key: DefaultFromNumber optional: true volumeMounts: - name: telephony-config mountPath: /app/appsettings.Production.json subPath: appsettings.Production.json readOnly: true - name: telephony-data mountPath: /data - name: tmp mountPath: /tmp - name: logs mountPath: /app/logs # Shared TTS audio — we write Piper .sln16 output here; Asterisk # pod reads the same hostPath at /var/lib/asterisk/sounds/tts and # plays via `sound:tts/`. Both pods are pinned to rke2-agent1. - name: shared-tts mountPath: /shared-tts resources: requests: memory: 256Mi cpu: 100m limits: memory: 1Gi cpu: "1" livenessProbe: httpGet: path: /health port: 5100 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /health port: 5100 initialDelaySeconds: 10 periodSeconds: 5 volumes: - name: telephony-config configMap: name: telephony-config - name: telephony-data persistentVolumeClaim: claimName: telephony-data - name: tmp emptyDir: {} - name: logs emptyDir: {} - name: shared-tts hostPath: path: /tmp/tts-audio type: DirectoryOrCreate --- # ClusterIP service apiVersion: v1 kind: Service metadata: name: telephony-web namespace: telephony spec: selector: app: telephony-web ports: - port: 5100 targetPort: 5100 name: http --- # Traefik IngressRoute — public via Cloudflare (primary: flowercore.io) apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: telephony-web namespace: telephony spec: entryPoints: - websecure routes: - kind: Rule match: Host(`telephony.flowercore.io`) services: - name: telephony-web port: 5100 - kind: Rule match: Host(`telephony.iamwork.in`) services: - name: telephony-web port: 5100 tls: secretName: cf-origin-flowercore-io --- # NetworkPolicy: deny-all baseline + Traefik ingress + SIP/RTP ingress + DNS egress + TTS egress apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: telephony-netpol namespace: telephony spec: podSelector: {} policyTypes: - Ingress - Egress ingress: # Allow Traefik ingress controller - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: traefik-system # Allow Selenium Grid for automated UI testing - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: selenium ports: - port: 5100 protocol: TCP # Allow SIP/RTP from external sources (Yealink phones, Twilio SIP trunk) - from: - ipBlock: cidr: 0.0.0.0/0 ports: - port: 5060 protocol: UDP - port: 5060 protocol: TCP - port: 10000 endPort: 20000 protocol: UDP egress: # Allow DNS resolution (CoreDNS in kube-system) - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system ports: - port: 53 protocol: UDP - port: 53 protocol: TCP # Allow Piper TTS on edge1 (10.0.57.17:8500) - to: - ipBlock: cidr: 10.0.57.17/32 ports: - port: 8500 protocol: TCP # Allow Twilio API outbound (HTTPS) - to: - ipBlock: cidr: 0.0.0.0/0 except: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 ports: - port: 443 protocol: TCP # Allow SIP/RTP responses (Asterisk → phones and Twilio) - to: - ipBlock: cidr: 0.0.0.0/0 ports: - port: 5060 protocol: UDP - port: 5060 protocol: TCP - port: 10000 endPort: 20000 protocol: UDP # Allow 1Password Connect for secret sync - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: onepassword-system --- # TLS Certificate for internal hostname via cert-manager apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: telephony-internal-tls namespace: telephony spec: secretName: telephony-internal-tls issuerRef: name: step-ca-acme kind: ClusterIssuer dnsNames: - telephony.iamworkin.lan --- # Traefik IngressRoute — internal LAN access apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: telephony-web-internal namespace: telephony spec: entryPoints: - websecure routes: - kind: Rule match: Host(`telephony.iamworkin.lan`) services: - name: telephony-web port: 5100 tls: secretName: telephony-internal-tls