Kellnr on Ubuntu 24.04 on Azure User Guide
Overview
Kellnr is an open source private registry for Rust crates: a crates.io you run yourself, for code your team does not publish to the world. It speaks the registry protocol cargo already knows, so publishing an internal crate is cargo publish and depending on one is an ordinary dependency line pointing at your own registry.
It serves the sparse index cargo fetches, stores and returns the .crate files themselves, and includes a web interface for browsing what has been published, managing users and read-only accounts, and issuing and revoking API tokens. Crate ownership, groups and per-crate access control let one registry serve several teams, and an optional caching proxy can mirror public crates so builds keep working when the public index does not.
The cloudimg image delivers Kellnr 6.8.0 on Ubuntu 24.04 as a single node, served over TLS, with an administrator password and a cargo API token generated on the first boot of your VM. Backed by 24/7 cloudimg support.
What is included:
- Kellnr 6.8.0, the unmodified upstream release binary at
/usr/local/bin/kellnr, pinned by tag and by the SHA-256 of the release asset, run by systemd as the unprivilegedkellnruser - nginx terminating TLS on port
443with a certificate generated for your VM. Kellnr speaks plain HTTP only, and both of its credentials are bearer credentials sent on every request, so it never leaves the loopback interface - Authentication required on every registry operation — the index, crate downloads and publishing all demand a token, so a private registry is genuinely private rather than merely unadvertised
- No default credential. Kellnr's upstream default administrator password is the string
admin, and it is fixed the moment the registry database is created. This image ships no database at all, so that default never exists on a running instance - The crate store at
/var/lib/kellnr, owned by a service account with no shell and no password - The address your registry advertises to cargo resolved per VM, so
cargo publishworks from a developer machine immediately - No compiler toolchain: a registry is used from a developer machine, so
cargo,rustcandrustupare all absent from the image - An on-VM self test that publishes a crate, reads it back byte for byte and removes it again
- The
kellnrandnginxsystemd services, enabled and active
Kellnr and Rust are trademarks of their respective owners. All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them. This image packages the unmodified open source software, which is distributed under the Apache License 2.0.

Prerequisites
An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU, 4 GiB RAM) is the recommended size: one small Rust binary, an embedded SQLite database and nginx sit comfortably inside it. Choose a larger size if you expect a large crate catalogue or heavy concurrent CI traffic. Network security group inbound rules: 22/tcp from your management network, and 443/tcp from wherever your developers and CI runners connect. There is deliberately no port 80 — every URL this image publishes is HTTPS.
On the machine you will run cargo from you need Rust 1.74 or newer. Authenticated registries became stable in cargo 1.74; older versions cannot send a token to the index.
Step 1: Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Kellnr Crate Registry by cloudimg and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22). Then select Review + create and Create. Open 443 afterwards, as shown in the next step.
Step 2: Deploy from the Azure CLI
Accept the image terms once per subscription, then deploy:
az vm image terms accept --urn cloudimg:kellnr-ubuntu-24-04:default:latest
az group create --name kellnr-rg --location eastus
az vm create \
--resource-group kellnr-rg \
--name kellnr-vm \
--image cloudimg:kellnr-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
az vm open-port --resource-group kellnr-rg --name kellnr-vm --port 443 --priority 1010
Step 3: Connect to your VM
ssh azureuser@<vm-public-ip>
The login user is azureuser unless you chose a different administrator name when you created the VM.
Step 4: Confirm the registry is running
systemctl is-active kellnr nginx
Both must report active. The kellnr unit does not start until first boot has finished generating this VM's credentials, and nginx does not start until first boot has minted its certificate, so if you have only just created the VM give it a minute and run the command again.
Check the version and the liveness endpoint:
/usr/local/bin/kellnr --version
curl -s http://127.0.0.1:8000/api/v1/health; echo
That prints kellnr 6.8.0 and then OK. The health endpoint is deliberately the one route outside the authentication layer, so a load balancer can probe it without a credential — it reveals nothing but that the process is alive.
Step 5: Check what the network can reach
sudo /usr/local/sbin/kellnr-port-check.sh
This prints the exact set of TCP ports reachable from outside the VM and fails if it is anything other than 22 and 443. Kellnr itself listens on 127.0.0.1:8000 and is proven unreachable on the VM's external address; nothing listens on port 80 at all.
You can see the raw picture too:
sudo ss -Hltn | awk '{print $4}' | sort -u
Step 6: Retrieve the first-boot credentials
Every credential this registry uses was generated on this VM's first boot and exists on no other instance. They are written to a file only root can read:
sudo sed -E 's/^(KELLNR_ADMIN_PASSWORD=|KELLNR_API_TOKEN=).*/\1<redacted>/' \
/root/kellnr-credentials.txt
That shows the file with the two secrets masked, so you can see its shape before you read the real values. The fields are:
| Field | What it is |
|---|---|
KELLNR_URL |
the web interface for this VM |
KELLNR_HOST |
this VM's customer-facing address, resolved at first boot |
KELLNR_ADMIN_USER |
always admin |
KELLNR_ADMIN_PASSWORD |
the web interface password for this VM |
KELLNR_API_TOKEN |
the cargo API token for this VM |
KELLNR_REGISTRY_INDEX |
the index URL to put in ~/.cargo/config.toml |
KELLNR_TLS_CERT |
this VM's certificate, for clients that verify it |
Read the real values when you need them:
sudo grep -E '^KELLNR_(URL|HOST|ADMIN_USER|REGISTRY_INDEX)=' /root/kellnr-credentials.txt

Kellnr's upstream default administrator password is the string admin, and the credential is decided once, at the instant the registry database is created. This image ships no database, so that default never reaches a running instance — the next step proves it rather than asserting it.
Step 7: Confirm the door refuses everything but your credential
sudo /usr/local/sbin/kellnr-verify-auth.sh /root/kellnr-credentials.txt
This runs the whole credential gate against the real TLS front door and prints KELLNR_AUTH_OK only if every one of these holds:
- the documented default
admin/adminis refused with401 - a blank password is refused with
400, which is Kellnr's own validation answering before any credential check - five well-known weak passwords are refused with
401 - this VM's password is accepted, reports
is_admin, and hands out a session the web API then honours - the registry index and the publish endpoint both refuse a missing token and a wrong token with
401 - the index advertises
https://at this VM's own address withauth-requiredset - kellnr's own port does not answer on the VM's external address, and nothing answers on port 80
- TLS verification refuses an address the certificate does not name
Each of those checks is itself tested against a known-bad input, so a check that could not fail is caught:
sudo /usr/local/sbin/kellnr-gate-selftest.sh /root/kellnr-credentials.txt

Step 8: Sign in to the web interface
Open https://<KELLNR_HOST>/ in a browser. The image ships a certificate generated for your VM's own address; because it is self-signed your browser will warn once, and Step 14 shows how to install your own certificate. Sign in as admin with the password from Step 6.

The home page is the registry dashboard: how many crates and versions you hold, how many downloads they have served, and what was published most recently.

Step 9: Point cargo at your registry
On the machine you build from, add the registry to ~/.cargo/config.toml. The index URL is the KELLNR_REGISTRY_INDEX value from Step 6 — it is a sparse+https:// URL, which is the modern protocol cargo uses by default:
[registries.kellnr]
index = "sparse+https://<KELLNR_HOST>/api/v1/crates/"
Put the token in ~/.cargo/credentials.toml, which cargo expects to be readable only by you:
[registries.kellnr]
token = "<KELLNR_API_TOKEN>"
Cargo does not use the address you typed to download crates: it reads the registry's index configuration and follows the URLs published there. This image resolves your VM's own address at first boot and publishes it over HTTPS, so cargo works from a developer machine straight away. You can see exactly what your cargo will be told:
sudo bash -c 'T=$(grep "^KELLNR_API_TOKEN=" /root/kellnr-credentials.txt | cut -d= -f2-); H=$(grep "^KELLNR_HOST=" /root/kellnr-credentials.txt | cut -d= -f2-); curl -s --cacert /etc/ssl/kellnr/kellnr.crt --resolve "$H:443:127.0.0.1" -H "Authorization: $T" "https://$H/api/v1/crates/config.json"; echo'
auth-required being true is what tells cargo to send your token when it fetches the index and downloads crates.
The
--resolveflag is there because an Azure VM cannot dial its own public IP: the Standard Load Balancer does not hairpin, so a request from the VM to its own public address times out.--resolvekeeps your real address in the URL and in the TLS check while sending the connection to loopback. From your laptop you just use the URL.
Step 10: Publish a crate and depend on it
From your own crate's directory, on the machine you build from — not on the registry VM, which deliberately carries no Rust toolchain:
cargo publish --registry kellnr
Then depend on it from another project by naming the registry:
[dependencies]
my-internal-crate = { version = "0.1.0", registry = "kellnr" }
The crate page in the web interface shows the version, the licence, the owners and the exact dependency line to paste:

You do not need a Rust toolchain on the registry itself to check that publishing works end to end. The image ships a self test that does the complete round trip with your own token, through the real TLS front door, and then puts the registry back exactly as it found it:
sudo /usr/local/sbin/kellnr-publish-probe.sh /root/kellnr-credentials.txt
It publishes a crate using cargo's own wire format, requires the sparse index to list it with a matching checksum, downloads it back and requires the bytes to be identical rather than merely a 200, deletes it, and then requires the registry to be empty again on disk and in the database. Along the way it requires the emptiness check to fail while the crate is present — a check that could not see a crate that is genuinely there would be worthless.

Confirm nothing was left behind:
sudo /usr/local/sbin/kellnr-registry-empty.sh --with-db /root/kellnr-credentials.txt
Step 11: Issue tokens for CI
The token in your credentials file belongs to the admin account. Give each CI pipeline its own instead, so you can revoke one without disturbing anything else. In the web interface choose Settings, then Auth. Tokens, name the token and select Create Token. Copy the value immediately — it is shown once.

For a build agent, put the token in the environment rather than in a file. These belong on the CI runner, not on the registry VM:
export CARGO_REGISTRIES_KELLNR_TOKEN="the token you just created"
export CARGO_REGISTRIES_KELLNR_INDEX="sparse+https://your-registry-address/api/v1/crates/"
Under Settings you will also find User Management, where you can add users, mark accounts read-only so they can consume crates but not publish, and Group Management for granting several people access to the same crates at once.
Step 12: Mirror crates.io (optional)
Kellnr can cache public crates alongside your private ones, so a build keeps working when the public index is slow or unreachable. It is off by default in this image, because a registry that silently reaches out to the internet and mirrors whatever is asked of it is not what most people want on first boot, and the cache grows without bound.
To turn it on, edit /etc/kellnr/kellnr.toml, set enabled = true under [proxy], and restart:
sudo sed -n '/^\[proxy\]/,/^$/p' /etc/kellnr/kellnr.toml
Apply the change with sudo systemctl restart kellnr once you have edited the file. Allow for the extra disk the cache will use.
Step 13: Back up your registry
The whole registry is one directory: the SQLite database and every .crate file live under /var/lib/kellnr. That is deliberate — it means a backup is a copy of one path and a restore cannot leave metadata describing crates that are not there.
sudo tar -czf /tmp/kellnr-backup.tar.gz -C /var/lib kellnr && sudo ls -lh /tmp/kellnr-backup.tar.gz
Copy the archive off the VM with scp and keep it somewhere durable. Your /etc/kellnr/kellnr.env file belongs with it: it carries the administrator password and API token, and because Kellnr fixes the administrator credential when the database is created, a restored database with no matching environment file cannot be signed into. Back up both, together.
sudo rm -f /tmp/kellnr-backup.tar.gz
Step 14: Use your own certificate and domain
The image generates a self-signed certificate for your VM's IP address on first boot. For a registry your whole team will use, point a DNS name at the VM and install a certificate for it.
Put your certificate chain at /etc/ssl/kellnr/kellnr.crt and your private key at /etc/ssl/kellnr/kellnr.key (0600 root:root), then tell Kellnr the hostname it should advertise to cargo by setting KELLNR_ORIGIN__HOSTNAME in /etc/kellnr/kellnr.env, and restart both services. If you obtain a certificate with certbot, use its --nginx installer or copy the issued files into the paths above.
The hostname matters as much as the certificate: it is the value cargo is handed in the index configuration, so changing the certificate without changing it leaves every client being told to fetch from the old address.
Step 15: Keep the image patched
Unattended security upgrades are enabled. To patch now:
sudo apt-get update && sudo apt-get upgrade -y
Kellnr itself is a pinned upstream release rather than an Ubuntu package, so apt will never update it and will never warn you that it is old. To move to a newer release, download the kellnr-x86_64-unknown-linux-gnu.zip asset from the Kellnr releases page, verify its SHA-256 against the value GitHub publishes for that asset, stop the service, replace /usr/local/bin/kellnr, and start it again. Your data directory is untouched by this, and Kellnr migrates its own database on the next start. Take the Step 13 backup first.
Step 16: Rotate the administrator password
printf '%s' '<new-password>' | sudo /usr/local/sbin/kellnr-set-admin-password.sh /root/kellnr-credentials.txt
Replace <new-password> with a password of at least 12 characters. The helper reads it from standard input rather than the command line, so it does not land in your shell history or the process table. Kellnr's own endpoint accepts anything that is not empty; this wrapper refuses blank, short, all-digit and well-known weak values, refuses the password you are already using, and then proves the rotation took by requiring the new password to be accepted and the old one to be refused before it updates your credentials file.
You can see it refuse a weak value without changing anything:
if printf '%s' 'short' | sudo /usr/local/sbin/kellnr-set-admin-password.sh /root/kellnr-credentials.txt >/dev/null 2>&1; then echo "UNEXPECTED: a 5-character password was accepted"; exit 1; else echo "a weak password was REJECTED and nothing changed"; fi
Because the administrator credential is fixed when the registry database is created, this is the only way to change it. Re-provisioning the VM will not rotate it: first boot deliberately preserves the credentials of a registry that already exists, because minting new ones would hand you a credentials file your own registry has never heard of, with your crates still inside it.
Troubleshooting
kellnr.service is inactive and systemctl status says the condition failed. First boot has not finished. It writes /var/lib/cloudimg/kellnr.bootstrap-ready as its very last action, and the service is gated on that file, so that the upstream default password can never be written into a database. Check journalctl -u kellnr-firstboot.service.
nginx will not start. It is gated on /etc/ssl/kellnr/kellnr.crt for the same reason. If you replaced the certificate, check the file exists and that sudo nginx -t is happy.
cargo says the registry index is unauthorized. Your token is missing or your cargo is older than 1.74. Authenticated registries became stable in cargo 1.74; before that, cargo cannot send a token to the index at all.
cargo downloads fail with a connection error to 127.0.0.1. Something has reset KELLNR_ORIGIN__HOSTNAME to the upstream default. Check /etc/kellnr/kellnr.env, then re-read the index configuration as in Step 9 — the dl URL it prints is what cargo will actually use.
A crate will not publish because the version already exists. Registries do not allow a version to be replaced. Publish a new version, or delete the existing one from its crate page in the web interface if it was a mistake.
You have lost the credentials file. Kellnr fixes the administrator credential when the database is created and there is no way to re-apply a new one. Restore /etc/kellnr/kellnr.env from backup. If you have no backup, the only route is a fresh registry, which means losing published crates — which is why Step 13 asks you to back both up together.
Support
cloudimg provides 24/7 support for this image: deployment, upgrades, TLS certificates and custom domains, cargo and CI integration, user and token management, crates.io mirroring and scaling. Contact support through the Azure Marketplace listing or at cloudimg.co.uk.