From 22f0fce9026ea51f957dfcb2fbfe153e72b4186f Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Sun, 23 Aug 2026 21:10:56 +0200 Subject: [PATCH] First try with CI --- .gitea/workflows/deploy.yml | 84 ++++++++++++++++ .gitignore | 5 + README.md | 40 +++++++- bootstrap-turn-secret.sh | 34 +++++++ ci-deployer.yaml | 82 +++++++++++++++ coturn-relay-service.yaml | 56 +++++++++++ coturn.yaml | 193 ++++++++++++++++++++++++++++++++++++ create-ci-kubeconfig.sh | 66 ++++++++++++ install.sh | 26 +++++ 9 files changed, 584 insertions(+), 2 deletions(-) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitignore create mode 100755 bootstrap-turn-secret.sh create mode 100644 ci-deployer.yaml create mode 100644 coturn-relay-service.yaml create mode 100644 coturn.yaml create mode 100755 create-ci-kubeconfig.sh create mode 100755 install.sh diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..2092efa --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,84 @@ +name: Validate and deploy coturn + +on: + push: + pull_request: + +env: + KUBECONFORM_VERSION: v0.7.0 + KUBECTL_VERSION: v1.36.3 + KUBERNETES_API: https://host.containers.internal:6443 + KUBERNETES_TLS_SERVER_NAME: 192.168.0.2 + NAMESPACE: coturn + +jobs: + validate: + name: Validate manifests + runs-on: ubuntu-latest + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Validate scripts and Kubernetes resources + run: | + sh -n install.sh bootstrap-turn-secret.sh + bash -n create-ci-kubeconfig.sh + docker run --rm --interactive \ + "ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION}@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c" \ + -strict -summary -ignore-missing-schemas < coturn.yaml + docker run --rm --interactive \ + "ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION}@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c" \ + -strict -summary < coturn-relay-service.yaml + docker run --rm --interactive \ + "ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION}@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c" \ + -strict -summary < ci-deployer.yaml + + deploy: + name: Deploy to Kubernetes + if: gitea.ref == 'refs/heads/main' + needs: validate + runs-on: ubuntu-latest + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Install kubectl + run: | + curl --fail --silent --show-error --location \ + --output "${RUNNER_TEMP}/kubectl" \ + "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" + curl --fail --silent --show-error --location \ + --output "${RUNNER_TEMP}/kubectl.sha256" \ + "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256" + printf '%s %s\n' "$(cat "${RUNNER_TEMP}/kubectl.sha256")" "${RUNNER_TEMP}/kubectl" | sha256sum --check + chmod 0700 "${RUNNER_TEMP}/kubectl" + + - name: Configure Kubernetes access + env: + KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64 }} + run: | + test -n "${KUBE_CONFIG_BASE64}" + printf '%s' "${KUBE_CONFIG_BASE64}" | base64 --decode > "${RUNNER_TEMP}/kubeconfig" + chmod 0600 "${RUNNER_TEMP}/kubeconfig" + export KUBECONFIG="${RUNNER_TEMP}/kubeconfig" + "${RUNNER_TEMP}/kubectl" config set-cluster cluster \ + --server="${KUBERNETES_API}" \ + --tls-server-name="${KUBERNETES_TLS_SERVER_NAME}" + + - name: Apply and verify + run: | + export KUBECONFIG="${RUNNER_TEMP}/kubeconfig" + "${RUNNER_TEMP}/kubectl" apply \ + --dry-run=server \ + --validate=false \ + --filename coturn.yaml \ + --filename coturn-relay-service.yaml + "${RUNNER_TEMP}/kubectl" apply \ + --validate=false \ + --filename coturn.yaml \ + --filename coturn-relay-service.yaml + + "${RUNNER_TEMP}/kubectl" --namespace "${NAMESPACE}" \ + rollout status deployment/coturn --timeout=5m + "${RUNNER_TEMP}/kubectl" --namespace "${NAMESPACE}" \ + wait --for=condition=Ready certificate/coturn --timeout=5m diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e786a4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Local secret/bootstrap material must never be committed. +.env +turn-secret.yaml +kubeconfig +*.kubeconfig diff --git a/README.md b/README.md index e4a6618..339bb1f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,39 @@ -# coturn-kubernetes +# coturn -Configuration files to run my coturn server for voip in matrix/synapse in my kubernetes cluster with cert-manager and L2 LoadBalancer \ No newline at end of file +Authenticated TURN for Synapse, exposed through MetalLB at `192.168.0.19`. + +## Local deployment + +The TURN shared secret is stored in Kubernetes as `turn-shared-secret` in the +`coturn` and `matrix` namespaces. It is never written into a committed file. + +Bootstrap the existing secret once, then deploy: + +```sh +TURN_SHARED_SECRET='<64-character-secret>' ./bootstrap-turn-secret.sh +./install.sh +``` + +## Gitea Actions CD + +Bootstrap the restricted deployer and generate its kubeconfig: + +```sh +./create-ci-kubeconfig.sh +``` + +When the Kubernetes API is reachable only through `dahoam`, use: + +```sh +KUBECTL_SSH_HOST=dahoam \ +SSH_CONFIG="$HOME/.ssh/config" \ +./create-ci-kubeconfig.sh +``` + +Create one repository Actions secret named `KUBE_CONFIG_BASE64` containing the +single base64 line printed by that command. The deployer can update only the +existing coturn Services, ServiceAccount, Deployment, and Certificate. It +cannot read Kubernetes Secrets or modify RBAC. + +No TURN secret is required in Gitea. The committed Deployment references the +existing Kubernetes Secret directly. diff --git a/bootstrap-turn-secret.sh b/bootstrap-turn-secret.sh new file mode 100755 index 0000000..bc7e015 --- /dev/null +++ b/bootstrap-turn-secret.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +set -eu + +SECRET_NAME=turn-shared-secret + +if [ -z "${TURN_SHARED_SECRET:-}" ]; then + echo "Set TURN_SHARED_SECRET to the existing 64-character shared secret." >&2 + exit 1 +fi + +case "$TURN_SHARED_SECRET" in + *[!0-9a-f]*|'') + echo "TURN_SHARED_SECRET must contain lowercase hexadecimal characters only." >&2 + exit 1 + ;; +esac + +if [ "${#TURN_SHARED_SECRET}" -ne 64 ]; then + echo "TURN_SHARED_SECRET must be exactly 64 characters." >&2 + exit 1 +fi + +for namespace in coturn matrix; do + kubectl create namespace "$namespace" --dry-run=client --output=yaml \ + | kubectl apply --filename=- + kubectl create secret generic "$SECRET_NAME" \ + --namespace "$namespace" \ + --from-literal="shared-secret=$TURN_SHARED_SECRET" \ + --dry-run=client --output=yaml \ + | kubectl apply --filename=- +done + +echo "Stored the existing TURN shared secret in coturn/$SECRET_NAME and matrix/$SECRET_NAME." diff --git a/ci-deployer.yaml b/ci-deployer.yaml new file mode 100644 index 0000000..47bed90 --- /dev/null +++ b/ci-deployer.yaml @@ -0,0 +1,82 @@ +# Bootstrap once with an administrator identity. CI can update only the +# existing coturn resources and cannot read Secrets or change RBAC. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: coturn-deployer + namespace: coturn +automountServiceAccountToken: false +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coturn-deployer + namespace: coturn +rules: + - apiGroups: + - "" + resources: + - serviceaccounts + resourceNames: + - coturn + verbs: + - get + - patch + - update + - apiGroups: + - "" + resources: + - services + resourceNames: + - coturn-tcp + - coturn-udp + - coturn-relay + verbs: + - get + - patch + - update + - apiGroups: + - apps + resources: + - deployments + resourceNames: + - coturn + verbs: + - get + - patch + - update + - watch + - apiGroups: + - cert-manager.io + resources: + - certificates + resourceNames: + - coturn + verbs: + - get + - patch + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: coturn-deployer + namespace: coturn +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coturn-deployer +subjects: + - kind: ServiceAccount + name: coturn-deployer + namespace: coturn +--- +apiVersion: v1 +kind: Secret +metadata: + name: coturn-deployer-token + namespace: coturn + annotations: + kubernetes.io/service-account.name: coturn-deployer +type: kubernetes.io/service-account-token diff --git a/coturn-relay-service.yaml b/coturn-relay-service.yaml new file mode 100644 index 0000000..d5d28ec --- /dev/null +++ b/coturn-relay-service.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: Service +metadata: + name: coturn-relay + namespace: coturn + annotations: + metallb.io/loadBalancerIPs: "192.168.0.19" + metallb.io/allow-shared-ip: coturn +spec: + type: LoadBalancer + externalTrafficPolicy: Local + selector: + app.kubernetes.io/instance: coturn + app.kubernetes.io/name: coturn + ports: + - {name: relay-49160, port: 49160, targetPort: 49160, protocol: UDP} + - {name: relay-49161, port: 49161, targetPort: 49161, protocol: UDP} + - {name: relay-49162, port: 49162, targetPort: 49162, protocol: UDP} + - {name: relay-49163, port: 49163, targetPort: 49163, protocol: UDP} + - {name: relay-49164, port: 49164, targetPort: 49164, protocol: UDP} + - {name: relay-49165, port: 49165, targetPort: 49165, protocol: UDP} + - {name: relay-49166, port: 49166, targetPort: 49166, protocol: UDP} + - {name: relay-49167, port: 49167, targetPort: 49167, protocol: UDP} + - {name: relay-49168, port: 49168, targetPort: 49168, protocol: UDP} + - {name: relay-49169, port: 49169, targetPort: 49169, protocol: UDP} + - {name: relay-49170, port: 49170, targetPort: 49170, protocol: UDP} + - {name: relay-49171, port: 49171, targetPort: 49171, protocol: UDP} + - {name: relay-49172, port: 49172, targetPort: 49172, protocol: UDP} + - {name: relay-49173, port: 49173, targetPort: 49173, protocol: UDP} + - {name: relay-49174, port: 49174, targetPort: 49174, protocol: UDP} + - {name: relay-49175, port: 49175, targetPort: 49175, protocol: UDP} + - {name: relay-49176, port: 49176, targetPort: 49176, protocol: UDP} + - {name: relay-49177, port: 49177, targetPort: 49177, protocol: UDP} + - {name: relay-49178, port: 49178, targetPort: 49178, protocol: UDP} + - {name: relay-49179, port: 49179, targetPort: 49179, protocol: UDP} + - {name: relay-49180, port: 49180, targetPort: 49180, protocol: UDP} + - {name: relay-49181, port: 49181, targetPort: 49181, protocol: UDP} + - {name: relay-49182, port: 49182, targetPort: 49182, protocol: UDP} + - {name: relay-49183, port: 49183, targetPort: 49183, protocol: UDP} + - {name: relay-49184, port: 49184, targetPort: 49184, protocol: UDP} + - {name: relay-49185, port: 49185, targetPort: 49185, protocol: UDP} + - {name: relay-49186, port: 49186, targetPort: 49186, protocol: UDP} + - {name: relay-49187, port: 49187, targetPort: 49187, protocol: UDP} + - {name: relay-49188, port: 49188, targetPort: 49188, protocol: UDP} + - {name: relay-49189, port: 49189, targetPort: 49189, protocol: UDP} + - {name: relay-49190, port: 49190, targetPort: 49190, protocol: UDP} + - {name: relay-49191, port: 49191, targetPort: 49191, protocol: UDP} + - {name: relay-49192, port: 49192, targetPort: 49192, protocol: UDP} + - {name: relay-49193, port: 49193, targetPort: 49193, protocol: UDP} + - {name: relay-49194, port: 49194, targetPort: 49194, protocol: UDP} + - {name: relay-49195, port: 49195, targetPort: 49195, protocol: UDP} + - {name: relay-49196, port: 49196, targetPort: 49196, protocol: UDP} + - {name: relay-49197, port: 49197, targetPort: 49197, protocol: UDP} + - {name: relay-49198, port: 49198, targetPort: 49198, protocol: UDP} + - {name: relay-49199, port: 49199, targetPort: 49199, protocol: UDP} + - {name: relay-49200, port: 49200, targetPort: 49200, protocol: UDP} diff --git a/coturn.yaml b/coturn.yaml new file mode 100644 index 0000000..c988204 --- /dev/null +++ b/coturn.yaml @@ -0,0 +1,193 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: coturn + namespace: coturn + labels: + app.kubernetes.io/name: coturn + app.kubernetes.io/instance: coturn +automountServiceAccountToken: false +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: coturn + namespace: coturn + labels: + app.kubernetes.io/name: coturn + app.kubernetes.io/instance: coturn + app.kubernetes.io/version: 4.10.0-r1 +spec: + replicas: 1 + revisionHistoryLimit: 3 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: coturn + app.kubernetes.io/instance: coturn + template: + metadata: + labels: + app.kubernetes.io/name: coturn + app.kubernetes.io/instance: coturn + spec: + serviceAccountName: coturn + automountServiceAccountToken: false + containers: + - name: coturn + image: coturn/coturn:4.10.0-r1 + imagePullPolicy: IfNotPresent + args: + - --listening-port=3478 + - --tls-listening-port=5349 + - --fingerprint + - --cert=/etc/ssl/certs/cert.pem + - --pkey=/etc/ssl/private/privkey.pem + - --realm=brunner.ninja + - --log-file=stdout + - --no-software-attribute + - --no-cli + - --use-auth-secret + - --static-auth-secret=$(TURN_SHARED_SECRET) + - --external-ip=37.186.4.16 + - --min-port=49160 + - --max-port=49200 + - --no-multicast-peers + env: + - name: TURN_SHARED_SECRET + valueFrom: + secretKeyRef: + name: turn-shared-secret + key: shared-secret + ports: + - name: tcp + containerPort: 3478 + protocol: TCP + - name: udp + containerPort: 3478 + protocol: UDP + - name: tcp-tls + containerPort: 5349 + protocol: TCP + - name: udp-tls + containerPort: 5349 + protocol: UDP + readinessProbe: + tcpSocket: + port: tcp + livenessProbe: + tcpSocket: + port: tcp + initialDelaySeconds: 30 + resources: + requests: + cpu: 100m + memory: 50Mi + limits: + cpu: 100m + memory: 50Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + add: + - NET_BIND_SERVICE + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65534 + volumeMounts: + - name: certs + mountPath: /etc/ssl/certs + readOnly: true + - name: private + mountPath: /etc/ssl/private + readOnly: true + volumes: + - name: certs + secret: + secretName: coturn + items: + - key: tls.crt + path: cert.pem + - name: private + secret: + secretName: coturn + items: + - key: tls.key + path: privkey.pem +--- +apiVersion: v1 +kind: Service +metadata: + name: coturn-tcp + namespace: coturn + labels: + app.kubernetes.io/name: coturn + app.kubernetes.io/instance: coturn + annotations: + metallb.io/loadBalancerIPs: "192.168.0.19" + metallb.io/allow-shared-ip: coturn +spec: + type: LoadBalancer + externalTrafficPolicy: Local + selector: + app.kubernetes.io/name: coturn + app.kubernetes.io/instance: coturn + ports: + - name: tcp + port: 3478 + targetPort: tcp + protocol: TCP + - name: tcp-tls + port: 5349 + targetPort: tcp-tls + protocol: TCP +--- +apiVersion: v1 +kind: Service +metadata: + name: coturn-udp + namespace: coturn + labels: + app.kubernetes.io/name: coturn + app.kubernetes.io/instance: coturn + annotations: + metallb.io/loadBalancerIPs: "192.168.0.19" + metallb.io/allow-shared-ip: coturn +spec: + type: LoadBalancer + externalTrafficPolicy: Local + selector: + app.kubernetes.io/name: coturn + app.kubernetes.io/instance: coturn + ports: + - name: udp + port: 3478 + targetPort: udp + protocol: UDP + - name: udp-tls + port: 5349 + targetPort: udp-tls + protocol: UDP +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: coturn + namespace: coturn + labels: + app.kubernetes.io/name: coturn + app.kubernetes.io/instance: coturn +spec: + secretName: coturn + dnsNames: + - brunner.ninja + issuerRef: + group: cert-manager.io + kind: ClusterIssuer + name: letsencrypt-dns + usages: + - digital signature + - key encipherment diff --git a/create-ci-kubeconfig.sh b/create-ci-kubeconfig.sh new file mode 100755 index 0000000..67bd403 --- /dev/null +++ b/create-ci-kubeconfig.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +namespace=${NAMESPACE:-coturn} +service_account=${SERVICE_ACCOUNT:-coturn-deployer} +secret=${TOKEN_SECRET:-coturn-deployer-token} +ssh_host=${KUBECTL_SSH_HOST:-} +ssh_config=${SSH_CONFIG:-} + +cluster_kubectl() { + if [[ -n "${ssh_host}" ]]; then + if [[ -n "${ssh_config}" ]]; then + ssh -F "${ssh_config}" "${ssh_host}" kubectl "$@" + else + ssh "${ssh_host}" kubectl "$@" + fi + else + kubectl "$@" + fi +} + +if [[ -n "${ssh_host}" ]]; then + cluster_kubectl apply --filename=- < "${project_dir}/ci-deployer.yaml" >&2 +else + cluster_kubectl apply --filename "${project_dir}/ci-deployer.yaml" >&2 +fi + +for attempt in {1..30}; do + if cluster_kubectl --namespace "${namespace}" get secret "${secret}" \ + --output=jsonpath='{.data.token}' 2>/dev/null | grep -q .; then + break + fi + if [[ "${attempt}" == 30 ]]; then + echo "Timed out waiting for the service-account token" >&2 + exit 1 + fi + sleep 1 +done + +workdir=$(mktemp --directory) +trap 'rm -rf "${workdir}"' EXIT + +server=$(cluster_kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}') +cluster_kubectl --namespace "${namespace}" get secret "${secret}" \ + --output=jsonpath='{.data.ca\.crt}' | base64 --decode > "${workdir}/ca.crt" +token=$(cluster_kubectl --namespace "${namespace}" get secret "${secret}" \ + --output=jsonpath='{.data.token}' | base64 --decode) + +export KUBECONFIG="${workdir}/config" +kubectl config set-cluster cluster \ + --server="${server}" \ + --certificate-authority="${workdir}/ca.crt" \ + --embed-certs=true >/dev/null +kubectl config set-credentials "${service_account}" --token="${token}" >/dev/null +kubectl config set-context coturn \ + --cluster=cluster \ + --user="${service_account}" \ + --namespace="${namespace}" >/dev/null +kubectl config use-context coturn >/dev/null + +echo "Store the following single line as the Gitea Actions secret KUBE_CONFIG_BASE64." >&2 +echo "Treat it as a password; do not commit it." >&2 +base64 --wrap=0 "${KUBECONFIG}" +printf '\n' diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..640bebf --- /dev/null +++ b/install.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +set -eu + +NAMESPACE=coturn +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) + +if ! command -v kubectl >/dev/null 2>&1; then + echo "Required command not found: kubectl" >&2 + exit 1 +fi + +kubectl apply --dry-run=server --validate=false \ + --filename "$SCRIPT_DIR/coturn.yaml" \ + --filename "$SCRIPT_DIR/coturn-relay-service.yaml" +kubectl apply --validate=false \ + --filename "$SCRIPT_DIR/coturn.yaml" \ + --filename "$SCRIPT_DIR/coturn-relay-service.yaml" +kubectl wait --for=condition=Ready certificate/coturn \ + --namespace "$NAMESPACE" --timeout=10m +kubectl rollout status deployment/coturn \ + --namespace "$NAMESPACE" --timeout=5m + +echo +echo "coturn is available at brunner.ninja on 3478 and 5349." +echo "MetalLB: 192.168.0.19; UDP relay range: 49160-49200."