密码硬编码(Hard-Coded Secrets)是指将敏感信息(如数据库密码、API密钥、加密密钥、第三方服务凭证等)以明文形式直接写入应用程序的源代码、配置文件或固件中的行为。
常见的密钥硬编码场景:
- 密钥放在环境变量、配置
- 在配置文件中:
<add key="ConnectionString" value="Server=...;User Id=sa;Password=password;" />
- 在配置文件中:
- 密钥加密存储在代码里
- 在Python代码中:
db_password = "MySuperSecretPassword123!"
- 在Java代码中:
String apiKey = "AKIAIOSFODNN7EXAMPLE";
- 在Python代码中:
自查方法:
通过关键搜索
String pass="fadfdafad"
# Password key pwd pass jdbc encryption key Cipher key 等关键字搜索
危害说明
硬编码是安全实践中的大忌,主要原因如下:
- 丧失机密性: 源代码通常会被存储在版本控制系统(如Git)中,可能会被多位开发者访问,也可能意外泄露到公共平台(如GitHub)。一旦泄露,所有硬编码的密码都将暴露。
- 难以轮换: 如果一个密码被泄露或需要定期更换,开发人员必须修改代码、重新测试、重新部署整个应用程序。这个过程非常繁琐、耗时,且容易出错,导致密码轮换策略无法有效执行。
- 权限扩散: 任何有权访问代码的人(包括不应知道生产环境密码的开发者)都获得了敏感信息的访问权限,违反了“最小权限原则”。
- 缺乏审计: 无法追踪和审计是谁、在什么时候、使用了哪个密钥访问了关键服务。
防护核心
核心点在于密钥、代码、开发人员分离,尽可能少的让密钥被人接触到。(只是减缓因人导致的安全风险,无法杜绝研发人员恶意通过各种手段去获取到真实Key并利用。)
总结:将秘密与代码分离;
- 分离配置与代码: 敏感信息不属于代码的一部分。代码是逻辑,配置是环境依赖。两者应该完全分开。
- 使用安全的凭据管理系统: 将秘密集中存储在专门设计的安全系统中,如KMS、HashiCorp Vault、Azure Key Vault等。应用程序在运行时动态地从这些系统获取凭据。
- 最小权限原则: 应用程序和访问秘密的系统身份(如IAM角色)只应被授予完成其功能所必需的最小权限。
- 环境隔离: 为开发、测试、生产等不同环境使用不同的凭据。开发环境的数据库密码绝不能和生产环境相同。
- 加密与审计: 对静态存储(At-Rest)和传输中(In-Transit)的秘密进行加密,并记录所有对敏感凭据的访问和操作日志。
企业应制定安全策略:
密钥的安全策略中,密钥分层、密钥定期更换是重要的防护策略,因为你无法知道在何时何处密钥泄漏过,因此定期更换密钥能降低风险。
不同场景下的防范实践
场景一:拥有KMS或专业 vault(推荐的最佳实践)
当你的云平台或基础设施提供了成熟的密钥管理服务(如AWS KMS, GCP KMS, Azure Key Vault)或专业的秘密管理工具(如HashiCorp Vault)时,应采用以下模式:
实践方法:
- 存储秘密: 将数据库密码、API密钥等秘密加密后存储在安全的地方(如云平台的 Secrets Manager服务)或直接由Vault管理。
- 身份认证: 应用程序(例如运行在ECS、EC2、Kubernetes Pod中)需要一个身份来访问这些服务。这个身份不应是写死的AK/SK,而应是:
- 云上: 分配给应用程序运行环境的 IAM角色。例如,给EC2实例分配一个IAM角色,该角色被授权读取特定的Secrets Manager中的内容。
- 自建: 给Kubernetes Pod分配一个 Service Account,该账户有权访问Vault。
- 动态获取: 应用程序在启动时,凭借其身份(由云平台元数据服务或Kubernetes自动提供)安全地访问KMS/Vault,动态获取解密后的秘密,然后使用它来建立数据库连接等。
- 代码示例(概念):
# Python示例:使用Boto3从AWS Secrets Manager获取数据库密码 import boto3 import json from botocore.exceptions import ClientError def get_secret(): secret_name = "MyApp/Database" region_name = "us-west-2" session = boto3.session.Session() client = session.client(service_name='secretsmanager', region_name=region_name) try: get_secret_value_response = client.get_secret_value(SecretId=secret_name) except ClientError as e: ... # 处理异常 else: secret = get_secret_value_response['SecretString'] # 解析JSON字符串,假设secret存储的是JSON:{"username":"...","password":"..."} secret_dict = json.loads(secret) return secret_dict['username'], secret_dict['password'] # 主程序 db_user, db_password = get_secret() # 然后用获取到的user和password去连接数据库
场景二:没有KMS/加密机等高级工具的“简陋”环境
即使在没有高级工具的情况下,也绝对不应该硬编码到源代码里。可以采用以下“虽不完美,但远胜于硬编码”的实践方法:
实践方法:
-
使用环境变量:
- 将密码通过环境变量传递给应用程序。
- 优点: 简单、通用,与代码完全分离。部署时(Docker、系统d服务、手动启动)注入变量。
- 缺点: 环境变量可能在意外输出日志时被记录下来,或者在进程信息中可见(
ps -ef
)。切记不要将环境变量文件(如.env
)提交到版本控制。 - 示例:
# 启动脚本中 export DB_PASSWORD="super_secret_pass" python my_app.py
# my_app.py中 import os db_password = os.environ.get('DB_PASSWORD') if not db_password: raise RuntimeError("DB_PASSWORD environment variable not set!")
-
使用外部配置文件:
- 将秘密放在一个独立的配置文件(如
config.ini
,secrets.json
)中。 - 关键: 此文件必须被加入到
.gitignore
中,防止误提交。并通过部署工具(Ansible, SaltStack)或手动方式安全地分发到生产服务器。 - 示例 (
config.json
):{ "database": { "host": "localhost", "user": "myuser", "password": "mysecretpassword" } }
- 将秘密放在一个独立的配置文件(如
-
严格的访问控制:
- 即使使用了上述“简陋”方法,也必须对存储密码的服务器、文件和环境变量设置严格的权限。
- 文件权限: 确保只有运行该应用程序的用户和必要的管理员有权限读取该配置文件。例如:
chmod 600 config.json
(仅所有者可读可写)。 - 基础设施安全: 保护好你的服务器,限制SSH访问,做好基础的安全防护。
即使资源有限,也应当优先选择环境变量或受权限严格保护的外部配置文件,并逐步升级到更专业的密钥管理方案。