Securing Your Software Supply Chain with harbor

Securing Your Software Supply Chain: A Practical Approach
In today’s cloud-native world, your container supply chain is only as secure as its weakest image. Pulling third-party containers from public registries like Docker Hub may be convenient—but without proper controls, you risk introducing outdated, vulnerable, or even malicious software into your infrastructure.
To lock this down, we’ll use:
- 🎯 Harbor – Your private, policy-driven container registry.
- 🔐 Cosign – To sign and verify container images.
- 🛡 Vulnerability scanning – To stop insecure images before they ship.
- ⚙️ Terraform – To define all configurations as code with minimal manual work.
This tutorial will show you how to build a secure, automated image pipeline that helps stop supply chain attacks before they happen.
Architecture Overview: Secure by Default
We’re aiming for four simple but powerful principles:
- ✅ Pull once, verify always – Mirror third-party images into our internal Harbor.
- 🔏 Sign with confidence – Use Cosign to cryptographically sign images and enforce trust.
- 🧪 Scan like a hawk – Automatically check every image for known CVEs.
- 🕹 Enforce with policy – Prevent unsigned or vulnerable images from running on Kubernetes.
Using Harbor’s built-in features and a few Terraform files, you can enforce all of this without maintaining a massive amount of tooling or administrative overhead.
1. Setting up Environment Variables and Providers
To securely store your Harbor username and password, we will use Terraform environment variables TF_VAR_harbor_username
and TF_VAR_harbor_password
.
export TF_VAR_harbor_username="your_username"
export TF_VAR_harbor_password="your_password"
Now, let's configure the Harbor provider and define variables for better management in Terraform.
provider.tf
- Configure Harbor Provider
# providers.terraform
terraform {
required_providers {
harbor = {
source = "goharbor/harbor"
version = "3.10.15" # Specify the latest version
}
}
}
provider "harbor" {
url = "https://harbor.example.com"
username = var.harbor_username
password = var.harbor_password
}
Then run terraform init
2. Automating Harbor Project Setup with Terraform
Now let’s automate creation of a Harbor project with all security features enabled. We’ll include:
✅ vulnerability scanning
✅ Cosign-based content trust
✅ auto SBOM generation
✅ high deployment security
✅ dynamic CVE allowlist
📄 harbor.tf
resource "harbor_project" "secure_project" {
name = var.project_name
public = false
vulnerability_scanning = true
enable_content_trust = true
enable_content_trust_cosign = true
auto_sbom_generation = true
deployment_security = "high"
cve_allowlist = [for item in var.cve_allowlist : item.cve_id]
}
📄 vars.tf
- To be able to dynamically be able to add more configuration
# vars.tf
variable "project_name" {
description = "The name of the Harbor project"
type = string
default = "my-secure-project"
}
variable "harbor_username" {
description = "Admin username"
type = string
}
variable "harbor_password" {
description = "Admin password"
type = string
sensitive = true
}
variable "cve_allowlist" {
description = "List of CVEs to allow, with optional expiry"
type = list(object({
cve_id = string
expires_at = string # ISO8601 format e.g. "2025-12-31T23:59:59Z"
}))
default = [
{
cve_id = "CVE-2023-0001"
expires_at = "2025-12-31T23:59:59Z"
},
{
cve_id = "CVE-2023-0002"
expires_at = "2026-01-01T00:00:00Z"
}
]
}
Run the apply:
terraform plan
terraform apply
You'll see an output like this
Terraform will perform the following actions:
# harbor_project.secure_project will be created
+ resource "harbor_project" "secure_project" {
+ auto_sbom_generation = true
+ cve_allowlist = [
+ "CVE-2023-0001",
+ "CVE-2023-0002",
]
+ deployment_security = "high"
+ enable_content_trust = true
+ enable_content_trust_cosign = true
+ force_destroy = false
+ id = (known after apply)
+ name = "my-secure-project"
+ project_id = (known after apply)
+ public = false
+ registry_id = (known after apply)
+ storage_quota = -1
+ vulnerability_scanning = true
}
Plan: 1 to add, 0 to change, 0 to destroy.
3. Connect External Registries (like Docker Hub)
To control and scan 3rd party images, mirror them into Harbor. Let’s register Docker Hub:
📄 Add to harbor.tf
:
# harbor.tf
resource "harbor_registry" "external" {
for_each = { for reg in var.external_registries : reg.name => reg }
name = each.value.name
endpoint_url = each.value.endpoint
provider_name = each.value.provider
}
📄 Add to vars.tf
:
# vars.tf
variable "external_registries" {
description = "List of external registries to connect"
type = list(object({
name = string
endpoint = string
provider = string # e.g. "docker-hub", "harbor"
access_id = string
access_secret = string
}))
default = [
{
name = "dockerhub"
endpoint = "https://hub.docker.com"
provider = "docker-hub"
access_id = ""
access_secret = "1234"
}
]
}
terraform apply
The results should look like
Terraform will perform the following actions:
# harbor_registry.external["dockerhub"] will be created
+ resource "harbor_registry" "external" {
+ endpoint_url = "https://hub.docker.com"
+ id = (known after apply)
+ insecure = false
+ name = "dockerhub"
+ provider_name = "docker-hub"
+ registry_id = (known after apply)
+ status = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Now in Harbor under regestries tab you'll notice dockerhub with healthy tag.
3. Add Images dynamically
Adding a list of images you would like to add to replicate.
# vars.tf
variable "external_repositories" {
description = "List of repositories to pull manually from each external registry"
type = list(object({
registry_name = string
name = string
tag = string
labels = list(string) # optional
}))
default = [
{
registry_name = "dockerhub"
name = "postgres"
tag = "15\\.[1-9][0-9]*"
labels = []
},
{
registry_name = "dockerhub"
name = "keycloak/keycloak"
tag = "22\\.[1-9][0-9]*"
labels = []
}
]
}
And then
# harbor.tf
resource "harbor_replication" "external_manual_pull" {
for_each = {
for repo in var.external_repositories :
"${repo.registry_name}-${repo.name}" => repo
}
name = "pull-${each.value.registry_name}-${replace(each.value.name, "/", "-")}"
action = "pull"
registry_id = harbor_registry.external[each.value.registry_name].registry_id
schedule = "manual"
dest_namespace = var.project_name
filters {
name = each.value.name
}
filters {
tag = each.value.tag
}
# Include labels filter only if labels are provided
filters {
resource = "artifact"
}
override = true
}
terraform apply
And now you can navigate to the replication tab and see your replications there, for this tutorial we've set it to manual so you can click the replicate button to pull new images but this can easily be set on a schedule and automated.
4. Kubernetes: Enforce Only Trusted Images
Finally, let’s write a Kubernetes policy to enforce that only images pulled from our Harbor instance, and signed with Cosign, are allowed.
📄 Example ClusterImagePolicy:
apiVersion: policy.sigstore.dev/v1alpha1
kind: ClusterImagePolicy
metadata:
name: allow-harbor-only
spec:
images:
- glob: "harbor.example.com/**"
authorities:
- keyless: {}
TL;DR – Minimal Admin, Maximum Security
✔ Sign all images using Harbor and Cosign
✔ Mirror and verify all 3rd party content using Harbor replication
✔ Scan EVERYTHING – automated CVE detection built-in
✔ Enforce Kubernetes policies that only accept signed images
✔ No secret management in K8s – let Harbor handle signatures and trust!
What You’ve Achieved
We’ve just built a secure, automated, and scalable container image pipeline:
✅ Secure access to Harbor using Terraform environment variables
✅ Created a Harbor project with content trust, auto signing, SBOM, and CVE scanning
✅ Connected Harbor to Docker Hub as a source registry
✅ Defined and replicated specific external images securely
✅ Set Kubernetes to accept only signed images from your registry
Member discussion