diff --git a/.gitignore b/.gitignore index 4c68f15..1456754 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,6 @@ result result-* - +/.terraform/ diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 0000000..a3ae364 --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,44 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/consul" { + version = "2.17.0" + hashes = [ + "h1:tiKb9pAW5/dl1Y0HUeUdl+/leNQpaPXwpnbo1AJPR6k=", + ] +} + +provider "registry.terraform.io/hashicorp/nomad" { + version = "1.4.20" + hashes = [ + "h1:SCMAWGssM1bSbLNlrS8GdZuJ4MWpb3X4AXO/jLDYfXA=", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.1" + hashes = [ + "h1:tXn3DUW3RQLZERTKY85/iMGh1MYpp8ZK9dKJDExY888=", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.5.1" + hashes = [ + "h1:8ccmyrDBWfKr/qY7gQoAriinamltIC/LLvz53ar4a4k=", + ] +} + +provider "registry.terraform.io/hashicorp/vault" { + version = "3.15.2" + hashes = [ + "h1:uiBosZpSRpOArbzoiKpqhwp0bmf6vUA1kYJOgerydaw=", + ] +} + +provider "registry.terraform.io/magicrb/influxdb-v2" { + version = "0.4.6" + hashes = [ + "h1:0atGQr/mtBBm6IsHdZ+r+9H2rCCfzI/ZMYrPNsAkzNU=", + ] +} diff --git a/flake.lock b/flake.lock index 2b33359..79666f9 100644 --- a/flake.lock +++ b/flake.lock @@ -878,8 +878,8 @@ "terranix": "terranix" }, "locked": { - "lastModified": 1680122738, - "narHash": "sha256-Dvb6ZpRSPt5G6VkHzNOqCNRyHD7J9AdFYgPfHEHi7BU=", + "lastModified": 1686862391, + "narHash": "sha256-U7q2iwNNdB0A7GHyLjNYLWluOVJO+K0LtiOV0Y3/vuY=", "path": "/home/main/uterranix", "type": "path" }, diff --git a/flake.nix b/flake.nix index 446c186..6469b6d 100644 --- a/flake.nix +++ b/flake.nix @@ -79,8 +79,39 @@ overlays/terraform-provider-vault.nix overlays/terraform-provider-influxdb-v2.nix overlays/bootloadHID.nix + + inputs.uterranix.flakeModule ]; + uterranix.config = [ + ./terranix/default.nix + { + _module.args.vars = { + flake_rev = self.rev or null; + flake_sha = self.sha or null; + flake_ref = self.ref or null; + flake_host = "git+https://git.sr.ht/~magic_rb/cluster"; + }; + } + ]; + + uterranix.terraform = pkgs: + let + hpkgs = inputs.nixpkgs.legacyPackages.${pkgs.stdenv.system}.appendOverlays (with config.flake.overlays; [ + terraform-provider-vault + terraform-provider-influxdb-v2 + ]); + in + hpkgs.terraform.withPlugins (p: [ + p.consul + p.nomad + p.local + p.vault + p.random + p.null + p.influxdb-v2 + ] ); + flake.nixosModules = { hashicorp = nixos/modules/hashicorp.nix; hashicorp-envoy = nixos/modules/hashicorp-envoy.nix; diff --git a/terranix/blowhole.nix b/terranix/blowhole.nix new file mode 100644 index 0000000..2cfd327 --- /dev/null +++ b/terranix/blowhole.nix @@ -0,0 +1,105 @@ +{ tflib, config, secret, ... }: +let + inherit (tflib) + tf; + + paths.consul = { + encryption_key = "homelab-1/blowhole/consul/encryption_key"; + agent_token = "homelab-1/blowhole/consul/agent_token"; + anonymous_token = "homelab-1/blowhole/consul/anonymous_token"; + }; + + paths.nomad = { + encryption_key = "homelab-1/blowhole/nomad/encryption_key"; + vault_token = "homelab-1/blowhole/nomad/vault_token"; + consul_token = "homelab-1/blowhole/nomad/consul_token"; + }; + + vaultKvMount = config.resource."vault_mount"."kv".path; + vaultConsulMount = config.resource."vault_consul_secret_backend"."consul".path; +in +{ + prefab.consulAgent."blowhole" = { + datacenter = "homelab-1"; + + inherit vaultKvMount; + + paths = { + encryptionKey = paths.consul.encryption_key; + agentToken = paths.consul.agent_token; + anonymousToken = paths.consul.anonymous_token; + }; + encryptionKey = tf "random_id.homelab-1_consul_encryption_key.b64_std"; + + anonymousToken = { + secret = tf "data.consul_acl_token_secret_id.anonymous.secret_id"; + accessor = tf "consul_acl_token.anonymous.id"; + }; + }; + + prefab.nomadServer."blowhole" = { + datacenters = [ "homelab-1" ]; + + inherit vaultKvMount; + + encryptionKey = tf "random_id.nomad_encryption_key.b64_std"; + + paths = { + encryptionKey = paths.nomad.encryption_key; + vaultToken = paths.nomad.vault_token; + consulToken = paths.nomad.consul_token; + }; + }; + + resource."vault_policy"."vault-agent-blowhole" = { + name = "blowhole-id_ed_camera"; + + policy = '' + path "${vaultKvMount}/data/homelab-1/blowhole/id_ed_camera" { + capabilities = ["read"] + } + + path "${vaultKvMount}/data/homelab-1/blowhole/hostapd/wpa_psk" { + capabilities = ["read"] + } + + path "${vaultConsulMount}/creds/${tf "module.blowhole.envoy_grafana.name"}" { + capabilities = ["read"] + } + + path "${vaultConsulMount}/creds/${tf "module.blowhole.envoy_blowhole.name"}" { + capabilities = ["read"] + } + + path "${vaultConsulMount}/creds/${tf "module.blowhole.envoy_klipper.name"}" { + capabilities = ["read"] + } + + path "${vaultKvMount}/data/homelab-1/blowhole/monitor/telegraf" { + capabilities = ["read"] + } + + path "${vaultKvMount}/data/homelab-1/blowhole/monitor/grafana" { + capabilities = ["read"] + } + ''; + }; + + prefab.pushApproles."blowhole" = { + host = secret.network.ips.blowhole.ip or ""; + user = "main"; + + policies = [ + config.resource."vault_policy"."blowhole_consul".name + config.resource."vault_policy"."blowhole_nomad".name + config.resource."vault_policy"."pki_inra_update".name + config.resource."vault_policy"."vault-agent-blowhole".name + ]; + + metadata = { + "ip_address" = "blowhole.in.redalder.org"; + }; + + approlePath = tf "vault_auth_backend.approle.path"; + }; +} diff --git a/terranix/default.nix b/terranix/default.nix new file mode 100644 index 0000000..83a9155 --- /dev/null +++ b/terranix/default.nix @@ -0,0 +1,203 @@ +{ config, elib, tflib, lib, pkgs, ... }: +let + paths.toothpick.consul = { + encryption_key = "do-1/toothpick/consul/encryption_key"; + agent_token = "do-1/toothpick/consul/agent_token"; + anonymous_token = "do-1/toothpick/consul/anonymous_token"; + replication_token = "do-1/toothpick/consul/replication_token"; + }; + + paths.toothpick.nomad = { + encryption_key = "do-1/toothpick/nomad/encryption_key"; + vault_token = "do-1/toothpick/nomad/vault_token"; + consul_token = "do-1/toothpick/nomad/consul_token"; + replication_token = "do-1/toothpick/nomad/replication_token"; + }; + + inherit (tflib) + tf + ; + inherit (lib) + singleton + ; +in +{ + provider."vault" = { + address = "https://vault.in.redalder.org:8200"; + }; + + provider."consul" = { + address = "http://consul.in.redalder.org:8500"; + }; + + provider."nomad" = { + address = "http://nomad.in.redalder.org:4646"; + }; + + provider."influxdb-v2" = { + url = "http://influx.in.redalder.org"; + }; + + module."syncthing" = elib.terraformModule { + name = "syncthing"; + source = ./containers/syncthing; + }; + + module."website" = elib.terraformModule { + name = "website"; + source = ./containers/website; + }; + + module."hydra" = elib.terraformModule { + name = "hydra"; + source = ./containers/hydra; + }; + + module."matrix" = elib.terraformModule { + name = "matrix"; + source = ./containers/matrix; + }; + + module."jellyfin" = elib.terraformModule { + name = "jellyfin"; + source = ./containers/jellyfin; + }; + + module."gitea" = elib.terraformModule { + name = "gitea"; + source = ./containers/gitea; + }; + + module."home-assistant" = elib.terraformModule { + name = "home-assistant"; + source = ./containers/home-assistant; + }; + + module."ingress-blowhole" = elib.terraformModule { + name = "ingress-blowhole"; + source = ./containers/ingress-blowhole; + }; + + module."ingress-toothpick" = elib.terraformModule { + name = "ingress-toothpick"; + source = ./containers/ingress-toothpick; + }; + + module."gateway-mesh" = elib.terraformModule { + name = "gateway-mesh"; + source = ./containers/gateway-mesh; + }; + + module."nfs-csi" = elib.terraformModule { + name = "nfs-csi"; + source = ./containers/nfs-csi; + }; + + imports = [ + ./lib + ./modules/push_approles.nix + ./modules/consul_agent.nix + ./modules/nomad_server.nix + ./pki.nix + ./blowhole.nix + ./toothpick.nix + ]; + + terraform.backend."consul" = { + address = "consul.in.redalder.org:8500"; + scheme = "http"; + path = "terraform/dotfiles"; + }; + + terraform.required_providers = { + influxdb-v2 = { + source = "MagicRB/influxdb-v2"; + }; + }; + + resource."vault_auth_backend"."approle" = { + type = "approle"; + + tune = singleton { + max_lease_ttl = "90000s"; + listing_visibility = "unauth"; + allowed_response_headers = null; + audit_non_hmac_request_keys = null; + audit_non_hmac_response_keys = null; + default_lease_ttl = null; + passthrough_request_headers = null; + token_type = null; + }; + }; + + resource."vault_mount"."kv" = { + path = "kv"; + type = "kv"; + options.version = "2"; + description = "KV Version 2 secret engine mount"; + }; + + resource."vault_kv_secret_backend_v2"."config" = { + mount = config.resource."vault_mount"."kv".path; + max_versions = 5; + }; + + resource."consul_acl_token"."vault_management_token" = { + description = "Vault management token"; + policies = ["global-management"]; + local = false; + }; + + data."consul_acl_token_secret_id"."vault_management_token" = { + accessor_id = tf "consul_acl_token.vault_management_token.id"; + }; + + resource."vault_consul_secret_backend"."consul" = { + path = "consul"; + description = "Manages the Consul backend"; + + address = "consul.in.redalder.org:8500"; + token = tf "data.consul_acl_token_secret_id.vault_management_token.secret_id"; + }; + + resource."vault_token_auth_backend_role"."nomad_cluster" = { + role_name = "nomad-cluster"; + disallowed_policies = ["nomad-server"]; + orphan = true; + token_period = "259200"; + renewable = true; + token_explicit_max_ttl = "0"; + }; + + resource."random_id"."nomad_encryption_key" = { + byte_length = 32; + }; + + resource."random_id"."homelab-1_consul_encryption_key" = { + byte_length = 32; + }; + + resource."random_id"."do-1_consul_encryption_key" = { + byte_length = 32; + }; + + resource."consul_acl_policy"."anonymous" = { + name = "consul-anonymous"; + rules = '' + service_prefix "" { policy = "read" } + node_prefix "" { policy = "read" } + ''; + }; + + resource."consul_acl_token"."anonymous" = { + description = "Consul anonymous token"; + policies = [ + config.resource.consul_acl_policy.anonymous.name + ]; + local = false; + }; + + data."consul_acl_token_secret_id"."anonymous" = { + accessor_id = tf "consul_acl_token.anonymous.id"; + }; +} diff --git a/terranix/lib/default.nix b/terranix/lib/default.nix new file mode 100644 index 0000000..ceac2d3 --- /dev/null +++ b/terranix/lib/default.nix @@ -0,0 +1,18 @@ +{ elib, lib, pkgs, tflib, config, ... }: +let + inherit (lib) + callPackageWith; + + callPackage = callPackageWith { + inherit lib elib pkgs tflib config; + }; +in +{ + _module.args = { + elib = { + nfsVolume = callPackage ./nfs_volume.nix {}; + terraformModule = callPackage ./terraform-module.nix {}; + nomadJob = callPackage ./nomad_job.nix {}; + }; + }; +} diff --git a/terranix/lib/nfs_volume.nix b/terranix/lib/nfs_volume.nix new file mode 100644 index 0000000..b78b060 --- /dev/null +++ b/terranix/lib/nfs_volume.nix @@ -0,0 +1,30 @@ +{}: +{ volume_name +, access_mode +, server +, share +, mount_flags ? [] +}: +{ + type = "csi"; + plugin_id = "org.democratic-csi.nfs"; + volume_id = volume_name; + name = volume_name; + external_id = volume_name; + + capability = { + inherit access_mode; + attachment_mode = "file-system"; + }; + + context = { + inherit server share; + node_attach_driver = "nfs"; + provisioner_driver = "node-manual"; + }; + + mount_options = { + fs_type = "nfs"; + inherit mount_flags; + }; +} diff --git a/terranix/lib/nomad_job.nix b/terranix/lib/nomad_job.nix new file mode 100644 index 0000000..bef91ea --- /dev/null +++ b/terranix/lib/nomad_job.nix @@ -0,0 +1,11 @@ +{}: +{ jobspec +, vars ? {} +}: +{ + jobspec = "\${file(\"${jobspec}\")}"; + hcl2 = { + enabled = true; + inherit vars; + }; +} diff --git a/terranix/lib/terraform-module.nix b/terranix/lib/terraform-module.nix new file mode 100644 index 0000000..eec7f6b --- /dev/null +++ b/terranix/lib/terraform-module.nix @@ -0,0 +1,27 @@ +{ tflib +, pkgs +, config +}: +{ + name +, source +}: +{ + source = + let + module = (tflib.mkTerranixConfiguration { + inherit pkgs; + modules = [ + source + { + _file = "terraform-module.nix"; + _module.args = builtins.removeAttrs config._module.args [ "pkgs" ]; + } + ]; + } ).config.build.json; + in + pkgs.runCommandNoCC "${name}-module" {} '' + mkdir -p $out/ + ln -s ${module} $out/main.tf.json + ''; +} diff --git a/terranix/modules/consul_agent.nix b/terranix/modules/consul_agent.nix new file mode 100644 index 0000000..3127114 --- /dev/null +++ b/terranix/modules/consul_agent.nix @@ -0,0 +1,228 @@ +{ config, pkgs, lib, tflib, ... }: +let + cfg = config.prefab.consulAgent; + inherit (lib) + mkOption + types + mapAttrsToList + fix + optionalString + optionalAttrs + singleton + mkMerge + flip + ; + inherit (tflib) + tf; + + submoduleOptions = { + datacenter = mkOption { + description = '' + ''; + type = types.str; + }; + + replicationDatacenters = mkOption { + description = '' + ''; + type = with types; listOf str; + }; + + encryptionKey = mkOption { + description = '' + DO NOT hardcode the secret in Nix, generate it with Terraform + and let Terraform substitute it. + ''; + type = types.str; + }; + + anonymousToken = mkOption { + description = '' + DO NOT hardcode the secret in Nix, generate it with Terraform + and let Terraform substitute it. + ''; + type = types.submodule { + options.secret = mkOption { + description = ''''; + type = types.str; + }; + + options.accessor = mkOption { + description = ''''; + type = types.str; + }; + }; + }; + + paths = { + encryptionKey = mkOption { + description = '' + ''; + type = types.str; + }; + agentToken = mkOption { + description = '' + ''; + type = types.str; + }; + replicationToken = mkOption { + description = '' + ''; + type = with types; nullOr str; + default = null; + }; + anonymousToken = mkOption { + description = '' + ''; + type = types.str; + }; + }; + + vaultKvMount = mkOption { + description = '' + ''; + type = types.str; + }; + }; +in +{ + options.prefab.consulAgent = mkOption { + description = '' + ''; + type = with types; attrsOf (submodule { options = submoduleOptions; }); + default = {}; + }; + + config.resource = mkMerge + (flip mapAttrsToList cfg (hostname: value: + fix (self: { + "consul_acl_policy"."${hostname}_agent" = { + name = "${hostname}-consul-agent"; + rules = '' + node "${hostname}" { + policy = "write" + } + agent "${hostname}" { + policy = "write" + } + service_prefix "" { + policy = "write" + } + ''; + }; + + "consul_acl_token"."${hostname}_consul_agent" = { + description = "Consul agent token on ${hostname}"; + node_identities = singleton { + node_name = hostname; + datacenter = value.datacenter; + }; + local = false; + }; + + "vault_kv_secret_v2"."${hostname}_consul_encryption_key" = { + mount = value.vaultKvMount; + name = value.paths.encryptionKey; + delete_all_versions = true; + data_json = builtins.toJSON { + key = value.encryptionKey; + }; + }; + + "vault_kv_secret_v2"."${hostname}_consul_anonymous_token" = { + mount = value.vaultKvMount; + name = value.paths.anonymousToken; + delete_all_versions = true; + data_json = builtins.toJSON { + secret = value.anonymousToken.secret; + accessor = value.anonymousToken.accessor; + }; + }; + + "vault_kv_secret_v2"."${hostname}_consul_agent" = { + mount = value.vaultKvMount; + name = value.paths.agentToken; + delete_all_versions = true; + data_json = builtins.toJSON { + secret = tf "data.consul_acl_token_secret_id.${hostname}_consul_agent.secret_id"; + accessor = tf "consul_acl_token.${hostname}_consul_agent.id"; + }; + }; + + "vault_policy"."${hostname}_consul" = { + name = "${hostname}_consul_agent"; + + policy = '' + path "${value.vaultKvMount}/data/${value.paths.encryptionKey}" { + capabilities = ["read"] + } + + path "${value.vaultKvMount}/data/${value.paths.agentToken}" { + capabilities = ["read"] + } + + ${optionalString (value.paths.replicationToken != null) '' + path "${value.vaultKvMount}/data/${value.paths.replicationToken}" { + capabilities = ["read"] + } + ''} + + path "${value.vaultKvMount}/data/${value.paths.anonymousToken}" { + capabilities = ["read"] + } + ''; + }; + })) ++ + (flip mapAttrsToList cfg (hostname: value: + (optionalAttrs (value.paths.replicationToken != null) { + "consul_acl_policy"."${hostname}_replication" = { + name = "${hostname}_consul_replication"; + datacenters = value.replicationDatacenters; + rules = '' + acl = "write" + + operator = "write" + + service_prefix "" { + policy = "read" + intentions = "read" + } + ''; + }; + + "consul_acl_token"."${hostname}_consul_replication" = { + description = "Consul replication token on ${hostname}"; + policies = [ + (tf "consul_acl_policy.${hostname}_replication.name") + ]; + local = false; + }; + + "vault_kv_secret_v2"."${hostname}_consul_replication" = { + mount = value.vaultKvMount; + name = value.paths.replicationToken; + delete_all_versions = true; + data_json = builtins.toJSON { + secret = tf "data.consul_acl_token_secret_id.${hostname}_consul_replication.secret_id"; + accessor = tf "consul_acl_token.${hostname}_consul_replication.id"; + }; + }; + }) + ))); + + config.data = mkMerge + (flip mapAttrsToList cfg (hostname: value: + { + "consul_acl_token_secret_id"."${hostname}_consul_agent" = { + accessor_id = tf "consul_acl_token.${hostname}_consul_agent.id"; + }; + } + ) ++ + flip mapAttrsToList cfg (hostname: value: + (optionalAttrs (value.paths.replicationToken != null) { + "consul_acl_token_secret_id"."${hostname}_consul_replication" = { + accessor_id = tf "consul_acl_token.${hostname}_consul_replication.id"; + }; + }) + )); +} diff --git a/terranix/modules/nomad_server.nix b/terranix/modules/nomad_server.nix new file mode 100644 index 0000000..ea6fe2d --- /dev/null +++ b/terranix/modules/nomad_server.nix @@ -0,0 +1,256 @@ +{ config, pkgs, lib, tflib, ... }: +let + cfg = config.prefab.nomadServer; + inherit (lib) + mapAttrsToList + foldAttrs + mergeAttrs + fix + flip + mkOption + types + optionalString + optionalAttrs + mkMerge + ; + + inherit ((a: builtins.break a) tflib) + tf + ; + + submoduleOptions = { + datacenters = mkOption { + description = '' + ''; + type = with types; listOf str; + }; + + encryptionKey = mkOption { + description = '' + DO NOT hardcode the secret in Nix, generate it with Terraform + and let Terraform substitute it. + ''; + type = types.str; + }; + + paths = { + encryptionKey = mkOption { + description = '' + ''; + type = types.str; + }; + replicationToken = mkOption { + description = '' + ''; + type = with types; nullOr str; + default = null; + }; + vaultToken = mkOption { + description = '' + ''; + type = types.str; + }; + consulToken = mkOption { + description = '' + ''; + type = types.str; + }; + }; + + vaultKvMount = mkOption { + description = '' + ''; + type = types.str; + }; + }; +in +{ + options.prefab.nomadServer = mkOption { + description = '' + ''; + type = with types; attrsOf (submodule { options = submoduleOptions; }); + default = {}; + }; + + config.resource = mkMerge + (flip mapAttrsToList cfg (hostname: value: + fix (self: { + "vault_policy"."${hostname}_nomad" = { + name = "${hostname}-nomad-server-agent"; + + policy = '' + path "${value.vaultKvMount}/data/${value.paths.encryptionKey}" { + capabilities = ["read"] + } + + path "${value.vaultKvMount}/data/${value.paths.vaultToken}" { + capabilities = ["read"] + } + + path "${value.vaultKvMount}/data/${value.paths.consulToken}" { + capabilities = ["read"] + } + + ${optionalString (value.paths.replicationToken != null) '' + path "${value.vaultKvMount}/data/${value.paths.replicationToken}" { + capabilities = ["read"] + } + ''} + ''; + }; + + "vault_kv_secret_v2"."${hostname}_nomad_encryption_key" = { + mount = value.vaultKvMount; + name = value.paths.encryptionKey; + delete_all_versions = true; + data_json = builtins.toJSON { + key = value.encryptionKey; + }; + }; + + "consul_acl_policy"."${hostname}_nomad_server" = { + name = "${hostname}_nomad_server"; + rules = '' + agent_prefix "" { + policy = "read" + } + + node_prefix "" { + policy = "read" + } + + service_prefix "" { + policy = "write" + } + + acl = "write" + ''; + }; + + "consul_acl_token"."${hostname}_nomad_server" = { + description = "Consul token for nomad_server on ${hostname}"; + policies = [ + (tf "consul_acl_policy.${hostname}_nomad_server.name") + ]; + local = false; + }; + + "vault_kv_secret_v2"."${hostname}_nomad_server_consul" = { + mount = value.vaultKvMount; + name = value.paths.consulToken; + delete_all_versions = true; + data_json = builtins.toJSON { + secret = tf "data.consul_acl_token_secret_id.${hostname}_nomad_server.secret_id"; + accessor = tf "consul_acl_token.${hostname}_nomad_server.accessor_id"; + }; + }; + + "vault_policy"."${hostname}_nomad_server" = { + name = "${hostname}-nomad-server"; + + policy = '' + # Allow creating tokens under "nomad-cluster" token role. The token role name + # should be updated if "nomad-cluster" is not used. + path "auth/token/create/nomad-cluster" { + capabilities = ["update"] + } + + # Allow looking up "nomad-cluster" token role. The token role name should be + # updated if "nomad-cluster" is not used. + path "auth/token/roles/nomad-cluster" { + capabilities = ["read"] + } + + # Allow looking up the token passed to Nomad to validate the token has the + # proper capabilities. This is provided by the "default" policy. + path "auth/token/lookup-self" { + capabilities = ["read"] + } + + # Allow looking up incoming tokens to validate they have permissions to access + # the tokens they are requesting. This is only required if + # `allow_unauthenticated` is set to false. + path "auth/token/lookup" { + capabilities = ["update"] + } + + # Allow revoking tokens that should no longer exist. This allows revoking + # tokens for dead tasks. + path "auth/token/revoke-accessor" { + capabilities = ["update"] + } + + # Allow checking the capabilities of our own token. This is used to validate the + # token upon startup. Note this requires update permissions because the Vault API + # is a POST + path "sys/capabilities-self" { + capabilities = ["update"] + } + + # Allow our own token to be renewed. + path "auth/token/renew-self" { + capabilities = ["update"] + } + ''; + }; + + "vault_token_auth_backend_role"."${hostname}_nomad_server" = { + role_name = "${hostname}_nomad_server"; + allowed_policies = [ + (tf "vault_policy.${hostname}_nomad_server.name") + ]; + orphan = true; + renewable = true; + }; + + "vault_token"."${hostname}_nomad_server" = { + policies = [ + (tf "vault_policy.${hostname}_nomad_server.name") + ]; + renewable = true; + ttl = "24h"; + explicit_max_ttl = 0; + role_name = tf "vault_token_auth_backend_role.${hostname}_nomad_server.role_name"; + display_name = "${hostname}-nomad-server-Vault-token"; + }; + + "vault_kv_secret_v2"."${hostname}_nomad_server_vault" = { + mount = value.vaultKvMount; + name = value.paths.vaultToken; + delete_all_versions = true; + data_json = builtins.toJSON { + secret = tf "vault_token.${hostname}_nomad_server.client_token"; + }; + }; + + })) ++ + (flip mapAttrsToList cfg (hostname: value: + (optionalAttrs (value.paths.replicationToken != null) + { + "nomad_acl_token"."${hostname}_replication" = { + name = "${hostname} replication token"; + type = "management"; + }; + + "vault_kv_secret_v2"."${hostname}_nomad_replication" = { + mount = value.vaultKvMount; + name = value.paths.replicationToken; + delete_all_versions = true; + data_json = builtins.toJSON { + secret = tf "nomad_acl_token.${hostname}_replication.secret_id"; + accessor = tf "nomad_acl_token.${hostname}_replication.id"; + }; + }; + } + ) + ))); + + config.data = mkMerge + (flip mapAttrsToList cfg (hostname: value: + { + "consul_acl_token_secret_id"."${hostname}_nomad_server" = { + accessor_id = tf "consul_acl_token.${hostname}_nomad_server.id"; + }; + })); + +} diff --git a/terranix/modules/push_approles.nix b/terranix/modules/push_approles.nix new file mode 100644 index 0000000..ee975d7 --- /dev/null +++ b/terranix/modules/push_approles.nix @@ -0,0 +1,109 @@ +{ config, pkgs, lib, tflib, ... }: +let + cfg = config.prefab.pushApproles; + inherit (lib) + mkOption + mdDoc + types + mapAttrsToList + mkMerge + flip + ; + + inherit (tflib) + tf + ; + metadataType = pkgs.formats.json {}; + submoduleOptions = { + policies = mkOption { + description = mdDoc '' + Vault policies added to the approle generated. + ''; + type = with types; listOf str; + default = []; + }; + + host = mkOption { + description = mdDoc '' + The address of the machine, either IP address, domain name or any other identificator accepted by `ssh`. + ''; + type = types.str; + }; + + user = mkOption { + description = mdDoc '' + The user to connect as. + ''; + type = types.str; + }; + + metadata = mkOption { + description = mdDoc '' + ''; + type = metadataType.type; + default = {}; + }; + + approlePath = mkOption { + description = mdDoc '' + ''; + type = types.str; + }; + }; +in +{ + options.prefab.pushApproles = mkOption { + description = '' + ''; + type = with types; attrsOf (submodule { options = submoduleOptions; }); + default = {}; + }; + + config.resource = mkMerge + (flip mapAttrsToList cfg (hostname: value: + { + "vault_approle_auth_backend_role"."system-${hostname}" = { + backend = value.approlePath; + role_name = hostname; + token_policies = value.policies; + }; + + "vault_approle_auth_backend_role_secret_id"."system-${hostname}" = { + backend = value.approlePath; + role_name = tf "vault_approle_auth_backend_role.system-${hostname}.role_name"; + + metadata = builtins.toJSON value.metadata; + }; + + "null_resource"."approles-${hostname}" = { + triggers = { + secret_id = tf "vault_approle_auth_backend_role_secret_id.system-${hostname}.secret_id"; + role_id = tf "data.vault_approle_auth_backend_role_id.system-${hostname}.role_id"; + }; + + connection = { + inherit (value) + host + user; + }; + + provisioner = { + "remote-exec" = { + inline = [ + "echo \${vault_approle_auth_backend_role_secret_id.system-${hostname}.secret_id} > /var/secrets/approle.secretid" + "echo \${data.vault_approle_auth_backend_role_id.system-${hostname}.role_id} > /var/secrets/approle.roleid" + ]; + }; + }; + }; + })); + + config.data = mkMerge + (flip mapAttrsToList cfg (hostname: value: + { + "vault_approle_auth_backend_role_id"."system-${hostname}" = { + backend = value.approlePath; + role_name = tf "vault_approle_auth_backend_role.system-${hostname}.role_name"; + }; + })); +} diff --git a/terranix/pki.nix b/terranix/pki.nix new file mode 100644 index 0000000..4113e85 --- /dev/null +++ b/terranix/pki.nix @@ -0,0 +1,41 @@ +{ config, ... }: +{ + resource."vault_mount"."pki_inra" = { + path = "pki-inra"; + type = "pki"; + description = "in.redalder.org"; + default_lease_ttl_seconds = 8640000; + max_lease_ttl_seconds = 8640000; + }; + + resource."vault_policy"."pki_inra_update" = { + name = "pki-inra-update"; + + policy = '' + path "${config.resource."vault_mount"."pki_inra".path}/config/ca" { + capabilities = ["update"] + } + ''; + }; + + resource."vault_pki_secret_backend_config_urls"."example" = { + backend = config.resource."vault_mount"."pki_inra".path; + issuing_certificates = [ + "https://vault.in.redalder.org:8200/v1/pki/ca" + ]; + crl_distribution_points = [ + "https://vault.in.redalder.org:8200/v1/pki_int/crl" + ]; + }; + + resource."vault_pki_secret_backend_role"."test_role" = { + backend = config.resource."vault_mount"."pki_inra".path; + name = "test_role"; + ttl = 3600; + allow_ip_sans = true; + key_type = "rsa"; + key_bits = 4096; + allowed_domains = ["test.in.redalder.org"]; + allow_subdomains = false; + }; +} diff --git a/terranix/toothpick.nix b/terranix/toothpick.nix new file mode 100644 index 0000000..53af8e8 --- /dev/null +++ b/terranix/toothpick.nix @@ -0,0 +1,73 @@ +{ tflib, config, ... }: +let + inherit (tflib) + tf; + + paths.consul = { + encryption_key = "do-1/toothpick/consul/encryption_key"; + agent_token = "do-1/toothpick/consul/agent_token"; + anonymous_token = "do-1/toothpick/consul/anonymous_token"; + replication_token = "do-1/toothpick/consul/replication_token"; + }; + + paths.nomad = { + encryption_key = "do-1/toothpick/nomad/encryption_key"; + vault_token = "do-1/toothpick/nomad/vault_token"; + consul_token = "do-1/toothpick/nomad/consul_token"; + replication_token = "do-1/toothpick/nomad/replication_token"; + }; + + vaultKvMount = config.resource."vault_mount"."kv".path; +in +{ + prefab.consulAgent."toothpick" = { + datacenter = "do-1"; + replicationDatacenters = [ "homelab-1" ]; + + inherit vaultKvMount; + + paths = { + encryptionKey = paths.consul.encryption_key; + agentToken = paths.consul.agent_token; + anonymousToken = paths.consul.anonymous_token; + replicationToken = paths.consul.replication_token; + }; + encryptionKey = tf "random_id.do-1_consul_encryption_key.b64_std"; + + anonymousToken = { + secret = tf "data.consul_acl_token_secret_id.anonymous.secret_id"; + accessor = tf "consul_acl_token.anonymous.id"; + }; + }; + + prefab.nomadServer."toothpick" = { + datacenters = [ "do-1" ]; + + inherit vaultKvMount; + + encryptionKey = tf "random_id.nomad_encryption_key.b64_std"; + + paths = { + encryptionKey = paths.nomad.encryption_key; + vaultToken = paths.nomad.vault_token; + consulToken = paths.nomad.consul_token; + replicationToken = paths.nomad.replication_token; + }; + }; + + prefab.pushApproles."toothpick" = { + host = "10.64.0.1"; + user = "main"; + + policies = [ + (tf "vault_policy.toothpick_consul.name") + (tf "vault_policy.toothpick_nomad.name") + ]; + + metadata = { + "ip_address" = "redalder.org"; + }; + + approlePath = tf "vault_auth_backend.approle.path"; + }; +}