Compare commits

..

46 Commits

Author SHA1 Message Date
John Lancaster
2bbf9b3f53 README tweaks 2026-01-03 09:20:46 -06:00
John Lancaster
86339f5115 default path updates 2026-01-03 00:50:17 -06:00
John Lancaster
5b55b02a76 readme updates 2026-01-03 00:46:48 -06:00
John Lancaster
d7cb02f506 fixes 2026-01-03 00:34:04 -06:00
John Lancaster
3ca2a092fd another rbac 2026-01-02 23:11:47 -06:00
John Lancaster
5e52facc5c tweak 2026-01-02 23:08:38 -06:00
John Lancaster
7914368111 more rbac 2026-01-02 23:08:29 -06:00
John Lancaster
586c4b47bc comments 2026-01-02 15:08:12 -06:00
John Lancaster
cb530aa864 comments 2026-01-02 14:49:10 -06:00
John Lancaster
a5d0b1cb2f comment 2026-01-02 14:37:26 -06:00
John Lancaster
b7537179c9 comments 2026-01-02 14:32:35 -06:00
John Lancaster
be29375bee comments 2026-01-02 14:24:51 -06:00
John Lancaster
454a6fe00c comments 2026-01-02 14:18:20 -06:00
John Lancaster
405e1fc05c ignore log files 2026-01-02 14:12:37 -06:00
John Lancaster
34898d9b11 access log for envoy 2026-01-02 14:12:30 -06:00
John Lancaster
be34df6324 envoy config tweaks 2026-01-02 14:12:22 -06:00
John Lancaster
f92924b74f added role-based access control (RBAC) 2026-01-02 13:32:42 -06:00
John Lancaster
cd190d2e3f converted to envoy for spiffe identity filtering 2026-01-02 09:10:27 -06:00
John Lancaster
13438044e6 caddyfile update 2026-01-01 21:05:11 -06:00
John Lancaster
ecda3d4369 fixed openssl call 2026-01-01 15:40:39 -06:00
John Lancaster
3d377634aa added spiffe identity 2026-01-01 15:29:14 -06:00
John Lancaster
eb37a1992a fixed 2025-12-30 00:40:43 -06:00
John Lancaster
d834a543ee again 2025-12-30 00:38:57 -06:00
John Lancaster
80a59bb0a8 again 2025-12-30 00:37:17 -06:00
John Lancaster
2bfdd5543f tweaked output 2025-12-30 00:35:46 -06:00
John Lancaster
39650c2122 tweaked output 2025-12-30 00:34:27 -06:00
John Lancaster
4fbf229c0d more file checks 2025-12-30 00:25:38 -06:00
John Lancaster
f9347eb2bc enabling both service and timer 2025-12-29 23:54:06 -06:00
John Lancaster
4123ac3c00 line break 2025-12-29 23:52:51 -06:00
John Lancaster
f3bd116e91 removed hostname question 2025-12-29 23:51:08 -06:00
John Lancaster
f6492a2c5f changed defaults 2025-12-29 23:50:41 -06:00
John Lancaster
8f60dad7f6 not changing directory 2025-12-29 23:48:48 -06:00
John Lancaster
b0f1ae358b wizard now bootstrapping cert if it doesn't exist 2025-12-29 23:45:42 -06:00
John Lancaster
487dfe2e45 listing unit files at the end 2025-12-29 23:43:19 -06:00
John Lancaster
d08c3c750e changed default filenames 2025-12-29 22:48:55 -06:00
John Lancaster
83fd98a29b changed default cert dir 2025-12-29 21:57:16 -06:00
John Lancaster
f62d110adc added overwrite protection 2025-12-29 18:45:14 -06:00
John Lancaster
cba3d0eab9 added step-ca installation part 2025-12-29 18:14:47 -06:00
John Lancaster
0fee09099a service tweaks 2025-12-29 01:01:05 -06:00
John Lancaster
8000b32cea checking for crt before setting up service 2025-12-28 23:49:28 -06:00
John Lancaster
11efff6829 pruning 2025-12-28 23:38:24 -06:00
John Lancaster
83ada5bd70 using step path for cert default 2025-12-28 23:34:01 -06:00
John Lancaster
6109e54a63 setup wizard 2025-12-28 23:32:34 -06:00
John Lancaster
735e9a758b started setup_wizard 2025-12-28 18:30:33 -06:00
John Lancaster
b5998954ab more notes 2025-12-28 18:21:11 -06:00
John Lancaster
d9dfe3aa7b container names 2025-12-28 18:21:00 -06:00
9 changed files with 410 additions and 129 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
certs/
*.log

View File

@@ -7,7 +7,9 @@
protocols tls1.3
client_auth {
mode require_and_verify
trusted_ca_cert_file /certs/root_ca.crt
trust_pool file {
pem_file /certs/root_ca.crt
}
}
}
reverse_proxy rest-server:8000

View File

@@ -16,6 +16,12 @@ Connect solely through wireguard to `192.168.1.142` and serve the REST server wi
## Restic Repos
`/etc/fstab` entry on Proxmox host:
```
john-nas:/volume1/restic /mnt/nfs/restic nfs nofail,_netdev,x-systemd.automount,x-systemd.idle-timeout=600,timeo=14,retrans=3,hard,tcp,nfsvers=3 0 0
```
Mounted using a bind mount point in the LXC.
https://pve.proxmox.com/wiki/Linux_Container#_bind_mount_points
@@ -31,39 +37,86 @@ pct set 103 -mp0 /mnt/nfs/restic,mp=/mnt/restic
Generate a new private key and (public) certificate in the right places. This will use the `admin` provisioner.
```
step ca certificate soteria.john-stream.com certs/soteria.crt certs/soteria.key --provisioner admin
export HOSTNAME=$(hostname -s) && \
export DOMAIN="john-stream.com" && \
export CERT_DIR="/var/lib/tls" && \
export IP_ADDRESS=$(ip -4 addr show dev eth0 | awk '/inet /{print $2}' | cut -d/ -f1)
```
```
(umask 077; mkdir -p "$CERT_DIR") && cd "$CERT_DIR" && \
step ca root root_ca.crt && \
step ca certificate "$HOSTNAME" cert.pem key.pem \
--san "$HOSTNAME" \
--san "$HOSTNAME.$DOMAIN" \
--san "$IP_ADDRESS" \
--san spiffe://john-stream.com/role/docker-agent \
--provisioner admin
```
Convert the key for Envoy to use:
```
(umask 027; openssl pkcs8 -topk8 -nocrypt -in key.pem -out key_pkcs8.pem)
```
Check the resultant certificate:
```
openssl x509 -noout -subject -issuer -ext extendedKeyUsage -ext subjectAltName -in certs/soteria.crt
openssl x509 -noout -subject -issuer -ext extendedKeyUsage,subjectAltName -in /var/lib/tls/cert.pem
```
## Envoy Proxy
Validate config:
```shell
docker compose run -it --rm envoy --mode validate -c /etc/envoy/envoy.yaml
```
## Clients
To set up a client, run the following command. It will prompt for the provisioner password and the repository name.
```bash
curl -sL https://gitea.john-stream.com/john/soteria/raw/branch/main/scripts/setup_client.sh | bash
```
```bash
curl -sL https://gitea.john-stream.com/john/soteria/raw/branch/main/scripts/check_status.sh | bash
```
```bash
curl -sL https://gitea.john-stream.com/john/soteria/raw/branch/main/scripts/wizard_setup.sh | bash
```
### Manual Setup
Set up provisioner password by running this and pasting in the current JWK provisioner password for `admin`
```
read -s secret && (umask 077; echo "$secret" > $(step path)/certs/secret.txt)
```
Generate the client TLS private key and (public) certificate for mTLS. This will combine them both into a file called `restic.pem`, which can be used with the `--tls-client-cert` option with the restic CLI.
```
cd $(step path)/certs && \
step ca certificate \
--provisioner admin --password-file secret.txt \
$(hostnamectl hostname) restic.crt restic.key && \
(umask 077; cat restic.crt restic.key > restic.pem)
export HOSTNAME=$(hostname -s) && \
export DOMAIN="john-stream.com" && \
export CERT_DIR="/var/lib/tls" && \
export IP_ADDRESS=$(ip -4 addr show dev eth0 | awk '/inet /{print $2}' | cut -d/ -f1) && \
(umask 077; mkdir -p "$CERT_DIR" && cd "$CERT_DIR" && \
step ca root root_ca.crt && \
step ca certificate "$HOSTNAME" cert.pem key.pem \
--san "$HOSTNAME" \
--san "$HOSTNAME.$DOMAIN" \
--san "$IP_ADDRESS" \
--san spiffe://john-stream.com/role/docker-agent \
--provisioner admin && \
cat {cert,key}.pem > restic.pem) && \
chmod 644 cert.pem root_ca.crt
```
Need restic 0.16+ for the env vars `RESTIC_CACERT` and `RESTIC_TLS_CLIENT_CERT` to work.
```
export RESTIC_CACERT=$(step path)/certs/root_ca.crt
export RESTIC_TLS_CLIENT_CERT=$(step path)/certs/restic.pem
export RESTIC_REPOSITORY=rest:https://soteria.john-stream.com/john-ubuntu
export RESTIC_CACERT=$(step path)/certs/root_ca.crt && \
export RESTIC_TLS_CLIENT_CERT=$(step path)/certs/restic.pem && \
export RESTIC_REPOSITORY=rest:https://soteria.john-stream.com/john-ubuntu && \
export RESTIC_PASSWORD_FILE=$(readlink -f ~/.config/resticprofile/password.txt)
```
@@ -75,6 +128,8 @@ restic snapshots
### Installing Latest Binary
Do this in case the restic version from apt is too old.
```
curl -s https://api.github.com/repos/restic/restic/releases/latest | grep tag_name
```

View File

@@ -1,21 +1,23 @@
services:
rest-server:
image: restic/rest-server
container_name: restic
restart: unless-stopped
volumes:
- /mnt/restic:/data
environment:
OPTIONS: --no-auth
caddy:
image: caddy:alpine
envoy:
image: envoyproxy/envoy:v1.33-latest
user: root
container_name: envoy
restart: unless-stopped
ports:
- "443:443"
- "443:10000"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./certs/soteria.crt:/certs/soteria.crt:ro
- ./certs/soteria.key:/certs/soteria.key:ro
- ${HOME}/.step/certs/root_ca.crt:/certs/root_ca.crt:ro
- ./envoy.yaml:/etc/envoy/envoy.yaml:ro
- /var/lib/tls:/certs
- ./access.log:/var/log/envoy/access.log
depends_on:
- rest-server

122
envoy.yaml Normal file
View File

@@ -0,0 +1,122 @@
static_resources:
# --8<-- [start:listener]
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: 10000
# --8<-- [end:listener]
filter_chains:
- filter_chain_match:
server_names: ["*.john-stream.com"]
# --8<-- [start:transport_socket]
- transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
require_client_certificate: true
common_tls_context:
tls_params:
tls_minimum_protocol_version: TLSv1_3 # (1)!
validation_context:
trusted_ca: { filename: /certs/root_ca.crt }
match_typed_subject_alt_names:
- san_type: URI
matcher:
prefix: spiffe://john-stream.com # (2)!
tls_certificates:
- certificate_chain: { filename: /certs/cert.pem }
private_key: { filename: /certs/key_pkcs8.pem } # (3)!
# --8<-- [end:transport_socket]
filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
use_remote_address: true
http2_protocol_options:
max_concurrent_streams: 100
# --8<-- [start:access_log]
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: "/var/log/envoy/access.log"
# --8<-- [end:access_log]
# --8<-- [start:cluster_route]
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: restic
# --8<-- [end:cluster_route]
http_filters:
# --8<-- [start:rbac]
- name: envoy.filters.http.rbac
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
rules:
action: ALLOW
policies:
"ubuntu-policy":
permissions:
- and_rules:
rules:
- header:
name: ":path"
string_match:
prefix: "/john-ubuntu"
principals:
- authenticated:
principal_name:
exact: "spiffe://john-stream.com/ubuntu"
"p14-policy":
permissions:
- and_rules:
rules:
- header:
name: ":path"
string_match:
prefix: "/john-p14s"
principals:
- authenticated:
principal_name:
exact: "spiffe://john-stream.com/john-p14s"
"gitea-policy":
permissions:
- and_rules:
rules:
- header:
name: ":path"
string_match:
prefix: "/gitea"
principals:
- authenticated:
principal_name:
exact: "spiffe://john-stream.com/gitea"
# --8<-- [end:rbac]
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
# --8<-- [start:cluster]
clusters:
- name: restic
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: restic
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: rest-server
port_value: 8000
# --8<-- [end:cluster]

View File

@@ -22,9 +22,9 @@ print_status() {
EXIT_CODE=0
CERTS_DIR="$(readlink -f ~/.step/certs)"
SERVER_CERT="$CERTS_DIR/restic.crt"
SERVER_KEY="$CERTS_DIR/restic.key"
CERTS_DIR="/var/lib/tls"
SERVER_CERT="$CERTS_DIR/cert.pem"
SERVER_KEY="$CERTS_DIR/key.pem"
TIMER_NAME="cert-renewer.timer"
# 1. Check Certificates Existence

View File

@@ -1,90 +0,0 @@
#!/bin/bash
set -e
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() {
echo -e "${YELLOW}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check for sudo/root
if [ "$EUID" -ne 0 ]; then
log_error "Please run as root or with sudo"
exit 1
fi
# Determine paths
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
SYSTEMD_DIR="$PROJECT_ROOT/systemd"
DEST_DIR="/etc/systemd/system"
SERVICE_FILE="cert-renewer.service"
TIMER_FILE="cert-renewer.timer"
install_unit() {
local unit_file=$1
local src_path="$SYSTEMD_DIR/$unit_file"
local dest_path="$DEST_DIR/$unit_file"
if [ ! -f "$src_path" ]; then
log_error "Source file not found: $src_path"
exit 1
fi
log_info "Installing $unit_file..."
# Remove existing link or file if it exists to ensure clean install
if [ -L "$dest_path" ] || [ -f "$dest_path" ]; then
log_info "Removing existing $dest_path"
rm -f "$dest_path"
fi
# Create symlink
ln -s "$src_path" "$dest_path"
if [ -L "$dest_path" ]; then
log_success "Linked $src_path to $dest_path"
else
log_error "Failed to link $unit_file"
exit 1
fi
}
# Main execution
log_info "Starting installation of systemd services..."
install_unit "$SERVICE_FILE"
install_unit "$TIMER_FILE"
log_info "Reloading systemd daemon..."
systemctl daemon-reload
log_success "Systemd daemon reloaded"
log_info "Enabling and starting $TIMER_FILE..."
systemctl enable --now "$TIMER_FILE"
log_success "$TIMER_FILE enabled and started"
log_info "Checking status of $TIMER_FILE..."
if systemctl is-active --quiet "$TIMER_FILE"; then
systemctl status "$TIMER_FILE" --no-pager
echo ""
log_success "Installation complete!"
else
log_error "$TIMER_FILE is not active"
systemctl status "$TIMER_FILE" --no-pager
exit 1
fi

196
scripts/setup_wizard.sh Executable file
View File

@@ -0,0 +1,196 @@
#!/bin/bash
set -e
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() {
echo -e "${YELLOW}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# -----------------------------------------------------------------------------
# Input Framework
# -----------------------------------------------------------------------------
# Ensure we have a tty for input
if [ ! -e /dev/tty ]; then
echo "Error: Script must be run in an interactive terminal (cannot find /dev/tty)." >&2
exit 1
fi
# Function to prompt for user input
# Usage: get_input "VARIABLE_NAME" "Prompt Text" "Default Value" "is_secret(true/false)"
get_input() {
local var_name="$1"
local prompt_text="$2"
local default_value="$3"
local is_secret="$4"
local input_val=""
local prompt_full="${GREEN}${prompt_text}${NC}"
if [ -n "$default_value" ]; then
prompt_full+=" [$default_value]"
fi
prompt_full+=": "
while true; do
# Print prompt to stderr so it shows up even if stdout is redirected
if [ "$is_secret" == "true" ]; then
echo -ne "$prompt_full" >&2
read -s input_val < /dev/tty
echo "" >&2 # Newline after secret input
else
echo -ne "$prompt_full" >&2
read input_val < /dev/tty
fi
# Use default if input is empty
if [ -z "$input_val" ] && [ -n "$default_value" ]; then
input_val="$default_value"
fi
# Validation: Require input if no default exists
if [ -z "$input_val" ]; then
echo -e "${RED}Error: This value is required.${NC}" >&2
else
break
fi
done
# Set the variable dynamically in the parent scope
printf -v "$var_name" "%s" "$input_val"
export "$var_name=$input_val"
}
# Function to confirm collected inputs
# Usage: confirm_inputs "VAR1" "VAR2" "VAR3" ...
confirm_inputs() {
echo "" >&2
echo -e "${GREEN}=== Configuration Summary ===${NC}" >&2
for var in "$@"; do
local val="${!var}"
# Mask secrets in summary if needed, or just show length
# For now, just printing value.
# To improve: pass a list of secret vars to mask them.
echo -e "${YELLOW}$var:${NC} $val" >&2
done
echo "" >&2
get_input "CONFIRM" "Is this correct? (y/n)" "y" "false"
if [[ "${CONFIRM,,}" != "y" ]]; then
echo -e "${RED}Aborted by user.${NC}" >&2
exit 1
fi
}
install_unit() {
local template_url=$1
local filename=$(basename "$template_url")
local dest_path=/etc/systemd/system/"$filename"
if [ -e "$dest_path" ]; then
get_input "CONFIRM_OVERWRITE" "Overwrite $dest_path? (y/n)" "y" "false"
if [[ "${CONFIRM_OVERWRITE,,}" != "y" ]]; then
echo "Skipping overwrite of ${dest_path}."
return
fi
fi
log_info "Installing $filename..."
curl -sL $template_url | envsubst > "$dest_path"
log_success "$filename installed to $dest_path"
}
# -----------------------------------------------------------------------------
# Script Logic
# -----------------------------------------------------------------------------
echo "Starting Interactive Setup..."
echo "-----------------------------"
# Verify required external binaries
if ! command -v step >/dev/null 2>&1; then
# Prompt the user to install the step CLI
get_input "INSTALL_STEP" "The 'step' CLI was not found. Install now? (y/n)" "y" "false"
if [[ "${INSTALL_STEP,,}" == "y" ]]; then
apt-get update && apt-get install -y --no-install-recommends curl vim gpg ca-certificates
curl -fsSL https://packages.smallstep.com/keys/apt/repo-signing-key.gpg -o /etc/apt/trusted.gpg.d/smallstep.asc && \
echo 'deb [signed-by=/etc/apt/trusted.gpg.d/smallstep.asc] https://packages.smallstep.com/stable/debian debs main' \
| tee /etc/apt/sources.list.d/smallstep.list
apt-get update && apt-get -y install step-cli step-ca
else
log_error "Cannot continue without 'step'. Aborting." >&2
exit 1
fi
else
log_success "Step CA installed\n"
fi
get_input "CERT_DIR" "Enter directory for certificates" "/var/lib/tls" "false"
get_input "CERT_FILENAME" "Name for cert file" "cert.pem" "false"
get_input "KEY_FILENAME" "Name for private key" "key.pem" "false"
get_input "SPIFFE" "SPIFFE identity" "node" "false"
if [ ! -e "$CERT_DIR" ]; then
(umask 077; mkdir -p "${CERT_DIR}")
log_info "Created ${CERT_DIR}"
fi
# These need to get set so that they get filled into the service correctly.
export CERT_LOCATION=$(readlink -f ${CERT_DIR}/$CERT_FILENAME)
export KEY_LOCATION=$(readlink -f ${CERT_DIR}/$KEY_FILENAME)
confirm_inputs "CERT_LOCATION" "KEY_LOCATION"
if [ ! -e "${CERT_DIR}/root_ca.crt" ]; then
step ca root "${CERT_DIR}/root_ca.crt"
fi
if [ ! -f "$CERT_LOCATION" ] || [ ! -f "$KEY_LOCATION" ]; then
hostname=$(hostname -s)
ip_address=$(ip -4 addr show dev eth0 | awk '/inet /{print $2}' | cut -d/ -f1)
step ca certificate "$hostname" \
"${CERT_DIR}/cert.pem" "${CERT_DIR}/key.pem" \
--san "$hostname" \
--san "$hostname.john-stream.com" \
--san "$ip_address" \
--san "spiffe://john-stream.com/$SPIFFE" \
--provisioner admin
fi
echo "" >&2
echo -e "${GREEN}=== Cert information ===${NC}" >&2
openssl x509 -noout -subject -issuer -ext extendedKeyUsage,subjectAltName -enddate -in "$CERT_LOCATION"
SERVICE_FILE="cert-renewer.service"
TIMER_FILE="cert-renewer.timer"
REPO_URL_BASE=https://gitea.john-stream.com/john/soteria/raw/branch/main/
SERVICE_TEMPLATE_URL="${REPO_URL_BASE}systemd/${SERVICE_FILE}"
TIMER_TEMPLATE_URL="${REPO_URL_BASE}systemd/${TIMER_FILE}"
echo "" >&2
echo -e "${GREEN}=== Installing rotation services ===${NC}" >&2
install_unit ${SERVICE_TEMPLATE_URL}
install_unit ${TIMER_TEMPLATE_URL}
echo "" >&2
echo -e "${GREEN}=== Reloading services ===${NC}" >&2
systemctl daemon-reload
systemctl enable --now "${TIMER_FILE}" "${SERVICE_FILE}"
systemctl list-unit-files $SERVICE_FILE $TIMER_FILE

View File

@@ -3,25 +3,18 @@ Description=Certificate renewal
After=network-online.target
Documentation=https://smallstep.com/docs/step-ca/certificate-authority-server-production
StartLimitIntervalSec=0
; PartOf=cert-renewer.target
[Service]
Type=oneshot
User=root
Environment=CERT_LOCATION=/home/john/soteria/certs/soteria.crt \
KEY_LOCATION=/home/john/soteria/certs/soteria.key
; ExecCondition checks if the certificate is ready for renewal.
; ExecCondition=/usr/bin/step certificate needs-renewal ${CERT_LOCATION}
; ExecCondition checks if the certificate is ready for renewal,
; based on the exit status of the command.
; (In systemd <242, you can use ExecStartPre= here.)
ExecCondition=/usr/bin/step certificate needs-renewal ${CERT_LOCATION}
; ExecStart renews the certificate, if ExecStartPre was successful.
; ExecStart renews the certificate, if ExecCondition was successful.
ExecStart=/usr/bin/step ca renew --force ${CERT_LOCATION} ${KEY_LOCATION}
ExecStartPost=/usr/bin/openssl x509 -noout -enddate -in ${CERT_LOCATION}
ExecStartPost=/usr/bin/docker exec caddy caddy reload --config /etc/caddy/Caddyfile
ExecStartPost=/usr/bin/openssl x509 -noout -subject -issuer -enddate -in ${CERT_LOCATION}
[Install]
WantedBy=multi-user.target