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
11 changes: 8 additions & 3 deletions .agents/skills/dsh-auth-gate-config/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: dsh-auth-gate-config
description: Use when the user asks about dsh-auth-gate configuration - supported options (mode/totp/sessionTtl/cookieName/tokenRef/cookieSecure/usersFile/publicHost/revokeSweepMs/clientIpHeader/trustedProxyCidrs/logoutOrder), how to configure them, the dsh-auth CLI (user add/passwd/role/list/disable/enable/totp, skill install), the password policy (14+ chars, four character classes, not the old password), the Settings account-security block (self-service change, admin reset), login trouble, rate limiting, or the logout button. Ships with the dsh-auth-gate package; install with `dsh-auth skill install`. / 用户询问 dsh-auth-gate 配置(mode/totp/sessionTtl/cookieName/tokenRef/cookieSecure/usersFile/publicHost/revokeSweepMs/clientIpHeader/trustedProxyCidrs/logoutOrder)、dsh-auth CLI(user add/passwd/role/list/disable/enable/totp、skill install)、口令策略(14 位起、四类字符、不得同旧)、设置面板账号安全块(自助改密、管理员重置)、登录故障、限速或退出按钮时使用。随 dsh-auth-gate 包发布;用 `dsh-auth skill install` 安装。
description: Use when the user asks about dsh-auth-gate configuration - supported options (mode/totp/sessionTtl/cookieName/tokenRef/cookieSecure/usersFile/publicHost/revokeSweepMs/clientIpHeader/trustedProxyCidrs/logoutOrder), how to configure them, the dsh-auth CLI (user add/passwd/role/list/disable/enable/totp, skill install), the password policy (14+ chars, four character classes, not the old password), the Settings account-security block (self-service change, admin reset), login trouble, the first-run setup wizard, rate limiting, or the logout button. Ships with the dsh-auth-gate package; install with `dsh-auth skill install`. / 用户询问 dsh-auth-gate 配置(mode/totp/sessionTtl/cookieName/tokenRef/cookieSecure/usersFile/publicHost/revokeSweepMs/clientIpHeader/trustedProxyCidrs/logoutOrder)、dsh-auth CLI(user add/passwd/role/list/disable/enable/totp、skill install)、口令策略(14 位起、四类字符、不得同旧)、设置面板账号安全块(自助改密、管理员重置)、登录故障、首次配置向导、限速或退出按钮时使用。随 dsh-auth-gate 包发布;用 `dsh-auth skill install` 安装。
# 低频查询技能:不让模型自动发现/调用(避免常驻技能目录稀释注意力),
# 用户显式打开技能面板调用(user-invocable 默认 true,UI 显示 "user-only")。
disable-model-invocation: true
Expand All @@ -17,9 +17,9 @@ CLI 用法、常见故障时使用。
## Triggers / 触发词

auth-gate / login door / logout button / cookieSecure / usersFile / logoutOrder /
mode / totp / two-factor / authenticator code / dsh-auth commands / login failures / rate-limit 429 / config 403
mode / totp / two-factor / authenticator code / dsh-auth commands / login failures / rate-limit 429 / config 403 / first run / bootstrap / setup wizard
auth-gate / 登录门 / 退出登录按钮 / cookieSecure / usersFile / logoutOrder / mode /
totp / 两步验证 / 验证码 / dsh-auth 命令 / 登录不上 / 限速锁定 429 / 配置面 403
totp / 两步验证 / 验证码 / dsh-auth 命令 / 登录不上 / 限速锁定 429 / 配置面 403 / 首次运行 / 配置向导 / 引导会话

## What it is / 一句话定位

Expand Down Expand Up @@ -59,6 +59,7 @@ Override by `id` in `$DSH_HOME/cordis.patch.yml`:
`.credentials.yaml`(`DSH_AUTH_TOKEN`)。
- `users.yaml` / `.credentials.yaml` must be `0600`. / 均 0600。
- Token mode then hands the browser dsh's own launch token after a successful sign-in (a `302` to `/?token=...`, needs dsh >= 0.1.2-alpha), so you never paste `?token=` by hand; on older hosts the login keeps its plain redirect. / token 模式登录成功后会把 dsh 的 launch token 交给浏览器(`302` 到 `/?token=...`,需 dsh >= 0.1.2-alpha),不用手动粘 `?token=`;旧版宿主退回普通跳转。
- First run needs no shell: an **unconfigured** instance (the credential read succeeds and returns nothing, or `users.yaml` has zero users) turns dsh's launch URL into a one-time bootstrap session: `/auth/bootstrap` validates the token, the browser lands on `/auth/setup`, and the wizard either generates and saves the shared token or creates the first administrator. The window closes as soon as one of those exists, so the same launch URL then goes to `/auth/bootstrap`, which finds the instance configured and answers `302 /auth/login?next=%2F` with no cookie; that is expected, not a lost credential. Remove the token or the last user and restart dsh to get the wizard back. The wizard cannot change `mode`/`totp`/`cookieSecure`/`sessionTtl`/`proxy`, which need `$DSH_HOME/cordis.patch.yml` plus a restart. / 首次运行不用开 shell:**未配置**的实例(凭据读取成功但为空,或 `users.yaml` 零用户)会把 dsh 的启动链接换成一次性 bootstrap 会话,`/auth/bootstrap` 校验令牌后浏览器落到 `/auth/setup`,向导在那里生成并保存共享令牌,或创建第一个管理员。两者之一出现后窗口关闭,同一条启动链接改成先到 `/auth/bootstrap`、再回 `302 /auth/login?next=%2F`,全程不下发 cookie;这是预期,不是凭据丢了。要重开向导,删掉令牌或最后一个用户再重启 dsh。向导改不了 `mode`/`totp`/`cookieSecure`/`sessionTtl`/`proxy`,这些得改 `$DSH_HOME/cordis.patch.yml` 再重启。

## CLI (`dsh-auth`)

Expand Down Expand Up @@ -123,6 +124,9 @@ change, 0.16.0 user management). / 密码模式在 设置 → 账号安全 有
| `dsh-auth-proxy` exits right after start | Since 0.16.0 `--target` is required (there is no default upstream); since 0.17.0 the bin also works when it is reached through a symlink. Pass `--listen 127.0.0.1:8443 --target <https url>`. / 代理起来就退出:0.16.0 起 `--target` 必填(不再有默认上游);0.17.0 修了 symlink 入口。写成 `--listen 127.0.0.1:8443 --target <https url>` |
| Opening dsh from another site shows `401 dsh web authentication required` | Fixed: a cross-site top-level navigation (link from another site, `127.0.0.1` -> `localhost`) carries the Lax `dsh_auth` cookie but not dsh's Strict cookie; the gate now answers an authenticated one with a same-origin bounce page that re-navigates to the same URL (brief blank flash; without JS click `Continue`). Upgrade the plugin; F5 never helped. / 已修复:跨站顶层导航只带 Lax 的 `dsh_auth`,不带 dsh 的 Strict cookie;门现对已认证请求回同源弹回页重新导航到同一 URL(可能闪一下;无 JS 点 `Continue`)。升级插件即可,刷新无效 |
| Upgrading the host to dsh 0.2.x | Since 0.16.0 the package declares its host corridor in both `engines.dsh` and the storage-domain peer (identical strings, D12/D26); install or upgrade through the host's own `dsh plugin` and the gate mounts normally. / 升级宿主到 dsh 0.2.x:0.16.0 起包内在 `engines.dsh` 与 storage-domain peer 两处声明同一条走廊(D12/D26),照常用宿主自己的 `dsh plugin` 安装或升级即可 |
| Fresh install, but the launch URL shows the Access token box, no wizard | The instance already counts as configured, so the window is closed: the token reference resolves to something non-empty, `users.yaml` holds at least one user, or the probe errored (unreadable file, bad permissions). Clear the credential or the user list and restart; a cached credentials value may need exactly that restart. / 全新安装却停在 Access token 输入框、没有向导:实例已被判为已配置(令牌引用非空、`users.yaml` 至少一个用户,或探针读取出错)。清掉凭据或用户列表再重启;credentials 缓存可能正需要这次重启 |
| Login over plain http, then back on the login page | `cookieSecure: true` (the default) makes the browser refuse the session cookie on `http://`. Set it to `false` for plain-http tests and keep `true` behind TLS. / 纯 http 登录后又回到登录页:`cookieSecure` 默认 true,浏览器在 `http://` 下不保存会话 cookie。纯 http 测试设 false,TLS 之后保持 true |
| The launch URL stopped working after setup | Expected: the wizard closes the window once a token or a user exists, and the URL then goes to `/auth/bootstrap` and ends at `302 /auth/login?next=%2F`. Sign in on the login page; to reopen the wizard, remove the token or the last user and restart dsh. / 配好之后启动链接进不来:预期行为,令牌或用户一出现窗口就关闭,之后该 URL 先到 `/auth/bootstrap`、再回 `302 /auth/login?next=%2F`。用登录页登录;要重开向导就删掉令牌或最后一个用户再重启 |

## Verification tips / 验证技巧

Expand All @@ -144,6 +148,7 @@ Run on an isolated test instance. / 隔离测试实例上验证:
page (6-digit code input); the challenge lasts ≤5 min and rides the session
cookie. / TOTP 第二步:密码正确后登录页切到 "Verify" 页(6 位验证码);
挑战态最长 5 分钟、随会话 cookie 携带。
- First run: on an unconfigured instance `GET /?token=<launch token>` -> `302 /auth/bootstrap?token=...`, then `302 /auth/setup` with a `dsh_auth` cookie whose `Max-Age` is at most 3600 plus `HttpOnly` and `SameSite=Lax`. The wizard shows the generated token once; a second `GET /?token=` goes to `/auth/bootstrap` and then answers `302 /auth/login?next=%2F` with no `Set-Cookie`. / 首次运行:未配置实例上 `GET /?token=<启动令牌>` 先 302 到 `/auth/bootstrap?token=...`,再 302 到 `/auth/setup`,同时下发 `dsh_auth` cookie(`Max-Age` 不超过 3600、`HttpOnly`、`SameSite=Lax`)。向导只显示一次令牌;之后再 `GET /?token=` 会先到 `/auth/bootstrap`、再 302 到 `/auth/login?next=%2F`,且不带 `Set-Cookie`。

- Account security: signing in as an admin shows the user list in Settings; a non-admin
sees only the self-service form. A reset makes the target user set a new password
Expand Down
90 changes: 78 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ in first.
- [What it does not do](#what-it-does-not-do)
- [The market's credentials badge](#the-markets-credentials-badge)
- [Quick start](#quick-start)
- [First run](#first-run)
- [See it in action](#see-it-in-action)
- [Configuration](#configuration)
- [Command-line tool](#command-line-tool)
Expand Down Expand Up @@ -119,26 +120,55 @@ If you would rather run an instance that never reads a credential, use password

## Quick start

Install the plugin, restart dsh, and open the link dsh prints on startup. A fresh
install has nothing configured yet, so that link goes straight into the first-run
wizard: getting in needs no shell command and no configuration edit.

```sh
# 1. Install the plugin from npm into your dsh profile.
# Since 0.4.1 the package declares a `dsh.bundle` manifest, so `dsh plugin add`
# also registers the mount (dsh.profile.bundles) automatically:
dsh plugin --profile web add dsh-auth-gate

# 2. Create an admin account.
# `dsh plugin add` installs the plugin into the profile's node_modules
# ($DSH_HOME/profiles/web, default ~/.dsh/...) — the CLI is NOT added to your
# PATH, so call it through the profile. `dsh plugin` already requires pnpm:
printf '%s\n' 'choose-a-strong-password' | \
pnpm --dir "$DSH_HOME/profiles/web" exec dsh-auth user add admin --password-stdin

# 3. Turn on password login: override the plugin config in $DSH_HOME/cordis.patch.yml
# (a ready-to-use config-override template ships in deploy/cordis.patch.yml;
# see Configuration below — the mount itself needs no manual patch row)

# 4. Restart dsh. Open your site — you will be asked to sign in.
# 2. Restart dsh, then open the URL it prints at startup
# (http://<host>:<port>/?token=<launch token>).
dsh --profile web
```

The wizard offers what the current mode allows: `token` mode, the shipped default,
generates and saves the shared access token, and `password` mode creates the first
administrator. The new token is shown once on the result page, and one click continues
into dsh. From the next visit on that launch link is just a URL again: sign in on the
login page with the token or the password you just set. Everything else (more users,
TOTP, changing `mode`) still happens in the shell: see [First run](#first-run),
[Configuration](#configuration) and [Command-line tool](#command-line-tool).

## First run

Whether the launch link opens the wizard is decided on every request, from one
question: is this instance configured?

- **The default is `mode: "token"`.** With no `DSH_AUTH_TOKEN`, an unconfigured
token-mode instance rejects everyone, so the launch link is the only way in. Only a
successful read that returns nothing counts as unconfigured; a missing credentials
service or a throwing read is treated like a configured instance and opens no wizard.
`password` mode follows the same rule while `users.yaml` is missing or holds no users.
- **What `?token=` means.** It is dsh's own launch token, printed at startup and valid
only for the lifetime of that dsh process. The plugin takes it as proof that you can
read the server console, then issues a bootstrap session: at most one hour, and it
can open nothing but the plugin's own `/auth/*` pages.
- **What the wizard can change.** It generates and saves the shared token (token mode)
or creates the first administrator (password mode), and the window closes as soon as
one of the two exists. It cannot change `mode`, `totp`, `cookieSecure`, `sessionTtl`
or `proxy`: edit `$DSH_HOME/cordis.patch.yml` and restart dsh for those.
- **What happens to the launch URL afterwards.** It stops admitting you, in two hops: the
gate first bounces the request to `/auth/bootstrap?token=...`, that endpoint finds the
instance configured, and it answers `302 /auth/login?next=%2F`. Neither hop sets a
cookie and nothing is let through, so the launch token is not a second key.
- **How to open the wizard again.** Remove the saved token (or leave `users.yaml` with
no users) and restart dsh. If a credentials service hands back a cached value, that
restart is what makes it see the change.

## See it in action

Every screenshot below uses the **English UI**, and both READMEs share the same set of
Expand Down Expand Up @@ -386,6 +416,34 @@ request the gate allows, it answers with a tiny same-origin page that re-navigat
same URL, so the second request carries both cookies. You may see a brief blank flash; no action is
needed. Without JavaScript, click the `Continue` link.

### The session cookie does not stick on plain HTTP

With `cookieSecure: true`, the default, the browser is told to store the session cookie
for HTTPS only. On a plain `http://` site the login submits, the server answers normally,
and the browser then drops the cookie, so you land back on the login page with no
session. Set `cookieSecure: false` for a plain-http test instance and keep it `true`
wherever TLS terminates in front of dsh. The same trap bites twice when
`$DSH_HOME/cordis.patch.yml`, a layer every profile shares, sets `true` while a test
profile sets `false`: boot the test profile with `--patch` to override it.

### The launch URL no longer signs me in

That is expected once the instance is configured. The launch token only opens the
first-run wizard, and the wizard closes that window the moment a token or a user exists;
afterwards the request goes to `/auth/bootstrap?token=...`, that endpoint finds the
instance configured, and it answers `302 /auth/login?next=%2F` without setting a cookie,
exactly as before the wizard existed. Sign in on the login page instead. Nothing is lost:
to open the wizard again, remove the saved token (or leave `users.yaml` with no users) and
restart dsh.

### The launch link lands on the login page right after a restart

dsh opens the launch URL while the credentials service may still be starting up. Until
that service answers, the plugin cannot tell "not configured" from "cannot read the
configuration", so it fails closed and shows the login page. Reload the link once the
instance has finished booting: on a truly unconfigured instance the wizard opens, and on a
configured one the login page is the correct destination.

## Deployment

- [Reverse-proxy deployment guide](docs/deployed/reverse-proxy.md) — Caddy/nginx
Expand Down Expand Up @@ -477,6 +535,14 @@ is in `docs/deployed/known-limitations.md`.
it sign in (the login path rejects disabled users), so run `dsh-auth user enable <name>` first.
- An administrator without TOTP makes the admin endpoints single-factor; enable TOTP for
administrators in production.
- The first-run wizard is a one-time window: its bootstrap session lasts at most one hour,
can only reach `/auth/*`, and closes as soon as a token or a user exists. Removing that
token (or the last user) and restarting dsh opens it again.
- The wizard assumes a single dsh process, because each process mints its own launch token;
behind several replicas without sticky routing the request can land on a process that does
not know the token and falls back to the login page. A credentials service that caches
values may need a restart before the token saved in the wizard becomes the only credential,
and dsh versions without `connection.authenticatedUrl` have no wizard at all.
- The plugin protects dsh's web surface only. Keep the OS user and the config files
private.

Expand Down
Loading
Loading