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(orapp-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(orAWS 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 issues — warp-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.
-
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. -
(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.comThen delete the corresponding key in GitHub → Settings → SSH and GPG keys.
-
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 |