From 2d3d322b0e0bc66ccbbc72d3c1b273fa2c211222 Mon Sep 17 00:00:00 2001 From: William Gill Date: Tue, 22 Sep 2026 09:21:11 -0500 Subject: [PATCH] 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 --- deployments/openbao/README.md | 37 +++ deployments/openbao/build.sh | 4 +- deployments/openbao/deploy.sh | 272 ++++++++++++++------- deployments/openbao/update.sh | 447 ++++++++++++++++++++++++++++++++++ 4 files changed, 675 insertions(+), 85 deletions(-) create mode 100644 deployments/openbao/update.sh diff --git a/deployments/openbao/README.md b/deployments/openbao/README.md index 49f604e..0e1527e 100644 --- a/deployments/openbao/README.md +++ b/deployments/openbao/README.md @@ -169,6 +169,43 @@ ssh -L 8200::8200 root@ 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= 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: diff --git a/deployments/openbao/build.sh b/deployments/openbao/build.sh index 41e9a0e..4bbff85 100644 --- a/deployments/openbao/build.sh +++ b/deployments/openbao/build.sh @@ -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 diff --git a/deployments/openbao/deploy.sh b/deployments/openbao/deploy.sh index 6cbb4d0..1d793f5 100644 --- a/deployments/openbao/deploy.sh +++ b/deployments/openbao/deploy.sh @@ -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() { # : 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= 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== diff --git a/deployments/openbao/update.sh b/deployments/openbao/update.sh new file mode 100644 index 0000000..6d663ff --- /dev/null +++ b/deployments/openbao/update.sh @@ -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= bash update.sh update +# BAO_TOKEN= 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() { # : 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() { # -> 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() { # + 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() { # -> 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:-}" + printf ' running (bao status): %s\n' "${running:-}" + printf ' latest (%s): %s\n' "$GH_REPO" "${latest:-}" + 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= 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:-}." + [[ -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:-}." + [[ -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 < ${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:-} +================================================================ +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" < /etc/periodic/daily/openbao-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 < /etc/systemd/system/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