From 6f154ff4b4ba2c64f00cbfb00d322d46d9e4f462 Mon Sep 17 00:00:00 2001 From: Finn Date: Mon, 16 Sep 2024 23:56:35 -0700 Subject: [PATCH] terraform refactor + keycloak terraform tests --- .envrc | 4 ++ tf/.terraform.lock.hcl | 25 ++++++++ tf/bao-mounts.tf | 6 -- tf/bao-policies.tf | 8 --- tf/{bao-auth-backends.tf => bao.tf} | 16 ++++++ tf/keycloak-client/main.tf | 24 ++++++++ tf/keycloak-client/outputs.tf | 11 ++++ tf/keycloak-client/providers.tf | 8 +++ tf/keycloak-client/variables.tf | 49 ++++++++++++++++ tf/keycloak-clients.tf | 22 +++++++ tf/keycloak-normal-flow.tf | 37 ++++++++++++ tf/keycloak-passkey-flow.tf | 89 +++++++++++++++++++++++++++++ tf/keycloak.tf | 11 ++++ tf/providers.tf | 16 +++++- 14 files changed, 311 insertions(+), 15 deletions(-) delete mode 100644 tf/bao-mounts.tf delete mode 100644 tf/bao-policies.tf rename tf/{bao-auth-backends.tf => bao.tf} (58%) create mode 100644 tf/keycloak-client/main.tf create mode 100644 tf/keycloak-client/outputs.tf create mode 100644 tf/keycloak-client/providers.tf create mode 100644 tf/keycloak-client/variables.tf create mode 100644 tf/keycloak-clients.tf create mode 100644 tf/keycloak-normal-flow.tf create mode 100644 tf/keycloak-passkey-flow.tf create mode 100644 tf/keycloak.tf diff --git a/.envrc b/.envrc index 3a32c4d..e6f4586 100644 --- a/.envrc +++ b/.envrc @@ -8,3 +8,7 @@ if [ -f "${kubeconfig}" ]; then fi export VAULT_ADDR=https://openbao.k8s.home.finn.io +export VAULT_TOKEN="$(jq -r .root_token bao-root.json)" + +export KEYCLOAK_CLIENT_ID="$(bao kv get -format=json static-secrets/tofu/default/oidc-client-credentials-tofu | jq -r .data.data.client_id)" +export KEYCLOAK_CLIENT_SECRET="$(bao kv get -format=json static-secrets/tofu/default/oidc-client-credentials-tofu | jq -r .data.data.client_secret)" diff --git a/tf/.terraform.lock.hcl b/tf/.terraform.lock.hcl index aee520c..4518940 100644 --- a/tf/.terraform.lock.hcl +++ b/tf/.terraform.lock.hcl @@ -17,3 +17,28 @@ provider "registry.opentofu.org/hashicorp/vault" { "zh:f465a955cc96f640e7426a648ba672c169a4a2959bad6146fe61583d67642561", ] } + +provider "registry.opentofu.org/mrparkers/keycloak" { + version = "4.4.0" + constraints = ">= 4.0.0" + hashes = [ + "h1:FH9j76zRv05qxk7I/w0mycmBEuew/+XP+Qx+Ptz/onw=", + "zh:0116d63fb4a4436d67cc793038899e0de23c3a5c78f5bf3cf76ee006ad886979", + "zh:0fa399fcdeef21dd914ff7413b8489e47900cbe7bc65b50eeb0d75b71a2b561d", + "zh:30371fee6d0ae438908b1bf03278f6d0a0cb2992a97814028676a05a55d92f19", + "zh:39218a95fe6430ac2b44470cb991dbb98f57c5306017a80b81d3a319855094f4", + "zh:3b436c471cde4eb9120f609e3aecf12d383e8032aeb9cd12c7476faa7c8b4afb", + "zh:9a2a5cc77332e6cd9f6d101d3aff35520a2361fc02f4d436fe176dbd5351f24b", + "zh:9a89cc61c303100174cda3783b13fa4f6e2648eb436c1259d1c72264998534e8", + "zh:b588cd78d9939523de1fa8202c2757c497a20dcf2bf67cf4daf61836194bfe3a", + "zh:c04e6ac2367f55d9cd0893ebebbecb9da685312077e8a7fff299b8d8009955d5", + "zh:c23286693edf2024272219f6728bb7eded5ee087956fc527a63f10ea9ec9c9e4", + "zh:d7a29a2023f17b24236079789931d53662a2696b13d30140cb75dc0e693a1f94", + "zh:ddc0cad0a8ec9e5afc4f4502aed75089c3e9e0bc6da9d4b796728ef5580b94ef", + "zh:de8833a1a0a726401380e52302892de782dddb7efa51122c33104dde8e119561", + "zh:dee864f90327b149d126d603c5ed58cc196682153ebd1bfa73dd67398f6cbe38", + "zh:f63ef9950ebb06fa1daad784a3d0f342803f65404107186bdadb3198ce4d03b2", + "zh:f6d2414fec3fcaefc80cbe8e49647221dbbcfd2fe1b0f7619bd68d06c93c30f4", + "zh:fb659b5a21ba0ad9ec1c7484f167c51c752abea84dd27e726cc3567e7006e99e", + ] +} diff --git a/tf/bao-mounts.tf b/tf/bao-mounts.tf deleted file mode 100644 index affdb29..0000000 --- a/tf/bao-mounts.tf +++ /dev/null @@ -1,6 +0,0 @@ -resource "vault_mount" "static_secrets" { - path = "static-secrets" - type = "kv" - options = { version = "2" } - description = "Static secrets, organized by //*" -} diff --git a/tf/bao-policies.tf b/tf/bao-policies.tf deleted file mode 100644 index 051b8fc..0000000 --- a/tf/bao-policies.tf +++ /dev/null @@ -1,8 +0,0 @@ -resource "vault_policy" "k8s_default" { - name = "k8s-default" - - policy = templatefile("bao-policies/k8s-default.hcl", { - k8s_auth_backend_accessor = vault_auth_backend.kubernetes.accessor, - k8s_secrets_path = vault_mount.static_secrets.path, - }) -} diff --git a/tf/bao-auth-backends.tf b/tf/bao.tf similarity index 58% rename from tf/bao-auth-backends.tf rename to tf/bao.tf index 88b2ce9..e32691a 100644 --- a/tf/bao-auth-backends.tf +++ b/tf/bao.tf @@ -17,3 +17,19 @@ resource "vault_kubernetes_auth_backend_role" "k8s-default" { vault_policy.k8s_default.name ] } + +resource "vault_mount" "static_secrets" { + path = "static-secrets" + type = "kv" + options = { version = "2" } + description = "Static secrets, organized by //*" +} + +resource "vault_policy" "k8s_default" { + name = "k8s-default" + + policy = templatefile("bao-policies/k8s-default.hcl", { + k8s_auth_backend_accessor = vault_auth_backend.kubernetes.accessor, + k8s_secrets_path = vault_mount.static_secrets.path, + }) +} diff --git a/tf/keycloak-client/main.tf b/tf/keycloak-client/main.tf new file mode 100644 index 0000000..93ed6ce --- /dev/null +++ b/tf/keycloak-client/main.tf @@ -0,0 +1,24 @@ +resource "keycloak_openid_client" "oidc" { + realm_id = var.realm + client_id = var.client_id + name = var.name != null ? var.name : var.client_id + enabled = true + use_refresh_tokens = var.use_refresh_tokens + service_accounts_enabled = var.service_accounts_enabled + + access_type = "CONFIDENTIAL" + standard_flow_enabled = true + root_url = var.root_url != null ? var.root_url : "https://${var.client_id}.janky.solutions" + valid_redirect_uris = length(var.valid_redirect_uris) == 0 ? ["/*"] : var.valid_redirect_uris +} + +# resource "keycloak_openid_client_service_account_realm_role" "" + +resource "vault_kv_secret_v2" "oidc" { + mount = var.vault_mount + name = "${var.namespace != null ? var.namespace : var.client_id}/default/oidc-client-credentials-${var.client_id}" + data_json = jsonencode({ + client_id = keycloak_openid_client.oidc.client_id, + client_secret = keycloak_openid_client.oidc.client_secret + }) +} diff --git a/tf/keycloak-client/outputs.tf b/tf/keycloak-client/outputs.tf new file mode 100644 index 0000000..8707884 --- /dev/null +++ b/tf/keycloak-client/outputs.tf @@ -0,0 +1,11 @@ +output "client_id" { + value = keycloak_openid_client.oidc.client_id +} + +output "client_secret" { + value = keycloak_openid_client.oidc.client_secret +} + +output "service_account_user_id" { + value = keycloak_openid_client.oidc.service_account_user_id +} diff --git a/tf/keycloak-client/providers.tf b/tf/keycloak-client/providers.tf new file mode 100644 index 0000000..25a1359 --- /dev/null +++ b/tf/keycloak-client/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + keycloak = { + source = "mrparkers/keycloak" + version = ">= 4.0.0" + } + } +} diff --git a/tf/keycloak-client/variables.tf b/tf/keycloak-client/variables.tf new file mode 100644 index 0000000..402ae76 --- /dev/null +++ b/tf/keycloak-client/variables.tf @@ -0,0 +1,49 @@ +variable "realm" { + type = string +} + +variable "vault_mount" { + type = string +} + +variable "client_id" { + type = string + description = "the keycloak client ID. Ideally this matches the Kubernetes namespace the resource is deployed to." +} + +variable "name" { + type = string + default = null + nullable = true + description = "An (optional) display name shown to the user in certain dark corners of Keycloak. client_id is used by default." +} + +variable "namespace" { + type = string + default = null + nullable = true + description = "Kubernetes namespace that will use this client, used for creating vault path. client_id is used by default." +} + +variable "root_url" { + type = string + default = null + nullable = true + description = "The root URL of the app. https://.janky.solutions is used by default" +} + +variable "valid_redirect_uris" { + type = list(string) + default = [] + description = "URIs the client will ask keycloak to send the user back to after auth. /* is used by default." +} + +variable "use_refresh_tokens" { + type = bool + default = false +} + +variable "service_accounts_enabled" { + type = bool + default = false +} diff --git a/tf/keycloak-clients.tf b/tf/keycloak-clients.tf new file mode 100644 index 0000000..62d40b2 --- /dev/null +++ b/tf/keycloak-clients.tf @@ -0,0 +1,22 @@ +// own client +module "keycloak_client_tofu" { + source = "./keycloak-client" + + realm = keycloak_realm.dev.id + vault_mount = vault_mount.static_secrets.path + + client_id = "tofu" + service_accounts_enabled = true +} + +data "keycloak_openid_client" "realm_management" { + realm_id = keycloak_realm.dev.id + client_id = "realm-management" +} + +resource "keycloak_openid_client_service_account_role" "client_service_account_role" { + realm_id = keycloak_realm.dev.id + client_id = data.keycloak_openid_client.realm_management.id + service_account_user_id = module.keycloak_client_tofu.service_account_user_id + role = "realm-admin" +} diff --git a/tf/keycloak-normal-flow.tf b/tf/keycloak-normal-flow.tf new file mode 100644 index 0000000..aba91dc --- /dev/null +++ b/tf/keycloak-normal-flow.tf @@ -0,0 +1,37 @@ +resource "keycloak_authentication_flow" "webauthn_browser" { + realm_id = keycloak_realm.dev.id + alias = "webauthn_browser" + description = "browser based authentication" +} + +resource "keycloak_authentication_execution" "auth_cookie" { + realm_id = keycloak_realm.dev.id + parent_flow_alias = keycloak_authentication_flow.webauthn_browser.alias + authenticator = "auth-cookie" + requirement = "ALTERNATIVE" +} + +resource "keycloak_authentication_subflow" "webauthn_flow" { + realm_id = keycloak_realm.dev.id + alias = "webauthn browser forms" + description = "Username, password, otp and other auth forms." + parent_flow_alias = keycloak_authentication_flow.webauthn_browser.alias + provider_id = "basic-flow" + requirement = "ALTERNATIVE" + depends_on = [ keycloak_authentication_execution.auth_cookie ] +} + +resource "keycloak_authentication_execution" "user_pass" { + realm_id = keycloak_realm.dev.id + parent_flow_alias = keycloak_authentication_subflow.webauthn_flow.alias + authenticator = "auth-username-password-form" + requirement = "REQUIRED" +} + +resource "keycloak_authentication_execution" "webauthn" { + realm_id = keycloak_realm.dev.id + parent_flow_alias = keycloak_authentication_subflow.webauthn_flow.alias + authenticator = "webauthn-authenticator" + requirement = "REQUIRED" +} + diff --git a/tf/keycloak-passkey-flow.tf b/tf/keycloak-passkey-flow.tf new file mode 100644 index 0000000..5813bf8 --- /dev/null +++ b/tf/keycloak-passkey-flow.tf @@ -0,0 +1,89 @@ +resource "keycloak_authentication_flow" "passkey" { + realm_id = keycloak_realm.dev.id + alias = "passkey" + description = "browser based authentication" +} + +resource "keycloak_authentication_execution" "passkey_auth_cookie" { + realm_id = keycloak_realm.dev.id + parent_flow_alias = keycloak_authentication_flow.passkey.alias + authenticator = "auth-cookie" + requirement = "ALTERNATIVE" +} + +resource "keycloak_authentication_subflow" "passkey_forms" { + realm_id = keycloak_realm.dev.id + alias = "passkey browser forms" + parent_flow_alias = keycloak_authentication_flow.passkey.alias + provider_id = "basic-flow" + requirement = "ALTERNATIVE" + depends_on = [ keycloak_authentication_execution.auth_cookie ] +} + +resource "keycloak_authentication_execution" "passkey_username" { + realm_id = keycloak_realm.dev.id + parent_flow_alias = keycloak_authentication_subflow.passkey_forms.alias + authenticator = "auth-username-form" + requirement = "REQUIRED" +} + +resource "keycloak_authentication_subflow" "passkey_passwordless_or_2fa" { + realm_id = keycloak_realm.dev.id + alias = "passkey passkey or 2fa" + parent_flow_alias = keycloak_authentication_subflow.passkey_forms.alias + provider_id = "basic-flow" + requirement = "REQUIRED" + depends_on = [ keycloak_authentication_execution.passkey_username ] +} + +resource "keycloak_authentication_execution" "passkey_webauthn_passwordless" { + realm_id = keycloak_realm.dev.id + parent_flow_alias = keycloak_authentication_subflow.passkey_passwordless_or_2fa.alias + authenticator = "webauthn-authenticator-passwordless" + requirement = "ALTERNATIVE" + depends_on = [ keycloak_authentication_execution.passkey_username ] +} + +resource "keycloak_authentication_subflow" "passkey_password_and_second_factor" { + realm_id = keycloak_realm.dev.id + parent_flow_alias = keycloak_authentication_subflow.passkey_passwordless_or_2fa.alias + alias = "passkey password and 2fa" + provider_id = "basic-flow" + requirement = "ALTERNATIVE" +} + +resource "keycloak_authentication_execution" "passkey_password" { + realm_id = keycloak_realm.dev.id + parent_flow_alias = keycloak_authentication_subflow.passkey_password_and_second_factor.alias + authenticator = "auth-password-form" + requirement = "REQUIRED" +} + +resource "keycloak_authentication_subflow" "passkey_second_factor" { + realm_id = keycloak_realm.dev.id + parent_flow_alias = keycloak_authentication_subflow.passkey_password_and_second_factor.alias + alias = "passkey second factor" + provider_id = "basic-flow" + requirement = "CONDITIONAL" +} + +resource "keycloak_authentication_execution" "passkey_user_configured_condition" { + realm_id = keycloak_realm.dev.id + parent_flow_alias = keycloak_authentication_subflow.passkey_second_factor.alias + authenticator = "conditional-user-configured" + requirement = "REQUIRED" +} + +resource "keycloak_authentication_execution" "passkey_webauthn" { + realm_id = keycloak_realm.dev.id + parent_flow_alias = keycloak_authentication_subflow.passkey_second_factor.alias + authenticator = "webauthn-authenticator" + requirement = "ALTERNATIVE" +} + +resource "keycloak_authentication_execution" "passkey_otp" { + realm_id = keycloak_realm.dev.id + parent_flow_alias = keycloak_authentication_subflow.passkey_second_factor.alias + authenticator = "auth-otp-form" + requirement = "ALTERNATIVE" +} diff --git a/tf/keycloak.tf b/tf/keycloak.tf new file mode 100644 index 0000000..f777612 --- /dev/null +++ b/tf/keycloak.tf @@ -0,0 +1,11 @@ +resource "keycloak_realm" "dev" { + realm = "dev.janky.solutions" + enabled = true + display_name = "Janky Solutions (dev)" + default_signature_algorithm = "RS256" +} + +resource "keycloak_authentication_bindings" "browser_authentication_binding" { + realm_id = keycloak_realm.dev.id + browser_flow = keycloak_authentication_flow.passkey.alias +} diff --git a/tf/providers.tf b/tf/providers.tf index ae5935b..1af373c 100644 --- a/tf/providers.tf +++ b/tf/providers.tf @@ -1,4 +1,4 @@ -data "terraform_remote_state" "foo" { +data "terraform_remote_state" "kube" { backend = "kubernetes" config = { secret_suffix = "state" @@ -8,3 +8,17 @@ data "terraform_remote_state" "foo" { } provider "vault" {} + +terraform { + required_providers { + keycloak = { + source = "mrparkers/keycloak" + version = ">= 4.0.0" + } + } +} + +provider "keycloak" { + realm = "dev.janky.solutions" + url = "https://auth.janky.solutions" +}