Skip to content
Cloud & Infra Intermediate Tutorial

Build a Private Harbor Registry That Blocks Vulnerable Images

Self-host Harbor 2.15 with Trivy scan-on-push and stop critical-CVE images at pull time.

Ji-ho Choi
Ji-ho Choi
Security & Cloud Editor · Aug 3, 2026 · 5 min read
Build a Private Harbor Registry That Blocks Vulnerable Images

What you'll build

A self-hosted Harbor registry with the Trivy scanner built in, configured so every pushed image is scanned automatically and any image carrying critical CVEs is refused at pull time — meaning your servers and clusters physically can't deploy it.

Prerequisites

Verified against Harbor v2.15.2 (current stable) and the Harbor 2.15 docs.

  • A Linux host (Ubuntu 22.04/24.04 works fine) with 2 CPUs, 4 GB RAM, and 40 GB disk — Harbor's documented minimums. The Harbor server doesn't run on macOS or Windows.
  • Docker Engine newer than 20.10 with the Compose plugin newer than 2.3 (any current Docker install satisfies both).
  • A DNS name or static IP for the host. This tutorial uses registry.lab.example.com — substitute yours everywhere.
  • A second machine (or your laptop) with Docker, acting as the client that pushes and pulls.

We'll run Harbor over plain HTTP to keep the lab self-contained; the Next steps section covers TLS, which you must do before production.

1. Download and extract the installer

On the Harbor host, grab the online installer for v2.15.2:

curl -LO https://github.com/goharbor/harbor/releases/download/v2.15.2/harbor-online-installer-v2.15.2.tgz
tar xzvf harbor-online-installer-v2.15.2.tgz
cd harbor

The online installer pulls Harbor's images from Docker Hub during install. If your host is air-gapped, use the harbor-offline-installer asset from the same release instead — the rest of the steps are identical.

2. Configure harbor.yml

Harbor ships a template you copy and edit:

cp harbor.yml.tmpl harbor.yml

Open harbor.yml and change three things. Set the hostname, set a real admin password, and comment out the entire https block (the installer aborts if https is enabled without certificates):

hostname: registry.lab.example.com

http:
  port: 80

# https:
#   port: 443
#   certificate: /your/certificate/path
#   private_key: /your/private/key/path

harbor_admin_password: S0urceFeed-demo1

Leave data_volume: /data and the rest of the defaults alone. harbor_admin_password only seeds the account on first boot — after that, change it in the UI.

3. Install Harbor with the Trivy scanner

Trivy isn't installed by default; you opt in with a flag:

sudo ./install.sh --with-trivy

The script generates configs, pulls images, and brings everything up with Docker Compose. A successful run ends with:

✔ ----Harbor has been installed and started successfully.----

sudo docker ps should show ten containers, including harbor-core, registry, harbor-db, nginx, and — critically for this tutorial — trivy-adapter. To stop or restart Harbor later, run sudo docker compose down / sudo docker compose up -d from this same harbor/ directory, since that's where the generated docker-compose.yml lives.

4. Create a project with scan-on-push and a blocking policy

Open http://registry.lab.example.com in a browser and log in as admin with the password from harbor.yml.

  1. Go to Projects → New Project, name it demo, leave it private, and create it.
  2. Open the demo project and click the Configuration tab.
  3. Under Vulnerability scanning, check Automatically scan images on push.
  4. Check Prevent vulnerable images from running and set the severity dropdown to Critical — Harbor will then refuse pulls of any image whose scan found CVEs at or above that level.
  5. Click Save.

These policies are per-project, so you can enforce Critical-blocking on production projects while leaving a sandbox project unrestricted.

5. Point Docker at the registry and push images

Because the registry speaks HTTP, the Docker client machine must be told not to demand TLS. Add this to /etc/docker/daemon.json (create the file if it doesn't exist), then restart Docker:

{
  "insecure-registries": ["registry.lab.example.com"]
}
sudo systemctl restart docker
docker login registry.lab.example.com -u admin

Now push two images: a current Alpine (clean) and an old nginx (loaded with known critical CVEs in its 2020-era Debian base):

docker pull alpine:latest
docker tag alpine:latest registry.lab.example.com/demo/alpine:latest
docker push registry.lab.example.com/demo/alpine:latest

docker pull nginx:1.19
docker tag nginx:1.19 registry.lab.example.com/demo/nginx:1.19
docker push registry.lab.example.com/demo/nginx:1.19

The path format matters: it's always host/project/repo:tag, and the project must already exist in Harbor.

Verify it works

In the UI, open Projects → demo → Repositories → demo/nginx. Within a minute or two the artifact's Vulnerabilities column fills in with a severity bar — nginx:1.19 will show dozens of findings including Criticals, while demo/alpine should show none (click an artifact to see the full CVE list with Trivy as the scanner).

Now prove the policy bites. From the client, remove the local copies and pull both back:

docker rmi registry.lab.example.com/demo/alpine:latest registry.lab.example.com/demo/nginx:1.19
docker pull registry.lab.example.com/demo/alpine:latest
docker pull registry.lab.example.com/demo/nginx:1.19

The Alpine pull succeeds normally. The nginx pull is rejected by Harbor with:

Error response from daemon: unknown: current image with 12 vulnerable cannot be pulled due to configured policy in 'Prevent images with vulnerability severity of "Critical" from running.' Please contact your project administrator for help.

(The vulnerability count in the message will vary as Trivy's database updates.) That refusal is the whole point: any Kubernetes node or Docker host pulling from this project gets the same denial, so critical-CVE images can't reach runtime.

Troubleshooting

http: server gave HTTP response to HTTPS client on docker login or push — the client is still demanding TLS. You skipped the insecure-registries entry in /etc/docker/daemon.json, put it on the wrong machine (it goes on the client, not the Harbor host), or forgot sudo systemctl restart docker afterwards.

ERROR:root:Error: The protocol is https but attribute ssl_cert is not set from install.sh — your harbor.yml still has the https block active without certificate paths. Comment out the whole block for HTTP, or fill in certificate and private_key.

denied: requested access to the resource is denied on push — the project segment of your image path doesn't exist in Harbor (e.g., you pushed to registry.lab.example.com/alpine with no project), or the logged-in user lacks push permission on it. Create the project first and tag as host/project/repo:tag.

Pull blocked even though the image looks clean — Harbor blocks pulls while an artifact's scan status is Pending or Error, not just when it found CVEs. Right after a push, wait for the scan to finish; if it shows Error, open the artifact and click Scan to retry.

Next steps

Switch to TLS before exposing this to anything real — follow Configure HTTPS Access to Harbor, then remove the insecure-registries workaround. From there: exempt specific findings with a per-project CVE allowlist so one unfixable CVE doesn't block releases, create robot accounts so CI pushes without human credentials, and schedule a recurring Scan All under Administration → Interrogation Services so old artifacts get re-checked as new CVEs are published — an image that was clean at push time rarely stays that way.

Sources & further reading

  1. Harbor Releases (v2.15.2) — github.com
  2. Harbor Installation Prerequisites — goharbor.io
  3. Configure the Harbor YML File — goharbor.io
  4. Run the Installer Script — goharbor.io
  5. Vulnerability Scanning — goharbor.io
  6. Configure Project Policies — goharbor.io
Ji-ho Choi
Written by
Ji-ho Choi · Security & Cloud Editor

Ji-ho covers the increasingly tangled overlap between cloud architecture and security, drawing on a background as a penetration tester to keep his reporting grounded in real-world attack paths. He never lets a vendor claim go unquestioned and insists that every buzzword come with a proof of concept.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading