Hosting a Private Package Index with stout
Set up a private stout index for your organization — curated packages, Ed25519-signed updates, and internal tool distribution.
Organizations often need to distribute internal tools, enforce approved package lists, or patch upstream packages with custom configurations. Homebrew’s tap system can handle some of this, but it requires maintaining Ruby formula files in a git repository, and there is no built-in mechanism for signing, access control, or curation. stout’s private index feature lets you create a fully signed, curated package index for your organization — one that can include internal tools alongside upstream packages, enforced via Ed25519 cryptographic verification.
Why a private index
There are several scenarios where a private index is valuable:
Internal tool distribution. Your organization builds CLI tools, automation scripts, and developer utilities. You want engineers to install them with stout install our-tool rather than cloning a repository and running make install.
Curated package lists. Your security team has approved a specific set of packages and versions. A private index can contain only those packages, ensuring engineers cannot install unapproved software through the package manager.
Custom builds. Some packages need custom compile flags, patches, or configurations for your environment. A private index can distribute custom-built bottles while maintaining the same install experience.
Pinned versions. You need every developer and CI machine to use exactly the same package versions. A private index lets you pin versions centrally rather than relying on individual Brewfile discipline.
Creating a private index
Initialize a new private index:
stout index init --path /srv/stout-index --name "acme-internal"
# Initialized private index 'acme-internal'
# Signing keypair generated:
# Public: /srv/stout-index/keys/acme-internal.pub
# Private: /srv/stout-index/keys/acme-internal.sec
# Index database created: /srv/stout-index/index.db
The generated Ed25519 keypair is the root of trust for your index. Every index update is signed with the private key, and client machines verify the signature with the public key before accepting any metadata.
Adding upstream packages
Import packages from the public Homebrew ecosystem into your private index:
# Import specific packages (including all dependencies)
stout index import --path /srv/stout-index jq ripgrep git curl
# Import from an approved-packages list
stout index import --path /srv/stout-index --from-file approved-packages.txt
# Import with version pinning
stout index import --path /srv/stout-index "postgresql@15=15.6" "[email protected]=3.12.2"
When you import a package, stout copies its metadata into the private index database and downloads the bottle files to local storage. The bottle URLs in the private index point to your server, not to the upstream CDN. This means clients never need to contact ghcr.io or any public endpoint.
stout index import --path /srv/stout-index ffmpeg
# Resolving dependencies for ffmpeg...
# Importing 26 packages with bottles for arm64_sonoma...
# [========================================] 26/26
# Signing index... done
Adding custom internal packages
To distribute your own internal tools, create a package definition file:
# my-internal-tool.toml
[formula]
name = "acme-cli"
version = "2.4.1"
description = "ACME Corp internal developer CLI"
homepage = "https://wiki.internal/acme-cli"
license = "proprietary"
depends_on = ["jq", "curl"]
[bottle.arm64_sonoma]
url = "file:///srv/bottles/acme-cli-2.4.1.arm64_sonoma.bottle.tar.gz"
sha256 = "a1b2c3d4..."
[bottle.sonoma]
url = "file:///srv/bottles/acme-cli-2.4.1.sonoma.bottle.tar.gz"
sha256 = "e5f6a7b8..."
Add it to the index:
stout index add-formula --path /srv/stout-index my-internal-tool.toml
# Added acme-cli 2.4.1 to index
# Signing index... done
For organizations that build tools with standard build systems, stout can create bottles from build output:
# Build your tool
cd /path/to/acme-cli
cargo build --release
# Package it as a bottle
stout bottle create \
--name acme-cli \
--version 2.4.1 \
--bin target/release/acme-cli \
--output /srv/bottles/
# This creates acme-cli-2.4.1.arm64_sonoma.bottle.tar.gz
Index curation and approval workflows
A private index gives your security team control over what can be installed. The index only contains packages that have been explicitly imported or added — there is no fallback to the public index unless configured.
Implement an approval workflow:
# Developer requests a new package
stout index request --index acme-internal --package neovim
# Request submitted: neovim (pending approval)
# Security team reviews and approves
stout index approve --path /srv/stout-index neovim
# neovim approved. Importing...
# Importing neovim and 12 dependencies...
# Signing index... done
For automated workflows, you can integrate index management with your CI system:
# .github/workflows/index-update.yml
name: Update Private Index
on:
pull_request:
paths:
- 'approved-packages.txt'
jobs:
update-index:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Diff approved packages
id: diff
run: |
git diff origin/main -- approved-packages.txt | grep '^+' | grep -v '^+++' | sed 's/^+//' > new-packages.txt
- name: Import new packages to staging index
run: |
stout index import --path /srv/stout-index-staging --from-file new-packages.txt
- name: Run security scan on new packages
run: |
stout audit --index /srv/stout-index-staging --severity critical,high --exit-code
Serving the private index
Host the private index using stout’s built-in server or any HTTP server:
# Built-in server
stout index serve --path /srv/stout-index --bind 0.0.0.0:8443 --tls-cert /etc/ssl/cert.pem --tls-key /etc/ssl/key.pem
# Serving index 'acme-internal' at https://0.0.0.0:8443
# 156 packages available
The built-in server supports TLS, basic authentication, and bearer token authentication for access control:
stout index serve --path /srv/stout-index \
--bind 0.0.0.0:8443 \
--tls-cert /etc/ssl/cert.pem \
--tls-key /etc/ssl/key.pem \
--auth-token-file /etc/stout/auth-tokens
For production deployments, place the index behind your existing reverse proxy (nginx, Caddy, etc.) and use your organization’s standard authentication mechanism.
Client configuration
Configure developer machines to use the private index:
# Add the private index
stout config set index_url "https://stout.internal:8443"
# Trust the signing key
stout key trust /path/to/acme-internal.pub --name "acme-internal"
# Optional: require that all packages come from the private index
stout config set require_trusted_index true
With require_trusted_index enabled, stout will refuse to install any package that is not present in the private index and signed by a trusted key. This prevents engineers from bypassing the approved package list.
Once configured, the private index is transparent to end users:
stout search acme
# acme-cli — ACME Corp internal developer CLI
# acme-deploy — ACME Corp deployment automation
# acme-lint — ACME Corp code linting rules
stout install acme-cli
# Installing acme-cli 2.4.1...
# Verified signature: acme-internal (ed25519:b4c5...d6e7)
# Installation complete
Ed25519 signing details
stout uses Ed25519 for all index signing operations. Ed25519 was chosen for its speed (signing and verification in microseconds), its small key and signature sizes (32-byte keys, 64-byte signatures), and its resistance to implementation pitfalls that affect other signature schemes.
The signing process works as follows:
- The index SQLite database is finalized (all changes written, WAL checkpointed)
- A SHA-256 hash of the database file is computed
- The hash is signed with the Ed25519 private key
- The signature is written to
index.db.sigalongside the database
On the client side, verification happens before any metadata is read from the index:
# Verification flow (automatic, shown for illustration)
stout update
# Downloading index from https://stout.internal:8443...
# Verifying signature against trusted key 'acme-internal'...
# Signature valid. Index contains 156 packages.
If the signature does not match — whether due to corruption, tampering, or key mismatch — stout rejects the index and retains the previous valid version:
stout update
# Downloading index from https://stout.internal:8443...
# ERROR: Signature verification failed
# Expected signer: acme-internal
# The downloaded index has been rejected.
# Retaining previous index (last updated 6 hours ago).
Key rotation
When you need to rotate the signing key — whether on a schedule or in response to a compromise — stout supports a transition period where both old and new keys are trusted:
# Generate a new keypair
stout index rotate-key --path /srv/stout-index
# New keypair generated:
# Public: /srv/stout-index/keys/acme-internal-2026.pub
# Private: /srv/stout-index/keys/acme-internal-2026.sec
# Index is now dual-signed (old + new key)
# Distribute the new public key to clients within 30 days
# On client machines, trust the new key
stout key trust /path/to/acme-internal-2026.pub --name "acme-internal-2026"
# After all clients have the new key, revoke the old one
stout index revoke-key --path /srv/stout-index --key acme-internal
During the transition period, the index carries two signatures and clients accept either one. This allows a gradual rollout without a flag-day cutover.
Multi-index configuration
stout supports multiple indexes with priority ordering. This lets you overlay a private index on top of the public one:
stout config set indexes '[
{"name": "acme-internal", "url": "https://stout.internal:8443", "priority": 1},
{"name": "public", "url": "https://index.stout.neullabs.com", "priority": 2}
]'
When you run stout install jq, stout checks the highest-priority index first. If jq exists in the private index (perhaps at a pinned version), that version is used. If a package is not in the private index, stout falls back to the public index. Setting require_trusted_index to true disables this fallback entirely.
Need Rust performance engineering or AI agent expertise?
Neul Labs — the team behind stout — consults on Rust development, performance optimization, CLI tool design, and AI agent infrastructure. We build fast, reliable systems that ship.