MVP
This commit is contained in:
@@ -8,14 +8,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
TF_VAR_gcp_project: ${{ vars.GCP_PROJECT || 'stoat-burrow' }}
|
||||
TF_VAR_gcp_region: ${{ vars.GCP_REGION || 'us-west1' }}
|
||||
TF_VAR_gcp_zone: ${{ vars.GCP_ZONE || 'us-west1-b' }}
|
||||
TF_VAR_gcp_project: ${{ secrets.GCP_PROJECT }}
|
||||
TF_VAR_gcp_region: ${{ vars.GCP_REGION }}
|
||||
TF_VAR_gcp_zone: ${{ vars.GCP_ZONE }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -49,16 +45,12 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cat > backend.hcl <<EOF
|
||||
bucket = "stoat-tofu-state"
|
||||
key = "webrtc-relay/terraform.tfstate"
|
||||
region = "us-east-2"
|
||||
endpoints = {
|
||||
s3 = "https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com"
|
||||
s3 = ${{ secrets.R2_ENDPOINT_URL }}
|
||||
}
|
||||
skip_credentials_validation = true
|
||||
skip_region_validation = true
|
||||
skip_requesting_account_id = true
|
||||
skip_s3_checksum = true
|
||||
access_key = "${{ secrets.R2_ACCESS_KEY_ID }}"
|
||||
secret_key = "${{ secrets.R2_SECRET_ACCESS_KEY }}"
|
||||
EOF
|
||||
|
||||
- name: Tofu init and apply
|
||||
@@ -68,18 +60,18 @@ jobs:
|
||||
run: |
|
||||
tofu init -backend-config=backend.hcl
|
||||
tofu apply -auto-approve -input=false
|
||||
echo "STATIC_IP=$(tofu output -raw relay_ip)" >> "${GITHUB_ENV}"
|
||||
echo "${{ secrets.STATIC_IP }}=$(tofu output -raw relay_ip)" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Wait for SSH
|
||||
shell: bash
|
||||
run: |
|
||||
for attempt in {1..30}; do
|
||||
if nc -z -w 5 "${STATIC_IP}" 22; then
|
||||
if nc -z -w 5 "${${{ secrets.STATIC_IP }}}" 22; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "SSH did not become reachable on ${STATIC_IP}" >&2
|
||||
echo "SSH did not become reachable on ${${{ secrets.STATIC_IP }}}" >&2
|
||||
exit 1
|
||||
|
||||
- name: Run Ansible Playbook
|
||||
@@ -90,13 +82,13 @@ jobs:
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
inventory: |
|
||||
[turn_nodes]
|
||||
${{ env.STATIC_IP }} ansible_user=ubuntu
|
||||
${{ secrets.STATIC_IP }} ansible_user=ubuntu
|
||||
options: --ssh-common-args='-o StrictHostKeyChecking=no'
|
||||
|
||||
- name: SCP compose and config files
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ env.STATIC_IP }}
|
||||
host: ${{ secrets.STATIC_IP }}
|
||||
username: ubuntu
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
source: "compose/*"
|
||||
@@ -106,22 +98,18 @@ jobs:
|
||||
- name: Deploy Podman compose stack
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ env.STATIC_IP }}
|
||||
host: ${{ secrets.STATIC_IP }}
|
||||
username: ubuntu
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
set -eu
|
||||
cd /opt/stoat-turn
|
||||
cat > .env <<EOF
|
||||
STATIC_IP=${{ env.STATIC_IP }}
|
||||
STATIC_IP = ${{ secrets.STATIC_IP }}
|
||||
GITHUB_REPOSITORY_OWNER=${{ github.repository_owner }}
|
||||
TURN_REALM=${{ vars.TURN_REALM || 'turn.example.com' }}
|
||||
TURN_REALM=${{ secrets.TURN_REALM }}
|
||||
TURN_SHARED_SECRET=${{ secrets.TURN_SHARED_SECRET }}
|
||||
CADDY_DOMAIN=${{ vars.CADDY_DOMAIN || 'relay.example.com' }}
|
||||
CADDY_EMAIL=${{ vars.CADDY_EMAIL || 'ops@example.com' }}
|
||||
STOAT_UPSTREAM=${{ vars.STOAT_UPSTREAM || 'http://host.containers.internal:8080' }}
|
||||
TURN_TLS_CERT_FILE=${{ vars.TURN_TLS_CERT_FILE }}
|
||||
TURN_TLS_KEY_FILE=${{ vars.TURN_TLS_KEY_FILE }}
|
||||
CADDY_EMAIL=${{ secrets.CADDY_EMAIL }}
|
||||
EOF
|
||||
|
||||
printf '%s\n' "${{ secrets.GITHUB_TOKEN }}" | podman login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# WebRTC Outpost
|
||||
|
||||
Configuration repository for a Coturn relay that supports a Stoat deployment on GCP. The stack provisions a `relay-main` Ubuntu 24.04 instance, hardens the host with `nftables`, `fail2ban`, and `unattended-upgrades`, then deploys Coturn plus a Coraza-enabled Caddy reverse proxy with Podman Compose.
|
||||
Configuration repository for a Coturn relay that supports a Stoat deployment on GCP. The stack provisions a `relay-main` Ubuntu 24.04 instance, hardens the host with `nftables`, `fail2ban`, and `unattended-upgrades`, then deploys Coturn plus a Coraza-enabled Caddy service with Podman Compose. Caddy handles ACME certificate issuance, HTTPS health checks, and deny-by-default web responses on the TURN hostname. Coturn handles STUN and TURN traffic directly on `3478` and `5349`, reusing the certificate that Caddy stores in the shared data volume.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
@@ -25,20 +25,22 @@ Configuration repository for a Coturn relay that supports a Stoat deployment on
|
||||
- `GCP_REGION`
|
||||
- `GCP_ZONE`
|
||||
- `TURN_REALM`
|
||||
- `CADDY_DOMAIN`
|
||||
- `CADDY_EMAIL`
|
||||
- `STOAT_UPSTREAM`
|
||||
- `TURN_TLS_CERT_FILE`
|
||||
- `TURN_TLS_KEY_FILE`
|
||||
|
||||
## Notes
|
||||
|
||||
- The OpenTofu S3 backend is configured at deploy time so the Cloudflare R2 endpoint does not need to be committed to the repository.
|
||||
- The Google provider reads service account credentials from the standard `GOOGLE_APPLICATION_CREDENTIALS` shell environment variable. Set it to the JSON key file path for local `tofu` runs.
|
||||
- The custom Coturn image renders runtime settings from environment variables before starting `turnserver`.
|
||||
- `TURN_TLS_CERT_FILE` and `TURN_TLS_KEY_FILE` are optional. If they are omitted, Coturn starts on `3478` only and skips the `5349` TLS listener.
|
||||
- The custom Coturn image waits for the Caddy-managed certificate for `TURN_REALM` to appear in the shared `caddy_data` volume before starting the TLS listener on `5349`.
|
||||
- Caddy on the relay host only answers `/health` and returns `403` for other HTTPS requests. TURN and STUN traffic does not pass through Caddy; Coturn receives it directly through host networking.
|
||||
- The Ansible playbook lowers `net.ipv4.ip_unprivileged_port_start` to `80` so a rootless Podman-managed Caddy container can bind to `80` and `443`.
|
||||
|
||||
## DNS Setup
|
||||
|
||||
- Create a DNS `A` record so the hostname used by `TURN_REALM` resolves to the OpenTofu-provisioned `relay_ip`.
|
||||
- If you use Cloudflare, keep that record set to DNS-only. The orange-cloud proxy does not support TURN or STUN over UDP.
|
||||
- Optional SRV records can advertise the default ports: `_stun._udp` on `3478`, `_turn._udp` on `3478`, and `_turns._tcp` on `5349` for the same hostname.
|
||||
|
||||
## Local OpenTofu Usage
|
||||
|
||||
Export the Google credentials path and required OpenTofu variables before running `tofu` locally:
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
STATIC_IP=203.0.113.10
|
||||
STATIC_IP=0.0.0.0
|
||||
GITHUB_REPOSITORY_OWNER=example-owner
|
||||
TURN_REALM=turn.example.com
|
||||
TURN_SHARED_SECRET=replace-me
|
||||
CADDY_DOMAIN=relay.example.com
|
||||
CADDY_EMAIL=ops@example.com
|
||||
STOAT_UPSTREAM=http://host.containers.internal:8080
|
||||
TURN_TLS_CERT_FILE=
|
||||
TURN_TLS_KEY_FILE=
|
||||
TURN_TLS_LISTENING_PORT=5349
|
||||
|
||||
+6
-5
@@ -4,7 +4,7 @@
|
||||
admin off
|
||||
}
|
||||
|
||||
{$CADDY_DOMAIN:localhost} {
|
||||
{$TURN_REALM:localhost} {
|
||||
log {
|
||||
output stdout
|
||||
format console
|
||||
@@ -18,9 +18,6 @@
|
||||
Referrer-Policy no-referrer
|
||||
}
|
||||
|
||||
@health path /healthz
|
||||
respond @health 200 "ok"
|
||||
|
||||
coraza_waf {
|
||||
load_owasp_crs
|
||||
directives `
|
||||
@@ -30,5 +27,9 @@
|
||||
`
|
||||
}
|
||||
|
||||
reverse_proxy {$STOAT_UPSTREAM:http://host.containers.internal:8080}
|
||||
route {
|
||||
@health path /health /healthz
|
||||
respond @health "Stoat Relay is Online" 200
|
||||
respond 403
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ services:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
environment:
|
||||
CADDY_DOMAIN: ${CADDY_DOMAIN}
|
||||
CADDY_EMAIL: ${CADDY_EMAIL}
|
||||
STOAT_UPSTREAM: ${STOAT_UPSTREAM}
|
||||
TURN_REALM: ${TURN_REALM}
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
@@ -19,17 +18,16 @@ services:
|
||||
image: ghcr.io/${GITHUB_REPOSITORY_OWNER}/stoat-coturn:4.9.0-trixie
|
||||
container_name: coturn-relay
|
||||
restart: always
|
||||
depends_on:
|
||||
- caddy
|
||||
network_mode: host
|
||||
environment:
|
||||
EXTERNAL_IP: ${STATIC_IP}
|
||||
TURN_REALM: ${TURN_REALM}
|
||||
TURN_SHARED_SECRET: ${TURN_SHARED_SECRET}
|
||||
TURN_TLS_CERT_FILE: ${TURN_TLS_CERT_FILE:-}
|
||||
TURN_TLS_KEY_FILE: ${TURN_TLS_KEY_FILE:-}
|
||||
TURN_TLS_LISTENING_PORT: ${TURN_TLS_LISTENING_PORT:-5349}
|
||||
volumes:
|
||||
- ./turnserver.conf:/etc/coturn/turnserver.conf:ro
|
||||
- caddy_data:/caddy-data:ro
|
||||
- caddy_data:/caddy-certs:ro
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
|
||||
@@ -3,6 +3,7 @@ use-auth-secret
|
||||
lt-cred-mech
|
||||
|
||||
listening-port=3478
|
||||
tls-listening-port=5349
|
||||
min-port=49152
|
||||
max-port=65535
|
||||
|
||||
@@ -10,3 +11,5 @@ no-cli
|
||||
stale-nonce=600
|
||||
no-loopback-peers
|
||||
no-multicast-peers
|
||||
no-stdout-log
|
||||
log-file=/var/log/turnserver.log
|
||||
|
||||
@@ -3,6 +3,7 @@ FROM coturn:4.9.0-trixie
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
|
||||
|
||||
@@ -3,26 +3,64 @@ set -eu
|
||||
|
||||
conf_source="/etc/coturn/turnserver.conf"
|
||||
conf_rendered="/tmp/turnserver.conf"
|
||||
caddy_cert_root="/caddy-certs/caddy/certificates"
|
||||
|
||||
: "${TURN_REALM:?TURN_REALM is required}"
|
||||
: "${TURN_SHARED_SECRET:?TURN_SHARED_SECRET is required}"
|
||||
|
||||
discover_tls_files() {
|
||||
cert_file=""
|
||||
key_file=""
|
||||
|
||||
if [ -d "${caddy_cert_root}" ]; then
|
||||
cert_file=$(find "${caddy_cert_root}" -path "*/${TURN_REALM}/${TURN_REALM}.crt" -print -quit)
|
||||
key_file=$(find "${caddy_cert_root}" -path "*/${TURN_REALM}/${TURN_REALM}.key" -print -quit)
|
||||
fi
|
||||
|
||||
if [ -n "${cert_file}" ] && [ -n "${key_file}" ]; then
|
||||
printf '%s\n%s\n' "${cert_file}" "${key_file}"
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_tls_files() {
|
||||
elapsed=0
|
||||
interval=5
|
||||
timeout=300
|
||||
|
||||
while [ "${elapsed}" -le "${timeout}" ]; do
|
||||
tls_files=$(discover_tls_files)
|
||||
if [ -n "${tls_files}" ]; then
|
||||
printf '%s\n' "${tls_files}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "${elapsed}" -eq "${timeout}" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
echo "Waiting for Caddy certificate files for ${TURN_REALM} in ${caddy_cert_root}..." >&2
|
||||
sleep "${interval}"
|
||||
elapsed=$((elapsed + interval))
|
||||
done
|
||||
|
||||
echo "Timed out waiting for Caddy certificate files for ${TURN_REALM} in ${caddy_cert_root}." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
tls_files=$(wait_for_tls_files)
|
||||
tls_cert_file=$(printf '%s\n' "${tls_files}" | sed -n '1p')
|
||||
tls_key_file=$(printf '%s\n' "${tls_files}" | sed -n '2p')
|
||||
|
||||
cp "${conf_source}" "${conf_rendered}"
|
||||
|
||||
{
|
||||
echo
|
||||
echo "realm=${TURN_REALM}"
|
||||
echo "static-auth-secret=${TURN_SHARED_SECRET}"
|
||||
echo "cert=${tls_cert_file}"
|
||||
echo "pkey=${tls_key_file}"
|
||||
} >> "${conf_rendered}"
|
||||
|
||||
if [ -n "${TURN_TLS_CERT_FILE:-}" ] && [ -n "${TURN_TLS_KEY_FILE:-}" ]; then
|
||||
{
|
||||
echo "tls-listening-port=${TURN_TLS_LISTENING_PORT:-5349}"
|
||||
echo "cert=${TURN_TLS_CERT_FILE}"
|
||||
echo "pkey=${TURN_TLS_KEY_FILE}"
|
||||
} >> "${conf_rendered}"
|
||||
fi
|
||||
|
||||
set -- turnserver -n -c "${conf_rendered}"
|
||||
|
||||
if [ -n "${EXTERNAL_IP:-}" ]; then
|
||||
|
||||
Reference in New Issue
Block a user