Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,35 @@

All notable changes to this project are documented here.

## v2.9.2 - 2026-08-01

- Validate conventional mail-spool roots before `useradd`, then reopen and
revalidate them during cleanup bound to the newly selected UID. Accept the
root-owned layouts used by supported systems, including `root:mail 3777` and
Arch Linux's `root:root 1777`; a world-writable root now requires sticky-bit
protection. Continue to reject a non-root owner, setuid, world-write without
sticky, and a `/var/mail` or `/var/spool/mail` alias that escapes those two
accepted locations. Mail cleanup remains limited to a non-symlink regular mbox
file whose owner matches the captured UID.
- Return a proved post-`useradd` passwd identity with mail or Home preparation
failures so invite rollback can safely use it, while classifying failures that
occur before the helper runs so a confirmed-absent account does not leave a
stale creation-intent row. Persist the deletion-recovery witness before
controlled mail/Home cleanup, preserving narrow post-disappearance recovery
when an account vanishes during teardown.
- Permit manual recovery of a live pending creation row retained by a release
before v2.9.2 only through a direct interactive `revoke --user <name> --force`
and full-name confirmation. Require the row's random generation to match the
pending GECOS marker plus the exact safe account shape, then convert it to a
generation-less UID-only `DeletionStarted` witness before artifact cleanup.
`--yes`, automatic tasks, uninstall bulk removal, non-TTY input, and every
mismatched or incomplete pending identity remain fail-closed.
- Recheck both the local passwd database and NSS between absent-account mail
sweeps, and recheck mailbox absence after syncing its parent directory. A
same-name NSS identity or a mailbox recreated during durability confirmation
now stops cleanup and retains the recovery witness instead of being reported as
a completed deletion.

## v2.9.1 - 2026-08-01

- Accept the exact C-locale diagnostic emitted by systemd 256 and later when
Expand Down
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ The real private key is shown only once. Never put an invite bundle in a group c
/usr/bin/sudo /usr/local/sbin/linux-temp-admin revoke --user xxvcc-a1b2c3d4e5
```

By default, automatic revocation is scheduled after 24 hours. When the complete account identity can still be checked, a successful revoke removes the personal crontab, UID-matched `at`/`batch` jobs, account, home directory, SSH key, sudo grant, and any tool-created sshd exception. If the account disappeared outside the tool, revoke cleans only the registry, name-scoped grants, and tasks it can still identify safely; it does not guess at Home or mail cleanup after losing the identity witness. If a safety check or cleanup fails, the command returns nonzero, retains the account when it still exists and the registry witness, and attempts to disable any surviving account for a systemd retry or manual recovery. Revoke access immediately when work is finished even when automatic revocation is enabled.
By default, automatic revocation is scheduled after 24 hours. When the complete account identity can still be checked, a successful revoke removes the personal crontab, UID-matched `at`/`batch` jobs, account, home directory, SSH key, sudo grant, and any tool-created sshd exception. If the account disappeared outside the tool without a deletion-recovery witness, revoke cleans only the registry, name-scoped grants, and tasks it can still identify safely; it does not guess at Home or mail cleanup. When an already-authorized teardown persisted that witness first, recovery permits only an owner-checked conventional single-file mail-spool sweep and never recursively removes the absent account's old Home path. If a safety check or cleanup fails, the command returns nonzero, retains the account when it still exists and the registry witness, and attempts to disable any surviving account for a systemd retry or manual recovery. Revoke access immediately when work is finished even when automatic revocation is enabled.

## Everyday commands

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ ssh -i ./xxvcc-a1b2c3d4e5.key -p 22 xxvcc-a1b2c3d4e5@203.0.113.10
/usr/bin/sudo /usr/local/sbin/linux-temp-admin revoke --user xxvcc-a1b2c3d4e5
```

默认会安排在 24 小时后自动撤销。对于仍可用完整身份核对的账号,撤销成功会删除个人 crontab、UID 匹配的 `at`/`batch` 任务、账号、家目录、SSH key、sudo 授权和本工具创建的 sshd 例外;若账号已在程序外消失,只清理可安全识别的登记、按用户名授权和任务,不会猜测删除失去身份见证的 Home/mail。若安全检查或清理失败,程序会返回非零,保留账号(若仍存在)和登记,并尽力禁用仍存在的账号,供 systemd 重试或人工处理。即使启用了自动撤销,用完后也应立即手动撤销。
默认会安排在 24 小时后自动撤销。对于仍可用完整身份核对的账号,撤销成功会删除个人 crontab、UID 匹配的 `at`/`batch` 任务、账号、家目录、SSH key、sudo 授权和本工具创建的 sshd 例外;若账号已在程序外消失且没有删除恢复见证,只清理可安全识别的登记、按用户名授权和任务,不会猜测删除 Home/mail。已经授权的撤销若提前持久化了恢复见证,则只允许继续核对 UID 属主的传统单文件 mail spool 清扫,绝不会按缺失账号的旧路径恢复性删除 Home。若安全检查或清理失败,程序会返回非零,保留账号(若仍存在)和登记,并尽力禁用仍存在的账号,供 systemd 重试或人工处理。即使启用了自动撤销,用完后也应立即手动撤销。

## 常用命令

Expand Down
15 changes: 15 additions & 0 deletions docs/installing.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ This guide is for administrators who install and maintain `linux-temp-admin`. Se

The binary has no dynamic-library or language-runtime dependency. Account lifecycle operations still use the system's `id`, `useradd`, `userdel`, `usermod`, and `chage`; password login additionally requires `chpasswd`, while granting sudo requires `sudo` and `visudo` for pre-commit policy validation. The tool does not fall back to a distro `adduser`/`deluser` or an arbitrary BusyBox account applet: command names alone cannot prove equivalent arguments, configuration, or compile-time shadow/group semantics. Missing tools can be installed through apt, dnf, yum, or apk after interactive confirmation.

### Conventional mail-spool compatibility boundary

Account creation and revocation inspect the actual metadata of `/var/mail` and `/var/spool/mail` instead of allowing a layout by distribution name alone. The FHS does not specify an owner or mode for these directories. The following layouts were checked for this change; they do not make every custom layout automatically supported:

| Distribution/family | Actual mail spool | Other path | Verified owner and mode |
| --- | --- | --- | --- |
| Debian 12/13, Ubuntu 22.04/24.04 | `/var/mail` | `/var/spool/mail -> ../mail` | `root:mail 2775` |
| RHEL, Rocky, Alma, Oracle Linux, Fedora, CentOS, and Amazon Linux families | `/var/spool/mail` | `/var/mail -> spool/mail` | `root:mail 0775` |
| Alpine | `/var/mail` | depends on the installation | `root:root 0755` |
| current Arch Linux `filesystem` package | `/var/spool/mail` | `/var/mail -> spool/mail` | `root:root 1777` |

The product policy accepts only a real root-owned mail-spool directory. It may belong to group `mail` and carry setgid; if it is world-writable it must have sticky protection, and any setuid bit is refused. The observed `root:mail 3777` and Arch's `root:root 1777` therefore work, while mode `0777`/`2777` without sticky, a non-root owner such as `mail:mail`, or a symlink escaping the two paths fails closed before `useradd`. Mail-delivery services and local identities authorized to write this directory are inside the trust boundary.

This compatibility covers only the traditional single-file mbox at `/var/mail/<username>` or `/var/spool/mail/<username>`. The tool does not traverse Maildir or treat aaPanel's `/www/vmail` as a mail spool. A complete revoke still removes the whole tool-managed Home under its separate Home-safety rules.

Arch Linux has no safe partial-upgrade mode, while `pacman -Syu` upgrades the whole system. The tool therefore never runs pacman automatically while creating an account. Complete the prompted upgrade and dependency installation deliberately first.

## Convenience install
Expand Down
15 changes: 15 additions & 0 deletions docs/installing.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@

二进制本身不依赖动态库或语言运行时。账号生命周期仍会使用系统的 `id`、`useradd`、`userdel`、`usermod` 和 `chage`;密码登录还需要 `chpasswd`,授予 sudo 时还需要 `sudo` 和用于写入前策略校验的 `visudo`。程序不回退到发行版 `adduser`/`deluser` 或任意 BusyBox 账号 applet:这些实现的参数、配置及编译期 shadow/group 语义不能仅凭命令名证明与 shadow 工具链等价。缺失依赖可在交互确认后通过 apt、dnf、yum 或 apk 安装。

### 传统系统邮箱目录兼容边界

账号创建与撤销会检查 `/var/mail` 和 `/var/spool/mail` 的实际元数据,而不是只按发行版名称放行。FHS 没有规定这些目录的属主和模式;以下是本次核验到的常见布局,不代表其他自定义布局自动受支持:

| 发行版/系列 | 实际邮箱目录 | 另一路径 | 已核验属主与模式 |
| --- | --- | --- | --- |
| Debian 12/13、Ubuntu 22.04/24.04 | `/var/mail` | `/var/spool/mail -> ../mail` | `root:mail 2775` |
| RHEL、Rocky、Alma、Oracle Linux、Fedora、CentOS、Amazon Linux 系 | `/var/spool/mail` | `/var/mail -> spool/mail` | `root:mail 0775` |
| Alpine | `/var/mail` | 依具体安装而定 | `root:root 0755` |
| Arch Linux 当前 `filesystem` 包 | `/var/spool/mail` | `/var/mail -> spool/mail` | `root:root 1777` |

产品策略只接受 root-owned 的真实系统邮箱目录;目录可以属于 `mail` 组并带 setgid,world-writable 时则必须有 sticky bit,同时任何 setuid 都会拒绝。因此现场可见的 `root:mail 3777` 和 Arch 的 `root:root 1777` 均兼容,而无 sticky 的 `0777`/`2777`、`mail:mail` 等非 root 属主及逃出上述两个路径的符号链接会在 `useradd` 前失败关闭。邮件投递服务和获准写入该目录的本地身份属于信任边界。

这里的兼容性只针对 `/var/mail/<用户名>` 或 `/var/spool/mail/<用户名>` 的传统单文件 mbox;程序不会把 Maildir 或宝塔 `/www/vmail` 当作系统邮箱目录遍历。完整撤销仍会按独立的 Home 安全规则清理本工具管理的整个 Home。

Arch Linux 不允许安全的部分升级,而 `pacman -Syu` 会升级整个系统,因此本工具不会在创建账号时自动运行 pacman。请根据提示由管理员先完成完整升级和依赖安装。

## 便利安装
Expand Down
8 changes: 6 additions & 2 deletions docs/operator-guide.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,19 @@ Status reports account identity, UID, expiry, auto-delete task, and registry ano
/usr/bin/sudo /usr/local/sbin/linux-temp-admin revoke --user xxvcc-a1b2c3d4e5
```

When the complete identity can still be checked, revoke first disables login; removes and verifies the personal crontab and target-UID `at`/`batch` jobs; waits for a 65-second daemon drain; repeats job/process cleanup; and then removes the account, deterministic `/home/<username>` directory, any UID-matched conventional mail spool, public key, sudoers grant, account-scoped sshd exception, and automatic task. If the account disappeared outside the tool, it cleans only the registry, name-scoped grants, and tasks that remain safely identifiable. Recursive Home cleanup proceeds only for a real directory owned by the registered account's UID/GID with no mount boundary underneath; a mail spool must likewise be a non-symlink regular file in an accepted system mail directory and is swept again after account absence is confirmed. Home cleanup uses directory descriptors and rejects a symlink at the Home root; an internal symlink is unlinked without following its target. Traversal checks cooperative budgets of 100,000 entries, 128 levels, and two minutes between filesystem calls, so the deadline cannot interrupt one blocked filesystem call. Cron/at and process results are repeated snapshots, not an atomic freeze. If a safety condition, resource limit, job/process inventory, or name-scoped grant cannot be confirmed, revoke attempts to disable the account, retains any surviving account and the registry witness, and returns nonzero so username reuse cannot inherit old data, deferred work, or privilege.
When the complete identity can still be checked, revoke first disables login; removes and verifies the personal crontab and target-UID `at`/`batch` jobs; waits for a 65-second daemon drain; repeats job/process cleanup; and then removes the account, deterministic `/home/<username>` directory, any UID-matched conventional mail spool, public key, sudoers grant, account-scoped sshd exception, and automatic task. If the account disappeared outside the tool, it cleans only the registry, name-scoped grants, and tasks that remain safely identifiable, plus the narrow mail-spool cleanup authorized by an existing deletion-recovery witness. Recursive Home cleanup proceeds only for a real directory owned by the registered account's UID/GID with no mount boundary underneath. Home cleanup uses directory descriptors and rejects a symlink at the Home root; an internal symlink is unlinked without following its target. Traversal checks cooperative budgets of 100,000 entries, 128 levels, and two minutes between filesystem calls, so the deadline cannot interrupt one blocked filesystem call. Cron/at and process results are repeated snapshots, not an atomic freeze. If a safety condition, resource limit, job/process inventory, or name-scoped grant cannot be confirmed, revoke attempts to disable the account, retains any surviving account and the registry witness, and returns nonzero so username reuse cannot inherit old data, deferred work, or privilege.

Mail-specific cleanup handles only a traditional single-file mbox at `/var/mail/<username>` or `/var/spool/mail/<username>`. An existing mail spool must be root-owned and have no setuid bit; a world-writable spool must have sticky protection. Thus `root:mail 3777` and Arch Linux's `root:root 1777` work, while mode `0777`/`2777` without sticky and a non-root owner such as `mail:mail` fail closed. The target mailbox must also be a non-symlink regular file owned by the captured UID. `invite` checks the spool before `useradd` and again after the UID is known. A preflight failure leaves no account; a post-helper failure attempts rollback and retains an expired, locked, credential-less account plus its registry witness when cleanup cannot be confirmed. This specialized path neither searches nor traverses Maildir and never touches aaPanel's `/www/vmail`; a Maildir inside a tool-managed Home is still removed with the whole Home under the normal complete-revocation rules.

Before deleting an `at` job, the tool rereads its body and rechecks the UID or exact revoke command so a reused job ID cannot authorize deletion of an unrelated task. `at` has no atomic compare-and-delete interface, so a very short local-root trust-boundary interval remains between that read and `atrm`.

For compatibility with old automatic tasks, a `revoke --yes` command without UID/generation arguments cannot prove that its old deletion intent still names the same account if it collides with a concurrent same-name `invite`. The command warns explicitly, deletes no account, and exits successfully so systemd cannot retry the old task against the new generation; a manually issued non-interactive command of the same shape follows the same rule. After the concurrent operation finishes, run `doctor` and invoke `revoke` again against the current account.

An account reported by `doctor` as `legacy-unverified` carries an old fixed identity marker, so same-name/same-UID reuse cannot be excluded. After manual inspection, it can be recovered only by running `revoke --user <name> --force` in an interactive terminal and typing the complete username. The historical timer's `--yes --force --confirm-force` arguments and every other non-interactive invocation are denied deletion authority for this account class. `doctor` reports any surviving old task as orphaned, and `cleanup-expired --compact` cancels that task while retaining the live account and registry row for manual handling.

After every pre-deletion check passes, the tool persists a deletion-recovery witness before invoking `userdel`. If account deletion, the post-deletion mail-spool sweep, or task cleanup is interrupted, `status` and `doctor` show the recovery state, a same-name `invite` refuses to overwrite the witness, and `cleanup-expired --compact` does not discard the witness. When the account is absent or still exactly matches the recorded generation, run `revoke --user <name>` to resume. An identifiable automatic task is retained in either state, but only a systemd job retries automatically under its restart policy; `at` and legacy one-shot jobs require a manual retry. Legacy, unregistered, and pending-rollback paths retain only a UID witness. If that account is still live, inspect it and run `revoke --user <name> --force` in an interactive terminal, then type the complete username; every non-interactive invocation is refused. The old automatic task for such a live account is treated as orphaned and cancelled, while the registry witness remains for manual recovery.
After upgrading from v2.9.1 or another older release, manually verify any live pending creation row shown by `status` or `doctor` really came from a failed invite. Recovery requires `revoke --user <name> --force` directly in an interactive terminal plus the full username. The tool also checks the random pending generation, GECOS, recorded UID (zero or the current UID), managed Home, non-root UID/GID, and nonempty shell. Only then does it convert the row to a UID-only deletion-recovery witness before cleanup. `--yes`, automatic revoke, uninstall bulk removal, piped input, and every identity mismatch refuse deletion.

After deletion authorization, identity checks, and the pre-artifact job/process quiescence pass succeed, the tool persists a deletion-recovery witness before controlled mail/Home cleanup and `userdel`; after Home cleanup it checks jobs, processes, and the complete identity again immediately before `userdel`. If account deletion, the post-deletion mail-spool sweep, or task cleanup is interrupted, `status` and `doctor` show the recovery state, a same-name `invite` refuses to overwrite the witness, and `cleanup-expired --compact` does not discard the witness. When the account is absent or still exactly matches the recorded generation, run `revoke --user <name>` to resume. For an absent account, every narrow mail sweep confirms before and after that neither the local passwd database nor NSS contains the name. An identifiable automatic task is retained in either state, but only a systemd job retries automatically under its restart policy; `at` and legacy one-shot jobs require a manual retry. Legacy, unregistered, and pending-rollback paths retain only a UID witness. If that account is still live, inspect it and run `revoke --user <name> --force` in an interactive terminal, then type the complete username; every non-interactive invocation is refused. The old automatic task for such a live account is treated as orphaned and cancelled, while the registry witness remains for manual recovery.

Deleting an unregistered account requires explicit `--force` and an additional username confirmation; it does not override protection for reserved names, UID 0, or unregistered/legacy-identity low-UID accounts. If a system assigns a new tool-created account a low UID, it remains normally revocable only while the current registry UID, random generation, and exact GECOS marker are fully bound. A real account without the tool's exact marker is never treated as managed.

Expand Down
Loading