Or: how to use /bin/true as a hinge of sorts
I was recently tasked with adding a zero-knowledge secrets module for our Puppet environment, using age and mustache. At first glance this seemed like a simple task, just decrypt the secrets and use mustache to drop them into the target files
This works great for single-secret files, but multi-secret files turned out to be a bit of a problem:
$ cat demo.pp
# core::age
#
# define: core::age
# this class takes no parameters, it only installs basic requirements to use core::age::decrypt
#
# if your {secrets,templates} are in a subdir of /home/user/.age/{secrets,templates} then you must define that subdir on your own
#
# README in files/age/README.md
class core::age::demo {
$tmpdir = '/tmp/age'
['age', 'ruby-mustache',].each | $package | {
case $::kernel {
'Darwin': {
$pkg = $package ? {
'ruby-mustache' => 'mustache',
default => $package,
}
$provider = $package ? {
'ruby-mustache' => 'gem',
default => 'brew',
}
$ensure = 'present'
}
default: {
$pkg = $package
$provider = 'apt'
$ensure = $package ? {
# require age >=1.0.0 https://github.com/sthagen/FiloSottile-age/pull/22/files
'age' => '>=1.0.0-1~bpo11+1',
default => 'present',
}
}
}
package {$pkg:
ensure => $ensure,
provider => $provider,
}
}
file {
default:
ensure => directory,
owner => 'user',
group => 'user',
mode => '0700',
# don't allow unknown files in these dirs, they're old secrets
recurse => true,
purge => true,
force => true,
;
"/home/user/.age":
recurse => remote,
source => 'puppet:///modules/core/age',
;
"/home/user/.age/config":
# this is where the host keys live, puppet doesn't manage them
purge => false,
recurse => false,
;
$tmpdir:
owner => root,
group => roo,
}
}
$ cat decryptdemo.pp
define core::age::decryptdemo (
Hash $secrets,
String $template,
Enum['present', 'absent'] $ensure = present,
String $target = $name,
Enum[
'cluster',
'host'
] $key_type = 'cluster',
String $template_source = "puppet:///modules/core/age/templates/${template}",
Boolean $plant_template = true,
String $mode = '0600',
Optional[String] $owner = undef,
Optional[String] $group = undef,
) {
include core::age::demo
$random = generate('/bin/bash', '-c', 'tr -dc A-Za-z0-9 '/usr/bin/true',
default => '/bin/true',
}
$false = $::kernel ? {
'Darwin' => '/usr/bin/false',
default => '/bin/false',
}
# fail if can't find the key, run max 1x per key type
if ! defined(Exec["ensure ${key_type} key"]) {
exec {"ensure ${key_type} key":
command => $false,
unless => "test -f ${keypath}",
refreshonly => true,
}
}
if $plant_template {
if ! defined (File[$template_path]) {
file {$template_path:
ensure => core::bool2ensure($plant_template),
owner => 'user',
group => 'user',
mode => '0600',
source => $template_source,
}
}
}
if $ensure == 'present' {
$secrets.each | $reference, $secret | {
$secret_path = "/home/user/.age/secrets/${secret}.age"
file {$secret_path:
owner => 'user',
group => 'user',
mode => '0600',
source => "puppet:///modules/core/age/secrets/${secret}.age",
notify => Exec["create_${target}",],
}
~> exec {"fail_if_cannot_decrypt_${secret}":
# errors get hidden in decrypt_${secret} by the $(command substitution)
command => "age --decrypt -i ${keypath} ${secret_path} > /dev/null",
path => "/usr/local/bin:/bin:/usr/bin",
subscribe => File[$secret_path,],
logoutput => false,
refreshonly => true,
}
~> exec{"decrypt_${secret}":
command => "echo ${reference}: \"$(age --decrypt -i ${keypath} ${secret_path})\" >> ${tmpfile}",
path => "/usr/local/bin:/bin:/usr/bin",
subscribe => Exec["fail_if_cannot_decrypt_${secret}",],
notify => Exec["create_${target}",],
require => [File[$secret_path,],],
refreshonly => true,
}
}
exec {"create_${target}":
command => "mustache ${tmpfile} ${template_path} >| ${target}",
logoutput => false,
path => "/usr/local/bin:/bin:/usr/bin",
# require $target bc otherwise puppet thinks the file doesn't exist
# (bc it doesn't at the start of the first puppet run) and overwrites it blank
require => [File[$template_path, $target,],],
refreshonly => true,
}
}
file {$target:
# file resource is only here to apply ownership & perms
ensure => $ensure,
owner => $owner,
group => $group,
mode => $mode,
}
}
$ cat ../../files/age/templates/demo_test
this is a test file
the secret is: {{{ demo_test }}}
the secret is above
$ cat ../../../../manifests/site.pp
node /^demo[0-9].demo.net$/
{
core::age::decryptdemo {'/home/user/demo_test':
ensure => present,
secrets => {
'demo_test' => 'demo_test',
},
template => 'demo_test',
}
}
$ echo test | age -a -R secrets/cluster/demo.pub -o secrets/puppet/demo_test.age
user@demo1:~$ sudo puppet agent -t --environment demo
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/File[/home/user/.age/secrets/demo_test.age]/ensure: defined content as '{sha256}579c545e1cbea83fe5dc5d69c36e2cbe9ed43ac12b3886202b16386e4441ac44'
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/File[/home/user/.age/secrets/demo_test.age]: Scheduling refresh of Exec[create_/home/user/demo_test]
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/File[/home/user/.age/secrets/demo_test.age]: Scheduling refresh of Exec[fail_if_cannot_decrypt_demo_test]
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/File[/home/user/.age/secrets/demo_test.age]: Scheduling refresh of Exec[fail_if_cannot_decrypt_demo_test]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/Exec[fail_if_cannot_decrypt_demo_test]: Triggered 'refresh' from 2 events
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/Exec[fail_if_cannot_decrypt_demo_test]: Scheduling refresh of Exec[decrypt_demo_test]
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/Exec[fail_if_cannot_decrypt_demo_test]: Scheduling refresh of Exec[decrypt_demo_test]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/Exec[decrypt_demo_test]: Triggered 'refresh' from 2 events
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/Exec[decrypt_demo_test]: Scheduling refresh of Exec[create_/home/user/demo_test]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/Exec[create_/home/user/demo_test]: Triggered 'refresh' from 2 events
Notice: Applied catalog in 9.65 seconds
user@demo1:~$ cat /home/user/demo_test
this is a test file
the secret is: test
the secret is above
This looks great! But things get tricky if you add a second secret, or change one secret but not the other:
$ echo test2 | age -a -R secrets/cluster/demo.pub -o secrets/puppet/demo_test2.age
$ cat ../../../../manifests/site.pp
node /^demo[0-9].demo.net$/
{
core::age::decryptdemo {'/home/user/demo_test':
ensure => present,
secrets => {
'demo_test' => 'demo_test',
'demo_test2' => 'demo_test2',
},
template => 'demo_test',
}
}
user@demo1:~$ sudo puppet agent -t --environment demo
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/File[/home/user/.age/templates/demo_test]/content:
--- /home/user/.age/templates/demo_test 2022-09-02 20:14:43.003075691 +0000
+++ /tmp/puppet-file20220902-1060326-16onh8q 2022-09-02 20:23:47.190767817 +0000
@@ -1,3 +1,6 @@
this is a test file
the secret is: {{{ demo_test }}}
the secret is above
+secret 2 is below
+secret2 is: {{{ demo_test2 }}}
+secret 2 is above
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/File[/home/user/.age/templates/demo_test]/content: content changed '{sha256}f1247e0b81a9a0f7899a7aa342001033728142fa89af54cc81fe3fab8e784ff8' to '{sha256}54dea21440b9c83833fcf35c2ffcadf2e7134ef2a62a9eecba56f12658ba0168'
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/File[/home/user/.age/secrets/demo_test2.age]/ensure: defined content as '{sha256}4a930d67df8ee0af7e5a7c7574427e1b8a363fe20f534a3f0eb79a659bf07d7e'
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/File[/home/user/.age/secrets/demo_test2.age]: Scheduling refresh of Exec[create_/home/user/demo_test]
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/File[/home/user/.age/secrets/demo_test2.age]: Scheduling refresh of Exec[fail_if_cannot_decrypt_demo_test2]
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/File[/home/user/.age/secrets/demo_test2.age]: Scheduling refresh of Exec[fail_if_cannot_decrypt_demo_test2]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/Exec[fail_if_cannot_decrypt_demo_test2]: Triggered 'refresh' from 2 events
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/Exec[fail_if_cannot_decrypt_demo_test2]: Scheduling refresh of Exec[decrypt_demo_test2]
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/Exec[fail_if_cannot_decrypt_demo_test2]: Scheduling refresh of Exec[decrypt_demo_test2]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/Exec[decrypt_demo_test2]: Triggered 'refresh' from 2 events
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/Exec[decrypt_demo_test2]: Scheduling refresh of Exec[create_/home/user/demo_test]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decryptdemo[/home/user/demo_test]/Exec[create_/home/user/demo_test]: Triggered 'refresh' from 2 events
Notice: Applied catalog in 9.90 seconds
user@demo1:~$ sudo cat /home/user/demo_test
this is a test file
the secret is:
the secret is above
secret 2 is below
secret2 is: test2
secret 2 is above
user@demo1:~$
As you can see, we've placed secret2 but we've lost the original secret
In order to work around this, I used /bin/true as a 'hinge' of sorts between the File resources and the Exec resources:
$ cat base.pp decrypt.pp secret.pp
# core::age
#
# define: core::age
# this class takes no parameters, it only installs basic requirements to use core::age::decrypt
#
# if your {secrets,templates} are in a subdir of /home/user/.age/{secrets,templates} then you must define that subdir on your own
#
# README in files/age/README.md
class core::age::base {
$tmpdir = '/tmp/age'
['age', 'ruby-mustache',].each | $package | {
case $::kernel {
'Darwin': {
$pkg = $package ? {
'ruby-mustache' => 'mustache',
default => $package,
}
$provider = $package ? {
'ruby-mustache' => 'gem',
default => 'brew',
}
$ensure = 'present'
}
default: {
$pkg = $package
$provider = 'apt'
$ensure = $package ? {
# require age >=1.0.0 https://github.com/sthagen/FiloSottile-age/pull/22/files
'age' => '>=1.0.0-1~bpo11+1',
default => 'present',
}
}
}
package {$pkg:
ensure => $ensure,
provider => $provider,
}
}
file {
default:
ensure => directory,
owner => 'user',
group => 'user',
mode => '0700',
# don't allow unknown files in these dirs, they're old secrets
recurse => true,
purge => true,
force => true,
;
"/home/user/.age":
recurse => remote,
source => 'puppet:///modules/core/age',
;
"/home/user/.age/config":
# this is where the host keys live, puppet doesn't manage them
purge => false,
recurse => false,
;
$tmpdir:
owner => root,
group => root,
}
}
# define: core::age::decrypt
#
# README in files/age/README.md
#
# Parameters:
# [*ensure*] - Ensure that the files is (present|absent). Default: present
# [*secrets*] - A hash of {'references' => 'secrets'}. Required
# [*template*] - A file template that will be placed on the target host, for age+mustache to fill. Required
# - This is not the final file, but a mustache template that age+mustache will interact with to build the final target
# [*target*] - The target file to be created. Default: $name.
# [*key_type*] - The key type, choose between {cluster,host}. Default: cluster
# [*template_source*] - The template source file. Default: puppet:///modules/core/age/templates/${template}. Required.
# [*plant_template*] - If you want puppet to automatically source the template file using $template_source. Default: true
# - Set to false in order to fill a template file with other (non-secret) vars using a file resource + epp
# [*mode*] - The file mode to be set on the target. Default: '0600'. Optional
# [*user*] - The file owner to be set on the target. Default: undef. Optional
# [*group*] - The file group to be set on the target. Default: undef. Optional
#
# A note about the use of ` > /dev/null` in many of the `exec` statements in this file
# The purpose of this module is to _not_ leak secrets.
# By default an exec statment will push its results back to the puppet catalog, even when `logoutput => false,`
# Hence we use `> /dev/null` to keep the secrets from being placed on the puppet catalog
#
# Because this is a zero-knowledge module, puppet can't know if the content of your target file has changed outside of this module
# The only thing that will update the file is
# - removing the target file
# - removing the corresponding secret or template in ~/.age/{secrets,templates}
# - changing the secret in puppet
# simply changing the content of the target file will not trigger an update of the secrets
#
# or you can wait one hour and an automatic refresh of the secrets will be performed
define core::age::decrypt (
Hash $secrets,
String $template,
Enum['present', 'absent'] $ensure = present,
String $target = $name,
Enum[
'cluster',
'host'
] $key_type = 'cluster',
String $template_source = "puppet:///modules/core/age/templates/${template}",
Boolean $plant_template = true,
String $mode = '0600',
Optional[String] $owner = undef,
Optional[String] $group = undef,
) {
include core::age::base
$random = generate('/bin/bash', '-c', 'tr -dc A-Za-z0-9 '/usr/bin/true',
default => '/bin/true',
}
$false = $::kernel ? {
'Darwin' => '/usr/bin/false',
default => '/bin/false',
}
exec {"kickoff_${target}":
# a harmless kicker-offer that every file notifies and every exec subscribes to
# this is the entrypoint to any secret
command => $true,
refreshonly => true,
}
# fail if can't find the key, run max 1x per key type
if ! defined(Exec["ensure ${key_type} key"]) {
exec {"ensure ${key_type} key":
command => $false,
unless => "test -f ${keypath}",
refreshonly => true,
subscribe => Exec["kickoff_${target}",],
}
}
if $plant_template {
if ! defined (File[$template_path]) {
file {$template_path:
ensure => core::bool2ensure($plant_template),
owner => 'user',
group => 'user',
mode => '0600',
source => $template_source,
notify => Exec["kickoff_${target}",],
}
}
}
if $ensure == 'present' {
$secrets.each | $reference, $secret | {
core::age::secret {$secret:
reference => $reference,
tmpfile => $tmpfile,
target => $target,
keypath => $keypath,
tag => $::hostname,
}
}
# collect all the secrets before working on decryption
Core::Age::Secret <<| tag == $::hostname |>>
~> exec {"create_${target}":
command => "mustache ${tmpfile} ${template_path} >| ${target}",
logoutput => false,
path => "/usr/local/bin:/bin:/usr/bin",
# require $target bc otherwise puppet thinks the file doesn't exist
# (bc it doesn't at the start of the first puppet run) and overwrites it blank
require => [File[$template_path, $target,],],
subscribe => Exec["kickoff_${target}",],
refreshonly => true,
}
exec {"hourly_refresh_${target}":
# allows ${target} to be refreshed by other changes (as often as necessary) or regularly on a schedule
command => $true,
notify => Exec["kickoff_${target}",],
schedule => hourly,
}
}
file {$target:
# file resource is only here to apply ownership & perms
ensure => $ensure,
owner => $owner,
group => $group,
mode => $mode,
notify => Exec["kickoff_${target}",],
}
}
# core::age::secret
define core::age::secret (
$reference,
$tmpfile,
$target,
$keypath,
$secret = $name,
) {
$secret_path = "/home/user/.age/secrets/${secret}.age"
file {$secret_path:
owner => 'user',
group => 'user',
mode => '0600',
source => "puppet:///modules/core/age/secrets/${secret}.age",
notify => Exec["kickoff_${target}",],
}
~> exec {"fail_if_cannot_decrypt_${secret}":
# errors get hidden in decrypt_${secret} by the $(command substitution)
command => "age --decrypt -i ${keypath} ${secret_path} > /dev/null",
path => ":/usr/local/bin:/bin:/usr/bin",
subscribe => Exec["kickoff_${target}",],
logoutput => false,
refreshonly => true,
}
~> exec{"decrypt_${secret}":
command => "echo ${reference}: \"$(age --decrypt -i ${keypath} ${secret_path})\" >> ${tmpfile}",
path => "/usr/local/bin:/bin:/usr/bin",
subscribe => Exec["kickoff_${target}",],
notify => Exec["create_${target}",],
require => [File[$secret_path,],],
refreshonly => true,
}
}
node /^demo[0-9].demo.net$/
{
core::age::decrypt {'/home/user/demo_test':
ensure => present,
secrets => {
'demo_test' => 'demo_test',
},
template => 'demo_test',
}
}
user@demo4:~$ sudo puppet agent -t --environment damon
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/File[/home/user/demo_test]/ensure: created (corrective)
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/File[/home/user/demo_test]: Scheduling refresh of Exec[kickoff_/home/user/demo_test]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Exec[kickoff_/home/user/demo_test]: Triggered 'refresh' from 1 event
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Exec[kickoff_/home/user/demo_test]: Scheduling refresh of Exec[create_/home/user/demo_test]
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Exec[kickoff_/home/user/demo_test]: Scheduling refresh of Exec[fail_if_cannot_decrypt_demo_test]
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Exec[kickoff_/home/user/demo_test]: Scheduling refresh of Exec[decrypt_demo_test]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test]/Exec[fail_if_cannot_decrypt_demo_test]: Triggered 'refresh' from 1 event
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test]/Exec[fail_if_cannot_decrypt_demo_test]: Scheduling refresh of Exec[decrypt_demo_test]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test]/Exec[decrypt_demo_test]: Triggered 'refresh' from 2 events
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test]/Exec[decrypt_demo_test]: Scheduling refresh of Exec[create_/home/user/demo_test]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Exec[create_/home/user/demo_test]: Triggered 'refresh' from 2 events
Notice: Applied catalog in 10.44 seconds
user@demo4:~$ sudo cat /home/user/demo_test
this is a test file
the secret is: test
the secret is above
secret 2 is below
secret2 is:
secret 2 is above
node /^demo[0-9].demo.net$/
{
core::age::decrypt {'/home/user/demo_test':
ensure => present,
secrets => {
'demo_test' => 'demo_test',
'demo_test2' => 'demo_test2',
},
template => 'demo_test',
}
}
user@demo4:~$ sudo puppet agent -t --environment damon
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test2]/File[/home/user/.age/secrets/demo_test2.age]/mode: mode changed '0700' to '0600'
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test2]/File[/home/user/.age/secrets/demo_test2.age]: Scheduling refresh of Exec[kickoff_/home/user/demo_test]
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test2]/File[/home/user/.age/secrets/demo_test2.age]: Scheduling refresh of Exec[fail_if_cannot_decrypt_demo_test2]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Exec[kickoff_/home/user/demo_test]: Triggered 'refresh' from 1 event
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Exec[kickoff_/home/user/demo_test]: Scheduling refresh of Exec[create_/home/user/demo_test]
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Exec[kickoff_/home/user/demo_test]: Scheduling refresh of Exec[fail_if_cannot_decrypt_demo_test]
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Exec[kickoff_/home/user/demo_test]: Scheduling refresh of Exec[decrypt_demo_test]
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Exec[kickoff_/home/user/demo_test]: Scheduling refresh of Exec[fail_if_cannot_decrypt_demo_test2]
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Exec[kickoff_/home/user/demo_test]: Scheduling refresh of Exec[decrypt_demo_test2]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test]/Exec[fail_if_cannot_decrypt_demo_test]: Triggered 'refresh' from 1 event
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test]/Exec[fail_if_cannot_decrypt_demo_test]: Scheduling refresh of Exec[decrypt_demo_test]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test]/Exec[decrypt_demo_test]: Triggered 'refresh' from 2 events
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test]/Exec[decrypt_demo_test]: Scheduling refresh of Exec[create_/home/user/demo_test]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test2]/Exec[fail_if_cannot_decrypt_demo_test2]: Triggered 'refresh' from 2 events
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test2]/Exec[fail_if_cannot_decrypt_demo_test2]: Scheduling refresh of Exec[decrypt_demo_test2]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test2]/Exec[decrypt_demo_test2]: Triggered 'refresh' from 2 events
Info: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Core::Age::Secret[demo_test2]/Exec[decrypt_demo_test2]: Scheduling refresh of Exec[create_/home/user/demo_test]
Notice: /Stage[main]/Main/Node[__node_regexp__demo0-9.demo.net]/Core::Age::Decrypt[/home/user/demo_test]/Exec[create_/home/user/demo_test]: Triggered 'refresh' from 3 events
Notice: Applied catalog in 10.53 seconds
user@demo4:~$ sudo cat /home/user/demo_test
this is a test file
the secret is: test
the secret is above
secret 2 is below
secret2 is: test2
secret 2 is above
user@demo4:~$
As you can see, we have successfully added a secret to our manifest but convinced Puppet to decrypt all secrets, and only then run mustache to drop the secrets in the file
This is acheived by having every File resource notify our 'kicker-offer' and every Exec resource subscribe to the 'kicker-offer', and by moving secrets to its own defined type that all get collected before the decrypt step runs