diff --git a/.hgignore b/.hgignore index 550971ec60..3f09af28fe 100644 --- a/.hgignore +++ b/.hgignore @@ -7,6 +7,7 @@ result result-* ops/vault/cfg/tf/ +ops/vault/cfg/secrets.nix web/quotes/theme/static/ diff --git a/ops/vault/cfg/authbackend-approle.nix b/ops/vault/cfg/authbackend-approle.nix new file mode 100644 index 0000000000..43616e8886 --- /dev/null +++ b/ops/vault/cfg/authbackend-approle.nix @@ -0,0 +1,14 @@ +{ ... }: + +{ + imports = [ ./module-authbackend.nix ]; + + config.my.authBackend.approle = { + tune = { + default_lease_ttl = "1h"; + max_lease_ttl = "1h"; + listing_visibility = "hidden"; + token_type = "default-service"; + }; + }; +} diff --git a/ops/vault/cfg/authbackend-oidc.nix b/ops/vault/cfg/authbackend-oidc.nix new file mode 100644 index 0000000000..da774ee0ff --- /dev/null +++ b/ops/vault/cfg/authbackend-oidc.nix @@ -0,0 +1,19 @@ +{ ... }: + +{ + resource.vault_jwt_auth_backend.oidc = { + default_role = "user"; + namespace_in_state = true; + + oidc_discovery_url = "https://accounts.google.com"; + oidc_client_id = "620300851636-6ha1a7t9r4gatrn9gdqa82toem3cbq3b.apps.googleusercontent.com"; + # oidc_client_secret in secrets.nix + }; + + my.authBackend.oidc = { + resourceType = "vault_jwt_auth_backend"; + + tune.default_lease_ttl = "24h"; + tune.max_lease_ttl = "24h"; + }; +} diff --git a/ops/vault/cfg/config.nix b/ops/vault/cfg/config.nix index e880de6bdf..0c5473b91e 100644 --- a/ops/vault/cfg/config.nix +++ b/ops/vault/cfg/config.nix @@ -1,6 +1,21 @@ -{ ... }: +{ lib, config, ... }: { + imports = [ + ./secrets.nix + + ./policies-raw.nix + ./policies-app.nix + + ./authbackend-approle.nix + ./authbackend-oidc.nix + + ./ssh-ca-client.nix + ./ssh-ca-server.nix + + ./servers.nix + ]; + terraform = { backend.gcs = { bucket = "lukegb-terraform-state"; @@ -12,4 +27,11 @@ version = "3.3.1"; }; }; + + provider.vault = { + address = "https://vault.int.lukegb.com"; + }; + + my.apps.pomerium = {}; + my.servers.etheroute-lon01.apps = [ "pomerium" ]; } diff --git a/ops/vault/cfg/default.nix b/ops/vault/cfg/default.nix index d94511955a..82bcc3b130 100644 --- a/ops/vault/cfg/default.nix +++ b/ops/vault/cfg/default.nix @@ -4,8 +4,12 @@ let terranix = import "${pkgs.terranix}/core/default.nix" { inherit pkgs; terranix_config = { imports = [ ./config.nix ]; }; - strip_nulls = true; - extraArgs = args; + strip_nulls = false; + extraArgs = args // { + lib = args.lib // { + mapToAttrs = pred: onWhat: builtins.listToAttrs (map pred onWhat); + }; + }; }; config = (pkgs.formats.json { }).generate "config.tf.json" terranix.config; diff --git a/ops/vault/cfg/module-authbackend.nix b/ops/vault/cfg/module-authbackend.nix new file mode 100644 index 0000000000..5c2e7ea44e --- /dev/null +++ b/ops/vault/cfg/module-authbackend.nix @@ -0,0 +1,38 @@ +{ config, lib, ... }: + +let + inherit (lib) types mkOption mapAttrsToList mkMerge; +in { + options = { + my.authBackend = mkOption { + default = {}; + type = types.attrsOf (types.submodule ({ name, ... }: { + options = { + type = mkOption { type = types.str; default = name; }; + path = mkOption { type = types.str; default = name; }; + resourceType = mkOption { type = types.str; default = "vault_auth_backend"; }; + + tune = { + default_lease_ttl = mkOption { type = with types; nullOr str; default = null; }; + max_lease_ttl = mkOption { type = with types; nullOr str; default = null; }; + audit_non_hmac_response_keys = mkOption { type = with types; listOf str; default = []; }; + audit_non_hmac_request_keys = mkOption { type = with types; listOf str; default = []; }; + listing_visibility = mkOption { type = types.enum [ "unauth" "hidden" ]; default = "unauth"; }; + passthrough_request_headers = mkOption { type = with types; listOf str; default = []; }; + allowed_response_headers = mkOption { type = with types; listOf str; default = []; }; + token_type = mkOption { type = types.enum [ "default-service" "default-batch" "service" "batch" ]; default = "default-service"; }; + }; + }; + })); + }; + }; + + config = { + resource = mkMerge (mapAttrsToList (name: cfg: { + ${cfg.resourceType}.${name} = { + inherit (cfg) type path; + tune = [cfg.tune]; + }; + }) config.my.authBackend); + }; +} diff --git a/ops/vault/cfg/module-kv.nix b/ops/vault/cfg/module-kv.nix new file mode 100644 index 0000000000..c9fccb567a --- /dev/null +++ b/ops/vault/cfg/module-kv.nix @@ -0,0 +1,22 @@ +{ lib, config, ... }: + +{ + options = let + inherit (lib) mkOption types; + in { + my.secrets.apps = mkOption { + default = {}; + type = with types; attrsOf attrs; + }; + }; + + config = let + inherit (lib) nameValuePair mapAttrs'; + in { + resource.vault_generic_secret = mapAttrs' (name: value: nameValuePair "apps_${name}" { + path = "kv/apps/${name}"; + + data_json = builtins.toJSON value; + }) config.my.secrets.apps; + }; +} diff --git a/ops/vault/cfg/policies-app.nix b/ops/vault/cfg/policies-app.nix new file mode 100644 index 0000000000..07d682a0ba --- /dev/null +++ b/ops/vault/cfg/policies-app.nix @@ -0,0 +1,37 @@ +{ lib, config, ... }: + +let + inherit (lib) mkOption types mkMerge mapAttrsToList; +in { + options.my.apps = mkOption { + type = types.attrsOf (types.submodule ({ name, ... }: { + options = { + resourceName = mkOption { + type = types.str; + default = "app_${name}"; + internal = true; + }; + + policy = mkOption { + type = types.lines; + default = '' + path "kv/data/apps/${name}" { + capabilities = ["read"] + } + + path "kv/metadata/apps/${name}" { + capabilities = ["read"] + } + ''; + }; + }; + })); + }; + + config.resource = mkMerge (mapAttrsToList (appName: appCfg: { + vault_policy.${appCfg.resourceName} = { + name = "app/${appName}"; + policy = appCfg.policy; + }; + }) config.my.apps); +} diff --git a/ops/vault/cfg/policies-raw.nix b/ops/vault/cfg/policies-raw.nix new file mode 100644 index 0000000000..f73cd03599 --- /dev/null +++ b/ops/vault/cfg/policies-raw.nix @@ -0,0 +1,15 @@ +{ lib, ... }: + +let + inherit (lib) hasSuffix filterAttrs removeSuffix attrNames nameValuePair mapToAttrs; + + policiesFiles = builtins.readDir ./policies; + rawPolicies = attrNames (filterAttrs (filename: filetype: filetype == "regular" && hasSuffix ".hcl" filename) policiesFiles); +in { + resource.vault_policy = (mapToAttrs (filename: let + name = removeSuffix ".hcl" filename; + in nameValuePair name { + inherit name; + policy = builtins.readFile (./policies + "/${filename}"); + }) rawPolicies); +} diff --git a/ops/vault/cfg/policies/admin.hcl b/ops/vault/cfg/policies/admin.hcl new file mode 100644 index 0000000000..788c11dcfb --- /dev/null +++ b/ops/vault/cfg/policies/admin.hcl @@ -0,0 +1,3 @@ +path "*" { + capabilities = ["create", "read", "update", "delete", "list", "sudo"] +} diff --git a/ops/vault/cfg/policies/default.hcl b/ops/vault/cfg/policies/default.hcl new file mode 100644 index 0000000000..2a7bbce808 --- /dev/null +++ b/ops/vault/cfg/policies/default.hcl @@ -0,0 +1,96 @@ + +# Allow tokens to look up their own properties +path "auth/token/lookup-self" { + capabilities = ["read"] +} + +# Allow tokens to renew themselves +path "auth/token/renew-self" { + capabilities = ["update"] +} + +# Allow tokens to revoke themselves +path "auth/token/revoke-self" { + capabilities = ["update"] +} + +# Allow a token to look up its own capabilities on a path +path "sys/capabilities-self" { + capabilities = ["update"] +} + +# Allow a token to look up its own entity by id or name +path "identity/entity/id/{{identity.entity.id}}" { + capabilities = ["read"] +} +path "identity/entity/name/{{identity.entity.name}}" { + capabilities = ["read"] +} + + +# Allow a token to look up its resultant ACL from all policies. This is useful +# for UIs. It is an internal path because the format may change at any time +# based on how the internal ACL features and capabilities change. +path "sys/internal/ui/resultant-acl" { + capabilities = ["read"] +} + +# Allow a token to renew a lease via lease_id in the request body; old path for +# old clients, new path for newer +path "sys/renew" { + capabilities = ["update"] +} +path "sys/leases/renew" { + capabilities = ["update"] +} + +# Allow looking up lease properties. This requires knowing the lease ID ahead +# of time and does not divulge any sensitive information. +path "sys/leases/lookup" { + capabilities = ["update"] +} + +# Allow a token to manage its own cubbyhole +path "cubbyhole/*" { + capabilities = ["create", "read", "update", "delete", "list"] +} + +# Allow a token to wrap arbitrary values in a response-wrapping token +path "sys/wrapping/wrap" { + capabilities = ["update"] +} + +# Allow a token to look up the creation time and TTL of a given +# response-wrapping token +path "sys/wrapping/lookup" { + capabilities = ["update"] +} + +# Allow a token to unwrap a response-wrapping token. This is a convenience to +# avoid client token swapping since this is also part of the response wrapping +# policy. +path "sys/wrapping/unwrap" { + capabilities = ["update"] +} + +# Allow general purpose tools +path "sys/tools/hash" { + capabilities = ["update"] +} +path "sys/tools/hash/*" { + capabilities = ["update"] +} + +# Allow checking the status of a Control Group request if the user has the +# accessor +path "sys/control-group/request" { + capabilities = ["update"] +} + +# Everyone can read the SSH CA public properties +path "ssh-host/config/ca" { + capabilities = ["read"] +} +path "ssh-client/config/ca" { + capabilities = ["read"] +} diff --git a/ops/vault/cfg/policies/server.hcl b/ops/vault/cfg/policies/server.hcl new file mode 100644 index 0000000000..be13083810 --- /dev/null +++ b/ops/vault/cfg/policies/server.hcl @@ -0,0 +1,27 @@ + # Allow everyone to manage things under kv/server/ +path "kv/data/server/{{identity.entity.name}}/*" { + capabilities = ["create", "update", "read", "delete"] +} + +path "kv/metadata/server/{{identity.entity.name}}/*" { + capabilities = ["list"] +} +path "kv/metadata/server" { + capabilities = ["list"] +} + +path "kv/metadata/+" { + capabilities = ["list"] +} + +path "acme/certs/*" { + capabilities = ["create"] +} + +# Servers can always get nix-daemon data +path "kv/data/apps/nix-daemon" { + capabilities = ["read"] +} +path "kv/metadata/apps/nix-daemon" { + capabilities = ["read"] +} diff --git a/ops/vault/cfg/policies/user.hcl b/ops/vault/cfg/policies/user.hcl new file mode 100644 index 0000000000..d22309e8c9 --- /dev/null +++ b/ops/vault/cfg/policies/user.hcl @@ -0,0 +1,29 @@ +# Allow everyone to manage things under kv/users/ +path "kv/data/user/{{identity.entity.name}}/*" { + capabilities = ["create", "update", "read", "delete"] +} + +path "kv/metadata/user/{{identity.entity.name}}/*" { + capabilities = ["list"] +} +path "kv/metadata/user" { + capabilities = ["list"] +} + +path "kv/metadata/+" { + capabilities = ["list"] +} + +# Users can manage things under kv/server/ too. +path "kv/data/server/*" { + capabilities = ["create", "update", "read", "delete"] +} + +path "kv/metadata/server/*" { + capabilities = ["list"] +} + +# Users can get SSH keys signed. +path "ssh-client/sign/user" { + capabilities = ["update"] +} diff --git a/ops/vault/cfg/servers.nix b/ops/vault/cfg/servers.nix new file mode 100644 index 0000000000..f9c189d632 --- /dev/null +++ b/ops/vault/cfg/servers.nix @@ -0,0 +1,114 @@ +{ depot, lib, config, ... }: + +let + inherit (lib) mkOption nameValuePair mapToAttrs types mkEnableOption mapAttrs' filterAttrs mkMerge mapAttrsToList concatStringsSep; + + minutes = m: m * 60; + + serversType = types.attrsOf (types.submodule ({ name, ... }: { + options = { + enable = mkOption { + type = types.bool; + default = true; + }; + + resourceName = mkOption { + type = types.str; + default = "server_${name}"; + internal = true; + }; + + extraPolicies = mkOption { + type = with types; listOf str; + default = []; + }; + + apps = mkOption { + type = with types; listOf str; + default = []; + }; + + hostnames = mkOption { + type = with types; listOf str; + default = [ + "${name}.as205479.net" + "${name}.blade.as205479.net" + "${name}.int.as205479.net" + ]; + }; + + policy = mkOption { + type = types.lines; + default = '' + path "ssh-host/sign/${name}" { + capabilities = ["update"] + allowed_parameters = { + "cert_type" = ["host"] + "public_key" = [] + "valid_principals" = [] + } + } + ''; + }; + }; + })); + + cfg = config.my.enabledServers; +in { + options = { + my.servers = mkOption { + type = serversType; + }; + + my.enabledServers = mkOption { + internal = true; + readOnly = true; + default = filterAttrs (n: v: v.enable) config.my.servers; + type = serversType; + }; + }; + + config.my.servers = mapToAttrs (name: nameValuePair name {}) (builtins.attrNames depot.ops.nixos.systemConfigs); + + config.resource = mkMerge (mapAttrsToList (serverName: serverCfg: { + vault_policy.${serverCfg.resourceName} = { + name = "server/${serverName}"; + inherit (serverCfg) policy; + }; + + vault_approle_auth_backend_role.${serverCfg.resourceName} = { + backend = "\${vault_auth_backend.approle.path}"; + role_name = serverName; + role_id = serverName; + secret_id_num_uses = 0; + token_ttl = minutes 20; + token_max_ttl = minutes 30; + }; + + vault_identity_entity.${serverCfg.resourceName} = { + name = serverName; + policies = + ["default" "server" "\${vault_policy.${serverCfg.resourceName}.name}"] + ++ serverCfg.extraPolicies + ++ (map (name: "\${vault_policy.app_${name}.name}") serverCfg.apps); + metadata.server = serverName; + }; + + vault_identity_entity_alias.${serverCfg.resourceName} = { + name = serverName; + mount_accessor = "\${vault_auth_backend.approle.accessor}"; + canonical_id = "\${vault_identity_entity.${serverCfg.resourceName}.id}"; + }; + + vault_ssh_secret_backend_role.${serverCfg.resourceName} = { + name = serverName; + backend = "\${vault_mount.ssh-host.path}"; + key_type = "ca"; + allow_host_certificates = true; + allow_bare_domains = true; + allowed_domains = concatStringsSep "," serverCfg.hostnames; + ttl = 7 * 24 * 60 * 60; + max_ttl = 7 * 24 * 60 * 60; + }; + }) cfg); +} diff --git a/ops/vault/cfg/ssh-ca-client.nix b/ops/vault/cfg/ssh-ca-client.nix new file mode 100644 index 0000000000..db12bb5797 --- /dev/null +++ b/ops/vault/cfg/ssh-ca-client.nix @@ -0,0 +1,31 @@ +{ ... }: + +{ + resource.vault_mount.ssh-client = { + type = "ssh"; + path = "ssh-client"; + }; + + resource.vault_ssh_secret_backend_ca.ssh-client = { + backend = "\${vault_mount.ssh-client.path}"; + }; + + resource.vault_ssh_secret_backend_role.ssh-client_user = { + name = "user"; + backend = "\${vault_mount.ssh-client.path}"; + key_type = "ca"; + allow_user_certificates = true; + allowed_users_template = true; + allowed_users = "{{identity.entity.name}}"; + allowed_extensions = "permit-agent-forwarding,permit-port-forwarding,permit-pty,permit-user-rc,permit-X11-forwarding"; + ttl = 24 * 60 * 60; + max_ttl = 24 * 60 * 60; + default_extensions = { + permit-agent-forwarding = ""; + permit-port-forwarding = ""; + permit-pty = ""; + permit-user-rc = ""; + permit-X11-forwarding = ""; + }; + }; +} diff --git a/ops/vault/cfg/ssh-ca-server.nix b/ops/vault/cfg/ssh-ca-server.nix new file mode 100644 index 0000000000..f116eade97 --- /dev/null +++ b/ops/vault/cfg/ssh-ca-server.nix @@ -0,0 +1,12 @@ +{ ... }: + +{ + resource.vault_mount.ssh-host = { + type = "ssh"; + path = "ssh-host"; + }; + + resource.vault_ssh_secret_backend_ca.ssh-host = { + backend = "\${vault_mount.ssh-host.path}"; + }; +} diff --git a/ops/vault/cfg/terraform b/ops/vault/cfg/terraform new file mode 100755 index 0000000000..4d19637d93 --- /dev/null +++ b/ops/vault/cfg/terraform @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +DEPOT_ROOT="${SCRIPT_DIR%/depot/*}/depot" + +exec "$(nix-build "$DEPOT_ROOT" -A ops.vault.cfg.terraform --no-out-link --option builders '')" "$@"