AUTOMATE NOW

Cloudflare Access and Tunnel Configuration Guide

Table of Contents

How the connection flows

The SFTP server opens an outbound connection to Cloudflare and holds it. Nothing dials in. That single fact is what lets you close TCP/22 to the internet and still serve clients.

Figure1: Connection flow using cloudflare
Figure1: Connection flow using cloudflare

Both legs terminate at the Cloudflare edge, and the server’s leg is outbound. No firewall rule publishes port 22, and the server needs no public IP for this path. Existing IP-whitelist access stays the primary route; the tunnel is the backup.

Prerequisites

Before you begin

A Cloudflare account with the target domain on it — the hostname you publish must sit on a zone in that account.

Cloudflare Zero Trust enabled on the same account.

sudo on the Linux SFTP server, and outbound HTTPS (TCP/443) permitted from it to Cloudflare.

A dedicated SFTP user on that server.

Administrative access on the Local development environment that will connect, to install cloudflared there in Phase 6.

Placeholders used throughout

PlaceholderDescription
<HOSTNAME>The public hostname clients will use, e.g. sftp.example.com
<TUNNEL_TOKEN>The token Cloudflare generates in Phase 2. Treat it as a credential.
<YOUR_SFTP_SERVER>The dedicated SFTP account on the Linux server
<CLIENT_ID>Service token Client ID from Phase 5
<CLIENT_SECRET>Service token Client Secret from Phase 5 — shown once

Phase 01

Do this on: Linux SFTP server (Terminal)

Prepare the SFTP server

Cloudflare Tunnel forwards to a service that already works. Everything here must pass locally before any tunnel exists, or a later failure is impossible to attribute.

Confirm SSH is running:

sudo systemctl status ssh


On distributions that name the unit differently:

sudo systemctl status sshd


Confirm it is listening on port 22:

sudo ss -lntp | grep :22


Connect locally:

ssh localhost


Configure authentication for the SFTP user.
Use a dedicated account — <YOUR_SFTP_SERVER> throughout this guide. Either method works:

  • SSH key authentication
  • Password authentication

For key authentication, add the client’s public key to the user’s authorized_keys:

/home/<YOUR_SFTP_SERVER>/.ssh/authorized_keys


Then set the permissions OpenSSH requires:

chmod 700 /home/<YOUR_SFTP_SERVER>/.ssh
chmod 600 /home/<YOUR_SFTP_SERVER>/.ssh/authorized_keys

chown -R <YOUR_SFTP_SERVER>:<YOUR_SFTP_SERVER> /home/<YOUR_SFTP_SERVER>/.ssh

Gate — do not continue past this point

Authenticate as <YOUR_SFTP_SERVER> over localhost and complete a file transfer. If that fails now, it will fail through the tunnel too, and the tunnel will get the blame.

Phase 02

Do this on: Cloudflare dashboard

Create the tunnel

Login Cloudflare dashboard

Go to Networking → Tunnels → Create a tunnel → Cloudflared.

Name the tunnel.
Use a name that identifies the server it terminates on, not the service it carries — you will read it in a list months from now.

Set the tunnel environment:

  • Operating system: Windows, or Linux / Debian
  • Architecture: 64-bit

Copy the generated install command.

Figure 2 : Create a new tunnel  
Figure 2 : Create a new tunnel  

For Linux, Cloudflare produces a command in this form. You will run it in Phase 3 — do not run it yet.

sudo cloudflared service install <TUNNEL_TOKEN>

The token is a credential

Anyone holding <TUNNEL_TOKEN> can register a connector for this tunnel. Do not paste it into a ticket, a chat message, or a shell history you share.

Phase 03

Do this on: Linux SFTP server (Terminal)

Install the connector

The connector is the process that holds the outbound leg open. Install and manage it on your own server, so control over access to the SFTP host stays with you.

Install cloudflared.

c# Add cloudflare gpg key
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null

# Add this repo to your apt repositories
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list

# install cloudflared
sudo apt-get update && sudo apt-get install cloudflared


Match the architecture you selected in Phase 2.

Register the connector.

Run the command Cloudflare generated in Phase 2:

sudo cloudflared service install <TUNNEL_TOKEN>

Verify the service:

sudo systemctl status cloudflared

Expected state:

active (running)


Verify the tunnel in the dashboard.

The tunnel’s status must read HEALTHY. A running service with an unhealthy tunnel means the connector started but could not register — read the logs below.

Read the logs when it does not.
Last 100 lines:

journalctl -u cloudflared --no-pager -n 100

Follow in real time:

journalctl -u cloudflared -f

Service lifecycle reference

ToRun
Startsudo systemctl start cloudflared
Stopsudo systemctl stop cloudflared
Restartsudo systemctl restart cloudflared
Start on bootsudo systemctl enable cloudflared

No inbound rule is needed here

The connector dials out to Cloudflare. Do not open TCP/22 — or any other inbound port — to make this work. If you added an inbound rule while testing, remove it.

Phase 04

Do this on: Cloudflare dashboard

Publish the SSH route

The tunnel is connected but carries nothing yet. A route tells the connector which local service to hand traffic to.

Open the tunnel’s routes.
Networking → Tunnels → your tunnel → Routes → Add route → Published application.

Figure 3 : Create published application
Figure 3 : Create published application

Point the hostname at local SSH.

SettingValue
Hostname<HOSTNAME>
ServiceSSH
URLlocalhost:22


Which is the same as entering the service URL directly:

ssh://localhost:22

localhost is the server’s own loopback

localhost:22 is resolved by the connector on the SFTP server. It is not your machine, and it is not a public address — which is why the SSH daemon never needs to listen on anything else.

Phase 05

Do this on: Cloudflare dashboard

Put Access in front of the route

Right now the hostname is reachable by anyone who knows it. Cloudflare Access adds the identity check that makes the route safe to leave published.

Create the Access application.
Zero Trust → Access controls → Applications → Add an application → Self-hosted.

Figure 4 : Create new self-host application
Figure 4 : Create new self-host application
SettingValue
Application nameBeehexa SFTP Access
Domain<HOSTNAME>


The domain must be the hostname you published in Phase 4, or the policies below will guard nothing.

You need two policies, and they do different jobs

People and machines prove who they are in incompatible ways: one can read an emailed PIN, the other cannot open a browser. Both policies sit on the same application.

Policy A — peoplePolicy B — machines
Who it is forNamed Beehexa staffAutomated server-to-server jobs
ActionAllowService Auth
IncludeEmails: team@beehexa.comService Token: Beehexa SFTP Service
Caller presentsA one-time PIN sent to their inboxClient ID and Client Secret headers
Browser neededYes, once per sessionNo

Policy A — allow access by email

Create the policy.

Figure 5 : Set access policies for self-host application
Figure 5 : Set access policies for self-host application
SettingValue
Policy nameAllow Beehexa SFTP Access
ActionAllow
Include → Emailsteam@beehexa.com


List addresses, not domains
Include specific accounts rather than an entire email domain. A domain rule silently admits every account created on that domain afterwards.

Enable One-Time PIN for the application.

Figure 6: Setup authentication for Cloudflare Access Application
Figure 6: Setup authentication for Cloudflare Access Application

In the Access application: Authentication → Choose available identity providers for this application → select onetimepin.
Two different questions
One-Time PIN decides how a user authenticates. The Access policy decides who is allowed to connect. Enabling the first without the second admits nobody; the second without the first gives your allowed users no way to prove they are themselves.

Policy B — allow access by service token

Create the service token.

Figure 7 : Create service tokens
Figure 7 : Create service tokens

Zero Trust → Access controls → Service credentials → Service Tokens → Create Service Token.

SettingValue
Token nameBeehexa SFTP Service
Duration1 year


Cloudflare then displays a Client ID and a Client Secret.
The secret is shown once
Store both values in your secret manager before leaving the screen. The Client Secret cannot be retrieved again — recovering from a loss means issuing a new token and updating every job that uses it.
A one-year duration expires
Not in the source pages
Put the expiry date in a calendar with a month of warning. An expired service token fails as a refused connection in an automated job, usually at the least convenient moment.

Create the policy.

SettingValue
Policy nameAllow Beehexa SFTP Service
ActionService Auth
Include → Service TokenBeehexa SFTP Service


This lets automated services reach the SFTP server without a one-time PIN and without a browser.

Phase 06

Do this on: Local development environment (Terminal)

Connect from the command line

Entire phase is new — verify before publishing

Neither source page covered the client side, yet page two’s checklist required it. The client reaches the hostname through cloudflared, which handles the Access challenge and then hands a plain SSH stream to your SFTP client.

Two ways to connect — pick before you read on

This phase uses a proxy command: no listening port, one authenticated stream per connection, and it works only with tools that accept a ProxyCommand — the ssh and sftp binaries, and little else. Phase 7 binds a real local port instead, which is what FileZilla, WinSCP, and any application library will need.

Install cloudflared on the client.
The same binary as the server — use the install instructions in Phase 3, picking the tab for the client’s operating system. Nothing else is needed here: on the client cloudflared runs on demand as a proxy command, so do not run service install and do not give it a tunnel token.

Connect as a person.

ssh -o ProxyCommand="cloudflared access ssh --hostname %h" \
  <YOUR_SFTP_SERVER>@<HOSTNAME>

A browser window opens for the one-time PIN. After that the SSH session proceeds normally, and your existing key or password authenticates you to the server itself.

Make it permanent in ~/.ssh/config.

Host <HOSTNAME>
  User <YOUR_SFTP_SERVER>
  
  ProxyCommand cloudflared access ssh --hostname %h

Any SSH-aware tool then works against the hostname directly:

sftp <YOUR_SFTP_SERVER>@<HOSTNAME>

Connect as an automated job.
Pass the service token from Phase 5 to cloudflared. No browser is involved.

sftp -o ProxyCommand="cloudflared access ssh --hostname %h \
  --service-token-id <CLIENT_ID> \
  --service-token-secret <CLIENT_SECRET>" \
  
  <YOUR_SFTP_SERVER>@<HOSTNAME>

Access authenticates the connection; the SFTP server still authenticates the user. The job needs the service token and the SSH key. To keep the secret out of the process list, set TUNNEL_SERVICE_TOKEN_ID and TUNNEL_SERVICE_TOKEN_SECRET in the job’s environment and drop both flags.

Editor — confirm before this page goes live

These four steps follow Cloudflare’s standard client pattern but were not present in either source page, so nothing here has been checked against the Beehexa environment. Run all four against the real hostname and paste the working output back into this section.

Phase 07

Do this on: Local development environment

Bind a local port for other applications

Entire phase is new — verify before publishing

A proxy command only helps tools that accept one. FileZilla, WinSCP, a scheduled ETL job, an SFTP library inside a HexaSync connector — none of them will shell out to cloudflared. They need a socket.

cloudflared access ssh opens one: it listens on a port on the Local development environment, authenticates to Cloudflare Access on its own, and forwards everything to the published hostname. To the application it looks like an ordinary SFTP server on 127.0.0.1.

tcp, ssh, rdp and smb are the same command

Cloudflare’s protocol reference says to use access ssh for an SSH route and access tcp for a TCP route, which reads as though they behave differently. In the CLI they do not: tcp is declared with ssh, rdp and smb as aliases of one subcommand, sharing every flag. Either spelling reaches the Phase 4 route — this guide uses ssh so the command matches the route type. Verified in cmd/cloudflared/access/cmd.go, cloudflared 2026.8.2.

How the two credentials map

Browser sign-inAccess token
What it isThe identity provider flow on the Access application — today One-Time PIN, and the same client command if you later add Google, Azure or OktaThe Phase 5 service token: a Client ID and Client Secret pair, the non-interactive equivalent of client credentials
Policy it matchesPolicy A, action AllowPolicy B, action Service Auth
SuitsA person at a desk with FileZilla openAn unattended job or a service on an integration host
Re-authenticationWhen the session expiresNever, until the token expires


Bind the port with a browser sign-in.

cloudflared access ssh --hostname <HOSTNAME> --url 127.0.0.1:2222


A browser opens for the Access challenge on the first run. After that the command stays in the foreground holding the listener open — closing the terminal closes the port. Any free port works; 2222 keeps it clear of a local SSH daemon on 22.

Point the application at the local port.

Leave the command running and configure the client as though the SFTP server were on this machine:

SettingValue
ProtocolSFTP / SSH File Transfer Protocol
Host127.0.0.1
Port2222
User<YOUR_SFTP_SERVER>
AuthenticationThe key or password from Phase 1


That configuration works unchanged in FileZilla, WinSCP, and any SFTP library. From the shell it is simply:

sftp -P 2222 <YOUR_SFTP_SERVER>@127.0.0.1


The host key will look wrong the first time


The server presents its own SSH host key, but the client records it against [127.0.0.1]:2222. Expect an unknown-host prompt on first connection, and expect a mismatch warning if that port was ever used for a different host. Verify the fingerprint against the SFTP server rather than accepting it blindly.

Bind the port without a browser.

For an unattended host, authenticate with the Phase 5 service token instead. Nothing opens, and no one has to be present.

cloudflared access ssh --hostname <HOSTNAME> --url 127.0.0.1:2222 \
  --service-token-id <CLIENT_ID> \
  --service-token-secret <CLIENT_SECRET>


A secret on the command line is readable
Anyone who can run ps on that machine sees both values, and your shell writes them to its history. Every flag below has an environment variable equivalent — use those instead, from a 0600 file, as the service in the next step does.

FlagShort formEnvironment variable
–hostname-T, –tunnel-hostTUNNEL_SERVICE_HOSTNAME
–url-L, –listenerTUNNEL_SERVICE_URL
–service-token-id–idTUNNEL_SERVICE_TOKEN_ID
–service-token-secret–secretTUNNEL_SERVICE_TOKEN_SECRET
–header-H


Keep the listener up.

On an integration host the port has to survive reboots. Run the bind as a systemd service — this is the Local development environment, and it is unrelated to the connector service from Phase 3.
/etc/cloudflared-sftp.env — the secret, kept out of the process list:

TUNNEL_SERVICE_TOKEN_ID=<CLIENT_ID>
TUNNEL_SERVICE_TOKEN_SECRET=<CLIENT_SECRET>

/etc/systemd/system/cloudflared-sftp.service:

[Unit]
Description=Cloudflare Access bind for SFTP
After=network-online.target

[Service]
EnvironmentFile=/etc/cloudflared-sftp.env
ExecStart=/usr/bin/cloudflared access ssh \
  --hostname <HOSTNAME> --url 127.0.0.1:2222
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo chmod 600 /etc/cloudflared-sftp.env
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared-sftp
sudo systemctl status cloudflared-sftp

The bound port has no access control of its own

Once bound, that port is an authenticated path to the SFTP server for any process on the machine — access has already been satisfied, and the local listener does not ask again. Bind it to 127.0.0.1, never 0.0.0.0: the second turns the client into an unauthenticated gateway on your LAN and undoes the reason for closing port 22 in the first place.

Editor — confirm before this page goes live

Every command, flag and environment variable here is verified against cloudflared 2026.8.2 and Cloudflare’s current documentation, but none of it has been run against the Beehexa hostname. Bind the port, connect a GUI client through it, start the service once, and paste the working output into this section before publishing.

Final validation

Work top to bottom. The last item is a separate decision, not another checkbox — it removes your fallback.

01 — SSH answers on localhost:22 on the SFTP server

01 — The dedicated SFTP user exists

01 — That user’s authentication works locally, by key or password

03 — cloudflared is installed and active (running)

03 — cloudflared is enabled to start on boot

03 — The tunnel reads HEALTHY in the Cloudflare dashboard

04 — The route resolves the hostname to ssh://localhost:22

05 — A staff member connects with a one-time PIN

05 — An automated job connects with the service token, no browser

05 — Someone outside the policy is refused

07 — A local bind on 127.0.0.1:2222 accepts a GUI SFTP client

07 — The unattended bind starts with a service token, no browser

07 — No bind listens on 0.0.0.0

No inbound rule publishes TCP/22 to the internet

Only once every item above passes: retire the old VPN or IP-whitelist access

Troubleshooting

New section — assembled from the failure points above

SymptomWhere to look first
Service is active (running), tunnel is not HEALTHYThe connector started but could not register. Read journalctl -u cloudflared -n 100 — usually a bad or already-used token, or outbound 443 blocked by an egress firewall.
Tunnel is HEALTHY, the hostname refuses to connectThe route (Phase 4). Confirm the published application’s hostname matches exactly and the service URL is ssh://localhost:22.
Browser opens, then access is deniedThe Access policy (Phase 5), not the tunnel. Your address is not in the Include list, or the Access application’s domain is not the published hostname.
No browser opens and the connection is refusedOne-Time PIN is not enabled as an identity provider for this application.
Automated job is refused, staff connections workThe service token. Check the two environment variables are set in the job’s own environment, that the token has not expired, and that Policy B’s action is Service Auth rather than Allow.
A browser opens even though service token flags were passedA cached Access token from an earlier interactive sign-in can take precedence. Clear the cached tokens in ~/.cloudflared/ and retry, and confirm Policy B’s action is Service Auth — with Allow, Access prompts for an identity provider login regardless of the token.
The local bind fails to startSomething already holds the port. Pick another, or find the holder with sudo ss -lntp | grep :2222.
Host key verification failed on 127.0.0.1Expected if that local port previously reached a different host. Remove the stale entry for [127.0.0.1]:2222 from known_hosts, then verify the new fingerprint against the SFTP server before accepting it.
Access passes, then SSH rejects the loginServer-side authentication (Phase 1), which Access does not touch. Re-test over ssh localhost and check the .ssh permissions.

Currency

What this was checked against

Everything added beyond the two source pages was verified on 25 August 2026 against the references below. Cloudflare has reorganised its documentation paths, so the links here are the current ones, not the ones the old pages pointed at.

ClaimChecked against
apt key, repository line and install commandpkg.cloudflare.com — verbatim
yum / dnf repository filepkg.cloudflare.com — verbatim
Release asset filenames, including cloudflared-linux-x86_64.rpmcloudflared 2026.8.2 release manifest
Windows has no repository and does not self-updateDownloads
ProxyCommand cloudflared access ssh –hostname %hSSH with client-side cloudflared
Local port bind with –url host:portArbitrary TCP
tcp, ssh, rdp and smb are aliases of one subcommand; flag short forms and environment variablescloudflared source, cmd/cloudflared/access/cmd.go at 2026.8.2 — the documentation does not state this
Service token headers, and that the policy action must be Service AuthService tokens
Route protocol types and service URL formatsProtocols for published applications

Two things to re-check at the next review

Dashboard navigation labels move faster than anything else in this guide — Cloudflare has already renamed these sections once, and the paths in Phases 2, 4 and 5 are inherited from the source pages rather than re-verified in the console. And cloudflared ships roughly monthly, so confirm the release tag above is still recent before treating the CLI details as current.