Add post: NocoDB SSO Is a Licensed Feature (EN + ZH)
Deploy / build (push) Successful in 25s

This commit is contained in:
2026-09-29 01:14:17 +08:00
parent e7e4ee58ef
commit 2d0de72f51
4 changed files with 300 additions and 0 deletions
@@ -0,0 +1,150 @@
---
title: "NocoDB SSO 是授权功能:环境变量不会告诉你的事"
description: "NocoDB 的 OIDC 环境变量是真的,启动时也会校验,所以自托管 SSO 看起来完全受支持。但在未授权的构建上,第一次登录尝试就会让整个实例崩溃。"
pubDate: 2026-09-29
category: notes
tags: ["nocodb", "sso", "oidc", "self-hosting", "licensing", "docker"]
ogImage: /og/nocodb-sso-is-a-licensed-feature.png
banner: /banners/nocodb-sso-is-a-licensed-feature.png
draft: false
---
如果你自托管 NocoDB,去搜一下「nocodb sso self-hosted」,你会找到一篇介绍 OIDC 单点登录的文档页、一组环境变量,以及一堆论坛回复告诉你「设置好这些就能用」。这些在技术上都是真的。但没有一样会告诉你真正要紧的那部分。
所以直说了:**NocoDB 的 OIDC SSO 是付费功能。**在未授权的自托管构建上,代码路径是存在的,但它会直接抛弃进程——而且失败的方式也不是一句客气的「此实例未启用 SSO」。整个服务器直接退出。
## 为什么这件事重要
有两种人会在这里踩坑。
**如果你在生产环境启用它,你就给自己造好了一个拒绝服务的触发器。**处理 OIDC 回调的路由不需要认证就能访问——它必须如此,登录流程就是这样开始的。在一个这条路由会让进程崩溃的实例上,任何请求它的人都能把你的数据库界面打挂。不是变慢,不是返回错误页:而是直接干掉容器。你甚至不必成为攻击目标;一个好奇的访客,或者一个顺着 URL 爬的链接扫描器,就够了。
**如果你正在评估 NocoDB 这个平台,这是一个你该读对的授权信号。**功能在开源镜像里是有的,变量会被强制校验,代码也一并发布。缺的只是许可。一个*已实现*、*未授权*的功能,看起来和能正常工作的功能一模一样,直到你真的去试——而这正是整个陷阱所在。
## 文档说了什么,镜像又做了什么
NocoDB 自己的文档,只要你读到授权那一行,其实写得很清楚:*「OIDC SSO 在 NocoDB Cloud(Business 及以上版本)以及获得授权的自托管部署(Business 及以上版本)中可用。」*但另一半——未授权的二进制不会优雅降级——文档里没有写,从文档里也无从发现,因为这是关于镜像本身的运行时事实。
环境变量是真的。以下是我对照随镜像发布的内容确认过的:
```yaml
NC_SSO: oidc
NC_SSO_OIDC_ISSUER: https://auth.example.com/application/o/your-app/
NC_SSO_OIDC_AUTHORIZATION_URL: https://auth.example.com/application/o/authorize/
NC_SSO_OIDC_TOKEN_URL: https://auth.example.com/application/o/token/
NC_SSO_OIDC_USERINFO_URL: https://auth.example.com/application/o/userinfo/
NC_SSO_OIDC_CLIENT_ID: ...
NC_SSO_OIDC_CLIENT_SECRET: ...
NC_OIDC_PROVIDER_NAME: Hoelee SSO
```
公开文档里只出现了 `NC_SSO` 和 provider-name 变量。其余六个按 provider 区分的变量没有文档,但**会被强制校验**:用 `NC_SSO=oidc` 启动容器、却不给任何 URL,它直接拒绝启动:
```
Open ID SSO is enabled but missing required env keys
```
正是这个细节让陷阱显得可信。启动时的校验意味着这个功能是*接好线的*,不是残留代码。那条错误消息没有任何地方暗示:没有授权,这条代码路径就不可达——它读起来就像一个改改配置就能修好的错误。
## 怎么在零风险的情况下测试
千万别在你关心的实例上测试。一个用镜像内置 SQLite 元数据存储的一次性容器就够了,大约一分钟:
```bash
docker run -d --name nc-sso-test -p 10399:8080 \
-e 'NC_SSO=oidc' \
-e 'NC_SSO_OIDC_ISSUER=https://auth.example.com/application/o/your-app/' \
-e 'NC_SSO_OIDC_AUTHORIZATION_URL=https://auth.example.com/application/o/authorize/' \
-e 'NC_SSO_OIDC_TOKEN_URL=https://auth.example.com/application/o/token/' \
-e 'NC_SSO_OIDC_USERINFO_URL=https://auth.example.com/application/o/userinfo/' \
-e 'NC_SSO_OIDC_CLIENT_ID=test' \
-e 'NC_SSO_OIDC_CLIENT_SECRET=test' \
-e 'NC_OIDC_PROVIDER_NAME=Test SSO' \
nocodb/nocodb:<your-tag>
```
注意这里刻意省略了 `NC_DB`——没有配置外部数据库,容器会自己拉起一个 SQLite 存储,所以它不需要任何凭据、不碰生产数据,一条 `docker rm -f nc-sso-test` 就能删掉。
观察启动过程,然后请求一次 SSO 路由:
```bash
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:10399/auth/oidc
docker ps -a --filter name=nc-sso-test
```
我得到的、两次都是——第二次我加了个 `?workspaceId=` 参数,排除请求格式不对的可能:
```
### UNCAUGHT EXCEPTION ###
unhandledRejection TypeError: _0x418c23 is not a function at docker/index.js:1:27483406
```
……然后容器就不再运行了。一次未认证的 GET,退出码 1。在 `2026.09.0` 上复现,两次结果一模一样。这不是配置错误,也不是客户端把请求形状搞错了。
注意启动日志里那句解释了一切的话:
```
No license key found — running in CE mode
```
## 授权故事的另一半
如果你的反应是「行,那我买授权」,在把它写进预算之前先读读这段,因为这里还有一个同样不明显的先决条件:
```
Instance ID unavailable — PostgreSQL is required for enterprise licensing
```
我跑的社区版用 **MySQL** 作为元数据存储,而 MySQL 无法生成授权所依赖的实例 ID。所以授权不是你在现有实例上激活一下就完事——它是授权*外加一次从 MySQL 到 Postgres 的元数据迁移*,这本身就是个自带风险的项目,还得在线上实例上执行。NocoDB 为此专门发布了迁移指南,光这一点就说明这种情况有多常见。
这不是在抱怨定价模式。这是我希望能提前拥有的评估:对任何跑默认 MySQL 配置的人来说,「直接买」的真实成本是授权**加上**一次存储引擎迁移。
## 搜索结果推荐的那个 fork——先确认它还活着
不买授权搜 NocoDB SSO,搜索结果会指向 `brunostjohn/nocodb-oidc`——一个给开源构建加上 OIDC 的社区 fork。它是即插即用的镜像,正是你想要的东西。但它也被弃养了,这一点大约两分钟就能查证:
```bash
# what version of NocoDB is the fork based on?
curl -s https://raw.githubusercontent.com/brunostjohn/nocodb-oidc/main/packages/nocodb/package.json \
| python -c "import sys,json; print(json.load(sys.stdin)['version'])"
# 0.255.2
# when was it last touched?
curl -s 'https://api.github.com/repos/brunostjohn/nocodb-oidc/commits?per_page=1' \
| python -c "import sys,json; print(json.load(sys.stdin)[0]['commit']['author']['date'])"
# 2024-10-29
```
一个 2024 年的版本,对上一个按月发版的实例,不是即插即用——那是倒退两年的降级,还会带走你的自动化已经依赖的 schema 迁移、安全修复和 API 行为。建议的好坏只取决于 fork 有没有人继续维护,而搜索结果是不会因为仓库弃养而过时的。
**在围绕它做规划之前,先查一下 fork 的基础版本和最后一次提交的日期。**两条 curl,就能免掉你原本要在凌晨一点做的回滚。
## 那该怎么办
1. **先想清楚你需要的是应用内的 SSO,还是应用前面的 SSO。**如果你要的是「互联网够不到这个登录表单」,那么在主机名前放一个 forward-auth 代理,用开源构建就能达成。它不是应用内的单点登录——应用仍然会要自己的密码。我把这个搭建方案,以及那个让我的第一次尝试看起来成功、实际上毫无作用的 ingress 陷阱,写在了[验证完美通过、实际并未生效的 Forward-Auth Gate](/posts/authentik-forward-auth-gate-wasnt-live/)里。
2. **保留应用自己的登录,在边缘把门锁上。**对只有几个用户的内部工具来说,这才是大多数人真正需要的。
3. **如果确实需要授权构建,把迁移也计入成本,而不只是授权本身。**先确认你用的是哪种元数据存储。
## 如果重来一次,我会怎么改
**当一个功能的可用性模糊不清时,从镜像里读答案,而不是看文档。**文档描述的是意图;镜像才是真相。一条 `docker exec ... printenv | grep -iE 'NC_|OIDC'`、一次对随镜像发布代码的 grep、一个一次性容器,二十分钟就回答了读一下午文档都答不出来的问题。
还有一条更直白、也适用于 NocoDB 之外的规则:**没有文档、却被强制校验的环境变量,是一条授权边界,而不是功能开关。**如果一个变量在没填完整时拒绝启动、而文档又藏起了它一半的兄弟变量,那么缺的那块几乎从来不是配置。
## 结果
在临时容器上五分钟复现两次,两次容器都死了。还有一个我宁可别在生产环境才学到的意外发现:对 SSO 路由的一次未认证请求,就足以让整个实例停下来。
好的一面是,我不再试图让应用自己去做 SSO,而是把决定放到了它该在的位置——主机名前面,现在也能用一句话回答「NocoDB 能免费做 SSO 吗?」,而不是花一下午。
---
## 想给业务用上这套?
如果你正在评估自托管软件、需要内部工具前面的单点登录,或者想让别人去核对你的技术栈实际做了什么、对照文档声称它做了什么——这正是我做的工作。
- **WhatsApp:** [011-797 2969](https://wa.me/60127972969) ——点一下就能聊
- **Email:** [[email protected]](mailto:[email protected]?subject=Self-hosted%20SSO%20enquiry)
- **Website:** [hoelee.com](https://www.hoelee.com)
我为马来西亚的小企业搭建 authentik 单点登录、自托管 Docker 技术栈和反向代理——并且把做这些事时学到的东西写下来。