probation 0.2.1

A filtering proxy for npm and PyPI that withholds packages until they are eligible.
# Probation sample configuration (SPEC §4).
#
# Config changes need a restart. Blocklist changes do not.

listen = "127.0.0.1:8080"
public_url = "https://packages.example.org"
data_dir = "/var/lib/probation"
# The snapshot your blocklist producer writes and replaces atomically. This is
# not the `blocklist.sample.json` in this repository: that sample has a far-off
# `expires_at` so it always validates. Without a valid snapshot the service
# starts, answers /health/live but not /health/ready, and refuses every
# package request.
blocklist_file = "/etc/probation/blocklist.json"

# How long a release is withheld after publication. 0 turns off the age rule
# only; blocks still apply. At most 315360000 (ten years).
cooldown_seconds = 86400
metadata_ttl_seconds = 300
# How old a cached project snapshot may get since its last FULL fetch, however
# often upstream answers `304` (SPEC §10). Each project expires a little early,
# by up to a tenth, so projects fetched together do not all expire at once. An
# over-age snapshot is never served: if the full fetch fails, the request is
# refused. Must not be below `metadata_ttl_seconds`. 0 removes the limit and
# brings back the unbounded staleness it exists to close.
metadata_max_age_seconds = 86400
blocklist_poll_seconds = 5

cache_max_bytes = 107374182400        # 100 GiB, including temporary downloads
memory_cache_max_bytes = 268435456    # 256 MiB cache budget, not total process RSS
max_artifact_bytes = 5368709120       # 5 GiB
max_metadata_bytes = 67108864         # 64 MiB after decompression
max_blocklist_bytes = 134217728       # 128 MiB
max_upstream_requests = 32
max_artifact_downloads = 8
max_active_requests = 1024
max_references_per_project = 20000

# Decision log delivery (§8 of docs/operations.md). Both sinks are off by
# default: with no `log_file_path` no file is opened, and with no `siem_url` no
# HTTP client is built. The stdout decision line is always written.
#
# Every decision is appended to this file as one JSON object per line. Exactly
# one process may write it. Never hand it to `logrotate`: this process rolls
# the file itself.
# log_file_path = "/var/log/probation/decisions.ndjson"
# The live file rolls over to `<log_file_path>.1` at this size, replacing any
# previous `.1`. One generation is kept, so the pair costs at most twice this.
# Refused without `log_file_path`.
# log_file_max_bytes = 104857600        # 100 MiB live, 200 MiB with the rollover
# How much memory this sink's queue may hold while the file falls behind. A
# memory budget like `memory_cache_max_bytes`, and on top of it. Once the queue
# is full, records are dropped and counted. Absent means 1875 MiB, enough for a
# 5-minute outage at 200 requests/second. Refused without `log_file_path`.
# log_queue_max_bytes = 1966080000      # 1875 MiB

# The collector decisions are also POSTed to, as newline-delimited JSON in
# batches of 256 records or 2 seconds, whichever comes first. Must use `https`
# unless the host is a loopback address or `localhost`.
# siem_url = "https://siem.example.org/services/collector/raw"
# The header the credential is sent under. The credential itself never goes in
# this file: it is read from the environment variable `PROBATION_SIEM_AUTH` at
# startup. With that variable unset, delivery is unauthenticated. Refused
# without `siem_url`.
# siem_auth_header = "Authorization"
# The same kind of budget for this sink's queue. It decides how long a collector
# outage costs nothing: records queue here while the collector is away and are
# dropped and counted once it is full. Absent means 1875 MiB. Two sinks are two
# queues, so a deployment with both pays both. Refused without `siem_url`.
# siem_queue_max_bytes = 1966080000     # 1875 MiB

# Whether every delivered record also carries the peer IP of the connection
# that asked, as a `consumer` field. Off by default, and then no record carries
# the field. Privacy note: this records who is installing packages. It is the
# peer address of the accepted TCP connection, never a port and never anything
# the caller supplies. Behind NAT or a load balancer it identifies that
# hop, not the machine that ran the install (§8 of docs/operations.md).
# Turning it off later erases nothing already written to the log file, its
# `.1`, the collector, or the host's console log.
# A log file this process creates gets mode 0600. A pre-existing file keeps its
# mode, and a group- or world-readable one is warned about once. With this on,
# a `log_file_path` that cannot be opened stops startup.
# Refused without `log_file_path` or `siem_url`, so the addresses it records
# reach a durable destination you chose.
# log_consumer_identification = false

# OSV vulnerability intelligence: every candidate your own blocklist would
# allow is also checked against OSV before it is served. A candidate is never
# blocked while an OSV lookup is unresolved: a slow or unreachable OSV costs
# staleness, never availability.
#
# How long an OSV answer is trusted before it is asked again. Optional, 300
# (5 minutes) if left out.
osv_cache_ttl_seconds = 300
# How long one OSV batch's outbound call may take before it fails open.
# Optional, 500 if left out.
osv_request_timeout_ms = 500
# What an OSV match does: `enforce` denies, `diagnostic` logs the match and
# allows, `off` skips the check. Optional, `enforce` if left out.
osv_mode = "enforce"

# TLS: `serve` terminates it itself when both keys are set (§6 of
# docs/operations.md says what still belongs in front of this service). Both
# or neither: one without the other is refused. Absent by default, and then
# `serve` binds plain HTTP.
# tls_cert_path = "/etc/probation/tls/cert.pem"
# tls_key_path = "/etc/probation/tls/key.pem"