Merge pull request #79 from smallstep/autocert-move

Autocert move
This commit is contained in:
Mariano Cano 2019-06-18 17:33:02 -07:00 committed by GitHub
commit 5bc867a1aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 5 additions and 4903 deletions

View file

@ -54,10 +54,10 @@ website](https://smallstep.com/certificates) and the [blog
post](https://smallstep.com/blog/step-certificates.html) announcing Step
Certificate Authority.
> ## 🆕 Autocert <a href="autocert/README.md"><img width="50%" src="https://raw.githubusercontent.com/smallstep/certificates/autocert/autocert/autocert-logo.png"></a>
> ## 🆕 Autocert <a href="https://github.com/smallstep/autocert"><img width="50%" src="https://raw.githubusercontent.com/smallstep/autocert/master/autocert-logo.png"></a>
>
> If you're using Kubernetes, make sure you [check out
> autocert](autocert/README.md): a kubernetes add-on that builds on `step
> autocert](https://github.com/smallstep/autocert): a kubernetes add-on that builds on `step
> certificates` to automatically inject TLS/HTTPS certificates into your containers.
## Installation Guide

View file

@ -1,180 +0,0 @@
# Installing `autocert`
### Prerequisites
To get started you'll need [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl) and a cluster running kubernetes `1.9` or later with [admission webhooks](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#admission-webhooks) enabled:
```bash
$ kubectl version --short
Client Version: v1.13.1
Server Version: v1.10.11
$ kubectl api-versions | grep "admissionregistration.k8s.io/v1beta1"
admissionregistration.k8s.io/v1beta1
```
### Install
The easiest way to install `autocert` is to run:
```bash
kubectl run autocert-init -it --rm --image smallstep/autocert-init --restart Never
```
💥 installation complete.
> You might want to [check out what this command does](init/autocert.sh) before running it.
## Manual install
To install manually you'll need to [install step](https://github.com/smallstep/cli#installing) version `0.8.3` or later.
```
$ step version
Smallstep CLI/0.8.3 (darwin/amd64)
Release Date: 2019-01-16 01:46 UTC
```
### Create a CA
Set your `STEPPATH` to a working directory where we can stage our CA artifacts before we push them to kubernetes. You can delete this directory once installation is complete.
```
$ export STEPPATH=$(mktemp -d /tmp/step.XXX)
$ step path
/tmp/step.0kE
```
Run `step ca init` to generate a root certificate and CA configuration for your cluster. You'll be prompted for a password that will be used to encrypt key material.
```
$ step ca init \
--name Autocert \
--dns "ca.step.svc.cluster.local,127.0.0.1" \
--address ":4443" \
--provisioner admin \
--with-ca-url "ca.step.svc.cluster.local"
```
For older versions of `step` run this command without the flags.
Add provisioning credentials for use by `autocert`. You'll be prompted for a password for `autocert`.
```
$ step ca provisioner add autocert --create
```
For older versions of `step`:
* Run `step ca init` and follow prompts
* Edit `$(step path)/config/ca.json` and change base paths to `/home/step`
* Edit `$(step path)/config/defaults.json` to change base paths to `/home/step` and remove port from CA URL
```
$ sed -i "" "s|$(step path)|/home/step/.step|g" $(step path)/config/ca.json
$ sed -i "" "s|$(step path)|/home/step/.step|g" $(step path)/config/defaults.json
$ sed -i "" "s|ca.step.svc.cluster.local:4443|ca.step.svc.cluster.local|" $(step path)/config/defaults.json
```
### Install the CA in Kubernetes
We'll be creating a new kubernetes namespace and setting up some RBAC rules during installation. You'll need appropriate permissions in your cluster (e.g., you may need to be cluster-admin). GKE, in particular, does not give the cluster owner these rights by default. You can give yourself cluster-admin rights on GKE by running:
```bash
kubectl create clusterrolebinding cluster-admin-binding \
--clusterrole cluster-admin \
--user $(gcloud config get-value account)
```
We'll install our CA and the `autocert` controller in the `step` namespace.
```
$ kubectl create namespace step
```
To install the CA we need to configmap the CA certificates, signing keys, and configuration artifacts. Note that key material is encrypted so we don't need to use secrets.
```
$ kubectl -n step create configmap config --from-file $(step path)/config
$ kubectl -n step create configmap certs --from-file $(step path)/certs
$ kubectl -n step create configmap secrets --from-file $(step path)/secrets
```
But we will need to create secrets for the CA and autocert to decrypt their keys:
```
$ kubectl -n step create secret generic ca-password --from-literal password=<ca-password>
$ kubectl -n step create secret generic autocert-password --from-literal password=<autocert-password>
```
Where `<ca-password>` is the password you entered during `step ca init` and `<autocert-password>` is the password you entered during `step ca provisioner add`.
Next, we'll install the CA.
```
$ kubectl apply -f https://raw.githubusercontent.com/smallstep/certificates/master/autocert/install/01-step-ca.yaml
```
Once you've done this you can delete the temporary `$STEPPATH` directory and `unset STEPPATH` (though you may want to retain it as a backup).
### Install `autocert` in Kubernetes
Install the `autocert` controller.
```
$ kubectl apply -f https://raw.githubusercontent.com/smallstep/certificates/master/autocert/install/02-autocert.yaml
```
Autocert creates secrets containing single-use bootstrap tokens for pods to authenticate with the CA and obtain a certificate. The tokens are automatically cleaned up after they expire. To do this, `autocert` needs permission to create and delete secrets in your cluster.
If you have RBAC enabled in your cluster, apply `rbac.yaml` to give `autocert` these permissions.
```
$ kubectl apply -f https://raw.githubusercontent.com/smallstep/certificates/master/autocert/install/03-rbac.yaml
```
Finally, register the `autocert` mutation webhook with kubernetes.
```
$ cat <<EOF | kubectl apply -f -
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: autocert-webhook-config
labels: {app: autocert}
webhooks:
- name: autocert.step.sm
clientConfig:
service:
name: autocert
namespace: step
path: "/mutate"
caBundle: $(cat $(step path)/certs/root_ca.crt | base64)
rules:
- operations: ["CREATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
failurePolicy: Ignore
namespaceSelector:
matchLabels:
autocert.step.sm: enabled
EOF
```
### Check your work
If everything worked you should have CA and controller pods running in the `step` namespace and your webhook configuration should be installed:
```
$ kubectl -n step get pods
NAME READY STATUS RESTARTS AGE
ca-7577d7d667-vtfq5 1/1 Running 0 1m
controller-86bd99bd96-s9zlc 1/1 Running 0 28s
$ kubectl get mutatingwebhookconfiguration
NAME CREATED AT
autocert-webhook-config 2019-01-17T22:57:57Z
```
### Move on to usage instructions
Make sure to follow the autocert usage steps at https://github.com/smallstep/certificates/tree/master/autocert#usage

View file

@ -1,414 +1,4 @@
![Autocert architecture diagram](https://raw.githubusercontent.com/smallstep/certificates/autocert/autocert/autocert-logo.png)
# ⚠️ Autocert has moved to https://github.com/smallstep/autocert
# Autocert
[![GitHub release](https://img.shields.io/github/release/smallstep/certificates.svg)](https://github.com/smallstep/certificates/releases)
[![Join the chat at https://gitter.im/smallstep/community](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/smallstep/community)
[![CA Image](https://images.microbadger.com/badges/image/smallstep/step-ca.svg)](https://microbadger.com/images/smallstep/step-ca)
[![Go Report Card](https://goreportcard.com/badge/github.com/smallstep/certificates)](https://goreportcard.com/report/github.com/smallstep/certificates)
[![GitHub stars](https://img.shields.io/github/stars/smallstep/certificates.svg?style=social)](https://github.com/smallstep/certificates/stargazers)
[![Twitter followers](https://img.shields.io/twitter/follow/smallsteplabs.svg?label=Follow&style=social)](https://twitter.com/intent/follow?screen_name=smallsteplabs)
**Autocert** is a kubernetes add-on that automatically injects TLS/HTTPS certificates into your containers.
To get a certificate **simply annotate your pods** with a name. An X.509 (TLS/HTTPS) certificate is automatically created and mounted at `/var/run/autocert.step.sm/` along with a corresponding private key and root certificate (everything you need for [mTLS](#motivation)).
We ❤️ feedback. Please [report bugs](https://github.com/smallstep/certificates/issues/new?template=autocert_bug.md) & [suggest enhancements](https://github.com/smallstep/certificates/issues/new?template=autocert_enhancement.md). [Fork](https://github.com/smallstep/certificates/fork) and send a PR. [Give us a ⭐](https://github.com/smallstep/certificates/stargazers) if you like what we're doing.
![Autocert demo gif](https://raw.githubusercontent.com/smallstep/certificates/autocert/autocert/demo.gif)
## Motivation
`Autocert` exists to **make it easy to use mTLS** ([mutual TLS](examples/hello-mtls/README.md#mutual-tls)) to **improve security** within a cluster and to **secure communication into, out of, and between kubernetes clusters**.
TLS (and HTTPS, which is HTTP over TLS) provides _authenticated encryption_: an _identity dialtone_ and _end-to-end encryption_ for your workloads. It's like a secure line with caller ID. This has all sorts of benefits: better security, compliance, and easier auditability for starters. It **makes workloads identity-aware**, improving observability and enabling granular access control. Perhaps most compelling, mTLS lets you securely communicate with workloads running anywhere, not just inside kubernetes.
![Connect with mTLS diagram](https://raw.githubusercontent.com/smallstep/certificates/autocert/autocert/connect-with-mtls.png)
Unlike VPNs & SDNs, deploying and scaling mTLS is pretty easy. You're (hopefully) already using TLS, and your existing tools and standard libraries will provide most of what you need. If you know how to operate DNS and reverse proxies, you know how to operate mTLS infrastructure.
There's just one problem: **you need certificates issued by your own certificate authority (CA)**. Building and operating a CA, issuing certificates, and making sure they're renewed before they expire is tricky. `Autocert` does all of this for you.
## Features
First and foremost, `autocert` is easy. You can **get started in minutes**.
`Autocert` uses [`step certificates`](https://github.com/smallstep/certificates) to generate keys and issue certificates. This process is secure and automatic, all you have to do is [install autocert](#install) and [annotate your pods](#enable-autocert-per-namespace).
Features include:
* A fully featured private **certificate authority** (CA) for workloads running on kubernetes and elsewhere
* [RFC5280](https://tools.ietf.org/html/rfc5280) and [CA/Browser Forum](https://cabforum.org/baseline-requirements-documents/) compliant certificates that work **for TLS**
* Namespaced installation into the `step` namespace so it's **easy to lock down** your CA
* Short-lived certificates with **fully automated** enrollment and renewal
* Private keys are never transmitted across the network and aren't stored in `etcd`
Because `autocert` is built on [`step certificates`](https://github.com/smallstep/certificates) you can easily [extend access](#connecting-from-outside-the-cluster) to developers, endpoints, and workloads running outside your cluster, too.
## Getting Started
> ⚠️ Warning: *this project is in **ALPHA**. DON'T use it for anything mission critical. EXPECT breaking changes in minor revisions with little or no warning. PLEASE [provide feedback](https://github.com/smallstep/certificates/issues/new?template=autocert_enhancement.md).*
### Prerequisites
All you need to get started is [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl) and a cluster running kubernetes `1.9` or later with [admission webhooks](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#admission-webhooks) enabled:
```bash
$ kubectl version --short
Client Version: v1.13.1
Server Version: v1.10.11
$ kubectl api-versions | grep "admissionregistration.k8s.io/v1beta1"
admissionregistration.k8s.io/v1beta1
```
### Install
To install `autocert` run:
```bash
kubectl run autocert-init -it --rm --image smallstep/autocert-init --restart Never
```
💥 installation complete.
> You might want to [check out what this command does](init/autocert.sh) before running it. You can also [install `autocert` manually](INSTALL.md#manual-install) if that's your style.
## Usage
Using `autocert` is also easy:
* Enable `autocert` for a namespace by labelling it with `autocert.step.sm=enabled`, then
* Inject certificates into containers by annotating pods with `autocert.step.sm/name: <name>`
### Enable autocert (per namespace)
To enable `autocert` for a namespace it must be labelled `autocert.step.sm=enabled`.
To label the `default` namespace run:
```bash
kubectl label namespace default autocert.step.sm=enabled
```
To check which namespaces have `autocert` enabled run:
```bash
$ kubectl get namespace -L autocert.step.sm
NAME STATUS AGE AUTOCERT.STEP.SM
default Active 59m enabled
...
```
### Annotate pods to get certificates
To get a certificate you need to tell `autocert` your workload's name using the `autocert.step.sm/name` annotation (this name will appear as the X.509 common name and SAN).
Let's deploy a [simple mTLS server](examples/hello-mtls/go/server/server.go) named `hello-mtls.default.svc.cluster.local`:
```yaml
cat <<EOF | kubectl apply -f -
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 ANNOTATION HERE -v ###############################
autocert.step.sm/name: hello-mtls.default.svc.cluster.local #
# AUTOCERT ANNOTATION HERE -^ ###############################
labels: {app: hello-mtls}
spec:
containers:
- name: hello-mtls
image: smallstep/hello-mtls-server-go:latest
EOF
```
In our new container we should find a certificate, private key, and root certificate mounted at `/var/run/autocert.step.sm`:
```bash
$ export HELLO_MTLS=$(kubectl get pods -l app=hello-mtls -o jsonpath={$.items[0].metadata.name})
$ kubectl exec -it $HELLO_MTLS -c hello-mtls -- ls /var/run/autocert.step.sm
root.crt site.crt site.key
```
We're done. Our container has a certificate, issued by our CA, which `autocert` will automatically renew.
✅ Certificates.
## Hello mTLS
It's easy to deploy certificates using `autocert`, but it's up to you to use them correctly. To get you started, [`hello-mtls`](examples/hello-mtls) demonstrates the right way to use mTLS with various tools and languages (contributions welcome :). If you're a bit fuzzy on how mTLS works, [the `hello-mtls` README](examples/hello-mtls/README.md) is a great place to start.
To finish out this tutorial let's keep things simple and try `curl`ing the server we just deployed from inside and outside the cluster.
### Connecting from inside the cluster
First, let's expose our workload to the rest of the cluster using a service:
```
kubectl expose deployment hello-mtls --port 443
```
Now let's deploy a client, with its own certificate, that [`curl`s our server in a loop](examples/hello-mtls/curl/client.sh):
```yaml
cat <<EOF | kubectl apply -f -
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 ANNOTATION HERE -v ######################################
autocert.step.sm/name: hello-mtls-client.default.pod.cluster.local #
# AUTOCERT ANNOTATION HERE -^ ######################################
labels: {app: hello-mtls-client}
spec:
containers:
- name: hello-mtls-client
image: smallstep/hello-mtls-client-curl:latest
env: [{name: HELLO_MTLS_URL, value: https://hello-mtls.default.svc.cluster.local}]
EOF
```
> Note that **the authority portion of the URL** (the `HELLO_MTLS_URL` env var) **matches the name of the server we're connecting to** (both are `hello-mtls.default.svc.cluster.local`). That's required for standard HTTPS and can sometimes require some DNS trickery.
Once deployed we should start seeing the client log responses from the server [saying hello](examples/hello-mtls/go/server/server.go#L71-L72):
```
$ export HELLO_MTLS_CLIENT=$(kubectl get pods -l app=hello-mtls-client -o jsonpath={$.items[0].metadata.name})
$ kubectl logs $HELLO_MTLS_CLIENT -c hello-mtls-client
Thu Feb 7 23:35:23 UTC 2019: Hello, hello-mtls-client.default.pod.cluster.local!
Thu Feb 7 23:35:28 UTC 2019: Hello, hello-mtls-client.default.pod.cluster.local!
```
For kicks, let's `exec` into this pod and try `curl`ing ourselves:
```
$ kubectl exec $HELLO_MTLS_CLIENT -c hello-mtls-client -- 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 \
https://hello-mtls.default.svc.cluster.local
Hello, hello-mtls-client.default.pod.cluster.local!
```
✅ mTLS inside cluster.
### Connecting from outside the cluster
Connecting from outside the cluster is a bit more complicated. We need to handle DNS and obtain a certificate ourselves. These tasks were handled automatically inside the cluster by kubernetes and `autocert`, respectively.
That said, because our server uses mTLS **only clients that have a certificate issued by our certificate authority will be allowed to connect**. That means it can be safely and easily exposed directly to the public internet using a [LoadBalancer service type](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer):
```
kubectl expose deployment hello-mtls --name=hello-mtls-lb --port=443 --type=LoadBalancer
```
To connect we need a certificate. There are a [couple](RUNBOOK.md#federation) [different](RUNBOOK.md#multiple-intermediates) [ways](RUNBOOK.md#exposing-the-ca) to get one, but for simplicity we'll just forward a port.
```
kubectl -n step port-forward $(kubectl -n step get pods -l app=ca -o jsonpath={$.items[0].metadata.name}) 4443:4443
```
In another window we'll use `step` to grab the root certificate, generate a key pair, and get a certificate.
> To follow along you'll need to [`install step`](https://github.com/smallstep/cli#installing) if you haven't already. You'll also need your admin password and CA fingerprint, which were output during installation (see [here](RUNBOOK.md#recover-admin-and-ca-password) and [here](RUNBOOK.md#recompute-root-certificate-fingerprint) if you already lost them :).
```bash
$ export CA_POD=$(kubectl -n step get pods -l app=ca -o jsonpath={$.items[0].metadata.name})
$ step ca root root.crt --ca-url https://127.0.0.1:4443 --fingerprint <fingerprint>
$ step ca certificate mike mike.crt mike.key --ca-url https://127.0.0.1:4443 --root root.crt
✔ Key ID: H4vH5VfvaMro0yrk-UIkkeCoPFqEfjF6vg0GHFdhVyM (admin)
✔ Please enter the password to decrypt the provisioner key: 0QOC9xcq56R1aEyLHPzBqN18Z3WfGZ01
✔ CA: https://127.0.0.1:4443/1.0/sign
✔ Certificate: mike.crt
✔ Private Key: mike.key
```
Now we can simply `curl` the service:
> If you're using minikube or docker for mac the load balancer's "IP" might be `localhost`, which won't work. In that case, simply `export HELLO_MTLS_IP=127.0.0.1` and try again.
```
$ export HELLO_MTLS_IP=$(kubectl get svc hello-mtls-lb -ojsonpath={$.status.loadBalancer.ingress[0].ip})
$ curl --resolve hello-mtls.default.svc.cluster.local:443:$HELLO_MTLS_IP \
--cacert root.crt \
--cert mike.crt \
--key mike.key \
https://hello-mtls.default.svc.cluster.local
Hello, mike!
```
> Note that we're using `--resolve` to tell `curl` to override DNS and resolve the name in our workload's certificate to its public IP address. In a real production infrastructure you could configure DNS manually, or you could propagate DNS to workloads outside kubernetes using something like [ExternalDNS](https://github.com/kubernetes-incubator/external-dns).
✅ mTLS outside cluster.
<!--- TODO: CTA or Further Reading... Move "How it works" maybe? Or put this below that? --->
### Cleanup & uninstall
To clean up after running through the tutorial remove the `hello-mtls` and `hello-mtls-client` deployments and services:
```
kubectl delete deployment hello-mtls
kubectl delete deployment hello-mtls-client
kubectl delete service hello-mtls
kubectl delete service hello-mtls-lb
```
See the runbook for instructions on [uninstalling `autocert`](RUNBOOK.md#uninstalling).
## How it works
### Architecture
`Autocert` is an [admission webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#admission-webhooks) that intercepts and patches pod creation requests with [some YAML](install/02-autocert.yaml#L26-L44) to inject an [init container](bootstrapper/) and [sidecar](renewer/) that handle obtaining and renewing certificates, respectively.
![Autocert architecture diagram](https://raw.githubusercontent.com/smallstep/certificates/autocert/autocert/autocert-arch.png)
### Enrollment & renewal
It integrates with [`step certificates`](https://github.com/smallstep/certificates) and uses the [one-time token bootstrap protocol](https://smallstep.com/blog/step-certificates.html#automated-certificate-management) from that project to mutually authenticate a new pod with your certificate authority, and obtain a certificate.
![Autocert bootstrap protocol diagram](https://raw.githubusercontent.com/smallstep/certificates/autocert/autocert/autocert-bootstrap.png)
Tokens are [generated by the admission webhook](controller/provisioner.go#L46-L72) and [transmitted to the injected init container via a kubernetes secret](controller/main.go#L91-L125). The init container [uses the one-time token](bootstrapper/bootstrapper.sh) to obtain a certificate. A sidecar is also installed to [renew certificates](renewer/Dockerfile#L8) before they expire. Renewal simply uses mTLS with the CA.
## Further Reading
* We tweet [@smallsteplabs](https://twitter.com/smallsteplabs)
* Read [our blog](https://smallstep.com/blog)
* Check out the [runbook](RUNBOOK.md)
* Check out [`step` CLI](https://github.com/smallstep/cli)
## Questions
#### Wait, so any pod can get a certificate with any identity? How is that secure?
1. Don't give people `kubectl` access to your production clusters
2. Use a deploy pipeline based on `git` artifacts
3. Enforce code review on those `git` artifacts
If that doesn't work for you, or if you have a better idea, we'd love to hear! Please [open an issue](https://github.com/smallstep/certificates/issues/new?template=autocert_enhancement.md)!
#### Why do I have to tell you the name to put in a certificate? Why can't you automatically bind service names?
Mostly because monitoring the API server to figure out which services are associated with which workloads is complicated and somewhat magical. And it might not be what you want.
That said, we're not totally opposed to this idea. If anyone has strong feels and a good design please [open an issue](https://github.com/smallstep/certificates/issues/new?template=autocert_enhancement.md).
#### Doesn't kubernetes already ship with a certificate authority?
Yes, it uses [a bunch of CAs](https://jvns.ca/blog/2017/08/05/how-kubernetes-certificates-work/) for different sorts of control plane communication. Technically, kubernetes doesn't _come with_ a CA. It has integration points that allow you to use any CA (e.g., [Kubernetes the hard way](https://github.com/kelseyhightower/kubernetes-the-hard-way) [uses CFSSL](https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/2983b28f13b294c6422a5600bb6f14142f5e7a26/docs/02-certificate-authority.md). You could use [`step certificates`](https://github.com/smallstep/certificates), which `autocert` is based on, instead.
In any case, these CAs are meant for control plane communication. You could use them for your service-to-service data plane, but it's probably not a good idea.
#### What permissions does `autocert` require in my cluster and why?
`Autocert` needs permission to create and delete secrets cluster-wide. You can [check out our RBAC config here](install/03-rbac.yaml). These permissions are needed in order to transmit one-time tokens to workloads using secrets, and to clean up afterwards. We'd love to scope these permissions down further. If anyone has any ideas please [open an issue](https://github.com/smallstep/certificates/issues/new?template=autocert_enhancement.md).
#### Why does `autocert` create secrets?
The `autocert` admission webhook needs to securely transmit one-time bootstrap tokens to containers. This could be accomplished without using secrets. The webhook returns a [JSONPatch](https://tools.ietf.org/html/rfc6902) response that's applied to the pod spec. This response could patch the literal token value into our init container's environment.
Unfortunately, the kubernetes API server does not authenticate itself to admission webhooks by default, and configuring it to do so [requires passing a custom config file](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#authenticate-apiservers) at apiserver startup. This isn't an option for everyone (e.g., on GKE) so we opted not to rely on it.
Since our webhook can't authenticate callers, including bootstrap tokens in patch responses would be dangerous. By using secrets an attacker can still trick `autocert` into generating superflous bootstrap tokens, but they'd also need read access to cluster secrets to do anything with them.
Hopefully this story will improve with time.
#### Why not use kubernetes service accounts instead of bootstrap tokens?
Great idea! This should be pretty easy to add. However, existing service accounts are [somewhat broken](https://github.com/kubernetes/community/pull/1460) for this use case. The upcoming [TokenRequest API](https://github.com/kubernetes/kubernetes/issues/58790) should fix most of these issues.
TODO: Link to issue for people who want this.
#### Too. many. containers. Why do you need to install an init container and sidecar?
We don't. It's just easier for you. Your containers can generate key pairs, exchange them for certificates, and manage renewals themselves. This is pretty easy if you [install `step`](https://github.com/smallstep/cli#installing) in your containers, or integrate with our [golang SDK](https://godoc.org/github.com/smallstep/certificates/ca). To support this we'd need to add the option to inject a bootstrap token without injecting these containers.
TODO: Link to issue for people who want this.
That said, the init container and sidecar are both super lightweight.
#### Why are keys and certificates managed via volume mounts? Why not use a secret or some custom resource?
Because, by default, kubernetes secrets are stored in plaintext in `etcd` and might even be transmitted unencrypted across the network. Even if secrets were properly encrypted, transmitting a private key across the network violates PKI best practices. Key pairs should always be generated where they're used, and private keys should never be known by anyone but their owners.
That said, there are use cases where a certificate mounted in a secret resource is desirable (e.g., for use with a kubernetes `Ingress`). We may add support for this in the future. However, we think the current method is easier and a better default.
TODO: Link to issue for people who want this.
#### Why not use kubernetes CSR resources for this?
It's harder and less secure. If any good and simple design exists for securely automating CSR approval using this resource we'd love to see it!
#### How is this different than [`cert-manager`](https://github.com/jetstack/cert-manager)
`Cert-manager` is a great project. But it's design is focused on managing Web PKI certificates issued by [Let's Encrypt's](https://letsencrypt.org/) public certificate authority. These certificates are useful for TLS ingress from web browsers. `Autocert` is different. It's purpose-built to manage certificates issued by your own private CA to support the use of mTLS for internal communication (e.g., service-to-service).
#### What sorts of keys are issued and how often are certificates rotated?
`Autocert` builds on `step certificates` which issues ECDSA certificates using the P256 curve with ECDSA-SHA256 signatures by default. If this is all Greek to you, rest assured these are safe, sane, and modern defaults that are suitable for the vast majority of environments.
#### What crypto library is under the hood?
https://golang.org/pkg/crypto/
## Building
This project is based on four docker containers. They use [multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/) so all you need in order to build them is `docker`.
> Caveat: the `controller` container uses [`dep`](https://github.com/golang/dep) and `dep init` isn't run during the build. You'll need to run `dep init` in the `controller/` subdirectory prior to building, and you'll need to run `dep ensure -update` if you change any dependencies.
Building `autocert-controller` (the admission webhook):
```
cd controller
docker build -t smallstep/autocert-controller:latest .
```
Building `autocert-bootstrapper` (the init container that generates a key pair and exchanges a bootstrap token for a certificate):
```
cd bootstrapper
docker build -t smallstep/autocert-bootstrapper:latest .
```
Building `autocert-renewer` (the sidecar that renews certificates):
```
cd renewer
docker build -t smallstep/autocert-renewer:latest .
```
Building `autocert-init` (the install script):
```
cd init
docker build -t smallstep/autocert-init:latest .
```
If you build your own containers you'll probably need to [install manually](INSTALL.md). You'll also need to adjust which images are deployed in the [deployment yaml](install/02-autocert.yaml).
## Contributing
If you have improvements to `autocert`, send us your pull requests! For those just getting started, GitHub has a [howto](https://help.github.com/articles/about-pull-requests/). A team member will review your pull requests, provide feedback, and merge your changes. In order to accept contributions we do need you to [sign our contributor license agreement](https://cla-assistant.io/smallstep/certificates).
If you want to contribute but you're not sure where to start, take a look at the [issues with the "good first issue" label](https://github.com/smallstep/certificates/issues?q=is%3Aopen+label%3A%22good+first+issue%22+label%3Aarea%2Fautocert). These are issues that we believe are particularly well suited for outside contributions, often because we probably won't get to them right now. If you decide to start on an issue, leave a comment so that other people know that you're working on it. If you want to help out, but not alone, use the issue comment thread to coordinate.
If you've identified a bug or have ideas for improving `autocert` that you don't have time to implement, we'd love to hear about them. Please open an issue to [report a bug](https://github.com/smallstep/certificates/issues/new?template=autocert_bug.md) or [suggest an enhancement](https://github.com/smallstep/certificates/issues/new?template=autocert_enhancement.md)!
## License
Copyright 2019 Smallstep Labs
Licensed under [the Apache License, Version 2.0](https://github.com/smallstep/certificates/blob/master/LICENSE)
If you're looking for hello-mTLS examples they're at
https://github.com/smallstep/autocert/tree/master/examples/hello-mtls

View file

@ -1,122 +0,0 @@
# Runbook
## Common admin tasks
#### Recover `admin` and CA password
```
kubectl -n step get secret ca-password -o jsonpath='{$.data.password}' | base64 -D
```
#### Recover `autocert` password
```
kubectl -n step get secret autocert-password -o jsonpath='{$.data.password}' | base64 -D
```
#### Recompute root certificate fingerprint
```
export CA_POD=$(kubectl -n step get pods -l app=ca -o jsonpath={$.items[0].metadata.name})
kubectl -n step exec -it $CA_POD step certificate fingerprint /home/step/.step/certs/root_ca.crt
```
> Tip: Some slight fanciness is necessary to trim this string if you want to put it into an environment variable:
>
> ```
> export FINGERPRINT="$(kubectl -n step exec -it $CA_POD step certificate fingerprint /home/step/.step/certs/root_ca.crt | tr -d '[:space:]')"
> ```
#### Inspect a certificate
```
kubectl exec -it <pod> -c autocert-renewer -- step certificate inspect /var/run/autocert.step.sm/site.crt
```
#### Labelling a namespace (enabling `autocert` for a namespace)
To enable `autocert` for a namespace it must be labelled. To label an existing namespace run:
```
kubectl label namespace <namespace> autocert.step.sm=enabled
```
#### Checking which namespaces are labelled
```
kubectl get namespace -L autocert.step.sm
```
#### Removing a label from a namespace (disabling `autocert` for a namespace)
```
kubectl label namespace <namespace> autocert.step.sm-
```
#### Naming considerations
Use hostnames. Must be global. Everyone who connects to the service using mTLS must use the same hostname. For internal communication it's easy enough to use the FQDN of a service. For stuff you expose publicly you'll need to manage DNS yourself...
In any case, the critical invariant is: ...
Diagram here?
#### Cleaning up one-time token secrets
```
for ns in $(kubectl get namespace --selector autocert.step.sm=enabled -o jsonpath='{$.items[*].metadata.name}'); do
kubectl -n "$ns" delete secrets --selector="autocert.step.sm/token=true"
done
```
### TODO:
* Change admin password
* Change autocert password
* Federating with another CA
* DNS tips and tricks
* Multiple SANs
* Getting rid of the sidecar
* Getting logs from the CA (certificates weren't issued)
* Getting logs from the init container / renewer (didn't start properly)
* Adjusting certificate expiration (default 24h)
* Remove label
* Clean up secrets
* Naming considerations (maybe this should be in hello-mtls)
## Federation
TODO: Example of federating a CA running in kubernetes with another CA.
For now, see https://smallstep.com/blog/step-v0.8.3-federation-root-rotation.html
## Multiple intermediates
TODO: Example of creating an additional intermediate signing certificate off of our kubernetes root CA.
For now, see https://smallstep.com/docs/cli/ca/init/ (specifically, the `--root` flag)
## Exposing the CA
Beware that the CA exposes an unauthenticated endpoint that lists your configured provisioners and their encrypted private keys. For this reason, you may not want to expose it directly to the public internet.
## Uninstalling
To uninstall `autocert` completely simply delete the mutating webhook configuration, the `step` namespace and the `autocert` RBAC artifacts:
```
kubectl delete mutatingwebhookconfiguration autocert-webhook-config
kubectl delete namespace step
kubectl delete clusterrolebinding autocert-controller
kubectl delete clusterrole autocert-controller
```
Remove any namespace labels and clean up any stray secrets that `autocert` hasn't cleaned up yet:
```
for ns in $(kubectl get namespace --selector autocert.step.sm=enabled -o jsonpath='{$.items[*].metadata.name}'); do
kubectl label namespace "$ns" autocert.step.sm-
kubectl -n "$ns" delete secrets --selector="autocert.step.sm/token=true"
done
```
Any remaining sidecar containers will go away once you remove annotations and re-deploy your workloads.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 566 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

View file

@ -1,10 +0,0 @@
FROM smallstep/step-cli:0.9.0
USER root
ENV CRT="/var/run/autocert.step.sm/site.crt"
ENV KEY="/var/run/autocert.step.sm/site.key"
ENV STEP_ROOT="/var/run/autocert.step.sm/root.crt"
COPY bootstrapper.sh /home/step/
RUN chmod +x /home/step/bootstrapper.sh
CMD ["/home/step/bootstrapper.sh"]

View file

@ -1,7 +0,0 @@
#!/bin/sh
# Download the root certificate and set permissions
step ca certificate $COMMON_NAME $CRT $KEY
chmod 644 $CRT $KEY
step ca root $STEP_ROOT

Binary file not shown.

Before

Width:  |  Height:  |  Size: 669 KiB

View file

@ -1,19 +0,0 @@
# build stage
FROM golang:alpine AS build-env
RUN apk update && apk upgrade && \
apk add --no-cache git
RUN go get -u github.com/golang/dep/cmd/dep
WORKDIR $GOPATH/src/github.com/step-certificates-k8s/controller
# copy dep files and run dep separately from code for better caching
COPY Gopkg.toml Gopkg.lock ./
RUN dep ensure --vendor-only
COPY . ./
RUN go build -o /server .
# final stage
FROM smallstep/step-cli:0.9.0
ENV STEPPATH="/home/step/.step"
ENV PWDPATH="/home/step/password/password"
ENV CONFIGPATH="/home/step/autocert/config.yaml"
COPY --from=build-env /server .
ENTRYPOINT ./server $CONFIGPATH

View file

@ -1,514 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:9afc639ef88d907f2e87ab68cbc63117b88d0d84238fd6b08224515d00a8136a"
name = "github.com/alecthomas/gometalinter"
packages = ["."]
pruneopts = "UT"
revision = "df395bfa67c5d0630d936c0044cf07ff05086655"
version = "v3.0.0"
[[projects]]
branch = "master"
digest = "1:c198fdc381e898e8fb62b8eb62758195091c313ad18e52a3067366e1dda2fb3c"
name = "github.com/alecthomas/units"
packages = ["."]
pruneopts = "UT"
revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
[[projects]]
branch = "master"
digest = "1:454adc7f974228ff789428b6dc098638c57a64aa0718f0bd61e53d3cd39d7a75"
name = "github.com/chzyer/readline"
packages = ["."]
pruneopts = "UT"
revision = "2972be24d48e78746da79ba8e24e8b488c9880de"
[[projects]]
digest = "1:848ef40f818e59905140552cc49ff3dc1a15f955e4b56d1c5c2cc4b54dbadf0c"
name = "github.com/client9/misspell"
packages = [
".",
"cmd/misspell",
]
pruneopts = "UT"
revision = "b90dc15cfd220ecf8bbc9043ecb928cef381f011"
version = "v0.3.4"
[[projects]]
digest = "1:2cd7915ab26ede7d95b8749e6b1f933f1c6d5398030684e6505940a10f31cfda"
name = "github.com/ghodss/yaml"
packages = ["."]
pruneopts = "UT"
revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:41cf598af650689375f647ed7ed87951e3aedb8d5f40400b6e81a84410650626"
name = "github.com/go-chi/chi"
packages = ["."]
pruneopts = "UT"
revision = "d0891661345200ebf8b816cc7de785e9bd570647"
[[projects]]
digest = "1:b7a8552c62868d867795b63eaf4f45d3e92d36db82b428e680b9c95a8c33e5b1"
name = "github.com/gogo/protobuf"
packages = [
"proto",
"sortkeys",
]
pruneopts = "UT"
revision = "342cbe0a04158f6dcb03ca0079991a51a4248c02"
version = "v0.5"
[[projects]]
branch = "travis-1.9"
digest = "1:e8f5d9c09a7209c740e769713376abda388c41b777ba8e9ed52767e21acf379f"
name = "github.com/golang/lint"
packages = [
".",
"golint",
]
pruneopts = "UT"
revision = "883fe33ffc4344bad1ecd881f61afd5ec5d80e0a"
[[projects]]
branch = "master"
digest = "1:3ee90c0d94da31b442dde97c99635aaafec68d0b8a3c12ee2075c6bdabeec6bb"
name = "github.com/google/gofuzz"
packages = ["."]
pruneopts = "UT"
revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1"
[[projects]]
digest = "1:750e747d0aad97b79f4a4e00034bae415c2ea793fd9e61438d966ee9c79579bf"
name = "github.com/google/shlex"
packages = ["."]
pruneopts = "UT"
revision = "6f45313302b9c56850fc17f99e40caebce98c716"
[[projects]]
branch = "master"
digest = "1:824d147914b40e56e9e1eebd602bc6bb9761989d52fd8e4a498428467980eb17"
name = "github.com/gordonklaus/ineffassign"
packages = ["."]
pruneopts = "UT"
revision = "1003c8bd00dc2869cb5ca5282e6ce33834fed514"
[[projects]]
digest = "1:eaefc85d32c03e5f0c2b88ea2f79fce3d993e2c78316d21319575dd4ea9153ca"
name = "github.com/json-iterator/go"
packages = ["."]
pruneopts = "UT"
revision = "ab8a2e0c74be9d3be70b3184d9acc634935ded82"
version = "1.1.4"
[[projects]]
branch = "master"
digest = "1:e51f40f0c19b39c1825eadd07d5c0a98a2ad5942b166d9fc4f54750ce9a04810"
name = "github.com/juju/ansiterm"
packages = [
".",
"tabwriter",
]
pruneopts = "UT"
revision = "720a0952cc2ac777afc295d9861263e2a4cf96a1"
[[projects]]
digest = "1:31e761d97c76151dde79e9d28964a812c46efc5baee4085b86f68f0c654450de"
name = "github.com/konsorten/go-windows-terminal-sequences"
packages = ["."]
pruneopts = "UT"
revision = "f55edac94c9bbba5d6182a4be46d86a2c9b5b50e"
version = "v1.0.2"
[[projects]]
digest = "1:2dc8db55c5b223e6cd50aa7915e698cfcf56d1ddfa89bbbf65e24729e0a0200a"
name = "github.com/lunixbochs/vtclean"
packages = ["."]
pruneopts = "UT"
revision = "88cfb0c2efe8ed7b0ccf0af83db39359829027bb"
version = "v1.0.0"
[[projects]]
digest = "1:2a2a76072bd413b3484a0b5bb2fbb078b0b7dd8950e9276c900e14dce2354679"
name = "github.com/manifoldco/promptui"
packages = [
".",
"list",
"screenbuf",
]
pruneopts = "UT"
revision = "20f2a94120aa14a334121a6de66616a7fa89a5cd"
version = "v0.3.2"
[[projects]]
digest = "1:2fa7b0155cd54479a755c629de26f888a918e13f8857a2c442205d825368e084"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = "UT"
revision = "3a70a971f94a22f2fa562ffcc7a0eb45f5daf045"
version = "v0.1.1"
[[projects]]
digest = "1:e150b5fafbd7607e2d638e4e5cf43aa4100124e5593385147b0a74e2733d8b0d"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "UT"
revision = "c2a7a6ca930a4cd0bc33a3f298eb71960732a3a7"
version = "v0.0.7"
[[projects]]
digest = "1:33422d238f147d247752996a26574ac48dcf472976eda7f5134015f06bf16563"
name = "github.com/modern-go/concurrent"
packages = ["."]
pruneopts = "UT"
revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94"
version = "1.0.3"
[[projects]]
digest = "1:c56ad36f5722eb07926c979d5e80676ee007a9e39e7808577b9d87ec92b00460"
name = "github.com/modern-go/reflect2"
packages = ["."]
pruneopts = "UT"
revision = "94122c33edd36123c84d5368cfb2b69df93a0ec8"
version = "v1.0.1"
[[projects]]
digest = "1:266d082179f3a29a4bdcf1dcc49d4a304f5c7107e65bd22d1fecacf45f1ac348"
name = "github.com/newrelic/go-agent"
packages = [
".",
"internal",
"internal/cat",
"internal/jsonx",
"internal/logger",
"internal/sysinfo",
"internal/utilization",
]
pruneopts = "UT"
revision = "f5bce3387232559bcbe6a5f8227c4bf508dac1ba"
version = "v1.11.0"
[[projects]]
digest = "1:07140002dbf37da92090f731b46fa47be4820b82fe5c14a035203b0e813d0ec2"
name = "github.com/nicksnyder/go-i18n"
packages = [
"i18n",
"i18n/bundle",
"i18n/language",
"i18n/translation",
]
pruneopts = "UT"
revision = "0dc1626d56435e9d605a29875701721c54bc9bbd"
version = "v1.10.0"
[[projects]]
digest = "1:95741de3af260a92cc5c7f3f3061e85273f5a81b5db20d4bd68da74bd521675e"
name = "github.com/pelletier/go-toml"
packages = ["."]
pruneopts = "UT"
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
version = "v1.2.0"
[[projects]]
digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = "UT"
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4"
version = "v0.8.1"
[[projects]]
digest = "1:2e76a73cb51f42d63a2a1a85b3dc5731fd4faf6821b434bd0ef2c099186031d6"
name = "github.com/rs/xid"
packages = ["."]
pruneopts = "UT"
revision = "15d26544def341f036c5f8dca987a4cbe575032c"
version = "v1.2.1"
[[projects]]
branch = "master"
digest = "1:8baa3b16f20963c54e296627ea1dabfd79d1b486f81baf8759e99d73bddf2687"
name = "github.com/samfoo/ansi"
packages = ["."]
pruneopts = "UT"
revision = "b6bd2ded7189ce35bc02233b554eb56a5146af73"
[[projects]]
digest = "1:9421f6e9e28ef86933e824b5caff441366f2b69bb281085b9dca40e1f27a1602"
name = "github.com/shurcooL/sanitized_anchor_name"
packages = ["."]
pruneopts = "UT"
revision = "7bfe4c7ecddb3666a94b053b422cdd8f5aaa3615"
version = "v1.0.0"
[[projects]]
digest = "1:e4c72127d910a96daf869a44f3dd563b86dbe6931a172863a0e99c5ff04b59e4"
name = "github.com/sirupsen/logrus"
packages = ["."]
pruneopts = "UT"
revision = "dae0fa8d5b0c810a8ab733fbd5510c7cae84eca4"
version = "v1.4.0"
[[projects]]
digest = "1:b66ff4abd77f39e94020219427c0c62bc1474d41edb2b445d2058adede69475f"
name = "github.com/smallstep/certificates"
packages = [
"api",
"authority",
"authority/provisioner",
"ca",
"logging",
"monitoring",
"server",
]
pruneopts = "UT"
revision = "1bb25b517115cebfb8ce9e5ecb48fc7bbea055ec"
version = "v0.9.0"
[[projects]]
digest = "1:8b36444f30009b5e124a3ac48b353558024a95c3fccdf3e6bb557a091e67342b"
name = "github.com/smallstep/cli"
packages = [
"command",
"config",
"crypto/keys",
"crypto/pemutil",
"crypto/randutil",
"crypto/tlsutil",
"crypto/x509util",
"errs",
"jose",
"pkg/blackfriday",
"pkg/x509",
"token",
"token/provision",
"ui",
"usage",
"utils",
]
pruneopts = "UT"
revision = "b8972dd3035caefb9f493c4a2c664cc6cf557f93"
version = "v0.9.0"
[[projects]]
branch = "master"
digest = "1:ba52e5a5fb800ce55108b7a5f181bb809aab71c16736051312b0aa969f82ad39"
name = "github.com/tsenart/deadcode"
packages = ["."]
pruneopts = "UT"
revision = "210d2dc333e90c7e3eedf4f2242507a8e83ed4ab"
[[projects]]
branch = "master"
digest = "1:8286f653bf8b8fd155a0c9c3b9ee3dbc2a95b1b51f7a1dc11fe2c66018454a0c"
name = "github.com/urfave/cli"
packages = ["."]
pruneopts = "UT"
revision = "693af58b4d51b8fcc7f9d89576da170765980581"
[[projects]]
branch = "master"
digest = "1:8cee4cbf1682070ab96818200f7f43b78850d68ada71698b551cb6c351420016"
name = "golang.org/x/crypto"
packages = [
"cryptobyte",
"cryptobyte/asn1",
"ed25519",
"ed25519/internal/edwards25519",
"pbkdf2",
"ssh/terminal",
]
pruneopts = "UT"
revision = "a5d413f7728c81fb97d96a2b722368945f651e78"
[[projects]]
branch = "master"
digest = "1:4ebfd72ba817efd42cb4be720c20c200d5a2f3694e8a3bd243b95d558fc5f423"
name = "golang.org/x/net"
packages = [
"html",
"html/atom",
"http/httpguts",
"http2",
"http2/hpack",
"idna",
]
pruneopts = "UT"
revision = "63eda1eb0650888965ead1296efd04d0b2b61128"
[[projects]]
branch = "master"
digest = "1:6b3e6ddcebac95be1d690dbd53b5aa2e520715becb7e521bb526ccf3b4c53c15"
name = "golang.org/x/sys"
packages = [
"unix",
"windows",
]
pruneopts = "UT"
revision = "f49334f85ddcf0f08d7fb6dd7363e9e6d6b777eb"
[[projects]]
digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18"
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"internal/colltab",
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable",
]
pruneopts = "UT"
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
branch = "master"
digest = "1:a45ec3bb7c73e52430410dff3e0a5534ce518f72a8eb4355bc8502c546b91ecc"
name = "golang.org/x/tools"
packages = [
"go/ast/astutil",
"go/gcexportdata",
"go/internal/gcimporter",
"go/types/typeutil",
]
pruneopts = "UT"
revision = "8f05a32dce9ff80c9bf11baeb89d26b410f39281"
[[projects]]
branch = "v3-unstable"
digest = "1:39efb07a0d773dc09785b237ada4e10b5f28646eb6505d97bc18f8d2ff439362"
name = "gopkg.in/alecthomas/kingpin.v3-unstable"
packages = ["."]
pruneopts = "UT"
revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306"
[[projects]]
digest = "1:ef72505cf098abdd34efeea032103377bec06abb61d8a06f002d5d296a4b1185"
name = "gopkg.in/inf.v0"
packages = ["."]
pruneopts = "UT"
revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4"
version = "v0.9.0"
[[projects]]
digest = "1:7fbe10f3790dc4e6296c7c844c5a9b35513e5521c29c47e10ba99cd2956a2719"
name = "gopkg.in/square/go-jose.v2"
packages = [
".",
"cipher",
"json",
"jwt",
]
pruneopts = "UT"
revision = "ef984e69dd356202fd4e4910d4d9c24468bdf0b8"
version = "v2.1.9"
[[projects]]
digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
version = "v2.2.2"
[[projects]]
branch = "master"
digest = "1:e0db54f895460bc12675b12b1c2f8931447051736f370f541214236a84d08023"
name = "k8s.io/api"
packages = [
"admission/v1beta1",
"authentication/v1",
"core/v1",
]
pruneopts = "UT"
revision = "92d2ee7fc726fd16632fbd2dff1466f551f5b8b4"
[[projects]]
branch = "master"
digest = "1:8d51d10f66dbcd39c53f17dc65b9113ca623a72ff99c5e52dfad908b49c07f18"
name = "k8s.io/apimachinery"
packages = [
"pkg/api/resource",
"pkg/apis/meta/v1",
"pkg/apis/meta/v1/unstructured",
"pkg/conversion",
"pkg/conversion/queryparams",
"pkg/fields",
"pkg/labels",
"pkg/runtime",
"pkg/runtime/schema",
"pkg/runtime/serializer",
"pkg/runtime/serializer/json",
"pkg/runtime/serializer/protobuf",
"pkg/runtime/serializer/recognizer",
"pkg/runtime/serializer/versioning",
"pkg/selection",
"pkg/types",
"pkg/util/errors",
"pkg/util/framer",
"pkg/util/intstr",
"pkg/util/json",
"pkg/util/naming",
"pkg/util/net",
"pkg/util/runtime",
"pkg/util/sets",
"pkg/util/validation",
"pkg/util/validation/field",
"pkg/util/yaml",
"pkg/watch",
"third_party/forked/golang/reflect",
]
pruneopts = "UT"
revision = "4ceb6b6c5db56a2f8f454dd837c07160a3d6e131"
[[projects]]
digest = "1:69367163a23cd68971724f36a6759a01d50968e58936808b7eb5e5c186a3a382"
name = "k8s.io/klog"
packages = ["."]
pruneopts = "UT"
revision = "8e90cee79f823779174776412c13478955131846"
[[projects]]
digest = "1:7719608fe0b52a4ece56c2dde37bedd95b938677d1ab0f84b8a7852e4c59f849"
name = "sigs.k8s.io/yaml"
packages = ["."]
pruneopts = "UT"
revision = "fd68e9863619f6ec2fdd8625fe1f02e7c877e480"
version = "v1.1.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/ghodss/yaml",
"github.com/pkg/errors",
"github.com/sirupsen/logrus",
"github.com/smallstep/certificates/authority/provisioner",
"github.com/smallstep/certificates/ca",
"github.com/smallstep/cli/config",
"github.com/smallstep/cli/crypto/pemutil",
"github.com/smallstep/cli/crypto/randutil",
"github.com/smallstep/cli/jose",
"github.com/smallstep/cli/token",
"github.com/smallstep/cli/token/provision",
"k8s.io/api/admission/v1beta1",
"k8s.io/api/core/v1",
"k8s.io/apimachinery/pkg/apis/meta/v1",
"k8s.io/apimachinery/pkg/runtime",
"k8s.io/apimachinery/pkg/runtime/serializer",
]
solver-name = "gps-cdcl"
solver-version = 1

View file

@ -1,62 +0,0 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/ghodss/yaml"
version = "1.0.0"
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.1"
[[constraint]]
name = "github.com/sirupsen/logrus"
version = "1.4.0"
[[constraint]]
name = "github.com/smallstep/certificates"
version = "0.9.0"
[[constraint]]
name = "github.com/smallstep/cli"
version = "0.9.0"
[[constraint]]
branch = "master"
name = "k8s.io/api"
[[constraint]]
branch = "master"
name = "k8s.io/apimachinery"
[[override]]
name = "gopkg.in/square/go-jose.v2"
version = "=2.1.9"
[prune]
go-tests = true
unused-packages = true

View file

@ -1,115 +0,0 @@
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"strings"
"time"
)
const (
serviceAccountToken = "/var/run/secrets/kubernetes.io/serviceaccount/token"
serviceAccountCACert = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
)
// Client is minimal kubernetes client interface
type Client interface {
Do(req *http.Request) (*http.Response, error)
GetRequest(url string) (*http.Request, error)
PostRequest(url, body, contentType string) (*http.Request, error)
DeleteRequest(url string) (*http.Request, error)
Host() string
}
type k8sClient struct {
host string
token string
httpClient *http.Client
}
func (kc *k8sClient) GetRequest(url string) (*http.Request, error) {
if !strings.HasPrefix(url, kc.host) {
url = fmt.Sprintf("%s/%s", kc.host, url)
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
if len(kc.token) > 0 {
req.Header.Set("Authorization", "Bearer "+kc.token)
}
return req, nil
}
func (kc *k8sClient) PostRequest(url string, body string, contentType string) (*http.Request, error) {
if !strings.HasPrefix(url, kc.host) {
url = fmt.Sprintf("%s/%s", kc.host, url)
}
req, err := http.NewRequest("POST", url, strings.NewReader(body))
if err != nil {
return nil, err
}
if len(kc.token) > 0 {
req.Header.Set("Authorization", "Bearer "+kc.token)
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
return req, nil
}
func (kc *k8sClient) DeleteRequest(url string) (*http.Request, error) {
if !strings.HasPrefix(url, kc.host) {
url = fmt.Sprintf("%s/%s", kc.host, url)
}
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return nil, err
}
if len(kc.token) > 0 {
req.Header.Set("Authorization", "Bearer "+kc.token)
}
return req, nil
}
func (kc *k8sClient) Do(req *http.Request) (*http.Response, error) {
return kc.httpClient.Do(req)
}
func (kc *k8sClient) Host() string {
return kc.host
}
// NewInClusterK8sClient creates K8sClient if it is inside Kubernetes
func NewInClusterK8sClient() (Client, error) {
host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
if len(host) == 0 || len(port) == 0 {
return nil, fmt.Errorf("unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined")
}
token, err := ioutil.ReadFile(serviceAccountToken)
if err != nil {
return nil, err
}
ca, err := ioutil.ReadFile(serviceAccountCACert)
if err != nil {
return nil, err
}
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(ca)
transport := &http.Transport{TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS10,
RootCAs: certPool,
}}
httpClient := &http.Client{Transport: transport, Timeout: time.Nanosecond * 0}
return &k8sClient{
host: "https://" + net.JoinHostPort(host, port),
token: string(token),
httpClient: httpClient,
}, nil
}

View file

@ -1,647 +0,0 @@
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/smallstep/certificates/ca"
"github.com/smallstep/cli/crypto/pemutil"
"k8s.io/api/admission/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
)
var (
runtimeScheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(runtimeScheme)
deserializer = codecs.UniversalDeserializer()
// GetRootCAPath() is broken; points to secrets not certs. So
// we'll hard code instead for now.
//rootCAPath = pki.GetRootCAPath()
rootCAPath = "/home/step/.step/certs/root_ca.crt"
)
const (
admissionWebhookAnnotationKey = "autocert.step.sm/name"
admissionWebhookStatusKey = "autocert.step.sm/status"
provisionerPasswordFile = "/home/step/password/password"
volumeMountPath = "/var/run/autocert.step.sm"
tokenSecretKey = "token"
tokenSecretLabel = "autocert.step.sm/token"
)
// Config options for the autocert admission controller.
type Config struct {
LogFormat string `yaml:"logFormat"`
CaURL string `yaml:"caUrl"`
CertLifetime string `yaml:"certLifetime"`
Bootstrapper corev1.Container `yaml:"bootstrapper"`
Renewer corev1.Container `yaml:"renewer"`
CertsVolume corev1.Volume `yaml:"certsVolume"`
RestrictCertificatesToNamespace bool `yaml:"restrictCertificatesToNamespace"`
ClusterDomain string `yaml:"clusterDomain"`
}
// GetClusterDomain returns the Kubernetes cluster domain, defaults to
// "cluster.local" if not specified in the configuration.
func (c Config) GetClusterDomain() string {
if c.ClusterDomain != "" {
return c.ClusterDomain
}
return "cluster.local"
}
// PatchOperation represents a RFC6902 JSONPatch Operation
type PatchOperation struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value,omitempty"`
}
// RFC6901 JSONPath Escaping -- https://tools.ietf.org/html/rfc6901
func escapeJSONPath(path string) string {
// Replace`~` with `~0` then `/` with `~1`. Note that the order
// matters otherwise we'll turn a `/` into a `~/`.
path = strings.Replace(path, "~", "~0", -1)
path = strings.Replace(path, "/", "~1", -1)
return path
}
func loadConfig(file string) (*Config, error) {
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, nil
}
// createTokenSecret generates a kubernetes Secret object containing a bootstrap token
// in the specified namespce. The secret name is randomly generated with a given prefix.
// A goroutine is scheduled to cleanup the secret after the token expires. The secret
// is also labelled for easy identification and manual cleanup.
func createTokenSecret(prefix, namespace, token string) (string, error) {
secret := corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
GenerateName: prefix,
Namespace: namespace,
Labels: map[string]string{
tokenSecretLabel: "true",
},
},
StringData: map[string]string{
tokenSecretKey: token,
},
Type: corev1.SecretTypeOpaque,
}
client, err := NewInClusterK8sClient()
if err != nil {
return "", err
}
body, err := json.Marshal(secret)
if err != nil {
return "", err
}
log.WithField("secret", string(body)).Debug("Creating secret")
req, err := client.PostRequest(fmt.Sprintf("api/v1/namespaces/%s/secrets", namespace), string(body), "application/json")
if err != nil {
return "", err
}
resp, err := client.Do(req)
if err != nil {
log.Errorf("Secret creation error. Response: %v", resp)
return "", errors.Wrap(err, "secret creation")
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Errorf("Secret creation error (!2XX). Response: %v", resp)
var rbody []byte
if resp.Body != nil {
if data, err := ioutil.ReadAll(resp.Body); err == nil {
rbody = data
}
}
log.Error("Error body: ", string(rbody))
return "", errors.New("Not 200")
}
var rbody []byte
if resp.Body != nil {
if data, err := ioutil.ReadAll(resp.Body); err == nil {
rbody = data
}
}
if len(rbody) == 0 {
return "", errors.New("Empty response body")
}
var created *corev1.Secret
if err := json.Unmarshal(rbody, &created); err != nil {
return "", errors.Wrap(err, "Error unmarshalling secret response")
}
// Clean up after ourselves by deleting the Secret after the bootstrap
// token expires. This is best effort -- obviously we'll miss some stuff
// if this process goes away -- but the secrets are also labelled so
// it's also easy to clean them up in bulk using kubectl if we miss any.
go func() {
time.Sleep(tokenLifetime)
req, err := client.DeleteRequest(fmt.Sprintf("api/v1/namespaces/%s/secrets/%s", namespace, created.Name))
ctxLog := log.WithFields(log.Fields{
"name": created.Name,
"namespace": namespace,
})
if err != nil {
ctxLog.WithField("error", err).Error("Error deleting expired boostrap token secret")
return
}
resp, err := client.Do(req)
if err != nil {
ctxLog.WithField("error", err).Error("Error deleting expired boostrap token secret")
return
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
ctxLog.WithFields(log.Fields{
"status": resp.Status,
"statusCode": resp.StatusCode,
}).Error("Error deleting expired boostrap token secret")
return
}
ctxLog.Info("Deleted expired bootstrap token secret")
}()
return created.Name, err
}
// mkBootstrapper generates a bootstrap container based on the template defined in Config. It
// generates a new bootstrap token and mounts it, along with other required coniguration, as
// environment variables in the returned bootstrap container.
func mkBootstrapper(config *Config, commonName string, namespace string, provisioner Provisioner) (corev1.Container, error) {
b := config.Bootstrapper
token, err := provisioner.Token(commonName)
if err != nil {
return b, errors.Wrap(err, "token generation")
}
// Generate CA fingerprint
crt, err := pemutil.ReadCertificate(rootCAPath)
if err != nil {
return b, errors.Wrap(err, "CA fingerprint")
}
sum := sha256.Sum256(crt.Raw)
fingerprint := strings.ToLower(hex.EncodeToString(sum[:]))
secretName, err := createTokenSecret(commonName+"-", namespace, token)
if err != nil {
return b, errors.Wrap(err, "create token secret")
}
log.Infof("Secret name is: %s", secretName)
b.Env = append(b.Env, corev1.EnvVar{
Name: "COMMON_NAME",
Value: commonName,
})
b.Env = append(b.Env, corev1.EnvVar{
Name: "STEP_TOKEN",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secretName,
},
Key: tokenSecretKey,
},
},
})
b.Env = append(b.Env, corev1.EnvVar{
Name: "STEP_CA_URL",
Value: config.CaURL,
})
b.Env = append(b.Env, corev1.EnvVar{
Name: "STEP_FINGERPRINT",
Value: fingerprint,
})
b.Env = append(b.Env, corev1.EnvVar{
Name: "STEP_NOT_AFTER",
Value: config.CertLifetime,
})
return b, nil
}
// mkRenewer generates a new renewer based on the template provided in Config.
func mkRenewer(config *Config) corev1.Container {
r := config.Renewer
r.Env = append(r.Env, corev1.EnvVar{
Name: "STEP_CA_URL",
Value: config.CaURL,
})
return r
}
func addContainers(existing, new []corev1.Container, path string) (ops []PatchOperation) {
if len(existing) == 0 {
return []PatchOperation{
{
Op: "add",
Path: path,
Value: new,
},
}
}
for _, add := range new {
ops = append(ops, PatchOperation{
Op: "add",
Path: path + "/-",
Value: add,
})
}
return ops
}
func addVolumes(existing, new []corev1.Volume, path string) (ops []PatchOperation) {
if len(existing) == 0 {
return []PatchOperation{
{
Op: "add",
Path: path,
Value: new,
},
}
}
for _, add := range new {
ops = append(ops, PatchOperation{
Op: "add",
Path: path + "/-",
Value: add,
})
}
return ops
}
func addCertsVolumeMount(volumeName string, containers []corev1.Container) (ops []PatchOperation) {
volumeMount := corev1.VolumeMount{
Name: volumeName,
MountPath: volumeMountPath,
ReadOnly: true,
}
for i, container := range containers {
if len(container.VolumeMounts) == 0 {
ops = append(ops, PatchOperation{
Op: "add",
Path: fmt.Sprintf("/spec/containers/%v/volumeMounts", i),
Value: []corev1.VolumeMount{volumeMount},
})
} else {
ops = append(ops, PatchOperation{
Op: "add",
Path: fmt.Sprintf("/spec/containers/%v/volumeMounts/-", i),
Value: volumeMount,
})
}
}
return ops
}
func addAnnotations(existing, new map[string]string) (ops []PatchOperation) {
if len(existing) == 0 {
return []PatchOperation{
{
Op: "add",
Path: "/metadata/annotations",
Value: new,
},
}
}
for k, v := range new {
if existing[k] == "" {
ops = append(ops, PatchOperation{
Op: "add",
Path: "/metadata/annotations/" + escapeJSONPath(k),
Value: v,
})
} else {
ops = append(ops, PatchOperation{
Op: "replace",
Path: "/metadata/annotations/" + escapeJSONPath(k),
Value: v,
})
}
}
return ops
}
// patch produces a list of patches to apply to a pod to inject a certificate. In particular,
// we patch the pod in order to:
// - Mount the `certs` volume in existing containers defined in the pod
// - Add the autocert-renewer as a container (a sidecar)
// - Add the autocert-bootstrapper as an initContainer
// - Add the `certs` volume definition
// - Annotate the pod to indicate that it's been processed by this controller
// The result is a list of serialized JSONPatch objects (or an error).
func patch(pod *corev1.Pod, namespace string, config *Config, provisioner Provisioner) ([]byte, error) {
var ops []PatchOperation
annotations := pod.ObjectMeta.GetAnnotations()
commonName := annotations[admissionWebhookAnnotationKey]
renewer := mkRenewer(config)
bootstrapper, err := mkBootstrapper(config, commonName, namespace, provisioner)
if err != nil {
return nil, err
}
ops = append(ops, addCertsVolumeMount(config.CertsVolume.Name, pod.Spec.Containers)...)
ops = append(ops, addContainers(pod.Spec.Containers, []corev1.Container{renewer}, "/spec/containers")...)
ops = append(ops, addContainers(pod.Spec.InitContainers, []corev1.Container{bootstrapper}, "/spec/initContainers")...)
ops = append(ops, addVolumes(pod.Spec.Volumes, []corev1.Volume{config.CertsVolume}, "/spec/volumes")...)
ops = append(ops, addAnnotations(pod.Annotations, map[string]string{admissionWebhookStatusKey: "injected"})...)
return json.Marshal(ops)
}
// shouldMutate checks whether a pod is subject to mutation by this admission controller. A pod
// is subject to mutation if it's annotated with the `admissionWebhookAnnotationKey` and if it
// has not already been processed (indicated by `admissionWebhookStatusKey` set to `injected`).
// If the pod requests a certificate with a subject matching a namespace other than its own
// and restrictToNamespace is true, then shouldMutate will return a validation error
// that should be returned to the client.
func shouldMutate(metadata *metav1.ObjectMeta, namespace string, clusterDomain string, restrictToNamespace bool) (bool, error) {
annotations := metadata.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
// Only mutate if the object is annotated appropriately (annotation key set) and we haven't
// mutated already (status key isn't set).
if annotations[admissionWebhookAnnotationKey] == "" || annotations[admissionWebhookStatusKey] == "injected" {
return false, nil
}
if !restrictToNamespace {
return true, nil
}
subject := strings.Trim(annotations[admissionWebhookAnnotationKey], ".")
err := fmt.Errorf("subject \"%s\" matches a namespace other than \"%s\" and is not permitted. This check can be disabled by setting restrictCertificatesToNamespace to false in the autocert-config ConfigMap", subject, namespace)
if strings.HasSuffix(subject, ".svc") && !strings.HasSuffix(subject, fmt.Sprintf(".%s.svc", namespace)) {
return false, err
}
if strings.HasSuffix(subject, fmt.Sprintf(".svc.%s", clusterDomain)) && !strings.HasSuffix(subject, fmt.Sprintf(".%s.svc.%s", namespace, clusterDomain)) {
return false, err
}
return true, nil
}
// mutate takes an `AdmissionReview`, determines whether it is subject to mutation, and returns
// an appropriate `AdmissionResponse` including patches or any errors that occurred.
func mutate(review *v1beta1.AdmissionReview, config *Config, provisioner Provisioner) *v1beta1.AdmissionResponse {
ctxLog := log.WithField("uid", review.Request.UID)
request := review.Request
var pod corev1.Pod
if err := json.Unmarshal(request.Object.Raw, &pod); err != nil {
ctxLog.WithField("error", err).Error("Error unmarshalling pod")
return &v1beta1.AdmissionResponse{
Allowed: false,
UID: request.UID,
Result: &metav1.Status{
Message: err.Error(),
},
}
}
ctxLog = ctxLog.WithFields(log.Fields{
"kind": request.Kind,
"operation": request.Operation,
"name": pod.Name,
"generateName": pod.GenerateName,
"namespace": request.Namespace,
"user": request.UserInfo,
})
mutationAllowed, validationErr := shouldMutate(&pod.ObjectMeta, request.Namespace, config.GetClusterDomain(), config.RestrictCertificatesToNamespace)
if validationErr != nil {
ctxLog.WithField("error", validationErr).Info("Validation error")
return &v1beta1.AdmissionResponse{
Allowed: false,
UID: request.UID,
Result: &metav1.Status{
Message: validationErr.Error(),
},
}
}
if !mutationAllowed {
ctxLog.WithField("annotations", pod.Annotations).Info("Skipping mutation")
return &v1beta1.AdmissionResponse{
Allowed: true,
UID: request.UID,
}
}
patchBytes, err := patch(&pod, request.Namespace, config, provisioner)
if err != nil {
ctxLog.WithField("error", err).Error("Error generating patch")
return &v1beta1.AdmissionResponse{
Allowed: false,
UID: request.UID,
Result: &metav1.Status{
Message: err.Error(),
},
}
}
ctxLog.WithField("patch", string(patchBytes)).Info("Generated patch")
return &v1beta1.AdmissionResponse{
Allowed: true,
Patch: patchBytes,
UID: request.UID,
PatchType: func() *v1beta1.PatchType {
pt := v1beta1.PatchTypeJSONPatch
return &pt
}(),
}
}
func main() {
if len(os.Args) != 2 {
log.Errorf("Usage: %s <config>\n", os.Args[0])
os.Exit(1)
}
config, err := loadConfig(os.Args[1])
if err != nil {
panic(err)
}
log.SetOutput(os.Stdout)
if config.LogFormat == "json" {
log.SetFormatter(&log.JSONFormatter{})
}
if config.LogFormat == "text" {
log.SetFormatter(&log.TextFormatter{})
}
log.WithFields(log.Fields{
"config": config,
}).Info("Loaded config")
provisionerName := os.Getenv("PROVISIONER_NAME")
provisionerKid := os.Getenv("PROVISIONER_KID")
log.WithFields(log.Fields{
"provisionerName": provisionerName,
"provisionerKid": provisionerKid,
}).Info("Loaded provisioner configuration")
provisioner, err := NewProvisioner(provisionerName, provisionerKid, config.CaURL, rootCAPath, provisionerPasswordFile)
if err != nil {
log.Errorf("Error loading provisioner: %v", err)
os.Exit(1)
}
log.WithFields(log.Fields{
"name": provisioner.Name(),
"kid": provisioner.Kid(),
}).Info("Loaded provisioner")
namespace := os.Getenv("NAMESPACE")
if namespace == "" {
log.Errorf("$NAMESPACE not set")
os.Exit(1)
}
name := fmt.Sprintf("autocert.%s.svc", namespace)
token, err := provisioner.Token(name)
if err != nil {
log.WithField("error", err).Errorf("Error generating bootstrap token during controller startup")
os.Exit(1)
}
log.WithField("name", name).Infof("Generated bootstrap token for controller")
// make sure to cancel the renew goroutine
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
srv, err := ca.BootstrapServer(ctx, token, &http.Server{
Addr: ":4443",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/healthz" {
log.Info("/healthz")
fmt.Fprintf(w, "ok")
w.WriteHeader(http.StatusOK)
return
}
/*
var name string
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
name = r.TLS.PeerCertificates[0].Subject.CommonName
}
*/
if r.URL.Path != "/mutate" {
log.WithField("path", r.URL.Path).Error("Bad Request: 404 Not Found")
http.NotFound(w, r)
return
}
var body []byte
if r.Body != nil {
if data, err := ioutil.ReadAll(r.Body); err == nil {
body = data
}
}
if len(body) == 0 {
log.Error("Bad Request: 400 (Empty Body)")
http.Error(w, "Bad Request (Empty Body)", http.StatusBadRequest)
return
}
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
log.WithField("Content-Type", contentType).Error("Bad Request: 415 (Unsupported Media Type)")
http.Error(w, fmt.Sprintf("Bad Request: 415 Unsupported Media Type (Expected Content-Type 'application/json' but got '%s')", contentType), http.StatusUnsupportedMediaType)
return
}
var response *v1beta1.AdmissionResponse
review := v1beta1.AdmissionReview{}
if _, _, err := deserializer.Decode(body, nil, &review); err != nil {
log.WithFields(log.Fields{
"body": body,
"error": err,
}).Error("Can't decode body")
response = &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Message: err.Error(),
},
}
} else {
response = mutate(&review, config, provisioner)
}
resp, err := json.Marshal(v1beta1.AdmissionReview{
Response: response,
})
if err != nil {
log.WithFields(log.Fields{
"uid": review.Request.UID,
"error": err,
}).Info("Marshal error")
http.Error(w, fmt.Sprintf("Marshal Error: %v", err), http.StatusInternalServerError)
} else {
log.WithFields(log.Fields{
"uid": review.Request.UID,
"response": string(resp),
}).Info("Returning review")
if _, err := w.Write(resp); err != nil {
log.WithFields(log.Fields{
"uid": review.Request.UID,
"error": err,
}).Info("Write error")
}
}
}),
}, ca.VerifyClientCertIfGiven())
if err != nil {
panic(err)
}
log.Info("Listening on :4443 ...")
if err := srv.ListenAndServeTLS("", ""); err != nil {
panic(err)
}
}

View file

@ -1,75 +0,0 @@
package main
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"testing"
)
func TestGetClusterDomain(t *testing.T) {
c := Config{}
if c.GetClusterDomain() != "cluster.local" {
t.Errorf("cluster domain should default to cluster.local, not: %s", c.GetClusterDomain())
}
c.ClusterDomain = "mydomain.com"
if c.GetClusterDomain() != "mydomain.com" {
t.Errorf("cluster domain should default to cluster.local, not: %s", c.GetClusterDomain())
}
}
func TestShouldMutate(t *testing.T) {
testCases := []struct {
description string
subject string
namespace string
expected bool
}{
{"full cluster domain", "test.default.svc.cluster.local", "default", true},
{"full cluster domain wrong ns", "test.default.svc.cluster.local", "kube-system", false},
{"left dots get stripped", ".test.default.svc.cluster.local", "default", true},
{"left dots get stripped wrong ns", ".test.default.svc.cluster.local", "kube-system", false},
{"right dots get stripped", "test.default.svc.cluster.local.", "default", true},
{"right dots get stripped wrong ns", "test.default.svc.cluster.local.", "kube-system", false},
{"dots get stripped", ".test.default.svc.cluster.local.", "default", true},
{"dots get stripped wrong ns", ".test.default.svc.cluster.local.", "kube-system", false},
{"partial cluster domain", "test.default.svc.cluster", "default", true},
{"partial cluster domain wrong ns is still allowed because not valid hostname", "test.default.svc.cluster", "kube-system", true},
{"service domain", "test.default.svc", "default", true},
{"service domain wrong ns", "test.default.svc", "kube-system", false},
{"two part domain", "test.default", "default", true},
{"two part domain different ns", "test.default", "kube-system", true},
{"one hostname", "test", "default", true},
{"no subject specified", "", "default", false},
{"three part not cluster", "test.default.com", "kube-system", true},
{"four part not cluster", "test.default.svc.com", "kube-system", true},
{"five part not cluster", "test.default.svc.cluster.com", "kube-system", true},
{"six part not cluster", "test.default.svc.cluster.local.com", "kube-system", true},
}
for _, testCase := range testCases {
t.Run(testCase.description, func(t *testing.T) {
mutationAllowed, validationErr := shouldMutate(&metav1.ObjectMeta{
Annotations: map[string]string{
admissionWebhookAnnotationKey: testCase.subject,
},
}, testCase.namespace, "cluster.local", true)
if mutationAllowed != testCase.expected {
t.Errorf("shouldMutate did not return %t for %s", testCase.expected, testCase.description)
}
if testCase.subject != "" && mutationAllowed == false && validationErr == nil {
t.Errorf("shouldMutate should return validation error for invalid hostname")
}
})
}
}
func TestShouldMutateNotRestrictToNamespace(t *testing.T) {
mutationAllowed, _ := shouldMutate(&metav1.ObjectMeta{
Annotations: map[string]string{
admissionWebhookAnnotationKey: "test.default.svc.cluster.local",
},
}, "kube-system", "cluster.local", false)
if mutationAllowed == false {
t.Errorf("shouldMutate should return true even with a wrong namespace if restrictToNamespace is false.")
}
}

View file

@ -1,197 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"path/filepath"
"time"
"github.com/pkg/errors"
provisioners "github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/ca"
"github.com/smallstep/cli/config"
"github.com/smallstep/cli/crypto/randutil"
"github.com/smallstep/cli/jose"
"github.com/smallstep/cli/token"
"github.com/smallstep/cli/token/provision"
)
const (
tokenLifetime = 5 * time.Minute
)
// Provisioner is an authorized entity that can sign tokens necessary for
// signature requests.
type Provisioner interface {
Name() string
Kid() string
Token(subject string) (string, error)
}
type provisioner struct {
name string
kid string
caURL string
caRoot string
jwk *jose.JSONWebKey
tokenLifetime time.Duration
}
// Name returns the provisioner's name.
func (p *provisioner) Name() string {
return p.name
}
// Kid returns the provisioners key ID.
func (p *provisioner) Kid() string {
return p.kid
}
// Token generates a bootstrap token for a subject.
func (p *provisioner) Token(subject string) (string, error) {
// A random jwt id will be used to identify duplicated tokens
jwtID, err := randutil.Hex(64) // 256 bits
if err != nil {
return "", err
}
notBefore := time.Now()
notAfter := notBefore.Add(tokenLifetime)
signURL := fmt.Sprintf("%v/1.0/sign", p.caURL)
tokOptions := []token.Options{
token.WithJWTID(jwtID),
token.WithKid(p.kid),
token.WithIssuer(p.name),
token.WithAudience(signURL),
token.WithValidity(notBefore, notAfter),
token.WithRootCA(p.caRoot),
}
tok, err := provision.New(subject, tokOptions...)
if err != nil {
return "", err
}
return tok.SignedString(p.jwk.Algorithm, p.jwk.Key)
}
func decryptProvisionerJWK(encryptedKey, passFile string) (*jose.JSONWebKey, error) {
decrypted, err := jose.Decrypt("", []byte(encryptedKey), jose.WithPasswordFile(passFile))
if err != nil {
return nil, err
}
jwk := new(jose.JSONWebKey)
if err := json.Unmarshal(decrypted, jwk); err != nil {
return nil, errors.Wrap(err, "error unmarshalling provisioning key")
}
return jwk, nil
}
// loadProvisionerJWKByKid retrieves a provisioner key from the CA by key ID and
// decrypts it using the specified password file.
func loadProvisionerJWKByKid(kid, caURL, caRoot, passFile string) (*jose.JSONWebKey, error) {
encrypted, err := getProvisionerKey(caURL, caRoot, kid)
if err != nil {
return nil, err
}
return decryptProvisionerJWK(encrypted, passFile)
}
// loadProvisionerJWKByName retrieves the list of provisioners and encrypted key then
// returns the key of the first provisioner with a matching name that can be successfully
// decrypted with the specified password file.
func loadProvisionerJWKByName(name, caURL, caRoot, passFile string) (key *jose.JSONWebKey, err error) {
provisioners, err := getProvisioners(caURL, caRoot)
if err != nil {
err = errors.Wrap(err, "error getting the provisioners")
return
}
for _, provisioner := range provisioners {
if provisioner.GetName() == name {
if _, encryptedKey, ok := provisioner.GetEncryptedKey(); ok {
key, err = decryptProvisionerJWK(encryptedKey, passFile)
if err == nil {
return
}
}
}
}
return nil, errors.Errorf("provisioner '%s' not found (or your password is wrong)", name)
}
// NewProvisioner loads and decrypts key material from the CA for the named
// provisioner. The key identified by `kid` will be used if specified. If `kid`
// is the empty string we'll use the first key for the named provisioner that
// decrypts using `passFile`.
func NewProvisioner(name, kid, caURL, caRoot, passFile string) (Provisioner, error) {
var jwk *jose.JSONWebKey
var err error
if kid != "" {
jwk, err = loadProvisionerJWKByKid(kid, caURL, caRoot, passFile)
} else {
jwk, err = loadProvisionerJWKByName(name, caURL, caRoot, passFile)
}
if err != nil {
return nil, err
}
return &provisioner{
name: name,
kid: jwk.KeyID,
caURL: caURL,
caRoot: caRoot,
jwk: jwk,
tokenLifetime: tokenLifetime,
}, nil
}
// getRootCAPath returns the path where the root CA is stored based on the
// STEPPATH environment variable.
func getRootCAPath() string {
return filepath.Join(config.StepPath(), "certs", "root_ca.crt")
}
// getProvisioners returns the map of provisioners on the given CA.
func getProvisioners(caURL, rootFile string) (provisioners.List, error) {
if len(rootFile) == 0 {
rootFile = getRootCAPath()
}
client, err := ca.NewClient(caURL, ca.WithRootFile(rootFile))
if err != nil {
return nil, err
}
cursor := ""
var provisioners provisioners.List
for {
resp, err := client.Provisioners(ca.WithProvisionerCursor(cursor), ca.WithProvisionerLimit(100))
if err != nil {
return nil, err
}
provisioners = append(provisioners, resp.Provisioners...)
if resp.NextCursor == "" {
return provisioners, nil
}
cursor = resp.NextCursor
}
}
// getProvisionerKey returns the encrypted provisioner key with the for the
// given kid.
func getProvisionerKey(caURL, rootFile, kid string) (string, error) {
if len(rootFile) == 0 {
rootFile = getRootCAPath()
}
client, err := ca.NewClient(caURL, ca.WithRootFile(rootFile))
if err != nil {
return "", err
}
resp, err := client.ProvisionerKey(kid)
if err != nil {
return "", err
}
return resp.Key, nil
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 MiB

View file

@ -1,131 +0,0 @@
# 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
```
## Mutual TLS
Unlike the _server auth TLS_ that's typical with web browsers, where the browser authenticates the server but not vice versa, _mutual TLS_ (mTLS) connections have both remote peers (client and server) authenticate to one another by presenting certificates. mTLS is not a different protocol. It's just a variant of TLS that's not usually turned on by default. This repository demonstrates **how to turn on mTLS** with different tools and languages. It also demonstrates other **TLS best practices** like certificate rotation.
mTLS provides _authenticated encryption_: an _identity dialtone_ and _end-to-end encryption_ for your workloads. It's like a secure line with caller ID. This has [all sorts of benefits](https://smallstep.com/blog/use-tls.html): better security, compliance, and easier auditability for starters. It **makes workloads identity-aware**, improving observability and enabling granular access control. Perhaps most compelling, mTLS lets you securely communicate with workloads running anywhere. Code, containers, devices, people, and anything else can connect securely using mTLS as long as they know one anothers' names and can resolve those names to routable IP addresses.
With properly configured mTLS, services can be safely exposed directly to the public internet: **only clients that have a certificate issued by the internal certificate authority will be allowed to connect**.
Here's a rough approximation of how an mTLS handshake works:
![mTLS handshake diagram](https://raw.githubusercontent.com/smallstep/certificates/autocert/autocert/mtls-handshake.png)
A few things to note:
* It's the signing of random numbers that proves we're talking to the right remote. It's the digital equivalent of asking someone to send you a photo of them with today's newspaper.
* The client and server need to have prior knowledge of the root certificate(s) used for signing other certificates.
* The client and server need to be configured to use the correct certificate and private key (the certificate must have been issued by a CA with a trusted root certificate)
* Private keys are never shared. This is the magic of public key cryptography: unlike passwords or access tokens, certificates let you prove who you are without giving anyone the ability to impersonate you.
## 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!
[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
[nginx/](nginx/)
- [X] Server
- [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
[envoy/](envoy/)
- [X] Server
- [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
[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)
- [X] Automatic certificate rotation
- [X] Restrict to safe ciphersuites and TLS versions
- [ ] TLS stack configuration loaded from `step-ca`
- [ ] Root certificate rotation
[go-grpc/](go-grpc/)
- [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)
- [X] Automatic certificate rotation
- [X] Restrict to safe ciphersuites and TLS versions
- [ ] TLS stack configuration loaded from `step-ca`
- [ ] Root certificate rotation
[node/](node/)
- [X] Server
- [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)
- [X] Automatic certificate rotation
- [X] Restrict to safe ciphersuites and TLS versions
- [ ] TLS stack configuration loaded from `step-ca`
- [ ] Root certificate rotation
[py-gunicorn/](py-gunicorn/)
- [X] Server (gunicorn + Flask)
- [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 (python)
- [X] mTLS (send client certificate if server asks for it)
- [X] Automatic certificate rotation
- [X] Restrict to safe ciphersuites and TLS versions
- [ ] TLS stack configuration loaded from `step-ca`
- [ ] Root certificate rotation

View file

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

View file

@ -1,11 +0,0 @@
#!/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

@ -1,22 +0,0 @@
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

@ -1,21 +0,0 @@
FROM envoyproxy/envoy-alpine
RUN apk update
RUN apk add python3
RUN apk add inotify-tools
RUN mkdir /src
ADD entrypoint.sh /src
ADD certwatch.sh /src
ADD hot-restarter.py /src
ADD start-envoy.sh /src
ADD server.yaml /src
# Flask app
ADD server.py /src
ADD requirements.txt /src
RUN pip3 install -r /src/requirements.txt
# app, certificate watcher and envoy
ENTRYPOINT ["/src/entrypoint.sh"]
CMD ["python3", "/src/hot-restarter.py", "/src/start-envoy.sh"]

View file

@ -1,6 +0,0 @@
#!/bin/sh
while true; do
inotifywait -e modify /var/run/autocert.step.sm/site.crt
kill -HUP 1
done

View file

@ -1,10 +0,0 @@
#!/bin/sh
# start hello world app
python3 /src/server.py &
# watch for the update of the cert and reload nginx
/src/certwatch.sh &
# Run docker CMD
exec "$@"

View file

@ -1,33 +0,0 @@
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-envoy:latest
imagePullPolicy: Never
resources: {requests: {cpu: 10m, memory: 20Mi}}

View file

@ -1,209 +0,0 @@
#!/usr/bin/env python
from __future__ import print_function
import os
import signal
import sys
import time
# The number of seconds to wait for children to gracefully exit after
# propagating SIGTERM before force killing children.
# NOTE: If using a shutdown mechanism such as runit's `force-stop` which sends
# a KILL after a specified timeout period, it's important to ensure that this
# constant is smaller than the KILL timeout
TERM_WAIT_SECONDS = 30
restart_epoch = 0
pid_list = []
def term_all_children():
""" Iterate through all known child processes, send a TERM signal to each of
them, and then wait up to TERM_WAIT_SECONDS for them to exit gracefully,
exiting early if all children go away. If one or more children have not
exited after TERM_WAIT_SECONDS, they will be forcibly killed """
# First uninstall the SIGCHLD handler so that we don't get called again.
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
global pid_list
for pid in pid_list:
print("sending TERM to PID={}".format(pid))
try:
os.kill(pid, signal.SIGTERM)
except OSError:
print("error sending TERM to PID={} continuing".format(pid))
all_exited = False
# wait for TERM_WAIT_SECONDS seconds for children to exit cleanly
retries = 0
while not all_exited and retries < TERM_WAIT_SECONDS:
for pid in list(pid_list):
ret_pid, exit_status = os.waitpid(pid, os.WNOHANG)
if ret_pid == 0 and exit_status == 0:
# the child is still running
continue
pid_list.remove(pid)
if len(pid_list) == 0:
all_exited = True
else:
retries += 1
time.sleep(1)
if all_exited:
print("all children exited cleanly")
else:
for pid in pid_list:
print("child PID={} did not exit cleanly, killing".format(pid))
force_kill_all_children()
sys.exit(1) # error status because a child did not exit cleanly
def force_kill_all_children():
""" Iterate through all known child processes and force kill them. Typically
term_all_children() should be attempted first to give child processes an
opportunity to clean up state before exiting """
global pid_list
for pid in pid_list:
print("force killing PID={}".format(pid))
try:
os.kill(pid, signal.SIGKILL)
except OSError:
print("error force killing PID={} continuing".format(pid))
pid_list = []
def shutdown():
""" Attempt to gracefully shutdown all child Envoy processes and then exit.
See term_all_children() for further discussion. """
term_all_children()
sys.exit(0)
def sigterm_handler(signum, frame):
""" Handler for SIGTERM. """
print("got SIGTERM")
shutdown()
def sigint_handler(signum, frame):
""" Handler for SIGINT (ctrl-c). The same as the SIGTERM handler. """
print("got SIGINT")
shutdown()
def sighup_handler(signum, frame):
""" Handler for SIGUP. This signal is used to cause the restarter to fork and exec a new
child. """
print("got SIGHUP")
fork_and_exec()
def sigusr1_handler(signum, frame):
""" Handler for SIGUSR1. Propagate SIGUSR1 to all of the child processes """
global pid_list
for pid in pid_list:
print("sending SIGUSR1 to PID={}".format(pid))
try:
os.kill(pid, signal.SIGUSR1)
except OSError:
print("error in SIGUSR1 to PID={} continuing".format(pid))
def sigchld_handler(signum, frame):
""" Handler for SIGCHLD. Iterates through all of our known child processes and figures out whether
the signal/exit was expected or not. Python doesn't have any of the native signal handlers
ability to get the child process info directly from the signal handler so we need to iterate
through all child processes and see what happened."""
print("got SIGCHLD")
kill_all_and_exit = False
global pid_list
pid_list_copy = list(pid_list)
for pid in pid_list_copy:
ret_pid, exit_status = os.waitpid(pid, os.WNOHANG)
if ret_pid == 0 and exit_status == 0:
# This child is still running.
continue
pid_list.remove(pid)
# Now we see how the child exited.
if os.WIFEXITED(exit_status):
exit_code = os.WEXITSTATUS(exit_status)
print("PID={} exited with code={}".format(ret_pid, exit_code))
if exit_code == 0:
# Normal exit. We assume this was on purpose.
pass
else:
# Something bad happened. We need to tear everything down so that whoever started the
# restarter can know about this situation and restart the whole thing.
kill_all_and_exit = True
elif os.WIFSIGNALED(exit_status):
print("PID={} was killed with signal={}".format(ret_pid, os.WTERMSIG(exit_status)))
kill_all_and_exit = True
else:
kill_all_and_exit = True
if kill_all_and_exit:
print("Due to abnormal exit, force killing all child processes and exiting")
# First uninstall the SIGCHLD handler so that we don't get called again.
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
force_kill_all_children()
# Our last child died, so we have no purpose. Exit.
if not pid_list:
print("exiting due to lack of child processes")
sys.exit(1 if kill_all_and_exit else 0)
def fork_and_exec():
""" This routine forks and execs a new child process and keeps track of its PID. Before we fork,
set the current restart epoch in an env variable that processes can read if they care. """
global restart_epoch
os.environ['RESTART_EPOCH'] = str(restart_epoch)
print("forking and execing new child process at epoch {}".format(restart_epoch))
restart_epoch += 1
child_pid = os.fork()
if child_pid == 0:
# Child process
os.execl(sys.argv[1], sys.argv[1])
else:
# Parent process
print("forked new child process with PID={}".format(child_pid))
pid_list.append(child_pid)
def main():
""" Script main. This script is designed so that a process watcher like runit or monit can watch
this process and take corrective action if it ever goes away. """
print("starting hot-restarter with target: {}".format(sys.argv[1]))
signal.signal(signal.SIGTERM, sigterm_handler)
signal.signal(signal.SIGINT, sigint_handler)
signal.signal(signal.SIGHUP, sighup_handler)
signal.signal(signal.SIGCHLD, sigchld_handler)
signal.signal(signal.SIGUSR1, sigusr1_handler)
# Start the first child process and then go into an endless loop since everything else happens via
# signals.
fork_and_exec()
while True:
time.sleep(60)
if __name__ == '__main__':
main()

View file

@ -1 +0,0 @@
Flask

View file

@ -1,9 +0,0 @@
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!\n"
if __name__ == "__main__":
app.run(host='127.0.0.1', port=8080, debug=False)

View file

@ -1,50 +0,0 @@
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 443
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
codec_type: auto
stat_prefix: ingress_http
route_config:
name: hello
virtual_hosts:
- name: hello
domains:
- "hello-mtls.default.svc.cluster.local"
routes:
- match:
prefix: "/"
route:
cluster: hello-mTLS
http_filters:
- name: envoy.router
config: {}
tls_context:
common_tls_context:
tls_params:
tls_minimum_protocol_version: TLSv1_2
tls_maximum_protocol_version: TLSv1_3
cipher_suites: "[ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305]"
tls_certificates:
- certificate_chain:
filename: "/var/run/autocert.step.sm/site.crt"
private_key:
filename: "/var/run/autocert.step.sm/site.key"
validation_context:
trusted_ca:
filename: "/var/run/autocert.step.sm/root.crt"
require_client_certificate: true
clusters:
- name: hello-mTLS
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
hosts:
- socket_address:
address: 127.0.0.1
port_value: 8080

View file

@ -1,4 +0,0 @@
#!/bin/sh
ulimit -n 65536
/usr/local/bin/envoy -c /src/server.yaml --service-cluster hello-mTLS --restart-epoch $RESTART_EPOCH

View file

@ -1,16 +0,0 @@
# build stage
FROM golang:alpine AS build-env
RUN apk update
RUN apk add git
RUN mkdir /src
WORKDIR /go/src/github.com/smallstep/certificates/autocert/examples/hello-mtls/go-grpc
ADD client/client.go .
COPY hello hello
RUN go get -d -v ./...
RUN go build -o client
# final stage
FROM alpine
COPY --from=build-env /go/src/github.com/smallstep/certificates/autocert/examples/hello-mtls/go-grpc/client .
CMD ["./client"]

View file

@ -1,164 +0,0 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"github.com/smallstep/certificates/autocert/examples/hello-mtls/go-grpc/hello"
)
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
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.RWMutex
certificate *tls.Certificate
}
func (r *rotator) getClientCertificate(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
r.RLock()
defer r.RUnlock()
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 sayHello(c hello.GreeterClient) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &hello.HelloRequest{Name: "world"})
if err != nil {
return err
}
log.Printf("Greeting: %s", r.Message)
return nil
}
func sayHelloAgain(c hello.GreeterClient) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHelloAgain(ctx, &hello.HelloRequest{Name: "world"})
if err != nil {
return err
}
log.Printf("Greeting: %s", r.Message)
return nil
}
func main() {
// Read the root certificate for our CA from disk
roots, err := loadRootCertPool()
if err != nil {
log.Fatal(err)
}
// Load certificate
r := &rotator{}
if err := r.loadCertificate(autocertFile, autocertKey); err != nil {
log.Fatal("error loading certificate and key", err)
}
tlsConfig := &tls.Config{
RootCAs: roots,
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,
},
// GetClientCertificate is called when a server requests a
// certificate from a client.
//
// In this example keep alives will cause the certificate to
// only be called once, but if we disable them,
// GetClientCertificate will be called on every request.
GetClientCertificate: r.getClientCertificate,
}
// Schedule periodic re-load of certificate
// A real implementation can use something like
// https://github.com/fsnotify/fsnotify
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)
// Set up a connection to the server.
address := os.Getenv("HELLO_MTLS_URL")
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := hello.NewGreeterClient(conn)
for {
if err := sayHello(client); err != nil {
log.Fatalf("could not greet: %v", err)
}
if err := sayHelloAgain(client); err != nil {
log.Fatalf("could not greet: %v", err)
}
time.Sleep(requestFrequency)
}
}

View file

@ -1,22 +0,0 @@
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-grpc:latest
imagePullPolicy: Never
resources: {requests: {cpu: 10m, memory: 20Mi}}
env:
- name: HELLO_MTLS_URL
value: hello-mtls.default.svc.cluster.local:443

View file

@ -1,231 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: hello.proto
package hello
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// The request message containing the user's name.
type HelloRequest struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HelloRequest) Reset() { *m = HelloRequest{} }
func (m *HelloRequest) String() string { return proto.CompactTextString(m) }
func (*HelloRequest) ProtoMessage() {}
func (*HelloRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_hello_4c93420831fe68fb, []int{0}
}
func (m *HelloRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HelloRequest.Unmarshal(m, b)
}
func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic)
}
func (dst *HelloRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_HelloRequest.Merge(dst, src)
}
func (m *HelloRequest) XXX_Size() int {
return xxx_messageInfo_HelloRequest.Size(m)
}
func (m *HelloRequest) XXX_DiscardUnknown() {
xxx_messageInfo_HelloRequest.DiscardUnknown(m)
}
var xxx_messageInfo_HelloRequest proto.InternalMessageInfo
func (m *HelloRequest) GetName() string {
if m != nil {
return m.Name
}
return ""
}
// The response message containing the greetings
type HelloReply struct {
Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HelloReply) Reset() { *m = HelloReply{} }
func (m *HelloReply) String() string { return proto.CompactTextString(m) }
func (*HelloReply) ProtoMessage() {}
func (*HelloReply) Descriptor() ([]byte, []int) {
return fileDescriptor_hello_4c93420831fe68fb, []int{1}
}
func (m *HelloReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HelloReply.Unmarshal(m, b)
}
func (m *HelloReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HelloReply.Marshal(b, m, deterministic)
}
func (dst *HelloReply) XXX_Merge(src proto.Message) {
xxx_messageInfo_HelloReply.Merge(dst, src)
}
func (m *HelloReply) XXX_Size() int {
return xxx_messageInfo_HelloReply.Size(m)
}
func (m *HelloReply) XXX_DiscardUnknown() {
xxx_messageInfo_HelloReply.DiscardUnknown(m)
}
var xxx_messageInfo_HelloReply proto.InternalMessageInfo
func (m *HelloReply) GetMessage() string {
if m != nil {
return m.Message
}
return ""
}
func init() {
proto.RegisterType((*HelloRequest)(nil), "HelloRequest")
proto.RegisterType((*HelloReply)(nil), "HelloReply")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for Greeter service
type GreeterClient interface {
// Sends a greeting
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
// Sends another greeting
SayHelloAgain(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}
type greeterClient struct {
cc *grpc.ClientConn
}
func NewGreeterClient(cc *grpc.ClientConn) GreeterClient {
return &greeterClient{cc}
}
func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
out := new(HelloReply)
err := grpc.Invoke(ctx, "/Greeter/SayHello", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *greeterClient) SayHelloAgain(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
out := new(HelloReply)
err := grpc.Invoke(ctx, "/Greeter/SayHelloAgain", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Greeter service
type GreeterServer interface {
// Sends a greeting
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
// Sends another greeting
SayHelloAgain(context.Context, *HelloRequest) (*HelloReply, error)
}
func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
s.RegisterService(&_Greeter_serviceDesc, srv)
}
func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GreeterServer).SayHello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/Greeter/SayHello",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Greeter_SayHelloAgain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GreeterServer).SayHelloAgain(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/Greeter/SayHelloAgain",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GreeterServer).SayHelloAgain(ctx, req.(*HelloRequest))
}
return interceptor(ctx, in, info, handler)
}
var _Greeter_serviceDesc = grpc.ServiceDesc{
ServiceName: "Greeter",
HandlerType: (*GreeterServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "SayHello",
Handler: _Greeter_SayHello_Handler,
},
{
MethodName: "SayHelloAgain",
Handler: _Greeter_SayHelloAgain_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "hello.proto",
}
func init() { proto.RegisterFile("hello.proto", fileDescriptor_hello_4c93420831fe68fb) }
var fileDescriptor_hello_4c93420831fe68fb = []byte{
// 141 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xce, 0x48, 0xcd, 0xc9,
0xc9, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x57, 0x52, 0xe2, 0xe2, 0xf1, 0x00, 0x71, 0x83, 0x52,
0x0b, 0x4b, 0x53, 0x8b, 0x4b, 0x84, 0x84, 0xb8, 0x58, 0xf2, 0x12, 0x73, 0x53, 0x25, 0x18, 0x15,
0x18, 0x35, 0x38, 0x83, 0xc0, 0x6c, 0x25, 0x35, 0x2e, 0x2e, 0xa8, 0x9a, 0x82, 0x9c, 0x4a, 0x21,
0x09, 0x2e, 0xf6, 0xdc, 0xd4, 0xe2, 0xe2, 0xc4, 0x74, 0x98, 0x22, 0x18, 0xd7, 0x28, 0x89, 0x8b,
0xdd, 0xbd, 0x28, 0x35, 0xb5, 0x24, 0xb5, 0x48, 0x48, 0x83, 0x8b, 0x23, 0x38, 0xb1, 0x12, 0xac,
0x4b, 0x88, 0x57, 0x0f, 0xd9, 0x06, 0x29, 0x6e, 0x3d, 0x84, 0x61, 0x4a, 0x0c, 0x42, 0xba, 0x5c,
0xbc, 0x30, 0x95, 0x8e, 0xe9, 0x89, 0x99, 0x79, 0xf8, 0x95, 0x27, 0xb1, 0x81, 0x9d, 0x6d, 0x0c,
0x08, 0x00, 0x00, 0xff, 0xff, 0xa6, 0x84, 0x2d, 0xb6, 0xc5, 0x00, 0x00, 0x00,
}

View file

@ -1,19 +0,0 @@
syntax = "proto3";
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
// Sends another greeting
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}

View file

@ -1,15 +0,0 @@
# build stage
FROM golang:alpine AS build-env
RUN apk update
RUN apk add git
WORKDIR /go/src/github.com/smallstep/certificates/autocert/examples/hello-mtls/go-grpc
ADD server/server.go .
COPY hello hello
RUN go get -d -v ./...
RUN go build -o server
# final stage
FROM alpine
COPY --from=build-env /go/src/github.com/smallstep/certificates/autocert/examples/hello-mtls/go-grpc/server .
CMD ["./server"]

View file

@ -1,33 +0,0 @@
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-grpc:latest
imagePullPolicy: Never
resources: {requests: {cpu: 10m, memory: 20Mi}}

View file

@ -1,151 +0,0 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/peer"
"github.com/smallstep/certificates/autocert/examples/hello-mtls/go-grpc/hello"
)
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.RWMutex
certificate *tls.Certificate
}
func (r *rotator) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
r.RLock()
defer r.RUnlock()
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
}
// Greeter is a service that sends greetings.
type Greeter struct{}
// SayHello sends a greeting
func (g *Greeter) SayHello(ctx context.Context, in *hello.HelloRequest) (*hello.HelloReply, error) {
return &hello.HelloReply{Message: "Hello " + in.Name + " (" + getServerName(ctx) + ")"}, nil
}
// SayHelloAgain sends another greeting
func (g *Greeter) SayHelloAgain(ctx context.Context, in *hello.HelloRequest) (*hello.HelloReply, error) {
return &hello.HelloReply{Message: "Hello again " + in.Name + " (" + getServerName(ctx) + ")"}, nil
}
func getServerName(ctx context.Context) string {
if p, ok := peer.FromContext(ctx); ok {
if tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo); ok {
return tlsInfo.State.ServerName
}
}
return "unknown"
}
func main() {
roots, err := loadRootCertPool()
if err != nil {
log.Fatal(err)
}
// Load certificate
r := &rotator{}
if err := r.loadCertificate(autocertFile, autocertKey); err != nil {
log.Fatal("error loading certificate and key", err)
}
tlsConfig := &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,
}
// Schedule periodic re-load of certificate
// A real implementation can use something like
// https://github.com/fsnotify/fsnotify
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)
lis, err := net.Listen("tcp", ":443")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
srv := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))
hello.RegisterGreeterServer(srv, &Greeter{})
log.Println("Listening on :443")
if err := srv.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

View file

@ -1,10 +0,0 @@
# 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

@ -1,142 +0,0 @@
package main
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"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"
requestFrequency = 5 * time.Second
tickFrequency = 15 * time.Second
)
type rotator struct {
sync.RWMutex
certificate *tls.Certificate
}
func (r *rotator) getClientCertificate(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
r.RLock()
defer r.RUnlock()
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() {
url := os.Getenv("HELLO_MTLS_URL")
// Read the root certificate for our CA from disk
roots, err := loadRootCertPool()
if err != nil {
log.Fatal(err)
}
// Load certificate
r := &rotator{}
if err := r.loadCertificate(autocertFile, autocertKey); err != nil {
log.Fatal("error loading certificate and key", err)
}
// Create an HTTPS client using our cert, key & pool
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: roots,
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,
},
// GetClientCertificate is called when a server requests a
// certificate from a client.
//
// In this example keep alives will cause the certificate to
// only be called once, but if we disable them,
// GetClientCertificate will be called on every request.
GetClientCertificate: r.getClientCertificate,
},
// Add this line to get the certificate on every request.
// DisableKeepAlives: true,
},
}
// Schedule periodic re-load of certificate
// A real implementation can use something like
// https://github.com/fsnotify/fsnotify
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)
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

@ -1,22 +0,0 @@
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

@ -1,10 +0,0 @@
# 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

@ -1,33 +0,0 @@
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

@ -1,137 +0,0 @@
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.RWMutex
certificate *tls.Certificate
}
func (r *rotator) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
r.RLock()
defer r.RUnlock()
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
// A real implementation can use something like
// https://github.com/fsnotify/fsnotify
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)
}
}

View file

@ -1,11 +0,0 @@
FROM nginx:alpine
RUN apk add inotify-tools
RUN mkdir /src
ADD site.conf /etc/nginx/conf.d
ADD certwatch.sh /src
ADD entrypoint.sh /src
# Certificate watcher and nginx
ENTRYPOINT ["/src/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

View file

@ -1,6 +0,0 @@
#!/bin/sh
while true; do
inotifywait -e modify /var/run/autocert.step.sm/site.crt
nginx -s reload
done

View file

@ -1,7 +0,0 @@
#!/bin/sh
# watch for the update of the cert and reload nginx
/src/certwatch.sh &
# Run docker CMD
exec "$@"

View file

@ -1,33 +0,0 @@
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-nginx:latest
imagePullPolicy: Never
resources: {requests: {cpu: 10m, memory: 20Mi}}

View file

@ -1,16 +0,0 @@
server {
listen 443 ssl;
server_name localhost;
ssl_protocols TLSv1.2;
ssl_certificate /var/run/autocert.step.sm/site.crt;
ssl_certificate_key /var/run/autocert.step.sm/site.key;
ssl_client_certificate /var/run/autocert.step.sm/root.crt;
ssl_verify_client on;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}

View file

@ -1,6 +0,0 @@
FROM node:lts-alpine
RUN mkdir /src
ADD client.js /src
CMD ["node", "/src/client.js"]

View file

@ -1,6 +0,0 @@
FROM node:lts-alpine
RUN mkdir /src
ADD server.js /src
CMD ["node", "/src/server.js"]

View file

@ -1,44 +0,0 @@
const fs = require('fs');
const https = require('https');
const config = {
ca: '/var/run/autocert.step.sm/root.crt',
key: '/var/run/autocert.step.sm/site.key',
cert: '/var/run/autocert.step.sm/site.crt',
url: process.env.HELLO_MTLS_URL,
requestFrequency: 5000
};
var options = {
ca: fs.readFileSync(config.ca),
key: fs.readFileSync(config.key),
cert: fs.readFileSync(config.cert),
ciphers: 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256',
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.2',
// Not necessary as it defaults to true
rejectUnauthorized: true
};
fs.watch(config.cert, (event, filename) => {
if (event == 'change') {
options.cert = fs.readFileSync(config.cert);
}
});
function loop() {
var req = https.request(config.url, options, function(res) {
res.on('data', (data) => {
process.stdout.write(options.cert)
process.stdout.write(data)
setTimeout(loop, config.requestFrequency);
});
});
req.on('error', (e) => {
process.stderr.write('error: ' + e.message + '\n');
setTimeout(loop, config.requestFrequency);
})
req.end();
}
loop();

View file

@ -1,22 +0,0 @@
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-node:latest
imagePullPolicy: Never
resources: {requests: {cpu: 10m, memory: 20Mi}}
env:
- name: HELLO_MTLS_URL
value: https://hello-mtls.default.svc.cluster.local

View file

@ -1,33 +0,0 @@
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-node:latest
imagePullPolicy: Never
resources: {requests: {cpu: 10m, memory: 20Mi}}

View file

@ -1,42 +0,0 @@
const https = require('https');
const tls = require('tls');
const fs = require('fs');
var config = {
ca: '/var/run/autocert.step.sm/root.crt',
key: '/var/run/autocert.step.sm/site.key',
cert: '/var/run/autocert.step.sm/site.crt',
ciphers: 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256',
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.2'
};
function createSecureContext() {
return tls.createSecureContext({
ca: fs.readFileSync(config.ca),
key: fs.readFileSync(config.key),
cert: fs.readFileSync(config.cert),
ciphers: config.ciphers,
});
}
var ctx = createSecureContext()
fs.watch(config.cert, (event, filename) => {
if (event == 'change') {
ctx = createSecureContext();
}
});
https.createServer({
requestCert: true,
rejectUnauthorized: true,
SNICallback: (servername, cb) => {
cb(null, ctx);
}
}, (req, res) => {
res.writeHead(200);
res.end('hello nodejs\n');
}).listen(443);
console.log("Listening on :443 ...");

View file

@ -1,9 +0,0 @@
FROM python:alpine
RUN mkdir /src
ADD client.py /src
ADD client.requirements.txt /src
RUN pip3 install -r /src/client.requirements.txt
CMD ["python", "/src/client.py"]

View file

@ -1,14 +0,0 @@
FROM python:alpine
RUN mkdir /src
# Gunicorn configuration
ADD gunicorn.conf /src
# Flask app
ADD server.py /src
ADD requirements.txt /src
RUN pip3 install -r /src/requirements.txt
# app, certificate watcher and envoy
CMD ["gunicorn", "--config", "/src/gunicorn.conf", "--pythonpath", "/src", "server:app"]

View file

@ -1,79 +0,0 @@
#!/usr/bin/env python
import os
import sys
import ssl
import signal
import time
import logging
import threading
import http.client
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from urllib.parse import urlparse
ca_certs = '/var/run/autocert.step.sm/root.crt'
cert_file = '/var/run/autocert.step.sm/site.crt'
key_file = '/var/run/autocert.step.sm/site.key'
# RenewHandler is an even file system event handler that reloads the certs in
# the context when a file is modified.
class RenewHandler(FileSystemEventHandler):
def __init__(self, ctx):
self.ctx = ctx
super().__init__()
def on_modified(self, event):
logging.info("reloading certs ...")
ctx.load_cert_chain(cert_file, key_file)
# Monitor is a thread that watches for changes in a path and calls to the
# RenewHandler when a file is modified.
class Monitor(threading.Thread):
def __init__(self, handler, path):
super().__init__()
self.handler = handler
self.path = path
def run(self):
observer = Observer()
observer.schedule(self.handler, self.path)
observer.start()
# Signal handler
def handler(signum, frame):
print("exiting ...")
sys.exit(0)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
# Start signal handler to exit
signal.signal(signal.SIGTERM, handler)
# url from the environment
url = urlparse(os.environ['HELLO_MTLS_URL'])
# ssl context
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
ctx.set_ciphers('ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256')
ctx.load_verify_locations(ca_certs)
ctx.load_cert_chain(cert_file, key_file)
# initialize the renewer with the ssl context
renewer = RenewHandler(ctx)
# start file monitor
monitor = Monitor(renewer, os.path.dirname(cert_file))
monitor.start()
# Do requests
while True:
try:
conn = http.client.HTTPSConnection(url.netloc, context=ctx)
conn.request("GET", url.path)
r = conn.getresponse()
data = r.read()
logging.info("%d - %s - %s", r.status, r.reason, data)
except Exception as err:
print('Something went wrong:', err)
time.sleep(5)

View file

@ -1,14 +0,0 @@
bind = '0.0.0.0:443'
workers = 2
accesslog = '-'
# mTLS configuration with TLSv1.2 and requiring and validating client
# certificates
ssl_version = 5 # ssl.PROTOCOL_TLSv1_2
cert_reqs = 2 # ssl.CERT_REQUIRED
ciphers = 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256'
ca_certs = '/var/run/autocert.step.sm/root.crt'
certfile = '/var/run/autocert.step.sm/site.crt'
keyfile = '/var/run/autocert.step.sm/site.key'

View file

@ -1,22 +0,0 @@
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-py-gunicorn:latest
imagePullPolicy: Never
resources: {requests: {cpu: 10m, memory: 20Mi}}
env:
- name: HELLO_MTLS_URL
value: https://hello-mtls.default.svc.cluster.local

View file

@ -1,33 +0,0 @@
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-py-gunicorn:latest
imagePullPolicy: Never
resources: {requests: {cpu: 10m, memory: 20Mi}}

View file

@ -1,2 +0,0 @@
Flask
gunicorn

View file

@ -1,9 +0,0 @@
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!\n"
if __name__ == "__main__":
app.run(host='127.0.0.1', port=8080, debug=False)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View file

@ -1,193 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xl="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="985.6917 4772.835 272.38144 274.09766" width="272.38144" height="274.09766">
<defs/>
<metadata> Produced by OmniGraffle 7.9.4
<dc:date>2019-05-24 18:12:06 +0000</dc:date>
</metadata>
<g id="Canvas_3" stroke="none" stroke-opacity="1" fill-opacity="1" fill="none" stroke-dasharray="none">
<title>Canvas 3</title>
<g id="Canvas_3: Layer 1">
<title>Layer 1</title>
<g id="Graphic_1074">
<path d="M 1251.4366 4909.882 C 1251.4366 4981.429 1193.4327 5039.433 1121.8859 5039.433 C 1050.3351 5039.433 992.3351 4981.429 992.3351 4909.882 C 992.3351 4838.331 1050.3351 4780.331 1121.8859 4780.331 C 1193.4327 4780.331 1251.4366 4838.331 1251.4366 4909.882 L 1251.4366 4909.882" fill="#f8fbff"/>
</g>
<g id="Graphic_1073">
<path d="M 1121.8859 4780.331 C 1113.1398 4780.331 1104.5968 4781.202 1096.3351 4782.8546 C 1155.6476 4794.722 1200.3351 4847.077 1200.3351 4909.882 C 1200.3351 4972.683 1155.6476 5025.042 1096.3351 5036.9054 C 1104.5968 5038.558 1113.1398 5039.433 1121.8859 5039.433 C 1193.4327 5039.433 1251.4366 4981.429 1251.4366 4909.882 C 1251.4366 4838.331 1193.4327 4780.331 1121.8859 4780.331 L 1121.8859 4780.331" fill="#e4eaf7"/>
</g>
<g id="Graphic_1071">
<path d="M 1251.4288 4917.5343 C 1247.3156 4917.0656 1243.5929 4920.007 1243.1124 4924.12 C 1235.9757 4985.585 1183.8624 5031.933 1121.8859 5031.933 C 1059.8741 5031.933 1007.757 4985.554 1000.6515 4924.054 C 1000.1749 4919.9406 996.4562 4916.9796 992.339 4917.464 C 988.2257 4917.9406 985.2726 4921.6593 985.7491 4925.7765 C 993.7296 4994.847 1052.257 5046.933 1121.8859 5046.933 C 1191.4757 5046.933 1249.9991 4994.878 1258.0148 4925.847 C 1258.4913 4921.7335 1255.5421 4918.011 1251.4288 4917.5343 L 1251.4288 4917.5343" fill="black"/>
</g>
<g id="Graphic_1070">
<path d="M 1121.8859 4772.835 C 1052.2335 4772.835 993.7023 4824.9406 985.7413 4894.038 C 985.2687 4898.1515 988.2218 4901.87 992.3351 4902.347 C 992.628 4902.382 992.9171 4902.3976 993.2023 4902.3976 C 996.9601 4902.3976 1000.2023 4899.577 1000.6437 4895.753 C 1007.7335 4834.2296 1059.8546 4787.835 1121.8859 4787.835 C 1183.9015 4787.835 1236.0226 4834.218 1243.1202 4895.7257 C 1243.5968 4899.839 1247.3116 4902.792 1251.4327 4902.3156 C 1255.5499 4901.843 1258.4991 4898.12 1258.0226 4894.007 C 1250.0499 4824.929 1191.5226 4772.835 1121.8859 4772.835 L 1121.8859 4772.835" fill="black"/>
</g>
<g id="Graphic_1068">
<path d="M 1142.6322 4838.79 C 1142.6322 4847.0837 1137.7739 4854.223 1130.7533 4857.5105 C 1128.0587 4858.811 1125.0662 4859.5253 1121.8972 4859.5253 C 1118.6978 4859.5253 1115.6764 4858.8137 1112.9803 4857.5105 C 1105.993 4854.194 1101.1621 4847.055 1101.1621 4838.79 C 1101.1621 4827.3264 1110.4362 4818.055 1121.8972 4818.055 C 1133.3307 4818.055 1142.6322 4827.3264 1142.6322 4838.79 M 1130.7836 4838.79 C 1130.7836 4833.8726 1126.7844 4829.904 1121.8972 4829.904 C 1116.9795 4829.904 1113.0107 4833.8726 1113.0107 4838.79 C 1113.0107 4843.708 1116.9795 4847.6767 1121.8972 4847.6767 C 1126.7844 4847.6767 1130.7836 4843.708 1130.7836 4838.79" fill="white"/>
</g>
<g id="Graphic_1067">
<path d="M 1089.9947 4880.289 C 1091.4136 4881.86 1092.2757 4883.934 1092.2757 4886.2136 C 1092.2757 4888.4974 1091.4165 4890.5397 1090.0237 4892.109 C 1088.424 4893.946 1086.0245 4895.1 1083.3892 4895.1 C 1078.4716 4895.1 1074.5027 4891.131 1074.5027 4886.2136 L 1074.5027 4886.1847 C 1074.5317 4881.267 1078.4716 4877.327 1083.3892 4877.327 C 1086.0288 4877.327 1088.3922 4878.454 1089.9947 4880.289" fill="white"/>
</g>
<g id="Graphic_1066">
<path d="M 1169.2612 4886.1847 L 1169.2612 4886.2136 C 1169.2612 4891.131 1165.2924 4895.1 1160.3748 4895.1 C 1157.7352 4895.1 1155.3718 4893.946 1153.7692 4892.109 L 1153.7403 4892.109 C 1152.3475 4890.5397 1151.4883 4888.4945 1151.4883 4886.2136 C 1151.4883 4883.934 1152.3475 4881.86 1153.7692 4880.289 C 1155.4022 4878.454 1157.7713 4877.327 1160.3748 4877.327 C 1165.2924 4877.327 1169.2323 4881.267 1169.2612 4886.1847" fill="white"/>
</g>
<g id="Graphic_1065">
<path d="M 1112.9803 4892.109 L 1090.0237 4892.109 C 1091.4165 4890.5397 1092.2757 4888.4945 1092.2757 4886.2136 C 1092.2757 4883.934 1091.4165 4881.86 1089.9947 4880.289 L 1153.7692 4880.289 C 1152.3503 4881.86 1151.4883 4883.934 1151.4883 4886.2136 C 1151.4883 4888.4974 1152.3475 4890.5397 1153.7403 4892.109 L 1112.9803 4892.109" fill="white"/>
</g>
<g id="Graphic_1064">
<path d="M 1130.7533 4880.2604 L 1130.7533 4857.5105 C 1128.0587 4858.811 1125.0662 4859.5253 1121.8972 4859.5253 C 1118.6978 4859.5253 1115.6764 4858.8137 1112.9803 4857.5105 L 1112.9803 4880.289 L 1130.7533 4880.289 L 1130.7533 4880.2604" fill="#d7e9ff"/>
</g>
<g id="Graphic_1063">
<path d="M 1184.072 4933.579 L 1178.1477 4935.948 L 1178.1477 4939.5034 C 1178.1477 4957.5135 1163.5741 4972.087 1145.564 4972.087 L 1142.6322 4972.087 C 1138.9295 4972.087 1135.6449 4970.399 1133.4493 4967.7624 C 1131.7874 4965.719 1130.7533 4963.1124 1130.7533 4960.2385 L 1130.7533 4892.109 L 1112.9803 4892.109 L 1112.9803 4960.2095 C 1112.9803 4964.326 1110.9077 4967.9375 1107.7677 4970.072 C 1105.8686 4971.3465 1103.5906 4972.087 1101.1317 4972.087 L 1098.2 4972.087 C 1080.1898 4972.087 1065.6163 4957.5135 1065.6163 4939.5034 L 1065.6163 4935.948 L 1059.692 4933.579 L 1053.7677 4935.948 L 1053.7677 4939.5034 C 1053.7677 4968.9774 1077.6153 4992.822 1107.0864 4992.822 L 1113.0107 4992.822 C 1117.898 4992.822 1121.8972 4996.791 1121.8972 5001.7086 C 1121.8972 4996.791 1125.866 4992.822 1130.7836 4992.822 L 1136.6776 4992.822 C 1166.1487 4992.822 1189.9963 4968.9774 1189.9963 4939.5034 L 1189.9963 4935.948 L 1184.072 4933.579" fill="#d7e9ff"/>
</g>
<g id="Graphic_1062">
<path d="M 1184.072 4912.844 L 1198.8827 4939.5034 L 1189.9963 4935.948 L 1184.072 4933.579 L 1178.1477 4935.948 L 1169.2612 4939.5034 L 1184.072 4912.844" fill="white"/>
</g>
<g id="Graphic_1061">
<path d="M 1059.692 4912.844 L 1074.5027 4939.5034 L 1065.6163 4935.948 L 1059.692 4933.579 L 1053.7677 4935.948 L 1044.8812 4939.5034 L 1059.692 4912.844" fill="white"/>
</g>
<g id="Group_1013">
<g id="Graphic_1060">
<path d="M 1121.8827 5004.671 C 1120.244 5004.671 1118.9206 5003.3473 1118.9206 5001.7086 C 1118.9206 4995.174 1124.2345 4989.86 1130.7692 4989.86 C 1132.4064 4989.86 1133.7313 4991.1834 1133.7313 4992.822 C 1133.7313 4994.461 1132.4064 4995.784 1130.7692 4995.784 C 1127.5018 4995.784 1124.8449 4998.441 1124.8449 5001.7086 C 1124.8449 5003.3473 1123.52 5004.671 1121.8827 5004.671" fill="#116fff"/>
</g>
<g id="Graphic_1059">
<path d="M 1121.8827 5004.671 C 1120.244 5004.671 1118.9206 5003.3473 1118.9206 5001.7086 C 1118.9206 4998.441 1116.2621 4995.784 1112.9963 4995.784 C 1111.3575 4995.784 1110.0341 4994.461 1110.0341 4992.822 C 1110.0341 4991.1834 1111.3575 4989.86 1112.9963 4989.86 C 1119.5295 4989.86 1124.8449 4995.174 1124.8449 5001.7086 C 1124.8449 5003.3473 1123.52 5004.671 1121.8827 5004.671" fill="#116fff"/>
</g>
<g id="Graphic_1058">
<path d="M 1136.6689 4995.784 C 1135.0316 4995.784 1133.7067 4994.461 1133.7067 4992.822 C 1133.7067 4991.1834 1135.0316 4989.86 1136.6689 4989.86 C 1164.4362 4989.86 1187.0255 4967.2707 1187.0255 4939.5034 C 1187.0255 4937.8647 1188.3503 4936.541 1189.9876 4936.541 C 1191.6263 4936.541 1192.9498 4937.8647 1192.9498 4939.5034 C 1192.9498 4970.538 1167.7035 4995.784 1136.6689 4995.784 L 1136.6689 4995.784" fill="#116fff"/>
</g>
<g id="Graphic_1057">
<path d="M 1145.5669 4975.049 C 1143.9296 4975.049 1142.6048 4973.726 1142.6048 4972.087 C 1142.6048 4970.4483 1143.9296 4969.125 1145.5669 4969.125 C 1161.9007 4969.125 1175.1884 4955.837 1175.1884 4939.5034 C 1175.1884 4937.8647 1176.5133 4936.541 1178.1506 4936.541 C 1179.7893 4936.541 1181.1127 4937.8647 1181.1127 4939.5034 C 1181.1127 4959.1045 1165.168 4975.049 1145.5669 4975.049" fill="#116fff"/>
</g>
<g id="Graphic_1056">
<path d="M 1136.6689 4995.784 L 1130.7692 4995.784 C 1129.1304 4995.784 1127.807 4994.461 1127.807 4992.822 C 1127.807 4991.1834 1129.1304 4989.86 1130.7692 4989.86 L 1136.6689 4989.86 C 1138.3076 4989.86 1139.631 4991.1834 1139.631 4992.822 C 1139.631 4994.461 1138.3076 4995.784 1136.6689 4995.784" fill="#116fff"/>
</g>
<g id="Graphic_1055">
<path d="M 1169.267 4942.4655 C 1168.781 4942.4655 1168.2864 4942.347 1167.8337 4942.0924 C 1166.4032 4941.2983 1165.8883 4939.495 1166.6809 4938.067 L 1181.4917 4911.399 C 1182.2857 4909.97 1184.0908 4909.4494 1185.5183 4910.2464 C 1186.9488 4911.0404 1187.4637 4912.844 1186.6697 4914.2716 L 1171.8589 4940.9396 C 1171.318 4941.9145 1170.3098 4942.4655 1169.267 4942.4655" fill="#116fff"/>
</g>
<g id="Graphic_1054">
<path d="M 1198.8943 4942.4655 C 1197.853 4942.4655 1196.8448 4941.9145 1196.3024 4940.9396 L 1181.4917 4914.2716 C 1180.6962 4912.844 1181.2111 4911.0375 1182.6444 4910.2464 C 1184.072 4909.4465 1185.88 4909.967 1186.6697 4911.399 L 1201.4804 4938.067 C 1202.2774 4939.495 1201.7625 4941.301 1200.3291 4942.0924 C 1199.875 4942.347 1199.3803 4942.4655 1198.8943 4942.4655 L 1198.8943 4942.4655" fill="#116fff"/>
</g>
<g id="Graphic_1053">
<path d="M 1198.8914 4942.4655 C 1198.524 4942.4655 1198.1509 4942.4005 1197.7907 4942.256 L 1184.0807 4936.764 L 1170.372 4942.256 C 1168.8534 4942.859 1167.1293 4942.124 1166.5189 4940.6055 C 1165.9114 4939.0854 1166.6491 4937.3613 1168.1692 4936.751 L 1182.98 4930.821 C 1183.6901 4930.5374 1184.4726 4930.5374 1185.1828 4930.821 L 1199.9936 4936.751 C 1201.5137 4937.3613 1202.2513 4939.0854 1201.6439 4940.6055 C 1201.1781 4941.764 1200.0673 4942.4655 1198.8914 4942.4655 L 1198.8914 4942.4655" fill="#116fff"/>
</g>
<g id="Graphic_1052">
<path d="M 1189.9876 4942.4655 C 1188.3503 4942.4655 1187.0255 4941.142 1187.0255 4939.5034 L 1187.0255 4935.9425 C 1187.0255 4934.305 1188.3503 4932.9803 1189.9876 4932.9803 C 1191.6263 4932.9803 1192.9498 4934.305 1192.9498 4935.9425 L 1192.9498 4939.5034 C 1192.9498 4941.142 1191.6263 4942.4655 1189.9876 4942.4655" fill="#116fff"/>
</g>
<g id="Graphic_1051">
<path d="M 1178.1506 4942.4655 C 1176.5133 4942.4655 1175.1884 4941.142 1175.1884 4939.5034 L 1175.1884 4935.9425 C 1175.1884 4934.305 1176.5133 4932.9803 1178.1506 4932.9803 C 1179.7893 4932.9803 1181.1127 4934.305 1181.1127 4935.9425 L 1181.1127 4939.5034 C 1181.1127 4941.142 1179.7893 4942.4655 1178.1506 4942.4655" fill="#116fff"/>
</g>
<g id="Graphic_1050">
<path d="M 1107.0951 4995.7814 C 1076.0605 4995.7814 1050.8142 4970.535 1050.8142 4939.5005 C 1050.8142 4937.862 1052.1376 4936.5384 1053.7764 4936.5384 C 1055.4136 4936.5384 1056.7385 4937.862 1056.7385 4939.5005 C 1056.7385 4967.268 1079.3278 4989.857 1107.0951 4989.857 C 1108.7324 4989.857 1110.0572 4991.1805 1110.0572 4992.819 C 1110.0572 4994.4565 1108.7324 4995.7814 1107.0951 4995.7814" fill="#116fff"/>
</g>
<g id="Graphic_1049">
<path d="M 1098.197 4975.0463 C 1078.596 4975.0463 1062.6512 4959.1016 1062.6512 4939.5005 C 1062.6512 4937.862 1063.9747 4936.5384 1065.6134 4936.5384 C 1067.2507 4936.5384 1068.5755 4937.862 1068.5755 4939.5005 C 1068.5755 4955.834 1081.8633 4969.122 1098.197 4969.122 C 1099.8343 4969.122 1101.1592 4970.4454 1101.1592 4972.084 C 1101.1592 4973.7215 1099.8343 4975.0463 1098.197 4975.0463" fill="#116fff"/>
</g>
<g id="Graphic_1048">
<path d="M 1112.9963 4995.7814 L 1107.0951 4995.7814 C 1105.4564 4995.7814 1104.133 4994.4565 1104.133 4992.819 C 1104.133 4991.1805 1105.4564 4989.857 1107.0951 4989.857 L 1112.9963 4989.857 C 1114.6335 4989.857 1115.9584 4991.1805 1115.9584 4992.819 C 1115.9584 4994.4565 1114.6335 4995.7814 1112.9963 4995.7814" fill="#116fff"/>
</g>
<g id="Graphic_1047">
<path d="M 1074.497 4942.4627 C 1073.4541 4942.4627 1072.446 4941.9116 1071.9051 4940.937 L 1057.0943 4914.2687 C 1056.2974 4912.841 1056.8123 4911.0346 1058.2456 4910.2435 C 1059.6732 4909.441 1061.4811 4909.9643 1062.2723 4911.396 L 1077.083 4938.064 C 1077.8786 4939.492 1077.3636 4941.2983 1075.9303 4942.0895 C 1075.4776 4942.344 1074.983 4942.4627 1074.497 4942.4627" fill="#116fff"/>
</g>
<g id="Graphic_1046">
<path d="M 1044.8697 4942.4627 C 1044.3837 4942.4627 1043.889 4942.344 1043.4349 4942.0895 C 1042.0044 4941.2954 1041.4895 4939.492 1042.2836 4938.064 L 1057.0943 4911.396 C 1057.8884 4909.967 1059.692 4909.4436 1061.1195 4910.2435 C 1062.55 4911.0375 1063.0649 4912.841 1062.2723 4914.2687 L 1047.4615 4940.937 C 1046.9192 4941.9116 1045.911 4942.4627 1044.8697 4942.4627 L 1044.8697 4942.4627" fill="#116fff"/>
</g>
<g id="Graphic_1045">
<path d="M 1074.4941 4942.4627 C 1074.1267 4942.4627 1073.7535 4942.3976 1073.392 4942.253 L 1059.6833 4936.761 L 1045.9732 4942.253 C 1044.4545 4942.859 1042.7305 4942.1213 1042.1201 4940.6026 C 1041.5126 4939.0825 1042.2503 4937.3584 1043.7704 4936.748 L 1058.5812 4930.818 C 1059.2913 4930.5345 1060.0738 4930.5345 1060.784 4930.818 L 1075.5947 4936.748 C 1077.1149 4937.3584 1077.8525 4939.0825 1077.245 4940.6026 C 1076.7779 4941.761 1075.6671 4942.4627 1074.4941 4942.4627" fill="#116fff"/>
</g>
<g id="Graphic_1044">
<path d="M 1053.7764 4942.4627 C 1052.1376 4942.4627 1050.8142 4941.138 1050.8142 4939.5005 L 1050.8142 4935.9396 C 1050.8142 4934.302 1052.1376 4932.9774 1053.7764 4932.9774 C 1055.4136 4932.9774 1056.7385 4934.302 1056.7385 4935.9396 L 1056.7385 4939.5005 C 1056.7385 4941.138 1055.4136 4942.4627 1053.7764 4942.4627" fill="#116fff"/>
</g>
<g id="Graphic_1043">
<path d="M 1065.6134 4942.4627 C 1063.9747 4942.4627 1062.6512 4941.138 1062.6512 4939.5005 L 1062.6512 4935.9396 C 1062.6512 4934.302 1063.9747 4932.9774 1065.6134 4932.9774 C 1067.2507 4932.9774 1068.5755 4934.302 1068.5755 4935.9396 L 1068.5755 4939.5005 C 1068.5755 4941.138 1067.2507 4942.4627 1065.6134 4942.4627" fill="#116fff"/>
</g>
<g id="Graphic_1042">
<path d="M 1101.1288 4975.0463 L 1098.197 4975.0463 C 1096.5583 4975.0463 1095.2349 4973.7215 1095.2349 4972.084 C 1095.2349 4970.4454 1096.5583 4969.122 1098.197 4969.122 L 1101.1288 4969.122 C 1102.7676 4969.122 1104.091 4970.4454 1104.091 4972.084 C 1104.091 4973.7215 1102.7676 4975.0463 1101.1288 4975.0463" fill="#116fff"/>
</g>
<g id="Graphic_1041">
<path d="M 1145.5669 4975.049 L 1142.6178 4975.049 C 1140.979 4975.049 1139.6556 4973.726 1139.6556 4972.087 C 1139.6556 4970.4483 1140.979 4969.125 1142.6178 4969.125 L 1145.5669 4969.125 C 1147.2056 4969.125 1148.529 4970.4483 1148.529 4972.087 C 1148.529 4973.726 1147.2056 4975.049 1145.5669 4975.049" fill="#116fff"/>
</g>
<g id="Graphic_1040">
<path d="M 1112.989 4963.183 C 1111.3517 4963.183 1110.0269 4961.8584 1110.0269 4960.221 L 1110.0269 4892.1205 C 1110.0269 4890.483 1111.3517 4889.1584 1112.989 4889.1584 C 1114.6278 4889.1584 1115.9512 4890.483 1115.9512 4892.1205 L 1115.9512 4960.221 C 1115.9512 4961.8584 1114.6278 4963.183 1112.989 4963.183" fill="#116fff"/>
</g>
<g id="Graphic_1039">
<path d="M 1130.7619 4963.186 C 1129.1246 4963.186 1127.7998 4961.861 1127.7998 4960.224 L 1127.7998 4892.1205 C 1127.7998 4890.483 1129.1246 4889.1584 1130.7619 4889.1584 C 1132.4007 4889.1584 1133.7241 4890.483 1133.7241 4892.1205 L 1133.7241 4960.224 C 1133.7241 4961.861 1132.4007 4963.186 1130.7619 4963.186" fill="#116fff"/>
</g>
<g id="Graphic_1038">
<path d="M 1112.989 4883.234 C 1111.3517 4883.234 1110.0269 4881.9107 1110.0269 4880.272 L 1110.0269 4857.5235 C 1110.0269 4855.885 1111.3517 4854.5614 1112.989 4854.5614 C 1114.6278 4854.5614 1115.9512 4855.885 1115.9512 4857.5235 L 1115.9512 4880.272 C 1115.9512 4881.9107 1114.6278 4883.234 1112.989 4883.234" fill="#116fff"/>
</g>
<g id="Graphic_1037">
<path d="M 1130.7619 4883.234 C 1129.1246 4883.234 1127.7998 4881.9107 1127.7998 4880.272 L 1127.7998 4857.5235 C 1127.7998 4855.885 1129.1246 4854.5614 1130.7619 4854.5614 C 1132.4007 4854.5614 1133.7241 4855.885 1133.7241 4857.5235 L 1133.7241 4880.272 C 1133.7241 4881.9107 1132.4007 4883.234 1130.7619 4883.234" fill="#116fff"/>
</g>
<g id="Graphic_1036">
<path d="M 1101.1288 4975.0463 C 1099.4916 4975.0463 1098.1667 4973.7215 1098.1667 4972.084 C 1098.1667 4970.4454 1099.4916 4969.122 1101.1288 4969.122 C 1106.0349 4969.122 1110.0269 4965.1286 1110.0269 4960.221 C 1110.0269 4958.5824 1111.3517 4957.259 1112.989 4957.259 C 1114.6278 4957.259 1115.9512 4958.5824 1115.9512 4960.221 C 1115.9512 4968.396 1109.3022 4975.0463 1101.1288 4975.0463" fill="#116fff"/>
</g>
<g id="Graphic_1035">
<path d="M 1142.6178 4975.0463 C 1134.4415 4975.0463 1127.7882 4968.396 1127.7882 4960.221 C 1127.7882 4958.5824 1129.1131 4957.259 1130.7504 4957.259 C 1132.3891 4957.259 1133.7125 4958.5824 1133.7125 4960.221 C 1133.7125 4965.1286 1137.7088 4969.122 1142.6178 4969.122 C 1144.255 4969.122 1145.5799 4970.4454 1145.5799 4972.084 C 1145.5799 4973.7215 1144.255 4975.0463 1142.6178 4975.0463" fill="#116fff"/>
</g>
<g id="Graphic_1034">
<path d="M 1153.7606 4895.0827 L 1090.0092 4895.0827 C 1088.3705 4895.0827 1087.047 4893.759 1087.047 4892.1205 C 1087.047 4890.483 1088.3705 4889.1584 1090.0092 4889.1584 L 1153.7577 4889.1584 C 1155.3964 4889.1584 1156.7198 4890.483 1156.7198 4892.1205 C 1156.7198 4893.759 1155.3964 4895.0827 1153.7606 4895.0827" fill="#116fff"/>
</g>
<g id="Graphic_1033">
<path d="M 1153.7606 4883.2485 L 1090.0034 4883.2485 C 1088.3647 4883.2485 1087.0413 4881.925 1087.0413 4880.2864 C 1087.0413 4878.649 1088.3647 4877.324 1090.0034 4877.324 L 1153.7577 4877.324 C 1155.3964 4877.324 1156.7198 4878.649 1156.7198 4880.2864 C 1156.7198 4881.925 1155.3964 4883.2485 1153.7606 4883.2485" fill="#116fff"/>
</g>
<g id="Graphic_1032">
<path d="M 1083.3805 4898.0593 C 1076.8459 4898.0593 1071.5319 4892.7454 1071.5319 4886.2107 C 1071.5319 4879.6775 1076.8459 4874.362 1083.3805 4874.362 C 1089.9137 4874.362 1095.2291 4879.6775 1095.2291 4886.2107 C 1095.2291 4892.7454 1089.9137 4898.0593 1083.3805 4898.0593 M 1083.3805 4880.2864 C 1080.1132 4880.2864 1077.4562 4882.9434 1077.4562 4886.2107 C 1077.4562 4889.478 1080.1132 4892.135 1083.3805 4892.135 C 1086.6479 4892.135 1089.3048 4889.478 1089.3048 4886.2107 C 1089.3048 4882.9434 1086.6435 4880.2864 1083.3805 4880.2864" fill="#116fff"/>
</g>
<g id="Graphic_1031">
<path d="M 1160.3834 4898.0593 C 1153.8502 4898.0593 1148.5348 4892.7454 1148.5348 4886.2107 C 1148.5348 4879.6775 1153.8502 4874.362 1160.3834 4874.362 C 1166.9181 4874.362 1172.232 4879.6775 1172.232 4886.2107 C 1172.232 4892.7454 1166.9181 4898.0593 1160.3834 4898.0593 M 1160.3834 4880.2864 C 1157.1161 4880.2864 1154.4591 4882.9434 1154.4591 4886.2107 C 1154.4591 4889.478 1157.1161 4892.135 1160.3834 4892.135 C 1163.6508 4892.135 1166.3078 4889.478 1166.3078 4886.2107 C 1166.3078 4882.9434 1163.6508 4880.2864 1160.3834 4880.2864" fill="#116fff"/>
</g>
<g id="Graphic_1030">
<path d="M 1121.8827 4862.4875 C 1108.8163 4862.4875 1098.1855 4851.8567 1098.1855 4838.79 C 1098.1855 4825.724 1108.8163 4815.093 1121.8827 4815.093 C 1134.9477 4815.093 1145.5799 4825.724 1145.5799 4838.79 C 1145.5799 4851.8567 1134.9477 4862.4875 1121.8827 4862.4875 M 1121.8827 4821.0173 C 1112.0807 4821.0173 1104.1098 4828.988 1104.1098 4838.79 C 1104.1098 4848.592 1112.0807 4856.563 1121.8827 4856.563 C 1131.6833 4856.563 1139.6556 4848.592 1139.6556 4838.79 C 1139.6556 4828.988 1131.6833 4821.0173 1121.8827 4821.0173" fill="#116fff"/>
</g>
<g id="Graphic_1029">
<path d="M 1121.8827 4850.639 C 1115.348 4850.639 1110.0341 4845.325 1110.0341 4838.79 C 1110.0341 4832.2556 1115.348 4826.9416 1121.8827 4826.9416 C 1128.4159 4826.9416 1133.7313 4832.2556 1133.7313 4838.79 C 1133.7313 4845.325 1128.4159 4850.639 1121.8827 4850.639 M 1121.8827 4832.866 C 1118.6154 4832.866 1115.9584 4835.523 1115.9584 4838.79 C 1115.9584 4842.0576 1118.6154 4844.7145 1121.8827 4844.7145 C 1125.1486 4844.7145 1127.807 4842.0576 1127.807 4838.79 C 1127.807 4835.523 1125.1486 4832.866 1121.8827 4832.866" fill="#116fff"/>
</g>
<g id="Graphic_1028">
<path d="M 1163.334 4841.7524 L 1121.8827 4841.7524 C 1120.244 4841.7524 1118.9206 4840.429 1118.9206 4838.79 C 1118.9206 4837.1515 1120.244 4835.828 1121.8827 4835.828 L 1163.334 4835.828 C 1164.9728 4835.828 1166.2962 4837.1515 1166.2962 4838.79 C 1166.2962 4840.429 1164.9728 4841.7524 1163.334 4841.7524" fill="#116fff"/>
</g>
<g id="Graphic_1027">
<path d="M 1163.334 4865.458 C 1161.6967 4865.458 1160.3719 4864.135 1160.3719 4862.496 C 1160.3719 4860.859 1161.6967 4859.534 1163.334 4859.534 C 1168.2343 4859.534 1172.2205 4855.5435 1172.2205 4850.642 C 1172.2205 4845.743 1168.2343 4841.7524 1163.334 4841.7524 C 1161.6967 4841.7524 1160.3719 4840.429 1160.3719 4838.79 C 1160.3719 4837.1515 1161.6967 4835.828 1163.334 4835.828 C 1171.5016 4835.828 1178.1448 4842.4727 1178.1448 4850.642 C 1178.1448 4858.811 1171.5016 4865.458 1163.334 4865.458" fill="#116fff"/>
</g>
<g id="Graphic_1026">
<path d="M 1163.3398 4865.444 L 1130.7692 4865.444 C 1129.1304 4865.444 1127.807 4864.119 1127.807 4862.4817 C 1127.807 4860.843 1129.1304 4859.5195 1130.7692 4859.5195 L 1163.3398 4859.5195 C 1164.9785 4859.5195 1166.302 4860.843 1166.302 4862.4817 C 1166.302 4864.119 1164.9785 4865.444 1163.3398 4865.444" fill="#116fff"/>
</g>
<g id="Graphic_1025">
<path d="M 1074.4883 4889.15 C 1066.3178 4889.15 1059.6703 4882.505 1059.6703 4874.336 C 1059.6703 4866.167 1066.3178 4859.5195 1074.4883 4859.5195 C 1076.1256 4859.5195 1077.4504 4860.843 1077.4504 4862.4817 C 1077.4504 4864.119 1076.1256 4865.444 1074.4883 4865.444 C 1069.5851 4865.444 1065.5946 4869.4343 1065.5946 4874.336 C 1065.5946 4879.235 1069.5851 4883.2254 1074.4883 4883.2254 C 1076.1256 4883.2254 1077.4504 4884.55 1077.4504 4886.1876 C 1077.4504 4887.826 1076.1256 4889.15 1074.4883 4889.15" fill="#116fff"/>
</g>
<g id="Graphic_1024">
<path d="M 1112.9963 4865.458 L 1074.4883 4865.458 C 1072.8496 4865.458 1071.5261 4864.135 1071.5261 4862.496 C 1071.5261 4860.859 1072.8496 4859.534 1074.4883 4859.534 L 1112.9963 4859.534 C 1114.6335 4859.534 1115.9584 4860.859 1115.9584 4862.496 C 1115.9584 4864.135 1114.6335 4865.458 1112.9963 4865.458" fill="#116fff"/>
</g>
<g id="Graphic_1023">
<path d="M 1098.2086 4936.544 C 1090.0396 4936.544 1083.3921 4929.8995 1083.3921 4921.7305 C 1083.3921 4913.5614 1090.0396 4906.914 1098.2086 4906.914 C 1099.8459 4906.914 1101.1708 4908.2374 1101.1708 4909.876 C 1101.1708 4911.5134 1099.8459 4912.838 1098.2086 4912.838 C 1093.3055 4912.838 1089.3164 4916.829 1089.3164 4921.7305 C 1089.3164 4926.6293 1093.3055 4930.62 1098.2086 4930.62 C 1099.8459 4930.62 1101.1708 4931.9447 1101.1708 4933.582 C 1101.1708 4935.221 1099.8459 4936.544 1098.2086 4936.544" fill="#116fff"/>
</g>
<g id="Graphic_1022">
<path d="M 1112.9963 4912.853 L 1098.2086 4912.853 C 1096.5699 4912.853 1095.2465 4911.529 1095.2465 4909.8906 C 1095.2465 4908.253 1096.5699 4906.9284 1098.2086 4906.9284 L 1112.9963 4906.9284 C 1114.6335 4906.9284 1115.9584 4908.253 1115.9584 4909.8906 C 1115.9584 4911.529 1114.6335 4912.853 1112.9963 4912.853" fill="#116fff"/>
</g>
<g id="Graphic_1021">
<path d="M 1169.2771 4912.847 C 1167.6384 4912.847 1166.315 4911.5235 1166.315 4909.885 C 1166.315 4908.2475 1167.6384 4906.9226 1169.2771 4906.9226 C 1174.176 4906.9226 1178.1636 4902.932 1178.1636 4898.0304 C 1178.1636 4893.1315 1174.176 4889.141 1169.2771 4889.141 C 1167.6384 4889.141 1166.315 4887.816 1166.315 4886.179 C 1166.315 4884.54 1167.6384 4883.217 1169.2771 4883.217 C 1177.4433 4883.217 1184.0879 4889.8613 1184.0879 4898.0304 C 1184.0879 4906.1994 1177.4433 4912.847 1169.2771 4912.847" fill="#116fff"/>
</g>
<g id="Graphic_1020">
<path d="M 1169.2771 4912.8325 L 1130.7692 4912.8325 C 1129.1304 4912.8325 1127.807 4911.5076 1127.807 4909.8703 C 1127.807 4908.2316 1129.1304 4906.908 1130.7692 4906.908 L 1169.2771 4906.908 C 1170.9144 4906.908 1172.2393 4908.2316 1172.2393 4909.8703 C 1172.2393 4911.5076 1170.9144 4912.8325 1169.2771 4912.8325" fill="#116fff"/>
</g>
<g id="Graphic_1019">
<path d="M 1112.9963 4936.544 L 1098.2086 4936.544 C 1096.5699 4936.544 1095.2465 4935.221 1095.2465 4933.582 C 1095.2465 4931.9447 1096.5699 4930.62 1098.2086 4930.62 L 1112.9963 4930.62 C 1114.6335 4930.62 1115.9584 4931.9447 1115.9584 4933.582 C 1115.9584 4935.221 1114.6335 4936.544 1112.9963 4936.544" fill="#116fff"/>
</g>
<g id="Graphic_1018">
<path d="M 1139.6195 4960.253 C 1137.9807 4960.253 1136.6573 4958.9295 1136.6573 4957.291 C 1136.6573 4955.6535 1137.9807 4954.3286 1139.6195 4954.3286 C 1144.5212 4954.3286 1148.5117 4950.3396 1148.5117 4945.4364 C 1148.5117 4940.5375 1144.5212 4936.547 1139.6195 4936.547 C 1137.9807 4936.547 1136.6573 4935.2236 1136.6573 4933.585 C 1136.6573 4931.9476 1137.9807 4930.623 1139.6195 4930.623 C 1147.7885 4930.623 1154.436 4937.2673 1154.436 4945.4364 C 1154.436 4953.607 1147.7885 4960.253 1139.6195 4960.253" fill="#116fff"/>
</g>
<g id="Graphic_1017">
<path d="M 1139.6195 4960.2385 L 1130.7619 4960.2385 C 1129.1246 4960.2385 1127.7998 4958.915 1127.7998 4957.2763 C 1127.7998 4955.6376 1129.1246 4954.314 1130.7619 4954.314 L 1139.6195 4954.314 C 1141.2567 4954.314 1142.5816 4955.6376 1142.5816 4957.2763 C 1142.5816 4958.915 1141.2567 4960.2385 1139.6195 4960.2385" fill="#116fff"/>
</g>
<g id="Graphic_1016">
<path d="M 1139.6195 4936.547 L 1130.7619 4936.547 C 1129.1246 4936.547 1127.7998 4935.2236 1127.7998 4933.585 C 1127.7998 4931.9476 1129.1246 4930.623 1130.7619 4930.623 L 1139.6195 4930.623 C 1141.2567 4930.623 1142.5816 4931.9476 1142.5816 4933.585 C 1142.5816 4935.2236 1141.2567 4936.547 1139.6195 4936.547" fill="#116fff"/>
</g>
<g id="Graphic_1015">
<path d="M 1127.794 4986.886 C 1127.0361 4986.886 1126.2782 4986.5955 1125.6997 4986.0184 L 1113.8641 4974.1785 C 1112.7055 4973.02 1112.7055 4971.1484 1113.8641 4969.99 C 1115.0212 4968.8313 1116.8942 4968.8313 1118.0513 4969.99 L 1129.8883 4981.83 C 1131.0469 4982.9883 1131.0469 4984.86 1129.8883 4986.0184 C 1129.3112 4986.5955 1128.5533 4986.886 1127.794 4986.886" fill="#116fff"/>
</g>
<g id="Graphic_1014">
<path d="M 1115.97 4986.886 C 1115.2106 4986.886 1114.4527 4986.5955 1113.8756 4986.0184 C 1112.7171 4984.86 1112.7171 4982.9883 1113.8756 4981.83 L 1125.7127 4969.993 C 1126.8698 4968.834 1128.7428 4968.834 1129.8999 4969.993 C 1131.0584 4971.151 1131.0584 4973.023 1129.8999 4974.1814 L 1118.0643 4986.0184 C 1117.4858 4986.5955 1116.7279 4986.886 1115.97 4986.886" fill="#116fff"/>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 27 KiB

View file

@ -1,18 +0,0 @@
FROM smallstep/step-cli:0.9.0
ENV CA_NAME="Autocert"
ENV CA_DNS="ca.step.svc.cluster.local,127.0.0.1"
ENV CA_ADDRESS=":4443"
ENV CA_DEFAULT_PROVISIONER="admin"
ENV CA_URL="ca.step.svc.cluster.local"
ENV KUBE_LATEST_VERSION="v1.14.0"
USER root
RUN curl -L https://storage.googleapis.com/kubernetes-release/release/${KUBE_LATEST_VERSION}/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl \
&& chmod +x /usr/local/bin/kubectl
RUN apk --update add expect
COPY autocert.sh /home/step/
RUN chmod +x /home/step/autocert.sh
CMD ["/home/step/autocert.sh"]

View file

@ -1,164 +0,0 @@
#!/bin/bash
#set -x
echo "Welcome to Autocert configuration. Press return to begin."
read ANYKEY
STEPPATH=/home/step/.step
CA_PASSWORD=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32 ; echo '')
AUTOCERT_PASSWORD=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32 ; echo '')
echo -e "\e[1mChecking cluster permissions...\e[0m"
function permission_error {
# TODO: Figure out the actual service account instead of assuming default.
echo
echo -e "\033[0;31mPERMISSION ERROR\033[0m"
echo "Set permissions by running the following command, then try again:"
echo -e "\e[1m"
echo " kubectl create clusterrolebinding autocert-init-binding \\"
echo " --clusterrole cluster-admin \\"
echo " --user \"system:serviceaccount:default:default\""
echo -e "\e[0m"
echo "Once setup is complete you can remove this binding by running:"
echo -e "\e[1m"
echo " kubectl delete clusterrolebinding autocert-init-binding"
echo -e "\e[0m"
exit 1
}
echo -n "Checking for permission to create step namespace: "
kubectl auth can-i create namespaces
if [ $? -ne 0 ]; then
permission_error "create step namespace"
fi
echo -n "Checking for permission to create configmaps in step namespace: "
kubectl auth can-i create configmaps --namespace step
if [ $? -ne 0 ]; then
permission_error "create configmaps"
fi
echo -n "Checking for permission to create secrets in step namespace: "
kubectl auth can-i create secrets --namespace step
if [ $? -ne 0 ]; then
permission_error "create secrets"
fi
echo -n "Checking for permission to create deployments in step namespace: "
kubectl auth can-i create deployments --namespace step
if [ $? -ne 0 ]; then
permission_error "create deployments"
fi
echo -n "Checking for permission to create services in step namespace: "
kubectl auth can-i create services --namespace step
if [ $? -ne 0 ]; then
permission_error "create services"
fi
echo -n "Checking for permission to create cluster role: "
kubectl auth can-i create clusterrole
if [ $? -ne 0 ]; then
permission_error "create cluster roles"
fi
echo -n "Checking for permission to create cluster role binding:"
kubectl auth can-i create clusterrolebinding
if [ $? -ne 0 ]; then
permission_error "create cluster role bindings"
exit 1
fi
# Setting this here on purpose, after the above section which explicitly checks
# for and handles exit errors.
set -e
step ca init \
--name "$CA_NAME" \
--dns "$CA_DNS" \
--address "$CA_ADDRESS" \
--provisioner "$CA_DEFAULT_PROVISIONER" \
--with-ca-url "$CA_URL" \
--password-file <(echo "$CA_PASSWORD")
echo
echo -e "\e[1mCreating autocert provisioner...\e[0m"
expect <<EOD
spawn step ca provisioner add autocert --create
expect "Please enter a password to encrypt the provisioner private key? \\\\\\[leave empty and we'll generate one\\\\\\]: "
send "${AUTOCERT_PASSWORD}\n"
expect eof
EOD
echo
echo -e "\e[1mCreating step namespace and preparing environment...\e[0m"
kubectl create namespace step
kubectl -n step create configmap config --from-file $(step path)/config
kubectl -n step create configmap certs --from-file $(step path)/certs
kubectl -n step create configmap secrets --from-file $(step path)/secrets
kubectl -n step create secret generic ca-password --from-literal "password=${CA_PASSWORD}"
kubectl -n step create secret generic autocert-password --from-literal "password=${AUTOCERT_PASSWORD}"
# Deploy CA and wait for rollout to complete
echo
echo -e "\e[1mDeploying certificate authority...\e[0m"
kubectl apply -f https://raw.githubusercontent.com/smallstep/certificates/autocert/autocert/install/01-step-ca.yaml
kubectl -n step rollout status deployment/ca
# Deploy autocert, setup RBAC, and wait for rollout to complete
echo
echo -e "\e[1mDeploying autocert...\e[0m"
kubectl apply -f https://raw.githubusercontent.com/smallstep/certificates/autocert/autocert/install/02-autocert.yaml
kubectl apply -f https://raw.githubusercontent.com/smallstep/certificates/autocert/autocert/install/03-rbac.yaml
kubectl -n step rollout status deployment/autocert
# Some `base64`s wrap lines... no thanks!
CA_BUNDLE=$(cat $(step path)/certs/root_ca.crt | base64 | tr -d '\n')
cat <<EOF | kubectl apply -f -
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: autocert-webhook-config
labels: {app: autocert}
webhooks:
- name: autocert.step.sm
clientConfig:
service:
name: autocert
namespace: step
path: "/mutate"
caBundle: $CA_BUNDLE
rules:
- operations: ["CREATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
failurePolicy: Ignore
namespaceSelector:
matchLabels:
autocert.step.sm: enabled
EOF
FINGERPRINT=$(step certificate fingerprint $(step path)/certs/root_ca.crt)
echo
echo -e "\e[1mAutocert installed!\e[0m"
echo
echo "Store this information somewhere safe:"
echo " CA & admin provisioner password: ${CA_PASSWORD}"
echo " Autocert password: ${AUTOCERT_PASSWORD}"
echo " CA Fingerprint: ${FINGERPRINT}"
echo

View file

@ -1,87 +0,0 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: ca
name: ca
namespace: step
spec:
type: ClusterIP
ports:
- port: 443
targetPort: 4443
selector:
app: ca
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ca
namespace: step
labels:
app: ca
spec:
replicas: 1
selector:
matchLabels:
app: ca
template:
metadata:
labels:
app: ca
spec:
containers:
- name: ca
image: smallstep/step-ca:0.8.3
env:
- name: PWDPATH
value: /home/step/password/password
resources:
requests:
cpu: 100m
memory: 20Mi
readinessProbe:
httpGet:
path: /health
port: 4443
scheme: HTTPS
initialDelaySeconds: 3
periodSeconds: 3
livenessProbe:
httpGet:
path: /health
port: 4443
scheme: HTTPS
initialDelaySeconds: 3
periodSeconds: 3
volumeMounts:
- name: config
mountPath: /home/step/.step/config
readOnly: true
- name: certs
mountPath: /home/step/.step/certs
readOnly: true
- name: secrets
mountPath: /home/step/.step/secrets
readOnly: true
- name: ca-password
mountPath: /home/step/password
readOnly: true
securityContext:
runAsUser: 1000
allowPrivilegeEscalation: false
volumes:
- name: certs
configMap:
name: certs
- name: config
configMap:
name: config
- name: secrets
configMap:
name: secrets
- name: ca-password
secret:
secretName: ca-password

View file

@ -1,108 +0,0 @@
apiVersion: v1
kind: Service
metadata:
labels: {app: autocert}
name: autocert
namespace: step
spec:
type: ClusterIP
ports:
- port: 443
targetPort: 4443
selector: {app: autocert}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: autocert-config
namespace: step
data:
config.yaml: |
logFormat: json # or text
restrictCertificatesToNamespace: false
clusterDomain: cluster.local
caUrl: https://ca.step.svc.cluster.local
certLifetime: 24h
renewer:
name: autocert-renewer
image: smallstep/autocert-renewer:0.8.3
resources: {requests: {cpu: 10m, memory: 20Mi}}
imagePullPolicy: IfNotPresent
volumeMounts:
- name: certs
mountPath: /var/run/autocert.step.sm
bootstrapper:
name: autocert-bootstrapper
image: smallstep/autocert-bootstrapper:0.8.3
resources: {requests: {cpu: 10m, memory: 20Mi}}
imagePullPolicy: IfNotPresent
volumeMounts:
- name: certs
mountPath: /var/run/autocert.step.sm
certsVolume:
name: certs
emptyDir: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: autocert
namespace: step
labels: {app: autocert}
spec:
replicas: 1
selector: {matchLabels: {app: autocert}}
template:
metadata: {labels: {app: autocert}}
spec:
containers:
- name: autocert
image: smallstep/autocert-controller:0.8.3
resources: {requests: {cpu: 100m, memory: 20Mi}}
env:
- name: PROVISIONER_NAME
value: autocert
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumeMounts:
- name: config
mountPath: /home/step/.step/config
readOnly: true
- name: certs
mountPath: /home/step/.step/certs
readOnly: true
- name: autocert-password
mountPath: /home/step/password
readOnly: true
- name: autocert-config
mountPath: /home/step/autocert
readOnly: true
securityContext:
runAsUser: 1000
allowPrivilegeEscalation: false
livenessProbe:
httpGet:
path: /healthz
port: 4443
scheme: HTTPS
readinessProbe:
httpGet:
path: /healthz
port: 4443
scheme: HTTPS
volumes:
- name: config
configMap: {name: config}
- name: certs
configMap: {name: certs}
- name: autocert-password
secret: {secretName: autocert-password}
- name: autocert-config
configMap: {name: autocert-config}

View file

@ -1,36 +0,0 @@
# Create a ClusterRole for managing autocert secrets, which should
# only exist in namespaces with autocert enabled and should always
# be labeled `autocert.step.sm/token: true`.
#
# To create this ClusterRole you need cluster-admin privileges. On
# GKE you can give yourself cluster-admin privileges using the
# following command:
#
# kubectl create clusterrolebinding cluster-admin-binding \
# --clusterrole cluster-admin \
# --user $(gcloud config get-value account)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: autocert-controller
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: autocert-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: autocert-controller
subjects:
- kind: ServiceAccount
name: default
namespace: step

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -1,8 +0,0 @@
FROM smallstep/step-cli:0.9.0
USER root
ENV CRT="/var/run/autocert.step.sm/site.crt"
ENV KEY="/var/run/autocert.step.sm/site.key"
ENV STEP_ROOT="/var/run/autocert.step.sm/root.crt"
ENTRYPOINT ["/bin/bash", "-c", "step ca renew --daemon $CRT $KEY"]