WIUNIX ACADEMY
Set up Cloudflare Tunnel and Access for a private web app on Ubuntu and Debian
Page 2 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.
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
LET’S BUILD
Install prerequisites from distribution repositories
On the connector host, install these ordinary distribution packages. Ubuntu 26.10 is the reviewed default; the selector also retains LTS Ubuntu and Debian. This is not a full-system upgrade. Python serves a harmless marker only; it is not a production application server.
Run at the location stated in this step
sudo apt-get update
sudo apt-get install --no-install-recommends ca-certificates curl gnupg python3 dnsutils
APT completes without signature or repository errors.
LET’S BUILD
Add the official signed cloudflared repository
Use the vendor-recommended any Debian-based distribution suite rather than inventing an Ubuntu codename. Retrieve the current signing key over HTTPS and inspect its fingerprint and confirm the key URL/current vendor instructions through your normal independent supply-chain process. The package page provides the key URL; if your organization requires an independently published fingerprint, obtain that before trusting the key. Stop on mismatch; do not bypass APT signature verification. The scoped signed-by binding trusts this key only for this repository. Do not replace an existing organization-managed key or repository file.
Run at the location stated in this step
sudo install -d -m 0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
gpg --show-keys --fingerprint /usr/share/keyrings/cloudflare-main.gpg
printf '%s\n' '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
sudo apt-get update
apt-cache policy cloudflared
sudo apt-get install --no-install-recommends cloudflared
cloudflared --version
cloudflared tunnel run --help
Signed vendor candidate installed; help includes token-file. Use a maintained release at least 2025.4.0; record the exact installed version.
LET’S BUILD
Approve only the required outbound path
Use the current official firewall destination list: outbound UDP/7844 for QUIC and TCP/7844 for HTTP/2, plus DNS to your approved resolver. Repository downloads require HTTPS; connector-side Access JWT validation also needs HTTPS to your team cloudflareaccess.com endpoint. Do not assume TCP/443 replaces 7844. Keep existing inbound management policy unchanged, add no inbound origin port, and do not flush your firewall. The auto protocol can fall back to HTTP/2 when UDP is blocked. A successful TCP probe is not a QUIC handshake or an authorization test.
Run at the location stated in this step
getent ahostsv4 region1.v2.argotunnel.com
getent ahostsv4 region2.v2.argotunnel.com
dig SRV _v2-origintunneld._tcp.argotunnel.com
Resolvers return tunnel destinations. Network owner has approved required egress; actual connector transport is checked after startup.
LET’S BUILD
Create a dedicated harmless origin
On the same host, use only a fresh directory. It contains a non-secret marker, not application data. Never point this Python server at a home directory, repository, token directory or backup. Loopback is intentional and must remain fixed. The service user can read only the public marker directory, while the token remains root-only.
Run at the location stated in this step
sudo bash -eu <<'ORIGIN' &&
test ! -e '/srv/academy-origin'
test ! -e /etc/systemd/system/academy-origin.service
install -d -m 0755 '/srv/academy-origin'
install -d -m 0755 "$(dirname '/srv/academy-origin/health.txt')"
printf '%s\n' 'academy-tunnel-origin-ok' > '/srv/academy-origin/health.txt'
chmod 0644 '/srv/academy-origin/health.txt'
ORIGIN
sudoedit /etc/systemd/system/academy-origin.service
The marker exists; the editor is open for the following dedicated unit.
Origin service file
FILE CONTENT: save this dedicated unit, not a shell command. It listens only on 127.0.0.1:8080 as an unprivileged user.
[Unit]
Description=Academy loopback marker origin
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
ExecStart=/usr/bin/python3 -m http.server 8080 --bind 127.0.0.1 --directory /srv/academy-origin
Restart=on-failure
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.target
LET’S BUILD
Start and verify the loopback receiver
On the connector host, validate the unit before starting it. Verify the exact body, not just HTTP status. Inspect the listener address: no wildcard IPv4 or IPv6 binding. From a different machine, the host’s LAN/public address at this port must be unreachable; retain existing firewall policy. If that check fails, stop here and close the exposure before creating a route.
Run at the location stated in this step
sudo systemd-analyze verify /etc/systemd/system/academy-origin.service
sudo systemctl daemon-reload
sudo systemctl enable --now academy-origin.service
curl --fail --silent --show-error --noproxy '*' http://127.0.0.1:8080/health.txt
sudo ss -lntp 'sport = :8080'
Body is academy-tunnel-origin-ok and the listener is only 127.0.0.1:8080.