diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml deleted file mode 100644 index 57cd9cf..0000000 --- a/.gitea/workflows/deploy.yml +++ /dev/null @@ -1,103 +0,0 @@ -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}" \ - -strict -summary -ignore-missing-schemas < coturn.yaml - docker run --rm --interactive \ - "ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION}" \ - -strict -summary < coturn-relay-service.yaml - docker run --rm --interactive \ - "ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION}" \ - -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" - 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 - - for attempt in {1..60}; do - IFS='|' read -r generation observed desired updated available unavailable <<< "$( - "${RUNNER_TEMP}/kubectl" --namespace "${NAMESPACE}" \ - get deployment/coturn \ - --output=jsonpath='{.metadata.generation}|{.status.observedGeneration}|{.spec.replicas}|{.status.updatedReplicas}|{.status.availableReplicas}|{.status.unavailableReplicas}' - )" - certificate_ready=$( - "${RUNNER_TEMP}/kubectl" --namespace "${NAMESPACE}" \ - get certificate/coturn \ - --output=jsonpath='{.status.conditions[?(@.type=="Ready")].status}' - ) - - echo "Check ${attempt}/60: deployment generation ${observed}/${generation}, replicas ${updated}/${desired} updated, ${available}/${desired} available; certificate Ready=${certificate_ready:-False}" - if [[ "${observed}" == "${generation}" \ - && "${updated}" == "${desired}" \ - && "${available}" == "${desired}" \ - && ( -z "${unavailable}" || "${unavailable}" == "0" ) \ - && "${certificate_ready}" == "True" ]]; then - echo "coturn deployment and certificate are ready" - exit 0 - fi - - sleep 5 - done - - echo "coturn did not become ready within five minutes" >&2 - exit 1 diff --git a/README.md b/README.md index 339bb1f..627575d 100644 --- a/README.md +++ b/README.md @@ -2,38 +2,20 @@ Authenticated TURN for Synapse, exposed through MetalLB at `192.168.0.19`. -## Local deployment +The live deployment is managed by Argo CD from +[`homelab-gitops/apps/coturn`](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/coturn). +That public chart references existing Kubernetes Secrets by name and contains +no credential values. This repository no longer deploys to Kubernetes. -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. +## Secret bootstrap -Bootstrap the existing secret once, then deploy: +The shared TURN secret must exist as `turn-shared-secret` in both the `coturn` +and `matrix` namespaces. For disaster recovery, restore the same 64-character +value with: ```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. +Do not generate different values for the two namespaces. Rotate the shared +credential as a coordinated Coturn/Synapse operation. diff --git a/ci-deployer.yaml b/ci-deployer.yaml deleted file mode 100644 index 199bd37..0000000 --- a/ci-deployer.yaml +++ /dev/null @@ -1,80 +0,0 @@ -# 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 - - apiGroups: - - cert-manager.io - resources: - - certificates - resourceNames: - - coturn - verbs: - - get - - patch - - update ---- -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 deleted file mode 100644 index 28fd5f7..0000000 --- a/coturn-relay-service.yaml +++ /dev/null @@ -1,59 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: coturn-relay - namespace: coturn - annotations: - metallb.io/loadBalancerIPs: "192.168.0.19" - metallb.io/allow-shared-ip: coturn - mikrotik.brunner.ninja/router: newgw - mikrotik.brunner.ninja/forwardings: >- - [{"name":"relay-udp","servicePortRange":"49160-49200","dstAddressList":"WANs"}] -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 deleted file mode 100644 index 6730b11..0000000 --- a/coturn.yaml +++ /dev/null @@ -1,199 +0,0 @@ -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 - mikrotik.brunner.ninja/router: newgw - mikrotik.brunner.ninja/forwardings: >- - [{"name":"3478-tcp","servicePort":"tcp","dstAddressList":"WANs"},{"name":"5349-tcp","servicePort":"tcp-tls","dstAddressList":"WANs"}] -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 - mikrotik.brunner.ninja/router: newgw - mikrotik.brunner.ninja/forwardings: >- - [{"name":"3478-udp","servicePort":"udp","dstAddressList":"WANs"},{"name":"5349-udp","servicePort":"udp-tls","dstAddressList":"WANs"}] -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 deleted file mode 100755 index 67bd403..0000000 --- a/create-ci-kubeconfig.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/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 deleted file mode 100755 index 640bebf..0000000 --- a/install.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/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."