Installing Harbor on Kubernetes with Project Contour, Cert Manager, and Let’s Encrypt
Tony Vetter & Paul Czarkowski
Running a private container image registry has been a staple in many enterprise environments for years. These registries allow full control over access, updates, and the software platform itself. And while your organization may have an official image registry, having your own can also be a benefit!
Maybe it’s just for learning. Or maybe you like the idea of self-hosting your own services. Or maybe, like so many companies that have also made the decision to self-host, you are concerned about security and access. Whatever the reason, you have made the decision to deploy your own. But this process includes a lot more than just an initial install.
A container image registry needs to be accessible to many online services to be useful, not the least of which is your desktop. It’s what makes pulling and pushing images possible. And you probably want it to be accessible from outside your own network, too, so that you can collaborate and share your projects. These days, to be secure, this requires TLS encryption to enable HTTPS traffic.
In this guide, you will deploy Harbor to Kubernetes as the actual image registry application. You will also use Project Contour to manage ingress to your Kubernetes cluster.
Harbor and Contour
Harbor is a powerful registry for containers and Helm charts. It is fully open source and backed by the Cloud Native Computing Foundation (CNCF). But getting it up and running, with automated TLS certificate renewal in particular, can be a challenge—especially with the multiple services Harbor uses that require east-west and north-south network communication.
Contour, also a CNCF project, is an ingress controller built for Kubernetes. It functions as a control plane for Envoy while also offering advanced routing functionality beyond the default ingress controller provided by Kubernetes.
You will deploy Harbor and Contour, and use cert-manager and Let’s Encrypt to automate TLS certificate generation and renewal for your Harbor installation. This will allow you to keep your Harbor installation up and running and utilizing HTTPS without manually generating and applying new certificates.
Also, using the patterns in this guide, you should be able to deploy other services to Kubernetes and secure their ingress as well. And once you understand the basics of certificate management, ingress, and routing services, you’ll be able to keep on deploying other services to Kubernetes and enabling secure access over the internet!
Prerequisites
Before you get started, you’ll need to do the following:
-
Install Helm 3: Helm is used in this guide to install Contour and Harbor. A guide for installing Helm can be found here.
-
Create a Kubernetes cluster: This guide was built using Google Kubernetes Engine (GKE) with Kubernetes version 1.17. Any Kubernetes cluster with access to the internet should work fine, but your results may vary depending on the version you use. Initial testing with 1.16 resulted in errors during the Harbor install. For a guide on creating a GKE cluster, see this page from Google. Ensure your
kubectl
context is using this cluster. -
Install watch: watch is a small command-line utility that continually shows the output of a command being run. This allows you to monitor
kubectl get pods
, for example, without explicitly re-running the command multiple times. Instructions for installing on macOS are here. -
About 30 minutes: It could take more time than that; it will depend on how long the Let’s Encrypt servers take to issue certificates. But in most of my testing for writing this post, it took about 30 minutes.
-
Optional - buy a domain name: You can use
.xip.io
(a service that provides dynamic DNS based on IP address) addresses to avoid needing to buy a domain. Otherwise you will need a domain name that you control in order to configure DNS. This guide uses a Google-managed domain and DNS zone, but instructions can be modified for other providers. Note: if you do decide to use.xip.io
, you may experience issues using theletsencrypt-prod
ClusterIssuer later in the demo due to rate limiting. Often you can just wait a few hours and it’ll eventually work. For best results, we recommend using your own domain.
Prepare the Environment
Create a Kubernetes cluster. In GKE this can be as simple as running:
gcloud container clusters create jan8 --num-nodes 3
Being organized is key to any successful project. So before you dig in, create a
new project directory and cd
into it.
mkdir harbor-install && cd $_
Install Project Contour
While you are going to use Helm to install Project Contour, Helm will not create the namespace for you. So the first step in this installation is to create that namespace.
$ kubectl create namespace projectcontour
If you do not have the Bitnami repo referenced in Helm yet (you can check by
running helm repo list
), you will need to install it.
helm repo add bitnami https://charts.bitnami.com/bitnami
You will also need to update your Helm repositories.
helm repo update
Next, run helm install
to finish the installation. Here you will install
Bitnami’s image for Contour. This chart includes defaults that will work out of
the box for this guide. And since it comes from Bitnami, you can trust that the
image has been thoroughly tested and scanned.
helm install ingress bitnami/contour -n projectcontour --version 3.3.1
Now simply wait for the Pods to become READY
.
watch kubectl get pods -n projectcontour
Then define some environment variables for your proposed Harbor domain and your email address. It is recommended that you use a subdomain under the domain procured in the prerequisites section (e.g., harbor.example.com). This way you can use other subdomain URLs to access other services you may deploy in the future. The email address will be used for your Let’s Encrypt certificate so the cert authority can notify you about expirations.
Defining these variables will make the rest of the commands in this guide more simple to run. The email address and the domain do not have to use the same domain (a Gmail address is fine).
If you do not have a custom domain run:
IP=$(kubectl describe svc ingress-contour-envoy --namespace projectcontour | grep Ingress | awk '{print $3}')
export DOMAIN=$IP.xip.io
Otherwise run:
export DOMAIN=your.domain.com
Set your email address for cert-manager:
export EMAIL_ADDRESS=username@example.com
Set Up DNS
This section is not applicable for those using xip.io
.
If that’s you, please skip this entire section.
In this section, things can vary a bit. Because this guide was written using Google Cloud Platform (GCP) tooling, the instructions below will reflect that. But should you be using another provider, I will try to provide a generalized description of what is being done so you can look up those specific steps. I have also numbered these steps for clarity.
- In order to set up DNS records, you need the IP address of the Envoy ingress
router installed by Project Contour. Use the following command to describe
the service. If the
EXTERNAL-IP
is still in a<pending>
state, just wait until one is assigned. Record this value for a future step.
watch kubectl get service ingress-contour-envoy -n projectcontour -o wide
- Now set up a DNS zone within your cloud provider.
-
For GCP, this can be found in the GCP web UI, under Networking Services, Cloud DNS. Click
Create Zone
and follow the instructions to give the zone a descriptive name, as well as provide the DNS name. The DNS name will be whatever domain you have registered (e.g.,example.com
). -
For AWS, this is done via a service called Route 53, and for Azure, this is done by creating a resource in the Azure Portal.
-
Once completed, the important part is that you now have a list of name servers. For example, one or more of the format
ns-cloud-x1.googledomains.com.
Record these for a future step.
- Next, add an A record to your DNS zone for a wildcard (
*
) subdomain. An A record is an “Address” record, one of the most fundamental types of DNS records; it maps the more user-friendly URL (harbor.example.com) to an IP address. Setting this up as a wildcard will allow you to configure any traffic coming into any subdomain to first be resolved by Project Contour and Envoy, then be routed to its final destination based on the specific subdomain requested.
-
For GCP, click into the DNS zone you just created and select
Add Record Set
. From here, add a*
as the DNS name field so the full DNS name reads*.example.com
. In the IPv4 address field, enter theEXTERNAL_IP
of the Envoy service recorded earlier in Step 1. Then click create. -
For AWS and Azure, this is done via the respective services listed in the previous step.
-
Once completed, your DNS zone should have an A record set up for any subdomain (
*
) to be mapped to the IP address of the Envoy service running in Kubernetes.
- For the last of the somewhat confusing UI-based steps, you need to add the list of name servers from Step 2 to your personal domain. This will allow any HTTP/HTTPS requests for your domain to reference the records you set up previously in your DNS zone.
-
For Google-managed domains, in the Google Domains UI, click
manage
next to the domain you want to modify. Then click onDNS
. In the Name Servers section at the top, clickedit
. Now, one at a time, simply paste in the list of name servers recorded in Step 2. There should be four name server addresses. Then clickSave
. -
For other domain name registrars, the process is similar. Contact your registrar if you require further assistance with this process.
That’s it for configuring DNS. Now you need to wait for all the new records to propagate. Run the following command and wait for the output to show that your domain is now referencing the IP address of the Envoy service. This could take a few minutes.
watch host $DOMAIN
Install cert-manager
Cert-manager will automate certificate renewal for your services behind Project Contour. When a certificate is set to expire, cert-manager will automatically request a new one from the certificate issuer (you will set this up in the next section). This is especially important when using a project like Let’s Encrypt, whose certificates are only valid for 90 days.
To install cert-manager, this guide uses Helm, for uniformity. As with installing Project Contour, Helm will not create a namespace, so the first step is to create one.
kubectl create namespace cert-manager
If you do not have the Jetstack repo referenced in Helm yet (you can check by
running helm repo list
), you will need to install that reference.
helm repo add jetstack https://charts.jetstack.io
Once again, update your Helm repositories.
helm repo update
Next, run helm install
to install cert-manager.
helm install cert-manager jetstack/cert-manager --namespace cert-manager \
--version v1.0.2 --set installCRDs=true
Finally, wait for the Pods to become READY
before moving on to the next step.
This should only take a minute or so.
watch kubectl get pods -n cert-manager
Set up Let’s Encrypt Staging Certificates
When initially setting up your Harbor service, or any service that will be using Let’s Encrypt, it is important to start by using certificates from their staging server as opposed to the production server. Staging certificates allow you to test your service without the risk of running up against any API rate limiting, which Let’s Encrypt imposes on its production environment. This is not a requirement, however; production certificates can be used initially. But using the staging ones is a good habit to get into, and will highlight how these certificates are applied.
Create the deployment YAML for the staging certificate by pasting the block below into a new file. Once applied, this will set up the staging cert configuration along with your email address, for certificate expirations notifications. You won’t need to worry about these emails, however, as cert-manager will take care of the renewal for you.
Notice that this is of kind: ClusterIssuer
. That means this certificate issuer
is scoped to all namespaces in this Kubernetes cluster. For more granular
controls in production, you may decide to simply change this to kind: Issuer
,
which will be scoped to a specific namespace and only allow services in that
namespace to request certificates from that issuer. But be aware that this will
necessitate changing other configuration options throughout this guide. We are
using ClusterIssuer
because it is secure enough for our use case, and
presumably you are not using a multi-tenant Kubernetes cluster when following
this guide.
Finally, this sets up a “challenge record" in the solvers
section, which
allows Let’s Encrypt to verify that the certificate it is issuing is really
controlled by you.
cat << EOF > letsencrypt-staging.yaml
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
email: $EMAIL_ADDRESS
privateKeySecretRef:
name: letsencrypt-staging
server: https://acme-staging-v02.api.letsencrypt.org/directory
solvers:
- http01:
ingress:
class: contour
EOF
Now apply
the file.
kubectl apply -f letsencrypt-staging.yaml
The process of creating the cluster issuer should be fairly quick, but you can confirm it completed successfully by running the following and ensuring the cluster issuer was issued.
$ kubectl get clusterissuers.cert-manager.io
NAME READY AGE
letsencrypt-staging True 74s
Install Harbor
Now that you have your staging certificate, you can install Harbor, for which this guide uses Helm in combination with the Bitnami repo set up earlier. As with your other install steps, the first thing to do is create the namespace.
kubectl create namespace harbor
Next, create the values file. The Bitnami Helm chart includes defaults that, for the most part, will work for your needs. However, there are a few configuration options that need to be set.
Here you will notice that you are giving the TLS secret in Kubernetes a name,
harbor-tls-staging
. You can choose any name you like, but it should be
descriptive and reflect that this secret will be distinct from the production
certificate you will apply later.
You are also setting up references to your domain so Harbor and Contour can set
up routing. The Annotations section is important as it tells Harbor about our
configuration. Notice that for
cert-manager.io/cluster-issuer: letsencrypt-staging
you are telling Harbor to
use the ClusterIssuer
called letsencrypt-staging, the one you set up earlier.
This will come up again later when you move to production certificates. Comments
are provided in the file for further detail.
Finally, this values file will disable the Harbor Notary service. At the time of
this writing there is a bug in the Bitnami Helm chart (already reported) that
doesn’t allow a TLS certificate to be applied for both notary.$DOMAIN
and
registry.$DOMAIN
. I will try to update this post once that bug is fixed.
cat << EOF > harbor-values.yaml
harborAdminPassword: Password12345
service:
type: ClusterIP
tls:
enabled: true
existingSecret: harbor-tls-staging
notaryExistingSecret: notary-tls-staging
ingress:
enabled: true
hosts:
core: registry.$DOMAIN
notary: notary.$DOMAIN
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging # use letsencrypt-staging as the cluster issuer for TLS certs
ingress.kubernetes.io/force-ssl-redirect: "true" # force https, even if http is requested
kubernetes.io/ingress.class: contour # using Contour for ingress
kubernetes.io/tls-acme: "true" # using ACME certificates for TLS
externalURL: https://registry.$DOMAIN
portal:
tls:
existingSecret: harbor-tls-staging
EOF
Now install Harbor using this values file.
helm install harbor bitnami/harbor -f harbor-values.yaml -n harbor --version 9.4.4
And wait for the Pods to become <READY>
. This may take a minute or two.
watch kubectl get pods -n harbor
Finally, ensure that the certificates were requested and returned successfully. This should happen fairly quickly, but may take up to an hour. It all depends on the server load at that time.
$ kubectl -n harbor get certificate
NAME READY SECRET AGE
harbor-tls-staging True harbor-tls-staging 2m26s
notary-tls-staging True notary-tls-staging 2m26s
Print out the URL, username, and password:
cat << EOF
url: https://registry.$DOMAIN
username: admin
password: $(kubectl get secret --namespace harbor harbor-core-envvars -o jsonpath="{.data.HARBOR_ADMIN_PASSWORD}" | base64 --decode)
EOF
Now open your browser of choice and go to your URL. You will notice that you will need to accept the security warning that the site is “untrusted.” This is because you are still using the staging certificates, which are not signed by a trusted certificate authority (CA).
Once you ensure that you can log in and that Harbor is working as intended, you can move to production certificates.
Set Up Let’s Encrypt Production Certificates
Reminder, if using .xip.io
you may encounter rate limit issues with Lets
Encrypt causing major delays in the certificate issuance.
Similar to how you set up the Let’s Encrypt staging certificate, you now need to
create the ClusterIssuer
for production certificates. First, echo
the
following to create the file.
cat << EOF > letsencrypt-prod.yaml
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: $EMAIL_ADDRESS
privateKeySecretRef:
name: letsencrypt-prod
server: https://acme-v02.api.letsencrypt.org/directory
solvers:
- http01:
ingress:
class: contour
EOF
Then apply it to your Kubernetes cluster.
kubectl apply -f letsencrypt-prod.yaml
Again, you can confirm this process completed successfully by running the following and ensuring the cluster issuer was issued.
kubectl get clusterissuers
Recall how earlier in the annotations of the Harbor values.yml
file you told
Harbor to use the letsencrypt-staging
cluster issuer, as well as the secret
harbor-tls-staging
. You must now tell Harbor to use the production cluster
issuer you just created, and trigger it to create a new secret based on that
certificate. To do this, you are going to update the harbor-values.yaml
file
using sed
:
sed -i 's/-staging/-prod/' harbor-values.yaml
Run helm delete
then helm install
to uninstall and reinstall Harbor:
Note: The persistent volumes are not deleted during the helm delete
so any
changes in Harbor you may have made should persist.
helm delete -n harbor harbor
helm install harbor bitnami/harbor -f harbor-values.yaml -n harbor --version 9.4.4
Wait for the new Pods to become <READY>
. This may take a minute or two.
watch kubectl get pods -n harbor
This process may take as little as a minute or as long as an hour. It just depends on the server load at that time. But in most of my testing, it took less than 10 minutes.
watch kubectl get certificate harbor-tls-prod -n harbor
Once the certificates are generated successfully, your Harbor instance should be up and running with valid and trusted TLS certificates. Try logging into Harbor again.
Your browser should no longer present a warning, as the certificate you are now using is signed by a trusted CA.
Note: If you still see certificate warnings, you may need to re-open it from a fresh browser.
Test Harbor
Run docker login
to log into your new registry:
docker login https://registry.$DOMAIN
Push an image to the new registry:
docker pull nginx:latest
docker tag nginx:latest registry.$DOMAIN/library/nginx:latest
docker push registry.$DOMAIN/library/nginx:latest
Test that Kubernetes can access the new image:
kubectl create deployment nginx --image=registry.$DOMAIN/library/nginx:latest
Wait a few moments and check that the Pod is running:
$ NAME READY STATUS RESTARTS AGE
pod/nginx-7cdffc88cf-p8v9r 1/1 Running 0 5s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.123.240.1 <none> 443/TCP 166m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nginx 1/1 1 1 5s
NAME DESIRED CURRENT READY AGE
replicaset.apps/nginx-7cdffc88cf 1 1 1 5s
Summary
That’s it! You now have a service running in Kubernetes with TLS encryption enforced and certificate generation automated. And by using these patterns, you should be able to install and configure other services as well. Specific steps, especially around the configuration of the service itself, will be different, but after using this guide you should have a leg up in getting started.