diff --git a/infrastructure/main.tf b/infrastructure/main.tf index e6c8ac8..2981c6b 100644 --- a/infrastructure/main.tf +++ b/infrastructure/main.tf @@ -90,7 +90,7 @@ module "garage" { # Cloudnative PG Deployment for PostgreSQL Database Solution module "cnpg" { - source = "git::https://github.com/necro-cloud/modules//modules/cnpg?ref=main" + source = "git::https://github.com/necro-cloud/modules//modules/cnpg?ref=task/117/cnpg-external-secrets" // Cluster Secret Store Details cluster_secret_store_name = module.openbao.cluster_secret_store_name @@ -171,8 +171,11 @@ module "ferretdb" { # Keycloak Cluster Deployment for Identity Solution module "keycloak" { - source = "git::https://github.com/necro-cloud/modules//modules/keycloak?ref=main" + source = "git::https://github.com/necro-cloud/modules//modules/keycloak?ref=task/117/cnpg-external-secrets" + // Cluster Secret Store Details + cluster_secret_store_name = module.openbao.cluster_secret_store_name + // PostgreSQL Database Details for database details cluster_issuer_name = module.cluster-issuer.cluster-issuer-name postgres_namespace = module.cnpg.namespace diff --git a/modules/cnpg/README.md b/modules/cnpg/README.md index da5c2d9..fb072bb 100644 --- a/modules/cnpg/README.md +++ b/modules/cnpg/README.md @@ -7,6 +7,7 @@ Required Modules to deploy Cloudnative PG PostgreSQL Database: 2. [Cluster Issuer](../cluster-issuer) 3. [Garage](../garage) 4. [Observability](../observability) +5. [OpenBao](../openbao) ## Providers @@ -26,50 +27,55 @@ Required Modules to deploy Cloudnative PG PostgreSQL Database: | [kubernetes_manifest.barman_object_store](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.client_certificate_authority](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.client_certificates](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.client_database_credentials_sync](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.client_issuer](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.client_keycloak_certificate](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.client_streaming_replica_certificate](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.cluster](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.cluster_image_catalog](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.databases](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.garage_certificate_authority_sync](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.garage_configuration_sync](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.ingress_certificate](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.keycloak_database](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.keycloak_database_credentials_sync](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.password_generator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.pgadmin_credentials_sync](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.pgadmin_internal_certificate](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.public_issuer](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.push_client_certificates](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.push_client_database_credentials](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.push_client_keycloak_certificate](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.push_keycloak_database_credentials](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.push_pgadmin_credentials](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.push_server_certificate_authority](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.server_certificate](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.server_certificate_authority](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_manifest.server_issuer](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | | [kubernetes_namespace.namespace](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | | [kubernetes_network_policy.cnpg_network_policy](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/network_policy) | resource | | [kubernetes_pod_disruption_budget_v1.cnpg_pdb](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/pod_disruption_budget_v1) | resource | -| [kubernetes_secret.client_database_credentials](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | | [kubernetes_secret.cloudflare_token](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | -| [kubernetes_secret.garage_certificate_authority](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | -| [kubernetes_secret.garage_configuration](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | -| [kubernetes_secret.keycloak_database_credentials](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | -| [kubernetes_secret.pgadmin_credentials](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | | [kubernetes_service.pgadmin4](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service) | resource | -| [random_password.client_password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | -| [random_password.keycloak_password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | -| [random_password.pgadmin_password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [acme\_server](#input\_acme\_server) | URL for the ACME Server to be used, defaults to production URL for LetsEncrypt | `string` | `"https://acme-v02.api.letsencrypt.org/directory"` | no | +| [acme\_server](#input\_acme\_server) | URL for the ACME Server to be used, defaults to production URL for LetsEncrypt | `string` | `"https://acme-v02 | [app\_name](#input\_app\_name) | App name for deploying PostgreSQL Database | `string` | `"postgres"` | no | | [backup\_bucket\_name](#input\_backup\_bucket\_name) | Name of the bucket for storing PITR Backups in Garage | `string` | n/a | yes | -| [client\_certificate\_authority\_name](#input\_client\_certificate\_authority\_name) | Name of the Certificate Authority to be used with PostgreSQL Client | `string` | `"postgresql-client-certificate-authority"` | no | -| [client\_issuer\_name](#input\_client\_issuer\_name) | Name of the Issuer to be used with PostgreSQL Client | `string` | `"postgresql-client-issuer"` | no | +| [client\_certificate\_authority\_name](#input\_client\_certificate\_authority\_name) | Name of the Certificate Authority to be +| [client\_issuer\_name](#input\_client\_issuer\_name) | Name of the Issuer to be used with PostgreSQL Client | `string` | `"postgresql-client-i | [client\_streaming\_replica\_certificate\_name](#input\_client\_streaming\_replica\_certificate\_name) | Name of the Certificate to be used with PostgreSQL Streaming Replica Client | `string` | `"postgresql-streaming-replica-client-certificate"` | no | | [clients](#input\_clients) | Object List of clients who need databases and users to be configured for |
list(object({
namespace = string
user = string
database = string
derRequired = bool
privateKeyEncoding = string
}))
| `[]` | no | -| [cloudflare\_email](#input\_cloudflare\_email) | Email for generating Ingress Certificates to be associated with PGAdmin | `string` | n/a | yes | +| [cloudflare\_email](#input\_cloudflare\_email) | Email for generating Ingress Certificates to be associated with PGAdmin | `string` | n/a | yes | [cloudflare\_issuer\_name](#input\_cloudflare\_issuer\_name) | Name of the Cloudflare Issuer to be associated with PGAdmin | `string` | `"cnpg-cloudflare-issuer"` | no | -| [cloudflare\_token](#input\_cloudflare\_token) | Token for generating Ingress Certificates to be associated with PGAdmin | `string` | n/a | yes | -| [cluster\_issuer\_name](#input\_cluster\_issuer\_name) | Name for the Cluster Issuer to be used to generate internal self signed certificates | `string` | n/a | yes | +| [cloudflare\_token](#input\_cloudflare\_token) | Token for generating Ingress Certificates to be associated with PGAdmin | `string` | n/a | yes +| [cluster\_issuer\_name](#input\_cluster\_issuer\_name) | Name for the Cluster Issuer to be used to generate internal self signed certificates | [cluster\_name](#input\_cluster\_name) | Name of the PostgreSQL Database Cluster to be created | `string` | `"postgresql-cluster"` | no | | [cluster\_postgresql\_version](#input\_cluster\_postgresql\_version) | Version of PostgreSQL Database to use and deploy | `number` | `17` | no | +| [cluster\_secret\_store\_name](#input\_cluster\_secret\_store\_name) | Name of the cluster secret store to be used for pulling and push | [cluster\_size](#input\_cluster\_size) | Number of pods to deploy for the PostgreSQL Cluster | `number` | `2` | no | | [country\_name](#input\_country\_name) | Country name for deploying PostgreSQL Database | `string` | `"India"` | no | | [domain](#input\_domain) | Domain for which Ingress Certificate is to be generated for | `string` | n/a | yes | diff --git a/modules/cnpg/certificates.tf b/modules/cnpg/certificates.tf index ec5261b..1ff51f4 100644 --- a/modules/cnpg/certificates.tf +++ b/modules/cnpg/certificates.tf @@ -98,6 +98,50 @@ resource "kubernetes_manifest" "server_certificate_authority" { } } +// Pushing the certificate to OpenBao for distribution +resource "kubernetes_manifest" "push_server_certificate_authority" { + manifest = { + apiVersion = "external-secrets.io/v1alpha1" + kind = "PushSecret" + metadata = { + name = "push-server-certificate-authority" + namespace = kubernetes_namespace.namespace.metadata[0].name + } + spec = { + refreshInterval = "1h" + deletionPolicy = "None" + secretStoreRefs = [{ + name = var.cluster_secret_store_name + kind = "ClusterSecretStore" + }] + selector = { + secret = { + name = kubernetes_manifest.server_certificate_authority.object.spec.secretName + } + } + data = [ + { + match = { + remoteRef = { + remoteKey = "${kubernetes_namespace.namespace.metadata[0].name}/certificates/${kubernetes_manifest.server_certificate_authority.object.spec.secretName}" + } + } + } + ] + } + } + + // Ensure the certificate is actually issued before trying to push it + depends_on = [kubernetes_manifest.server_certificate_authority] + + wait { + condition { + type = "Ready" + status = "True" + } + } +} + // Issuer to be used with PostgreSQL Server resource "kubernetes_manifest" "server_issuer" { manifest = { @@ -339,11 +383,6 @@ resource "kubernetes_manifest" "client_keycloak_certificate" { "reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces" = "keycloak" } } - "additionalOutputFormats" = [ - { - "type" : "DER" - } - ] "privateKey" = { "encoding" = "PKCS8" } @@ -367,6 +406,53 @@ resource "kubernetes_manifest" "client_keycloak_certificate" { } } +// Pushing the certificate to OpenBao for distribution +resource "kubernetes_manifest" "push_client_keycloak_certificate" { + manifest = { + apiVersion = "external-secrets.io/v1alpha1" + kind = "PushSecret" + metadata = { + name = "push-client-keycloak-certificate" + namespace = kubernetes_namespace.namespace.metadata[0].name + } + spec = { + refreshInterval = "1h" + deletionPolicy = "None" + secretStoreRefs = [{ + name = var.cluster_secret_store_name + kind = "ClusterSecretStore" + }] + selector = { + secret = { + name = kubernetes_manifest.client_keycloak_certificate.object.spec.secretName + } + } + data = [ + { + match = { + remoteRef = { + remoteKey = "${kubernetes_namespace.namespace.metadata[0].name}/certificates/${kubernetes_manifest.client_keycloak_certificate.object.spec.secretName}" + } + } + metadata = { + encoding = "base64" + } + } + ] + } + } + + // Ensure the certificate is actually issued before trying to push it + depends_on = [kubernetes_manifest.client_keycloak_certificate] + + wait { + condition { + type = "Ready" + status = "True" + } + } +} + // Certificates for all clients resource "kubernetes_manifest" "client_certificates" { count = length(var.clients) @@ -424,6 +510,51 @@ resource "kubernetes_manifest" "client_certificates" { } } +// Pushing the certificate to OpenBao for distribution +resource "kubernetes_manifest" "push_client_certificates" { + count = length(var.clients) + manifest = { + apiVersion = "external-secrets.io/v1alpha1" + kind = "PushSecret" + metadata = { + name = "push-${var.clients[count.index].user}-client-certificate" + namespace = kubernetes_namespace.namespace.metadata[0].name + } + spec = { + refreshInterval = "1h" + deletionPolicy = "None" + secretStoreRefs = [{ + name = var.cluster_secret_store_name + kind = "ClusterSecretStore" + }] + selector = { + secret = { + name = kubernetes_manifest.client_certificates[count.index].object.spec.secretName + } + } + data = [ + { + match = { + remoteRef = { + remoteKey = "${kubernetes_namespace.namespace.metadata[0].name}/certificates/${kubernetes_manifest.client_certificates[count.index].object.spec.secretName}" + } + } + } + ] + } + } + + // Ensure the certificate is actually issued before trying to push it + depends_on = [kubernetes_manifest.client_certificates] + + wait { + condition { + type = "Ready" + status = "True" + } + } +} + // Internal Certificate for PGAdmin resource "kubernetes_manifest" "pgadmin_internal_certificate" { manifest = { diff --git a/modules/cnpg/cluster.tf b/modules/cnpg/cluster.tf index e62dade..77192cb 100644 --- a/modules/cnpg/cluster.tf +++ b/modules/cnpg/cluster.tf @@ -56,7 +56,7 @@ resource "kubernetes_manifest" "cluster" { "login" = true "name" = "keycloak" "passwordSecret" = { - "name" = kubernetes_secret.keycloak_database_credentials.metadata[0].name + "name" = kubernetes_manifest.keycloak_database_credentials_sync.object.spec.target.name } "replication" = false "superuser" = false @@ -123,4 +123,11 @@ resource "kubernetes_manifest" "cluster" { update = "10m" delete = "10m" } + + depends_on = [ + kubernetes_manifest.barman_object_store, + kubernetes_manifest.keycloak_database_credentials_sync, + kubernetes_manifest.client_database_credentials_sync, + kubernetes_manifest.garage_configuration_sync + ] } diff --git a/modules/cnpg/locals.tf b/modules/cnpg/locals.tf index 184ffcb..0773f17 100644 --- a/modules/cnpg/locals.tf +++ b/modules/cnpg/locals.tf @@ -1,17 +1,17 @@ locals { access_namespaces = [for config in var.clients : config.namespace] - managed_roles = [for secret in kubernetes_secret.client_database_credentials : { + managed_roles = [for index, client in var.clients : { "bypassrls" = false - "comment" = "${secret.data.username} user for postgresql" + "comment" = "${client.user} user for postgresql" "connectionLimit" = -1 "createdb" = true "createrole" = true "ensure" = "present" "inherit" = true "login" = true - "name" = secret.data.username + "name" = client.user "passwordSecret" = { - "name" = secret.metadata[0].name + "name" = kubernetes_manifest.client_database_credentials_sync[index].object.spec.target.name } "replication" = false "superuser" = false diff --git a/modules/cnpg/pgadmin.tf b/modules/cnpg/pgadmin.tf index d14006d..166beb7 100644 --- a/modules/cnpg/pgadmin.tf +++ b/modules/cnpg/pgadmin.tf @@ -30,7 +30,7 @@ resource "kubernetes_deployment" "pgadmin" { env_from { secret_ref { - name = kubernetes_secret.pgadmin_credentials.metadata[0].name + name = kubernetes_manifest.pgadmin_credentials_sync.object.spec.target.name } } @@ -175,7 +175,7 @@ resource "kubernetes_deployment" "pgadmin" { projected { sources { secret { - name = kubernetes_secret.keycloak_database_credentials.metadata[0].name + name = kubernetes_manifest.keycloak_database_credentials_sync.object.spec.target.name items { key = "password" path = "keycloak/password" @@ -183,12 +183,12 @@ resource "kubernetes_deployment" "pgadmin" { } dynamic "secret" { - for_each = kubernetes_secret.client_database_credentials + for_each = kubernetes_manifest.client_database_credentials_sync content { - name = secret.value.metadata[0].name + name = secret.value.object.spec.target.name items { key = "password" - path = "${split("-", secret.value.metadata[0].name)[1]}/password" + path = "${split("-", secret.value.object.spec.target.name)[1]}/password" } } } diff --git a/modules/cnpg/secrets.tf b/modules/cnpg/secrets.tf index b1fdded..fda0c7b 100644 --- a/modules/cnpg/secrets.tf +++ b/modules/cnpg/secrets.tf @@ -38,95 +38,225 @@ resource "kubernetes_manifest" "garage_configuration_sync" { } } -// Database credentials configuration for Keycloak -resource "random_password" "keycloak_password" { - length = 20 - lower = true - numeric = true - special = false +// Password Generator for generating random passwords +resource "kubernetes_manifest" "password_generator" { + manifest = { + apiVersion = "generators.external-secrets.io/v1alpha1" + kind = "Password" + metadata = { + name = "password-generator" + namespace = kubernetes_namespace.namespace.metadata[0].name + } + spec = { + length = 20 + digits = 5 + symbols = 0 + noUpper = true + } + } } -resource "kubernetes_secret" "keycloak_database_credentials" { - metadata { - name = "credentials-keycloak" - namespace = kubernetes_namespace.namespace.metadata[0].name - - labels = { - app = var.app_name - component = "secret" +// Database credentials configuration for Keycloak +resource "kubernetes_manifest" "keycloak_database_credentials_sync" { + manifest = { + apiVersion = "external-secrets.io/v1" + kind = "ExternalSecret" + metadata = { + name = "credentials-keycloak" + namespace = kubernetes_namespace.namespace.metadata[0].name } - - annotations = { - "reflector.v1.k8s.emberstack.com/reflection-allowed" = "true" - "reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces" = "keycloak" + spec = { + refreshInterval = "0" + target = { + name = "credentials-keycloak" + template = { + type = "kubernetes.io/basic-auth" + data = { + username = "keycloak" + password = "{{ .password }}" + } + } + } + dataFrom = [{ + sourceRef = { + generatorRef = { + apiVersion = "generators.external-secrets.io/v1alpha1" + kind = "Password" + name = kubernetes_manifest.password_generator.object.metadata.name + } + } + }] } } +} - data = { - "username" = "keycloak" - "password" = random_password.keycloak_password.result +resource "kubernetes_manifest" "push_keycloak_database_credentials" { + manifest = { + apiVersion = "external-secrets.io/v1alpha1" + kind = "PushSecret" + metadata = { + name = "push-${kubernetes_manifest.keycloak_database_credentials_sync.object.spec.target.name}" + namespace = kubernetes_namespace.namespace.metadata[0].name + } + spec = { + refreshInterval = "1h" + deletionPolicy = "None" + secretStoreRefs = [{ + name = var.cluster_secret_store_name + kind = "ClusterSecretStore" + }] + selector = { + secret = { + name = kubernetes_manifest.keycloak_database_credentials_sync.object.spec.target.name + } + } + data = [ + { + match = { + remoteRef = { + remoteKey = "${kubernetes_namespace.namespace.metadata[0].name}/credentials/${kubernetes_manifest.keycloak_database_credentials_sync.object.spec.target.name}" + } + } + } + ] + } } - - type = "kubernetes.io/basic-auth" + depends_on = [kubernetes_manifest.keycloak_database_credentials_sync] } // Database credentials configuration for all clients -resource "random_password" "client_password" { +resource "kubernetes_manifest" "client_database_credentials_sync" { count = length(var.clients) - length = 20 - lower = true - numeric = true - special = false -} - -resource "kubernetes_secret" "client_database_credentials" { - count = length(var.clients) - metadata { - name = "credentials-${var.clients[count.index].user}" - namespace = kubernetes_namespace.namespace.metadata[0].name - - labels = { - app = var.app_name - component = "secret" + manifest = { + apiVersion = "external-secrets.io/v1" + kind = "ExternalSecret" + metadata = { + name = "credentials-${var.clients[count.index].user}" + namespace = kubernetes_namespace.namespace.metadata[0].name } - - annotations = { - "reflector.v1.k8s.emberstack.com/reflection-allowed" = "true" - "reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces" = var.clients[count.index].namespace + spec = { + refreshInterval = "0" + target = { + name = "credentials-${var.clients[count.index].user}" + template = { + type = "kubernetes.io/basic-auth" + data = { + username = var.clients[count.index].user + password = "{{ .password }}" + } + } + } + dataFrom = [{ + sourceRef = { + generatorRef = { + apiVersion = "generators.external-secrets.io/v1alpha1" + kind = "Password" + name = kubernetes_manifest.password_generator.object.metadata.name + } + } + }] } } - - data = { - "username" = var.clients[count.index].user - "password" = random_password.client_password[count.index].result - } - - type = "kubernetes.io/basic-auth" } - -resource "random_password" "pgadmin_password" { - length = 20 - lower = true - numeric = true - special = false +resource "kubernetes_manifest" "push_client_database_credentials" { + count = length(var.clients) + manifest = { + apiVersion = "external-secrets.io/v1alpha1" + kind = "PushSecret" + metadata = { + name = "push-${kubernetes_manifest.client_database_credentials_sync[count.index].object.spec.target.name}" + namespace = kubernetes_namespace.namespace.metadata[0].name + } + spec = { + refreshInterval = "1h" + deletionPolicy = "None" + secretStoreRefs = [{ + name = var.cluster_secret_store_name + kind = "ClusterSecretStore" + }] + selector = { + secret = { + name = kubernetes_manifest.client_database_credentials_sync[count.index].object.spec.target.name + } + } + data = [ + { + match = { + remoteRef = { + remoteKey = "${kubernetes_namespace.namespace.metadata[0].name}/credentials/${kubernetes_manifest.client_database_credentials_sync[count.index].object.spec.target.name}" + } + } + } + ] + } + } + depends_on = [kubernetes_manifest.client_database_credentials_sync] } -resource "kubernetes_secret" "pgadmin_credentials" { - metadata { - name = "pgadmin-credentials" - namespace = kubernetes_namespace.namespace.metadata[0].name - - labels = { - app = var.app_name - component = "secret" +// PGAdmin UI Credentials +resource "kubernetes_manifest" "pgadmin_credentials_sync" { + manifest = { + apiVersion = "external-secrets.io/v1" + kind = "ExternalSecret" + metadata = { + name = "pgadmin-credentials" + namespace = kubernetes_namespace.namespace.metadata[0].name + } + spec = { + refreshInterval = "0" + target = { + name = "pgadmin-credentials" + template = { + data = { + "PGADMIN_DEFAULT_EMAIL" = "noreply@${var.organization_name}.com" + "PGADMIN_DEFAULT_PASSWORD" = "{{ .password }}" + } + } + } + dataFrom = [{ + sourceRef = { + generatorRef = { + apiVersion = "generators.external-secrets.io/v1alpha1" + kind = "Password" + name = kubernetes_manifest.password_generator.object.metadata.name + } + } + }] } } +} - data = { - "PGADMIN_DEFAULT_EMAIL" = "noreply@${var.organization_name}.com" - "PGADMIN_DEFAULT_PASSWORD" = random_password.pgadmin_password.result +resource "kubernetes_manifest" "push_pgadmin_credentials" { + manifest = { + apiVersion = "external-secrets.io/v1alpha1" + kind = "PushSecret" + metadata = { + name = "push-${kubernetes_manifest.pgadmin_credentials_sync.object.spec.target.name}" + namespace = kubernetes_namespace.namespace.metadata[0].name + } + spec = { + refreshInterval = "1h" + deletionPolicy = "None" + secretStoreRefs = [{ + name = var.cluster_secret_store_name + kind = "ClusterSecretStore" + }] + selector = { + secret = { + name = kubernetes_manifest.pgadmin_credentials_sync.object.spec.target.name + } + } + data = [ + { + match = { + remoteRef = { + remoteKey = "${kubernetes_namespace.namespace.metadata[0].name}/credentials/ui/${kubernetes_manifest.pgadmin_credentials_sync.object.spec.target.name}" + } + } + } + ] + } } - - type = "Opaque" + depends_on = [kubernetes_manifest.pgadmin_credentials_sync] } diff --git a/modules/garage/README.md b/modules/garage/README.md index 2f0dce4..a6d63b0 100644 --- a/modules/garage/README.md +++ b/modules/garage/README.md @@ -5,6 +5,7 @@ OpenTofu Module to deploy [Garage](https://garagehq.deuxfleurs.fr/) Object Stora Required Modules to deploy Garage Object Storage: 1. [Cluster Issuer](../cluster-issuer) 2. [Observability](../observability) +3. [OpenBao](../openbao) ## Providers diff --git a/modules/keycloak/certificates.tf b/modules/keycloak/certificates.tf index 14aa423..89d0b56 100644 --- a/modules/keycloak/certificates.tf +++ b/modules/keycloak/certificates.tf @@ -1,53 +1,88 @@ // Database Certificate Authority to be used for database connections -resource "kubernetes_secret" "database_server_certificate_authority" { - metadata { - name = var.database_server_certificate_authority_name - namespace = kubernetes_namespace.namespace.metadata[0].name - labels = { - app = var.app_name - component = "secret" +resource "kubernetes_manifest" "database_server_certificate_authority_sync" { + manifest = { + apiVersion = "external-secrets.io/v1" + kind = "ExternalSecret" + metadata = { + name = var.database_server_certificate_authority_name + namespace = kubernetes_namespace.namespace.metadata[0].name + labels = { + app = var.app_name + component = "secret" + } } - - annotations = { - "reflector.v1.k8s.emberstack.com/reflects" = "${var.postgres_namespace}/${var.database_server_certificate_authority_name}" + spec = { + refreshInterval = "1h" + secretStoreRef = { + name = var.cluster_secret_store_name + kind = "ClusterSecretStore" + } + target = { + name = var.database_server_certificate_authority_name + template = { + type = "kubernetes.io/tls" + engineVersion = "v2" + } + } + dataFrom = [ + { + extract = { + key = "${var.postgres_namespace}/certificates/${var.database_server_certificate_authority_name}" + } + } + ] } } - data = { - "ca.crt" = "" - "tls.crt" = "" - "tls.key" = "" - } - - lifecycle { - ignore_changes = [metadata[0].annotations] + wait { + condition { + type = "Ready" + status = "True" + } } } // Database Client Certificate to be used for database connections -resource "kubernetes_secret" "database_client_certificate" { - metadata { - name = var.database_client_certificate_name - namespace = kubernetes_namespace.namespace.metadata[0].name - labels = { - app = var.app_name - component = "secret" +resource "kubernetes_manifest" "database_client_certificate_sync" { + manifest = { + apiVersion = "external-secrets.io/v1" + kind = "ExternalSecret" + metadata = { + name = var.database_client_certificate_name + namespace = kubernetes_namespace.namespace.metadata[0].name + labels = { + app = var.app_name + component = "secret" + } } - - annotations = { - "reflector.v1.k8s.emberstack.com/reflects" = "${var.postgres_namespace}/${var.database_client_certificate_name}" + spec = { + refreshInterval = "1h" + secretStoreRef = { + name = var.cluster_secret_store_name + kind = "ClusterSecretStore" + } + target = { + name = var.database_client_certificate_name + template = { + type = "kubernetes.io/tls" + engineVersion = "v2" + } + } + dataFrom = [ + { + extract = { + key = "${var.postgres_namespace}/certificates/${var.database_client_certificate_name}" + } + } + ] } } - data = { - "ca.crt" = "" - "tls.crt" = "" - "tls.key" = "" - "key.der" = "" - } - - lifecycle { - ignore_changes = [metadata[0].annotations] + wait { + condition { + type = "Ready" + status = "True" + } } } diff --git a/modules/keycloak/deployment.tf b/modules/keycloak/deployment.tf index f90a298..be58a91 100644 --- a/modules/keycloak/deployment.tf +++ b/modules/keycloak/deployment.tf @@ -45,6 +45,40 @@ resource "kubernetes_stateful_set" "keycloak_cluster" { // Pod Spec spec { + // Using init container to convert + // PEM Key to a DER Key for Keycloak + // to consume + init_container { + name = "certificate-converter" + image = "alpine/openssl:3.5.5" + command = ["/bin/sh", "-c"] + + // User 1000 does not + // exist in Alpine + security_context { + run_as_user = 0 + } + + // Generate some logs + // to indicate signs + // of life + args = [ + "echo 'Starting certificate conversion...' && openssl pkcs8 -topk8 -inform PEM -outform DER -in /mnt/certs/database/certificate/tls.key -out /mnt/der/key.der -nocrypt && chown 1000:0 /mnt/der/key.der && chmod 600 /mnt/der/key.der && echo 'Conversion successful!'" + ] + + // Volume Mounts + volume_mount { + name = "database-client-certificate" + mount_path = "/mnt/certs/database/certificate" + read_only = true + } + + volume_mount { + name = "database-der-key" + mount_path = "/mnt/der" + } + } + // Node Affinity rule to run only on worker nodes affinity { node_affinity { @@ -129,7 +163,7 @@ resource "kubernetes_stateful_set" "keycloak_cluster" { name = "KC_DB_USERNAME" value_from { secret_key_ref { - name = kubernetes_secret.database_credentials.metadata[0].name + name = kubernetes_manifest.database_credentials_sync.object.spec.target.name key = "username" } } @@ -139,7 +173,7 @@ resource "kubernetes_stateful_set" "keycloak_cluster" { name = "KC_DB_PASSWORD" value_from { secret_key_ref { - name = kubernetes_secret.database_credentials.metadata[0].name + name = kubernetes_manifest.database_credentials_sync.object.spec.target.name key = "password" } } @@ -210,12 +244,18 @@ resource "kubernetes_stateful_set" "keycloak_cluster" { // Volume mounts volume_mount { - name = kubernetes_secret.database_server_certificate_authority.metadata[0].name + name = "database-certificate-authority" mount_path = "/mnt/certs/database/certificate-authority" } volume_mount { - name = kubernetes_secret.database_client_certificate.metadata[0].name + name = "database-der-key" + mount_path = "/mnt/der" + read_only = true + } + + volume_mount { + name = "database-client-certificate" mount_path = "/mnt/certs/database/certificate" } @@ -232,16 +272,21 @@ resource "kubernetes_stateful_set" "keycloak_cluster" { // Volumes volume { - name = kubernetes_secret.database_server_certificate_authority.metadata[0].name + name = "database-certificate-authority" secret { - secret_name = kubernetes_secret.database_server_certificate_authority.metadata[0].name + secret_name = kubernetes_manifest.database_server_certificate_authority_sync.object.spec.target.name } } volume { - name = kubernetes_secret.database_client_certificate.metadata[0].name + name = "database-der-key" + empty_dir {} + } + + volume { + name = "database-client-certificate" secret { - secret_name = kubernetes_secret.database_client_certificate.metadata[0].name + secret_name = kubernetes_manifest.database_client_certificate_sync.object.spec.target.name } } @@ -275,5 +320,11 @@ resource "kubernetes_stateful_set" "keycloak_cluster" { } } - depends_on = [kubernetes_service.keycloak_service, kubernetes_service.keycloak_discovery] + depends_on = [ + kubernetes_service.keycloak_service, + kubernetes_service.keycloak_discovery, + kubernetes_manifest.database_credentials_sync, + kubernetes_manifest.database_client_certificate_sync, + kubernetes_manifest.database_server_certificate_authority_sync + ] } diff --git a/modules/keycloak/secrets.tf b/modules/keycloak/secrets.tf index e4c8a96..4499364 100644 --- a/modules/keycloak/secrets.tf +++ b/modules/keycloak/secrets.tf @@ -1,30 +1,43 @@ // Database Credentials to connect to the PostgreSQL Database -resource "kubernetes_secret" "database_credentials" { - metadata { - name = var.database_credentials - namespace = kubernetes_namespace.namespace.metadata[0].name - - labels = { - app = var.app_name - component = "secret" +resource "kubernetes_manifest" "database_credentials_sync" { + manifest = { + apiVersion = "external-secrets.io/v1" + kind = "ExternalSecret" + metadata = { + name = var.database_credentials + namespace = kubernetes_namespace.namespace.metadata[0].name + labels = { + app = var.app_name + component = "secret" + } } - - annotations = { - "reflector.v1.k8s.emberstack.com/reflects" = "${var.postgres_namespace}/${var.database_credentials}" + spec = { + refreshInterval = "1h" + secretStoreRef = { + name = var.cluster_secret_store_name + kind = "ClusterSecretStore" + } + target = { + name = var.database_credentials + template = { + # Using Opaque since the original secret was Opaque + type = "Opaque" + engineVersion = "v2" + } + } + dataFrom = [{ + extract = { + key = "${var.postgres_namespace}/credentials/${var.database_credentials}" + } + }] } - - - } - - data = { - username = "" - password = "" } - type = "Opaque" - - lifecycle { - ignore_changes = [metadata[0].annotations] + wait { + condition { + type = "Ready" + status = "True" + } } } diff --git a/modules/keycloak/variables.tf b/modules/keycloak/variables.tf index 59dfb59..46313f4 100644 --- a/modules/keycloak/variables.tf +++ b/modules/keycloak/variables.tf @@ -36,6 +36,13 @@ variable "observability_namespace" { nullable = false } +# --------------- CLUSTER SECRET STORE VARIABLES --------------- # +variable "cluster_secret_store_name" { + description = "Name of the cluster secret store to be used for pulling and pushing secrets to OpenBao" + type = string + nullable = false +} + # --------------- DATABASE VARIABLES --------------- # variable "cluster_name" { description = "Database Cluster Name to allow Network Connections to" @@ -198,7 +205,7 @@ variable "keycloak_environment_variables" { }, { name = "KC_DB_URL" - value = "jdbc:postgresql://postgresql-cluster-rw.postgres.svc/keycloak?ssl=true&sslmode=verify-full&sslrootcert=/mnt/certs/database/certificate-authority/ca.crt&sslcert=/mnt/certs/database/certificate/tls.crt&sslkey=/mnt/certs/database/certificate/key.der" + value = "jdbc:postgresql://postgresql-cluster-rw.postgres.svc/keycloak?ssl=true&sslmode=verify-full&sslrootcert=/mnt/certs/database/certificate-authority/ca.crt&sslcert=/mnt/certs/database/certificate/tls.crt&sslkey=/mnt/der/key.der" }, { name = "KC_DB_POOL_INITIAL_SIZE" diff --git a/modules/openbao/README.md b/modules/openbao/README.md index ffcb607..c787e5f 100644 --- a/modules/openbao/README.md +++ b/modules/openbao/README.md @@ -2,9 +2,10 @@ OpenTofu Module to deploy [OpenBao](https://openbao.org/) Secrets Management Solution on the Kubernetes Cluster. -Required Modules to deploy OpenBao Secrets Manageemnt Solution: -1. [Cluster Issuer](../cluster-issuer) -2. [Observability](../observability) +Required Modules to deploy OpenBao Secrets Management Solution: +1. [Helm](../helm) +2. [Cluster Issuer](../cluster-issuer) +3. [Observability](../observability) ## Providers