Warden(守望者)是一个轻量的准入名单数据服务。

它负责维护“谁可以登录”,以及这些人对应的邮箱、手机号、用户 ID、状态、角色和权限范围等信息。它可以接在 Stargate 后面,作为认证入口背后的准入名册;也可以独立运行,给内部网关、平台、脚本或其他系统提供统一的用户查询能力。

写在前面

上一篇里,我们聊了 Stargate。

它负责把认证逻辑前移到网关侧,让反向代理先替我们拦住请求,而不是让每个内部服务都重复实现登录、鉴权、跳转和会话判断。这一步解决的是一个很基础的问题:

有没有登录?

但门装上之后,很快会遇到下一个问题:

谁可以通过这道门?

一开始,用共享口令就够了。给 Stargate 配一个密码,知道密码的人能进,不知道密码的人进不来。对 HomeLab、测试环境、小团队内部工具,或者几个临时服务来说,这个方案很轻,也很容易跑通。

这没什么问题。

很多内部服务一开始都应该这么做。先把门装上,比继续裸奔要好得多。

但只要使用的人多起来,共享口令就开始变得不那么舒服。有人离开项目之后,要不要换密码?如果换密码,是不是所有还在使用的人都要一起更新?如果密码被复制到聊天记录、脚本、配置文件里,又该怎么算?

更麻烦的是,共享口令只能证明一件事:

这个人知道密码。

但它证明不了另一件事:

这个人是谁?他现在还应该被允许登录吗?

当问题走到这里,就不能只靠一串密码了。你需要一份名单。

这份名单不一定要很复杂。它可以先从一个本地 JSON 文件开始,里面写着谁可以登录,对应的邮箱、手机号、用户 ID、状态、角色和权限范围是什么。但它应该是独立的,可以被 Stargate 查询,也可以被其他内部系统查询;可以先用本地文件维护,后面再接远程数据源、Redis、多实例和监控。

我不希望 Stargate 变成用户数据库。

Stargate 应该继续做入口、会话和登录流程。用户是谁、现在还能不能进、验证码应该发到哪里,这些信息可以单独放到另一个地方。

这就是 Warden 想解决的事情。

它不是完整 IAM,也不想替代公司里的 SSO、LDAP、OIDC 或身份中台。它只先做好一件小事:

维护一份可以被机器稳定查询的准入名册。

soulteary/warden

开源项目地址:https://github.com/soulteary/warden

如果 Stargate 是先把门装上,那么 Warden 做的事情,就是让这道门不要只认一把共享钥匙,让它开始认人。

共享口令的问题是怎么出现的

很多内部服务最早的认证方式,都是从一个共享口令开始的。这没什么问题,尤其是在 HomeLab、测试环境和小团队内部工具里,共享口令很适合做第一步。

它不需要数据库、账号系统和注册流程,也不需要接入复杂的身份服务。只要准备一个密码,再把入口保护起来,服务就可以先安全一点地跑起来。

这也是我在做 Stargate 时比较在意的事情:

先把门装上。

很多时候,第一步不应该太重。如果一开始就要求接 SSO、配回调、建用户表、发验证码、接短信邮件服务,那很多内部小工具最后大概率还是继续裸奔。

所以,共享口令不是问题。

长期只靠共享口令,才是问题。

当服务只有你自己用,或者只有两三个人临时用一下,共享口令足够简单。但只要使用时间拉长,使用的人变多,它的边界就会慢慢出现。

第一个问题,是换密码很麻烦。有人离开项目之后,要不要换密码?如果换,所有还在使用的人都要一起更新;如果不换,这个密码就会变成一把一直流转在外面的钥匙。

第二个问题,是你很难知道密码到底流到哪里去了。它可能在聊天记录里,可能在某个脚本里,可能在浏览器密码管理器里,也可能被复制到某个临时文档、部署说明或者 .env 文件里。一旦它扩散出去,你很难再把它完整收回来。

第三个问题,是共享口令没有“人”的概念。它只能回答一个很粗的问题:

这个请求里带来的密码对不对?

但它回答不了更多后续问题:

  • 这个人是谁;
  • 他现在还在不在项目里;
  • 他应该看到哪些服务;
  • 验证码应该发到哪个邮箱或手机号;
  • 下游服务能不能知道他的角色。

这些问题不是共享口令擅长回答的。因为共享口令本质上只是一把钥匙,谁拿到这把钥匙,谁就能开门。

但真实的访问控制,很多时候并不是只看钥匙。你还会关心拿钥匙的人是谁,他现在是否还应该拿着这把钥匙,他的状态是不是正常,他能不能继续走后面的登录流程。

当问题走到这一步,就需要把“密码”往后退一步,把“人”放到前面来。

这时候,我们需要的不是更长的共享口令,也不是把更多用户信息塞进 Stargate 的环境变量里。我们需要一份名单,一份可以被系统查询的名单。

它不一定复杂。一开始甚至可以只是一个 JSON 文件。但这份名单应该能回答几个基础问题:

  • 这个邮箱在不在;
  • 这个手机号对应谁;
  • 这个用户现在是不是 active
  • 他有没有 user_id
  • 后续验证码应该发到哪里;
  • 下游服务需要的话,能不能拿到 rolescope

这就是 Warden 要补上的那一块,Stargate 负责把门装上,Warden 负责告诉这道门:谁还应该被允许通过。

以前通常怎么解决

遇到“谁可以登录”这个问题,常见做法大概有几种。

第一种,是继续把名单写在认证入口里。

比如把允许登录的邮箱、手机号、用户状态,直接写进 Stargate 的配置里。用户很少的时候,这样当然能用。两三个人、几个测试账号、一两个内部服务,放在环境变量或者配置文件里都不算麻烦。

但这个做法很容易越走越偏。一开始只是加几个邮箱,后来要加手机号,再后来要区分用户状态,接着又想返回 user_idrolescope,让下游服务也能知道当前用户是谁。再往后,可能还想接远程数据源、做缓存、做同步、做审计。

到这一步,认证入口就不再只是认证入口了。它会慢慢变成半个用户数据库。

这不是我希望 Stargate 走的方向。

Stargate 应该继续守入口、管会话、处理登录流程,而不是把所有用户资料都塞进自己身体里。

第二种,是每个服务自己维护一份用户列表。

这也很常见。Grafana 有一份,文档站有一份,内部后台有一份,测试系统有一份,自动化脚本里可能还有一份。

如果这些系统本来就有自己的用户体系,这没什么问题。但很多内部工具并不是这样。它们只是被放在同一个内部环境里,临时给一组人使用。

这时每个服务都维护一份名单,就会出现很现实的麻烦:加一个人,要改很多地方;禁用一个人,也要改很多地方;有的地方改了,有的地方忘了改。最后你很难回答一个简单问题:

这个人到底还能访问哪些东西?

名单一旦散在各处,真正麻烦的不是“添加用户”,而是“确认一个人已经被完整移除”。这件事靠人肉检查,很容易漏。

第三种,是用表格或文档维护名单。

这可能是很多团队最自然的做法。一个表格里写着邮箱、手机号、备注、所属项目、是否还在使用。人看起来很清楚,编辑起来也方便。

如果只是管理流程,表格很好。但如果它开始参与登录流程,问题就来了。

表格适合人看,不适合系统在运行时依赖。

你当然可以把表格作为数据源,比如定期导出成 JSON,或者通过内部接口同步到某个服务。但认证流程本身不应该靠人手复制粘贴,也不应该在登录时临时去翻一个文档。

登录链路需要的是稳定、可查询、可控制返回字段的接口。

不是一个“大家记得去更新”的共享文档。

第四种,是直接接入完整身份系统。

比如公司 SSO、LDAP、OIDC、IAM,或者 Keycloak、Authentik、Authelia 这类更完整的身份方案。

如果你已经有这些东西,并且它们已经稳定服务于团队,那当然应该优先使用它们。完整身份系统能处理的问题更多:用户生命周期、组织架构、权限策略、多因素认证、单点登录、审计、协议兼容,这些都不是一个轻量小工具应该重新发明的东西。

但很多内部服务遇到的问题,并没有一开始就到这个规模。你可能只是有几个 HomeLab 服务、几个测试面板、一个内部文档站、一组临时工具,或者一个还没确定会不会长期使用的小平台。

为了这些东西第一天就接完整 IAM,有时会显得太重。不是能力不好,而是启动成本太高。

你可能要准备数据库、回调地址、证书、邮件服务、管理员账号、用户导入、策略配置,还要让每个服务都理解这套登录方式。最后很容易变成:明明只是想把几个人的访问控制住,却先搭了一整套身份中台。

所以,我想要的是一个中间方案。

它不把用户资料继续塞进认证入口里,也不要求每个服务自己维护一份名单;它可以从本地文件开始,也可以后面再接远程系统。它不负责完整身份治理,不负责登录页面,不负责发送验证码,也不负责校验 OTP。

先做好一件事:维护一份可以被系统查询的准入名册。

这份名单可以很小,也可以慢慢变大。一开始,它只是一个 data.json。后面,它可以接远程 API、Redis、多实例、监控和审计。

但不管后面怎么变,它回答的问题都很具体:

  • 这个人,在不在名单里?
  • 他现在是不是 active
  • 后面的登录流程,应该继续,还是停在这里?

这就是 Warden 想站立的位置。

为什么要把名单单独拆出来

做到这里,其实会有一个很自然的选择:

既然 Stargate 已经站在入口前面了,那能不能直接让 Stargate 维护这份名单?

当然可以。

如果只是几个人、几个邮箱、几个手机号,把它们写进 Stargate 的配置里,并不是不能用。甚至在最早期,这样可能还更省事。

但,我不太想让 Stargate 往这个方向长。

因为一旦认证入口开始维护用户资料,它很快就会多出很多原本不属于自己的事情。比如用户的邮箱是什么,手机号是什么,user_id 是什么,现在是不是 activerolescope 要不要返回给下游服务,名单来自本地文件还是远程系统。如果远程系统挂了,要不要回退到本地数据?多个来源里同一个用户冲突了,又应该以谁为准?

这些问题看起来都和“登录”有关,但它们其实不是入口认证本身。

Stargate 真正要做的事情,是守在门口。它应该关心请求有没有登录、会话是不是有效、没有登录时要不要跳转登录页、API 请求要不要返回 401、登录完成后怎么把用户带回原来的服务。

这些事情已经足够具体了。

如果再把用户资料、名单同步、状态判断、字段过滤、远程数据源、缓存和合并策略都塞进去,Stargate 会变得越来越重。最后它就不再只是一个入口认证网关,而会变成半个用户系统。

这不是我想要的方向。

所以我把“名单”这件事单独拆了出来。Warden 只关心一件事:谁在准入名单里。

它不负责展示登录页,不负责签发会话,不负责发送验证码,不负责校验 OTP,也不打算替代完整 IAM。它要做的是更小的一块:从某个地方拿到用户数据,把这份数据整理好,然后提供一个稳定的查询接口。

调用方可以问它几个很具体的问题:

  • 这个邮箱在不在;
  • 这个手机号对应谁;
  • 这个 user_id 是否存在;
  • 这个用户现在是不是 active
  • 后续验证码应该发到哪里;
  • 如果下游服务需要,能不能拿到 rolescope

Warden 回答这些问题。

至于拿到答案之后,要不要继续登录、要不要发验证码、要不要创建会话,那是调用方的事情。

这样拆开以后,后面替换数据源、验证码通道和入口逻辑时,都不需要互相牵连。

  • Stargate 负责守入口。
  • Warden 负责管名单。
  • Herald 负责送验证码。

这个分工不是为了把系统拆得更复杂,恰恰相反,是为了让每一块都不要长得太快。Stargate 不需要知道名单到底来自哪里,Warden 不需要知道登录页长什么样,Herald 也不需要判断谁有资格登录。每个服务只把自己的事情做好,后面替换起来也会更容易。

比如,一开始你可以让 Warden 只读本地 data.json。等名单变多了,再把不同团队拆成多个 JSON 文件。再往后,如果已经有内部用户系统,可以让 Warden 去同步远程数据。如果服务变多了,再加 Redis、缓存、多实例和监控。

这些变化都不应该影响 Stargate 的入口逻辑。

同样,后面如果验证码通道从邮件换成短信,或者从某个服务商换到另一个服务商,也不应该影响 Warden 的数据来源。

这就是拆开的好处。

不是每个小工具都要做成平台,也不是每个内部服务都需要完整身份中台。很多时候,我们只是需要在共享口令和完整 IAM 之间,补上一个很薄、但能长期维护的中间层。

Warden 就是这个中间层。

它可以接在 Stargate 后面,也可以独立给其他系统用。内部网关可以查它,脚本可以查它,平台可以查它,验证码服务也可以查它。

只要你的问题是:这个邮箱、手机号或 user_id,现在还允不允许继续往下走?那么,Warden 就可以站在这里,先把这件事回答清楚。

Warden 是怎么工作的

Warden 的工作方式很直接:你给它一份用户数据,它把这份数据加载起来,然后通过 HTTP API 对外提供查询能力。

调用方可以用邮箱、手机号或用户 ID 来问它:这个人是不是在名单里?他现在是什么状态?后续验证应该发到哪里?如果下游服务需要,能不能拿到他的 user_idrolescope

Warden 不负责决定整个登录流程。

它只是把准入名单这部分信息整理好,稳定地返回给调用方。至于拿到结果之后,是继续发验证码,还是直接拒绝登录,还是写入会话,那是 Stargate、Herald 或其他调用方要做的事情。

最小的数据文件可以很简单:

[
  {
    "phone": "13800138000",
    "mail": "admin@example.com"
  }
]

这已经能回答一个基础问题:

这个手机号或邮箱,在不在名单里?

但真实使用时,我更建议至少把 user_idstatus 写上。

比如:

[
  {
    "phone": "13800138000",
    "mail": "admin@example.com",
    "user_id": "admin-001",
    "status": "active"
  }
]

这样 Warden 就不只是一个联系方式列表,而是开始变成一份真正的准入名册。

稍微完整一点的数据,可以继续加上角色、权限范围和显示名称:

[
  {
    "phone": "13800138000",
    "mail": "admin@example.com",
    "user_id": "admin-001",
    "status": "active",
    "scope": ["read", "write", "admin"],
    "role": "admin",
    "name": "管理员"
  },
  {
    "phone": "13900139000",
    "mail": "user@example.com",
    "user_id": "user-001",
    "status": "active",
    "scope": ["read"],
    "role": "user",
    "name": "普通用户"
  }
]

这里最重要的字段,是 status

很多时候,名单里有没有这个人,和这个人现在能不能继续登录,不完全是一回事。

比如,一个人曾经在项目里,后来离开了。你可以直接把他从名单里删掉,但有时候,保留记录会更方便排查问题。这时就可以把状态从:

{
  "mail": "user@example.com",
  "status": "active"
}

改成:

{
  "mail": "user@example.com",
  "status": "inactive"
}

或者:

{
  "mail": "user@example.com",
  "status": "suspended"
}

这样调用方看到的就不是一个模糊的“查不到用户”,而是一个更明确的结果:

这个人存在,但现在不应该继续登录。

这对排查问题很有用,也比直接删除更容易留下管理痕迹。

作为独立服务时,Warden 的流程大概是这样:

调用方拿到邮箱、手机号或 user_id
调用 Warden 查询用户
Warden 在准入名单里查找对应用户
Warden 返回用户状态、联系方式和基础身份信息
调用方根据返回结果决定后续动作

如果只是给某个内部脚本或平台使用,这个流程已经够了。

脚本可以问:

admin@example.com 是否在名单里?

内部平台可以问:

这个 user_id 对应的角色是什么?

验证码服务可以问:

后续验证码应该发到邮箱,还是手机号?

这些问题都可以通过同一份名单回答。

如果 Warden 接在 Stargate 后面,流程会更像这样:

用户访问受保护服务
Stargate 发现用户还没有登录
用户输入邮箱或手机号
Stargate 调用 Warden 查询用户
Warden 返回用户是否存在、当前状态和联系方式
Stargate 判断是否继续后续登录流程

通常来说,只有用户存在,并且状态是 active 时,登录流程才应该继续。如果用户不存在,流程应该停下来;如果用户存在,但状态是 inactivesuspended,也不应该继续发验证码。

这件事不应该靠共享口令来判断,也不应该靠人工确认。

它应该变成登录流程里一次稳定的机器查询。

如果后面再接 Herald,流程会再多一步:

用户访问受保护服务
Stargate 发现用户还没有登录
用户输入邮箱或手机号
Stargate 调用 Warden 查询用户
Warden 返回用户状态和联系方式
Stargate 判断用户是否允许继续
允许继续:Stargate 调用 Herald 发送验证码
用户输入验证码
Stargate 完成会话

这个过程里,Warden 仍然只负责名单和状态。后续要不要发验证码、怎么创建会话,都交给调用方继续处理。

这样拆开以后,后面调整起来会轻很多。

比如,用户数据一开始来自本地 data.json,后来你想接远程 API,再后来你想接 Redis、缓存和多实例,这些变化主要发生在 Warden 里。Stargate 不需要知道名单到底从哪里来。

同样,如果验证码以后从邮件换成短信,或者从一个服务商换成另一个服务商,也主要是 Herald 的事情。Warden 仍然只需要返回联系方式和状态。

这就是我希望 Warden 保持的边界。

它不是登录系统,不是验证码系统,也不是完整身份中台。它只是在认证链路里,把原来最容易散落在配置、表格、脚本和聊天记录里的那部分东西,收成一份可以被机器查询的名单。

先让系统知道:谁是这个人。

以及,他现在还能不能继续往下走。

用 Docker Compose 快速跑起来

理解了 Warden 的工作方式之后,我们先把它跑起来。

这一节,我们先不接 Stargate,不接 Herald,不接远程 API,也不启用 Redis。只做一件事:

让 Warden 能从本地文件里加载一份名单,然后回答“这个人是不是在名单里”。

先创建一个 data.json

cat > data.json <<EOF
[
  {
    "phone": "13800138000",
    "mail": "admin@example.com",
    "user_id": "admin-001",
    "status": "active",
    "scope": ["read", "write", "admin"],
    "role": "admin",
    "name": "管理员"
  },
  {
    "phone": "13900139000",
    "mail": "user@example.com",
    "user_id": "user-001",
    "status": "active",
    "scope": ["read"],
    "role": "user",
    "name": "普通用户"
  }
]
EOF

这份文件里只有两个用户:第一个是管理员,第二个是普通用户。它们都有手机号、邮箱、用户 ID 和状态。

第一次跑起来时,重点不是把用户模型一次性想完整,而是确认 Warden 能正常加载数据、保护接口、返回查询结果。所以字段不用一开始就设计得太复杂,先让最短链路跑通。

接着创建 compose.yaml

services:
  warden:
    image: ghcr.io/soulteary/warden:latest
    container_name: warden
    restart: unless-stopped

    ports:
      - "8081:8081"

    environment:
      - PORT=8081
      - MODE=ONLY_LOCAL
      - DATA_FILE=/app/data.json
      - REDIS_ENABLED=false
      - API_KEY=your-secret-api-key-here
      - RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name

    volumes:
      - ./data.json:/app/data.json:ro
      - /etc/localtime:/etc/localtime:ro

    healthcheck:
      test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:8081/health || exit 1"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 10s

这里为了本地演示,直接把 8081 端口映射到了宿主机。

真实部署时,不建议把 Warden 直接暴露到公网。更合适的方式,是让它只跑在内部网络里,由 Stargate、内部网关或其他可信调用方访问。这里先映射端口,只是为了方便本地用 curl 验证。

这段配置里,第一次需要看懂的配置不多。

- MODE=ONLY_LOCAL

表示只使用本地数据文件,也就是刚才创建的 data.json

- DATA_FILE=/app/data.json

表示 Warden 在容器里从这个路径读取用户数据。前面的 volumes 已经把本地的 ./data.json 挂载到了这个位置。

- REDIS_ENABLED=false

表示这个最小示例先不启用 Redis。本地文件、单实例、少量用户的场景里,先禁用它更容易排查问题。

- API_KEY=your-secret-api-key-here

表示查询用户数据时需要带上 API Key。Warden 管的是准入名单,不应该让任何人都能直接查。即使只是本地测试,也建议从第一天就把 API Key 带上。

- RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name

表示接口只返回这些字段。这个配置不是本地测试必须的,但我建议早点用起来。Warden 后面会越来越像一份内部身份目录,字段能少给就少给。

确认文件准备好之后,启动服务:

docker compose up -d

先看容器状态:

docker compose ps

如果服务正常,再看健康检查:

curl http://localhost:8081/health

正常情况下,会看到类似这样的结果:

{
  "status": "ok",
  "details": {
    "redis": "disabled",
    "data_loaded": true,
    "user_count": 2
  },
  "mode": "ONLY_LOCAL"
}

这里重点看两个字段:data_loadedtrue,说明本地名单已经加载成功;user_count2,说明刚才写入的两个用户都被 Warden 读到了。

健康检查通过之后,再查一下完整名单:

curl -H "X-API-Key: your-secret-api-key-here" \
  http://localhost:8081/

也可以查看当前合并后的 data.json

curl -H "X-API-Key: your-secret-api-key-here" \
  http://localhost:8081/data.json

这两个接口更适合排查数据是否加载正确。真实登录流程里,一般不会频繁拉完整名单,更多时候只需要查一个具体的人。

比如按邮箱查询:

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/user?mail=admin@example.com"

按手机号查询:

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/user?phone=13800138000"

或者按用户 ID 查询:

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/user?user_id=admin-001"

如果后面准备接 Stargate、Herald 或其他登录流程,更建议先看这个接口:

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/v1/lookup?identifier=admin@example.com"

identifier 可以是邮箱、手机号或用户 ID。返回结果大概会像这样:

{
  "user_id": "admin-001",
  "destination": {
    "email": "admin@example.com",
    "phone": "13800138000"
  },
  "status": "active",
  "channel_hint": "sms",
  "name": "管理员"
}

这里的 destination 是后续验证流程可能用到的联系方式。如果用户有手机号,channel_hint 可以提示后续流程优先走短信;如果只有邮箱,就可以走邮件。

当然,Warden 本身不负责发送验证码。

它只是把“这个人是谁、状态是什么、后续应该联系哪里”这几件事返回给调用方。

到这里,Warden 的最小链路就跑通了。我们已经验证了几件事:

Warden 能正常启动
Warden 能加载本地 data.json
Warden 能通过 API Key 保护查询接口
Warden 能按邮箱、手机号或 user_id 查到用户
Warden 能给登录流程返回 lookup 结果

这一步完成后,先不要急着接远程数据源,也不要急着上 Redis,更不要一上来就把所有服务都接进来。

我更建议先把这个最小示例跑顺。

可以改一下 data.json,加一个用户,把某个用户的 statusactive 改成 inactive,再查一次 /v1/lookup。确认自己理解 Warden 怎么读数据、怎么返回状态之后,再继续接 Stargate。

基础设施工具最好一步一步来。

先让名单能被稳定查询,再让入口服务依赖这份名单。

常用配置说明

Warden 的配置方式比较灵活,可以用命令行参数,也可以用环境变量,还可以用配置文件。不过第一次使用时,不需要把所有配置都看完。

我建议先把配置分成两组。

第一组,是第一天就会用到的配置:

API_KEY
DATA_FILE / DATA_DIR
MODE
RESPONSE_FIELDS

第二组,是后面接远程数据源、多实例和长期运行时再看的配置:

CONFIG / KEY
REDIS_ENABLED / REDIS
PORT

这样拆开以后,配置会好理解很多。先让本地文件、API Key 和查询接口跑通,再去考虑远程数据源、Redis、多实例和监控。

API_KEY:先把查询接口保护起来

Warden 管的是准入名单。这份名单里可能有邮箱、手机号、用户 ID、角色、权限范围,后面还可能有更多内部字段,所以它不应该裸着给人查。

最小示例里也建议设置:

API_KEY=your-secret-api-key-here

查询时带上:

curl -H "X-API-Key: your-secret-api-key-here" \
  http://localhost:8081/

也可以使用 Bearer 形式:

curl -H "Authorization: Bearer your-secret-api-key-here" \
  http://localhost:8081/

本地测试时,示例密钥只是为了方便演示。但只要进入真实环境,就不要继续用这种占位值,也不要把 API_KEY 写进仓库。

Warden 本身不负责登录页面,也不签发会话,但它会回答“谁可以继续往下走”。这类接口从第一天开始就应该被保护起来。

DATA_FILE:从一个本地文件开始

最简单的数据来源,就是一个本地 JSON 文件。

默认可以从当前目录的 data.json 开始:

DATA_FILE=./data.json

在 Docker Compose 里,一般会把本地文件挂载进容器:

volumes:
  - ./data.json:/app/data.json:ro

如果想写得更明确,也可以直接指定容器里的路径:

environment:
  - DATA_FILE=/app/data.json

本地文件很适合 Warden 的第一步。数据量不大、变更不频繁、使用的人也不多时,一个 data.json 已经足够好用。

它的好处是直观。你能很清楚地看到名单里有哪些人,每个人的邮箱、手机号、状态、角色和权限范围是什么。调试时也简单,改完文件,重启服务,重新查一次接口,就能确认结果。

这比一开始就接远程 API、缓存、多实例要轻很多。

DATA_DIR:名单变长之后再拆开

等用户多起来之后,一个 data.json 可能会变得不太好维护。这时可以把名单拆成多个文件。

比如:

data/
  admin.json
  team-a.json
  team-b.json
  readonly-users.json

然后配置:

DATA_DIR=/app/data

Warden 会读取这个目录下的数据文件,并把它们合并成一份可以查询的名单。

这个方式很适合把管理员和普通用户分开维护,或者让不同团队各自维护自己的名单。你也可以把手工维护的数据和导出的数据分开放,后面排查问题时会更清楚:

  • 这个用户来自哪一份名单;
  • 这个状态是谁改的;
  • 这个团队是不是已经把离开的成员移除了。

这些问题在一个很长的 JSON 文件里会比较难看出来。拆成目录之后,会好很多。

MODE:先只用本地数据

MODE 用来决定 Warden 怎么使用本地数据和远程数据。

第一次使用时,我建议直接用:

MODE=ONLY_LOCAL

也就是只读取本地文件。这最容易理解,也最方便排查问题。等本地文件、API Key、用户查询都跑顺之后,再考虑远程数据源。

常见模式可以先这样理解:

ONLY_LOCAL
只使用本地文件。

ONLY_REMOTE
只使用远程数据。

REMOTE_FIRST
远程数据优先,本地数据作为补充。

LOCAL_FIRST
本地数据优先,远程数据作为补充。

REMOTE_FIRST_ALLOW_REMOTE_FAILED
远程优先,但远程失败时允许回退到本地数据。

LOCAL_FIRST_ALLOW_REMOTE_FAILED
本地优先,但本地失败时允许回退到远程数据。

这些名字看起来有点长,但含义比较直接。如果你只是本地测试,选 ONLY_LOCAL。如果已经有一套内部用户系统,Warden 只是同步一份准入名单,可以考虑 ONLY_REMOTEREMOTE_FIRST

ALLOW_REMOTE_FAILED 的模式要谨慎。它会让系统更稳,但也可能带来另一个问题:远程系统里已经禁用的人,本地文件里是不是还存在?如果本地文件没有同步更新,回退时会不会放过不该放过的人?

所以生产环境里,不要只看“可用性”,还要看这份名单是不是仍然可信。

RESPONSE_FIELDS:不要把所有字段都返回出去

默认情况下,如果用户对象里有很多字段,调用方可能会拿到比较完整的数据。这在本地调试时很方便,但真实使用时不一定合适。

比如你的用户数据里可能有一些内部备注:

{
  "mail": "admin@example.com",
  "phone": "13800138000",
  "user_id": "admin-001",
  "status": "active",
  "role": "admin",
  "scope": ["read", "write", "admin"],
  "name": "管理员",
  "source": "manual",
  "note": "临时加入测试环境"
}

Stargate 可能只需要知道:

user_id
mail
phone
status
scope
role
name

那就没有必要把 sourcenote 或其他内部字段都返回出去。可以用 RESPONSE_FIELDS 控制返回字段:

RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name

这个配置很容易被忽略,但我建议尽早用起来。因为 Warden 的数据会越来越像一份内部身份目录,一旦调用方变多,字段暴露就应该收紧。

让每个调用方只拿到自己需要的字段,比“先全给出去,后面再收回来”要好很多。

CONFIGKEY:接远程数据源

如果名单不再适合手工维护,或者你已经有内部用户系统,可以让 Warden 从远程接口读取用户数据。

比如:

CONFIG=https://example.com/api/users.json

如果远程接口需要认证,可以加上:

KEY=Bearer your-token-here

远程接口返回的数据格式,应该和本地 data.json 保持一致,也就是一组用户对象:

[
  {
    "mail": "admin@example.com",
    "phone": "13800138000",
    "user_id": "admin-001",
    "status": "active",
    "scope": ["read", "write", "admin"],
    "role": "admin"
  }
]

这样 Warden 不需要关心数据到底来自哪里。本地文件也好,远程 API 也好,最后都整理成同一份准入名单。

这也是把名单单独拆出来的好处。Stargate 不需要知道用户数据是手写的,还是从远程系统同步的。它只需要问 Warden:

这个人现在能不能继续往下走?

不过,远程数据源进入生产环境时要多注意几件事:尽量使用 HTTPS,远程接口需要认证,不要把 Token 写进仓库,不要跳过 TLS 校验,也不要把远程数据源指向不可信地址。

Warden 是认证链路里的一环。它的数据源如果不可信,后面的登录流程也就不可信了。

REDIS_ENABLEDREDIS:后面再考虑多实例

最小使用时,可以先禁用 Redis:

REDIS_ENABLED=false

如果只是一个 Warden 实例,读本地文件,用户数量也不多,先不用 Redis 会更容易排查问题。

等 Warden 开始长期运行,或者你准备做多实例部署时,再考虑打开:

REDIS_ENABLED=true
REDIS=warden-redis:6379

Redis 主要用于缓存、分布式锁和多实例场景。它不是第一天必须要有的东西,但当 Warden 变成多个服务都会依赖的准入名册时,它会让同步、缓存和实例之间的协调更稳一些。

这里还是同一个原则:先让最短链路跑通,再让它跑得更稳。

PORT:通常不用改

Warden 默认监听 8081

PORT=8081

本地测试时,保持默认即可。如果你的环境里这个端口已经被占用,或者你想和其他服务统一端口规划,再改它。

比如:

PORT=18081

在 Docker Compose 里,也要同步调整映射关系:

ports:
  - "18081:18081"

environment:
  - PORT=18081

不过真实部署时,我更建议不要太依赖宿主机端口暴露。Warden 更适合跑在内部网络里,让 Stargate、内部网关或可信调用方通过服务名访问。

比如在同一个 Compose 网络里,Stargate 可以直接访问:

http://warden:8081

这样就不需要把 Warden 暴露给外部访问者。

一个更完整一点的配置示例

如果只是本地文件起步,配置可以保持很小:

environment:
  - PORT=8081
  - MODE=ONLY_LOCAL
  - DATA_FILE=/app/data.json
  - REDIS_ENABLED=false
  - API_KEY=your-secret-api-key-here
  - RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name

如果后面改成远程数据源,可以变成:

environment:
  - PORT=8081
  - MODE=REMOTE_FIRST_ALLOW_REMOTE_FAILED
  - DATA_FILE=/app/data.json
  - CONFIG=https://example.com/api/users.json
  - KEY=Bearer your-token-here
  - REDIS_ENABLED=false
  - API_KEY=your-secret-api-key-here
  - RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name

如果再往后需要多实例和缓存,可以继续加 Redis:

environment:
  - PORT=8081
  - MODE=REMOTE_FIRST_ALLOW_REMOTE_FAILED
  - DATA_FILE=/app/data.json
  - CONFIG=https://example.com/api/users.json
  - KEY=Bearer your-token-here
  - REDIS_ENABLED=true
  - REDIS=warden-redis:6379
  - API_KEY=your-secret-api-key-here
  - RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name

但,我不建议一开始就用最后这一版。

第一次跑 Warden,最重要的是确认三件事:

数据能不能加载
接口能不能查询
返回结果是不是你预期的那个人

这些都确认之后,再去考虑远程数据、Redis、多实例和监控。配置不是越多越好,能解释清楚每一项为什么存在,才比较重要。

作为独立服务怎么用

Warden 不一定要接在 Stargate 后面。它本身也可以作为一个很小的准入数据服务,给内部系统使用。

比如,你有一个内部网关,需要判断某个邮箱能不能访问;或者有一个自动化脚本,需要确认某个手机号是不是还在允许名单里;又或者有一个内部平台,只想从统一的地方拿到用户的 user_idrolescope

这些场景里,Warden 都可以独立工作。

它不关心调用方是不是 Stargate。只要调用方能带着 API Key 来查,它就负责回答几个问题:

  • 这个人是不是在名单里;
  • 这个人现在是什么状态;
  • 后续流程需要联系他时,应该用邮箱还是手机号;
  • 下游系统需要用户信息时,能不能拿到稳定的 user_id

常用接口其实不多。

查询当前名单

如果你想看 Warden 当前加载出来的完整名单,可以访问根路径:

curl -H "X-API-Key: your-secret-api-key-here" \
  http://localhost:8081/

也可以访问合并后的 data.json

curl -H "X-API-Key: your-secret-api-key-here" \
  http://localhost:8081/data.json

这两个接口更适合排查数据。比如你刚改完本地文件,或者刚接入远程数据源,想确认 Warden 最终看到的名单是什么样。

如果用户数量比较多,也可以分页查询:

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/?page=1&page_size=100"

不过真实登录流程里,一般不会频繁拉完整名单。更多时候,调用方只需要查一个具体的人。

查询单个用户

Warden 支持按手机号、邮箱或用户 ID 查询用户。

按手机号查:

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/user?phone=13800138000"

按邮箱查:

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/user?mail=admin@example.com"

按用户 ID 查:

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/user?user_id=admin-001"

这个接口适合内部系统做精确查询。比如内部平台已经知道当前用户的邮箱,只想查他的 rolescope;或者某个脚本拿到了手机号,想确认这个人是不是还在准入名单里。

返回结果里通常会包含用户对象中的字段:

{
  "phone": "13800138000",
  "mail": "admin@example.com",
  "user_id": "admin-001",
  "status": "active",
  "scope": ["read", "write", "admin"],
  "role": "admin",
  "name": "管理员"
}

这里需要注意一件事:

查到用户,不等于一定应该继续放行。

调用方还应该看 status。一般来说,只有 active 状态才应该继续往下走。如果是 inactivesuspended,即使这个人还在名单里,也不应该继续登录或访问。

这也是我建议保留 status 字段的原因。它能把“查不到用户”和“用户存在但不允许继续访问”区分开。

给登录流程用的 lookup

如果调用方是在做登录流程,推荐使用 /v1/lookup

它的输入更简单:

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/v1/lookup?identifier=admin@example.com"

identifier 可以是邮箱、手机号,也可以是用户 ID。调用方不需要提前判断用户输入的是哪一种。Warden 会尝试在名单里匹配对应用户,然后返回更适合登录流程使用的数据。

比如:

{
  "user_id": "admin-001",
  "destination": {
    "email": "admin@example.com",
    "phone": "13800138000"
  },
  "status": "active",
  "channel_hint": "sms",
  "name": "管理员"
}

这里的 destination 是后续验证流程可以使用的联系方式。如果用户有手机号,后面可以走短信;如果只有邮箱,后面可以走邮件。channel_hint 只是提示后续流程更适合使用哪个通道。

Warden 自己不发送验证码。

它只是把“可以联系到哪里”这件事告诉调用方。

比如 Stargate 可以先调用 Warden,确认这个邮箱是不是在名单里、用户现在是不是 active、验证码应该发到哪里。拿到结果之后,再决定要不要继续调用 Herald 发送验证码。

内部平台也可以这样用:确认某个 user_id 是否有效,判断用户有没有 admin scope,或者在用户状态是 suspended 时停止后续流程。

Warden 的价值不在于它有很多接口,而在于这些系统不用再各自维护一份名单。同一个用户,尽量从同一份准入数据里查出来。

健康检查

Warden 也提供健康检查接口:

curl http://localhost:8081/health

或者:

curl http://localhost:8081/healthcheck

这类接口适合接入 Docker、Kubernetes、反向代理或监控系统。本地调试时,你可以直接用它确认服务是否启动、数据是否加载。

比如:

{
  "status": "ok",
  "details": {
    "redis": "disabled",
    "data_loaded": true,
    "user_count": 2
  },
  "mode": "ONLY_LOCAL"
}

这里重点看 data_loadeduser_count。如果 data_loadedfalse,说明数据文件没有正确加载;如果 user_count 不符合预期,说明名单内容可能没有被正确读取,或者远程数据源没有按预期返回。

但生产环境里,不建议把健康检查直接暴露给公网。它虽然不是用户查询接口,但仍然可能暴露服务状态、数据加载情况和运行模式。更合适的做法,是只允许内部网络、反向代理或监控系统访问。

一个独立使用的小例子

假设你有一个内部脚本,准备在执行敏感操作前确认某个邮箱是否还在准入名单里。它可以先调用:

curl -s -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/v1/lookup?identifier=admin@example.com"

然后检查返回里的 status

status == active      继续执行
status != active      停止执行
用户不存在            停止执行

这样脚本不需要自己维护一份邮箱列表,也不需要知道名单到底来自本地文件,还是远程 API。它只需要问 Warden。

再比如,你有一个内部平台,需要展示当前用户的角色和权限范围。它也可以按邮箱或 user_id 查询:

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/user?user_id=admin-001"

拿到结果后,只使用自己需要的字段:

{
  "user_id": "admin-001",
  "status": "active",
  "role": "admin",
  "scope": ["read", "write", "admin"]
}

这时最好配合前面说的 RESPONSE_FIELDS

一个内部脚本可能只需要 status,一个登录流程可能只需要 user_iddestinationstatus,一个后端服务可能只需要 rolescope。字段能少给,就少给。

这不是为了把系统做复杂,而是因为 Warden 后面会越来越接近一份内部身份目录。一旦调用方变多,最开始随手返回出去的字段,后面再想收回来就比较麻烦。

所以独立使用 Warden 时,我会把它当成一个很薄的内部能力:它不负责登录,不负责验证码,不负责会话,也不负责复杂权限策略。它只负责让其他系统不用再各自维护一份名单。

大家都问同一个地方:

  • 这个人是谁?
  • 他现在还能不能继续往下走?

需要的话,应该把哪些基础身份信息交给下游?

接到 Stargate 后会变成什么样

前面我们一直把 Warden 当成一个独立服务来看。它能加载名单,能查询用户,能返回状态、联系方式、角色和权限范围。

但如果你已经按照上一篇把 Stargate 跑起来,那么接入 Warden 之后,登录流程会多一个很关键的判断:

这个输入的邮箱、手机号或 user_id,是不是还在准入名单里?

上一篇里,Stargate 可以只靠共享口令完成登录。这很轻,也很适合第一步。但它的判断方式比较简单:

你知道密码吗?

接入 Warden 之后,判断方式会变成:

你是谁?
你在不在名单里?
你现在是不是 active?
后续验证应该发到哪里?

这不是把 Stargate 变复杂,而是把原来塞不进共享口令里的那部分信息,交给 Warden 来回答。

原来的流程大概是:

用户访问受保护服务
Stargate 发现用户还没有登录
用户输入共享口令
口令正确,创建会话
回到原来的服务

接入 Warden 之后,流程会变成:

用户访问受保护服务
Stargate 发现用户还没有登录
用户输入邮箱、手机号或 user_id
Stargate 调用 Warden 查询用户
Warden 返回用户是否存在、当前状态和联系方式
Stargate 根据结果决定是否继续登录流程

如果用户不存在,流程应该停下来。如果用户存在,但 status 不是 active,流程也应该停下来。只有用户存在,并且状态正常,后面才继续做验证、创建会话或进入其他登录步骤。

这样一来,Stargate 仍然负责入口和会话,Warden 只负责回答名单问题。Stargate 不需要自己维护用户数据,也不需要知道名单来自本地文件,还是远程 API。

它只需要在登录流程里问一次 Warden:这个人现在还能不能继续往下走?

Stargate 侧怎么配置

Stargate 侧启用 Warden,大概需要这些配置:

WARDEN_ENABLED=true
WARDEN_URL=http://warden:8081
WARDEN_API_KEY=your-secret-api-key-here
WARDEN_CACHE_TTL=300

WARDEN_ENABLED 表示启用 Warden 查询:

WARDEN_ENABLED=true

不开这个开关时,Stargate 可以继续按原来的共享口令方式工作。打开之后,Stargate 就会在登录流程里增加 Warden 查询。

WARDEN_URL 表示 Warden 服务地址:

WARDEN_URL=http://warden:8081

如果 Stargate 和 Warden 在同一个 Docker Compose 网络里,这里通常可以直接使用服务名:

http://warden:8081

不一定需要把 Warden 暴露到宿主机端口。真实部署时,我也更建议这样做。Warden 只给内部服务访问,不直接对公网开放。

WARDEN_API_KEY 需要和 Warden 侧的 API_KEY 保持一致:

WARDEN_API_KEY=your-secret-api-key-here

也就是说,Warden 里配置的是:

API_KEY=your-secret-api-key-here

Stargate 里配置的是:

WARDEN_API_KEY=your-secret-api-key-here

这两个值不一致时,Stargate 就查不到 Warden。这类问题排查起来很容易绕路,所以第一次接入时,建议先不要同时打开太多能力。先确认 Stargate 能访问 Warden,再确认 API Key 没写错,最后再看用户状态和后续验证流程。

WARDEN_CACHE_TTL 用来控制缓存时间:

WARDEN_CACHE_TTL=300

这里的 300 通常表示缓存 300 秒(具体缓存行为以 Stargate 当前版本实现为准)。缓存的好处是减少重复查询,比如同一个用户短时间内多次触发登录流程,Stargate 不需要每次都去问 Warden。

但缓存也有代价。如果你刚刚把某个用户从 active 改成 inactive,Stargate 可能会在缓存过期前继续使用旧结果。所以这个值不要随手设得太长。内部测试环境可以稍微宽松一点,对准入变更比较敏感的环境,缓存时间就应该短一些,甚至先关掉缓存做验证。

一个简单的组合示例

如果把 Stargate 和 Warden 放在同一个 Compose 项目里,结构可以类似这样:

services:
  stargate:
    image: soulteary/stargate:latest
    container_name: stargate
    restart: unless-stopped

    environment:
      - AUTH_HOST=auth.test.localhost
      - LANGUAGE=zh

      - WARDEN_ENABLED=true
      - WARDEN_URL=http://warden:8081
      - WARDEN_API_KEY=your-secret-api-key-here
      - WARDEN_CACHE_TTL=300

    networks:
      - proxy
      - auth-internal

    labels:
      - traefik.enable=true
      - traefik.docker.network=proxy

      - traefik.http.routers.stargate.entrypoints=http
      - traefik.http.routers.stargate.rule=Host(`auth.test.localhost`) || Path(`/_session_exchange`)
      - traefik.http.routers.stargate.service=stargate

      - traefik.http.services.stargate.loadbalancer.server.port=80

      - traefik.http.middlewares.stargate.forwardauth.address=http://stargate/_auth
      - traefik.http.middlewares.stargate.forwardauth.trustForwardHeader=true

  warden:
    image: ghcr.io/soulteary/warden:latest
    container_name: warden
    restart: unless-stopped

    environment:
      - PORT=8081
      - MODE=ONLY_LOCAL
      - DATA_FILE=/app/data.json
      - REDIS_ENABLED=false
      - API_KEY=your-secret-api-key-here
      - RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name

    volumes:
      - ./data.json:/app/data.json:ro
      - /etc/localtime:/etc/localtime:ro

    networks:
      - auth-internal

    healthcheck:
      test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:8081/health || exit 1"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 10s

networks:
  proxy:
    external: true

  auth-internal:
    internal: true

这只是一个示意配置,关键点有两个。

第一,Stargate 同时在 proxyauth-internal 网络里。它需要通过 proxy 被 Traefik 访问,也需要通过 auth-internal 访问 Warden。

第二,Warden 只在 auth-internal 网络里。它不需要挂 Traefik label,也不需要暴露 ports,因为正常情况下,外部用户不应该直接访问 Warden。

真正会访问 Warden 的,是 Stargate 或其他可信内部调用方。

这和前面本地演示不一样。本地演示为了方便,我们用了:

ports:
  - "8081:8081"

真实部署时,如果 Stargate 和 Warden 在同一个 Docker 网络里,就可以去掉端口暴露。这样访问路径会更清楚:

用户浏览器
Traefik
Stargate
Warden

用户能看到 Stargate 的登录页面,但看不到 Warden。

用户状态怎么影响登录

接入 Warden 之后,status 就变得很重要了。

假设名单里有一个用户:

{
  "mail": "admin@example.com",
  "phone": "13800138000",
  "user_id": "admin-001",
  "status": "active",
  "role": "admin",
  "scope": ["read", "write", "admin"],
  "name": "管理员"
}

当用户输入 admin@example.com 时,Stargate 会去问 Warden。如果 Warden 返回:

{
  "user_id": "admin-001",
  "destination": {
    "email": "admin@example.com",
    "phone": "13800138000"
  },
  "status": "active",
  "channel_hint": "sms",
  "name": "管理员"
}

Stargate 就可以继续后面的登录流程。

如果你把这个用户改成:

{
  "mail": "admin@example.com",
  "phone": "13800138000",
  "user_id": "admin-001",
  "status": "inactive",
  "role": "admin",
  "scope": ["read", "write", "admin"],
  "name": "管理员"
}

那他虽然还在名单里,但不应该继续登录。

这和直接删除用户不一样。直接删除时,系统只能知道:

查不到这个人

改成 inactive 时,系统能知道:

这个人存在,但现在不允许继续访问

对排查问题来说,后者会清楚很多。尤其是多人协作时,有人反馈“我为什么登录不了”,你可以直接从名单里看到他的状态,而不是反复猜他是不是输错了邮箱、手机号或密码。

rolescope 怎么用

Warden 返回 rolescope,并不代表它要变成权限系统。它只是把基础身份信息提供给调用方。真正要不要使用这些字段,要看后面的系统怎么设计。

比如,Stargate 可以在认证成功后,把一部分用户信息写入请求头,再交给后端服务。后端服务可以看到:

X-Forwarded-User-Id: admin-001
X-Forwarded-Email: admin@example.com
X-Forwarded-Role: admin

如果你使用 Traefik ForwardAuth,还需要在 middleware 里显式配置允许转发哪些响应头。比如:

- traefik.http.middlewares.stargate.forwardauth.authResponseHeaders=X-Forwarded-User,X-Forwarded-Email,X-Forwarded-User-Id,X-Forwarded-Role

这件事要谨慎。

请求头一旦进入后端服务,就会被后端当成可信身份信息。所以后端服务应该只接受来自反向代理的流量,不要让用户绕过 Traefik 直接访问后端。否则,用户就有机会自己伪造这些请求头。

这个问题和 Stargate 本身一样:

入口要统一。

后端不要裸露。

Warden 只负责提供身份信息,不能替你修复绕过入口的问题。

接入时最容易出错的地方

第一次把 Warden 接到 Stargate 后面,最容易出问题的地方通常不是代码,而是网络和配置。

比如,Stargate 访问不到 Warden。这时可以进到 Stargate 容器里测试:

wget -qO- http://warden:8081/health

如果这里都访问不了,先看 Docker 网络,不要急着看登录逻辑。

再比如,API Key 不一致。Warden 配的是:

API_KEY=abc

Stargate 配的是:

WARDEN_API_KEY=abcd

这种情况下,Warden 会拒绝查询。看起来像是“用户不存在”或者“登录失败”,但根因其实是服务间鉴权没通过。

还有一种常见问题,是用户状态不对。名单里有这个人,但状态是:

"status": "inactive"

或者字段名写错了。比如写成了:

"email": "admin@example.com"

但 Warden 期望的是:

"mail": "admin@example.com"

这类问题最好先用 /v1/lookup 单独验证:

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/v1/lookup?identifier=admin@example.com"

确认 Warden 自己能返回正确结果之后,再去看 Stargate。

基础设施排错时,我一般会把链路拆开看:

Warden 自己能不能查到用户?
Stargate 能不能访问 Warden?
Stargate 带的 API Key 对不对?
用户 status 是不是 active?
后续登录流程有没有继续?

不要一上来就从浏览器登录失败开始猜。链路越长,越要一段一段确认。

接入之后,Stargate 还是 Stargate

最后再强调一下边界。

接入 Warden 之后,Stargate 并没有变成用户系统,它只是多了一个准入判断来源。用户数据仍然在 Warden,登录流程仍然在 Stargate。后续如果再接 Herald,验证码发送和校验也应该交给 Herald。

这样拆开之后,后面替换起来会比较轻。你可以先用本地 data.json 跑 Warden,等名单变多了,再让 Warden 接远程 API。你可以先用 Stargate 的简单登录流程,等多人使用了,再加验证码。你也可以先不传 rolescope 给后端,等确实有服务需要这些字段,再通过请求头往下传。

不要第一天就把所有能力都打开。

先让 Stargate 能问 Warden。

先让 Warden 能稳定回答:

这个人还在不在名单里。

他现在还能不能继续往下走。

这一步跑通之后,再继续接 Herald。

真实使用时的几个建议

如果你准备把 Warden 放进自己的环境里,我建议不要一上来就把所有东西都接上。

基础设施类工具最怕的不是能力不够,而是第一步就铺得太大。一开始就接 Stargate、接 Herald、接远程数据源、接 Redis、配多实例、开监控,看起来很完整,但一旦出了问题,排查起来会很麻烦。

你很难判断问题到底出在哪里:是 Warden 没读到数据,是 API Key 不对,是 Stargate 访问不到 Warden,是用户状态不是 active,是 Herald 没把验证码发出去,还是反向代理和网络本身有问题。

所以我更建议小步接入。

先让名单自己跑起来,再让入口服务依赖这份名单,最后再把验证码、远程数据源、多实例和监控这些能力补上。

第一步,先用本地文件跑通

第一次使用 Warden,不要急着接远程 API,也不要急着接 Stargate。先准备一个最简单的 data.json,里面放一两个用户。

比如:

[
  {
    "phone": "13800138000",
    "mail": "admin@example.com",
    "user_id": "admin-001",
    "status": "active",
    "scope": ["read", "write", "admin"],
    "role": "admin",
    "name": "管理员"
  }
]

然后用 MODE=ONLY_LOCAL 启动 Warden。

这一步只验证一件事:

Warden 能不能稳定地回答“这个人是不是在名单里”。

可以依次确认几个接口:

curl http://localhost:8081/health

确认服务启动正常。

curl -H "X-API-Key: your-secret-api-key-here" \
  http://localhost:8081/

确认名单被加载。

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/user?mail=admin@example.com"

确认可以按邮箱查到用户。

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://localhost:8081/v1/lookup?identifier=admin@example.com"

确认登录流程用的查询接口也能返回预期结果。

这一步跑通之前,不要急着接其他东西。因为 Warden 自己如果都还没稳定返回数据,后面接 Stargate 或 Herald,只会把问题藏得更深。

先把最短链路跑顺。

这比一次性把配置写完整更重要。

第二步,把 status 用起来

不要只把 Warden 当成邮箱和手机号列表。如果只是维护联系方式,那它很快会变成另一个通讯录。

Warden 更重要的价值,是让名单里的人有状态。

比如:

{
  "mail": "user@example.com",
  "phone": "13900139000",
  "user_id": "user-001",
  "status": "active"
}

当这个人还应该继续访问内部服务时,状态是 active。如果他已经离开项目,或者暂时不应该继续登录,可以改成:

{
  "mail": "user@example.com",
  "phone": "13900139000",
  "user_id": "user-001",
  "status": "inactive"
}

或者:

{
  "mail": "user@example.com",
  "phone": "13900139000",
  "user_id": "user-001",
  "status": "suspended"
}

这样做比直接删除更容易排查问题。

直接删除时,系统只能知道:

查不到这个人。

保留用户并修改状态时,系统能知道:

这个人存在,但现在不允许继续访问。

这两个结果在管理上是不一样的。前者更像数据缺失,后者更像明确拒绝。

多人使用时,这个区别很有用。有人反馈“我为什么登录不了”,你可以直接看名单里的状态,而不是反复猜他是不是输错了邮箱、手机号,或者是不是某个配置没生效。

所以我建议从一开始就把 status 当成必填字段。

哪怕你的名单现在只有两个人,也最好写上。后面用户变多时,这个习惯会省很多事。

第三步,再接 Stargate 或其他调用方

等 Warden 自己跑稳之后,再让 Stargate、内部网关、脚本或平台来调用它。

这一步重点不是“登录页面好不好看”,而是确认服务之间的调用链路是不是可靠。

比如先确认 Stargate 能访问 Warden:

wget -qO- http://warden:8081/health

如果这里访问不了,先看 Docker 网络、服务名、端口和网络隔离,不要急着看登录逻辑。

然后确认 API Key 是否一致。

Warden 侧配置的是:

API_KEY=your-secret-api-key-here

Stargate 侧就应该是:

WARDEN_API_KEY=your-secret-api-key-here

这两个值不一致时,Stargate 查询 Warden 会失败。看起来可能像是“用户不存在”或者“登录失败”,但根因其实只是服务间鉴权没通过。

再确认用户状态。名单里有这个人,不代表他应该继续登录。如果返回的是:

{
  "status": "inactive"
}

或者:

{
  "status": "suspended"
}

那调用方就应该停下来。

最后再确认返回字段是否符合预期。比如 Stargate 需要 user_idmailphonestatus,下游服务可能还需要 rolescope,那就检查 RESPONSE_FIELDS 有没有把这些字段放出来。

可以先用 /v1/lookup 单独验证:

curl -H "X-API-Key: your-secret-api-key-here" \
  "http://warden:8081/v1/lookup?identifier=admin@example.com"

确认 Warden 返回正确之后,再去看 Stargate 的登录流程。

排查时尽量按顺序拆开:

Warden 自己能不能查到用户?
调用方能不能访问 Warden?
API Key 是否一致?
用户状态是不是 active?
返回字段是否够用?
后续登录流程有没有继续?

不要一开始就从浏览器里的登录失败开始猜。

链路越长,越要一段一段确认。

第四步,再接 Herald

当你确认 Warden 已经能稳定回答“谁可以登录”之后,再接 Herald。

这时登录流程就可以从共享口令,逐步变成更适合多人使用的方式:

用户输入邮箱或手机号
Stargate 调用 Warden 查询用户
Warden 确认用户存在,并且状态是 active
Stargate 调用 Herald 发送验证码
用户输入验证码
Stargate 创建会话

这里 Warden 仍然不发送验证码。它只返回用户状态和联系方式。

比如:

{
  "user_id": "admin-001",
  "destination": {
    "email": "admin@example.com",
    "phone": "13800138000"
  },
  "status": "active",
  "channel_hint": "sms",
  "name": "管理员"
}

Herald 再根据这些信息决定怎么发送验证码:发短信,还是发邮件;验证码多久过期;失败几次要不要限制。这些都不应该塞进 Warden。

Warden 只管名单,Herald 只管送信,Stargate 只管登录流程和会话。

这样拆开之后,后面替换起来会轻很多。短信服务商换了,是 Herald 的事情;名单从本地文件换成远程 API,是 Warden 的事情;登录页、Cookie、ForwardAuth 和会话,是 Stargate 的事情。

不要让一个工具因为“顺手”就开始管太多。

一开始看起来省事,后面会很难拆。

第五步,再考虑远程数据源、Redis 和多实例

如果只是少量用户,本地文件已经很好用。尤其是 HomeLab、测试环境、小团队内部工具,或者几个临时服务,一个 data.json 加上 Git 记录,可能就够用了。

等名单开始变多,或者已经有其他系统在维护用户数据,再考虑远程数据源。

比如:

CONFIG=https://example.com/api/users.json
KEY=Bearer your-token-here
MODE=REMOTE_FIRST_ALLOW_REMOTE_FAILED

这时 Warden 可以从远程接口同步名单,也可以在远程异常时回退到本地数据。

但回退策略要谨慎。

它不是只有好处。如果远程系统里已经禁用了某个用户,而本地文件里还保留着旧的 active 状态,那么远程失败后回退到本地数据,就可能带来风险。

所以只要启用回退,就要想清楚几个问题:

  • 本地数据是不是可信;
  • 本地数据多久更新一次;
  • 远程和本地冲突时,以谁为准;
  • 远程失败时,是宁可拒绝登录,还是允许使用本地兜底。

这些不是配置问题,而是准入策略问题。

等 Warden 被多个系统依赖,或者需要多实例运行时,再考虑 Redis。

REDIS_ENABLED=true
REDIS=warden-redis:6379

Redis 可以帮助做缓存、分布式锁和多实例协调,但它不应该成为第一天的门槛。如果你只是想确认 Warden 能不能读一个 JSON 文件、查一个邮箱、返回一个状态,Redis 只会增加排查成本。

等最小链路稳定之后,再让系统变得更稳。

同样,OpenTelemetry、Prometheus、审计日志、健康检查访问限制,也都适合后面再加。这些能力很重要,但它们不应该挡在第一步前面。

我更建议的顺序是:

本地文件
status 状态
Stargate 或其他调用方
Herald
远程数据源
Redis、多实例、监控和审计

这条路径不是最完整的,但它比较容易排错,也比较容易建立信心。

最后再提醒一句:Warden 不是完整 IAM。

它不管理组织架构,不负责复杂权限策略,不处理 OIDC、SAML,不提供完整管理后台,也不应该替代公司里已经成熟的 SSO 或身份中台。

如果你已经有这些系统,应该优先使用它们。

Warden 更适合站在中间地带:

你还没有必要引入完整身份系统,但已经不想继续靠共享口令、表格、聊天记录和散落在各个服务里的配置维护准入关系。

这时 Warden 可以先把名单收起来,让它变成一个可以被机器查询、可以被多个系统复用、可以逐步替换数据来源的小服务。

先别急着把它做大。

让它先把“谁还能进”这件事回答清楚。

上线前的最小安全检查

Warden 管的是准入名单,位置比普通内部小工具更敏感。

它本身不负责登录页面,也不签发会话,但会告诉调用方:这个人是谁、现在是不是 active、后续验证应该联系哪里,以及下游服务能不能拿到 rolescope

所以 Warden 不应该裸奔,哪怕它只跑在内网里,也应该先把最基本的安全边界立起来。

查询接口必须有 API Key

第一件事,是确认 API_KEY 已经换成真实密钥,并且所有用户查询接口都必须带上它。不要让任何人都能直接查询用户名单。

至少下面这些接口,都应该被保护起来:

GET /
GET /data.json
GET /user
GET /v1/lookup

本地测试时,你可能会写:

API_KEY=your-secret-api-key-here

本地示例里的 your-secret-api-key-here 只适合演示,不要带进真实环境。真实环境里,不要继续用这种占位值,也不要用太短、太容易猜的字符串。

调用时带上:

curl -H "X-API-Key: your-real-api-key" \
  http://warden:8081/v1/lookup?identifier=admin@example.com

或者使用 Bearer 形式:

curl -H "Authorization: Bearer your-real-api-key" \
  http://warden:8081/v1/lookup?identifier=admin@example.com

这一步看起来很基础,但很重要。Warden 不是公开通讯录,更不是让外部用户随便探测“某个邮箱在不在名单里”的接口。

哪怕只是内部服务调用,也应该先有服务间鉴权。

不要把 Warden 直接暴露到公网

本地演示时,为了方便测试,我们用了:

ports:
  - "8081:8081"

这样浏览器和 curl 都能直接访问 Warden。

但真实部署时,我不建议这样做。Warden 更适合只跑在内部网络里,由 Stargate、内部网关或其他可信调用方访问。

比如在 Docker Compose 里,让 Stargate 和 Warden 共享一个内部网络:

networks:
  auth-internal:
    internal: true

然后 Warden 只加入这个网络:

services:
  warden:
    image: ghcr.io/soulteary/warden:latest
    networks:
      - auth-internal

Stargate 也加入这个网络:

services:
  stargate:
    image: soulteary/stargate:latest
    networks:
      - proxy
      - auth-internal

这样访问链路会更清楚:

用户浏览器
Traefik / Nginx
Stargate
Warden

用户能访问的是入口服务,Warden 只给 Stargate 或其他可信内部调用方访问。

如果你的环境里确实需要跨机器访问 Warden,也尽量通过内网地址、VPN、专线、服务网格或受控反向代理来做。不要把它直接挂到公网域名下面。

如果不得不暴露,也至少要同时考虑:

TLS
API Key
IP 白名单
反向代理访问控制
速率限制
访问日志

不要只依赖“接口路径没人知道”。

这不是安全边界。

健康检查也不要随便暴露

Warden 的健康检查很方便:

curl http://localhost:8081/health

或者:

curl http://localhost:8081/healthcheck

它能告诉你服务是否正常、数据是否加载、Redis 是否启用、当前模式是什么。这对运维和排查问题很有用,但它也可能暴露一些你不想给外部知道的信息。

比如:

{
  "status": "ok",
  "details": {
    "redis": "disabled",
    "data_loaded": true,
    "user_count": 2
  },
  "mode": "ONLY_LOCAL"
}

这里虽然没有直接泄露用户数据,但已经能看出服务状态、数据加载情况和运行模式。

所以生产环境里,不建议把健康检查直接暴露给公网。更合适的方式,是只允许本机、内网、反向代理或监控系统访问。

可以使用 Warden 的健康检查白名单:

HEALTH_CHECK_IP_WHITELIST=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16

也可以在 Nginx、Traefik、VPN 或内网安全组层面再做一层访问限制。

控制返回字段,别把所有信息都给出去

Warden 的用户对象可能会慢慢变多。

一开始只有:

{
  "mail": "admin@example.com",
  "phone": "13800138000",
  "status": "active"
}

后面可能会加上:

{
  "user_id": "admin-001",
  "role": "admin",
  "scope": ["read", "write", "admin"],
  "name": "管理员",
  "source": "manual",
  "note": "临时加入测试环境"
}

调试时,返回完整对象很方便。但真实环境里,不一定应该把所有字段都返回给所有调用方。

比如 Stargate 可能只需要:

user_id
mail
phone
status
scope
role
name

那就可以配置:

RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name

不要把 notesource、内部标记、同步来源、管理备注这类字段顺手给出去。

尤其是当 Warden 不只被 Stargate 调用,而是被多个内部系统复用时,更应该收紧返回字段。

字段暴露出去很容易,后面想收回来会麻烦很多。

远程数据源要可信

如果只用本地 data.json,风险主要在文件和部署环境。但如果开始接远程数据源,就要多想一步。

比如:

CONFIG=https://example.com/api/users.json
KEY=Bearer your-token-here

这里的远程接口会变成 Warden 的数据来源。也就是说,它会影响后续登录流程里“谁可以继续往下走”。

所以生产环境里,远程数据源至少应该满足几件事:

使用 HTTPS
接口需要认证
Token 不写进仓库
不跳过 TLS 校验
不指向不可信地址
远程返回的数据结构可控

不要从一个不受控的公开地址加载名单,也不要为了“先跑起来”就关闭 TLS 校验。

Warden 自己再怎么保护接口,如果数据源不可信,整条准入链路也会变得不可信。

另外,如果你用了远程失败回退模式,比如:

MODE=REMOTE_FIRST_ALLOW_REMOTE_FAILED

也要想清楚本地兜底数据是不是仍然可信。

远程系统里已经禁用的人,本地文件里是不是还保留着旧的 active 状态?如果远程挂了,回退到本地时,会不会把不该放行的人重新放进来?

这类问题不是 Warden 能替你自动决定的。它属于准入策略,你需要根据自己的环境做取舍。

对某些场景来说,远程失败时宁可拒绝登录;对另一些内部测试环境来说,本地兜底可能更实用。

关键是不要无意识地开启回退。

密钥不要进仓库

Warden 相关的密钥,最好都不要写进 Git 仓库。

包括:

API_KEY
KEY
REDIS_PASSWORD
HMAC 密钥
TLS 私钥
远程 API Token

本地测试写在 compose.yaml 里没什么问题。但只要进入真实环境,就应该换成更合适的方式。

比如:

环境变量
.env 文件
Docker Secret
Kubernetes Secret
部署平台的 Secret 管理
专门的密钥管理系统

如果使用 .env,至少确认它不会被提交:

.env
*.env

示例文件可以保留,比如:

.env.example

里面只放占位值:

API_KEY=change-me
KEY=Bearer change-me
REDIS_PASSWORD=change-me

不要为了方便,把真实 Token 写进示例配置。

这类东西一旦进了仓库,后面就不只是删掉那么简单。你还需要轮换密钥。

服务间通信可以逐步增强

第一阶段,用 API Key 就够简单。

Stargate 调用 Warden 时带上:

WARDEN_API_KEY=your-real-api-key

Warden 侧配置:

API_KEY=your-real-api-key

这能解决最基础的服务间鉴权问题。

但如果 Warden 已经变成认证链路里的关键组件,就可以继续往上加。

比如 HMAC。调用方不仅带一个静态 API Key,还对请求内容和时间戳做签名,这样可以降低请求被重放或伪造的风险。

再比如 mTLS。让 Stargate 和 Warden 之间通过客户端证书互相确认身份。

这类方案部署成本会高一些,不是第一天必须要做。但当 Warden 开始被多个服务依赖,或者运行在更复杂的网络环境里,就值得考虑。

我更建议的顺序是:

第一阶段:内部网络 + API Key
第二阶段:反向代理访问控制 + IP 白名单 + TLS
第三阶段:HMAC 或 mTLS
第四阶段:审计日志、指标和告警

不要第一天就把链路弄得太复杂,但也不要长期停留在“只要能访问到内网就能调用”的状态。

后端服务不要绕过入口

这一点更多和 Stargate 有关,但接入 Warden 后也一样重要。

如果后端服务可以绕过 Traefik、Nginx 或 Stargate 直接访问,那么前面的认证链路就不完整。

比如你让 Stargate 查询 Warden,Warden 返回用户身份,Stargate 再把用户信息写进请求头转给后端:

X-Forwarded-User-Id: admin-001
X-Forwarded-Email: admin@example.com
X-Forwarded-Role: admin

这时后端服务会把这些请求头当成可信身份信息。但前提是:这些请求只能来自反向代理或 Stargate。

如果用户可以直接访问后端服务,他就有机会自己伪造这些请求头。

所以后端服务应该只暴露在内部网络里,外部用户只能走统一入口。

这和 Stargate 的原则一样:

门装上了,后门也要关掉。

否则 Warden 再认真维护名单,入口链路也可能被绕开。

日志里不要留下太多敏感信息

Warden 排查问题时,日志很有用。但日志也很容易变成另一个数据泄露点。

尤其是用户查询接口,可能会带邮箱、手机号、user_id。如果日志里完整记录了请求参数、返回结果、Header 和 Token,就要小心了。

至少不要把这些东西原样打进日志:

API_KEY
Authorization
远程数据源 Token
完整手机号
完整邮箱
验证码相关字段
内部备注字段

调试环境可以稍微放开一点,生产环境里最好做脱敏。比如手机号只保留后四位,邮箱只保留部分字符,Token 只显示前后几位,或者完全不显示。

日志是为了排查问题。

不是为了复制一份用户数据。

一个最小但比较稳的部署形态

如果只是内部使用,我觉得比较稳的起步方式大概是这样:

Warden 只在内部网络里
查询接口必须带 API Key
健康检查只给监控或内网访问
返回字段用 RESPONSE_FIELDS 收紧
密钥通过环境变量或 Secret 注入
后端服务不能绕过 Stargate

对应到 Compose,大概像这样:

services:
  warden:
    image: ghcr.io/soulteary/warden:latest
    container_name: warden
    restart: unless-stopped

    environment:
      - PORT=8081
      - MODE=ONLY_LOCAL
      - DATA_FILE=/app/data.json
      - REDIS_ENABLED=false
      - API_KEY=${WARDEN_API_KEY}
      - RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name
      - HEALTH_CHECK_IP_WHITELIST=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16

    volumes:
      - ./data.json:/app/data.json:ro
      - /etc/localtime:/etc/localtime:ro

    networks:
      - auth-internal

    healthcheck:
      test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:8081/health || exit 1"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 10s

networks:
  auth-internal:
    internal: true

这里没有 ports。这意味着 Warden 不会直接暴露到宿主机端口。

同一个内部网络里的 Stargate 可以访问它,外部用户不应该直接访问它。

如果你需要本地调试,可以临时加上:

ports:
  - "8081:8081"

调试完成后再去掉。不要把调试配置直接带进生产环境。 上线前,可以简单过一遍:

API_KEY 是否已经设置?
示例密钥是否已经替换?
Warden 是否只在内部网络里?
是否去掉了不必要的 ports?
健康检查是否限制访问范围?
RESPONSE_FIELDS 是否收紧?
远程数据源是否使用 HTTPS?
远程 API Token 是否通过 Secret 注入?
Redis 密码是否没有写进仓库?
后端服务是否只能从统一入口访问?
日志里是否避免记录 Token 和完整用户信息?

这不是为了把 Warden 做成一个很重的系统,而是因为它在认证链路里位置比较敏感。它管的不是普通配置,它管的是:

  • 谁还能进。
  • 谁不该进。
  • 后续验证应该发给谁。

这几件事值得稍微认真一点。

最后

很多内部服务的认证问题,并不是一开始就很复杂。

最开始只是一个密码、一个测试面板、一个临时后台、一个内部文档站。几个人知道地址,几个人知道口令,这样先跑起来,没什么问题。

真正的问题通常是后来出现的。

服务慢慢变多,使用的人慢慢变多,入口也慢慢变多。这时候,原来那把共享钥匙就开始不够用了。

你会开始关心更多问题:

  • 这个人是谁;
  • 他现在还在不在项目里;
  • 他是不是还应该继续登录;
  • 验证码应该发到哪里;
  • 下游服务能不能拿到他的 user_id
  • 有没有必要区分 rolescope

如果这些问题全部塞进认证入口里,Stargate 会越来越重。

如果每个服务都自己维护一份名单,用户数据又会散得到处都是。

如果直接上完整 IAM,对很多 HomeLab、小团队内部工具、测试环境和临时服务来说,又可能太早、太重。

所以这里还是那个中间地带。

我们不是不知道完整身份系统更强,只是很多时候,眼前的问题没到那一步。我们需要的可能只是:先把裸奔的服务挡住,再把共享口令换成名单,然后让验证码、会话、用户状态、下游身份信息这些东西,一步一步接上来。

Stargate 解决的是第一步:先把门装上。

Warden 解决的是第二步:让这道门不要只认一把共享钥匙,而是开始认人。

Herald 后面要解决的是第三步:把验证码和消息送到该送的人手里。

这三个工具放在一起,并不是想重新做一套完整 IAM,它们也不应该假装自己是完整 IAM。完整 IAM 要解决的是组织、账号、协议、权限、审计、生命周期和一整套治理问题。

而 Stargate、Warden、Herald 更像是几个可以独立工作的基础设施小工具。

每个工具只先解决一个具体问题:

  • Stargate 负责守入口;
  • Warden 负责管名单;
  • Herald 负责送验证码。

这几件事拆开之后,Stargate 不需要变成用户数据库,Warden 也不需要理解登录页和验证码流程,Herald 也不用关心谁有资格登录。

它们可以单独使用,也可以组合起来。

你可以先只用 Stargate,用共享口令把几个内部服务保护起来。等共享口令不够用了,再接 Warden,用邮箱、手机号或 user_id 判断谁还能继续登录。等登录流程需要更明确的验证,再接 Herald,把验证码发出去。

再往后,如果服务更多、要求更高,再考虑 Redis、多实例、审计日志、监控指标、HMAC、mTLS,或者直接接入更完整的身份体系。

这条路径的重点不是“功能最多”,而是每一步都能解释清楚为什么要加。

第一天不要太重,但也不要一直停在共享口令里。

Warden 想补的,就是共享口令之后、完整 IAM 之前的那一段。

它让“谁可以进”这件事,从聊天记录、表格、脚本、环境变量和各个服务自己的配置里收回来,变成一份可以被机器稳定查询的准入名册。

这份名单一开始可以只是一个 data.json,但它至少让系统开始知道:谁是这个人、他现在是什么状态、后续验证应该联系哪里,以及需要的话,下游服务能拿到哪些基础身份信息。

这不是什么宏大的系统。

但对很多内部工具、测试服务、小团队平台和 HomeLab 场景来说,已经能把一件长期别扭的事情理顺很多。

先装门,再认人,最后送信。

如果这个项目对你有帮助,欢迎到 GitHub 顺手点个 Star:https://github.com/soulteary/warden

星门启闭,守望验身,鸦使送信。

下一篇,我们继续聊“鸦使”:Herald。

–EOF