效率工具

把项目里的 .env 明文密钥换成 1Password 密钥引用

把项目里的 .env 明文密钥换成 1Password 密钥引用:仓库只提交 .env.tpl(里面是 op://保险库/条目/字段 地址),本地用 op run 启动程序时临时注入真实值;再加一个提交前检查,防止有人把明文密钥写回去。

BINBHO · 图文教程

把项目里的 .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 最大的问题是它会被复制、被提交、被发到聊天里。换成引用以后,文件本身不再敏感,密钥只在程序运行的那一刻出现在内存里。