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).
no-vault-in-inventory
Dustin 2024-01-09 17:21:18 -06:00
parent 60fa380e5d
commit e3d0b5e918
3 changed files with 45 additions and 0 deletions

2
.gitignore vendored
View File

@ -2,4 +2,6 @@
.fact-cache .fact-cache
/victoria-metrics-*.tar.gz /victoria-metrics-*.tar.gz
/victoria-metrics-*/ /victoria-metrics-*/
__pycache__/
/tmp/ /tmp/
/age.key

37
filter_plugins/age.py Normal file
View File

@ -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')

6
pyproject.toml Normal file
View File

@ -0,0 +1,6 @@
[tool.black]
line-length = 79
skip-string-normalization = true
[tool.isort]
lines_after_imports = 2