feat(openbao): add update.sh, an updater that refuses to strand a sealed vault

Follows the copyparty/ergo updater idiom -- check/update/run/install/uninstall,
a conf file the environment overrides, the version pinned into .env so the
running release is explicit, DRY_RUN -- but inverts its central assumption.

copyparty and ergo come back by themselves after a recreate. OpenBao comes back
SEALED: with the default Shamir seal a restart needs three unseal keys typed in
by a human. A scheduled `latest` update would therefore take the vault offline
at 03:00 and leave it there. So the scheduled path defaults to UPDATE_POLICY=
notify and never changes the running version; `install` schedules a daily CHECK
and says so. UPDATE_POLICY=auto opts in, and is STILL refused unless an
uncommented `seal` stanza is present in config.hcl -- only auto-unseal makes an
unattended update defensible.

`update` preflights before touching anything, because every one of these fails
worse halfway through than up front:
- the container must be running;
- the vault must be UNSEALED, since a sealed vault cannot produce a snapshot and
  there would be no rollback plan;
- BAO_TOKEN must be present, because the snapshot is token-gated on
  sys/storage/raft/snapshot;
- the target must not cross into 2.7.x while a built-in seal "pkcs11" stanza is
  active. That stanza is REMOVED in 2.7.0, not deprecated, so the vault would
  start with no way to unseal at all.

The snapshot is the rollback plan, not a formality: OpenBao's upgrade guide
states that reverting the image alone does not roll back the data store. It is
streamed out with `exec -T ... cat` rather than `compose cp`, which emits a TAR
wrapper that will not restore; written 0600 to /var/backups/openbao; and checked
for being a non-empty valid gzip archive, with a failure treated as fatal.
SKIP_SNAPSHOT=1 exists and warns exactly what it costs.

A failed pull or start rolls the OPENBAO_TAG pin back and restarts the previous
version. BAO_TOKEN is deliberately never written to the conf file: a long-lived
root token sitting next to the vault it opens defeats the vault.

Verified: the 2.7-with-active-pkcs11 refusal and its three negative cases
(2.6.2 active, 3.x active, 2.7 commented); auto-unseal detection distinguishing
a commented stanza from a live one; set_env/env_get; the release-tag parse with
and without a leading v; and all four `run` policy branches, including that
auto refuses without auto-unseal and that an unknown policy dies.

Not verified without a live host: the snapshot, pull and recreate themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-22 09:21:11 -05:00
co-authored by Claude Opus 5
parent f8c839a163
commit 2d3d322b0e
4 changed files with 675 additions and 85 deletions
+37
View File
@@ -169,6 +169,43 @@ ssh -L 8200:<bind-addr>:8200 root@<host>
This is also the better way to do the first `operator init`: the unseal keys and
root token are shown in your browser instead of a root shell's scrollback.
## Updating
`update.sh` is installed alongside the stack. It is **not** run by `deploy.sh`,
because upgrading a live vault seals it — that is an operator's decision, not a
deploy step.
```bash
cd /srv/openbao
bash update.sh check # declared / running / latest + seal state
BAO_TOKEN=<token> bash update.sh update # snapshot, then upgrade
```
Unlike the copyparty and ergo updaters in this repo, this one **will not update
on a schedule by default**. Those services come back by themselves; OpenBao
comes back *sealed*, so an unattended 03:00 update would take the vault offline
until someone arrives with three unseal keys. `update.sh install` therefore
schedules a daily **check** (`UPDATE_POLICY=notify`). `UPDATE_POLICY=auto` opts
into unattended updates and is still refused unless a `seal` stanza is
configured — only auto-unseal makes the vault come back on its own.
Before it changes anything, `update` requires that the container is running, the
vault is **unsealed** (a sealed vault cannot produce a snapshot, so there would
be no rollback plan), and a `BAO_TOKEN` with `sys/storage/raft/snapshot` — the
root token works. It writes the snapshot to `/var/backups/openbao` at `0600`,
verifies it is a valid non-empty gzip archive, and refuses to continue if it is
not. **Copy it off the host**: it is the rollback plan, and OpenBao's upgrade
guide is explicit that reverting the image alone does not roll back the data
store. `SKIP_SNAPSHOT=1` exists and says loudly what you are giving up.
If the pull or the start fails, the `OPENBAO_TAG` pin is rolled back and the
previous version is started again — still sealed.
`update.sh` also refuses to cross into 2.7.x while an active built-in
`seal "pkcs11"` stanza is present: that stanza is *removed* in 2.7.0, not merely
deprecated, so the vault would come up with no way to unseal at all. Migrate to
the external `plugin "kms" "pkcs11"` first.
## Backup / DR
The vault is the sole recovery path for encrypted tapes — back it up:
+2 -2
View File
@@ -11,11 +11,11 @@ SCRIPT="$DIR/deploy.sh"
MARKER="__ARCHIVE_BELOW__"
[[ -f "$SCRIPT" ]] || { echo "deploy.sh not found at $SCRIPT" >&2; exit 1; }
for f in docker-compose.yml config.hcl gen-tls.sh .env.example; do
for f in docker-compose.yml config.hcl gen-tls.sh update.sh .env.example; do
[[ -f "$DIR/$f" ]] || { echo "Missing $DIR/$f" >&2; exit 1; }
done
PAYLOAD=$(tar -czf - -C "$DIR" docker-compose.yml config.hcl gen-tls.sh .env.example | base64)
PAYLOAD=$(tar -czf - -C "$DIR" docker-compose.yml config.hcl gen-tls.sh update.sh .env.example | base64)
TMP=$(mktemp)
trap 'rm -f "$TMP"' EXIT
+189 -83
View File
@@ -5,7 +5,7 @@
#
# What this does:
# 1. Installs docker + compose if missing.
# 2. Lays down docker-compose.yml, config.hcl, gen-tls.sh in $STACK_DIR.
# 2. Lays down docker-compose.yml, config.hcl, gen-tls.sh, update.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
@@ -16,7 +16,7 @@
#
# Idempotent: re-run to apply config changes / pull new images.
#
# Self-contained: docker-compose.yml, config.hcl, gen-tls.sh, .env.example are
# Self-contained: docker-compose.yml, config.hcl, gen-tls.sh, update.sh, .env.example are
# embedded as a base64 tar.gz at the bottom. Rebuild with build.sh after edits.
#
# Usage:
@@ -199,7 +199,7 @@ 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
for f in docker-compose.yml config.hcl gen-tls.sh update.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
@@ -318,6 +318,9 @@ 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"
# The updater. Deliberately NOT run by deploy.sh: upgrading a live vault seals
# it, so it is an operator-invoked step rather than part of a deploy.
install -m 0750 "$SCRIPT_DIR/update.sh" "$STACK_DIR/update.sh"
ENV_FILE="$STACK_DIR/.env"
set_env() { # <KEY> <value>: update KEY in .env, or append if absent
@@ -507,7 +510,9 @@ TLS: ${STACK_DIR}/tls/tls.crt (give this to the Kanrisha daemon as
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:
Manage (run these from ${STACK_DIR}):
bash update.sh check # declared/running/latest + seal state
BAO_TOKEN=<token> bash update.sh update # snapshot, then upgrade (comes back SEALED)
docker compose logs -f
docker compose pull && docker compose up -d # update
docker compose down # stop, keep the vault data
@@ -525,82 +530,183 @@ fi
# __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=
H4sIAAAAAAAAA+xb63LbRrL2bz7FBHIiMhF4kSwroUNXaIm2WZIplSgllVK8NEgMSaxwWwxAiuto
ax9in3Cf5HzdMwBBSt7s1kmyp04FdknkYKanp2/TX8/IjSa3MrEnURBHStZXgf/kV3+aeJ4/e/ak
+bzZOjps8e9ms8XteA73Dw+etPDz2SHeHO0/abYO0PxENH99Vh4+mUqdRIgnh0ejHyJ/IX+POf8P
PTviPJbhKycSkMPkVvzz7/8QcydxZShdkTqxtGU4SVZx6kWhuJUrdIsSKaZRIk6dMPHU3KlXdio7
4iqKIz+arURVOYG0z7qDPTE4F4lcyERJESfR3arWRkdRDGT6Yh6pVNj2PE1jZdsvRQR+xk7U/nq/
2UTnauik3kKKq7OhAAvpXIqFk/mp8FIl/WmNJ78Ofe9W8ssIPxK9GCU8GuApcBFH9Bac41sYiWPH
dVeiIc5kuqtETy+xLRzQUnKSyFSZhaYyCTywIBVzoCcVTugKJutM5pBThDXy5LzqMErpC0jF2dj3
JuACVEKZ1jUJJRxBVGzlzUjKE5mkourK2I9WdTUXM8g+4Rm9tCYgaOLquJt3jx0vEasoE24SxbTC
eiP1lajK+qwuFp5D7xIxDBzfV6mMMVLz1z1+16vVK5UQ6mnnUq5UFpGfBVK1KyJvs10nddqCnx2R
ONOUVzAjnlwWizOTbCiFMiDDGGr2MGGYkvBTWakomSy8yQZp+iiEF4BAwUIjf/f04/lFb/Cqez66
6r5p2/v15/X9ex4wicLU8SCV0Sbz+l0QQB1tQdNhmQhl4dSbdXKyDf3d/KrPJz4PSyT5PVSehb5U
ysay4li6Fb3oq7mEzabLSDiwgf6gd3lVLxyF5I6uIvARO4XK4jiCArFwL5H+ak+oiExg7oUzyM0Q
JEkVqxA0UIlABlGyYlv64HrKGftypGl2xNTxlfxA2l3zzZZj6JGLCl5BFguZJFFSJ6ZXzO+tjFP4
ir8SYzlxMsWOoV9hXEDrpVkNqQRsLIhZdArEMsp8F23wAShRkPeuhLZNMtViCRQA4EczfKsbQqeY
I8CgxHOYV83GdCqwuFsxXomT/rD76qw/eCOGP3Qv9sRy7k3mYm34biSVJjZx4hE8VJuLELboXxyP
zs6PT7kh870Ajpi/hRxJam1ht7iF1FG83BEDjh82uV73ol8XF+SUmM0EE7gsqGtxVXMDfNUfnNRI
kV6qA0ZakJN3tFG6NDwLoXB2CvbwqQNjryPoXfUQScSJ3lljPR05LpkJqJ0Mule7bkGQ9D9exY5S
8HjiiANif3BxfSWmMKklHJmNClEDnj2RbLuJN+GQrIMaSW694PMr6AXKA/PkpaED+1gKZzIhxS+9
dC7K6xRf6bjJIR6aCyCpglYVoWWUyL9kYGQETkewB2+6Gk18DwY/otBVIzsM/+psGuuejpGhl8Ia
PCULiqUAHgTS9cAoSd4hca67u6bPBO1jyXR2SUyguopCCdOJCorUhyMxxct62Z7klIiozeWmUfG9
e3JyCVIy1HouqQQmUS+Mz1pHJqLQtpt1/nfPexT/sLhzKZbqkfVS2Gl/OiK1k6g0BCJf96Uvpbcb
IbroRN+4iwwXXhKFAXSzdoCL0+PhTqslnCyN7CxUEkq+6A/YNtjsITxXi4GUw++t+HaiWi2r0O1a
ODKkSOVuqrsu3uaRpZqFCDluTe/ESygzlyQJ8O3w3QiTt8tCzdvse6uY5W0Xu3MA7Xl2GLmSGYVX
ubC/1OOIxn7pyl2zDRNT7MyIG3AQVS9IsaYv+qxtzMuJRrvRWM9PL+5LaswHHZ9dD696l//GwJZl
YmATsVuLGBKqzvE7na9qL8Q+2k0rUpwsNG+0W/MCQwlnQBRmR6CuhiKWufCUh9XVhQ1rsNWtF9va
DcvxXfi8+yI0B85KUOPDLEPLRM+NxAUx06wXyQa2whvr+N2JtScs2BT9on08U/TJNkLt5DJo7R+x
D7S02KjLFm/We0ObY+PC8dvioJlbUeoFMsow5WHegpQr8eA64sA08OY2QlbhRdjcWxj6306V/18+
axf+7eb4Bfz3/GD/2Rr/HaJfiwDgH/jv93jW+K+M/JBvGBD3H2HAwQOoVgSlKpLFDSxI+8cUe1Va
2yvDC4IbBMIMyqhOfImIqEInVvMotccO5QYnlzVKdyk06kxCJ07D87NehRLaScRpa+wg1SEuDfsG
0Speqh8pk/SuE3tso6JLKQIaTRDeWae0GIGxOiPepFgXrwg7I4lANl5lxJRzrGqcCBkwyajQbMEQ
pGokEYHF6BZ7L+IhsmUxpu5VJXVQv+x1T971kKIhUUmycBxFt4ThcvFYNJclPiJm8mLxdIS1kRfQ
zkQbzMhz6V2eQGDHuq8QbD8b2jnGJXnkCqsLxPP6JEkpA6nfGvhQ3lDGq1KqBXUjV1WkOrC5x6j1
38WsnlKZpvcAuYJcgV1Za5tyYSuQ63qC6wDLwPASOLXSKQIAMYjcrA24biTwvj5xRnpTLIzUSiex
lqbZ7/RWRHIzKV+RJVBeTKNHUw95R1noeNMwsss7Qny636Md8ZY6kieWUnBersm8oX+z4TtCZ966
aAD/CVgcRSGlzYT0rP86bQcrkJMs9c9fOobXrWebdd3dnjh6pWxNp1LGZEW3ZShYQECow6Xk/QEU
LFc/CMqw6mp7FY1l0J0B3hqBM2TdRuBtKs6IaJmnpTA6BJsE8CChzMI4H9JzxZ6T05o7qkD0OZan
mGGmvED8UdLMaEJaSIR+sjYw+09Wjtq1VtiUDSPk0aazWEcjoZYO+QJsz3HrFkgOEYIiXjLMD6re
LArQvBQgpMcVLmRUmRS2/anaQWVHV5oeqxTk8Frc5Lj6PTNpgHWOqKl8sQOWtivEHAqoCpAKX05T
8lCk27QA6N6XOmGnyLRO2MkoipyRGJvSavL01UFsxVceR0z7S2elQK97fNX/vseshRR3oQYXFj0B
i9r6EEK4tAFBh2jGOhPHm80ppBbg3rgywuaOeA3xr5FF7Os5FbY7mW7gBHjcNgIgmowyNergjeYR
3KFrjlTtYXkRJCuUXHFijwSf5A71iXS6MvERwWSi+36qX4tshuLfOPP81Ma8SzkW1/26GGZjlXpp
lm6HaTbNHL1Qz35KfCuq1IZyWTHljSyRbVP64Kqaq7dz+oq0oNgjNMYHItfFrrU40JsXUtHg3PEJ
Ya7IuHn31N3TeRJlszkLiwszV1kSctGMilc093LukIWlVK2inSCmsig0uC4TNMxeSnz40czkFAEM
apxES0Usqqi0nfOmS+ZLKcacYoXRlumea4r8zMkrimoufR8wU02SyPfH2OnrlczbiIzfXfe/Iwf4
5z/+jv+iW0La1bywUuOIftJ73b0+uxL9oXjXHVx3z8T1YNjDLz30f/8fXJz0Li57x92r3kmbF/fq
un92ZQPxa1CvEf5Y+tFSF7F1MIUk8pi42K/Dzli9FV2xAUSncASMNs44DaRY7imqB3qh2b3RL6w/
Wm3Iu5HTUIWaKMo7Korj5em7oYj9bIb5qx/MB+s2UNa6CPHx/kMtL4HxKUUKywAtwsxUJYc7K/yg
CD2WU860nFsyJVrHHTn+D1T1QgrK7JiaRungI4GNkRZ6JybYEN0P2OnWRqeX8kFU7w5qIIjoBtPQ
yaCpJcOCo41lO1QdBWNOIRSIscHZ3h7oUeWatnItZey5vvYxD2+DKAv5FKEYGkRu5lMm4EqqqlPO
HG2WlbU7IpKB2lZphVMuRKI2p+liq8bzkdsE/Hpc2uszlTTQ0lDRNJ2rgD6bj/t1FVlmjPKRwBZJ
Ut6KJGBNieZtNErc5L0oLfIdGCL1ujVJnBFe3ieQiCmhpwKmf9dqfn1klbOSHXF8+m7U7Q1Hb47f
8Zj7PwoF/8EzkyFVbrA5/HZzEMg/Ojz8FP4/aB7p89/m84ODowPG/3Qk/Af+/x2enc/Y0cde2KBM
Bfh6zjFibRe8beVnkltFRcIpDCa+4pSf8uYyoC4SBUpGp4JyPH2aUqQEMWInYmBdDDi/o1RymXh0
9invMJhiOIERpffxSNc1N45D9enpLwHJ0hGo+DNtFgxEDUwTX9EuZ2BuEVdhFjNkhUg4i6yA4iuH
aCSuEZAE127tKNbVj2vFx5osw7L8biCl0Un/8n0es6rmYEKjYNpNegiR9KY4AMWIYXcwFOZ8U+AL
S3NP8Eqtk8GwzbWPuu+Ee/2LdksD1FaTv+RpYh5Ft2mfdH8k2sAQnuulXItxkXKvOft6/7A44Q2c
uzw/UnyKFKc1Ot1NhS2zCLE+xiDPr1Swxo719GOrbfPC7q0KraFTqvTn62rbtABgDMcn1LrBM4YR
e9vDqK1tgy+8rwS3pBY7FtZTTIoGWNfNjbCnuqHA3+KLLzYbCWuL9+9fcLIC7IvdJRLWjdHWezbo
ArhuGSlJiQjp8pF0FiZZdQi81AnCw2ZT0axMvUrFHEsLe8FnNkr54mUDW3cjzLDP77/8oiV+/ll8
fMBA3pmcZIokwLXEyy/2X2jSrReCYXbv+GTYFRf2/uHzhzV+hgRkL5wUeSGwhJsf5OYYZaNmAIJV
YvX4rF9jB0sjc55WnNVxQQffIHSSEFcTvInDmSDpHvAvZzyRfxH23WHzG2EDUZBHyQk0hQ9IhPF5
BFjuBFjvaJIhvW7HiRdILGTREgwclfgJkrSpf5Zua87ebGMV22y5aISBWHqwysZ/RgZzPOiYggV6
YelIOYVF7wAVu346AJbpPP1I9kg2NZkjxRLPm82tSfMXz55tzVypbCvvjYmS7oZW8mjIkcrYkPVg
rCCdqbYwDJW8k9pocffuw1FvqN5a5ipHvts1MUc9NuW/qoshTukD8HI5LY/oNdj7f3vr+uP5FZ4s
dmGwv2n69wv537P9g+f7+fnPUevZEeV/raP9P/K/3+P5VP5X2AWVGPWXjdSuQJ11cRwFMSAalYii
da1pj7MmZGCS0C3tkTPluZoIg3hzbfBtfyiuL066V71LKoqc9M76r3qX+Hr2o7genPVPe+jTE+f4
cTkUgLI84rJ3cV4HE/EK20m6MhhaJjPiLJCCqjSmJhEgFi8oV2PE7hR3m+qltVAux0N0EaDNt2RM
6SNPiYZzJ8D+R+B0j8kw6De1gnSeyM0iU7qKdVGFLq2IeRZwDSsvTxSXYLjGpNZFNK4Z1EVXKLpZ
mNGFASN9B6nFQRvbE1/R0swlRbWjTDOaTrlYnIWp5yNxDiRl3mmWhIoOqPgGEPUmPrUWhnrTWM/J
p0mD3vdQCuHvmbmalGQhl+io4ksK5+Iii4dLiXTNMkmJplHp6OL8rH/8Y4eqInTDSBc3KNlAPsKV
N756g2VkdLam7+Jt1FA8qhnpOnqW0O0Ju6ga890VJmHODPL1ry2AClX6UECvk8qdPiEJCIyy942j
ukI5UTbRujBKyW+qhab0Hk3NYYK+a6r1g9zYmAJV8Qvj2iWRzxIHlj/LPF3flnex701oZ6UCJ5+K
EtIx9sa3I3VSqRdGBwJUedRLYpOkg0o+UjT1Tae0Ci8/6sirlSImjKBhF0E0wCJkFTpP3BhFhSl7
xvkLqWjihCFLhcVF1mtqq+Yii8kMx5ACiUTLSFeJJ4mHZI+vmLDJ8UEvpM3yArtMDHkjSXnu+NOl
s8rrwcYgs7HJoJW+M8y0tvBTDcY38R0yi4Va2yayFLo9m+4V1b5UaqNbziVxUAJF+uE7N06KzMZd
+5u+CYnVK2eKRBm+hlBnfMHc76wzJdOfH5czpjBatgk1wA3pMMJ+uZYzPrtUd8ZvKpLRLwIEdCko
ZmpYRsEXF8DLjgn3WUBL6oWIxhRkNryMh5ugq2tz+ShtMx7c5QPIf9Bch+uupdOtfAxr4SKCna5E
dWMac4kbIqADybI29BmNjhePhYsXJkCw5zIRjgtiLUO+hObQUQQf9uizN64EKPZ8ZP/nA2wMgHsP
gkTpWceLOld+CTV4sAdefR6l9LFctKFNDcUBhyhI8LU4NvmGTCf52adtdkaa40X5wh3iaqiMcIZX
3eNTwvydhkoW+VDN3GZk1GLkQYx0z097g4548JizXC5ufNAcfNDF/U94sGZj0L0Yvj2/0pwsHOzz
iAdZrHKOuNdV9/JN72qEWD/sn29NTjYKh4/lhABfEfdLxyTa15jQyeWPo8vrQaf5kH94g0ciIoPW
jjV34pjq4MZcjA4026f9i1HO+xa1k+7gTe/y/HrYLuLq0tT2N+IdE3rzdkSJQo4C16suqjXcjQs2
64SHQ82WRr6lixovt3vqT4933ZIq32b/xPjHainD3tnrjvW0OnGBOquul9DNd7o0+ao7fDsanl9f
Hvdumu/vrRoXOeKlW2s8rdL1mE93rFQ2rl0aQzw+H7xudz5l4kDGpraSYOwjQzfKKVSP7225xPqv
I4heW2/btGI2hq16INr1JUXsJghc/dfDjvZDTD/ipObnn5mbENxwg57fza/ICnpbvOr8Tfzp5qat
Ymci2+/ff7lDw+2/khS4R6NRetu4J1okzfy8qqA5uu3kQz7/vPPlvfVCjBZF086XHW7RnW63iZY5
03zf8kRg5cFEeZePn41uv7q7z/ux90ypmKRH736udunjQhPnWxXffkI/XI5ixRdRqb0RlsAhv94I
TG0TmfKXhX23O3lTObq0Hw0vec9NX1hTMAGj3WkWNMuuX2o3ntzedmUq3PQG349e9896kH6xwgad
dlkVEkD/zejt8dnGy/WlRwz3o1m1JsTHXMi7PzUPDm5aLw72g5uv3vOXZiA+Vz+FJPIvLarBLZ0k
xKCHYw6Cm88eGaNrePcV15OPz9UKbu4+OW6j9qevYxTXCxQwEN1Ci9bAikrWnBeWb5/vmStXtOmR
bAi/ENytV9wJr6QqONQUQuKwsjWZ9fQ7S9SYETjqCGR4KJz+29Pejy/Ft3wp5WU7387RmJ886ith
FPJd3r3HVFE1EeN7vsoy00n8+lIA9Nq/PB+86w2u9kxuwXeJ46j4ows+EaZym044bdtQpGti3zw7
mnz9zTeiIVpfH06fNZ/x8cRyvtIBhovPBIFgGS2Lim34sG+JNIj1Hylx1PtMl49zE9sKdsXGNiWP
7OR641Kl9RQkocCX5eHFqEQSHBNNboCD0i/MTBE/uE1lENd039GwN4IUO4Yoff2+S8bMxJ3lrdgt
aL7qvQE8/ijoDpCR3Y1lCFhge7HdDkrUDmGFeNUU9wWpz7gNBuAhI76rPm3ugarVsWqi0xGt3H51
m1gUJGCjIRVZ14TynuuW3uAErZBulSepbZIyHXc3ZE4ihHAs86dBqfkqHoo2CbS+uDesFIY3msl0
00qRa+vLU5zbsTNUd3fXVlmr5JF4W/W6ZJ9rfPdFocYXhm+yRYrfqvGnp61OoxFbGxR+BpICIrfD
lr4uZ/96D2GmVCcjvybVSo6wRib1Y1EasYrS3+txUDD5frmvcUi+S6H/nuCDFqAS1vcmmxSCsyML
yXqU3KocXxaXLpDG52SgALq1+D/tXXtXG8cV/38/xUaYIwmjFeC6SUXkHAo45sSxfQDXbY1jL9IC
G0taRbsyVrC+e+e+5qUV4JokTc/O6cNIo9nZmTv3+bt3IDPOeJME7aTG6vBDkGfm4RZZFDTIuonf
cTrLCgCFAMwW5+CrJzRGwbGSHthGeVpw+kq/p8aEUMaxJOGE5p3CGxIkFjM3tqxo0Ik+HJ/oSLd/
4qVp6wN0T+TAvB7yIhAN2Rkn61aeybrxKSWASuwqLg56N2anJoNBFEC/t2gd640ipjjpdTd+i3c2
71kSClMPvfdd4HJUYAOTHp5kkwLKRObNOaWEurQPLFTNe6zm94uetb3avHrMYEXhSvtywPlMb8rX
MB6JQUVzY0AZKr5Qv7qK8Lzh/07z+bzOg3iPwpjLdPR+pIx+paor9ilG8Zs3tIE7I4MFUlT/jrA5
URTVwqt3glUaJvHIdnHRjuecjywOKrSDMZkPdGy1ZP0UQ3RTxcUn3KfU1wForuQsGVGeUQC29Vt6
xFtjS+sVF9ZotKvStTtXhrbahP2w7ujgMKr19/1a3R0KVoWMyhI+8iwLf/4F3Hro/1K6zs5gDFYB
xDG3CfHLwc1zBNSjbZgLA+lNJ4r7nuVHTxWTHMYfW5CHFG5tGIBlPE6jc/Wb6amyfoZtcBXkba1+
ztvsk8jbNMPa0nPMkqCet6O1mpoNZi7X7HXo2H/UPpxcbaxvnsxPGq9/qr1ZO2nWorX2yWZ7XHeG
vUBbaLTp8oC75Pgr4QvtuLqvvQt3LVY84KoNTFS0eLj/4/N/7O+B7ohIRMgeKdQZgPxqiDBMkh5o
gE3h6CD1bkIoRuHuJMs55wNwpUA22hXOxyzuQQILwF9S9MrzacMfsDtUXE6mEIDxk+rMv9NZeIFe
bnQYRwG6FN7SWwLO/uxMqyQEM2kxqT+yWBp9Q+rpMP45m7wdpqNsgj2sv8HopK6r0dpclCTU1q1e
NclFD1Hcfv1pK/pG/fdvnx5Ea81we1t/p/4SrUY+TvK4Z+nEZWdfqYq3Pe6MOvROvadVK0MprFmp
74DSByQx7o+hG9yKEx7yRGerMlUIhEdRgizRHNzecWHSWhdprdWy+Cz5qijSI34mdFZnvNlR+GOK
qUtigmlca52xrCcAZlVzM9OsU65MVBPtXx1icLG/ldO2jDhAdQWBwtEO7fqDaM0SwlFTH47VmcnJ
2MI/UUTClFvT8P7qv1aHq/3j1SerP64e/ZulIvTveib+XLuH1Plr6QVV/8JB5xFMR9nThlCUNWmb
8yj9apslNhSY02HNdfttUuARXHxwZnHxoZ6I7eSLwkMJnpSHTuBcLgudRMYa0xr9UvvM0RW0L0TE
HlKr/pRA1eSvJR+tG2byQi2NfJa3OaGrDT3b0pPTm6wcsUvQjyNvkdmLsnx5CfehunWcOMS9K9jm
OToZSsxZOKDQYbnRSrPgKEJLyaVhuPE14mdsuqkxZZ6HtWNCZftxN3VyeC6o9ch0VsJXAAW0VX0L
64yBP8Xuk5hSoA53XiFUEMR+FL4Tl0Vv/M5o+xBJm1IeDsRzMMchwYyXGPy14eUEPBQT4veXAMhG
AkqIXnyVuJVYXl+HKkTtNJLbgZK7b5/HiqN9ti7dViZuGw4ie8Hx9JVr2EKgR/JE8DBTKBUFmpwx
CG8lfEyI3IDrLqVOPj8lNgKY6OXzeyQ0pSe1q40Say/12oAKtzDNpc8l63/JkxctDk5/g9giALw2
CPpFJM8Es+Oc2jg8/1XtAFEJZH5OItDek+G4mCFATqn2qJgQIWJlDq24S7UVm38ZJoUxLgrmAjgB
YYW52qgCzDETNlUHIB8mRdqLNEPK9ZJavEhvtRqKjxYGnnGmSxZUsRMLtAhv6i+ax1awS6vQz9+6
jubciVAsGgFuNEw86V2AZF8yN/aQIRPRQz7/IWzca1wqEuih8xtn0VS6l9LMmwBLGc9gHZ8/fkxG
P9gIFnsp43S/hVfGRLLv3DeTvUXF0jHMPCeuRRTPpMYX7IXuArnGhQlYNiPh16BJ6Pi6GK1k9lBM
HUOwrC5QN1QsPIcRaxT0Q+jg2nZNI8wWTHyP3vhTGMNzNDWNjMKZQRfj3eBvwQuyONbCLyE67xBd
uTEMGi98052pE6xWGP89ygKHtMzKhA30M1oOs2ZH+/mvpFen9S08qHjEarwZR9Y/bBgHTJNrdJlx
uJcaBuUW/bU4GG+iGmwVB5Hfs5ULPssr6qNGGmTZe0BwoMx45NoXuGSOZbHRdB9lEBgyWatR5tv+
HjzdskC2PmcMztpqEPbmAWKamt6AsKXN2w5oSlxZa+gNuPY5M2Tnj71z4OxapWnqRfRtLTO6l0nH
BNhx9x46+fs8RvhGyZTsHzpRP0ezxDip+DmW6OxGfBvE+Pdp8WR6KvCNHJI7t8VIzpXtxCgc9ESR
+4kQHtH14RFRd+WwkL67fIIoLF6OEY8Fjrob9FjuTlC7D4raQU3shPbh/G5OajM9cl5zpoUbAFNS
FuKSxdrRoEHULSGFwIbtMSm7qD1wCk5K8IXW69Dgh1OE0qG/Gyot9FC9QFW248ATULVjKIOy8Paf
Pp7zPtj2qJIt9KEtXO7tvzzYU3rvL+GGLVjg0UpbATslMl7VOxBEliSiUgcZ6mM0TfXBNfJGcePF
wHNrQfrMXS8wbKE1UUPd/YSKZEACFHsVBAvTYHp3GKXSP45UHw8E8jGaRb/65hu8CNMyPb6Mjnck
FSUdjSjcqwzwbL6tKw0S6vAGGl90Q9FD+Vtf/up1OMb6cKzca/7Iehx3jiD+NEHNFb0bHYgnW5iD
eUk8WS1Xq28r2xawUDjGeJL1pz1MPGPFT3K6JxZGz4L/8GjkH9iNC6wLR8q4rUkTHNity4i1ByRX
/XJkB4qRS29fo15cJxJd4aaXVBevoZMf2eaGB74sh15euw68Ai+JZ+CesNWsbCO1aVHNE2keySPs
RheEI43Pk2Ho+imSeRMcMWfTnAlRgFmng3TUj1zR5vFa+AUjZYHXwlsaPltkfDxhVWDRHScZkW7z
C5whHjXClm8jMslS07p8zry+ANpc+JDJ+dojuBLuUkWji2mBQFAGHUMxkLHjx1oHnoqlCxz8cJy/
zyOzikdQt1R+R1y1YbkvQc6cJkI2lxpvjWDwJrhbxJiGyQQcxEZoh70KNptQK/0V/ICWoETMvQAD
MDujDWR+uI1kKfMEVCF55DK96aL5WjxMuDJxSPIyl08Oexh9r0db4Rj5+AVQuMj402xSQFgATuJ4
knxIM0XUGuCYk9NXQ/K3jePf2KTOetAjWy0C07ayyVixmbzc0ylD/0mW6ODM850ipwRHErIhdpCp
ecLZ7LRsF848HDObpZJN9dwqZeWtJBN0DA+lpYkM5uPbb/efPw6C7he2gPTdvbCM2wRBKVt+xjIW
va+sn4GKpSmiA/P3xF0Q+uyB3FXkNISCMst8fdp7SN68kioUi+5AbivhxwfrrCz2lXhXsgGwNUpf
xFcbcRmpzp1OTuAAS+YUmskJ4AMXe11pSMiVsF5REByy3FqHE0UHY5jmp8mF0pLzjkmTuGWSRGB5
+pV00gXYQss1aLnDW15pMZdOg1B37WgyV0b2iMXzo/mXEyaQNyne6qlWyPvVsiQAYl2XF5n6aJwB
SAQTUyA9fRKPEA5AnmxT3BZTaSDlCAv1dcrBAFj7ZhiPpvFgMGvpmCaHQjFzJhXonptqxHlOIVTc
+iBJ+4LyTQQGLBlDttrkWqG2+kQw1GZgU5I4vpwP/aglNDDK3J8ity536Xis+tqHGV5ekugEaQpe
rRm1AOZBuiiRF2Oc6Ro0OlXNiTyeZk7FBPSTkg5lSQR/OZwv2JSW5phWbsfrrStpn2dlWXsg7gWw
eJQIutkKunE3bve+56UbhmWsnHyS0CYLWeUykeGtvDabbyTNNY+kUe7aU6P8faW81ekI1CHEUIdp
1sPGudr8ugfZntebnrKt2AlW00Ai1yzFj9g9fOimGJSByJtS4NiJcvaTQXqKGe+DGXJgeFoBgSOM
rmFZt/PWIIXyUlYUM1edINeDRsQME5ggRU0QQFrY5wPqgUFxAYQoxQUpxRbD437CTwqMMpVmKpAO
sRKW5DmABECL53RmEjS2KaK/JKEBq9gBXMpk+zgKgEto/mYFTnKOF2oPJHPFQI8Cb+0BCj2iymJq
8Qm5GoUnnBx08o6zEUFESVkWEWEAlMohGpEWUv/MfkfqHqNlH5UlcprEU8SZoVqkRkMuZR8jKKUa
y55Px+NBClBtSM6kxMnLeBah2MONs4NwpXkMJByZev8rt5Q5DI7N6Akf9OFhclu5L+aI5DBFsimt
bvfJ/u4PYYMcrpw5gdFlCiLfkBcnMRcnQrHsUZAfKA/yaarphst04g4l9+QtyahzADth/aeDvW6M
OLq639V7/xLWgT+gMt1pr41z1N3pMJb08DKN5GB+hXno+UWA2ih6KGv3wENZg9fWpGLI5f7HW4xu
Zu9EOCc9efr1YU5k4LovVObsqZPS13nh10WWrd/zbSj8Y5LrN/3UEl9IDgeSUX+Lty6jKWs/8lle
JMM+/7//W5ks7cvrl6O0eBPsJZRmoSi2u1AGQLKggx3QXbpkXcg4QfD6iP71JjiejZOu4kqIKtxX
+4wGX3ehCkGIG7+477d9BYB2Tpa/wB4eJnkN3lzSLILXx/DbN8Hz0a7SfEf9WL0P0vULfcdNF/fp
UJFSNkx/Tfp7ySCeHSW97uaF+j3v05vgVQyY3r/PujibPCLXsfNCNPteMeBKLXDysrhf8j1dvKAE
1Si79EUYve1N9OQRUWHcuu44vLJYpn8S+WGB0ZewYMZo3O7QugfWrMT1B9ZaUS66+wVLZs33NvR2
+7N1EwFcOzMHDyFmIRfyjMLSFM851uoFeT8exD1gD4CqJ+sLSnYh7c+16YV/YphTq9yiWtJ7NPkr
Pjr8nTquTX4vMmTlC6aZJn0hckRGHOmvbQKTr8U3zQXWnDTXjZqiQpzeJ5rJJ/XQTzzAJz3UHBVj
VIr/6Ior/1sNYAlR8jEejge/Wembm+7/+8uDLf/+h4d/fVjV//k92grhpJRWjwAVYLVYiRaK0Pgx
jxSqeWC36RgV/aEyiyFNagaFMEyV6RwtD+zIFerTXN8QACxiGxxPusAjdhQPEVf8Z0NS6kKCFEgL
NvzGU8r/RNI1VZeh8PIhlaBOocDJnVVU/t3+A8lAXKCvAVeSTcK9Z0chMLom1XTbeXZ4cPRkx7o3
EcvmWPX69JUY2YhTgZ7uPINYNBUpcm9wwCpvUCmQakHzpYaYlg/ubTSxl12eIJUEVd/GdTcqNaPA
/rCr61TCxh3ItWm0s/qmtJ0XB3RbmlIIqaIOXyeCEL7wKQInTgfx6H0bxAQVfzL0RxeeYWHo0su+
xCWJ13xRxA+AwFyVB4oKQWEMr/QF3RMXhY2d0LvS7abL2+TiCt4QvtIDP8zYX5vS0hdYbTMdmfvk
7LveomZgX0PW5Ssp3BOAN0f80YR8t2di/5/HhztYHdF4MjwqXlckPAO7ytns+4DJGGNAoIHJbw6V
0C0DUiAzlxtl6FrPvrlbw0pDB2qnAVA1hrK2VJYWrEI4p1jbKSMUBAd2xda3CqNBCVcsGuKXRe0u
Leu6STaRHgRqdppXgRtvJnzAz1JIPpF6sRQO4Zq2qTBlwKAnGBxSimAzPAZyp3sYvLq6UueF6uua
grqmmu66V0lX6uiCdwmeaqoOcOJtqIbPABG5eJ2LYRV6QQK/BGz3m62HLsW/wksP/h+IXr3WJubv
ThgX5l3sIFzQuYTB1F9ax1xg4J3glgMKe4Yw61FyaV3roLOCJamNZQeF95Ze12BV7sfLGl5J5hxN
SxBn5moGulCEwSfunQzgK8Rc1PdJMmaMg1U5jy9CWrh/QdFPkU1mhkpeHnQ3XVo4wLAgJH/+4Zv5
JVTwAu6xwENK6CfIyuWzn1uHZOd7KivkrkHp5RPb2l2lTiBfPEF9mnezVHDnADiD4erHG++SLL9D
kqoQF3TbBFzVgzcetOW+g5wuRZDCHw5ySzHv5wXfNsm4SlQPLCKEATmmKZc3ANnL3Q2Bd2NCt7IT
q1a1qlWtalWrWtWqVrWqVa1qVata1apWtapVrWpVq1rVqla1qlWtalWrWtWq9ido/wEUjQw/AKAA
AA==
+447
View File
@@ -0,0 +1,447 @@
#!/usr/bin/env bash
#
# update.sh -- update the OpenBao container. Companion to deploy.sh, installed
# alongside the stack.
#
# THIS UPDATER IS DELIBERATELY UNLIKE THE OTHERS IN THIS REPO. copyparty and
# ergo come back by themselves after a recreate. OpenBao comes back SEALED: with
# the default Shamir seal, a restart needs three unseal keys typed in by a human
# before the vault serves anything again. A scheduled update at 03:00 would
# therefore take the vault offline until someone turns up with the keys.
#
# So the scheduled path NEVER changes the running version by default. It reports.
# UPDATE_POLICY=auto opts in, and even then it refuses unless auto-unseal is
# configured -- because only then does the vault come back on its own.
#
# It also takes a raft snapshot before touching anything, which neither of the
# other updaters needs to do. OpenBao's upgrade guide is explicit that reverting
# the image alone does NOT roll back the data store, so that snapshot is the
# rollback plan, not a formality. The snapshot is token-gated and cannot be taken
# from a sealed vault, both of which this script checks up front rather than
# failing halfway through.
#
# Subcommands:
# check (default) declared vs running vs latest, seal state, and whether
# an unattended update would be safe here. Changes nothing.
# update do it now: preflight -> snapshot -> down -> pin -> pull -> up
# run what the schedule invokes; obeys UPDATE_POLICY
# install schedule the daily `run`
# uninstall remove the schedule
#
# Policy (UPDATE_POLICY):
# notify (default) never change the running version; report only
# auto update when a newer release exists -- but ONLY if auto-unseal is
# configured. Without it, `run` reports and does nothing.
#
# Env (also read from /etc/openbao-update.conf; environment wins):
# STACK_DIR=/srv/openbao UPDATE_POLICY=notify
# BAO_TOKEN= required by `update`: the snapshot is token-gated
# SNAPSHOT_DIR=/var/backups/openbao
# TARGET_VERSION= pin a specific version instead of latest
# DRY_RUN=0 print what would happen, change nothing
# SKIP_SNAPSHOT=0 DANGEROUS: upgrade with no rollback plan
# GH_REPO=openbao/openbao
#
# Usage:
# bash update.sh check
# BAO_TOKEN=<root> bash update.sh update
# BAO_TOKEN=<root> TARGET_VERSION=2.6.2 bash update.sh update
set -euo pipefail
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
: "${OPENBAO_UPDATE_CONF:=/etc/openbao-update.conf}"
if [[ -r "$OPENBAO_UPDATE_CONF" ]]; then
# Environment wins over the conf: only set what is not already set.
while IFS= read -r _line || [[ -n "$_line" ]]; do
[[ "$_line" =~ ^[[:space:]]*# || -z "${_line//[[:space:]]/}" ]] && continue
_k="${_line%%=*}"; _v="${_line#*=}"; _k="${_k//[[:space:]]/}"
[[ -n "$_k" ]] || continue
[[ -n "${!_k+x}" ]] || printf -v "$_k" '%s' "$_v"
done < "$OPENBAO_UPDATE_CONF"
fi
: "${STACK_DIR:=/srv/openbao}"
: "${UPDATE_POLICY:=notify}"
: "${BAO_TOKEN:=}"
: "${SNAPSHOT_DIR:=/var/backups/openbao}"
: "${TARGET_VERSION:=}"
: "${DRY_RUN:=0}"
: "${SKIP_SNAPSHOT:=0}"
: "${GH_REPO:=openbao/openbao}"
ENV_FILE="$STACK_DIR/.env"
CONFIG_HCL="$STACK_DIR/config.hcl"
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; }
# docker compose scoped to the stack dir, so ./config.hcl, ./tls and .env resolve.
dc() { ( cd "$STACK_DIR" && docker compose "$@" ); }
set_env() { # <KEY> <value>: update KEY in .env, or append if absent
# Value goes through the ENVIRONMENT, never interpolated into a sed script --
# see 947c899 / 185f404 for why.
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"
rm -f "$tmp"
}
env_get() { # <KEY> -> value from .env ('' if absent)
[[ -f "$ENV_FILE" ]] || { printf ''; return 0; }
sed -n "s/^$1=//p" "$ENV_FILE" | tail -n1
}
# ---------------------------------------------------------------------------
# State
# ---------------------------------------------------------------------------
declared_version() { env_get OPENBAO_TAG; }
running_version() {
# `bao status` prints "Version 2.6.2". Works sealed or unsealed.
# `|| true` inside the substitution: status exits 2 when sealed, and this is
# a bare assignment at every call site.
dc exec -T openbao bao status -address=https://127.0.0.1:8200 -tls-skip-verify 2>/dev/null \
| awk '/^Version/ { print $2; exit }' || true
}
# 0 = unsealed, 2 = sealed, anything else = could not tell.
seal_state() {
local rc=0
dc exec -T openbao bao status -address=https://127.0.0.1:8200 -tls-skip-verify \
>/dev/null 2>&1 || rc=$?
printf '%s' "$rc"
}
container_running() {
local id
id="$(dc ps -q openbao 2>/dev/null || true)"
[[ -n "$id" ]] || return 1
[[ "$(docker inspect -f '{{.State.Status}}' "$id" 2>/dev/null || echo unknown)" == running ]]
}
# An uncommented `seal "..." {` stanza means the vault unseals itself, which is
# the only condition under which an unattended update is defensible.
auto_unseal_configured() {
[[ -f "$CONFIG_HCL" ]] || return 1
grep -qE '^[[:space:]]*seal[[:space:]]+"' "$CONFIG_HCL"
}
latest_version() {
# No jq on a stock Alpine host; parse the tag out with sed.
curl -fsSL --max-time 20 "https://api.github.com/repos/${GH_REPO}/releases/latest" 2>/dev/null \
| sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v\{0,1\}\([^"]*\)".*/\1/p' \
| head -n1 || true
}
# ---------------------------------------------------------------------------
# Preflight + snapshot
# ---------------------------------------------------------------------------
# The built-in pkcs11 seal is REMOVED in 2.7.0 (not merely deprecated), and the
# HSM distribution is discontinued. Crossing that line with the stanza active
# gives a vault that cannot unseal itself and cannot be unsealed by hand either.
check_pkcs11_cliff() { # <target-version>
local target="$1" major_minor
major_minor="${target%.*}"
case "$major_minor" in
2.7|2.8|2.9|3.*) ;;
*) return 0 ;;
esac
if [[ -f "$CONFIG_HCL" ]] && grep -qE '^[[:space:]]*seal[[:space:]]+"pkcs11"' "$CONFIG_HCL"; then
die "config.hcl has an active built-in seal \"pkcs11\" stanza and the target is ${target}. That stanza is REMOVED in 2.7.0 -- the vault would start with no way to unseal. Migrate to the external 'plugin \"kms\" \"pkcs11\"' first."
fi
}
take_snapshot() { # <target-version> -> echoes the snapshot path
local target="$1" stamp dest
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
dest="${SNAPSHOT_DIR}/openbao-pre-${target}-${stamp}.snap"
if [[ "$SKIP_SNAPSHOT" == "1" ]]; then
warn "SKIP_SNAPSHOT=1 -- upgrading with NO rollback plan. Reverting the image alone does not roll back the data store."
printf ''
return 0
fi
[[ -n "$BAO_TOKEN" ]] || die "BAO_TOKEN is required: the raft snapshot is token-gated (sys/storage/raft/snapshot). The root token works."
if [[ "$DRY_RUN" == "1" ]]; then
echo "DRY: snapshot -> ${dest}" >&2
printf '%s' "$dest"
return 0
fi
install -d -m 0700 "$SNAPSHOT_DIR"
log "Taking a raft snapshot to ${dest}..." >&2
# Write inside the container, then stream the RAW file out. `compose cp` is
# NOT usable here: it emits a TAR wrapper that will not restore.
dc exec -T -e BAO_TOKEN="$BAO_TOKEN" openbao \
bao operator raft snapshot save -address=https://127.0.0.1:8200 -tls-skip-verify /tmp/pre-upgrade.snap \
>/dev/null || die "Snapshot failed -- not upgrading. Check the token has sys/storage/raft/snapshot."
dc exec -T openbao cat /tmp/pre-upgrade.snap > "$dest" || die "Could not stream the snapshot out -- not upgrading."
dc exec -T openbao rm -f /tmp/pre-upgrade.snap >/dev/null 2>&1 || true
chmod 0600 "$dest"
# A snapshot is a gzip-wrapped tar. An empty or truncated file here means the
# rollback plan does not exist, so treat it as fatal rather than cosmetic.
[[ -s "$dest" ]] || die "Snapshot at ${dest} is empty -- not upgrading."
if command -v gzip >/dev/null 2>&1; then
gzip -t "$dest" 2>/dev/null || die "Snapshot at ${dest} is not a valid gzip archive -- not upgrading."
fi
log "Snapshot OK ($(wc -c < "$dest") bytes). Copy it OFF this host." >&2
printf '%s' "$dest"
}
# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------
do_check() {
[[ -d "$STACK_DIR" ]] || die "No stack at $STACK_DIR (set STACK_DIR)."
local declared running latest state auto
declared="$(declared_version)"
latest="$(latest_version)"
if container_running; then
running="$(running_version)"
state="$(seal_state)"
else
running=""
state="down"
fi
auto_unseal_configured && auto=yes || auto=no
printf ' declared (.env OPENBAO_TAG): %s\n' "${declared:-<unset>}"
printf ' running (bao status): %s\n' "${running:-<not running>}"
printf ' latest (%s): %s\n' "$GH_REPO" "${latest:-<lookup failed>}"
case "$state" in
0) printf ' seal state: UNSEALED\n' ;;
2) printf ' seal state: SEALED (needs 3 keys)\n' ;;
down) printf ' seal state: container not running\n' ;;
*) printf ' seal state: unknown (bao status rc=%s)\n' "$state" ;;
esac
printf ' auto-unseal configured: %s\n' "$auto"
printf ' policy: %s\n' "$UPDATE_POLICY"
if [[ -z "$latest" ]]; then
warn "Could not reach the GitHub releases API; cannot say whether an update exists."
return 0
fi
if [[ "$declared" == "$latest" ]]; then
log "Up to date."
return 0
fi
log "Update available: ${declared:-?} -> ${latest}"
if [[ "$auto" == no ]]; then
warn "A restart will leave the vault SEALED until someone enters three unseal keys."
warn "Run it when you can do that: BAO_TOKEN=<token> bash ${SELF} update"
fi
}
do_update() {
[[ $EUID -eq 0 ]] || die "Run as root."
[[ -d "$STACK_DIR" ]] || die "No stack at $STACK_DIR (set STACK_DIR)."
local from to snap
from="$(declared_version)"
to="${TARGET_VERSION:-$(latest_version)}"
[[ -n "$to" ]] || die "Could not determine a target version (GitHub lookup failed). Set TARGET_VERSION=x.y.z."
if [[ "$from" == "$to" ]]; then
log "Already pinned to ${to}; nothing to do."
return 0
fi
check_pkcs11_cliff "$to"
container_running || die "The openbao container is not running. Start it first: cd ${STACK_DIR} && docker compose up -d"
# A sealed vault cannot produce a snapshot, so there would be no rollback
# plan. Catch it here rather than after the container is already down.
local state; state="$(seal_state)"
case "$state" in
0) ;;
2) die "The vault is SEALED. A snapshot cannot be taken from a sealed vault, so there would be no rollback plan. Unseal first, then re-run." ;;
*) die "Could not read the seal state (bao status rc=${state}). Refusing to upgrade blind." ;;
esac
log "Updating OpenBao: ${from:-?} -> ${to}"
snap="$(take_snapshot "$to")"
if [[ "$DRY_RUN" == "1" ]]; then
echo "DRY: docker compose down; set OPENBAO_TAG=${to}; docker compose pull; docker compose up -d"
return 0
fi
# Clean shutdown before swapping the image, as the upgrade guide asks.
log "Stopping the stack (the vault will be sealed when it returns)..."
dc down
set_env OPENBAO_TAG "$to"
if ! dc pull; then
warn "Pull of ${to} failed; rolling the pin back to ${from:-<unset>}."
[[ -n "$from" ]] && set_env OPENBAO_TAG "$from"
dc up -d || true
die "Update aborted. The previous version is starting again; unseal it."
fi
if ! dc up -d --remove-orphans; then
warn "Starting ${to} failed; rolling the pin back to ${from:-<unset>}."
[[ -n "$from" ]] && set_env OPENBAO_TAG "$from"
dc up -d || true
die "Update aborted. If the data store is at fault, restore ${snap:-the snapshot} per the README's DR runbook."
fi
log "Started ${to}."
cat <<EOF
================================================================
UPDATED ${from:-?} -> ${to}
The vault is SEALED. Nothing works until you unseal it:
cd ${STACK_DIR}
docker compose exec -e BAO_ADDR=https://127.0.0.1:8200 openbao \\
bao operator unseal -tls-skip-verify # x3, three different keys
Then verify:
docker compose exec -e BAO_ADDR=https://127.0.0.1:8200 openbao \\
bao status -tls-skip-verify # Version ${to}, Sealed false
Rollback, if ${to} misbehaves: reverting the image alone does NOT roll back the
data store. Restore the pre-upgrade snapshot -- see the README's DR runbook.
snapshot: ${snap:-<none taken>}
================================================================
EOF
}
do_run() {
# What the schedule invokes. The whole point of this branch is that it is
# conservative: an unattended update of a manually-unsealed vault takes it
# offline until a human arrives, so that is never the default.
case "$UPDATE_POLICY" in
notify)
do_check
return 0 ;;
auto)
if ! auto_unseal_configured; then
do_check
warn "UPDATE_POLICY=auto but no seal stanza is configured, so the vault would stay SEALED after a restart with nobody present. Not updating."
return 0
fi
local from to
from="$(declared_version)"
to="${TARGET_VERSION:-$(latest_version)}"
if [[ -z "$to" || "$from" == "$to" ]]; then
do_check
return 0
fi
log "UPDATE_POLICY=auto and auto-unseal is configured; updating ${from:-?} -> ${to}."
do_update
return 0 ;;
*)
die "UPDATE_POLICY must be 'notify' or 'auto' (got '${UPDATE_POLICY}')." ;;
esac
}
write_conf() {
install -d -m 0755 "$(dirname "$OPENBAO_UPDATE_CONF")"
# BAO_TOKEN is deliberately NOT written here: a long-lived root token sitting
# in a conf file next to the vault it opens defeats the point of the vault.
cat > "$OPENBAO_UPDATE_CONF" <<EOF
# openbao-update.conf -- read by update.sh; the environment wins over this file.
STACK_DIR=${STACK_DIR}
UPDATE_POLICY=${UPDATE_POLICY}
SNAPSHOT_DIR=${SNAPSHOT_DIR}
GH_REPO=${GH_REPO}
# BAO_TOKEN is intentionally absent. \`update\` needs one for the snapshot; pass
# it in the environment for that run. UPDATE_POLICY=auto therefore only works
# with auto-unseal AND a token supplied some other way.
EOF
chmod 0600 "$OPENBAO_UPDATE_CONF"
}
do_install() {
[[ $EUID -eq 0 ]] || die "Run as root."
write_conf
if [[ "$UPDATE_POLICY" == notify ]]; then
log "Scheduling a daily CHECK (policy=notify: it will never change the running version)."
else
log "Scheduling a daily run (policy=${UPDATE_POLICY})."
fi
if [[ -r /etc/os-release ]] && grep -q '^ID=alpine' /etc/os-release; then
install -d -m 0755 /etc/periodic/daily
cat > /etc/periodic/daily/openbao-update <<EOF
#!/bin/sh
exec bash "$SELF" run
EOF
chmod +x /etc/periodic/daily/openbao-update
if command -v rc-update >/dev/null 2>&1; then
rc-update add crond default >/dev/null 2>&1 || true
rc-service crond start >/dev/null 2>&1 || true
fi
log "Installed /etc/periodic/daily/openbao-update."
else
cat > /etc/systemd/system/openbao-update.service <<EOF
[Unit]
Description=OpenBao container updater
After=docker.service
[Service]
Type=oneshot
ExecStart=/usr/bin/env bash $SELF run
EOF
cat > /etc/systemd/system/openbao-update.timer <<EOF
[Unit]
Description=Daily OpenBao update check
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1h
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now openbao-update.timer >/dev/null 2>&1 || true
log "Installed the openbao-update.timer systemd timer."
fi
}
do_uninstall() {
[[ $EUID -eq 0 ]] || die "Run as root."
rm -f /etc/periodic/daily/openbao-update
if command -v systemctl >/dev/null 2>&1; then
systemctl disable --now openbao-update.timer >/dev/null 2>&1 || true
rm -f /etc/systemd/system/openbao-update.timer /etc/systemd/system/openbao-update.service
systemctl daemon-reload >/dev/null 2>&1 || true
fi
log "Schedule removed. ${OPENBAO_UPDATE_CONF} left in place."
}
case "${1:-check}" in
check) do_check ;;
update) do_update ;;
run) do_run ;;
install) do_install ;;
uninstall) do_uninstall ;;
*) die "Usage: $(basename "$0") {check|update|run|install|uninstall}" ;;
esac