它是一个面向 Traefik 和 Nginx 的极简 ForwardAuth 开源鉴权网关,负责登录会话与请求访问控制,让你的后端服务哪怕没有写认证代码,也能被统一保护起来。

写在前面

几个月前,为了解决内部服务、工具面板、多个域名应用之间反复处理“登录”和“访问控制”的问题,我把前几年折腾反向代理、内部服务和认证入口的经验,整理成了一个开源小工具:Stargate(星空之门)。

它的目标很简单:把认证逻辑前移到网关侧,让反向代理先替你拦住请求,而不是让每个业务服务都重新写一套登录、鉴权、跳转和会话判断。

soulteary/stargate

开源项目地址在:https://github.com/soulteary/stargate,如果你觉得还不错,欢迎“一键三连”,顺手点个 Star。

如果你手里有一堆内部面板、测试服务、临时工具站点,或者 HomeLab 里的各种小服务,那么这类问题大概率并不陌生。

一开始,我们往往只是想“先跑起来”。

但服务越来越多之后,真正麻烦的事情就来了。

问题是怎么出现的

一开始,问题通常并不明显。

你可能只是有一个监控面板、一个测试后台、一个临时文档站,或者一个内部 API 调试页。它们都在内网里,访问的人也不多,于是很容易先裸奔。

这在早期很正常。

毕竟,比起搭一套登录系统,我们更关心的是服务能不能跑、功能能不能用、问题能不能先解决。

但随着时间往后走,服务会越来越多,域名会越来越多,访问的人也会越来越多。这个时候,“先裸奔一下”就会慢慢变成一个长期存在的隐患。

常见的问题包括:

  • 每个服务都单独做登录,重复劳动很多;
  • 有些服务是第三方工具,根本不好改代码;
  • 多个域名之间反复登录,体验很差;
  • 临时口令散落在不同服务里,维护起来很麻烦;
  • 想接完整 SSO,但眼前只是想先把门装上;
  • 明知道应该加认证,但又不想为了一个小工具引入一套大系统。

所以很多时候,我们并不是缺一个登录页面。

我们缺的是一个能够稳定工作在入口处的认证闸门。它不需要特别复杂,但要足够好用。

它最好能放在反向代理后面,业务服务前面;先替我们把未认证的请求拦下来,再把通过认证的请求放进去。

这就是我做 Stargate 的出发点。

以前通常怎么解决

遇到这类问题,常见做法大概有几种。

第一种,是每个服务自己写登录。

如果只有一个应用,这样当然没问题。用户体系本来就是业务的一部分,登录、注册、权限、会话都放在应用里,是很自然的事情。

但内部工具和业务系统不太一样。

很多时候,它们只是一些监控面板、调试页面、临时后台、文档站或者第三方工具。为了这些服务分别实现一套登录逻辑,不仅麻烦,而且后面维护起来也很碎。

登录页风格不统一,口令规则不统一,会话过期逻辑不统一,API 请求和浏览器请求的处理方式也不统一。

等服务数量多起来之后,这些“小麻烦”就会变成长期负担。

第二种,是继续裸奔。

开发环境里这样做问题不大。服务只在本机跑,或者只给自己临时用一下,确实没必要一开始就把认证体系做得很重。

但如果这些服务开始暴露到公网,或者团队里越来越多人一起使用,再继续裸奔就不太合适了。

很多安全问题不是一开始就爆发的,而是从“这个服务应该没人知道吧”“这个地址应该没人会访问吧”开始的。

第三种,是直接接入完整的 SSO 或 IAM 系统。

比如 Keycloak、Authentik、Authelia 这类方案,能力都很完整,可以处理用户、权限、MFA、OIDC、SAML、策略控制等一整套问题。

如果你要做的是企业级身份治理,这些方案当然更合适。

但对很多个人项目、小团队内部工具、HomeLab 或临时服务来说,它们又显得有点重。

你可能只是想给几个服务先加一道门,并不想第一天就把完整身份中台、数据库、邮件服务、回调地址、证书、策略规则全都配起来。

第四种,是使用 OAuth2 Proxy 这类认证代理。

如果你已经有 GitHub、Google、企业 IdP 或公司 SSO 作为统一登录入口,这类方案很好。它们可以把第三方身份提供方接到反向代理前面,让服务复用已有登录体系。

但如果你的需求更简单:只是想先保护一组内部服务,不想依赖外部 IdP,也不想为了一个工具站点配置一长串 OAuth 回调和 client secret,那么它仍然会有一些额外成本。

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

  • 不需要改业务代码。
  • 不要求你一开始就有完整身份系统。
  • 不强制依赖第三方 IdP。
  • 先解决一个问题:在入口处,把未认证的请求拦下来。

为什么我做了 Stargate

我希望 Stargate 先做一件很小、但很常见的事情:把认证变成入口能力。

它不是完整身份中台,也不试图替代企业里的 IAM / SSO 系统。它更像一块基础设施胶水,放在反向代理后面,接在业务服务前面,用尽可能低的部署成本,先把一组服务保护起来。

所以 Stargate 的取舍很明确。

  • 它应该足够轻,最好一个服务就能跑起来。
  • 它应该足够直接,不需要复杂配置文件,主要通过环境变量完成配置。
  • 它应该能独立工作,即使你还没有 Warden、Herald、SSO 或其他外部身份系统,也能先用密码认证把入口保护起来。
  • 它应该能和 Traefik 这类反向代理自然配合,不要求业务服务改代码。
  • 它也应该给后续增强留出空间,比如多域名登录态、白名单、验证码、TOTP、Redis 会话、审计日志和监控指标。

但这些增强能力,不应该成为第一天使用它的门槛。

这也是我在做 Stargate 时比较在意的一点:先把门装上,再把门锁换好。

很多工具之所以最后没有被用起来,不是因为能力不够,而是因为第一步太重。

Stargate 想反过来,先让第一步足够轻。

它的起点应该是轻的:先让你能用很少的配置,把一个服务保护起来。等它真的成为入口的一部分,再逐步增加更强的认证能力和生产部署能力。

对很多内部工具、测试服务和 HomeLab 场景来说,这种节奏会更舒服。

Stargate 是怎么工作的

Stargate 使用的是 Forward Auth 模式。

这个模式名字听起来有点抽象,但它的工作方式其实很简单。正常情况下,用户访问一个服务时,请求会直接经过反向代理,再进入后端服务。

加入 Stargate 之后,反向代理会先多问一步:

这个请求已经登录了吗?可以放行吗?

如果 Stargate 判断这个请求已经通过认证,就返回通过,反向代理继续把请求转发给后端服务。

一个基础的验证页面

如果 Stargate 判断这个请求还没有登录,就不会让它直接进入后端服务,而是跳转到登录页,或者对 API 请求返回 401 Unauthorized

用一张简单的流程图表示,大概是这样:

用户访问内部服务
Traefik 收到请求
Traefik 先请求 Stargate 的 /_auth
Stargate 检查登录状态
已登录:返回 200,Traefik 放行请求
未登录:跳转登录页,或者返回 401

这个过程里,

  • 后端服务并不需要知道“登录”这件事。
  • 不需要实现登录页面。
  • 不需要处理会话。
  • 不需要判断用户是否已经认证。

只需要继续提供自己的业务能力,入口处的 Stargate 会负责把未认证的请求挡在外面。

这也是 Stargate 最适合解决的问题:不改业务代码,先在入口处加一道门。

当然,这里有一个前提:你的服务访问路径应该统一经过反向代理。如果用户可以绕过 Traefik 或 Nginx,直接访问后端服务本身,那 Stargate 就拦不住它。

所以 Stargate 更适合放在“统一入口”已经存在,或者你准备整理统一入口的环境里。

用 Docker Compose 快速跑起来

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

下面的示例使用示例域名是 auth.test.localhostwhoami.test.localhost。如果你的环境不能自动解析这些域名,需要提前在 DNS 或 /etc/hosts 中把它们指向 Traefik 所在机器。

Stargate 的最小运行配置并不复杂,核心只需要两个配置:

  • AUTH_HOST:认证服务自己的访问域名;
  • PASSWORDS:允许登录的口令。

下面是一个最小化示例。

这里假设你已经有一个 Traefik 网络,名字叫 proxy。如果你的网络名字不同,把下面的 proxy 替换成自己的网络名即可。

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

    environment:
      - AUTH_HOST=auth.test.localhost
      - PASSWORDS=plaintext:test1234|test1337
      - LANGUAGE=zh

    networks:
      - proxy

    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

networks:
  proxy:
    external: true

这段配置里,真正需要先看懂的是三部分。

第一部分,是 Stargate 自己的配置:

environment:
  - AUTH_HOST=auth.test.localhost
  - PASSWORDS=plaintext:test1234|test1337
  - LANGUAGE=zh

AUTH_HOST 表示认证服务自己的域名。

PASSWORDS 表示可以用来登录的口令。

这里使用的是 plaintext,也就是明文口令,适合本地测试和快速体验。

真实环境里不要这样用,后面我们会再说。

第二部分,是把 Stargate 暴露给 Traefik:

- traefik.http.routers.stargate.rule=Host(`auth.test.localhost`) || Path(`/_session_exchange`)
- traefik.http.services.stargate.loadbalancer.server.port=80

这表示当用户访问 auth.test.localhost 时,会进入 Stargate 的登录页面和认证相关接口。(这里额外匹配 /_session_exchange,是为了后续多域名登录态交换使用。第一次只跑单域名体验时,可以先不深究它。)

第三部分,是定义一个叫 stargate 的 ForwardAuth 中间件:

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

后面其他服务只要挂上这个中间件,请求就会先经过 Stargate 检查。

启动服务:

docker compose up -d

启动后,可以先访问:

http://auth.test.localhost/_login?callback=localhost

如果能看到登录页,说明 Stargate 已经跑起来了。

这一步完成后,它还只是一个认证服务。

接下来,我们需要把它挂到真正要保护的后端服务上。

接入一个真实服务

Stargate 跑起来之后,下一步就是把它挂到真正要保护的服务上。

这里用 traefik/whoami 做例子。

whoami 是一个很适合测试反向代理配置的小服务。访问它时,它会把请求信息原样展示出来,方便我们确认 Traefik 路由和中间件是否生效。

示例配置如下:

services:
  whoami:
    image: traefik/whoami:v1.11
    container_name: whoami
    restart: unless-stopped

    networks:
      - proxy

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

      - traefik.http.routers.whoami.entrypoints=http
      - traefik.http.routers.whoami.rule=Host(`whoami.test.localhost`)
      - traefik.http.routers.whoami.service=whoami

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

      - traefik.http.routers.whoami.middlewares=stargate

networks:
  proxy:
    external: true

这里最关键的是这一行:

- traefik.http.routers.whoami.middlewares=stargate

它表示:访问 whoami.test.localhost 时,请求不要直接进入 whoami 服务,而是先经过前面定义好的 stargate 中间件。

也就是说,访问链路会变成:

用户访问 whoami.test.localhost
Traefik 收到请求
Traefik 调用 Stargate 的 /_auth
Stargate 判断是否已经登录
已登录:进入 whoami
未登录:跳转 Stargate 登录页

启动服务:

docker compose up -d

这里需要注意,如果你把 Stargate 和 whoami 写在同一个 compose.yaml 里,执行一次 docker compose up -d 即可。如果分成两个目录,则分别在对应目录执行。

然后访问:

http://whoami.test.localhost

如果你还没有登录,会先看到 Stargate 的登录页面。

输入前面配置的测试口令:test1234,登录完成后,再回到 whoami.test.localhost,就能看到 whoami 返回的请求信息。

到这里,一个最小的认证入口就跑通了。

后端服务没有改代码,也没有自己实现登录逻辑,只是因为在 Traefik 上挂了一个中间件,就被 Stargate 保护起来了。

常用配置说明

Stargate 的配置方式比较直接,主要通过环境变量完成。

第一次使用时,不需要把所有配置都看完。先理解下面几个最常用的就够了。

AUTH_HOST:认证服务在哪里

AUTH_HOST 表示 Stargate 自己的访问域名。

比如:

AUTH_HOST=auth.example.com

它告诉 Stargate:认证入口应该运行在哪个域名下。

当用户访问被保护的服务但还没有登录时,请求会被引导到这个认证域名下完成登录。

所以一般来说,你会准备一个单独的认证域名,比如:

auth.example.com

然后把其他服务放在类似下面的域名上:

grafana.example.com
whoami.example.com
docs.example.com

这样结构会比较清楚:

auth.example.com      负责登录
grafana.example.com   被保护的服务
whoami.example.com    被保护的服务
docs.example.com      被保护的服务

PASSWORDS:用什么口令登录

PASSWORDS 用来配置可以登录 Stargate 的口令。

它的格式是:

algorithm:password1|password2|password3

前面的 algorithm 表示口令使用什么算法,后面是具体口令,多个口令之间用 | 分隔。

例如:

PASSWORDS=plaintext:test123|admin456

这表示允许两个口令登录:

test123
admin456

其中 plaintext 表示明文口令。

它很适合本地测试,因为直观、方便、好排查问题。

但真实环境里不要使用明文口令。

生产环境建议使用更安全的方式,比如:

PASSWORDS=bcrypt:<your-bcrypt-hash>

或者:

PASSWORDS=sha512:<your-sha512-hash>

我个人比较建议先用 plaintext 在本地或测试环境把链路跑通。

确认 Traefik 路由、登录跳转、Cookie、后端服务访问都没问题之后,再把口令算法换成更适合生产环境的配置。

还是那句话:先把门装上,再把门锁换好。

如果你只保护一个服务,通常不需要太关心 COOKIE_DOMAIN

但如果你有多个子域名,比如:

auth.example.com
grafana.example.com
whoami.example.com
docs.example.com

你大概率不希望每访问一个服务都重新登录一次。

这时可以配置:

COOKIE_DOMAIN=.example.com

这样 Stargate 设置的会话 Cookie 就可以在 *.example.com 之间共享。

用户只需要在 auth.example.com 登录一次,后续访问 grafana.example.comwhoami.example.comdocs.example.com 时,就可以复用这次登录状态。

这对 HomeLab、多内部面板、多工具站点的场景很有用。

登录页和请求头的几个可选配置

除了上面三个核心配置,Stargate 也提供了一些轻量的定制项。

比如界面语言:

LANGUAGE=zh

登录页标题:

LOGIN_PAGE_TITLE=我的认证服务

登录页页脚:

LOGIN_PAGE_FOOTER_TEXT=©2026 Your Company

认证成功后,Stargate 也可以给后端服务写入用户相关请求头。默认情况下,会使用类似这样的请求头:

X-Forwarded-User: authenticated

如果你的后端服务需要感知“这个请求已经通过入口认证”,可以读取这类请求头。不过要注意:后端服务应该只信任来自反向代理的请求。不要让用户绕过 Traefik 直接访问后端服务,否则这些请求头就失去了意义。

这也是前面反复强调“统一入口”的原因。

在 Traefik ForwardAuth 场景下,如果希望认证服务返回的用户头传递给后端服务,需要在 Traefik middleware 上显式配置要转发哪些响应头,比如:

- traefik.http.middlewares.stargate.forwardauth.authResponseHeaders=X-Forwarded-User

如果想暴露更多的 Stargate 返回的用户信息,比如邮箱、用户 ID,也可以扩展成:

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

真实使用时的几个建议

如果你准备在自己的环境里使用 Stargate,我建议不要一上来就全量切换。

基础设施类工具最怕的不是“能力不够”,而是第一步就铺得太大。

一开始就把所有服务、所有域名、所有高级配置都接进来,看起来很完整,但一旦出了问题,很难判断到底是路由问题、Cookie 问题、认证问题,还是后端服务自己的问题。

所以我更建议小步接入。

第一步,先选一个不那么敏感的内部工具。

比如测试面板、文档站、开发环境 Dashboard,或者像前面一样用 whoami 做验证。

这个阶段只需要确认几件事:

  • Traefik 路由是否正确;
  • Stargate 登录页是否能打开;
  • 未登录时是否会跳转;
  • 登录后是否能回到原服务;
  • 后端服务是否只能通过反向代理访问。

先把这条最短链路跑通,比一上来追求完整更重要。

第二步,再接入多个子域名。

当单个服务跑通之后,再把多个内部服务接进来。

比如:

grafana.example.com
docs.example.com
whoami.example.com

这时再配置:

COOKIE_DOMAIN=.example.com

重点验证多域名之间的登录态是否符合预期。

也就是:登录一次之后,访问其他子域名时,是否还需要重复登录。

第三步,把测试口令换成生产口令。

开发环境里使用 plaintext 很方便,配置直观,也容易排查问题。

但真实环境里不要使用明文口令。

等路由、跳转、Cookie 都确认没问题之后,再把口令配置换成更安全的形式,比如:

PASSWORDS=bcrypt:<your-bcrypt-hash>

或者:

PASSWORDS=sha512:<your-sha512-hash>

这一步很像换锁。

门已经装好了,接下来把临时锁换成更结实的锁。

第四步,再考虑多实例、审计和监控。

如果 Stargate 只是保护少量内部服务,单实例已经足够简单好用。

但如果它开始保护多个系统,甚至变成一组服务的统一入口,就可以继续考虑:

  • 使用 Redis 保存会话,方便多实例共享登录状态;
  • 开启审计日志,记录关键认证行为;
  • 接入健康检查和指标监控,方便排查问题;
  • 限制 /metrics 等内部接口的访问范围。

这些能力很适合生产部署,但不必在第一天全部打开。

先让系统跑起来,再让系统跑得更稳。

第五步,再考虑 Warden、Herald 和 TOTP。

如果共享口令已经不够用了,再考虑把认证能力往上加。

比如用 Warden 管理用户白名单,用 Herald 发送验证码,再用 TOTP 支持 Authenticator 动态码。

这样登录流程就可以从“大家共用一个口令”,逐步升级成“每个人都有自己的身份和验证方式”。

这条路径会比一开始就引入完整身份系统轻很多。

小步接入,逐步增强,才容易排错,也容易建立信心。

后续还能怎么增强

Stargate 可以完全独立运行,只使用密码认证。

这也是它最轻的一面。

你只需要一个认证域名、一组口令,再把中间件挂到后端服务上,就能先把内部服务保护起来。

但它并不是只能停留在共享口令阶段。

如果你希望它更接近生产环境,也可以继续往上加能力。

Warden:守望者

Warden 可以理解成 Stargate 后面的“准入名册”。

它负责回答一个问题:

这个人是不是允许进来?

当你不想再依赖共享口令,而是希望根据用户邮箱、手机号或账号判断准入资格时,就可以把 Warden 接进来。

在这套体系里,Stargate 负责守入口,Warden 负责判断“谁可以进”。

Herald:鸦使

Herald 可以理解成负责送信的“鸦使”。

它负责验证码、OTP 和通知送达。

比如用户登录时,需要通过邮箱或短信收到验证码;或者某些验证流程需要发送一次性口令,这类事情就可以交给 Herald。

在这套体系里,Stargate 负责拦请求,Warden 负责认人,Herald 负责把验证信息送到用户手里。

TOTP:动态验证码

如果你希望支持 Authenticator 这类动态验证码,也可以继续接入 TOTP 能力。

这样用户可以先绑定动态验证码,再使用账号加 6 位动态码完成登录。

这比共享口令更适合多人协作场景。

Redis:多实例会话

默认情况下,小规模使用不需要引入额外依赖。

但如果你要运行多个 Stargate 实例,或者希望服务重启后登录状态不容易丢失,就可以使用 Redis 保存会话。

这样多个 Stargate 实例之间可以共享登录状态,后续做滚动更新或故障恢复也会更稳。

Audit Log 和 Metrics:可观测性

认证系统一旦变成入口,就不能只关注“能不能跑”。

还要关注:

  • 谁登录了;
  • 谁失败了;
  • 哪些请求被拦住了;
  • 验证码服务有没有异常;
  • 会话创建和销毁是否正常。

所以 Stargate 也可以继续接入审计日志和指标监控。

这些能力不是第一天必须开启的东西,但它们会让 Stargate 从“能用”变成“更适合长期运行”。

我更建议的节奏是:

  • 先用 Stargate 把门装上。
  • 再用 Warden 管好谁能进。
  • 再用 Herald 把验证码送出去。
  • 最后,再补上 Redis、审计和监控,把它变成一套更完整的内部认证入口。

最后

很多内部服务的安全问题,并不是因为大家不知道认证重要。

而是因为“给每个服务都写一遍登录”太麻烦,“一次性引入完整身份系统”又太重。

于是中间地带长期空着。

要么先裸奔,等以后再说。

要么为了一个不大的问题,引入一套很重的系统。

Stargate 想填的,就是这个中间地带。

它把认证前移到入口,把复杂度从业务代码里拿出来,把“每个应用重复实现登录”变成“接入反向代理的一项通用能力”。

这不是一个很宏大的目标。

但在真实的内部服务、测试环境、HomeLab 和小团队工具场景里,它能解决的问题很具体。

你可以先用最简单的密码认证把门装上。

等服务变多、使用的人变多、认证要求变高之后,再逐步接入 Warden、Herald、TOTP、Redis、审计日志和监控指标。

当你需要完整 IAM 时,应该选择完整 IAM。

当你只是想今天就把一堆内部站点先保护起来,Stargate 会是一个很顺手的选择。

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

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

下一篇系列文章,让我们聊聊“守望者”。

–EOF