• Stars
    star
    126
  • Rank 283,272 (Top 6 %)
  • Language
    Rust
  • License
    MIT License
  • Created almost 6 years ago
  • Updated 7 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A simple, encrypted, git-friendly, file-backed secrets manager for rust

SecureStore for Rust

crates.io docs.rs

This repository houses the ssclient command-line utility for creating and manipulating SecureStore secrets files as well as the securestore rust crate providing an API for retrieving and decrypting secrets from a SecureStore at runtime.

Usage

This rust implementation of the SecureStore protocol ships in two separate, complementary parts: a command line client (ssclient) and a rust library/crate (securestore). Both (mostly) expose the same functionality, but are intended to be used together for maximum productivity and ergonomics.

The typical workflow for deploying versioned secrets alongside a restricted-access binary (e.g. a web app, a kiosk, or similar where the application isn't distributed directly to end users and is running in what is considered to be a privileged environment) is demonstrated here.

Adding a SecureStore vault to your rust workspace

Start by installing a copy of ssclient, the interactive SecureStore cli. Pre-built binaries are available (see releases), but the majority of rust developers will find the most convenient option for distribution to be direct installation via 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 will be installed and added to the cargo binary directory in your $PATH, making it available in a terminal session merely by invoking ssclient.

Let's imagine your worktree is a typical rust binary application with a layout matching a typical Cargo-based rust project:

> tree
.
β”œβ”€β”€ .git/
β”œβ”€β”€ Cargo.toml
└── src
    └─ main.rs

We'll be storing our secrets in a folder called secure in the root of our cargo workspace. Open a terminal, cd into your rust project, and execute the following to create a new secure directory and use ssclient to create a new secrets vault called secrets.json (which is the SecureStore protocol default name):

> 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 will prompt you to enter (then confirm) a password before it creates a new secrets.json file (currently a skeleton SecureStore vault without any secrets). The vault is symmetrically encrypted but can be decrypted either with the password you just entered or with the key file we just exported (secrets.key). When updating or interacting with the SecureStore vault via ssclient at the command line, we'll use password-based encryption/decryption, but when we deploy the app in dev or production, we'll be using key-based encryption/decryption instead.

The layout of our project folder now looks like this:

> tree .
.
β”œβ”€β”€ .git/
β”œβ”€β”€ Cargo.toml
β”œβ”€β”€ secure
β”‚Β Β  β”œβ”€β”€ .gitignore
β”‚Β Β  β”œβ”€β”€ secrets.json
β”‚Β Β  └── secrets.key
└── src
    └── main.rs

As you can see, there are three new files under the secrets subdirectory: secrets.json (the human-readable SecureStore vault), secrets.key (the sensitive & secure master key that lets us bypass password protection to decrypt secrets in production without manual intervention), and a .gitignore file that was helpfully created by ssclient and prevents secrets.key from being accidentally committed to the git repo.

The SecureStore protocol specifies that secrets.json should be added to the git repository and versioned alongside the rest of the code that uses its secrets, and absolutely forbids secrets.key from being committed to a VCS.

Here's what the "empty" secrets.json file looks like right now, before we've added any secrets:

{
  "version": 3,
  "iv": "eebJviP8bIC6XF6fp1g4Cw==",
  "sentinel": {
    "iv": "dVEOSg1OaM6LLF2fB7W7jg==",
    "hmac": "mY1LP5gBDQaYFenC2oHHRb7LXSg=",
    "payload": "7mFgSJSYclBBPo+Xbel0DA5y8e24QKqUh7m8EXy5+8bSagUoHGoIi2sJSKlSDP4X"
  },
  "secrets": {}
}

This is the basic "skeleton" of a SecureStore (v3) vault, containing just enough info to let ssclient or the securestore crate verify that we're using the correct encryption/decryption key the next time we open the vault.

The generated secure/.gitignore file contains the following (one) rule:

secrets.key

All it does is stop secure/secrets.key from being accidentally added to the git repo.

Go ahead and add the secure directory to git, then move on to the next section to see how we'll add secrets and then access them at run-time from our rust application:

git add secure/
git commit -m "Add empty SecureStore vault"
git push

Adding secrets to your SecureStore vault

At this point, the vault has been created and we're ready to add one or more secrets to the vault. The secrets will be stored encrypted in secrets.json and versioned in git alongside the same code using them, making it easy to roll back to earlier versions and make sure that managed secrets are kept in lock-step with the code that depends on them, across merges and PRs.

Starting from the terminal where we left off, let's add a secret or two (say, the credentials for accessing PostgreSQL and the AWS IAM credentials to upload or sign S3 urls). We can either specify the secret value at the command line (ssclient set secret_name secret_value) or enter it at a prompt provided by ssclient for more security (e.g. to avoid the secret being stored in our bash history) or for convenience (e.g. entering special characters that would otherwise need to be escaped under bash) by only specifying the secret name (ssclient set secret_name). We can omit the store name/path because we're using the default (secrets.json) and we don't need to specify -p or --password because ssclient defaults to password-based decryption.

Let's cd into the secure directory and set the first secret:

> cd secure/
> ssclient set db:username
Password: ************
Value: pgadmin

To demonstrate how the SecureStore protocol allows interchangeable encryption/decryption with either a password or a key file, we'll set the next secret value using the key file we generated earlier (secrets.key) and we'll notice that the secret is added without ssclient prompting us to enter a password (but it's still encrypted, of course):

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

Let's go ahead and set two more secrets, representing the AWS S3 IAM username and password:

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

The secrets vault (secrets.json) now contains four secrets (two not-actually-secret usernames and two actually-secret passwords). The only file that has changed in our git worktree is the secrets vault:

> git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
    (use "git restore <file>..." to discard changes in working directory)
            modified:   secrets.json

Let's see what our secrets.json file looks like now:

{
  "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=="
    }
  }
}

We can see there are four new named JSON objects under secrets, one for each secret. They're encrypted and encoded per the cross-platform SecureStore protocol in a way that's human-readable (plain-text, compact base64 representation of binary payloads, well-formatted), git-friendly (sorted by name, formatted with new lines between each secret and delimiting each secrets component, etc), and cross-platform compatible (standardized encryption and base64 encoding).

If you're following along in your own terminal, you'll hopefully notice that while the basic structure of your secrets.json file is the same as this one, the secret values themselves differ - this isn't just because we picked different passwords, though! This is part of what makes a SecureStore vault so secure, and prevents attackers from guessing your password or the encrypted secret contents if they get their hands on your secrets.json (which is safe enough to publish to GitHub or even the front page of The New York Times without worry) -- as long as your secrets.key stays secure, of course!

Go ahead and commit your updated secrets to git:

> git add secrets.json
> git commit -m "Add secrets for db and s3 access"

Retrieving secrets at runtime

At this point, we're going to set ssclient aside and focus on the rust side of things to see how secrets can be retrieved at runtime. First, let's add a dependency on the securestore crate to our Cargo.toml. We'll also add a dependency on once_cell to use securestore::SecretsManager as a singleton for demonstration purposes:

[package]
name = "sstemp"
version = "0.1.0"
edition = "2021"

[dependencies]
once_cell = "1.13.0"
securestore = "0.100.0"

After which we can open src/main.rs and add some code to open the secrets file and decrypt + retrieve one or more secrets at runtime. The ssclient documentation covers how the securestore crate and its primary SecretsManager type are used, but we'll demo the basics below.

At the top of main.rs, let's add some imports and a once_cell::sync::Lazy instance to hold our SecretsManager, since it's highly recommended to only have one SecretsManager instance servicing all secrets requests for the lifetime of the application:

use securestore::{KeySource, SecretsManager};
use std::path::Path;
use once_cell::sync::Lazy;

static SECRETS: Lazy<SecretsManager> = Lazy::new(|| {
    let keyfile = Path::new("secure/secrets.key");
    SecretsManager::load("secure/secrets.json", KeySource::File(keyfile))
        .expect("Failed to load SecureStore vault!")
})

As for accessing the secrets, let's add a helper function get_db_credentials() that will look up the secrets named db:username and db:password that we previously saved to the vault and return them as a (String, String) tuple:

fn get_db_credentials() -> Result<(String, String), securestore::Error> {
    let username = SECRETS.get("db:username")?;
    let password = SECRETS.get("db:password")?;
    Ok((username, password))
}

and verify that the application builds without any issues:

> cargo build
   Compiling sstemp v0.1.0 (/tmp/sstemp)
    Finished dev [unoptimized + debuginfo] target(s) in 0.74s

And that's all there is to it! We can demonstrate that it works by adding a test asserting the contents of the secrets (of course this would be a no-no in the real world!):

#[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(());
}

Then run the test:

> 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

We just need to make sure that when we deploy our code (via whatever method we normally deploy our app into production), we also send a copy of the ./secure folder and store it alongside the executable (or in the CWD we execute our app under) -- this includes both the versioned and safe-to-share-with-the-world secure/secrets.json as well as the highly secret secure/secrets.key that mustn't ever be leaked to a non-secure environment.

Advanced secrets management topics

You'll notice that in the guide above, we stored two secrets (db:password and aws:s3:access_key) and two not-so-secret values (db:username and aws:s3:access_id) that could have been instead hard-coded into the application. What gives?

The answer lies in how SecureStore should be used in the real world, with teams of n > 1 and where not everyone may have the commit bit. Ideally, your dev and production infrastructure should be completely separate, as should your secrets. The SecureStore protocol makes this easy because it's really just a glorified (but secure!) key-value database, versioned and stored alongside your code itself. In practice, you won't just have secrets.key and secrets.json - you'll actually have a vault/key pair for each of your dev/staging/production environments, with all the vaults saved under the secure/ folder and committed to the repository, but with the corresponding secrets.key file for each environment's vault only shared with the people that should have access to it. Perhaps everyone has access to secrets.dev.key and can add/retrieve/remove secrets from secrets.dev.json but the production secrets.prod.key is only stored on the CI servers as a protected asset deployed to production but protected from being retrieved by any team members except for those with direct access to the secure CI environment or remote access to the production servers (your actual scenario will likely differ, but the basic idea stands).

The generic KV-nature of the SecureStore protocol and the SecureStore crate make it easy to abstract over the different dev/staging/prod configurations by simply loading a different store at startup depending on the environment (identified, say, by an environment variable), combined with storing any configuration-dependent non-secret values (usernames, connection strings, host addresses, etc) as if they were secrets in the same SecureStore vault, all automatically resolved just by determining and loading the correct store just once. For example, your actual SECRETS declaration could look like this:

static SECRETS: Lazy<SecretsManager> = Lazy::new(|| {
    let (store_path, key_path) = match std::env::var("MYWEBAPP_ENV").as_ref().map(|s| s.as_str()) {
        Ok("STAGING") => ("secure/secrets.staging.json", Path::new("secure/secrets.staging.key")),
        Ok("PRODUCTION") => ("secure/secrets.prod.json", Path::new("secure/secrets.prod.key")),
        _ => ("secure/secrets.dev.json", Path::new("secure/secrets.dev.key")),
    };

    SecretsManager::load(store_path, KeySource::File(key_path))
        .expect("Failed to load SecureStore vault!")
});

So while we could have hard-coded db:username, that would have meant also testing MYWEBAPP_ENV in get_db_credentials() to figure out what username to return in addition to the MYWEBAPP_ENV check in the SECRETS declaration that determined which secrets store to load. But if we store db:username as a secret the same way we store db:password, all that differentiation is done automatically for us. In addition, it makes sense to keep all the credentials info in one place (the secrets vault), wholly separate from the runtime logic - if you need to update the username and password in the future, you just need to update them in one place (the vault) rather than needing to patch both secrets.json and src/main.rs (in addition to remembering to do so).

More Repositories

1

pevents

Implementation of Win32 events for *nix platforms, built on top of pthreads.
C++
265
star
2

AsyncLock

An async/await-friendly lock for .NET, complete with asynchronous waits, safe reëntrance, and more.
C#
185
star
3

CppSQLite

A simple and easy-to-use cross-platform C++ wrapper for the SQLite API. Fork of the CppSQLite project, originally by Rob Groves, currently updated and maintained by NeoSmart Technologies.
C++
127
star
4

tac

A high-performance, cross-platform file reverse utility
Rust
108
star
5

SecureStore

A .NET implementation of the cross-platform SecureStore (symmetrically-encrypted secrets) protocol
C#
93
star
6

RunInBash

Run Linux commands under WSL without leaving your PowerShell or CMD!
C++
92
star
7

unicode.net

A Unicode library for .NET, supporting UTF8, UTF16, and UTF32. With an extra helping of emoji for good measure πŸ”₯🌢️😁
C#
87
star
8

msvcrt.lib

msvcrt.lib for linking against msvcrt.dll on all versions of Windows
C
82
star
9

SqliteCache

An ASP.NET Core IDistributedCache provider backed by SQLite
C#
80
star
10

prettysize-rs

Pretty-print file sizes and more
Rust
42
star
11

ln-win

JunctionPoint library and ln clone for Windows.
C++
33
star
12

udpproxy

Cross-platform UDP proxy
Rust
31
star
13

PrettySize.net

Format/print file sizes in a human-readable format.
C#
30
star
14

UrlBase64

A standards-compliant implementation of web/url-safe base64 encoding and decoding for .NET targets
C#
30
star
15

paste

A Windows utility that simply dumps the clipboard data to stdout
C++
26
star
16

rewrite

An in-place file rewrite utility, useful for redirecting output to same file as source.
Rust
26
star
17

betterpad

A better notepad. Still simple. Still fast.
C#
24
star
18

rsproxy

A simple, command-line TCP/UDP proxy server
Rust
23
star
19

cryptostream

Read and Write stream adapters for on-the-fly encryption and decryption for rust
Rust
21
star
20

rsevents

Auto- and manual-reset events for rust
Rust
19
star
21

uuidxx

Cross-platform C++ library for parsing and generating UUIDs or GUIDs. Windows, Mac, Linux, and BSD.
C++
19
star
22

LastPassTo1Password

A LastPass to 1Password converter.
C#
16
star
23

rsevents-extra

Extra event types built on top of rsevents
Rust
16
star
24

gmake-proxy

A BSD make (bmake) proxy that invokes GNU make (gmake) instead
Makefile
15
star
25

EasyBCD-Localization

EasyBCD localization files for NLT. Help us translate EasyBCD into your language, it's easy and rewarding!
Shell
14
star
26

StreamCompare

A .NET library and nuget package for stream comparison.
C#
12
star
27

UrlPreview

A .NET library for loading URL previews.
C#
11
star
28

collections

A collection of various data structures and containers for .NET Standard
C#
11
star
29

zipcat

Pipe contents of zip file to stdout, supporting multiple files.
Rust
11
star
30

open

Like macOS `open` but for Windows
C++
11
star
31

web

A collection of helpful classes and functions for writing ASP.NET projects, for both MVC and forms. Save time and effort with this small and highly-performant "framework."
C#
10
star
32

Localization

The NeoSmart Localization and Regionalization Toolkit (NLRT).
C#
9
star
33

TaskThreads

A Task-based System.Threading.Threads implementation for Universal Windows Platform applications.
C#
9
star
34

CargoMake

A Makefile for Cargo-based rust projects
Makefile
8
star
35

UtfRedirect

Helper application used to host a guest exe, transparently proxying bi-directional stdin and stdout traffic, forcing the console input and output codepages to UTF8/6501 in order to support internationalized stdin/stdout on Windows.
Rust
8
star
36

pkg-graph

A graphviz generator for pkg, the FreeBSD package manager
Rust
8
star
37

ExtensionMethods

A collection of helpful .NET extension methods for desktop and web development
C#
6
star
38

paypalnvp-dotnet

Minimal and lightweight PayPal NVP library for C# and .NET
C#
5
star
39

paypalnvp-php

Minimal and lightweight PayPal NVP Library for PHP.
PHP
4
star
40

run

A simple, invisible application launcher for Windows
C++
4
star
41

xf86-video-scfb

FreeBSD syscons framebuffer Xorg driver
Shell
4
star
42

nettest

Tiny and bare-bones command-line utility to test for the presence of an internet connection on Windows.
Rust
4
star
43

ignore-result

Adds `.ignore()` to rust `Result` types
Rust
4
star
44

RedisDictionary

A redis-backed persistent dictionary for C#, .NET, and .NET Core
C#
3
star
45

UwpTaskbarIcon

Taskbar icon library for Windows 10
C#
3
star
46

linqplus

Additional LINQ methods optimized for specific data structures, functional LINQ methods, and more.
C#
3
star
47

textlist

Generate properly formatted and grammatically correct text lists from arrays and IEnumerables.
C#
2
star
48

hashing.net

NeoSmart hashing library for .NET
C#
2
star
49

synchronization

A collection of async-friendly synchronization objects for .NET and .NET Core
C#
2
star
50

Stream.CopyTo

A Stream.CopyTo() implementation for .NET 2.0, 3.0, and 3.5
C#
2
star
51

bandit.net

A multi-armed bandit A/B testing library for .NET platforms
C#
2
star
52

nst-ses

Community fork of Amazon's AWS SES (Simple Email Service) Perl framework. Contains important compatibility and integration improvements.
Perl
2
star
53

ms-sys

C
2
star
54

nst-log

A basic and cross-platform logging library for C++ applications. Features an easy to use singleton class, a simple API, convience wrappers, and very little overhead. For those looking for a logging library that doesn't overwhelm.
C++
2
star
55

showkeys

Rust
1
star
56

wp-prism

A githubesque syntax highlighting and code fencing plugin for WordPress.
PHP
1
star
57

relaunch

A simple, cross-platform, zero-configuration process monitor/relauncher
Rust
1
star
58

hivexx

C++ wrapper around the excellent hivex library, focused on safety and speed.
C++
1
star
59

UwpCache

An object cache for UWP applications.
C#
1
star