neosmart/securestore-rs

GitHub: neosmart/securestore-rs

一个基于 Rust 的轻量级加密密钥管理工具,提供命令行客户端和运行时 API,支持将加密后的密钥文件安全地纳入版本控制系统。

Stars: 156 | Forks: 12

# Rust 版 SecureStore [![crates.io](https://img.shields.io/crates/v/securestore.svg)](https://crates.io/crates/securestore) [![docs.rs](https://docs.rs/securestore/badge.svg)](https://docs.rs/crate/securestore) 本仓库包含用于创建和操作 [SecureStore secrets 文件](http://neosmart.net/blog/2020/securestore-open-secrets-format/)的 [`ssclient` 命令行工具](./ssclient/),以及提供 API 以在运行时从 SecureStore 检索和解密 secrets 的 [`securestore` rust crate](./securestore/)。 ## 用法 SecureStore 协议的 Rust 实现分为两个独立但互补的部分:[命令行客户端](./ssclient/)(`ssclient`)和 [rust 库/crate](./securestore/)(`securestore`)。两者(大部分)暴露相同的功能,但旨在配合使用以实现最大的生产力和人体工程学。 此处演示了在受限访问二进制文件(例如 Web 应用程序、kiosk 或类似应用程序不直接分发给最终用户且运行在被认为是特权环境中)旁边部署版本化 secrets 的典型工作流程。 ### 构建特性(加密后端) 加密由两个后端之一提供,在构建时通过 Cargo 特性选择: | 特性 | 描述 | |--------|-------------| | `openssl` (默认) | 使用 [OpenSSL] 库。需要预先安装 OpenSSL。 | | `openssl-vendored` | 在构建时构建并静态链接 [OpenSSL] 库。需要目标平台的 C 工具链。 | | `rustls` | 纯 Rust 后端(无需 OpenSSL 和 C 工具链)。使用 `getrandom`、`aes`、`cbc`、`hmac`、`sha1`、`pbkdf2` 和 `subtle` crates。当您想避免 OpenSSL 时(例如 Windows、musl 或交叉编译)非常理想。 | - **默认:**`securestore` crate 和 `ssclient` 默认都使用 **OpenSSL** 后端。 - **Vendored OpenSSL:**要静态链接 OpenSSL(例如用于便携式 Linux 二进制文件),使用 **openssl-vendored** 特性:`--features openssl-vendored`。 - **原生 Rust:**使用 `--no-default-features --features rustls` 构建以使用纯 Rust 后端。不需要系统 OpenSSL 或 C 工具链。 使用一个后端创建或解密的 Vault 完全兼容;存储格式和算法是相同的。 **示例:** ``` # 默认,需要 OpenSSL: cargo build cargo install ssclient # 纯 Rust,无 OpenSSL 依赖: cargo build --no-default-features --features rustls cargo install ssclient --no-default-features --features rustls # 静态 OpenSSL 构建,例如用于 musl 或 Alpine: cargo build --target x86_64-unknown-linux-musl --features openssl-vendored ``` 在 `Cargo.toml` 中,使用默认特性依赖 `securestore` 以使用 OpenSSL,或者显式选择一个后端: ``` [dependencies] # 默认(需要已安装 OpenSSL): securestore = "0.100" # 纯 Rust(无 OpenSSL 或其他系统依赖): # securestore = { version = "0.100", default-features = false, features = ["rustls"] } # 从源码构建 OpenSSL: # securestore = { version = "0.100", features = ["openssl-vendored"] } ``` ## 将 SecureStore vault 添加到您的 rust workspace 首先安装 `ssclient`,即交互式 SecureStore cli。预构建的二进制文件可用(参见 releases),但大多数 rust 开发人员会发现最方便的分发方式是通过 `cargo` 直接安装:

> cargo install ssclient

    Updating crates.io index

  Installing ssclient v0.100.0

   Compiling securestore v0.100.0

   Compiling ssclient v0.100.0

    Finished release [optimized] target(s) in 36.14s

  Installing /home/mqudsi/.cargo/bin/ssclient

   Installed package `ssclient v0.100.0` (executable `ssclient`)

`ssclient` 将被安装并添加到您 `$PATH` 中的 cargo 二进制目录中,使得只需调用 `ssclient` 即可在终端会话中使用它。

假设您的工作树是一个典型的 rust 二进制应用程序,其布局与典型的基于 Cargo 的 rust 项目相匹配:


```
> tree

.

├── .git/

├── Cargo.toml

└── src

    └─ main.rs
```


我们将把我们的 secrets 存储在 cargo workspace 根目录下一个名为 `secure` 的文件夹中。打开终端,`cd` 进入您的 rust 项目,并执行以下命令以创建一个新的 `secure` 目录并使用 `ssclient` 创建一个名为 `secrets.json` 的新 secrets vault(这是 SecureStore 协议的默认名称):


```
> cd my-rust-project

> mkdir secure

> cd secure

> ssclient create secrets.json --export-key secrets.key

Password: ************

Confirm password: ************

Saving newly generated key to secrets.key

Excluding key file in newly-created VCS ignore file .gitignore
```


`ssclient` 将提示您输入(然后确认)密码,然后创建一个新的 `secrets.json` 文件(目前是一个没有任何 secrets 的骨架 SecureStore vault)。vault 是对称加密的,但可以使用您刚才输入的密码或我们刚才导出的密钥文件(`secrets.key`)进行解密。当在命令行通过 `ssclient` 更新或与 SecureStore vault 交互时,我们将使用基于密码的加密/解密,但是当我们在开发或生产环境中部署应用程序时,我们将使用基于密钥的加密/解密。

我们项目文件夹的布局现在如下所示:


```
> tree .

.

├── .git/

├── Cargo.toml

├── secure

│   ├── .gitignore

│   ├── secrets.json

│   └── secrets.key

└── src

    └── main.rs
```


如您所见,`secrets` 子目录下有三个新文件:`secrets.json`(人类可读的 SecureStore vault),`secrets.key`(敏感且安全的主密钥,让我们无需手动干预即可在生产环境中绕过密码保护来解密 secrets),以及一个由 `ssclient` 自动创建的 `.gitignore` 文件,用于防止 `secrets.key` 被意外提交到 git repo。

[SecureStore 协议](http://neosmart.net/blog/2020/securestore-open-secrets-format/)指定 `secrets.json` _应该_ 被添加到 git 仓库,并与使用其 secrets 的其余代码一起进行版本控制,同时绝对禁止 `secrets.key` 被提交到 VCS。

这就是“空” `secrets.json` 文件现在的样子,在我们添加任何 secrets 之前:


```
{

  "version": 3,

  "iv": "eebJviP8bIC6XF6fp1g4Cw==",

  "sentinel": {

    "iv": "dVEOSg1OaM6LLF2fB7W7jg==",

    "hmac": "mY1LP5gBDQaYFenC2oHHRb7LXSg=",

    "payload": "7mFgSJSYclBBPo+Xbel0DA5y8e24QKqUh7m8EXy5+8bSagUoHGoIi2sJSKlSDP4X"

  },

  "secrets": {}

}
```


这是 SecureStore (v3) vault 的基本“骨架”,仅包含足够的信息,让我们下次打开 vault 时 `ssclient` 或 `securestore` crate 可以验证我们正在使用正确的加密/解密密钥。

生成的 `secure/.gitignore` 文件包含以下(一条)规则:


```
secrets.key
```


它所做的只是阻止 `secure/secrets.key` 被意外添加到 git repo。

继续将 `secure` 目录添加到 git,然后继续下一节,看看我们将如何添加 secrets 然后在运行时从我们的 rust 应用程序中访问它们:


```
git add secure/

git commit -m "Add empty SecureStore vault"

git push
```


## 将 secrets 添加到您的 SecureStore vault

此时,vault 已经创建,我们准备向 vault 添加一个或多个 secrets。secrets 将以加密形式存储在 `secrets.json` 中,并在 git 中与使用它们的相同代码一起进行版本控制,这使得回滚到早期版本变得容易,并确保托管 secrets 与依赖于它们的代码在合并和 PR 中保持同步。

从我们刚才离开的终端开始,让我们添加一两个 secrets(例如,用于访问 PostgreSQL 的凭证和用于上传或签署 S3 urls 的 AWS IAM 凭证)。我们可以在命令行指定 secret 值(`ssclient set secret_name secret_value`),或者在 `ssclient` 提供的提示符下输入以提高安全性(例如,避免 secret 存储在我们的 bash 历史记录中)或为了方便(例如,输入否则需要在 bash 下转义的特殊字符),只需指定 secret 名称(`ssclient set secret_name`)。我们可以省略 store 名称/路径,因为我们使用的是默认值(`secrets.json`),而且我们不需要指定 `-p` 或 `--password`,因为 `ssclient` 默认使用基于密码的解密。

让我们 `cd` 进入 `secure` 目录并设置第一个 secret:


```
> cd secure/

> ssclient set db:username

Password: ************

Value: pgadmin
```


为了演示 SecureStore 协议如何允许使用密码或密钥文件进行互换的加密/解密,我们将使用我们之前生成的密钥文件(`secrets.key`)设置下一个 secret 值,我们会注意到添加 secret 时 `ssclient` 没有提示我们输入密码(但它仍然是加密的,当然):


```
> ssclient -k secrets.key set db:password pgsql123

Value: pgsql123
```


让我们继续设置另外两个 secrets,代表 AWS S3 IAM 用户名和密码:


```
> ssclient -k secrets.key set aws:s3:access_id AKIAIOSFODNN7

> ssclient -k secrets.key set aws:s3:access_key wJalrXUtnFEMI/K7MDENG/bPxRfiCY
```


secrets vault(`secrets.json`)现在包含四个 secrets(两个非真正秘密的用户名和两个真正秘密的密码)。在我们的 git worktree 中唯一改变的文件是 secrets vault:


```
> git status

On branch master

Changes not staged for commit:

  (use "git add ..." to update what will be committed)

    (use "git restore ..." to discard changes in working directory)

            modified:   secrets.json
```


让我们看看我们的 `secrets.json` 文件现在是什么样子:


```
{

  "version": 3,

  "iv": "eebJviP8bIC6XF6fp1g4Cw==",

  "sentinel": {

    "iv": "dVEOSg1OaM6LLF2fB7W7jg==",

    "hmac": "mY1LP5gBDQaYFenC2oHHRb7LXSg=",

    "payload": "7mFgSJSYclBBPo+Xbel0DA5y8e24QKqUh7m8EXy5+8bSagUoHGoIi2sJSKlSDP4X"

  },

  "secrets": {

    "aws:s3:access_id": {

      "iv": "4YEmTY9rwW7pq7/Iv6Vncg==",

      "hmac": "tFO2cN/Fh/jO7ijbAXh98yrm2Nk=",

      "payload": "ejKs4D2anovxS1OyX8e4Eg=="

    },

    "aws:s3:access_key": {

      "iv": "czwulU+ejDXD0dryqA6aaA==",

      "hmac": "w/egLShBDwu9L/Wagk/EKQVzpK0=",

      "payload": "OfJM7ZDLOkGhD8OwjfOOQrHNgyeQqYPuDZKwARxN/nw="

    },

    "db:password": {

      "iv": "rN0a3GVuTkBctAbCO51VQA==",

      "hmac": "ko8YB33XxhCEIge8Rxdyab1EA4Y=",

      "payload": "uvq/NsjCfVkc6Upv8EWg8A=="

    },

    "db:username": {

      "iv": "CkWYqyIWOTquFco/NZGiKA==",

      "hmac": "ODcnI82hkzQ+9feyf/1WlV3yxt8=",

      "payload": "oEmn2rQE9Hva97XwdNYkoA=="

    }

  }

}
```


我们可以看到 `secrets` 下有四个新的命名 JSON 对象,每个 secret 一个。它们根据跨平台 SecureStore 协议进行加密和编码,方式是人类可读的(纯文本,二进制 payload 的紧凑 base64 表示,格式良好),对 git 友好(按名称排序,格式化带有换行符在每个 secret 之间并分隔每个 secrets 组件等),以及跨平台兼容(标准化的加密和 base64 编码)。

如果您在自己的终端中跟着操作,您 hopefully 会注意到,虽然您的 `secrets.json` 文件的基本结构与这个相同,但 secret 值本身是不同的 —— 这不仅仅是因为我们选择了不同的密码!这是 SecureStore vault 如此安全的部分原因,如果攻击者获得了您的 `secrets.json`(这足够安全,可以发布到 GitHub 甚至 _The New York Times_ 的头版而无需担心),可以防止他们猜测您的密码或加密的 secret 内容 —— 当然,前提是您的 `secrets.key` 保持安全!

继续将您更新的 secrets 提交到 git:


```
> git add secrets.json

> git commit -m "Add secrets for db and s3 access"
```


## 在运行时检索 secrets

此时,我们将把 `ssclient` 放在一边,专注于 rust 方面的事情,看看如何在运行时检索 secrets。首先,让我们在 `Cargo.toml` 中添加对 `securestore` crate 的依赖。我们还将添加对 `once_cell` 的依赖,以便为了演示目的将 `securestore::SecretsManager` 用作单例:


```
[package]

name = "sstemp"

version = "0.1.0"

edition = "2021"



[dependencies]

once_cell = "1.13.0"

securestore = "0.100.0"

# 可选:使用纯 Rust 加密后端(无 OpenSSL)

# securestore = { version = "0.100.0", default-features = false, features = ["rustls"] }
```


之后我们可以打开 `src/main.rs` 并添加一些代码来打开 secrets 文件并在运行时解密 + 检索一个或多个 secrets。[ssclient 文档](https://docs.rs/securestore/latest/securestore/)涵盖了 `securestore` crate 及其主要 `SecretsManager` 类型的使用方式,但我们将在下面演示基础知识。

在 `main.rs` 的顶部,让我们添加一些导入和一个 `once_cell::sync::Lazy` 实例来保存我们的 `SecretsManager`,因为强烈建议在应用程序的生命周期内只有一个 `SecretsManager` 实例为所有 secrets 请求提供服务:


```
use securestore::{KeySource, SecretsManager};

use std::path::Path;

use once_cell::sync::Lazy;



static SECRETS: Lazy = Lazy::new(|| {

    let keyfile = Path::new("secure/secrets.key");

    SecretsManager::load("secure/secrets.json", KeySource::File(keyfile))

        .expect("Failed to load SecureStore vault!")

})
```


至于访问 secrets,让我们添加一个辅助函数 `get_db_credentials()`,它将查找我们之前保存到 vault 中的名为 `db:username` 和 `db:password` 的 secrets,并将它们作为 `(String, String)` 元组返回:


```
fn get_db_credentials() -> Result<(String, String), securestore::Error> {

    let username = SECRETS.get("db:username")?;

    let password = SECRETS.get("db:password")?;

    Ok((username, password))

}
```


并验证应用程序构建没有任何问题:


```
> cargo build

   Compiling sstemp v0.1.0 (/tmp/sstemp)

    Finished dev [unoptimized + debuginfo] target(s) in 0.74s
```


这就是全部了!我们可以通过添加一个断言 secrets 内容的测试来演示它有效(当然在现实世界中这是一个禁忌!):


```
#[test]

fn verify_db_credentials() -> Result<(), securestore::Error> {

    let expected = ("pgadmin".to_owned(), "pgsql123".to_owned());

    let actual = get_db_credentials()?;



    assert_eq!(expected, actual);

    return Ok(());

}
```


然后运行测试:


```
> cargo test

    Finished test [unoptimized + debuginfo] target(s) in 0.06s

         Running unittests src/main.rs (tar et/debug/deps/sstemp-b3b06ea1ff58c9da)



         running 1 test

         test verify_db_credentials ... ok
```


我们只需要确保当我们部署代码时(通过我们通常将应用程序部署到生产环境的任何方法),我们也发送一份 `./secure` 文件夹的副本并将其存储在可执行文件旁边(或在我们执行应用程序的 CWD 中)——这包括版本化且可安全共享的 `secure/secrets.json` 以及绝不能泄露到非安全环境的高度机密的 `secure/secrets.key`。

# 高级 secrets 管理主题

您会注意到在上面的指南中,我们存储了两个 secrets(`db:password` 和 `aws:s3:access_key`)和两个不那么秘密的值(`db:username` 和 `aws:s3:access_id`),这些值本可以硬编码到应用程序中。这是为什么?

答案在于如何在现实世界中使用 SecureStore,在 *n* > 1 的团队中,并非每个人都可能拥有提交权限。理想情况下,您的开发和生产基础设施应该是完全分开的,您的 secrets 也应该如此。SecureStore 协议使这变得容易,因为它实际上只是一个美化的(但安全的!)键值数据库,与您的代码本身一起进行版本控制和存储。在实践中,您不会只有 `secrets.key` 和 `secrets.json` —— 实际上,您的 dev/staging/production 环境各有一个 vault/key 对,所有 vault 都保存在 `secure/` 文件夹下并提交到仓库,但每个环境 vault 对应的 `secrets.key` 文件仅与应该有权访问它的人共享。也许每个人都有权访问 `secrets.dev.key` 并且可以从 `secrets.dev.json` 添加/检索/删除 secrets,但生产环境的 `secrets.prod.key` 仅作为受保护的资产存储在 CI 服务器上,部署到生产环境但受保护,任何团队成员都无法检索,除了那些拥有直接访问安全 CI 环境或远程访问生产服务器权限的人(您的实际情况可能会有所不同,但基本思路是成立的)。

SecureStore 协议和 SecureStore crate 的通用 KV 特性使得抽象不同的 dev/staging/prod 配置变得容易,只需在启动时根据环境(例如,由环境变量标识)加载不同的 store,并结合将任何依赖于配置的非秘密值(用户名、连接字符串、主机地址等)_当作_ 它们是同一个 SecureStore vault 中的 secrets 来存储,所有这些都只需通过确定并加载正确的 store一次即可自动解析。例如,您实际的 `SECRETS` 声明可能如下所示:


```
static SECRETS: Lazy = Lazy::new(|| {

    let (name, store_path, key_path) = match std::env::var("MYWEBAPP_ENV").as_deref()) {

        Ok("STAGING") => ("staging", "secure/secrets.staging.json", Path::new("secure/secrets.staging.key")),

        Ok("PRODUCTION") => ("prod", "secure/secrets.prod.json", Path::new("secure/secrets.prod.key")),

        _ => ("dev", "secure/secrets.dev.json", Path::new("secure/secrets.dev.key")),

    };



    SecretsManager::load(store_path, KeySource::File(key_path))

        .expect(&format!("Failed to load {name} SecureStore vault!"))

});
```


因此,虽然我们 _本可以_ 硬编码 `db:username`,但这也意味着还要在 `get_db_credentials()` 中测试 `MYWEBAPP_ENV` 以确定要返回的用户名,除了在 `SECRETS` 声明中确定要加载哪个 secrets store 的 `MYWEBAPP_ENV` 检查之外。但是,如果我们像存储 `db:password` 一样将 `db:username` 存储为 secret,所有这些区分都会自动为我们完成。此外,将所有凭证信息保存在一个地方(secrets vault)也是有意义的,完全独立于运行时逻辑 —— 如果将来需要更新用户名和密码,您只需要在一个地方(vault)更新它们,而不需要同时修补 `secrets.json` 和 `src/main.rs`(此外还要记得这样做)。
标签:AES, CBC, DNS 反向解析, Git友好, OpenSSL, PBKDF2, Rust, Rustls, SecureStore, 加密, 可视化界面, 大语言模型安全, 安全, 安全可观测性, 安全测试工具, 密码管理器, 密钥存储, 开发库, 机密管理, 漏洞扫描器, 版本控制, 网络流量审计, 超时处理, 通知系统, 静态链接