Configure Harbor Registry
0. Scope Legend
1. Generic Organization and Repository Inputs
1.1. Harbor Organization and Repository Settings
<<APP_SHORT_FORM>>-ci-cd. Each private GitHub repository becomes a separate Harbor repository beneath that project.2. 🧱 Shared Harbor Infrastructure Inputs
3. 🧮 Computed Isolation Values
<<SERVICE_OR_APP_NAME>><<APP_SHORT_FORM>><<GITHUB_ORGANIZATION>><<APP_SHORT_FORM>>-ci-cd<<APP_SHORT_FORM>>-arc-ghapp-secret~/arc/<<APP_SHORT_FORM>><<APP_SHORT_FORM>>-github-ci-cd-robot-01robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01<<APP_SHORT_FORM>>-runtime-pull-robot-01robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-runtime-pull-robot-01arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>><<APP_SHORT_FORM>>-harbor-regcred<<APP_SHORT_FORM>>-harbor-credentialshttp://harbor.aspireclan.comhttps://harbor.aspireclan.comharbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/actions-runner-azcli:v1.0.0https://github.com/<<GITHUB_ORGANIZATION>>/<<SERVICE_OR_APP_NAME>><<SERVICE_OR_APP_NAME>>-<<ENVIRONMENT>>-arc~/arc/<<APP_SHORT_FORM>>/<<SERVICE_OR_APP_NAME>>-<<ENVIRONMENT>>-values.yamlharbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/<<PRIVATE_REPOSITORY_NAME>>:<<ENVIRONMENT>>-<git-sha>4. 📦 VM Specs
CPU: 4 vCPU
Memory: 8192 MB
Disk: 200 GB thin5. 🖥️ Prepare Base VM Before Templating
Log in to the VM and complete the base OS preparation.
lsblksudo growpart /dev/sda 3
sudo pvresize /dev/sda3
sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv
sudo resize2fs /dev/ubuntu-vg/ubuntu-lv
df -hHDD resize reference: Resize HDD conversation
sudo apt update && sudo apt full-upgrade -ysudo reboot nowsudo apt install -y \
ca-certificates \
curl \
wget \
gnupg \
lsb-release \
jq \
unzip \
zip \
tar \
vim \
nano \
htop \
net-tools \
dnsutils \
openssl \
apt-transport-https \
software-properties-common \
qemu-guest-agentsudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.ascecho \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/nullsudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugindocker --version
docker compose version
sudo systemctl enable docker
sudo systemctl status docker --no-pagersudo usermod -aG docker $USERtimedatectl
sudo timedatectl set-timezone America/New_York
sudo systemctl restart systemd-timesyncd
timedatectlecho 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-harbor.conf
sudo sysctl --systemsudo mkdir -p /srv
sudo mkdir -p /opt
sudo mkdir -p /var/localsudo hostnamectl set-hostname tmpl-ac-harbor-00
hostnamectlsudo nano /etc/hostssudo apt update
sudo apt install -y iputils-pingping -c 4 192.168.8.1
ping -c 4 1.1.1.1
ping -c 4 google.com6. 🧹 Clean the VM Before Templating
sudo apt autoremove -y
sudo apt clean
sudo journalctl --vacuum-time=3d
sudo truncate -s 0 /etc/machine-id
sudo rm -f /var/lib/dbus/machine-id
sudo ln -s /etc/machine-id /var/lib/dbus/machine-idOptional cleanup:
sudo rm -rf /tmp/*
sudo rm -rf /var/tmp/*Then shut it down:
sudo shutdown -h now7. 📀 Create Template
tmpl-ac-harbor-00Create the Proxmox/VM template only after all base configuration and cleanup steps are complete.
8. 🧬 Clone Template for Production Harbor VM
sudo hostnamectl set-hostname ac-harbor-prod-01
hostnamectlSet static IP using: Configure VMs
ac-harbor-prod-01192.168.8.8Add an A record in db.aspireclan.com in production DNS and assign the production proxy IP.
192.168.8.59. 📥 Download and Extract Harbor
sudo mkdir -p /opt/harbor
cd /opt
sudo wget https://github.com/goharbor/harbor/releases/download/v2.15.0/harbor-online-installer-v2.15.0.tgz
sudo tar xzf harbor-online-installer-v2.15.0.tgz
cd /opt/harbor
ls -lasudo mkdir -p /data
sudo chmod 700 /data10. ⚙️ Configure harbor.yml
cd /opt/harbor
sudo cp harbor.yml harbor.yml.bak
sudo cp harbor.yml.tmpl harbor.yml
sudo nano /opt/harbor/harbor.ymlOnly change these kinds of values:
harbor.aspireclan.com<<ENTER_ADMIN_PASSWORD>><<ENTER_DATABASE_PASSWORD>>Comment the complete HTTPS section because TLS terminates at the reverse proxy:
#https:
# # https port for harbor, default is 443
# # port: 443
# # The path of cert and key files for nginx
# # certificate: /your/certificate/path
# # private_key: /your/private/key/path11. 🚀 Install Harbor
cd /opt/harbor
sudo ./install.shdocker pscurl -I http://127.0.0.1
curl -I http://192.168.8.8sudo ufw allow from 192.168.8.5 to any port 80 proto tcp
sudo ufw reload
sudo ufw status12. 🌐 Configure Reverse Proxy
Configure the reverse proxy using: prod-proxy-01 instructions
https://harbor.aspireclan.com13. 🔐 Create the Product-Isolated Harbor Project
Log in to Harbor as an administrator and create one private project for <<SERVICE_OR_APP_NAME>>.
<<APP_SHORT_FORM>>-ci-cd- Public: disabled
- Automatically scan images on push: enabled
- Project quota: set according to available Harbor storage
- Content trust/signature enforcement: introduce later after Cosign is implemented
<service-one> and <service-two> should live beneath <<APP_SHORT_FORM>>-ci-cd. Create a separate project only when a repository requires different administrators, retention, replication, legal/compliance controls, or a stricter trust boundary.14. 🤖 Create the CI Push/Pull Robot
Go to project <<APP_SHORT_FORM>>-ci-cd → Robot Accounts → New Robot Account.
<<APP_SHORT_FORM>>-github-ci-cd-robot-01robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01365 days- Repository: Pull Repository
- Repository: Push Repository
- Do not grant delete, project administration, member management, or robot management.
Export or copy the generated robot secret immediately and save it in NordPass. Harbor cannot display the original secret again; rotate the secret when needed.
15. 🔽 Create the Runtime Pull-Only Robot
Create a second robot account for Kubernetes and deployment-time image pulls.
<<APP_SHORT_FORM>>-runtime-pull-robot-01robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-runtime-pull-robot-01365 days- Repository: Pull Repository only
- Do not grant Push Repository.
- Do not grant delete or project administration permissions.
Save the generated runtime robot secret in NordPass.
16. 🧹 Configure Tag Retention and Immutability
Configure a project-level retention policy that keeps approximately the latest 20 development/build tags per repository. Preserve production releases according to your release and rollback requirements.
- Allow mutable development tags only when the workflow requires them.
- Prefer immutable Git SHA and release tags.
- Apply immutability to production, release, and semantic-version tags.
- Do not make a moving tag such as
latestimmutable unless that is intentional. - Schedule garbage collection only after validating retention behavior.
17. 🔑 Configure GitHub Free Private Repository Secrets
Selected private repositories:
https://github.com/<<GITHUB_ORGANIZATION>>/<<PRIVATE_REPOSITORY_NAME>>Repository-level values:
robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01<<ENTER_CI_ROBOT_SECRET>>harbor.aspireclan.com<<APP_SHORT_FORM>>-ci-cdRun the following from a trusted admin workstation with GitHub CLI authenticated to .
gh auth status
read -s -p "Harbor CI robot secret: " HARBOR_CI_PASSWORD
echo
HARBOR_CI_USERNAME='robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01'
HARBOR_REGISTRY='harbor.aspireclan.com'
HARBOR_PROJECT='<<APP_SHORT_FORM>>-ci-cd'
REPOSITORIES=(
"<<ENTER_PRIVATE_REPOSITORY_NAME>>"
)
for REPOSITORY in "${REPOSITORIES[@]}"; do
FULL_REPOSITORY='<<GITHUB_ORGANIZATION>>/'"${REPOSITORY}"
echo "Configuring ${FULL_REPOSITORY}..."
printf '%s' "${HARBOR_CI_USERNAME}" | \
gh secret set HARBOR_USERNAME --repo "${FULL_REPOSITORY}"
printf '%s' "${HARBOR_CI_PASSWORD}" | \
gh secret set HARBOR_PASSWORD --repo "${FULL_REPOSITORY}"
gh variable set HARBOR_REGISTRY \
--repo "${FULL_REPOSITORY}" \
--body "${HARBOR_REGISTRY}"
gh variable set HARBOR_PROJECT \
--repo "${FULL_REPOSITORY}" \
--body "${HARBOR_PROJECT}"
gh secret list --repo "${FULL_REPOSITORY}"
gh variable list --repo "${FULL_REPOSITORY}"
echo
done
unset HARBOR_CI_PASSWORD
unset HARBOR_CI_USERNAME
unset HARBOR_REGISTRY
unset HARBOR_PROJECT18. ☸️ Create Product-Isolated Kubernetes Harbor Secrets
Create the pull-only Docker registry secret in the ARC runner namespace. This secret is used by imagePullSecrets.
read -s -p "Harbor runtime pull-only robot secret: " HARBOR_RUNTIME_PASSWORD
echo
kubectl create namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> \
--dry-run=client \
-o yaml | kubectl apply -f -
kubectl create secret docker-registry <<APP_SHORT_FORM>>-harbor-regcred \
--namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> \
--docker-server='harbor.aspireclan.com' \
--docker-username='robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-runtime-pull-robot-01' \
--docker-password="${HARBOR_RUNTIME_PASSWORD}" \
--docker-email='noreply@aspireclan.com' \
--dry-run=client \
-o yaml | kubectl apply -f -
unset HARBOR_RUNTIME_PASSWORD
kubectl get secret <<APP_SHORT_FORM>>-harbor-regcred \
--namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>Create the CI push/pull credentials secret separately. The runner container can expose these values to workflows that must authenticate to Harbor.
read -s -p "Harbor CI push/pull robot secret: " HARBOR_CI_PASSWORD
echo
kubectl create secret generic <<APP_SHORT_FORM>>-harbor-credentials \
--namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> \
--from-literal=HARBOR_USERNAME='robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01' \
--from-literal=HARBOR_PASSWORD="${HARBOR_CI_PASSWORD}" \
--dry-run=client \
-o yaml | kubectl apply -f -
unset HARBOR_CI_PASSWORD
kubectl get secret <<APP_SHORT_FORM>>-harbor-credentials \
--namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>><<APP_SHORT_FORM>>-harbor-regcred for pull-only image access and <<APP_SHORT_FORM>>-harbor-credentials for CI push/pull credentials. Never reuse one GitHub organization's Harbor credentials in another organization's runner namespace.19. 🧪 Build the Product Runner Image on the Build VM
Log in to the build VM.
prod-build-02mkdir -p ~/arc-runner-azcli/<<APP_SHORT_FORM>>
cd ~/arc-runner-azcli/<<APP_SHORT_FORM>>Create a Dockerfile with the following content:
FROM ghcr.io/actions/actions-runner:latest
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates curl apt-transport-https lsb-release gnupg \
&& curl -sL https://aka.ms/InstallAzureCLIDeb | bash \
&& az version \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
USER runnerdocker build -t harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/actions-runner-azcli:v1.0.0 .Log in with the selected product's CI robot:
docker login harbor.aspireclan.com -u 'robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01'Paste the CI robot secret when prompted.
Push the image:
docker push harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/actions-runner-azcli:v1.0.020. 🏷️ Application Image Naming Convention
Store every service image from beneath the selected product project. Use the GitHub repository name as the Harbor repository name.
# Recommended immutable image:
harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/<github-repository-name>:<environment>-<git-sha>
# Example:
harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/<<PRIVATE_REPOSITORY_NAME>>:<<ENVIRONMENT>>-<git-sha>
# Optional human-friendly release tag:
harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/<github-repository-name>:v1.2.321. ✅ Final Isolation Verification
echo "Service / App: <<SERVICE_OR_APP_NAME>>"
echo "GitHub organization: <<GITHUB_ORGANIZATION>>"
echo "Harbor project: <<APP_SHORT_FORM>>-ci-cd"
echo "ARC namespace: arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>"
echo "Kubernetes pull secret: <<APP_SHORT_FORM>>-harbor-regcred"
echo "Kubernetes CI secret: <<APP_SHORT_FORM>>-harbor-credentials"
echo "CI robot: robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01"
echo "Runtime robot: robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-runtime-pull-robot-01"
kubectl get secrets \
--namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> \
| grep -E '<<APP_SHORT_FORM>>-harbor-regcred|<<APP_SHORT_FORM>>-harbor-credentials'- The Harbor project is private.
- The CI robot belongs only to the selected product project.
- The runtime robot has pull-only permission.
- Every private GitHub repository has repository-level Harbor secrets and variables.
- No workflow depends on GitHub Free organization-level secrets or private-repository environment secrets.
- The ARC namespace and Kubernetes secrets use the selected app short form.