diff --git a/apps/dbgate/application.tf b/apps/dbgate/application.tf index 28e1324..51ae441 100644 --- a/apps/dbgate/application.tf +++ b/apps/dbgate/application.tf @@ -1,39 +1,46 @@ locals { - app-name = var.component == var.instance ? var.instance : format("%s-%s", var.component, var.instance) - main-group = format("app-%s", local.app-name) -} -data "authentik_group" "akadmin" { - name = "authentik Admins" -} -resource "authentik_group" "groups" { - name = local.main-group - attributes = jsonencode({"${local.app-name}" = true}) + dns-name = "${var.sub-domain}.${var.domain-name}" + dns-names = [local.dns-name] } -resource "authentik_application" "prj_app" { - name = "${var.instance}" - slug = "${var.component}-${var.instance}" - group = var.app-group - protocol_provider = authentik_provider_oauth2.oauth2.id - meta_launch_url = format("https://%s.%s", var.sub-domain, var.domain-name) - meta_icon = format("https://%s.%s/%s", var.sub-domain, var.domain-name, "logo192.png") +module "ingress" { + source = "../../modules/ingress" + "component" = var.component + "instance" = var.instance + "namespace" = var.namespace + "issuer" = var.issuer + "ingress-class" = var.ingress-class + "labels" = local.common-labels + "dns-names" = local.dns-names } -resource "authentik_policy_expression" "policy" { - name = local.main-group - expression = <<-EOF - attr = request.user.group_attributes() - return attr['${local.app-name}'] if '${local.app-name}' in attr else False - EOF +module "application" { + source = "../../modules/application" + "component" = var.component + "instance" = var.instance + "app-group" = var.app-group + "sub-domain" = var.sub-domain + "domain-name" = var.domain-name + "icon" = "logo192.png" + "protocol_provider" = var.use-oauth?module.oauth2.provider-id:module.forward.provider-id } -resource "authentik_policy_binding" "prj_access_users" { - target = authentik_application.prj_app.uuid - policy = authentik_policy_expression.policy.id - order = 0 +module "oauth2" { + count = var.use-oauth?1:0 + source = "../../modules/oauth2" + "component" = var.component + "instance" = var.instance } -resource "authentik_policy_binding" "prj_access_vynil" { - target = authentik_application.prj_app.uuid - group = data.authentik_group.akadmin.id - order = 1 + +module "forward" { + count = var.use-oauth?0:1 + source = "../../modules/forward" + "component" = var.component + "instance" = var.instance + "domain" = var.domain + "namespace" = var.namespace + "ingress-class" = var.ingress-class + "labels" = local.common-labels + "dns-names" = local.dns-names + "authentik-token" = data.kubernetes_secret_v1.authentik.data["AUTHENTIK_BOOTSTRAP_TOKEN"] } diff --git a/apps/dbgate/configs.tf b/apps/dbgate/configs.tf index 4fa7c25..da957d2 100644 --- a/apps/dbgate/configs.tf +++ b/apps/dbgate/configs.tf @@ -51,27 +51,6 @@ locals { connection_secrets = merge(local.pg_secrets,local.mongo_secrets) } - -resource "kubectl_manifest" "dbgate-init" { - yaml_body = <<-EOF - apiVersion: v1 - kind: ConfigMap - metadata: - name: "${var.component}-${var.instance}-init" - namespace: "${var.namespace}" - labels: ${jsonencode(local.common-labels)} - data: - start.sh: |- - if [ -e /etc/local-ca/ca.crt ];then - cp /etc/local-ca/ca.crt /usr/local/share/ca-certificates/ - update-ca-certificates - fi - exec /bin/su node -c /home/dbgate-docker/entrypoint.sh "$@" - EOF -} - - - resource "kubectl_manifest" "dbgate-config" { yaml_body = <<-EOF apiVersion: v1 diff --git a/apps/dbgate/deploy.tf b/apps/dbgate/deploy.tf index 452963a..c1fa4cb 100644 --- a/apps/dbgate/deploy.tf +++ b/apps/dbgate/deploy.tf @@ -1,3 +1,7 @@ +locals { + deploy-envs = merge({}, +} + resource "kubectl_manifest" "deploy" { yaml_body = <<-EOF apiVersion: apps/v1 diff --git a/apps/dbgate/index.yaml b/apps/dbgate/index.yaml index cc247de..961b7c4 100644 --- a/apps/dbgate/index.yaml +++ b/apps/dbgate/index.yaml @@ -6,117 +6,12 @@ metadata: name: dbgate description: null options: - mongo: - default: [] - examples: - - [] - items: - properties: - dbname: - default: '' - type: string - name: - default: '' - type: string - namespace: - default: '' - type: string - secret: - properties: - key: - default: '' - type: string - name: - default: '' - type: string - type: object - username: - default: '' - type: string - type: object - type: array issuer: default: letsencrypt-prod examples: - letsencrypt-prod type: string - ingress-class: - default: traefik - examples: - - traefik - type: string - storage: - default: - accessMode: ReadWriteOnce - size: 1Gi - type: Filesystem - examples: - - accessMode: ReadWriteOnce - size: 1Gi - type: Filesystem - properties: - accessMode: - default: ReadWriteOnce - enum: - - ReadWriteOnce - - ReadOnlyMany - - ReadWriteMany - type: string - size: - default: 1Gi - type: string - type: - default: Filesystem - enum: - - Filesystem - - Block - type: string - type: object - pg: - default: [] - examples: - - [] - items: - properties: - dbname: - default: '' - type: string - name: - default: '' - type: string - namespace: - default: '' - type: string - secret: - properties: - key: - default: '' - type: string - name: - default: '' - type: string - type: object - username: - default: '' - type: string - type: object - type: array - domain-name: - default: your_company.com - examples: - - your_company.com - type: string - sub-domain: - default: dbgate - examples: - - dbgate - type: string - domain: - default: your-company - examples: - - your-company - type: string - maria: + mongo: default: [] examples: - [] @@ -151,20 +46,20 @@ options: pullPolicy: IfNotPresent registry: docker.io repository: dbgate/dbgate - tag: 5.2.7 + tag: 5.2.7-alpine examples: - dbgate: pullPolicy: IfNotPresent registry: docker.io repository: dbgate/dbgate - tag: 5.2.7 + tag: 5.2.7-alpine properties: dbgate: default: pullPolicy: IfNotPresent registry: docker.io repository: dbgate/dbgate - tag: 5.2.7 + tag: 5.2.7-alpine properties: pullPolicy: default: IfNotPresent @@ -180,10 +75,120 @@ options: default: dbgate/dbgate type: string tag: - default: 5.2.7 + default: 5.2.7-alpine type: string type: object type: object + sub-domain: + default: dbgate + examples: + - dbgate + type: string + use-oauth: + default: false + examples: + - false + type: boolean + domain: + default: your-company + examples: + - your-company + type: string + ingress-class: + default: traefik + examples: + - traefik + type: string + pg: + default: [] + examples: + - [] + items: + properties: + dbname: + default: '' + type: string + name: + default: '' + type: string + namespace: + default: '' + type: string + secret: + properties: + key: + default: '' + type: string + name: + default: '' + type: string + type: object + username: + default: '' + type: string + type: object + type: array + maria: + default: [] + examples: + - [] + items: + properties: + dbname: + default: '' + type: string + name: + default: '' + type: string + namespace: + default: '' + type: string + secret: + properties: + key: + default: '' + type: string + name: + default: '' + type: string + type: object + username: + default: '' + type: string + type: object + type: array + storage: + default: + accessMode: ReadWriteOnce + size: 1Gi + type: Filesystem + examples: + - accessMode: ReadWriteOnce + size: 1Gi + type: Filesystem + properties: + accessMode: + default: ReadWriteOnce + enum: + - ReadWriteOnce + - ReadOnlyMany + - ReadWriteMany + type: string + size: + default: 1Gi + type: string + type: + default: Filesystem + enum: + - Filesystem + - Block + type: string + type: object + domain-name: + default: your_company.com + examples: + - your_company.com + type: string app-group: default: dev examples: @@ -201,7 +206,7 @@ providers: authentik: true kubectl: true postgresql: null - restapi: null - http: null + restapi: true + http: true gitea: null tfaddtype: null diff --git a/apps/dbgate/oauth2.tf b/apps/dbgate/oauth2.tf index 5173d82..c22857c 100644 --- a/apps/dbgate/oauth2.tf +++ b/apps/dbgate/oauth2.tf @@ -1,4 +1,5 @@ resource "kubectl_manifest" "oauth2-secret" { + count = var.use-oauth?1:0 ignore_fields = ["metadata.annotations"] yaml_body = <<-EOF apiVersion: "secretgenerator.mittwald.de/v1alpha1" @@ -15,6 +16,7 @@ resource "kubectl_manifest" "oauth2-secret" { EOF } data "kubernetes_secret_v1" "oauth2-client-id" { + count = var.use-oauth?1:0 depends_on = [kubectl_manifest.oauth2-secret] metadata { name = kubectl_manifest.oauth2-secret.name @@ -41,6 +43,7 @@ data "authentik_flow" "default-authentication-flow" { } resource "authentik_provider_oauth2" "oauth2" { + count = var.use-oauth?1:0 name = "${var.component}-${var.instance}" client_id = "${data.kubernetes_secret_v1.oauth2-client-id.data["client-id"]}" authentication_flow = data.authentik_flow.default-authentication-flow.id @@ -55,6 +58,7 @@ resource "authentik_provider_oauth2" "oauth2" { } resource "kubernetes_secret_v1" "oauth2-client-secret" { + count = var.use-oauth?1:0 metadata { name = "${var.component}-${var.instance}-secret" namespace = var.namespace diff --git a/modules/application/application.tf b/modules/application/application.tf new file mode 100644 index 0000000..6548bb7 --- /dev/null +++ b/modules/application/application.tf @@ -0,0 +1,45 @@ +locals { + app-name = var.component == var.instance ? var.instance : format("%s-%s", var.component, var.instance) + main-group = format("app-%s", local.app-name) +} +data "authentik_group" "akadmin" { + name = "authentik Admins" +} +resource "authentik_group" "groups" { + name = local.main-group + attributes = jsonencode({"${local.app-name}" = true}) +} + +resource "authentik_group" "subgroup" { + count = length(var.sub-groups) + name = format("%s-%s", local.app-name, var.sub-groups[count.index]) + parent = authentik_group.prj_users.id +} + +resource "authentik_application" "prj_app" { + name = "${var.instance}" + slug = "${var.component}-${var.instance}" + group = var.app-group + protocol_provider = var.protocol_provider + meta_launch_url = format("https://%s.%s", var.sub-domain, var.domain-name) + meta_icon = format("https://%s.%s/%s", var.sub-domain, var.domain-name, var.icon) +} + +resource "authentik_policy_expression" "policy" { + name = local.main-group + expression = <<-EOF + attr = request.user.group_attributes() + return attr['${local.app-name}'] if '${local.app-name}' in attr else False + EOF +} + +resource "authentik_policy_binding" "prj_access_users" { + target = authentik_application.prj_app.uuid + policy = authentik_policy_expression.policy.id + order = 0 +} +resource "authentik_policy_binding" "prj_access_vynil" { + target = authentik_application.prj_app.uuid + group = data.authentik_group.akadmin.id + order = 1 +} diff --git a/modules/application/variables.tf b/modules/application/variables.tf new file mode 100644 index 0000000..8d33b45 --- /dev/null +++ b/modules/application/variables.tf @@ -0,0 +1,25 @@ +variable "component" { + type = string +} +variable "instance" { + type = string +} +variable "icon" { + type = string +} +variable "app-group" { + type = string +} +variable "protocol_provider" { + type = number +} +variable "sub-domain" { + type = string +} +variable "domain-name" { + type = string +} +variable "sub-groups" { + type = list(string) + default = [] +} diff --git a/modules/forward/forward.tf b/modules/forward/forward.tf new file mode 100644 index 0000000..48a5f20 --- /dev/null +++ b/modules/forward/forward.tf @@ -0,0 +1,125 @@ +locals { + request_headers = { + "Content-Type" = "application/json" + Authorization = "Bearer ${var.authentik-token}" + } + forward-outpost-providers = jsondecode(data.http.get_forward_outpost.response_body).results[0].providers + forward-outpost-pk = jsondecode(data.http.get_forward_outpost.response_body).results[0].pk + app-name = var.component == var.instance ? var.instance : format("%s-%s", var.component, var.instance) + main-group = format("app-%s", local.app-name) + external-url = format("https://%s", var.dns-names[0]) + rules-icons = [ for v in var.dns-names : { + "host" = "${v}" + "http" = { + "paths" = [{ + "backend" = { + "service" = local.service + } + "path" = "/${var.icon}" + "pathType" = "Prefix" + }] + } + }] +} + +resource "kubectl_manifest" "prj_ingress_icon" { + force_conflicts = true + yaml_body = <<-EOF + apiVersion: "networking.k8s.io/v1" + kind: "Ingress" + metadata: + name: "${var.instance}-icons" + namespace: "${var.namespace}" + labels: ${jsonencode(var.labels)} + spec: + ingressClassName: "${var.ingress-class}" + rules: ${jsonencode(local.rules-icons)} + tls: + - hosts: ${jsonencode(var.dns-names)} + secretName: "${var.instance}-cert" + EOF +} + +data "authentik_flow" "default-authorization-flow" { + depends_on = [authentik_group.prj_users] + slug = "default-provider-authorization-implicit-consent" +} + +resource "authentik_provider_proxy" "prj_forward" { + name = local.app-name + external_host = local.external-url + authorization_flow = data.authentik_flow.default-authorization-flow.id + mode = "forward_single" + access_token_validity = var.access-token-validity +} + + + +resource "authentik_policy_binding" "prj_access_users" { + target = authentik_application.prj_application.uuid + policy = authentik_policy_expression.policy.id + order = 0 +} +resource "authentik_policy_binding" "prj_access_vynil" { + target = authentik_application.prj_application.uuid + group = data.authentik_group.vynil-admin.id + order = 1 +} + +data "http" "get_forward_outpost" { + depends_on = [authentik_provider_proxy.prj_forward] + url = "http://authentik.${var.domain}-auth.svc/api/v3/outposts/instances/?name__iexact=forward" + method = "GET" + request_headers = local.request_headers + lifecycle { + postcondition { + condition = contains([200], self.status_code) + error_message = "Status code invalid" + } + } +} + +provider "restapi" { + uri = "http://authentik.${var.domain}-auth.svc/api/v3/" + headers = local.request_headers + create_method = "PATCH" + update_method = "PATCH" + destroy_method = "PATCH" + write_returns_object = true + id_attribute = "name" +} + +resource "restapi_object" "forward_outpost_binding" { + path = "/outposts/instances/${local.forward-outpost-pk}/" + data = jsonencode({ + name = "forward" + providers = contains(local.forward-outpost-providers, authentik_provider_proxy.prj_forward.id) ? local.forward-outpost-providers : concat(local.forward-outpost-providers, [authentik_provider_proxy.prj_forward.id]) + }) +} + +resource "kubectl_manifest" "prj_middleware" { + yaml_body = <<-EOF + apiVersion: traefik.containo.us/v1alpha1 + kind: Middleware + metadata: + name: "forward-${local.app-name}" + namespace: "${var.namespace}" + labels: ${jsonencode(var.labels)} + spec: + forwardAuth: + address: http://ak-outpost-forward.${var.domain}-auth.svc:9000/outpost.goauthentik.io/auth/traefik + trustForwardHeader: true + authResponseHeaders: + - X-authentik-username + # - X-authentik-groups + # - X-authentik-email + # - X-authentik-name + # - X-authentik-uid + # - X-authentik-jwt + # - X-authentik-meta-jwks + # - X-authentik-meta-outpost + # - X-authentik-meta-provider + # - X-authentik-meta-app + # - X-authentik-meta-version + EOF +} diff --git a/modules/forward/outputs.tf b/modules/forward/outputs.tf new file mode 100644 index 0000000..f70b80c --- /dev/null +++ b/modules/forward/outputs.tf @@ -0,0 +1,3 @@ +output "provider-id" { + value = authentik_provider_proxy.prj_forward.id +} \ No newline at end of file diff --git a/modules/forward/variables.tf b/modules/forward/variables.tf new file mode 100644 index 0000000..640e766 --- /dev/null +++ b/modules/forward/variables.tf @@ -0,0 +1,33 @@ +variable "component" { + type = string +} +variable "instance" { + type = string +} +variable "domain" { + type = string +} +variable "namespace" { + type = string +} +variable "ingress-class" { + type = string +} + + +variable "labels" { + type = map(string) +} + +variable "authentik-token" { + type = string +} +variable "dns-names" { + type = list(string) +} +variable "access-token-validity" { + type = string + default = "hours=10" // ;minutes=10 +} + + diff --git a/apps/dbgate/ingress.tf b/modules/ingress/ingress.tf similarity index 67% rename from apps/dbgate/ingress.tf rename to modules/ingress/ingress.tf index f9b9e47..20e847f 100644 --- a/apps/dbgate/ingress.tf +++ b/modules/ingress/ingress.tf @@ -1,20 +1,11 @@ locals { - dns-name = "${var.sub-domain}.${var.domain-name}" - dns-names = [local.dns-name] - middlewares = ["${var.instance}-https"] - service = { - "name" = "${var.component}-${var.instance}" - "port" = { - "number" = 80 - } - } - rules = [ for v in local.dns-names : { + rules = [ for v in var.dns-names : { "host" = "${v}" "http" = { "paths" = [{ "backend" = { - "service" = local.service + "service" = var.service } "path" = "/" "pathType" = "Prefix" @@ -30,10 +21,10 @@ resource "kubectl_manifest" "prj_certificate" { metadata: name: "${var.instance}" namespace: "${var.namespace}" - labels: ${jsonencode(local.common-labels)} + labels: ${jsonencode(var.labels)} spec: secretName: "${var.instance}-cert" - dnsNames: ${jsonencode(local.dns-names)} + dnsNames: ${jsonencode(var.dns-names)} issuerRef: name: "${var.issuer}" kind: "ClusterIssuer" @@ -48,7 +39,7 @@ resource "kubectl_manifest" "prj_https_redirect" { metadata: name: "${var.instance}-https" namespace: "${var.namespace}" - labels: ${jsonencode(local.common-labels)} + labels: ${jsonencode(var.labels)} spec: redirectScheme: scheme: "https" @@ -64,14 +55,14 @@ resource "kubectl_manifest" "prj_ingress" { metadata: name: "${var.instance}" namespace: "${var.namespace}" - labels: ${jsonencode(local.common-labels)} + labels: ${jsonencode(var.labels)} annotations: - "traefik.ingress.kubernetes.io/router.middlewares": "${join(",", [for m in local.middlewares : format("%s-%s@kubernetescrd", var.namespace, m)])}" + "traefik.ingress.kubernetes.io/router.middlewares": "${join(",", [for m in var.middlewares : format("%s-%s@kubernetescrd", var.namespace, m)])}" spec: ingressClassName: "${var.ingress-class}" rules: ${jsonencode(local.rules)} tls: - - hosts: ${jsonencode(local.dns-names)} + - hosts: ${jsonencode(var.dns-names)} secretName: "${var.instance}-cert" EOF } diff --git a/modules/ingress/variables.tf b/modules/ingress/variables.tf new file mode 100644 index 0000000..dad8803 --- /dev/null +++ b/modules/ingress/variables.tf @@ -0,0 +1,35 @@ +variable "component" { + type = string +} +variable "instance" { + type = string +} +variable "namespace" { + type = string +} +variable "issuer" { + type = string +} +variable "ingress-class" { + type = string +} + +variable "labels" { + type = map(string) +} +variable "dns-names" { + type = list(string) +} +variable "middlewares" { + type = list(string) + default = ["${var.instance}-https"] +} +variable "service" { + default = { + "name" = "${var.component}-${var.instance}" + "port" = { + "number" = 80 + } + } +} + diff --git a/modules/oauth2/oauth2.tf b/modules/oauth2/oauth2.tf new file mode 100644 index 0000000..70a71e0 --- /dev/null +++ b/modules/oauth2/oauth2.tf @@ -0,0 +1,66 @@ +resource "kubectl_manifest" "oauth2-secret" { + ignore_fields = ["metadata.annotations"] + yaml_body = <<-EOF + apiVersion: "secretgenerator.mittwald.de/v1alpha1" + kind: "StringSecret" + metadata: + name: "${var.component}-${var.instance}-id" + namespace: "${var.namespace}" + labels: ${jsonencode(var.labels)} + spec: + forceRegenerate: false + fields: + - fieldName: "client-id" + length: "32" + EOF +} +data "kubernetes_secret_v1" "oauth2-client-id" { + depends_on = [kubectl_manifest.oauth2-secret] + metadata { + name = kubectl_manifest.oauth2-secret.name + namespace = var.namespace + } +} + +data "authentik_certificate_key_pair" "ca" { + name = "authentik Self-signed Certificate" +} + +data "authentik_scope_mapping" "oauth2" { + managed_list = [ + "goauthentik.io/providers/oauth2/scope-email", + "goauthentik.io/providers/oauth2/scope-openid", + "goauthentik.io/providers/oauth2/scope-profile" + ] +} +data "authentik_flow" "default-authorization-flow" { + slug = "default-provider-authorization-implicit-consent" +} +data "authentik_flow" "default-authentication-flow" { + slug = "default-authentication-flow" +} + +resource "authentik_provider_oauth2" "oauth2" { + name = "${var.component}-${var.instance}" + client_id = "${data.kubernetes_secret_v1.oauth2-client-id.data["client-id"]}" + authentication_flow = data.authentik_flow.default-authentication-flow.id + authorization_flow = data.authentik_flow.default-authorization-flow.id + client_type = "confidential" + sub_mode = "user_username" + signing_key = data.authentik_certificate_key_pair.ca.id + property_mappings = data.authentik_scope_mapping.oauth2.ids + redirect_uris = [ + "https://${var.dns-name}/" + ] +} + +resource "kubernetes_secret_v1" "oauth2-client-secret" { + metadata { + name = "${var.component}-${var.instance}-secret" + namespace = var.namespace + labels = var.labels + } + data = { + client-secret = authentik_provider_oauth2.oauth2.client_secret + } +} diff --git a/modules/oauth2/outputs.tf b/modules/oauth2/outputs.tf new file mode 100644 index 0000000..3c8d209 --- /dev/null +++ b/modules/oauth2/outputs.tf @@ -0,0 +1,3 @@ +output "provider-id" { + value = authentik_provider_oauth2.oauth2.id +} \ No newline at end of file diff --git a/modules/oauth2/variables.tf b/modules/oauth2/variables.tf new file mode 100644 index 0000000..e0ccbf0 --- /dev/null +++ b/modules/oauth2/variables.tf @@ -0,0 +1,15 @@ +variable "component" { + type = string +} +variable "instance" { + type = string +} +variable "namespace" { + type = string +} +variable "labels" { + type = map(string) +} +variable "dns-name" { + type = string +}