Security Policy¶
Scope¶
xferry is a controlled security-testing tool, not a hardened multi-tenant
service. Use it only with explicit authorization and test data. Binding to a
public address, enabling TLS, or adding Basic Auth does not by itself make an
internet-facing deployment safe.
The supported distribution is a reviewed source checkout. Project automation does not publish packages, release binaries, or registry images.
Report a vulnerability¶
Do not open a public issue. Create a private GitHub Security Advisory with the affected version, configuration, reproduction steps, and expected impact. Do not include live credentials or user data.
Reports are handled on a best-effort basis. We usually acknowledge a report within three working days and provide an initial assessment within seven.
External exposure baseline¶
Before exposing xferry outside a trusted network, provide all of the following:
- TLS with a hostname clients verify.
- Strong Basic Auth read from a permission-restricted file.
- A firewall or reverse proxy with finite connection, header, body, and timeout limits.
- Finite upload, note, and temporary-artifact quotas, plus a free-space reserve or external hard quota.
- An explicit request-Host allowlist matching every certificate and gateway authority. Do not treat CORS origins as request authorities.
- Exact allowed browser origins. Wildcard CORS permits read-only requests and does not authorize mutations.
- Proxy-side per-client throttling when the direct TCP peer is a proxy.
- Process or container resource limits, logs, and monitoring.
- Backups and a tested recovery procedure for operator-owned state.
- Test data or data the operator is explicitly permitted to handle.
The public deployment guide turns this baseline into a configuration procedure.
Security boundaries¶
localandlocal-secureare intended for loopback or trusted-local use.public-directenables strict configuration validation, but presets do not add or remove HTTP methods.- Basic Auth credentials travel with every request and require TLS on an untrusted network.
- The runtime identifies the direct accepted-socket peer. Forwarding headers do not establish client identity or a loopback boundary.
- Every HTTP/1.1 request must carry one valid admitted
Host; HTTP/1.0 may omit it. Duplicate or folded security fields fail before authentication, routing, CORS, or WebSocket handling. A valid but unapproved authority receives 421. - Configure request authorities with
[security] allowed_hosts,XFERRY_ALLOWED_HOSTS, or repeatable--allowed-host. CORS is independent and never expands this allowlist. - Each active WebSocket occupies one worker.
- Request-body admission limits are not a process memory ceiling.
- Filesystem quotas assume the operator controls other writers to the data directory.
- A generated SMUGGLE URL is one-shot. A scanner, preloader, or
HEADrequest can consume it first. - ACME account keys and certificate private keys are secrets.
Release and update supply chain¶
ADR-011 accepts a staged controlled-distribution architecture, but it does not activate a public channel. The supported distribution remains a reviewed source checkout, and the current Release Verification workflow must remain unable to write to PyPI, GHCR, or GitHub Releases until the channel-specific stages close.
Release tags, workflow definitions, publisher identities, candidate artifacts, digests, manifests, checksums, SBOMs, provenance, signing identities, registry records, installer metadata, and update metadata are security-sensitive assets. The release boundary also includes GitHub-hosted runners, protected repository environments, PyPI, GHCR, GitHub Releases, the signing service, installers, and update clients.
The following controls govern every future publisher:
- Only a protected
vX.Y.Ztag may authorize production publication. Branch and pull-request workflows cannot reach a production publisher. - The tagged version must match the source, package, changelog, and manifest versions. Build jobs create one candidate; publisher jobs promote those exact verified bytes or OCI digests and never rebuild them.
- External actions use full commit-SHA pins. Workflow permissions default to
read-only, with channel-specific write scopes granted only to a publisher job
behind the protected
productionenvironment and required review. - PyPI uses OIDC trusted publishing and never a static API token. Registry, release, attestation, and signing credentials are short-lived and scoped to the one job and environment that need them wherever the service supports it.
- Published versions are immutable. Installation, update, and rollback select an exact version or digest rather than a mutable convenience label.
- Installers and update clients verify the signed metadata, artifact digest, release identity, trusted key, and downgrade policy before replacing a working installation. Verification fails closed.
Designated release maintainers own signing-key custody, rotation, revocation, and recovery. Private signing material must stay outside the repository, ordinary build jobs, artifacts, and logs. Reviewed verifier trust roots and key rotation metadata belong in the repository; an independent protected-environment approval gates access to the signing identity. A compromise stops promotion, revokes or yanks affected releases where supported, rotates identities, and publishes updated trust or revocation metadata before release work resumes.
Signed release metadata contract¶
Remote managed releases use manifest schema v2 plus the detached
xferry-release.json.sig envelope. The signed bytes are exactly the UTF-8 bytes
emitted by ReleaseManifest.to_bytes(): fixed field order, two-space JSON
indentation, lexically sorted artifact digests, and one trailing newline. A
signed manifest declares ed25519 and exactly one lowercase key ID. The
detached envelope fixes its schema, algorithm, payload type, key ID, and base64
signature; signature input is domain-separated and binds both the payload type
and key ID. Semantically equivalent but non-canonical JSON is rejected.
The signed fields include the exact version and tag, platform-specific artifact name, byte size, SHA-256 digest, source commit, workflow run, and complete declared artifact-digest map. The client authenticates those fields before it derives or downloads an executable URL. HTTPS and redirect policy, byte size, SHA-256, platform/version admission, downgrade prevention, config validation, health checks, and automatic rollback remain additional mandatory controls.
The embedded public-key ring supports two active keys during a planned
rotation. Each manifest still selects one signer: artifacts signed by either
active key verify during the overlap. After cutover, mark the old key
REVOKED; its signatures then fail with release_signing_key_revoked. Removing
an ID makes it unknown and fails with release_signing_key_unknown. Do not
remove or revoke the old key until every still-supported artifact has been
re-signed or retired. A revoked release is never an eligible signed rollback
target.
The initial production key is enrolled with the following reviewed record:
- key ID:
xferry-release-2026-09; - raw public key (hex):
4fe9ccd46e154ff6d866957995395993a584f50c80e3384d5db4068791684efc; - raw-public-key SHA-256 fingerprint:
e5e2e0f8bc8e051c540edea2ff5e0570c22876485c3f1cb6c3fd668a02b54859; - private-key custody: GitHub Environment
production-release, secretXFERRY_RELEASE_ED25519_PRIVATE_KEY_PEM; - environment protection: required reviewer
kgmnotes, admin bypass disabled; - rotation and revocation owner:
kgmnotes.
The matching environment variable XFERRY_RELEASE_SIGNING_KEY_ID carries the
non-secret key ID. Test suites generate separate disposable keys in memory;
test keys must never enter the production ring.
production-release is the private signing-key boundary. It is deliberately
separate from the production publisher-approval environment required by
ADR-011. Later release jobs may pass only signed artifacts and recorded digests
between those boundaries; they must never pass or export private key material.
Before any workflow is allowed to consume the signing secret, its environment
ref policy must be restricted to the reviewed release workflow and authorized
release refs.
If the signing identity may be compromised, stop every release and promotion
job, preserve the workflow and artifact evidence, and disable use of the
environment secret. Mark the affected public key REVOKED in the shipped ring,
publish an advisory and withdraw or deprecate affected artifacts without
deleting evidence, then create a new protected identity and ship its reviewed
public key before release work resumes. Planned rotation may use a two-active-key
overlap; suspected compromise must not. Loss of the environment secret is
recovered by rotation, never by copying private material into the repository or
logs.
The signing helper reads an Ed25519 private key only from a permission-restricted
path or an already-protected file descriptor. It signs already-built canonical
manifest or installer bytes, never rebuilds artifacts, and never prints private
material. Release automation must sign both xferry-release.json and
install.sh at the protected signing boundary.
Privileged installer verification order¶
The production key is enrolled, but the managed channel is not public until the release-assets stage closes. The required shape is download, offline verification with the shipped public-key ring, and only then explicit privileged execution:
curl --fail --silent --show-error --remote-name "$release_url/xferry-release.json"
curl --fail --silent --show-error --remote-name "$release_url/xferry-release.json.sig"
curl --fail --silent --show-error --remote-name "$release_url/install.sh"
curl --fail --silent --show-error --remote-name "$release_url/install.sh.sig"
python tools/verify_release_signature.py --manifest xferry-release.json --signature xferry-release.json.sig
python tools/verify_release_signature.py --installer install.sh --signature install.sh.sig
sudo sh ./install.sh
Run the verifier from a reviewed checkout. Never stream a network response into
a shell, and never use
curl | sudo sh: the installer is itself part of the root trust chain.
Payloads and notes¶
Advanced upload and SMUGGLE support none, XOR, and AES payload modes. XOR is
compatibility obfuscation, not confidentiality. AES uses the documented
AES-256-GCM format and fails closed; it never falls back to XOR. Use TLS for
transport confidentiality.
Secure Notepad stores ciphertext and metadata, but the server does not retain the client-derived AES key. Losing the client session key makes an existing note unrecoverable even when the server data directory survives.
See the threat model for assets, trust boundaries, and out-of-scope threats.