hello-mtls examples

This commit is contained in:
Mike Malone 2019-01-24 17:22:36 -08:00
parent 8e1505d03f
commit f58000c28f
10 changed files with 389 additions and 0 deletions

View file

@ -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-<lang> .
docker build -f Dockerfile.client -t hello-mtls-client-<lang> .
```
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

View file

@ -0,0 +1,5 @@
FROM alpine
RUN apk add --no-cache bash curl
COPY client.sh .
RUN chmod +x client.sh
ENTRYPOINT ./client.sh

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
}
}

View file

@ -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

View file

@ -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}}

View file

@ -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)
}
}