diff --git a/terraform/k8sapps/config.tf b/terraform/k8sapps/config.tf index 7b3e086..494d8c2 100644 --- a/terraform/k8sapps/config.tf +++ b/terraform/k8sapps/config.tf @@ -9,9 +9,11 @@ locals { primary_domain = "alexpires.me" websites = { "alexpires.me" = { - domain_name = "alexpires.me" - fqdn = ["blog.alexpires.me", "www.alexpires.me", "alexpires.me"] - config = file("${path.module}/resources/alexpires.me.conf") + domain_name = "alexpires.me" + fqdn = ["blog.alexpires.me", "www.alexpires.me", "alexpires.me"] + tls = true + issuer = "internal-ca" + custom_snippet = file("${path.module}/resources/alexpires.me.conf") } } diff --git a/terraform/k8sapps/issuer.tf b/terraform/k8sapps/issuer.tf new file mode 100644 index 0000000..612815a --- /dev/null +++ b/terraform/k8sapps/issuer.tf @@ -0,0 +1,33 @@ +resource "kubernetes_secret" "root_ca" { + metadata { + name = "root-ca" + namespace = "cert-manager" + } + data = { + "tls.crt" = var.root_ca_pem + "tls.key" = var.root_ca_key + } + + type = "kubernetes.io/tls" +} + +resource "kubernetes_manifest" "cluster_issuer" { + + manifest = { + apiVersion = "cert-manager.io/v1" + kind = "ClusterIssuer" + metadata = { + name = "internal-ca" + annotations = { + "cert-manager.io/issuer-group" = "cert-manager.io" + "cert-manager.io/issuer-kind" = "Issuer" + "cert-manager.io/issuer-name" = "internal-ca" + } + } + spec = { + ca = { + secretName = "root-ca" + } + } + } +} diff --git a/terraform/k8sapps/resources/alexpires.me.conf b/terraform/k8sapps/resources/alexpires.me.conf index cc8f60a..9b784c2 100644 --- a/terraform/k8sapps/resources/alexpires.me.conf +++ b/terraform/k8sapps/resources/alexpires.me.conf @@ -1,46 +1,11 @@ -server { - listen 8080; - server_name localhost; - root /tmp/site; +resolver kube-dns.kube-system.svc.cluster.local valid=10s; - resolver kube-dns.kube-system.svc.cluster.local valid=10s; - - gzip on; - gzip_comp_level 9; - gzip_types - text/html - text/plain - text/css - text/js - text/xml - text/javascript - application/javascript - application/json - application/xml - application/rss+xml - image/svg+xml; - - location ^~ /config/dav { - proxy_pass http://http.sftpgo.svc.cluster.local:8080/config/dav; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Port $server_port; - } - - location / { - index index.html index.htm; - } - - location ~* ^/([^/]+) { - index index.html index.htm; - error_page 404 = @error; - } - - error_page 404 /404.html; - location @error { - try_files /$1/404.html /404.html =404; - } +location ^~ /config/dav { + proxy_pass https://web.sftpgo.svc.cluster.local/config/dav; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; } diff --git a/terraform/k8sapps/secrets.tf b/terraform/k8sapps/secrets.tf index e24d0f3..65517de 100644 --- a/terraform/k8sapps/secrets.tf +++ b/terraform/k8sapps/secrets.tf @@ -71,3 +71,14 @@ variable "sftpgo_admin_password" { sensitive = true } +variable "root_ca_pem" { + description = "Root CA PEM" + type = string + sensitive = true +} + +variable "root_ca_key" { + description = "Root CA Key" + type = string + sensitive = true +} diff --git a/terraform/k8sapps/sectool.env b/terraform/k8sapps/sectool.env index 685f5ff..dab5461 100644 --- a/terraform/k8sapps/sectool.env +++ b/terraform/k8sapps/sectool.env @@ -21,4 +21,6 @@ TF_VAR_alexpires_me_email_username=$ALEXPIRES_ME_EMAIL_USERNAME TF_VAR_alexpires_me_email_password=$ALEXPIRES_ME_EMAIL_PASSWORD TF_VAR_mail_crypt_private_key=$DOVECOT_CRYPT_PRIVATE_KEY_FILE TF_VAR_mail_crypt_public_key=$DOVECOT_CRYPT_PUB_KEY_FILE -TF_VAR_sftpgo_admin_password=$SFTPGO_ADMIN_PASSWORD \ No newline at end of file +TF_VAR_sftpgo_admin_password=$SFTPGO_ADMIN_PASSWORD +TF_VAR_root_ca_pem=$K8S_ROOT_CA_PEM +TF_VAR_root_ca_key=$K8S_ROOT_CA_KEY diff --git a/terraform/modules/apps/nginx/main.tf b/terraform/modules/apps/nginx/main.tf index 076a19e..3575741 100644 --- a/terraform/modules/apps/nginx/main.tf +++ b/terraform/modules/apps/nginx/main.tf @@ -22,7 +22,6 @@ resource "kubernetes_service_account" "website_service_account" { automount_service_account_token = false } - resource "kubernetes_service" "website" { for_each = local.websites metadata { @@ -31,9 +30,18 @@ resource "kubernetes_service" "website" { } spec { port { + name = "http" port = 80 target_port = "http" } + dynamic "port" { + for_each = each.value.tls ? [443] : [] + content { + name = "https" + port = 443 + target_port = "https" + } + } selector = { app = format("website-%s", replace(each.key, ".", "-")) } @@ -41,6 +49,29 @@ resource "kubernetes_service" "website" { } } +resource "kubernetes_manifest" "internal_cert" { + for_each = { + for k, v in local.websites : k => v if v.tls == true + } + manifest = { + apiVersion = "cert-manager.io/v1" + kind = "Certificate" + metadata = { + name = format("website-%s-tls-internal", replace(each.key, ".", "-")) + namespace = kubernetes_namespace.website.metadata[0].name + } + spec = { + secretName = format("website-%s-tls-internal", replace(each.key, ".", "-")) + issuerRef = { + name = each.value.issuer + kind = "ClusterIssuer" + } + commonName = each.value.domain_name + dnsNames = each.value.fqdn + } + } +} + resource "kubernetes_config_map" "nginx_config" { metadata { name = "nginx-config" @@ -60,7 +91,11 @@ resource "kubernetes_config_map" "nginx_default_config" { } data = { - "default.conf" = each.value.config + "default.conf" = coalesce(each.value.config, templatefile("${path.module}/resources/conf.d/default.conf.tpl", { + tls = each.value.tls + fqdn = each.value.domain_name + custom_snippet = each.value.custom_snippet + })) } } @@ -70,10 +105,17 @@ resource "kubernetes_ingress_v1" "website_ingress" { name = "website-ingress" namespace = kubernetes_namespace.website.metadata[0].name - annotations = { + annotations = merge({ "kubernetes.io/ingress.class" = "public" "cert-manager.io/cluster-issuer" = "letsencrypt-prod" - } + }, + each.value.tls ? { + "nginx.org/ssl-services" = format("website-%s", replace(each.key, ".", "-")) + "nginx.org/ssl-redirect" = "true" + "nginx.org/redirect-to-https" = "true" + "nginx.ingress.kubernetes.io/backend-protocol" = "HTTPS" + } : {} + ) } spec { tls { @@ -90,7 +132,7 @@ resource "kubernetes_ingress_v1" "website_ingress" { service { name = format("website-%s", replace(each.key, ".", "-")) port { - number = 80 + name = each.value.tls ? "https" : "http" } } } @@ -123,8 +165,13 @@ resource "kubernetes_deployment" "website" { app = format("website-%s", replace(each.key, ".", "-")) } annotations = { - "checksum/nginx" = local.nginx_config_checksum - "checksum/default" = sha256(each.value.config) + "checksum/nginx" = local.nginx_config_checksum + "checksum/default" = sha256(coalesce(each.value.config, templatefile("${path.module}/resources/conf.d/default.conf.tpl", { + tls = each.value.tls + fqdn = each.value.domain_name + custom_snippet = each.value.custom_snippet + })) + ) } } spec { @@ -175,6 +222,14 @@ resource "kubernetes_deployment" "website" { container_port = 8080 name = "http" } + + dynamic "port" { + for_each = each.value.tls ? [443] : [] + content { + container_port = 8443 + name = "https" + } + } volume_mount { name = "nginx" mount_path = "/etc/nginx/nginx.conf" @@ -192,6 +247,14 @@ resource "kubernetes_deployment" "website" { mount_path = "/tmp/site" read_only = true } + dynamic "volume_mount" { + for_each = each.value.tls ? [443] : [] + content { + name = "tls" + mount_path = "/etc/ssl/certs/private" + read_only = true + } + } } volume { name = "nginx" @@ -211,9 +274,27 @@ resource "kubernetes_deployment" "website" { path = format("%s/%s", local.websites_path, each.key) } } + dynamic "volume" { + for_each = each.value.tls ? [443] : [] + content { + name = "tls" + secret { + secret_name = kubernetes_manifest.internal_cert[each.key].manifest["metadata"]["name"] + items { + key = "tls.crt" + path = "tls.crt" + } + items { + key = "tls.key" + path = "tls.key" + } + } + } + } } } } + lifecycle { ignore_changes = [spec[0].template[0].metadata[0].annotations["kubectl.kubernetes.io/restartedAt"]] } diff --git a/terraform/modules/apps/nginx/resources/conf.d/default.conf b/terraform/modules/apps/nginx/resources/conf.d/default.conf.tpl similarity index 67% rename from terraform/modules/apps/nginx/resources/conf.d/default.conf rename to terraform/modules/apps/nginx/resources/conf.d/default.conf.tpl index d352c7b..348bea6 100644 --- a/terraform/modules/apps/nginx/resources/conf.d/default.conf +++ b/terraform/modules/apps/nginx/resources/conf.d/default.conf.tpl @@ -1,7 +1,12 @@ server { listen 8080; - server_name localhost; - +%{ if tls ~} + listen 8443 ssl; + ssl_certificate /etc/ssl/certs/private/tls.crt; + ssl_certificate_key /etc/ssl/certs/private/tls.key; + ssl_protocols TLSv1.2 TLSv1.3; +%{ endif ~} + server_name ${fqdn}; gzip on; gzip_comp_level 9; gzip_types @@ -16,18 +21,17 @@ server { application/xml application/rss+xml image/svg+xml; - root /tmp/site; - +%{ if custom_snippet != "" ~} +${custom_snippet} +%{ endif ~} location / { index index.html index.htm; } - location ~* ^/([^/]+) { index index.html index.htm; error_page 404 = @error; } - error_page 404 /404.html; location @error { try_files /$1/404.html /404.html =404; diff --git a/terraform/modules/apps/nginx/variables.tf b/terraform/modules/apps/nginx/variables.tf index 131df0b..75652b3 100644 --- a/terraform/modules/apps/nginx/variables.tf +++ b/terraform/modules/apps/nginx/variables.tf @@ -12,8 +12,12 @@ variable "tag" { variable "websites" { description = "A list of websites to be deployed" type = map(object({ - domain_name = string - fqdn = list(string) - config = optional(string, "") + domain_name = string + fqdn = list(string) + config = optional(string, "") + tls = optional(bool, false) + issuer = optional(string, "internal-ca") + custom_snippet = optional(string, "") })) } + diff --git a/terraform/modules/apps/sftpgo/config.tf b/terraform/modules/apps/sftpgo/config.tf index fed45a0..73a7220 100644 --- a/terraform/modules/apps/sftpgo/config.tf +++ b/terraform/modules/apps/sftpgo/config.tf @@ -3,70 +3,76 @@ locals { sftpgo_config = { - "common" : { - "defender" : { - "enabled" : false + common = { + defender = { + enabled = false } }, - "sftpd" : { - "bindings" : [ + sftpd = { + bindings = [ { - "port" : 0 + port = 0 } ] }, - "webdavd" : { - "bindings" : [ + webdavd = { + bindings = [ { - "port" : 8081, - "address" : "0.0.0.0" + port = 8081 + address = "0.0.0.0" + enable_https = var.tls_enabled + certificate_file = "/etc/ssl/certs/private/tls.crt" + certificate_key_file = "/etc/ssl/certs/private/tls.key" } ] }, - "data_provider" : { - "create_default_admin" : true, - "backups_path" : "/var/lib/sftpgo/backup" + data_provider = { + create_default_admin = true + backups_path = "/var/lib/sftpgo/backup" }, - "httpd" : { - "bindings" : [ + httpd = { + bindings = [ { - "port" : 8080, - "address" : "0.0.0.0", - "branding" : { - "web_client" : { - "name" : "alexpires.me cloud", - "short_name" : "alexpires.me" + port = 8080 + address = "0.0.0.0" + enable_https = var.tls_enabled + certificate_file = "/etc/ssl/certs/private/tls.crt" + certificate_key_file = "/etc/ssl/certs/private/tls.key" + branding = { + web_client = { + name = "alexpires.me cloud" + short_name = "alexpires.me" }, - "web_admin" : { - "name" : "alexpires.me cloud", - "short_name" : "alexpires.me" + web_admin = { + name = "alexpires.me cloud" + short_name = "alexpires.me" } } } ], - "web_root" : "/config/dav", + web_root = "/config/dav", }, - "telemetry" : { - "bind_port" : 10000, - "bind_address" : "0.0.0.0" + telemetry = { + bind_port = 10000 + bind_address = "0.0.0.0" }, - "mfa" : { - "totp" : [ + mfa = { + totp = [ { - "name" : "Default", - "issuer" : "alexpires.me", - "algo" : "sha1" + name = "Default", + issuer = "alexpires.me" + algo = "sha1" } ] }, - "smtp" : { - "host" : var.smtp_host, - "port" : var.smtp_port, - "from" : var.from_email - "encryption" : 1, - "domain" : "alexpires.me" + smtp = { + host = var.smtp_host, + port = var.smtp_port, + from = var.from_email + encryption = 1, + domain = "alexpires.me" }, - "plugins" : [] + plugins = [] } config = jsonencode(local.sftpgo_config) diff --git a/terraform/modules/apps/sftpgo/ingress.tf b/terraform/modules/apps/sftpgo/ingress.tf index 76dae08..ab5d6e0 100644 --- a/terraform/modules/apps/sftpgo/ingress.tf +++ b/terraform/modules/apps/sftpgo/ingress.tf @@ -6,7 +6,7 @@ resource "kubernetes_ingress_v1" "ingress" { name = "webdav-ingress" namespace = kubernetes_namespace.sftpgo.metadata[0].name - annotations = { + annotations = merge({ "kubernetes.io/ingress.class" = "public" "cert-manager.io/cluster-issuer" = "letsencrypt-prod" "kubernetes.io/tls-acme" = "true" @@ -15,7 +15,15 @@ resource "kubernetes_ingress_v1" "ingress" { "nginx.ingress.kubernetes.io/proxy-request-buffering" = "on" "nginx.ingress.kubernetes.io/proxy-buffer-size" = "32k" "nginx.ingress.kubernetes.io/proxy-buffers-number" = "16" - } + }, + var.tls_enabled ? { + "nginx.org/ssl-services" = "webdav" + "nginx.org/ssl-redirect" = "true" + "nginx.org/redirect-to-https" = "true" + "nginx.ingress.kubernetes.io/backend-protocol" = "HTTPS" + } : {} + ) + } spec { tls { @@ -30,7 +38,7 @@ resource "kubernetes_ingress_v1" "ingress" { service { name = "webdav" port { - number = 8081 + name = var.tls_enabled ? "davs" : "dav" } } } diff --git a/terraform/modules/apps/sftpgo/main.tf b/terraform/modules/apps/sftpgo/main.tf index 37e56d9..65bd048 100644 --- a/terraform/modules/apps/sftpgo/main.tf +++ b/terraform/modules/apps/sftpgo/main.tf @@ -4,6 +4,27 @@ resource "kubernetes_namespace" "sftpgo" { } } +resource "kubernetes_manifest" "internal_cert" { + count = var.tls_enabled ? 1 : 0 + manifest = { + apiVersion = "cert-manager.io/v1" + kind = "Certificate" + metadata = { + name = "internal-cert" + namespace = kubernetes_namespace.sftpgo.metadata[0].name + } + spec = { + secretName = "internal-cert" + issuerRef = { + name = var.issuer + kind = "ClusterIssuer" + } + commonName = var.fqdn + dnsNames = [var.fqdn] + } + } +} + resource "kubernetes_config_map" "sftpgo" { metadata { name = "sftpgo" @@ -121,12 +142,13 @@ resource "kubernetes_deployment" "sftpgo" { } port { container_port = 8081 - name = "webdav" + name = "dav" protocol = "TCP" } + port { container_port = 8080 - name = "http" + name = "web" protocol = "TCP" } port { @@ -166,6 +188,14 @@ resource "kubernetes_deployment" "sftpgo" { sub_path = "credentials" read_only = true } + dynamic "volume_mount" { + for_each = var.tls_enabled ? [1] : [] + content { + mount_path = "/etc/ssl/certs/private" + name = "internal-cert" + read_only = true + } + } } volume { name = "config" @@ -191,6 +221,15 @@ resource "kubernetes_deployment" "sftpgo" { name = kubernetes_config_map.object_store_credentials.metadata[0].name } } + dynamic "volume" { + for_each = var.tls_enabled ? [1] : [] + content { + name = "internal-cert" + secret { + secret_name = kubernetes_manifest.internal_cert[0].manifest["metadata"]["name"] + } + } + } } } } @@ -206,17 +245,18 @@ resource "kubernetes_service" "sftpgo_webdav" { app = "sftpgo" } port { + name = var.tls_enabled ? "davs" : "dav" protocol = "TCP" - port = 8081 - target_port = "webdav" + port = var.tls_enabled ? 443 : 80 + target_port = "dav" } cluster_ip = "None" } } -resource "kubernetes_service" "sftpgo_http" { +resource "kubernetes_service" "sftpgo_web" { metadata { - name = "http" + name = "web" namespace = kubernetes_namespace.sftpgo.metadata[0].name } spec { @@ -225,9 +265,9 @@ resource "kubernetes_service" "sftpgo_http" { } port { protocol = "TCP" - port = 8080 - target_port = "http" + port = var.tls_enabled ? 443 : 80 + target_port = "web" } - cluster_ip = "None" + type = "ClusterIP" } } diff --git a/terraform/modules/apps/sftpgo/variables.tf b/terraform/modules/apps/sftpgo/variables.tf index 278a9d9..49ff9ad 100644 --- a/terraform/modules/apps/sftpgo/variables.tf +++ b/terraform/modules/apps/sftpgo/variables.tf @@ -71,3 +71,16 @@ variable "fqdn" { description = "The fully qualified domain name for the Rustdesk server" type = string } + +variable "issuer" { + description = "The issuer for the certificate" + type = string + default = "internal-ca" +} + +variable "tls_enabled" { + description = "Enable TLS for the SFTPGo server" + type = bool + default = true +} +