1. 项目背景与核心需求拆解1.1 为什么非root用户做免密验证是个真问题批量管理服务器这件事做过运维的人都知道一旦机器数量超过五台手动登录就是噩梦。Ansible 作为无代理的自动化工具核心依赖 SSH 通道而 SSH 通道要跑通第一步就是免密验证。很多教程一上来就用 root 用户操作但真实生产环境里root 直接登录往往是被禁止的服务器上跑批量任务的通常是一个普通运维账号比如 deploy、ops、appuser 这类。这就带来一个很具体的矛盾Ansible 官方文档默认你用的是能 sudo 的账号但非 root 用户做免密时密钥路径、权限、sudo 提权、known_hosts 校验这几个环节都会出问题。我自己第一次在 CentOS 7 上给一个普通账号配 Ansible 免密光是 SSH 报的 Permission denied 就排查了快两个小时最后发现是家目录权限被改成了 777SSH 直接拒绝读取密钥。所以这个项目的核心需求可以拆成三层第一层是让非 root 用户能通过 SSH 密钥登录目标机第二层是让这个用户在执行 Ansible 任务时能顺利提权到 root 完成需要特权的操作第三层是保证这套机制在多台机器上批量生效且可重复执行。这三层缺一层Ansible 就跑不起来。1.2 适用场景与目标读者这套方案适用的场景很明确你手上有几台到几十台 CentOS 服务器系统版本集中在 7.x 或 8.x你有一个普通运维账号这个账号在目标机上可以通过 sudo 执行命令现在你想用 Ansible 做批量配置管理、软件部署或者日常巡检。目标读者分两类。一类是刚接触 Ansible 的运维新人知道要配免密但被非 root 的各种权限问题卡住另一类是有经验但一直用 root 做免密的工程师想切换到更规范的非 root 模式。两类人关注的点不一样新人需要完整的操作步骤和原理老手更关心 sudo 配置和批量分发的效率。我下面讲的内容命令和配置都是可以直接复制去用的但我会把每一步为什么这么做讲清楚因为环境千差万别照抄命令不理解原理遇到报错还是抓瞎。1.3 整体方案设计思路整个方案我按四个阶段来组织准备阶段确认环境、生成密钥阶段在控制机创建密钥对、分发阶段把公钥推到目标机、验证阶段用 Ansible 的 ping 模块和实际任务测试连通性。每个阶段都有非 root 用户特有的坑。方案选型上我坚持用 SSH 密钥而不是密码原因有三个。一是 Ansible 的密码认证需要额外装 sshpass而且每次执行都要传密码安全性差二是密钥认证可以配合 ssh-agent 做密钥转发在多跳场景下更灵活三是密钥可以设置 passphrase即使私钥泄露也多一层保护。至于用不用 ssh-copy-id我建议在机器少的时候用机器多了直接写循环脚本更可控。sudo 提权这块我选择在 Ansible 的 inventory 里配置ansible_become_pass或者在 ansible.cfg 里设置become_methodsudo而不是去改目标机的 sudoers 免密。改 sudoers 免密虽然省事但等于把提权门槛降到零任何能登录这个账号的人都能直接拿 root安全上不划算。用密码提权密码可以通过 Ansible Vault 加密存储相对可控。2. 环境准备与前置检查2.1 控制机与目标机的角色划分先明确两个概念。控制机是你执行 ansible 命令的那台机器目标机是被管理的服务器。控制机上需要装 Ansible目标机上不需要装任何东西只要有 SSH 服务和 Python 就行。CentOS 7 自带 Python 2.7CentOS 8 自带 Python 3.6Ansible 对这两个版本都支持但新版本 Ansible 已经逐步放弃 Python 2所以如果是 CentOS 7建议手动装一个 Python 3。控制机的 Ansible 安装很简单CentOS 7 用 yum 装yum install -y epel-release yum install -y ansible装完用ansible --version确认版本。我实测下来2.9 以上的版本对非 root 用户的支持更完善尤其是 become 相关的参数。如果版本低于 2.8建议升级否则后面 become 的配置方式会有差异。目标机这边确认三件事SSH 服务正常运行、目标用户存在、sudo 可用。检查命令systemctl status sshd id deploy sudo -l -U deploy第三条命令会列出 deploy 用户能执行的 sudo 命令如果输出里有(ALL) ALL或者具体的命令列表说明 sudo 配置没问题。如果提示用户不在 sudoers 文件里那得先让有权限的人加进去这一步绕不过去。2.2 非 root 用户的家目录权限陷阱这是我最想强调的一个点也是新手最容易踩的坑。SSH 对密钥文件的权限要求极其严格私钥必须是 600公钥可以是 644家目录不能是 group 或 other 可写。很多人图省事chmod -R 777 ~结果 SSH 直接拒绝使用密钥报错信息还很模糊只说 Permission denied。正确的权限设置是这样的chmod 700 /home/deploy chmod 700 /home/deploy/.ssh chmod 600 /home/deploy/.ssh/id_rsa chmod 644 /home/deploy/.ssh/id_rsa.pub chmod 600 /home/deploy/.ssh/authorized_keys注意 authorized_keys 在目标机上权限也必须是 600而且它的父目录 .ssh 必须是 700。我遇到过一种情况目标机的 .ssh 目录属主是 rootdeploy 用户虽然能读但 SSH 会拒绝因为 SSH 检查的是属主和权限双重条件。所以分发公钥的时候一定要确保目标机上 .ssh 目录和 authorized_keys 的属主是 deploy 自己。提示如果家目录权限被改过SSH 的日志里会有明确提示用tail -f /var/log/secure在目标机上实时看登录失败的原因一目了然。2.3 SSH 服务端关键配置确认目标机的 sshd_config 有几个参数会影响免密登录需要确认。用grep快速检查grep -E PubkeyAuthentication|AuthorizedKeysFile|PermitRootLogin|PasswordAuthentication /etc/ssh/sshd_config关键参数说明参数推荐值作用PubkeyAuthenticationyes开启公钥认证必须为 yesAuthorizedKeysFile.ssh/authorized_keys指定公钥存放路径PermitRootLoginno禁止 root 直接登录更安全PasswordAuthenticationno关闭密码登录强制密钥改完 sshd_config 要重启 SSH 服务systemctl restart sshd。这里有个操作禁忌重启 SSH 之前一定要确保当前会话不会断或者另开一个会话测试新配置能登录否则配置写错会导致自己也被锁在外面。我一般会先sshd -t测试配置文件语法通过了再重启。3. 密钥生成与分发实操3.1 在控制机生成密钥对在控制机上切换到 deploy 用户然后生成密钥。命令su - deploy ssh-keygen -t rsa -b 4096 -C deploycontrol -f ~/.ssh/id_rsa_ansible参数解释一下。-t rsa指定算法虽然现在 ed25519 更先进但 CentOS 7 的 OpenSSH 版本对 ed25519 支持不一定完整rsa 兼容性最好。-b 4096指定密钥长度2048 够用但 4096 更稳妥代价是生成慢几秒。-C是注释方便识别这个密钥的用途。-f指定文件名我特意不叫默认的 id_rsa而是叫 id_rsa_ansible这样以后有多个密钥时不会混淆。执行过程中会问你要不要设 passphrase。我的建议是设但设了之后每次用都要输所以配合 ssh-agent 使用。如果嫌麻烦至少在生产环境要设。设了 passphrase 的密钥即使私钥文件被拷走没有 passphrase 也用不了。生成完检查一下ls -l ~/.ssh/id_rsa_ansible*应该看到两个文件私钥 600 权限公钥 644 权限。如果权限不对手动改。3.2 用 ssh-copy-id 分发公钥到单台目标机机器少的时候ssh-copy-id 是最省事的。命令ssh-copy-id -i ~/.ssh/id_rsa_ansible.pub deploy192.168.1.101这条命令会提示你输入目标机 deploy 用户的密码输完它自动把公钥追加到目标机的~/.ssh/authorized_keys。注意它不会覆盖已有的公钥是追加模式所以重复执行不会出问题。但 ssh-copy-id 有个前提目标机的 deploy 用户必须能用密码登录。如果目标机已经关了密码认证那 ssh-copy-id 就用不了得用别的方式。另外如果目标机的 SSH 端口不是 22要加-p参数指定端口。分发完立刻测试ssh -i ~/.ssh/id_rsa_ansible deploy192.168.1.101 hostname whoami如果直接返回主机名和 deploy说明免密成功。如果还要输密码说明公钥没生效去目标机看/var/log/secure的报错。3.3 批量分发公钥的脚本方案机器超过五台一台台 ssh-copy-id 就太慢了。我一般写个循环脚本配合 sshpass 做首次密码认证。脚本大概长这样#!/bin/bash KEY~/.ssh/id_rsa_ansible.pub USERdeploy PASSyour_temp_password HOSTS192.168.1.101 192.168.1.102 192.168.1.103 for host in $HOSTS; do echo 分发到 $host sshpass -p $PASS ssh-copy-id -i $KEY -o StrictHostKeyCheckingno $USER$host if [ $? -eq 0 ]; then echo $host 分发成功 else echo $host 分发失败 fi done这里有几个细节。StrictHostKeyCheckingno是跳过首次连接的主机指纹确认批量操作时如果不加每台机器都要手动输 yes脚本就卡住了。但跳过指纹确认有中间人风险所以这个脚本只适合在可信内网用用完把 known_hosts 清理掉。sshpass 需要额外安装CentOS 上yum install -y sshpass。密码写在脚本里不安全生产环境建议用 Ansible Vault 加密或者用 expect 交互式输入。我自己的做法是脚本里不写密码执行时用read -s让操作者手动输一次然后传给 sshpass。分发完同样要批量验证写个循环for host in $HOSTS; do ssh -i ~/.ssh/id_rsa_ansible -o BatchModeyes -o ConnectTimeout5 $USER$host echo OK 2/dev/null if [ $? -eq 0 ]; then echo $host 免密正常; else echo $host 免密异常; fi doneBatchModeyes的作用是禁止交互式密码输入如果密钥不通就直接失败不会卡在密码提示上。ConnectTimeout5是连接超时避免某台机器网络不通导致整个循环卡死。4. Ansible 配置与 sudo 提权4.1 inventory 文件的非 root 写法Ansible 的 inventory 文件定义目标机列表和连接参数。针对非 root 用户我推荐这样写[web] 192.168.1.101 ansible_userdeploy ansible_ssh_private_key_file~/.ssh/id_rsa_ansible 192.168.1.102 ansible_userdeploy ansible_ssh_private_key_file~/.ssh/id_rsa_ansible [web:vars] ansible_python_interpreter/usr/bin/python3 ansible_becomeyes ansible_become_methodsudo ansible_become_userroot逐行解释。ansible_userdeploy指定连接用户ansible_ssh_private_key_file指定私钥路径。ansible_python_interpreter指定目标机的 Python 路径CentOS 8 默认是/usr/bin/python3CentOS 7 如果装了 Python 3 也是这个路径如果没装就是/usr/bin/python。这个参数不写Ansible 会自己探测但探测有时会警告显式指定更干净。ansible_becomeyes开启提权ansible_become_methodsudo指定用 sudoansible_become_userroot指定提权到 root。这三个参数是非 root 用户跑 Ansible 的核心缺了它们所有需要写文件、装软件的任务都会失败。4.2 sudo 密码的安全处理方式上面配置里没写 sudo 密码执行时 Ansible 会提示你输入BECOME password。如果不想每次输有三种方式。第一种是命令行传ansible-playbook -K site.yml-K会提示输入提权密码。这种方式密码不落盘最安全但每次都要输。第二种是 inventory 里写ansible_become_passyour_password明文存储方便但不安全只适合测试环境。第三种是用 Ansible Vault 加密密码文件。先创建加密文件ansible-vault create group_vars/web/vault.yml在打开的编辑器里写ansible_become_pass: your_password保存后文件是加密的执行时用--ask-vault-pass或--vault-password-file提供解密密码。这种方式兼顾了安全和便利生产环境我推荐这种。注意sudo 密码和 SSH 密码是两回事。SSH 免密之后登录不需要密码但 sudo 提权仍然需要密码除非目标机配置了 NOPASSWD。很多人以为免密登录了 sudo 也免密这是误解。4.3 目标机 sudoers 的最小化配置如果确实想免 sudo 密码可以在目标机的 sudoers 里给 deploy 用户配置 NOPASSWD。用visudo编辑加一行deploy ALL(ALL) NOPASSWD: ALL但这等于给 deploy 用户完全的无密码 root 权限安全风险很高。折中方案是只对 Ansible 需要的命令免密比如deploy ALL(ALL) NOPASSWD: /usr/bin/yum, /usr/bin/systemctl, /bin/cp, /bin/mv这样 deploy 只能免密执行这几个命令其他命令还是要密码。但 Ansible 的任务千变万化很难穷举所有命令所以实际用起来会经常遇到权限不足。我的经验是内网可信环境可以用 NOPASSWD: ALL但一定要配合 SSH 密钥的 passphrase 和登录来源限制把风险控制在可接受范围。5. 验证与常见问题排查5.1 用 ad-hoc 命令快速验证连通性配置完先别急着跑 playbook用 ad-hoc 命令验证。第一步测连接ansible web -i inventory.ini -m ping返回pong说明 SSH 和 Python 都正常。如果报UNREACHABLE是 SSH 层的问题去查密钥和网络。如果报FAILED且提示 Python 找不到是解释器路径问题。第二步测提权ansible web -i inventory.ini -m command -a whoami --become如果返回root说明提权成功。如果返回deploy说明 become 没生效检查 inventory 里的 become 配置。如果报Missing sudo password说明需要提供 sudo 密码加-K参数再试。第三步测文件写入ansible web -i inventory.ini -m file -a path/tmp/ansible_test statetouch ownerroot --become这条命令在目标机创建文件并设属主为 root能成功说明提权后的写权限也没问题。5.2 常见报错与排查速查表我把实际运维中遇到的报错整理成表方便对照排查报错信息根本原因解决方法Permission denied (publickey)公钥未生效或权限不对检查 authorized_keys 权限 600.ssh 目录 700Host key verification failedknown_hosts 里有旧指纹删除对应条目或设 StrictHostKeyCheckingnoMissing sudo password未提供提权密码加 -K 参数或配置 ansible_become_passPython not found解释器路径错误设置 ansible_python_interpreterUNREACHABLE网络不通或 SSH 端口错检查防火墙和 sshd 端口sudo: sorry, you must have a ttysudo 要求 tty在 sudoers 里注释掉 Defaults requiretty最后一条requiretty是 CentOS 上很典型的问题。CentOS 的 sudoers 默认有Defaults requiretty意思是 sudo 必须在终端里执行而 Ansible 是通过 SSH 非交互执行的没有 tty所以 sudo 会拒绝。解决方法是在 sudoers 里注释掉这一行或者给 deploy 用户单独加Defaults:deploy !requiretty。5.3 实操心得与避坑经验分享几个我踩过的坑。第一个是密钥文件名的问题。如果你用了非默认的密钥文件名比如 id_rsa_ansible在 Ansible 里必须显式指定ansible_ssh_private_key_file否则 Ansible 默认找 id_rsa找不到就报错。我一开始没指定排查了半天才发现是文件名不对。第二个是 known_hosts 的批量清理。目标机重装系统后 SSH 指纹会变Ansible 连接时报 Host key verification failed。手动一台台删太慢用这条命令批量清理ssh-keygen -R 192.168.1.101或者干脆在 ansible.cfg 里设host_key_checking False但这样有安全风险只建议在测试环境用。第三个是 sudo 密码里的特殊字符。如果 sudo 密码包含$、!这类字符写在 inventory 里会被 shell 解释导致密码错误。解决方法是密码用单引号包裹或者用 Vault 存储避免 shell 解析。第四个是并发数控制。Ansible 默认并发 5 个主机机器多的时候可以调大-f 20但调太大目标机 SSH 连接数会爆反而变慢。我一般设 10 到 20 之间根据目标机性能调整。6. 批量任务实战与扩展6.1 一个完整的 playbook 示例光验证连通性不够实际用起来得跑 playbook。我写一个典型的初始化 playbook做三件事更新系统、装常用工具、配置时区。--- - name: 服务器初始化 hosts: web become: yes tasks: - name: 更新 yum 缓存 yum: update_cache: yes when: ansible_os_family RedHat - name: 安装常用工具 yum: name: - vim - wget - net-tools - chrony state: present - name: 设置时区为上海 timezone: name: Asia/Shanghai - name: 启动 chrony 服务 systemd: name: chronyd state: started enabled: yes这个 playbook 里become: yes在 play 级别声明所有 task 都提权执行。when条件判断操作系统家族保证只在 RedHat 系上执行 yum 相关任务。timezone和systemd模块都是幂等的重复执行不会出问题。执行命令ansible-playbook -i inventory.ini init.yml -K-K提示输入 sudo 密码。执行完看输出changed表示有变更ok表示无变更failed表示失败。如果有 failed用-v或-vvv看详细日志。6.2 用 roles 组织复杂任务playbook 写长了不好维护用 roles 拆分。目录结构roles/ common/ tasks/main.yml handlers/main.yml defaults/main.yml nginx/ tasks/main.yml templates/nginx.conf.j2每个 role 负责一块功能playbook 里引用- hosts: web become: yes roles: - common - nginxroles 的好处是复用common 这个 role 可以在所有项目里用nginx 这个 role 只在需要 web 服务的机器上用。defaults/main.yml 里放默认变量比如 nginx 的端口、worker 进程数不同环境覆盖不同的值就行。6.3 密钥轮换与安全维护密钥不是配一次就一劳永逸的。安全规范要求定期轮换密钥比如每半年换一次。轮换流程是生成新密钥、分发新公钥、验证新密钥可用、删除旧公钥、删除旧私钥。删除旧公钥用 Ansible 的 authorized_key 模块- name: 删除旧公钥 authorized_key: user: deploy key: {{ lookup(file, old_key.pub) }} state: absent这个模块的好处是幂等执行多次结果一样。轮换的时候新旧密钥可以并存一段时间等所有机器都验证新密钥可用后再删旧的避免中途出问题导致失联。另外私钥文件一定要备份到安全的地方比如加密的密码管理器或者硬件密钥设备。私钥丢了所有机器都进不去只能走控制台救援那成本就高了。我自己的做法是私钥加密后存两份一份在本地加密盘一份在离线存储passphrase 单独记。这套非 root 用户的 Ansible 免密方案我从 CentOS 7 用到 CentOS 8中间踩的坑基本都在这了。核心就三句话密钥权限要严格、sudo 提权要配好、批量操作要幂等。把这三件事做扎实几十台机器的批量管理就跟管一台一样轻松。