Artifact Firewall

The firewall section of the firewall’s configuration file, which also holds the license, otel and config_watch sections.

Example:

firewall:
  registries:
  - name: npmjs
    remote_url: https://registry.npmjs.org
    routes:
    - host: npm.example.com
  default_action: allow
  rulesets:
  - path: /etc/varnish-artifact-firewall/rulesets
  - git:
      name: npm-osv-rules
      url: https://github.com/varnish/osv-rules.git
      sub_path: rulesets/npm/all.yaml

license:
  file: /etc/varnish-artifact-firewall/license.lic

A firewall bundled with the Virtual Registry reads the same section from the Supervisor’s configuration file instead, as described under Bundled with the Virtual Registry.

deployment

firewall:
  deployment: standalone

Type: String

Default: standalone

registries

firewall:
  registries:
  - name: npmjs
    remote_url: https://registry.npmjs.org
    routes:
    - host: npm.example.com
  - name: pypi
    remote_url: https://pypi.org
    base_url: https://pypi.example.com
    routes:
    - host: pypi.example.com

Type: List

The registries behind a standalone firewall. It refuses to start without one.

name

Type: String

Unique name of the registry. Mandatory. A ruleset’s virtual_registries names it, and the audit log records it as registry.

remote_url

Type: String

The registry to proxy to. Mandatory. The request path is sent to its scheme, host and port as it arrived, and a path in remote_url is not used, so it names the root of the server.

Sonatype Nexus and JFrog Artifactory repository paths, and the /maven2 path of Maven Central, are recognized in the request path, so a remote that is a repository manager needs no more configuration.

base_url

Type: String

The firewall’s URL for this registry, as clients reach it. It is written into the package URLs of a rewritten manifest, so that downloads come back through the firewall. Without it, the firewall writes the scheme and the Host of the request.

The firewall does not read X-Forwarded-Proto, so behind a proxy that terminates TLS, set base_url to the public HTTPS URL.

routes

Type: List

The requests that go to this registry. At least one is required. A request that matches no route of any registry gets a 404.

host

Type: String

Matches a request whose Host header names it, in any case and with the port left out. A host can be routed to one registry only.

address

firewall:
  address: 0.0.0.0

Type: String

Default: empty, every interface

Address for the Artifact Firewall proxy to listen on. Bundled with the Virtual Registry, the Supervisor binds it to 127.0.0.1 when omitted.

port

firewall:
  port: 6089

Type: Integer

Default: 6089

Port for the Artifact Firewall proxy to listen on.

api_address

firewall:
  api_address: 127.0.0.1:6090

Type: String

Default: 127.0.0.1:6090

Listen address for the admin API, in host:port form. An integrated Virtual Registry calls this API before it serves each artifact, from its cache or not. An empty string disables the admin API.

An address that cannot be bound, for example a port already in use, stops the firewall at startup, and a bundled firewall stops the Supervisor with it. The reason is written to log_output.

log_level

firewall:
  log_level: info

Type: String

Default: info

Log level for Artifact Firewall output.

Options:

  • debug
  • info
  • warn
  • error

At info the firewall records what it is running as it starts: its version and the revision it was built from, the configuration file it read, and the configuration itself as every setting that differs from its default. A log therefore says what a firewall was running without its files having to be found.

INFO is reserved for state changes, so a ruleset reload names the rulesets that changed rather than counting them.

log_output

firewall:
  log_output: stdout

Type: String

Destination for Artifact Firewall process logs. This is the firewall’s own operational log, separate from the audit log.

Options:

  • stdout
  • stderr
  • A file path (e.g. /var/log/artifact-firewall.log)
  • otel: export the logs as OpenTelemetry log records under the scope artifact-firewall/log, which needs an endpoint for otel.logs. This one tees rather than replaces: the logs are written to standard error as well, so startup logs are never lost

A firewall that refuses to start writes the reason here as an ERROR record, and the line saying it stopped cleanly is written here too. Both are worth knowing about, since a process that never came up is otherwise looked for in the wrong place.

audit_log_output

firewall:
  audit_log_output: /var/log/artifact-firewall-audit.log

Type: String

Default: stdout

Destination for the audit log, which records every package the Artifact Firewall evaluated and what it decided. When empty, audit logging is disabled.

Options:

  • stdout
  • stderr
  • A file path (e.g. /var/log/artifact-firewall-audit.log)
  • otel: export the entries as OpenTelemetry log records instead of JSON, which needs an endpoint for otel.logs

A file that cannot be opened, for example in a directory that does not exist, stops the firewall at startup, and a bundled firewall stops the Supervisor with it. The reason is written to log_output. Before Artifact Firewall 0.8.0 the firewall started anyway, with audit logging off.

default_action

firewall:
  default_action: deny

Type: String

Default: allow

Action taken when a request does not match any rule in the configured rulesets. The value is read in any case, so DENY is the same as deny. Any other value stops the firewall at startup.

Options:

  • allow: Pass the request through.
  • hide: Remove version from “latest” consideration.
  • deny: Return a 403 response.

mode

firewall:
  mode: report

Type: String

Default: normal

Global mode that reshapes how matched rule actions are enforced, useful for testing rulesets before enforcing them.

Options:

  • normal: Enforce rule actions as configured.
  • hide: Downgrade deny actions to hide, so blocked versions are removed from “latest” consideration instead of being rejected outright.
  • report: Downgrade all actions to allow, so nothing is blocked. Decisions are still recorded in the audit log, where action holds what the rule asked for and enforced_action holds allow, making this a report-only mode for evaluating a ruleset’s impact.

default_quarantine_days

firewall:
  default_quarantine_days: 2  # example value, not the default

Type: Integer

Default: 0 (disabled)

When set to a positive integer, newly published packages within this many days of their release are blocked regardless of ruleset matches. This provides a quarantine window to guard against newly introduced malicious packages.

Set to 0 to disable quarantine.

severity_deny_threshold

firewall:
  severity_deny_threshold: 9.0

Type: Number

Default: 9

Severity score at or above which a matching rule resolves to deny. Rules with a severity score at or above this threshold override their configured action and deny the request.

severity_allow_threshold

firewall:
  severity_allow_threshold: 4.0

Type: Number

Default: 4

Severity score at or below which a matching rule resolves to allow. Rules with a severity score at or below this threshold override their configured action and allow the request.

rulesets

firewall:
  rulesets:
  - path: /etc/varnish-artifact-firewall/rulesets

Type: List

List of ruleset sources. Each entry sets exactly one of path, for a ruleset on local disk, git, for one kept in a remote repository, or http, for one in an archive fetched over HTTP(S).

path

firewall:
  rulesets:
  - path: /etc/varnish-artifact-firewall/rulesets

Type: String

Path to a ruleset file or a directory containing ruleset files. Files ending in .yaml or .yml are read as rulesets, and a directory is scanned non-recursively.

git

firewall:
  rulesets:
  - git:
      name: npm-osv-rules
      url: https://github.com/varnish/osv-rules.git
      sub_path: rulesets/npm/all.yaml

Fetch rulesets from a remote Git repository. The repository is cloned locally and refreshed on the configured interval.

The clone is a bare repository holding only the latest revision of the rulesets it was asked for, so it stays at roughly the size of one compressed ruleset however long it runs.

A ruleset republished with no change other than its top-level date field is fetched but not reloaded.

Take every ruleset wanted from a repository in a single entry, using sub_paths. Each entry is a clone of its own, so naming the same repository in six entries fetches and stores it six times over.

name

Type: String

Unique name for this Git ruleset source. Used to identify the source in logs and as a subdirectory name under work_dir.

url

Type: String

URL of the Git repository.

ref

firewall:
  rulesets:
  - git:
      url: https://github.com/example/rulesets.git
      ref: main

Type: String

Default: main

Branch or tag to track.

sub_path

firewall:
  rulesets:
  - git:
      url: https://github.com/example/rulesets.git
      sub_path: policies/npm

Type: String

A single ruleset file, or a directory of them, within the repository. A directory contributes every .yaml and .yml file under it, at any depth, except hidden files and directories such as .github/, unless the sub path names them. . names the root of the repository, and so takes every ruleset in it. A directory is searched recursively only here and in an http source, a local path entry is not.

Exactly one of sub_path or sub_paths is required. A source that matches no ruleset fails the load rather than enforcing less than was asked for.

sub_paths

firewall:
  rulesets:
  - git:
      name: osv
      url: https://github.com/varnish/osv-rules.git
      sub_paths:
      - rulesets/npm/all.yaml
      - rulesets/pypi

Type: List

Several of the above, from one clone of the repository. Each entry is either a ruleset file or a directory.

A repository that files its rulesets per ecosystem can be named whole, and ecosystems added to a tracked directory later are picked up without a configuration change.

interval

firewall:
  rulesets:
  - git:
      url: https://github.com/example/rulesets.git
      interval: 30m

Type: String

Default: 1h

How often to pull updates from the remote repository. Accepts duration strings such as 30m or 2h.

fetch_timeout

firewall:
  rulesets:
  - git:
      url: https://github.com/example/rulesets.git
      fetch_timeout: 60s

Type: String

Default: 30s

Timeout for each fetch operation. Accepts duration strings such as 30s or 2m.

boot_path

firewall:
  rulesets:
  - git:
      name: osv
      url: https://github.com/varnish/osv-rules.git
      sub_path: rulesets/npm/all.yaml
      boot_path: /opt/artifact-firewall/boot

Type: String

A local directory of rulesets to start on, so that startup does not wait on the remote. It is laid out like the repository, and sub_path or sub_paths pick rulesets out of it the same way, so the example starts on /opt/artifact-firewall/boot/rulesets/npm/all.yaml. A sub path that selects nothing in it fails startup. Give an absolute path, since a relative one is resolved against the firewall’s working directory. Symbolic links are followed, as in a mounted Kubernetes ConfigMap.

Without boot_path, the source is synced before the firewall serves, and a firewall that cannot reach the remote and has no cache from an earlier run does not start. With it, the firewall starts on a usable cache from an earlier run, and on the boot_path rulesets otherwise. It serves straight away and syncs the source once its listeners are up. The fetched rulesets replace the boot ones on the first sync that succeeds, and until then a failed sync is logged and the firewall keeps enforcing its startup rulesets.

Until the source’s own rulesets have loaded once, the boot rulesets are also its fallback: a cache or a first fetch that does not load is set aside for them, and tried again after each successful sync. After that, a revision that does not load is refused as for any other source, and the rules of the last revision that loaded stay in force.

auth

firewall:
  rulesets:
  - git:
      url: https://github.com/example/rulesets.git
      auth:
        token_env: GITHUB_TOKEN

Credentials for authenticating with the Git remote. At most one of token_env or github_app can be set.

token_env

Type: String

Name of the environment variable that holds the authentication token, for example a GitHub personal access token. The token is passed as an HTTP Bearer token when fetching from the remote, and is read on every sync, so rotating it needs no restart.

github_app
firewall:
  rulesets:
  - git:
      url: https://github.com/example/rulesets.git
      auth:
        github_app:
          client_id: Iv23liABCDEFGHIJKLMN
          private_key_path: /etc/varnish-artifact-firewall/rulesets-app.pem

Authenticate as a GitHub App. The firewall signs a JWT with the App private key, exchanges it for a short-lived installation access token, and refreshes that token before it expires.

The private key comes from exactly one of private_key_path or private_key_env. It is re-read on each token mint, so rotating it needs no restart, and it is validated at startup, so a missing or malformed key fails the firewall immediately rather than at the first sync.

There is no installation ID to configure. The firewall derives the owner and repository from url and looks the installation up itself, so url has to be an HTTPS URL of the form https://host/owner/repo.git. An SSH remote cannot be used with github_app, because the credential is sent as an HTTP header.

client_id

Type: String

GitHub App client ID. Mandatory. GitHub recommends the client ID over the numeric App ID.

private_key_path

Type: String

Path to the App private key in PEM form.

private_key_env
firewall:
  rulesets:
  - git:
      url: https://github.com/example/rulesets.git
      auth:
        github_app:
          client_id: Iv23liABCDEFGHIJKLMN
          private_key_env: RULESETS_APP_KEY

Type: String

Name of an environment variable holding the App private key in PEM form.

api_base_url
firewall:
  rulesets:
  - git:
      url: https://github.example.com/example/rulesets.git
      auth:
        github_app:
          client_id: Iv23liABCDEFGHIJKLMN
          private_key_path: /etc/varnish-artifact-firewall/rulesets-app.pem
          api_base_url: https://github.example.com/api/v3

Type: String

Default: https://api.github.com

GitHub REST API base URL. Set it for GitHub Enterprise Server.

A GitHub App adds an outbound dependency on this host, separate from the Git host of the clone.

http

firewall:
  rulesets:
  - http:
      name: osv
      url: https://my-bucket.s3.eu-north-1.amazonaws.com/osv/rules.tar.gz
      sub_path: rulesets
      interval: 5m

Fetch rulesets from a zip or tar archive over HTTP(S), gzipped or not, such as an S3 or GCS object, a file on a web server or a CDN, an Artifactory generic repository, or a source archive from a Git forge. The format is read from the content, not from the URL.

Only the selected rulesets are extracted. A new archive is extracted beside the cache and swapped in whole, so a failed sync keeps the previous set, and the cache keeps what is needed to poll conditionally after a restart too. An archive is refused whole if an entry points outside it, as an absolute path or through .., or if a selected ruleset appears in it twice. Symbolic and hard links are not extracted.

name

Type: String

Unique name for this source, also used as its cache directory name. Mandatory.

url

Type: String

URL of the archive. Mandatory. A URL with a user or password in it is refused. The URL is logged as configured when the firewall starts, so a credential belongs in auth.token_env, not in the query string.

A private S3 bucket can be reached through a bucket policy, such as a condition on a VPC endpoint or a source IP, or through CloudFront. A presigned URL expires, so it does not suit a source that is polled.

sub_path

Type: String

A ruleset file, or a directory of them, within the archive, picked out as for a git source. Exactly one of sub_path or sub_paths is required.

Sub paths are matched against the paths as stored in the archive. An archive built by a Git forge wraps everything in one top-level folder, usually named after the repository and the ref, and a sub path has to include it:

firewall:
  rulesets:
  - http:
      name: internal
      url: https://git.example.com/org/rules/archive/main.zip
      sub_path: rules-main/rulesets

sub_paths

firewall:
  rulesets:
  - http:
      name: internal
      url: https://git.example.com/org/rules/archive/main.zip
      sub_paths:
      - rules-main/rulesets/npm
      - rules-main/rulesets/pypi/all.yaml

Type: List

Several of the above, from one download of the archive.

interval

Type: String

Default: 1h

How often to poll the URL, as a duration such as 5m.

Each poll is a conditional request, so an unchanged archive costs a 304 and no download. A server that sends neither an ETag nor a Last-Modified header is downloaded in full on every poll, and a warning says so. A ruleset whose only change is its top-level date field is not reloaded, and neither is an archive whose changes all fall outside the sub paths.

fetch_timeout

firewall:
  rulesets:
  - http:
      name: osv
      url: https://my-bucket.s3.eu-north-1.amazonaws.com/osv/rules.tar.gz
      sub_path: rulesets
      fetch_timeout: 2m

Type: String

Default: 30s

Timeout for one sync, the download and the extraction together.

max_size

firewall:
  rulesets:
  - http:
      name: osv
      url: https://my-bucket.s3.eu-north-1.amazonaws.com/osv/rules.tar.gz
      sub_path: rulesets
      max_size: 512M

Type: String

Default: 256M

The largest archive accepted, and the most extracted from one, as a size such as 512M.

boot_path

firewall:
  rulesets:
  - http:
      name: osv
      url: https://my-bucket.s3.eu-north-1.amazonaws.com/osv/rules.tar.gz
      sub_path: rulesets
      boot_path: /opt/artifact-firewall/boot

Type: String

Local rulesets to start on, laid out like the archive, as for a git source.

auth

firewall:
  rulesets:
  - http:
      name: internal
      url: https://git.example.com/org/rules/archive/main.zip
      sub_path: rules-main/rulesets
      auth:
        token_env: RULES_TOKEN

Credentials for the download.

token_env

Type: String

Name of the environment variable that holds a token, sent as Authorization: Bearer. It is read on every sync, and sent on a redirect only to the same scheme, host and port.

webhook

firewall:
  webhook:
    url: https://example.com/hooks/ruleset-reloaded

Optional webhook called after rulesets are successfully reloaded. When url is not set, no webhook is sent.

Bundled with the Virtual Registry, the Supervisor sets the webhook itself, to purge the manifests cached by Varnish under the previous rulesets, and replaces a value set here. A firewall run on its own and integrated with a Virtual Registry has to be given that webhook in its own configuration.

url

Type: String

URL to send an HTTP request to when rulesets are reloaded.

method

firewall:
  webhook:
    url: https://example.com/hooks/ruleset-reloaded
    method: POST

Type: String

Default: GET

HTTP method to use for the webhook request.

headers

firewall:
  webhook:
    url: https://example.com/hooks/ruleset-reloaded
    headers:
      Authorization: "Bearer my-secret-token"

Type: Map

Custom HTTP headers to include in the webhook request. A credential set here is kept out of the log when the configuration is recorded at startup.

preserve_manifest

firewall:
  preserve_manifest: true

Type: Boolean

Default: false

Pass manifests through to the client without any transformation. When disabled, the firewall may rewrite manifest content.

max_manifest_size

firewall:
  max_manifest_size: 67108864

Type: Integer

Default: 67108864 (64 MiB)

Maximum manifest body size in bytes that the firewall will read and process. Manifests larger than this limit are rejected.

The limit applies to the decompressed body, and the whole body is held in memory while it is filtered.

Two kinds of manifest regularly run past the default:

  • A conda repodata.json. Filtering a channel the size of conda-forge needs this raised. See Conda enforcement.
  • A VS Code Marketplace listing. The Extensions view asks for the full version history of many extensions at once, which runs past 100 MiB, and the view stays empty when the firewall refuses it. Set about 256 MiB (268435456). See VS Code enforcement.

work_dir

firewall:
  work_dir: /var/cache/artifact-firewall

Type: String

Default: /var/lib/varnish-artifact-firewall

Working directory used to store cached data for remote ruleset sources, such as cloned Git repositories.

storage

firewall:
  storage:
    path: /var/lib/varnish-artifact-firewall/storage
    size: 10G

Persistent storage for the ruleset cache: cloned Git repositories and the rulesets extracted from http archives. When path is set, it takes precedence over work_dir for these caches.

path

Type: String

Directory where the ruleset cache is stored. Overrides work_dir for these caches when set.

size

Type: String

Declared storage budget for the ruleset cache, such as 10G. When both path and size are set, the firewall enforces it as a growth limit: once usage reaches size, it stops growing cloned repositories and downloading http archives, for each source that has a cached set to fall back on, preventing the cache from filling the disk over time. A download briefly needs room for the archive and what is extracted from it. It also warns when usage reaches 80% of size, and warns at startup if the filesystem hosting path is significantly smaller than size.

Sizes follow Varnish conventions: a bare integer is bytes, while the suffixes K, M, G, and T (case-insensitive) are binary multipliers (for example 10G is 10 × 2^30 bytes).

metrics_address

Type: String

Deprecated since Artifact Firewall 0.4.0, and ignored. A configuration that still sets it loads and logs a warning.

Firewall metrics are exported through the otel section, and appear under the instrumentation scope artifact-firewall.

preserve_order

firewall:
  preserve_order: true

Type: Boolean

Default: false

Preserve the original JSON map key order in manifests. Enabling this is significantly slower and is primarily useful for debugging or diffing manifest transformations.

redirects

firewall:
  redirects:
    signing:
      enabled: true
      key_env: FIREWALL_SIGNING_KEY

The download URLs rewritten by the firewall to send a client back through it.

signing

VS Code is the one ecosystem that needs it. Its download URL carries no publish time, so the firewall stamps one onto each URL in a listing, as a matrix parameter, and applies the quarantine window from it when the extension is downloaded. Unsigned, a client can edit that parameter to backdate an extension out of quarantine. See VS Code enforcement.

Signing appends an HMAC over the publish time, the expiry and the extension version, so none can be edited and a URL cannot be moved onto another extension. With signing on, a download URL without them is refused, since each URL issued by the firewall carries them. With it off, such a download is served without a quarantine check. A refused download is recorded in the audit log with a block_reason.

This is separate from the Virtual Registry’s own virtual_registry.redirects.signing, which signs /redirectz URLs. Both take a key in the same format, so one key can serve both.

enabled

Type: Boolean

Default: false

Sign the data in the rewritten URLs.

key_env

Type: String

Name of the environment variable that holds the key, hex-encoded, such as the output of openssl rand -hex 32. Required when enabled is set, and the firewall fails to start if the variable is missing, empty or not hex. Set without enabled, it logs a warning and signs nothing.

url_ttl

firewall:
  redirects:
    signing:
      enabled: true
      key_env: FIREWALL_SIGNING_KEY
      url_ttl: 4h

Type: String

Default: 1h

How long a signed URL stays valid, as a duration. 0 disables expiry.

An expired URL fails at the client as a download error, and a fresh listing fixes it, so url_ttl has to cover the time between a developer reading a listing and installing from it. Values below about fifteen minutes cause failures for little gain.

maven

firewall:
  maven:
    solr_url: https://search.maven.org/solrsearch/select

Maven-specific configuration, used when filtering Maven repositories.

solr_url

Type: String

Default: https://search.maven.org/solrsearch/select

Solr search endpoint used to look up Maven artifact publish timestamps for the quarantine window (see default_quarantine_days). When empty, Solr lookups are disabled and the firewall falls back to HEAD requests to determine timestamps.

pypi

firewall:
  pypi:
    api_url: https://pypi.org/pypi

PyPI-specific configuration, used when filtering PyPI repositories.

api_url

Type: String

Default: empty (disabled)

PyPI JSON API root (for example https://pypi.org/pypi) used to look up package publish timestamps for the quarantine window (see default_quarantine_days) on the PEP 503 HTML simple index, where the upstream response does not carry a reliable timestamp. When empty, this lookup is disabled and quarantine is not enforced on the HTML simple index.


®Varnish Software, Wallingatan 12, 111 60 Stockholm, Organization nr. 556805-6203