Introducción

En Kubernetes, una visión práctica vimos como desplegar automáticamente una aplicación. Kubernetes nos permite además decorarla desde su definición en el codebase con cualquier artefacto que necesitemos. En este post veremos cómo añadir, for free, autenticación a nuestra plataforma.

Para ésto Keycloak es una estupenda herramienta.

Paso 1: personalizar la imagen de keycloak

La imagen base de keycloak levanta una instancia pelada que hay que configurar, pero nosotros queremos que todo esté automatizado, por lo que añadiremos un script que luego usaremos en el lifecycle de kubernetes. La creación de la imagen puede ser:

FROM jboss/keycloak:11.0.2
COPY ./images/keycloak/keycloak-set-up /keycloak-set-up
USER root
RUN chmod +x /keycloak-set-up
RUN sed -i 's/jboss.http.port:[0-9]\+/jboss.http.port:8081/g' /opt/jboss/keycloak/standalone/configuration/standalone-ha.xml
USER 1000

Donde añadimos nuestro script, le damos permiso de ejecución y ya de paso nos ponemos un puerto más cómodo en el JBoss.

Vale, con eso ya tenemos una imagen con nuestro script.

Paso 2: hacer el build de la imagen

Igual que hacíamos con nuestro servidor, debemos generar la imagen a partir del Dockerfile creado, así, añadimos a la pipe del Action de GitHub:

- name: Build and push keycloak
  uses: docker/build-push-action@v2
  with:
    context: .
    file: ./images/keycloak/Dockerfile
    platforms: linux/amd64
    push: true
    tags: myrepo/gol:keycloak-$

Vale, ya tenemos la imagen creada y subida a nuestro repositorio.

Paso 3: añadir keycloak a nuestro despliegue

Añadir un pod con keycloack es modificar nuestra definición de despliegue kubernetes añadiendo:

apiVersion: v1
kind: Pod
metadata:
  namespace: gol-main
  name: keycloak
  labels:
    type: keycloak
spec:
  imagePullSecrets:
    - name: regcred
  containers:
    - name: keycloak
      image: myrepo/gol:keycloak-main
      imagePullPolicy: Always
      env:
        - name: KEYCLOAK_USER
          value: "admin"
        - name: KEYCLOAK_PASSWORD
          value: "SECRET_KEYCLOAK_ADMIN_PASSWORD"
        - name: PROXY_ADDRESS_FORWARDING
          value: "true"
      lifecycle:
        postStart:
          exec:
            command: ["/bin/bash", "-c", "/keycloak-set-up"]
      ports:
        - name: http
          containerPort: 8081
      readinessProbe:
        httpGet:
          path: /auth/realms/master
          port: 8081

Hemos creado un único secret, el del password por defecto al levantarse el pod.

Vale, ya será desplegado keycloak en nuestra nube.

Paso 4: establecer configuración inicial

Queremos cargar automáticamente en keycloak la configuración inicial que precisemos, en este caso un Client de autenticación. Habíamos comentado que lo haríamos en el script keycloak-set-up usando el comando kcreg.sh:

#!/bin/bash

# from deployment secrets
# KEYCLOAK_USER=...
# KEYCLOAK_PASSWORD=...

KREG=/opt/jboss/keycloak/bin/kcreg.sh
KEYCLOAK_URL=http://keycloak:8081/auth
KEYCLOAK_REALM=master

# wait for keycloak available
while [ "`curl -s -w %{http_code} -o /dev/null $KEYCLOAK_URL/`" != "200" ]
do
  sleep 5
  echo "`date`: waiting for keycloak..." >> /tmp/keycloak-set-up.log
done
echo "`date`: keycloak ready" >> /tmp/keycloak-set-up.log

$KREG config credentials --server $KEYCLOAK_URL --realm $KEYCLOAK_REALM --user $KEYCLOAK_USER --password $KEYCLOAK_PASSWORD

echo "`date`: logged in" >> /tmp/keycloak-set-up.log

$KREG create -f - <<EOF
{
  "clientId" : "golauth",
  "surrogateAuthRequired" : false,
  "enabled" : true,
  "alwaysDisplayInConsole" : false,
  "clientAuthenticatorType" : "client-secret",
  "redirectUris" : [ "http://www.keycloak.config.authentication.site" ],
  "webOrigins" : [ ],
  "notBefore" : 0,
  "bearerOnly" : false,
  "consentRequired" : false,
  "standardFlowEnabled" : true,
  "implicitFlowEnabled" : false,
  "directAccessGrantsEnabled" : true,
  "serviceAccountsEnabled" : false,
  "publicClient" : true,
  "frontchannelLogout" : false,
  "protocol" : "openid-connect",
  "attributes" : {
    "saml.assertion.signature" : "false",
    "saml.force.post.binding" : "false",
    "saml.multivalued.roles" : "false",
    "saml.encrypt" : "false",
    "saml.server.signature" : "false",
    "saml.server.signature.keyinfo.ext" : "false",
    "exclude.session.state.from.auth.response" : "false",
    "saml_force_name_id_format" : "false",
    "saml.client.signature" : "false",
    "tls.client.certificate.bound.access.tokens" : "false",
    "saml.authnstatement" : "false",
    "display.on.consent.screen" : "false",
    "saml.onetimeuse.condition" : "false"
  },
  "authenticationFlowBindingOverrides" : { },
  "fullScopeAllowed" : true,
  "nodeReRegistrationTimeout" : -1,
  "defaultClientScopes" : [ "web-origins", "role_list", "profile", "roles", "email" ],
  "optionalClientScopes" : [ ]
}
EOF

echo "`date`: done" >> /tmp/keycloak-set-up.log

Primero esperamos que se levante JBoss con keycloak, luego registramos las credenciales del usuario admin y finalmente inyectamos la configuración del Client que queremos.

¿Cómo generamos o extraemos una configuración desde keycloak?

$ /opt/jboss/keycloak/bin/kcreg.sh config credentials --server $KEYCLOAK_URL --realm $KEYCLOAK_REALM --user $KEYCLOAK_USER --password $KEYCLOAK_PASSWORD
Logging into http://localhost:8081/auth/ as user admin of realm master
$ /opt/jboss/keycloak/bin/kcreg.sh get golauth
{
  "id" : "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "clientId" : "golauth",
  "surrogateAuthRequired" : false,
  "enabled" : true,
  "alwaysDisplayInConsole" : false,
...

En otros escenarios y en entornos productivos que requieran copias de seguridad o persistencia, consultar:

  1. Export/Import
  2. jboss/keycloak (ver bases de datos)

Vale, ya podemos configurar declarativamente nuestro keycloak.

Paso 5: inyectar los secret al despliegue

Como los secrets no deben estar en plano en el código, debemos inyectar como hacíamos al ejecutar el comando. Actualizando también la pipe cambiando:

- name: Kubernetes apply
  run: |
    cat cloud/${GITHUB_REF##*/}_cloud.yml | \
      sed 's/DOCKER_CONFIGJSON/$/' | \
      sed 's/SECRET_KEYCLOAK_ADMIN_PASSWORD/$/' | \
      ssh cloud 'microk8s kubectl apply -f -'

Ahora sí, se despliega el servidor keycloak y se pone en marcha todo lo anterior.

Vale, ya tenemos keycloak desplegado y autoconfigurado.

Paso 6: configurar los servicios para consultar a keycloak

keycloak tiene múltiples clientes para los servicios que requieran autenticar, las aplicaciones que requieran autenticarse, proveedores de autenticación (GitHub, Google, Twitter, Facebook, …). Por ejemplo en Java con Spring Boot viene a ser suficiente con añadir las dependencias:

        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
            <version>11.0.2</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.keycloak.bom</groupId>
                <artifactId>keycloak-adapter-bom</artifactId>
                <version>11.0.2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

Y en la configuración de spring boot algo como:

keycloak.security-constraints[0].auth-roles[0] = user
keycloak.security-constraints[0].security-collections[0].patterns[0] = /v1/*

keycloak.realm = master
keycloak.auth-server-url = http://keycloak:8081/auth/
keycloak.ssl-required = external
keycloak.resource = golauth
keycloak.public-client = true
keycloak.confidential-port = 0
keycloak.use-resource-role-mappings = true
keycloak.enabled = true

Que dado que nuestro keycloak va implícita en nuestro despliegue puede ir fija en el codebase.

Vale, ya tenemos securizado nuestros servicios.

Paso 6: probar la autenticación

Probemos primero que el servicio está protegido:

$ curl -v http://localhost:8080/v1/status
...
< HTTP/1.1 302
...
< Location: http://keycloak:8081/auth/realms/master/protocol/openid-connect/...
...

Bien, redirige a la url de autenciación. Ahora veamos si autenticando entramos:

$ TOKEN=`curl -s -X POST "http://keycloak:8081/auth/realms/master/protocol/openid-connect/token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "username=pepito" \
    -d "password=grillo" \
    -d 'grant_type=password' \
    -d 'client_id=golauth' | jq -r .access_token`

$ curl -s -H "Authentication: Bearer $TOKEN" http://apiserver:8080/v1/status
local-env

Y ya está, tenemos autenticación y autorización con todo el soporte de keycloak.

Referencias