Compare commits
1
Commits
main
..
fix-ssh-key
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd23627547 |
@@ -32,12 +32,12 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Coturn image
|
||||
- name: Build and push TURN server image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./docker/coturn
|
||||
context: ./docker/turn-server
|
||||
push: true
|
||||
tags: ghcr.io/${{ env.OWNER_LC }}/stoat-coturn:4.9.0-trixie
|
||||
tags: ghcr.io/${{ env.OWNER_LC }}/stoat-turn-server:4.0.1
|
||||
|
||||
- name: Build and push Caddy WAF image
|
||||
uses: docker/build-push-action@v5
|
||||
|
||||
@@ -8,8 +8,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
env:
|
||||
TF_VAR_gcp_project: ${{ secrets.GCP_PROJECT }}
|
||||
TF_VAR_gcp_project: ${{ vars.GCP_PROJECT }}
|
||||
TF_VAR_gcp_region: ${{ vars.GCP_REGION }}
|
||||
TF_VAR_gcp_zone: ${{ vars.GCP_ZONE }}
|
||||
|
||||
@@ -17,17 +18,56 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate deploy configuration
|
||||
shell: bash
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||
R2_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT_URL }}
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
TURN_SHARED_SECRET: ${{ secrets.TURN_SHARED_SECRET }}
|
||||
GCP_PROJECT: ${{ vars.GCP_PROJECT }}
|
||||
GCP_REGION: ${{ vars.GCP_REGION }}
|
||||
GCP_ZONE: ${{ vars.GCP_ZONE }}
|
||||
TURN_REALM: ${{ vars.TURN_REALM }}
|
||||
CADDY_EMAIL: ${{ vars.CADDY_EMAIL }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
require_value() {
|
||||
local kind="$1"
|
||||
local name="$2"
|
||||
local value="$3"
|
||||
|
||||
if [ -z "${value}" ]; then
|
||||
echo "::error::Missing required GitHub ${kind} ${name}." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_value "secret" "SSH_PRIVATE_KEY" "${SSH_PRIVATE_KEY}"
|
||||
require_value "secret" "GCP_SA_KEY" "${GCP_SA_KEY}"
|
||||
require_value "secret" "R2_ENDPOINT_URL" "${R2_ENDPOINT_URL}"
|
||||
require_value "secret" "R2_ACCESS_KEY_ID" "${R2_ACCESS_KEY_ID}"
|
||||
require_value "secret" "R2_SECRET_ACCESS_KEY" "${R2_SECRET_ACCESS_KEY}"
|
||||
require_value "secret" "TURN_SHARED_SECRET" "${TURN_SHARED_SECRET}"
|
||||
require_value "variable" "GCP_PROJECT" "${GCP_PROJECT}"
|
||||
require_value "variable" "GCP_REGION" "${GCP_REGION}"
|
||||
require_value "variable" "GCP_ZONE" "${GCP_ZONE}"
|
||||
require_value "variable" "TURN_REALM" "${TURN_REALM}"
|
||||
require_value "variable" "CADDY_EMAIL" "${CADDY_EMAIL}"
|
||||
|
||||
- name: Prepare SSH key for provisioning
|
||||
shell: bash
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
install -m 700 -d "${HOME}/.ssh"
|
||||
printf '%s\n' "${{ secrets.SSH_PRIVATE_KEY }}" > "${HOME}/.ssh/id_ed25519"
|
||||
printf '%s\n' "${SSH_PRIVATE_KEY}" | tr -d '\r' > "${HOME}/.ssh/id_ed25519"
|
||||
chmod 600 "${HOME}/.ssh/id_ed25519"
|
||||
{
|
||||
echo "TF_VAR_admin_ssh_public_key<<EOF"
|
||||
ssh-keygen -y -f "${HOME}/.ssh/id_ed25519"
|
||||
echo "EOF"
|
||||
} >> "${GITHUB_ENV}"
|
||||
admin_ssh_public_key="$(ssh-keygen -y -f "${HOME}/.ssh/id_ed25519")"
|
||||
printf 'TF_VAR_admin_ssh_public_key=%s\n' "${admin_ssh_public_key}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Prepare Google credentials file
|
||||
shell: bash
|
||||
@@ -47,7 +87,7 @@ jobs:
|
||||
cat > backend.hcl <<EOF
|
||||
key = "webrtc-relay/terraform.tfstate"
|
||||
endpoints = {
|
||||
s3 = ${{ secrets.R2_ENDPOINT_URL }}
|
||||
s3 = "${{ secrets.R2_ENDPOINT_URL }}"
|
||||
}
|
||||
access_key = "${{ secrets.R2_ACCESS_KEY_ID }}"
|
||||
secret_key = "${{ secrets.R2_SECRET_ACCESS_KEY }}"
|
||||
@@ -60,18 +100,20 @@ jobs:
|
||||
run: |
|
||||
tofu init -backend-config=backend.hcl
|
||||
tofu apply -auto-approve -input=false
|
||||
echo "${{ secrets.STATIC_IP }}=$(tofu output -raw relay_ip)" >> "${GITHUB_ENV}"
|
||||
relay_ip="$(tofu output -raw relay_ip)"
|
||||
echo "STATIC_IP=${relay_ip}" >> "${GITHUB_ENV}"
|
||||
echo "relay_ip=${relay_ip}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Wait for SSH
|
||||
shell: bash
|
||||
run: |
|
||||
for attempt in {1..30}; do
|
||||
if nc -z -w 5 "${${{ secrets.STATIC_IP }}}" 22; then
|
||||
if nc -z -w 5 "${STATIC_IP}" 22; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "SSH did not become reachable on ${${{ secrets.STATIC_IP }}}" >&2
|
||||
echo "SSH did not become reachable on ${STATIC_IP}" >&2
|
||||
exit 1
|
||||
|
||||
- name: Run Ansible Playbook
|
||||
@@ -82,13 +124,13 @@ jobs:
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
inventory: |
|
||||
[turn_nodes]
|
||||
${{ secrets.STATIC_IP }} ansible_user=ubuntu
|
||||
${{ steps.tofu.outputs.relay_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: ${{ secrets.STATIC_IP }}
|
||||
host: ${{ steps.tofu.outputs.relay_ip }}
|
||||
username: ubuntu
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
source: "compose/*"
|
||||
@@ -98,20 +140,21 @@ jobs:
|
||||
- name: Deploy Podman compose stack
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.STATIC_IP }}
|
||||
host: ${{ steps.tofu.outputs.relay_ip }}
|
||||
username: ubuntu
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
set -eu
|
||||
cd /opt/stoat-turn
|
||||
owner_lc="$(printf '%s' "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')"
|
||||
cat > .env <<EOF
|
||||
STATIC_IP = ${{ secrets.STATIC_IP }}
|
||||
GITHUB_REPOSITORY_OWNER=${{ github.repository_owner }}
|
||||
TURN_REALM=${{ secrets.TURN_REALM }}
|
||||
STATIC_IP=${{ steps.tofu.outputs.relay_ip }}
|
||||
GITHUB_REPOSITORY_OWNER=${owner_lc}
|
||||
TURN_REALM=${{ vars.TURN_REALM }}
|
||||
TURN_SHARED_SECRET=${{ secrets.TURN_SHARED_SECRET }}
|
||||
CADDY_EMAIL=${{ secrets.CADDY_EMAIL }}
|
||||
CADDY_EMAIL=${{ vars.CADDY_EMAIL }}
|
||||
EOF
|
||||
|
||||
printf '%s\n' "${{ secrets.GITHUB_TOKEN }}" | podman login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||
podman compose pull
|
||||
podman compose up -d
|
||||
podman compose up -d --remove-orphans
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# 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 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.
|
||||
Configuration repository for a TURN 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 a `turn-rs`-based TURN server 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. The TURN server handles STUN and TURN traffic on `3478` and TURN over TLS on `5349/TCP`, reusing the certificate that Caddy stores in the shared data volume.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
- `tofu/`: OpenTofu infrastructure for the static IP, VM, and GCP firewall rule.
|
||||
- `ansible/`: Host preparation and hardening for Ubuntu 24.04.
|
||||
- `compose/`: Runtime configuration for Coturn and Caddy.
|
||||
- `docker/`: Custom images for Coturn and Caddy.
|
||||
- `compose/`: Runtime configuration for the TURN server and Caddy.
|
||||
- `docker/`: Custom images for the TURN server wrapper and Caddy.
|
||||
- `.github/workflows/`: CI workflows for building images and deploying the stack.
|
||||
|
||||
## Required GitHub Secrets
|
||||
@@ -31,8 +31,8 @@ Configuration repository for a Coturn relay that supports a Stoat deployment on
|
||||
|
||||
- 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 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 custom TURN wrapper image is built from `ghcr.io/mycrl/turn-server:4.0.1`. It waits for the Caddy-managed certificate for `TURN_REALM`, renders the final TOML config, and then starts `turn-server`.
|
||||
- Caddy on the relay host only answers `/health` and returns `403` for other HTTPS requests. TURN and STUN traffic does not pass through Caddy; the TURN server 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
|
||||
|
||||
@@ -15,7 +15,7 @@ table inet filter {
|
||||
|
||||
tcp dport 22 accept
|
||||
tcp dport { 80, 443, 3478, 5349 } accept
|
||||
udp dport { 3478, 5349 } accept
|
||||
udp dport 3478 accept
|
||||
udp dport 49152-65535 accept
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ services:
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
|
||||
coturn:
|
||||
image: ghcr.io/${GITHUB_REPOSITORY_OWNER}/stoat-coturn:4.9.0-trixie
|
||||
container_name: coturn-relay
|
||||
turn:
|
||||
image: ghcr.io/${GITHUB_REPOSITORY_OWNER}/stoat-turn-server:4.0.1
|
||||
container_name: turn-relay
|
||||
restart: always
|
||||
depends_on:
|
||||
- caddy
|
||||
@@ -26,7 +26,7 @@ services:
|
||||
TURN_REALM: ${TURN_REALM}
|
||||
TURN_SHARED_SECRET: ${TURN_SHARED_SECRET}
|
||||
volumes:
|
||||
- ./turnserver.conf:/etc/coturn/turnserver.conf:ro
|
||||
- ./turn-server.toml:/etc/turn-server/config.toml.tmpl:ro
|
||||
- caddy_data:/caddy-certs:ro
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Rendered at container start by docker/turn-server/entrypoint.sh.
|
||||
|
||||
[server]
|
||||
port-range = "49152..65535"
|
||||
realm = "__TURN_REALM__"
|
||||
|
||||
[[server.interfaces]]
|
||||
transport = "udp"
|
||||
listen = "0.0.0.0:3478"
|
||||
external = "__EXTERNAL_IP__:3478"
|
||||
|
||||
[[server.interfaces]]
|
||||
transport = "tcp"
|
||||
listen = "0.0.0.0:3478"
|
||||
external = "__EXTERNAL_IP__:3478"
|
||||
|
||||
[[server.interfaces]]
|
||||
transport = "tcp"
|
||||
listen = "0.0.0.0:5349"
|
||||
external = "__EXTERNAL_IP__:5349"
|
||||
|
||||
[server.interfaces.ssl]
|
||||
private-key = "__TLS_KEY_FILE__"
|
||||
certificate-chain = "__TLS_CERT_FILE__"
|
||||
|
||||
[log]
|
||||
level = "info"
|
||||
stdout = true
|
||||
|
||||
[auth]
|
||||
enable-hooks-auth = false
|
||||
static-auth-secret = "__TURN_SHARED_SECRET__"
|
||||
@@ -1,14 +0,0 @@
|
||||
fingerprint
|
||||
use-auth-secret
|
||||
lt-cred-mech
|
||||
|
||||
listening-port=3478
|
||||
tls-listening-port=5349
|
||||
min-port=49152
|
||||
max-port=65535
|
||||
|
||||
no-cli
|
||||
stale-nonce=600
|
||||
no-loopback-peers
|
||||
no-multicast-peers
|
||||
log-file=stdout
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM coturn/coturn:4.9.0-trixie
|
||||
FROM ghcr.io/mycrl/turn-server:4.0.1
|
||||
|
||||
COPY --chmod=0755 entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
conf_source="/etc/coturn/turnserver.conf"
|
||||
conf_rendered="/tmp/turnserver.conf"
|
||||
conf_template="/etc/turn-server/config.toml.tmpl"
|
||||
conf_rendered="/tmp/turn-server.toml"
|
||||
caddy_cert_root="/caddy-certs/caddy/certificates"
|
||||
|
||||
: "${TURN_REALM:?TURN_REALM is required}"
|
||||
: "${TURN_SHARED_SECRET:?TURN_SHARED_SECRET is required}"
|
||||
: "${EXTERNAL_IP:?EXTERNAL_IP is required}"
|
||||
|
||||
discover_tls_files() {
|
||||
cert_file=""
|
||||
@@ -47,24 +48,28 @@ wait_for_tls_files() {
|
||||
return 1
|
||||
}
|
||||
|
||||
escape_toml_string() {
|
||||
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
|
||||
}
|
||||
|
||||
escape_sed_replacement() {
|
||||
printf '%s' "$1" | sed -e 's/[&|]/\\&/g'
|
||||
}
|
||||
|
||||
render_value() {
|
||||
escape_sed_replacement "$(escape_toml_string "$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}"
|
||||
sed \
|
||||
-e "s|__TURN_REALM__|$(render_value "${TURN_REALM}")|g" \
|
||||
-e "s|__TURN_SHARED_SECRET__|$(render_value "${TURN_SHARED_SECRET}")|g" \
|
||||
-e "s|__EXTERNAL_IP__|$(render_value "${EXTERNAL_IP}")|g" \
|
||||
-e "s|__TLS_CERT_FILE__|$(render_value "${tls_cert_file}")|g" \
|
||||
-e "s|__TLS_KEY_FILE__|$(render_value "${tls_key_file}")|g" \
|
||||
"${conf_template}" > "${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}"
|
||||
|
||||
set -- turnserver -n -c "${conf_rendered}"
|
||||
|
||||
if [ -n "${EXTERNAL_IP:-}" ]; then
|
||||
set -- "$@" --external-ip "${EXTERNAL_IP}"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
exec turn-server --config="${conf_rendered}"
|
||||
+1
-1
@@ -48,7 +48,7 @@ resource "google_compute_firewall" "webrtc_rules" {
|
||||
|
||||
allow {
|
||||
protocol = "udp"
|
||||
ports = ["3478", "5349", "49152-65535"]
|
||||
ports = ["3478", "49152-65535"]
|
||||
}
|
||||
|
||||
target_tags = ["webrtc-outpost", "caddy-web"]
|
||||
|
||||
Reference in New Issue
Block a user