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:
@@ -0,0 +1,166 @@
|
||||
---
|
||||
name: agent-provisioning
|
||||
description: >
|
||||
Provision TSYS Group agent identities end-to-end: Cloudron account (unique
|
||||
password, TOTP with seed captured to the TSGCOO vault), SSO first-login into
|
||||
Redmine / Gitea / Discourse / GLPI, per-system API key minting, vault item
|
||||
schema, and verification. Use whenever asked to "stand up" an identity or
|
||||
agent, create/fix Cloudron users, wire SSO, debug identity logins or 2FA,
|
||||
or store identity credentials — even if the word "provisioning" is not used.
|
||||
Policy-critical: encodes founder rulings #942 (unique passwords, Cloudron-only
|
||||
accounts, no reachableceo credentials).
|
||||
---
|
||||
|
||||
# Agent identity provisioning (TSYS Group)
|
||||
|
||||
Stand up an agent identity so it can operate its own accounts in Cloudron and
|
||||
the four SSO-wired systems, with every credential landing in the TSGCOO
|
||||
Bitwarden vault. The Cloudron is the ONLY identity source: never create local
|
||||
accounts in Redmine, Gitea, Discourse, or GLPI — the system account comes into
|
||||
existence when the identity completes its first SSO login there.
|
||||
|
||||
## Non-negotiable policy (founder rulings — violating these invalidates the work)
|
||||
|
||||
1. **Unique password per identity.** One identity, one password, used only for
|
||||
its Cloudron account. Shared passwords are banned (Redmine #942).
|
||||
2. **Cloudron-only accounts.** System access = SSO first-login. No local
|
||||
accounts, no admin-created users in the downstream systems.
|
||||
3. **Never use reachableceo credentials.** Items like `creds/gitea-admin`,
|
||||
`creds/gitea`, `creds/redmine` are the founder's personal accounts and
|
||||
carry RESTRICTED notes. If an auth test resolves to user `reachableceo`,
|
||||
stop — you are holding the founder's identity.
|
||||
4. **TOTP seeds MUST be stored in Bitwarden** (founder ruling 2026-09-08).
|
||||
Enroll TOTP and write the base32 seed to the vault item in the same
|
||||
breath — a seed that is not in the vault is treated as lost, because
|
||||
there is no admin 2FA reset on this Cloudron and no other record exists.
|
||||
5. **Secrets live only in the vault** (KNELSecretsManager `sm` CLI) — never in
|
||||
repos, command output, logs, or transcripts. Mask credentials in every
|
||||
command's visible output.
|
||||
6. **Bookkeeping in the same turn:** Redmine ticket on the provisioning
|
||||
program (#942 or its successor), cross-linked; vault item created;STATUS
|
||||
updated.
|
||||
|
||||
## Scope check
|
||||
|
||||
Identity stand-up is the COO C2 lane's assigned program (founder directive,
|
||||
2026-09-08). If your chat runs in another lane (SRE, security, OAM), confirm
|
||||
the founder assigned the stand-up to you before executing, or route a ticket
|
||||
to the COO C2 lane instead.
|
||||
|
||||
## Admin lever: Cloudron API token
|
||||
|
||||
`creds/cloudron` holds a working admin API token (service user `api-token`,
|
||||
64 chars). Use it as `Authorization: Bearer $PASSWORD`. Do NOT log into the
|
||||
dashboard interactively for admin work — the token is the compliant path.
|
||||
|
||||
```bash
|
||||
scripts/cloudron-api.sh GET /users?limit=100 # list users (note uid- UUIDs)
|
||||
scripts/cloudron-api.sh GET /users/<uid> # fetch one user object
|
||||
```
|
||||
|
||||
Route notes (this Cloudron, 2026-09): users are addressed by `uid-…` UUIDs,
|
||||
not usernames; there is NO `/api/v1/login` (dashboard auth is its own OIDC
|
||||
flow); there is NO admin route to reset another user's 2FA — so the seed
|
||||
capture in step 2 is not optional.
|
||||
|
||||
## Procedure
|
||||
|
||||
### 0. Prerequisites
|
||||
|
||||
- Vault reachable: `sm get creds/cloudron` works (see references/vault.md).
|
||||
- For a NEW Cloudron user: the founder generates the invite, or you create the
|
||||
user via the admin token if the founder has authorized self-service.
|
||||
- A Redmine ticket to record the work (create one if none exists).
|
||||
|
||||
### 1. Cloudron account (unique password)
|
||||
|
||||
Verify the account exists and matches the identity email pattern
|
||||
(`tsgstaff-<login>@turnsys.com` for COO-line identities). Generate the unique
|
||||
password (32 chars), set it, and confirm a login works (step 2 proves it).
|
||||
|
||||
### 2. TOTP 2FA — enroll and CAPTURE THE SEED
|
||||
|
||||
Enroll TOTP via the profile self-service surface and write the base32 seed to
|
||||
the vault item before anything else:
|
||||
|
||||
```bash
|
||||
sm setfield "<login> Cloudron" totp_seed "<BASE32SEED>"
|
||||
```
|
||||
|
||||
If the identity already has 2FA enforced and no seed exists anywhere: STOP and
|
||||
ask the founder. There is no programmatic recovery (verified 2026-09-08).
|
||||
Compute codes offline when needed: `scripts/totp.sh <seed>`.
|
||||
|
||||
### 3. SSO first-login into each system
|
||||
|
||||
The interactive login is scriptable with a cookie jar — no browser needed.
|
||||
`scripts/oidc-login.sh <system>` runs the whole flow and leaves a session
|
||||
cookie jar at `/tmp/oidc-<login>-<system>.txt`. Entry points:
|
||||
|
||||
| System | SSO start point | After login |
|
||||
|---|---|---|
|
||||
| GLPI | `https://cmdb.knownelement.com/plugins/singlesignon/front/callback.php/provider/1?remember=1` | account auto-provisioned |
|
||||
| Gitea | `https://git.knownelement.com/user/oauth2/cloudron` | account auto-provisioned |
|
||||
| Discourse | `https://community.turnsys.com/auth/openid_connect` | account auto-provisioned |
|
||||
| Redmine | `https://projects.knownelement.com/oauth` | account auto-provisioned (if the bounce in references/systems.md is hit, see that file) |
|
||||
|
||||
Key gotcha (cost a debugging session — do not rediscover): the OIDC
|
||||
interaction login is `POST /openid/interaction/<uid>/login` with JSON body
|
||||
`{"username": "...", "password": "..."}` — the field is **`username`**, not
|
||||
`login`, and the response is `{"redirectTo": ...}` on success.
|
||||
|
||||
### 4. Mint per-system API keys (as the identity, via its session)
|
||||
|
||||
- **Redmine**: My account → API key (visible to self); also returned by
|
||||
`GET /users/current.json` with the session.
|
||||
- **Gitea**: Settings → Applications → new token with write scopes; or
|
||||
`POST /api/v1/users/<login>/tokens` using the session (requires basic auth
|
||||
of the same user for token endpoints).
|
||||
- **Discourse**: user API key flow; or admin-generated key if an admin lever
|
||||
is ever provided (none currently is — do not use founder keys).
|
||||
- **GLPI**: My account → API token (personal token); pair with the app token
|
||||
from `creds/glpi` (password field) for REST calls.
|
||||
|
||||
Store each key in the vault item immediately (schema below).
|
||||
|
||||
### 5. Vault item schema — `creds/<login>`
|
||||
|
||||
One item per identity (NOT per system). Create it as a LOGIN item with:
|
||||
|
||||
- login username = Cloudron username; password = the unique Cloudron password
|
||||
- URI = `https://my.knownelement.com`
|
||||
- fields: `email`, `organization`, `role`, `reports_to`, `created`,
|
||||
`services`, plus per-system: `redmine_api_key`, `gitea_token`,
|
||||
`discourse_user_api_key`, `glpi_user_token`, and `totp_seed`
|
||||
|
||||
Historical items that predate this schema (e.g. `sec-*`, `sre-*` shared-
|
||||
password items) are remediation targets under #942 — upgrade them when the
|
||||
owner lane assigns the work, do not replicate their shape.
|
||||
|
||||
### 6. Verify the trail
|
||||
|
||||
Prove each system knows the identity (expected: 200 + the identity's own
|
||||
name; a 200 from the wrong identity is a FAIL):
|
||||
|
||||
```bash
|
||||
curl -sf -H "X-Redmine-API-Key: $KEY" https://projects.knownelement.com/users/current.json | jq .user.login
|
||||
curl -sf -H "Authorization: token $TOKEN" https://git.knownelement.com/api/v1/user | jq .login
|
||||
```
|
||||
|
||||
Then update the Redmine ticket (full URLs, `[agent] <your-identity>` note) and
|
||||
the workspace STATUS/memory.
|
||||
|
||||
## References — read before touching the corresponding system
|
||||
|
||||
- `references/cloudron.md` — admin API routes, OIDC interaction flow, 2FA.
|
||||
- `references/systems.md` — per-system SSO/API details and known breakage.
|
||||
- `references/vault.md` — sm CLI entry points, gotchas, fallbacks.
|
||||
- `references/incident-log.md` — what went wrong on 2026-09-07/08 and why, so
|
||||
the same failures are not re-derived.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `scripts/cloudron-api.sh METHOD PATH [JSON]` — authenticated admin call.
|
||||
- `scripts/oidc-login.sh <glpi|gitea|discourse|redmine> <login>` — full SSO
|
||||
login via cookie jar; reads the identity's creds from the vault.
|
||||
- `scripts/totp.sh <base32-seed>` — current 6-digit TOTP code (pure bash).
|
||||
@@ -0,0 +1,62 @@
|
||||
# Cloudron admin API + OIDC (verified 2026-09-08)
|
||||
|
||||
## Instance
|
||||
|
||||
- Dashboard: `https://my.knownelement.com` (admin surface; this host also
|
||||
fronts the org's public vhosts — it is NOT the tailscale-only control box).
|
||||
- API token: vault item `creds/cloudron`, password field, 64 chars, service
|
||||
user `api-token`. Verified: reads full user directory.
|
||||
|
||||
## Routes (this is a newer Cloudron — older route knowledge may not apply)
|
||||
|
||||
- Users are addressed by `uid-<uuid>`, never username.
|
||||
`GET /api/v1/users?limit=100` → list; `GET /api/v1/users/<uid>` → object
|
||||
(`twoFactorAuthenticationEnabled` is null/absent on objects even when
|
||||
enforced — the OIDC interaction is the source of truth).
|
||||
- There is NO `POST /api/v1/login`. Dashboard auth is its own OIDC browser
|
||||
flow; the SPA bundle (as of 2026-09-08) contains no login route.
|
||||
- There is NO admin route to reset another user's 2FA (PUT/PATCH/DELETE on
|
||||
`/users/<uid>` 2FA variants all 404; the dashboard UI only offers profile
|
||||
self-service enrollment). Consequence: TOTP seeds not captured at
|
||||
enrollment are unrecoverable without the founder.
|
||||
- Other useful routes seen in the SPA bundle: `/api/v1/tokens`,
|
||||
`/api/v1/oidc/clients`, `/api/v1/groups`, `/api/v1/eventlog`,
|
||||
`/api/v1/app_passwords`. Verify behavior at runtime before relying on a
|
||||
specific payload shape.
|
||||
|
||||
## OIDC interaction login (how identity SSO works)
|
||||
|
||||
The GLPI/Gitea/Discourse/Redmine SSO flows all redirect here:
|
||||
|
||||
1. `GET <app SSO start point>` → 302 to
|
||||
`https://my.knownelement.com/openid/interaction/<uid>` (Cloudron runs a
|
||||
node-oidc-provider at `/openid`). Cookies matter — keep a cookie jar.
|
||||
2. `POST /openid/interaction/<uid>/login` with JSON
|
||||
`{"username": "<login>", "password": "<unique password>"}`.
|
||||
- Field name is **`username`** — `login` returns
|
||||
`"A username must be non-empty string"` (400).
|
||||
- Success → `{"redirectTo": "..."}` (or
|
||||
`{"twoFactorRequired":true,"totpRequired":true}` if 2FA is enabled).
|
||||
- Wrong credentials → 401 `{"status":"Unauthorized","message":"Username
|
||||
and password does not match"}`.
|
||||
3. `GET` the `redirectTo` (same cookie jar) → completes at the app.
|
||||
|
||||
Email-style usernames work (`tsgstaff-coo@turnsys.com`); bare usernames also
|
||||
exist (`coo`). Identity emails follow `tsgstaff-<login>@turnsys.com`.
|
||||
|
||||
## 2FA
|
||||
|
||||
- TOTP enforced per user at the interaction step
|
||||
(`twoFactorRequired`/`totpRequired` in the login response).
|
||||
- Enrollment is profile self-service ONLY. Capture the base32 seed into the
|
||||
vault (`sm setfield "<login> Cloudron" totp_seed <seed>`) at enrollment —
|
||||
there is no admin reset and no recovery path.
|
||||
- Policy RULED (founder, 2026-09-08): TOTP seeds MUST be stored in Bitwarden.
|
||||
Enroll 2FA only when the seed is being captured to the vault item in the
|
||||
same step. Legacy identities whose seeds were never captured (the whole
|
||||
August batch) are reset-on-sight candidates: founder clears 2FA or hands
|
||||
over the seed, then re-enroll with capture.
|
||||
|
||||
---
|
||||
|
||||
**Records:** Redmine #942 — https://projects.knownelement.com/issues/942 · Discourse docs home: https://community.turnsys.com (skill topic pending admin-key provisioning, tracked on #942)
|
||||
@@ -0,0 +1,52 @@
|
||||
# Incident log — what broke and what it taught (append-only)
|
||||
|
||||
## 2026-09-07 ~20:00 CT — shared-box degradation (RESOLVED 09-08)
|
||||
|
||||
One box (152.53.37.179, mail.knownelement.com) hosts gitea, redmine,
|
||||
discourse, GLPI, pwvault, and fronts my.knownelement.com. During the
|
||||
incident:
|
||||
|
||||
- Gitea `git-receive-pack` → HTTP 500 on ANY ref (pushes impossible);
|
||||
`POST /api/v1/repos` → plain-text 404. Reads + `POST /api/v1/markdown`
|
||||
worked fine — partial failure, not an outage.
|
||||
- my.knownelement.com → HTTP 500 leaking
|
||||
`"Access denied for user 'root'@'localhost'"` (a MySQL root credential
|
||||
breakage in whatever backs that vhost).
|
||||
- pwvault: unauthenticated endpoints answered in 0.2s, but AUTHENTICATED
|
||||
`/api/sync` hung to timeout — a lock/db-layer signature. All vault item
|
||||
reads fleet-wide were down; nothing credential-driven could proceed.
|
||||
- tsys-cloudron (tailscale 100.107.35.78, self-signed cert — use `curl -k`)
|
||||
404'd its hostname; with `Host: my.knownelement.com` it served the same
|
||||
JSON as the public vhost (proxy topology). devbox/preprod/sectestbed
|
||||
cloudron peers were offline for 1–2 days.
|
||||
|
||||
Lessons: (1) when several "unrelated" systems on that box fail at once,
|
||||
treat it as one box incident — file one ticket, verify recovery per system;
|
||||
(2) HTTP-status-only checks lie — the vault was "up" (200s on static paths)
|
||||
while 100% of useful reads hung; test the authenticated path you actually
|
||||
need; (3) correlate carefully — the MySQL leak was the public box's vhost,
|
||||
not the cloudron control box; wrong attribution sent diagnosis down a
|
||||
rabbit hole (see #939 correction).
|
||||
|
||||
## 2026-09-07 — identity/SSO archaeology (the reason this skill exists)
|
||||
|
||||
- The August "10 identities enrolled + TOTP" batch captured NO seeds. coo's
|
||||
TOTP is therefore enforced-but-unrecoverable; no admin reset exists in
|
||||
this Cloudron version. Every future enrollment MUST store the seed.
|
||||
- `creds/sec-*` / `creds/sre-*` (Sep 07 batch, by VpEngOps) used ONE shared
|
||||
password across cloudron+gitea+redmine+discourse, and the stored password
|
||||
already fails Cloudron auth (401 tested). Shared passwords are banned
|
||||
(#942); treat those items as stale until their owning lane remediates.
|
||||
- Several "admin" vault candidates resolved to the founder's personal
|
||||
reachableceo accounts (gitea id 3, redmine user 5) and none were admin
|
||||
(gitea: 403 "user should be the site admin"; redmine: 403 on /users.json).
|
||||
They now carry RESTRICTED notes. The compliant Cloudron admin lever is the
|
||||
`api-token` user's token in `creds/cloudron`.
|
||||
- Debug order that works for auth weirdness: (1) what identity does this
|
||||
credential ACTUALLY resolve to (call /user or /users/current)?
|
||||
(2) does the route exist at all (SPA-bundle grep beats guessing)?
|
||||
(3) is the failure at nginx (HTML status pages) or the app (JSON errors)?
|
||||
|
||||
---
|
||||
|
||||
**Records:** Redmine #942 — https://projects.knownelement.com/issues/942 · Discourse docs home: https://community.turnsys.com (skill topic pending admin-key provisioning, tracked on #942)
|
||||
@@ -0,0 +1,73 @@
|
||||
# Per-system SSO + API key details (verified 2026-09-07/08 unless noted)
|
||||
|
||||
## GLPI (cmdb.knownelement.com) — SSO VERIFIED WORKING
|
||||
|
||||
- SSO: "Login with KNEL Cloud" = plugin route
|
||||
`/plugins/singlesignon/front/callback.php/provider/1?remember=1` → Cloudron
|
||||
OIDC → auto-provision.
|
||||
- The login page defaults to whichever tab was last toggled ("Use GLPI login
|
||||
form" / "Use Single Sign-on"); a hidden `noAUTO=1` input forces the local
|
||||
form — irrelevant under Cloudron-only policy, useful to recognize.
|
||||
- API: GLPI 11 is OAuth/token-based. The legacy REST at
|
||||
`/apirest.php/initSession` exists: password login is DISABLED (400);
|
||||
user_token auth exists but the tokens in `creds/glpi` / `creds/mglpi` are
|
||||
STALE (401 ERROR_GLPI_LOGIN_USER_TOKEN). The app token in `creds/glpi`
|
||||
password field IS accepted (used as `App-Token` header).
|
||||
- Per-user API token: My account (self-service) after SSO provisioning. Pair
|
||||
with the app token: `GET /apirest.php/initSession` with headers
|
||||
`App-Token: <app>` + `Authorization: user_token <user>`.
|
||||
- Local web login as `glpi` super-admin hits a 6-digit TOTP at
|
||||
`/MFA/Prompt` whose secret is not stored anywhere — do not rely on the
|
||||
admin UI; the SSO path makes it unnecessary.
|
||||
|
||||
## Gitea (git.knownelement.com)
|
||||
|
||||
- SSO: `/user/oauth2/cloudron` (button "Sign in with …" on the login page).
|
||||
- Standalone install behind nginx on the shared box — local accounts exist
|
||||
from the August batch; Cloudron-only policy means new identities use SSO
|
||||
and legacy ones migrate.
|
||||
- API: tokens via `POST /api/v1/users/<login>/tokens` REQUIRE basic auth AS
|
||||
that user (admin tokens cannot mint for others; there is no
|
||||
`/admin/users/<name>/tokens` route; `Sudo` header needs a real site admin).
|
||||
- Token auth against `/api/v1/user`: the JSON has `login`; admin-ness shows
|
||||
in `site_admin` (absent = not admin; do not misread null as false).
|
||||
- KNOWN BROKEN (2026-09-07/08): receive-pack 500s during the box incident —
|
||||
RESOLVED after the 09-08 repair (pushes verified). If pushes 500 again,
|
||||
check the shared box first (see incident-log.md).
|
||||
|
||||
## Discourse (community.turnsys.com)
|
||||
|
||||
- SSO: `/auth/openid_connect` — a bare GET returns 403 (CSRF-gated); that is
|
||||
NORMAL and means the route exists. Complete the flow from the login page
|
||||
button or with a session + CSRF token.
|
||||
- The login page is an Ember SPA: server-rendered HTML shows no SSO markers,
|
||||
which fooled one audit (2026-09-07) into "no SSO" — wrong. Check routes,
|
||||
not page source.
|
||||
- API: user API keys (User-Api-Key header) minted via the user-approval flow;
|
||||
admin Api-Key + Api-Username impersonation needs an admin key. The vault's
|
||||
`creds/discourse` NOTE says its keys are NOT admin (they are also not hex —
|
||||
do not assume key format). `creds/vptechops-discourse` holds a WORKING
|
||||
user key (user `vptechopsvptechops`) — #933's "dead key" note is wrong.
|
||||
|
||||
## Redmine (projects.knownelement.com)
|
||||
|
||||
- SSO: "Continue with KNEL Cloud" button POSTs/GETs `/oauth`.
|
||||
KNOWN BROKEN: `/oauth` 302-bounces straight back to /login with no
|
||||
provider redirect — verified 2026-09-07 AND 2026-09-08 (IdP healthy both
|
||||
times), so it is a Redmine plugin configuration issue, NOT the IdP.
|
||||
Retest before relying on it; until fixed, Redmine accounts cannot be
|
||||
created via SSO.
|
||||
- API keys: `GET /users/current.json` (session or basic auth) returns the
|
||||
user's `api_key`; X-Redmine-API-Key header for calls. Vault convention:
|
||||
the API key is stored in the item's password field.
|
||||
- Admin surface: `/users.json` lists users (admin only — 403 otherwise).
|
||||
Every admin candidate tested in the vault so far is NON-admin (they are
|
||||
all the founder's personal accounts anyway — RESTRICTED).
|
||||
|
||||
## Shared infrastructure hazard
|
||||
|
||||
Gitea, Redmine, Discourse, GLPI, the pwvault, and the my. vhost all run on
|
||||
ONE box (152.53.37.179 = mail.knownelement.com). Symptoms there cluster:
|
||||
when several systems misbehave at once, suspect the box (see
|
||||
incident-log.md), file/update one ticket, and do not burn hours debugging
|
||||
four clients.
|
||||
@@ -0,0 +1,48 @@
|
||||
# TSGCOO vault (`sm` CLI) — entry points and gotchas
|
||||
|
||||
The KNELSecretsManager vault (Vaultwarden at pwvault.turnsys.com, account
|
||||
coo@turnsys.com) is the ONLY credential store. Full doctrine:
|
||||
OAM workspace memory `tsgcoo-account-ops` + `creds-migration`; tool
|
||||
reference: KNEL workspace memory `knelsecretsmanager-reference`.
|
||||
|
||||
## Invocation
|
||||
|
||||
```bash
|
||||
# from the reachableceo account (this workstation):
|
||||
sudo -u TSGCOO /data2/TSGCOO/.local/bin/sm <cmd> # TSGCOO crossover
|
||||
# from inside the TSGCOO account:
|
||||
/data2/TSGCOO/.local/bin/sm <cmd>
|
||||
# raw container form:
|
||||
docker exec -i ukrrs-secretsmgr-cli smcli <cmd>
|
||||
```
|
||||
|
||||
Commands: `status`, `list [pattern]`, `get <name> [--field KEY]`,
|
||||
`env <name>`, `set`, `setfield <name> <key> <value>`, `rm`, `folders`.
|
||||
Item naming: `creds/<name>` for services; `<login> <System>` for identity
|
||||
login items; `creds/<login>` for the new per-identity schema.
|
||||
|
||||
## Gotchas that have actually bitten
|
||||
|
||||
1. `sm env` / `sm get` in scripts: append `</dev/null` — docker exec -i eats
|
||||
stdin and hangs the call inside loops/pipelines.
|
||||
2. `--field URI` NEVER matches — URIs are login URIs, not custom fields. Use
|
||||
`sm env` (synthetic URI/USERNAME/PASSWORD) or `--field password`.
|
||||
3. `sm env` PASSWORD is the login password, which for token-style items IS
|
||||
the token (e.g. `creds/cloudron`). Always confirm what a field holds
|
||||
before assuming password-vs-token semantics.
|
||||
4. zsh does not word-split `$VAR` — `SM="sudo -u ... sm"; $SM get` fails with
|
||||
"no such file". Use a function: `smget() { sudo -u TSGCOO /data2/TSGCOO/.local/bin/sm "$@" </dev/null; }`.
|
||||
5. Mask secrets in ALL visible output: pipe through
|
||||
`sed -E 's/((password|token|secret|key|seed|totp)[^:=]*[=:]).*/\1 [MASKED]/Ig'`
|
||||
and jq projections of field NAMES/lengths, never values.
|
||||
6. `smcli` self-heals expired sessions (v13: refresh-token rotation +
|
||||
fallback relogin using injected env). If `smcli status` is fine but
|
||||
fetches hang (`/api/sync` context deadline), the vault server is sick —
|
||||
stop retrying, check the shared box (incident-log.md), escalate.
|
||||
7. Never print a filled password form's DOM snapshot (browser work) — the
|
||||
ARIA tree includes field values; one coo password hit a transcript this
|
||||
way (2026-09-07).
|
||||
|
||||
---
|
||||
|
||||
**Records:** Redmine #942 — https://projects.knownelement.com/issues/942 · Discourse docs home: https://community.turnsys.com (skill topic pending admin-key provisioning, tracked on #942)
|
||||
+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