Introduction

rops aims to make the process of handling sensitive credentials a bit more refined than simply encrypting an entire config file. It does this by allowing the encryption of sensitive values only, whilst making sure that other parts can’t be changed with malicious intent. One example use case is the remote hosting of version controlled rops-files with meaningful diff outputs. The SOPS project, (which rops can be seen as Rust rewrite of), contains an excellent motivation section in its README for those interested in further reading materials.

The goals chapter of this book points out some expected differences between these two projects, and the concepts chapter takes a deeper dive into how rops operates. Those two chapters should serve both library and CLI users well. Remaining chapters primarily target the latter group. Library usage instructions are instead located at docs.rs.

rops the library, the CLI application, and its documentation is free and open source under the Mozilla Public License Version 2.0. The code can be found on GitHub where issues and feature requests may also be posted. Please follow the project’s Security Policy if you encounter any vulnerability. All contributions are warmly welcomed, but make sure to read the Contributing Guidelines before opening a pull request. Feel free, however, to simply use the “Suggest an edit” button in the top right corner if you encounter any documentation errors along the way 😊

Goals and Non-Goals

Goals

  • Full sops encrypted file compatibility. Decrypt any sops supported format using rops and vice versa.
  • Be consistent in how credentials are used, set and retrieved across integrations.
  • Disincentivize unsecure operations.
  • Support standard input (stdin) as an alternative to file paths whenever possible.
  • Available as a rust library.
  • Support a wide variety of file formats and integrations.
File formatsIntegrations
✅ YAMLage - Asymmetric
✅ JSONaws_kms - Symmetric 1
✅ TOML 2pgp 3
❎ INIgcp_kms
❎ ENVazure_kv
❎ BINARYhashicorp_kv

The difference between asymmetric and symmetric integrations is later explained in the concepts chapter.

1

AWS KMS effectively becomes a symmetric encryption scheme when it requires private credentials to a remote encryption service, even if that service uses asymmetric encryption internally.

2

Currently exclusive to rops.

3

Awaiting status update for: OpenPGP Crypto Refresh.

Non-Goals

Preliminary Non-Goals

This list includes a collection of SOPS features which are currently not under consideration to be included in rops. Nothing here is set in stone, so feel free to open up an issue if there’s anything you don’t agree with 🙂

  • The --output flag: Use rops decrypt > FILE_NAME instead?

  • Partial retrieval: Use rops decrypt FILE | jq instead?

  • Partial modification: Use rops edit or rops decrypt FILE | jq map | rops encrypt --format FORMAT instead? This will unfortunately skip initialization vector reuse of unchanged values.

  • The --ignore-mac flag: Deemed too insecure. rops files are instead encouraged to be placed under and then recovered with version control systems such as git.

  • Manual key rotation (--rotate/-r): rops will automatically rotate the secret data key upon integration key id removal.

  • Integrated formatting configuration: Might be better achieved by piping output through more powerful formatters.

  • Integrated secrets publishing: This too might be better handled externally.

  • rops as a remote key service: Possibly as a separate crate+binary conforming to KMIP 2.1 or higher.

  • Access logging: Better handled by the respective integrations for now. Might become relevant to include in the remote key service.

Currently missing features

  • Sub-process secret passing.
  • Key groups.
  • Storing file comments.
  • Compute an additional MAC over active integration keys to prevent against manual removal without rotating the secret data key. (Currently not done by SOPS either.)
  • Specify keys by --key-file INTEGRATION PATH flag.
  • Show decrypted metadata with --show-metadata/-s. (Note that directly modifying the metadata will most likely break its integrity and prevent future decryption.)
  • Integration sub-features such as AWS Profiles, Roles and Context.

Concepts

A rops file is first created by encrypting a plaintext map (collection of key-value pairs) of a specified file format. The resulting rops file will then contain the encrypted version of the map, followed by some rops file metadata.

Only the value part of any key-value pair is encrypted. This is done using an authenticated encryption with additional data scheme. The secret key is a randomly generated 32 byte array called the data key, and the additional data is a concatenated path for key in question. Key paths as additional data is how unlawful key name changes and some forms of reorderings are protected against.

Integrations

One or multiple integrations encrypt the data key in their own way once all values have been encrypted. The use of multiple integrations and key pairs within each integration add the necessary redundancy in case one integration private key is lost, or simply not present.

It is also how access to a rops file may be revoked without needing access to/knowledge of the file itself. To do so one would use an identity and access management (IAM) service in combination with a key management service (KMS) as the integration. If those services are used to encrypt and decrypt the data key, then access to the rops file may be revoked by removing the users’ ability to access the KMS.

Integration Key ID:

Integration key IDs used to encrypt the data key are all stored inside the rops file metadata section. The key IDs also serve as a way to identify which private key is to be used when things need to be decrypted it again. If the integration relies on an asymmetric encryption scheme, then only the integration key ID is needed when the rops file is being created, symmetric integration require on the other hand both key id and private key up front.

The key IDs conform to an integration specific syntax/format, and should not be any different as a function of where it is used.

IntegrationSyntaxExample
age<age_recipient>age1se5ghfycr4n8kcwc3qwf234ymvmr2lex2a99wh8gpfx97glwt9hqch4569
aws_kms<profile>.<aws_key_arn>default.arn:aws:kms:eu-north-1:822284028627:key/029dba6d-60de-4364-ac5c-cbdd284acd0a

Private Integration Key:

Only one private integration key that can decrypt data key needs to be found for the data key to then decrypt the entire rops file map. 1 Private keys are not stored in the metadata section, but instead retrieved by iterating over the stored integration key IDs. The strategy for where to look for a private key goes as follows:

  1. In the environment variables.
  2. In the rops key files.
  3. (Future) In the default integration key files.

Private integration keys follow—just like the respective key IDs—a syntax/form that is unchanged regardless of context.

IntegrationSyntaxExample
age<age_secret_key>AGE-SECRET-KEY-1CZG0RPQJNDZWZMRMJLNYSF6H00WK0ECYAVE83ALFC2KE53WJ2FRSNZ8GC
aws_kms<profile>.<aws_access_key_id>.<aws_secret_access_key>default.AKIAXXXXXXXXXXXXXXL2.BRZXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXigu
1

Unless the unimplemented key group future is.

To supply private keys by environment variables

Syntax is ROPS_<INTEGRATION>='key1,key2'. For example:

export ROPS_AWS_KMS='default.AKIAXXXXXXXXXXXXXXL2.BRZXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXigu'
To supply private keys by rops key files

Private keys can also be read from files, each containing a new-line separated list of private keys. They reside by default in <local_config_dir>/rops/<integration>_keys.

<local_config_dir> varies by platform and defaults to:

PlatformValueExample
Linux$XDG_CONFIG_HOME or $HOME/.config/home/alice/.config
macOS$HOME/Library/Application Support/Users/Alice/Library/Application Support
Windows{FOLDERID_LocalAppData}C:\Users\Alice\AppData\Local

Linux users wishing to use the Age integration could for example save a file in $HOME/.config/rops/age_keys containing:

AGE-SECRET-KEY-1VR0S4...KD8D
AGE-SECRET-KEY-1GQ6XJ...DZ5W

(As opposed to setting ROPS_AGE=AGE-SECRET-KEY-1VR0S4...KD8D,AGE-SECRET-KEY-1GQ6XJ...DZ5W.)

The rops key file location can be overridden by setting a ROPS_<INTEGRATION>_KEY_FILE=<path> environment variable. ROPS_AGE_KEY_FILE=/tmp/temp_age_keys for instance.

To supply private keys using integration key files (Future)

Many integrations already store their keys in a dedicated location but in wildly different structures. rops does currently not parse these files, but it aims to so in the future:

IntegrationWindowsMacOSLinuxFallbackFormat
ageN/AN/AN/AN/AN/A
aws_kms%UserProfile%\.aws\credentials$HOME/.aws/credentials$HOME/.aws/credentialsAWS_SHARED_CREDENTIALS_FILEReference

Private key rotation

Compromised private keys can always be removed from a rops file. Such removals will automatically create a new data key used to re-encrypt all values. Other integration must be able to encrypt the new data key for storage when this happens. Or in other words; symmetric integration require the presence of their respective private keys during the removal of other private keys.

Partial Encryption

All keys are encrypted by default, unless one of the encrypted_suffix, encrypted_regex, unencrypted_suffix or unencrypted_regex settings is present in the metadata:

VariantEncrypt by defaultMatched value
encrypted_{suffix,regex}NoIs encrypted
unncrypted_{suffix,regex}YesEscapes encryption

Note that any matched key “locks” the triggered encryption config for all descendant key-value pairs. If the metadata contains for instance encrypted_suffix: "_encrypted", then the values for i and ii become encrypted as shown below:

foo: bar
nested_encrypted:
  a:
      i: encrypted
  b:
      ii: encrypted

Compute MAC for encrypted values only

Unauthenticated plaintext value changes in a partially encrypted rops files will still cause subsequent decryption attempts to fail. This is because all values are hashed into a message authentication code (MAC). The calculation happens before any encryption, and values are read in the other they appear. MAC verification at the decryption stage will, as such, deny any unauthenticated addition, removal or reordering of unique values. (Recall how concatenated key paths as additional encryption data prevents the other class of unauthenticated reordering; equal values but with different key names).

The mac_only_encrypted metadata setting can be enabled to lift this limitation, that is; enable the direct changes to plaintext values without causing MAC mismatch errors when attempting to decrypt the rest.

Installation

From crates.io

# (binary is still named rops)
cargo install rops-cli

If is binary not found in path

cargo install usually places binaries in its user level binary directory, default is usually being $HOME/.cargo/bin on Unix systems. Make sure that it’s in your $PATH by appending the following to your shell startup scripts, if applicable:

export PATH="$HOME/.cargo/bin:$PATH"

Or for those that have manually set $CARGO_HOME:

# export CARGO_HOME="$XDG_DATA_HOME"/cargo
export PATH="$CARGO_HOME/bin:$PATH"

CLI Reference

Usage: rops <COMMAND>

Commands:
  encrypt  Encrypt plaintext maps [aliases: e]
  decrypt  Decrypt rops files [aliases: d]
  edit     Edit an encrypted rops file using $EDITOR. (Fallbacks to vim then nano and lastly vi.) Outputs to stdout if input is piped
  keys     Manage encrypted rops file keys [aliases: k]
  refresh  Make a config the single source of configuration truth for an encrypted rops file
  help     Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help

Encrypt subcommand

Encrypt plaintext maps

Usage: rops encrypt [OPTIONS] [FILE]

Arguments:
  [FILE]  Input may alternatively be supplied through stdin

Options:
  -c, --config <PATH>                Read config from provided path
  -i, --in-place                     Encrypt file in place rather than printing the result to stdout
      --age <AGE>                    Space separated list of public age keys
      --aws-kms <AWS_KMS>            Space separated list of AWS KMS rops key id strings
      --encrypted-regex <REGEX>      Encrypt values matching key regex
      --encrypted-suffix <STRING>    Encrypt values matching key suffix
      --unencrypted-regex <REGEX>    Skip encrypting values matching key regex
      --unencrypted-suffix <STRING>  Skip encrypting values matching key suffix
      --mac-only-encrypted           Requires a partial encryption setting
  -f, --format <FORMAT>              Required if no file argument is found to infer by extension [possible values: yaml, json, toml]
  -h, --help                         Print help

Decrypt subcommand

Decrypt rops files

Usage: rops decrypt [OPTIONS] [FILE]

Arguments:
  [FILE]  Input may alternatively be supplied through stdin

Options:
  -c, --config <PATH>    Read config from provided path
  -i, --in-place         Decrypt file in place rather than printing the result to stdout, metadata excluded
  -f, --format <FORMAT>  Required if no file argument is found to infer by extension [possible values: yaml, json, toml]
  -h, --help             Print help

Edit subcommand

Edit an encrypted rops file using $EDITOR. (Fallbacks to vim then nano and lastly vi.) Outputs to stdout if input is piped

Usage: rops edit [OPTIONS] [FILE]

Arguments:
  [FILE]  Input may alternatively be supplied through stdin

Options:
  -c, --config <PATH>    Read config from provided path
  -f, --format <FORMAT>  Required if no file argument is found to infer by extension [possible values: yaml, json, toml]
  -h, --help             Print help

Keys subcommand

Manage encrypted rops file keys

Usage: rops keys <COMMAND>

Commands:
  add     Add integration key IDs to an encrypted rops file
  remove  Remove intregration key IDs of an encrypted rops file and rotate the data key
  help    Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help

Refresh subcommand

Make a config the single source of configuration truth for an encrypted rops file

Usage: rops refresh [OPTIONS] [FILE]

Arguments:
  [FILE]  Input may alternatively be supplied through stdin

Options:
  -c, --config <PATH>    Read config from provided path
  -i, --in-place         Refresh the file in place rather than printing the result to stdout
  -f, --format <FORMAT>  Required if no file argument is found to infer by extension [possible values: yaml, json, toml]
  -h, --help             Print help

CLI Configuration

A CLI config file can be used with so-called creation rules. These rules allow metadata settings to be provided “automatically”, opposed repetitively including them as CLI flags.

Each creation rule has a path regex that file names are matched against. They’re read in the order they appear, and the first match of any lookup cancels the next. (So there’s no concept of creation rule merging.)

Stdin content can be matched with a creation rule that captures the pseudo file name of an empty string. The .* path regex can therefore be used as a general fallback for both input sources.

rops uses the following config file look up strategy:

  1. --config/-c <FILE> flag.
  2. File path set by the environment variable $ROPS_CONFIG.
  3. File of the name .rops.toml found in the current working directory or any ancestor thereof.
# [[creation_rules]]
# path_regex = "<REGEX>"
# # Optional: Defaults to false if not set.
# mac_only_encrypted = false
# # Optional: Defaults to nothing unless set.
# # Variant may be one of:
# # - encrypted_suffix
# # - encrypted_regex
# # - unencrypted_suffix
# # - unencrypted_regex
# partial_encryption.<variant> = ""
# # Opional: The arrays of key IDs for each integration
# # are also optional and default to being empty.
# [creation_rules.integration_keys]
# age = ["<Age Key ID>"]
# aws_kms = ["<AWS KMS KeyID>"]

# Example:
[[creation_rules]]
path_regex = "testing/**"
mac_only_encrypted = true
partial_encryption.unencrypted_regex = "config"
integration_keys.age = [
  "age1se5ghfycr4n8kcwc3qwf234ymvmr2lex2a99wh8gpfx97glwt9hqch4569",
]

[[creation_rules]]
path_regex = "production/**"
mac_only_encrypted = false
partial_encryption.unencrypted_regex = "config"
integration_keys.age = [
  "age1qazf43xll4ramx3wcn7h2yl9scycxdhrwge8862vv6zj97pafdvq0d5mn6",
]

[[creation_rules]]
path_regex = ".*"
partial_encryption.encrypted_regex = "pass|token"