diff --git a/README.md b/README.md index 5413cc0..66c28ef 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ The following modules have been implemented and their usage instructions written 1. [Helm](modules/helm) 2. [Cluster Issuer for internal certificates](modules/cluster-issuer) 3. [Observability](modules/observability) -4. [Garage Storage](modules/garage) -5. [Cloudnative PG PostgreSQL Database](modules/cnpg) -6. [FerretDB (MongoDB) Database](modules/ferretdb) -7. [Valkey In Memory Database](modules/valkey) -8. [Keycloak Identity Management](modules/keycloak) +4. [OpenBao Secrets Management](modules/openbao) +5. [Garage Storage](modules/garage) +6. [Cloudnative PG PostgreSQL Database](modules/cnpg) +7. [FerretDB (MongoDB) Database](modules/ferretdb) +8. [Valkey In Memory Database](modules/valkey) +9. [Keycloak Identity Management](modules/keycloak) diff --git a/infrastructure/main.tf b/infrastructure/main.tf index c09b011..9192b61 100644 --- a/infrastructure/main.tf +++ b/infrastructure/main.tf @@ -33,6 +33,30 @@ module "observability" { depends_on = [module.cluster-issuer] } +# OpenBao Secrets Management Solution deployment +module "secrets" { + source = "git::https://github.com/necro-cloud/modules//modules/openbao?ref=task/110/openbao-deployment" + + // Certificates Details + cluster_issuer_name = module.cluster-issuer.cluster-issuer-name + cloudflare_token = var.cloudflare_token + cloudflare_email = var.cloudflare_email + domain = var.domain + + // Observability details + observability_namespace = module.observability.observability_namespace + + // Granting required namespaces access to the OpenBao cluster + access_namespaces = "external-secrets,cloud" + + // Whitelisting Kubernetes API Endpoints in the Network Policy + kubernetes_api_ip = one(flatten(data.kubernetes_endpoints_v1.kubernetes_api_endpoint.subset[*].address[*].ip)) + kubernetes_api_protocol = one(flatten(data.kubernetes_endpoints_v1.kubernetes_api_endpoint.subset[*].port[*].protocol)) + kubernetes_api_port = one(flatten(data.kubernetes_endpoints_v1.kubernetes_api_endpoint.subset[*].port[*].port)) + + depends_on = [module.observability] +} + # Garage Deployment for an S3 compatible object storage solution module "garage" { source = "git::https://github.com/necro-cloud/modules//modules/garage?ref=main" diff --git a/modules/openbao/README.md b/modules/openbao/README.md new file mode 100644 index 0000000..7778a56 --- /dev/null +++ b/modules/openbao/README.md @@ -0,0 +1,71 @@ +## necronizer's cloud openbao module + +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) + +## Providers + +| Name | Version | +|------|---------| +| [helm](#provider\_helm) | 3.1.1 | +| [kubernetes](#provider\_kubernetes) | 2.38.0 | +| [random](#provider\_random) | 3.7.2 | + +## Resources + +| Name | Type | +|------|------| +| [helm_release.openbao](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | +| [kubernetes_config_map.configurator_script](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/config_map) | resource | +| [kubernetes_ingress_v1.ui_ingress](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/ingress_v1) | resource | +| [kubernetes_job.configurator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/job) | resource | +| [kubernetes_manifest.certificate_authority](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.internal_certificate](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.issuer](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_namespace.namespace](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | +| [kubernetes_network_policy.openbao_network_access_policy](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/network_policy) | resource | +| [kubernetes_role.configurator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role) | resource | +| [kubernetes_role_binding.configurator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role_binding) | resource | +| [kubernetes_secret.cloudflare_token](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | +| [kubernetes_secret.static_unseal_key](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | +| [kubernetes_service_account.configurator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service_account) | resource | +| [random_id.static_unseal_key](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [access\_namespaces](#input\_access\_namespaces) | Namespaces requiring accesses to the OpenBao Cluster in a comma seperated list | `string` | +| [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 OpenBao Secrets Management Solution | `string` | `"openbao"` | no | +| [certificate\_authority\_name](#input\_certificate\_authority\_name) | Name of the Certificate Authority to be associated with OpenBao +| [cloudflare\_email](#input\_cloudflare\_email) | Email for generating Ingress Certificates to be associated with OpenBao Secrets Management Solu +| [cloudflare\_issuer\_name](#input\_cloudflare\_issuer\_name) | Name of the Cloudflare Issuer to be associated with OpenBao Secrets Management Solution | `string` | `"secrets-cloudflare-issuer"` | no | +| [cloudflare\_token](#input\_cloudflare\_token) | Token for generating Ingress Certificates to be associated with OpenBao Secrets Management Solution | `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 | +| [cluster\_size](#input\_cluster\_size) | Number of pods to be deployed for High Availability for OpenBao Secrets Management Solution | `number` | `3` | no | +| [configurator\_image](#input\_configurator\_image) | Docker image to be used for deployment of OpenBao Configurator | `string` | `"openbao"` | no | +| [configurator\_repository](#input\_configurator\_repository) | Repository to be used for deployment of OpenBao Configurator | `string` | `"quay.io/openbao"` | no | +| [configurator\_tag](#input\_configurator\_tag) | Docker tag to be used for deployment of OpenBao Configurator | `string` | `"2.5.1"` | no | +| [country\_name](#input\_country\_name) | Country name for deploying OpenBao Secrets Management Solution | `string` | `"India"` | no | +| [domain](#input\_domain) | Domain for which Ingress Certificate is to be generated for | `string` | n/a | yes | +| [host\_name](#input\_host\_name) | Host name for which Ingress Certificate is to be generated for | `string` | `"secrets"` | no | +| [ingress\_certificate\_name](#input\_ingress\_certificate\_name) | Name of the Ingress Certificate to be associated with OpenBao Secrets Management Solution | `string` | `"secrets-ingress-certificate"` | no | +| [internal\_certificate\_name](#input\_internal\_certificate\_name) | Name of the Internal Certificate to be associated with OpenBao Secrets Management Solution | `string` | `"secrets-internal-certificate"` | no | +| [issuer\_name](#input\_issuer\_name) | Name of the Issuer to be associated with OpenBao Secrets Management Solution | `string` | `"secrets-certificate-issuer"` | no | +| [kubernetes\_api\_ip](#input\_kubernetes\_api\_ip) | IP Address for the Kubernetes API | `string` | n/a | yes | +| [kubernetes\_api\_port](#input\_kubernetes\_api\_port) | Port for the Kubernetes API | `number` | n/a | yes | +| [kubernetes\_api\_protocol](#input\_kubernetes\_api\_protocol) | Protocol for the Kubernetes API | `string` | n/a | yes | +| [namespace](#input\_namespace) | Namespace to be used for deploying OpenBao Secrets Management Solution | `string` | `"openbao"` | no | +| [observability\_namespace](#input\_observability\_namespace) | Namespace where all components for observability are deployed | `string` | n/a | yes | +| [openbao\_configuration](#input\_openbao\_configuration) | Dictionary filled with OpenBao Configuration Details | `map(string)` |
{
"chart": "openbao",
"name": "openbao",
"repository": "https://openbao.github.io/openbao-helm",
"version": "0.25.6"
}
| no | +| [organization\_name](#input\_organization\_name) | Organization name for deploying OpenBao Secrets Management Solution | `string` | `"cloud"` | no | + +## Outputs + +No outputs. diff --git a/modules/openbao/certificates.tf b/modules/openbao/certificates.tf new file mode 100644 index 0000000..92df5ac --- /dev/null +++ b/modules/openbao/certificates.tf @@ -0,0 +1,254 @@ +// Certificate Authority to be used with OpenBao Cluster +resource "kubernetes_manifest" "certificate_authority" { + manifest = { + "apiVersion" = "cert-manager.io/v1" + "kind" = "Certificate" + "metadata" = { + "name" = var.certificate_authority_name + "namespace" = kubernetes_namespace.namespace.metadata[0].name + "labels" = { + "app" = var.app_name + "component" = "certificate-authority" + } + } + "spec" = { + "isCA" = true + "subject" = { + "organizations" = [var.organization_name] + "countries" = [var.country_name] + "organizationalUnits" = [var.app_name] + } + "commonName" = var.certificate_authority_name + "secretName" = var.certificate_authority_name + "duration" = "70128h" + "privateKey" = { + "algorithm" = "ECDSA" + "size" = 256 + } + "issuerRef" = { + "name" = "${var.cluster_issuer_name}" + "kind" = "ClusterIssuer" + "group" = "cert-manager.io" + } + } + } + + wait { + condition { + type = "Ready" + status = "True" + } + } + + timeouts { + create = "5m" + update = "5m" + delete = "5m" + } +} + +// Issuer for the OpenBao Cluster +resource "kubernetes_manifest" "issuer" { + manifest = { + "apiVersion" = "cert-manager.io/v1" + "kind" = "Issuer" + "metadata" = { + "name" = var.issuer_name + "namespace" = kubernetes_namespace.namespace.metadata[0].name + "labels" = { + "app" = var.app_name + "component" = "issuer" + } + } + "spec" = { + "ca" = { + "secretName" = kubernetes_manifest.certificate_authority.manifest.spec.secretName + } + } + } + + wait { + condition { + type = "Ready" + status = "True" + } + } + + timeouts { + create = "5m" + update = "5m" + delete = "5m" + } +} + +// Internal Certificate for OpenBao Cluster +resource "kubernetes_manifest" "internal_certificate" { + manifest = { + "apiVersion" = "cert-manager.io/v1" + "kind" = "Certificate" + "metadata" = { + "name" = var.internal_certificate_name + "namespace" = kubernetes_namespace.namespace.metadata[0].name + "labels" = { + "app" = var.app_name + "component" = "internal-certificate" + } + } + "spec" = { + "dnsNames" = [ + "${var.host_name}.${var.domain}", + "localhost", + "127.0.0.1", + "*.${kubernetes_namespace.namespace.metadata[0].name}.svc.cluster.local", + "openbao-internal", + "openbao-internal.${kubernetes_namespace.namespace.metadata[0].name}.svc", + "openbao-internal.${kubernetes_namespace.namespace.metadata[0].name}.svc.cluster.local", + "*.openbao-internal.${kubernetes_namespace.namespace.metadata[0].name}.svc.cluster.local", + ] + "subject" = { + "organizations" = [var.organization_name] + "countries" = [var.country_name] + "organizationalUnits" = [var.app_name] + } + "commonName" = var.internal_certificate_name + "secretName" = var.internal_certificate_name + "issuerRef" = { + "name" = kubernetes_manifest.issuer.manifest.metadata.name + } + } + } + + wait { + condition { + type = "Ready" + status = "True" + } + } + timeouts { + create = "5m" + update = "5m" + delete = "5m" + } +} + +// Kubernetes Secret for Cloudflare Tokens +resource "kubernetes_secret" "cloudflare_token" { + metadata { + name = "cloudflare-token" + namespace = kubernetes_namespace.namespace.metadata[0].name + labels = { + "app" = var.app_name + "component" = "secret" + } + } + + data = { + cloudflare-token = var.cloudflare_token + } + + type = "Opaque" +} + +// Cloudflare Issuer for Openbao Ingress Service +resource "kubernetes_manifest" "public_issuer" { + manifest = { + "apiVersion" = "cert-manager.io/v1" + "kind" = "Issuer" + "metadata" = { + "name" = var.cloudflare_issuer_name + "namespace" = kubernetes_namespace.namespace.metadata[0].name + "labels" = { + "app" = var.app_name + "component" = "cloudflare-issuer" + } + } + "spec" = { + "acme" = { + "email" = var.cloudflare_email + "server" = var.acme_server + "privateKeySecretRef" = { + "name" = var.cloudflare_issuer_name + } + "solvers" = [ + { + "dns01" = { + "cloudflare" = { + "email" = var.cloudflare_email + "apiTokenSecretRef" = { + "name" = "cloudflare-token" + "key" = "cloudflare-token" + } + } + } + } + ] + } + } + } + + depends_on = [kubernetes_secret.cloudflare_token] + + wait { + condition { + type = "Ready" + status = "True" + } + } + + timeouts { + create = "5m" + update = "5m" + delete = "5m" + } +} + +// Certificate to be used for OpenBao Ingress +resource "kubernetes_manifest" "ingress_certificate" { + + manifest = { + "apiVersion" = "cert-manager.io/v1" + "kind" = "Certificate" + "metadata" = { + "name" = var.ingress_certificate_name + "namespace" = kubernetes_namespace.namespace.metadata[0].name + "labels" = { + "app" = var.app_name + "component" = "ingress-certificate" + } + } + "spec" = { + "duration" = "2160h" + "renewBefore" = "360h" + "subject" = { + "organizations" = [var.organization_name] + "countries" = [var.country_name] + "organizationalUnits" = [var.app_name] + } + "privateKey" = { + "algorithm" = "RSA" + "encoding" = "PKCS1" + "size" = "2048" + } + "dnsNames" = ["${var.host_name}.${var.domain}"] + "secretName" = var.ingress_certificate_name + "issuerRef" = { + "name" = kubernetes_manifest.public_issuer.manifest.metadata.name + "kind" = "Issuer" + "group" = "cert-manager.io" + } + } + } + + wait { + condition { + type = "Ready" + status = "True" + } + } + + timeouts { + create = "5m" + update = "5m" + delete = "5m" + } +} diff --git a/modules/openbao/config/configurator.sh b/modules/openbao/config/configurator.sh new file mode 100644 index 0000000..307cd9f --- /dev/null +++ b/modules/openbao/config/configurator.sh @@ -0,0 +1,76 @@ +#!/bin/sh +set -e + +# Installing required dependencies +echo "Installing required dependencies..." +apk add --no-cache curl jq + +# Required Environment Variables +NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace) +K8S_API="https://kubernetes.default.svc" +K8S_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) +K8S_CACERT="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + +# OpenBao TLS Setup +BAO_ADDR="https://openbao-internal.$NAMESPACE.svc:8200" +BAO_CACERT="/openbao/userconfig/${cert_secret_name}/ca.crt" + +export BAO_ADDR=$BAO_ADDR +export BAO_CACERT=$BAO_CACERT + +# Wait for OpenBao API to respond over HTTPS +echo "Waiting for OpenBao API at $BAO_ADDR..." +until curl -s --cacert "$BAO_CACERT" "$BAO_ADDR/v1/sys/health" | grep -q 'initialized'; do + echo "Still waiting..." + sleep 5 +done + +# Initialize the Cluster +if ! bao operator init -status > /dev/null 2>&1; then + echo "Initializing OpenBao Cluster..." + bao operator init -format=json > /tmp/keys.json + + ROOT_TOKEN=$(jq -r '.root_token' /tmp/keys.json) + export BAO_TOKEN=$ROOT_TOKEN + + # Save Keys to K8s Secret + echo "Persisting recovery keys to Kubernetes..." + B64_DATA=$(cat /tmp/keys.json | base64 | tr -d '\n') + + curl -s --cacert "$K8S_CACERT" \ + -X POST \ + -H "Authorization: Bearer $K8S_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"apiVersion\": \"v1\", + \"kind\": \"Secret\", + \"metadata\": { \"name\": \"bao-init-recovery\" }, + \"data\": { \"keys.json\": \"$B64_DATA\" } + }" \ + "$K8S_API/api/v1/namespaces/$NAMESPACE/secrets" + + # Configure Kubernetes Auth for External Secrets Operator + echo "Configuring Kubernetes Auth..." + bao auth enable kubernetes + bao write auth/kubernetes/config \ + kubernetes_host="$K8S_API" \ + kubernetes_ca_cert="@$K8S_CACERT" \ + disable_iss_validation=true + + # Setup ESO Access + bao secrets enable -path=secret kv-v2 + bao policy write eso-policy - <