Add personal-backup playground (KV + transit) and AppRole demo
Two isolated personal-credential backup options to evaluate, both excluded
from the admin policy (root-only break-glass):
- Scenario A: personal/ KV v2 (versioned) + personal-{put,get,list}.sh
- Scenario B: transit-personal/ key personal-backup + secret-{encrypt,decrypt}.sh
Plus an AppRole example of how a system should consume a secret:
- demo-app role/policy (read-only secret/demo-app/*), short-lived tokens
- scripts/app-get-secret.sh: login (role_id+secret_id) -> token -> read
All tokens/credentials live under ~/.config/openbao (outside the repo).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
44
README.md
44
README.md
@@ -222,6 +222,50 @@ The push credential for the gitea remote is stored in OpenBAO KV
|
||||
Rotate the PAT by re-running `store-gitea-cred.sh`. The helper token is a
|
||||
periodic, read-only token scoped to just that one KV path.
|
||||
|
||||
## Personal credential backup — two playground scenarios
|
||||
|
||||
Two isolated setups to evaluate using OpenBAO as a *backup* for personal
|
||||
passwords. **Both are deliberately excluded from the `admin` policy** (only
|
||||
root is break-glass), so your day-to-day admin token cannot read them.
|
||||
|
||||
**Scenario A — structured KV** (`personal/` KV v2, versioned):
|
||||
|
||||
```bash
|
||||
scripts/personal-put.sh github # prompts user/password(hidden)/url
|
||||
scripts/personal-list.sh
|
||||
scripts/personal-get.sh github # all fields
|
||||
scripts/personal-get.sh github password # one field
|
||||
```
|
||||
|
||||
**Scenario B — transit-encrypted blob** (`transit-personal/` key
|
||||
`personal-backup`; OpenBAO holds only the key, the ciphertext is portable):
|
||||
|
||||
```bash
|
||||
scripts/secret-encrypt.sh passwords-export.csv # -> passwords-export.csv.vaultenc
|
||||
scripts/secret-decrypt.sh passwords-export.csv.vaultenc out.csv
|
||||
```
|
||||
|
||||
Scoped tokens live in `~/.config/openbao/{personal-kv,personal-transit}.token`.
|
||||
Trade-offs discussed inline; A = granular/versioned but OpenBAO holds plaintext,
|
||||
B = OpenBAO holds only ciphertext (store the blob anywhere offsite).
|
||||
|
||||
## Consuming a secret from a system (AppRole)
|
||||
|
||||
How a *service* should authenticate (vs. a static token on disk): it holds a
|
||||
**RoleID** (non-secret) + **SecretID** (secret), logs in for a **short-lived**
|
||||
token, then reads. Demo role `demo-app` can read only `secret/demo-app/*`.
|
||||
|
||||
```bash
|
||||
scripts/app-get-secret.sh demo-app/config
|
||||
# authenticated: token ttl=1200s policies=["default","demo-app"]
|
||||
# { "api_key": "...", "db_url": "..." }
|
||||
```
|
||||
|
||||
RoleID/SecretID in `~/.config/openbao/approle/`. The minted token is 20m TTL and
|
||||
scoped to `demo-app` only — proven denied on other paths. This is the
|
||||
recommended pattern for real services; the static-token helpers elsewhere in
|
||||
this repo are the simpler home-lab shortcut.
|
||||
|
||||
## Backups (Raft snapshots) — automated
|
||||
|
||||
`scripts/backup-raft-snapshots.sh` snapshots **both** instances and prunes to
|
||||
|
||||
23
scripts/app-get-secret.sh
Executable file
23
scripts/app-get-secret.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# AppRole demo: how a *system* consumes a secret.
|
||||
# It holds a RoleID (non-secret) + SecretID (secret), logs in to get a
|
||||
# SHORT-LIVED token, then reads a secret with it. Contrast with the static-token
|
||||
# helpers (git-credential-openbao.sh etc.) that keep a long-lived token on disk.
|
||||
#
|
||||
# app-get-secret.sh <kv-path-under-secret/> e.g. app-get-secret.sh demo-app/config
|
||||
set -euo pipefail
|
||||
ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
|
||||
DIR="${OPENBAO_APPROLE_DIR:-$HOME/.config/openbao/approle}"
|
||||
path="${1:?usage: app-get-secret.sh <kv-path under secret/>}"
|
||||
RID="$(cat "$DIR/role_id")"; SID="$(cat "$DIR/secret_id")"
|
||||
|
||||
# 1) authenticate -> short-lived token
|
||||
login="$(curl -sS --data "$(RID="$RID" SID="$SID" python3 -c \
|
||||
'import json,os;print(json.dumps({"role_id":os.environ["RID"],"secret_id":os.environ["SID"]}))')" \
|
||||
"$ADDR/v1/auth/approle/login")"
|
||||
tok="$(printf '%s' "$login" | jq -r '.auth.client_token // empty')"
|
||||
[ -n "$tok" ] || { echo "login failed: $(printf '%s' "$login" | jq -c '.errors // .')" >&2; exit 1; }
|
||||
echo "authenticated: token ttl=$(printf '%s' "$login" | jq -r '.auth.lease_duration')s policies=$(printf '%s' "$login" | jq -c '.auth.token_policies')" >&2
|
||||
|
||||
# 2) read the secret with that token
|
||||
curl -sS -H "X-Vault-Token: $tok" "$ADDR/v1/secret/data/$path" | jq '.data.data'
|
||||
18
scripts/personal-get.sh
Executable file
18
scripts/personal-get.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scenario A: read a personal credential back.
|
||||
# personal-get.sh <name> # show all fields
|
||||
# personal-get.sh <name> password # print one field (for piping)
|
||||
set -euo pipefail
|
||||
ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
|
||||
TOKF="$HOME/.config/openbao/personal-kv.token"
|
||||
name="${1:?usage: personal-get.sh <name> [field]}"; field="${2:-}"
|
||||
[ -r "$TOKF" ] || { echo "no token at $TOKF" >&2; exit 1; }
|
||||
|
||||
resp="$(curl -sS -H "X-Vault-Token: $(cat "$TOKF")" "$ADDR/v1/personal/data/$name")"
|
||||
if printf '%s' "$resp" | jq -e '.errors and (.errors|length>0)' >/dev/null 2>&1; then
|
||||
echo "not found or no access: $name" >&2; exit 1; fi
|
||||
if [ -n "$field" ]; then
|
||||
printf '%s' "$resp" | jq -r ".data.data.$field // empty"
|
||||
else
|
||||
printf '%s' "$resp" | jq -r '.data.data | to_entries[] | "\(.key): \(.value)"'
|
||||
fi
|
||||
8
scripts/personal-list.sh
Executable file
8
scripts/personal-list.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scenario A: list the names stored in the personal/ KV engine.
|
||||
set -euo pipefail
|
||||
ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
|
||||
TOKF="$HOME/.config/openbao/personal-kv.token"
|
||||
[ -r "$TOKF" ] || { echo "no token at $TOKF" >&2; exit 1; }
|
||||
curl -sS -H "X-Vault-Token: $(cat "$TOKF")" "$ADDR/v1/personal/metadata?list=true" \
|
||||
| jq -r '.data.keys[]? // empty'
|
||||
19
scripts/personal-put.sh
Executable file
19
scripts/personal-put.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scenario A (structured KV): store a personal credential in the isolated
|
||||
# `personal/` KV v2 engine (NOT readable by the day-to-day admin policy).
|
||||
# personal-put.sh <name> e.g. personal-put.sh github
|
||||
set -euo pipefail
|
||||
ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
|
||||
TOKF="$HOME/.config/openbao/personal-kv.token"
|
||||
name="${1:?usage: personal-put.sh <name>}"
|
||||
[ -r "$TOKF" ] || { echo "no token at $TOKF" >&2; exit 1; }
|
||||
|
||||
read -rp "username: " u
|
||||
read -rsp "password (hidden): " p; echo
|
||||
read -rp "url/notes (optional): " n
|
||||
|
||||
payload="$(U="$u" P="$p" N="$n" python3 -c \
|
||||
'import json,os;print(json.dumps({"data":{"username":os.environ["U"],"password":os.environ["P"],"url":os.environ["N"]}}))')"
|
||||
code="$(printf '%s' "$payload" | curl -sS -o /dev/null -w '%{http_code}' \
|
||||
-H "X-Vault-Token: $(cat "$TOKF")" --data @- "$ADDR/v1/personal/data/$name")"
|
||||
case "$code" in 200|204) echo "stored personal/$name (versioned)";; *) echo "FAILED (HTTP $code)"; exit 1;; esac
|
||||
19
scripts/secret-decrypt.sh
Executable file
19
scripts/secret-decrypt.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scenario B: decrypt a transit blob produced by secret-encrypt.sh.
|
||||
# secret-decrypt.sh <encfile> [outfile] (default outfile: <encfile> w/o .vaultenc, +.dec)
|
||||
set -euo pipefail
|
||||
ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
|
||||
TOKF="$HOME/.config/openbao/personal-transit.token"
|
||||
KEY="personal-backup"
|
||||
in="${1:?usage: secret-decrypt.sh <encfile> [outfile]}"
|
||||
out="${2:-${in%.vaultenc}.dec}"
|
||||
[ -r "$TOKF" ] || { echo "no token at $TOKF" >&2; exit 1; }
|
||||
[ -r "$in" ] || { echo "no input file: $in" >&2; exit 1; }
|
||||
|
||||
b64="$(cat "$in" \
|
||||
| python3 -c 'import json,sys;print(json.dumps({"ciphertext":sys.stdin.read().strip()}))' \
|
||||
| curl -sS -H "X-Vault-Token: $(cat "$TOKF")" --data @- "$ADDR/v1/transit-personal/decrypt/$KEY" \
|
||||
| jq -r '.data.plaintext // empty')"
|
||||
[ -n "$b64" ] || { echo "decrypt failed (wrong key/blob?)" >&2; exit 1; }
|
||||
printf '%s' "$b64" | base64 -d > "$out"
|
||||
echo "decrypted $in -> $out"
|
||||
20
scripts/secret-encrypt.sh
Executable file
20
scripts/secret-encrypt.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scenario B (transit blob): encrypt a file with OpenBAO's transit key
|
||||
# `personal-backup`. OpenBAO holds only the KEY; the ciphertext is a portable
|
||||
# blob you can store anywhere (Synology, git, offsite). Output is ASCII.
|
||||
# secret-encrypt.sh <infile> [outfile] (default outfile: <infile>.vaultenc)
|
||||
set -euo pipefail
|
||||
ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
|
||||
TOKF="$HOME/.config/openbao/personal-transit.token"
|
||||
KEY="personal-backup"
|
||||
in="${1:?usage: secret-encrypt.sh <infile> [outfile]}"; out="${2:-$in.vaultenc}"
|
||||
[ -r "$TOKF" ] || { echo "no token at $TOKF" >&2; exit 1; }
|
||||
[ -r "$in" ] || { echo "no input file: $in" >&2; exit 1; }
|
||||
|
||||
ct="$(base64 -w0 "$in" \
|
||||
| python3 -c 'import json,sys;print(json.dumps({"plaintext":sys.stdin.read()}))' \
|
||||
| curl -sS -H "X-Vault-Token: $(cat "$TOKF")" --data @- "$ADDR/v1/transit-personal/encrypt/$KEY" \
|
||||
| jq -r '.data.ciphertext // empty')"
|
||||
[ -n "$ct" ] || { echo "encrypt failed" >&2; exit 1; }
|
||||
printf '%s\n' "$ct" > "$out"
|
||||
echo "encrypted $in -> $out ($(wc -c < "$out") bytes ciphertext)"
|
||||
Reference in New Issue
Block a user