Compare commits
51 Commits
43f898a2db
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bbf9b3f53 | ||
|
|
86339f5115 | ||
|
|
5b55b02a76 | ||
|
|
d7cb02f506 | ||
|
|
3ca2a092fd | ||
|
|
5e52facc5c | ||
|
|
7914368111 | ||
|
|
586c4b47bc | ||
|
|
cb530aa864 | ||
|
|
a5d0b1cb2f | ||
|
|
b7537179c9 | ||
|
|
be29375bee | ||
|
|
454a6fe00c | ||
|
|
405e1fc05c | ||
|
|
34898d9b11 | ||
|
|
be34df6324 | ||
|
|
f92924b74f | ||
|
|
cd190d2e3f | ||
|
|
13438044e6 | ||
|
|
ecda3d4369 | ||
|
|
3d377634aa | ||
|
|
eb37a1992a | ||
|
|
d834a543ee | ||
|
|
80a59bb0a8 | ||
|
|
2bfdd5543f | ||
|
|
39650c2122 | ||
|
|
4fbf229c0d | ||
|
|
f9347eb2bc | ||
|
|
4123ac3c00 | ||
|
|
f3bd116e91 | ||
|
|
f6492a2c5f | ||
|
|
8f60dad7f6 | ||
|
|
b0f1ae358b | ||
|
|
487dfe2e45 | ||
|
|
d08c3c750e | ||
|
|
83fd98a29b | ||
|
|
f62d110adc | ||
|
|
cba3d0eab9 | ||
|
|
0fee09099a | ||
|
|
8000b32cea | ||
|
|
11efff6829 | ||
|
|
83ada5bd70 | ||
|
|
6109e54a63 | ||
|
|
735e9a758b | ||
|
|
b5998954ab | ||
|
|
d9dfe3aa7b | ||
|
|
2387bb1e6a | ||
|
|
c2253fb62a | ||
|
|
84776ad57b | ||
|
|
57ceb8d4ec | ||
|
|
705241f451 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
certs/
|
||||
certs/
|
||||
*.log
|
||||
@@ -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
|
||||
|
||||
87
README.md
87
README.md
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
122
envoy.yaml
Normal 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]
|
||||
65
scripts/check_status.sh
Executable file
65
scripts/check_status.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_status() {
|
||||
local item="$1"
|
||||
local status="$2"
|
||||
local error_msg="$3"
|
||||
if [ "$status" -eq 0 ]; then
|
||||
echo -e "${item}: ${GREEN}OK${NC}"
|
||||
else
|
||||
echo -e "${item}: ${RED}FAIL${NC}"
|
||||
if [ -n "$error_msg" ]; then
|
||||
echo -e " ${RED}Error:${NC} $error_msg"
|
||||
fi
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
}
|
||||
|
||||
EXIT_CODE=0
|
||||
|
||||
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
|
||||
if [ -f "$SERVER_CERT" ] && [ -f "$SERVER_KEY" ]; then
|
||||
print_status "Certificate Files" 0
|
||||
else
|
||||
print_status "Certificate Files" 1 "Missing $SERVER_CERT or $SERVER_KEY"
|
||||
fi
|
||||
|
||||
# 2. Check Certificate Validity (Is it valid NOW?)
|
||||
if [ -f "$SERVER_CERT" ] && command -v openssl &> /dev/null; then
|
||||
# Check if valid for at least 60 seconds
|
||||
if openssl x509 -checkend 60 -noout -in "$SERVER_CERT" &> /dev/null; then
|
||||
print_status "Certificate Validity" 0
|
||||
else
|
||||
print_status "Certificate Validity" 1 "Certificate at $SERVER_CERT is expired or expiring within 60s"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Check Timer Status
|
||||
if systemctl is-active "$TIMER_NAME" &> /dev/null; then
|
||||
print_status "Renewal Timer" 0
|
||||
else
|
||||
# Check if unit exists
|
||||
if systemctl list-unit-files "$TIMER_NAME" &> /dev/null; then
|
||||
print_status "Renewal Timer" 1 "Systemd timer '$TIMER_NAME' is installed but not active"
|
||||
else
|
||||
# Check if source file exists
|
||||
TIMER_FILE="/etc/systemd/system/$TIMER_NAME"
|
||||
if [ -f "$TIMER_FILE" ]; then
|
||||
print_status "Renewal Timer" 1 "Systemd timer '$TIMER_NAME' is not installed (found source at $TIMER_FILE)"
|
||||
else
|
||||
print_status "Renewal Timer" 1 "Systemd timer '$TIMER_NAME' is missing entirely (expected at $TIMER_FILE)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
139
scripts/setup_client.sh
Executable file
139
scripts/setup_client.sh
Executable file
@@ -0,0 +1,139 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
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 required tools
|
||||
check_command() {
|
||||
if ! command -v "$1" &> /dev/null; then
|
||||
log_error "$1 is required but not installed."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_command step
|
||||
check_command curl
|
||||
check_command wget
|
||||
check_command bunzip2
|
||||
|
||||
# 1. Setup Step Certificates
|
||||
log_info "Setting up Step Certificates..."
|
||||
|
||||
STEP_PATH="$(step path)"
|
||||
CERTS_DIR="$STEP_PATH/certs"
|
||||
|
||||
if [ ! -d "$CERTS_DIR" ]; then
|
||||
log_info "Creating directory $CERTS_DIR"
|
||||
mkdir -p "$CERTS_DIR"
|
||||
fi
|
||||
|
||||
# Prompt for secret securely (reading from /dev/tty to support pipe execution)
|
||||
echo -e "${YELLOW}Please enter the provisioner password for 'admin':${NC}"
|
||||
read -s secret < /dev/tty
|
||||
echo ""
|
||||
|
||||
if [ -z "$secret" ]; then
|
||||
log_error "Password cannot be empty."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Prompt for Repo Name
|
||||
DEFAULT_REPO_NAME=$(hostnamectl hostname 2>/dev/null || hostname)
|
||||
echo -e "${YELLOW}Please enter the Restic Repository Name (default: $DEFAULT_REPO_NAME):${NC}"
|
||||
read repo_name < /dev/tty
|
||||
|
||||
if [ -z "$repo_name" ]; then
|
||||
repo_name="$DEFAULT_REPO_NAME"
|
||||
fi
|
||||
|
||||
# Save secret temporarily
|
||||
SECRET_FILE="$CERTS_DIR/secret.txt"
|
||||
(umask 077; echo "$secret" > "$SECRET_FILE")
|
||||
log_success "Secret saved to $SECRET_FILE"
|
||||
|
||||
# Generate Certificates
|
||||
log_info "Generating certificates for repo/client: $repo_name"
|
||||
|
||||
cd "$CERTS_DIR"
|
||||
|
||||
if step ca certificate \
|
||||
--provisioner admin --password-file secret.txt \
|
||||
"$repo_name" restic.crt restic.key; then
|
||||
|
||||
# Combine into PEM
|
||||
(umask 077; cat restic.crt restic.key > restic.pem)
|
||||
log_success "Certificates generated and combined into restic.pem"
|
||||
|
||||
# Clean up secret? The README keeps it, but usually it's good to ask.
|
||||
# The README implies keeping it for renewal maybe?
|
||||
# But for client certs, renewal might need the password again if using the same provisioner.
|
||||
# I'll leave it as per README instructions.
|
||||
else
|
||||
log_error "Failed to generate certificates. Check your password and connection to the CA."
|
||||
rm -f "$SECRET_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Install Restic
|
||||
log_info "Checking for Restic..."
|
||||
|
||||
if ! command -v restic &> /dev/null; then
|
||||
log_info "Restic not found. Installing latest version..."
|
||||
|
||||
RESTIC_VERSION="0.18.1"
|
||||
DOWNLOAD_URL="https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_amd64.bz2"
|
||||
|
||||
TMP_DIR=$(mktemp -d)
|
||||
pushd "$TMP_DIR" > /dev/null
|
||||
|
||||
wget -q -O restic.bz2 "$DOWNLOAD_URL"
|
||||
bunzip2 restic.bz2
|
||||
chmod +x restic
|
||||
|
||||
log_info "Installing restic to /usr/local/bin (requires sudo)..."
|
||||
if sudo mv restic /usr/local/bin/; then
|
||||
log_success "Restic installed successfully."
|
||||
else
|
||||
log_error "Failed to move restic to /usr/local/bin"
|
||||
popd > /dev/null
|
||||
rm -rf "$TMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
popd > /dev/null
|
||||
rm -rf "$TMP_DIR"
|
||||
else
|
||||
CURRENT_VERSION=$(restic version | awk '{print $2}')
|
||||
log_success "Restic is already installed (version $CURRENT_VERSION)"
|
||||
fi
|
||||
|
||||
# 3. Final Instructions
|
||||
ROOT_CA="$CERTS_DIR/root_ca.crt"
|
||||
CLIENT_PEM="$CERTS_DIR/restic.pem"
|
||||
|
||||
# Ensure root_ca exists (it should if step is bootstrapped)
|
||||
if [ ! -f "$ROOT_CA" ]; then
|
||||
log_info "Downloading Root CA..."
|
||||
step ca root "$ROOT_CA"
|
||||
fi
|
||||
|
||||
log_success "Setup complete!"
|
||||
echo ""
|
||||
echo -e "${GREEN}=== Environment Configuration ===${NC}"
|
||||
echo "Add the following lines to your shell configuration (.bashrc, .zshrc, etc) or script:"
|
||||
echo ""
|
||||
echo "export RESTIC_CACERT=$ROOT_CA"
|
||||
echo "export RESTIC_TLS_CLIENT_CERT=$CLIENT_PEM"
|
||||
echo "export RESTIC_REPOSITORY=rest:https://soteria.john-stream.com/$repo_name"
|
||||
echo "export RESTIC_PASSWORD_FILE=~/.config/resticprofile/password.txt"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Note: Adjust RESTIC_REPOSITORY and RESTIC_PASSWORD_FILE as needed.${NC}"
|
||||
196
scripts/setup_wizard.sh
Executable file
196
scripts/setup_wizard.sh
Executable 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
|
||||
20
systemd/cert-renewer.service
Normal file
20
systemd/cert-renewer.service
Normal file
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Certificate renewal
|
||||
After=network-online.target
|
||||
Documentation=https://smallstep.com/docs/step-ca/certificate-authority-server-production
|
||||
StartLimitIntervalSec=0
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
|
||||
; ExecCondition checks if the certificate is ready for renewal.
|
||||
; ExecCondition=/usr/bin/step certificate needs-renewal ${CERT_LOCATION}
|
||||
|
||||
; 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 -subject -issuer -enddate -in ${CERT_LOCATION}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
19
systemd/cert-renewer.timer
Normal file
19
systemd/cert-renewer.timer
Normal file
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=Timer for certificate renewal
|
||||
Documentation=https://smallstep.com/docs/step-ca/certificate-authority-server-production
|
||||
; PartOf=cert-renewer.target
|
||||
|
||||
[Timer]
|
||||
Persistent=true
|
||||
|
||||
; Run the timer unit every 15 minutes.
|
||||
OnCalendar=*:1/15
|
||||
|
||||
; Always run the timer on time.
|
||||
AccuracySec=1us
|
||||
|
||||
; Add jitter to prevent a "thundering hurd" of simultaneous certificate renewals.
|
||||
RandomizedDelaySec=5m
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Reference in New Issue
Block a user