Developer Tools Azure

Artifact Hub on Ubuntu 24.04 LTS on Azure

| Product: Artifact Hub on Ubuntu 24.04 LTS

Overview

Artifact Hub is a web based catalogue for finding, installing and publishing cloud native packages. It indexes Helm charts, OLM operators, OPA and Kyverno policies, Falco rules, Tekton tasks and pipelines, Krew kubectl plugins, container images and around thirty other artifact kinds, and presents them with versions, changelogs, values, security context and rendered documentation. It is a Cloud Native Computing Foundation incubating project.

Running it yourself is a different proposition from browsing the public site. A self hosted Artifact Hub is an internal, private catalogue of your own charts and operators: your platform team publishes to it, your developers search it, your organisations and accounts control who sees what, and nothing depends on a third party service being reachable.

The cloudimg image runs Artifact Hub 1.23.0, compiled from the pinned upstream release, alongside PostgreSQL 16 on the same VM, on a hardened, fully patched Ubuntu 24.04 LTS base. The database is a same box dependency rather than a separate service to provision, so the VM boots and serves on its own with nothing else to stand up.

Upstream's own sample data seeds an account of demo@artifacthub.io / changeme. This image never creates it. The database is migrated with sample data switched off, so that account has never existed on your VM, and the image ships with zero accounts at all. On the first boot of every VM a one shot service generates this VM's own database password, session cookie key, CSRF key and TLS certificate, creates a single administrator with a random password, and proves that administrator can sign in before anything is exposed. Nothing is bound to a network port until that finishes, and no secret is ever shared between customers. Backed by 24/7 cloudimg support.

What is included:

  • Artifact Hub 1.23.0 (Apache 2.0 licensed), built from the pinned upstream release, managed by systemd
  • The web interface and REST API on :443 over HTTPS behind nginx, with :80 redirecting to it
  • The repository tracker as a systemd timer that re indexes every registered repository every 30 minutes
  • The bundled documentation at /docs and the OpenAPI reference at /docs/api, served by your own VM
  • The ah command line linter for validating repository metadata before you publish
  • PostgreSQL 16 on the same VM, reachable only over loopback
  • A per VM database password, session cookie key, CSRF key and administrator password generated on first boot and recorded in a root only file
  • Zero accounts in the shipped image, so the upstream demo credential can never be used against your VM
  • A TLS certificate generated on first boot for this VM's own addresses, so no private key ships in the image
  • A starter public repository registered under your administrator account so the catalogue is populated on day one, removable in one click (registered with vulnerability scanning switched off, because the optional Trivy scanner is not part of this image)
  • A default drop nftables firewall so only 22, 80 and 443 are reachable from the network
  • postgresql.service, artifacthub-hub.service, nginx.service, nftables.service and artifacthub-tracker.timer as enabled units so the stack returns after a reboot
  • 24/7 cloudimg support

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2ms (2 vCPU / 8 GiB RAM) is the recommended starting point: the VM runs PostgreSQL, the web server and a tracker that parses every chart in every registered repository on a schedule. Size up if you register many large repositories. NSG inbound: allow 22/tcp from your management network and 443/tcp so your browser can reach the catalogue, plus 80/tcp if you want the automatic redirect from http:// to work. The VM needs outbound HTTPS to reach whichever repositories you register.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Artifact Hub 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), HTTPS (443) and HTTP (80). Then Review + create and Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name artifact-hub \
  --image <marketplace-image-urn> \
  --size Standard_B2ms \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Then open the web ports so your browser can reach the catalogue:

az vm open-port --resource-group <your-rg> --name artifact-hub --port 443 --priority 1001
az vm open-port --resource-group <your-rg> --name artifact-hub --port 80  --priority 1002

First boot provisioning takes about 30 seconds after the VM reports Running. SSH in with the administrator username you chose:

ssh azureuser@<public-ip>

Step 3 - Confirm the services are running

Four units make up the appliance, plus the timer that keeps the catalogue fresh. Every one of them is enabled, so the stack comes back on its own after a reboot.

for u in postgresql.service artifacthub-hub.service nginx.service nftables.service; do
  printf '%-28s %s / %s\n' "$u" "$(systemctl is-active "$u")" "$(systemctl is-enabled "$u")"
done
printf '%-28s %s / %s\n' artifacthub-tracker.timer \
  "$(systemctl is-active artifacthub-tracker.timer)" "$(systemctl is-enabled artifacthub-tracker.timer)"

Expected output:

postgresql.service           active / enabled
artifacthub-hub.service      active / enabled
nginx.service                active / enabled
nftables.service             active / enabled
artifacthub-tracker.timer    active / enabled

Now confirm the web interface answers, and that plain HTTP redirects to it. The certificate is the one your own VM generated on first boot, so curl is told to accept it with -k; your browser will show a warning until you install a certificate of your own in Step 12.

printf 'HTTPS  : HTTP %s\n' "$(curl -ks -o /dev/null -w '%{http_code}' https://127.0.0.1/)"
printf 'HTTP 80: HTTP %s\n' "$(curl -s  -o /dev/null -w '%{http_code}' http://127.0.0.1/)"
curl -ks https://127.0.0.1/ | grep -o 'artifacthub:siteName" content="[^"]*"'

Expected output:

HTTPS  : HTTP 200
HTTP 80: HTTP 301
artifacthub:siteName" content="Artifact hub"

The API is served by this VM on the same origin as the web interface. Ask it for the catalogue statistics:

All four appliance services active and enabled, with PostgreSQL, the API server and the metrics endpoint bound to loopback only

curl -ks https://127.0.0.1/api/v1/packages/stats

Expected output. One starter repository is registered out of the box, so the catalogue is already populated; your figures grow as you add your own repositories:

{"packages" : 1, "releases" : 44}

Step 4 - Confirm no default credential shipped

Artifact Hub's upstream sample data seeds demo@artifacthub.io with the password changeme. This image is built with sample data switched off, so the account was never created. Prove both halves: the database has no such row, and the running server rejects the credential.

echo "accounts on this VM      : $(sudo -u postgres psql -tAc 'SELECT count(*) FROM "user"' -d hub | tr -d ' ')"
echo "upstream demo account    : $(sudo -u postgres psql -tAc "SELECT count(*) FROM \"user\" WHERE email='demo@artifacthub.io'" -d hub | tr -d ' ') row(s)"
JAR=$(mktemp)
TOK=$(curl -ks -c "$JAR" -D - -o /dev/null https://127.0.0.1/api/v1/csrf | tr -d '\r' | awk 'tolower($1) ~ /^x-csrf-token:/ {print $2}')
CODE=$(printf '{"email":"demo@artifacthub.io","password":"changeme"}' \
  | curl -ks -b "$JAR" -H "X-CSRF-Token: $TOK" -H 'Content-Type: application/json' \
      -H 'Referer: https://127.0.0.1/' --data @- \
      -o /dev/null -w '%{http_code}' https://127.0.0.1/api/v1/users/login)
rm -f "$JAR"
echo "demo credential sign in  : HTTP $CODE"
test "$CODE" != "200" || { echo "UNEXPECTED: the demo credential was accepted"; exit 1; }

Expected output:

accounts on this VM      : 1
upstream demo account    : 0 row(s)
demo credential sign in  : HTTP 401

The single account is the administrator your own VM generated in Step 6.

The appliance's own API returning catalogue statistics and search results, its bundled documentation answering 200, and the upstream demo credential rejected with HTTP 401

Step 5 - Review the host firewall

The image ships a default drop nftables ruleset. PostgreSQL and the server's Prometheus metrics endpoint are bound to loopback only, so widening your NSG later cannot expose them.

sudo nft list ruleset | grep -E 'policy drop|dport'
echo "--- listening sockets ---"
sudo ss -tlnp | awk '{print $1, $4}' | grep -E ':(22|80|443|5432|8000|8001)$'

Expected output:

        type filter hook input priority filter; policy drop;
        tcp dport { 22, 80, 443 } accept
        type filter hook forward priority filter; policy drop;
        ip daddr 168.63.129.16 tcp dport 53 counter packets 0 bytes 0 accept
--- listening sockets ---
LISTEN 0.0.0.0:80
LISTEN 0.0.0.0:22
LISTEN 0.0.0.0:443
LISTEN 127.0.0.1:5432
LISTEN 127.0.0.1:8000
LISTEN 127.0.0.1:8001
LISTEN [::]:80
LISTEN [::]:22
LISTEN [::]:443

Only 22, 80 and 443 accept traffic. The last rule in the input chain is Azure's own platform DNS resolver, added by the agent. PostgreSQL on 5432, the application server on 8000 and its unauthenticated Prometheus endpoint on 8001 are all bound to 127.0.0.1, so they are unreachable from the network whatever your NSG allows.

Step 6 - Retrieve this VM's administrator credentials

Every password, signing key and certificate on this VM was generated during its own first boot. They are recorded in a root only file.

sudo stat -c '%n  %a  %U:%G' /root/artifact-hub-credentials.txt
sudo grep -E '^artifacthub\.(url|admin\.email|admin\.alias)=' /root/artifact-hub-credentials.txt
echo "administrator password length: $(sudo grep '^artifacthub.admin.password=' /root/artifact-hub-credentials.txt | cut -d= -f2- | wc -c) characters"

Expected output (the URL is your own VM's address):

/root/artifact-hub-credentials.txt  600  root:root
artifacthub.url=https://4.246.179.114/
artifacthub.admin.email=admin@4.246.179.114
artifacthub.admin.alias=cloudimg
administrator password length: 25 characters

Print the password itself when you are ready to sign in:

sudo grep '^artifacthub.admin.password=' /root/artifact-hub-credentials.txt | cut -d= -f2-

Confirm those credentials really do authenticate against the running server. This reads them straight from the file, so nothing is ever typed on a command line:

E=$(sudo grep '^artifacthub.admin.email=' /root/artifact-hub-credentials.txt | cut -d= -f2-)
P=$(sudo grep '^artifacthub.admin.password=' /root/artifact-hub-credentials.txt | cut -d= -f2-)
JAR=$(mktemp)
TOK=$(curl -ks -c "$JAR" -D - -o /dev/null https://127.0.0.1/api/v1/csrf | tr -d '\r' | awk 'tolower($1) ~ /^x-csrf-token:/ {print $2}')
CODE=$(printf '{"email":"%s","password":"%s"}' "$E" "$P" \
  | curl -ks -b "$JAR" -c "$JAR" -H "X-CSRF-Token: $TOK" -H 'Content-Type: application/json' \
      -H 'Referer: https://127.0.0.1/' --data @- \
      -o /dev/null -w '%{http_code}' https://127.0.0.1/api/v1/users/login)
echo "sign in           : HTTP $CODE  (204 = a session cookie was issued)"
PROFILE=$(curl -ks -b "$JAR" https://127.0.0.1/api/v1/users/profile)
rm -f "$JAR"
echo "authenticated as  : $(echo "$PROFILE" | grep -o '"alias":"[^"]*"')"
echo "$PROFILE" | grep -q '"alias"' || { echo "FAILED: the issued session is not usable"; exit 1; }

A successful sign in returns 204 and no body: the useful part is the session cookie it sets, so the block then uses that session to read your own profile back.

Expected output:

sign in           : HTTP 204  (204 = a session cookie was issued)
authenticated as  : "alias":"cloudimg"

Change the password from Control Panel -> Settings -> Profile once you are in, and keep the file for the database credentials.

The first-boot service completed, the credentials file at mode 0600 root:root, and exactly one account on the VM with its password stored as a bcrypt hash

Step 7 - Sign in to Artifact Hub

Browse to https://<public-ip>/. Because the certificate was generated by the VM for itself, your browser shows a warning the first time; accept it, or install your own certificate first with Step 12.

Click Sign in at the top right and use the email address and password from Step 6.

Artifact Hub catalogue landing page

Self service sign up is deliberately switched off in the shipped configuration. Artifact Hub creates self registered accounts with their email unverified and refuses to sign them in until a verification mail is confirmed, so on an appliance with no mail server a customer who signed up could never get in. Step 11 turns both on together once you have an SMTP server.

Step 8 - Point Artifact Hub at your own repositories

This is what a self hosted catalogue is for. One starter repository is registered under your administrator account so the catalogue is not empty on day one; add your own beside it, or remove it entirely.

In the web interface go to Control Panel -> Repositories -> Add repository, choose the kind (Helm charts, OLM operators, Falco rules, OPA policies, Tekton tasks and so on), give it a name and the URL your charts are published at, and save. The Add repository dialog links to the bundled documentation for each kind, served by this VM:

for d in /docs/ /docs/api/ /docs/topics/repositories/helm-charts/; do
  printf '%-42s HTTP %s\n' "$d" "$(curl -ks -o /dev/null -w '%{http_code}' "https://127.0.0.1$d")"
done

Expected output:

/docs/                                     HTTP 200
/docs/api/                                 HTTP 200
/docs/topics/repositories/helm-charts/     HTTP 200

List what is registered right now:

sudo -u postgres psql -c "SELECT name, url, last_tracking_ts IS NOT NULL AS indexed FROM repository ORDER BY name" -d hub

Expected output:

     name     |                    url                     | indexed 
--------------+--------------------------------------------+---------
 artifact-hub | https://artifacthub.github.io/helm-charts/ | t
(1 row)

To remove the starter repository, open Control Panel -> Repositories, use the row menu and choose Delete. Deleting it removes its indexed packages with it.

Container image vulnerability scanning is not included in this image. Artifact Hub's security reports come from a separate upstream component that needs its own long running Trivy server and vulnerability database. The starter repository is therefore registered with scanning switched off, so the control panel does not advertise a scan that will never run. Everything else in the catalogue, including the packages' own declared security context and signature metadata, works exactly as upstream.

Before you publish, lint your repository metadata with the bundled CLI so the catalogue renders it properly:

ah version
ah lint --help | head -7

Expected output:

Version: 1.23.0
Git commit: f90cbb4ee79a31eaa348dbb2868ffa9d2cbe15d8
Check the repository's packages are ready for Artifact Hub

Use this command to check that the packages in your repository are ready to be
listed on Artifact Hub. This command checks that the packages metadata provided
is valid and displays some information about the data that will be collected so

Step 9 - Watch the tracker index your repositories

The tracker is a one shot job on a 30 minute timer, mirroring the schedule upstream uses. Systemd will not start a second copy while one is running, so runs can never overlap.

systemctl list-timers artifacthub-tracker.timer --no-pager | head -3

Expected output:

NEXT                         LEFT LAST PASSED UNIT                      ACTIVATES
Fri 2026-08-07 13:31:00 UTC 14min -         - artifacthub-tracker.timer artifacthub-tracker.service

Run it immediately rather than waiting for the timer, then confirm packages actually landed:

sudo systemctl start artifacthub-tracker.service
echo "tracker result: $(systemctl show -p Result --value artifacthub-tracker.service)"
echo "packages in the catalogue: $(sudo -u postgres psql -tAc 'SELECT count(*) FROM package' -d hub | tr -d ' ')"
curl -ks 'https://127.0.0.1/api/v1/packages/search?limit=3&facets=false' | head -c 220; echo

Expected output:

tracker result: success
packages in the catalogue: 1
{"packages":[{"package_id":"ceb9ab96-d1dc-49cb-be94-34e2b3b83d92","name":"artifact-hub","normalized_name":"artifact-hub","logo_image_id":"019ab0f2-2b4a-47d9-abe3-cc8ddb92a4b2","stars":0,"description":"Artifact Hub is a w

If a repository reports errors, they are recorded against it and shown in the Control Panel:

A tracker run completing successfully, the registered repository marked as indexed, and the resulting packages and releases in the catalogue

sudo -u postgres psql -c "SELECT name, coalesce(last_tracking_errors,'(none)') AS errors FROM repository" -d hub

Expected output:

     name     | errors 
--------------+--------
 artifact-hub | (none)
(1 row)

Step 10 - Use the REST API with an API key

Requests carrying an API key skip the browser CSRF handshake, which makes them convenient for CI. Create one from Control Panel -> Settings -> API keys, then use the two headers Artifact Hub expects:

curl -k -H "X-API-KEY-ID: <your-token>" -H "X-API-KEY-SECRET: <your-token>" \
  https://<public-ip>/api/v1/repositories/user

Anonymous read endpoints need no key at all, which is what most search integrations use:

curl -ks https://127.0.0.1/api/v1/packages/stats
curl -ks 'https://127.0.0.1/api/v1/packages/search?limit=1&facets=false' \
  | grep -o '"packages":\[' | head -1

Expected output:

{"packages" : 1, "releases" : 44}
"packages":[

The full OpenAPI reference is served by your own VM at https://<public-ip>/docs/api.

Step 11 - Enable email and self service sign up (optional)

Sign up and email are two halves of one feature. Configure an SMTP server first, then allow sign ups; enabling sign up on its own produces accounts that can never sign in.

Edit /etc/artifacthub/hub.yaml, uncomment the email: block and fill in your own server, then set allowUserSignUp: true:

sudo nano /etc/artifacthub/hub.yaml
sudo systemctl restart artifacthub-hub.service

A secret free reference copy of every available setting ships alongside it:

sudo ls -l /etc/artifacthub/
echo "--- current sign up setting ---"
sudo grep 'allowUserSignUp' /etc/artifacthub/hub.yaml

Expected output:

total 12
-rw-r----- 1 root artifacthub 2444 Aug  7 13:10 hub.yaml
-rw-r--r-- 1 root root         788 Aug  7 13:00 hub.yaml.sample
-rw-r----- 1 root artifacthub  587 Aug  7 13:10 tracker.yaml
--- current sign up setting ---
  allowUserSignUp: false

The same file holds theme.siteName. Leave it as shipped unless you intend to rebrand: changing it switches the front end into white label mode, whose footer loads a logo from artifacthub.io and therefore shows a broken image on a VM without outbound access to that host.

Step 12 - Install your own TLS certificate

The certificate generated on first boot is valid for this VM's own addresses but is self signed, so browsers warn. Point a DNS name at the VM and replace it. nginx terminates TLS, so only these two files change:

sudo certbot certonly --standalone -d <your-domain> --agree-tos -m <your-email>
sudo cp /etc/letsencrypt/live/<your-domain>/fullchain.pem /etc/nginx/tls/artifact-hub.crt
sudo cp /etc/letsencrypt/live/<your-domain>/privkey.pem   /etc/nginx/tls/artifact-hub.key
sudo chmod 600 /etc/nginx/tls/artifact-hub.key
sudo systemctl restart nginx.service

Then update the address Artifact Hub uses in outbound links and in its CSRF allowlist. Edit /etc/artifacthub/hub.yaml, set server.baseURL to https://<your-domain> and add <your-domain> to server.csrf.trustedOrigins, then restart:

sudo nano /etc/artifacthub/hub.yaml
sudo systemctl restart artifacthub-hub.service

Inspect what is presented today:

echo | openssl s_client -connect 127.0.0.1:443 2>/dev/null | openssl x509 -noout -subject -dates

Expected output:

subject=CN = 4.246.179.114, O = cloudimg Artifact Hub appliance
notBefore=Aug  7 13:10:58 2026 GMT
notAfter=Aug  4 13:10:58 2036 GMT

Step 13 - Check persistence and hardening

The provisioning service runs exactly once and disables itself, so a reboot never regenerates your passwords or wipes your catalogue.

systemctl is-enabled artifact-hub-firstboot.service 2>&1 || true
sudo test -f /var/lib/cloudimg/artifact-hub-firstboot.done && echo "first boot sentinel: present"
sudo test -f /var/lib/cloudimg/artifact-hub.ready && echo "bootstrap ready marker: present"
echo "swap: $(swapon --show | wc -l) entries"
echo "held packages: $(apt-mark showhold | wc -l)"

Expected output:

disabled
first boot sentinel: present
bootstrap ready marker: present
swap: 0 entries
held packages: 0

disabled is correct and deliberate: the unit disabled itself after provisioning this VM. The server and the tracker are both gated on the ready marker, so neither can start against an unprovisioned database.

Unattended security upgrades are on, as they are on stock Ubuntu:

systemctl is-enabled unattended-upgrades.service
grep -h Unattended /etc/apt/apt.conf.d/20auto-upgrades

Expected output:

enabled
APT::Periodic::Unattended-Upgrade "1";

Step 14 - Back up and maintain your VM

Everything that matters lives in PostgreSQL and in /etc/artifacthub. Take an Azure disk snapshot for the whole VM, and a logical dump for the catalogue itself:

sudo -u postgres pg_dump -Fc hub > /var/backups/artifact-hub-$(date +%F).dump
sudo tar czf /var/backups/artifact-hub-config-$(date +%F).tgz /etc/artifacthub /etc/nginx/tls

Keep the credentials file with those backups: it holds the database password the configuration depends on.

To see what the appliance was built from:

sudo cat /usr/share/doc/cloudimg/artifact-hub-build-manifest.txt

Expected output:

Artifact Hub 1.23.0
  upstream tag     v1.23.0
  upstream commit  f90cbb4ee79a31eaa348dbb2868ffa9d2cbe15d8
  LICENSE sha256   cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30  (Apache-2.0)
  hub sha256       62a7713abf9aa31f00a59bf29a9b82d3e8711611de3269fdc7568f17f19f4441
  tracker sha256   f86ffc64e1e377f630f19e07efe3a8ec51543262147f9fc4127cd711f3c6b337
  ah sha256        998cf59797b5c4752738b88b40be35e105ab12e021f14a55cea432d221422ba8
  go               1.26.4
  node             24.19.0
  hugo             0.111.3
  libtensorflow    2.11.0
  opm              v1.60.0
  tern             v1.13.0
  postgresql       16.14(Ubuntu16.14-0ubuntu0.24.04.1)
  schema version   64

Artifact Hub search results listing indexed packages

An Artifact Hub package detail page with its documentation rendered

The Artifact Hub control panel listing the registered repository

Security notes

  • No credential ships in the image. The database role has no password until first boot sets one, there are no accounts at all, and the session cookie key and CSRF key are generated per VM. Upstream's compiled defaults for those keys are the literal strings sample hash key and sample auth key; neither survives into a running VM.
  • The upstream demo account never exists. The schema is migrated with sample data disabled rather than seeded and then cleaned, so there is no window in which demo@artifacthub.io is live.
  • Nothing is served until provisioning succeeds. The server and tracker units are gated on a marker written only after every secret exists and the administrator has been proven able to sign in.
  • PostgreSQL and the metrics endpoint are loopback only. Artifact Hub's Prometheus endpoint is unauthenticated and binds to all interfaces by default upstream; here it is bound to 127.0.0.1 and the firewall never opens it.
  • The catalogue is genuinely yours. The front end calls the API on its own origin. No part of the shipped interface uses artifacthub.io as a backend.
  • Container image vulnerability scanning is not included. It is an optional upstream component with its own Trivy server and database; repositories seeded by this image have scanning switched off so nothing promises a report that will not arrive.
  • Secrets never reach the journal. Provisioning passes every password bearing statement to PostgreSQL on standard input rather than as a command argument, so sudo's command logging and ps never see them.
  • Restrict 22/tcp to your management network, put 443/tcp behind your own certificate, and rotate the administrator password after first sign in.

Architecture summary

Component Detail
Application Artifact Hub 1.23.0, Apache 2.0, CNCF incubating project
Server /opt/artifacthub/bin/hub, artifacthub-hub.service, listening on 127.0.0.1:8000
Indexer /opt/artifacthub/bin/tracker, artifacthub-tracker.timer, every 30 minutes
CLI /usr/local/bin/ah, repository metadata linter
Database PostgreSQL 16 on the same VM, database hub, loopback only
Web front nginx, TLS on 443, permanent redirect from 80
Documentation Served by the VM at /docs, OpenAPI reference at /docs/api
Firewall nftables, default drop, 22/80/443 only
Per VM secrets Database password, cookie hash key, CSRF auth key, administrator password, TLS key
Credentials file /root/artifact-hub-credentials.txt, mode 0600 root:root
Base image Ubuntu 24.04 LTS, fully patched, unattended security upgrades enabled

Support

cloudimg provides 24/7 support for this image. Contact support@cloudimg.co.uk with the offer name, your VM size and region, and the output of systemctl status artifacthub-hub.service and journalctl -u artifact-hub-firstboot.service.