terraform refactor + keycloak terraform tests

This commit is contained in:
Finn 2024-09-16 23:56:35 -07:00
parent 341809bd18
commit 6f154ff4b4
14 changed files with 311 additions and 15 deletions

4
.envrc
View file

@ -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)"

View file

@ -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",
]
}

View file

@ -1,6 +0,0 @@
resource "vault_mount" "static_secrets" {
path = "static-secrets"
type = "kv"
options = { version = "2" }
description = "Static secrets, organized by <k8s-namespace>/<service-account>/*"
}

View file

@ -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,
})
}

View file

@ -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 <k8s-namespace>/<service-account>/*"
}
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,
})
}

View file

@ -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
})
}

View file

@ -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
}

View file

@ -0,0 +1,8 @@
terraform {
required_providers {
keycloak = {
source = "mrparkers/keycloak"
version = ">= 4.0.0"
}
}
}

View file

@ -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://<client_id>.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
}

22
tf/keycloak-clients.tf Normal file
View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

11
tf/keycloak.tf Normal file
View file

@ -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
}

View file

@ -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"
}