1. 项目背景与核心需求拆解
1.1 这个标题到底在说什么
先把标题拆开看:Linux 使用 Samba 控制 Active Directory 指定 uid。这句话里其实藏着三个独立的技术域,以及一个把它们串起来的核心诉求。
第一个域是Linux 身份认证。Linux 系统里每个用户都有一个数字化的 uid,文件权限、进程归属、家目录归属全靠这个数字说话。第二个域是Samba 服务,它是 Linux 世界里实现 SMB/CIFS 协议的事实标准,负责和 Windows 生态做文件共享与身份对接。第三个域是Active Directory,也就是常说的 AD 域,它是企业里集中管理账号、权限、组策略的目录服务。
把它们串起来的核心诉求是:当 Linux 机器加入 AD 域之后,AD 里的用户登录到这台 Linux 机器时,系统给他们分配的 uid 不能是随机漂移的,必须由我们人为指定并稳定控制。
为什么这件事重要?因为 uid 一旦漂移,后果很直接:昨天某个 AD 用户在家目录里创建的文件,uid 是 10001,今天他再登录,系统给他分配了 10057,那么他昨天创建的文件就变成了"别人的文件",读写权限全乱。对于跑着数据库、共享存储、CI 构建节点的服务器来说,这种漂移是灾难性的。
1.2 谁需要看这篇内容
如果你符合下面任意一条,这篇内容就是写给你的:
- 你所在的环境用 AD 统一管理账号,同时有一批 Linux 服务器需要让 AD 用户直接登录;
- 你正在用 Samba 做文件共享,共享目录的属主属组需要和 AD 用户精确对应;
- 你遇到过 AD 用户登录 Linux 后 uid 每次不一样、文件权限错乱的问题;
- 你在做容器或构建系统,需要 AD 用户在宿主机上有固定 uid,才能和挂载卷权限对齐。
反过来说,如果你的环境里 Linux 用户全是本地账号,或者你只是想让 Windows 访问一个匿名共享目录,那这篇内容对你价值不大,可以跳过。
1.3 整体方案选型的逻辑
要让 AD 用户在 Linux 上有稳定 uid,业界常见有三条路。
第一条是纯 SSSD 方案,用ldap_id_mapping或者ldap_user_uid_number从 AD 属性里取 uid。这条路的问题在于,AD 默认的uidNumber属性往往没填,或者填得乱七八糟,而且不同 AD 架构(单域、多域、林)行为差异大,调起来很折磨人。
第二条是winbind 方案,Samba 自带的 winbind 负责把 AD 用户映射成 Linux 用户。它默认用一套算法从 SID 推导出 uid,也就是所谓的idmap机制。这套机制本身是稳定的,但推导出来的 uid 通常落在 10000 以上的大区间,而且你没法直接说"我要让张三的 uid 是 2001"。
第三条就是本文要讲的Samba + AD + 显式 uid 映射。核心思路是:让 Samba 负责和 AD 通信、做认证,但 uid 的分配不走自动推导,而是通过idmap config里的range配合ldb数据库,或者通过ad后端的idmap_ldb手动写入映射关系,把指定 AD 用户或组钉死在一个确定的 uid/gid 上。
我选第三条作为主线,原因是它可控性最强。自动推导的 uid 你只能接受,显式映射的 uid 你能决定。对于需要和既有系统(比如老 NFS 存储、老备份系统)对接的场景,可控性比省事重要得多。
2. 核心原理与关键概念解析
2.1 SID 与 uid 的映射关系
要理解 uid 控制,先得理解 AD 里的SID。SID 是安全标识符,形如S-1-5-21-1111111111-2222222222-3333333333-1001。前面那串S-1-5-21-...是域 SID,最后一段1001是相对标识符 RID。AD 里每个用户和组都有唯一的 SID,RID 在域内唯一。
Linux 这边只认 uid 这个数字。所以 Samba 的 winbind 必须做一件事:把 SID 翻译成 uid。翻译方式有两种:
- 算法映射(idmap_autorid / idmap_rid / idmap_tdb):拿 RID 做数学运算,加上一个基数,得到 uid。比如
idmap_rid就是uid = RID + 基数。这种方式不需要存储,但 uid 区间不可控。 - 显式映射(idmap_ldb / idmap_ad):直接查一张表,表里写着"SID 对应 uid 是多少"。这张表可以是 AD 里的
uidNumber属性,也可以是本地 ldb 数据库。
本文要控制 uid,走的就是显式映射这条路。区别只在于这张"表"放在哪:放在 AD 里就是idmap_ad,放在本地就是idmap_ldb或手动写 tdb。
2.2 idmap 后端的选择逻辑
Samba 的idmap config支持多种后端,选哪个直接决定你能不能控制 uid。我把常见后端列个表对比一下。
| 后端 | uid 来源 | 能否指定具体 uid | 适用场景 |
|---|---|---|---|
| idmap_tdb | 本地 tdb 数据库自动分配 | 可以,但需手动写库 | 单机、小规模 |
| idmap_rid | RID + 基数算法推导 | 不能,只能控区间 | 单域、追求简单 |
| idmap_autorid | 自动划分区间 | 不能 | 多域、免维护 |
| idmap_ad | 读 AD 的 uidNumber 属性 | 可以,改 AD 属性即可 | 企业域环境 |
| idmap_ldb | 本地 ldb 显式记录 | 可以,手动写映射 | 需要精细控制 |
从这张表能看出来,要精确控制 uid,idmap_ad和idmap_ldb是主力。idmap_ad的好处是映射关系跟着 AD 走,多台 Linux 机器天然一致;坏处是你得有权限改 AD 的uidNumber属性,而且这个属性默认在 AD 用户属性里是隐藏的,需要先扩展 schema 或者用高级工具才能写。
idmap_ldb的好处是不依赖 AD 属性,本地说了算;坏处是每台机器都得维护一份映射,机器多了容易不一致。
我的建议是:能改 AD 就优先idmap_ad,改不了 AD 就用idmap_ldb配合配置管理工具批量下发。下面两节分别展开。
2.3 为什么不能直接用 nss_ldap 取 uidNumber
有人会问:既然 AD 里有uidNumber属性,我直接用nss_ldap或sssd读它不就行了,为什么要绕 Samba?
原因是Samba 和纯 LDAP 客户端在 AD 环境下的认证机制不同。AD 用的是 Kerberos,而且它的 LDAP 实现有一堆私有扩展。纯 LDAP 客户端要对接 AD,得处理 referral、加密、SASL 绑定等一堆细节,配置复杂且容易出问题。Samba 的 winbind 把这些都封装好了,你只要net ads join一下,认证、票据、映射它全包了。
所以在这个场景里,Samba 不只是文件共享服务,它同时承担了AD 客户端的角色。uid 控制是它众多职责中的一项。
3. 环境准备与前置配置
3.1 基础环境要求
在动手之前,确认你的环境满足这些条件:
- Linux 发行版:主流发行版均可,本文以通用配置为例,不绑定具体版本;
- Samba 版本:建议 4.8 以上,低版本对
idmap_ad的支持有坑; - 网络:Linux 机器能解析并访问 AD 域控的 DNS、Kerberos、LDAP 端口;
- 时间:Linux 和域控时间偏差不能超过 5 分钟,否则 Kerberos 直接拒绝;
- 权限:你有 AD 域的加入权限,以及(如果用
idmap_ad)修改用户属性的权限。
时间同步这条我要单独强调。Kerberos 对时间极其敏感,偏差超过默认的 5 分钟窗口,票据申请就会失败,报错通常是KRB5KRB_AP_ERR_SKEW。很多人排查半天认证问题,最后发现是时间没同步。上生产前务必确认 NTP 或 chrony 正常工作。
3.2 安装必要组件
不同发行版包名不同,核心是这几个:Samba 主体、winbind、Kerberos 客户端、以及 AD 命令行工具。
# 通用思路,按你的发行版调整包管理器 # 安装 samba、winbind、krb5 客户端 # 以及用于测试的 ldap 工具安装完成后,确认smbd、nmbd、winbindd三个服务都在。winbindd是负责 uid 映射的关键进程,它没起来,映射一定不生效。
3.3 配置 Kerberos 与 DNS
先配/etc/krb5.conf,把默认域、域控地址填对。关键字段是default_realm、dns_lookup_kdc、dns_lookup_realm。如果 DNS 能正确解析域控的 SRV 记录,后两个设为 true 最省事。
DNS 这块有个常见坑:Linux 机器的/etc/resolv.conf必须指向能解析 AD 域的 DNS 服务器,通常是域控本身。如果你指向了公网 DNS,_kerberos._tcp.域名这类 SRV 记录解析不出来,加入域会失败。测试方法很简单:
# 查询 kerberos 服务的 SRV 记录 # 能返回域控地址说明 DNS 配置正确3.4 加入 AD 域
配置好/etc/samba/smb.conf的基础部分后,执行加入域的操作。核心配置项包括workgroup(填域名的短名)、realm(填大写域名)、security = ads。
[global] workgroup = YOURDOMAIN realm = YOURDOMAIN.LOCAL security = ads # idmap 配置稍后单独展开加入域的命令是net ads join -U 有权限的账号。执行成功后会提示加入成功。如果失败,看报错:Failed to join domain通常是 DNS 或时间问题;Preauthentication failed通常是密码或账号问题。
提示:加入域之前,建议先用
kinit 账号测试能否拿到 Kerberos 票据。票据能拿到,说明 Kerberos 链路通了,再加入域会顺利很多。
4. 指定 uid 的两种落地方式
4.1 方式一:idmap_ad 读取 AD 属性
这是最"正统"的做法。思路是:在 AD 里给每个需要登录 Linux 的用户填上uidNumber和gidNumber属性,然后让 Samba 的idmap_ad后端去读。
配置长这样:
[global] security = ads idmap config * : backend = tdb idmap config * : range = 3000-7999 idmap config YOURDOMAIN : backend = ad idmap config YOURDOMAIN : schema_mode = rfc2307 idmap config YOURDOMAIN : range = 10000-999999 winbind nss info = rfc2307 winbind enum users = yes winbind enum groups = yes这里几个参数要解释清楚。idmap config * : range是给非域用户(比如本地系统账号映射)用的兜底区间。idmap config YOURDOMAIN : range是给域用户用的区间,AD 里填的 uidNumber 必须落在这个区间内,否则 winbind 会拒绝使用,回退到算法映射。
schema_mode = rfc2307是关键,它告诉 Samba 按 RFC2307 标准去读uidNumber、gidNumber、loginShell、homeDirectory这些属性。winbind nss info = rfc2307则让 winbind 把这些属性通过 NSS 暴露给系统。
AD 那边怎么填uidNumber?默认情况下这个属性在图形界面里看不到,需要用高级工具。填的时候注意:uidNumber 在域内必须唯一,且不能和 Linux 本地已有 uid 冲突。我一般建议从 10000 开始规划,给本地账号留出 0-9999 的空间。
4.2 方式二:idmap_ldb 本地显式映射
如果改不了 AD 属性,就走本地映射。Samba 4 自带一个 ldb 数据库,可以用net idmap系列命令操作。
先配置后端:
[global] security = ads idmap config * : backend = tdb idmap config * : range = 3000-7999 idmap config YOURDOMAIN : backend = ldb idmap config YOURDOMAIN : range = 10000-999999然后手动写入映射。核心命令是net idmap set,把 SID 和 uid 绑起来:
# 先查出用户的 SID # wbinfo -n 用户名 # 再把 SID 映射到指定 uid # net idmap set <SID> <uid>写入之后,用wbinfo -i 用户名验证,看返回的 uid 是不是你指定的那个。如果是,说明映射生效。
这种方式的好处是完全本地可控,不依赖 AD 属性;坏处是每台机器都要维护。机器多了,建议用配置管理工具把映射命令做成幂等脚本,每次部署时统一执行。
4.3 两种方式的取舍
我把关键差异整理成表,方便你决策。
| 维度 | idmap_ad | idmap_ldb |
|---|---|---|
| 映射存储位置 | AD 属性 | 本地 ldb |
| 多机一致性 | 天然一致 | 需手动同步 |
| 依赖 AD 改权限 | 需要 | 不需要 |
| 维护成本 | 低(集中) | 高(分散) |
| 适合规模 | 中大型 | 小型或受限环境 |
我的经验是:只要你能拿到 AD 的写权限,就选 idmap_ad。集中管理带来的收益,远大于配置时多花的那些功夫。只有在 AD 被严格管控、你碰不了 schema 的情况下,才退而求其次用 idmap_ldb。
5. 实操验证与常见问题排查
5.1 验证 uid 是否真的固定
配置完别急着上生产,先做几轮验证。核心命令是wbinfo -i 用户名,它会返回这个 AD 用户在 Linux 视角下的 uid、gid、家目录、shell。
# 查看指定 AD 用户的映射信息 # wbinfo -i 用户名 # 查看 SID 到 uid 的映射 # wbinfo -n 用户名验证要点有三个:第一,uid 是不是你指定的值;第二,重启 winbind 服务后再查,uid 是否不变;第三,用id 用户名命令查,结果是否和wbinfo -i一致。三者都对,才算真正稳定。
我踩过的一个坑是:配置改完只重启了 smbd,没重启 winbindd,结果映射一直不生效。改 idmap 配置后,必须重启 winbindd,这是硬性要求。
5.2 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| uid 每次登录都变 | idmap 后端是算法映射 | 检查是否配了 ad/ldb 后端 |
| uid 落在 10000 以上随机值 | 回退到了 autorid/rid | 检查 range 是否覆盖目标 uid |
| wbinfo -i 报错找不到用户 | winbindd 未运行或域未加入 | 检查服务状态和 net ads testjoin |
| 映射写入后不生效 | 缓存未刷新 | 重启 winbindd 或清 idmap 缓存 |
| 加入域失败 | DNS 或时间问题 | 查 SRV 记录、对时 |
| 家目录权限错乱 | uid 漂移或 gid 不匹配 | 核对 uid/gid 映射一致性 |
5.3 几个容易忽略的细节
第一,range 区间不能重叠。idmap config * : range和idmap config 域名 : range如果有交集,winbind 行为会变得不可预测。规划时就把区间划清楚,本地账号、域用户、系统保留各占一段。
第二,gid 也要一起控制。很多人只盯着 uid,忘了 gid。AD 用户的主组 gid 如果漂移,共享目录的组权限一样会乱。idmap_ad下要同时填gidNumber,idmap_ldb下要同时映射组的 SID。
第三,缓存会骗人。winbind 有缓存机制,改完配置后旧映射可能还在缓存里。除了重启服务,必要时可以清一下缓存目录再重启。验证时最好换一个没查过的用户,避免命中旧缓存。
第四,SELinux 和 AppArmor 可能拦路。在某些强制访问控制开启的系统上,winbind 读写 ldb 数据库可能被拦。如果映射命令执行成功但查询无效,看看审计日志里有没有拒绝记录。
6. 生产环境落地经验
6.1 规划 uid 区间的方法
上生产前,先做一张 uid 规划表。我的习惯是:
- 0-999:系统保留,不动;
- 1000-2999:本地普通用户;
- 3000-7999:本地服务账号和 idmap 兜底区间;
- 10000-999999:AD 域用户和组。
这个划分的好处是层次清晰,出问题时一眼能看出某个 uid 属于哪一类。AD 用户的 uidNumber 就从 10000 开始,按部门或按批次分配,留足扩展空间。
6.2 多机一致性怎么保证
如果走idmap_ad,一致性是天然的,因为映射存在 AD 里,所有机器读的是同一份数据。你要做的只是保证每台机器的smb.conf里 range 配置一致。
如果走idmap_ldb,一致性就得靠工具。我的做法是写一个幂等脚本,把"用户名到 uid"的对应关系写在一个清单文件里,脚本读清单,逐条执行net idmap set。这个脚本纳入配置管理,每次新机器上线或映射变更时统一跑一遍。脚本要能重复执行不出错,也就是先查再写,已存在且正确的就跳过。
6.3 和文件共享权限的配合
uid 控制最终是为了文件权限。共享目录的属主属组,要和 AD 用户的 uid/gid 对上。配置共享时,用force user、force group要谨慎,它们会覆盖真实身份,可能掩盖 uid 映射的问题。
我的建议是:先确保 uid 映射正确,再配共享权限。顺序反了,你会分不清是映射问题还是共享配置问题。测试时用一个 AD 用户实际登录,在共享目录里创建文件,然后ls -ln看数字属主,和你预期的 uid 对比。数字对上了,整条链路才算通。
6.4 监控与变更管理
uid 映射这种东西,平时不出问题,一出就是权限事故。建议加两个监控点:一是定期用wbinfo -i抽查关键用户的 uid,和基线对比;二是监控 winbindd 进程状态,它挂了映射就全失效。
变更管理上,任何对 idmap 配置或 AD 属性的修改,都要走变更流程并记录。因为这类改动的影响面是"所有依赖该用户的文件权限",回滚成本高。改之前先备份smb.conf和 ldb 数据库,改之后按验证清单逐项确认。
我个人在实际操作中的体会是,uid 控制这件事,前期规划花的时间,会在后期省下成倍的排查时间。见过太多环境是"先跑起来再说",结果半年后文件权限乱成一锅粥,回头重构的成本远高于当初认真规划。把区间划清楚、把映射方式定下来、把验证脚本写好,这三件事做扎实,后面基本不用再操心。