# Deploy an edge tunnel gateway with the edge-tunnel-gateway template

Source: https://docs.quake.ai/resources/deployments/deploy-edge-tunnel-gateway-template
Markdown: https://docs.quake.ai/resources/deployments/deploy-edge-tunnel-gateway-template.md

---

# Deploy an edge tunnel gateway with the edge-tunnel-gateway template

Stand up a [Pangolin](https://github.com/fosrl/pangolin) tunnel endpoint on one Quake AI instance using the [validated OpenTofu template](/docs/platform/validation#how-infrastructure-templates-are-checked) `edge-tunnel-gateway`. You apply the template, complete the Pangolin dashboard setup, connect a Newt tunnel client from a private machine, register a resource with TLS on a domain, add an access policy, and confirm the service is reachable only through the endpoint.

The tunnel endpoint holds the only public floating IP. The private service stays behind a subnet with no inbound ports open and no address on the internet. You operate the endpoint and the tunnel client yourself; this is a self-hosted tunnel gateway you run in one region, not a global edge network or DDoS scrubber.

<Figure size="md" caption="What you'll build: Pangolin on an endpoint VM with a floating IP routes authenticated HTTPS over a WireGuard tunnel to a private service reached by Newt">

```d2
direction: right

client: Browser {shape: person}
fip: Floating IP\n80 / 443\n51820 UDP
endpoint: Endpoint VM\nPangolin {
  traefik: Traefik
  gerbil: Gerbil\nWireGuard
}
private: Private VM\nno floating IP {
  newt: Newt client
  app: App :8080
  newt -> app: localhost
}

client -> fip: HTTPS
fip -> endpoint.traefik
endpoint.gerbil -> private.newt: WireGuard tunnel
endpoint.traefik -> private.app: proxied resource
```

</Figure>

<PricingCompanion
  components={[
    { kind: "template", slug: "edge-tunnel-gateway", required: true },
    { kind: "primitive", required: true, label: "Private service host (Newt client)", vm: { flavor: "s1a.small", count: 1 } },
  ]}
/>

## Prerequisites

You need:

- OpenTofu 1.6.0 or later (or Terraform 1.6.0 or later) installed locally.
- Your OpenStack credentials sourced into the shell (`source openrc.sh`). See [the OpenStack CLI guide](/docs/tools/openstack-cli).
- An SSH keypair that already exists in your project. Record its name for the `key_name` variable.
- A copy of the `edge-tunnel-gateway` template directory from [the template reference page](/resources/iac-templates/edge-tunnel-gateway).
- Two hostnames on a domain you control: one for the Pangolin dashboard (for example `pangolin.example.com`) and one for the resource you expose (for example `app.example.com`). Both DNS **A records** point at the endpoint floating IP. Follow [How to point a domain at a Quake AI resource](/docs/network/how-to/point-domain-to-quake-ai).

## Step 1: Apply the tunnel endpoint template

Copy the template's example variables file and set `key_name`, `domain`, `base_domain`, and `letsencrypt_email`:

```bash
cp terraform.tfvars.example terraform.tfvars
```

```hcl
key_name            = "YOUR_KEY_NAME"
domain              = "pangolin.example.com"
base_domain         = "example.com"
letsencrypt_email   = "you@example.com"
```

Point `pangolin.example.com` at the floating IP before Traefik requests a certificate. If DNS is not ready yet, apply with `domain` empty first, test over HTTP on the floating IP, then update `domain` and re-apply.

Initialize, preview, and apply:

```bash
tofu init
tofu plan
tofu apply
```

OpenTofu provisions a private network, router, security group, data volume, endpoint instance, and floating IP. cloud-init installs Docker, starts Pangolin, Gerbil, and Traefik, generates a server secret on first boot, and writes setup instructions to `/root/tunnel-admin-credentials`.

Record the outputs:

```bash
tofu output floating_ip
tofu output dashboard_url
tofu output tunnel_endpoint
tofu output admin_hint
```

Wait three to five minutes for cloud-init and container health checks to finish.

## Step 2: Complete Pangolin initial setup

No admin credential ships in the repository. SSH to the endpoint and read the generated credentials file:

```bash
ssh ubuntu@YOUR_FLOATING_IP 'sudo cat /root/tunnel-admin-credentials'
```

Note the `dashboard_url`, `setup_token`, and `tunnel_endpoint` values.

In a browser, open the dashboard URL from the credentials file (for example `https://pangolin.example.com/auth/initial-setup`). Paste the setup token, create the admin account, and sign in.

Port 3001 is not open in the endpoint security group. If you need the raw dashboard API during troubleshooting, open an SSH tunnel:

```bash
ssh -L 3001:127.0.0.1:3001 ubuntu@YOUR_FLOATING_IP
```

## Step 3: Stand up a private service and connect Newt

This walkthrough runs the tunnel client on a private Quake AI instance with no floating IP. The same Newt container runs on a home lab or any machine behind CGNAT that has outbound internet access.

1. Create a security group for the private host. Allow SSH from your workstation while you configure it; do not attach a floating IP.

```bash
openstack security group create tunnel-private-sg \
  --description "Private service host; no inbound application ports from the internet"
openstack security group rule create \
  --protocol tcp --dst-port 22 --remote-ip YOUR_IP/32 tunnel-private-sg
```

2. Launch a small instance on any private network that reaches the internet outbound (the template's `edge-tunnel-gateway-net` works). Serve a test page on port 8080:

```bash
cat > private-cloud-init.yaml <<'EOF'
#cloud-config
package_update: true
packages:
  - nginx
runcmd:
  - |
    echo 'private app ok' > /var/www/html/index.html
    printf '%s\n' 'server {' '  listen 8080;' '  root /var/www/html;' '}' > /etc/nginx/sites-available/default
    systemctl restart nginx
EOF

openstack server create \
  --flavor s1a.small \
  --image Ubuntu-24.04 \
  --key-name YOUR_KEY_NAME \
  --network edge-tunnel-gateway-net \
  --security-group tunnel-private-sg \
  --user-data private-cloud-init.yaml \
  tunnel-private
```

Wait until the instance reports `ACTIVE`. SSH to the private host and confirm the app responds locally:

```bash
curl -s http://127.0.0.1:8080/
```

The response body should include `private app ok`.

3. In the Pangolin dashboard, create an organization if prompted, then create a **Site** for your private network. Pangolin prints a site ID and secret for the Newt client.

4. On the private host, run Newt with the site credentials and your dashboard URL:

```bash
docker run -d --name newt --restart unless-stopped --network host \
  -e PANGOLIN_ENDPOINT=https://pangolin.example.com \
  -e NEWT_ID=YOUR_SITE_ID \
  -e NEWT_SECRET=YOUR_SITE_SECRET \
  fosrl/newt
```

Return to the dashboard and confirm the site reports **Online**. Newt maintains an outbound WireGuard tunnel to `tunnel_endpoint` (UDP port 51820 on the floating IP).

## Step 4: Register a resource with TLS

In the Pangolin dashboard, add a **Resource** (or **Application**) for the private service:

1. Set the public hostname to `app.example.com` (a name under your `base_domain`).
2. Set the internal target to `http://127.0.0.1:8080` on the site where Newt runs.
3. Enable HTTPS for the resource. Traefik on the endpoint obtains a certificate through the HTTP-01 challenge once DNS resolves.

Create a DNS **A record** for `app.example.com` pointing at `YOUR_FLOATING_IP`. Wait until it resolves:

```bash
dig +short app.example.com
```

From your workstation, request the resource hostname. Pangolin should route traffic over the tunnel:

```bash
curl -s -o /dev/null -w "%{http_code}\n" https://app.example.com/
```

Until you add an access policy in the next step, the status code may be `200` or `302` depending on your resource defaults. A `502` usually means Newt is offline or the internal target is wrong; check the site status in the dashboard and `docker logs newt` on the private host.

## Step 5: Add an access policy

In the Pangolin dashboard, open the resource you created and attach an access policy. For a first deployment, **One-time PIN** or **Email PIN** is enough to confirm identity-aware access without wiring an OIDC provider.

Save the policy. Unauthenticated browser requests should redirect to the Pangolin login or PIN flow instead of reaching the app directly.

Verify from your workstation without a session:

```bash
curl -s -o /dev/null -w "%{http_code}\n" https://app.example.com/
```

The status code should be `302` or `401`, not `200` with the app body. Sign in through a browser, complete the PIN or SSO step Pangolin presents, then load `https://app.example.com/` again. The page should show `private app ok`.

## Step 6: Confirm the private service is not reachable directly

The private host has no floating IP and no inbound application ports open to the internet. Confirm you cannot reach the app on the private address from your workstation:

```bash
curl -s -o /dev/null -w "%{http_code}\n" --connect-timeout 3 http://PRIVATE_INSTANCE_IP:8080/ || echo "unreachable"
```

The command should time out or fail to connect. Traffic to the app flows only through the authenticated tunnel endpoint at `https://app.example.com/`.

If you run Newt on a home network instead of a Quake AI private instance, the same check applies: the home router keeps port 8080 closed inbound; only the outbound tunnel carries traffic.

## What you built

- **Applied the `edge-tunnel-gateway` template** to provision a private network, endpoint security group, data volume, Pangolin stack, and floating IP
- **Completed Pangolin initial setup** and signed in to the dashboard
- **Connected Newt** from a private host over an outbound WireGuard tunnel
- **Registered a resource** at `app.example.com` with TLS terminated at the endpoint
- **Added an access policy** so unauthenticated requests do not reach the private app
- **Confirmed the private service** is not reachable directly from the internet

## Scope of this deployment

This endpoint runs in one region on a VM you operate. It exposes a service through your Quake AI VM; it is not a global edge network and it does not absorb volumetric DDoS traffic. Quake AI has no anycast, no global PoPs, and no first-party CDN. For geographic distribution and edge absorption, [front the origin with a third-party CDN](/docs/network/how-to/front-with-cdn).

The walkthrough adds a second small instance for the private service host; that VM is not part of the OpenTofu template. Newt also runs on a home lab or any outbound-only network when you prefer not to host the private side on Quake AI.

## Next steps

- [Edge tunnel gateway template](/resources/iac-templates/edge-tunnel-gateway): parameters, ports, and resource map
- [Edge reverse proxy template](/resources/iac-templates/edge-reverse-proxy): TLS termination without tunneling when both sides sit on Quake AI
- [Edge WAF template](/resources/iac-templates/edge-waf): L7 filtering on a public front door
- [API gateway template](/resources/iac-templates/api-gateway): rate limits and key auth for APIs
- [Security hardening checklist](/docs/security/hardening-checklist): audit security groups and floating IP usage

## Clean up

When you no longer need the deployment, destroy the OpenTofu stack and delete the private host you created in step 3:

```bash
tofu destroy
openstack server delete tunnel-private
openstack security group delete tunnel-private-sg
```

On the private host before deletion, stop Newt with `docker rm -f newt`. Remove DNS **A records** for `pangolin.example.com` and `app.example.com`.
