Setting up Server TLS

Gloo Edge can encrypt traffic coming from external clients over TLS/HTTPS. We can also configure Gloo Edge to do mTLS with external clients as well. In this document, we’ll explore configuring Gloo Edge for server TLS.


Server TLS

Gloo Edge supports server-side TLS where the server presents a certificate to the client based on the domain specified in the client request. This means we can support multiple virtual hosts on a single port and use Server Name Identification (SNI) to determine what certificate to serve depending what domain the client is requesting. In Gloo Edge, we associate our TLS configuration with a specific Virtual Service which can then describe which SNI hosts would be candidates for both the TLS certificates as well as the routing rules that are defined in the Virtual Service. Let’s look at setting up TLS.

Prepare sample environment

Before we walk through setting up TLS for our virtual hosts, let’s deploy our sample applications with a default Virtual Service and routes.

To start, let’s make sure the petstore application is deployed:

kubectl apply -f https://raw.githubusercontent.com/solo-io/gloo/v1.14.x/example/petstore/petstore.yaml

If we query the Gloo Edge Upstreams we should see it:

glooctl get upstream default-petstore-8080
+-----------------------+------------+----------+-------------------------+
|       UPSTREAM        |    TYPE    |  STATUS  |         DETAILS         |
+-----------------------+------------+----------+-------------------------+
| default-petstore-8080 | Kubernetes | Accepted | svc name:      petstore |
|                       |            |          | svc namespace: default  |
|                       |            |          | port:          8080     |
|                       |            |          |                         |
+-----------------------+------------+----------+-------------------------+

Now let’s create a route to the petstore like we did in the hello world tutorial:

glooctl add route \
    --path-exact /sample-route-1 \
    --dest-name default-petstore-8080 \
    --prefix-rewrite /api/pets

Since we didn’t explicitly create a Virtual Service, adding this route will create a default Virtual Service named default.

glooctl get virtualservice default -o kube-yaml
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: default
  namespace: gloo-system
spec:
  virtualHost:
    domains:
    - '*'
    routes:
    - matchers:
      - exact: /sample-route-1
      options:
        prefixRewrite: /api/pets
      routeAction:
        single:
          upstream:
            name: default-petstore-8080
            namespace: gloo-system
status:
  reportedBy: gateway
  state: 1
  subresourceStatuses:
    '*v1.Proxy.gloo-system.gateway-proxy':
      reportedBy: gloo
      state: 1

If we want to query the service to verify routing is working, we can do so like this:

curl $(glooctl proxy url --port http)/sample-route-1
[{"id":1,"name":"Dog","status":"available"},{"id":2,"name":"Cat","status":"pending"}]

Let’s enable HTTPS by configuring TLS/SSL for our Virtual Service.

Configuring TLS/SSL in a Virtual Service

Before we add the TLS/SSL configuration, let’s create a private key and certificate to use in our Virtual Service. Obviously, if you have your own key/cert pair, you can use those instead of creating self-signed certs here.

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
   -keyout tls.key -out tls.crt -subj "/CN=petstore.example.com"

Now we should create the Kubernetes secrets to hold this cert:

kubectl create secret tls upstream-tls --key tls.key \
   --cert tls.crt --namespace gloo-system

You could also use glooctl to create the TLS secret which also allows storing a root certificate authority (CA) which can be used for client cert verification (for example, if you set up downstream mTLS for your Virtual Services). glooctl adds extra annotations so we can catalog the different secrets we may need like tls, aws, azure to make it easier to serialize/deserialize in the correct format. For example, to create the TLS secret with glooctl:

glooctl create secret tls --name upstream-tls --certchain tls.crt --privatekey tls.key

If you’ve created your secret with kubectl, you don’t need to use glooctl to do the same.

Lastly, let’s configure the Virtual Service to use this cert via the Kubernetes secrets:

glooctl edit virtualservice --name default --namespace gloo-system \
   --ssl-secret-name upstream-tls --ssl-secret-namespace gloo-system

Now if we get the default Virtual Service, we should see the new SSL configuration:

glooctl get virtualservice default -o kube-yaml
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: default
  namespace: gloo-system
spec:
  sslConfig:
    secretRef:
      name: upstream-tls
      namespace: gloo-system
  virtualHost:
    domains:
    - '*'
    routes:
    - matchers:
      - exact: /sample-route-1
      options:
        prefixRewrite: /api/pets
      routeAction:
        single:
          upstream:
            name: default-petstore-8080
            namespace: gloo-system
status:
  reportedBy: gateway
  state: 1
  subresourceStatuses:
    '*v1.Proxy.gloo-system.gateway-proxy':
      reportedBy: gloo
      state: 1

If we try to query the HTTP port, we should not get a successful response (it should hang, or timeout since we no longer have a route on the HTTP listener and Envoy will give a grace period to drain requests. After the drain is completed, the HTTP port will be closed if there are no other routes on the listener). By default when there are no routes for a listener, the port will not be opened.

curl $(glooctl proxy url --port http)/sample-route-1

If we try with the HTTPS port, it should work:

curl $(glooctl proxy url --port https)/sample-route-1

It’s possible that if you used self-signed certs, curl cannot validate the certificate. In this case, SPECIFICALLY FOR THIS EXAMPLE, you can skip certificate validation with curl -k ...(note this is not secure):

curl -k $(glooctl proxy url --port https)/sample-route-1
[{"id":1,"name":"Dog","status":"available"},{"id":2,"name":"Cat","status":"pending"}]

Configuring downstream mTLS in a Virtual Service

Gloo Edge can be configured to verify downstream client certificates. As seen in the example above, you can reference a Kubernetes secret on your Virtual Service which allows Gloo Edge to verify the Upstream. If this secret also contains a root CA, Gloo Edge will use it to verify downstream client certificates.

We need to create a new set of self-signed certs to use in between the client and Gloo Edge.

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
   -keyout mtls.key -out mtls.crt -subj "/CN=gloo.gloo-system.com"

Since they are self-signed, we can use mtls.crt as both our client cert and our root CA file for Gloo Edge to verify the client.

We will use glooctl to create the TLS secret, adding the rootca with an additional flag:

glooctl create secret tls --name downstream-mtls --certchain tls.crt --privatekey tls.key --rootca mtls.crt

The cert and key files were generated from the previous example (tls.crt and tls.key). The root CA file comes from the self-signed cert provided in this example (mtls.crt).

Next, let’s configure the Virtual Service to use this cert via the Kubernetes secrets:

glooctl edit virtualservice --name default --namespace gloo-system \
   --ssl-secret-name downstream-mtls --ssl-secret-namespace gloo-system

Now if we get the default Virtual Service, we should see the new SSL configuration:

glooctl get virtualservice default -o kube-yaml
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: default
  namespace: gloo-system
spec:
  sslConfig:
    secretRef:
      name: downstream-mtls
      namespace: gloo-system
  virtualHost:
    domains:
    - '*'
    routes:
    - matchers:
      - exact: /sample-route-1
      options:
        prefixRewrite: /api/pets
      routeAction:
        single:
          upstream:
            name: default-petstore-8080
            namespace: gloo-system
status:
  reportedBy: gateway
  state: 1
  subresourceStatuses:
    '*v1.Proxy.gloo-system.gateway-proxy':
      reportedBy: gloo
      state: 1

If we try query the HTTP port, we should not get a successful response (it should hang, or timeout since we no longer have a route on the HTTP listener and Envoy will give a grace period to drain requests. After the drain is completed, the HTTP port will be closed if there are no other routes on the listener). By default when there are no routes for a listener, the port will not be opened.

curl $(glooctl proxy url --port http)/sample-route-1

If we try with the HTTPS port, it should be denied due to not being verified:

Since we used self-signed certs, curl cannot validate the certificate. In this case, SPECIFICALLY FOR THIS EXAMPLE, you can skip certificate validation with curl -k ...(note this is not secure):

curl -k $(glooctl proxy url --port https)/sample-route-1

This will fail with Gloo Edge refusing the client connection because the client has not provided any certs.

curl: (35) error:1401E410:SSL routines:CONNECT_CR_FINISHED:sslv3 alert handshake failure

We can provide certs by passing in the mtls.key and mtls.crt files.

curl --cert mtls.crt --key mtls.key -k $(glooctl proxy url --port https)/sample-route-1
[{"id":1,"name":"Dog","status":"available"},{"id":2,"name":"Cat","status":"pending"}]

Serving certificates for multiple virtual hosts with SNI

Let’s say we had another Virtual Service that serves a different certificate for a different virtual host. Gloo Edge allows you to serve multiple virtual hosts from a single HTTPS port and use SNI to determine which certificate to present to which virtual host. In the previous example, we create a certificate for the petstore.example.com domain. Let’s create a new self-signed certificate for a different domain, animalstore.example.com and see how Gloo Edge can serve multiple virtual hosts on a single port/listener.

First we’ll create the self-signed certificate for the domain animalstore.example.com.

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
   -keyout tls.key -out tls.crt -subj "/CN=animalstore.example.com"

Then we will create a Kubernetes secret to store the certificate:

kubectl create secret tls animal-certs --key tls.key \
    --cert tls.crt --namespace gloo-system

We’ll also create a new Virtual Service and attach this new certificate to it. When we create the Virtual Service, let’s also specify exactly which domains we’ll match and to which we’ll serve the animalstore.example.com certificate:

glooctl  create virtualservice --name animal --domains animalstore.example.com

Now add the TLS/SSL config with the appropriate SNI domain information:

glooctl edit virtualservice --name animal --namespace gloo-system \
   --ssl-secret-name animal-certs --ssl-secret-namespace gloo-system \
   --ssl-sni-domains animalstore.example.com

As you can see in the previous step, we need to specify the SNI domains that will match for this certificate with the --ssl-sni-domains parameter. If you do NOT specify this parameter, Envoy will become confused about which certificates to serve because there will effectively be two (or more) with no qualifying information. If that’s the case, you can expect to see logs similar to the following in your gateway-proxy logs:

gateway-proxy-9b55c99c7-x7r7c gateway-proxy [2019-03-20 19:01:01.763][6][warning][config] 
[bazel-out/k8-opt/bin/external/envoy/source/common/config/_virtual_includes/grpc_mux_subscription_lib/common/config/grpc_mux_subscription_impl.h:70] 
gRPC config for type.googleapis.com/envoy.api.v2.Listener 
rejected: Error adding/updating listener listener-::-8443: error adding listener 
'[::]:8443': multiple filter chains with the same matching rules are defined

If you end up with logs like that, double check your SNI settings.

Lastly, let’s add a route for this Virtual Service:

glooctl add route --name animal\
   --path-exact /animals \
   --dest-name default-petstore-8080 \
   --prefix-rewrite /api/pets

Note, we’re giving this service a different API, namely /animals instead of /sample-route-1.

Now if we get the Virtual Service, we should see this one set up with a different cert/secret:

glooctl get virtualservice animal -o kube-yaml
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: animal
  namespace: gloo-system
spec:
  displayName: animal
  sslConfig:
    secretRef:
      name: animal-certs
      namespace: gloo-system
    sniDomains:
    - animalstore.example.com
  virtualHost:
    domains:
    - animalstore.example.com
    routes:
    - matchers:
      - exact: /animals
      options:
        prefixRewrite: /api/pets
      routeAction:
        single:
          upstream:
            name: default-petstore-8080
            namespace: gloo-system
status:
  reportedBy: gateway
  state: 1
  subresourceStatuses:
    '*v1.Proxy.gloo-system.gateway-proxy':
      reportedBy: gloo
      state: 1

If everything up to this point looks good, let’s try to query the service and make sure to pass in the qualifying SNI information so that Envoy can serve the correct certificates.

Since we used self-signed certs, curl cannot validate the certificate. In this case, SPECIFICALLY FOR THIS EXAMPLE, you can skip certificate validation with curl -k ...(note this is not secure):

curl -k --resolve animalstore.example.com:443:$(kubectl get svc -n gloo-system gateway-proxy -o=jsonpath='{.status.loadBalancer.ingress[0].ip}') https://animalstore.example.com/animals
[{"id":1,"name":"Dog","status":"available"},{"id":2,"name":"Cat","status":"pending"}]

Understanding how it all works

By default, when a Virtual Service does NOT have any SSL/TLS configuration, it will be attached to the HTTP listener that we have for Gloo Edge proxy (listening on port 8080 by default, but exposed in Kubernetes on port 80 in the gateway-proxy service). When we add the SSL/TLS configuration, that Virtual Service will automatically become bound to the HTTPS port (listening on port 8443 on the gateway-proxy, but mapped to port 443 on the Kubernetes service).

To verify that, let’s take a look at the Gloo Edge proxy. The Gloo Edge proxy reflects the configuration that Gloo Edge sends to Envoy. All of the other custom Gloo resources like Gateway and VirtualService drive the proxy configuration.

glooctl get proxy -n gloo-system gateway-proxy -o yaml

Note that the proxy’s TLS listener (the one with bindPort 8443) has multiple sslConfigurations. If any of those valid TLS configs match a request, they can be routed to any route on the listener. This means that SSL config can be shared between virtual services if they are part of the same listener (i.e., HTTP or HTTPS).

...
spec:
  listeners:
  - bindAddress: '::'
    bindPort: 8080
    httpListener: {}
    metadata:
      sources:
      - kind: '*v1.Gateway'
        name: gateway-proxy
        namespace: gloo-system
    name: listener-::-8080
    useProxyProto: false
  - bindAddress: '::'
    bindPort: 8443
    httpListener:
      virtualHosts:
      - domains:
        - animalstore.example.com
        metadata:
          sources:
          - kind: '*v1.VirtualService'
            name: animal
            namespace: gloo-system
        name: gloo-system.animal
        routes:
        - matchers:
          - exact: /animals
          metadata:
            sources:
            - kind: '*v1.VirtualService'
              name: animal
              namespace: gloo-system
          options:
            prefixRewrite: /api/pets
          routeAction:
            single:
              upstream:
                name: default-petstore-8080
                namespace: gloo-system
      - domains:
        - '*'
        metadata:
          sources:
          - kind: '*v1.VirtualService'
            name: default
            namespace: gloo-system
        name: gloo-system.default
        routes:
        - matchers:
          - exact: /sample-route-1
          metadata:
            sources:
            - kind: '*v1.VirtualService'
              name: default
              namespace: gloo-system
          options:
            prefixRewrite: /api/pets
          routeAction:
            single:
              upstream:
                name: default-petstore-8080
                namespace: gloo-system
    metadata:
      sources:
      - kind: '*v1.Gateway'
        name: gateway-proxy-ssl
        namespace: gloo-system
    name: listener-::-8443
    sslConfigurations:
    - secretRef:
        name: animal-certs
        namespace: gloo-system
      sniDomains:
      - animalstore.example.com
    - secretRef:
        name: upstream-tls
        namespace: gloo-system
    useProxyProto: false
status:
  reportedBy: gloo
  state: 1

Next Steps

As we mentioned earlier, you can configure Gloo Edge to perform mutual TLS (mTLS) and client side TLS with Upstreams. Check out these guides to learn more: