From e3d0b5e91857dc758f70dd232dae7339151d4847 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Tue, 9 Jan 2024 17:21:18 -0600 Subject: [PATCH] filter_plugins: Add decrypt Jinja2 filter The `decrypt` filter decrypts an ASCII-armored string encrypted with `age`. It simply pipes the string to `age -d -i age.key` and returns the contents of standard output. The path to the key file (passed with the `-i` argument) can be changed using the `key` keyword to the filter. Using `age`-encrypted data in this way has a few advantages over Ansible Vault. Different values can be encrypted with different keys, which Ansible Vault does support with vault IDs, but it is very cumbersome, almost to the point of being useless. Using multiple IDs requires explicitly specifying the IDs to use (thus knowing ahead of time which ones are needed) and storing each password in a separate file. With the `decrypt` filter, all the keys one has can be stored in a single file, and `age` will find the correct one. More importantly, though, the values remain encrypted until they are **explicitly** decrypted (e.g. when rendered in a template). Contrast with Vault, where values are **implicitly** decrypted any time they are used (including printing with `debug`, etc.), which could potentially lead inappropriate exposure. Finally, the `age` tooling is easier to work with and more composable than Ansible Vault, especially given that the latter literally _only_ works with Ansible. In the next series of commits, I will be converting all usage of Ansible Vault in inventory variables (i.e. those in `host_vars` and `group_vars`) to use `age` (or outright removing those that are no longer relevant). --- .gitignore | 2 ++ filter_plugins/age.py | 37 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 6 ++++++ 3 files changed, 45 insertions(+) create mode 100644 filter_plugins/age.py create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore index 2f1273a..dd26263 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ .fact-cache /victoria-metrics-*.tar.gz /victoria-metrics-*/ +__pycache__/ /tmp/ +/age.key diff --git a/filter_plugins/age.py b/filter_plugins/age.py new file mode 100644 index 0000000..413e389 --- /dev/null +++ b/filter_plugins/age.py @@ -0,0 +1,37 @@ +import os +import subprocess +from typing import Callable, Optional + +from ansible.errors import AnsibleError + + +class AgeError(AnsibleError): + pass + + +class FilterModule: + def filters(self) -> dict[str, Callable[..., str]]: + return { + 'decrypt': age_filter, + } + + +def age_filter(data: str, key: Optional[str] = None) -> str: + if key is None: + key = 'age.key' + key = os.path.expanduser(key) + p = subprocess.Popen( + ['age', '-d', '-i', key], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout, stderr = p.communicate(data.encode('utf-8')) + if p.returncode != 0: + error = ' '.join( + l + for l in stderr.decode('utf-8', errors='replace').splitlines() + if not l.startswith('age: report unexpected') + ) + raise AgeError(error) + return stdout.decode('utf-8') diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..82fbb1b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[tool.black] +line-length = 79 +skip-string-normalization = true + +[tool.isort] +lines_after_imports = 2