How to Build and Host a Custom pkg Repository
A secure FreeBSD pkg repository workflow covering package provenance, offline signing, atomic publication, HTTPS serving, client trust, and rollback.
FreeBSD pkg can consume additional repositories for internally built software, controlled package options, or a curated package set. A repository is more than a directory of archives: clients trust its catalogue to select executable code. Package provenance, signing-key custody, atomic publication, transport security, retention, and client policy all belong in the design.
Use Poudriere for repeatable Ports builds. pkg create can package already installed software, but it does not make an uncontrolled build reproducible or trustworthy. This guide begins after approved .pkg archives exist.
Inventory packages and preserve provenance
Create a new versioned staging directory rather than modifying the live repository:
install -d -o root -g wheel -m 0755 /srv/pkg/releases/2026-07-12
find /path/to/approved/packages -type f -name '*.pkg' -print
Copy only the packages accepted for this publication. Record their hashes, origins, versions, build jail, Ports commit, options, build logs, and approving change. A valid signature proves that the repository signer approved the catalogue; it does not prove that the source or build host was safe.
Inspect package metadata before publication:
pkg info -F /path/to/package.pkg
sha256 /path/to/package.pkg
Reject unexpected scripts, dependencies, prefixes, users, groups, or architecture. Do not mix packages built for different FreeBSD ABIs in one client-facing repository unless the layout and URL selection keep them unambiguous.
Generate and protect a signing key
The current pkg-repo(8) supports RSA, ECDSA, EdDSA, and an external signing command. A local RSA example from the manual is:
install -d -o root -g wheel -m 0700 /root/pkg-signing
openssl genrsa -out /root/pkg-signing/mycompany.key 4096
chmod 0400 /root/pkg-signing/mycompany.key
openssl rsa -in /root/pkg-signing/mycompany.key \
-out /root/pkg-signing/mycompany.pub -pubout
The private key must never be copied to clients or placed under the web root. Back it up encrypted with documented recovery and revocation procedures. For a stronger separation, use pkg repo ... signing_command: with a dedicated signing host so compromise of the web server cannot sign a malicious catalogue.
Distribute the public key to clients through a trusted configuration-management channel. Serving it beside the repository is convenient but does not establish trust when an attacker can replace both the repository and the key.
Build signed metadata in staging
Place the approved packages in the versioned directory, then create its catalogue:
pkg repo /srv/pkg/releases/2026-07-12 \
/root/pkg-signing/mycompany.key
ls -la /srv/pkg/releases/2026-07-12
pkg repo scans .pkg archives and writes repository metadata at the top level. Treat a nonzero exit or warning as a failed release. Confirm the catalogue, package count, ownership, and permissions before exposing it.
Do not run pkg repo directly in a directory clients are reading. A client can otherwise fetch a new catalogue while some referenced packages are still absent, or an old catalogue after packages were removed. Publish a complete versioned directory by atomically switching a web-server mapping or symlink only after validation. Keep at least one last-known-good release so rollback changes one pointer instead of rebuilding under pressure.
Poudriere already creates ready-to-export repository metadata and performs its own publication workflow. Do not blindly regenerate a Poudriere repository in place; configure its signing and serve the completed output it produces.
Serve only repository content over HTTPS
Use a supervised production web server, not python3 -m http.server. The Python command is useful for a temporary laboratory test but provides no durable service management, TLS policy, controlled logging, or deployment boundary.
Expose a read-only path such as /srv/pkg/current through nginx or another reviewed server. Disable directory listing unless operators need it, deny writes by the web-service account, and avoid serving build logs, keys, staging trees, or unrelated files. HTTPS protects confidentiality and transport integrity, but catalogue signing remains necessary because TLS termination and repository storage can be compromised independently.
From a separate client network, verify the final URL and representative package:
fetch -o /dev/null https://pkg.example.net/meta.conf
fetch -o /dev/null https://pkg.example.net/data.pkg
Monitor HTTP failures, certificate expiry, available space, repository age, and package count. A web server returning 200 for an obsolete catalogue is still an operational failure.
Configure client trust explicitly
Install the public key with root ownership and non-writable permissions:
install -d -o root -g wheel -m 0755 /usr/local/etc/pkg/keys
install -o root -g wheel -m 0644 mycompany.pub \
/usr/local/etc/pkg/keys/mycompany.pub
install -d -o root -g wheel -m 0755 /usr/local/etc/pkg/repos
Create /usr/local/etc/pkg/repos/mycompany.conf:
mycompany: {
url: "https://pkg.example.net",
signature_type: "pubkey",
pubkey: "/usr/local/etc/pkg/keys/mycompany.pub",
enabled: yes
}
The old pkg -R /usr/local/etc/pkg/repos update example did not verify the intended repository. Inspect effective configuration and force a fresh catalogue instead:
pkg -vv
pkg update -f
pkg rquery -r mycompany '%n-%v'
pkg install -r mycompany package-name
Replace package-name with a real package. Review the install plan and source repository before confirmation. Test a deliberately altered signature in an isolated environment to prove the client rejects it.
Decide whether repositories may be mixed
If the internal repository merely adds unique package names, the official FreeBSD repository may remain enabled. If both publish the same origin with different options, pkg can select an unintended version or later replace it during an upgrade. Document priorities and always rehearse pkg upgrade -n on a representative client.
For an exclusive managed set, create /usr/local/etc/pkg/repos/FreeBSD.conf:
FreeBSD: { enabled: no }
Disabling the official repository transfers responsibility for every required dependency and security update to the internal publisher. Prove the curated repository is complete before rollout. Do not edit the packaged default repository file directly; a package update can replace it.
Automate safely and test rollback
Automation should build into a new release, verify hashes and metadata, scan packages with the current vulnerability database, sign through the approved key path, test from a disposable client, and only then publish. A cron line that rewrites live metadata nightly provides scheduling, not quality control.
Retain release manifests and last-known-good packages. Test installing, upgrading, reinstalling, and rolling back representative packages. When removing a vulnerable release, preserve the audit record without leaving clients pointed at it. Alert when a build fails, publication stops advancing, signatures cannot be verified, or clients still use an old catalogue.
A custom repository is successful when clients can identify its origin, verify its signature, reproduce the approved package set, survive an interrupted publication, and return to a known-good release.
Related:
- pkgng Ships, Replacing FreeBSD’s Aging pkg_* Tools
- The FreeBSD Ports Collection vs. pkg: Choosing (and Combining) the Right Tool
Sources: