Authentik 接管 Atlassian Data Center 全家桶

动机

公司的 Atlassian 全家桶(Jira / Confluence / Bitbucket / Bamboo,全是 Data Center 版)一直挂在 Crowd 上做用户中心。Crowd 的角色很简单:把 Windows AD 当后端,自己再同步一份用户/组到下游各个 app 里去。单论 Atlassian 这一摊事儿,跑得也算稳,本来没什么动它的理由。

真正想动它,是因为 Authentik 这条新线。Authentik 是不久前刚搭起来的统一身份入口,长期目标是把公司里越来越多的新业务(自建工具、内部看板、第三方 SaaS、新上的开源服务)都收到一个 SSO 入口下面——新业务要么走 SAML、要么走 OIDC,统一接到 Authentik 就完事,不再需要每上一个新服务都重新写一遍 AD 集成、也不再需要把 LDAP 凭据发出去给一堆服务各自持有。

Atlassian 这一摊老资格自然该并进来。从「Atlassian → Crowd → AD」改成「Atlassian → Authentik → AD」之后:

  • 公司里所有服务的登录入口长一个样,不会出现「这套走 Crowd、那套走 Authentik、新业务又是另一套」的割裂
  • 直接跟 AD 说话的服务从一堆收敛到「只有 Authentik」,AD 那边的攻击面和暴露的 bind 凭据数都跟着收
  • 以后想给 Atlassian 加任何额外的访问策略(按 IP、按设备、按角色),在 Authentik 一处加完即可,不用挨个 app 改
  • Crowd 这一层可以一并退役,少一个 Java + Postgres 实例要养

折腾下来发现:只要先做 SAML、再换用户目录、最后退役 Crowd,整条链路可以平滑切。下游已有用户的 issue 分配、权限、关注者一个都不会丢。这篇把每一步、踩过的坑、最终配置都写下来,下次再切别的 app 直接抄。

名词速览

文章里频繁出现的几个缩写和概念,怕第一次接触的读者懵,先过一下:

  • SSO(Single Sign-On)—— 单点登录。一次登录,下游多个系统通用,登录这件事统一收到一个身份服务来做。
  • IdP / SP —— SSO 里的两个角色。IdP(Identity Provider,身份提供方)负责验证用户、签发凭证,本文就是 Authentik;SP(Service Provider,服务提供方)是消费这份凭证的业务系统,本文就是 Jira / Confluence / Bitbucket / Bamboo。
  • SAML(Security Assertion Markup Language)—— 上一代主流 SSO 协议,基于 XML,2005 年定型。Atlassian / Salesforce / 各种企业 SaaS 这类老牌产品基本都支持,本文用的就是它。
  • OIDC(OpenID Connect)—— 新一代 SSO 协议,构建在 OAuth 2.0 之上,基于 JSON / JWT,对 Web 和移动 App 友好。Atlassian Data Center 这一侧,Jira / Confluence / Bitbucket 已经原生支持 OIDC,只有 Bamboo 还得靠第三方插件。本文统一走 SAML 主要是为了四个 app 走同一套配置路径、模板可复用,并不是 OIDC 跑不了——如果改走 OIDC,Bamboo 那一档就要单独引入第三方插件来维护。
  • LDAP / AD —— LDAP 是用户/组目录的查询协议;AD(Active Directory)是微软自家的 LDAP 实现。本文里 AD 是「事实源」,所有用户身份最终都从这里来。
  • NameID / Audience / ACS URL —— SAML 协议里的几个关键字段。NameID 是 IdP 告诉 SP「这个登录的人是谁」用的标识;Audience 是 SP 自报的接收方身份(IdP 用来判断这份断言该发给谁);ACS URL(Assertion Consumer Service)是 SP 用来接收 SAML 断言的入口。后面填配置时反复见到。
  • JIT provisioning(Just-In-Time provisioning)—— 用户第一次 SSO 登录时,SP 在本地数据库自动给 ta 建一个账号。本文没启用,理由后面「迁移思路」里说。
  • Crowd —— Atlassian 自家的用户中心 / SSO 中转层产品,跑在 Server / Data Center 模式下。本文要退役的就是它。

整体架构

切换前:

flowchart LR
    AD[Active Directory
dc.example.com] Crowd[Crowd
crowd.example.com] Jira[Jira] Conf[Confluence] Bit[Bitbucket] Bam[Bamboo] User[User Browser] AD -->|LDAP sync| Crowd Crowd -->|user directory| Jira Crowd -->|user directory| Conf Crowd -->|user directory| Bit Crowd -->|user directory| Bam User -->|login| Crowd

切换后:

flowchart LR
    AD[Active Directory
dc.example.com] Auth[Authentik
auth.example.com] Jira[Jira] Conf[Confluence] Bit[Bitbucket] Bam[Bamboo] User[User Browser] AD -->|LDAP sync
authentik-bind| Auth AD -->|user directory
atlassian-bind read-only| Jira AD -->|user directory
atlassian-bind read-only| Conf AD -->|user directory
atlassian-bind read-only| Bit AD -->|user directory
atlassian-bind read-only| Bam User -->|SAML SSO| Auth Auth -.assertion.-> Jira Auth -.assertion.-> Conf Auth -.assertion.-> Bit Auth -.assertion.-> Bam

迁移思路

两个原则定下来后剩下的就好拆:

  1. AD 是唯一事实源,所有用户和组都从 AD 来
  2. 只允许 Authentik 拿特权 bind(带 Reset Password 那种),别的服务一律用只读 bind

按这两条,主线分三步:

第一步:先上 SAML,用户目录不动 —— 每个 app 加一个 Authentik 的 SAML 配置,登录走 Authentik,但用户依然落在 Crowd 那个目录上。SAML NameID 用 sAMAccountName,恰好就是 Crowd 同步过来的 username key,登录时不会建新账号、也不会有重复。回滚成本极低——一个 REST PATCH 就能把本地登录框打开。

第二步:把用户目录从 Crowd 换成 AD 直连 —— 每个 app 加一个新的「Microsoft Active Directory」目录,bind 用一个新的只读账号 atlassian-bind,base DN 限定 OU=People,group 限定 OU=Atlassian,OU=Groups。用户/组数和 Crowd 那边对齐了,再把 Crowd 那个目录关掉(不删,留底)。

第三步:退役 Crowd —— 先备份整个 Crowd 实例,停掉,观察一个月没人来抱怨,再彻底销毁 + 删 DNS + 删 AD 里的 crowd-administrators 组。

主线之外还有两件配套的事,跟主线穿插着做:第二步完事之后顺手把 REST 的 Basic Auth 关掉收紧攻击面、第三步退役 Crowd 之后调整新员工开通流程。下面按时间顺序一段段写。

JIT provisioning 我没用。理由:JIT 之后组同步是滞后的(首次登录才创建),用户选择器里搜不到没登录过的人,预先建账号也做不了。AD 直连同步的话这些都是顺带就有的。

准备工作

涉及到的服务版本和域名:

服务 版本 域名
Authentik 2026.2.2 auth.example.com
Jira Software Data Center 11.3.1 jira.example.com
Confluence Data Center 10.2.2 wiki.example.com
Bitbucket Data Center 9.4.16 git.example.com
Bamboo Data Center 12.1.1 ci.example.com
Crowd Data Center(待退役) 7.1.3 crowd.example.com

部署上,所有服务都跑在 Docker 里,Docker 又跑在 PVE 的 LXC 容器中。这套基础设施跟本文 SAML 配置没关系,下文不展开——但能解释为什么后面看到 ssh root@auth.example.com 接着 docker exec authentik-server ... 这种写法。

⚠️ 前提:SAML SSO 是 Atlassian Data Center 限定功能。Server 版(已 EOL)和早期 Cloud Standard 都不带。Data Center 自带的「SSO 2.0 / Authentication methods」模块够用,完全不需要装 Resolution / miniorange / Kantega 之类的第三方插件。

AD 这边事先把组建好:

  • confluence-users / bitbucket-users / jira-software-users / bamboo-users
  • 注意 Jira 那个组是 jira-software-users不是 jira-users
  • 全部放在 OU=Atlassian,OU=Groups 下面

每个组里把要给访问权限的人加进去(一开始我同步过来 ~67 人)。

把 SAML 接进来

这一段是登录入口的切换:在 Authentik 上建好四个 SAML provider 和对应的 application,每个 Atlassian app 里再把 SAML 配置填进去。这一阶段 Crowd 还在原位继续做用户目录,登录入口先切走、用户库不动,回滚成本压到最低。

Authentik 侧

共享签名证书

四个 SAML provider 共用一张自签证书,方便统一管理。不要复用通配 TLS 证书——下次 TLS 轮替时四个 SP 全炸。也不要复用 Authentik 自己的 authentik Self-signed Certificate,它有效期只有两年,太短。

直接 Web UI 也能建,但用 ak shell 几行就出来:

1
2
ssh root@auth.example.com
docker exec -it authentik-server ak shell
1
2
3
4
from authentik.crypto.builder import CertificateBuilder
b = CertificateBuilder("atlassian-saml")
b.build(subject_alt_names=[], validity_days=3650) # 10 年
ckp = b.save()

之后在 System → Certificates 里能看到 atlassian-saml,subject 是 CN=atlassian-saml,O=authentik,OU=Self-signed

Property mappings

NameID 用 username(sAMAccountName),下游各个 app 的本地用户 key 就是这个,对得上。三个 default mapping 直接复用:

Mapping 名 SAML attribute 取值
Username http://schemas.goauthentik.io/2021/02/saml/username request.user.username(= sAMAccountName)
Email http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress request.user.email(= AD mail
Name http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name request.user.name

再加一个自定义 Groups mapping 过滤一下:AD 里我有 ~16 个组,但只有 10 个跟 Atlassian 相关,其它的(部门组、内部工具组等)不该塞进 SAML 里。

Customisation → Property Mappings → Create → SAML Provider Property Mapping

字段
Name atlassian-groups
SAML Attribute Name http://schemas.xmlsoap.org/claims/Group
Friendly Name Group

Expression:

1
2
3
4
for group in request.user.groups.all():
dn = group.attributes.get("distinguishedName", "")
if ",OU=Atlassian,OU=Groups," in dn:
yield group.name

Confluence / Bitbucket / Bamboo 的 SAML 配置里没有 Groups 字段可填——它们读组靠 user directory,不靠 SAML。但发出去无害,留个口子万一以后想用 group attribute 决策。

SAML Provider — 一份模板套四次

每个 app 一个 provider。Web UI 点一遍可以,但 ak shell 跑脚本更不容易出错。下面这份是 Bitbucket 的,其它三个改名字、改 ACS、改 audience 就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from authentik.providers.saml.models import SAMLProvider, SAMLPropertyMapping
from authentik.core.models import Application, Group
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.policies.models import PolicyBinding

cert = CertificateKeyPair.objects.get(name="atlassian-saml")
auth_flow = Flow.objects.get(slug="default-authentication-flow")
authz_flow = Flow.objects.get(slug="default-provider-authorization-implicit-consent")
inval_flow = Flow.objects.get(slug="default-provider-invalidation-flow")

m_username = SAMLPropertyMapping.objects.get(name="authentik default SAML Mapping: Username")
m_email = SAMLPropertyMapping.objects.get(name="authentik default SAML Mapping: Email")
m_name = SAMLPropertyMapping.objects.get(name="authentik default SAML Mapping: Name")
m_groups = SAMLPropertyMapping.objects.get(name="atlassian-groups")

p, _ = SAMLProvider.objects.update_or_create(
name="bitbucket-saml",
defaults=dict(
authentication_flow=auth_flow,
authorization_flow=authz_flow,
invalidation_flow=inval_flow,
acs_url="https://git.example.com/plugins/servlet/samlconsumer",
issuer="https://auth.example.com",
audience="https://git.example.com",
sp_binding="post",
signing_kp=cert,
sign_assertion=True,
sign_response=True,
name_id_mapping=m_username,
digest_algorithm="http://www.w3.org/2001/04/xmlenc#sha256",
signature_algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
),
)
p.property_mappings.set([m_username, m_email, m_name, m_groups])

app, _ = Application.objects.update_or_create(
slug="bitbucket",
defaults=dict(
name="Bitbucket",
provider=p,
meta_launch_url="https://git.example.com",
),
)

group = Group.objects.get(name="bitbucket-users")
PolicyBinding.objects.update_or_create(
target=app, group=group,
defaults=dict(order=10, enabled=True),
)

几个非默认值要留意:

  • sp_binding="post":ACS 走 HTTP-POST,不是 Redirect。Atlassian Data Center 这边吃 POST。
  • sign_assertion=Truesign_response=True:内层 <saml:Assertion> 必须签,外层 <samlp:Response> 也签上做纵深防御。Atlassian 两个都接受。第一遍漏了 sign_response,回头补的
  • verification_kp 留空:Atlassian 默认不签 AuthnRequest,Authentik 也不强制要验。
  • encryption_kp 留空:TLS 已经把信道加密了,再做 SAML payload 加密性价比不高。
  • 时钟容差是 Authentik 默认(前后各 5 分钟),别忘了 NTP。

四个 provider 创建完,记下来每个的 ID(在 Applications → Providers 里能看到,比如 5/6/7/8),后面取 IdP metadata 用得到:

1
https://auth.example.com/api/v3/providers/saml/<id>/metadata/?download

每个 application 在 Applications → Applications 里再绑一个 Group policy binding,限定只有 <app>-users 这个 AD 组的成员能 SSO 进来。组外用户在 IdP 这一层就被拒了,根本到不了 Atlassian。

不需要 Outpost

四个 app 都直接说 SAML,跟 Authentik 之间不走 LDAP outpost,也不走 proxy outpost。Outpost 那一套这里完全用不上。

Atlassian 侧

四个 app 的 SAML 配置长得几乎一样,最大差异是各自的 break-glass URL 不一样——下面单独列表。

共通字段

字段
Configuration name Authentik
Login button text Sign in with Authentik
Single sign-on issuer https://auth.example.com
Identity provider single sign-on URL https://auth.example.com/application/saml/<slug>/sso/binding/redirect/
X.509 Certificate atlassian-saml 的完整 PEM
Username mapping ${NameID}
JIT provisioning OFF
Update users on login OFF
Encrypt assertions OFF
Show IdP on the login page ON
Remember user logins ON

关于 Username mapping —— Atlassian 这个字段既能填 SAML 属性 URI(比如 http://schemas.goauthentik.io/2021/02/saml/username),也能填占位符 ${NameID}优先用 ${NameID}:URI 形式在不同版本的 Atlassian SAML 里支持度参差,${NameID} 永远 work,少一层映射。

关键步骤顺序

四个 app 的操作我都按这个顺序走,最重要的是先开 fallback 再保存 SAML。否则 SAML 一保存出问题,自己也进不去:

  1. REST PATCH 打开 fallback(先做这一步):

    1
    2
    3
    curl -u <admin> -X PATCH "https://<app>/rest/authconfig/1.0/sso" \
    -H "Content-Type: application/json" \
    -d '{"enable-authentication-fallback": true}'
  2. UI 进 Authentication methods → Add configuration → SAML single sign-on

  3. 共通字段照上表填,证书 PEM 用「SSH 拿出来 → pbcopy」的办法贴(见后面踩坑 #2)

  4. 保存,开无痕窗口测:浏览器到 https://<app> → 跳 Authentik → AD 密码 → 落到原来那个用户(不应该是新建账号)

  5. 测 break-glass URL(每个 app 不一样,下一节列表)能进本地登录框

  6. 一切 OK 后,再 PATCH 一次把本地登录框藏起来,让 SAML 成默认入口:

    1
    2
    3
    curl -u <admin> -X PATCH "https://<app>/rest/authconfig/1.0/sso" \
    -H "Content-Type: application/json" \
    -d '{"show-login-form": false}'
  7. 下一个 app

Confluence

UI 路径:Confluence Administration → General Configuration → Authentication methods(在「USERS & SECURITY」组里)。

Break-glass URL:https://wiki.example.com/login.action?auth_fallback

Confluence 的 SAML 表单没有 Groups attribute 字段,也没有 Sign requests 字段(那是 Jira-only)。

Bitbucket

UI 路径:Bitbucket Administration → Accounts → Authentication methods → Add configuration → SAML single sign-on

Break-glass URL:https://git.example.com/login?auth_fallback(注意是 /login不是 /login.action

Git over SSH / git over HTTPS 都不走 SAML——SSH 用 key,HTTPS 用 HTTP access token(前缀 BBDC- 那种)。SAML 切完不影响 git 操作。后面要禁 Basic Auth 时记得 /scm/* 加白名单,否则 git push/pull 直接挂。

Jira

UI 路径:Jira Administration → System → Authentication methods → Add configuration → SAML single sign-on

Break-glass URL:https://jira.example.com/login.jsp?auth_fallback

Jira 11.1+ 的表单比其它三个多两栏:

  • NameID Policy:选 Unspecified(跟 Authentik 默认对齐)
  • Sign requests:OFF(Authentik 这边没要求 SP 签 AuthnRequest)

还有一个 Jira-only 的开关 Include customer logins (JSM)——我把它关掉,让 JSM 的客户门户继续走本地登录:

1
{"show-login-form-for-jsm": true}

⚠️ Jira 还有个「匿名用户能看到 dashboard 骨架」的坑,跟 SAML 没关系但是切完 SSO 一定会注意到。详见后面踩坑 #5。

Bamboo

UI 路径:Bamboo Administration → Security → Authentication methods → Add configuration → SAML single sign-on

Break-glass URL:https://ci.example.com/userlogin.action?auth_fallback(注意是 userlogin.action

Bamboo 12.1.1 没有原生 OIDC 支持(要 OIDC 得装 Kantega / miniorange 之类的第三方插件),SAML 是 Atlassian 自带方案的唯一选项。Remote agent(比如 agent-win)走 agent token 鉴权,不受 SAML 影响

让 SAML 成为默认

四个 app 都没有「Set as default」按钮。机制是:把 Username/password 那一行的 Show on login page 关掉。关掉之后匿名用户访问 https://<app> 直接重定向到 Authentik;?auth_fallback URL 仍然能用,因为它走的是另一条 servlet。

REST 等价:{"show-login-form": false}

Break-glass URL 大表

四个 app 的 break-glass 全都不一样,单独存一份留着:

App URL
Confluence https://wiki.example.com/login.action?auth_fallback
Bitbucket https://git.example.com/login?auth_fallback
Jira https://jira.example.com/login.jsp?auth_fallback
Bamboo https://ci.example.com/userlogin.action?auth_fallback

前提是 enable-authentication-fallback: true 已经 PATCH 过。

踩过的坑

坑 1 — 第一遍漏了 sign_response

Authentik 的 SAML provider 默认 sign_response=Falsesign_assertion=True。第一遍创建 Confluence 的时候直接保存了,后来回头补。理论上 sign_assertion 已经够 Atlassian 验证完整性了,但同时签 response 没坏处,做纵深防御:

1
2
3
p = SAMLProvider.objects.get(name="confluence-saml")
p.sign_response = True
p.save()

后面 Bitbucket / Jira / Bamboo 一开始就两个都开。

坑 2 — 证书 PEM 贴进去每行第一个字符被吞了

atlassian-saml 的 PEM 从 Authentik UI 复制出来贴到 Confluence 的 X.509 字段,结果 -----BEGIN CERTIFICATE----- 变成了 ----BEGIN CERTIFICATE----,每行 base64 也都丢了第一个字符。SAML 这种是直接签名校验失败。

不是证书的问题,是渲染层吃字符——某些 UI 控件对每行第一个字节的处理有 bug。绕开:

1
2
3
4
5
6
7
8
9
10
# 在 Authentik 容器里把证书导出到 /tmp
ssh root@auth.example.com \
"docker exec authentik-server ak shell -c \
'from authentik.crypto.models import CertificateKeyPair; \
print(CertificateKeyPair.objects.get(name=\"atlassian-saml\").certificate_data)' \
| grep -v '^{' | grep -v '###' | grep -v 'objects imported' \
> /tmp/atlassian-saml.crt"

# 直接拉到本机剪贴板
ssh root@auth.example.com cat /tmp/atlassian-saml.crt | pbcopy

然后 ⌘V 贴进去,零字符丢失。

坑 3 — ?auth_fallback 也跳 Authentik

第一次配完 Confluence,访问 https://wiki.example.com/login.action?auth_fallback,结果也跳了 Authentik。意味着 fallback 没生效。

原因是 enable-authentication-fallback 默认是 false,UI 里没那个开关,必须走 REST:

1
2
3
curl -u <admin> -X PATCH "https://wiki.example.com/rest/authconfig/1.0/sso" \
-H "Content-Type: application/json" \
-d '{"enable-authentication-fallback": true}'

PATCH 完再访问 ?auth_fallback,本地登录框就回来了。所以这一步必须放在保存 SAML 之前——否则 SAML 一旦配错,自己也进不去。

坑 4 — UI 路径已经改名了

网上很多老教程写「User Management → SSO 2.0 → Add SAML configuration」,Data Center 10.x 现在的路径是「General Configuration → Authentication methods」。SSO 2.0 这个老名字基本只在文档里能见到。四个 app 的菜单都重命名过,照新的找。

坑 5 — Jira 匿名页面绕开 SAML

切完 SAML 后,开无痕窗口访问 https://jira.example.com/secure/Dashboard.jspa直接 200 返回 dashboard 骨架——虽然没有真实数据(只有 “Loading…” 占位),但页面能渲染、URL 不跳登录。/browse/ 倒是正常 302 到登录。

试过两条路:

  1. 把全局 Use Jira / Browse Projects 权限里的 Anyone 摘掉——没用,dashboard 骨架照样出来
  2. Jira mode 切 Private——我已经是 Private 了

最后是开 Jira 的 dark feature:

1
https://jira.example.com/secure/SiteDarkFeatures!default.jspa

里面输 public.access.disabled,Enable。这个 dark feature 从 Jira 7.2.10 就有了,全局把所有匿名 URL 都重定向到 /login.jsp,再也没有骨架可看。

⚠️ 副作用:监控的健康检查 不能再打 /,要打 /status。后者永远匿名访问,返回 {"state":"RUNNING"}。LB 那边记得改。

坑 6 — Confluence 的 SAML 表单没有 “Sign requests” 字段

UI 上确实没有,因为「Sign requests」是 Jira 11.1+ 才加的,其它三个 app 都不暴露这个开关。Authentik 这边 verification_kp=None,本来就不要求 SP 签 AuthnRequest,所以表单上有没有都无所谓,不用纠结。

验证

每个 app 切完之后这套都跑一遍:

  1. 无痕窗口 → https://<app> → 跳 Authentik → AD 密码 → 落到已有用户(不是新建)
  2. ?auth_fallback URL → 本地登录框出来
  3. 把测试账号从 <app>-users 组里临时拿掉 → 重新登录 → Authentik 直接拒绝(”You do not have access to this application”)
  4. 跨 app SSO:登完 Confluence 再访问 Jira,无须重新输密码
  5. App link / 各种集成(Confluence 引 Jira issue、Bitbucket commit 关联 Jira ticket)依然能用——这些走 OAuth 不走 SAML
  6. Bitbucket 的 git push/pull、Bamboo 的 remote agent 也都正常

Authentik 这边查事件日志确认登录成功:

1
2
3
4
5
6
7
8
ssh root@auth.example.com "docker exec authentik-server ak shell -c '
from authentik.events.models import Event
from datetime import datetime, timedelta, timezone
since = datetime.now(timezone.utc) - timedelta(minutes=15)
for e in Event.objects.filter(created__gte=since).order_by(\"-created\")[:20]:
if e.action in (\"login\",\"login_failed\",\"authorize_application\",\"configuration_error\"):
print(e.created, e.action, e.user, e.context.get(\"application\", \"-\"))
'"

正常情况会看到 login 后面跟着 authorize_application,application 字段就是 confluence / jira 之类。

把用户目录从 Crowd 换成 AD 直连

SAML 跑稳一周以后,开始换用户目录。整体思路:每个 app 加一个新的 AD 目录,bind 用一个新的只读账号 atlassian-bind,等同步完毕、用户/组数对上、user key 也对上,再把 Crowd 那个目录 disable(不删)

创建 atlassian-bind AD 账号

在 AD 域控上 PowerShell:

1
2
3
4
5
6
7
8
9
10
11
12
$pw = Read-Host -AsSecureString "Password for atlassian-bind"

New-ADUser `
-Name "atlassian-bind" `
-SamAccountName "atlassian-bind" `
-UserPrincipalName "atlassian-bind@example.com" `
-Path "OU=Services,DC=example,DC=com" `
-AccountPassword $pw `
-Enabled $true `
-PasswordNeverExpires $true `
-CannotChangePassword $true `
-Description "Atlassian apps LDAP read-only bind"

不要给它 Reset Password 之类的特权 ACL——只用默认 Domain UsersOU=People/OU=Groups 上的读权限就够了。Atlassian 这边只读模式不会写回 AD。

在每个 Atlassian app 里加 AD 用户目录

User Management → User Directories → Add Directory → Microsoft Active Directory

字段
Name AD - example.com
Directory type Microsoft Active Directory
Permissions Read Only, with Local Groups ← 必须这个,atlassian-bind 没写权限
Hostname dc.example.com
Use SSL
Port 636
Username CN=atlassian-bind,OU=Services,DC=example,DC=com
Password 上面建的密码
Base DN DC=example,DC=com
Additional User DN OU=People
Additional Group DN OU=Atlassian,OU=Groups ← 注意限定到 Atlassian 子 OU,不要拉整个 OU=Groups
User name attribute sAMAccountName
User unique ID objectSid
User RDN cn
User object filter (&(objectCategory=person)(objectClass=user))
Group object filter (objectCategory=group)
Group membership attribute member
User membership attribute distinguishedName
Synchronisation interval 60 minutes

保存 → Test → Sync。Sync 完成后把新目录的优先级拉到 Crowd 上面,然后访问用户选择器搜几个人,确认拿到的是新目录里的副本(看 Directory 列)。

验数

直接到每个 app 的 Postgres 里查:

1
2
3
4
5
6
7
8
-- Confluence / Jira / Bitbucket / Bamboo 都是 cwd_* 这套表
SELECT id, directory_name, lower_directory_name, active
FROM cwd_directory ORDER BY directory_position;

SELECT directory_id, COUNT(*)
FROM cwd_user
WHERE active = 'T'
GROUP BY directory_id;

新旧两个目录的活跃用户数应该一致(我这里两边都是 67~68)。Confluence/Jira 的 user key(比如 JIRAUSER101012c9280828f99ac2a018f99b208a90003)是按 username 落进 cwd_user 的,目录切换不会重发新 key——这就是为什么前面坚持 NameID = sAMAccountName 的原因,整条链路 user identity 都用 username 串起来,issue 分配、权限、关注者全部稳如老狗。

关掉 Crowd 那个目录

每个 app 的 Crowd 目录 Disable,不要 Delete。Disable 之后用户不会消失(key 还在),只是不再从 Crowd 拉新数据。万一 AD 这边出问题,Crowd 一键回滚。

关 Basic Auth on REST

第二步切完用户目录之后,顺手把 REST 接口的 Basic Auth 关了。四个 app 健康检查里都会有一条「Basic Authentication Disabled」的告警,Atlassian 推荐切 SSO 后把 REST API 的 Basic Auth 关掉,逼大家用 PAT(personal access token)。

1
PUT /rest/basicauth/1.0/config
App body 备注
Confluence {"block-requests": true, "allowed-paths": [], "allowed-users": []} 没东西用 Basic
Jira 同上 同上
Bamboo 同上 同上
Bitbucket {"block-requests": true, "allowed-paths": ["/scm/*"], "allowed-users": []} /scm/* 必须放过,git over HTTPS 还得用 HTTP access token 走 Basic

验证:

1
2
3
4
5
6
7
8
# 匿名 Basic → 应该 403
curl -u 'fake:fake' https://wiki.example.com/rest/api/space

# Bearer PAT → 200
curl -H "Authorization: Bearer $PAT" https://wiki.example.com/rest/api/space

# Bitbucket /scm/* → 401(白名单内,只是没真凭据)
curl -u 'fake:fake' https://git.example.com/scm/myrepo/info/refs?service=git-upload-pack

退役 Crowd

跑了一两个月,没人来抱怨「我登不进 Jira」「我看不到 Confluence 空间」之后,可以彻底干掉 Crowd 了:

  1. 给 Crowd 实例做一份完整备份,单独存档并标记为受保护,免得被保留策略误删
  2. 停服务,关掉开机自启
  3. 观察一个月,期间没人哭就继续
  4. 彻底销毁实例

收尾:

  • DNS 里删掉 crowd.example.com 的 A 记录
  • AD 里 Remove-ADGroup -Identity "crowd-administrators" 那个组也清掉

Crowd 退役后的新员工开通流程

Crowd 还在的时候有个隐藏作用:「Group memberships when creating accounts」会在新账号第一次登录时自动塞进 confluence-users 等几个组。Crowd 没了之后,新员工进 AD 但没自动加 Atlassian 组,Authentik 这一层就直接拒了,根本登不进任何 Atlassian app。

替代方案两条:

  1. 在 AD 域控上跑 Sync-AtlassianGroups.ps1 这种定时任务,根据 OU 自动把 Employees 加到对应 *-users 组里
  2. 在「新员工建账号」流程里直接加上四个组——我后来改的是这条,写了个 New-EmployeeUser.ps1,建账号时 Add-ADGroupMember 一并搞定,省得再起一个定时任务

任选一种。外包之类的临时账号要走人工审核,塞默认。

一些零碎经验

  • AD bind 严格分两份authentik-bind 有 Reset Password ACL(密码 writeback 用),只给 Authentik 用;atlassian-bind 只读,给四个 app。其它服务想接 AD 都各自再开只读账号,不要互相串。
  • Audience / Issuer / ACS URL 必须严丝合缝:差一个 / 都会在 Atlassian 这边被拒。Authentik 的 audience 写 https://jira.example.com,Atlassian 这边的 base URL 也得是同样的、不带末尾斜杠。
  • NTP:Authentik 和四个 Atlassian 实例都要走同一台 NTP,时钟差超过 5 分钟 SAML 直接拒。
  • 不要拿通配 TLS 证书签 SAML:TLS 证书每年/每两年一轮替,每次都得重新分发 IdP 元数据给四个 SP。专门一张 10 年自签 atlassian-saml
  • App link 不动:四个 app 之间的 OAuth 互信不依赖 SAML,切了不用动。
  • SP-initiated only:我没配 IdP-initiated 流程,从 Authentik My Apps 里点 tile 是 IdP-initiated,会走得通但不是主用法。

小结

切下来回头看,整条链路其实没那么吓人——把节奏拆成「先 SAML、再换用户目录、再退 Crowd」三段,每一段都能独立回滚。最容易踩的反而不是 SAML 本身,是这些边边角角:回退开关没开、UI 路径改名、Jira 匿名页面、证书贴歪。把这些列出来记一份,下次再切别的 app 就轻车熟路。

Crowd 服役这么多年也算尽职尽责,但一个十多年没换 UI、还要单独跑 Java 实例的 SSO 中转层,确实可以光荣退役了。Authentik 这边除了少了一跳,多端 SSO、MFA、未来想加什么社交登录也都顺手。

References

Atlassian Data Center 官方文档

Authentik

第三方扩展(Bamboo OIDC 替代方案)