Protecting a Kubernetes ingress for ingress-nginx with HTTP basic auth using Terraform

Revision history
Tags: kubernetes nginx terraform


I am working in a multi-cluster environment now where we are unable to see the real client IP. The Kubernetes clusters are behind “dumb” load balancers for ingress traffic which forwards traffic to our ingress-nginx service endpoints. When traffic arrives, the source IP stems from the LB, and the X-Forwarded-For header cannot be trusted. This means we cannot use ingress-nginx’s to protect our Ingress resources from the public. Instead we will rely on authentication.

Please note the caveats near the end of this post.


If you’re using ingress-nginx in Kubernetes, you can apply HTTP basic auth using annotations.

For this example I am using the auth-map as the auth-secret-type, which reads a Secret’s data fields and uses its keys as usernames and its values as the hashed passwords. basic my-namespace/my-auth-secret auth-map

Now, the secret in my-namespace can be populated with username: <base64 encoded bcrypt hash>. Since this is also about Terraform, I’ll be creating the secret using Terraform.

locals {
  app_namespace = "my-namespace"
  app_name      = "my-app"
  auth_username = "stigok"

resource "random_password" "ingress-auth" {
  length           = 32
  special          = false
  override_special = ",.-_!"

resource "kubernetes_secret" "ingress-auth" {
  metadata {
    name      = "${local.app_name}-basic-auth"
    namespace = local.namespace
    labels = {
      app       = local.app_name
  data = {
    local.auth_username = bcrypt(random_password.ingress-auth.result)

resource "kubernetes_ingress" "my-app" {
  metadata {
    name      = local.app_name
    namespace = local.app_namespace
    annotations = {
      ""        = "basic",
      "" = "auth-map",
      ""      = "${local.app_namespace}/${}"
      ""       = "auth required for ${local.app_name}"
    labels = {
      app = local.app_name

  spec {
    rule {
      host = local.app_hostname
      http {
        path {
          path = "/"
          backend {
            service_name = "my-service"
            service_port = "8080"

    tls {
      hosts = [local.app_hostname]


However, this has some implications…


This requires auth for all paths for the host which will make cert-manager unable to validating certificate requests using HTTP01 validation (paths under /.well-known). So if you rely on ACME HTTP01 for aquiring certificates for a single host then this will break your setup. You should be good if you have an existing wildcard certificate installed in your cluster or if you’re using DNS01 validation.

External monitoring solutions

It also blocks external /health requests. So if you have a monitoring solution outside your cluster that needs unauthenticated access to specific routes, you can create an additional ingress without auth, that only matches the /health endpoint. If using the "true", you can make your ingress match a single route only using /health$ as the path in the ingress spec.

However, if your monitoring solution allows you to provide basic auth credentials this will not be a problem. You can create new set of credentials for it and have it authenticate like all other clients.

State is always dirty

A big pain here is that bcrypt uses a randomly selected salt value causing it to return a new hash on every call. This has the effect of making your state dirty on each and every plan. For those who strives to always have a clean state (like me) this is a tad annoying. I haven’t figured out a work-around yet.


If you have any comments or feedback, please send me an e-mail. (stig at stigok dotcom).

Did you find any typos, incorrect information, or have something to add? Then please propose a change to this post.

Creative Commons License This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.