写Shell脚本时把密码写死,这事我干过不止一次。早年间做运维,数据库密码、API密钥直接拼在脚本里,想着服务器在自己手里,权限也锁死了,能出什么事?直到有一次我把一个部署脚本发给同事评审,他当场敲了个 history 命令,把三个月前我带着明文密码的curl命令翻了出来。从那一刻起我才明白:所谓“内部安全”远比你想象的脆弱,脚本本身、进程列表、Shell历史、临时文件、日志文件,任何一环漏出去,密码就没了。这篇文章就是来聊Shell脚本中敏感信息的保护,核心就两个词:密码加密存储、避免明文传输。我会从风险讲起,把加密存储的几种方案、传输环节的注意点全部拆开,用我改造二十多个脚本的真实踩坑经历,给你一套可以直接照抄的安全改造方案。不管是刚入门Shell的新人,还是写了好几年自动化脚本的老手,这篇都有值得看的东西。
1. 明文密码的三大现实风险
1.1 脚本文件本身:权限挡不住的水平越权
先说最直接的:你写了一个 backup.sh,里面写着MySQL密码,文件权限设成700。看起来安全对吧?可实际生产环境里,服务器上的账号多得像繁星,运维组内部互相共用账号、开发要查日志、新来的实习生会用跳板机,一旦脚本被拷到共享目录或者评审工具里,密码就跟着走了。更隐蔽的情况是,代码仓库管理不善,脚本被传到Git仓库,哪怕后来改掉了,历史提交里还留着旧版本。这个场景我真实遇到过一次:项目组迁移配置库,隔壁组一个同事从SVN历史里捞出了一个三年前带数据库密码的脚本,而那个数据库至今仍在使用,等于被人白拿了三年的钥匙。
这时候你会发现,所谓“脚本权限设好就安全”根本不成立。安全防护最大的误区是高估环境,低估内部变量。解决问题不是靠把文件藏得更深,而是从源头做到脚本里根本没有明文密码,泄露出去也白搭。这也是我后来做所有改造的第一原则:密码不以明文形式出现在任何脚本代码里。
1.2 进程列表:ps aux 一眼看到全貌
第二个风险比第一个更隐蔽,也更常被忽略:运行时泄漏。很多人以为密码只要不写在文件里,写成变量名传参就不会暴露,其实恰恰相反。最典型的错误是 mysql -u root --password=xxx、curl http://user:password@host、export DB_PASSWORD=xxx 这种写法。当命令执行时,在另一个终端跑一个 ps aux 或者 ps -ef,你就可以看到命令行的完整内容,密码直接挂在参数栏里。有些系统监控还会把进程参数记录到审计日志,等于你的密码以明文形式写进了系统日志。
不要觉得“服务器上没有别人”就没事,监控agent、运维平台、堡垒机的录屏审计,都会捕捉到完整的命令参数。我曾经在一个客户现场排问题,堡垒机系统把所有的命令参数都录下来做安全审计,结果一个同事写的脚本每次启动都用 --password=明文 方式调用工具,密码被清清楚楚地记录在审计系统里。排查问题的同事一检索,所有密码都出来了。这就是典型的进程级泄漏。
也是从那次之后我养成了一个习惯:写脚本前先问自己,如果这条命令被 ps 和审计日志完整记录下来,我愿不愿意让密码出现在里面?不愿意,就换一种传递方式。补充一句,在受控环境中可以使用 MYSQL_PWD 等专用环境变量,但这绝对不是完全安全的,后面我会专门展开说。
1.3 历史记录与日志:你以为删了就没了
第三个坑是“死了还在泄露”。登录服务器敲命令,Shell会把命令写入 ~/.bash_history;脚本执行过程中的debug输出、set -x 追踪、echo "密码是xxx" 这类调试代码,会把敏感信息打到屏幕上;还有应用自身的日志框架,比如数据库连接失败时打印完整的连接串。这些数据一旦记录下来,几乎不可能彻底清除。~/.bash_history 可能被同步到集中日志、被备份到带库、被取证工具恢复。
也许你会说,操作完清理一下不就行了?问题是大部分时候你根本不知道它落在哪。我举个具体例子:一个脚本里调用了某个命令行工具,需要在配置文件里填一个数据库连接字符串,工具内部出错时会把这个连接串原样写入日志,日志又被采集到ELK。你以为删掉了本地的明文配置,实际上日志里已经留了至少一年的副本。这种链路非常隐蔽。
所以,保护敏感信息,绝对不是“删掉就完了”这么简单。你必须在源头上阻止密码以明文形态出现在任何可被获取的介质中。也正是因为经历了上面这一轮,我才开始系统性地研究加密存储和避免明文传输,也就是接下来要讲的正文。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 密码加密存储的主流方案
2.1 openssl enc:最轻量、接受度最高的加密方案
第一个方案,也是我日常用得最多的:用 openssl enc 对密码文件做对称加密。思路很简单:把真正要用的数据库密码、API Key放在一个密文文件里,脚本运行时用一段“主密码”调用openssl解密,然后把解密出的密码读入变量,用完立刻释放。
先看加密过程,我在实际改造中建议把待加密的密码先放进一个临时明文文件,加密完成后用 shred 安全删除,避免直接在命令行里敲密码被Shell历史记下来。下面为了演示流程,先用stdin输入:
bash复制# 生成一个随机主密码(强烈建议用随机数,不要自己拍脑袋)
MASTER_PASS=$(openssl rand -base64 24)
# 用主密码对真实密码加密
printf '%s' 'YourRealDatabaseP@ss' | openssl enc -aes-256-cbc -pbkdf2 -salt -base64 \
-pass pass:"$MASTER_PASS" -out mysql_pass.enc
这里我重点说一下参数选择:
-aes-256-cbc:AES-256是当前主流的对称加密算法,CBC分组模式适合加密文件。-pbkdf2:从密码派生密钥时使用PBKDF2,而不是传统的-md5或-sha256直接哈希。PBKDF2会做多次迭代,抗暴力破解能力强很多。新版OpenSSL 3.0里不加-pbkdf2会提示警告,建议直接用。-salt:加盐,保证同样的密码加密同一份明文,每次输出密文不同,防止彩虹表攻击。-base64:输出base64格式,方便把密文拷贝到配置管理工具或粘贴到文档中。
解密过程如下:
bash复制MYSQL_PASS=$(openssl enc -d -aes-256-cbc -pbkdf2 -base64 \
-pass pass:"$MASTER_PASS" -in mysql_pass.enc)
echo "$MYSQL_PASS" # 只在终端临时验证,脚本实际使用不能这样打印
注意要点:把密文文件和脚本放在不同目录,密文文件权限设成600。主密码不要写在脚本里,可以放在单独的仅root可读的文件中,或者干脆在每次手动运行时输入。执行完脚本后用 unset MYSQL_PASS 释放变量,再用 history -c 清理本条记录,虽然这不算高明的手段,但至少能减小现场泄漏面。
提示:openssl解密的输出默认以换行符结尾,如果后续要把密码拼接到URL或配置文件里,记得先去掉换行,比如用
DB_PASS=$(printf '%s' "$DB_PASS")或tr -d '\n'。这个细节第一次很容易踩坑。
这个方案的优点:只有一个文件,openssl是操作系统自带的工具,几乎零依赖;缺点:主密码如果泄漏,所有密文都能解。所以主密码的保护非常重要。
2.2 GPG非对称加密:钥匙和锁分离
第二个方案是使用GnuPG(gpg)做非对称加密。场景适合需要多人维护服务器、不同人负责不同密钥的团队。思路是:为每次部署生成一对密钥,公钥用来加密配置文件,私钥保存在受保护的本地环境,脚本运行时用私钥解密。
具体操作:
bash复制# 生成一个专用密钥对(交互式)
gpg --full-generate-key
# 用公钥加密
gpg --encrypt --recipient 'backup@example.com' mysql_pass.txt
# 生成 mysql_pass.txt.gpg 后,明文文件随即删除
# 脚本中解密
MYSQL_PASS=$(gpg --decrypt --quiet --batch mysql_pass.txt.gpg 2>/dev/null)
GPG我用下来最深刻的体会有三点:
第一,公钥加密、私钥解密这种“钥匙和锁分离”的设计,安全感比对称加密强很多。对称加密是同一把钥匙既锁又开,一旦钥匙外泄,密文全完;非对称加密下,即使公钥被全世界知道了也解不开,只有持有私钥的人才能解密。
第二,GPG还支持在密钥上设置有效期、吊销证书,当某台服务器退役或某个成员离开时,可以单独吊销相应的加密身份,而不用像openssl那样换一把主密码然后全部文件重新加密。
第三,代价是私钥管理复杂度上来了。私钥要备份到离线存储,解密机的gpg密钥环权限要严格控制,否则私钥本身被偷走,反而成了新的风险点。
说一个实际例子:我们团队用GPG管理一批数据库密码,每个业务线一个gpg密钥,加解密命令通过一个统一的 secret.sh 封装。封装脚本里不出现任何真实密码,只接受配置文件路径。这样即使封装脚本代码泄露,攻击者拿到的也只是一堆密文。
2.3 系统密钥环与外部密钥管理服务
第三种方案是借助操作系统或专门的密钥管理工具来保管密码。常见的做法有以下几种,我用一个表格快速对比:
| 方案 | 加密方式 | 依赖 | 适用场景 |
|---|---|---|---|
| openssl enc | 对称加密 | openssl,几乎所有Linux自带 | 单机脚本、个人服务器 |
| GPG | 非对称加密 | gpg,需配置密钥 | 多人团队、需要吊销 |
| secret-tool/keyring | 系统密钥环存储 | libsecret/gnome-keyring | 桌面环境或有DBus的服务器 |
| Vault/云厂商Secrets | 外部密钥服务 | 客户端SDK/API | 有条件接入的企业环境 |
拿secret-tool举例,Linux下把密码写进系统密钥环的方式:
bash复制# 写入密码
printf 'MyDatabaseP@ss' | secret-tool store --label='MySQL backup' \
--key=app_name 'backup' --key=db_name 'mysql_main'
# 读取密码
MYSQL_PASS=$(secret-tool lookup --key=app_name 'backup' --key=db_name 'mysql_main')
这么做的好处是:密码真正不落盘,系统密钥环本身有访问控制,只有对应用户才能读取,而且多数桌面环境会在解锁会话后才允许访问。缺点是它依赖桌面会话的DBus服务,纯服务器环境配置略麻烦;而外部密钥管理服务虽然功能完善,但对小团队来说引入成本和运维成本偏高,需要自己权衡。
选型建议:脚本数量少、边界清晰,openssl和GPG足够;如果公司已有Vault或云厂商密钥管理,优先接入;如果是个人服务器,secret-tool或简单加密文件都是合理的。核心原则是“让密码以密文形态存在,而不是明文躺在脚本里”。
3. 避免明文传输的实战要点
3.1 SSH密钥认证:把密码从传输链路里拿掉
加密存储只能解决“文件落地”的问题,但脚本经常需要把数据传到另一台机器、备份到远程服务器、从远端拉取配置,传输环节同样不能让密码明文出现。最典型也最推荐的方案是用SSH密钥认证替代密码登录。
bash复制# 生成密钥对
ssh-keygen -t ed25519 -C "backup-script@yourhost" -f ~/.ssh/backup_ed25519
# 安装公钥到远程服务器
ssh-copy-id -i ~/.ssh/backup_ed25519.pub backup_user@remote_host
# 脚本里直接免密执行远程命令
ssh -i ~/.ssh/backup_ed25519 backup_user@remote_host \
'tar -czf - /data/applogs' > /backup/logs_$(date +%Y%m%d_%H%M%S).tar.gz
这里有两个细节容易被忽略:
第一,私钥文件权限必须设成600或更严格,否则SSH会拒绝加载或产生告警。我见过不少真实的服务器,因为把私钥权限放在644,导致每次连远程都要手动输入私钥密码(passphrase);然后有人图省事直接把passphrase也去掉了,结果私钥一旦被复制,等于没有保护。这是典型的省事一时、后悔无穷。
第二,SSH密钥建议设置passphrase,并用 ssh-agent 来缓存,而不是直接在脚本里传私钥密码。使用 ssh-agent 的方式是:
bash复制eval "$(ssh-agent -s)"
ssh-add ~/.ssh/backup_ed25519 # 提示输入passphrase,手动输入
ssh remote_host 'echo success'
用密钥认证后,远程操作不再需要把密码放在命令行参数或者脚本变量里,传输链路就从“明文密码”变成了“密钥挑战”。这是目前最稳妥、应用最广泛的做法。SSH本身的数据交换也是加密的,避免了明文传输。
3.2 环境变量的边界与风险
刚才提到环境变量这个点,需要展开说清楚。环境变量看似比命令行参数安全——它不会出现在 ps 的输出里,但环境变量有一个致命问题:所有子进程都能继承。用 export DB_PASSWORD=xxx 之后,你启动的任何子进程、包括可能被注入的恶意程序,都能通过 /proc/<pid>/environ 读取到当前进程的环境变量。也就是说,环境变量并不是保险箱,而是一个半透明袋子:比命令行好一点,但是不等于安全。
安全的做法要分几种情况:
如果密码只在本脚本内部使用,不调用外部程序,用普通变量 + 结束后 unset 就够了:
bash复制DB_PASS=$(openssl ...) # 解密
# ... 这里做纯bash逻辑处理,不重启其他进程
unset DB_PASS
如果必须传给外部程序,比如 mysqldump 这种不能读标准输入的,优先找程序自己支持的凭据文件选项。mysqldump 支持读取 ~/.my.cnf,你可以把凭据放在权限为600的配置文件里,命令行里只传 --defaults-extra-file=/path/to/.my.cnf。MySQL客户端在启动时会读取该文件,并不会把密码暴露在 ps 输出中。
实在只能用环境变量,那就要缩短暴露窗口:临时导出、用完马上 unset,并且在整个操作期间不执行不可信的外部命令。借此机会我想强调一个最高原则:敏感信息的暴露窗口永远是一个函数。窗口越短,风险越低。你能控制的不是环境100%安全,而是把“密码可见的时间”压缩到无限小。
3.3 命令行参数、日志与调试输出
在传输和配置之外的另一个高频泄漏点,是命令日志与调试输出。下面列几个常见坑和对应的规避方式:
第一个坑是 set -x。在脚本调试时开启 set -x 会把每条命令展开打印,如果命令里包含 --password=$PASS 或 curl -u "$USER:$PASS",密码就全部显示在stdout和日志里。调试完忘记关掉,脚本上线后每个执行日志都是明文密码。规避:调试时先用掩码或占位符替换真实值,上线前全面检查。
第二个坑是返回值错误日志。很多工具在连接失败时会打印完整的参数或连接串。MySQL报错时会显示 Access denied for user 'root'@'localhost' (using password: YES);curl的 -v 模式会打印请求头,包括Authorization。规避:调用工具时关闭debug输出,比如 curl -sS 而不是 -v,mysqldump加 --log-error=/dev/null 或把错误日志重定向到特定路径。
第三个坑是 echo。不要图方便在脚本里 echo "数据库密码是:$DB_PASS"。就算日志只临时看一遍,它也会留在终端scrollback、日志采集器、CI系统的job log里面。
这部分的实操基线很简单:写log的地方、输出到stdout的地方、任何可能被采集的位置,一律不允许出现真实密码。如果一定要打日志,用掩码代替,比如 echo "DB password loaded" 而不是把密码本身打出来。
3.4 通过安全通道获取远端配置
还有一类情况:脚本需要在运行时从配置中心或另一台服务器拉取包含凭据的配置文件。这里要注意两点:
第一,使用带加密的传输协议,比如HTTPS、SCP/SFTP,避免用明文HTTP把含有密码的配置直接下载下来。不是说不能用HTTP,而是配置内容本身必须是密文。一种常见做法:配置文件本身用openssl或GPG加密,通过任意通道拉下来之后在脚本里解密,这样即使通道被人嗅探,拿到也只是密文。
第二,不要通过URL参数传递密码。我知道很多内部API为了图方便,把token直接放在URL的query string里,比如 curl 'http://internal.example.com/api/config?token=AbC123&id=42'。这会被Nginx访问日志、网关日志、浏览器历史记录完整记下来。要么放在Header里,比如 Authorization: Bearer,要么放在POST请求体里,要么干脆用短时有效的临时凭证、密钥文件、证书认证。
4. 完整案例:备份脚本的安全改造
4.1 改造前:一份每天都在裸奔的备份脚本
下面用一个真实的日常场景来串起全套方法。我的服务器上定期用mysqldump备份MySQL数据库,改造前的脚本长这样:
bash复制#!/bin/bash
# 每天凌晨2点执行
DB_HOST="localhost"
DB_USER="backup_user"
DB_PASS="Backup@123456"
DATE_STR=$(date +%Y%m%d_%H%M%S)
mkdir -p /backup/mysql
mysqldump -h "$DB_HOST" -u "$DB_USER" \
--password="$DB_PASS" \
--single-transaction --routines --triggers \
--databases mydb | gzip > "/backup/mysql/mydb_${DATE_STR}.sql.gz"
echo "backup done: /backup/mysql/mydb_${DATE_STR}.sql.gz"
这份脚本至少有三个问题,每个都致命:
DB_PASS="Backup@123456"以明文形式写在脚本里,脚本文件一旦被读取或泄露,数据库密码直接暴露。--password="$DB_PASS"把密码放进了命令行参数,任何能执行ps aux的人、任何堡垒机审计系统,都能看到完整密码。- 脚本运行产生的错误日志、
echo输出、mysqldump的告警信息都直接输出到了stdout和可能被收集的日志里。
4.2 改造后:加密存储 + 不落命令行的完整版本
我的改造思路分四步:密文落地、主密码保护、命令不暴露、日志不露底。改造后的脚本长这样:
bash复制#!/bin/bash
set -euo pipefail
BACKUP_DIR="/backup/mysql"
DATE_STR=$(date +%Y%m%d_%H%M%S)
DB_HOST="localhost"
DB_USER="backup_user"
# 1. 加密文件 mysql_pass.enc 预先放在 /etc/secure 目录,权限600
# 2. 主密码不需要记录在脚本里,从单独文件读取,权限也设600
MASTER_PASS_FILE="/etc/secure/master.key"
[ -r "$MASTER_PASS_FILE" ] || { echo "master key file missing or unreadable"; exit 1; }
MASTER_PASS=$(cat "$MASTER_PASS_FILE")
# 3. 解密出真实密码,存入变量,用完立即释放
DB_PASS=$(openssl enc -d -aes-256-cbc -pbkdf2 -base64 \
-pass pass:"$MASTER_PASS" -in /etc/secure/mysql_pass.enc 2>/dev/null)
if [ -z "$DB_PASS" ]; then
echo "Failed to decrypt DB password" >&2
exit 1
fi
# 4. 用配置文件方式传凭据,避免命令行参数
cat > /tmp/.my_backup.cnf.$$ <<EOF
[client]
user = $DB_USER
password = $DB_PASS
EOF
chmod 600 /tmp/.my_backup.cnf.$$
trap 'rm -f /tmp/.my_backup.cnf.$$' EXIT
mkdir -p "$BACKUP_DIR"
mysqldump --defaults-extra-file=/tmp/.my_backup.cnf.$$ \
-h "$DB_HOST" \
--single-transaction --routines --triggers \
--databases mydb | gzip > "${BACKUP_DIR}/mydb_${DATE_STR}.sql.gz"
# 5. 敏感变量清理
unset DB_PASS MASTER_PASS
echo "backup done: ${BACKUP_DIR}/mydb_${DATE_STR}.sql.gz"
逐行说明改动背后的理由:
/etc/secure目录权限做成700,目录里的密文文件和主密钥文件权限均为600,只有root能读。openssl enc -d解密失败时返回非零,所以用2>/dev/null抑制错误输出,避免误把错误打印到什么日志里。- 用
--defaults-extra-file生成运行时配置文件,这个文件只存在极短时间,而且用trap在脚本退出时立刻删除。这样密码不会出现在命令行参数里,ps和审计日志都拿不到。 set -euo pipefail让未定义变量、管道失败都显式报错,避免某个中间命令执行失败却继续往下跑,把密码留在临时文件中。- 脚本最后
unset把内存里的敏感变量尽早释放。
这套方案不需要额外安装任何软件,全是系统自带的openssl、cat、chmod、trap,运维人员也容易理解。
实际运行一次,正常输出只有一行:
code复制backup done: /backup/mysql/mydb_20250102_020001.sql.gz
这时候用 ps aux | grep mysqldump 查看进程,命令行里只有 --defaults-extra-file=/tmp/.my_backup.cnf.12345,不会出现密码;/etc/secure 目录下只有密文。这些细节用 history、cat /proc/$pid/cmdline 都验证过,没有明文残留。
不过也要说一句,上面的方案不是终极完美方案:运行时配置文件即使只存在几毫秒,如果被root以外的进程刚好读到,还是有微小风险。更严格的方案是使用 memfd_create 或 mkstemp 并在进程内读写,但这已经超出大多数Shell脚本需要的安全等级。对我这个场景来说,把风险从“长期明文暴露”压缩成“毫秒级临时文件”已经是质的提升。
4.3 批量加密配置:用for循环处理多个密码文件
如果你像我一样被分配去改造一批旧脚本,手头有几十个密码要加密,一个个手动敲openssl命令太累了。可以写一个一次性的批量加密脚本,顺带演示一下Shell的for循环:
bash复制#!/bin/bash
# 批量加密 passwords/ 目录下的明文密码文件
MASTER_PASS=$(openssl rand -base64 24)
ENC_DIR="/etc/secure"
mkdir -p "$ENC_DIR"
chmod 700 "$ENC_DIR"
for file in passwords/*.txt; do
[ -f "$file" ] || continue
base=$(basename "$file" .txt)
openssl enc -aes-256-cbc -pbkdf2 -salt -base64 \
-pass pass:"$MASTER_PASS" \
-in "$file" -out "$ENC_DIR/${base}.enc"
shred -u "$file" 2>/dev/null || rm -f "$file"
echo "encrypted $base"
done
echo "$MASTER_PASS" > /etc/secure/master.key
chmod 600 /etc/secure/master.key
echo "master key saved to /etc/secure/master.key"
shred -u 是安全删除明文文件的常用命令,虽然对某些文件系统比如日志型文件系统、SSD效果有限,但好过直接 rm。就我个人的习惯,在改造期间明文密码文件一定不会继续留在原目录里,处理完立刻销毁。
5. 常见问题与排查技巧实录
5.1 密钥权限与文件权限:改了半天才发现是权限问题
我用openssl方案的时候,最常见的问题是脚本报“解密失败”,排查下来大多数不是算法错了,而是权限问题。
具体来说:
/etc/secure目录权限如果是755,普通用户虽然不能读目录里的文件,但能看到文件列表;更糟的是如果某一天不小心把密文文件权限设成644,任何能登录系统的用户都能用openssl enc -d尝试暴力破解。- 主密钥文件权限务必600,如果脚本用普通用户跑,那普通用户就得能读主密钥,风险就上来了。所以我的建议是,这类脚本统一用root或专用的部署账号运行,不要开给所有用户。
- 复制密钥文件到其他机器时,注意
scp/rsync后的权限沿袭问题。默认umask是022,这会让你在远端生成一个644的文件,也就是说其他用户也能读取你刚拷贝过去的私钥。每次传输完成必须立刻执行chmod 600,把权限收回来。我因为这个问题吃过亏,有一次把私钥从跳板机拷贝到生产机,忘记了设置权限,第二天巡检时发现文件是644,惊出一身冷汗。养成顺手chmod 600的习惯,能省掉很多麻烦。
权限问题用一行命令自检:
bash复制ls -l /etc/secure && stat -c '%a %U %n' /etc/secure/*
理想结果是目录为700,所有文件为600,属主是脚本运行账户。
5.2 解密失败:遇到版本差异和参数兼容怎么排查
第二个高频问题出现在加密与解密两端的环境不一致上,尤其是openssl版本差异。旧版系统自带的openssl可能不支持 -pbkdf2 参数,而新版openssl又可能默认拒绝较弱的 -md5 派生方式。最典型的现象就是“开发机上好好的,生产机上解不开”。我第一次遇到时,第一反应怀疑是密码文件被破坏了,折腾了半天才发现两个系统里openssl版本不同、默认算法行为也不同。排查步骤我建议这样走:
第一,先确认生产环境的openssl版本。用 openssl version 查看,如果版本在3.0以上,那么对较老的加密参数兼容性会差一些,除非在加密时明确指定了兼容参数。
第二,加密解密两端的参数必须完全一致:算法、迭代次数、是否base64、盐是否复用。尤其是用 -pbkdf2 的时候,不同版本默认迭代次数可能不同,建议在加密命令里显式指定迭代次数:
bash复制openssl enc -aes-256-cbc -pbkdf2 -iter 10000 -salt -base64 \
-pass pass:"$MASTER_PASS" -in password.txt -out password.enc
解密时同样带上 -iter 10000。迭代次数越高,暴力破解代价越大,但同时解密耗时会增加,建议在安全和性能之间折中,10万次以内对现代机器都很快。
第三,确认密文没有被意外修改。base64格式的密文如果拷贝到邮箱、Markdown、聊天工具,很容易在行首末尾出现多余空格或换行符,导致解密时报bad magic。我建议密文始终以文本文件形式保存,不做格式转换,也不要手打密文。
GPG也一样,最容易出现问题的是密钥环路径错误和pinentry界面在纯SSH模式下无法弹出。排查时用 gpg --list-keys 和 gpg --list-secret-keys 看是否能看到正确的密钥指纹,解密命令加 --batch 避免交互提示。
5.3 安全与可用性的平衡:我的几条实战心得
最后这部分,我从二十多次脚本改造里沉淀出几条原则,供你参考。
第一,不要追求绝对安全。我见过有人为了“绝对安全”给脚本引入了一整套复杂的密钥管理系统,结果一个备份脚本调试了三天还没跑通,最后不得不放弃。对大部分内部运维场景,把明文密码改成加密存储+避免命令行泄漏,安全等级已经提升了几个量级。先做到这层,再考虑更完整的方案。
第二,主密码是最容易出问题的点。加密最主要的漏洞来自主密码的选择:有人用123456、公司名、机器名做主密码,等于把保险箱钥匙挂在保险箱旁边。正确做法是用 openssl rand -base64 24 生成随机主密码,写在权限为600的文件里,必要时再对文件本身加密。当然你也要接受一个事实:如果服务器彻底沦陷,样本都被拿走了,主密码迟早会被破解,所以要配合定期轮换。
第三,轮换策略很关键。密码要定期改,加密文件也要定期重新生成。我建议每次数据库密码变更时,加密配置文件里的密文同步更新,保持“密文与实际密码一致性”的审计。可以用CI/CD的Secret扫描工具检查脚本库中是不是又出现了明文密码,比如基于gitleaks、trufflehog的流水线,做一层自动防线。
第四,调试永远留一手。运行时想临时看密码?可以用掩码形式确认解密结果,比如 echo "${DB_PASS:0:3}...${DB_PASS: -2}" 打印前3位和后2位来判断格式是否正确,而不是完整回显。这个方法我屡试不爽,既能看到结构又不会泄露完整内容。
根据我的个人经验,改造脚本中最难的不是写出加密方案,而是打破“反正只有我能看到”的侥幸心理。第一次把脚本里的明文密码去掉时,我花了不少时间适应每次都要去翻密钥文件;但当我后来真的遇到一次服务器审计、一次代码仓库迁移泄露事件时,所有担忧都成了庆幸。如果你手上也有带着明文密码的脚本,别拖,今天就选一个方案动手:openssl加密也好、SSH密钥也罢,先把风险降下来。最后再提醒一句,安全改造不是一次性的工作,密码会更新、访问权限会换人、系统会升级,定期检查你的脚本里是否又出现了明文密码,比任何一次大改造都重要。
