Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

docs/v2/03-three-products.md

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") ],
)),
cat "$HOLGER_DATA/holger-passkeys/first-sign-in.url"   # open it in a browser and register a passkey

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

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