From f58000c28f0bff64c66eb7f41a2c5ce58724c9bf Mon Sep 17 00:00:00 2001 From: Mike Malone Date: Thu, 24 Jan 2019 17:22:36 -0800 Subject: [PATCH] hello-mtls examples --- autocert/examples/hello-mtls/README.md | 55 +++++++ .../hello-mtls/curl/Dockerfile.client | 5 + autocert/examples/hello-mtls/curl/client.sh | 11 ++ .../hello-mtls/curl/hello-mtls.client.yaml | 22 +++ .../examples/hello-mtls/go/Dockerfile.client | 10 ++ .../examples/hello-mtls/go/Dockerfile.server | 10 ++ autocert/examples/hello-mtls/go/client.go | 85 +++++++++++ .../hello-mtls/go/hello-mtls.client.yaml | 22 +++ .../hello-mtls/go/hello-mtls.server.yaml | 33 +++++ autocert/examples/hello-mtls/go/server.go | 136 ++++++++++++++++++ 10 files changed, 389 insertions(+) create mode 100644 autocert/examples/hello-mtls/README.md create mode 100644 autocert/examples/hello-mtls/curl/Dockerfile.client create mode 100644 autocert/examples/hello-mtls/curl/client.sh create mode 100644 autocert/examples/hello-mtls/curl/hello-mtls.client.yaml create mode 100644 autocert/examples/hello-mtls/go/Dockerfile.client create mode 100644 autocert/examples/hello-mtls/go/Dockerfile.server create mode 100644 autocert/examples/hello-mtls/go/client.go create mode 100644 autocert/examples/hello-mtls/go/hello-mtls.client.yaml create mode 100644 autocert/examples/hello-mtls/go/hello-mtls.server.yaml create mode 100644 autocert/examples/hello-mtls/go/server.go diff --git a/autocert/examples/hello-mtls/README.md b/autocert/examples/hello-mtls/README.md new file mode 100644 index 00000000..5a8f97ed --- /dev/null +++ b/autocert/examples/hello-mtls/README.md @@ -0,0 +1,55 @@ +# hello-mtls + +This repository contains examples of dockerized [m]TLS clients and servers in +various languages. There's a lot of confusion and misinformation regarding how +to do mTLS properly with an internal public key infrastructure. The goal of +this repository is to demonstrate best practices like: + + * Properly configuring TLS to use your internal CA's root certificate + * mTLS (client certificates / client authentication) + * Short-lived certificate support (clients and servers automatically load + renewed certificates) + +Examples use multi-stage docker builds and can be built via without any +required local dependencies (except `docker`): + +``` +docker build -f Dockerfile.server -t hello-mtls-server- . +docker build -f Dockerfile.client -t hello-mtls-client- . +``` + +Once built, you should be able to deploy via: + +``` +kubectl apply -f hello-mtls.server.yaml +kubectl apply -f hello-mtls.client.yaml +``` + +## Feature matrix + +This matrix shows the set of features we'd like to demonstrate in each language +and where each language is. Bug fixes, improvements, and examples in new +languages are appreciated! + +[go/](go/) +- [X] Server using autocert certificate & key + - [X] mTLS (client authentication using internal root certificate) + - [X] Automatic certificate renewal + - [X] Restrict to safe ciphersuites and TLS versions + - [ ] TLS stack configuration loaded from `step-ca` + - [ ] Root certificate rotation +- [X] Client using autocert root certificate + - [X] mTLS (send client certificate if server asks for it) + - [ ] Automatic certificate rotation + - [X] Restrict to safe ciphersuites and TLS versions + - [ ] TLS stack configuration loaded from `step-ca` + - [ ] Root certificate rotation + +[curl/](curl/) +- [X] Client + - [X] mTLS (send client certificate if server asks for it) + - [X] Automatic certificate rotation + - [ ] Restrict to safe ciphersuites and TLS versions + - [ ] TLS stack configuration loaded from `step-ca` + - [ ] Root certificate rotation + diff --git a/autocert/examples/hello-mtls/curl/Dockerfile.client b/autocert/examples/hello-mtls/curl/Dockerfile.client new file mode 100644 index 00000000..b34112ae --- /dev/null +++ b/autocert/examples/hello-mtls/curl/Dockerfile.client @@ -0,0 +1,5 @@ +FROM alpine +RUN apk add --no-cache bash curl +COPY client.sh . +RUN chmod +x client.sh +ENTRYPOINT ./client.sh diff --git a/autocert/examples/hello-mtls/curl/client.sh b/autocert/examples/hello-mtls/curl/client.sh new file mode 100644 index 00000000..6ef5119a --- /dev/null +++ b/autocert/examples/hello-mtls/curl/client.sh @@ -0,0 +1,11 @@ +#!/bin/bash +while : +do + response=$(curl -sS \ + --cacert /var/run/autocert.step.sm/root.crt \ + --cert /var/run/autocert.step.sm/site.crt \ + --key /var/run/autocert.step.sm/site.key \ + ${HELLO_MTLS_URL}) + echo "$(date): ${response}" + sleep 5 +done \ No newline at end of file diff --git a/autocert/examples/hello-mtls/curl/hello-mtls.client.yaml b/autocert/examples/hello-mtls/curl/hello-mtls.client.yaml new file mode 100644 index 00000000..cf175626 --- /dev/null +++ b/autocert/examples/hello-mtls/curl/hello-mtls.client.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hello-mtls-client + labels: {app: hello-mtls-client} +spec: + replicas: 1 + selector: {matchLabels: {app: hello-mtls-client}} + template: + metadata: + annotations: + autocert.step.sm/name: hello-mtls-client.default.pod.cluster.local + labels: {app: hello-mtls-client} + spec: + containers: + - name: hello-mtls-client + image: hello-mtls-client-curl:latest + imagePullPolicy: Never + resources: {requests: {cpu: 10m, memory: 20Mi}} + env: + - name: HELLO_MTLS_URL + value: https://hello-mtls.default.svc.cluster.local diff --git a/autocert/examples/hello-mtls/go/Dockerfile.client b/autocert/examples/hello-mtls/go/Dockerfile.client new file mode 100644 index 00000000..af64096f --- /dev/null +++ b/autocert/examples/hello-mtls/go/Dockerfile.client @@ -0,0 +1,10 @@ +# build stage +FROM golang:alpine AS build-env +RUN mkdir /src +ADD client.go /src +RUN cd /src && go build -o client + +# final stage +FROM alpine +COPY --from=build-env /src/client . +ENTRYPOINT ./client diff --git a/autocert/examples/hello-mtls/go/Dockerfile.server b/autocert/examples/hello-mtls/go/Dockerfile.server new file mode 100644 index 00000000..5400d6df --- /dev/null +++ b/autocert/examples/hello-mtls/go/Dockerfile.server @@ -0,0 +1,10 @@ +# build stage +FROM golang:alpine AS build-env +RUN mkdir /src +ADD server.go /src +RUN cd /src && go build -o server + +# final stage +FROM alpine +COPY --from=build-env /src/server . +ENTRYPOINT ./server diff --git a/autocert/examples/hello-mtls/go/client.go b/autocert/examples/hello-mtls/go/client.go new file mode 100644 index 00000000..7ef4de6c --- /dev/null +++ b/autocert/examples/hello-mtls/go/client.go @@ -0,0 +1,85 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + "time" +) + +const ( + autocertFile = "/var/run/autocert.step.sm/site.crt" + autocertKey = "/var/run/autocert.step.sm/site.key" + autocertRoot = "/var/run/autocert.step.sm/root.crt" + requestFrequency = 5 * time.Second +) + +func loadRootCertPool() (*x509.CertPool, error) { + root, err := ioutil.ReadFile(autocertRoot) + if err != nil { + return nil, err + } + + pool := x509.NewCertPool() + if ok := pool.AppendCertsFromPEM(root); !ok { + return nil, errors.New("Missing or invalid root certificate") + } + + return pool, nil +} + +func main() { + url := os.Getenv("HELLO_MTLS_URL") + + // Read our leaf certificate and key from disk + cert, err := tls.LoadX509KeyPair(autocertFile, autocertKey) + if err != nil { + log.Fatal(err) + } + + // Read the root certificate for our CA from disk + roots, err := loadRootCertPool() + if err != nil { + log.Fatal(err) + } + + // Create an HTTPS client using our cert, key & pool + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: roots, + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + }, + }, + }, + } + + for { + // Make request + r, err := client.Get(url) + if err != nil { + log.Fatal(err) + } + + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%s: %s\n", time.Now().Format(time.RFC3339), strings.Trim(string(body), "\n")) + + time.Sleep(requestFrequency) + } +} diff --git a/autocert/examples/hello-mtls/go/hello-mtls.client.yaml b/autocert/examples/hello-mtls/go/hello-mtls.client.yaml new file mode 100644 index 00000000..68f84450 --- /dev/null +++ b/autocert/examples/hello-mtls/go/hello-mtls.client.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hello-mtls-client + labels: {app: hello-mtls-client} +spec: + replicas: 1 + selector: {matchLabels: {app: hello-mtls-client}} + template: + metadata: + annotations: + autocert.step.sm/name: hello-mtls-client.default.pod.cluster.local + labels: {app: hello-mtls-client} + spec: + containers: + - name: hello-mtls-client + image: hello-mtls-client-go:latest + imagePullPolicy: Never + resources: {requests: {cpu: 10m, memory: 20Mi}} + env: + - name: HELLO_MTLS_URL + value: https://hello-mtls.default.svc.cluster.local diff --git a/autocert/examples/hello-mtls/go/hello-mtls.server.yaml b/autocert/examples/hello-mtls/go/hello-mtls.server.yaml new file mode 100644 index 00000000..4f19880e --- /dev/null +++ b/autocert/examples/hello-mtls/go/hello-mtls.server.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Service +metadata: + labels: {app: hello-mtls} + name: hello-mtls +spec: + type: ClusterIP + ports: + - port: 443 + targetPort: 443 + selector: {app: hello-mtls} + +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hello-mtls + labels: {app: hello-mtls} +spec: + replicas: 1 + selector: {matchLabels: {app: hello-mtls}} + template: + metadata: + annotations: + autocert.step.sm/name: hello-mtls.default.svc.cluster.local + labels: {app: hello-mtls} + spec: + containers: + - name: hello-mtls + image: hello-mtls-server-go:latest + imagePullPolicy: Never + resources: {requests: {cpu: 10m, memory: 20Mi}} diff --git a/autocert/examples/hello-mtls/go/server.go b/autocert/examples/hello-mtls/go/server.go new file mode 100644 index 00000000..6c10a3e3 --- /dev/null +++ b/autocert/examples/hello-mtls/go/server.go @@ -0,0 +1,136 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "sync" + "time" +) + +const ( + autocertFile = "/var/run/autocert.step.sm/site.crt" + autocertKey = "/var/run/autocert.step.sm/site.key" + autocertRoot = "/var/run/autocert.step.sm/root.crt" + tickFrequency = 15 * time.Second +) + +// Uses techniques from https://diogomonica.com/2017/01/11/hitless-tls-certificate-rotation-in-go/ +// to automatically rotate certificates when they're renewed. + +type rotator struct { + sync.Mutex + certificate *tls.Certificate +} + +func (r *rotator) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + r.Lock() + defer r.Unlock() + + return r.certificate, nil +} + +func (r *rotator) loadCertificate(certFile, keyFile string) error { + r.Lock() + defer r.Unlock() + + c, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return err + } + + r.certificate = &c + + return nil +} + +func loadRootCertPool() (*x509.CertPool, error) { + root, err := ioutil.ReadFile(autocertRoot) + if err != nil { + return nil, err + } + + pool := x509.NewCertPool() + if ok := pool.AppendCertsFromPEM(root); !ok { + return nil, errors.New("Missing or invalid root certificate") + } + + return pool, nil +} + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { + fmt.Fprintf(w, "Unauthenticated") + } else { + name := r.TLS.PeerCertificates[0].Subject.CommonName + fmt.Fprintf(w, "Hello, %s!\n", name) + } + }) + mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Ok\n") + }) + + roots, err := loadRootCertPool() + if err != nil { + log.Fatal(err) + } + + r := &rotator{} + cfg := &tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: roots, + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, + PreferServerCipherSuites: true, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + }, + GetCertificate: r.getCertificate, + } + srv := &http.Server{ + Addr: ":443", + Handler: mux, + TLSConfig: cfg, + } + + // Load certificate + err = r.loadCertificate(autocertFile, autocertKey) + if err != nil { + log.Fatal("Error loading certificate and key", err) + } + + // Schedule periodic re-load of certificate + done := make(chan struct{}) + go func() { + ticker := time.NewTicker(tickFrequency) + defer ticker.Stop() + for { + select { + case <-ticker.C: + fmt.Println("Checking for new certificate...") + err := r.loadCertificate(autocertFile, autocertKey) + if err != nil { + log.Println("Error loading certificate and key", err) + } + case <- done: + return + } + } + }() + defer close(done) + + log.Println("Listening no :443") + + // Start serving HTTPS + err = srv.ListenAndServeTLS("", "") + if err != nil { + log.Fatal("ListenAndServerTLS: ", err) + } +}