├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ └── update.yml ├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── agent-config.hcl ├── example.ctmpl ├── flake.lock ├── flake.nix ├── foreman ├── agent.sh ├── terraform-apply.sh └── vault.sh ├── messenger ├── Cargo.lock ├── Cargo.toml ├── default.nix └── src │ └── main.rs ├── module ├── definition.nix ├── definition.tests.nix ├── helpers.nix ├── helpers.tests.nix ├── implementation.nix ├── implementation.tests.nix └── mock-systemd-module.nix └── terraform ├── .gitignore └── main.tf /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig configuration for nixpkgs 2 | # https://EditorConfig.org 3 | 4 | # Top-most EditorConfig file 5 | root = true 6 | 7 | # Unix-style newlines with a newline ending every file, utf-8 charset 8 | [*] 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | charset = utf-8 13 | 14 | # Ignore diffs/patches 15 | [*.{diff,patch}] 16 | end_of_line = unset 17 | insert_final_newline = unset 18 | trim_trailing_whitespace = unset 19 | 20 | # see https://nixos.org/nixpkgs/manual/#chap-conventions 21 | 22 | # Match json/lockfiles/markdown/nix/perl/python/ruby/shell/docbook files, set indent to spaces 23 | [*.{json,lock,md,nix,pl,pm,py,rb,sh,xml}] 24 | indent_style = space 25 | 26 | # Match docbook files, set indent width of one 27 | [*.xml] 28 | indent_size = 1 29 | 30 | # Match json/lockfiles/markdown/nix/ruby files, set indent width of two 31 | [*.{json,lock,md,nix,rb}] 32 | indent_size = 2 33 | 34 | # Match perl/python/shell scripts, set indent width of four 35 | [*.{pl,pm,py,sh}] 36 | indent_size = 4 37 | 38 | # Disable file types or individual files 39 | # some of these files may be auto-generated and/or require significant changes 40 | 41 | [*.{c,h}] 42 | insert_final_newline = unset 43 | trim_trailing_whitespace = unset 44 | 45 | [*.{asc,key,ovpn}] 46 | insert_final_newline = unset 47 | end_of_line = unset 48 | trim_trailing_whitespace = unset 49 | 50 | [*.lock] 51 | indent_size = unset 52 | 53 | [*.ctmpl] 54 | insert_final_newline = unset 55 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ##### Description 2 | 3 | 7 | 8 | ##### Checklist 9 | 10 | 14 | 15 | - [ ] Formatted with `nixpkgs-fmt` 16 | - [ ] Ran tests with: 17 | - `nix flake check -L` 18 | - `cargo test --manifest-path ./messenger/Cargo.toml` 19 | - [ ] Added or updated relevant tests (leave unchecked if not applicable) 20 | - [ ] Added or updated relevant documentation (leave unchecked if not applicable) 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "cargo" 8 | directory: "/" 9 | schedule: 10 | interval: "monthly" 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | CI: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | id-token: write 13 | contents: read 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Install Nix 19 | uses: DeterminateSystems/nix-installer-action@main 20 | with: 21 | determinate: true 22 | - name: Enable FlakeHub cache 23 | uses: DeterminateSystems/flakehub-cache-action@main 24 | 25 | - uses: greut/eclint-action@v0 26 | if: success() || failure() 27 | 28 | - name: Check nixpkgs-fmt formatting 29 | if: success() || failure() 30 | run: | 31 | find . -path '*.nix' -print0 | xargs -0 nix develop -c nixpkgs-fmt --check 32 | 33 | - name: Terraform fmt 34 | if: success() || failure() 35 | run: nix develop -c terraform fmt -check -recursive ./terraform 36 | - name: Terraform init 37 | run: | 38 | for dir in $(find terraform -type d); do 39 | echo "initializing dir $dir" 40 | nix develop -c terraform -chdir=$dir init -backend=false 41 | done 42 | - name: Terraform validate 43 | if: success() || failure() 44 | run: | 45 | for dir in $(find terraform -type d); do 46 | echo "validating dir $dir" 47 | nix develop -c terraform -chdir=$dir validate -no-color 48 | done 49 | - name: Shellcheck 50 | if: success() || failure() 51 | run: nix develop -c shellcheck $(git ls-files | grep "\.sh$") 52 | 53 | - name: Check rustfmt 54 | if: success() || failure() 55 | working-directory: ./messenger 56 | run: nix develop --command cargo fmt -- --check 57 | 58 | - name: Build messenger 59 | if: success() || failure() 60 | run: nix build .#messenger -L 61 | 62 | - name: General nix flake check 63 | if: success() || failure() 64 | run: nix flake check --keep-going -L 65 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: update-flake-lock 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 0 * * 0" 6 | 7 | jobs: 8 | lockfile: 9 | runs-on: ubuntu-22.04 10 | permissions: 11 | id-token: write 12 | contents: read 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Install Nix 17 | uses: DeterminateSystems/nix-installer-action@main 18 | with: 19 | determinate: true 20 | - name: Enable magic Nix cache 21 | uses: DeterminateSystems/flakehub-cache-action@main 22 | - name: Check flake 23 | uses: DeterminateSystems/flake-checker-action@main 24 | - name: Update flake.lock 25 | uses: DeterminateSystems/update-flake-lock@main 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | secret_id 2 | role_id 3 | example.output 4 | result* 5 | target 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Determinate Systems, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | vault: ./foreman/vault.sh 2 | tfapp: ./foreman/terraform-apply.sh 3 | agent: ./foreman/agent.sh 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nixos-vault-service 2 | 3 | The NixOS Vault Service module is a NixOS module that allows easily integrating 4 | Vault with existing systemd services. 5 | 6 | > **NOTE**: The goal is not magic, so some services may need to be changed or patched. 7 | 8 | ## Usage 9 | 10 | ### With Flakes 11 | 12 | ```nix 13 | # flake.nix 14 | { 15 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 16 | inputs.vaultModule = { 17 | url = "github:DeterminateSystems/nixos-vault-service/main"; 18 | inputs.nixpkgs.follows = "nixpkgs"; 19 | }; 20 | 21 | outputs = { self, nixpkgs, vaultModule }: { 22 | nixosConfigurations.nixos = nixpkgs.lib.nixosSystem { 23 | system = "x86_64-linux"; 24 | modules = [ 25 | vaultModule.nixosModule 26 | ./configuration.nix 27 | ]; 28 | }; 29 | }; 30 | } 31 | ``` 32 | 33 | ### Without Flakes 34 | 35 | There are many ways to make this module available in your system configuration 36 | without flakes. This is an example of just one possible method: 37 | 38 | ```nix 39 | # vault.nix 40 | let 41 | vaultModuleSrc = builtins.fetchGit { 42 | url = "https://github.com/DeterminateSystems/nixos-vault-service.git"; 43 | ref = "main"; 44 | }; 45 | 46 | vaultModule = import vaultModuleSrc; 47 | in 48 | { 49 | imports = [ vaultModule.nixosModule ]; 50 | } 51 | ``` 52 | 53 | ## Configuration 54 | 55 | After you have the module imported by your system's configuration, you can now 56 | being integrating your services with Vault. 57 | 58 | ### Options 59 | 60 | * `detsys.vaultAgent.defaultAgentConfig` (optional, default: `{ }`) – The default configuration for all Vault agents. Defers to individual service's `agentConfig`, if set. See [`agentConfig` options](#agentconfig-options) for more information. 61 | * `detsys.vaultAgent.systemd.services..enable` (optional, default: `false`) – Whether to enable Vault integration with the service specified by ``. 62 | * `detsys.vaultAgent.systemd.services..agentConfig` (optional, default: `{ }`) – The Vault agent configuration for this service. See [`agentConfig` options](#agentconfig-options) for more information. 63 | * `detsys.vaultAgent.systemd.services..environment` (optional, default: `{ }`) – Environment variable secret configuration. 64 | * `detsys.vaultAgent.systemd.services..environment.changeAction` (optional, default: `"restart"`) – What action to take if any secrets in the environment change. One of `"restart"`, `"stop"`, or `"none"`. 65 | * `detsys.vaultAgent.systemd.services..environment.templateFiles` (optional, default: `{ }`) – Set of files containing environment variables for Vault to template. 66 | * `detsys.vaultAgent.systemd.services..environment.templateFiles..file` (required) – The file containing the environment variable(s) for Vault to template. 67 | * `detsys.vaultAgent.systemd.services..environment.template` (optional, default: `null`) – A multi-line string containing environment variables for Vault to template. 68 | * `detsys.vaultAgent.systemd.services..secretFiles` (optional, default: `{ }`) – Secret file configuration. 69 | * `detsys.vaultAgent.systemd.services..secretFiles.defaultChangeAction` (optional, default: `"restart"`) – What action to take if any secrets in any of these files change. One of `"restart"`, `"reload"`, `"stop"`, or `"none"`. 70 | * `detsys.vaultAgent.systemd.services..secretFiles.files` (optional: default `{ }`) – Set of files for Vault to template. 71 | * `detsys.vaultAgent.systemd.services..secretFiles.files..changeAction` (optional, default: the `defaultChangeAction`) – What action to take if the secret file changes. One of `"restart"`, `"reload"`, `"stop"`, or `"none"`. 72 | * `detsys.vaultAgent.systemd.services..secretFiles.files..templateFile` (optional, default: `null`) – A file containing a Vault template. Conflicts with `template`. 73 | * `detsys.vaultAgent.systemd.services..secretFiles.files..template` (optional, default: `null`) – A string containing a Vault template. Conflicts with `templateFile`. 74 | * `detsys.vaultAgent.systemd.services..secretFiles.files..perms` (optional, default: `"0400"`) – The octal mode of the secret file. 75 | * `detsys.vaultAgent.systemd.services..secretFiles.files..path` (read-only) – The path to the secret file inside ``'s namespace's `PrivateTmp`. 76 | 77 | #### `agentConfig` options 78 | 79 | The `agentConfig` options are partially typed in order to allow us to set defaults, as well as prevent users from using obviously broken configurations. 80 | 81 | These options apply for both `detsys.vaultAgent.defaultAgentConfig` and `detsys.vaultAgent.systemd.services..agentConfig`. 82 | 83 | * `agentConfig.auto_auth` (optional, default: `{ }`) – The Vault agent's `auto_auth` configuration. 84 | * `agentConfig.auto_auth.method` (optional, default: `[ ]`) – The `auto_auth`'s `method` configuration. Note that this does not support the HCL-esque way of defining this option with `method "aws" { ... }` -- you must specify the `type` and `config` separately. 85 | * `agentConfig.auto_auth.method.[].type` (required) – The `auto_auth.method`'s type. 86 | * `agentConfig.auto_auth.method.[].config` (required) – The `auto_auth.method`'s configuration. 87 | * `agentConfig.template_config` (optional, default: `{ }`) – The Vault agent's `template_config` configuration. 88 | * `agentConfig.template_config.exit_on_retry_failure` (optional, default: `true`) – Whether or not to exit the Vault agent when it fails to retry any further. Must be true. 89 | 90 | Any options not listed here may be manually specified, but will not be type-checked. 91 | 92 | For example, to specify the `cache.use_auto_auth_token` option, you would only need to specify `agentConfig.cache.use_auto_auth_token = true;`. 93 | 94 | ### Examples 95 | 96 | #### Demonstrating all the options 97 | 98 | ```nix 99 | { 100 | detsys.vaultAgent.defaultAgentConfig = { 101 | # The configuration passed to `vault agent` -- will be converted to JSON. 102 | # This is where your `vault`, `auto_auth`, `template_config`, etc., configuration should go. 103 | }; 104 | 105 | detsys.vaultAgent.systemd.services."service-name" = { 106 | enable = true; 107 | 108 | agentConfig = { 109 | # Overrides the entirety of `detsys.vaultAgent.defaultAgentConfig`. 110 | }; 111 | 112 | environment = { 113 | changeAction = "restart"; 114 | 115 | templateFiles = { 116 | "example-a".file = ./example-a.ctmpl; 117 | "example-b".file = ./example-b.ctmpl; 118 | }; 119 | 120 | template = '' 121 | EXAMPLE_C={{ with secret "secret/super_secret" }}{{ .Data.c }}{{ end }} 122 | EXAMPLE_D={{ with secret "secret/super_secret" }}{{ .Data.d }}{{ end }} 123 | ''; 124 | }; 125 | 126 | secretFiles = { 127 | defaultChangeAction = "restart"; 128 | 129 | files."example-e" = { 130 | changeAction = "reload"; 131 | perms = "0440"; 132 | 133 | # NOTE: You can only use either: 134 | templateFile = ./example-e.ctmpl; 135 | # or: 136 | template = '' 137 | {{ with secret "secret/super_secret" }}{{ .Data.e }}{{ end }} 138 | ''; 139 | # but not both. 140 | }; 141 | 142 | files."example-f".template = '' 143 | {{ with secret "secret/super_secret" }}{{ .Data.f }}{{ end }} 144 | ''; 145 | }; 146 | }; 147 | } 148 | ``` 149 | 150 | #### Default Vault Agent configuration 151 | 152 | You can set the default `agentConfig` for all units by using the `detsys.vaultAgent.defaultAgentConfig` interface. 153 | 154 | > **NOTE**: Manually-specified unit `agentConfig`s will override _**all**_ of the the settings specified in the `detsys.vaultAgent.defaultAgentConfig` option. 155 | 156 | > **NOTE**: Some of these options _must_ be wrapped in a list (e.g. see `auto_auth`) in order for the generated JSON to be valid. Wrapping them all in a list doesn't hurt. 157 | 158 | ```nix 159 | { 160 | detsys.vaultAgent.defaultAgentConfig = { 161 | vault = { address = "http://127.0.0.1:8200"; }; 162 | auto_auth = { 163 | method = [{ 164 | type = "approle"; 165 | config = { 166 | remove_secret_id_file_after_reading = false; 167 | role_id_file_path = "/role_id"; 168 | secret_id_file_path = "/secret_id"; 169 | }; 170 | }]; 171 | }; 172 | template_config = { 173 | static_secret_render_interval = "5s"; 174 | }; 175 | }; 176 | } 177 | ``` 178 | 179 | #### Accessing the path of a file in `secretFiles` 180 | 181 | All `secretFiles.files.` expose a `path` attribute, so you don't need to memorize where the secrets are written to: 182 | 183 | ```nix 184 | { config, ... }: 185 | { 186 | detsys.vaultAgent.systemd.services.prometheus = { 187 | enable = true; 188 | 189 | secretFiles.files."vault.token".template = '' 190 | {{ with secret "secrets/nginx-basic-auth"}} 191 | {{ .Data.data.htpasswd }} 192 | {{ end }} 193 | ''; 194 | }; 195 | } 196 | ``` 197 | 198 | You can then access the path to the above `vault.token` secret file via `config.detsys.vaultAgent.systemd.services.prometheus.secretFiles.files."vault.token".path`. 199 | 200 | ### How to override systemd service configuration 201 | 202 | By using the NixOS module system, it is possible to override the sidecar's systemd service configuration (e.g. to tune how often the service is allowed to restart): 203 | Sidecar unit names follow the pattern of `detsys-vaultAgent-${service-name}`. 204 | 205 | ```nix 206 | { 207 | detsys.vaultAgent.systemd.services.prometheus = { 208 | enable = true; 209 | 210 | secretFiles = { 211 | defaultChangeAction = "none"; 212 | files."vault.token".templateFile = ./vault-token.ctmpl; 213 | }; 214 | }; 215 | 216 | systemd.services.detsys-vaultAgent-prometheus = { 217 | unitConfig = { 218 | StartLimitIntervalSec = 300; 219 | StartLimitBurst = 10; 220 | }; 221 | 222 | serviceConfig = { 223 | RestartSec = 30; 224 | Restart = "always"; 225 | }; 226 | }; 227 | } 228 | ``` 229 | 230 | ## Running tests 231 | 232 | We have tests for the module's definition, helpers, and implementation. These can be run like so: 233 | 234 | ```bash 235 | nix-instantiate --strict --eval --json ./default.nix -A checks.definition 236 | nix-instantiate --strict --eval --json ./default.nix -A checks.helpers 237 | nix-build ./default.nix -A checks.implementation 238 | ``` 239 | 240 | ### Tips for writing tests 241 | 242 | To read the secret file (e.g. to verify the contents), you will need to join the namespace of the sidecar vaultAgent unit: 243 | 244 | ```bash 245 | systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-serviceName.service -p PrivateTmp=true cat /tmp/detsys-vault/some-secret-file 246 | ``` 247 | 248 | # License 249 | 250 | [MIT](./LICENSE) 251 | -------------------------------------------------------------------------------- /agent-config.hcl: -------------------------------------------------------------------------------- 1 | vault { 2 | address = "http://127.0.0.1:8200" 3 | } 4 | 5 | auto_auth { 6 | method { 7 | type = "approle" 8 | config = { 9 | role_id_file_path = "role_id" 10 | secret_id_file_path = "secret_id" 11 | remove_secret_id_file_after_reading = false 12 | } 13 | } 14 | } 15 | 16 | template_config { 17 | error_on_missing_key = true 18 | static_secret_render_interval = "5s" 19 | } 20 | 21 | template { 22 | source = "./example.ctmpl" 23 | destination = "./example.output" 24 | command = "echo systemctl reload foobar" 25 | } 26 | -------------------------------------------------------------------------------- /example.ctmpl: -------------------------------------------------------------------------------- 1 | {{ with secret "sys/tools/random/1" "format=base64" }} 2 | cloudamqp = {{ .Data.random_bytes }} 3 | {{ end }} -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1745234285, 6 | "narHash": "sha256-GfpyMzxwkfgRVN0cTGQSkTC0OHhEkv3Jf6Tcjm//qZ0=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "c11863f1e964833214b767f4a369c6e6a7aba141", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "NixOS integration for Vault-backed secrets on systemd services."; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 6 | }; 7 | 8 | outputs = 9 | { nixpkgs 10 | , self 11 | , ... 12 | }@inputs: 13 | let 14 | nameValuePair = name: value: { inherit name value; }; 15 | genAttrs = names: f: builtins.listToAttrs (map (n: nameValuePair n (f n)) names); 16 | 17 | pkgsFor = pkgs: system: 18 | import pkgs { inherit system; config.allowUnfree = true; }; 19 | 20 | allSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 21 | forAllSystems = f: genAttrs allSystems 22 | (system: f { 23 | inherit system; 24 | pkgs = pkgsFor nixpkgs system; 25 | }); 26 | 27 | inherit (nixpkgs) lib; 28 | in 29 | { 30 | nixosModule = self.nixosModules.nixos-vault-service; 31 | nixosModules = { 32 | nixos-vault-service = { 33 | imports = [ 34 | ./module/implementation.nix 35 | ]; 36 | 37 | nixpkgs.overlays = [ 38 | self.overlays.default 39 | ]; 40 | }; 41 | }; 42 | 43 | packages = forAllSystems 44 | ({ pkgs, ... }: rec { 45 | messenger = pkgs.callPackage ./messenger { }; 46 | 47 | default = messenger; 48 | }); 49 | 50 | overlays.default = final: prev: { 51 | detsys-messenger = self.packages.${final.stdenv.system}.messenger; 52 | }; 53 | 54 | devShells = forAllSystems 55 | ({ pkgs, ... }: 56 | { 57 | default = pkgs.mkShell { 58 | buildInputs = with pkgs; [ 59 | (terraform_1.withPlugins (tf: [ 60 | tf.local 61 | tf.vault 62 | ])) 63 | cargo 64 | rustfmt 65 | foreman 66 | jq 67 | nixpkgs-fmt 68 | shellcheck 69 | vault 70 | ] ++ lib.optionals (pkgs.stdenv.isDarwin) (with pkgs; [ 71 | libiconv 72 | ]); 73 | }; 74 | } 75 | ); 76 | 77 | checks = forAllSystems 78 | ({ system, pkgs, ... }: ({ 79 | definition = pkgs.writeText "definition.json" (builtins.toJSON (import ./module/definition.tests.nix { 80 | inherit nixpkgs; 81 | inherit (nixpkgs) lib; 82 | })); 83 | 84 | helpers = pkgs.writeText "helpers.json" (builtins.toJSON (import ./module/helpers.tests.nix { 85 | inherit nixpkgs; 86 | inherit (nixpkgs) lib; 87 | })); 88 | } // (if system == "x86_64-linux" || system == "aarch64-linux" then { 89 | implementation = pkgs.writeText "implementation.json" (builtins.toJSON (import ./module/implementation.tests.nix { 90 | inherit nixpkgs self system; 91 | inherit (nixpkgs) lib; 92 | })); 93 | } 94 | else { }))); 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /foreman/agent.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | 6 | unset VAULT_ADDR 7 | # Give terraform-apply-force.sh time to delete the role_id 8 | sleep 0.1 9 | while [ ! -f role_id ]; do 10 | sleep 0.1 11 | done 12 | 13 | vault agent -config=./agent-config.hcl 14 | -------------------------------------------------------------------------------- /foreman/terraform-apply.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | rm -f role_id 6 | unset VAULT_ADDR 7 | cd terraform 8 | terraform init 9 | terraform apply -auto-approve 10 | sleep infinity 11 | -------------------------------------------------------------------------------- /foreman/vault.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | exec vault server -dev -dev-root-token-id=abc123 6 | -------------------------------------------------------------------------------- /messenger/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.18" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.10" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.7" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell", 61 | "windows-sys", 62 | ] 63 | 64 | [[package]] 65 | name = "async-attributes" 66 | version = "1.1.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" 69 | dependencies = [ 70 | "quote", 71 | "syn 1.0.109", 72 | ] 73 | 74 | [[package]] 75 | name = "async-channel" 76 | version = "1.9.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" 79 | dependencies = [ 80 | "concurrent-queue", 81 | "event-listener 2.5.3", 82 | "futures-core", 83 | ] 84 | 85 | [[package]] 86 | name = "async-channel" 87 | version = "2.3.1" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" 90 | dependencies = [ 91 | "concurrent-queue", 92 | "event-listener-strategy", 93 | "futures-core", 94 | "pin-project-lite", 95 | ] 96 | 97 | [[package]] 98 | name = "async-executor" 99 | version = "1.13.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" 102 | dependencies = [ 103 | "async-task", 104 | "concurrent-queue", 105 | "fastrand", 106 | "futures-lite", 107 | "slab", 108 | ] 109 | 110 | [[package]] 111 | name = "async-global-executor" 112 | version = "2.4.1" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" 115 | dependencies = [ 116 | "async-channel 2.3.1", 117 | "async-executor", 118 | "async-io", 119 | "async-lock", 120 | "blocking", 121 | "futures-lite", 122 | "once_cell", 123 | ] 124 | 125 | [[package]] 126 | name = "async-io" 127 | version = "2.4.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" 130 | dependencies = [ 131 | "async-lock", 132 | "cfg-if", 133 | "concurrent-queue", 134 | "futures-io", 135 | "futures-lite", 136 | "parking", 137 | "polling", 138 | "rustix 0.38.44", 139 | "slab", 140 | "tracing", 141 | "windows-sys", 142 | ] 143 | 144 | [[package]] 145 | name = "async-lock" 146 | version = "3.4.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" 149 | dependencies = [ 150 | "event-listener 5.4.0", 151 | "event-listener-strategy", 152 | "pin-project-lite", 153 | ] 154 | 155 | [[package]] 156 | name = "async-process" 157 | version = "2.3.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" 160 | dependencies = [ 161 | "async-channel 2.3.1", 162 | "async-io", 163 | "async-lock", 164 | "async-signal", 165 | "async-task", 166 | "blocking", 167 | "cfg-if", 168 | "event-listener 5.4.0", 169 | "futures-lite", 170 | "rustix 0.38.44", 171 | "tracing", 172 | ] 173 | 174 | [[package]] 175 | name = "async-signal" 176 | version = "0.2.10" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" 179 | dependencies = [ 180 | "async-io", 181 | "async-lock", 182 | "atomic-waker", 183 | "cfg-if", 184 | "futures-core", 185 | "futures-io", 186 | "rustix 0.38.44", 187 | "signal-hook-registry", 188 | "slab", 189 | "windows-sys", 190 | ] 191 | 192 | [[package]] 193 | name = "async-std" 194 | version = "1.13.1" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" 197 | dependencies = [ 198 | "async-attributes", 199 | "async-channel 1.9.0", 200 | "async-global-executor", 201 | "async-io", 202 | "async-lock", 203 | "async-process", 204 | "crossbeam-utils", 205 | "futures-channel", 206 | "futures-core", 207 | "futures-io", 208 | "futures-lite", 209 | "gloo-timers", 210 | "kv-log-macro", 211 | "log", 212 | "memchr", 213 | "once_cell", 214 | "pin-project-lite", 215 | "pin-utils", 216 | "slab", 217 | "wasm-bindgen-futures", 218 | ] 219 | 220 | [[package]] 221 | name = "async-task" 222 | version = "4.7.1" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" 225 | 226 | [[package]] 227 | name = "atomic-waker" 228 | version = "1.1.2" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 231 | 232 | [[package]] 233 | name = "autocfg" 234 | version = "1.4.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 237 | 238 | [[package]] 239 | name = "backoff" 240 | version = "0.4.0" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" 243 | dependencies = [ 244 | "async-std", 245 | "futures-core", 246 | "getrandom 0.2.16", 247 | "instant", 248 | "pin-project-lite", 249 | "rand", 250 | ] 251 | 252 | [[package]] 253 | name = "bitflags" 254 | version = "2.9.0" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 257 | 258 | [[package]] 259 | name = "blocking" 260 | version = "1.6.1" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" 263 | dependencies = [ 264 | "async-channel 2.3.1", 265 | "async-task", 266 | "futures-io", 267 | "futures-lite", 268 | "piper", 269 | ] 270 | 271 | [[package]] 272 | name = "bumpalo" 273 | version = "3.17.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 276 | 277 | [[package]] 278 | name = "cfg-if" 279 | version = "1.0.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 282 | 283 | [[package]] 284 | name = "clap" 285 | version = "4.5.37" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" 288 | dependencies = [ 289 | "clap_builder", 290 | "clap_derive", 291 | ] 292 | 293 | [[package]] 294 | name = "clap_builder" 295 | version = "4.5.37" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" 298 | dependencies = [ 299 | "anstream", 300 | "anstyle", 301 | "clap_lex", 302 | "strsim", 303 | ] 304 | 305 | [[package]] 306 | name = "clap_derive" 307 | version = "4.5.32" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 310 | dependencies = [ 311 | "heck", 312 | "proc-macro2", 313 | "quote", 314 | "syn 2.0.100", 315 | ] 316 | 317 | [[package]] 318 | name = "clap_lex" 319 | version = "0.7.4" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 322 | 323 | [[package]] 324 | name = "colorchoice" 325 | version = "1.0.3" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 328 | 329 | [[package]] 330 | name = "concurrent-queue" 331 | version = "2.5.0" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 334 | dependencies = [ 335 | "crossbeam-utils", 336 | ] 337 | 338 | [[package]] 339 | name = "crossbeam-utils" 340 | version = "0.8.21" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 343 | 344 | [[package]] 345 | name = "errno" 346 | version = "0.3.11" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 349 | dependencies = [ 350 | "libc", 351 | "windows-sys", 352 | ] 353 | 354 | [[package]] 355 | name = "event-listener" 356 | version = "2.5.3" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" 359 | 360 | [[package]] 361 | name = "event-listener" 362 | version = "5.4.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" 365 | dependencies = [ 366 | "concurrent-queue", 367 | "parking", 368 | "pin-project-lite", 369 | ] 370 | 371 | [[package]] 372 | name = "event-listener-strategy" 373 | version = "0.5.4" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" 376 | dependencies = [ 377 | "event-listener 5.4.0", 378 | "pin-project-lite", 379 | ] 380 | 381 | [[package]] 382 | name = "fastrand" 383 | version = "2.3.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 386 | 387 | [[package]] 388 | name = "futures" 389 | version = "0.3.31" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 392 | dependencies = [ 393 | "futures-channel", 394 | "futures-core", 395 | "futures-executor", 396 | "futures-io", 397 | "futures-sink", 398 | "futures-task", 399 | "futures-util", 400 | ] 401 | 402 | [[package]] 403 | name = "futures-channel" 404 | version = "0.3.31" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 407 | dependencies = [ 408 | "futures-core", 409 | "futures-sink", 410 | ] 411 | 412 | [[package]] 413 | name = "futures-core" 414 | version = "0.3.31" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 417 | 418 | [[package]] 419 | name = "futures-executor" 420 | version = "0.3.31" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 423 | dependencies = [ 424 | "futures-core", 425 | "futures-task", 426 | "futures-util", 427 | ] 428 | 429 | [[package]] 430 | name = "futures-io" 431 | version = "0.3.31" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 434 | 435 | [[package]] 436 | name = "futures-lite" 437 | version = "2.6.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" 440 | dependencies = [ 441 | "fastrand", 442 | "futures-core", 443 | "futures-io", 444 | "parking", 445 | "pin-project-lite", 446 | ] 447 | 448 | [[package]] 449 | name = "futures-macro" 450 | version = "0.3.31" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 453 | dependencies = [ 454 | "proc-macro2", 455 | "quote", 456 | "syn 2.0.100", 457 | ] 458 | 459 | [[package]] 460 | name = "futures-sink" 461 | version = "0.3.31" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 464 | 465 | [[package]] 466 | name = "futures-task" 467 | version = "0.3.31" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 470 | 471 | [[package]] 472 | name = "futures-util" 473 | version = "0.3.31" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 476 | dependencies = [ 477 | "futures-channel", 478 | "futures-core", 479 | "futures-io", 480 | "futures-macro", 481 | "futures-sink", 482 | "futures-task", 483 | "memchr", 484 | "pin-project-lite", 485 | "pin-utils", 486 | "slab", 487 | ] 488 | 489 | [[package]] 490 | name = "getrandom" 491 | version = "0.2.16" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 494 | dependencies = [ 495 | "cfg-if", 496 | "libc", 497 | "wasi 0.11.0+wasi-snapshot-preview1", 498 | ] 499 | 500 | [[package]] 501 | name = "getrandom" 502 | version = "0.3.2" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 505 | dependencies = [ 506 | "cfg-if", 507 | "libc", 508 | "r-efi", 509 | "wasi 0.14.2+wasi-0.2.4", 510 | ] 511 | 512 | [[package]] 513 | name = "gloo-timers" 514 | version = "0.3.0" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" 517 | dependencies = [ 518 | "futures-channel", 519 | "futures-core", 520 | "js-sys", 521 | "wasm-bindgen", 522 | ] 523 | 524 | [[package]] 525 | name = "heck" 526 | version = "0.5.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 529 | 530 | [[package]] 531 | name = "hermit-abi" 532 | version = "0.4.0" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 535 | 536 | [[package]] 537 | name = "instant" 538 | version = "0.1.13" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" 541 | dependencies = [ 542 | "cfg-if", 543 | ] 544 | 545 | [[package]] 546 | name = "is_terminal_polyfill" 547 | version = "1.70.1" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 550 | 551 | [[package]] 552 | name = "js-sys" 553 | version = "0.3.77" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 556 | dependencies = [ 557 | "once_cell", 558 | "wasm-bindgen", 559 | ] 560 | 561 | [[package]] 562 | name = "kv-log-macro" 563 | version = "1.0.7" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" 566 | dependencies = [ 567 | "log", 568 | ] 569 | 570 | [[package]] 571 | name = "lazy_static" 572 | version = "1.5.0" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 575 | 576 | [[package]] 577 | name = "libc" 578 | version = "0.2.172" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 581 | 582 | [[package]] 583 | name = "linux-raw-sys" 584 | version = "0.4.15" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 587 | 588 | [[package]] 589 | name = "linux-raw-sys" 590 | version = "0.9.4" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 593 | 594 | [[package]] 595 | name = "log" 596 | version = "0.4.27" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 599 | dependencies = [ 600 | "value-bag", 601 | ] 602 | 603 | [[package]] 604 | name = "matchers" 605 | version = "0.1.0" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 608 | dependencies = [ 609 | "regex-automata 0.1.10", 610 | ] 611 | 612 | [[package]] 613 | name = "memchr" 614 | version = "2.7.4" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 617 | 618 | [[package]] 619 | name = "messenger" 620 | version = "0.1.0" 621 | dependencies = [ 622 | "async-std", 623 | "backoff", 624 | "clap", 625 | "futures", 626 | "sd-notify", 627 | "tempfile", 628 | "tracing", 629 | "tracing-subscriber", 630 | "tracing-test", 631 | ] 632 | 633 | [[package]] 634 | name = "nu-ansi-term" 635 | version = "0.46.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 638 | dependencies = [ 639 | "overload", 640 | "winapi", 641 | ] 642 | 643 | [[package]] 644 | name = "once_cell" 645 | version = "1.21.3" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 648 | 649 | [[package]] 650 | name = "overload" 651 | version = "0.1.1" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 654 | 655 | [[package]] 656 | name = "parking" 657 | version = "2.2.1" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 660 | 661 | [[package]] 662 | name = "pin-project-lite" 663 | version = "0.2.16" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 666 | 667 | [[package]] 668 | name = "pin-utils" 669 | version = "0.1.0" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 672 | 673 | [[package]] 674 | name = "piper" 675 | version = "0.2.4" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" 678 | dependencies = [ 679 | "atomic-waker", 680 | "fastrand", 681 | "futures-io", 682 | ] 683 | 684 | [[package]] 685 | name = "polling" 686 | version = "3.7.4" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" 689 | dependencies = [ 690 | "cfg-if", 691 | "concurrent-queue", 692 | "hermit-abi", 693 | "pin-project-lite", 694 | "rustix 0.38.44", 695 | "tracing", 696 | "windows-sys", 697 | ] 698 | 699 | [[package]] 700 | name = "ppv-lite86" 701 | version = "0.2.21" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 704 | dependencies = [ 705 | "zerocopy", 706 | ] 707 | 708 | [[package]] 709 | name = "proc-macro2" 710 | version = "1.0.95" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 713 | dependencies = [ 714 | "unicode-ident", 715 | ] 716 | 717 | [[package]] 718 | name = "quote" 719 | version = "1.0.40" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 722 | dependencies = [ 723 | "proc-macro2", 724 | ] 725 | 726 | [[package]] 727 | name = "r-efi" 728 | version = "5.2.0" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 731 | 732 | [[package]] 733 | name = "rand" 734 | version = "0.8.5" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 737 | dependencies = [ 738 | "libc", 739 | "rand_chacha", 740 | "rand_core", 741 | ] 742 | 743 | [[package]] 744 | name = "rand_chacha" 745 | version = "0.3.1" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 748 | dependencies = [ 749 | "ppv-lite86", 750 | "rand_core", 751 | ] 752 | 753 | [[package]] 754 | name = "rand_core" 755 | version = "0.6.4" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 758 | dependencies = [ 759 | "getrandom 0.2.16", 760 | ] 761 | 762 | [[package]] 763 | name = "regex" 764 | version = "1.11.1" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 767 | dependencies = [ 768 | "aho-corasick", 769 | "memchr", 770 | "regex-automata 0.4.9", 771 | "regex-syntax 0.8.5", 772 | ] 773 | 774 | [[package]] 775 | name = "regex-automata" 776 | version = "0.1.10" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 779 | dependencies = [ 780 | "regex-syntax 0.6.29", 781 | ] 782 | 783 | [[package]] 784 | name = "regex-automata" 785 | version = "0.4.9" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 788 | dependencies = [ 789 | "aho-corasick", 790 | "memchr", 791 | "regex-syntax 0.8.5", 792 | ] 793 | 794 | [[package]] 795 | name = "regex-syntax" 796 | version = "0.6.29" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 799 | 800 | [[package]] 801 | name = "regex-syntax" 802 | version = "0.8.5" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 805 | 806 | [[package]] 807 | name = "rustix" 808 | version = "0.38.44" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 811 | dependencies = [ 812 | "bitflags", 813 | "errno", 814 | "libc", 815 | "linux-raw-sys 0.4.15", 816 | "windows-sys", 817 | ] 818 | 819 | [[package]] 820 | name = "rustix" 821 | version = "1.0.5" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 824 | dependencies = [ 825 | "bitflags", 826 | "errno", 827 | "libc", 828 | "linux-raw-sys 0.9.4", 829 | "windows-sys", 830 | ] 831 | 832 | [[package]] 833 | name = "rustversion" 834 | version = "1.0.20" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 837 | 838 | [[package]] 839 | name = "sd-notify" 840 | version = "0.4.5" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "b943eadf71d8b69e661330cb0e2656e31040acf21ee7708e2c238a0ec6af2bf4" 843 | dependencies = [ 844 | "libc", 845 | ] 846 | 847 | [[package]] 848 | name = "sharded-slab" 849 | version = "0.1.7" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 852 | dependencies = [ 853 | "lazy_static", 854 | ] 855 | 856 | [[package]] 857 | name = "signal-hook-registry" 858 | version = "1.4.5" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 861 | dependencies = [ 862 | "libc", 863 | ] 864 | 865 | [[package]] 866 | name = "slab" 867 | version = "0.4.9" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 870 | dependencies = [ 871 | "autocfg", 872 | ] 873 | 874 | [[package]] 875 | name = "smallvec" 876 | version = "1.15.0" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 879 | 880 | [[package]] 881 | name = "strsim" 882 | version = "0.11.1" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 885 | 886 | [[package]] 887 | name = "syn" 888 | version = "1.0.109" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 891 | dependencies = [ 892 | "proc-macro2", 893 | "quote", 894 | "unicode-ident", 895 | ] 896 | 897 | [[package]] 898 | name = "syn" 899 | version = "2.0.100" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 902 | dependencies = [ 903 | "proc-macro2", 904 | "quote", 905 | "unicode-ident", 906 | ] 907 | 908 | [[package]] 909 | name = "tempfile" 910 | version = "3.19.1" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 913 | dependencies = [ 914 | "fastrand", 915 | "getrandom 0.3.2", 916 | "once_cell", 917 | "rustix 1.0.5", 918 | "windows-sys", 919 | ] 920 | 921 | [[package]] 922 | name = "thread_local" 923 | version = "1.1.8" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 926 | dependencies = [ 927 | "cfg-if", 928 | "once_cell", 929 | ] 930 | 931 | [[package]] 932 | name = "tracing" 933 | version = "0.1.41" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 936 | dependencies = [ 937 | "pin-project-lite", 938 | "tracing-attributes", 939 | "tracing-core", 940 | ] 941 | 942 | [[package]] 943 | name = "tracing-attributes" 944 | version = "0.1.28" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 947 | dependencies = [ 948 | "proc-macro2", 949 | "quote", 950 | "syn 2.0.100", 951 | ] 952 | 953 | [[package]] 954 | name = "tracing-core" 955 | version = "0.1.33" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 958 | dependencies = [ 959 | "once_cell", 960 | "valuable", 961 | ] 962 | 963 | [[package]] 964 | name = "tracing-log" 965 | version = "0.2.0" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 968 | dependencies = [ 969 | "log", 970 | "once_cell", 971 | "tracing-core", 972 | ] 973 | 974 | [[package]] 975 | name = "tracing-subscriber" 976 | version = "0.3.19" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 979 | dependencies = [ 980 | "matchers", 981 | "nu-ansi-term", 982 | "once_cell", 983 | "regex", 984 | "sharded-slab", 985 | "smallvec", 986 | "thread_local", 987 | "tracing", 988 | "tracing-core", 989 | "tracing-log", 990 | ] 991 | 992 | [[package]] 993 | name = "tracing-test" 994 | version = "0.2.5" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" 997 | dependencies = [ 998 | "tracing-core", 999 | "tracing-subscriber", 1000 | "tracing-test-macro", 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "tracing-test-macro" 1005 | version = "0.2.5" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" 1008 | dependencies = [ 1009 | "quote", 1010 | "syn 2.0.100", 1011 | ] 1012 | 1013 | [[package]] 1014 | name = "unicode-ident" 1015 | version = "1.0.18" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1018 | 1019 | [[package]] 1020 | name = "utf8parse" 1021 | version = "0.2.2" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1024 | 1025 | [[package]] 1026 | name = "valuable" 1027 | version = "0.1.1" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 1030 | 1031 | [[package]] 1032 | name = "value-bag" 1033 | version = "1.11.1" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" 1036 | 1037 | [[package]] 1038 | name = "wasi" 1039 | version = "0.11.0+wasi-snapshot-preview1" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1042 | 1043 | [[package]] 1044 | name = "wasi" 1045 | version = "0.14.2+wasi-0.2.4" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1048 | dependencies = [ 1049 | "wit-bindgen-rt", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "wasm-bindgen" 1054 | version = "0.2.100" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1057 | dependencies = [ 1058 | "cfg-if", 1059 | "once_cell", 1060 | "rustversion", 1061 | "wasm-bindgen-macro", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "wasm-bindgen-backend" 1066 | version = "0.2.100" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1069 | dependencies = [ 1070 | "bumpalo", 1071 | "log", 1072 | "proc-macro2", 1073 | "quote", 1074 | "syn 2.0.100", 1075 | "wasm-bindgen-shared", 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "wasm-bindgen-futures" 1080 | version = "0.4.50" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1083 | dependencies = [ 1084 | "cfg-if", 1085 | "js-sys", 1086 | "once_cell", 1087 | "wasm-bindgen", 1088 | "web-sys", 1089 | ] 1090 | 1091 | [[package]] 1092 | name = "wasm-bindgen-macro" 1093 | version = "0.2.100" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1096 | dependencies = [ 1097 | "quote", 1098 | "wasm-bindgen-macro-support", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "wasm-bindgen-macro-support" 1103 | version = "0.2.100" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1106 | dependencies = [ 1107 | "proc-macro2", 1108 | "quote", 1109 | "syn 2.0.100", 1110 | "wasm-bindgen-backend", 1111 | "wasm-bindgen-shared", 1112 | ] 1113 | 1114 | [[package]] 1115 | name = "wasm-bindgen-shared" 1116 | version = "0.2.100" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1119 | dependencies = [ 1120 | "unicode-ident", 1121 | ] 1122 | 1123 | [[package]] 1124 | name = "web-sys" 1125 | version = "0.3.77" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1128 | dependencies = [ 1129 | "js-sys", 1130 | "wasm-bindgen", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "winapi" 1135 | version = "0.3.9" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1138 | dependencies = [ 1139 | "winapi-i686-pc-windows-gnu", 1140 | "winapi-x86_64-pc-windows-gnu", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "winapi-i686-pc-windows-gnu" 1145 | version = "0.4.0" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1148 | 1149 | [[package]] 1150 | name = "winapi-x86_64-pc-windows-gnu" 1151 | version = "0.4.0" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1154 | 1155 | [[package]] 1156 | name = "windows-sys" 1157 | version = "0.59.0" 1158 | source = "registry+https://github.com/rust-lang/crates.io-index" 1159 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1160 | dependencies = [ 1161 | "windows-targets", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "windows-targets" 1166 | version = "0.52.6" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1169 | dependencies = [ 1170 | "windows_aarch64_gnullvm", 1171 | "windows_aarch64_msvc", 1172 | "windows_i686_gnu", 1173 | "windows_i686_gnullvm", 1174 | "windows_i686_msvc", 1175 | "windows_x86_64_gnu", 1176 | "windows_x86_64_gnullvm", 1177 | "windows_x86_64_msvc", 1178 | ] 1179 | 1180 | [[package]] 1181 | name = "windows_aarch64_gnullvm" 1182 | version = "0.52.6" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1185 | 1186 | [[package]] 1187 | name = "windows_aarch64_msvc" 1188 | version = "0.52.6" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1191 | 1192 | [[package]] 1193 | name = "windows_i686_gnu" 1194 | version = "0.52.6" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1197 | 1198 | [[package]] 1199 | name = "windows_i686_gnullvm" 1200 | version = "0.52.6" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1203 | 1204 | [[package]] 1205 | name = "windows_i686_msvc" 1206 | version = "0.52.6" 1207 | source = "registry+https://github.com/rust-lang/crates.io-index" 1208 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1209 | 1210 | [[package]] 1211 | name = "windows_x86_64_gnu" 1212 | version = "0.52.6" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1215 | 1216 | [[package]] 1217 | name = "windows_x86_64_gnullvm" 1218 | version = "0.52.6" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1221 | 1222 | [[package]] 1223 | name = "windows_x86_64_msvc" 1224 | version = "0.52.6" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1227 | 1228 | [[package]] 1229 | name = "wit-bindgen-rt" 1230 | version = "0.39.0" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1233 | dependencies = [ 1234 | "bitflags", 1235 | ] 1236 | 1237 | [[package]] 1238 | name = "zerocopy" 1239 | version = "0.8.24" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 1242 | dependencies = [ 1243 | "zerocopy-derive", 1244 | ] 1245 | 1246 | [[package]] 1247 | name = "zerocopy-derive" 1248 | version = "0.8.24" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 1251 | dependencies = [ 1252 | "proc-macro2", 1253 | "quote", 1254 | "syn 2.0.100", 1255 | ] 1256 | -------------------------------------------------------------------------------- /messenger/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "messenger" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | async-std = { version = "1.11.0", features = ["attributes", "unstable"] } 10 | backoff = { version = "0.4.0", features = ["async-std"] } 11 | clap = { version = "4", features = ["derive"] } 12 | futures = "0.3.21" 13 | sd-notify = "0.4.0" 14 | tracing = "0.1.33" 15 | tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } 16 | 17 | [dev-dependencies] 18 | tempfile = "3.3.0" 19 | tracing-test = "0.2.1" 20 | -------------------------------------------------------------------------------- /messenger/default.nix: -------------------------------------------------------------------------------- 1 | { lib 2 | , rustPlatform 3 | , nix-gitignore 4 | }: 5 | rustPlatform.buildRustPackage rec{ 6 | pname = "messenger"; 7 | version = (lib.importTOML ./Cargo.toml).package.version; 8 | 9 | src = nix-gitignore.gitignoreSourcePure [ 10 | "!target" 11 | ] ./.; 12 | 13 | cargoLock.lockFile = ./Cargo.lock; 14 | } 15 | -------------------------------------------------------------------------------- /messenger/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{self, BufRead, BufReader}; 3 | use std::os::unix::process::ExitStatusExt; 4 | use std::path::PathBuf; 5 | 6 | use async_std::process::Command; 7 | use backoff::ExponentialBackoff; 8 | use clap::Parser; 9 | use futures::future::FutureExt; 10 | use sd_notify::NotifyState; 11 | use tracing::{error, info, trace}; 12 | use tracing_subscriber::filter::{EnvFilter, LevelFilter, Targets}; 13 | use tracing_subscriber::{fmt, prelude::*}; 14 | 15 | type Result> = core::result::Result; 16 | 17 | /// MESSENGER 18 | #[derive(Parser)] 19 | struct Cli { 20 | /// The path to the vault binary that will run an agent. 21 | #[clap(long)] 22 | vault_binary: PathBuf, 23 | 24 | /// The path to the vault agent's config. 25 | #[clap(long)] 26 | agent_config: PathBuf, 27 | 28 | /// The path to a file containing a list of files to wait to appear. 29 | #[clap(long)] 30 | files_to_monitor: PathBuf, 31 | 32 | /// The verbosity level of the logging. 33 | #[clap(long, short, action = clap::ArgAction::Count)] 34 | verbosity: u8, 35 | } 36 | 37 | #[async_std::main] 38 | async fn main() -> Result<()> { 39 | let cli = Cli::parse(); 40 | 41 | let crate_filter = Targets::default().with_target(env!("CARGO_PKG_NAME"), LevelFilter::TRACE); 42 | let env_filter = EnvFilter::builder() 43 | .with_default_directive( 44 | match cli.verbosity { 45 | 0 => LevelFilter::WARN, 46 | 1 => LevelFilter::INFO, 47 | 2 => LevelFilter::DEBUG, 48 | _ => LevelFilter::TRACE, 49 | } 50 | .into(), 51 | ) 52 | .from_env_lossy(); 53 | 54 | tracing_subscriber::registry() 55 | .with(fmt::layer()) 56 | .with(env_filter) 57 | .with(crate_filter) 58 | .try_init()?; 59 | 60 | let mut command = Command::new(cli.vault_binary); 61 | command.arg("agent"); 62 | command.arg("-config"); 63 | command.arg(cli.agent_config); 64 | 65 | trace!(?command, "spawning vault agent"); 66 | match command.spawn() { 67 | Ok(mut child) => { 68 | let files: Vec = self::get_files_to_monitor(cli.files_to_monitor)?; 69 | 70 | // TODO: maybe make the agent run something that will signal the messenger that the files exist, instead of waiting for them: 71 | // Something to consider is the agent could run something to signal messenger instead of waiting for the files to exist. 72 | // Then the messenger could restart etc. the target services only if it has finished startup. 73 | let backoff = self::backoff_until_files_exist(files).fuse(); 74 | let status = child.status().fuse(); 75 | 76 | futures::pin_mut!(backoff, status); 77 | 78 | loop { 79 | futures::select! { 80 | backoff = backoff => { 81 | if let Err(err) = backoff { 82 | error!(%err, "backoff failed"); 83 | let _ = child.kill(); 84 | std::process::exit(1); 85 | } 86 | }, 87 | status = status => { 88 | let status = status?; 89 | let mut status_msg = String::from("vault agent exited"); 90 | 91 | let errno = if let Some(errno) = status.code() { 92 | status_msg.push_str(&format!(" with code {}", errno)); 93 | errno 94 | } else if let Some(signal) = status.signal() { 95 | status_msg.push_str(&format!(" with signal {}", signal)); 96 | signal 97 | } else { 98 | status_msg.push_str(" with unknown cause (not exit code or signal)"); 99 | 1 100 | }; 101 | 102 | error!(%status_msg); 103 | sd_notify::notify(false, &[NotifyState::Status(&status_msg)])?; 104 | std::process::exit(errno); 105 | }, 106 | complete => break, 107 | }; 108 | } 109 | } 110 | Err(err) => { 111 | error!(?command, "failed to spawn vault agent"); 112 | error!(%err); 113 | sd_notify::notify(false, &[NotifyState::Status("failed to spawn vault agent")])?; 114 | std::process::exit(1); 115 | } 116 | } 117 | 118 | Ok(()) 119 | } 120 | 121 | /// Reads the file at `path` and constructs a `Vec` from the files 122 | /// listed on each line. 123 | fn get_files_to_monitor(path: PathBuf) -> Result> { 124 | info!("reading {} to find files to monitor", path.display()); 125 | let atlas = File::open(path)?; 126 | let reader = BufReader::new(atlas); 127 | let mut files = Vec::new(); 128 | 129 | for line in reader.lines() { 130 | let line = match line { 131 | Ok(line) if !line.is_empty() => line, 132 | Ok(_) => continue, 133 | Err(err) => { 134 | error!("while reading line: {:?}", err); 135 | continue; 136 | } 137 | }; 138 | 139 | trace!("adding {} to list of files to monitor", line); 140 | let file = PathBuf::from(line); 141 | files.push(file); 142 | } 143 | 144 | Ok(files) 145 | } 146 | 147 | /// Checks if the files specified by the input `&Vec` exist and returns 148 | /// a `Vec` of files that don't. 149 | fn check_if_files_exist(files: &[PathBuf]) -> Vec { 150 | let mut not_exists = Vec::new(); 151 | 152 | for path in files { 153 | trace!("checking if {} exists", path.display()); 154 | 155 | if path.exists() { 156 | trace!("{} exists", path.display()); 157 | } else { 158 | trace!("{} does not exist", path.display()); 159 | not_exists.push(path.clone()); 160 | } 161 | } 162 | 163 | not_exists 164 | } 165 | 166 | /// Uses [`backoff::ExponentialBackoff`] to wait for all listed files to exist, 167 | /// up to a maximum of 15 minutes. Uses [`sd_notify::notify`] to inform systemd 168 | /// if the files existed before the backoff hit its maximum. 169 | async fn backoff_until_files_exist(paths: Vec) -> async_std::io::Result<()> { 170 | info!("waiting for all files to exist"); 171 | 172 | async fn backoff_waiter(not_exists: Vec) -> Result<(), backoff::Error<&'static str>> { 173 | let not_exists = self::check_if_files_exist(¬_exists); 174 | 175 | if not_exists.is_empty() { 176 | info!("all files exist"); 177 | Ok(()) 178 | } else { 179 | info!("still waiting for some files to exist: {:?}", not_exists); 180 | Err(backoff::Error::transient( 181 | "still waiting for some files to exist", 182 | )) 183 | } 184 | } 185 | 186 | let backoff_notify = |err, dur| { 187 | let _ = err; 188 | info!("backing off for {:?}", dur); 189 | }; 190 | 191 | // Backs off to a maximum interval of 1 minute, and a maximum elapsed time of 15 minutes. 192 | let backoff = ExponentialBackoff::default(); 193 | let ret = 194 | backoff::future::retry_notify(backoff, || backoff_waiter(paths.clone()), backoff_notify) 195 | .await 196 | .map_err(|_| { 197 | io::Error::new( 198 | io::ErrorKind::Other, 199 | "files did not exist in a timely fashion", 200 | ) 201 | }); 202 | 203 | if ret.is_ok() { 204 | trace!("backoff succeeded, notifying systemd we're ready"); 205 | sd_notify::notify(false, &[NotifyState::Ready])?; 206 | } 207 | 208 | ret 209 | } 210 | 211 | #[cfg(test)] 212 | mod tests { 213 | use tracing_test::traced_test; 214 | 215 | #[test] 216 | #[traced_test] 217 | fn test_check_if_files_exist() { 218 | let temp_dir = tempfile::tempdir().unwrap(); 219 | let files = vec![ 220 | temp_dir.path().join("file1"), 221 | temp_dir.path().join("file2"), 222 | temp_dir.path().join("file3"), 223 | ]; 224 | 225 | let not_exist = super::check_if_files_exist(&files); 226 | assert_eq!(files, not_exist); 227 | 228 | for file in &files { 229 | std::fs::File::create(file).unwrap(); 230 | } 231 | 232 | let not_exist = super::check_if_files_exist(&files); 233 | assert!(not_exist.is_empty()); 234 | } 235 | 236 | #[test] 237 | #[traced_test] 238 | fn test_backoff_until_files_exist() { 239 | let temp_dir = tempfile::tempdir().unwrap(); 240 | let files = vec![ 241 | temp_dir.path().join("file1"), 242 | temp_dir.path().join("file2"), 243 | temp_dir.path().join("file3"), 244 | ]; 245 | 246 | let backoff_files = files.clone(); 247 | let backoff_handle = 248 | async_std::task::spawn(super::backoff_until_files_exist(backoff_files)); 249 | 250 | // Wait for 50ms so that the backoff functionality has time to see that 251 | // the files don't exist and wait at least once. 252 | std::thread::sleep(std::time::Duration::from_millis(50)); 253 | 254 | for file in &files { 255 | std::fs::File::create(file).unwrap(); 256 | } 257 | 258 | assert!(async_std::task::block_on(backoff_handle).is_ok()); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /module/definition.nix: -------------------------------------------------------------------------------- 1 | { lib, config, ... }: 2 | let 3 | inherit (lib) 4 | mkOption 5 | mkEnableOption 6 | types; 7 | 8 | inherit (import ./helpers.nix { inherit lib; }) 9 | mkScopedMerge 10 | secretFilesRoot 11 | ; 12 | 13 | autoAuthMethodModule = types.submodule { 14 | freeformType = types.attrsOf types.unspecified; 15 | 16 | options = { 17 | type = mkOption { 18 | type = types.str; 19 | }; 20 | 21 | config = mkOption { 22 | type = types.attrsOf types.unspecified; 23 | }; 24 | }; 25 | }; 26 | 27 | autoAuthModule = types.submodule { 28 | freeformType = types.attrsOf types.unspecified; 29 | 30 | options = { 31 | method = mkOption { 32 | type = types.listOf autoAuthMethodModule; 33 | default = [ ]; 34 | }; 35 | }; 36 | }; 37 | 38 | templateConfigModule = types.submodule { 39 | freeformType = types.attrsOf types.unspecified; 40 | 41 | options = { 42 | exit_on_retry_failure = mkOption { 43 | type = types.bool; 44 | default = true; 45 | }; 46 | }; 47 | }; 48 | 49 | agentConfigType = types.submodule { 50 | freeformType = types.attrsOf types.unspecified; 51 | 52 | options = { 53 | auto_auth = mkOption { 54 | type = autoAuthModule; 55 | default = { }; 56 | }; 57 | 58 | template_config = mkOption { 59 | type = templateConfigModule; 60 | default = { }; 61 | }; 62 | }; 63 | }; 64 | 65 | vaultAgentModule = { ... }: { 66 | options = { 67 | enable = mkEnableOption "vaultAgent"; 68 | 69 | agentConfig = mkOption { 70 | description = "Vault agent configuration. The only place to specify vault and auto_auth config."; 71 | type = agentConfigType; 72 | default = config.detsys.vaultAgent.defaultAgentConfig; 73 | }; 74 | 75 | # !!! should this be a submodule? 76 | environment = { 77 | changeAction = mkOption { 78 | description = "What to do if any secrets in the environment change."; 79 | type = types.enum [ 80 | "none" 81 | "restart" 82 | "stop" 83 | ]; 84 | default = "restart"; 85 | }; 86 | 87 | templateFiles = mkOption { 88 | type = types.attrsOf (types.submodule vaultAgentEnvironmentFileModule); 89 | default = { }; 90 | }; 91 | 92 | template = mkOption { 93 | description = "A consul-template snippet which produces EnvironmentFile-compatible output."; 94 | type = types.nullOr types.lines; 95 | default = null; 96 | }; 97 | 98 | perms = mkOption { 99 | readOnly = true; 100 | internal = true; 101 | description = "The octal mode of the environment file as a string."; 102 | type = types.str; 103 | default = "0400"; 104 | }; 105 | }; 106 | 107 | # !!! should this be a submodule? 108 | secretFiles = { 109 | defaultChangeAction = mkOption { 110 | description = '' 111 | What to do if any secrets in the files change. 112 | Provides the default value, and is overridable per secret file. 113 | ''; 114 | type = types.enum [ 115 | "none" 116 | "reload" 117 | "restart" 118 | "stop" 119 | ]; 120 | default = "restart"; 121 | }; 122 | 123 | files = mkOption { 124 | type = types.attrsOf (types.submodule vaultAgentSecretFilesModule); 125 | default = { }; 126 | }; 127 | }; 128 | }; 129 | }; 130 | 131 | vaultAgentEnvironmentFileModule = { ... }: { 132 | options = { 133 | file = mkOption { 134 | description = "A consul-template file which produces EnvironmentFile-compatible output."; 135 | type = types.path; 136 | }; 137 | 138 | perms = mkOption { 139 | readOnly = true; 140 | internal = true; 141 | description = "The octal mode of the environment file as a string."; 142 | type = types.str; 143 | default = "0400"; 144 | }; 145 | }; 146 | }; 147 | 148 | vaultAgentSecretFilesModule = { name, ... }: { 149 | options = { 150 | changeAction = mkOption { 151 | description = '' 152 | What to do if any secrets in this file changes. 153 | If left unspecified, the defaultChangeAction for this service takes effect. 154 | ''; 155 | type = types.nullOr (types.enum [ 156 | "none" 157 | "reload" 158 | "restart" 159 | "stop" 160 | ]); 161 | default = null; 162 | }; 163 | 164 | templateFile = mkOption { 165 | description = "A consul-template file. Conflicts with template."; 166 | type = types.nullOr types.path; 167 | default = null; 168 | }; 169 | 170 | template = mkOption { 171 | description = "A consul-template snippet. Conflicts with templateFile."; 172 | type = types.nullOr types.lines; 173 | default = null; 174 | }; 175 | 176 | perms = mkOption { 177 | description = "The octal mode of the secret file as a string."; 178 | type = types.str; 179 | default = "0400"; 180 | }; 181 | 182 | path = mkOption { 183 | readOnly = true; 184 | description = "The path to the secret file inside the unit's namespace's PrivateTmp."; 185 | type = types.str; 186 | default = "${secretFilesRoot}${name}"; 187 | }; 188 | }; 189 | }; 190 | 191 | in 192 | { 193 | options.detsys.vaultAgent = { 194 | defaultAgentConfig = mkOption { 195 | description = "Default Vault agent configuration. Defers to individual agentConfigs, if set."; 196 | type = agentConfigType; 197 | default = { }; 198 | }; 199 | 200 | systemd.services = mkOption { 201 | type = types.attrsOf (types.submodule vaultAgentModule); 202 | default = { }; 203 | }; 204 | }; 205 | 206 | config = lib.mkMerge [ 207 | (mkScopedMerge [ [ "assertions" ] ] 208 | (lib.mapAttrsToList 209 | (serviceName: serviceConfig: { 210 | assertions = lib.flatten (lib.mapAttrsToList 211 | (secretFileName: secretFileConfig: [ 212 | { 213 | assertion = !(secretFileConfig.templateFile == null && secretFileConfig.template == null); 214 | message = "detsys.vaultAgent.systemd.services.${serviceName}.secretFiles.${secretFileName}: One of the 'templateFile' and 'template' options must be specified."; 215 | } 216 | { 217 | assertion = !(secretFileConfig.templateFile != null && secretFileConfig.template != null); 218 | message = "detsys.vaultAgent.systemd.services.${serviceName}.secretFiles.${secretFileName}: Both 'templateFile' and 'template' options are specified, but they are mutually exclusive."; 219 | } 220 | ]) 221 | serviceConfig.secretFiles.files); 222 | }) 223 | config.detsys.vaultAgent.systemd.services)) 224 | (mkScopedMerge [ [ "assertions" ] ] 225 | (lib.mapAttrsToList 226 | (serviceName: _serviceConfig: { 227 | assertions = [ 228 | { 229 | assertion = 230 | let 231 | systemdServiceConfig = config.systemd.services."${serviceName}".serviceConfig; 232 | in 233 | !(systemdServiceConfig ? PrivateTmp && !systemdServiceConfig.PrivateTmp); 234 | message = '' 235 | detsys.vaultAgent.systemd.services.${serviceName}: 236 | The specified service has PrivateTmp= (systemd.exec(5)) disabled, but it must 237 | be enabled to share secrets between the sidecar service and the infected service. 238 | ''; 239 | } 240 | ]; 241 | }) 242 | config.detsys.vaultAgent.systemd.services)) 243 | (mkScopedMerge [ [ "assertions" ] ] 244 | (lib.mapAttrsToList 245 | (serviceName: serviceConfig: { 246 | assertions = [ 247 | { 248 | assertion = 249 | serviceConfig.agentConfig.template_config.exit_on_retry_failure; 250 | message = '' 251 | detsys.vaultAgent.systemd.services.${serviceName}: 252 | The agent config has template_config.exit_on_retry_failure 253 | set to false. This is not supported. 254 | ''; 255 | } 256 | ]; 257 | }) 258 | config.detsys.vaultAgent.systemd.services)) 259 | { 260 | assertions = [ 261 | { 262 | assertion = 263 | config.detsys.vaultAgent.defaultAgentConfig.template_config.exit_on_retry_failure; 264 | message = '' 265 | detsys.vaultAgent.defaultAgentConfig: 266 | The default agent config has template_config.exit_on_retry_failure 267 | set to false. This is not supported. 268 | ''; 269 | } 270 | ]; 271 | } 272 | ]; 273 | } 274 | -------------------------------------------------------------------------------- /module/definition.tests.nix: -------------------------------------------------------------------------------- 1 | { nixpkgs, lib, ... }: 2 | let 3 | suite = { ... } @ tests: 4 | (builtins.mapAttrs 5 | (name: value: 6 | (builtins.trace "test case '${name}':" value)) 7 | tests); 8 | in 9 | with 10 | ( 11 | let 12 | evalCfg = config: 13 | (lib.evalModules { 14 | modules = [ 15 | "${nixpkgs}/nixos/modules/misc/assertions.nix" 16 | ./mock-systemd-module.nix 17 | ./definition.nix 18 | config 19 | ]; 20 | }).config; 21 | 22 | safeEval = val: 23 | (builtins.tryEval 24 | (builtins.deepSeq val val) 25 | ) // { originalValue = val; }; 26 | in 27 | { 28 | expectOk = cfg: 29 | let 30 | evaluatedCfg = evalCfg cfg; 31 | result = safeEval evaluatedCfg; 32 | 33 | filteredAsserts = builtins.map (asrt: asrt.message) (lib.filter (asrt: !asrt.assertion) result.value.assertions); 34 | in 35 | if !result.success 36 | then 37 | evaluatedCfg 38 | else if (filteredAsserts != [ ] || result.value.warnings != [ ]) 39 | then 40 | throw "Unexpected assertions or warnings. Assertions: ${builtins.toJSON filteredAsserts}. Warnings: ${builtins.toJSON result.value.warnings}" 41 | else 42 | "ok"; 43 | 44 | expectEvalError = cfg: 45 | let 46 | result = safeEval (evalCfg cfg); 47 | in 48 | if result.success 49 | then throw "Unexpectedly evaluated successfully." 50 | else "ok"; 51 | 52 | expectAssertsWarns = { assertions ? [ ], warnings ? [ ] }: cfg: 53 | let 54 | evaluatedCfg = evalCfg cfg; 55 | result = safeEval evaluatedCfg; 56 | 57 | expect = { 58 | inherit assertions warnings; 59 | }; 60 | actual = { 61 | assertions = builtins.map (asrt: asrt.message) (lib.filter (asrt: !asrt.assertion) result.value.assertions); 62 | inherit (result.value) warnings; 63 | }; 64 | in 65 | if !result.success 66 | then 67 | evaluatedCfg 68 | else if (expect != actual) 69 | then 70 | throw "Unexpected assertions or warnings.\nExpected: ${builtins.toJSON expect}\nGot: ${builtins.toJSON actual}" 71 | else 72 | "ok"; 73 | } 74 | ); 75 | suite { 76 | nothingSet = expectOk { 77 | systemd.services.nothing-set = { }; 78 | detsys.vaultAgent.systemd.services.nothing-set = { }; 79 | detsys.vaultAgent.defaultAgentConfig = { }; 80 | }; 81 | 82 | envTemplate = expectOk { 83 | systemd.services.env-template = { }; 84 | detsys.vaultAgent.systemd.services.env-template = { 85 | enable = true; 86 | 87 | environment.template = '' 88 | {{ with secret "postgresql/creds/hydra" }} 89 | HYDRA_DBI=dbi:Pg:dbname=hydra;host=the-database-server;username={{ .Data.username }};password={{ .Data.password }}; 90 | {{ end }} 91 | ''; 92 | }; 93 | }; 94 | 95 | envTemplateFile = expectOk { 96 | systemd.services.env-template-file = { }; 97 | detsys.vaultAgent.systemd.services.env-template-file = { 98 | enable = true; 99 | environment.templateFiles."example".file = ./example.ctmpl; 100 | }; 101 | }; 102 | 103 | envTemplateFileNone = expectEvalError { 104 | systemd.services.env-template-file = { }; 105 | detsys.vaultAgent.systemd.services.env-template-file = { 106 | enable = true; 107 | environment.templateFiles."example" = { }; 108 | }; 109 | }; 110 | 111 | secretTemplateFile = expectOk { 112 | systemd.services.secret-template-file = { }; 113 | detsys.vaultAgent.systemd.services.secret-template-file = { 114 | enable = true; 115 | secretFiles = { 116 | files."example".templateFile = ./example.ctmpl; 117 | }; 118 | }; 119 | }; 120 | 121 | secretTemplate = expectOk { 122 | systemd.services.secret-template = { }; 123 | detsys.vaultAgent.systemd.services.secret-template = { 124 | enable = true; 125 | secretFiles = { 126 | defaultChangeAction = "reload"; 127 | files."example".template = '' 128 | ... 129 | ''; 130 | }; 131 | }; 132 | }; 133 | 134 | secretNoTemplate = expectAssertsWarns 135 | { 136 | assertions = [ 137 | "detsys.vaultAgent.systemd.services.secret-template.secretFiles.example: One of the 'templateFile' and 'template' options must be specified." 138 | ]; 139 | } 140 | { 141 | systemd.services.secret-template = { }; 142 | detsys.vaultAgent.systemd.services.secret-template = { 143 | enable = true; 144 | secretFiles = { 145 | defaultChangeAction = "reload"; 146 | files."example" = { }; 147 | }; 148 | }; 149 | }; 150 | 151 | secretMutuallyExclusiveTemplates = expectAssertsWarns 152 | { 153 | assertions = [ 154 | "detsys.vaultAgent.systemd.services.secret-template.secretFiles.example: Both 'templateFile' and 'template' options are specified, but they are mutually exclusive." 155 | ]; 156 | } 157 | { 158 | systemd.services.secret-template = { }; 159 | detsys.vaultAgent.systemd.services.secret-template = { 160 | enable = true; 161 | secretFiles = { 162 | defaultChangeAction = "reload"; 163 | files."example" = { 164 | template = "hi"; 165 | templateFile = ./example.ctmpl; 166 | }; 167 | }; 168 | }; 169 | }; 170 | 171 | mainServiceDisablesPrivateTmp = 172 | expectAssertsWarns 173 | { 174 | assertions = [ 175 | '' 176 | detsys.vaultAgent.systemd.services.no-private-tmp: 177 | The specified service has PrivateTmp= (systemd.exec(5)) disabled, but it must 178 | be enabled to share secrets between the sidecar service and the infected service. 179 | '' 180 | ]; 181 | } 182 | { 183 | systemd.services.no-private-tmp.serviceConfig.PrivateTmp = false; 184 | detsys.vaultAgent.systemd.services.no-private-tmp = { 185 | enable = true; 186 | secretFiles = { 187 | defaultChangeAction = "reload"; 188 | files."example".template = '' 189 | ... 190 | ''; 191 | }; 192 | }; 193 | }; 194 | 195 | globalConfig = expectOk { 196 | systemd.services.global-config = { }; 197 | detsys.vaultAgent.systemd.services.global-config = { }; 198 | detsys.vaultAgent.defaultAgentConfig = { 199 | vault = { 200 | address = "http://127.0.0.1:8200"; 201 | retry.num_retries = 1; 202 | }; 203 | auto_auth = { 204 | method = [{ 205 | config = { 206 | remove_secret_id_file_after_reading = false; 207 | role_id_file_path = "/role_id"; 208 | secret_id_file_path = "/secret_id"; 209 | }; 210 | type = "approle"; 211 | }]; 212 | }; 213 | template_config = { 214 | static_secret_render_interval = "5s"; 215 | exit_on_retry_failure = true; 216 | }; 217 | }; 218 | }; 219 | 220 | globalConfigErr = 221 | expectAssertsWarns 222 | { 223 | assertions = [ 224 | '' 225 | detsys.vaultAgent.systemd.services.global-config: 226 | The agent config has template_config.exit_on_retry_failure 227 | set to false. This is not supported. 228 | '' 229 | '' 230 | detsys.vaultAgent.defaultAgentConfig: 231 | The default agent config has template_config.exit_on_retry_failure 232 | set to false. This is not supported. 233 | '' 234 | ]; 235 | } 236 | { 237 | systemd.services.global-config = { }; 238 | detsys.vaultAgent.systemd.services.global-config = { }; 239 | detsys.vaultAgent.defaultAgentConfig = { 240 | vault = { 241 | address = "http://127.0.0.1:8200"; 242 | retry.num_retries = 1; 243 | }; 244 | auto_auth = { 245 | method = [{ 246 | config = { 247 | remove_secret_id_file_after_reading = false; 248 | role_id_file_path = "/role_id"; 249 | secret_id_file_path = "/secret_id"; 250 | }; 251 | type = "approle"; 252 | }]; 253 | }; 254 | template_config = { 255 | static_secret_render_interval = "5s"; 256 | exit_on_retry_failure = false; 257 | }; 258 | }; 259 | }; 260 | 261 | noExitOnRetry = 262 | expectAssertsWarns 263 | { 264 | assertions = [ 265 | '' 266 | detsys.vaultAgent.systemd.services.test: 267 | The agent config has template_config.exit_on_retry_failure 268 | set to false. This is not supported. 269 | '' 270 | ]; 271 | } 272 | { 273 | systemd.services.test = { }; 274 | detsys.vaultAgent.systemd.services.test.agentConfig = { 275 | template_config.exit_on_retry_failure = false; 276 | }; 277 | }; 278 | } 279 | -------------------------------------------------------------------------------- /module/helpers.nix: -------------------------------------------------------------------------------- 1 | { lib }: 2 | rec { 3 | # This is ~safe because we require PrivateTmp to be true. 4 | secretFilesRoot = "/tmp/detsys-vault/"; 5 | environmentFilesRoot = "/run/keys/environment/"; 6 | 7 | mkScopedMerge = attrs: 8 | let 9 | pluckFunc = attr: values: lib.mkMerge 10 | (builtins.map 11 | (v: 12 | lib.mkIf 13 | (lib.hasAttrByPath attr v) 14 | (lib.getAttrFromPath attr v)) 15 | values); 16 | 17 | pluckFuncs = attrs: values: 18 | lib.mkMerge (builtins.map 19 | (attr: lib.setAttrByPath attr (pluckFunc attr values)) 20 | attrs); 21 | 22 | in 23 | pluckFuncs attrs; 24 | 25 | renderAgentConfig = targetService: targetServiceConfig: cfg: 26 | let 27 | mkCommand = requestedAction: 28 | let 29 | restartAction = { 30 | restart = "try-restart"; 31 | reload = "try-reload-or-restart"; 32 | stop = "stop"; 33 | }."${requestedAction}"; 34 | in 35 | if requestedAction == "none" 36 | then 37 | null 38 | else 39 | "systemctl ${restartAction} ${lib.escapeShellArg "${targetService}.service"}"; 40 | 41 | environmentFileTemplates = 42 | let 43 | changeCommand = mkCommand cfg.environment.changeAction; 44 | in 45 | (lib.optional (cfg.environment.template != null) 46 | ({ 47 | destination = "${environmentFilesRoot}${targetService}/EnvFile"; 48 | contents = cfg.environment.template; 49 | inherit (cfg.environment) perms; 50 | } // lib.optionalAttrs (changeCommand != null) { 51 | command = changeCommand; 52 | })) 53 | ++ (lib.mapAttrsToList 54 | (name: { file, perms }: 55 | ({ 56 | destination = "${environmentFilesRoot}${targetService}/${name}.EnvFile"; 57 | source = file; 58 | inherit perms; 59 | } // lib.optionalAttrs (changeCommand != null) { 60 | command = changeCommand; 61 | })) 62 | cfg.environment.templateFiles); 63 | 64 | secretFileTemplates = lib.mapAttrsToList 65 | (_name: { changeAction, templateFile, template, perms, path }: 66 | rec { 67 | command = 68 | let 69 | user = targetServiceConfig.serviceConfig.User or null; 70 | group = targetServiceConfig.serviceConfig.Group or null; 71 | escapedUser = lib.escapeShellArg user; 72 | escapedGroup = lib.escapeShellArg group; 73 | changeCommand = mkCommand (if changeAction != null then changeAction else cfg.secretFiles.defaultChangeAction); 74 | in 75 | builtins.concatStringsSep ";" 76 | ([ 77 | "chown ${lib.optionalString (user != null) escapedUser}:${lib.optionalString (group!= null) escapedGroup} ${lib.escapeShellArg destination}" 78 | ] ++ lib.optionals (changeCommand != null) [ 79 | changeCommand 80 | ]); 81 | destination = path; 82 | inherit perms; 83 | } 84 | // lib.optionalAttrs (template != null) { contents = template; } 85 | // lib.optionalAttrs (templateFile != null) { source = templateFile; } 86 | ) 87 | cfg.secretFiles.files; 88 | in 89 | { 90 | inherit 91 | environmentFileTemplates 92 | secretFileTemplates 93 | ; 94 | 95 | environmentFiles = builtins.map 96 | (tpl: tpl.destination) 97 | environmentFileTemplates; 98 | 99 | secretFiles = builtins.map 100 | (tpl: tpl.destination) 101 | secretFileTemplates; 102 | 103 | agentConfig = cfg.agentConfig // { 104 | template = environmentFileTemplates 105 | ++ secretFileTemplates; 106 | }; 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /module/helpers.tests.nix: -------------------------------------------------------------------------------- 1 | { nixpkgs, lib, ... }: 2 | let 3 | helpers = import ./helpers.nix { inherit lib; }; 4 | suite = { ... } @ tests: 5 | (builtins.mapAttrs 6 | (name: value: 7 | (builtins.trace "test case '${name}':" value)) 8 | tests); 9 | in 10 | with 11 | ( 12 | let 13 | evalCfg = config: 14 | (lib.evalModules { 15 | modules = [ 16 | "${nixpkgs}/nixos/modules/misc/assertions.nix" 17 | ./mock-systemd-module.nix 18 | ./definition.nix 19 | config 20 | ]; 21 | }).config; 22 | 23 | safeEval = val: 24 | (builtins.tryEval 25 | (builtins.deepSeq val val) 26 | ) // { originalValue = val; }; 27 | in 28 | { 29 | expectRenderedConfig = globalCfg: cfg: expect: 30 | let 31 | evaluatedCfg = evalCfg { 32 | systemd.services.example' = { }; 33 | detsys.vaultAgent.defaultAgentConfig = globalCfg; 34 | detsys.vaultAgent.systemd.services.example' = cfg; 35 | }; 36 | result = safeEval evaluatedCfg; 37 | 38 | filteredAsserts = builtins.map (asrt: asrt.message) (lib.filter (asrt: !asrt.assertion) result.value.assertions); 39 | 40 | actual = (helpers.renderAgentConfig "example'" { } result.value.detsys.vaultAgent.systemd.services.example').agentConfig; 41 | in 42 | if !result.success 43 | then 44 | evaluatedCfg 45 | else if (filteredAsserts != [ ] || result.value.warnings != [ ]) 46 | then 47 | throw "Unexpected assertions or warnings. Assertions: ${builtins.toJSON filteredAsserts}. Warnings: ${builtins.toJSON result.value.warnings}" 48 | else if actual != expect 49 | then 50 | throw "Mismatched configuration.\nExp: ${builtins.toJSON expect}\nGot: ${builtins.toJSON actual}" 51 | else "ok"; 52 | } 53 | ); 54 | { 55 | nothingSet = expectRenderedConfig 56 | { } 57 | { } 58 | { 59 | auto_auth.method = [ ]; 60 | template_config.exit_on_retry_failure = true; 61 | template = [ ]; 62 | }; 63 | 64 | environmentOnlyInline = expectRenderedConfig 65 | { } 66 | { 67 | environment.template = '' 68 | {{ with secret "postgresql/creds/hydra" }} 69 | HYDRA_DBI=dbi:Pg:dbname=hydra;host=the-database-server;username={{ .Data.username }};password={{ .Data.password }}; 70 | {{ end }} 71 | ''; 72 | } 73 | { 74 | auto_auth.method = [ ]; 75 | template_config.exit_on_retry_failure = true; 76 | template = [ 77 | { 78 | command = "systemctl try-restart 'example'\\''.service'"; 79 | destination = "${helpers.environmentFilesRoot}example'/EnvFile"; 80 | contents = '' 81 | {{ with secret "postgresql/creds/hydra" }} 82 | HYDRA_DBI=dbi:Pg:dbname=hydra;host=the-database-server;username={{ .Data.username }};password={{ .Data.password }}; 83 | {{ end }} 84 | ''; 85 | perms = "0400"; 86 | } 87 | ]; 88 | }; 89 | 90 | environmentOneFile = expectRenderedConfig 91 | { } 92 | { 93 | environment.templateFiles."example'-a".file = ./helpers.tests.nix; 94 | } 95 | { 96 | auto_auth.method = [ ]; 97 | template_config.exit_on_retry_failure = true; 98 | template = [ 99 | { 100 | command = "systemctl try-restart 'example'\\''.service'"; 101 | destination = "${helpers.environmentFilesRoot}example'/example\'-a.EnvFile"; 102 | source = ./helpers.tests.nix; 103 | perms = "0400"; 104 | } 105 | ]; 106 | }; 107 | 108 | environmentChangeStop = expectRenderedConfig 109 | { } 110 | { 111 | environment = { 112 | changeAction = "stop"; 113 | templateFiles."example'-a".file = ./helpers.tests.nix; 114 | }; 115 | } 116 | { 117 | auto_auth.method = [ ]; 118 | template_config.exit_on_retry_failure = true; 119 | template = [ 120 | { 121 | command = "systemctl stop 'example'\\''.service'"; 122 | destination = "${helpers.environmentFilesRoot}example'/example'-a.EnvFile"; 123 | source = ./helpers.tests.nix; 124 | perms = "0400"; 125 | } 126 | ]; 127 | }; 128 | 129 | environmentChangeNone = expectRenderedConfig 130 | { } 131 | { 132 | environment = { 133 | changeAction = "none"; 134 | templateFiles."example'-a".file = ./helpers.tests.nix; 135 | }; 136 | } 137 | { 138 | auto_auth.method = [ ]; 139 | template_config.exit_on_retry_failure = true; 140 | template = [ 141 | { 142 | destination = "${helpers.environmentFilesRoot}example'/example'-a.EnvFile"; 143 | source = ./helpers.tests.nix; 144 | perms = "0400"; 145 | } 146 | ]; 147 | }; 148 | 149 | environmentTwoFiles = expectRenderedConfig 150 | { } 151 | { 152 | environment.templateFiles = { 153 | "example\'-a".file = ./helpers.tests.nix; 154 | "example'-b".file = ./helpers.tests.nix; 155 | }; 156 | } 157 | { 158 | auto_auth.method = [ ]; 159 | template_config.exit_on_retry_failure = true; 160 | template = [ 161 | { 162 | command = "systemctl try-restart 'example'\\''.service'"; 163 | destination = "${helpers.environmentFilesRoot}example'/example\'-a.EnvFile"; 164 | source = ./helpers.tests.nix; 165 | perms = "0400"; 166 | } 167 | { 168 | command = "systemctl try-restart 'example'\\''.service'"; 169 | destination = "${helpers.environmentFilesRoot}example'/example'-b.EnvFile"; 170 | source = ./helpers.tests.nix; 171 | perms = "0400"; 172 | } 173 | ]; 174 | }; 175 | 176 | environmentInlineAndFiles = expectRenderedConfig 177 | { } 178 | { 179 | environment = { 180 | template = "FOO=BAR"; 181 | templateFiles."example\'-a".file = ./helpers.tests.nix; 182 | }; 183 | } 184 | { 185 | auto_auth.method = [ ]; 186 | template_config.exit_on_retry_failure = true; 187 | template = [ 188 | { 189 | command = "systemctl try-restart 'example'\\''.service'"; 190 | destination = "${helpers.environmentFilesRoot}example'/EnvFile"; 191 | contents = "FOO=BAR"; 192 | perms = "0400"; 193 | } 194 | { 195 | command = "systemctl try-restart 'example'\\''.service'"; 196 | destination = "${helpers.environmentFilesRoot}example'/example\'-a.EnvFile"; 197 | source = ./helpers.tests.nix; 198 | perms = "0400"; 199 | } 200 | ]; 201 | }; 202 | 203 | secretFileInline = expectRenderedConfig 204 | { } 205 | { 206 | secretFiles.files."example'".template = "FOO=BAR"; 207 | } 208 | { 209 | auto_auth.method = [ ]; 210 | template_config.exit_on_retry_failure = true; 211 | template = [ 212 | { 213 | command = "chown : '${helpers.secretFilesRoot}example'\\''';systemctl try-restart 'example'\\''.service'"; 214 | destination = "${helpers.secretFilesRoot}example\'"; 215 | contents = "FOO=BAR"; 216 | perms = "0400"; 217 | } 218 | ]; 219 | }; 220 | 221 | secretFileTemplate = expectRenderedConfig 222 | { } 223 | { 224 | secretFiles.files."example'".templateFile = ./helpers.tests.nix; 225 | } 226 | { 227 | auto_auth.method = [ ]; 228 | template_config.exit_on_retry_failure = true; 229 | template = [ 230 | { 231 | command = "chown : '${helpers.secretFilesRoot}example'\\''';systemctl try-restart 'example'\\''.service'"; 232 | destination = "${helpers.secretFilesRoot}example\'"; 233 | source = ./helpers.tests.nix; 234 | perms = "0400"; 235 | } 236 | ]; 237 | }; 238 | 239 | secretFileChangedDefaultChangeAction = expectRenderedConfig 240 | { } 241 | { 242 | secretFiles = { 243 | defaultChangeAction = "reload"; 244 | files."example'".template = "FOO=BAR"; 245 | }; 246 | } 247 | { 248 | auto_auth.method = [ ]; 249 | template_config.exit_on_retry_failure = true; 250 | template = [ 251 | { 252 | command = "chown : '${helpers.secretFilesRoot}example'\\''';systemctl try-reload-or-restart 'example'\\''.service'"; 253 | destination = "${helpers.secretFilesRoot}example'"; 254 | contents = "FOO=BAR"; 255 | perms = "0400"; 256 | } 257 | ]; 258 | }; 259 | 260 | secretFileChangedDefaultChangeActionOverride = expectRenderedConfig 261 | { } 262 | { 263 | secretFiles = { 264 | defaultChangeAction = "reload"; 265 | files."example\'-a".template = "FOO=BAR"; 266 | files."example'-b" = { 267 | changeAction = "restart"; 268 | template = "FOO=BAR"; 269 | perms = "0600"; 270 | }; 271 | }; 272 | } 273 | { 274 | auto_auth.method = [ ]; 275 | template_config.exit_on_retry_failure = true; 276 | template = [ 277 | { 278 | command = "chown : '${helpers.secretFilesRoot}example'\\''-a';systemctl try-reload-or-restart 'example'\\''.service'"; 279 | destination = "${helpers.secretFilesRoot}example\'-a"; 280 | contents = "FOO=BAR"; 281 | perms = "0400"; 282 | } 283 | { 284 | command = "chown : '${helpers.secretFilesRoot}example'\\''-b';systemctl try-restart 'example'\\''.service'"; 285 | destination = "${helpers.secretFilesRoot}example\'-b"; 286 | contents = "FOO=BAR"; 287 | perms = "0600"; 288 | } 289 | ]; 290 | }; 291 | 292 | agentConfig = expectRenderedConfig 293 | { } 294 | { 295 | agentConfig = { 296 | vault = { address = "http://127.0.0.1:8200"; }; 297 | auto_auth = { 298 | method = [{ 299 | config = { 300 | remove_secret_id_file_after_reading = false; 301 | role_id_file_path = "role_id"; 302 | secret_id_file_path = "secret_id"; 303 | }; 304 | type = "approle"; 305 | }]; 306 | }; 307 | }; 308 | secretFiles = { 309 | defaultChangeAction = "reload"; 310 | files."example\'-a".template = "FOO=BAR"; 311 | files."example'-b" = { 312 | changeAction = "restart"; 313 | template = "FOO=BAR"; 314 | perms = "0700"; 315 | }; 316 | }; 317 | } 318 | { 319 | vault = { address = "http://127.0.0.1:8200"; }; 320 | auto_auth = { 321 | method = [{ 322 | config = { 323 | remove_secret_id_file_after_reading = false; 324 | role_id_file_path = "role_id"; 325 | secret_id_file_path = "secret_id"; 326 | }; 327 | type = "approle"; 328 | }]; 329 | }; 330 | template_config.exit_on_retry_failure = true; 331 | template = [ 332 | { 333 | command = "chown : '${helpers.secretFilesRoot}example'\\''-a';systemctl try-reload-or-restart 'example'\\''.service'"; 334 | destination = "${helpers.secretFilesRoot}example\'-a"; 335 | contents = "FOO=BAR"; 336 | perms = "0400"; 337 | } 338 | { 339 | command = "chown : '${helpers.secretFilesRoot}example'\\''-b';systemctl try-restart 'example'\\''.service'"; 340 | destination = "${helpers.secretFilesRoot}example'-b"; 341 | contents = "FOO=BAR"; 342 | perms = "0700"; 343 | } 344 | ]; 345 | }; 346 | 347 | defaultAgentConfig = expectRenderedConfig 348 | { 349 | vault = { address = "http://127.0.0.1:8200"; }; 350 | auto_auth = { 351 | method = [{ 352 | config = { 353 | remove_secret_id_file_after_reading = false; 354 | role_id_file_path = "role_id"; 355 | secret_id_file_path = "secret_id"; 356 | }; 357 | type = "approle"; 358 | }]; 359 | }; 360 | } 361 | { 362 | secretFiles = { 363 | defaultChangeAction = "reload"; 364 | files."example\'-a".template = "FOO=BAR"; 365 | files."example'-b" = { 366 | changeAction = "restart"; 367 | template = "FOO=BAR"; 368 | perms = "0700"; 369 | }; 370 | }; 371 | } 372 | { 373 | vault = { address = "http://127.0.0.1:8200"; }; 374 | auto_auth = { 375 | method = [{ 376 | config = { 377 | remove_secret_id_file_after_reading = false; 378 | role_id_file_path = "role_id"; 379 | secret_id_file_path = "secret_id"; 380 | }; 381 | type = "approle"; 382 | }]; 383 | }; 384 | template_config.exit_on_retry_failure = true; 385 | template = [ 386 | { 387 | command = "chown : '${helpers.secretFilesRoot}example'\\''-a';systemctl try-reload-or-restart 'example'\\''.service'"; 388 | destination = "${helpers.secretFilesRoot}example\'-a"; 389 | contents = "FOO=BAR"; 390 | perms = "0400"; 391 | } 392 | { 393 | command = "chown : '${helpers.secretFilesRoot}example'\\''-b';systemctl try-restart 'example'\\''.service'"; 394 | destination = "${helpers.secretFilesRoot}example\'-b"; 395 | contents = "FOO=BAR"; 396 | perms = "0700"; 397 | } 398 | ]; 399 | }; 400 | } 401 | -------------------------------------------------------------------------------- /module/implementation.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, config, ... }: 2 | let 3 | inherit (import ./helpers.nix { inherit lib; }) 4 | mkScopedMerge 5 | renderAgentConfig 6 | secretFilesRoot 7 | environmentFilesRoot; 8 | 9 | precreateDirectories = serviceName: { user ? null, group ? null }: 10 | let 11 | userEscaped = lib.escapeShellArg (toString user); 12 | groupEscaped = lib.escapeShellArg (toString group); 13 | in 14 | pkgs.writeShellScript "precreate-dirs-for-${serviceName}" '' 15 | set -eux 16 | ( 17 | umask 027 18 | mkdir -p ${environmentFilesRoot} 19 | 20 | mkdir -p ${secretFilesRoot} 21 | chown ${lib.optionalString (user != null) userEscaped}:${lib.optionalString (group != null) groupEscaped} ${secretFilesRoot} 22 | ) 23 | ''; 24 | 25 | makeAgentService = { serviceName, agentConfig, systemdUnitConfig }: 26 | let 27 | fullServiceName = "${serviceName}.service"; 28 | agentCfgFile = pkgs.writeText "detsys-vaultAgent-${serviceName}.json" 29 | (builtins.toJSON agentConfig.agentConfig); 30 | systemdServiceConfig = systemdUnitConfig.serviceConfig; 31 | 32 | in 33 | { 34 | requires = [ "network.target" ]; 35 | after = [ "network.target" ]; 36 | 37 | wants = [ fullServiceName ]; 38 | before = [ fullServiceName ]; 39 | 40 | # Vault needs getent in PATH 41 | path = [ pkgs.getent ]; 42 | 43 | unitConfig = { 44 | StartLimitIntervalSec = lib.mkDefault 30; 45 | StartLimitBurst = lib.mkDefault 6; 46 | StopPropagatedFrom = [ fullServiceName ]; 47 | }; 48 | 49 | serviceConfig = { 50 | PrivateTmp = lib.mkDefault true; 51 | Restart = lib.mkDefault "on-failure"; 52 | RestartSec = lib.mkDefault 5; 53 | Type = "notify"; 54 | 55 | ExecStartPre = precreateDirectories serviceName 56 | (lib.optionalAttrs (systemdServiceConfig ? User) { user = systemdServiceConfig.User; } 57 | // lib.optionalAttrs (systemdServiceConfig ? Group) { group = systemdServiceConfig.Group; }); 58 | 59 | ExecStart = 60 | let 61 | filesToMonitor = pkgs.writeText "files-to-monitor" 62 | (builtins.concatStringsSep "\n" 63 | (map (path: path.destination) agentConfig.environmentFileTemplates 64 | ++ map (path: path.destination) agentConfig.secretFileTemplates)); 65 | in 66 | builtins.concatStringsSep " " [ 67 | "${pkgs.detsys-messenger}/bin/messenger" 68 | "--vault-binary" 69 | "${pkgs.vault}/bin/vault" 70 | "--agent-config" 71 | agentCfgFile 72 | "--files-to-monitor" 73 | filesToMonitor 74 | "-vvvv" 75 | ]; 76 | }; 77 | 78 | }; 79 | 80 | makeTargetServiceInfection = { serviceName, agentConfig }: 81 | let 82 | sidecarServiceName = "detsys-vaultAgent-${serviceName}.service"; 83 | in 84 | { 85 | after = [ sidecarServiceName ]; 86 | bindsTo = [ sidecarServiceName ]; 87 | unitConfig = { 88 | JoinsNamespaceOf = sidecarServiceName; 89 | }; 90 | serviceConfig = { 91 | PrivateTmp = lib.mkDefault true; 92 | EnvironmentFile = agentConfig.environmentFiles; 93 | }; 94 | }; 95 | in 96 | { 97 | imports = [ 98 | ./definition.nix 99 | ]; 100 | 101 | config = lib.mkMerge [ 102 | (mkScopedMerge [ [ "systemd" "services" ] ] 103 | (lib.mapAttrsToList 104 | (serviceName: serviceConfig: 105 | let 106 | agentConfig = renderAgentConfig serviceName config.systemd.services.${serviceName} serviceConfig; 107 | in 108 | { 109 | systemd.services = { 110 | "${serviceName}" = makeTargetServiceInfection { 111 | inherit serviceName agentConfig; 112 | }; 113 | "detsys-vaultAgent-${serviceName}" = makeAgentService { 114 | inherit serviceName agentConfig; 115 | systemdUnitConfig = config.systemd.services."${serviceName}"; 116 | }; 117 | }; 118 | }) 119 | config.detsys.vaultAgent.systemd.services)) 120 | ]; 121 | } 122 | -------------------------------------------------------------------------------- /module/implementation.tests.nix: -------------------------------------------------------------------------------- 1 | { nixpkgs, system, self, lib, ... }: 2 | let 3 | testTools = import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }; 4 | mkTest = name: config: testScript: 5 | testTools.simpleTest { 6 | inherit name testScript; 7 | nodes.machine = { pkgs, ... }: { 8 | imports = [ 9 | self.nixosModule 10 | config 11 | ]; 12 | 13 | nixpkgs.config.allowUnfree = true; 14 | 15 | environment.variables.VAULT_ADDR = "http://127.0.0.1:8200"; 16 | environment.systemPackages = [ pkgs.vault pkgs.getent ]; 17 | 18 | systemd.services.vault = { 19 | wantedBy = [ "default.target" ]; 20 | path = [ pkgs.getent ]; 21 | script = '' 22 | ${pkgs.vault}/bin/vault server -dev -dev-root-token-id=abc123 23 | ''; 24 | }; 25 | 26 | systemd.services.setup-vault = { 27 | wantedBy = [ "default.target" ]; 28 | after = [ "vault.service" ]; 29 | path = [ 30 | (pkgs.terraform_1.withPlugins (tf: [ 31 | tf.local 32 | tf.vault 33 | ])) 34 | ]; 35 | 36 | serviceConfig.Type = "oneshot"; 37 | 38 | script = '' 39 | set -eux 40 | 41 | cd / 42 | mkdir -p terraform 43 | cd terraform 44 | 45 | cp -r ${../terraform}/* ./ 46 | terraform init 47 | terraform apply -auto-approve 48 | ''; 49 | }; 50 | }; 51 | }; 52 | 53 | tests = [ 54 | (mkTest "basicEnvironment" 55 | ({ pkgs, ... }: { 56 | detsys.vaultAgent.systemd.services.example = { 57 | agentConfig = { 58 | vault = { address = "http://127.0.0.1:8200"; }; 59 | auto_auth = { 60 | method = [{ 61 | config = { 62 | remove_secret_id_file_after_reading = false; 63 | role_id_file_path = "/role_id"; 64 | secret_id_file_path = "/secret_id"; 65 | }; 66 | type = "approle"; 67 | }]; 68 | }; 69 | template_config = { 70 | static_secret_render_interval = "5s"; 71 | }; 72 | }; 73 | 74 | environment.template = '' 75 | {{ with secret "sys/tools/random/1" "format=base64" }} 76 | MY_SECRET={{ .Data.random_bytes }} 77 | {{ end }} 78 | ''; 79 | secretFiles.files."example".template = "hello"; 80 | }; 81 | systemd.services.example = { 82 | script = '' 83 | echo My secret is $MY_SECRET 84 | sleep infinity 85 | ''; 86 | }; 87 | }) 88 | '' 89 | machine.wait_for_file("/secret_id") 90 | machine.start_job("example") 91 | machine.wait_for_job("detsys-vaultAgent-example") 92 | print(machine.succeed("cat /run/keys/environment/example/EnvFile")) 93 | '') 94 | 95 | (mkTest "secretFile" 96 | ({ pkgs, ... }: { 97 | detsys.vaultAgent.systemd.services.example = { 98 | agentConfig = { 99 | vault = { address = "http://127.0.0.1:8200"; }; 100 | auto_auth = { 101 | method = [{ 102 | config = { 103 | remove_secret_id_file_after_reading = false; 104 | role_id_file_path = "/role_id"; 105 | secret_id_file_path = "/secret_id"; 106 | }; 107 | type = "approle"; 108 | }]; 109 | }; 110 | template_config = { 111 | static_secret_render_interval = "5s"; 112 | }; 113 | }; 114 | 115 | secretFiles.files."rand_bytes".template = '' 116 | {{ with secret "sys/tools/random/3" "format=base64" }} 117 | Have THREE random bytes from a templated string! {{ .Data.random_bytes }} 118 | {{ end }} 119 | ''; 120 | 121 | secretFiles.files."rand_bytes-v2".templateFile = 122 | let 123 | file = pkgs.writeText "rand_bytes-v2.tpl" '' 124 | {{ with secret "sys/tools/random/6" "format=base64" }} 125 | Have SIX random bytes, but from a template file! {{ .Data.random_bytes }} 126 | {{ end }} 127 | ''; 128 | in 129 | file; 130 | }; 131 | 132 | systemd.services.example = { 133 | script = '' 134 | cat /tmp/detsys-vault/rand_bytes 135 | cat /tmp/detsys-vault/rand_bytes-v2 136 | sleep infinity 137 | ''; 138 | }; 139 | }) 140 | '' 141 | print(machine.succeed("sleep 5; journalctl -u setup-vault")) 142 | machine.start_job("example") 143 | machine.wait_for_job("detsys-vaultAgent-example") 144 | print(machine.succeed("sleep 5; ls /run/keys")) 145 | print(machine.succeed("sleep 1; ls /tmp")) 146 | print(machine.succeed("sleep 1; systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true cat /tmp/detsys-vault/rand_bytes")) 147 | print(machine.succeed("sleep 1; systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true cat /tmp/detsys-vault/rand_bytes-v2")) 148 | print(machine.succeed("sleep 1; journalctl -u detsys-vaultAgent-example")) 149 | '') 150 | 151 | (mkTest "secretFileSlow" 152 | ({ pkgs, ... }: { 153 | detsys.vaultAgent.systemd.services.example = { 154 | agentConfig = { 155 | vault = { address = "http://127.0.0.1:8200"; }; 156 | auto_auth = { 157 | method = [{ 158 | config = { 159 | remove_secret_id_file_after_reading = false; 160 | role_id_file_path = "/role_id"; 161 | secret_id_file_path = "/secret_id"; 162 | }; 163 | type = "approle"; 164 | }]; 165 | }; 166 | template_config = { 167 | static_secret_render_interval = "5s"; 168 | }; 169 | }; 170 | 171 | secretFiles.files."slow" = { 172 | changeAction = "none"; 173 | template = '' 174 | {{ with secret "sys/tools/random/3" "format=base64" }} 175 | Have THREE random bytes from a templated string! {{ .Data.random_bytes }} 176 | {{ end }} 177 | ''; 178 | }; 179 | }; 180 | 181 | systemd.services.example = { 182 | script = '' 183 | while sleep 5 184 | do 185 | echo Reading a secret from a file that is constantly being overwritten: 186 | cat /tmp/detsys-vault/slow 187 | done 188 | 189 | sleep infinity 190 | ''; 191 | }; 192 | }) 193 | '' 194 | print(machine.succeed("sleep 5; journalctl -u setup-vault")) 195 | machine.start_job("example") 196 | machine.wait_for_job("detsys-vaultAgent-example") 197 | print(machine.succeed("sleep 5; ls /run/keys")) 198 | print(machine.succeed("sleep 1; ls /tmp")) 199 | print(machine.succeed("sleep 1; systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true cat /tmp/detsys-vault/slow")) 200 | print(machine.succeed("sleep 1; journalctl -u detsys-vaultAgent-example")) 201 | '') 202 | 203 | (mkTest "prometheus" 204 | ({ pkgs, ... }: { 205 | services.nginx = { 206 | enable = true; 207 | virtualHosts."localhost" = { 208 | basicAuthFile = "/tmp/detsys-vault/prometheus-basic-auth"; 209 | root = pkgs.writeTextDir "index.html" "

Hi

"; 210 | }; 211 | }; 212 | 213 | detsys.vaultAgent.systemd.services.nginx = { 214 | agentConfig = { 215 | vault = { address = "http://127.0.0.1:8200"; }; 216 | auto_auth = { 217 | method = [{ 218 | config = { 219 | remove_secret_id_file_after_reading = false; 220 | role_id_file_path = "/role_id"; 221 | secret_id_file_path = "/secret_id"; 222 | }; 223 | type = "approle"; 224 | }]; 225 | }; 226 | template_config = { 227 | static_secret_render_interval = "5s"; 228 | }; 229 | }; 230 | 231 | secretFiles.files."prometheus-basic-auth" = { 232 | changeAction = "none"; 233 | template = '' 234 | {{ with secret "internalservices/kv/monitoring/prometheus-basic-auth" }} 235 | {{ .Data.data.username }}:{{ .Data.data.htpasswd }} 236 | {{ end }} 237 | ''; 238 | }; 239 | }; 240 | }) 241 | '' 242 | machine.start_job("nginx") 243 | machine.wait_for_job("detsys-vaultAgent-nginx") 244 | machine.succeed("sleep 5") 245 | machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-nginx.service -p PrivateTmp=true cat /tmp/detsys-vault/prometheus-basic-auth") 246 | 247 | machine.wait_for_unit("nginx") 248 | machine.wait_for_open_port(80) 249 | print(machine.fail("curl --fail http://localhost")) 250 | print(machine.succeed("curl --fail http://test:test@localhost")) 251 | '') 252 | 253 | (mkTest "token" 254 | ({ pkgs, ... }: { 255 | systemd.services.example.script = '' 256 | echo Vault token with special perms is: 257 | cat /tmp/detsys-vault/token 258 | sleep infinity 259 | ''; 260 | 261 | detsys.vaultAgent.systemd.services.example = { 262 | agentConfig = { 263 | vault = { address = "http://127.0.0.1:8200"; }; 264 | auto_auth = { 265 | method = [{ 266 | config = { 267 | remove_secret_id_file_after_reading = false; 268 | role_id_file_path = "/role_id"; 269 | secret_id_file_path = "/secret_id"; 270 | }; 271 | type = "approle"; 272 | }]; 273 | }; 274 | }; 275 | 276 | secretFiles.files."token" = { 277 | changeAction = "none"; 278 | template = '' 279 | {{ with secret "auth/token/create" "policies=token" "no_default_policy=true" }}{{ .Auth.ClientToken }}{{ end }} 280 | ''; 281 | }; 282 | }; 283 | }) 284 | '' 285 | machine.start_job("example") 286 | machine.wait_for_job("detsys-vaultAgent-example") 287 | machine.wait_for_open_port(8200) 288 | machine.succeed("sleep 5") 289 | 290 | # NOTE: it's necessary to cat the token to a more accessible location (for 291 | # this test) because `$(cat /token)` gets run by the calling shell and not 292 | # the systemd-run spawned shell -- /tmp/detsys-vault/token only exists for 293 | # the systemd-run spawned shell, and there') no easy way to tell the shell 294 | # "don't evaluate this, let the spawned shell evaluate it" 295 | machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true cat /tmp/detsys-vault/token > /token") 296 | 297 | machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true -E VAULT_ADDR=http://127.0.0.1:8200/ -E VAULT_TOKEN=$(cat /token) -E PATH=$PATH -t -- vault kv get internalservices/kv/needs-token") 298 | machine.fail("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true -E VAULT_ADDR=http://127.0.0.1:8200/ -E VAULT_TOKEN=zzz -E PATH=$PATH -t -- vault kv get internalservices/kv/needs-token") 299 | '') 300 | 301 | (mkTest "perms" 302 | ({ pkgs, ... }: { 303 | detsys.vaultAgent.systemd.services.example = { 304 | agentConfig = { 305 | vault = { address = "http://127.0.0.1:8200"; }; 306 | auto_auth = { 307 | method = [{ 308 | type = "approle"; 309 | config = { 310 | remove_secret_id_file_after_reading = false; 311 | role_id_file_path = "/role_id"; 312 | secret_id_file_path = "/secret_id"; 313 | }; 314 | }]; 315 | }; 316 | }; 317 | 318 | environment.template = '' 319 | {{ with secret "sys/tools/random/9" "format=base64" }} 320 | NINE_BYTES={{ .Data.random_bytes }} 321 | {{ end }} 322 | ''; 323 | 324 | secretFiles.files."rand_bytes" = { 325 | perms = "642"; 326 | template = '' 327 | {{ with secret "sys/tools/random/3" "format=base64" }} 328 | Have THREE random bytes from a templated string! {{ .Data.random_bytes }} 329 | {{ end }} 330 | ''; 331 | }; 332 | 333 | secretFiles.files."rand_bytes-v2" = { 334 | perms = "400"; 335 | template = '' 336 | {{ with secret "sys/tools/random/6" "format=base64" }} 337 | Have SIX random bytes, also from a templated string! {{ .Data.random_bytes }} 338 | {{ end }} 339 | ''; 340 | }; 341 | }; 342 | 343 | services.nginx.enable = true; 344 | 345 | systemd.services.example = { 346 | serviceConfig = { 347 | User = "nginx"; 348 | Group = "nginx"; 349 | }; 350 | script = '' 351 | cat /tmp/detsys-vault/rand_bytes 352 | cat /tmp/detsys-vault/rand_bytes-v2 353 | echo Have NINE random bytes, from a templated EnvironmentFile! $NINE_BYTES 354 | sleep infinity 355 | ''; 356 | }; 357 | }) 358 | '' 359 | machine.wait_for_file("/role_id") 360 | machine.start_job("example") 361 | machine.wait_for_job("detsys-vaultAgent-example") 362 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true stat /tmp/detsys-vault/rand_bytes")) 363 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true stat /tmp/detsys-vault/rand_bytes-v2")) 364 | machine.succeed("sleep 1") 365 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true cat /tmp/detsys-vault/rand_bytes")) 366 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true stat /tmp/detsys-vault/rand_bytes")) 367 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true cat /tmp/detsys-vault/rand_bytes-v2")) 368 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true stat /tmp/detsys-vault/rand_bytes-v2")) 369 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true cat /run/keys/environment/example/EnvFile")) 370 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true stat /run/keys/environment/example/EnvFile")) 371 | '') 372 | 373 | (mkTest "multiEnvironment" 374 | ({ pkgs, ... }: { 375 | detsys.vaultAgent.systemd.services.example = { 376 | agentConfig = { 377 | vault = { address = "http://127.0.0.1:8200"; }; 378 | auto_auth = { 379 | method = [{ 380 | config = { 381 | remove_secret_id_file_after_reading = false; 382 | role_id_file_path = "/role_id"; 383 | secret_id_file_path = "/secret_id"; 384 | }; 385 | type = "approle"; 386 | }]; 387 | }; 388 | template_config = { 389 | static_secret_render_interval = "5s"; 390 | }; 391 | }; 392 | 393 | environment.template = '' 394 | {{ with secret "sys/tools/random/1" "format=base64" }} 395 | MY_SECRET_0={{ .Data.random_bytes }} 396 | {{ end }} 397 | ''; 398 | environment.templateFiles = { 399 | "a".file = pkgs.writeText "a" '' 400 | {{ with secret "sys/tools/random/2" "format=base64" }} 401 | MY_SECRET_A={{ .Data.random_bytes }} 402 | {{ end }} 403 | ''; 404 | "b".file = pkgs.writeText "b" '' 405 | {{ with secret "sys/tools/random/3" "format=base64" }} 406 | MY_SECRET_B={{ .Data.random_bytes }} 407 | {{ end }} 408 | ''; 409 | }; 410 | }; 411 | systemd.services.example = { 412 | script = '' 413 | echo My 0 secret is $MY_SECRET_0 414 | echo My a secret is $MY_SECRET_A 415 | echo My b secret is $MY_SECRET_B 416 | sleep infinity 417 | ''; 418 | }; 419 | }) 420 | '' 421 | machine.wait_for_file("/role_id") 422 | machine.start_job("example") 423 | machine.wait_for_job("detsys-vaultAgent-example") 424 | print(machine.succeed("cat /run/keys/environment/example/EnvFile")) 425 | print(machine.succeed("cat /run/keys/environment/example/a.EnvFile")) 426 | print(machine.succeed("cat /run/keys/environment/example/b.EnvFile")) 427 | '') 428 | 429 | (mkTest "delayedVault" 430 | ({ pkgs, lib, ... }: { 431 | systemd.services.vault.wantedBy = lib.mkForce [ ]; 432 | systemd.services.setup-vault.wantedBy = lib.mkForce [ ]; 433 | 434 | detsys.vaultAgent.systemd.services.example = { 435 | agentConfig = { 436 | vault = { 437 | address = "http://127.0.0.1:8200"; 438 | }; 439 | auto_auth = { 440 | method = [{ 441 | type = "approle"; 442 | config = { 443 | remove_secret_id_file_after_reading = false; 444 | role_id_file_path = "/role_id"; 445 | secret_id_file_path = "/secret_id"; 446 | }; 447 | }]; 448 | }; 449 | template_config = { 450 | static_secret_render_interval = "5s"; 451 | }; 452 | }; 453 | 454 | secretFiles.files."rand_bytes" = { 455 | perms = "0642"; 456 | template = '' 457 | {{ with secret "sys/tools/random/3" "format=base64" }} 458 | Have THREE random bytes from a templated string! {{ .Data.random_bytes }} 459 | {{ end }} 460 | ''; 461 | }; 462 | 463 | secretFiles.files."rand_bytes-v2" = { 464 | template = '' 465 | {{ with secret "sys/tools/random/6" "format=base64" }} 466 | Have SIX random bytes, also from a templated string! {{ .Data.random_bytes }} 467 | {{ end }} 468 | ''; 469 | }; 470 | }; 471 | 472 | systemd.services.example = { 473 | script = '' 474 | cat /tmp/detsys-vault/rand_bytes 475 | cat /tmp/detsys-vault/rand_bytes-v2 476 | sleep infinity 477 | ''; 478 | }; 479 | }) 480 | '' 481 | # NOTE: starting example will block until detsys-vaultAgent-example 482 | # succeeds, which won't happen until all the secret files exist (which 483 | # obviously won') happen until after vault is available) 484 | machine.systemctl("start --no-block example") 485 | machine.succeed("sleep 3") 486 | machine.succeed("pkill -f messenger") 487 | machine.start_job("vault") 488 | machine.start_job("setup-vault") 489 | machine.wait_for_file("/secret_id") 490 | machine.start_job("example") 491 | machine.wait_for_job("detsys-vaultAgent-example") 492 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true cat /tmp/detsys-vault/rand_bytes")) 493 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true stat /tmp/detsys-vault/rand_bytes")) 494 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true cat /tmp/detsys-vault/rand_bytes-v2")) 495 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true stat /tmp/detsys-vault/rand_bytes-v2")) 496 | '') 497 | 498 | (mkTest "failedSidecar" 499 | ({ pkgs, ... }: { 500 | detsys.vaultAgent.systemd.services.example = { 501 | agentConfig = { 502 | vault = { 503 | address = "http://127.0.0.1:8200"; 504 | retry.num_retries = 1; 505 | }; 506 | auto_auth = { 507 | method = [{ 508 | config = { 509 | remove_secret_id_file_after_reading = false; 510 | role_id_file_path = "/role_id"; 511 | secret_id_file_path = "/secret_id"; 512 | }; 513 | type = "approle"; 514 | }]; 515 | }; 516 | template_config = { 517 | static_secret_render_interval = "5s"; 518 | }; 519 | }; 520 | 521 | environment.template = '' 522 | {{ with secret "sys/tools/random/3" "format=base64" }} 523 | MY_SECRET={{ .Data.non-existent-with-hyphen }} 524 | {{ end }} 525 | ''; 526 | }; 527 | systemd.services.example = { 528 | script = '' 529 | echo My secret is $MY_SECRET 530 | sleep infinity 531 | ''; 532 | }; 533 | }) 534 | '' 535 | machine.wait_for_file("/secret_id") 536 | machine.systemctl("start --no-block example") 537 | machine.succeed("sleep 3") 538 | print(machine.fail("systemctl status detsys-vaultAgent-example")) 539 | substate=machine.succeed("systemctl show -p SubState --value example") 540 | if "dead" not in substate: 541 | raise Exception(f"unit shouldn't have even started if the sidecar unit failed, but had substate {substate}") 542 | '') 543 | 544 | (mkTest "defaultConfig" 545 | ({ pkgs, ... }: { 546 | detsys.vaultAgent.defaultAgentConfig = { 547 | vault = { address = "http://127.0.0.1:8200"; }; 548 | auto_auth = { 549 | method = [{ 550 | config = { 551 | remove_secret_id_file_after_reading = false; 552 | role_id_file_path = "/role_id"; 553 | secret_id_file_path = "/secret_id"; 554 | }; 555 | type = "approle"; 556 | }]; 557 | }; 558 | template_config = { 559 | static_secret_render_interval = "5s"; 560 | }; 561 | }; 562 | 563 | detsys.vaultAgent.systemd.services.example = { 564 | secretFiles.files."rand_bytes".template = '' 565 | {{ with secret "sys/tools/random/3" "format=base64" }} 566 | Have THREE random bytes from a templated string! {{ .Data.random_bytes }} 567 | {{ end }} 568 | ''; 569 | 570 | secretFiles.files."rand_bytes-v2".templateFile = 571 | let 572 | file = pkgs.writeText "rand_bytes-v2.tpl" '' 573 | {{ with secret "sys/tools/random/6" "format=base64" }} 574 | Have SIX random bytes, but from a template file! {{ .Data.random_bytes }} 575 | {{ end }} 576 | ''; 577 | in 578 | file; 579 | }; 580 | 581 | detsys.vaultAgent.systemd.services.example2 = { 582 | secretFiles.files."rand_bytes".template = '' 583 | {{ with secret "sys/tools/random/3" "format=base64" }} 584 | Have THREE random bytes from a templated string! {{ .Data.random_bytes }} 585 | {{ end }} 586 | ''; 587 | }; 588 | 589 | detsys.vaultAgent.systemd.services.example3 = { 590 | agentConfig = { 591 | vault = { address = "http://127.0.0.1:8200"; }; 592 | auto_auth = { 593 | method = [{ 594 | config = { 595 | remove_secret_id_file_after_reading = false; 596 | role_id_file_path = "/role_id"; 597 | secret_id_file_path = "/secret_id"; 598 | }; 599 | type = "approle"; 600 | }]; 601 | }; 602 | template_config = { 603 | static_secret_render_interval = "1s"; 604 | }; 605 | }; 606 | 607 | secretFiles.files."rand_bytes".template = '' 608 | {{ with secret "sys/tools/random/3" "format=base64" }} 609 | Have THREE random bytes from a templated string! {{ .Data.random_bytes }} 610 | {{ end }} 611 | ''; 612 | }; 613 | 614 | systemd.services.example = { 615 | script = '' 616 | cat /tmp/detsys-vault/rand_bytes 617 | cat /tmp/detsys-vault/rand_bytes-v2 618 | sleep infinity 619 | ''; 620 | }; 621 | 622 | systemd.services.example2 = { 623 | script = '' 624 | cat /tmp/detsys-vault/rand_bytes 625 | sleep infinity 626 | ''; 627 | }; 628 | 629 | systemd.services.example3 = { 630 | script = '' 631 | cat /tmp/detsys-vault/rand_bytes 632 | sleep infinity 633 | ''; 634 | }; 635 | }) 636 | '' 637 | machine.wait_for_file("/secret_id") 638 | machine.start_job("example") 639 | machine.wait_for_job("detsys-vaultAgent-example") 640 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true cat /tmp/detsys-vault/rand_bytes")) 641 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true cat /tmp/detsys-vault/rand_bytes-v2")) 642 | machine.start_job("example2") 643 | machine.wait_for_job("detsys-vaultAgent-example2") 644 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example2.service -p PrivateTmp=true cat /tmp/detsys-vault/rand_bytes")) 645 | machine.start_job("example3") 646 | machine.wait_for_job("detsys-vaultAgent-example3") 647 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example3.service -p PrivateTmp=true cat /tmp/detsys-vault/rand_bytes")) 648 | machine.succeed("sleep 2") 649 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example3.service -p PrivateTmp=true cat /tmp/detsys-vault/rand_bytes")) 650 | '') 651 | 652 | (mkTest "pathToSecret" 653 | ({ config, pkgs, ... }: { 654 | detsys.vaultAgent.defaultAgentConfig = { 655 | vault = { address = "http://127.0.0.1:8200"; }; 656 | auto_auth = { 657 | method = [{ 658 | config = { 659 | remove_secret_id_file_after_reading = false; 660 | role_id_file_path = "/role_id"; 661 | secret_id_file_path = "/secret_id"; 662 | }; 663 | type = "approle"; 664 | }]; 665 | }; 666 | template_config = { 667 | static_secret_render_interval = "5s"; 668 | }; 669 | }; 670 | 671 | detsys.vaultAgent.systemd.services.example = { 672 | secretFiles.files."rand_bytes".template = '' 673 | {{ with secret "sys/tools/random/3" "format=base64" }} 674 | Have THREE random bytes from a templated string! {{ .Data.random_bytes }} 675 | {{ end }} 676 | ''; 677 | 678 | secretFiles.files."rand_bytes-v2".templateFile = 679 | let 680 | file = pkgs.writeText "rand_bytes-v2.tpl" '' 681 | {{ with secret "sys/tools/random/6" "format=base64" }} 682 | Have SIX random bytes, but from a template file! {{ .Data.random_bytes }} 683 | {{ end }} 684 | ''; 685 | in 686 | file; 687 | }; 688 | 689 | systemd.services.example = { 690 | script = '' 691 | cat /tmp/detsys-vault/rand_bytes 692 | cat /tmp/detsys-vault/rand_bytes-v2 693 | sleep infinity 694 | ''; 695 | }; 696 | 697 | environment.etc."rand_bytes-path".text = config.detsys.vaultAgent.systemd.services.example.secretFiles.files."rand_bytes".path; 698 | environment.etc."rand_bytes-v2-path".text = config.detsys.vaultAgent.systemd.services.example.secretFiles.files."rand_bytes-v2".path; 699 | }) 700 | '' 701 | machine.wait_for_file("/secret_id") 702 | machine.start_job("example") 703 | machine.wait_for_job("detsys-vaultAgent-example") 704 | print(machine.succeed("cat /etc/rand_bytes-path")) 705 | print(machine.succeed("cat /etc/rand_bytes-v2-path")) 706 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true cat $(cat /etc/rand_bytes-path)")) 707 | print(machine.succeed("systemd-run -p JoinsNamespaceOf=detsys-vaultAgent-example.service -p PrivateTmp=true cat $(cat /etc/rand_bytes-v2-path)")) 708 | '') 709 | 710 | (mkTest "failAfterRetries" 711 | ({ pkgs, ... }: { 712 | detsys.vaultAgent.systemd.services.example = { 713 | agentConfig = { 714 | vault = { 715 | address = "http://127.0.0.1:8200"; 716 | retry.num_retries = 3; 717 | }; 718 | auto_auth = { 719 | method = [{ 720 | config = { 721 | remove_secret_id_file_after_reading = false; 722 | role_id_file_path = "/role_id"; 723 | secret_id_file_path = "/secret_id"; 724 | }; 725 | type = "approle"; 726 | }]; 727 | }; 728 | template_config = { 729 | static_secret_render_interval = "5s"; 730 | }; 731 | }; 732 | 733 | environment.template = '' 734 | {{ with secret "sys/tools/randomzzz" "format=base64" }} 735 | MY_SECRET={{ .Data.random_bytes }} 736 | {{ end }} 737 | ''; 738 | secretFiles.files."example".template = "hello"; 739 | }; 740 | systemd.services.example = { 741 | script = '' 742 | echo My secret is $MY_SECRET 743 | sleep infinity 744 | ''; 745 | }; 746 | }) 747 | '' 748 | machine.wait_for_file("/secret_id") 749 | machine.start_job("example") 750 | machine.succeed("sleep 10") 751 | print(machine.fail("systemctl status detsys-vaultAgent-example")) 752 | print(machine.fail("systemctl status example")) 753 | '') 754 | 755 | (mkTest "failOnStartup" 756 | ({ pkgs, ... }: { 757 | detsys.vaultAgent.systemd.services.example = { 758 | agentConfig = { 759 | vault = { 760 | address = "http://127.0.0.1:8200"; 761 | retry.num_retries = 3; 762 | }; 763 | auto_auth = { 764 | method = [{ 765 | config = { 766 | remove_secret_id_file_after_reading = false; 767 | role_id_file_path = "/role_id"; 768 | secret_id_file_path = "/secret_id"; 769 | }; 770 | type = "approle"; 771 | }]; 772 | }; 773 | template_config = { 774 | static_secret_render_interval = "5s"; 775 | }; 776 | }; 777 | 778 | environment.template = '' 779 | {{ with secret "sys/tools/random/1" "format=base64" }} 780 | MY_SECRET={{ .Data.random_bytes }} 781 | {{ end }} 782 | ''; 783 | }; 784 | systemd.services.example = { 785 | script = '' 786 | echo My secret is $MY_SECRET 787 | sleep infinity 788 | ''; 789 | }; 790 | }) 791 | '' 792 | machine.wait_for_file("/secret_id") 793 | machine.systemctl("start --no-block example") 794 | machine.execute("while ! pkill -f messenger; do sleep 0.1; done") 795 | print(machine.fail("systemctl status example")) 796 | print(machine.fail("systemctl status detsys-vaultAgent-example")) 797 | machine.succeed("sleep 3") 798 | print(machine.succeed("systemctl status example")) 799 | print(machine.succeed("systemctl status detsys-vaultAgent-example")) 800 | '') 801 | 802 | (mkTest "unitsStopEachOther" 803 | ({ pkgs, ... }: { 804 | detsys.vaultAgent.defaultAgentConfig = { 805 | vault = { address = "http://127.0.0.1:8200"; }; 806 | auto_auth = { 807 | method = [{ 808 | config = { 809 | remove_secret_id_file_after_reading = false; 810 | role_id_file_path = "/role_id"; 811 | secret_id_file_path = "/secret_id"; 812 | }; 813 | type = "approle"; 814 | }]; 815 | }; 816 | template_config = { 817 | static_secret_render_interval = "5s"; 818 | }; 819 | }; 820 | 821 | detsys.vaultAgent.systemd.services.example = { 822 | secretFiles.files."rand_bytes".template = '' 823 | {{ with secret "sys/tools/random/3" "format=base64" }} 824 | Have THREE random bytes from a templated string! {{ .Data.random_bytes }} 825 | {{ end }} 826 | ''; 827 | }; 828 | 829 | detsys.vaultAgent.systemd.services.example2 = { 830 | secretFiles.files."rand_bytes-v2".template = '' 831 | {{ with secret "sys/tools/random/6" "format=base64" }} 832 | Have SIX random bytes from a template string! {{ .Data.random_bytes }} 833 | {{ end }} 834 | ''; 835 | }; 836 | 837 | detsys.vaultAgent.systemd.services.example3 = { 838 | secretFiles.files."rand_bytes-v3".template = '' 839 | {{ with secret "sys/tools/random/9" "format=base64" }} 840 | Have NINE random bytes from a template string! {{ .Data.random_bytes }} 841 | {{ end }} 842 | ''; 843 | }; 844 | 845 | systemd.services.example = { 846 | script = '' 847 | cat /tmp/detsys-vault/rand_bytes 848 | ''; 849 | }; 850 | 851 | systemd.services.example2 = { 852 | script = '' 853 | cat /tmp/detsys-vault/rand_bytes-v2 854 | exit 1 855 | ''; 856 | }; 857 | 858 | systemd.services.example3 = { 859 | script = '' 860 | cat /tmp/detsys-vault/rand_bytes-v3 861 | sleep infinity 862 | ''; 863 | }; 864 | }) 865 | '' 866 | machine.wait_for_file("/secret_id") 867 | machine.start_job("example") 868 | machine.start_job("example2") 869 | machine.start_job("example3") 870 | machine.succeed("sleep 1") 871 | 872 | if "dead" not in machine.succeed("systemctl show -p SubState --value example"): 873 | raise Exception("full unit should have exited successfully") 874 | if "dead" not in machine.succeed("systemctl show -p SubState --value detsys-vaultAgent-example"): 875 | raise Exception("sidecar unit should also have exited successfully") 876 | 877 | if "failed" not in machine.succeed("systemctl show -p SubState --value example2"): 878 | raise Exception("full unit should have failed") 879 | if "dead" not in machine.succeed("systemctl show -p SubState --value detsys-vaultAgent-example2"): 880 | raise Exception("sidecar unit should have exited successfully because the full unit failed") 881 | 882 | machine.stop_job("detsys-vaultAgent-example3") 883 | if "dead" not in machine.succeed("systemctl show -p SubState --value detsys-vaultAgent-example3"): 884 | raise Exception("sidecar unit should have exited because it was stopped") 885 | if "dead" not in machine.succeed("systemctl show -p SubState --value example3"): 886 | raise Exception("full unit should have exited successfully because the sidecar was stopped") 887 | '') 888 | ]; 889 | in 890 | builtins.listToAttrs 891 | (map 892 | (test: { name = test.config.name; value = test; }) 893 | tests) 894 | -------------------------------------------------------------------------------- /module/mock-systemd-module.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | let 3 | inherit (lib) 4 | mkOption 5 | ; 6 | 7 | inherit (lib.types) 8 | attrsOf 9 | submodule 10 | anything 11 | ; 12 | in 13 | { 14 | options = { 15 | systemd.services = lib.mkOption { 16 | default = { }; 17 | type = attrsOf (submodule { 18 | options = { 19 | serviceConfig = lib.mkOption { 20 | default = { }; 21 | type = attrsOf anything; 22 | }; 23 | }; 24 | }); 25 | }; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /terraform/.gitignore: -------------------------------------------------------------------------------- 1 | *.terraform.lock* 2 | *.tfstate* 3 | *.terraform 4 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | provider "vault" { 2 | address = "http://127.0.0.1:8200" 3 | token = "abc123" 4 | } 5 | 6 | resource "vault_mount" "kv-v2" { 7 | path = "internalservices/kv" 8 | type = "kv-v2" 9 | } 10 | 11 | resource "vault_auth_backend" "approle" { 12 | type = "approle" 13 | 14 | tune { 15 | max_lease_ttl = "60s" 16 | default_lease_ttl = "30s" 17 | } 18 | } 19 | 20 | resource "vault_approle_auth_backend_role" "agent" { 21 | backend = vault_auth_backend.approle.path 22 | role_name = "agent" 23 | token_policies = [vault_policy.agent.name, vault_policy.token.name] 24 | token_ttl = 30 25 | token_max_ttl = 60 26 | } 27 | 28 | resource "vault_policy" "agent" { 29 | name = "agent" 30 | policy = data.vault_policy_document.agent.hcl 31 | } 32 | 33 | data "vault_policy_document" "agent" { 34 | rule { 35 | path = "auth/token/create" 36 | capabilities = ["create", "update"] 37 | } 38 | 39 | rule { 40 | path = "sys/tools/random/*" 41 | capabilities = ["create", "update"] 42 | } 43 | 44 | # NOTE: When using kv-v2, the data/ prefix is required in the policy, but not 45 | # for `vault put`/`vault get` 46 | # https://support.hashicorp.com/hc/en-us/articles/4407386653843-Vault-KV-V2-Secrets-Engine-Permission-Denied- 47 | rule { 48 | path = "${vault_mount.kv-v2.path}/data/monitoring/prometheus-basic-auth" 49 | capabilities = ["read"] 50 | } 51 | } 52 | 53 | resource "vault_policy" "token" { 54 | name = "token" 55 | policy = data.vault_policy_document.token.hcl 56 | } 57 | 58 | data "vault_policy_document" "token" { 59 | rule { 60 | path = "auth/token/create" 61 | capabilities = ["create", "update"] 62 | } 63 | 64 | rule { 65 | path = "auth/token/renew-self" 66 | capabilities = ["create", "update"] 67 | } 68 | 69 | rule { 70 | path = "${vault_mount.kv-v2.path}/data/needs-token" 71 | capabilities = ["read"] 72 | } 73 | } 74 | 75 | resource "vault_generic_secret" "needs-token" { 76 | path = "${vault_mount.kv-v2.path}/needs-token" 77 | 78 | data_json = <