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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:

- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '1.26.6'
go-version: '1.26.10'
check-latest: false
cache: false

Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ All notable changes to this project are documented here.

## Unreleased

## v2.10.9 - 2026-10-23

- Refuse bootstrap reinstalls that would lower the installed version before
executing the signed candidate. Hold the lifecycle lock through the version
decision, atomic installation and uninstall-marker reactivation; require an
exact release and explicit consent for historical or unknown-version recovery.
- Preserve OpenSSH Include path semantics through symlink parents and globbed
`.` / `..` components. Distinguish recursive includes from completely scanned
inodes, and verify the scanner against real sshd configurations.
- Restore Latest after HUP, INT or TERM interrupts an unfinished publication,
including repeated signals. Preserve a private record of release identities
when restoration fails while removing credential-bearing temporary files.
- Raise the Go security floor and reproducible release/rebuild pins to 1.26.10;
check that these pins remain aligned.
- Remove the unused direct-at-ID cancellation API and adapters; keep cancellation
bound to verified job ownership and command contents. Document the compatibility
matrix and requirements for future lifecycle-concurrency changes.

## v2.10.8 - 2026-09-26

- Keep automatic cleanup scheduled until absent-account database, sudoers and
Expand Down
29 changes: 28 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The tool lives in `cmd/` and `internal/`, and ships as a signed static Go binary

## Local Checks

**Go** — requires Go 1.26.6+:
**Go** — requires Go 1.26.10+:

```bash
go build ./...
Expand Down Expand Up @@ -62,6 +62,33 @@ For changes that touch account creation, revoke, sudoers, systemd timers, or `at
- Update both languages of every affected user document when user-facing behavior changes.
- Add or update tests for validation, parsing, quoting, and safety boundary changes.

Keep compatibility and recovery decisions in their existing owners: registry
cross-field validation belongs in `Record.lifecycleState`, live-account
classification in `classifyRegisteredAccount`, and at cancellation in the
owner/body-checked scheduler path. Do not add another ID-only cancellation API or
another independent interpretation of the same lifecycle flags.

The supported compatibility matrix includes v2–v5 registry input, v1 task
namespaces, systemd and at scheduling, and native atd enablement on systemd,
OpenRC and SysV hosts. Removing a legacy branch requires an explicit supported
upgrade floor and a tested migration that leaves no old account, grant or task
without recovery. Consolidate equal contracts and shared test vectors first;
do not remove backend or identity checks merely to shorten the code.

Explicit-name creation and legacy identity migration can hold the global
lifecycle lock during the cron/at drain. Other revocations queue behind that
operation; the 65-second drain is not an upper bound on total lock time. A future
concurrency change must persist a disabled intermediate identity, retain the
same-name barrier, and revalidate generation, uninstall state and scheduler
ownership after reacquiring the lock. The existing generated-name fast path and
asynchronous quarantine are the preferred ways to avoid that wait.

Release and offline reconstruction use the exact patched Go version declared in
`go.mod`. Update both pins and their operator instructions together; ordinary CI
uses the latest patch within the supported Go series and cannot substitute for
checking the release pin. Test semantic boundaries with real signals and sshd
where available, in addition to checking the release scripts' structure.

## Pull Request Checklist

- [ ] `build`, `vet` (with `-printf.funcs`), `gofmt`, and `test -race` pass; integration tests pass or are unaffected. (`scripts/` changes: syntax checks, mirror receiver tests, and ShellCheck pass.)
Expand Down
12 changes: 11 additions & 1 deletion docs/installing.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This guide is for administrators who install and maintain `linux-temp-admin`. Se
- amd64 or arm64;
- primary support for Debian, Ubuntu, RHEL, Rocky, AlmaLinux, Fedora, and common aaPanel environments;
- best-effort support for Alpine and Arch Linux;
- root access plus curl, OpenSSL 3, sha256sum, and timeout for installation;
- root access plus curl, OpenSSL 3, sha256sum, timeout, sync, and flock for installation;
- `getent` or `nslookup` for GitHub CDN fallback, so every redirect target can be validated and pinned to a public address.

The binary has no dynamic-library or language-runtime dependency. Account lifecycle operations still use the system's `id`, `useradd`, `userdel`, `groupdel`, `usermod`, and `chage`; password login additionally requires `chpasswd`, while granting sudo requires `sudo` and `visudo` for pre-commit policy validation. `groupdel` removes a managed same-name private group only when `userdel` leaves it behind and the registry's `SequentialID` proves its GID; it is therefore a base dependency even on distributions where `userdel` normally removes that group itself. 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; the account helpers come from Debian's `passwd`, the RPM families' `shadow-utils`, or Alpine/Arch's `shadow` package. With apt, the tool attempts both the index refresh and package installation; a failed refresh remains an error with its diagnostic even if the old cache lets installation complete.
Expand Down Expand Up @@ -51,6 +51,16 @@ The convenience command obtains the script from the official mirror over HTTPS a

Once started, the installer applies a separate strict verification chain to the binary. SHA-256, the detached ed25519 signature, architecture, and candidate version must all pass before an atomic installation to `/usr/local/sbin/linux-temp-admin`. There is no unsigned or checksum-only fallback.

Reinstallation allows the same or a newer version by default and refuses downgrades. Before executing the candidate, the installer reads its static version witness from the signed bytes and compares it with the installed file under the lifecycle lock. Custom `DEST` paths follow the same rule. An unknown installed version, unsafe installed metadata, or a historical candidate without a witness stops the default installation. The uninstall marker is validated before commit and cleared only after a successful, durable installation at the standard path; custom destinations do not reactivate that installation.

For an intentional downgrade or recovery of an unknown installation, independently verify the target version and installer, then set both an exact release and the recovery switch when running the verified local script:

```bash
/usr/bin/sudo env LTA_RELEASE=v2.10.7 LTA_ALLOW_DOWNGRADE=1 /bin/sh /root/verified-install.sh
```

`LTA_ALLOW_DOWNGRADE=1` cannot be combined with `latest` and does not bypass candidate signatures, architecture checks, or exact version matching. A first installation still has no trusted historical version floor; pin an exact release when replay resistance is required. The installer commits the verified file itself and does not invoke the candidate's local `install` command. Its exit status and output report the result; it does not emit that command's application audit event, so retain the output in deployment records.

## Official mirror and GitHub fallback

The compiled-in preferred release source is:
Expand Down
12 changes: 11 additions & 1 deletion docs/installing.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- amd64 或 arm64;
- 主要支持 Debian、Ubuntu、RHEL、Rocky、AlmaLinux、Fedora 和常见宝塔环境;
- Alpine 和 Arch Linux 为尽力支持;
- 安装需要 root 权限、curl、OpenSSL 3、sha256sum 和 timeout;
- 安装需要 root 权限、curl、OpenSSL 3、sha256sum、timeout、sync 和 flock;
- GitHub CDN 回退还需要 `getent` 或 `nslookup`,用于验证并固定每个重定向目标的公网地址。

二进制本身不依赖动态库或语言运行时。账号生命周期仍会使用系统的 `id`、`useradd`、`userdel`、`groupdel`、`usermod` 和 `chage`;密码登录还需要 `chpasswd`,授予 sudo 时还需要 `sudo` 和用于写入前策略校验的 `visudo`。`groupdel` 用于清理 `userdel` 可能留下、且已由登记中的 `SequentialID` 证明 GID 的受管同名私有组;它因此属于基础依赖,即使目标发行版上的 `userdel` 通常会自行删除该组。程序不回退到发行版 `adduser`/`deluser` 或任意 BusyBox 账号 applet:这些实现的参数、配置及编译期 shadow/group 语义不能仅凭命令名证明与 shadow 工具链等价。缺失依赖可在交互确认后通过 apt、dnf、yum 或 apk 安装;账号 helper 分别由 Debian 系的 `passwd`、RPM 系的 `shadow-utils` 或 Alpine/Arch 的 `shadow` 包提供。使用 apt 时,程序会依次尝试更新索引和安装软件包;即使旧缓存让安装完成,索引更新失败仍会作为错误返回并保留诊断。
Expand Down Expand Up @@ -51,6 +51,16 @@ curl -fsSL https://dl.ll.cd/linux-temp-admin/install.sh | /usr/bin/sudo /bin/sh

安装器启动后,二进制下载进入另一条严格验证链:SHA-256、detached ed25519 签名、架构和候选版本全部通过后,才会原子安装到 `/usr/local/sbin/linux-temp-admin`。不存在未验签或只有校验和的降级路径。

重复安装默认允许同版和新版,拒绝降级。安装器在执行候选程序之前读取已签名字节中的静态版本见证,并在生命周期锁内与当前安装文件的静态版本比较;自定义 `DEST` 使用相同规则。当前版本无法确认、当前文件元数据不安全或历史候选缺少版本见证时,默认停止。卸载标记也会在提交前验证;只有标准路径安装成功并确认文件持久化后才清除标记,自定义路径不会重新激活标准安装。

有意降级或修复未知安装时,先独立确认目标版本和安装脚本,再对已验证的本地脚本同时设置精确版本与恢复开关,例如:

```bash
/usr/bin/sudo env LTA_RELEASE=v2.10.7 LTA_ALLOW_DOWNGRADE=1 /bin/sh /root/verified-install.sh
```

`LTA_ALLOW_DOWNGRADE=1` 不能与 `latest` 一起使用,也不会绕过候选签名、架构和精确版本检查。首次安装仍没有可信的历史版本下限,需要防止历史版本回放时应固定精确发行标签。安装器自行提交已验证文件,不调用候选的本地 `install` 命令;安装结果以其退出状态和输出为准,不生成该命令的应用审计事件,请在部署记录中保留安装输出。

## 官方镜像与 GitHub 回退

程序内置的首选发布源是:
Expand Down
53 changes: 45 additions & 8 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ unset TAR_OPTIONS GZIP BZIP2 BZIP XZ_OPT
GOTELEMETRY=off GOAUTH=off timeout -k 5 30 go version); then
fail "cannot execute the trusted Go toolchain"
fi
[[ "$(awk '{print $3}' <<<"$trusted_go_version")" == go1.26.6 ]] \
|| fail "trusted Go toolchain is not exactly go1.26.6"
[[ "$(awk '{print $3}' <<<"$trusted_go_version")" == go1.26.10 ]] \
|| fail "trusted Go toolchain is not exactly go1.26.10"
signer_arch=$(env -i PATH="$PATH" LC_ALL=C HOME=/root GOROOT= GOENV=off \
GOTOOLCHAIN=local GOFLAGS= GOWORK=off GOEXPERIMENT= GOFIPS140=off \
GOTELEMETRY=off GOAUTH=off timeout -k 5 30 go env GOARCH) \
Expand Down Expand Up @@ -254,7 +254,7 @@ unset TAR_OPTIONS GZIP BZIP2 BZIP XZ_OPT
LTA_TRUSTED_SIGNER
```

The preparation workstation must likewise have Go 1.26.6 installed as its
The preparation workstation must likewise have Go 1.26.10 installed as its
local toolchain. An automatically downloaded toolchain is not sufficient: the
version gate and every reproducible build use `GOENV=off GOTOOLCHAIN=local
GOFLAGS= GOWORK=off` so they cannot inspect one compiler and build with another
Expand Down Expand Up @@ -371,6 +371,31 @@ supply a durable exclusive lock that records its owner and tag and has an
explicit stale-lock recovery policy. Workflow concurrency and a workstation-local
`flock` do not satisfy this requirement.

For this repository, coordinators use the shared GitHub reference
`refs/tags/lta-publication-lock`. Before online preparation, create a unique
annotated tag object whose message records the release tag, exact commit,
authenticated maintainer login, UTC acquisition time and a random operation ID.
Atomically create the fixed reference with GitHub's `POST /repos/xxvcc/linux-temp-admin/git/refs`.
An existing reference means the release is busy; never update or replace it.
If the create request has an ambiguous result, read the reference and proceed
only if it points to this operation's exact tag-object SHA. Record that SHA in
the release evidence and recheck ownership before signing and publication.
The lock tag does not match the `v*` release trigger and is not a version tag.
Every workstation must use this same protocol; it coordinates participating
publishers and does not protect against an authorized maintainer deliberately
deleting the lock. A local lock may additionally coordinate older local tools.

Retain the reference through publication, mirror synchronization and independent
public verification. Release only the owned reference with an atomic expected-SHA
check, using `git push --force-with-lease=refs/tags/lta-publication-lock:<recorded-tag-object-sha>
origin :refs/tags/lta-publication-lock`. Do not use a separate GET followed by an
unconditional REST DELETE, which could remove a successor's lock. Locks do not
expire automatically. After an interrupted operation, first establish that its
recorded coordinator and workflows have stopped, inspect the bound draft/public
release and Latest state, and record the recovery decision. Resume the same
operation or remove only its recorded tag-object SHA with the same lease check;
elapsed time alone is never sufficient to reclaim a lock.

The OpenPGP tag-signing key is separate from the offline ed25519 release key.
Before the first release under this process, generate or import a dedicated
maintainer OpenPGP key whose UID contains an email verified on the `xxvcc`
Expand Down Expand Up @@ -449,7 +474,7 @@ API and require `verification.verified=true`, `verification.reason=valid`, and
an armored OpenPGP signature before continuing. Use that same fingerprint as
`LTA_EXPECTED_TAG_SIGNER_FINGERPRINT` during preparation and publication.

The `Release` workflow uses exactly Go 1.26.6. Its read-only `gate-build` job
The `Release` workflow uses exactly Go 1.26.10. Its read-only `gate-build` job
runs vet, uncached race tests, package-serialized root integration tests,
formatting, shell checks,
the mirror receiver policy tests, a clean-worktree check, and
Expand Down Expand Up @@ -478,7 +503,7 @@ objects, checks the commit's ancestry against GitHub's `main` rather than a
caller-configured `origin`, rejects symlinks and submodules, and exports the
self-contained tree with `git archive`. Ignored/untracked files and candidate
scripts therefore cannot affect the build. It checks the successful Release
workflow and draft, rebuilds with exactly Go 1.26.6, `GOWORK=off`, and the CI
workflow and draft, rebuilds with exactly Go 1.26.10, `GOWORK=off`, and the CI
flags, then `cmp`s every CI binary byte-for-byte. Draft assets are fetched one
at a time through the authenticated Release Asset API after an advertised-size
preflight and under a kernel file-size limit. The source archive has its own size
Expand Down Expand Up @@ -640,9 +665,12 @@ stable release, it clears Latest and confirms the REST Latest route is exactly a
404; authentication or transport errors never count as that empty state. The
Latest route is then independently compared, checksummed, and
signature-verified for both architectures, followed by another highest-version
check and a final immutable-Release check. An EXIT trap performs the same exact
restoration after any error or catchable terminating signal following a possibly
applied promotion.
check and a final immutable-Release check. Explicit HUP, INT, and TERM handlers
exit with a nonzero status and invoke the same EXIT cleanup after a possibly
applied promotion. Cleanup attempts exact restoration once and ignores repeated
HUP, INT, and TERM signals while it runs. Its external helpers remain bounded by
timeouts; if restoration fails, a private recovery record is retained as described
below. SIGKILL and machine loss cannot execute cleanup.
If a publish request fails or returns an ambiguous response while the same bound
Release is public but still mutable, the publisher returns that exact numeric ID
to draft and verifies the rollback. It never follows a replaced tag to another
Expand Down Expand Up @@ -684,6 +712,15 @@ published release to make a retry pass.
If the script reports that automatic Latest restoration itself failed, keep the
release unannounced and retain the organization-wide publication lock. Restore
network/API access, rerun the same publisher first, and inspect its exact result.
The script prints the path to a private `/tmp/lta-publish-recovery.*/context.tsv`
record (directory mode 0700, file mode 0600). It records the repository, candidate
tag and numeric Release ID, commit, independently pinned signed-bundle manifest
hash, baseline highest stable tag and ID, and failure status. Cleanup removes the
credential-bearing publication workspace with a bounded command and reports any
removal failure. The retained record contains no GitHub token, GitHub
configuration, environment dump, or raw API response. Keep this record and the
original signed bundle until recovery is independently verified, then remove the
record. Read the TSV as data; do not source or execute it.
For manual incident recovery only, use the following fail-closed procedure. It
uses decimal-string comparison rather than machine-sized arithmetic, rejects a
noncanonical stable tag or mutable recovery target, excludes the failed `TAG`,
Expand Down
Loading
Loading