SecureStore 0.100: KISS, git-versioned secrets management for rust

A viable alternative to networked or environment variable secrets

See discussion on r/rust or on Hacker News

A few days ago, we published a new version of both the securestore library/crate and the ssclient CLI used to create, manage, and retrieve secrets from SecureStore vaults, an open and cross-language protocol for KISS secrets management. SecureStore vaults provide a more secure and far more reliable solution to storing secrets in environment variables and a simpler and less error prone alternative to network-based secrets management solutions, and make setting up development environments a breeze.

For some background, the SecureStore protocol (first published in 2017) is an open specification and cross-language library/frontend for securely storing encrypted secrets versioned in git, alongside your code. We have implementations available in rust (crate, cli) and for C#/.NET (api and cli, nuget) and the specification is purposely designed to be both easy-to-use and easy-to-port to other languages or frameworks.

This is the first update with (minor) breaking changes to the securestore public api, although pains have been taken to ensure that most common workflows won’t break. The changes are primarily to improve ergonomics when retrieving secrets from rust, and come with completely rewritten docs and READMEs (for the project, the lib, and the cli).

Storing Secrets in GIT

We’ve previously written at length about the many reasons for preferring the SecureStore approach of storing your secrets in a human-readable format, versioned in git alongside your code. At this point, it’s probably unnecessary for us to explain why hard-coding secrets in your code and committing them to your repo is a bad idea. Similarly, unless you are deploying code to a fleet of servers from a dozen different repositories to multiple, non-interchangeable servers distributed across time and space, chances are you don’t need and shouldn’t use a networked secrets management server, thereby increasing both operational and runtime complexity, adding dependencies on external services, and worrying about managing and securing yet another bit of infrastructure in-prod. And you shouldn’t be saving secrets as environment variables where they’re hard to manage and even harder to secure against runtime leaks: environment variables were never meant to stay secret.

The SecureStore protocol lets you conveniently store secrets to an encrypted vault, and verify that your secrets and the code using them remain in sync. You can require that code isn’t merged/committed without the accompanying secrets (for dev, staging, and/or production) it uses being available (without disclosing their contents). You can unify the how secrets are stored and retrieved in-dev and in-prod, and greatly reduce the complexity of setting up a dev environment (compare the complexity of working on a codebase that has a runtime dependency on a networked PostgreSQL db vs a local SQLite one). You can see and track down or revert changes to secrets the same way you can with code, using the same tools you already know and love. You can deploy new secrets as easily as pushing to master and letting your CI do its thing. The list goes on and on.

Using SecureStore from rust

While the securestore crate exposes everything you need to create a SecureStore vault, add and update secrets, and retrieve them at runtime; most users will want to use the ssclient frontend (built on the securestore crate) to create new SecureStore vaults or to add/remove secrets, and then use the securestore crate in their app only to retrieve secrets at runtime.

Creating a new SecureStore vault

Creating a new SecureStore vault and adding secrets is made incredibly easy with the ssclient frontends, available as a dependency-free single executable and also installable via cargo. In this example, we’ll create a new SecureStore vault that’s encrypted with a password but can also be decrypted headlessly at runtime with an exported keyfile:

> cargo install ssclient
> ssclient create secrets.json --export-key secrets.key
Password: ********
Confirm password: ********
> ssclient set db:username pgsql
Password: ********
> ssclient set db:password
Value: pgsql123
Password: ********

We created a new SecureStore vault, stored as a JSON-encoded, human-readable text file called secrets.json, secured it with a password, and exported a key for headless use (secrets.key). We then added a not-so-secret db:username and a secret named db:password, securely stored with their values.

We’ll commit secrets.json to our git repository (ideally we’d commit the updated secrets.json containing the encrypted db:username and db:password secrets at the same time as we commit the code that retrieves these secrets at runtime). The secrets.key file is extremely sensitive and is never committed to git (ssclient helpfully adds a .gitignore rule for us to make sure that we never commit it by accident).

Retrieving secrets at runtime

In our rust app, we’ll add a dependency on the securestore crate to Cargo.toml and then add some code to load the SecureStore vault and retrieve the secret so we can connect to the postgres instance:

Cargo.toml:

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

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

This example also uses the once_cell crate to initialize a securestore::SecretsManager singleton, but you can use whatever approach you want to abstract the access to the vault (especially if we’re only going to be reading secrets rather than updating them).

In our src/main.rs, we’ll add some code to instantiate a SecretsManager singleton and then to interact with that singleton and retrieve the database credentials:

use securestore::SecretsManager;
use once_cell::sync::Lazy;

static SECRETS: Lazy = Lazy::new(|| {
    SecretsManager::load("secrets.json", "secrets.key")
        .expect("Failed to load SecureStore vault!")
});

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

fn main() {
    let credentials = get_db_credentials().unwrap();
    assert_eq!(credentials.0, String::from("pgsql"));
    assert_eq!(credentials.1, String::from("pgsql123"));

    // TODO: Actually connect to the database with these credentials
}

You can refer to the project README for a more complete and annotated example of using SecureStore from rust, how to separate dev, staging, and production secrets, what the encrypted secrets.json vault looks like and contains, and more.

Notable changes in securestore and ssclient 0.100

A complete changelog for this release is available on GitHub, but for our existing securestore users, here’s a rundown on what’s new and improved in this release and what you should look out for when migrating your existing code, in no particular order:

  • As mentioned, all the documentation has been overhauled extensively. This includes the command line help for ssclient and the crate docs for securestore, as well as all the accompanying README files and more.
  • SecretsManager::new() no longer takes a filename, you specify that out-of-band via the new SecretsManager::save_as() (the old save() function is still there).
  • SecretsManager::get() is no longer generic; the old, generic function is still there, but renamed to SecretsManager::get_as::<T>(). This lets you omit the return type for the 99.99% of the cases where you have a String secret.
  • There are some quality-of-life improvements to the BinarySerializable and BinaryDeserializable traits for all two of you storing arbitrary types as SecureStore secrets.
  • ssclient is now git-aware and excludes exported or generated key files in .gitignore by default (support for other VCS is coming; pull requests are welcome).
  • ssclient can now decrypt binary secrets and represent them as text (encoded as base64).
  • There is a new PEM-like keyfile format, generated by default by new versions of securestore and ssclient. The old binary format is still (and will remain) supported. Older versions of securestore cannot read the new ASCII-armored key format.
  • You should use KeySource::from_file(..) instead of KeySource::File(..) (usage is the same). KeySource::File(..) may be deprecated in the future (it’s no longer an actual enum variant but rather a function masquerading as an enum variant for backwards compatibility). Refer to the changelog to understand why.

If you would like to receive a notification the next time we release a rust library, publish a crate, or post some rust-related developer articles, you can subscribe below. Note that you'll only get notifications relevant to rust programming and development by NeoSmart Technologies. If you want to receive email updates for all NeoSmart Technologies posts and releases, please sign up in the sidebar to the right instead.

Leave a Reply

Your email address will not be published.