Package Provider
What it does
The package provider serves the packages, the apps and their
libraries, that people run in your deployment. It keeps a catalog of
every published version under one namespace, usually datumhue, and
takes new versions from the people your publish
rules allow. Workstations find it through the aggregator; see the
Overview.
Quick start
Describe the provider in the deployment description, deployment.kdl:
the host it runs on, the namespace it serves, and who may publish to it.
package-provider {
host "providers-1"
namespace "datumhue"
publish-rules {
issuer "https://sign-in.example.com/realms/acme" {
allow scope="@acme" claim="groups" \
array_contains="publishers" rights="rw"
}
}
}
Make its files with the deployment tool, generate or add-provider.
On the host, install the program, then follow the README.md in the
host's folder of the bundle, which installs the rest and starts it:
sudo install -D -m 755 package-provider-linux-x86_64 \
/opt/datumhue/package-provider
Its log names its public key, which matches the line for it in the
bundle's pubkeys.txt, and then each announcement.
Publish packages from a workstation with datumhue pkg push, or load a
folder of packages at each start by adding --seed-from <folder> to the
unit's ExecStart= line.
How it works
The catalog. Each published version is recorded once and never changes: no one can publish a different package under the same scope, name and version. Workstations see each new version as it is published.
Two ways in. At start, --seed-from loads every package in a
folder into the catalog. While it runs, datumhue pkg push from a
workstation publishes a package, when a publish rule allows the person
who signed in. The provider sells nothing, so neither takes a package
marked monetization "paid": a push of one is refused, and
--seed-from skips it with a warning. Both publish a package marked
sealed #true as compiled code, never its Lua source.
Yanking. A yank marks a version as withdrawn: workstations stop offering it, and its content stays. It is the only way to take a version back.
Several providers. A package provider serves one namespace. Package providers that serve the same namespace share its catalog.
Command line
package-provider <COMMAND>
| Command | Description |
|---|---|
package-provider keygen | Generate a new secret key. |
package-provider show-pubkey | Show the public key for a secret key file. |
package-provider serve | Start the service. |
package-provider keygen
package-provider keygen [OPTIONS]
| Flag | Default | Description |
|---|---|---|
--output <OUTPUT>, -o | ./package_provider_secret.key | Path to write the secret key file |
--plaintext | off | Store the key unencrypted. Not recommended for production |
--env | off | Read the encryption passphrase from DATUMHUE_KEYSTORE_PASSPHRASE instead of prompting. Fails if the variable is unset or empty |
--force | off | Replace an existing key file at the output path. Without it, keygen refuses to overwrite: the key on disk is the identity the aggregator administrator authorized, and replacing it means a key rotation |
With neither --plaintext nor --env, keygen asks for a passphrase
and encrypts the key with it; an empty passphrase writes it plaintext.
A lost passphrase cannot be recovered.
package-provider show-pubkey
package-provider show-pubkey [OPTIONS]
| Flag | Default | Description |
|---|---|---|
--file <FILE>, -f | ./package_provider_secret.key | Path to the secret key file |
--env | off | Read the decryption passphrase from DATUMHUE_KEYSTORE_PASSPHRASE instead of prompting. Plaintext keys ignore it |
package-provider serve
package-provider serve [OPTIONS]
| Flag | Default | Description |
|---|---|---|
--config <CONFIG>, -c | ./package-provider.kdl | Path to the configuration file |
--seed-from <SEED_FROM> | none | Load every package in this folder into the catalog before serving, skipping any marked paid. Without it, the catalog is the one the last run kept |
--aggregator-url <AGGREGATOR_URL>, -a | none | Override the aggregator-url from the configuration file |
serve creates the key secret-key-file names when the file is
missing, and logs its public key as Package Provider Node ID. It then
logs Waiting for the Write capability over '<namespace>'... and waits
until capability-file holds a capability to write the namespace's
catalog.
Configuration
The settings file is KDL with one provider block. A key the block
does not know stops the start.
provider {
secret-key-file "/var/lib/datumhue/package-provider/provider.key"
capability-file "/var/lib/datumhue/package-provider/provider.caps"
namespace "datumhue"
aggregator-url "https://aggregator.example.com"
relay-url "https://aggregator.example.com"
blob-store-path "/var/lib/datumhue/package-provider/blobs"
catalog-store-path "/var/lib/datumhue/package-provider/catalog.redb"
publish-rules {
issuer "https://sign-in.example.com/realms/acme" {
allow scope="@acme" claim="groups" \
array_contains="publishers" rights="rw"
}
}
}
Common provider settings
Every provider's settings block takes these keys beside its own. A relative path anywhere in the settings file resolves against the folder that holds the file, not the folder the provider starts in.
| Key | Required | Default | Description |
|---|---|---|---|
aggregator-url | yes | none | The aggregator's public HTTPS URL. --aggregator-url overrides it |
relay-url | yes | none | The aggregator's relay, at the same public HTTPS URL |
discovery-url | no | the aggregator's | Where the provider publishes its address and looks up others |
announce-ttl | no | 300 | How long each announcement stays valid: seconds, or a duration such as "5m", at least 2 seconds. The provider announces again after half of it |
webrtc-bind-addr | no | none | A fixed UDP address for direct browser connections, such as "0.0.0.0:7844". Set it with webrtc-public-addr when the provider runs in a container or behind port forwarding |
webrtc-public-addr | no | none | The host:port browsers reach webrtc-bind-addr at. Every address it resolves to must route to that socket |
The key and the capability file. The secret key is the provider's identity: the provider creates it at its first start when the file is missing, and logs its public key at every start. The capability file holds the capabilities that let the provider announce what it serves. The provider reads it again before every announcement and rewrites it as capabilities renew, so it refuses to start when it cannot write to the file's folder. See Provider authorization.
| Variable | Description |
|---|---|
DATUMHUE_KEYSTORE_PASSPHRASE | Encrypts the secret key the first start creates. While the key is encrypted the variable must be set, and while it is plaintext it must be unset |
DATUMHUE_CERT_PATH | A PEM file of CA certificates to trust beside the public ones, for an aggregator whose certificate a private CA signed. Set, it must name a file of CA certificates only |
RUST_LOG | Log levels, info unless set |
The access log. At info, on the log target datumhue_audit, the
provider logs each access it allows or refuses, each sign-in it cannot
verify, and each session it ends because a new sign-in no longer passes
the rules. A mount or channel's open is logged, and the operations it
then allows are not, one by one. A refusal that repeats is logged with a
count, in repeated, not each time. RUST_LOG=info keeps the access log; RUST_LOG=info,datumhue_audit=off
turns it off alone, and a RUST_LOG of warn or error, or one that
names only other targets, leaves it out.
Each line has the message access and the fields provider, action,
resource, endpoint, iss, sub, outcome, rights, basis,
claim, reason, credential and repeated, with - where a field
has nothing to say. It names the mount, channel or scope, the connecting
program's endpoint id, the signed-in person, the outcome (allow,
deny, refused, verify_failed or session_dropped), for a refusal
the claim that failed, and in repeated how often a refusal came. It
never holds a token, a secret or the value a rule expects.
Package provider settings
| Key | Required | Default | Description |
|---|---|---|---|
secret-key-file | yes | none | The provider's secret key |
capability-file | yes | none | The capabilities to announce and write the namespace |
namespace | yes | none | The namespace the provider serves |
blob-store-path | no | ./provider_blobs | The folder of the packages' files. The provider must be able to write it, or it stops at start |
catalog-store-path | no | ./catalog.redb | The catalog: every published version |
publish-rules | no | publishing off | The rules for who may publish and yank packages |
Access rules
publish-rules decides who may publish to the namespace and yank its
versions. With no rules, or an empty block, publishing is off: the
provider still serves its catalog, and --seed-from still loads
packages. Publishing always takes a sign-in: allow anyone stops the
start.
Access rules say who may use a resource. A rule that reads a sign-in
sits in the block of the sign-in service whose tokens it reads, named by
its issuer URL exactly as those tokens carry it in iss. datumhue login prints that URL. Each rule names who it admits and the rights it
grants:
issuer "https://sign-in.example.com/realms/acme" {
require claim="email_verified" equals=#true
allow claim="groups" array_contains="analysts" rights="r"
}
| Rule | Admits everyone the issuer signs in whose |
|---|---|
allow signed-in | sign-in passes the block's require tests |
allow claim="<name>" equals="<value>" | claim is the text, or a list that holds it |
allow claim="<name>" equals=#true | claim is the boolean true (#false for false) |
allow claim="<name>" array_contains="<value>" | claim is a list that holds the text |
rightsis"r"(read) or"rw"(read and write). A rule admits a request when it matches and its rights cover what the request asks for. Rules add up: any one rule that admits a request is enough.requiretakes the sameclaimand match, withoutrights. Everyrequireof a block must hold before any of its rules counts.- A sign-in counts only in the block of the issuer that signed it. The
URL is compared exactly:
https://accounts.google.comandhttps://accounts.google.com/are two issuers. A sign-in from an issuer with no block gets nothing a block grants. equalsis typed.equals="true"matches the texttrue, never the boolean; a number must be quoted and is compared as text.claimnames a top-level claim of the person's ID token. A value nested in another claim, such as Keycloak'srealm_access.roles, never matches: map it to a top-level claim at your sign-in service.- Prefer the groups or roles your sign-in service manages, or
sub, overemail. On Google, addrequire claim="hd" equals="<your domain>"to admit only your Workspace accounts. - The issuer must be one the deployment's Identity Provider serves.
- An
emailclaim counts only when the same token saysemail_verifiedistrue, and aphone_numberclaim only when it saysphone_number_verifiedistrue. Microsoft Entra ID does not sendemail_verified: matchoid,suborgroupsthere. - A sign-in that does not verify is refused, even where
allow anyoneadmits a request without one. - A refused request learns which claim failed, never the value a rule expects.
Every package has a name of the form @<scope>/<name>. A publish rule
also says which scopes it covers:
scope="@acme"covers that one scope.scope-in-claim="<claim>"covers the scopes a claim lists: a person may publish to@acmewhen the named claim is"acme"or a list that holds it. The rule takes no other claim and no match.- No scope covers every scope. Keep it for a release identity that owns the whole namespace.
Each rule must grant rights="rw", since publishing and yanking both
write; a rule with rights="r" stops the start. One person's rights
over a scope cover both publishing to it and yanking from it.
The provider reads the rules at start, so restart it after changing them. A change to a person's claims at the sign-in service, such as leaving a group a rule names, applies at their next push or yank.
Network, state and dependencies
Network surface
| Direction | What |
|---|---|
| from workstations | Reading the catalog, and downloading packages |
| from publishers | Publishing a version, and yanking or restoring one |
| to the aggregator | Announcing the namespace, every half announce-ttl |
| to the aggregator | The sign-in services' keys, to verify publishers |
| to other package providers of the namespace | The shared catalog |
The provider listens on no TCP port; workstations reach it through the aggregator. It takes 4096 connections at once; past that a connection is turned away as busy.
Persistent state
| Path | Holds |
|---|---|
secret-key-file | The provider's identity |
capability-file | Its capabilities, which it renews itself |
blob-store-path | The packages' files |
catalog-store-path | The catalog: every published version |
Keep every one on persistent storage.
- A lost catalog loses every version
pkg pushpublished, unless another package provider of the namespace still holds it:--seed-frombrings back only the packages still in its folder. - Lost package files leave catalog entries that fail to download, until the packages are loaded again.
- A lost key makes a new one at the next start, which has no capability: the aggregator refuses it until the deployment tool makes capabilities for the new key.
External dependencies
- The aggregator, for announcing and for the sign-in services' keys. Publishing stops within a minute of losing it.
- The Identity Provider, which publishes the keys of the sign-in services publishers use.
Running as a service
systemd
The deployment tool writes a unit like this one for every provider it
sets up. To run the provider under systemd by hand,
install the program as /opt/datumhue/package-provider, its settings as
/, and give the datumhue user the state
folder /.
/:
[Unit]
Description=DatumHue Package Provider
After=network.target
[Service]
Type=simple
User=datumhue
Group=datumhue
WorkingDirectory=/var/lib/datumhue/package-provider
ExecStart=/opt/datumhue/package-provider serve \
--config /etc/datumhue/package-provider.kdl
Environment=DATUMHUE_CERT_PATH=/etc/datumhue/ca-cert.pem
EnvironmentFile=/etc/datumhue/keystore.env
Restart=always
RestartSec=10
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/datumhue/package-provider
[Install]
WantedBy=multi-user.target
- The unit keeps
/etc/datumhueread-only, and a relative path in the settings file resolves there. Pointsecret-key-file,capability-fileand every other file the provider writes into/.var/ lib/ datumhue/ package-provider/ keystore.envholdsDATUMHUE_KEYSTORE_PASSPHRASE=...; keep it mode 600. Drop theEnvironmentFile=line for a plaintext key.- Drop the
DATUMHUE_CERT_PATHline when the aggregator's certificate comes from a public CA. - On the aggregator's host, add
datumhue-aggregator.servicetoAfter=.
sudo systemctl daemon-reload
sudo systemctl enable --now datumhue-package-provider
journalctl -u datumhue-package-provider -f
systemctl stop stops the provider the way Ctrl+C does: it stops
announcing and gives its provider seat back, so the next start takes a
seat at once.
To load a folder of packages at each start, add --seed-from <folder>
to ExecStart=; the unit hides /home, so keep the folder outside it.
Docker
To run the provider in a container, build an image from this
Dockerfile, in the folder that holds the program:
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY package-provider-linux-x86_64 /opt/datumhue/package-provider
ENTRYPOINT ["/opt/datumhue/package-provider"]
CMD ["serve", "--config", "/etc/datumhue/package-provider.kdl"]
Run it as the datumhue user that owns the state folder, with the
settings folder read-only:
docker build -t datumhue/package-provider .
state=/var/lib/datumhue/package-provider
docker run -d --name datumhue-package-provider --restart always \
--user "$(id -u datumhue):$(id -g datumhue)" \
--env-file /etc/datumhue/keystore.env \
-e DATUMHUE_CERT_PATH=/etc/datumhue/ca-cert.pem \
-v /etc/datumhue:/etc/datumhue:ro -v "$state:$state" \
datumhue/package-provider
The docker-compose.yml the deployment tool writes runs images by these
names, with the same folders.
Keep the catalog, the package files and the key on the mounted state folder: a container without it loses them at every restart.
Security notes
- Publish rules are the boundary. Anyone a rule admits can publish
new versions to its scopes and yank existing ones there. Bind rules
to a scope, or to
scope-in-claim, rather than to every scope. Published versions never change, so a stolen sign-in cannot alter one. - Key changes apply within a minute. A change to a sign-in service's keys at the aggregator, a revocation included, applies here within a minute.
- Encrypt the key.
keygenencrypts it unless told not to. The keyservemakes is plaintext unlessDATUMHUE_KEYSTORE_PASSPHRASEis set, so set it, or keep the key on an encrypted volume. - Key files must be mode 600, or the provider refuses to start.
- The write capability makes the catalog. Only a provider whose capability file grants writing the namespace adds to its catalog, and workstations verify every entry's author. A provider takes catalog entries only from the other providers of its namespace, and stops within a minute once one is revoked. Capability files belong to one provider key; never copy one to another machine.
Troubleshooting
Messages. The provider writes its log to standard output; under
systemd, read it with journalctl -u and the unit's name.
The aggregator is refused as untrusted. It serves the certificate it
made itself: point DATUMHUE_CERT_PATH at its tls/ca-cert.pem.
provider lease acquisition failed. With QuotaExhausted, every
provider seat of the license is taken: stop a provider, or extend the
license.
The seat or announcements are refused. capability-file holds no
current capability for this key: make one with
datumhue-deploy reissue-capabilities.
The provider stops at start, naming relay-url. Set relay-url to
the aggregator's public HTTPS URL.
The provider refuses its key file. A key file that others can read
is refused: chmod 600 it. DATUMHUE_KEYSTORE_PASSPHRASE must be set
for an encrypted key and unset for a plaintext one; the message says
which the key is.
The provider cannot write its capability file. Give the provider's user write access to the folder that holds the file; it rewrites the file as capabilities renew.
Announcements are refused, after a lost key. serve made a new key:
read it from the log line Package Provider Node ID or with
show-pubkey, then make capabilities for it with the deployment tool.
Waiting for the Write capability … never ends. capability-file
holds no capability to write the namespace the namespace setting
names: check that both name the same namespace, or make the provider's
capabilities with the deployment tool.
Workstations find no packages. The log shows
Announcer started at start and Announced namespace at each renewal,
and Catalog holds <n> package(s) at start. With none, start once with
--seed-from <folder>, or publish.
Publishing is refused. The access log's outcome=deny line names
the scope as resource and the claim that failed as claim. Check the
claims in the publisher's ID token, with your sign-in service's token
view, and that the rule's scope matches the package's @scope. A
package marked paid is always refused, with the reason that this
registry doesn't sell packages. Restart the provider after changing
the rules.
The catalog is empty after a restart. catalog-store-path is not on
persistent storage. Move it to a volume; versions published since are
lost, and --seed-from brings back only those in its folder.