/metals-example

Example HTTP Service that uses MeTaLS for drop-in mTLS

Primary LanguageShell

MeTaLS-example

Link to source repository: https://github.com/FreedomBen/metals-example

This is an example HTTP service that makes use of MeTaLS to add mTLS access control. It can be used as a starting point for adding mTLS to your own service.

For more details about MeTaLS, including various configuration options, there is extensive documentation available on the MeTaLS Github page

This service is a fork of echo-server with a few routes that return and process JSON so you can make sure request/response bodies are making it through the proxy properly. To see the JSON routes look in app.rb. It is written in Ruby with the Sinatra library so is quite declarative and readable for anyone familiar with web services.

Index of examples

Using this service

Starting the service locally

Before looking into the details of MeTaLS integration, let's briefly discuss what this example service does so you know how to run/test it. If you just want to accept it as a black box, you can skip to running the service with MeTaLS

For the most part the service will parse params and echo back the IP address of the caller, the HTTP verb and the path you requested, along with a Hash of the params. This makes it easy to anticipate what a correct response should look like so you know if the proxy is working correctly.

Here are some examples of the service being called directly:

First start the app (assuming dependencies are installed. If you aren't familiar with Bundler already, I recommend just using the pre-built image below):

$ ./app.rb

Or you can use the pre-built image:

$ podman run --rm -it quay.io/freedomben/metals-example

Also available on Docker Hub:

$ podman run --rm -it [docker.io/]freedomben/metals-example

Or if you want to build the image locally, that's easy to do as well:

$ podman build -t metals-example .

Hitting it with curl

Here are some example calls using curl:

[ben@host ~]$ curl localhost:8080
127.0.0.1 GET / - : {}
[ben@host ~]$ curl localhost:8080/some/path
127.0.0.1 GET /some/path - : {}
[ben@host ~]$ curl localhost:8080/some/path?with=params
127.0.0.1 GET /some/path - : {"with"=>"params"}
[ben@host ~]$ curl localhost:8080/some/path?with=params --data 'more=params&for=you'
127.0.0.1 POST /some/path - : {"with"=>"params", "more"=>"params", "for"=>"you"}
[ben@host ~]$

Example calls to the JSON endpoints

[ben@host ~]$ curl localhost:8080/objects
[{"name":"objectone"},{"name":"objecttwo"}]
[ben@host ~]$ curl localhost:8080/objects/1
{"name":"objectone"}
[ben@host ~]$ curl localhost:8080/objects/ --data 'these=are&basically=ignored'
127.0.0.1 POST /objects/ - : {"these"=>"are", "basically"=>"ignored"}

Running the service with MeTaLS

Pre-requisites

  1. A way to run OpenShift pods. I recommend using podman locally, or OpenShift remotely.

Deploying the image

If you are testing locally, you can build the image for this service locally and use it with a pod manager (like podman). If you want to deploy directly to an OpenShift or Kubernetes cluster, either build this image yourself and push it to your image registry, or use the pre-built one at quay.io/freedomben/metals-example.

If you are going to run this example on an OpenShift or Kubernetes cluster, it will have to able to pull down the image from the registry.

ConfigMaps and Secrets for the certificates

All configuration of MeTaLS is done through environment variables. When following the default setup, the only thing that is needed are the private key and certificates for the service. To provide these, we will use ConfigMaps and Secrets. This snippet is truncated for ease-of-reading. If you want a complete and functional example, see metals-example-ocp-secrets.yaml. Note that you will need a valid key and certificates (these can be self-generated. In the future I'll be writing a blog post about how to generate these for use in this example).

server-key.yaml

apiVersion: v1
kind: Secret
metadata:
  labels:
    app: metals-example
  name: metals-example-server-key
type: Opaque
stringData:
  METALS_PRIVATE_KEY: |
    -----BEGIN RSA PRIVATE KEY-----
    MIIJKQIBAAKCAgEAxYZUBrnPTzcnkKjg8bFtfW8lY2/xgiy9Mve0jjWEyhFPeITa
    gp5+yxdUaLJdWOMQ2qUn5LOOG20tB6L2cEXgQQEDZa0X8NbNAKI/JAhBUQUgIa/q
    PPLRAharZwpBdJ9d5rnjWpY4pODYt8gZRQKCAQAm6Wcssmi4N78Sw1YOuSSULYtW
    ...
    c9b/Y2F83Q6S0KDktekTkl1Ek+HXQujssoO2pdrNarnl7qPz3J9Ooogl81L7qYSc
    Z/O9sk5fWOkr24uVhD5hVpjJ75JR3sEaxr6Ma0aB1+RKfI5Te9YCOakvQWCMqf2h
    kc5Lsq391FjEDox1TmHBLMA9BymQg9T75y3rUD/s99XJgv1e+osHrnECSZ2l
    -----END RSA PRIVATE KEY-----

client-certificates.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: metals-example-certs
data:
  METALS_PUBLIC_CERT: |
    -----BEGIN CERTIFICATE-----
    MIIJQDCCBSigAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwVzELMAkGA1UEBhMCVVMx
    CzAJBgNVBAgMAklEMQ4wDAYDVQQHDAVCb2lzZTEXMBUGA1UECgwOQm9pc2UgQmFu
    ayBMVEQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMDAzMTMwMzExMzNaFw0yMjAz
    ...
    595sGnGOAyRcJ0BL6xcEHGBpUDqgpoYhILOsQ1umsbFBCyjtigu9Vj9fKEXD2Ml7
    4jx4azZl6/kWGclOL9eqbbQXtFOR3BRAvGh2vvKNSIytpdRvES7fCWnKDYOjoR4B
    JwE95codjcYbBmseuBJUc03wwtk=
    -----END CERTIFICATE-----
  METALS_SERVER_TRUST_CHAIN: |
    -----BEGIN CERTIFICATE-----
    MIIJgTCCBWmgAwIBAgIJAI45yy3ikizxMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV
    BAYTAlVTMQswCQYDVQQIDAJJRDEOMAwGA1UEBwwFQm9pc2UxFzAVBgNVBAoMDkJv
    aXNlIEJhbmsgTFREMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMjAwMzEzMDI0OTE2
    ...
    ZBzA4jTxO0Ov3hNivLahf/Bx4+Ek7y5x/zgcxWCsAgowpWXN4pIv3aEPn9unDJJz
    xnFvoY0R3gx/AvDM0+MHUMgbDBSVXBx8vK9JhYIFI+0E301bRgo3IGKzZeLTdTT1
    XuV865TpREo5JquzQWxJtbyKxjJa5RY7f9kN5lRFpzteY560YA==
    -----END CERTIFICATE-----

For more information about what these variables do, take a look at the comments in the example file, or consult the documentation for the variables in the project.

Apply the files to your cluster:

$ oc apply -f server-key.yaml
$ oc apply -f client-certificates.yaml

Create a Deployment

Now that we have our configuration data in place, we need to get some running pods. Rather than run them directly, we will use a Deployment to manage them. This was we can easily spin up additional replicas as we need to scale, and it the application crashes it will automatically be restarted for us but the supervisor.

metals-example-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: metals-example
  name: metals-example
spec:
  replicas: 1
  selector:
    matchLabels:
      app: metals-example
  template:
    metadata:
      labels:
        app: metals-example
    spec:
      containers:
      - image: quay.io/freedomben/metals-example:latest
        name: metals-example
        imagePullPolicy: Always
        env:
        - name: APP_ROOT
          value: /opt/app-root
        - name: HOME
          value: /opt/app-root/src
      - image: quay.io/freedomben/metals:latest
        name: metals
        imagePullPolicy: Always
        ports:
        - containerPort: 8443
          protocol: TCP
        envFrom:
        - configMapRef:
            name: metals-example-certs
        - secretRef:
            name: metals-example-server-key

Apply the file to your cluster:

$ oc apply -f metals-example-deployment.yaml

Expose the app with a Service

In order for our Deployment to be easily accessible, we need to create a Service for it:

metals-example-service.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    app: metals-example
  name: metals-example
spec:
  ports:
  - name: 8443-tcp
    port: 8443
    protocol: TCP
    targetPort: 8443
  selector:
    deployment: metals-example

Apply the file to your cluster:

$ oc apply -f metals-example-service.yaml

Expose the Service outside the cluster with a Route

For hitting the echo server from outside the cluster, we will expose it with a Route:

metals-example-route.yaml

apiVersion: route.openshift.io/v1
kind: Route
metadata:
  labels:
    app: metals-example
  name: metals-example
spec:
  port:
    targetPort: 8443-tcp
  to:
    kind: Service
    name: metals-example
    weight: 100
  wildcardPolicy: None
  tls:
    termination: passthrough

Apply the file to your cluster:

$ oc apply -f metals-example-route.yaml

Test the application

Now that the application is exposed with a Route, we can curl it from our local machine! Remember tho that we need a valid client certificate in order to get through the MeTaLS proxy. Your endpoints will be different, and you will need the client certificates and the trust chain locally for providing to curl. These examples should be adjusted appropriately.

Results should be the same as above when we were testing the app locally when our certificate is valid:

$ export TARGET_HOST=localhost:8443
$ curl \
    --cacert ./rootca.pem \
    --key ./client.key \
    --cert ./client.crt \
    https://$TARGET_HOST/testing/mtls/long/path?querystring=thisvalue

Reponse:

10.88.0.1 GET /testing/mtls/long/path - : {"querystring"=>"thisvalue"}

With a missing certificate, we get a 400 back and the request is never proxied to the echo server:

$ curl \
    --cacert ./rootca.pem \
    https://${TARGET_HOST}/testing/mtls/long/path?querystring=thisvalue

Response:

<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx/1.14.1</center>
</body>
</html>

With an invalid certificate, we likewise receive a 400 (with slightly different HTML) and the request is never proxied to the echo server.

$ curl \
    --cacert ./rootca.pem \
    --key ./valid-but-untrusted-client.key \
    --cert ./valid-but-untrusted-client.crt \
    https://${TARGET_HOST}/testing/mtls/long/path?querystring=thisvalue

Response:

<html>
<head><title>400 The SSL certificate error</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<center>The SSL certificate error</center>
<hr><center>nginx/1.14.1</center>
</body>
</html>

What's next?

Now that you've seen an example, get some certificates and try adding it to one of your own service!