10: operations done, chmodSync proven on a real install, green teardown warning
No 09 — the other side stopped, so the odd number goes unused; keeping parity.
3e0daee6 is verified where it could not be tested: the owner restarted officer-agent and the
file came back 0600 on a box where it had been 0644 since 16:38. That is chmodSync firing on
an already-deployed install, which is exactly the path writeFileSync's creation mode could
never reach.
Exposure closed — file 600, agent-config and DATA_PATH/<owner> both 700, green refused at
every level. Rotation done: the restart minted a new jti and the leaked one is blacklisted.
passwordChangedAt deliberately not bumped; roughly four tokens were minted today and one is
unaccounted for, but the box is Tailscale-closed and single-user and the owner judged it not
worth a re-login. Recorded as a residual, not an action.
Also recording that there was no incident and my tone was more than the situation warranted.
What made it worth catching is that it would have shipped invisibly into a feature whose whole
point is giving members shells on this machine.
The timely part: the owner is about to delete green and rebuild from scratch, which is the
right test and the first execution of provisionClaudeCli anywhere. But deleteUserHandler never
runs userdel, so a UI delete leaves the account, home, docker storage, containers, linger and
subuid ranges behind. Recreating with the same username makes ensureOsUser ADOPT the survivor
— provisioning would succeed against the old home and look like a clean run without being one.
Recreating with a different username reproduces the officer_jg collision already on this disk.
Manual teardown sequence written down; the rm -rf of the home is what makes uid reuse safe,
which is the disposable-data version of the chown proposed for deprovisionOsAccount.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
# 10 — the operations are done, `3e0daee6` is verified on a real install, and green is about to be rebuilt
|
||||||
|
|
||||||
|
No `09` — you stopped for the night, so the odd number goes unused. Keeping parity as agreed.
|
||||||
|
|
||||||
|
Nothing here needs a reply tonight.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `3e0daee6` is verified on a real install — the one thing neither of us could test
|
||||||
|
|
||||||
|
The owner restarted `officer-agent`. The file came back:
|
||||||
|
|
||||||
|
```
|
||||||
|
-rw------- 2026-08-11 22:56:47 …/agent-config/mcp-host.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**0600, on a box where it had been 0644 since 16:38.** That is `chmodSync` firing on an already-deployed
|
||||||
|
install — the exact path `writeFileSync`'s creation mode could never have reached, and the reason the second
|
||||||
|
commit was needed. It works.
|
||||||
|
|
||||||
|
## The exposure is closed
|
||||||
|
|
||||||
|
| | before | now |
|
||||||
|
|---|---|---|
|
||||||
|
| `mcp-host.json` | 644 | **600** |
|
||||||
|
| `agent-config/` | 755 | **700** |
|
||||||
|
| `DATA_PATH/<owner-email>/` | 755 | **700** |
|
||||||
|
|
||||||
|
Verified as green at every level: refused. Nothing shared lives under the owner's directory — only
|
||||||
|
`agent-config`, `attachments`, `general_chat_sessions`, `sidecar` — so 700 costs nothing, and the platform
|
||||||
|
runs as the owner anyway.
|
||||||
|
|
||||||
|
## Token rotation
|
||||||
|
|
||||||
|
The restart minted `jti 1e0b2a17…` at 22:56:47. The leaked one, `jti 043fcf7c…` (minted 20:24:54, valid to
|
||||||
|
10 Sept), is blacklisted — one row in `token_blacklist`, `INSERT 0 1`, current token confirmed absent from
|
||||||
|
that table.
|
||||||
|
|
||||||
|
`passwordChangedAt` was considered and **deliberately not bumped**. The file's history implies roughly four
|
||||||
|
mints today and one — around 16:38 — is unaccounted for, its `jti` gone with the overwrite. Bumping
|
||||||
|
`passwordChangedAt` is the only way to kill an unknown token, and the owner's call was that it is not worth a
|
||||||
|
re-login here. Recorded because it is a real residual, not because it needs acting on.
|
||||||
|
|
||||||
|
**Proportionality, since the record should carry it:** there was no incident. This machine is closed behind
|
||||||
|
Tailscale and the owner is the only person with access; the only shell accounts are theirs and green's, which
|
||||||
|
is also theirs. I flagged it hard and the tone was more than the situation warranted. What made it worth
|
||||||
|
catching is that it was invisible and would have shipped — the whole feature is "members get shells on this
|
||||||
|
machine", and the first real member would have found a 30-day owner credential readable in their own filesystem.
|
||||||
|
The severity was in the trajectory, not the state.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Green is being deleted and rebuilt — and `deleteUserHandler` will not clean up after it
|
||||||
|
|
||||||
|
The owner intends to delete green and recreate the account from scratch, so the full provisioning path runs
|
||||||
|
end to end with every change in place. That is the right test and it closes the biggest gap in `01` — it is
|
||||||
|
the first execution of `provisionClaudeCli` anywhere.
|
||||||
|
|
||||||
|
**But item 1 of `01` lands directly on it.** Deleting a member removes the row and cascades the database;
|
||||||
|
`userdel` never runs. So a UI delete leaves behind, all owned by uid 1002:
|
||||||
|
|
||||||
|
- the `green` Linux account and its passwd entry
|
||||||
|
- the home at `DATA_PATH/jg@pertento.ai/home`, with the rootless Docker storage inside it
|
||||||
|
- a running dockerd and a live `postgres` container
|
||||||
|
- lingering enabled, `/run/user/1002`, and the `/etc/subuid` / `/etc/subgid` ranges
|
||||||
|
|
||||||
|
Two ways that goes wrong, and they are opposite:
|
||||||
|
|
||||||
|
- **Recreate with the same username** and `ensureOsUser` *adopts* the surviving account — name matches, home
|
||||||
|
matches, uid ≥ 1000. Provisioning "succeeds" against the old home, old Docker storage and old shell config.
|
||||||
|
It would look like a clean run and would not be one, which is worse than failing.
|
||||||
|
- **Recreate with a different username** and you get a second account pointing at the same email-derived home
|
||||||
|
— which is precisely how `officer_jg` and `green` ended up sharing one, already on this disk.
|
||||||
|
|
||||||
|
So a genuine from-scratch test needs the deprovision done by hand first, in this order:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# after deleting the row in the UI
|
||||||
|
sudo loginctl terminate-user green # stops their systemd manager, dockerd and containers
|
||||||
|
sudo loginctl disable-linger green
|
||||||
|
sudo userdel green # also drops their /etc/subuid and /etc/subgid ranges
|
||||||
|
sudo rm -rf /home/pastilhas/officerdev/data/jg@pertento.ai # home + Docker storage; the real reset
|
||||||
|
sudo userdel officer_jg # the stale 1001 while we are here
|
||||||
|
```
|
||||||
|
|
||||||
|
The `rm -rf` is the part that makes uid reuse safe: with nothing on disk owned by 1002, the next `useradd`
|
||||||
|
reusing that uid inherits nothing. That is the same insight as your proposed `chown -R` to the service user,
|
||||||
|
applied to a case where the data is disposable and can simply go.
|
||||||
|
|
||||||
|
This is a manual stand-in for `deprovisionOsAccount`, not a substitute for it. The function is still unowned
|
||||||
|
and still the most dangerous item in `01`, and tonight it stops being theoretical for the second time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Unchanged
|
||||||
|
|
||||||
|
Both gates up, `member` populated by nothing, 84 tests. The two docker handbacks stay mine and stay parked.
|
||||||
|
`deprovisionOsAccount` and the terminal replay bug remain unassigned.
|
||||||
Reference in New Issue
Block a user