Using multiple SSL certificates in HTTP(S) load balancing with Ingress

This page shows you how to use multiple SSL certificates for Ingress with Internal and External load balancing.

Overview

If you want to accept HTTPS requests from your clients, the Internal or External HTTP(S) load balancer must have a certificate so it can prove its identity to your clients. The load balancer must also have a private key to complete the HTTPS handshake.

When the load balancer accepts an HTTPS request from a client, the traffic between the client and the load balancer is encrypted using TLS. However, the load balancer terminates the TLS encryption, and forwards the request without encryption to the application. When you configure an HTTP(S) load balancer through Ingress, you can configure the load balancer to present up to ten TLS certificates to the client.

The load balancer uses Server Name Indication (SNI) to determine which certificate to present to the client, based on the domain name in the TLS handshake. If the client does not use SNI, or if the client uses a domain name that does not match the Common Name (CN) in one of the certificates, the load balancer uses the first certificate listed in the Ingress. The following diagram depicts the load balancer sending traffic to different backends, depending on the domain name used in the request:

multiple SSL certificates with Ingress system diagram

You can provide an HTTPS load balancer with SSL certificates using one of three methods:

  • Google-managed SSL certificates. Refer to the managed certificates page for information on how to use them.

  • Google Cloud SSL Certificate that you are managing yourself. It uses a pre-shared certificate previously uploaded to your Google Cloud project.

  • Kubernetes Secrets. The Secret holds a certificate and key that you create yourself. To use a Secret, add its name in the tls field of your Ingress manifest.

You can use more than one method in the same Ingress. This allows for no-downtime migrations between methods.

Minimum GKE version

You must have GKE version 1.10.2 or later to use pre-shared certificates or to specify multiple certificates in an Ingress.

The big picture

Here's an overview of the steps in this topic:

  1. Create a Deployment

  2. Create a Service

  3. Create two certificate files and two key files.

  4. Create an Ingress that uses either Secrets or pre-shared certificates. As a result of creating the Ingress, GKE creates and configures an HTTP(S) load balancer.

  5. Test the HTTP(S) load balancer.

Before you begin

Before you start, make sure you have performed the following tasks:

Set up default gcloud settings using one of the following methods:

  • Using gcloud init, if you want to be walked through setting defaults.
  • Using gcloud config, to individually set your project ID, zone, and region.

Using gcloud init

  1. Run gcloud init and follow the directions:

    gcloud init

    If you are using SSH on a remote server, use the --console-only flag to prevent the command from launching a browser:

    gcloud init --console-only
  2. Follow the instructions to authorize gcloud to use your Google Cloud account.
  3. Create a new configuration or select an existing one.
  4. Choose a Google Cloud project.
  5. Choose a default Compute Engine zone.

Using gcloud config

  • Set your default project ID:
    gcloud config set project project-id
  • If you are working with zonal clusters, set your default compute zone:
    gcloud config set compute/zone compute-zone
  • If you are working with regional clusters, set your default compute region:
    gcloud config set compute/region compute-region
  • Update gcloud to the latest version:
    gcloud components update

Creating a Deployment

Here is a manifest for a Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-mc-deployment
spec:
  selector:
    matchLabels:
      app: products
      department: sales
  replicas: 3
  template:
    metadata:
      labels:
        app: products
        department: sales
    spec:
      containers:
      - name: hello
        image: "gcr.io/google-samples/hello-app:2.0"
        env:
        - name: "PORT"
          value: "50001"
      - name: hello-again
        image: "gcr.io/google-samples/node-hello:1.0"
        env:
        - name: "PORT"
          value: "50002"

The Deployment has three Pods, and each Pod has two containers. One container runs hello-app and listens on TCP port 50001. The other container runs node-hello and listens on TCP port 50002.

Copy the manifest to a file named my-mc-deployment.yaml, and create the Deployment:

kubectl apply -f my-mc-deployment.yaml

Creating a Service

Here is a manifest for a Service:

apiVersion: v1
kind: Service
metadata:
  name: my-mc-service
spec:
  type: NodePort
  selector:
    app: products
    department: sales
  ports:
  - name: my-first-port
    protocol: TCP
    port: 60001
    targetPort: 50001
  - name: my-second-port
    protocol: TCP
    port: 60002
    targetPort: 50002

The selector field in the Service manifest says any Pod that has the app: products label and the department: sales label is a member of this Service. So the Pods of the Deployment you created in the preceding step are members of the Service.

The ports field of the Service manifest is an array of ServicePort objects. When a client sends a request to the Service on my-first-port, the request is forwarded to one of the member Pods on port 50001. When a client sends a request to the Service on my-second-port, the request is forwarded to one of the member Pods on port 50002.

Copy the manifest to a file named my-mc-service.yaml, and create the Service:

kubectl apply -f my-mc-service.yaml

Creating certificates and keys

To do the exercises on this page, you need two certificates, each with a corresponding key. Each certificate must have a Common Name (CN) that is equal to a domain name that you own. If you already have two certificate files with the appropriate values for Common Name, you can skip ahead to the next section.

Create your first key:

openssl genrsa -out test-ingress-1.key 2048

Create your first certificate signing request:

openssl req -new -key test-ingress-1.key -out test-ingress-1.csr \
    -subj "/CN=first-domain"

where first-domain is a domain name that you own or a fake domain name.

For example, suppose you want the load balancer to serve requests from the your-store.example domain. Your certificate signing request would look like this:

openssl req -new -key test-ingress-1.key -out test-ingress-1.csr \
    -subj "/CN=your-store.example"

Create your first certificate:

openssl x509 -req -days 365 -in test-ingress-1.csr -signkey test-ingress-1.key \
    -out test-ingress-1.crt

Create your second key:

openssl genrsa -out test-ingress-2.key 2048

Create your second certificate signing request:

openssl req -new -key test-ingress-2.key -out test-ingress-2.csr \
    -subj "/CN=second-domain"

where second-domain is another domain name that you own or a fake domain name.

For example, suppose you want the load balancer to serve requests from the your-experimental-store.example domain. Your certificate signing request would look like this:

openssl req -new -key test-ingress-2.key -out test-ingress-2.csr \
    -subj "/CN=your-experimental-store.example"

Create your second certificate:

openssl x509 -req -days 365 -in test-ingress-2.csr -signkey test-ingress-2.key \
    -out test-ingress-2.crt

For more information about certificates and keys, see the SSL certificates overview.

You now have two certificate files and two key files.

The remaining tasks use the following placeholders to refer to your domains, certificates, and keys:

  • first-cert-file is the path to your first certificate file.
  • first-key-file is the path to the key file that goes with your first certificate.
  • first-domain is a domain name that you own or a fake domain name.
  • first-secret-name is the name of the Secret containing your first certificate and key.
  • second-cert-file is the path to your second certificate file.
  • second-key-file is the path to the key file that goes with your second certificate.
  • second-domain is a second domain name that you own or a second fake domain name.
  • second-secret-name is the name of the Secret containing your second certificate and key.

Specifying certificates for your Ingress

The next step is to create an Ingress object. In your Ingress manifest, you can use one of two methods to provide certificates for the load balancer:

  • Secrets
  • Pre-shared certificates

Choose one of the two methods by selecting the SECRETS tab or the PRE-SHARED CERTS tab:

Secrets

Creating Secrets

Create a Secret that holds your first certificate and key:

kubectl create secret tls first-secret-name \
  --cert first-cert-file --key first-key-file

Create a Secret that holds your second certificate and key:

kubectl create secret tls second-secret-name \
  --cert second-cert-file --key second-key-file

Creating an Ingress

Here is a manifest for an Ingress:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: my-mc-ingress
spec:
  tls:
  - secretName: first-secret-name
  - secretName: second-secret-name
  rules:
  - host: first-domain
    http:
      paths:
      - backend:
          serviceName: my-mc-service
          servicePort: my-first-port
  - host: second-domain
    http:
      paths:
      - backend:
          serviceName: my-mc-service
          servicePort: my-second-port

Copy the manifest to a file named my-mc-ingress.yaml. Replace first-domain and second-domain with domain names that you own or with fake domain names.

Create the Ingress:

kubectl apply -f my-mc-ingress.yaml

When you create an Ingress, the GKE ingress controller creates an HTTP(S) load balancer. Wait a minute for GKE assign an external IP address to the load balancer.

Describe your Ingress:

kubectl describe ingress my-mc-ingress

The output shows that two Secrets are associated with the Ingress. The output also shows the external IP address of the load balancer.

Name: my-mc-ingress
Address: 203.0.113.1
...
TLS:
  first-secret-name terminates
  second-secret-name terminates
Rules:
  Host              Path  Backends
  ----              ----  --------
  your-store.example
                     my-mc-service:my-first-port (<none>)
  your-experimental-store.example
                     my-mc-service:my-second-port (<none>)
Annotations:
...
Events:
  Type    Reason  Age   From                     Message
  ----    ------  ----  ----                     -------
  Normal  ADD     3m    loadbalancer-controller  default/my-mc-ingress
  Normal  CREATE  2m    loadbalancer-controller  ip: 203.0.113.1

Pre-shared certs

Using pre-shared certificates

Create a certificate resource in your Google Cloud project:

gcloud compute ssl-certificates create test-ingress-1 \
--certificate first-cert-file --private-key first-key-file

where:

Create a second certificate resource in your Google Cloud project:

gcloud compute ssl-certificates create test-ingress-2 \
--certificate second-cert-file --private-key second-key-file

where:

  • second-cert-file is your second certificate file.
  • second-key-file is your second key file.

View your certificate resources:

gcloud compute ssl-certificates list

The output shows you have certificate resources named test-ingres-1 and test-ingress-2:

NAME                CREATION_TIMESTAMP
test-ingress-1      2018-11-03T12:08:47.751-07:00
test-ingress-2      2018-11-03T12:09:25.359-07:00

Here's a manifest for an Ingress that lists pre-shared certificate resources in an annotation:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: my-psc-ingress
  annotations:
    ingress.gcp.kubernetes.io/pre-shared-cert: "test-ingress-1,test-ingress-2"
spec:
  rules:
  - host: first-domain
    http:
      paths:
      - backend:
          serviceName: my-mc-service
          servicePort: my-first-port
  - host: second-domain
    http:
      paths:
      - backend:
          serviceName: my-mc-service
          servicePort: my-second-port

Copy the manifest to a file named my-psc-ingress.yaml. Replace first-domain and second-domain with your domain names or with fake domain names.

Create the Ingress:

kubectl apply -f my-psc-ingress.yaml

Wait a minute for GKE assign an external IP address to the load balancer.

Describe your Ingress:

kubectl describe ingress my-psc-ingress

The output shows that the Ingress is associated with pre-shared certificates named test-ingress-1 and test-ingress-2. The output also shows the external IP address of the load balancer:

Name:             my-psc-ingress
Address:          203.0.113.2
...
Rules:
  Host              Path  Backends
  ----              ----  --------
  your-store.example
                     my-mc-service:my-first-port (<none>)
  your-experimental-store.example
                     my-mc-service:my-second-port (<none>)
Annotations:
  ...
  ingress.gcp.kubernetes.io/pre-shared-cert:    test-ingress-1,test-ingress-2
  ...
  ingress.kubernetes.io/ssl-cert:               test-ingress-1,test-ingress-2
Events:
  Type    Reason  Age   From                     Message
  ----    ------  ----  ----                     -------
  Normal  ADD     2m    loadbalancer-controller  default/my-psc-ingress
  Normal  CREATE  1m    loadbalancer-controller  ip: 203.0.113.2

Testing the load balancer

Wait about five minutes for GKE to finish configuring the load balancer.

To do this step, you need to own two domain names, and both of your domain names must resolve the external IP address of the HTTP(S) load balancer.

Send a request to the load balancer by using your first domain name:

curl -v https://first-domain

The output shows that your first certificate was used in the TLS handshake. If your first domain is your-store.example, the output is similar to this:

...
*   Trying 203.0.113.1...
...
* Connected to your-store.example (203.0.113.1) port 443 (#0)
...
* TLSv1.2 (IN), TLS handshake, Certificate (11):
...
* Server certificate:
*  subject: CN=your-store.example
...
> Host: your-store.example
...
&lt;
Hello, world!
Version: 2.0.0
...

Send a request to the load balancer by using your second domain name:

curl -v https://second-domain

The output shows that your second certificate was used in the TLS handshake. If your second domain is your-experimental-store.example, the output is similar to this:

...
*   Trying 203.0.113.1...
...
* Connected to your-experimental-store.example (203.0.113.1) port 443 (#0)
...
* Server certificate:
*  subject: CN=your-experimental-store.example
...
> Host: your-experimental-store.example
...
Hello Kubernetes!

The hosts field of an Ingress object

An IngressSpec has a tls field that is an array of IngressTLS objects. Each IngressTLS object has a hosts field and SecretName field. In GKE, the hosts field is not used. GKE reads the Common Name (CN) from the certificate in the Secret. If the Common Name matches the domain name in a client request, then the load balancer presents the matching certificate to the client.

Which certificate is presented?

The load balancer chooses a certificate according to these rules:

  • If both Secrets and pre-shared certificates are listed in the Ingress, the load balancer ignores the Secrets and uses the list of pre-shared certificates.

  • If no certificate has a Common Name (CN) that matches the domain name in the client request, the load balancer presents the primary certificate.

  • For Secrets listed in the tls block, the primary certificate is in the first Secret in the list.

  • For pre-shared certificates listed in the annotation, the primary certificate is the first certificate in the list.

Troubleshooting

Specifying invalid or non-existent Secrets results in a Kubernetes event error. You can check Kubernetes events for an Ingress as follows:

kubectl describe ingress

This output is similar to this:

Name:             my-ingress
Namespace:        default
Address:          203.0.113.3
Default backend:  hello-server:8080 (10.8.0.3:8080)
TLS:
  my-faulty-Secret terminates
Rules:
  Host  Path  Backends
  ----  ----  --------
  *     *     my-service:443 (10.8.0.3:443)
Events:
   Error during sync: cannot get certs for Ingress default/my-ingress:
 Secret "my-faulty-ingress" has no 'tls.crt'

What's next