← Start of tutorial

WIUNIX ACADEMY

Set up Cloudflare Tunnel and Access for a private web app on Ubuntu and Debian

Page 3 of 5. Review the earlier steps and prerequisites before continuing.

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Ubuntu 26.10Reviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment8 editable values
09

LET’S BUILD

Store the connector token without argv or history

Use the dedicated service account below. After the editor opens, paste ONLY the real connector token as one line, save and exit; no example token is provided. Do not paste the dashboard command or run it. Use a trusted editor with swap/backup disabled for secrets and no screen/session recording. The token file is root-owned mode0600; systemd loads a private read-only credential for the service user. This avoids a secret in argv, unit content and environment variables. Never print the token to verify it.

Bash

Run at the location stated in this step

sudo bash -eu <<'CREDENTIAL' &&
test ! -e '/etc/cloudflared-academy'
if getent passwd cloudflared-academy >/dev/null; then echo 'Existing service account: STOP' >&2; exit 1; fi
useradd --system --no-create-home --shell /usr/sbin/nologin cloudflared-academy
install -d -m 0700 '/etc/cloudflared-academy'
install -m 0600 /dev/null '/etc/cloudflared-academy/token'
CREDENTIAL
sudoedit '/etc/cloudflared-academy/token' &&
sudo test -s '/etc/cloudflared-academy/token'
sudo stat -c '%a %U %G' '/etc/cloudflared-academy/token'
sudoedit /etc/systemd/system/cloudflared-academy.service
How do I know it worked?

Token file is nonempty, owned by root with mode600; the next editor is for a unit with no secret content.

Connector service file with systemd credentials

FILE CONTENT. This dedicated unit uses the documented token-file parameter with systemd LoadCredential. All selected OS versions have this systemd capability. Metrics and readiness remain loopback-only on 20241. Package updates are managed by APT, so the internal updater is disabled. No root networking capability is required for this HTTP-only exercise.

ini · /etc/systemd/system/cloudflared-academy.service
[Unit]
Description=Academy Cloudflare Tunnel connector
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=cloudflared-academy
Group=cloudflared-academy
LoadCredential=token:/etc/cloudflared-academy/token
ExecStart=/usr/bin/cloudflared --no-autoupdate tunnel --metrics 127.0.0.1:20241 run --token-file ${CREDENTIALS_DIRECTORY}/token
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
UMask=0077

[Install]
WantedBy=multi-user.target
10

LET’S BUILD

Start the connector and inspect health

On the connector host, validate and start only this unit. Logs can contain identifiers/URLs; inspect locally and redact before sharing. Do not enable debug, request/body logging or upload a diagnostic bundle without reviewing it. The dashboard should show a healthy connector for academy-web. Readiness means edge connectivity, not that the origin or policy works.

Bash

Run at the location stated in this step

sudo systemd-analyze verify /etc/systemd/system/cloudflared-academy.service
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared-academy.service
sudo systemctl is-active cloudflared-academy.service
sudo journalctl -u cloudflared-academy.service --since "5 minutes ago" --no-pager
curl --fail --silent --show-error --noproxy '*' http://127.0.0.1:20241/ready
How do I know it worked?

Service active, registered tunnel connections in local logs, dashboard healthy, readiness HTTP200.

11

LET’S BUILD

Publish the exact protected hostname

Reconfirm the saved Access hostname before proceeding. In Networking → Tunnels → academy-web → Routes, add Published application for app.example.com, no path restriction, Service URL http://127.0.0.1:8080. Under its additional Access settings enable Protect with Access, select the correct team and the Access application’s audience (AUD). Save and verify the setting persisted. This connector-side JWT validation complements the edge policy. Do not add wildcard routes or private CIDRs. Cloudflare creates the tunnel DNS mapping; use a fresh hostname so there is no competing A/AAAA/CNAME. Confirm a valid edge certificate before testing HTTPS; never bypass a TLS warning.

How do I know it worked?

Only app.example.com maps to the loopback service; Access and connector-side JWT validation remain enabled.

12

LET’S BUILD

Test without a session first

From a separate client with no Access cookies, inspect the response without following redirects or supplying credentials. An Access login redirect or denial is expected; the origin marker must never appear. A200 with the marker is a security failure: remove the published route immediately, keep Access, and investigate host/path matching and Bypass policies. Status alone cannot distinguish a login page from the actual app.

Bash

Run at the location stated in this step

curl --silent --show-error --max-time 20 --dump-header - 'https://app.example.com/health.txt'
How do I know it worked?

Unauthenticated response leads to Access or is denied; academy-tunnel-origin-ok is absent.

13

LET’S BUILD

Prove allowed and denied browser access

In a clean browser session open https://app.example.com/health.txt. Sign in using the allowed test identity and complete the IdP’s required MFA; verify the exact marker. In an independent browser profile/private session with no shared Access/IdP session, sign in as the non-allowed identity and verify denial. Also test a completely signed-out browser. Check Access logs for each decision and match application/identity/time; do not publish identity logs. Test at least one other application path to confirm whole-host coverage. Revoke the test session and verify a new request cannot silently reuse it. A healthy tunnel without these tests is not acceptance.

How do I know it worked?

Allowed identity receives the exact marker; non-allowed and signed-out sessions cannot; Access decisions match the tests.

Explore all tutorials ↗