forked from TrueCloudLab/certificates
parent
23c60d5f3f
commit
2e98febcd9
10 changed files with 351 additions and 0 deletions
|
@ -74,3 +74,11 @@ languages are appreciated!
|
|||
- [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
|
||||
|
|
21
autocert/examples/hello-mtls/envoy/Dockerfile.server
Normal file
21
autocert/examples/hello-mtls/envoy/Dockerfile.server
Normal file
|
@ -0,0 +1,21 @@
|
|||
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"]
|
6
autocert/examples/hello-mtls/envoy/certwatch.sh
Executable file
6
autocert/examples/hello-mtls/envoy/certwatch.sh
Executable file
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
while true; do
|
||||
inotifywait -e modify /var/run/autocert.step.sm/site.crt
|
||||
kill -HUP 1
|
||||
done
|
10
autocert/examples/hello-mtls/envoy/entrypoint.sh
Executable file
10
autocert/examples/hello-mtls/envoy/entrypoint.sh
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/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 "$@"
|
33
autocert/examples/hello-mtls/envoy/hello-mtls.server.yaml
Normal file
33
autocert/examples/hello-mtls/envoy/hello-mtls.server.yaml
Normal file
|
@ -0,0 +1,33 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels: {app: hello-mtls}
|
||||
name: hello-mtls
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 443
|
||||
targetPort: 443
|
||||
selector: {app: hello-mtls}
|
||||
|
||||
---
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: hello-mtls
|
||||
labels: {app: hello-mtls}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector: {matchLabels: {app: hello-mtls}}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
autocert.step.sm/name: hello-mtls.default.svc.cluster.local
|
||||
labels: {app: hello-mtls}
|
||||
spec:
|
||||
containers:
|
||||
- name: hello-mtls
|
||||
image: hello-mtls-server-envoy:latest
|
||||
imagePullPolicy: Never
|
||||
resources: {requests: {cpu: 10m, memory: 20Mi}}
|
209
autocert/examples/hello-mtls/envoy/hot-restarter.py
Normal file
209
autocert/examples/hello-mtls/envoy/hot-restarter.py
Normal file
|
@ -0,0 +1,209 @@
|
|||
#!/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()
|
1
autocert/examples/hello-mtls/envoy/requirements.txt
Normal file
1
autocert/examples/hello-mtls/envoy/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Flask
|
9
autocert/examples/hello-mtls/envoy/server.py
Normal file
9
autocert/examples/hello-mtls/envoy/server.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
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)
|
50
autocert/examples/hello-mtls/envoy/server.yaml
Normal file
50
autocert/examples/hello-mtls/envoy/server.yaml
Normal file
|
@ -0,0 +1,50 @@
|
|||
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
|
4
autocert/examples/hello-mtls/envoy/start-envoy.sh
Executable file
4
autocert/examples/hello-mtls/envoy/start-envoy.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
|
||||
ulimit -n 65536
|
||||
/usr/local/bin/envoy -c /src/server.yaml --service-cluster hello-mTLS --restart-epoch $RESTART_EPOCH
|
Loading…
Reference in a new issue