so-telegraf-cred: thin bash wrapper around so-yaml.py

Swap the ~150-line Python implementation for a 48-line bash script that
delegates YAML mutation to so-yaml.py — the same helper so-minion and
soup already use. Same semantics: seed the creds pillar on first use,
idempotent add, silent remove.

SO minion ids are dot-free by construction (setup/so-functions:1884
strips everything after the first '.'), so using the raw id as the
so-yaml.py key path is safe.
This commit is contained in:
Mike Reeves
2026-04-22 11:09:53 -04:00
parent 614f32c5e0
commit f240a99e22
+43 -148
View File
@@ -1,159 +1,54 @@
#!/usr/bin/env python3 #!/bin/bash
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one # Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at # or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
# https://securityonion.net/license; you may not use this file except in compliance with the # https://securityonion.net/license; you may not use this file except in compliance with the
# Elastic License 2.0. # Elastic License 2.0.
""" # Single writer for the Telegraf Postgres credentials pillar. Thin wrapper
Single writer for the Telegraf Postgres credentials pillar. # around so-yaml.py that generates a password on first add and no-ops on
# re-add so the cred is stable across repeated so-minion runs.
#
# Note: so-yaml.py splits keys on '.' with no escape. SO minion ids are
# dot-free by construction (setup/so-functions:1884 takes the short_name
# before the first '.'), so using the raw minion id as the key is safe.
Maintains /opt/so/saltstack/local/pillar/telegraf/creds.sls with shape: CREDS=/opt/so/saltstack/local/pillar/telegraf/creds.sls
telegraf: usage() {
postgres_creds: echo "Usage: $0 <add|remove> <minion_id>" >&2
<minion_id>: exit 2
user: so_telegraf_<safe> }
pass: "<72-char random>"
...
Called by so-minion on add/delete. PyYAML safe_dump preserves ambiguous seed_creds_file() {
strings as quoted scalars, so passwords never round-trip through type mkdir -p "$(dirname "$CREDS")"
coercion (unlike so-yaml.py, which would). All mutations are serialized if [[ ! -f "$CREDS" ]]; then
by an flock on a sibling .creds.lock file. (umask 027 && printf 'telegraf:\n postgres_creds: {}\n' > "$CREDS")
""" chown socore:socore "$CREDS" 2>/dev/null || true
chmod 640 "$CREDS"
fi
}
import fcntl OP=$1
import os MID=$2
import pwd [[ -z "$OP" || -z "$MID" ]] && usage
import secrets
import string
import sys
import tempfile
import yaml case "$OP" in
add)
CREDS_PATH = "/opt/so/saltstack/local/pillar/telegraf/creds.sls" SAFE=$(echo "$MID" | tr '.-' '__' | tr '[:upper:]' '[:lower:]')
LOCK_PATH = "/opt/so/saltstack/local/pillar/telegraf/.creds.lock" seed_creds_file
OWNER_USER = "socore" if so-yaml.py get -r "$CREDS" "telegraf.postgres_creds.${MID}.user" >/dev/null 2>&1; then
OWNER_GROUP = "socore" exit 0
FILE_MODE = 0o640 fi
PASSWORD_LEN = 72 PASS=$(tr -dc 'A-Za-z0-9~!@#^&*()_=+[]|;:,.<>?-' < /dev/urandom | head -c 72)
# Matches salt/postgres/auth.sls's DIGITS+LOWERCASE+UPPERCASE+SYMBOLS. so-yaml.py replace "$CREDS" "telegraf.postgres_creds.${MID}.user" "so_telegraf_${SAFE}" >/dev/null
PASSWORD_CHARS = ( so-yaml.py replace "$CREDS" "telegraf.postgres_creds.${MID}.pass" "$PASS" >/dev/null
string.digits ;;
+ string.ascii_lowercase remove)
+ string.ascii_uppercase [[ -f "$CREDS" ]] || exit 0
+ "~!@#^&*()-_=+[]|;:,.<>?" so-yaml.py remove "$CREDS" "telegraf.postgres_creds.${MID}" >/dev/null 2>&1 || true
) ;;
*)
usage
def safe_minion_id(minion_id): ;;
return minion_id.replace(".", "_").replace("-", "_").lower() esac
def generate_password():
return "".join(secrets.choice(PASSWORD_CHARS) for _ in range(PASSWORD_LEN))
def load_creds():
if not os.path.exists(CREDS_PATH):
return {"telegraf": {"postgres_creds": {}}}
with open(CREDS_PATH, "r") as f:
data = yaml.safe_load(f) or {}
if not isinstance(data, dict):
data = {}
data.setdefault("telegraf", {})
if not isinstance(data["telegraf"], dict):
data["telegraf"] = {}
data["telegraf"].setdefault("postgres_creds", {})
if not isinstance(data["telegraf"]["postgres_creds"], dict):
data["telegraf"]["postgres_creds"] = {}
return data
def atomic_write(data):
os.makedirs(os.path.dirname(CREDS_PATH), exist_ok=True)
fd, tmp_path = tempfile.mkstemp(
prefix=".creds.", suffix=".tmp", dir=os.path.dirname(CREDS_PATH)
)
try:
with os.fdopen(fd, "w") as f:
yaml.safe_dump(data, f, default_flow_style=False, sort_keys=True)
f.flush()
os.fsync(f.fileno())
os.chmod(tmp_path, FILE_MODE)
try:
pw = pwd.getpwnam(OWNER_USER)
os.chown(tmp_path, pw.pw_uid, pw.pw_gid)
except KeyError:
pass
os.rename(tmp_path, CREDS_PATH)
except Exception:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
raise
def with_lock(fn):
os.makedirs(os.path.dirname(LOCK_PATH), exist_ok=True)
with open(LOCK_PATH, "a+") as lf:
fcntl.flock(lf.fileno(), fcntl.LOCK_EX)
try:
return fn()
finally:
fcntl.flock(lf.fileno(), fcntl.LOCK_UN)
def cmd_add(minion_id):
def go():
data = load_creds()
creds = data["telegraf"]["postgres_creds"]
if minion_id in creds:
return 0
safe = safe_minion_id(minion_id)
creds[minion_id] = {
"user": "so_telegraf_" + safe,
"pass": generate_password(),
}
atomic_write(data)
return 0
return with_lock(go)
def cmd_remove(minion_id):
def go():
data = load_creds()
creds = data["telegraf"]["postgres_creds"]
if minion_id in creds:
del creds[minion_id]
atomic_write(data)
return 0
return with_lock(go)
def usage():
print(
"Usage: so-telegraf-cred <add|remove> <minion_id>",
file=sys.stderr,
)
return 2
def main(argv):
if len(argv) != 3:
return usage()
op, minion_id = argv[1], argv[2]
if not minion_id:
return usage()
if op == "add":
return cmd_add(minion_id)
if op == "remove":
return cmd_remove(minion_id)
return usage()
if __name__ == "__main__":
sys.exit(main(sys.argv))