Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

cf-portguard

cf-portguard restricts selected TCP ports on an Ubuntu origin server to Cloudflare's published origin-facing IPv4 and IPv6 networks plus CIDRs you explicitly trust. It uses one isolated nftables table and refreshes it from Cloudflare's public IP API.

The project targets stable Zig 0.16.0, has no third-party Zig dependencies, and is licensed under MIT.

Threat model and scope

This tool reduces direct-origin exposure when a service is intended to receive traffic through Cloudflare. A remote source outside Cloudflare/trusted networks is rejected when connecting to a protected TCP port. IPv4 and IPv6 are treated independently and both Cloudflare lists must validate before any update.

Restricting an origin to Cloudflare IP ranges is not equivalent to cryptographically authenticating Cloudflare. Another Cloudflare customer may be able to cause traffic to originate from Cloudflare infrastructure, and a compromised trusted host remains trusted. For stronger origin identity, evaluate Cloudflare Tunnel or Authenticated Origin Pulls, and authenticate the application itself.

cf-portguard does not:

  • protect unlisted ports, UDP, local processes, or application-layer vulnerabilities;
  • configure Cloudflare, DNS, TLS, Tunnel, or Origin Pulls;
  • override drops in other nftables base chains;
  • manage SSH, UFW, iptables, unrelated nftables tables/chains, or the machine's INPUT policy;
  • persist the managed nftables table independently of its systemd boot-time sync.

Requirements

  • Ubuntu with nftables and systemd (deployment target)
  • network access and a valid CA trust store for https://api.cloudflare.com
  • Zig 0.16.0 for building
  • root execution for sync; the supplied unit bounds the service to CAP_NET_ADMIN

Cloudflare's source of truth is the public GET https://api.cloudflare.com/client/v4/ips endpoint. No API token or secret is used.

Build and test

zig version                 # must report 0.16.0
zig build
zig build test
zig build -Doptimize=ReleaseSafe

Cross-compile static Linux binaries from macOS or another Zig host:

zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe
zig build -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafe

The binary is written to zig-out/bin/cf-portguard. Unit tests are pure and never invoke nft or require root.

Configuration

The default file is /etc/cf-portguard.conf; override it with --config PATH before or after the command.

# Individual ports and inclusive ranges are accepted.
ports = 443,16001,16002,16000-16980

# Repeat trusted_cidr for direct administrative/monitoring sources.
trusted_cidr = 203.0.113.10/32
trusted_cidr = 2001:db8::10/128

Ports must be 1 through 65535. At least one is required. Duplicate, adjacent, and overlapping port intervals are merged; duplicate trusted CIDRs are removed. Unknown keys, invalid CIDRs, empty values, and malformed ranges fail before nftables is touched. # starts a comment and surrounding whitespace is ignored.

Commands

cf-portguard --config ./cf-portguard.conf check
cf-portguard --config ./cf-portguard.conf print
sudo cf-portguard sync
cf-portguard version
cf-portguard help

check validates configuration, live Cloudflare connectivity/response, Linux, and nft --version, without changing firewall state. print performs the same config/API validation and prints an audit transaction without applying it. Its transaction includes deletion of the managed table because it represents normal reconciliation; do not apply it manually unless that table already exists. sync also checks privilege, probes whether the owned table exists, and submits the correct complete transaction.

Logs are structured key=value messages on stdout/stderr for journald. They include the Cloudflare etag, network counts, normalized port-interval count, and outcome. There are no secrets to log.

Firewall behavior and failure safety

The tool exclusively owns inet cf_portguard, containing sets for Cloudflare IPv4, Cloudflare IPv6, trusted IPv4, trusted IPv6, and protected TCP ports. Its input base chain has an accept policy so unrelated traffic continues to the rest of the host ruleset. For a protected destination port it allows loopback, Cloudflare sources, and trusted sources, then rejects everything else with a TCP reset.

All inputs are validated before mutation. Replacement is one nftables batch: deletion of the old owned table and construction of its complete replacement commit together or not at all. A bad fetch, HTTP response, JSON value, CIDR, local config, missing privilege, or rejected transaction therefore does not remove a previously valid table. On a machine with no prior successful sync, failure leaves the firewall unchanged.

The API etag is logged but successful runs always reconcile. This ensures config changes and repaired external drift apply even if Cloudflare's list has not changed.

Ubuntu installation

Single-command remote deployment

From the repository on your workstation, deploy to an x86_64 Ubuntu host with:

CF_PORTGUARD_DEPLOY_HOST=ubuntu@origin.example.com \
CF_PORTGUARD_PORTS=443,16001-16010 \
make deploy

This compiles a static ReleaseSafe Linux binary, validates the requested ports locally, uploads the binary/config/systemd units through scp, installs them through explicit ssh argument vectors, runs a non-mutating remote check, enables the timer, performs the first sync, and prints the oneshot service result. The generated config contains only ports; therefore those ports accept Cloudflare IPv4/IPv6 sources and loopback, with no additional trusted CIDRs.

The SSH account must use working key-based authentication, already have its host key in known_hosts, and have non-interactive (NOPASSWD) sudo access for the fixed installation and systemctl operations. Deployment uses SSH batch mode and a 15-second connection timeout. Host values are deliberately limited to simple [user@]hostname or IPv4 forms; SSH aliases work when their names use the accepted characters. Shell fragments, SSH options, arbitrary remote commands, and IPv6-literal destinations are rejected. The helper invokes ssh and scp directly and never constructs sh -c commands, although OpenSSH itself necessarily asks the remote SSH server to execute the fixed command arguments.

Optional deployment settings:

# ARM64 Ubuntu target
CF_PORTGUARD_DEPLOY_ARCH=aarch64 \
CF_PORTGUARD_DEPLOY_HOST=ubuntu@arm-origin.example.com \
CF_PORTGUARD_PORTS=443 \
make deploy

# Non-default SSH port
CF_PORTGUARD_SSH_PORT=2222 \
CF_PORTGUARD_DEPLOY_HOST=ubuntu@origin.example.com \
CF_PORTGUARD_PORTS=443 \
make deploy

CF_PORTGUARD_DEPLOY_ARCH accepts x86_64 (default) or aarch64. Remote deployment intentionally overwrites /etc/cf-portguard.conf with the supplied port-only configuration. Use the manual installation path when preserving or adding trusted CIDRs. A failed validation, upload, connectivity check, or nftables sync does not delete the existing managed firewall table. Temporary upload files are removed on both success and ordinary failure. Review console/out-of-band access before remotely changing firewall policy.

Compile the selected deployment artifacts without making a connection with zig build deploy-artifacts; this is useful in CI.

Manual deployment

Build for the target architecture, then install the binary, reviewed configuration, and systemd units:

sudo install -m 0755 zig-out/bin/cf-portguard /usr/local/bin/cf-portguard
sudo install -m 0644 packaging/systemd/cf-portguard.service /etc/systemd/system/
sudo install -m 0644 packaging/systemd/cf-portguard.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now cf-portguard.timer
sudo systemctl start cf-portguard.service

On a first installation only, copy packaging/cf-portguard.conf.example to /etc/cf-portguard.conf and review it before enabling the timer. Do not overwrite an existing configuration during an upgrade. The service runs as root because the CLI deliberately checks effective UID before mutation, while its capability bounding and ambient sets contain only CAP_NET_ADMIN. Additional unit hardening restricts filesystems, devices, namespaces, and kernel interfaces.

Operations

systemctl status cf-portguard.timer
systemctl status cf-portguard.service
journalctl -u cf-portguard.service
systemctl list-timers cf-portguard.timer
sudo nft list table inet cf_portguard

To upgrade, build the desired version, replace /usr/local/bin/cf-portguard, verify with check, and start the service. Configuration is not migrated or overwritten automatically.

To uninstall conservatively:

sudo systemctl disable --now cf-portguard.timer
sudo rm -f /etc/systemd/system/cf-portguard.timer /etc/systemd/system/cf-portguard.service
sudo rm -f /usr/local/bin/cf-portguard
sudo systemctl daemon-reload

Keep /etc/cf-portguard.conf and the active managed nftables table so uninstalling cannot silently open protected ports. If you explicitly want to remove protection afterward, inspect first and run sudo nft delete table inet cf_portguard.

Existing nftables and UFW setups

cf-portguard does not configure UFW and does not assume it is absent. Multiple nftables base chains can share a hook. A Cloudflare/trusted accept in this table does not bypass a later drop in another table; a rejection here for protected traffic is final. Priority -10 is intended to reject untrusted protected connections before common filter chains, while leaving every other packet to existing policy. Inspect the full ruleset and test on a console-accessible host before production use.

Do not create unrelated rules inside inet cf_portguard; sync replaces that entire owned table. Choose another table for local rules.

Troubleshooting and recovery

  • config_read or config_validation: run cf-portguard --config PATH check; check spelling, ports, ranges, and numeric CIDRs.
  • cloudflare_fetch: verify DNS, outbound HTTPS, clock, and CA certificates. The existing managed table remains active.
  • NftUnavailable: install nftables (sudo apt install nftables) and ensure nft is on the service PATH.
  • InsufficientPrivileges: use the supplied systemd unit or sudo; print and most of check do not require firewall privilege.
  • NftTransactionFailed: inspect nft diagnostics in the journal and verify no administrator has created an incompatible object named inet cf_portguard.

If a configuration mistake blocks expected direct access, retain console or out-of-band access, correct the config, run print, then sync. For emergency recovery, inspect sudo nft list table inet cf_portguard and explicitly remove only that table with sudo nft delete table inet cf_portguard. Never flush the global ruleset. Removing the table opens only what the rest of the machine's firewall permits.

IPv4 clients are checked only against IPv4 sets and IPv6 clients only against IPv6 sets. A successful update requires non-empty valid Cloudflare lists for both, even on a host where one family is currently unused; this avoids accidental partial protection when IPv6 is enabled later.

License

MIT. See LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages