3. Three products by hand
The guide
The by-hand guide is in the README: Three products by hand. It installs the server, the console and the downstream from their crates, connects the downstream online (enrolled on the server's Sync board, the signed bundles pulled from the server's bundles repository), installs a pip package through it, and carries the same bundle over an airgap: holger-cli verify on the stick, holger-downstream bundle import on a downstream with network: Offline.
cargo xtask products runs exactly the guide's commands (the four products built from this checkout's crates, pip in the python image); it is a step of the full CI set.
A downstream behind an estate's front takes a signed WireGuard invite instead (chapter 6); over mutual TLS, holger pair downstream and holger pair upstream pin the peers' certificates.
The one-way setup: the server pushes signed bundles into a drop folder by itself (export: in its RON, holger export-push); the one-way downstream's admin grants the first import (holger-downstream bundle allow-first-import, holger-cli bundle allow-first-import, or the console's "First import" form), and the inbox sweep imports each bundle (chapter 8).
The first admin with a passkey
People sign in to the console with a passkey. The passkey method is in the builds with auth-passkey (cargo install holger --features auth-passkey, or full); the plain default build has bearer tokens and mutual TLS only (server/cli/Cargo.toml).
Name the method in the RON:
auth: Some((
methods: [ Passkey(rp_id: "holger.example.org", rp_origin: "https://holger.example.org") ],
)),
-
The people's groups. When the RON names no group with the Admin role, the start adds
holger-admins(Admin),holger-writers(Writer) andholger-readers(Reader) and says so in its log (server/lib/src/auth.rs,passkey_groups). -
The first sign-in link. While the box has no admin,
holger startwrites a single-use link to<data>/holger-passkeys/first-sign-in.url, mode 0600, and logs "this box has no admin yet: the first sign-in link (single use) is in ". The passkey enrolled with it is the admin (server/lib/src/passkey.rs,offer_first_sign_in; testa_plain_start_writes_a_first_sign_in_link_and_its_passkey_is_the_admininserver/lib/tests/passkey_door_e2e.rs).
cat "$HOLGER_DATA/holger-passkeys/first-sign-in.url" # open it in a browser and register a passkey
-
No link is written when an admin passkey exists, when a subject in
auth.rolesis an Admin (that admin mints the invite withholger-cli first-key), or on an estate box (the shop feeds its buyer's invite). A restart keeps an open link; it does not mint a new one. The file is not deleted after use; its invite is then spent. -
The session key defaults to
<data>/holger-passkeys/session.key.
Point the build tools at Holger
Every block below uses the client's normal, verifying configuration: no trusted=yes, no gpgcheck=0, no --trusted-host, no --tls-verify=false. Set three values first:
export HOLGER=https://repo.example.org # Holger's HTTP door; an estate: https://repo.<estate>
export HOST=repo.example.org
export TOKEN="$(cat ~/.holger-token)" # a repository token: console, Account, tokens; or holger-cli token mint
Where the packages are. There are two layouts (server/lib/src/pull_intake/door.rs, holger-recipe/src/estate_ron.rs):
| what | pulled through from the internet | an estate's chain view or repository |
| pip | $HOLGER/production/simple/ |
$HOLGER/production/simple/ |
| npm | $HOLGER/production/<source>/ |
$HOLGER/npm/ |
| cargo | sparse+$HOLGER/production/<source>/ |
sparse+$HOLGER/<repository>/index/ |
| Maven | $HOLGER/production/<source>/ |
$HOLGER/maven/ |
| apt | $HOLGER/deb |
|
| dnf | $HOLGER/rpm |
|
| OCI | $HOST/production/<source>/<image> |
$HOST/oci/<image> |
| Go | $HOLGER/production/<source> |
$HOLGER/go |
| Helm | $HOLGER/helm (index) or oci://$HOST/oci |
<source> is the source's name in pull_through: (chapter 4).
The token. Every door takes the token as Authorization: Bearer <token>, as the bare token (what cargo sends), or as HTTP Basic with the token as the password; the user name is ignored (server/lib/src/exposed/http.rs, credential_of; tests in server/lib/tests/a_container_client_can_authenticate_at_the_oci_door.rs). Reads need no token where they are open: the default and downstream builds read openly, an estate gates reads except for the repositories it makes public (chapter 9). The console's Account page writes these snippets for one repository with your token filled in (server/lib/src/account.rs).
The first request of a package starts its pull; the package then waits for its approvals. Run the client again after the approvals (chapter 5).
Tests. The real-client tests run these lines against Holger on loopback over plain http://; they are named under each client. A Holger certificate from a public CA needs nothing more. A private CA goes into the operating system's trust store (update-ca-certificates on Debian, update-ca-trust on Fedora); no test covers that yet.
A TLS proxy in front. Holger builds the absolute links in the npm packument and the cargo config.json of a pull-through source from the request: the scheme from X-Forwarded-Proto (http when absent) and the host from X-Forwarded-Host or Host (server/lib/src/exposed/http.rs, npm_request_base). A proxy that ends TLS must send X-Forwarded-Proto: https. Holger's own TLS door (ron_tls) is not read for this, so behind no proxy those links say http://.
pip
pip config --user set global.index-url "https://token:$TOKEN@$HOST/production/simple/"
pip install requests==2.32.3
Tests: intake_serves_pip.rs and repository_tokens_with_real_clients.rs (server/lib/tests/); the README guide. A pinned version that is not the newest pulls through (chapter 5).
npm
cat >> ~/.npmrc <<EOF
registry=$HOLGER/production/npmjs/
//$HOST/production/npmjs/:_authToken=$TOKEN
EOF
npm install left-pad@1.2.0
On an estate, publish and install through the chain's npm door: registry=$HOLGER/npm/ and //$HOST/npm/:_authToken=$TOKEN. Tests: pull_through_npm_maven_crates.rs, repository_tokens_with_real_clients.rs, the_estate_declaration_serves_npm_through_the_intake_chain.rs.
cargo
Use Holger in place of crates.io:
mkdir -p .cargo
cat > .cargo/config.toml <<EOF
[source.crates-io]
replace-with = "holger"
[source.holger]
registry = "sparse+$HOLGER/production/crates-io/"
EOF
cargo fetch --locked
A pull-through source's index sits right under the source (no /index/). Where reads are gated (an estate's private production repository), its config.json says "auth-required": true, and cargo sends a token for every read. Name the source as a registry with a credential provider and give it the token:
cat > .cargo/config.toml <<EOF
[registries.holger]
index = "sparse+$HOLGER/production/crates-io/"
[registry]
global-credential-providers = ["cargo:token"]
[source.crates-io]
replace-with = "holger"
EOF
CARGO_REGISTRIES_HOLGER_TOKEN="$TOKEN" cargo fetch --locked
Without a credential provider cargo stops with "authenticated registries require a credential-provider to be configured". A box behind a private CA also needs [http] cainfo = "<ca.pem>". Tests: pull_through_npm_maven_crates.rs and real_clients_over_https_with_a_private_ca.rs (real pip, npm, cargo and apt over HTTPS with a private CA, reads gated).
Publish with cargo publish. On a Holger with an intake chain (every estate), publish to the chain's cargo door, the repository promotion.intake.cargo names (crates on an estate; ron_repo_type: "rust", no storage of its own):
cat >> ~/.cargo/config.toml <<EOF
[registries.holger]
index = "sparse+$HOLGER/crates/index/"
credential-provider = "cargo:token"
EOF
export CARGO_REGISTRIES_HOLGER_TOKEN="$TOKEN"
cargo publish --registry holger
# … the crate walks the intake chain: scanned, checked, approved by two people …
cargo install --registry holger <crate>
cargo yank --registry holger <crate> --version <version> # --undo to unyank
cargo publishis INGESTED intoincomingatcrates/<name>/<name>-<version>.crate, and cargo prints that. The publisher needs the Importer duty. cargo then waits up to a minute for the version to show in the index and says it timed out: the version is not listed until it is AVAILABLE.- The sparse index lists only AVAILABLE versions. Each line is read from the sealed crate's own
Cargo.toml(by the scan worker, at intake), and itscksumis the sha256 of the sealed bytes. A download serves the sealed bytes of an AVAILABLE version. - A crate with a malware finding is refused at the publish, by the rule's name. A crate that pins a dependency version with a known advisory (an exact
=x.y.zrequirement of a normal or build dependency, or a registry package in a binary crate'sCargo.lock) is refused at SCANNED, by the advisory and the dependency (smallvec@1.6.0 (pinned in Cargo.toml): RUSTSEC-2021-0003). cargo yankandcargo yank --undogo through the door: an AVAILABLE version only (holger-cargo-3otherwise), audited and recorded. A yanked version is still downloaded by a lock file that names it. A sealed version is never published again (holger-intake-10).- The refusals are
holger-cargo-1(not one publish of one crate) andholger-cargo-2(the crate'sCargo.tomlnames another crate or version).holger-cargo-4refuses the boot whenpromotion.intake.cargonames norustrepository without storage.
Test: cargo_publish_walks_the_intake_chain.rs (the real cargo publish, cargo install and cargo yank).
Any other repository outside the intake chain refuses writes by name (holger-intake-15) where the policy requires a scan (every estate, security_profile: Critical, or promotion.intake.scan.required naming a kind), because what it took would be served unscanned. A writable rust repository there points at the cargo door. Test: a_write_outside_the_chain_is_refused.rs.
On a Holger whose policy requires no scan, a writable rust repository takes cargo publish directly:
cat >> ~/.cargo/config.toml <<EOF
[registries.holger]
index = "sparse+$HOLGER/crates/index/"
credential-provider = "cargo:token"
EOF
CARGO_REGISTRIES_HOLGER_TOKEN="$TOKEN" cargo publish --registry holger
Test: repository_tokens_with_real_clients.rs. A rust repository's download and API links come from its ron_base_url, so that must be the public https:// address (server/repository/znippy-rust/src/lib.rs).
Maven
mkdir -p ~/.m2
cat > ~/.m2/settings.xml <<EOF
<settings>
<servers>
<server><id>holger</id><username>token</username><password>$TOKEN</password></server>
</servers>
<mirrors>
<mirror><id>holger</id><mirrorOf>*</mirrorOf><url>$HOLGER/production/central/</url></mirror>
</mirrors>
</settings>
EOF
mvn -U dependency:get -Dartifact=org.apache.commons:commons-lang3:3.17.0
<mirrorOf>*</mirrorOf> sends the plugins through Holger too. Run with -U after the approvals: Maven caches a 404. On an estate the URL is $HOLGER/maven/. Tests: pull_through_npm_maven_crates.rs, server/cli/tests/e2e_internet_to_build_systems.rs.
apt
The deb view's InRelease and Release.gpg are signed with the box's repository key (chapter 10):
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL "$HOLGER/-/repo-key.asc" | sudo tee /etc/apt/keyrings/holger.asc > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/holger.asc] $HOLGER/deb stable main" | sudo tee /etc/apt/sources.list.d/holger.list
printf 'machine %s/deb\nlogin token\npassword %s\n' "$HOST" "$TOKEN" | sudo tee /etc/apt/auth.conf.d/holger.conf > /dev/null
sudo chmod 600 /etc/apt/auth.conf.d/holger.conf
sudo apt-get update && sudo apt-get install hello
Any suite name works (the Release echoes it); the component and architecture come from the pool path a package was uploaded under (server/repository/znippy-deb/src/lib.rs). Test: real_apt_and_dnf_verify_the_signed_metadata_and_refuse_it_tampered in server/lib/tests/the_chain_serves_apt_dnf_helm_maven_go_views.rs (it also refuses metadata with one byte changed or the signatures stripped).
dnf
The rpm view's repomd.xml.asc is signed with the box's repository key. Holger signs the repository metadata, not the packages: gpgcheck=1 checks each rpm against its vendor's key, so import that key too.
sudo rpm --import https://vendor.example.org/RPM-GPG-KEY-vendor # the packages' own signer
sudo tee /etc/yum.repos.d/holger.repo > /dev/null <<EOF
[holger]
name=holger
baseurl=$HOLGER/rpm
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=$HOLGER/-/repo-key.asc
username=token
password=$TOKEN
EOF
sudo dnf install hello
Test: the same real_apt_and_dnf_verify_the_signed_metadata_and_refuse_it_tampered (microdnf on fedora-minimal 42, a vendor-signed rpm).
OCI: podman, docker, skopeo
printf '%s' "$TOKEN" | podman login "$HOST" -u token --password-stdin
podman pull "$HOST/production/cgr/chainguard/static:latest" # pulled through the source "cgr"
podman tag myapp:1.0 "$HOST/oci/myapp:1.0" && podman push "$HOST/oci/myapp:1.0"
docker login and skopeo copy take the same host and credentials. A pushed image is scanned and served only after its approvals (chapter 5). To check Holger's signed scan evidence with cosign: cosign verify-attestation --key "$HOLGER/-/evidence-key" --type https://holger.rs/attestation/scan-evidence/v1 --insecure-ignore-tlog=true "$HOST/oci/myapp:1.0" (Holger's evidence is key-signed and not in Rekor; chapter 8). Tests: the_chain_serves_apt_dnf_helm_maven_go_views.rs, real_signing_clients_verify_through_holger.rs, keyless_cosign_offline_through_holger.rs.
Go
printf 'machine %s login token password %s\n' "$HOST" "$TOKEN" >> ~/.netrc && chmod 600 ~/.netrc
export GOPROXY="$HOLGER/production/goproxy"
export GOSUMDB=off # or GOSUMDB=<a checksum database your site runs>
export GOFLAGS=-mod=mod
go mod download golang.org/x/text@v0.14.0
Holger serves no checksum database and does not proxy sum.golang.org (server/lib/src/pull_intake/go.rs). The modules are still checked against go.sum where the project has one. On an estate, GOPROXY=$HOLGER/go. Test: server/cli/tests/e2e_internet_to_build_systems.rs.
Helm
printf '%s' "$TOKEN" | helm repo add holger "$HOLGER/helm" --username token --password-stdin
helm pull holger/demo --version 0.1.0
# charts as OCI artifacts
printf '%s' "$TOKEN" | helm registry login "$HOST" -u token --password-stdin
helm pull "oci://$HOST/oci/demo" --version 0.1.0
Test: the_chain_serves_apt_dnf_helm_maven_go_views.rs (the index and the OCI form; no test covers Helm with a token yet).
Still in v1
- v1/demo-airgap.md, v1/secure-update-gateway-operator-guide.md (the operator guide names the retired binaries
holger-serverandholger-mikro; the verbs are nowholgerandholger-downstream).