2.5.5 was two releases behind. Verified against the openbao/openbao source at
tag v2.6.2 that the bump is safe for an existing vault: raft's on-disk format
is unchanged (identical bbolt / hashicorp-raft / raft-boltdb pins,
byte-identical physical/raft/types.proto), snapshots restore in both
directions, and every stanza this config.hcl uses still parses -- the config
parser only moved import paths and the listener diff is additive.
Three corrections that came out of that check.
deploy.sh's printed crash-loop recovery command was already broken by 2.6.0.
That release adds `USER openbao` to the alpine image, so the container no
longer starts as root; the `docker run ... --entrypoint sh ... chown` it tells
the operator to run now executes as uid 100 and fails. Added `--user 0:0`, and
dropped the stale 2.5.5 literal from the same string. The detection logic
itself is unaffected -- `id -u openbao` still reads /etc/passwd and still
returns 100, verified by extracting the passwd layer from both published
images -- so a02524a holds and the volume does not need re-chowning.
The mlock story was wrong, and was already wrong on 2.5.5. OpenBao removed
mlock support: its own config parser carries "OpenBao has dropped support for
mlock. Please remove the line disable_mlock = false from your config and
disable or encrypt swap instead." So `cap_add: [IPC_LOCK]` and
`ulimits: memlock: -1` are inert, and config.hcl's "mlock keeps key material
off swap -- REQUIRED" was describing something that does not happen. The
BEHAVIOUR was right all along, because deploy.sh disables swap, which is the
actual mitigation; only the explanation was wrong. Corrected in config.hcl,
docker-compose.yml and the README. The two compose settings stay: they are
harmless, and removing them would recreate every deployed container for no
gain.
The built-in `seal "pkcs11"` stanza is deprecated in 2.6.0 for removal in
v2.7.0, and the HSM distribution is discontinued by then; PKCS#11 auto-unseal
continues only via the external `plugin "kms" "pkcs11" {}`. Noted where the
commented-out stanza lives, since a stack relying on it has to move before
taking 2.7.x.
Also worth recording and NOT acting on: do not switch to the openbao-distroless
image variant. It ships no shell, which breaks every `docker compose run
--entrypoint sh` probe deploy.sh uses.
Upgrading a LIVE vault is not automatic -- snapshot first, and the restart
comes back sealed. That is the next commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
607 lines
32 KiB
Bash
607 lines
32 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# deploy.sh -- deploy the hardened OpenBao tape-encryption key store on a
|
|
# same-LAN host (Alpine / Debian / Alma). Single-node, runs as root.
|
|
#
|
|
# What this does:
|
|
# 1. Installs docker + compose if missing.
|
|
# 2. Lays down docker-compose.yml, config.hcl, gen-tls.sh in $STACK_DIR.
|
|
# 3. Seeds .env on first run (OPENBAO_ADDR into the cert SAN); never
|
|
# overwrites an existing .env.
|
|
# 4. Generates a self-signed TLS cert (if ./tls is empty) -- drop a CA-signed
|
|
# pair there instead to use your Smallstep CA.
|
|
# 5. Disables swap (so mlock is meaningful) and opens 8200/tcp.
|
|
# 6. Pulls images and brings the stack up. OpenBao starts SEALED -- initialise
|
|
# + unseal once afterwards (printed at the end).
|
|
#
|
|
# Idempotent: re-run to apply config changes / pull new images.
|
|
#
|
|
# Self-contained: docker-compose.yml, config.hcl, gen-tls.sh, .env.example are
|
|
# embedded as a base64 tar.gz at the bottom. Rebuild with build.sh after edits.
|
|
#
|
|
# Usage:
|
|
# OPENBAO_ADDR=10.0.0.10 bash deploy.sh # interactive prompt for the rest
|
|
# OPENBAO_ADDR=vault.lan SKIP_PROMPTS=1 bash deploy.sh
|
|
# STACK_DIR=/opt/openbao bash deploy.sh
|
|
|
|
set -euo pipefail
|
|
|
|
: "${STACK_DIR:=/srv/openbao}"
|
|
: "${SKIP_DOCKER_INSTALL:=0}"
|
|
: "${SKIP_BIND_CHECK:=0}" # 1 = publish on an address this host does not (yet) have
|
|
: "${FORCE:=0}"
|
|
: "${SKIP_PROMPTS:=0}" # non-interactive: require values via env, no prompts
|
|
[[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1
|
|
: "${OPENBAO_ADDR:=}"
|
|
# Whether OPENBAO_BIND arrived in this script's ENVIRONMENT (automations.sh
|
|
# passes answers via `env VAR=... bash deploy.sh`, and a standalone run may
|
|
# export it too). If it did it is still exported when we reach compose, which
|
|
# prefers the shell environment over $STACK_DIR/.env; if this script derives it
|
|
# below instead, the assignment is NOT exported and the .env wins. Must be read
|
|
# before the := default, which would make an unset var look set.
|
|
BIND_FROM_ENV=0
|
|
[[ -n "${OPENBAO_BIND+x}" ]] && BIND_FROM_ENV=1
|
|
: "${OPENBAO_BIND:=0.0.0.0}"
|
|
|
|
# Same question for the UI switch: an explicit OPENBAO_UI=0 has to be told
|
|
# apart from "not mentioned", or a re-run would silently re-enable the UI.
|
|
UI_FROM_ENV=0
|
|
[[ -n "${OPENBAO_UI+x}" ]] && UI_FROM_ENV=1
|
|
: "${OPENBAO_UI:=}"
|
|
: "${DISABLE_SWAP:=1}" # set 0 to skip swapoff (mlock then only best-effort)
|
|
|
|
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
|
|
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
|
|
die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
[[ $EUID -eq 0 ]] || die "Run as root."
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OS detection + Docker install (Alpine / Debian / Alma). Inlined so this
|
|
# deploy.sh stays self-contained when scp'd standalone.
|
|
# ---------------------------------------------------------------------------
|
|
osfam() {
|
|
local id="" like=""
|
|
if [[ -r /etc/os-release ]]; then
|
|
id="$(. /etc/os-release 2>/dev/null && echo "${ID:-}")"
|
|
like="$(. /etc/os-release 2>/dev/null && echo "${ID_LIKE:-}")"
|
|
fi
|
|
case " $id $like " in
|
|
*" alpine "*) echo alpine ;;
|
|
*" debian "*|*" ubuntu "*) echo debian ;;
|
|
*" rhel "*|*" fedora "*|*" centos "*) echo rhel ;;
|
|
*) echo "${id:-unknown}" ;;
|
|
esac
|
|
}
|
|
|
|
install_docker() {
|
|
[[ "$SKIP_DOCKER_INSTALL" == "1" ]] && { log "Skipping Docker install."; return; }
|
|
if command -v docker >/dev/null 2>&1; then
|
|
log "Docker already installed: $(docker --version)"
|
|
else
|
|
log "Installing Docker (OS: $(osfam))..."
|
|
case "$(osfam)" in
|
|
alpine) apk add -q docker docker-cli-compose openrc ;;
|
|
debian|rhel) command -v curl >/dev/null 2>&1 || \
|
|
{ command -v apt-get >/dev/null 2>&1 && apt-get install -y -qq curl; } || \
|
|
{ command -v dnf >/dev/null 2>&1 && dnf install -y -q curl; }
|
|
curl -fsSL https://get.docker.com | sh ;;
|
|
*) die "Unsupported OS for auto Docker install. Set SKIP_DOCKER_INSTALL=1 and install Docker yourself." ;;
|
|
esac
|
|
fi
|
|
if command -v rc-update >/dev/null 2>&1; then
|
|
rc-update add docker default >/dev/null 2>&1 || true
|
|
rc-service docker status >/dev/null 2>&1 || rc-service docker start
|
|
elif command -v systemctl >/dev/null 2>&1; then
|
|
systemctl enable --now docker >/dev/null 2>&1 || systemctl start docker || true
|
|
fi
|
|
# dockerd is often started in the background (esp. openrc) and returns before
|
|
# the socket is listening -- poll so the first `docker compose` call doesn't
|
|
# race it and abort under set -e.
|
|
local i
|
|
for i in $(seq 1 30); do
|
|
docker info >/dev/null 2>&1 && return
|
|
sleep 1
|
|
done
|
|
warn "Docker daemon not ready after 30s; continuing (compose may fail -- check 'docker info')."
|
|
}
|
|
|
|
open_bao_port() {
|
|
# Register 8200/tcp (the vault API). Prefer the host firewall when present;
|
|
# else ufw/firewalld if active. Restrict the source to the tape host where
|
|
# you can -- this is a secrets store, not a public service.
|
|
if [[ -d /etc/firewall/ports.d && -x /usr/local/sbin/firewall-apply ]]; then
|
|
log "Registering 8200/tcp with host firewall..."
|
|
printf '8200/tcp\n' > /etc/firewall/ports.d/openbao.rule
|
|
/usr/local/sbin/firewall-apply
|
|
elif command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
|
|
log "ufw active -- allowing 8200/tcp..."
|
|
ufw allow 8200/tcp >/dev/null
|
|
elif command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
|
|
log "firewalld active -- allowing 8200/tcp..."
|
|
firewall-cmd -q --add-port=8200/tcp --permanent
|
|
firewall-cmd -q --reload
|
|
fi
|
|
}
|
|
|
|
# Addresses currently assigned to this host, one per line. Parses plain
|
|
# `ip addr show` output -- no -o/scope filters, since busybox ip (what Alpine
|
|
# ships by default) supports neither -- with an ifconfig fallback.
|
|
host_addrs() {
|
|
if command -v ip >/dev/null 2>&1; then
|
|
ip addr show 2>/dev/null | awk '$1=="inet"||$1=="inet6"{split($2,a,"/"); print a[1]}'
|
|
elif command -v ifconfig >/dev/null 2>&1; then
|
|
ifconfig 2>/dev/null | awk '$1=="inet"||$1=="inet6"{v=$2; if(v=="addr:") v=$3; sub(/^addr:/,"",v); split(v,a,"/"); if(a[1]!="") print a[1]}'
|
|
fi
|
|
}
|
|
|
|
# A published port can only bind an address this host actually owns. Docker does
|
|
# not find that out until `up`, where it fails with a bare "cannot assign
|
|
# requested address" -- by which point this script has seeded .env and burned the
|
|
# address into the cert SAN, neither of which a re-run rewrites. So check first.
|
|
check_bind_addr() {
|
|
local bind="$1" bare addrs
|
|
bare="${bind#[}"; bare="${bare%]}" # unwrap an [IPv6] publish literal
|
|
case "$bare" in ''|0.0.0.0|'::'|'*') return 0 ;; esac
|
|
if [[ "$SKIP_BIND_CHECK" == "1" ]]; then
|
|
warn "SKIP_BIND_CHECK=1 -- not checking whether ${bare} is local."
|
|
return 0
|
|
fi
|
|
# `|| true` is load-bearing: host_addrs ends in a pipeline, and under
|
|
# `set -o pipefail` a probe that fails AFTER printing usable addresses (or an
|
|
# absent awk) would make this plain assignment non-zero and kill the whole
|
|
# deploy at this line, silently -- before the fail-open below is ever reached.
|
|
addrs="$(host_addrs || true)"
|
|
# Empty means the probe found no tool to ask, not that the address is absent
|
|
# -- do not block a deploy on that.
|
|
if [[ -z "$addrs" ]]; then
|
|
warn "Could not list this host's addresses (no ip/ifconfig, or it failed); skipping the bind check."
|
|
return 0
|
|
fi
|
|
if printf '%s\n' "$addrs" | grep -qxF "$bare"; then
|
|
return 0
|
|
fi
|
|
warn "Addresses on this host: $(printf '%s\n' "$addrs" | tr '\n' ' ')"
|
|
die "Nothing here is assigned ${bare}, so Docker cannot publish 8200 on it. Fix OPENBAO_ADDR / OPENBAO_BIND (compose reads an exported OPENBAO_BIND first, then ${STACK_DIR}/.env), or set SKIP_BIND_CHECK=1 if the address only comes up later."
|
|
}
|
|
|
|
disable_swap() {
|
|
[[ "$DISABLE_SWAP" == "1" ]] || { warn "DISABLE_SWAP=0 -- mlock will be best-effort."; return; }
|
|
# Detect active swap via /proc/swaps (a header line + one line per device) so
|
|
# this works on musl/BusyBox too, where `swapon --show` does not exist.
|
|
if [[ -r /proc/swaps ]] && [[ "$(wc -l < /proc/swaps)" -gt 1 ]]; then
|
|
log "Disabling swap (mlock keeps key material off disk)..."
|
|
swapoff -a || warn "swapoff failed -- disable swap manually."
|
|
else
|
|
log "No active swap."
|
|
fi
|
|
# Persist: comment any swap lines in fstab so it stays off across reboots.
|
|
# [[:space:]] (not \s) so the match works under musl/BusyBox grep/sed.
|
|
if [[ -f /etc/fstab ]] && grep -qE '^[^#].*[[:space:]]swap[[:space:]]' /etc/fstab; then
|
|
sed -i.bak -E 's|^([^#].*[[:space:]]swap[[:space:]].*)$|# \1 # disabled for OpenBao mlock|' /etc/fstab
|
|
log "Commented swap entries in /etc/fstab (backup: /etc/fstab.bak)."
|
|
fi
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Extract embedded archive (docker-compose.yml, config.hcl, gen-tls.sh, .env.example)
|
|
# ----------------------------------------------------------------------------
|
|
SCRIPT_DIR=$(mktemp -d -t openbao-deploy.XXXXXX)
|
|
trap 'rm -rf "$SCRIPT_DIR"' EXIT
|
|
|
|
extract_archive() {
|
|
grep -a -A 9999999 '^__ARCHIVE_BELOW__$' "$0" \
|
|
| tail -n +2 \
|
|
| base64 -d \
|
|
| tar -xz -C "$SCRIPT_DIR" 2>/dev/null || true
|
|
}
|
|
extract_archive
|
|
# Fallback: run straight from the source dir (before build.sh embeds a payload).
|
|
if [[ ! -f "$SCRIPT_DIR/docker-compose.yml" ]]; then
|
|
SRC=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
for f in docker-compose.yml config.hcl gen-tls.sh .env.example; do
|
|
[[ -f "$SRC/$f" ]] || die "Missing $f (no embedded payload and not in $SRC -- run build.sh)."
|
|
cp "$SRC/$f" "$SCRIPT_DIR/$f"
|
|
done
|
|
fi
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Prompt for required values
|
|
# ----------------------------------------------------------------------------
|
|
prompt() {
|
|
local var="$1" msg="$2" cur="${!1}"
|
|
[[ -n "$cur" ]] && return
|
|
[[ "$SKIP_PROMPTS" == "1" ]] && die "$var required (set it in the environment; running with SKIP_PROMPTS=1)."
|
|
read -r -p "$msg: " "$var"
|
|
}
|
|
prompt OPENBAO_ADDR "LAN address the Kanrisha tape host reaches this vault at (IP or DNS)"
|
|
[[ -n "$OPENBAO_ADDR" ]] || die "OPENBAO_ADDR is required."
|
|
|
|
# Build the cert SAN list: loopback + whatever OPENBAO_ADDR is (IP vs DNS) +
|
|
# any extra SANs the operator exported in OPENBAO_TLS_SANS.
|
|
ADDR_KIND=dns
|
|
if [[ "$OPENBAO_ADDR" =~ ^[0-9.]+$ ]]; then
|
|
ADDR_SAN="IP:${OPENBAO_ADDR}"; ADDR_KIND=ipv4
|
|
elif [[ "$OPENBAO_ADDR" == *:* ]]; then
|
|
ADDR_SAN="IP:${OPENBAO_ADDR}"; ADDR_KIND=ipv6
|
|
else
|
|
ADDR_SAN="DNS:${OPENBAO_ADDR}"
|
|
fi
|
|
SANS="DNS:localhost,IP:127.0.0.1,${ADDR_SAN}"
|
|
[[ -n "${OPENBAO_TLS_SANS:-}" ]] && SANS="${SANS},${OPENBAO_TLS_SANS}"
|
|
|
|
# A Docker-published port bypasses the host INPUT firewall, so the interface bind
|
|
# is the real restriction. If the operator left OPENBAO_BIND at the all-interfaces
|
|
# default and OPENBAO_ADDR is an IP, narrow the publish to just that LAN IP. IPv6
|
|
# literals must be bracketed in the compose port mapping ([addr]:8200:8200).
|
|
if [[ "$OPENBAO_BIND" == "0.0.0.0" ]]; then
|
|
case "$ADDR_KIND" in
|
|
ipv4) OPENBAO_BIND="$OPENBAO_ADDR"; log "Binding the API to ${OPENBAO_BIND} only (set OPENBAO_BIND to override)." ;;
|
|
ipv6) OPENBAO_BIND="[${OPENBAO_ADDR}]"; log "Binding the API to ${OPENBAO_BIND} only (set OPENBAO_BIND to override)." ;;
|
|
*) warn "OPENBAO_BIND=0.0.0.0 and OPENBAO_ADDR is a DNS name -- API publishes on ALL interfaces. Set OPENBAO_BIND to a LAN IP to narrow it." ;;
|
|
esac
|
|
fi
|
|
|
|
# Which OPENBAO_BIND `docker compose` interpolates decides where the port lands,
|
|
# and compose reads the shell environment BEFORE $STACK_DIR/.env. So an exported
|
|
# value wins; one derived above does not, and the .env -- which deploy.sh never
|
|
# rewrites -- wins instead. Resolve which, say so when the two disagree, and
|
|
# confirm the address is really on this box before anything is written to disk.
|
|
EFFECTIVE_BIND="$OPENBAO_BIND"
|
|
if [[ -f "$STACK_DIR/.env" ]]; then
|
|
ENV_BIND=$(sed -n 's/^OPENBAO_BIND=//p' "$STACK_DIR/.env" | tail -n1)
|
|
ENV_ADDR=$(sed -n 's/^OPENBAO_ADDR=//p' "$STACK_DIR/.env" | tail -n1)
|
|
|
|
if [[ "$BIND_FROM_ENV" == "1" ]]; then
|
|
# Exported, so compose prefers it -- but a stale .env line still bites a
|
|
# later hand-run `docker compose up` that has no such environment.
|
|
if [[ -n "$ENV_BIND" && "$ENV_BIND" != "$OPENBAO_BIND" ]]; then
|
|
warn "OPENBAO_BIND=${OPENBAO_BIND} came from the environment, so compose prefers it: THIS run binds ${OPENBAO_BIND}."
|
|
warn "But ${STACK_DIR}/.env still says ${ENV_BIND} -- update that line, or a later plain 'docker compose up -d' will bind ${ENV_BIND}."
|
|
fi
|
|
elif [[ -n "$ENV_BIND" ]]; then
|
|
EFFECTIVE_BIND="$ENV_BIND"
|
|
if [[ "$ENV_BIND" != "$OPENBAO_BIND" ]]; then
|
|
warn "${STACK_DIR}/.env pins OPENBAO_BIND=${ENV_BIND}. Nothing was exported this run, so compose uses that, not the ${OPENBAO_BIND} derived here -- edit the .env to change the bind."
|
|
fi
|
|
else
|
|
# .env exists but has no OPENBAO_BIND line (hand-edited?): compose falls
|
|
# back to the compose-file default, which publishes on everything.
|
|
EFFECTIVE_BIND=0.0.0.0
|
|
warn "${STACK_DIR}/.env has no OPENBAO_BIND line and none was exported -- compose falls back to 0.0.0.0, publishing the API on ALL interfaces."
|
|
warn "Add 'OPENBAO_BIND=${OPENBAO_BIND}' to ${STACK_DIR}/.env to narrow it."
|
|
fi
|
|
|
|
if [[ -n "$ENV_ADDR" && "$ENV_ADDR" != "$OPENBAO_ADDR" ]]; then
|
|
warn "${STACK_DIR}/.env still says OPENBAO_ADDR=${ENV_ADDR}, and an existing cert in ${STACK_DIR}/tls is never regenerated over."
|
|
warn "To actually move the vault to ${OPENBAO_ADDR}: edit that .env, then 'rm -f ${STACK_DIR}/tls/tls.crt ${STACK_DIR}/tls/tls.key', then re-run."
|
|
fi
|
|
fi
|
|
check_bind_addr "$EFFECTIVE_BIND"
|
|
|
|
# config.hcl is rendered from OPENBAO_UI, so resolve which value actually
|
|
# applies: one passed to this run wins, else whatever .env already deploys,
|
|
# else the default. Without this, `OPENBAO_UI=0 bash deploy.sh` against an
|
|
# existing node would look like it worked and change nothing.
|
|
EFFECTIVE_UI="$OPENBAO_UI"
|
|
if [[ "$UI_FROM_ENV" != "1" && -f "$STACK_DIR/.env" ]]; then
|
|
_env_ui=$(sed -n 's/^OPENBAO_UI=//p' "$STACK_DIR/.env" | tail -n1)
|
|
[[ -n "$_env_ui" ]] && EFFECTIVE_UI="$_env_ui"
|
|
fi
|
|
[[ -n "$EFFECTIVE_UI" ]] || EFFECTIVE_UI=1
|
|
case "$EFFECTIVE_UI" in
|
|
1|true|yes|on) UI_HCL=true ;;
|
|
0|false|no|off) UI_HCL=false ;;
|
|
*) die "OPENBAO_UI must be 1 or 0 (got '${EFFECTIVE_UI}')." ;;
|
|
esac
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Lay down the stack
|
|
# ----------------------------------------------------------------------------
|
|
log "Setting up $STACK_DIR..."
|
|
install -d -m 0750 "$STACK_DIR"
|
|
install -m 0640 "$SCRIPT_DIR/docker-compose.yml" "$STACK_DIR/docker-compose.yml"
|
|
# config.hcl holds no secrets and is read by the in-container server process
|
|
# (which may be a non-root user) over a read-only mount -- keep it world-readable.
|
|
# Rendered rather than copied, so @UI@ reflects OPENBAO_UI. A bind-mounted
|
|
# file's CONTENTS are not part of the compose config hash, so `up -d` alone
|
|
# would leave a changed config.hcl unloaded -- track whether it actually
|
|
# changed and restart below only then, because restarting re-SEALS the vault.
|
|
CONFIG_CHANGED=0
|
|
_UI_HCL="$UI_HCL" awk '{ gsub(/@UI@/, ENVIRON["_UI_HCL"]); print }' \
|
|
"$SCRIPT_DIR/config.hcl" > "$SCRIPT_DIR/config.hcl.rendered"
|
|
# `cmp -s` exits 1 when they differ, so it stays inside an `if` condition:
|
|
# `cmp -s A B && CONFIG_CHANGED=1` would trip set -e whenever they matched.
|
|
if [[ -f "$STACK_DIR/config.hcl" ]] && ! cmp -s "$STACK_DIR/config.hcl" "$SCRIPT_DIR/config.hcl.rendered"; then
|
|
CONFIG_CHANGED=1
|
|
fi
|
|
install -m 0644 "$SCRIPT_DIR/config.hcl.rendered" "$STACK_DIR/config.hcl"
|
|
log "Web UI: ${UI_HCL} (OPENBAO_UI=${EFFECTIVE_UI})"
|
|
install -m 0750 "$SCRIPT_DIR/gen-tls.sh" "$STACK_DIR/gen-tls.sh"
|
|
|
|
ENV_FILE="$STACK_DIR/.env"
|
|
set_env() { # <KEY> <value>: update KEY in .env, or append if absent
|
|
# The value goes through the ENVIRONMENT, never interpolated into a sed
|
|
# script. Interpolating it corrupts any value containing & (sed expands it
|
|
# to the whole match) and aborts the run on one containing the s|||
|
|
# delimiter -- both reachable for an OIDC client secret or an issuer URL
|
|
# with a query string. Same helper as 947c899 gave the other stacks.
|
|
local key="$1" val="$2" tmp
|
|
if [[ ! -f "$ENV_FILE" ]]; then
|
|
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
|
|
return 0
|
|
fi
|
|
tmp="$(mktemp)"
|
|
_SE_KEY="$key" _SE_VAL="$val" awk '
|
|
BEGIN { k = ENVIRON["_SE_KEY"]; v = ENVIRON["_SE_VAL"]; seen = 0 }
|
|
!seen && index($0, k "=") == 1 { print k "=" v; seen = 1; next }
|
|
{ print }
|
|
END { if (!seen) print k "=" v }
|
|
' "$ENV_FILE" > "$tmp"
|
|
cat "$tmp" > "$ENV_FILE" # rewrite in place: keeps the 0600 mode/owner
|
|
rm -f "$tmp"
|
|
}
|
|
|
|
if [[ ! -f "$ENV_FILE" ]]; then
|
|
log "Seeding $ENV_FILE..."
|
|
install -m 0600 "$SCRIPT_DIR/.env.example" "$ENV_FILE"
|
|
set_env OPENBAO_ADDR "$OPENBAO_ADDR"
|
|
set_env OPENBAO_BIND "$OPENBAO_BIND"
|
|
set_env OPENBAO_TLS_SANS "$SANS"
|
|
set_env OPENBAO_UI "$EFFECTIVE_UI"
|
|
else
|
|
log ".env exists; leaving it alone."
|
|
# ...except a UI switch passed to THIS run: config.hcl is rendered from
|
|
# it above, so letting .env keep the old value means the next run
|
|
# silently reverts the UI. Only touch the key when it was actually passed.
|
|
if [[ "$UI_FROM_ENV" == "1" ]]; then
|
|
_cur_ui=$(sed -n 's/^OPENBAO_UI=//p' "$ENV_FILE" | tail -n1)
|
|
if [[ "$_cur_ui" != "$EFFECTIVE_UI" ]]; then
|
|
set_env OPENBAO_UI "$EFFECTIVE_UI"
|
|
log " OPENBAO_UI: ${_cur_ui:-<unset>} -> ${EFFECTIVE_UI}"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# TLS: self-signed unless a cert is already present (drop in a CA-signed pair
|
|
# to use your Smallstep CA -- see the README).
|
|
# ----------------------------------------------------------------------------
|
|
install -d -m 0750 "$STACK_DIR/tls"
|
|
OPENBAO_TLS_SANS="$SANS" bash "$STACK_DIR/gen-tls.sh" "$STACK_DIR/tls"
|
|
|
|
disable_swap
|
|
open_bao_port
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Bring up the stack
|
|
# ----------------------------------------------------------------------------
|
|
if [[ "$FORCE" != "1" ]]; then
|
|
printf '\nAbout to start OpenBao from %s (TLS on https://%s:8200). Continue? [y/N] ' "$STACK_DIR" "$OPENBAO_ADDR"
|
|
read -r ans
|
|
[[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]] || { warn "Aborted."; exit 0; }
|
|
fi
|
|
|
|
install_docker
|
|
|
|
cd "$STACK_DIR"
|
|
|
|
log "Pulling image..."
|
|
docker compose pull
|
|
|
|
# The OpenBao server process runs as the image's own user -- root in some image
|
|
# variants, a non-root service user in others -- and reads its config + TLS over
|
|
# read-only bind mounts and writes the raft data volume. So those must be owned by
|
|
# that UID. Detect it from the pulled image (-T: no TTY on the piped stdout) and
|
|
# align ownership; a no-op when the image runs as root.
|
|
# Ask for the account the ENTRYPOINT switches to, not the one a probe starts as.
|
|
# The image's entrypoint runs as root and then does `su-exec openbao "$@"` before
|
|
# exec'ing the server, so `--entrypoint id -u` bypasses that drop and reports 0.
|
|
# Chowning to 0 on the strength of that leaves the server running as the
|
|
# unprivileged account with a root-owned raft volume, and it crash-loops on
|
|
# "failed to open bolt file: /openbao/data/vault.db: permission denied".
|
|
BAO_UID=$(docker compose run --rm --no-deps -T --entrypoint sh openbao \
|
|
-c 'id -u openbao 2>/dev/null' 2>/dev/null | tr -dc '0-9')
|
|
if [[ -z "$BAO_UID" ]]; then
|
|
# No such account: this image runs the server as whatever the entrypoint
|
|
# started as, so the older probe is the right answer here.
|
|
BAO_UID=$(docker compose run --rm --no-deps -T --entrypoint id openbao -u 2>/dev/null | tr -dc '0-9')
|
|
fi
|
|
if [[ -z "$BAO_UID" ]]; then
|
|
# Both probes failed. Falling back to root is the DANGEROUS direction -- it is
|
|
# exactly what produced the crash loop -- so say how to recover.
|
|
warn "Could not detect the OpenBao service account; assuming root."
|
|
warn "If OpenBao crash-loops with a permission error on /openbao/data, run:"
|
|
warn " cd ${STACK_DIR} && docker compose down"
|
|
warn " docker run --rm -v openbao_openbao-data:/data -v ${STACK_DIR}/tls:/tls \\"
|
|
warn " --user 0:0 --entrypoint sh openbao/openbao:\${OPENBAO_TAG:-2.6.2} -c 'chown -R openbao:openbao /data /tls'"
|
|
BAO_UID=0
|
|
fi
|
|
log "OpenBao server runs as UID ${BAO_UID}; aligning file/volume ownership."
|
|
chown -R "${BAO_UID}:${BAO_UID}" "$STACK_DIR/config.hcl" "$STACK_DIR/tls"
|
|
# tls.key stays private to that UID; config.hcl + tls.crt are non-secret.
|
|
chmod 0600 "$STACK_DIR/tls/tls.key" 2>/dev/null || true
|
|
# Raft data volume. Gate on what the volume ACTUALLY is, not on whether this is
|
|
# the first run: a volume left root-owned by an earlier deploy (or by a botched
|
|
# UID detection) would otherwise never be repaired, since the first-run flag is
|
|
# false forever after. Reading the current owner costs one container start and
|
|
# still keeps the recursive chown off a healthy live raft dir.
|
|
_data_uid=$(docker compose run --rm --no-deps -T --user 0:0 --entrypoint stat openbao \
|
|
-c '%u' /openbao/data 2>/dev/null | tr -dc '0-9')
|
|
if [[ -z "$_data_uid" ]]; then
|
|
warn "Could not read the raft volume's ownership; skipping the data chown."
|
|
elif [[ "$_data_uid" != "$BAO_UID" ]]; then
|
|
log "Raft volume is owned by UID ${_data_uid}; chowning to ${BAO_UID}..."
|
|
docker compose run --rm --no-deps --user 0:0 --entrypoint chown openbao \
|
|
-R "${BAO_UID}:${BAO_UID}" /openbao/data 2>/dev/null || \
|
|
warn "Could not chown the raft data volume; OpenBao will fail to write storage. chown the openbao-data volume to UID ${BAO_UID}."
|
|
fi
|
|
|
|
log "Starting OpenBao..."
|
|
_bao_before="$(docker compose ps -q openbao 2>/dev/null || true)"
|
|
docker compose up -d --remove-orphans
|
|
_bao_after="$(docker compose ps -q openbao 2>/dev/null || true)"
|
|
if (( CONFIG_CHANGED )) && [[ -n "$_bao_before" && "$_bao_before" == "$_bao_after" ]]; then
|
|
warn "config.hcl changed and compose did not recreate the container; restarting to load it."
|
|
warn "NOTE: a restart re-SEALS the vault -- you will have to unseal again."
|
|
docker compose restart openbao || warn "Restart failed; run: cd ${STACK_DIR} && docker compose restart openbao"
|
|
fi
|
|
|
|
# OpenBao starts SEALED (and, first time, uninitialised), so it reports unhealthy
|
|
# until you init + unseal -- that is expected. Give it a moment to bind.
|
|
sleep 5
|
|
docker compose ps
|
|
|
|
# OpenBao starts SEALED and therefore reports UNHEALTHY until it is initialised
|
|
# and unsealed -- that is expected and the summary below explains it. A
|
|
# RESTARTING container is a different thing entirely: it crashed, compose is
|
|
# looping it, and every command in that summary will fail against it. Say so
|
|
# plainly rather than printing an unqualified DEPLOYED.
|
|
BAO_STATE="$(docker inspect -f '{{.State.Status}}' openbao 2>/dev/null || echo unknown)"
|
|
if [[ "$BAO_STATE" != "running" ]]; then
|
|
warn "Container state is '${BAO_STATE}', not 'running' -- OpenBao is crash-looping, not merely sealed."
|
|
warn "Nothing below will work until that is fixed. Start with:"
|
|
warn " cd ${STACK_DIR} && docker compose logs --tail=60 openbao"
|
|
fi
|
|
|
|
cat <<EOF
|
|
|
|
================================================================
|
|
DEPLOYED (OpenBao starts SEALED -- finish setup below)
|
|
|
|
Address: https://${OPENBAO_ADDR}:8200
|
|
Stack dir: ${STACK_DIR}
|
|
^ run every 'docker compose' command below from there:
|
|
cd ${STACK_DIR}
|
|
From anywhere else compose reports "no configuration file
|
|
provided: not found".
|
|
TLS: ${STACK_DIR}/tls/tls.crt (give this to the Kanrisha daemon as
|
|
[encryption.openbao].ca_cert)
|
|
|
|
>> Initialise NOW: an uninitialised vault reachable on the LAN can be init'd by
|
|
anyone who connects, capturing the root token + unseal keys. Do step 1 before
|
|
walking away.
|
|
|
|
1. Initialise + unseal (ONCE). Store the unseal keys + root token OUT OF BAND:
|
|
docker compose exec -e BAO_ADDR=https://127.0.0.1:8200 openbao \\
|
|
bao operator init -tls-skip-verify
|
|
docker compose exec -e BAO_ADDR=https://127.0.0.1:8200 openbao \\
|
|
bao operator unseal -tls-skip-verify <key> # x3, three different keys
|
|
|
|
2. Bootstrap for Kanrisha. Run the Kanrisha repo's deploy/openbao/bootstrap.sh
|
|
from a host that HAS the 'bao' CLI (the Kanrisha host or your workstation --
|
|
this vault host only ships Docker), pointed at this vault. Copy tls.crt there
|
|
first as the BAO_CACERT:
|
|
BAO_ADDR=https://${OPENBAO_ADDR}:8200 BAO_CACERT=/path/to/openbao-ca.crt \\
|
|
BAO_TOKEN=<root> bash bootstrap.sh
|
|
Then point the Kanrisha daemon at:
|
|
[encryption.openbao]
|
|
address = "https://${OPENBAO_ADDR}:8200"
|
|
ca_cert = "/etc/kanrisha/openbao-ca.crt" # = tls.crt above
|
|
|
|
3. Back it up (this vault is the sole recovery path for encrypted tapes). Snapshot
|
|
save is token-gated -- pass a token with sys/storage/raft/snapshot (the root
|
|
token works):
|
|
docker compose exec -T -e BAO_TOKEN=<token> openbao \\
|
|
bao operator raft snapshot save -address=https://127.0.0.1:8200 -tls-skip-verify /tmp/openbao.snap
|
|
docker compose exec -T openbao cat /tmp/openbao.snap > ${STACK_DIR}/openbao.snap
|
|
...then age-encrypt + copy it off-box. See the README for the full DR flow.
|
|
|
|
Manage:
|
|
docker compose logs -f
|
|
docker compose pull && docker compose up -d # update
|
|
docker compose down # stop, keep the vault data
|
|
docker compose down -v # stop, WIPE the vault (DESTROYS keys)
|
|
|
|
Re-running this script is idempotent (it won't re-init or touch .env / tls).
|
|
================================================================
|
|
EOF
|
|
|
|
if [[ "$BAO_STATE" != "running" ]]; then
|
|
warn "Reminder: the container is '${BAO_STATE}'. Fix that before step 1 above."
|
|
fi
|
|
|
|
# IMPORTANT: nothing executable below this line. Everything after
|
|
# __ARCHIVE_BELOW__ is the embedded tar.gz payload (base64), added by build.sh.
|
|
exit 0
|
|
__ARCHIVE_BELOW__
|
|
H4sIAAAAAAAAA+1af2/byNHO3/oU+8qHOwlnUpIdJ1cFPlSx1ZwQWzYsu9ciFygUubJYU1yWS9rW
|
|
mxroh+gn7CfpM7PLH5KTS4vmcm2hTWCb5P6cnZl9npkNlH8jU8dXy0Rp6a6W0ZPPXrooz54+fdJ9
|
|
1u09P+jx7263x+9RDvYO9p/08PPpAb4833vS7e3j9RPR/fxTeVxynXmpEE8Onk9/VNGt/BJj/geV
|
|
HXGWyPilpwTk4N+Iv//1b2LhpYGMZSAyL5GOjP10lWShisWNXKGaSqWYq1S89uI01AvPbew0dsSl
|
|
SlSkrleipb2ldE4G410xPhOpvJWpliJJ1f2q3UdFUTbk/sVC6Uw4ziLLEu043wuF+cw81f9ur9tF
|
|
5VbsZeGtFJcnE4EpZAspbr08ykSYaRnN2zz4VRyFN5I/KvxIzWK0CKlBqDGLRNFXzBxPsRJHXhCs
|
|
REecyOwbLYZmiX3hoS8t/VRm2i40k+kyxBSk5hmYQYUXB4K79fwF5KSwRh6cVx2rjB7QVZLPotDH
|
|
LNBLLDPXdKGFJ6gXR4fXJGVfpploBTKJ1MrVC3EN2ac8Ypi1BQRNszoaFNUTL0zFSuUiSFVCK3Q7
|
|
WaRFS7rXrrgNPfqWisnSiyKdyQQtzfwGR6fDtttoxNiefiHlRuNWRflS6n5DFO+cwMu8vuCyI1Jv
|
|
nvEKrmlOAYvFu5asKOVmQIYJtjnEgHFGws9ko6Flehv6a13Tn0KES3RQTqFTfPvq/dn5cPxycDa9
|
|
HLzqO3vuM3fvgRv4Ks68EFKZrk/efFsusR19QcNhmXBl8Ty8Piy67Zhn+8td+BE3SyXZPbY8jyOp
|
|
tYNlJYkMGmbRlwsJnc3ulPCgA6Px8OLSLQ2F5I6qYhnBdwqdJ4nCBmLhYSqj1a7QilRgEcbXkJvt
|
|
kCRVrkJQQy2WcqnSFevSuyDU3iySU9PnoZh7kZbvaHerebPm2P7IRAWvIE+ETFOVujTpFc/3RiYZ
|
|
bCVaiZn0vVyzYZhPaLek9dKotqsU07ilyaLSUtypPArwDjaATRRkvSthdJNUtVwCOQDY0TWeXNvR
|
|
a4yxRKM09HiuZhrzucDibsRsJY5Hk8HLk9H4lZj8ODjfFXeL0F+ISvEDJbXpzPeSKSzUqIsQjhid
|
|
H01Pzo5e84s8CpcwxOIr5EhS6wunx29oO8qPO2LM/sMh0xucj1xxTkaJ0awzgcmidyOuVqGAL0fj
|
|
4zZtZJgZh5GV3cl7OigDap7H2HA2CrbwuQdld+H0LofwJOLYnKyJGY4Ml9QEvR2PB5ffBGWHtP+z
|
|
VeJpDYunGbFDHI3Pry7FHCp1B0NmpYLXgGX7knU3DX12ycapkeSqBZ9dYl+weZg8WWnsQT/uhOf7
|
|
tPF3YbYQ9XWKb43fZBePnVtCUmVfLbiWaSr/nGMiU8x0Cn0I56upH4VQ+Cm5rjbpYfz/3rqy7hof
|
|
GYcZtCHUsuyx5sCXSxmEmChJ3iNxVtUDW8fH+5nkfr4hMaHXlYolVEeVPVId9sTkL926Psk5daLX
|
|
l5up8nlwfHyBrmRs9rm2JVAJt1S+ZuWZqIe+03X53wOfUfyjyZVrvtS0dGtup/9xj9RPVa0JRF7V
|
|
pYfa1zUXXVaiJ64i49swVfESe1MZwPnro8lOrye8PFNOHmuJTT4fjVk3WO0hvMCIgTaHvzeTG1/3
|
|
es1ybyvhyJg8VbC+3a74ofAsrTyGywna5iS+w2YWkiQB/jA5nWLwfl2oxTvnoVmO8sMAp/MSuxc6
|
|
sQokTxRWFUD/spA9GttlIL+xxzBNio0ZfgMGot2yK97p8xHvNsZloNHvdKrx6cNDbRuLRkcnV5PL
|
|
4cU/0bDXtD6wC99tRAwJtRb4nS1W7RdiD+/tW0CcPLZfjFnzAmMJY4AXZkOgqrZHLPM21CFW5woH
|
|
2uDomzBxjBnW/buI+PSFa156K0EvH6MMIxMzNoALfKZdL8AGjsI3zaPT4+auaEKn6Bed47mmvxwr
|
|
1MNCBr2952wDPSM2qrIxt+Zb2zf7xlsv6ov9bqFFWbiUKseQB8UbQK40hOmIffuCD7cpUEWocLj3
|
|
0PTXhsr/k6Uy4V9ujE/wv2f7e08r/neAej0igFv+9yVKxf/qzA94w5K4f4kDjh9RtdIptQAW17gg
|
|
nR9znFVZe7dOL4huEAmzLKPlRxIeUcdeohcqc2YeYYPjizbBXXKNBkkY4DQ5Oxk2CND6imFr4gHq
|
|
0Czt9C2j1bzUSGkLeitgj2NUDAgi4KV1wjsVpEULtDWIeL1HV7wk7gwQATTeYsZUzFi3GQhZMsms
|
|
0B7BEKTupIrIorrB2Qt/CLQsZlS9paVx6hfDwfHpEBANQCXN45lSN8ThCvE0aaymeA+fyYtFORTN
|
|
NVxAJxMdMNMwoG8FgMCJ9dAg2n4ycQqOS/IoNswV8Oeun2aEQNwbSx/qB8psVYNa2G5gVU1bh2nu
|
|
Mmv9ZzlrqHVu+nvEXNFdyV1519blwlogq3hC4IHLQPFSGLU2EAGEGJ28qRTYtRJ46/re1ByKpZI2
|
|
Mz8x0rTnnTmKSG4W8pUogXAxtZ7OQ+COutDxpWNlV1SE+Ey9D1bEV6pIlliD4Lxci7yx//bA94RB
|
|
3iZoAPtZsjjKQEqfOzKj/jxsx1QgJ1mrX3z07Fw3yubUTXXH98xKWZteS5mQFt3UqWBJAbEdAYH3
|
|
R1SwHv0gKsNb195tGC6D6kzwKgbOlHWTgfcpOCPUXQFLoXRwNinoQUrIwhof4Llmyyn6Wni6ZPQF
|
|
lyefYYc8h//R0o5oXVpMHf3UXOPsPzUL1m52hVXZToQs2lYWlTcS+s4jW4DueYHbRJcTuCDFS4b6
|
|
YavXgwI0LjkIGXKEC4gql8JxPhY7aOyYSNOHIgUFvRZvCl79lidpiXXBqCl8sYMpbUaI2RVQFCAT
|
|
kZxnZKGA27QA7H0kDWAnz1QBdlKKEjPSxOa0mgK+evCteOR2NOnozltp9Dc4uhz9fshTi8nvYhsC
|
|
aLSPKRrtgwvh0AYEHeM11pl64fWCXGpJ7q0pw23uiN9B/BWzSCIzpsZxJ7M1ngCL22QA1CezTMM6
|
|
+KD5AO8wMUeK9rC8iJKVm9zwkpAEnxYG9RE43fAjeDCZmrofq9cjnSH/N8vDKHMw7p2ciauRKyb5
|
|
TGdhlmebbppVs2AvVHOU0bw1RWpjedew4Y08lX0b+uCoWmCOc3oELCjPCMPxwchNsKsSB2rzQhqG
|
|
nHsRMcwVKTefnqZ6tkhVfr1gYXFg5jJPYw6aUfCKxr5beKRhGUWr6CRIKCyKHazCBB17ltI8InVt
|
|
McUSCjVL1Z2mKWpVO8750CX1JYixIF9hd8tWL3aK7MwrIop6IaMINFP7qYqiGU56t5GHa57xt1ej
|
|
35IB/P1vf8V/Magx7VYRWGmzRz8e/m5wdXIpRhNxOhhfDU7E1XgyxC/T9N//j1kcD88vhkeDy+Fx
|
|
nxf38mp0cumA8RtSbxj+TEbqzgSxjTOFJAqfeLvnQs94exsmYgOKTu4IHG2WMwwkXx5qigeGsT29
|
|
US92PxhtKKqR0VCEmnqU9xQUx8fXpxORRPk1xm+9s380b5a6WQUh3j+8axchMM5SZNAM9EWcmaLk
|
|
MGeNH+ShZ3LOSMu7IVWiddyT4f9IUS9AUJ6OjWnUEh8pdIx2YXhsnQ31+w4nXaV0ZinvROt+v40O
|
|
4d2gGgYM2lgyNFitLduj6Cgm5pVCgRg7jPZ20R9FrukoN1LGmRsZGwvxdanymLMIZdOlCvKIkEAg
|
|
KapOmFmth5WNOcKTobeN0ApDLniiPsN0sRHjec/vBOx6Vjvrc5128Kaj1Txb6CX9bf/cc7Vq2jY6
|
|
AoAtQVLxFiCg6onG7XRqsylqESyKPCgi1bqxIM4Kr6izlPApcaiX3P99r/vd82YdleyIo9en08Fw
|
|
Mn11dMptHraBgn+hXMuYIjc4HH65MYjkPz84+Bj/3+8+N/nf7rP9/ef7zP8pJbzl/1+g7PwfG/os
|
|
jDuEVMCvF+wjKr3gY6vISW4EFYmnMJn4liE/4eY6oS6BAoHRuSCMZ7IpJSRI4DvhA10xZnxHUPIu
|
|
DSn3Ke/RmHw4kRFtznFl4ppr6VCTPf0UkaylQMWf6LBgImppmviWTjlLc0u/CrW4BioE4CxRAflX
|
|
dtEArgpMgmO3jkpM9ONKc1qTZViX3xtIaXo8unhb+KyWTUwYFkynyRAukr6UCVC0mAzGE2HzmwIP
|
|
LM1dwSttHo8nfY59uJEX747O+z1DUHtdfihgYuFFN/s+HvyR+gaHCIMw41hMAMhdzey7vYMyw7v0
|
|
7gt8pDmLlGRtyu5mwpG5gq9P0CiMGg2s8bD51fte3+GFPTQbtIbDWqS/WFffoQWAY3gRsda1OaMZ
|
|
TW+zGb3rO5gXvjeWN7QtTiKaX2FQvIB2vXkjnLl5UfJv8fXX6y+Ja4u3b18wWAH3xemiRPON3a23
|
|
rNAlcd1QUpISdWTCR9K7tWDVI/LiEoWHzmai25iHjYZNSwvnlnM2Wkfi+w6O7k6c45zf+/7rnvjL
|
|
X8T7RxMoKpORzAECgqb4/uu9F6br3gvBNHt4dDwZiHNn7+DZ4xg/UwLSFwZFYQwuERSJ3IKjrMUM
|
|
0GGLpnp0MmqzgWXK5tPKXB0HdPAEoZOEOJoQ+h4jQdp70L9i4qn8s3DuD7q/EQ4YBVmU9LFT+ANA
|
|
GH9PQcu9JdY79XPA636ShkuJhdz2BBNHLX6CJB2qn2ebO+esv+Mtdlhz8RIK0jSNdT77ExDM0fjQ
|
|
BixQC0sH5BRN+gaqOIiyMbjM4VfvSR9Jp/wFIJZ41u1uDFp8ePp0Y+RGY3PzXlkvGaztSuEN2VNZ
|
|
HWo+aitoz3Rf2AnVrJPe0eIegsetXlG8tT6rgvluxsQ8/aEhfy4uBj9lEuD1cFrh0dvQ91/76NqW
|
|
z1CIFbjy3lsm0S8GfT51/+/p/t5m/ufg2cEW/32JAiilkhV5DdIERjjMRENQeRP5KyNZSUi3lrha
|
|
njDRxUmJUyCKVkSxqyiTZvrMFW2EGp6jyBBQfuAFZblLgMcVCRfSGWUj/gQCMwonGFxIp2lo+HCi
|
|
ktxcumHVraIuFHi5MCGoMCJQ87kiKl/sP0U87QHdoitJqQBKEnTlrW18+mB8MZr8MKjdm8z50lB1
|
|
XpcpMRXbyM3JYOyKCe+Iud2xcfGQkIKJBdlLjRx1o1xNooCFP5Y8KZAE6rZ+7kZF223UXx6WOJU2
|
|
blRcmzI7W96UGpyPzG0p0JKA12fTibRmV5wAe0kxA/K96UD7bACl0j9z4YkDQx+87EPRI44z0zWf
|
|
Fxz7RVV5n0ShH2YRWQNd3sRRDbaTSJ+wjr0n5orWQGxc6frU5a0icWU3xKb0+KWydCI0ojdoO4yr
|
|
+2T1u15uu1G/hnRoU1LrFsCZo19bkT+vTQz/cHkxYHRUEsxNLd6FCq8UhLi22d+KSKmEAraixXcN
|
|
17TEZBkKgKyLjLK51htUubXh+Peji7Px6XB8SdpuOuAbLERrDS2lfA3Zqeb4ecr6ZCORcHcc1S4H
|
|
NhSOidkmLTr8KK3rGVZZdkKYvVoKZbxTa+DzkIKqBV8UvHrLacPCKSvKpMxlCsOWbltckrqbPMwG
|
|
ry6Ir+HXFaGu2PTuBpMueDQxfhrVJHpL4kz/r2PIJfhAOrdyFaVAGpsU8BAMcF3jf+Skx/+C0mNZ
|
|
Pb4nltpU40Zip/CCa0mYkrNjK+j2GflOuixHGjamSAsldmppnTJdw16wujtp7tt/NF1Ti9xzsoYD
|
|
6vRkpkXnBbWtUjMmoWgC4Rs5GfRlrv7eSJnoR+kZexHiUf4F+pOpdFVpydXosLeuCyO6WY7j8fq/
|
|
WB2woHPKY7GRJqkKcp9u/1nb1zUjGbw65Evy6zL4YPLpRXEZlizQJp5MnfbnERXlHOguKV39/ORd
|
|
0g/fITVRCJttolQ9Zzw6Rb5Dm6SIuW29kfqA8z4rbptyaMbCg5oS8k2eOCe0WiRvSO2L3E1jI2Ny
|
|
uKW327It27It27It27It27It27It27It27It27It27It27It27It2/JfWP4ByV0gzABQAAA=
|