Metadata-Version: 2.4
Name: cf-ips-to-hcloud-fw
Version: 1.4.1
Summary: Update Hetzner Cloud firewall rules with Cloudflare IP ranges
Keywords: cloudflare,hcloud,hetzner,firewall,cli
Author: Jürgen Kreileder
Author-email: jk@blackdown.de
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Environment :: Other Environment
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Internet
Classifier: Topic :: System :: Networking :: Firewalls
Classifier: Topic :: Utilities
Requires-Dist: cloudflare>=5.6.0
Requires-Dist: hcloud>=2.23.0
Requires-Dist: pydantic>=2.13.4
Requires-Dist: pyyaml>=6.0.3
Requires-Dist: requests>=2.34.2
Requires-Python: >=3.10
Project-URL: Homepage, https://github.com/jkreileder/cf-ips-to-hcloud-fw
Project-URL: Source, https://github.com/jkreileder/cf-ips-to-hcloud-fw
Project-URL: Changelog, https://github.com/jkreileder/cf-ips-to-hcloud-fw/blob/main/CHANGELOG.md
Project-URL: Documentation, https://github.com/jkreileder/cf-ips-to-hcloud-fw
Project-URL: Issues, https://github.com/jkreileder/cf-ips-to-hcloud-fw/issues
Project-URL: X, https://x.com/jkreileder
Project-URL: Bluesky, https://bsky.app/profile/jk.blackdown.de
Project-URL: PyPI, https://pypi.org/project/cf-ips-to-hcloud-fw/
Description-Content-Type: text/markdown

# Update Hetzner Cloud Firewall Rules with Current Cloudflare IP Ranges  <!-- omit in toc -->

[![codecov](https://codecov.io/gh/jkreileder/cf-ips-to-hcloud-fw/graph/badge.svg?token=PCP1F2XWAT)](https://codecov.io/gh/jkreileder/cf-ips-to-hcloud-fw)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8275/badge)](https://www.bestpractices.dev/projects/8275)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/jkreileder/cf-ips-to-hcloud-fw/badge)](https://scorecard.dev/viewer/?uri=github.com/jkreileder/cf-ips-to-hcloud-fw)
[![CodeQL](https://github.com/jkreileder/cf-ips-to-hcloud-fw/actions/workflows/codeql.yaml/badge.svg)](https://github.com/jkreileder/cf-ips-to-hcloud-fw/actions/workflows/codeql.yaml)
[![Python package](https://github.com/jkreileder/cf-ips-to-hcloud-fw/actions/workflows/python-package.yaml/badge.svg)](https://github.com/jkreileder/cf-ips-to-hcloud-fw/actions/workflows/python-package.yaml)
[![Docker build](https://github.com/jkreileder/cf-ips-to-hcloud-fw/actions/workflows/docker.yaml/badge.svg)](https://github.com/jkreileder/cf-ips-to-hcloud-fw/actions/workflows/docker.yaml)
[![PyPI - Version](https://img.shields.io/pypi/v/cf-ips-to-hcloud-fw)](https://pypi.org/project/cf-ips-to-hcloud-fw/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cf-ips-to-hcloud-fw?logo=python)](https://pypi.org/project/cf-ips-to-hcloud-fw/)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jkreileder/cf-ips-to-hcloud-fw)
[![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev)

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 <!-- omit in toc -->

- [Overview](#overview)
- [Installation](#installation)
  - [Using Python](#using-python)
    - [Using `pipx` (Recommended for Most Users)](#using-pipx-recommended-for-most-users)
    - [Using `uvx` (Recommended for `uv` Users)](#using-uvx-recommended-for-uv-users)
    - [Using `pip`](#using-pip)
  - [Docker and Kubernetes](#docker-and-kubernetes)
- [Configuration](#configuration)
  - [Preparing the Hetzner Cloud Firewall](#preparing-the-hetzner-cloud-firewall)
  - [Securing the API Token](#securing-the-api-token)
  - [Configuring the Application](#configuring-the-application)
  - [Using Environment Variables (Single Project)](#using-environment-variables-single-project)
- [Usage](#usage)
  - [Command-line Options](#command-line-options)
- [Running on a Schedule](#running-on-a-schedule)
  - [Host cron](#host-cron)
  - [systemd timer](#systemd-timer)
  - [Docker Compose](#docker-compose)
  - [Kubernetes](#kubernetes)
- [Verifying SLSA Attestations](#verifying-slsa-attestations)
  - [Verifying Python Wheels and Source Code](#verifying-python-wheels-and-source-code)
  - [Verifying Docker Images](#verifying-docker-images)
- [Contributing](#contributing)
- [Security](#security)

## Overview

`cf-ips-to-hcloud-fw` fetches the current [Cloudflare IP
ranges](https://www.cloudflare.com/ips/) and updates your Hetzner Cloud firewall
rules using the [hcloud
API](https://docs.hetzner.cloud/reference/cloud#tag/firewall-actions/set_firewall_rules).

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`](https://pipx.pypa.io/) or [`uvx`](https://docs.astral.sh/uv/guides/tools/).
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)

1. Install `cf-ips-to-hcloud-fw` using `pipx`:

    ```shell
    pipx install cf-ips-to-hcloud-fw
    ```

2. Verify the installation:

    ```shell
    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`, run `pipx 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:

```shell
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]
> `uvx` always 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.

1. Create a virtual environment:

    ```shell
    python3 -m venv cf-ips-to-hcloud-fw-venv
    ```

2. Install `cf-ips-to-hcloud-fw` into the virtual environment:

    ```shell
    ./cf-ips-to-hcloud-fw-venv/bin/pip3 install cf-ips-to-hcloud-fw
    ```

3. Verify the installation:

    ```shell
    ./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-fw` in 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:

```shell
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.1
```

(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](#using-environment-variables-single-project)). The image auto-detects
a mounted `config.yaml` and otherwise falls back to these variables, so no
command override is needed:

```shell
docker run --rm \
  -e HCLOUD_TOKEN=your-api-token \
  -e HCLOUD_FIREWALLS=$'firewall-1\nfirewall-2' \
  jkreileder/cf-ips-to-hcloud-fw:1.4.1
```

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 latest `1.x.x` release.
- `1.4`: This tag always points to the latest `1.4.x` release.
- `1.4.1`: This tag points to the specific `1.4.1` release.
- `main`: This tag points to the most recent development version of
  `cf-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](https://hub.docker.com/r/jkreileder/cf-ips-to-hcloud-fw):
  `jkreileder/cf-ips-to-hcloud-fw` or `docker.io/jkreileder/cf-ips-to-hcloud-fw`
- [Quay.io](https://quay.io/repository/jkreileder/cf-ips-to-hcloud-fw):
  `quay.io/jkreileder/cf-ips-to-hcloud-fw`
- [GitHub Packages](https://github.com/jkreileder/cf-ips-to-hcloud-fw/pkgs/container/cf-ips-to-hcloud-fw):
  `ghcr.io/jkreileder/cf-ips-to-hcloud-fw`

Here's an example of how to create a Kubernetes Secret for your
[configuration](#configuration):

```yaml
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:

```yaml
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.1
              # 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:

```yaml
containers:
  - name: cf-ips-to-hcloud-fw
    image: jkreileder/cf-ips-to-hcloud-fw:1.4.1
    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:

1. **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.

2. **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](#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](#using-environment-variables-single-project)) keeps it out of any
  on-disk file, and lets Kubernetes inject it straight from a Secret with
  `secretKeyRef`. Docker Swarm secrets are mounted as files and never exported
  into the environment — mount one as `config.yaml` instead. Use `config.yaml`
  as 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](#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:

```yaml
- 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 example `ICMP, SSH 222 IPv6, Cloudflare`).

```shell
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:

```shell
cf-ips-to-hcloud-fw -c config.yaml
```

### Command-line Options

- `-c, --config FILE`: Path to the configuration file. If omitted, a
  `config.yaml` in the working directory is used when present, otherwise a single
  project is built from the `HCLOUD_TOKEN` and `HCLOUD_FIREWALLS` environment
  variables (see [Using Environment Variables](#using-environment-variables-single-project))
- `-d, --debug`: Enable debug logging for troubleshooting
- `-v, --version`: Display the installed version

Example with debug logging:

```shell
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:

```shell
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`:

```ini
[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`:

```ini
[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:

```shell
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:

```yaml
services:
  cf-ips-to-hcloud-fw:
    image: jkreileder/cf-ips-to-hcloud-fw:1.4.1
    volumes:
      - ./config.yaml:/usr/src/app/config.yaml
```

Then trigger it from the host (for example from cron) instead of keeping the
container running:

```shell
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:

```yaml
services:
  cf-ips-to-hcloud-fw:
    image: jkreileder/cf-ips-to-hcloud-fw:1.4.1
    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](#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](https://slsa.dev/spec/v1.0/levels):
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](https://github.com/slsa-framework/slsa-github-generator) and is
verified with [slsa-verifier](https://github.com/slsa-framework/slsa-verifier) rather than
with the `gh attestation verify --bundle` command shown below.

### Verifying Python Wheels and Source Code

```shell
GH_REPO=jkreileder/cf-ips-to-hcloud-fw
VERSION=1.4.1

# 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:

```shell
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:

```shell
GH_REPO=jkreileder/cf-ips-to-hcloud-fw
IMAGE_REPO=docker.io/jkreileder/cf-ips-to-hcloud-fw
VERSION=1.4.1
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](https://github.com/jkreileder/cf-ips-to-hcloud-fw?tab=contributing-ov-file#contributing-guidelines)
for guidelines on how to contribute to this project.

## Security

If you discover a security vulnerability, please see
[SECURITY.md](https://github.com/jkreileder/cf-ips-to-hcloud-fw?tab=security-ov-file#reporting-security-issues)
for responsible disclosure instructions.
