Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mTLS authn method #301

Merged
merged 7 commits into from
Jun 14, 2022
Merged

mTLS authn method #301

merged 7 commits into from
Jun 14, 2022

Conversation

guicassolato
Copy link
Collaborator

Adds support for client TLS certificate validation at application layer in Authorino for connections with the protected service and Authorino raw HTTP authorization interface relying on Mutual Transport Layer Security (mTLS) for authentication.

Closes #8

Verification steps

Create a cluster, build and deploy the apps:

make local-setup FF=1

Create a CA certificate and store it in a secret to be discovered by Authorino:

openssl req -x509 -sha256 -days 365 -nodes -newkey rsa:2048 -subj "/CN=talker-api-ca" -keyout /tmp/ca.key -out /tmp/ca.crt
kubectl create secret tls talker-api-ca --cert=/tmp/ca.crt --key=/tmp/ca.key
kubectl label secret talker-api-ca authorino.kuadrant.io/managed-by=authorino app=talker-api

Redeploy Envoy acknowledging the CA cert and enabling client certificate validation:

(This steps sucks, but otherwise Envoy will not pass the client peer certificate to Authorino in the payload to external authorization. Effectively, we're adding redundant client cert validation – i.e. Envoy doing it at L4 and Authorino at L7. For a hope on finding a way to avoid this, see envoyproxy/envoy#20563 (comment).)

kubectl apply -f -<<EOF
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app: envoy
  name: envoy
data:
  envoy.yaml: |
    static_resources:
      listeners:
      - address:
          socket_address:
            address: 0.0.0.0
            port_value: 8000
        filter_chains:
        - transport_socket:
            name: envoy.transport_sockets.tls
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
              common_tls_context:
                tls_certificates:
                - certificate_chain: {filename: "/etc/ssl/certs/talker-api/tls.crt"}
                  private_key: {filename: "/etc/ssl/certs/talker-api/tls.key"}
                validation_context:
                  trusted_ca:
                    filename: /etc/ssl/certs/talker-api/tls.crt
          filters:
          - name: envoy.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              stat_prefix: local
              route_config:
                name: local_route
                virtual_hosts:
                - name: local_service
                  domains: ['*']
                  routes:
                  - match: { prefix: / }
                    route: { cluster: talker-api }
              http_filters:
              - name: envoy.filters.http.ext_authz
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
                  transport_api_version: V3
                  failure_mode_allow: false
                  include_peer_certificate: true
                  grpc_service:
                    envoy_grpc: { cluster_name: authorino }
                    timeout: 1s
              - name: envoy.filters.http.router
                typed_config: {}
              use_remote_address: true
      clusters:
      - name: authorino
        connect_timeout: 0.25s
        type: strict_dns
        lb_policy: round_robin
        http2_protocol_options: {}
        load_assignment:
          cluster_name: authorino
          endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: authorino-authorino-authorization
                    port_value: 50051
        transport_socket:
          name: envoy.transport_sockets.tls
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
            common_tls_context:
              validation_context:
                trusted_ca:
                  filename: /etc/ssl/certs/authorino-ca-cert.crt
      - name: talker-api
        connect_timeout: 0.25s
        type: strict_dns
        lb_policy: round_robin
        load_assignment:
          cluster_name: talker-api
          endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: talker-api
                    port_value: 3000
    admin:
      access_log_path: "/tmp/admin_access.log"
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 8001
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: envoy
  name: envoy
spec:
  selector:
    matchLabels:
      app: envoy
  template:
    metadata:
      labels:
        app: envoy
    spec:
      containers:
      - args:
        - --config-path /usr/local/etc/envoy/envoy.yaml
        - --service-cluster front-proxy
        - --log-level info
        - --component-log-level filter:trace,http:debug,router:debug
        command:
        - /usr/local/bin/envoy
        image: envoyproxy/envoy:v1.19-latest
        name: envoy
        ports:
        - containerPort: 8000
          name: web
        - containerPort: 8001
          name: admin
        volumeMounts:
        - mountPath: /usr/local/etc/envoy
          name: config
          readOnly: true
        - mountPath: /etc/ssl/certs/authorino-ca-cert.crt
          name: authorino-ca-cert
          readOnly: true
          subPath: ca.crt
        - mountPath: /etc/ssl/certs/talker-api
          name: talker-api-ca
          readOnly: true
      volumes:
      - configMap:
          items:
          - key: envoy.yaml
            path: envoy.yaml
          name: envoy
        name: config
      - name: authorino-ca-cert
        secret:
          defaultMode: 420
          secretName: authorino-ca-cert
      - name: talker-api-ca
        secret:
          defaultMode: 420
          secretName: talker-api-ca
---
apiVersion: v1
kind: Service
metadata:
  name: envoy
spec:
  selector:
    app: envoy
  ports:
  - name: web
    port: 8000
    protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-wildcard-host
spec:
  rules:
  - host: talker-api-authorino.127.0.0.1.nip.io
    http:
      paths:
      - backend:
          service:
            name: envoy
            port: { number: 8000 }
        path: /
        pathType: Prefix
EOF

Create the AuthConfig:

kubectl apply -f -<<EOF
apiVersion: authorino.kuadrant.io/v1beta1
kind: AuthConfig
metadata:
  name: talker-api-protection
spec:
  hosts:
  - talker-api-authorino.127.0.0.1.nip.io
  identity:
  - name: mtls
    mtls:
      labelSelectors:
        app: talker-api
  authorization:
  - name: acme
    json:
      rules:
      - selector: auth.identity.Organization
        operator: incl
        value: ACME Inc.
EOF

Try the API with a TLS certificate signed by the trusted CA:

openssl genrsa -out /tmp/aisha.key 2048
openssl req -new -key /tmp/aisha.key -out /tmp/aisha.csr -subj "/CN=aisha/C=PK/L=Islamabad/O=ACME Inc./OU=Engineering"
openssl x509 -req -in /tmp/aisha.csr -CA /tmp/ca.crt -CAkey /tmp/ca.key -CAcreateserial -out /tmp/aisha.crt -days 1 -sha256

curl -k --cert /tmp/aisha.crt --key /tmp/aisha.key https://talker-api-authorino.127.0.0.1.nip.io:8000 -i
# HTTP/1.1 200 OK

Try the API with a TLS certificate signed by the trusted CA, though missing an authorized Organization:

openssl genrsa -out /tmp/john.key 2048
openssl req -new -key /tmp/john.key -out /tmp/john.csr -subj "/CN=john/C=UK/L=London"
openssl x509 -req -in /tmp/john.csr -CA /tmp/ca.crt -CAkey /tmp/ca.key -CAcreateserial -out /tmp/john.crt -days 1 -sha256

curl -k --cert /tmp/john.crt --key /tmp/john.key https://talker-api-authorino.127.0.0.1.nip.io:8000 -i
# HTTP/1.1 403 Forbidden
# x-ext-auth-reason: Unauthorized

Try the AuthConfig via raw HTTP authorization interface:

Expose the interface:

kubectl port-forward service/authorino-authorino-authorization 5001:5001 &

Consume the raw HTTP authorization interface with a TLS certificate signed by the trusted CA:

curl -k --cert /tmp/aisha.crt --key /tmp/aisha.key -H 'Content-Type: application/json' -d '{}' https://talker-api-authorino.127.0.0.1.nip.io:5001/check -i
# HTTP/2 200

Consume the raw HTTP authorization interface with a TLS certificate signed by a unknown authority:

openssl req -x509 -sha256 -days 365 -nodes -newkey rsa:2048 -subj "/CN=untrusted" -keyout /tmp/untrusted-ca.key -out /tmp/untrusted-ca.crt
openssl genrsa -out /tmp/niko.key 2048
openssl req -new -key /tmp/niko.key -out /tmp/niko.csr -subj "/CN=niko/C=JP/L=Osaka"
openssl x509 -req -in /tmp/niko.csr -CA /tmp/untrusted-ca.crt -CAkey /tmp/untrusted-ca.key -CAcreateserial -out /tmp/niko.crt -days 1 -sha256

curl -k --cert /tmp/niko.crt --key /tmp/niko.key -H 'Content-Type: application/json' -d '{}' https://talker-api-authorino.127.0.0.1.nip.io:5001/check -i
# HTTP/2 401
# www-authenticate: Basic realm="mtls"
# x-ext-auth-reason: x509: certificate signed by unknown authority

Revoke an entire chain of certificates:

kubectl delete secret/talker-api-ca

Even if the deleted root certificate is still cached and accepted at the gateway, Authorino will revoke access at application level immediately.

Try the API again with a previously accepted certificate to verify:

curl -k --cert /tmp/aisha.crt --key /tmp/aisha.key https://talker-api-authorino.127.0.0.1.nip.io:8000 -i
# HTTP/1.1 401 Unauthorized
# www-authenticate: Basic realm="mtls"
# x-ext-auth-reason: x509: certificate signed by unknown authority

Cleanup:

make local-cleanup

Implements Mutual Transport Layer Security (mTLS) identity verification in Authorino.

Trusted root Certificate Authorities (CA) certificates are stored as Kubernetes TLS Secrets, fetched and cached by Authorino, silimarly to how API key Secrets are handled. Label selectors and namespace/cluster scope are encoraged to be used.

It works for both interfaces, i.e. the gRPC Envoy protocol-based authorization interface and the raw HTTP authorization interface.

For integrations via Envoy, mTLS authn set as well in Authorino might be seen as redundant and only meaninful for the purpose of combining multiple authentication methods and/or for counting on better structured subject data to apply normalization. For simple use cases, Authorino's 'plain' identity method can be used instead, fetching Envoy-injected principal information (extracted from the client cert) from `context.request.source.principal`.
- Modified names of variables, pointers, imports - to have a better base for the implementation of the mTLS identity evaluator with propeor reconciliation of k8s secrets
- Fixed label of log values for the name of secret
Refreshes the entire list of cached root CA certs by reloading them all again from the cluster at any operation (add, update, delete secret).
This is because `x509.CertPool` does not provide a good interface for deleting and updating the list of certificates in the pool, despite its convinient `.AppendCertsFromPEM` function.
To allow better management of the cache (i.e., addition, update and deletion of individual k8s secrets to avoid reloading all secrets, using the qualified name `<namespace>/<name>` of the resource as key), another data structure to store the trusted root CA certs must replace the type of `rootCerts: *x509.CertPool`.
@guicassolato guicassolato self-assigned this Jun 13, 2022
@guicassolato guicassolato requested a review from a team June 13, 2022 12:41
@guicassolato guicassolato merged commit 6fe79a8 into main Jun 14, 2022
@guicassolato
Copy link
Collaborator Author

Reverted in #303
Reintroducing in #305

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

mTLS authN mode
1 participant