diff --git a/public/banners/nocodb-sso-is-a-licensed-feature.png b/public/banners/nocodb-sso-is-a-licensed-feature.png new file mode 100644 index 0000000..6002754 Binary files /dev/null and b/public/banners/nocodb-sso-is-a-licensed-feature.png differ diff --git a/public/og/nocodb-sso-is-a-licensed-feature.png b/public/og/nocodb-sso-is-a-licensed-feature.png new file mode 100644 index 0000000..f89351a Binary files /dev/null and b/public/og/nocodb-sso-is-a-licensed-feature.png differ diff --git a/src/content/posts/nocodb-sso-is-a-licensed-feature.md b/src/content/posts/nocodb-sso-is-a-licensed-feature.md new file mode 100644 index 0000000..77477a3 --- /dev/null +++ b/src/content/posts/nocodb-sso-is-a-licensed-feature.md @@ -0,0 +1,150 @@ +--- +title: "NocoDB SSO Is a Licensed Feature — and the Env Vars Won't Tell You" +description: "NocoDB's OIDC environment variables are real and validated at startup, so self-hosted SSO looks supported. On an unlicensed build, the first login attempt crashes the whole instance." +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 +--- + +If you self-host NocoDB and search for "nocodb sso self-hosted", you'll find a documentation page describing OIDC single sign-on, a set of environment variables, and forum answers telling you it works if you set them. All of that is technically true. None of it tells you the part that matters. + +So here it is: **NocoDB's OIDC SSO is a paid feature.** On an unlicensed self-hosted build, the code path exists but abandons the process — and the failure isn't a polite "SSO is not enabled on this instance". The whole server exits. + +## Why this matters + +Two audiences get bitten here. + +**If you enable it in production, you've built a denial-of-service trigger.** The route that handles the OIDC callback is reachable without authentication — it has to be, that's how a login flow starts. On an instance where that route crashes the process, anyone who requests it takes your database UI down. Not degrades it, not returns an error page: kills the container. You don't need to be a target; a single curious visitor, or a link scanner that follows URLs, is enough. + +**If you're evaluating NocoDB as a platform, this is a licensing signal you should read correctly.** The feature is present in the open-source image, the variables are enforced, the code ships. What's missing is permission. A feature that is *implemented* but *not licensed* looks identical to a feature that works, right up until you try it — and that's the whole trap. + +## What the docs say, and what the image does + +NocoDB's own docs are clear if you read the licensing line: *"OIDC SSO is available on NocoDB Cloud (Business plan and above) and licensed self-hosted deployments (Business plan and above)."* The other half — that the unlicensed binary will not degrade gracefully — isn't documented, and can't be discovered from the docs, because it's a runtime fact about the image. + +The environment variables are real. The ones I confirmed against the shipped image: + +```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 +``` + +Only `NC_SSO` and the provider-name variable appear in the public documentation. The six per-provider variables are undocumented but **enforced**: start the container with `NC_SSO=oidc` and no URLs and it refuses to boot: + +``` +Open ID SSO is enabled but missing required env keys +``` + +That's the detail that makes the trap convincing. Validation at startup means the feature is *wired in*, not vestigial. Nothing about that error message suggests the code path is unreachable without a licence — it reads like a configuration mistake you can fix. + +## How to test it without risking anything + +Never test this on the instance you care about. A throwaway container with an in-image SQLite meta store is enough, and it takes about a minute: + +```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: +``` + +Note the deliberate omission of `NC_DB` — with no external database configured, the container spins up its own SQLite store, so it needs no credentials, touches no production data, and can be deleted with `docker rm -f nc-sso-test`. + +Watch the startup, then request the SSO route once: + +```bash +curl -s -o /dev/null -w '%{http_code}\n' http://localhost:10399/auth/oidc +docker ps -a --filter name=nc-sso-test +``` + +What I got, twice — the second time with a `?workspaceId=` parameter to rule out a bad request shape: + +``` +### UNCAUGHT EXCEPTION ### +unhandledRejection TypeError: _0x418c23 is not a function at docker/index.js:1:27483406 +``` + +...and the container is no longer running. Exit code 1, from an unauthenticated GET. Reproduced on `2026.09.0` with the same result both times. It isn't a configuration error, and it isn't a request the client got wrong. + +Note the log line that explains it, from startup: + +``` +No license key found — running in CE mode +``` + +## The other half of the licensing story + +If your reaction is "fine, I'll buy the licence", read this before you budget for it, because there's a prerequisite that isn't obvious either: + +``` +Instance ID unavailable — PostgreSQL is required for enterprise licensing +``` + +The community edition I run uses **MySQL** as its metadata store, and MySQL cannot generate the instance ID that licensing depends on. So a licence isn't something you activate on the instance you have — it's a licence *plus a metadata migration from MySQL to Postgres*, which is its own project with its own risk, done on a live instance. NocoDB publishes a migration guide for exactly this, which tells you how common the situation is. + +That isn't a complaint about the pricing model. It's the evaluation I wish I'd had before spending an afternoon: the cost of "just buy it" is licence **plus** a storage-engine migration for anyone running the default MySQL setup. + +## The fork that search results recommend — check it's alive first + +Search results for NocoDB SSO without a licence point at `brunostjohn/nocodb-oidc`, a community fork that adds OIDC to the open-source build. It's a drop-in image and it's exactly the thing you want. It's also abandoned, and this is checkable in about two minutes: + +```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 +``` + +A version from 2024 against an instance on a monthly release cadence is not a drop-in — it's a two-year downgrade, taking with it the schema migrations, the security fixes and the API behaviour your automations already depend on. The advice is only as good as the fork is maintained, and search results don't go stale when a repo does. + +**Check the fork's base version and last commit date before you plan around it.** Two curl calls, and they replace a rollback you'd otherwise do at 1 a.m. + +## What to do instead + +1. **Decide whether you need SSO in the app, or in front of it.** If what you want is "the internet can't reach this login form", a forward-auth proxy in front of the hostname gets you there with the open-source build. It's not single sign-on in the app, and the app still asks for its own password — I wrote up that construction, and the ingress trap that made my first attempt look successful while being a no-op, in [The Forward-Auth Gate That Verified Perfectly — and Wasn't Live](/posts/authentik-forward-auth-gate-wasnt-live/). +2. **Keep the app's own login and lock the door at the edge.** For an internal tool with a handful of users, this is what most people actually need. +3. **If a licensed build is genuinely required, price the migration, not just the licence.** Confirm which metadata store you're on first. + +## What I'd do differently + +**When a feature's availability is ambiguous, read it out of the image instead of the docs.** The docs describe intent; the image is the truth. One `docker exec ... printenv | grep -iE 'NC_|OIDC'`, one grep of the shipped bundle, and one throwaway container answered in twenty minutes what an afternoon of documentation reading couldn't. + +And a blunter rule that generalises past NocoDB: **an undocumented-but-enforced environment variable is a licensing boundary, not a feature flag.** If a variable rejects your startup when it's incomplete, but the documentation hides half its siblings, the missing piece is almost never configuration. + +## The result + +Five minutes to reproduce on a throwaway container, twice, with the container dead both times. One accidental finding I'd rather not have learned in production: an unauthenticated request to the SSO route is enough to stop the instance. + +The upside is that I stopped trying to make the app do SSO, put the decision in front of the hostname where it belongs, and can now answer "can NocoDB do SSO for free?" in one sentence instead of an afternoon. + +--- + +## Want this for your business? + +If you're evaluating self-hosted software, need single sign-on in front of internal tools, or want someone to check what your stack actually does versus what its documentation claims, that's the work I do. + +- **WhatsApp:** [011-797 2969](https://wa.me/60127972969) — tap to chat +- **Email:** [me@hoelee.com](mailto:me@hoelee.com?subject=Self-hosted%20SSO%20enquiry) +- **Website:** [hoelee.com](https://www.hoelee.com) + +I set up authentik single sign-on, self-hosted Docker stacks and reverse proxies for small businesses in Malaysia — and I write up what I learn while doing it. diff --git a/src/content/posts/zh/nocodb-sso-is-a-licensed-feature.md b/src/content/posts/zh/nocodb-sso-is-a-licensed-feature.md new file mode 100644 index 0000000..f10fae7 --- /dev/null +++ b/src/content/posts/zh/nocodb-sso-is-a-licensed-feature.md @@ -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: +``` + +注意这里刻意省略了 `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:** [me@hoelee.com](mailto:me@hoelee.com?subject=Self-hosted%20SSO%20enquiry) +- **Website:** [hoelee.com](https://www.hoelee.com) + +我为马来西亚的小企业搭建 authentik 单点登录、自托管 Docker 技术栈和反向代理——并且把做这些事时学到的东西写下来。 \ No newline at end of file