Skip to content

Cloudflare Tunnel

This page configures secure access to the App-name EC2 hosts through Cloudflare Tunnel with Zero Trust authentication — so no public SSH port stays open. It enables admin SSH over the WARP client (short-lived certificates) and GitHub Actions CI/CD over a service token. Run the per-instance steps once per environment after completing the AWS EC2 Setup.

Architecture

flowchart TB
    subgraph ZT["Cloudflare Zero Trust"]
        direction LR
        admin["Admin SSH<br/>(WARP client)"]
        ci["GitHub Actions<br/>(service token)"]
    end
    admin -->|short-lived cert| tunnel
    ci -->|CF-Access-Client-Id/Secret| tunnel
    tunnel["Encrypted Cloudflare Tunnel"] --> ec2
    ec2["AWS EC2 host<br/>cloudflared daemon<br/>SSH CA configured<br/>no inbound port 22"]

Prerequisites

  • Cloudflare account with Zero Trust enabled and <your-domain> on Cloudflare DNS
  • An EC2 instance from AWS EC2 Setup with temporary bootstrap SSH access still attached
  • WARP client installed on admin devices (one.one.one.one)

Part 1 — One-time account setup

These steps are done once per Cloudflare organization. If your org already has them in place, skip straight to Part 2.

1.1 Enroll devices with WARP

In Zero Trust → Team & Resources → Devices → Management → Device enrollment, click Manage, and under Policies add a policy that allows your team's emails (e.g. Emails ending in <your-domain>) to enroll. Save.

1.2 Enable TCP proxy for SSH

In Traffic policies → Traffic settings → Proxy and inspection settings, turn on Allow Secure Web Gateway to proxy traffic and select TCP (optionally UDP). Save.

1.3 Configure split tunnels

In Team & Resources → Devices → Device profiles, open the default profile, and under Split Tunnels switch to Exclude mode. Remove any RFC 1918 range that overlaps your AWS VPC (e.g. remove 172.16.0.0/12) and re-add non-overlapping slices so private routing to the VPC works:

172.16.0.0/13
172.24.0.0/13
172.28.0.0/14

1.4 Add a gateway firewall policy

In Traffic policies → Firewall policies → Network, add a policy named Allow Access to Infrastructure Targets with Destination IP = your VPC CIDR (e.g. 172.31.0.0/16), action Allow. Drag it to the top for precedence.

Part 2 — Per-instance server setup

Repeat for each EC2 instance (test and production).

2.1 Install cloudflared

On the EC2 host:

sudo apt update
sudo apt install -y wget
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
cloudflared --version

2.2 Connect the host to a tunnel

There are two paths here. The Twycis setup reuses one shared tunnel (named AWS-servers) for all instances, so you normally take the reuse path.

In Cloudflare Zero Trust, go to Networks → Connectors, open the existing AWS-servers tunnel, and choose Configure from its menu. You do not create a new tunnel — a single tunnel can serve multiple hosts, each added as its own connector and access target. Continue to Install the connector below.

Only if no shared tunnel exists yet: Networks → Connectors → + Create a tunnel, select Cloudflared, name it (e.g. app-name), and Save tunnel. Then continue to Install the connector.

Install the connector. Under Choose your environment, select Debian, and copy the install command (it embeds your connector token). Run it on the EC2 host:

sudo cloudflared service install <CONNECTOR_TOKEN>
sudo systemctl start cloudflared
sudo systemctl enable cloudflared
sudo systemctl status cloudflared

Back in Cloudflare, wait for the connector to show Connected (~30 seconds).

Add the public hostname (for the web app served by Nginx):

Field Test Production
Subdomain test (root)
Domain <your-domain> <your-domain>
Service type HTTP HTTP
URL <private-ip>:80 <private-ip>:80

This maps test.<your-domain> and <your-domain> to the instance's Nginx on port 80. Cloudflare terminates TLS at the edge; traffic inside the tunnel reaches Nginx as plain HTTP.

2.3 Private network route

The shared AWS-servers tunnel already publishes the VPC CIDR route, so this is normally already done. If you are setting up a brand-new tunnel: find your VPC IPv4 CIDR in AWS Console → VPC (e.g. 172.31.0.0/16), then in Cloudflare open the tunnel, Edit → +CIDR → Add CIDR route, enter the CIDR and a description, and save.

2.4 Add the access target

In Access controls → Targets → Add a target:

  • Hostname: app-name-test (or app-name-prod)
  • IP: the instance's private IP (e.g. 172.31.43.254)
  • Virtual network: Default

On save, copy the SSH CA public key — you will install it on the server in step 2.6.

2.5 Add the infrastructure application

In Access controls → Applications → Add an application → Infrastructure:

  • Name: AWS SSH - Test (or AWS SSH - Production)
  • + Add public hostname: choose the target from 2.4

Add a policy: Allow emails Twycis, selector Emails ending in = <your-domain>, SSH users: ubuntu. Save.

Note

The Cloudflare UI sometimes fails to attach an existing policy to a new infrastructure app. If "Select existing policies" misbehaves, create the policy fresh inside this application instead.

2.6 Configure short-lived certificates on the server

Install the SSH CA public key copied in step 2.4 and tell sshd to trust it:

# Paste the CA public key (one line, starts with ecdsa-sha2-nistp256)
sudo nano /etc/ssh/ca.pub
sudo chmod 644 /etc/ssh/ca.pub

# Trust the CA in sshd
sudo nano /etc/ssh/sshd_config

Add near the top of sshd_config:

PubkeyAuthentication yes
TrustedUserCAKeys /etc/ssh/ca.pub

Then validate and reload:

sudo sshd -t            # no output = OK
sudo systemctl reload ssh

Part 3 — Admin device WARP setup

Do this once per admin device.

3.1 Install and connect WARP

# macOS
brew install --cask cloudflare-warp

On Windows, download from https://one.one.one.one/ and run the installer. Then open WARP → gear → Preferences → Account → Login to Cloudflare Zero Trust, enter the org name (Twycis), and authenticate. WARP's status should show your organization name.

3.2 Verify the connection

warp-cli status                    # Connected
warp-cli registration show         # shows org + account type
warp-cli teams-enrollment-status   # shows enrolled org
ping <ec2-private-ip>              # responds if routing works

3.3 Configure SSH for easy access

Each host is reached over the tunnel by its internal (private) IP — the same <ec2-private-ip> you pinged in 3.2 (find it in the AWS console under the instance's Private IPv4 address). Use that IP as the HostName.

List your targets to confirm routing, then add each host's private IP to ~/.ssh/config:

warp-cli target list
Host app-name-test
    HostName <ec2-private-ip-test>
    User ubuntu

Host app-name-prod
    HostName <ec2-private-ip-prod>
    User ubuntu

Use the private IP, not the hostname

Set HostName to the host's private IP (e.g. 10.0.1.23), not the label shown by warp-cli target list. The WARP tunnel routes the private IP range to your device, so the hostname is not resolvable as an SSH HostName and returns an error.

3.4 First connection

ssh app-name-test

The first connection opens a browser for Cloudflare Access authentication; Cloudflare then issues a short-lived certificate (cached ~12 hours by default) and subsequent connections reuse it automatically. No SSH key is needed.

Part 4 — GitHub Actions CI/CD (service token)

GitHub Actions reaches each host over the tunnel using a service token rather than WARP. See Pipeline Overview for how the workflow consumes these.

4.1 Publish an SSH route for CI

In Networks → Connectors → your tunnel → Published application route → Add an application:

Field Value
Subdomain github-deploy-test (or github-deploy-prod)
Domain <your-domain>
Service type SSH
Service URL localhost:22

4.2 Create the service token

In Access controls → Service credentials → Service Tokens → Create Service Token, name it GitHub Actions Deployment, duration non-expiring (or 12 months), and Generate token.

Copy both values now

The Client ID (...access) and Client Secret are shown only at creation. If you miss the secret, use the token's three-dot menu → Rotate.

4.3 Create the service-auth application

In Access controls → Applications → Add an application → Self-hosted:

  • Name: GitHub Actions - Test (or Production)
  • Subdomain: github-deploy-test, Domain: <your-domain>, Application type: SSH

Add a policy named CI/CD Service Token Access with action Service Auth (not "Allow"), including the Service Token identity GitHub Actions Deployment.

4.4 Add the GitHub secrets

In the repository, under Settings → Secrets and variables → Actions, add:

Secret Source
CF_ACCESS_CLIENT_ID Service token Client ID
CF_ACCESS_CLIENT_SECRET Service token Client Secret
SSH private key The EC2 .pem key (second-layer auth)

See Secrets & Environment for the full secret inventory.

Testing

Admin SSH:

warp-cli status        # Connected
ssh app-name-test      # first run authenticates in browser, then caches

CI/CD: push to the deploy branch (or git commit --allow-empty -m "test deploy" && git push) and confirm the workflow connects and deploys. Then in Zero Trust → Logs → Access, filter by the application and verify successful authentications.

Troubleshooting

SSH certificate authentication fails — verify the CA key and sshd config:

cat /etc/ssh/ca.pub
grep -A2 TrustedUserCAKeys /etc/ssh/sshd_config
sudo sshd -t
sudo systemctl restart ssh

Common causes: the CA key was pasted with line breaks (must be one line), wrong file permissions (should be 644), or sshd was not reloaded.

WARP issueswarp-cli status / warp-cli trace; re-authenticate via WARP menu → Account → sign out / sign in.

Private IP routing not working — confirm WARP is connected, split tunnels exclude your VPC CIDR, the tunnel has the CIDR route, and the gateway firewall policy has high precedence.

Security notes

  • Port 22 is never exposed publicly — no inbound security-group rule is needed once the tunnel is live.
  • Admin certificates auto-expire (default 12 hours).
  • CI/CD uses a service token — rotate it in Cloudflare and GitHub if compromised.
  • All traffic is encrypted through the tunnel, with an audit trail in Cloudflare Logs.

Cleanup — remove bootstrap SSH access

Once the tunnel works, remove the temporary access from AWS EC2 Setup.

  1. Remove the security-group SSH rule. In EC2 → Security Groups → the instance's group → Inbound rules → Edit, delete the port-22 (Allow-SSH-developers-IP) rule and save. Admin SSH now runs over WARP.

  2. (Optional) Remove the GitHub deploy key from the server, if CI/CD handles all deployments and you do not need manual git pull:

    rm -f ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub
    ssh-keygen -R github.com
    

    Then delete the corresponding key in GitHub → Settings → SSH and GPG keys.

  3. Verify that WARP access works and direct access is blocked:

    ssh app-name-test                          # works via WARP
    ssh -i ~/.ssh/app-name-key.pem ubuntu@<public-ip>   # should time out
    

What to keep vs remove

Item Keep Remove
EC2 key pair (.pem) Needed for GitHub Actions CI/CD
Security-group SSH rule (port 22) Remove — no longer needed
GitHub deploy key on server Only if manual git is needed If CI/CD handles everything
WARP on admin devices For ongoing admin SSH