4 min read

Securing Your Software Supply Chain with harbor

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:

  1. ✅ Pull once, verify always – Mirror third-party images into our internal Harbor.
  2. 🔏 Sign with confidence – Use Cosign to cryptographically sign images and enforce trust.
  3. 🧪 Scan like a hawk – Automatically check every image for known CVEs.
  4. 🕹 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