skills: agent-provisioning - codified identity stand-up workflow
Policy rulings (#942): unique passwords, Cloudron-only SSO accounts, no reachableceo credentials, TOTP seeds MUST be captured to the vault. Cloudron admin API wrapper, cookie-jar OIDC login, RFC-6238-tested TOTP helper, per-system SSO/API references, incident log.
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# Authenticated Cloudron admin API call (compliant lever: api-token service
|
||||
# account from vault item creds/cloudron — never the founder's accounts).
|
||||
# usage: cloudron-api.sh METHOD /api/v1/PATH [JSON-BODY]
|
||||
# body -> stdout; HTTP status -> stderr. Secrets never printed.
|
||||
set -euo pipefail
|
||||
BASE="https://my.knownelement.com"
|
||||
METHOD="${1:?usage: cloudron-api.sh METHOD /api/v1/PATH [JSON-BODY]}"
|
||||
PATHPART="${2:?usage: cloudron-api.sh METHOD /api/v1/PATH [JSON-BODY]}"
|
||||
BODY="${3:-}"
|
||||
if [ "$(id -un)" = "TSGCOO" ]; then
|
||||
SM() { /data2/TSGCOO/.local/bin/sm "$@" </dev/null; }
|
||||
else
|
||||
SM() { sudo -u TSGCOO /data2/TSGCOO/.local/bin/sm "$@" </dev/null; }
|
||||
fi
|
||||
TOKEN=$(SM get creds/cloudron --field password)
|
||||
[ -n "$TOKEN" ] || { echo "no cloudron token in vault" >&2; exit 1; }
|
||||
ARGS=(-sk -X "$METHOD" "$BASE$PATHPART"
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
-H 'Content-Type: application/json'
|
||||
-w '\nHTTP %{http_code}\n')
|
||||
[ -n "$BODY" ] && ARGS+=(-d "$BODY")
|
||||
curl "${ARGS[@]}" >&2
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
# Full SSO login for an identity into one of the four systems, via cookie jar.
|
||||
# usage: oidc-login.sh <glpi|gitea|discourse|redmine> <login>
|
||||
# Reads the Cloudron password from vault item "<login> Cloudron" (env login
|
||||
# triple; USERNAME may be the email form). Success leaves cookie jar at
|
||||
# /tmp/oidc-<login>-<system>.txt and prints the landing URL.
|
||||
# TOTP-enforced identity? Set TOTP_CODE=$(scripts/totp.sh <seed>) first.
|
||||
set -euo pipefail
|
||||
SYS="${1:?usage: oidc-login.sh <glpi|gitea|discourse|redmine> <login>}"
|
||||
LOGIN="${2:?usage: oidc-login.sh <glpi|gitea|discourse|redmine> <login>}"
|
||||
case "$SYS" in
|
||||
glpi) START="https://cmdb.knownelement.com/plugins/singlesignon/front/callback.php/provider/1?remember=1"; APP="https://cmdb.knownelement.com";;
|
||||
gitea) START="https://git.knownelement.com/user/oauth2/cloudron"; APP="https://git.knownelement.com";;
|
||||
discourse) START="https://community.turnsys.com/auth/openid_connect"; APP="https://community.turnsys.com";;
|
||||
redmine) START="https://projects.knownelement.com/oauth"; APP="https://projects.knownelement.com";;
|
||||
*) echo "unknown system: $SYS" >&2; exit 2;;
|
||||
esac
|
||||
if [ "$(id -un)" = "TSGCOO" ]; then
|
||||
SM() { /data2/TSGCOO/.local/bin/sm "$@" </dev/null; }
|
||||
else
|
||||
SM() { sudo -u TSGCOO /data2/TSGCOO/.local/bin/sm "$@" </dev/null; }
|
||||
fi
|
||||
eval "$(SM env "$LOGIN Cloudron")"
|
||||
[ -n "${PASSWORD:-}" ] || { echo "vault item '$LOGIN Cloudron' empty" >&2; exit 1; }
|
||||
USER_FIELD="${USERNAME:-$LOGIN}"
|
||||
JAR="/tmp/oidc-${LOGIN}-${SYS}.txt"; rm -f "$JAR"
|
||||
|
||||
# 1. Start the SSO flow; follow redirects to the OIDC interaction page.
|
||||
INTURL=$(curl -s -o /dev/null -w '%{redirect_url}' -c "$JAR" "$START")
|
||||
case "$INTURL" in
|
||||
https://my.knownelement.com/openid/interaction/*) : ;;
|
||||
*) echo "no interaction redirect (got: '${INTURL:-none}'). If this is redmine, see references/systems.md (known bounce). If discourse, the route may need CSRF via a real session." >&2; exit 3;;
|
||||
esac
|
||||
|
||||
# 2. Interaction login. Field name is `username` (NOT `login`).
|
||||
BODY="{\"username\":\"$USER_FIELD\",\"password\":\"$PASSWORD\""
|
||||
if [ -n "${TOTP_CODE:-}" ]; then BODY="$BODY,\"totpToken\":\"$TOTP_CODE\""; fi
|
||||
BODY="$BODY}"
|
||||
RESP=$(curl -s -b "$JAR" -c "$JAR" -X POST "${INTURL}/login" \
|
||||
-H 'Content-Type: application/json' -d "$BODY")
|
||||
unset BODY
|
||||
|
||||
case "$RESP" in
|
||||
*redirectTo*)
|
||||
RED=$(printf '%s' "$RESP" | sed -n 's/.*"redirectTo":"\([^"]*\)".*/\1/p')
|
||||
case "$RED" in http*) : ;; *) RED="$APP$RED" ;; esac
|
||||
FINAL=$(curl -s -L -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' "$RED")
|
||||
echo "logged in: cookie jar $JAR"
|
||||
echo "landing: $FINAL"
|
||||
echo "verify the identity before use (users/current or /api/v1/user)."
|
||||
;;
|
||||
*twoFactorRequired*)
|
||||
echo "2FA enforced and no/invalid TOTP_CODE given." >&2
|
||||
echo "If the seed is in the vault: TOTP_CODE=\$(scripts/totp.sh \$(sm get '$LOGIN Cloudron' --field totp_seed)) $0 $SYS $LOGIN" >&2
|
||||
exit 4;;
|
||||
*)
|
||||
echo "login rejected: $RESP" >&2
|
||||
exit 5;;
|
||||
esac
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Current TOTP code from a base32 seed (pure bash + openssl + coreutils).
|
||||
# usage: totp.sh <base32-seed> [step-seconds] [digits]
|
||||
set -euo pipefail
|
||||
SECRET="${1:?usage: totp.sh <base32-seed> [step] [digits]}"
|
||||
STEP="${2:-30}"; DIGITS="${3:-6}"
|
||||
NOW="${TOTP_NOW:-$(date +%s)}" # TOTP_NOW: RFC-6238 test vector override
|
||||
KEYHEX=$(printf '%s' "$SECRET" | tr -d ' =\n-' | base32 -d 2>/dev/null | od -An -tx1 | tr -d ' \n')
|
||||
[ -n "$KEYHEX" ] || { echo "bad base32 seed" >&2; exit 2; }
|
||||
COUNTER=$(printf '%016x' $(( NOW / STEP )))
|
||||
# bash vars cannot hold null bytes - keep the counter as a FORMAT string and
|
||||
# let printf emit the raw bytes straight into the pipe.
|
||||
FMT=$(printf '%s' "$COUNTER" | sed 's/../\\x&/g')
|
||||
# intentional: FMT emits raw counter bytes incl. NULs - the whole point
|
||||
# shellcheck disable=SC2059
|
||||
MAC=$(printf "$FMT" | openssl dgst -sha1 -mac HMAC -macopt "hexkey:$KEYHEX" -binary \
|
||||
| od -An -tx1 | tr -d ' \n')
|
||||
OFFSET=$(( 0x${MAC: -1} ))
|
||||
CODE=$(( (0x${MAC:$((OFFSET*2)):8} & 0x7fffffff) % (10 ** DIGITS) ))
|
||||
printf "%0${DIGITS}d\n" "$CODE"
|
||||
Reference in New Issue
Block a user