Initial Commit
This commit is contained in:
commit
d8e5f2796e
2
cloudflared-tunnel/.gitignore
vendored
Normal file
2
cloudflared-tunnel/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
dist/
|
||||||
|
publish.env
|
||||||
14
cloudflared-tunnel/DEBIAN/control
Normal file
14
cloudflared-tunnel/DEBIAN/control
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
Package: cloudflared-tunnel
|
||||||
|
Version: @VERSION@
|
||||||
|
Architecture: all
|
||||||
|
Maintainer: Mike Mainguy <mike.mainguy@gmail.com>
|
||||||
|
Section: net
|
||||||
|
Priority: optional
|
||||||
|
Depends: cloudflared, adduser, ca-certificates, iproute2, jq, systemd
|
||||||
|
Description: Cloudflare Tunnel to a local port, run as a systemd service
|
||||||
|
Provides cloudflared-tunnel-setup, which configures a named, dashboard-token
|
||||||
|
or quick (trycloudflare.com) Cloudflare Tunnel forwarding to
|
||||||
|
http://localhost:PORT and runs it as the unprivileged 'cloudflared' user.
|
||||||
|
.
|
||||||
|
A daily systemd timer upgrades cloudflared via apt and restarts the tunnel
|
||||||
|
when a new version is installed.
|
||||||
54
cloudflared-tunnel/DEBIAN/postinst
Executable file
54
cloudflared-tunnel/DEBIAN/postinst
Executable file
@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$1" = configure ]; then
|
||||||
|
# Unprivileged system account the daemon runs as. It only makes outbound
|
||||||
|
# connections and proxies to localhost, so it needs no root privileges.
|
||||||
|
if ! getent passwd cloudflared >/dev/null; then
|
||||||
|
adduser --system --group --no-create-home --home /nonexistent --quiet cloudflared
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = configure ] || [ "$1" = abort-upgrade ] || [ "$1" = abort-deconfigure ] || [ "$1" = abort-remove ]; then
|
||||||
|
# Update timer: enabled on first install, and an admin's later `systemctl
|
||||||
|
# disable` survives upgrades (the same logic dh_installsystemd generates).
|
||||||
|
deb-systemd-helper unmask cloudflared-update.timer >/dev/null || true
|
||||||
|
if deb-systemd-helper --quiet was-enabled cloudflared-update.timer; then
|
||||||
|
deb-systemd-helper enable cloudflared-update.timer >/dev/null || true
|
||||||
|
else
|
||||||
|
deb-systemd-helper update-state cloudflared-update.timer >/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d /run/systemd/system ]; then
|
||||||
|
systemctl --system daemon-reload >/dev/null || true
|
||||||
|
deb-systemd-invoke start cloudflared-update.timer >/dev/null || true
|
||||||
|
# cloudflared-tunnel-setup enables the tunnel, not the package. On upgrade,
|
||||||
|
# restart it if it's running so a changed unit file takes effect.
|
||||||
|
systemctl try-restart cloudflared-tunnel.service >/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# First install only ($2 is the previously-configured version on upgrade). The
|
||||||
|
# package ships no tunnel config, so tell the admin how to register one.
|
||||||
|
if [ "$1" = configure ] && [ -z "$2" ]; then
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
cloudflared-tunnel is installed, but no tunnel is configured yet. Pick a mode:
|
||||||
|
|
||||||
|
# Named tunnel on your own domain (prints a URL to log in to Cloudflare):
|
||||||
|
sudo cloudflared-tunnel-setup --hostname app.example.com --port 3000
|
||||||
|
|
||||||
|
# Dashboard-managed tunnel (Zero Trust > Networks > Tunnels; copy the token,
|
||||||
|
# then set that hostname's service to http://localhost:PORT in the dashboard):
|
||||||
|
sudo cloudflared-tunnel-setup --token <TUNNEL_TOKEN> --port 3000
|
||||||
|
|
||||||
|
# Random trycloudflare.com URL, no Cloudflare account needed (testing only):
|
||||||
|
sudo cloudflared-tunnel-setup --quick --port 3000
|
||||||
|
|
||||||
|
cloudflared-tunnel.service stays stopped until you run one of the above.
|
||||||
|
Details: cloudflared-tunnel-setup --help, and
|
||||||
|
/usr/share/doc/cloudflared-tunnel/README.Debian
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
41
cloudflared-tunnel/DEBIAN/postrm
Executable file
41
cloudflared-tunnel/DEBIAN/postrm
Executable file
@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
MARKER='Written by cloudflared-tunnel-setup'
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
remove)
|
||||||
|
deb-systemd-helper mask cloudflared-update.timer >/dev/null || true
|
||||||
|
;;
|
||||||
|
|
||||||
|
purge)
|
||||||
|
deb-systemd-helper purge cloudflared-update.timer >/dev/null || true
|
||||||
|
deb-systemd-helper unmask cloudflared-update.timer >/dev/null || true
|
||||||
|
|
||||||
|
# Delete what cloudflared-tunnel-setup wrote (it marks its files), including
|
||||||
|
# the tunnel secret. Tunnels and DNS records in your Cloudflare account, and
|
||||||
|
# the login in /root/.cloudflared, are left alone.
|
||||||
|
conf=/etc/cloudflared/config.yml
|
||||||
|
if [ -f "$conf" ] && grep -q "$MARKER" "$conf"; then
|
||||||
|
creds=$(sed -n 's/^credentials-file: *//p' "$conf")
|
||||||
|
case "$creds" in
|
||||||
|
/etc/cloudflared/*.json) rm -f "$creds" ;;
|
||||||
|
esac
|
||||||
|
rm -f "$conf"
|
||||||
|
fi
|
||||||
|
if [ -f /etc/cloudflared/tunnel.env ] && grep -q "$MARKER" /etc/cloudflared/tunnel.env; then
|
||||||
|
rm -f /etc/cloudflared/tunnel.env
|
||||||
|
fi
|
||||||
|
rmdir /etc/cloudflared 2>/dev/null || true
|
||||||
|
|
||||||
|
rm -f /etc/systemd/system/cloudflared-tunnel.service.d/setup.conf
|
||||||
|
rmdir /etc/systemd/system/cloudflared-tunnel.service.d 2>/dev/null || true
|
||||||
|
rm -f /etc/systemd/system/multi-user.target.wants/cloudflared-tunnel.service
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -d /run/systemd/system ]; then
|
||||||
|
systemctl --system daemon-reload >/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
8
cloudflared-tunnel/DEBIAN/prerm
Executable file
8
cloudflared-tunnel/DEBIAN/prerm
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$1" = remove ] && [ -d /run/systemd/system ]; then
|
||||||
|
deb-systemd-invoke stop cloudflared-update.timer cloudflared-tunnel.service >/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
124
cloudflared-tunnel/README.md
Normal file
124
cloudflared-tunnel/README.md
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# cloudflared-tunnel
|
||||||
|
|
||||||
|
A Debian package that runs a Cloudflare Tunnel to a local port as a systemd
|
||||||
|
service (the packaged form of `../setup-cloudflared.sh`), published to a Gitea
|
||||||
|
Debian package registry.
|
||||||
|
|
||||||
|
| Path | Installed as |
|
||||||
|
|---|---|
|
||||||
|
| `src/cloudflared-tunnel-setup` | `/usr/sbin/cloudflared-tunnel-setup`: run once per machine to pick a mode |
|
||||||
|
| `src/cloudflared-update` | `/usr/sbin/cloudflared-update`: daily cloudflared upgrade |
|
||||||
|
| `src/*.service`, `src/*.timer` | `/usr/lib/systemd/system/` |
|
||||||
|
| `src/README.Debian` | `/usr/share/doc/cloudflared-tunnel/README.Debian`: admin notes, shown as a pointer by `postinst` |
|
||||||
|
| `DEBIAN/` | package metadata and maintainer scripts |
|
||||||
|
| `build.sh` | builds `dist/cloudflared-tunnel_<VERSION>_all.deb` with plain `dpkg-deb` |
|
||||||
|
| `publish.sh` | uploads to Gitea, and mirrors cloudflared with `--cloudflared` |
|
||||||
|
|
||||||
|
## Publishing to Gitea
|
||||||
|
|
||||||
|
### 1. Create a Gitea token (one time)
|
||||||
|
|
||||||
|
In Gitea, go to **Settings → Applications → Generate New Token** and give it
|
||||||
|
**package: Read and Write** scope.
|
||||||
|
|
||||||
|
### 2. Create `publish.env` (one time)
|
||||||
|
|
||||||
|
Put it next to the scripts. It's git-ignored.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GITEA_URL=https://git.dasfad.com
|
||||||
|
GITEA_OWNER=mmainguy # user or org that owns the packages
|
||||||
|
GITEA_USER=mmainguy # account the token belongs to
|
||||||
|
GITEA_TOKEN=xxxxxxxx
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional: `DIST` (default `bookworm`), `COMPONENT` (default `main`) and
|
||||||
|
`CLOUDFLARED_ARCHES` (default `"amd64 arm64"`).
|
||||||
|
|
||||||
|
### 3. Publish
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh # -> dist/cloudflared-tunnel_<VERSION>_all.deb
|
||||||
|
./publish.sh # uploads it
|
||||||
|
./publish.sh --cloudflared # mirrors the latest cloudflared (amd64 + arm64)
|
||||||
|
```
|
||||||
|
|
||||||
|
Mirroring cloudflared means your machines only need your Gitea repo. The
|
||||||
|
cloudflared package is about 20 MB, so if Gitea is behind nginx, raise
|
||||||
|
`client_max_body_size` (e.g. `100m`) first.
|
||||||
|
|
||||||
|
Keep the mirror current, or the machines' daily updater has nothing new to
|
||||||
|
install. Re-uploading a version that's already there is just skipped, so this
|
||||||
|
is safe to run daily from cron on the machine that holds `publish.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
30 5 * * * /path/to/cloudflared-tunnel/publish.sh --cloudflared >/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
To check the upload, look at the owner's **Packages** tab in Gitea, or run
|
||||||
|
`curl https://git.dasfad.com/api/packages/mmainguy/debian/dists/bookworm/Release`.
|
||||||
|
|
||||||
|
### 4. Install on each machine
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo curl -fsSL https://git.dasfad.com/api/packages/mmainguy/debian/repository.key \
|
||||||
|
-o /etc/apt/keyrings/gitea-mmainguy.asc
|
||||||
|
echo 'deb [signed-by=/etc/apt/keyrings/gitea-mmainguy.asc] https://git.dasfad.com/api/packages/mmainguy/debian bookworm main' \
|
||||||
|
| sudo tee /etc/apt/sources.list.d/gitea-mmainguy.list
|
||||||
|
sudo apt update && sudo apt install cloudflared-tunnel
|
||||||
|
|
||||||
|
sudo cloudflared-tunnel-setup --hostname app.example.com --port 3000
|
||||||
|
# or: --token <TUNNEL_TOKEN> or: --quick
|
||||||
|
```
|
||||||
|
|
||||||
|
Installing the package doesn't start a tunnel. `cloudflared-tunnel-setup` does
|
||||||
|
that, and it's safe to re-run to change the mode or port.
|
||||||
|
|
||||||
|
If the Gitea owner is private, apt needs credentials. Keep them out of the
|
||||||
|
world-readable sources file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf 'machine git.dasfad.com\nlogin mmainguy\npassword <read-only token>\n' \
|
||||||
|
| sudo install -m 0600 /dev/stdin /etc/apt/auth.conf.d/gitea.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Release an update
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo 1.0.1 > VERSION # must change every release; Gitea won't overwrite a version
|
||||||
|
./build.sh && ./publish.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Machines pick it up with `apt upgrade`. If the tunnel is running, the upgrade
|
||||||
|
restarts it.
|
||||||
|
|
||||||
|
## Changes from `setup-cloudflared.sh`
|
||||||
|
|
||||||
|
- **The tunnel service is now `cloudflared-tunnel.service`.** This avoids a
|
||||||
|
clash with the `cloudflared.service` that `cloudflared service install`
|
||||||
|
writes. Logs are under `journalctl -u cloudflared-tunnel`.
|
||||||
|
- **Old setups are cleaned up automatically.** On machines set up with
|
||||||
|
`setup-cloudflared.sh`, install the package and re-run
|
||||||
|
`cloudflared-tunnel-setup` with the same options. It removes the old
|
||||||
|
`/etc/systemd/system/cloudflared*` units and `/usr/local/sbin/cloudflared-update`.
|
||||||
|
It identifies them by their contents and does this just before starting the
|
||||||
|
new service, so a failed login doesn't leave you with no tunnel. If you mirror
|
||||||
|
cloudflared, also delete `/etc/apt/sources.list.d/cloudflared.list`.
|
||||||
|
- **The daily updater refreshes all apt sources**, not just `cloudflared.list`,
|
||||||
|
so it works with the Gitea mirror.
|
||||||
|
- **`apt purge cloudflared-tunnel` deletes what the setup wrote**, including the
|
||||||
|
tunnel secret. It leaves your Cloudflare tunnels, DNS records and the
|
||||||
|
`/root/.cloudflared` login alone.
|
||||||
|
- **Maintainer** in `DEBIAN/control` is set to Mike Mainguy; change it if needed.
|
||||||
|
- `setup-cloudflared.sh` is unchanged and still works on its own.
|
||||||
|
|
||||||
|
## Day to day
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u cloudflared-tunnel -f # tunnel logs
|
||||||
|
sudo systemctl restart cloudflared-tunnel # restart the tunnel
|
||||||
|
systemctl list-timers cloudflared-update.timer # next update check
|
||||||
|
sudo systemctl start cloudflared-update # check for a cloudflared update now
|
||||||
|
journalctl -u cloudflared-update # update history
|
||||||
|
sudo apt purge cloudflared-tunnel # remove the service and the files the setup wrote
|
||||||
|
```
|
||||||
1
cloudflared-tunnel/VERSION
Normal file
1
cloudflared-tunnel/VERSION
Normal file
@ -0,0 +1 @@
|
|||||||
|
0.0.2
|
||||||
35
cloudflared-tunnel/build.sh
Executable file
35
cloudflared-tunnel/build.sh
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# build.sh — build dist/cloudflared-tunnel_<VERSION>_all.deb with plain dpkg-deb
|
||||||
|
# (no debhelper needed). Bump VERSION before every release: Gitea refuses to
|
||||||
|
# overwrite a version it already has.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
VERSION="$(tr -d '[:space:]' < VERSION)"
|
||||||
|
[[ "$VERSION" =~ ^[0-9][A-Za-z0-9.+~-]*$ ]] || { echo "Bad VERSION: '$VERSION'" >&2; exit 1; }
|
||||||
|
OUT="dist/cloudflared-tunnel_${VERSION}_all.deb"
|
||||||
|
|
||||||
|
stage="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$stage"' EXIT
|
||||||
|
chmod 0755 "$stage" # becomes the package's ./ entry; mktemp makes it 0700
|
||||||
|
|
||||||
|
install -D -m 0755 src/cloudflared-tunnel-setup "$stage/usr/sbin/cloudflared-tunnel-setup"
|
||||||
|
install -D -m 0755 src/cloudflared-update "$stage/usr/sbin/cloudflared-update"
|
||||||
|
install -D -m 0644 src/README.Debian \
|
||||||
|
"$stage/usr/share/doc/cloudflared-tunnel/README.Debian"
|
||||||
|
install -d "$stage/usr/lib/systemd/system"
|
||||||
|
install -m 0644 src/cloudflared-tunnel.service src/cloudflared-update.service \
|
||||||
|
src/cloudflared-update.timer "$stage/usr/lib/systemd/system/"
|
||||||
|
|
||||||
|
install -d "$stage/DEBIAN"
|
||||||
|
install -m 0755 DEBIAN/postinst DEBIAN/prerm DEBIAN/postrm "$stage/DEBIAN/"
|
||||||
|
{
|
||||||
|
sed "s/@VERSION@/${VERSION}/" DEBIAN/control
|
||||||
|
echo "Installed-Size: $(du -sk --exclude=DEBIAN "$stage" | cut -f1)"
|
||||||
|
} > "$stage/DEBIAN/control"
|
||||||
|
|
||||||
|
mkdir -p dist
|
||||||
|
dpkg-deb --root-owner-group -Zxz --build "$stage" "$OUT" >/dev/null
|
||||||
|
echo "Built $OUT"
|
||||||
87
cloudflared-tunnel/publish.sh
Executable file
87
cloudflared-tunnel/publish.sh
Executable file
@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# publish.sh — upload .deb files to a Gitea (or Forgejo) Debian package registry.
|
||||||
|
#
|
||||||
|
# ./publish.sh upload dist/cloudflared-tunnel_<VERSION>_all.deb
|
||||||
|
# ./publish.sh FILE.deb ... upload these files
|
||||||
|
# ./publish.sh --cloudflared mirror the latest cloudflared release into the
|
||||||
|
# registry, so clients need only your repo
|
||||||
|
#
|
||||||
|
# Settings come from the environment (or a ./publish.env file, which is sourced
|
||||||
|
# if present):
|
||||||
|
# GITEA_URL https://git.dasfad.com
|
||||||
|
# GITEA_OWNER user or org that owns the packages (e.g. mmainguy)
|
||||||
|
# GITEA_USER account to upload as (default: GITEA_OWNER)
|
||||||
|
# GITEA_TOKEN personal access token with package read/write scope
|
||||||
|
# DIST distribution (default: bookworm)
|
||||||
|
# COMPONENT component (default: main)
|
||||||
|
# CLOUDFLARED_ARCHES architectures to mirror (default: "amd64 arm64")
|
||||||
|
#
|
||||||
|
# Uploading a version that is already in the registry is reported and skipped,
|
||||||
|
# so --cloudflared is safe to run from cron.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
[[ -f publish.env ]] && . ./publish.env
|
||||||
|
: "${GITEA_URL:?set GITEA_URL (e.g. https://git.dasfad.com)}"
|
||||||
|
: "${GITEA_OWNER:?set GITEA_OWNER}"
|
||||||
|
: "${GITEA_TOKEN:?set GITEA_TOKEN}"
|
||||||
|
GITEA_USER="${GITEA_USER:-$GITEA_OWNER}"
|
||||||
|
DIST="${DIST:-bookworm}"
|
||||||
|
COMPONENT="${COMPONENT:-main}"
|
||||||
|
CLOUDFLARED_ARCHES="${CLOUDFLARED_ARCHES:-amd64 arm64}"
|
||||||
|
REPO="${GITEA_URL%/}/api/packages/${GITEA_OWNER}/debian"
|
||||||
|
|
||||||
|
log() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||||
|
die() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# The credentials go to curl on stdin (-K -), so the token never shows in `ps`.
|
||||||
|
upload() {
|
||||||
|
local deb="$1" body code
|
||||||
|
body="$(mktemp)"
|
||||||
|
code="$(printf 'user = "%s:%s"\n' "$GITEA_USER" "$GITEA_TOKEN" \
|
||||||
|
| curl -sS -K - -o "$body" -w '%{http_code}' --upload-file "$deb" \
|
||||||
|
"$REPO/pool/$DIST/$COMPONENT/upload")" || { rm -f "$body"; die "Upload of $deb failed"; }
|
||||||
|
case "$code" in
|
||||||
|
201) log "Published $(basename "$deb") to $DIST/$COMPONENT" ;;
|
||||||
|
409) log "$(basename "$deb") is already in $DIST/$COMPONENT; skipped" ;;
|
||||||
|
*) { cat "$body"; echo; } >&2; rm -f "$body"; die "Upload of $deb failed (HTTP $code)" ;;
|
||||||
|
esac
|
||||||
|
rm -f "$body"
|
||||||
|
}
|
||||||
|
|
||||||
|
mirror_cloudflared() {
|
||||||
|
local arch
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
for arch in $CLOUDFLARED_ARCHES; do
|
||||||
|
log "Downloading latest cloudflared ($arch)"
|
||||||
|
curl -fsSL -o "$tmp/cloudflared-$arch.deb" \
|
||||||
|
"https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch}.deb"
|
||||||
|
log "cloudflared $(dpkg-deb -f "$tmp/cloudflared-$arch.deb" Version) ($arch)"
|
||||||
|
upload "$tmp/cloudflared-$arch.deb"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
files=()
|
||||||
|
mirror=no
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--cloudflared) mirror=yes ;;
|
||||||
|
-h|--help) awk 'NR < 3 { next } !/^#/ { exit } { sub(/^# ?/, ""); print }' "$0"; exit 0 ;;
|
||||||
|
-*) die "Unknown option: $arg" ;;
|
||||||
|
*) files+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ ${#files[@]} -eq 0 && $mirror == no ]]; then
|
||||||
|
files=("dist/cloudflared-tunnel_$(tr -d '[:space:]' < VERSION)_all.deb")
|
||||||
|
fi
|
||||||
|
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
[[ -f "$f" ]] || die "$f not found (run ./build.sh first?)"
|
||||||
|
upload "$f"
|
||||||
|
done
|
||||||
|
[[ $mirror == yes ]] && mirror_cloudflared
|
||||||
|
|
||||||
|
log "Repository: $REPO $DIST $COMPONENT"
|
||||||
92
cloudflared-tunnel/src/README.Debian
Normal file
92
cloudflared-tunnel/src/README.Debian
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
cloudflared-tunnel for Debian
|
||||||
|
=============================
|
||||||
|
|
||||||
|
Installing this package does not start a tunnel. It installs the tunnel
|
||||||
|
service, the daily cloudflared updater, and the setup command; you register a
|
||||||
|
tunnel by running cloudflared-tunnel-setup once per machine.
|
||||||
|
|
||||||
|
Registering a tunnel
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Pick one of three modes. All of them take --port (default 3000), which is the
|
||||||
|
local port the tunnel forwards to (http://localhost:PORT).
|
||||||
|
|
||||||
|
1. Named tunnel on a domain in your Cloudflare account:
|
||||||
|
|
||||||
|
sudo cloudflared-tunnel-setup --hostname app.example.com --port 3000
|
||||||
|
|
||||||
|
This prints a URL to open in a browser to authorize cloudflared against the
|
||||||
|
zone, then creates the tunnel, writes /etc/cloudflared/config.yml with an
|
||||||
|
ingress rule to http://localhost:PORT, and creates the DNS CNAME. Add
|
||||||
|
--name my-tunnel to override the generated tunnel name (<short-hostname>-<port>).
|
||||||
|
|
||||||
|
The browser login is stored in /root/.cloudflared/cert.pem and is reused by
|
||||||
|
later runs, so subsequent tunnels on this machine need no second login.
|
||||||
|
|
||||||
|
2. Dashboard-managed tunnel. Create the tunnel under Zero Trust > Networks >
|
||||||
|
Tunnels, copy its token, and run:
|
||||||
|
|
||||||
|
sudo cloudflared-tunnel-setup --token <TUNNEL_TOKEN> --port 3000
|
||||||
|
|
||||||
|
The ingress lives in the dashboard, not on disk: set the public hostname's
|
||||||
|
service to http://localhost:PORT there, or the tunnel will connect and serve
|
||||||
|
nothing. --port only affects what this machine expects to proxy to.
|
||||||
|
|
||||||
|
3. Quick tunnel, no Cloudflare account needed:
|
||||||
|
|
||||||
|
sudo cloudflared-tunnel-setup --quick --port 3000
|
||||||
|
|
||||||
|
Serves a random trycloudflare.com URL, which changes on every restart of the
|
||||||
|
service. Read it out of the log (journalctl -u cloudflared-tunnel). Testing
|
||||||
|
only; do not use it for anything durable.
|
||||||
|
|
||||||
|
Re-running the setup is safe. It rewrites the configuration and restarts the
|
||||||
|
service, so it is also how you change mode or port later.
|
||||||
|
|
||||||
|
What ends up where
|
||||||
|
------------------
|
||||||
|
|
||||||
|
/etc/cloudflared/config.yml named mode: ingress and credentials-file
|
||||||
|
/etc/cloudflared/*.json named mode: the tunnel's credentials (secret)
|
||||||
|
/etc/cloudflared/tunnel.env token/quick mode: the token, mode and port
|
||||||
|
/etc/systemd/system/cloudflared-tunnel.service.d/setup.conf
|
||||||
|
the ExecStart the chosen mode needs
|
||||||
|
|
||||||
|
Files the setup writes carry a marker comment; the package's postrm deletes
|
||||||
|
exactly those on purge (including the tunnel credentials), and leaves your
|
||||||
|
Cloudflare tunnels, DNS records and /root/.cloudflared login untouched.
|
||||||
|
|
||||||
|
The service runs as the unprivileged system user 'cloudflared', created on
|
||||||
|
install. It only makes outbound connections and proxies to localhost, so it
|
||||||
|
needs no privileges of its own -- but that also means the port you expose must
|
||||||
|
be reachable from that user's network namespace, i.e. a normal localhost
|
||||||
|
listener. A service bound to 127.0.0.1 is fine; one inside a container's own
|
||||||
|
netns is not.
|
||||||
|
|
||||||
|
Updates
|
||||||
|
-------
|
||||||
|
|
||||||
|
cloudflared-update.timer runs daily, refreshes apt, upgrades cloudflared if a
|
||||||
|
new version is available, and restarts cloudflared-tunnel.service only when
|
||||||
|
the binary actually changed.
|
||||||
|
|
||||||
|
systemctl list-timers cloudflared-update.timer next scheduled check
|
||||||
|
sudo systemctl start cloudflared-update check right now
|
||||||
|
journalctl -u cloudflared-update what it has done
|
||||||
|
sudo systemctl disable cloudflared-update.timer opt out (survives upgrades)
|
||||||
|
|
||||||
|
The timer needs cloudflared to be installable from an apt source the machine
|
||||||
|
can reach -- either Cloudflare's own repository or a mirror in your package
|
||||||
|
registry. Without one there is nothing for it to install.
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
---------------
|
||||||
|
|
||||||
|
journalctl -u cloudflared-tunnel -f tunnel log, including the quick URL
|
||||||
|
systemctl status cloudflared-tunnel is it running, and why not
|
||||||
|
|
||||||
|
A tunnel that connects but returns 502 is almost always the local port: nothing
|
||||||
|
is listening on it, or it is bound to an interface the service user cannot
|
||||||
|
reach. In dashboard mode, also check the public hostname's service URL.
|
||||||
|
|
||||||
|
-- Mike Mainguy <mike.mainguy@gmail.com>
|
||||||
260
cloudflared-tunnel/src/cloudflared-tunnel-setup
Executable file
260
cloudflared-tunnel/src/cloudflared-tunnel-setup
Executable file
@ -0,0 +1,260 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# cloudflared-tunnel-setup — configure the Cloudflare Tunnel that the
|
||||||
|
# cloudflared-tunnel package runs as a systemd service, forwarding to a local
|
||||||
|
# port (default 3000).
|
||||||
|
#
|
||||||
|
# Modes (pick one):
|
||||||
|
#
|
||||||
|
# --hostname app.example.com [--name my-tunnel]
|
||||||
|
# Locally-managed tunnel. Logs in to Cloudflare (opens a URL you visit in a
|
||||||
|
# browser), creates the tunnel, writes /etc/cloudflared/config.yml with
|
||||||
|
# ingress -> http://localhost:PORT, and creates the DNS CNAME.
|
||||||
|
#
|
||||||
|
# --token <TUNNEL_TOKEN>
|
||||||
|
# Dashboard-managed tunnel (Zero Trust > Networks > Tunnels). The ingress is
|
||||||
|
# configured in the dashboard: set the public hostname's service to
|
||||||
|
# http://localhost:PORT there.
|
||||||
|
#
|
||||||
|
# --quick
|
||||||
|
# No Cloudflare account needed. Runs a trycloudflare.com quick tunnel. The
|
||||||
|
# URL is random and changes every time the service restarts. Testing only.
|
||||||
|
#
|
||||||
|
# Other options:
|
||||||
|
# --port N Local port to expose (default: 3000)
|
||||||
|
# -h, --help Show this help
|
||||||
|
#
|
||||||
|
# The tunnel runs as cloudflared-tunnel.service under the unprivileged
|
||||||
|
# 'cloudflared' user. The package's daily cloudflared-update.timer upgrades
|
||||||
|
# cloudflared via apt and restarts the tunnel when a new version is installed.
|
||||||
|
#
|
||||||
|
# Re-running is safe; it rewrites the config and restarts the service. It also
|
||||||
|
# takes over from the units written by the older standalone setup-cloudflared.sh.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PORT=3000
|
||||||
|
MODE=""
|
||||||
|
HOSTNAME_FQDN=""
|
||||||
|
TUNNEL_NAME=""
|
||||||
|
TOKEN=""
|
||||||
|
|
||||||
|
CONF_DIR=/etc/cloudflared
|
||||||
|
ENV_FILE=$CONF_DIR/tunnel.env
|
||||||
|
SERVICE=cloudflared-tunnel.service
|
||||||
|
DROPIN=/etc/systemd/system/$SERVICE.d/setup.conf
|
||||||
|
BIN=/usr/bin/cloudflared
|
||||||
|
SVC_USER=cloudflared
|
||||||
|
# The package's postrm removes files carrying this line on purge.
|
||||||
|
MARKER="# Written by cloudflared-tunnel-setup; re-run it to change."
|
||||||
|
|
||||||
|
log() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||||
|
warn() { printf '\033[1;33mWARN:\033[0m %s\n' "$*" >&2; }
|
||||||
|
die() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||||
|
usage() { awk 'NR < 3 { next } !/^#/ { exit } { sub(/^# ?/, ""); print }' "$0"; exit "${1:-0}"; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- arguments --
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--hostname) HOSTNAME_FQDN="${2:?--hostname needs a value}"; MODE=named; shift 2 ;;
|
||||||
|
--name) TUNNEL_NAME="${2:?--name needs a value}"; shift 2 ;;
|
||||||
|
--token) TOKEN="${2:?--token needs a value}"; MODE=token; shift 2 ;;
|
||||||
|
--quick) MODE=quick; shift ;;
|
||||||
|
--port) PORT="${2:?--port needs a value}"; shift 2 ;;
|
||||||
|
-h|--help) usage 0 ;;
|
||||||
|
*) warn "Unknown option: $1"; usage 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$MODE" ]] || { warn "Choose a mode: --hostname, --token, or --quick"; usage 1; }
|
||||||
|
[[ "$PORT" =~ ^[0-9]+$ ]] && (( PORT >= 1 && PORT <= 65535 )) || die "Invalid port: $PORT"
|
||||||
|
# Both end up in config files, so keep them to the characters they can contain.
|
||||||
|
[[ -z "$HOSTNAME_FQDN" || "$HOSTNAME_FQDN" =~ ^[A-Za-z0-9.-]+$ ]] || die "Invalid hostname: $HOSTNAME_FQDN"
|
||||||
|
[[ -z "$TOKEN" || "$TOKEN" =~ ^[A-Za-z0-9+/=_-]+$ ]] || die "That doesn't look like a tunnel token"
|
||||||
|
[[ $EUID -eq 0 ]] || die "Run as root (sudo $0 ...)"
|
||||||
|
[[ -x "$BIN" ]] && getent passwd "$SVC_USER" >/dev/null \
|
||||||
|
|| die "cloudflared or the '$SVC_USER' user is missing; (re)install the cloudflared-tunnel package"
|
||||||
|
|
||||||
|
SERVICE_URL="http://localhost:${PORT}"
|
||||||
|
TUNNEL_NAME="${TUNNEL_NAME:-$(hostname -s)-${PORT}}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- legacy units -----
|
||||||
|
# setup-cloudflared.sh (the pre-package script) wrote its units to
|
||||||
|
# /etc/systemd/system, where they would run a second tunnel or shadow the
|
||||||
|
# packaged update timer. Recognise them by their contents and remove them.
|
||||||
|
migrate_legacy() {
|
||||||
|
local old_unit=/etc/systemd/system/cloudflared.service
|
||||||
|
local old_upd=/etc/systemd/system/cloudflared-update
|
||||||
|
|
||||||
|
if [[ -f "$old_unit" ]]; then
|
||||||
|
if grep -q "^ExecStart=$BIN --no-autoupdate" "$old_unit" && grep -q "^User=$SVC_USER\$" "$old_unit"; then
|
||||||
|
log "Removing cloudflared.service left by setup-cloudflared.sh"
|
||||||
|
systemctl disable --now cloudflared.service >/dev/null 2>&1 || true
|
||||||
|
rm -f "$old_unit"
|
||||||
|
else
|
||||||
|
warn "$old_unit also runs a tunnel (from 'cloudflared service install'?). Remove it if it's the same one."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$old_upd.service" ]] && grep -q '^ExecStart=/usr/local/sbin/cloudflared-update$' "$old_upd.service"; then
|
||||||
|
log "Replacing the update timer left by setup-cloudflared.sh"
|
||||||
|
systemctl disable --now cloudflared-update.timer >/dev/null 2>&1 || true
|
||||||
|
rm -f "$old_upd.service" "$old_upd.timer" /usr/local/sbin/cloudflared-update
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now cloudflared-update.timer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- service ----
|
||||||
|
# The packaged unit runs `cloudflared --no-autoupdate $CLOUDFLARED_ARGS` from
|
||||||
|
# $ENV_FILE; the drop-in carries the per-mode service Type.
|
||||||
|
# $1 = systemd service Type, $2 = cloudflared arguments, $3 = optional token
|
||||||
|
configure_service() {
|
||||||
|
local type="$1" args="$2" token="${3:-}"
|
||||||
|
install -d -m 0755 "$CONF_DIR" "$(dirname "$DROPIN")"
|
||||||
|
|
||||||
|
log "Writing $ENV_FILE"
|
||||||
|
# Root-only, since it may hold the token. systemd reads it as root before
|
||||||
|
# dropping to $SVC_USER, and it keeps the token out of `ps` output.
|
||||||
|
(
|
||||||
|
umask 077
|
||||||
|
{
|
||||||
|
echo "$MARKER"
|
||||||
|
echo "CLOUDFLARED_ARGS=$args"
|
||||||
|
if [[ -n "$token" ]]; then echo "TUNNEL_TOKEN=$token"; fi
|
||||||
|
} > "$ENV_FILE"
|
||||||
|
)
|
||||||
|
|
||||||
|
cat > "$DROPIN" <<EOF
|
||||||
|
$MARKER
|
||||||
|
[Unit]
|
||||||
|
Description=Cloudflare Tunnel -> ${SERVICE_URL}
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=${type}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
migrate_legacy
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable "$SERVICE" >/dev/null 2>&1
|
||||||
|
log "Starting $SERVICE"
|
||||||
|
systemctl restart "$SERVICE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- modes ---
|
||||||
|
setup_named() {
|
||||||
|
local cert=/root/.cloudflared/cert.pem uuid creds
|
||||||
|
|
||||||
|
if [[ ! -f "$cert" ]]; then
|
||||||
|
log "Logging in to Cloudflare. Open the URL below in a browser and pick the zone for ${HOSTNAME_FQDN}."
|
||||||
|
$BIN tunnel login
|
||||||
|
else
|
||||||
|
log "Using existing Cloudflare login ($cert)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
uuid="$($BIN tunnel list --name "$TUNNEL_NAME" --output json 2>/dev/null | jq -r '.[0].id // empty')"
|
||||||
|
if [[ -z "$uuid" ]]; then
|
||||||
|
log "Creating tunnel '$TUNNEL_NAME'"
|
||||||
|
$BIN tunnel create "$TUNNEL_NAME"
|
||||||
|
uuid="$($BIN tunnel list --name "$TUNNEL_NAME" --output json | jq -r '.[0].id // empty')"
|
||||||
|
[[ -n "$uuid" ]] || die "Tunnel was created, but its ID could not be found"
|
||||||
|
else
|
||||||
|
log "Tunnel '$TUNNEL_NAME' already exists ($uuid)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -d -m 0755 "$CONF_DIR"
|
||||||
|
creds="$CONF_DIR/${uuid}.json"
|
||||||
|
if [[ ! -f "$creds" ]]; then
|
||||||
|
if [[ -f "/root/.cloudflared/${uuid}.json" ]]; then
|
||||||
|
install -m 0600 "/root/.cloudflared/${uuid}.json" "$creds"
|
||||||
|
else
|
||||||
|
log "Fetching credentials for existing tunnel"
|
||||||
|
$BIN tunnel token --cred-file "$creds" "$uuid"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Only the service user may read the tunnel secret. cert.pem stays in
|
||||||
|
# /root/.cloudflared, so the daemon cannot create or delete tunnels.
|
||||||
|
chown "$SVC_USER:$SVC_USER" "$creds"
|
||||||
|
chmod 0600 "$creds"
|
||||||
|
|
||||||
|
log "Writing $CONF_DIR/config.yml (${HOSTNAME_FQDN} -> ${SERVICE_URL})"
|
||||||
|
cat > "$CONF_DIR/config.yml" <<EOF
|
||||||
|
$MARKER
|
||||||
|
tunnel: ${uuid}
|
||||||
|
credentials-file: ${creds}
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
- hostname: ${HOSTNAME_FQDN}
|
||||||
|
service: ${SERVICE_URL}
|
||||||
|
- service: http_status:404
|
||||||
|
EOF
|
||||||
|
$BIN tunnel --config "$CONF_DIR/config.yml" ingress validate
|
||||||
|
|
||||||
|
log "Routing DNS: ${HOSTNAME_FQDN} -> tunnel ${TUNNEL_NAME}"
|
||||||
|
if ! $BIN tunnel route dns "$uuid" "$HOSTNAME_FQDN"; then
|
||||||
|
warn "DNS route failed. If a record for ${HOSTNAME_FQDN} already exists, delete it in the"
|
||||||
|
warn "Cloudflare dashboard or re-run: cloudflared tunnel route dns --overwrite-dns $uuid $HOSTNAME_FQDN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
configure_service notify "--config $CONF_DIR/config.yml tunnel run"
|
||||||
|
PUBLIC_URL="https://${HOSTNAME_FQDN}"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_token() {
|
||||||
|
configure_service notify "tunnel run" "$TOKEN"
|
||||||
|
PUBLIC_URL="(the public hostname set in the Zero Trust dashboard)"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_quick() {
|
||||||
|
configure_service simple "tunnel --url ${SERVICE_URL}"
|
||||||
|
log "Waiting for the trycloudflare.com URL"
|
||||||
|
PUBLIC_URL=""
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
PUBLIC_URL="$(journalctl -u "$SERVICE" --since '-2min' --no-pager -o cat 2>/dev/null \
|
||||||
|
| grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' | tail -n1 || true)"
|
||||||
|
[[ -n "$PUBLIC_URL" ]] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
PUBLIC_URL="${PUBLIC_URL:-(not found yet; check: journalctl -u cloudflared-tunnel | grep trycloudflare)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------- main ---
|
||||||
|
case "$MODE" in
|
||||||
|
named) setup_named ;;
|
||||||
|
token) setup_token ;;
|
||||||
|
quick) setup_quick ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
if systemctl is-active --quiet "$SERVICE"; then
|
||||||
|
log "cloudflared is running"
|
||||||
|
else
|
||||||
|
systemctl status "$SERVICE" --no-pager || true
|
||||||
|
die "cloudflared failed to start. See: journalctl -u cloudflared-tunnel -e"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! ss -ltnH "sport = :${PORT}" | grep -q .; then
|
||||||
|
warn "Nothing is listening on port ${PORT} yet. Visitors will get a 502 until your app is running."
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
------------------------------------------------------------------
|
||||||
|
Tunnel: ${PUBLIC_URL}
|
||||||
|
Origin: ${SERVICE_URL}
|
||||||
|
Runs as: ${SVC_USER} (${SERVICE})
|
||||||
|
Logs: journalctl -u cloudflared-tunnel -f
|
||||||
|
Restart: systemctl restart cloudflared-tunnel
|
||||||
|
Updates: daily (systemctl list-timers cloudflared-update.timer)
|
||||||
|
run now: systemctl start cloudflared-update
|
||||||
|
history: journalctl -u cloudflared-update
|
||||||
|
------------------------------------------------------------------
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [[ "$MODE" == token ]]; then
|
||||||
|
cat <<EOF
|
||||||
|
In the Zero Trust dashboard (Networks > Tunnels > your tunnel >
|
||||||
|
Public Hostname), set the service to: ${SERVICE_URL}
|
||||||
|
------------------------------------------------------------------
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
25
cloudflared-tunnel/src/cloudflared-tunnel.service
Normal file
25
cloudflared-tunnel/src/cloudflared-tunnel.service
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Cloudflare Tunnel
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
# Written by cloudflared-tunnel-setup; until then this unit does nothing.
|
||||||
|
ConditionPathExists=/etc/cloudflared/tunnel.env
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=notify
|
||||||
|
User=cloudflared
|
||||||
|
Group=cloudflared
|
||||||
|
EnvironmentFile=/etc/cloudflared/tunnel.env
|
||||||
|
ExecStart=/usr/bin/cloudflared --no-autoupdate $CLOUDFLARED_ARGS
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
TimeoutStartSec=120
|
||||||
|
|
||||||
|
# Sandboxing
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
23
cloudflared-tunnel/src/cloudflared-update
Executable file
23
cloudflared-tunnel/src/cloudflared-update
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# cloudflared-update — upgrade cloudflared via apt and restart the tunnel only
|
||||||
|
# when the version actually changed. Run daily by cloudflared-update.timer.
|
||||||
|
#
|
||||||
|
# cloudflared's built-in autoupdate disables itself for apt installs, and the
|
||||||
|
# unprivileged, read-only service couldn't replace its own binary anyway.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
before=$(dpkg-query -W -f='${Version}' cloudflared)
|
||||||
|
# Refresh every source: cloudflared may come from Cloudflare's repo or from a
|
||||||
|
# mirror in your own. A broken unrelated source shouldn't block the upgrade.
|
||||||
|
apt-get update -qq || echo "apt-get update reported errors; continuing" >&2
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade \
|
||||||
|
-o DPkg::Lock::Timeout=300 cloudflared
|
||||||
|
after=$(dpkg-query -W -f='${Version}' cloudflared)
|
||||||
|
|
||||||
|
if [ "$before" != "$after" ]; then
|
||||||
|
echo "cloudflared upgraded ${before} -> ${after}; restarting tunnel"
|
||||||
|
systemctl try-restart cloudflared-tunnel.service
|
||||||
|
else
|
||||||
|
echo "cloudflared ${after} is up to date"
|
||||||
|
fi
|
||||||
8
cloudflared-tunnel/src/cloudflared-update.service
Normal file
8
cloudflared-tunnel/src/cloudflared-update.service
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Upgrade cloudflared via apt and restart the tunnel if updated
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/sbin/cloudflared-update
|
||||||
10
cloudflared-tunnel/src/cloudflared-update.timer
Normal file
10
cloudflared-tunnel/src/cloudflared-update.timer
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Daily cloudflared update check
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=daily
|
||||||
|
RandomizedDelaySec=1h
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
320
setup-cloudflared.sh
Executable file
320
setup-cloudflared.sh
Executable file
@ -0,0 +1,320 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# setup-cloudflared.sh — install cloudflared on Debian 12 (bookworm) and run a
|
||||||
|
# Cloudflare Tunnel as a systemd service that forwards to a local port (default 3000).
|
||||||
|
#
|
||||||
|
# Modes (pick one):
|
||||||
|
#
|
||||||
|
# --hostname app.example.com [--name my-tunnel]
|
||||||
|
# Locally-managed tunnel. Logs in to Cloudflare (opens a URL you visit in a
|
||||||
|
# browser), creates the tunnel, writes /etc/cloudflared/config.yml with
|
||||||
|
# ingress -> http://localhost:PORT, and creates the DNS CNAME.
|
||||||
|
#
|
||||||
|
# --token <TUNNEL_TOKEN>
|
||||||
|
# Dashboard-managed tunnel (Zero Trust > Networks > Tunnels). The ingress is
|
||||||
|
# configured in the dashboard: set the public hostname's service to
|
||||||
|
# http://localhost:PORT there.
|
||||||
|
#
|
||||||
|
# --quick
|
||||||
|
# No Cloudflare account needed. Runs a trycloudflare.com quick tunnel. The
|
||||||
|
# URL is random and changes every time the service restarts. Testing only.
|
||||||
|
#
|
||||||
|
# Other options:
|
||||||
|
# --port N Local port to expose (default: 3000)
|
||||||
|
# -h, --help Show this help
|
||||||
|
#
|
||||||
|
# The tunnel runs as the unprivileged 'cloudflared' user. A daily systemd timer
|
||||||
|
# (cloudflared-update.timer) upgrades cloudflared via apt and restarts the
|
||||||
|
# tunnel when a new version is installed.
|
||||||
|
#
|
||||||
|
# Re-running the script is safe; it updates the config and restarts the service.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PORT=3000
|
||||||
|
MODE=""
|
||||||
|
HOSTNAME_FQDN=""
|
||||||
|
TUNNEL_NAME=""
|
||||||
|
TOKEN=""
|
||||||
|
|
||||||
|
CONF_DIR=/etc/cloudflared
|
||||||
|
UNIT_FILE=/etc/systemd/system/cloudflared.service
|
||||||
|
BIN=/usr/bin/cloudflared
|
||||||
|
SVC_USER=cloudflared
|
||||||
|
|
||||||
|
log() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||||
|
warn() { printf '\033[1;33mWARN:\033[0m %s\n' "$*" >&2; }
|
||||||
|
die() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||||
|
usage() { awk 'NR < 3 { next } !/^#/ { exit } { sub(/^# ?/, ""); print }' "$0"; exit "${1:-0}"; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- arguments --
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--hostname) HOSTNAME_FQDN="${2:?--hostname needs a value}"; MODE=named; shift 2 ;;
|
||||||
|
--name) TUNNEL_NAME="${2:?--name needs a value}"; shift 2 ;;
|
||||||
|
--token) TOKEN="${2:?--token needs a value}"; MODE=token; shift 2 ;;
|
||||||
|
--quick) MODE=quick; shift ;;
|
||||||
|
--port) PORT="${2:?--port needs a value}"; shift 2 ;;
|
||||||
|
-h|--help) usage 0 ;;
|
||||||
|
*) warn "Unknown option: $1"; usage 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$MODE" ]] || { warn "Choose a mode: --hostname, --token, or --quick"; usage 1; }
|
||||||
|
[[ "$PORT" =~ ^[0-9]+$ ]] && (( PORT >= 1 && PORT <= 65535 )) || die "Invalid port: $PORT"
|
||||||
|
[[ $EUID -eq 0 ]] || die "Run as root (sudo $0 ...)"
|
||||||
|
|
||||||
|
. /etc/os-release
|
||||||
|
[[ "${ID:-}" == debian ]] || warn "This script targets Debian; detected '${ID:-unknown}'. Continuing anyway."
|
||||||
|
[[ "${VERSION_CODENAME:-}" == bookworm ]] || warn "Written for Debian 12 (bookworm); detected '${VERSION_CODENAME:-unknown}'."
|
||||||
|
|
||||||
|
SERVICE_URL="http://localhost:${PORT}"
|
||||||
|
TUNNEL_NAME="${TUNNEL_NAME:-$(hostname -s)-${PORT}}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ install --
|
||||||
|
install_cloudflared() {
|
||||||
|
log "Installing prerequisites"
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq ca-certificates curl gnupg jq >/dev/null
|
||||||
|
|
||||||
|
log "Adding Cloudflare apt repository"
|
||||||
|
install -d -m 0755 /usr/share/keyrings
|
||||||
|
curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg \
|
||||||
|
-o /usr/share/keyrings/cloudflare-public-v2.gpg
|
||||||
|
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main' \
|
||||||
|
> /etc/apt/sources.list.d/cloudflared.list
|
||||||
|
|
||||||
|
log "Installing cloudflared"
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq cloudflared >/dev/null
|
||||||
|
log "$($BIN --version)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Unprivileged system account the daemon runs as. It only makes outbound
|
||||||
|
# connections and proxies to localhost, so it needs no root privileges.
|
||||||
|
create_service_user() {
|
||||||
|
if ! getent passwd "$SVC_USER" >/dev/null; then
|
||||||
|
log "Creating system user '$SVC_USER'"
|
||||||
|
useradd --system --user-group --no-create-home \
|
||||||
|
--home-dir /nonexistent --shell /usr/sbin/nologin "$SVC_USER"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------- systemd unit ----
|
||||||
|
# $1 = systemd service Type, $2 = cloudflared arguments, $3 = optional EnvironmentFile
|
||||||
|
write_unit() {
|
||||||
|
local type="$1" args="$2" envfile="${3:-}"
|
||||||
|
log "Writing $UNIT_FILE"
|
||||||
|
cat > "$UNIT_FILE" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Cloudflare Tunnel -> ${SERVICE_URL}
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=${type}
|
||||||
|
User=${SVC_USER}
|
||||||
|
Group=${SVC_USER}
|
||||||
|
${envfile:+EnvironmentFile=${envfile}}
|
||||||
|
ExecStart=${BIN} --no-autoupdate ${args}
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
TimeoutStartSec=120
|
||||||
|
|
||||||
|
# Sandboxing
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable cloudflared >/dev/null 2>&1
|
||||||
|
log "Starting cloudflared service"
|
||||||
|
systemctl restart cloudflared
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- updater ----
|
||||||
|
# cloudflared's built-in autoupdate disables itself for apt installs, and the
|
||||||
|
# unprivileged, read-only service couldn't replace its own binary anyway. So a
|
||||||
|
# root timer upgrades it through apt and restarts the tunnel only when the
|
||||||
|
# version actually changed (same idea as Cloudflare's cloudflared-update.timer).
|
||||||
|
install_updater() {
|
||||||
|
log "Installing daily auto-update timer"
|
||||||
|
|
||||||
|
cat > /usr/local/sbin/cloudflared-update <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
before=$(dpkg-query -W -f='${Version}' cloudflared)
|
||||||
|
# Refresh only the cloudflared repo; leave other package lists untouched.
|
||||||
|
apt-get update -qq \
|
||||||
|
-o Dir::Etc::sourcelist=/etc/apt/sources.list.d/cloudflared.list \
|
||||||
|
-o Dir::Etc::sourceparts=- -o APT::Get::List-Cleanup=0
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade \
|
||||||
|
-o DPkg::Lock::Timeout=300 cloudflared
|
||||||
|
after=$(dpkg-query -W -f='${Version}' cloudflared)
|
||||||
|
if [ "$before" != "$after" ]; then
|
||||||
|
echo "cloudflared upgraded ${before} -> ${after}; restarting tunnel"
|
||||||
|
systemctl try-restart cloudflared.service
|
||||||
|
else
|
||||||
|
echo "cloudflared ${after} is up to date"
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
chmod 0755 /usr/local/sbin/cloudflared-update
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/cloudflared-update.service <<'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=Upgrade cloudflared via apt and restart the tunnel if updated
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/cloudflared-update
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/cloudflared-update.timer <<'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=Daily cloudflared update check
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=daily
|
||||||
|
RandomizedDelaySec=1h
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now cloudflared-update.timer >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- modes ---
|
||||||
|
setup_named() {
|
||||||
|
local cert=/root/.cloudflared/cert.pem uuid creds
|
||||||
|
|
||||||
|
if [[ ! -f "$cert" ]]; then
|
||||||
|
log "Logging in to Cloudflare. Open the URL below in a browser and pick the zone for ${HOSTNAME_FQDN}."
|
||||||
|
$BIN tunnel login
|
||||||
|
else
|
||||||
|
log "Using existing Cloudflare login ($cert)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
uuid="$($BIN tunnel list --name "$TUNNEL_NAME" --output json 2>/dev/null | jq -r '.[0].id // empty')"
|
||||||
|
if [[ -z "$uuid" ]]; then
|
||||||
|
log "Creating tunnel '$TUNNEL_NAME'"
|
||||||
|
$BIN tunnel create "$TUNNEL_NAME"
|
||||||
|
uuid="$($BIN tunnel list --name "$TUNNEL_NAME" --output json | jq -r '.[0].id // empty')"
|
||||||
|
[[ -n "$uuid" ]] || die "Tunnel was created, but its ID could not be found"
|
||||||
|
else
|
||||||
|
log "Tunnel '$TUNNEL_NAME' already exists ($uuid)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -d -m 0755 "$CONF_DIR"
|
||||||
|
creds="$CONF_DIR/${uuid}.json"
|
||||||
|
if [[ ! -f "$creds" ]]; then
|
||||||
|
if [[ -f "/root/.cloudflared/${uuid}.json" ]]; then
|
||||||
|
install -m 0600 "/root/.cloudflared/${uuid}.json" "$creds"
|
||||||
|
else
|
||||||
|
log "Fetching credentials for existing tunnel"
|
||||||
|
$BIN tunnel token --cred-file "$creds" "$uuid"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Only the service user may read the tunnel secret. cert.pem stays in
|
||||||
|
# /root/.cloudflared, so the daemon cannot create or delete tunnels.
|
||||||
|
chown "$SVC_USER:$SVC_USER" "$creds"
|
||||||
|
chmod 0600 "$creds"
|
||||||
|
|
||||||
|
log "Writing $CONF_DIR/config.yml (${HOSTNAME_FQDN} -> ${SERVICE_URL})"
|
||||||
|
cat > "$CONF_DIR/config.yml" <<EOF
|
||||||
|
tunnel: ${uuid}
|
||||||
|
credentials-file: ${creds}
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
- hostname: ${HOSTNAME_FQDN}
|
||||||
|
service: ${SERVICE_URL}
|
||||||
|
- service: http_status:404
|
||||||
|
EOF
|
||||||
|
$BIN tunnel --config "$CONF_DIR/config.yml" ingress validate
|
||||||
|
|
||||||
|
log "Routing DNS: ${HOSTNAME_FQDN} -> tunnel ${TUNNEL_NAME}"
|
||||||
|
if ! $BIN tunnel route dns "$uuid" "$HOSTNAME_FQDN"; then
|
||||||
|
warn "DNS route failed. If a record for ${HOSTNAME_FQDN} already exists, delete it in the"
|
||||||
|
warn "Cloudflare dashboard or re-run: cloudflared tunnel route dns --overwrite-dns $uuid $HOSTNAME_FQDN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_unit notify "--config $CONF_DIR/config.yml tunnel run"
|
||||||
|
PUBLIC_URL="https://${HOSTNAME_FQDN}"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_token() {
|
||||||
|
install -d -m 0755 "$CONF_DIR"
|
||||||
|
# Keep the token out of the unit file and out of `ps` output. systemd reads
|
||||||
|
# this as root before dropping to $SVC_USER, so it can stay root-only.
|
||||||
|
(umask 077; printf 'TUNNEL_TOKEN=%s\n' "$TOKEN" > "$CONF_DIR/tunnel.env")
|
||||||
|
write_unit notify "tunnel run" "$CONF_DIR/tunnel.env"
|
||||||
|
PUBLIC_URL="(the public hostname set in the Zero Trust dashboard)"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_quick() {
|
||||||
|
write_unit simple "tunnel --url ${SERVICE_URL}"
|
||||||
|
log "Waiting for the trycloudflare.com URL"
|
||||||
|
PUBLIC_URL=""
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
PUBLIC_URL="$(journalctl -u cloudflared --since '-2min' --no-pager -o cat 2>/dev/null \
|
||||||
|
| grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' | tail -n1 || true)"
|
||||||
|
[[ -n "$PUBLIC_URL" ]] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
PUBLIC_URL="${PUBLIC_URL:-(not found yet; check: journalctl -u cloudflared | grep trycloudflare)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------- main ---
|
||||||
|
install_cloudflared
|
||||||
|
create_service_user
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
named) setup_named ;;
|
||||||
|
token) setup_token ;;
|
||||||
|
quick) setup_quick ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
install_updater
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
if systemctl is-active --quiet cloudflared; then
|
||||||
|
log "cloudflared is running"
|
||||||
|
else
|
||||||
|
systemctl status cloudflared --no-pager || true
|
||||||
|
die "cloudflared failed to start. See: journalctl -u cloudflared -e"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! ss -ltnH "sport = :${PORT}" | grep -q .; then
|
||||||
|
warn "Nothing is listening on port ${PORT} yet. Visitors will get a 502 until your app is running."
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
------------------------------------------------------------------
|
||||||
|
Tunnel: ${PUBLIC_URL}
|
||||||
|
Origin: ${SERVICE_URL}
|
||||||
|
Runs as: ${SVC_USER}
|
||||||
|
Logs: journalctl -u cloudflared -f
|
||||||
|
Restart: systemctl restart cloudflared
|
||||||
|
Updates: daily (systemctl list-timers cloudflared-update.timer)
|
||||||
|
run now: systemctl start cloudflared-update
|
||||||
|
history: journalctl -u cloudflared-update
|
||||||
|
------------------------------------------------------------------
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [[ "$MODE" == token ]]; then
|
||||||
|
cat <<EOF
|
||||||
|
In the Zero Trust dashboard (Networks > Tunnels > your tunnel >
|
||||||
|
Public Hostname), set the service to: ${SERVICE_URL}
|
||||||
|
------------------------------------------------------------------
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
Loading…
Reference in New Issue
Block a user