把项目里的
.env明文密钥换成 1Password 密钥引用:仓库只提交.env.tpl(里面是op://保险库/条目/字段地址),本地用op run启动程序时临时注入真实值;再加一个提交前检查,防止有人把明文密钥写回去。
先看清成品
本机安装了 1Password CLI 2.41.0。检查脚本 4 个测试通过,对样例模板输出“检查通过”,故意加入明文令牌后正确报错;
app.py在没有注入时给出明确提示。op run在本机因为没有登录账号报No accounts configured for use with 1Password CLI.,没有用真实账号注入密钥。
注意:1Password 是付费服务,计划和价格以官方价格页为准。不要共享个人账号或把主密码、Secret Key、服务账号令牌发给别人;团队共享请用共享保险库和权限设置。只管理你有权管理的凭据。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
.env.tpl |
可以提交,只有引用 |
op run 启动方式 |
密钥只存在于子进程环境变量中 |
| 提交前检查 | 模板里的密钥类变量必须是引用;扫描常见密钥格式 |
.gitignore |
忽略本地 .env |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| 1Password 账号和桌面 App | 已登录 | 打开 设置 → 开发者 → 与 1Password CLI 集成 |
| 1Password CLI | 本文用 2.41.0 | op --version |
| 保险库 | 例如 dev-shop,放开发用凭据 |
不要和生产凭据混放 |
| Python | 3.9 以上 | 运行示例程序和检查脚本 |
文件结构
23-1password-A-src/
├── .env.tpl
├── .gitignore
├── app.py
├── check_env.py
└── test_check_env.py
第 1 步:在 1Password 里建条目
在 dev-shop 保险库建两个条目:postgres(字段 username、password)和 stripe-test(API 凭据类型,字段 credential)。在条目字段菜单里选“复制密钥引用”,可以直接得到 op://... 地址。
第 2 步:写模板
.env.tpl:
# 可以提交到仓库:这里只有密钥“地址”,没有密钥本身
APP_ENV=development
DB_HOST=127.0.0.1
DB_USER=op://dev-shop/postgres/username
DB_PASSWORD=op://dev-shop/postgres/password
STRIPE_SECRET_KEY=op://dev-shop/stripe-test/credential
引用格式为 op://保险库/条目/[分区/]字段。名称含空格时整条引用照常写在等号右边即可。
第 3 步:程序只读环境变量
app.py:
import os, sys
REQUIRED = ["DB_USER", "DB_PASSWORD", "STRIPE_SECRET_KEY"]
missing = [k for k in REQUIRED if not os.environ.get(k) or os.environ[k].startswith("op://")]
if missing:
sys.exit(f"缺少或未解析的变量:{', '.join(missing)}。请用 op run --env-file .env.tpl -- python app.py 启动")
print(f"已连接 {os.environ['DB_USER']}@{os.environ.get('DB_HOST')},密码长度 {len(os.environ['DB_PASSWORD'])}")
直接运行(未注入)的本机实际输出:
缺少或未解析的变量:DB_USER, DB_PASSWORD, STRIPE_SECRET_KEY。请用 op run --env-file .env.tpl -- python app.py 启动
第 4 步:用 op run 启动(本文未用真实账号执行)
op run --env-file .env.tpl -- python app.py
预期结果:1Password 弹出授权(指纹或系统密码),之后输出 已连接 ...,密码长度 N。op run 默认会把输出里出现的密钥值打码,但这只是兜底,程序本身不要打印密钥。
需要生成配置文件的程序可以用 op inject:
op inject -i config.tpl.yml -o config.yml # 输出文件默认权限 0600,记得加入 .gitignore
第 5 步:提交前检查
check_env.py:
"""提交前检查:.env.tpl 里像密钥的变量必须是 op:// 引用;仓库里不能出现常见密钥格式。"""
import re, subprocess, sys
from pathlib import Path
SECRET_NAME = re.compile(r"(PASSWORD|SECRET|TOKEN|KEY|CREDENTIAL)", re.I)
REF = re.compile(r"^op://[^/]+/[^/]+/(?:[^/]+/)?[^/]+$")
PATTERNS = {
"AWS 访问密钥": re.compile(r"AKIA[0-9A-Z]{16}"),
"GitHub 令牌": re.compile(r"gh[pousr]_[A-Za-z0-9]{36,}"),
"私钥": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
"Stripe 密钥": re.compile(r"sk_(live|test)_[A-Za-z0-9]{16,}"),
}
def check_template(text):
problems = []
for n, line in enumerate(text.splitlines(), 1):
line = line.strip()
if not line or line.startswith("#") or "=" not in line: continue
k, v = (s.strip() for s in line.split("=", 1))
if SECRET_NAME.search(k) and not REF.match(v):
problems.append(f".env.tpl 第{n}行 {k} 看起来是密钥,但值不是 op:// 引用")
return problems
def scan(files):
hits = []
for f in files:
try: text = Path(f).read_text(encoding="utf-8")
except (UnicodeDecodeError, FileNotFoundError, IsADirectoryError): continue
for name, pat in PATTERNS.items():
if pat.search(text): hits.append(f"{f}: 疑似{name}")
return hits
if __name__ == "__main__":
files = sys.argv[1:] or subprocess.run(["git", "ls-files"], capture_output=True, text=True).stdout.split()
problems = check_template(Path(".env.tpl").read_text(encoding="utf-8")) + scan(files)
print("\n".join("✗ " + p for p in problems) or "检查通过")
sys.exit(1 if problems else 0)
python check_env.py # 默认扫描 git ls-files 列出的文件
python -m pytest -q # 4 passed
接入 Git 钩子:
printf '#!/bin/sh\npython check_env.py\n' > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
调试示例
在模板末尾加一行 API_TOKEN=abc123,本机实际输出:
✗ .env.tpl 第7行 API_TOKEN 看起来是密钥,但值不是 op:// 引用
没有登录时运行 op run,本机实际输出第一行:
No accounts configured for use with 1Password CLI.
后面会列出三种登录方式:桌面 App 集成、op account add、服务账号令牌。
出错了怎么办?
| 现象 | 原因 | 解决 |
|---|---|---|
No accounts configured |
CLI 没连上桌面 App | 打开 App 的 CLI 集成,或 op signin |
isn't an item in the "xxx" vault |
条目名或保险库名写错 | 用 App 的“复制密钥引用” |
程序拿到的还是 op://... |
没通过 op run 启动 |
按第 4 步启动 |
子命令里 $DB_PASSWORD 是空的 |
变量在 op run 替换前就被 shell 展开了 | 用 op run -- sh -c 'echo ...' 在子 shell 中展开 |
| 检查脚本误报 | 变量名含 KEY 但不是密钥 | 改名,或把它放进非密钥前缀(如 PUBLIC_)并调整正则 |
模板规则
- 仓库只放
.env.tpl,不放.env。 - 密钥类变量一律写引用。
- 开发、生产凭据放在不同保险库。
项目资源
| 用途 | 地址 |
|---|---|
| 1Password CLI 入门 | https://www.1password.dev/cli/get-started |
| 用 op run 加载环境变量 | https://www.1password.dev/cli/secrets-environment-variables |
| 密钥引用语法 | https://www.1password.dev/cli/secret-reference-syntax |
| 1Password 价格 | https://1password.com/pricing |
| 1Password 服务条款 | https://1password.com/legal/terms-of-service |
明文 .env 最大的问题是它会被复制、被提交、被发到聊天里。换成引用以后,文件本身不再敏感,密钥只在程序运行的那一刻出现在内存里。