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 anysops
supported format usingrops
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 formats | Integrations |
---|---|
✅ YAML | ✅ age - Asymmetric |
✅ JSON | ✅ aws_kms - Symmetric 1 |
✅ TOML 2 | ❎pgp 3 |
❎ INI | ❎gcp_kms |
❎ ENV | ❎azure_kv |
❎ BINARY | ❎hashicorp_kv |
The difference between asymmetric and symmetric integrations is later explained in the concepts chapter.
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.
Currently exclusive to rops
.
Awaiting status update for: OpenPGP Crypto Refresh.
Non-Goals
- Identical CLI to
sops
with full feature parity, see preliminary 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: Userops decrypt > FILE_NAME
instead? -
Partial retrieval: Use
rops decrypt FILE | jq
instead? -
Partial modification: Use
rops edit
orrops 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.
Integration | Syntax | Example |
---|---|---|
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:
- In the environment variables.
- In the
rops
key files. - (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.
Integration | Syntax | Example |
---|---|---|
age | <age_secret_key> | AGE-SECRET-KEY-1CZG0RPQJNDZWZMRMJLNYSF6H00WK0ECYAVE83ALFC2KE53WJ2FRSNZ8GC |
aws_kms | <profile>.<aws_access_key_id>.<aws_secret_access_key> | default.AKIAXXXXXXXXXXXXXXL2.BRZXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXigu |
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:
Platform | Value | Example |
---|---|---|
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:
Integration | Windows | MacOS | Linux | Fallback | Format |
---|---|---|---|---|---|
age | N/A | N/A | N/A | N/A | N/A |
aws_kms | %UserProfile%\.aws\credentials | $HOME/.aws/credentials | $HOME/.aws/credentials | AWS_SHARED_CREDENTIALS_FILE | Reference |
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:
Variant | Encrypt by default | Matched value |
---|---|---|
encrypted_{suffix,regex} | No | Is encrypted |
unncrypted_{suffix,regex} | Yes | Escapes 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:
--config/-c <FILE>
flag.- File path set by the environment variable
$ROPS_CONFIG
. - 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"