Add Chinese translation and make i18n mandatory for all posts
Deploy / build (push) Successful in 34s

This commit is contained in:
2026-09-09 03:07:29 +08:00
parent 203400a731
commit b3108b2c7c
3 changed files with 172 additions and 3 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ draft: false
## 8. i18n policy
- **English is primary and non-negotiable** — the whole SEO strategy targets English queries.
- **Chinese (zh):** translate only the 23 best case studies. Cheap differentiation, opens zh-SG/zh-MY search.
- **Chinese (zh): translate every post.** Same filename in `posts/zh/` (auto language-switch). Opens zh-SG/zh-MY search and covers bilingual recruiters.
- **Malay: skip for v1** — no dev-audience demand (the main site already has a dangling Malay config; don't repeat it).
### File layout (enforced by code)
+2 -2
View File
@@ -96,9 +96,9 @@ When you finish a difficult piece of work, use this shape — it's simultaneousl
## 8. i18n sync — Chinese translation (do this automatically, every post)
The DigiKedai case study established the standard: **flagship `case-studies` posts get a Chinese translation automatically.** When writing or updating a post, apply this flow without being asked:
Every post gets a Chinese translation, **every time**, without being asked:
1. **Translate flagship case studies.** Any `category: case-studies` post gets a Chinese version at `src/content/posts/zh/<same-slug>.md` — **same filename**, so the language switcher auto-links the two (no `translation:` field needed). Do NOT translate `notes`, quick tutorials, or tool roundups unless the user asks.
1. **Translate every post.** Any new post gets a Chinese version at `src/content/posts/zh/<same-slug>.md` — **same filename**, so the language switcher auto-links the two (no `translation:` field needed).
2. **Keep EN and ZH in sync.** If you edit the English post (add a section, fix a fact, update a link), make the **same edit** to the Chinese version in the same commit. Never let the two versions drift.
3. **Translate the *frontmatter* too** — title and description go to Chinese, but `category`, `tags`, and `pubDate` stay identical to the English post (they're data, not prose).
4. **Code/commands stay in English** inside the Chinese post (see §6) — only the prose around them is translated.
@@ -0,0 +1,169 @@
---
title: "Authentik 从 2025.8 升级到 2026.8:那些没人提醒你会坏掉的部分"
description: "一年份的 authentik 大版本升级踩坑记录:存储挂载变更、RBAC 会话清理、可信代理,以及 authorization_flow 与 authentication_flow 搞混导致 SSO 彻底瘫痪。"
pubDate: 2026-09-09
category: devops
tags: [authentik, sso, docker, portainer, self-hosting, oidc, upgrade]
---
我把 authentik 当作自托管体系前面的单点登录闸门——邮箱、仪表盘、Synology
应用、远程访问 outpost 都在它后面。很长一段时间它停在 **2025.8.3**,一年的
版本更新堆积成山。这篇文章记录我是怎么一口气把它升到 **2026.8.1** 的,以及
沿途每一步坏掉的地方——尤其是我犯的那个让单点登录彻底瘫痪、逼每个内部
应用都重新要密码的错误。
## 为什么要升级
2025.8.3 本身没坏。但它落后太久,后面的版本里已经累积了一堆 CVE 漏洞修复,
而且我正准备做按应用区分品牌。authentik 自己的策略是不允许跨大版本跳级——
它强制要求逐级走。所以计划是:
```
2025.8.3 → 2025.10 → 2025.12 → 2026.2 → 2026.5 → 2026.8
```
六跳,一次一步,每步之间跑一次迁移 + 一次健康检查。动手之前,有一个绝对
不能省的步骤:**备份数据库**。authentik 不支持降级。如果迁移只跑了一半,
你是从备份恢复,而不是回滚镜像 tag。
```bash
sudo docker exec authentik-postgres pg_dump -U authentik authentik > authentik-backup.sql
```
## 坑 1Portainer 才是真相源,不是 compose 文件
我的第一反应是编辑磁盘上的 `docker-compose.yml` 然后 `up`。错的。这个 stack
是由 **Portainer**(stack 143)管理的,真正的 compose 和环境变量都保存在它
自己的存储里。磁盘上的 `.env` 早就过时了——它的 `PG_PASS` 跟 Portainer 实际
运行的对不上。
正确的更新路径是走 Portainer API,而不是文件系统:
1. 更新 compose 内容里的镜像 tag。
2. **先** `docker pull` 新镜像(这样 API 调用不会在拉取中途超时)。
3. `docker stop` + `docker rm` 正在运行的容器(否则固定的 `container_name`
会在重新部署时冲突)。
4. 用新的 `StackFileContent` + `Env``PUT /api/stacks/143?endpointId=2`
我撞上了容器名冲突错误、拉取超时错误,还有一个网络连接问题——重建后的
`authentik-server` 只接入了它两个网络里的一个,导致无法解析 `postgres-server`
一旦你看懂了门道,每个问题都是五分钟的修复;但凑在一起,耗掉了我大半个晚上。
## 坑 2:存储挂载迁移了(2025.12)
在 2025.12 之前,品牌资源放在 `/media/` 下,通过 `/media/...` 提供服务。
2025.12 之后存储结构变了:文件迁移到 `/data/media` 结构,通过一个新的
`/files/media/public/<name>?token=...` URL(带 JWT 签名)提供服务。我的容器
还在挂载 `./media:/media`,所以我一跨过那个版本,每个 logo、favicon、背景图
全部 404。
修复方式就是官方文档里的迁移:
```bash
mkdir -p data && mv media data/media
```
……然后把挂载改成 `./data:/data`。新的文件后端还要求 `/data` 必须是一个真正的
挂载点才肯工作——我最开始的修复用的是符号链接,结果后端 `is_mount()` 检查
直接报错 `No file management backend configured`
## 坑 3:RBAC 迁移留下一张被污染的表
2025.12 为了 RBAC 重构,删除了旧的 `authentik_core.User_groups` 模型。迁移
本身跑得很干净,但**旧会话**仍然引用着这个已删除的模型。结果:登录页一直抛
`LookupError: App 'authentik_core' doesn't have a 'User_groups' model`
不是 `django_session`——那张表是空的。真正的元凶是 authentik 自己的
`authentik_core_session` 表。清空它(以及其他 session 表)强制所有人重新登录,
错误也随之消失:
```sql
TRUNCATE authentik_core_session;
```
有个副作用值得知道:这也会让你各应用持有的所有 OIDC refresh token 失效。它们
会把你弹去重新登录一次,然后恢复。这是一次性的麻烦,不是 bug。
## 坑 4:可信代理现在变成了显式选择(2026.8)
2026.8 收紧了默认的转发头处理。之前 authentik 信任所有内网段;现在它只信任
你显式列出的那些。在转发到 `localhost` 的 Synology 反向代理后面,意味着:
```yaml
environment:
AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS: 127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,::1/128
```
跳过这一步,代理头就会被拒绝,表现出来的认证失败症状,跟你实际上遇到的问题
长得几乎完全不像。
## 坑 5:那个搞坏 SSO 的——authorization_flow 和 authentication_flow 搞混
这是最贵的一个错误,而且如果你要做按应用区分品牌,特别容易搞混。
authentik 里的 provider 有**两个** flow 字段,含义完全不同:
```python
# "未登录用户访问关联应用时用的认证 flow" ← 登录页
authentication_flow = models.ForeignKey(...)
# "授权这个 provider 时用的 flow" ← OAuth 授权确认页
authorization_flow = models.ForeignKey(...)
```
- `authentication_flow` 是**登录页**。你要给每个应用定制标题和背景,应该把
各自独立的 flow 放在这里。
- `authorization_flow` 是**已登录**用户的授权确认(consent)步骤。
我想要每个应用不同的背景图,于是给每个应用建了一个 flow,然后把 provider 的
`authorization_flow` 指向它。瞬间就坏了:一个已经认证的用户,每次打开另一个
应用,还是会被拖过完整的识别 → 密码 → MFA 流程。单点登录,实际上等于没了。
修复就是一条 UPDATE,把两个字段放回它们该在的位置:
```sql
UPDATE authentik_core_provider
SET authorization_flow_id = '1d85b1b1-...', -- explicit-consent flow
authentication_flow_id = 'aa##-per-app-flow'
WHERE ...;
```
而品牌本身——按应用区分的背景和标题——是放在 **flow 自身**的 `background`
`title` 字段上(2026.8 起可写),而不是放在 brand 的域名匹配上。
这个坑后面还藏着一个相关的坑。我创建那 18 个按应用区分的 flow 时,它们全都
落成 `designation=authentication`,而我的 `auth.hoelee.com` brand 的
`flow_authentication``NULL`。authentik 在一个 brand 没有显式认证 flow 时的
回退逻辑,是**按 slug 字母序**选第一个 authentication flow——碰巧就是
`auth-agent`,而不是 `default-authentication-flow`。于是根登录页开始显示我
agent 的背景图。把 brand 的 `flow_authentication` 设成真正的默认 flow 才修好。
还有一个跟 RBAC 坑叠加的问题:创建 flow 会设置它的 `background`,但**不会设置
它的 stages**——`stages` 在 flow 对象上是只读的。一个没有任何 stage 绑定的空
flow,正是迁移早期登录页无限重定向循环的罪魁祸首。stage 绑定是单独创建的:
```
POST /api/v3/flows/bindings/ # { target: "<flow pk>", stage: "<stage pk>", order: N }
```
## 我会怎么做得不一样
整场折腾归结为三个本可避免的模式:
1. **绝不要靠猜字段的语义**——我把 `authorization_flow` 当成了「登录 flow」,
而真相源是模型定义,字段自己的 docstring 里就把区别写得明明白白。
2. **跟 API 表面保持距离**——每次服务器重启,API token 都中途过期,结果关键
修复我直接用 `psql` 对着数据库做了。可靠,但应该在集群着火**之前**就脚本化,
而不是着火的时候。
3. **每次重启只处理一个破坏性变更**——我试图一口气推理存储、RBAC、代理的
全部变化。每一个如果单独隔离、独立验证,都会很简单。
## 结果
authentik 现在跑着 **2026.8.1**——最新、已打补丁、所有容器健康——18 个应用各自
在登录页显示自己的背景和标题,SSO 在每个子域都正常工作。我日常用的 17 个应用,
从「每次切换应用都要重新登录」回到了「登录一次,畅通无阻」。
值得记住的教训:认证基础设施的大版本升级,大概 10% 是「改镜像 tag」,90% 是
「数据模型、存储结构、代理规则在你脚下全都挪了位」。做好备份,一次走一个版本,
当某个东西表现得完全不合理时,伸手去抓下一份配置之前,先把字段名再念一遍。