Istio, SDS y cert-manager
Tags: kubernetes, istio, devops
Published: 2019-06-08 15:26:36

Si has llegado hasta aquí, ya sabes qué es Kubernetes. Incluso puede que hayas levantado tu propio clúster en local, usando Minikube. O hasta en una de los cientos de nubes públicas y privadas que ofrecen un servicio de Kubernetes auto gestionado. ¡Será por opciones!

En esta serie de posts vamos a ver cómo instalar/actualizar Istio para hacerlo funcionar conjuntamente con [cert-manager](https://github.com/jetstack/cert-manager), que nos proporcionará certificados gratuitos mediante [Let's Encrypt](https://letsencrypt.org/), y [ExternalDNS](https://github.com/kubernetes-incubator/external-dns), que actualizará nuestros DNS cada vez que añadamos una entrada a un [Gateway](https://istio.io/docs/reference/config/networking/v1alpha3/gateway/) de Istio.

## ¿Qué es SDS? ¿y mTLS?

SDS son las siglas en inglés de _Secret Discovery Service_. Antes de Istio 1.1, las claves privadas y certificados generados por Istio eran manejados por _Citadel_, y montados como volúmenes en los pods sidecar de los pods. Esto tenía ciertas desventajas:

* Al renovar un certificado, Istio reinicia en caliente el sidecar de Envoy, provocando pérdidas de rendimiento.
* Las claves privadas se guardaban en secretos de Kubernetes, lo cual tiene [ciertos riesgos](https://kubernetes.io/docs/concepts/configuration/secret/#risks).

Resumiendo, SDS soluciona estos problemas generando una clave privada y CSR en _Citadel_ (que funciona como _DaemonSet_, es decir, en todos los nodos de Kubernetes) mediante token JWT enviado por el sidecar de Envoy. _Citadel_ comprueba el token y, si todo es correcto, genera y envía el certificado y la clave privada al sidecar.

Podéis encontrar más detalles de cómo funciona en el [blog de Istio](https://istio.io/docs/tasks/security/auth-sds/), y en la [documentación de Envoy](https://www.envoyproxy.io/docs/envoy/latest/configuration/secret#config-secret-discovery-service).

Por otra parte, mTLS (mutual TLS), configura todos los pods del clúster para que hablen entre ellos usando TLS, por lo que todas las comunicaciones entre microservicios estarán cifradas y los componentes de Istio se encargarán de gestionar los certificados.

## Instalando Istio con mTLS y SDS

En este artículo, partimos de un clúster vacío. Si ya tenemos un clúster funcionando sin Istio, tenemos que tener en cuenta que, para que Istio inyecte el pod sidecar en un _namespace_, tendremos que etiquetar ese _namespace_ con la siguiente _label_:

```bash
kubectl label namespace production istio-injection=enabled
```

Una vez aplicado, tendremos que levantar nuevos pods para que Istio inyecte el sidecar, y eliminar los antiguos.

-----

Para instalar Istio, usaremos _Helm_. Nos descargamos la última versión de [Istio](https://github.com/istio/istio/releases/tag/1.1.7), y la descomprimimos.

Instalamos primeros los CRDs de Istio en el clúster. Puedes omitir este paso si ya lo tenías instalado:

```bash
helm install --name istio-init --namespace istio-system istio-1.1.7/install/kubernetes/helm/istio-init
```

Crearemos un fichero de _values_ parecido a `values-istio-sds-auth.yaml` que viene en el tar, con ciertas modificaciones que explico comentadas en el fichero:

```yaml
$ cat values-istio-sds-auth.yaml
global:
  controlPlaneSecurityEnabled: false

  mtls:
    enabled: true

  sds:
    enabled: true
    udsPath: "unix:/var/run/sds/uds_path"
    useNormalJwt: true

nodeagent:
  enabled: true
  image: node-agent-k8s
  env:
    CA_PROVIDER: "Citadel"
    CA_ADDR: "istio-citadel:8060"
    VALID_TOKEN: true
# Fin del fichero original

# Valores añadidos manualmente
# En la sección anterior, el parámetro global.sds.enabled=true activa SDS únicamente en los pods sidecar de Istio.
# En esta sección, configuramos el ingressgateway para que reciba los certificados con el mismo método.
gateways:
  istio-ingressgateway:
    enabled: true
    sds:
      enabled: true

# Esta sección inferior soluciona los livenessProbe y readinessProbe de Kubernetes configurados mediante httpGet,
# que fallan por el sidecar de Istio.
sidecarInjectorWebhook:
  enabled: true
  rewriteAppHTTPProbe: true
```

Instalamos con el comando:

```bash
helm install \
  --name istio \
  --namespace istio-system \
  istio-1.1.7/install/kubernetes/helm/istio \
  --values values.yaml # values.yaml es nuestro fichero modificado
```

Tras unos minutos de tensión, deberías tener Istio funcionando en tu clúster:

```bash
$ kubectl get pods -n istio-system
NAME                                       READY   STATUS    RESTARTS   AGE
istio-citadel-7f447d4d4b-f8lzt             1/1     Running   0          1m
istio-galley-84749d54b7-7sngg              1/1     Running   0          1m
istio-ingressgateway-57c97bb7f6-8294g      2/2     Running   0          1m
istio-nodeagent-nrl7v                      1/1     Running   0          1m
istio-nodeagent-vkchb                      1/1     Running   0          1m
istio-pilot-76899788b6-xcw7t               2/2     Running   0          1m
istio-policy-578bcb878f-ddwh7              2/2     Running   2          1m
istio-sidecar-injector-6895997989-5qsnn    1/1     Running   0          1m
istio-telemetry-5448cbd995-jcpdg           2/2     Running   2          1m
```

¡Enhorabuena! Ya tienes Istio funcionando. Tómate un café, te lo has ganado.

---

## Instalando cert-manager

Debido a un bug con cert-manager y la última versión de Helm, vamos a instalar el primero templatizando el _chart_ de cert-manager.

{% github https://github.com/jetstack/cert-manager/issues/1702 %}

Descargamos el chart de cert-manager y descomprimimos:

```bash
helm fetch --untar stable/cert-manager
```

Como no tenemos que modificar ningún parámetro del `values.yaml` para instalarlo en el clúster, templatizamos y aplicamos:

```bash
helm template . --name cert-manager --namespace istio-system > cert-manager.yaml
kubectl apply --file cert-manager.yaml
```

**IMPORTANTE**: Como veis, instalamos cert-manager en el namespace de Istio, _istio-system_. Esto es un requisito, ya que para que funcione SDS, los secretos que contienen los certificados deben encontrarse en el mismo _namespace_ en el que está Istio. Los Gateways y VirtualServices de Istio podrán estar en su _namespace_ correspondiente.

**NOTA**: en el propio _chart_ de Istio podemos indicar una opción para instalar cert-manager. En este artículo lo instalamos de manera independiente para poder manejarlo independientemente de la instalación de Istio.

Una vez esté instalado cert-manager en el namespace de Istio, tendremos que configurar un emisor de certificados. Vamos a configurarlo para que use Let's Encrypt, y confirme los certificados resolviendo un _DNS01 Challenge_, que nos permitirá generar certificados de tipo _wildcard_, es decir, `*.example.com`.

En este caso, usamos **AWS Route 53** como DNS, por lo que el clúster o pods necesitarán unos [permisos específicos de IAM](https://docs.cert-manager.io/en/latest/tasks/issuers/setup-acme/dns01/route53.html) para trabajar con este servicio de AWS.

Generamos el fichero `clusterissuer.yaml` con este contenido:

```yaml
# clusterissuer.yaml
apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
  name: letsencrypt
  labels:
    app: certmanager
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: <EMAIL> # Sustituir por un correo válido
    privateKeySecretRef:
      name: letsencrypt # Secreto donde cert-manager guardará datos
    # Configuramos el proveedor para Route 53. En este caso no se indican credenciales de AWS,
    # dado que los pods y/o nodos de k8s tienen asignado un rol de IAM con permisos.
    dns01:
      providers:
      - name: route53
        route53:
          region: eu-west-1
#          # Descomenta esto, y modifica acorde,
#          # para usar unas credenciales de IAM que otorguen los permisos
#          accessKeyID: AKIAIOSFODNN7EXAMPLE
#          secretAccessKeySecretRef:
#            name: prod-route53-credentials-secret
#            key: secret-access-key
```

Y aplicamos:

```bash
kubectl apply -f clusterissuer.yaml
```

Tendremos disponible, a nivel de clúster, la creación de certificados usando los certificados de Let's Encrypt. Pero primero tenemos que definir un certificado, para que cert-manager se encargue de solicitarlo automáticamente:

```yaml
# wildcard.example.com.yaml
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
  name: wildcard.example.com
  namespace: istio-system # Recordad: es necesario que los certificados estén en el namespace de Istio
spec:
  secretName: wildcard.example.com
  issuerRef:
    kind: ClusterIssuer
    name: letsencrypt
  commonName: "*.example.com"
  dnsNames:
  - example.com
  acme:
    config:
    - dns01:
        provider: route53
      domains:
        - '*.example.com'
        - example.com
```

```bash
kubectl apply -f wildcard.example.com.yaml
```

Si todo está correctamente configurado, en unos minutos tendremos disponible el certificado:

```bash
$ kubectl describe certificates wildcard.example.com
[...]
Status:
  Conditions:
    Last Transition Time:  2019-05-30T09:27:56Z
    Message:               Certificate is up to date and has not expired
    Reason:                Ready
    Status:                True
    Type:                  Ready
  Not After:               2019-08-28T08:27:54Z
[...]
```

¡Excelente! Ya tenemos el certificado. Pero, ¿cómo hacemos para que Istio envie el tráfico a nuestros pods, y se encargue de configurar el certificado? Para ello, hacemos uso de los Gateways y VirtualServices de Istio.

## Configurando Gateways y VirtualServices

Esta es fácil. Simplemente tendremos que generar un Gateway de Istio donde configuramos el DNS y el certificado, y un VirtualService que envíe el tráfico según el host al servicio correspondiente de Kubernetes. Para este ejemplo, usaremos el servicio `nginx`:

```yaml
# route.yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: example-gateway
  namespace: production # Podemos crear los Gateways en el namespace que queramos
spec:
  selector:
    istio: ingressgateway # Gateway de Istio por donde entra el tráfico al service mesh
  servers:
  # Configuramos el puerto 80 para que Istio haga la redirección a HTTPS
  - port:
      number: 80
      name: http-redirect
      protocol: HTTP
    hosts:
    - "nginx.example.com"
    tls:
      httpsRedirect: true
  - port:
      number: 443
      name: nginx-https
      protocol: HTTPS
    tls:
      # En credentialName, indicamos el nombre del secreto configurado
      # en el Certificate anteriormente creado
      credentialName: wildcard.example.com
      mode: SIMPLE
      # En las dos siguientes claves, indicamos a Istio que use el sistema SDS
      # para obtener el certificado
      privateKey: sds
      serverCertificate: sds
    hosts:
      - "nginx.example.com"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: nginx-route
  namespace: production
spec:
  gateways:
    - example-gateway # OJO, hace referencia al gateway creado más arriba
  hosts:
    - nginx.example.com
  http:
    - match:
      - uri:
          prefix: "/"
      route:
      - destination:
          host: nginx.production.svc.cluster.local
          port:
            number: 443
```

Aplicamos este fichero con `kubectl apply -f routes.yaml`, et voilà! Istio inyectará automáticamente el certificado configurado (en este caso, el wildcard) cuando entremos al DNS del servicio, `nginx.example.com`.

¿Pero no íbamos a configurar ExternalDNS para que generara las entradas automáticamente en Route 53? De momento, hazlo a mano, y en el próximo post vemos cómo integrar ExternalDNS con Istio, y Route 53.

Prometo que será corto.

---

### Fuentes y más información
* [Istio 1.1.7 + Let's Encrypt: WORKING!](https://medium.com/@prune998/istio-1-1-7-lets-encrypt-working-9100cea9f503)
* [cert-manager documentation](https://docs.cert-manager.io/en/latest/)
* [Istio Docs](https://istio.io/docs/)