diff --git a/src/_nebari/stages/kubernetes_ingress/__init__.py b/src/_nebari/stages/kubernetes_ingress/__init__.py index df70e12b1e..077fd54228 100644 --- a/src/_nebari/stages/kubernetes_ingress/__init__.py +++ b/src/_nebari/stages/kubernetes_ingress/__init__.py @@ -1,5 +1,6 @@ import enum import logging +import os import socket import sys import time @@ -124,6 +125,11 @@ def to_yaml(cls, representer, node): return representer.represent_str(node.value) +class AcmeChallengeType(str, enum.Enum): + tls = "tls" + dns = "dns" + + class Certificate(schema.Base): type: CertificateEnum = CertificateEnum.selfsigned # existing @@ -131,6 +137,7 @@ class Certificate(schema.Base): # lets-encrypt acme_email: Optional[str] = None acme_server: str = "https://acme-v02.api.letsencrypt.org/directory" + acme_challenge_type: Optional[str] = AcmeChallengeType.tls.value class DnsProvider(schema.Base): @@ -183,7 +190,19 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): cert_details["certificate-secret-name"] = ( self.config.certificate.secret_name ) - + cert_details["acme-challenge-type"] = ( + self.config.certificate.acme_challenge_type + ) + if self.config.certificate.acme_challenge_type == AcmeChallengeType.dns.value: + if os.environ.get("CLOUDFLARE_TOKEN") is None: + raise ValueError( + "Environment variable 'CLOUDFLARE_TOKEN' must be set along with " + "'DNS:Edit' permission for DNS challenge type ('acme_challenge_type: dns')" + ) + else: + cert_details["cloudflare-dns-api-token"] = os.environ.get( + "CLOUDFLARE_TOKEN" + ) return { **{ "traefik-image": { diff --git a/src/_nebari/stages/kubernetes_ingress/template/main.tf b/src/_nebari/stages/kubernetes_ingress/template/main.tf index 6e275cb39a..b8476b8b49 100644 --- a/src/_nebari/stages/kubernetes_ingress/template/main.tf +++ b/src/_nebari/stages/kubernetes_ingress/template/main.tf @@ -10,6 +10,8 @@ module "kubernetes-ingress" { certificate-service = var.certificate-service acme-email = var.acme-email acme-server = var.acme-server + acme-challenge-type = var.acme-challenge-type + cloudflare-dns-api-token = var.cloudflare-dns-api-token certificate-secret-name = var.certificate-secret-name load-balancer-annotations = var.load-balancer-annotations load-balancer-ip = var.load-balancer-ip diff --git a/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/main.tf b/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/main.tf index 217039f420..77e0b7a58b 100644 --- a/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/main.tf +++ b/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/main.tf @@ -3,15 +3,33 @@ locals { "--entrypoints.websecure.http.tls.certResolver=default", "--entrypoints.minio.http.tls.certResolver=default", ] + certificate-challenge = { + dns = [ + "--certificatesresolvers.letsencrypt.acme.dnschallenge=true", + # Only cloudflare is supported at the moment for DNS challenge + # TODO: add support for other DNS providers + "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare" + ] + tls = [ + "--certificatesresolvers.letsencrypt.acme.tlschallenge", + ] + } + # for dns challenge, we need to set the cloudflare env vars + cloudflare_env_vars = var.acme-challenge-type == "dns" ? [ + { + name = "CLOUDFLARE_DNS_API_TOKEN" + value = var.cloudflare-dns-api-token + } + ] : [] certificate-settings = { - lets-encrypt = [ + lets-encrypt = concat([ "--entrypoints.websecure.http.tls.certResolver=letsencrypt", "--entrypoints.minio.http.tls.certResolver=letsencrypt", - "--certificatesresolvers.letsencrypt.acme.tlschallenge", "--certificatesresolvers.letsencrypt.acme.email=${var.acme-email}", "--certificatesresolvers.letsencrypt.acme.storage=/mnt/acme-certificates/acme.json", "--certificatesresolvers.letsencrypt.acme.caserver=${var.acme-server}", - ] + ], local.certificate-challenge[var.acme-challenge-type] + ) self-signed = local.default_cert existing = local.default_cert disabled = [] @@ -231,6 +249,14 @@ resource "kubernetes_deployment" "main" { image = "${var.traefik-image.image}:${var.traefik-image.tag}" name = var.name + dynamic "env" { + for_each = local.cloudflare_env_vars + content { + name = env.value.name + value = env.value.value + } + } + volume_mount { mount_path = "/mnt/acme-certificates" name = "acme-certificates" diff --git a/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/variables.tf b/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/variables.tf index 20e869a13d..a895d7b505 100644 --- a/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/variables.tf +++ b/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/variables.tf @@ -44,6 +44,23 @@ variable "acme-server" { default = "https://acme-staging-v02.api.letsencrypt.org/directory" } +variable "acme-challenge-type" { + # https://letsencrypt.org/docs/challenge-types + description = "ACME challenge type, 'tls' or 'dns'" + default = "tls" +} + +variable "cloudflare-dns-api-token" { + # https://go-acme.github.io/lego/dns/cloudflare/ + description = "Cloudflare dns api token for DNS challenge" + default = null +} + +variable "acme-dns-provider" { + description = "ACME DNS provider" + default = "route53" +} + variable "certificate-secret-name" { description = "Kubernetes secret used for certificate" type = string diff --git a/src/_nebari/stages/kubernetes_ingress/template/variables.tf b/src/_nebari/stages/kubernetes_ingress/template/variables.tf index ebafdac2f7..940d0597e3 100644 --- a/src/_nebari/stages/kubernetes_ingress/template/variables.tf +++ b/src/_nebari/stages/kubernetes_ingress/template/variables.tf @@ -37,6 +37,18 @@ variable "acme-server" { default = "https://acme-staging-v02.api.letsencrypt.org/directory" } +variable "acme-challenge-type" { + # https://letsencrypt.org/docs/challenge-types + description = "ACME challenge type, 'tls' or 'dns'" + default = "tls" +} + +variable "cloudflare-dns-api-token" { + # https://go-acme.github.io/lego/dns/cloudflare/ + description = "Cloudflare dns api token for DNS challenge" + default = null +} + variable "certificate-secret-name" { description = "Kubernetes secret used for certificate" default = "" diff --git a/tests/tests_unit/cli_validate/min.happy.certificate.yaml b/tests/tests_unit/cli_validate/min.happy.certificate.yaml new file mode 100644 index 0000000000..49448b0d1d --- /dev/null +++ b/tests/tests_unit/cli_validate/min.happy.certificate.yaml @@ -0,0 +1,6 @@ +project_name: test +certificate: + type: lets-encrypt + acme_email: admin@quansight.com + acme_server: https://acme-v02.api.letsencrypt.org/directory + acme_challenge_type: dns