Update Hetzner Cloud Firewall Rules with Current Cloudflare IP Ranges
This tool, cf-ips-to-hcloud-fw, helps you keep your Hetzner Cloud firewall
rules up-to-date with the current Cloudflare IP ranges.
Table of Contents
- Overview
- Installation
- Configuration
- Usage
- Running on a Schedule
- Verifying SLSA Attestations
- Contributing
- Security
Overview
cf-ips-to-hcloud-fw fetches the current Cloudflare IP
ranges and updates your Hetzner Cloud firewall
rules using the hcloud
API.
The tool specifically targets incoming firewall rules and replaces the
networks with Cloudflare networks if their description contains
__CLOUDFLARE_IPS_V4__, __CLOUDFLARE_IPS_V6__ or __CLOUDFLARE_IPS__.
| Text in rule description | Cloudflare IP ranges |
|---|---|
__CLOUDFLARE_IPS_V4__ |
IPv4 only |
__CLOUDFLARE_IPS_V6__ |
IPv6 only |
__CLOUDFLARE_IPS__ |
IPv4 + IPv6 |
Note: Having both __CLOUDFLARE_IPS_V4__ and __CLOUDFLARE_IPS_V6__ in a rule
description is equivalent to having __CLOUDFLARE_IPS__ there.
Installation
Using Python
To install cf-ips-to-hcloud-fw using Python, we recommend using
pipx or uvx.
Both are tools for installing and running Python applications in isolated
environments. If you already have uv installed, uvx is the quickest option.
Using pipx (Recommended for Most Users)
-
Install
cf-ips-to-hcloud-fwusingpipx:pipx install cf-ips-to-hcloud-fw
-
Verify the installation:
cf-ips-to-hcloud-fw -h
You should see the usage information for cf-ips-to-hcloud-fw.
To upgrade cf-ips-to-hcloud-fw, run:
[!TIP] To upgrade
cf-ips-to-hcloud-fw, runpipx upgrade cf-ips-to-hcloud-fw.
Using uvx (Recommended for uv Users)
If you have uv installed, you can run cf-ips-to-hcloud-fw directly without
installing it:
uvx cf-ips-to-hcloud-fw -c config.yaml
This approach automatically downloads and runs the latest version in an isolated environment without modifying your system Python.
[!TIP]
uvxalways fetches and runs the latest version, so no upgrade command is needed.
Using pip
We strongly recommend using a virtual environment when installing Python
packages with pip. This helps to avoid conflicts between packages and allows you
to manage packages on a per-project basis.
-
Create a virtual environment:
python3 -m venv cf-ips-to-hcloud-fw-venv
-
Install
cf-ips-to-hcloud-fwinto the virtual environment:./cf-ips-to-hcloud-fw-venv/bin/pip3 install cf-ips-to-hcloud-fw
-
Verify the installation:
./cf-ips-to-hcloud-fw-venv/bin/cf-ips-to-hcloud-fw -h
You should see the usage information for cf-ips-to-hcloud-fw.
[!TIP] To upgrade
cf-ips-to-hcloud-fwin your virtual environment, run./cf-ips-to-hcloud-fw-venv/bin/pip3 install --upgrade cf-ips-to-hcloud-fw.
Docker and Kubernetes
As an alternative, cf-ips-to-hcloud-fw can be run using Docker or a Kubernetes
CronJob. Simply mount your configuration file as /usr/src/app/config.yaml.
Here's an example using Docker:
docker run --rm \
--mount type=bind,source=$(pwd)/config.yaml,target=/usr/src/app/config.yaml,readonly \
jkreileder/cf-ips-to-hcloud-fw:1.4.2
(Add --pull=always if you use a rolling image tag.)
Alternatively, for a single project you can skip the mounted file and pass the
token and firewalls as environment variables (see Using Environment
Variables). The image auto-detects
a mounted config.yaml and otherwise falls back to these variables, so no
command override is needed:
docker run --rm \
-e HCLOUD_TOKEN=your-api-token \
-e HCLOUD_FIREWALLS=$'firewall-1\nfirewall-2' \
jkreileder/cf-ips-to-hcloud-fw:1.4.2
Docker images for cf-ips-to-hcloud-fw are available for both linux/amd64 and
linux/arm64 architectures. The Docker images support the following tags:
1: This tag always points to the latest1.x.xrelease.1.4: This tag always points to the latest1.4.xrelease.1.4.2: This tag points to the specific1.4.2release.main: This tag points to the most recent development version ofcf-ips-to-hcloud-fw. Use this at your own risk as it may contain unstable changes.
You can find the Docker images at:
- Docker Hub:
jkreileder/cf-ips-to-hcloud-fwordocker.io/jkreileder/cf-ips-to-hcloud-fw - Quay.io:
quay.io/jkreileder/cf-ips-to-hcloud-fw - GitHub Packages:
ghcr.io/jkreileder/cf-ips-to-hcloud-fw
Here's an example of how to create a Kubernetes Secret for your configuration:
apiVersion: v1
kind: Secret
metadata:
name: cf-ips-to-hcloud-fw-config
type: Opaque
stringData:
config.yaml: |
- token: API_TOKEN_FOR_PROJECT_1
firewalls:
- firewall-1
- firewall-2
- token: API_TOKEN_FOR_PROJECT_2
firewalls:
- default
And here's an example of a Kubernetes CronJob that uses the Secret:
apiVersion: batch/v1
kind: CronJob
metadata:
name: cf-ips-to-hcloud-fw
spec:
schedule: "0 * * * *" # Run every hour
jobTemplate:
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65534
containers:
- name: cf-ips-to-hcloud-fw
image: jkreileder/cf-ips-to-hcloud-fw:1.4.2
# imagePullPolicy: Always # Uncomment this if you use a rolling image tag
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: config-volume
mountPath: /usr/src/app/config.yaml
subPath: config.yaml
volumes:
- name: config-volume
secret:
secretName: cf-ips-to-hcloud-fw-config
restartPolicy: OnFailure
For a single project, you can drop the Secret, volume, and volumeMounts above
and instead supply the token and firewalls via env (for example from a
Secret). No command override is needed — with no config.yaml mounted, the tool
falls back to these variables:
containers:
- name: cf-ips-to-hcloud-fw
image: jkreileder/cf-ips-to-hcloud-fw:1.4.2
env:
- name: HCLOUD_TOKEN
valueFrom:
secretKeyRef:
name: cf-ips-to-hcloud-fw-token
key: token
- name: HCLOUD_FIREWALLS
value: |
firewall-1
firewall-2
Configuration
Preparing the Hetzner Cloud Firewall
To prepare your Hetzner Cloud Firewall:
-
Set the rule descriptions: Include
__CLOUDFLARE_IPS_V4__,__CLOUDFLARE_IPS_V6__, or__CLOUDFLARE_IPS__in the description of any incoming firewall rule where you want to insert Cloudflare networks. This will be used as a marker to identify which rules should be updated with the Cloudflare IP ranges. -
Generate an API token: You'll need an API token with write permissions for the project that contains the firewall. This token will be used to authenticate your requests to the Hetzner Cloud API. You can generate a token in the Hetzner Cloud Console by going to "Security" > "API Tokens" > "Generate API Token". Read Securing the API Token before you do — the token is more powerful than this tool needs.
Securing the API Token
Hetzner Cloud API tokens are project-wide and read-write. There is no firewall-only scope: the same token this tool uses to edit firewall rules can also create and delete servers, volumes, snapshots, and every other resource in that project.
This tool only ever reads firewalls and writes their rules, but the Hetzner API cannot enforce that restriction for you. Nor does moving the firewalls to a separate project help — a firewall must live in the same project as the servers it protects. So treat the token as equivalent to full control of the project and limit exposure operationally instead:
- Prefer environment variables over a config file. Passing the token via
HCLOUD_TOKEN(see Using Environment Variables) keeps it out of any on-disk file, and lets Kubernetes inject it straight from a Secret withsecretKeyRef. Docker Swarm secrets are mounted as files and never exported into the environment — mount one asconfig.yamlinstead. Useconfig.yamlas well when you need to drive several projects in one run. - Restrict the config file when you do use one. See the permission notes in Configuring the Application.
- Rotate the token periodically, and revoke it immediately if a host that held it is decommissioned or compromised. Hetzner tokens cannot be rotated in place: generate a replacement under "Security" > "API Tokens", roll it out, then delete the old one. The tool picks up the new value on its next run, with no other changes needed.
- Use a separate token per deployment rather than sharing one across hosts, so a single revocation doesn't take down everything else.
Configuring the Application
To configure the application, you'll need to create a config.yaml file with
your API tokens and the names of the firewalls you want to update:
- token: API_TOKEN_FOR_PROJECT_1 # Token with read-write permissions for a Hetzner Cloud project
firewalls:
- firewall-1
- firewall-2
- token: API_TOKEN_FOR_PROJECT_2 # Token with read-write permissions for another Hetzner Cloud project
firewalls:
- default
On POSIX systems, the tool checks config file permissions before loading the file:
- Config files writable by group or others are rejected.
- Group/other read access is allowed (common for mounted read-only Docker/Kubernetes secrets), but a warning is logged.
For local files, prefer owner-only access (for example chmod 600 config.yaml)
where practical.
Using Environment Variables (Single Project)
For the common single-project case — typical in Docker and Kubernetes — you can
skip the config file entirely and provide the token and firewalls through
environment variables instead. When -c/--config is omitted, the tool builds a
single project from:
HCLOUD_TOKEN: API token with read-write permissions for the Hetzner Cloud project.HCLOUD_FIREWALLS: newline-separated list of firewall names to update, one name per line. Newlines are used as the separator so names may contain commas and spaces (for exampleICMP, SSH 222 IPv6, Cloudflare).
export HCLOUD_TOKEN=your-api-token
export HCLOUD_FIREWALLS=$'firewall-1\nfirewall-2'
cf-ips-to-hcloud-fw
This keeps the token out of any on-disk file and lets you pass it as a native Docker/Kubernetes secret.
Configuration is resolved in this order: an explicit -c/--config file is the
sole source; otherwise a config.yaml in the working directory is used if
present; otherwise the environment variables above are used. A present config
file therefore takes precedence over the environment variables.
Usage
Run the tool with your configuration file:
cf-ips-to-hcloud-fw -c config.yaml
Command-line Options
-c, --config FILE: Path to the configuration file. If omitted, aconfig.yamlin the working directory is used when present, otherwise a single project is built from theHCLOUD_TOKENandHCLOUD_FIREWALLSenvironment variables (see Using Environment Variables)-d, --debug: Enable debug logging for troubleshooting-v, --version: Display the installed version
Example with debug logging:
cf-ips-to-hcloud-fw -c config.yaml -d
Running on a Schedule
cf-ips-to-hcloud-fw is a one-shot task, not a long-running daemon.
It fetches the current Cloudflare ranges, updates your firewalls, and exits.
Cloudflare's IP ranges change infrequently, so running it hourly or daily is
plenty — schedule it with your platform's usual mechanism.
Do not wrap it in a restart loop such as Docker's restart: unless-stopped.
The container exits cleanly after each run and would be restarted immediately,
which looks like a crash loop in your logs even though every run succeeded.
Host cron
Add a line to your crontab (crontab -e) to run every hour:
0 * * * * /path/to/cf-ips-to-hcloud-fw -c /path/to/config.yaml
systemd timer
Create a service unit at /etc/systemd/system/cf-ips-to-hcloud-fw.service:
[Unit]
Description=Sync Cloudflare IP ranges into Hetzner Cloud firewalls
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/path/to/cf-ips-to-hcloud-fw -c /etc/cf-ips-to-hcloud-fw/config.yaml
Create a matching timer at /etc/systemd/system/cf-ips-to-hcloud-fw.timer:
[Unit]
Description=Run cf-ips-to-hcloud-fw hourly
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.target
Reload systemd so it picks up the new units, then enable and start the timer:
systemctl daemon-reload
systemctl enable --now cf-ips-to-hcloud-fw.timer
Docker Compose
Pick one of the two approaches below — don't combine them.
Approach 1 — host-scheduled one-off runs (recommended). Define a plain service that uses the image's default one-shot entrypoint:
services:
cf-ips-to-hcloud-fw:
image: jkreileder/cf-ips-to-hcloud-fw:1.4.2
volumes:
- ./config.yaml:/usr/src/app/config.yaml
Then trigger it from the host (for example from cron) instead of keeping the container running:
0 * * * * cd /path/to/compose && docker compose run --rm cf-ips-to-hcloud-fw
Approach 2 — a single long-lived container. If you can't use a host
scheduler, override the entrypoint to loop internally instead of relying on
restart:. Use this service definition on its own — do not also drive it with
the docker compose run line above, or the override makes the run never exit:
services:
cf-ips-to-hcloud-fw:
image: jkreileder/cf-ips-to-hcloud-fw:1.4.2
volumes:
- ./config.yaml:/usr/src/app/config.yaml
# Re-run every 24h (86400s) inside one container instead of a scheduler.
# `sleep ... & wait` keeps the loop interruptible so `docker compose down`
# shuts down promptly instead of waiting for the stop grace period.
entrypoint: ["/bin/sh", "-c"]
command:
- |
trap 'exit 0' INT TERM
while true; do
.venv/bin/cf-ips-to-hcloud-fw -c config.yaml
sleep 86400 &
wait "$!"
done
Kubernetes
For Kubernetes, use a CronJob — see the example in
Docker and Kubernetes.
Verifying SLSA Attestations
Build provenance metadata and SBOM attestations are published with every artifact so you can verify their authenticity and contents.
These attestations are cryptographically signed. Use the commands below to validate
the signatures. For GitHub-hosted artifacts you can further restrict verification
with --signer-workflow. Container attestations can be fetched with docker scout attest get after verifying build provenance.
The attestations qualify for SLSA Build Level 3:
the build and the attestation signing run inside dedicated reusable workflows
(build-and-attest-dist.yaml for the Python distributions, docker-build.yaml for
the container images), which the --signer-workflow checks below pin.
For v1.3.1 and earlier the signer workflow was python-package.yaml (Python) or
docker.yaml (Docker) — substitute those paths when verifying older artifacts. Those
releases also carry a multiple.intoto.jsonl asset, but it was produced by
slsa-github-generator and is
verified with slsa-verifier rather than
with the gh attestation verify --bundle command shown below.
Verifying Python Wheels and Source Code
GH_REPO=jkreileder/cf-ips-to-hcloud-fw
VERSION=1.4.2
# Verifying build provenance
gh attestation verify cf_ips_to_hcloud_fw-$VERSION-py3-none-any.whl \
--repo $GH_REPO \
--signer-workflow $GH_REPO/.github/workflows/build-and-attest-dist.yaml@refs/tags/v$VERSION
gh attestation verify cf_ips_to_hcloud_fw-$VERSION.tar.gz \
--repo $GH_REPO \
--signer-workflow $GH_REPO/.github/workflows/build-and-attest-dist.yaml@refs/tags/v$VERSION
# Verifying and showing the SBOM (one attestation covers both distributions,
# so either file below verifies against it)
gh attestation verify cf_ips_to_hcloud_fw-$VERSION-py3-none-any.whl \
--repo $GH_REPO \
--signer-workflow $GH_REPO/.github/workflows/build-and-attest-dist.yaml@refs/tags/v$VERSION \
--predicate-type https://spdx.dev/Document/v2.3
# Add --format json --jq '.[].verificationResult.statement.predicate' to also output the SBOM
The commands above query GitHub's attestation API. Releases from v1.3.3 on also ship the
signed bundles as a multiple.intoto.jsonl release asset, covering both the wheel and the
sdist, so you can verify from disk instead:
gh release download v$VERSION --repo $GH_REPO --pattern multiple.intoto.jsonl
gh attestation verify cf_ips_to_hcloud_fw-$VERSION-py3-none-any.whl \
--repo $GH_REPO \
--bundle multiple.intoto.jsonl \
--signer-workflow $GH_REPO/.github/workflows/build-and-attest-dist.yaml@refs/tags/v$VERSION
This still fetches the Sigstore trust root. For fully offline verification, capture it
beforehand with gh attestation trusted-root > trusted_root.jsonl and pass
--custom-trusted-root trusted_root.jsonl.
v1.3.2 is the one release without a bundle asset — it was published between the
slsa-github-generator removal and this change, and immutable releases prevent adding one
after the fact. Verify it with the API-based commands above, or fetch its bundles yourself
with gh attestation download.
Verifying Docker Images
It's recommended that you use an immutable image reference (pin to a digest) so the image you verify is exactly the image you run — a mutable tag can be repointed between verification and use (a time-of-check/time-of-use attack).
Build provenance:
GH_REPO=jkreileder/cf-ips-to-hcloud-fw
IMAGE_REPO=docker.io/jkreileder/cf-ips-to-hcloud-fw
VERSION=1.4.2
IMAGE=$IMAGE_REPO@$(crane digest $IMAGE_REPO:$VERSION)
# Verifying build provenance
gh attestation verify oci://$IMAGE \
--repo $GH_REPO \
--signer-workflow $GH_REPO/.github/workflows/docker-build.yaml@refs/tags/v$VERSION
# The SBOMs are attached to the now verified image, you can view with
DIGEST=$(docker scout attest list --format json $IMAGE --predicate-type https://spdx.dev/Document \
| jq -r 'limit(1; .[] | select(.reference | startswith("jkreileder/cf-ips-to-hcloud-fw")) | .digest)')
docker scout attest get $IMAGE $DIGEST --predicate-type https://spdx.dev/Document
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines on how to contribute to this project.
Security
If you discover a security vulnerability, please see SECURITY.md for responsible disclosure instructions.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cf_ips_to_hcloud_fw-1.4.2.tar.gz.
File metadata
- Download URL: cf_ips_to_hcloud_fw-1.4.2.tar.gz
- Upload date:
- Size: 97.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0e8660b12d426e8b8b28c8fd249002bad9f6db8dd6888476c1cd21bada13d1bd
|
|
| MD5 |
d235af68740b5ccbce1f6638798ebbcd
|
|
| BLAKE2b-256 |
b13ecd79ef4789e798ca231c0c61be6b402def00758894bfbf9bc68e4a1a09d6
|
Provenance
The following attestation bundles were made for cf_ips_to_hcloud_fw-1.4.2.tar.gz:
Publisher:
python-package.yaml on jkreileder/cf-ips-to-hcloud-fw
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cf_ips_to_hcloud_fw-1.4.2.tar.gz -
Subject digest:
0e8660b12d426e8b8b28c8fd249002bad9f6db8dd6888476c1cd21bada13d1bd - Sigstore transparency entry: 2618245743
- Sigstore integration time:
-
Permalink:
jkreileder/cf-ips-to-hcloud-fw@89c0611652458fcb019540a47686ac49c260cd1d -
Branch / Tag:
refs/tags/v1.4.2 - Owner: https://github.com/jkreileder
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-package.yaml@89c0611652458fcb019540a47686ac49c260cd1d -
Trigger Event:
push
-
Statement type:
File details
Details for the file cf_ips_to_hcloud_fw-1.4.2-py3-none-any.whl.
File metadata
- Download URL: cf_ips_to_hcloud_fw-1.4.2-py3-none-any.whl
- Upload date:
- Size: 24.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9978e11678ca794cef0b994416751e4c0fd59c0dd22dcd555f950629cf311a72
|
|
| MD5 |
21c7eb5fec8840109cf82c2c66ca14aa
|
|
| BLAKE2b-256 |
cc330111b5ffed7b0c4a2d01a538ee26e5303002c35a19f717eb608361cb1a57
|
Provenance
The following attestation bundles were made for cf_ips_to_hcloud_fw-1.4.2-py3-none-any.whl:
Publisher:
python-package.yaml on jkreileder/cf-ips-to-hcloud-fw
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cf_ips_to_hcloud_fw-1.4.2-py3-none-any.whl -
Subject digest:
9978e11678ca794cef0b994416751e4c0fd59c0dd22dcd555f950629cf311a72 - Sigstore transparency entry: 2618245757
- Sigstore integration time:
-
Permalink:
jkreileder/cf-ips-to-hcloud-fw@89c0611652458fcb019540a47686ac49c260cd1d -
Branch / Tag:
refs/tags/v1.4.2 - Owner: https://github.com/jkreileder
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-package.yaml@89c0611652458fcb019540a47686ac49c260cd1d -
Trigger Event:
push
-
Statement type: