diff --git a/examples/all_in_one/.gitignore b/examples/all_in_one/.gitignore new file mode 100644 index 000000000..a6cf004c4 --- /dev/null +++ b/examples/all_in_one/.gitignore @@ -0,0 +1 @@ +config/secret.json diff --git a/examples/all_in_one/README.rst b/examples/all_in_one/README.rst new file mode 100644 index 000000000..d6b66343b --- /dev/null +++ b/examples/all_in_one/README.rst @@ -0,0 +1,81 @@ +All-in-one example +================== +This directory is an example configuration for SOPS inside of a project. We will cover the files used and relevant scripts for developers. + +This example is optimized for saving developer time by storing all secrets in a single file (e.g. ``secret.enc.json``). + +One downside is any configurations which should be stored side by side might not be. + +Getting started +--------------- +To use this example, run the following: + +.. code:: bash + + # From the `sops` root directory + # Import the test key + gpg --import tests/sops_functional_tests_key.asc + + # Navigate to our example directory + cd examples/all_in_one + + # Decrypt our secrets + bin/decrypt-config.sh + + # Optionally edit a secret + # bin/edit-secret.sh config/secret.enc.json + + # Run a script that uses our decrypted secrets + python main.py + +Storage +------- +In both development and production, we will be storing the secrets file unencrypted on disk. This is for a few reasons: + +- Can't store file in an encrypted manner because we would need to know the secret to decode it +- Loading it into memory at boot is impractical + + - Requires reimplementing SOPS' decryption logic to multiple languages which increases chance of human error which is bad for security + - If someone uses an automatic process reloader during development, then it could get expensive with AWS + + - We could cache the results from AWS but those secrets would wind up being stored on disk + +As peace of mind, think about this: + +- Unencrypted on disk is fine because if the attacker ever gains access to the server, then they can run ``sops --decrypt`` as well. + +Files +----- +- ``bin/decrypt-config.sh`` - Script to decrypt secret file +- ``bin/edit-config-file.sh`` - Script to edit a secret file and then decrypt it +- ``config/secret.enc.json`` - Catch-all file containing our secrets +- ``config/secret.json`` - Decrypted catch-all secrets file +- ``config/static.py`` - Configuration file which imports secrets +- ``.gitignore`` - Ignore file for decrypted secret file +- ``main.py`` - Example script + +Usage +----- +Development +~~~~~~~~~~~ +For development, each developer must have access to the PGP/KMS keys. This means: + +- If we are using PGP, then each developer must have the private key installed on their local machine +- If we are using KMS, then each developer must have AWS access to the appropriate key + +Testing +~~~~~~~ +For testing in a public CI, we can copy ``secret.enc.json`` to ``secret.json``. This will represent the same structure as ``secret.enc.json`` with an additional ``sops`` key but not reveal any secret information. + +.. + + For convenience, we can run ``CONFIG_COPY_ONLY=TRUE bin/decrypt-config.sh`` which will use ``cp`` rather than ``sops --decrypt``. + +For testing in a private CI where we need private information, see the `Production instructions <#production>`_. + +Production +~~~~~~~~~~ +For production, we have a few options: + +- Build an archive (e.g. ``.tar.gz``) in a private CI which contains the secrets and deploy our service via the archive +- Install PGP private key/KMS credentials on production machine, decrypt secrets during deployment process on production machine diff --git a/examples/all_in_one/bin/decrypt-config.sh b/examples/all_in_one/bin/decrypt-config.sh new file mode 100755 index 000000000..e53965bec --- /dev/null +++ b/examples/all_in_one/bin/decrypt-config.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Exit on first error +set -e + +# Define our secret files +secret_files="secret.enc.json" + +# For each of our files in our encrypted config +for file in $secret_files; do + # Determine src and target for our file + src_file="config/$file" + target_file="$(echo "config/$file" | sed -E "s/.enc.json/.json/")" + + # If we only want to copy, then perform a copy + # DEV: We allow `CONFIG_COPY_ONLY` to handle tests in Travis CI + if test "$CONFIG_COPY_ONLY" = "TRUE"; then + cp "$src_file" "$target_file" + # Otherwise, decrypt it + else + sops --decrypt "$src_file" > "$target_file" + fi +done diff --git a/examples/all_in_one/bin/edit-config-file.sh b/examples/all_in_one/bin/edit-config-file.sh new file mode 100755 index 000000000..32970f557 --- /dev/null +++ b/examples/all_in_one/bin/edit-config-file.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Exit on first error +set -e + +# Define our secret files +secret_files="secret.enc.json" + +# Look up our file +filepath="$1" +if test "$filepath" = ""; then + echo "Expected \`filepath\` but received nothing" 1>&2 + echo "Usage: $0 " 1>&2 + exit 1 +fi + +# If our file is a secret +filename="$(basename "$filepath")" +if echo "$secret_files" | grep "$filename"; then + # Load it into SOPS and run our sync script + sops "$filepath" + bin/decrypt-config.sh +# Otherwise (it's a normal file) +else + # Resolve our editor via `sops` logic + editor="$EDITOR" + if test "$editor" = ""; then + editor="$(which vim nano | head -n 1)" + fi + if test "$editor" = ""; then + echo "Expected \`EDITOR\` environment variable to be defined but it was not" 1>&2 + exit 1 + fi + + # Edit our file + "$editor" "$filepath" +fi diff --git a/examples/all_in_one/config/__init__.py b/examples/all_in_one/config/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/all_in_one/config/secret.enc.json b/examples/all_in_one/config/secret.enc.json new file mode 100644 index 000000000..f400b1eaf --- /dev/null +++ b/examples/all_in_one/config/secret.enc.json @@ -0,0 +1,22 @@ +{ + "github_oauth_token": "ENC[AES256_GCM,data:B2/q7WKJRqAUf4vvh8o=,iv:WpyxvYMHVMz4UvX2xZf79jaQYTSdepF96k87nMcuEro=,tag:FIqBSEuhh14JyjkGlW4hvw==,type:str]", + "sops": { + "lastmodified": "2016-02-12T05:14:20Z", + "attention": "This section contains key material that should only be modified with extra care. See `sops -h`.", + "unencrypted_suffix": "_unencrypted", + "mac": "ENC[AES256_GCM,data:In6U2fZvaX2JI9jSnNYwWEYAuyh5YPUwCaAV5qeWON47zxxy4f2ad9gNwqxS4WsA7jf/DsYXv73OyXTqWV0sb8CBtzyVjzm9tgIcwNkKncROXY2njT+Y1LZAdIDPnz1Rw9PHOfZwHM8xxlly78uK/TuzX0nXADBtkIAAksVa3xw=,iv:/GjlxMSNouhw8yqsAG+bfGIz+YWz+/LNfrOHh/LC4OU=,tag:/5I+0uEWQ3hIL9ztuPQZwA==,type:str]", + "version": 1.6, + "kms": [ + { + "arn": "" + } + ], + "pgp": [ + { + "fp": "1022470DE3F0BC54BC6AB62DE05550BC07FB1A0A", + "created_at": "2016-02-12T05:14:20Z", + "enc": "-----BEGIN PGP MESSAGE-----\nVersion: GnuPG v1\n\nhIwDEEVDpnzXnMABA/9hQVFyHXnNCnE0YIcXvar4YtFyRNnuV5zTaQskRqPYS14z\nPkqUpx03PVsT4c84YLx3bAu9OM0so8fsXAW7+YpX5A1ZChWpy0Qt7lg6k/4qyUSl\nMO5x+4ZdU5C866no0Q3UHrBy0oxORYUwbKsWU8IMSNWuSXGcqRsU0nyschvhb9Je\nARNsbbGsL2qeaYqjTwD3p3awkef2voVwYGTuSbvKcEfb5X1JrWrX1Igk8wwCe/uw\nlB2SXpk1bQ16ZfsT39+bOaeu6v8mREHG+KKk3k7ddFmML5TbPYqSme7BcW1IQA==\n=c6L/\n-----END PGP MESSAGE-----\n" + } + ] + } +} \ No newline at end of file diff --git a/examples/all_in_one/config/static.py b/examples/all_in_one/config/static.py new file mode 100644 index 000000000..0acb8e5c3 --- /dev/null +++ b/examples/all_in_one/config/static.py @@ -0,0 +1,12 @@ +# Load in our dependencies +import json + +# Load in our secrets +with open('config/secret.json', 'r') as file: + secret = json.loads(file.read()) + +# Define our configuration +common = { + 'github_oauth_token': secret['github_oauth_token'], + 'port': 8080, +} diff --git a/examples/all_in_one/main.py b/examples/all_in_one/main.py new file mode 100644 index 000000000..0b275f19e --- /dev/null +++ b/examples/all_in_one/main.py @@ -0,0 +1,18 @@ +# Load in our dependencies +from __future__ import absolute_import +from config.static import common + + +# Define our main function +def main(): + # Output our configuration + print('Configuration') + print('-------------') + for key in common: + # Example: `port: "8080"` + print('{key}: "{val}"'.format(key=key, val=common[key])) + + +# If this script is being invoked directly, then run our main function +if __name__ == '__main__': + main()