Skip to main content

Hashicorp Nomad for NGINX Web Proxies

Shen Zhen, China

NGINX can be used to reverse proxy web services and balance load across multiple instances of the same service. A reverse proxy has the added benefits of enabling multiple web services to share a single, memorable domain and authentication to view internal systems.

Configure NGINX Reverse Proxy for the Nomad / Consul Web UI

To ensure every feature in the Nomad UI remains fully functional, you must properly configure your reverse proxy to meet Nomad's specific networking requirements.

Create a basic NGINX configuration file to reverse proxy the Web UI. It is important to name the NGINX configuration file /opt/ingress/nginx.conf otherwise the file will not bind correctly:

# /opt/ingress/nginx.conf
events {}

http {
  # Since WebSockets are stateful connections but Nomad has multiple
  # server nodes, an upstream with ip_hash declared is required to ensure
  # that connections are always proxied to the same server node when possible.
  upstream nomad {
    ip_hash;
    server localhost:4646;
  }
  upstream consul {
    server localhost:8501;
  }
  server {
    listen 8080;
    server_name 0.0.0.0;
    location / {
      proxy_pass https://nomad;
      proxy_ssl_server_name on;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      # Nomad blocking queries will remain open for a default of 5 minutes.
      # Increase the proxy timeout to accommodate this timeout with an
      # additional grace period.
      proxy_read_timeout 310s;
      # Nomad log streaming uses streaming HTTP requests. In order to
      # synchronously stream logs from Nomad to NGINX to the browser
      # proxy buffering needs to be turned off.
      proxy_buffering off;
      # The Upgrade and Connection headers are used to establish
      # a WebSockets connection.
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";

      # The default Origin header will be the proxy address, which
      # will be rejected by Nomad. It must be rewritten to be the
      # host address instead.
      proxy_set_header Origin "${scheme}://${proxy_host}";
    }
  }
  server {
    listen 8081;
    server_name 0.0.0.0;
    location / {
      proxy_pass https://consul;
      proxy_ssl_server_name on;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_read_timeout 310s;
      proxy_buffering off;
    }
  }
}

Note that this takes the HTTPS connection from the Nomad (and Consul) and forwards it using HTTP. You basically downgrade attacking yourself. TODO need to add TLS to NGINX!

Security

Restricting Access with HTTP Basic Authentication

You can restrict access to your website or some parts of it by implementing a username/password authentication. Usernames and passwords are taken from a file created and populated by a password file creation tool, for example such as apt install apache2-utils (Debian, Ubuntu) or yum install httpd-tools (RHEL/CentOS/Oracle Linux).

Run the htpasswd utility with the -c flag (to create a new file), the file pathname as the first argument, and the username as the second argument:

htpasswd -c /opt/ingress/.htpasswd myuser

We can limit access to the whole website with basic authentication by adding auth_basic to the server block but still make some website areas public by specifying auth_basic off; in specific location blocks. Or just add auth_basic to every location block you want to lock down:

# /opt/ingress/nginx.conf
events {}

http {
  upstream nomad {
    ip_hash;
    server localhost:4646;
  }
  upstream consul {
    server localhost:8501;
  }
  server {
    listen 8080;
    server_name 0.0.0.0;
    auth_basic "Administrator’s Area";
    auth_basic_user_file /etc/nginx/.htpasswd; 
    location / {
      proxy_pass https://nomad;
      proxy_ssl_server_name on;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_read_timeout 310s;
      proxy_buffering off;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_set_header Origin "${scheme}://${proxy_host}";
    }
  }
  server {
    listen 8081;
    server_name 0.0.0.0;
    auth_basic "Administrator’s Area";
    auth_basic_user_file /etc/nginx/.htpasswd; 
    location / {
      proxy_pass https://consul;
      proxy_ssl_server_name on;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_read_timeout 310s;
      proxy_buffering off;
    }
  }
}

Test Run

Next in a new terminal session, start NGINX in Docker using this configuration file:

docker run --name ingress \
    --rm \
    --network host \
    --mount type=bind,source=/opt/ingress/nginx.conf,target=/etc/nginx/nginx.conf \
    --mount type=bind,source=/opt/ingress/.htpasswd,target=/etc/nginx/.htpasswd \
    nginx:1.23.0-alpine

The Nomad UI is now being proxied on your server IP with port 8080 and Consul's UI on port 8081 - use your user login to access them:

Hashicorp Nomad for NGINX Web Proxies

Create a Nomad Job to set up the NGINX Proxy

I thought that I could now use Nomad to deploy the web proxy on the Nomad Master server. But apparently this is not the way... (why can't you reach out to master servers?). Anyway, as a proof of concept I will now spawn two web services on one of the minion servers on ports 44555 and 55444 and use Nomad to provide the web proxy for those services:

/etc/nomad.d/jobs/nginx_ingress.nomad

locals {
  ports = [
    {
      port_label = "nomad"
      port       = 8080
    },
    {
      port_label = "consul"
      port       = 8081
    }
  ]
}

job "nginx" {
  datacenters = ["dc1"]

  group "nginx" {
    count = 1

    network {
      mode = "host"
      dynamic "port" {
        for_each = local.ports
        labels   = [port.value.port_label]

        content {
          to = port.value.port
        }
      }
    }

    service {
      name = "nginx"
    }

    task "nginx" {
      driver = "docker"

      config {
        network_mode = "host"
        image = "nginx:1.23.0-alpine"
        ports = ["nomad","consul"]
        volumes = [
          "local/conf/nginx.conf:/etc/nginx/nginx.conf",
          "local/conf/.htpasswd:/etc/nginx/conf.d/.htpasswd",
        ]
      }

      template {
        data = <<EOF
events {}

http {
  upstream nomad {
    ip_hash;
    server localhost:55444;
  }
  upstream consul {
    server localhost:44555;
  }
  server {
    listen 8080;
    server_name 0.0.0.0;
    auth_basic "Administrator’s Area";
    auth_basic_user_file /etc/nginx/conf.d/.htpasswd; 
    location / {
      proxy_pass http://nomad;
      proxy_ssl_server_name on;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_read_timeout 310s;
      proxy_buffering off;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_set_header Origin "${scheme}://${proxy_host}";
    }
  }
  server {
    listen 8081;
    server_name 0.0.0.0;
    auth_basic "Administrator’s Area";
    auth_basic_user_file /etc/nginx/conf.d/.htpasswd; 
    location / {
      proxy_pass http://consul;
      proxy_ssl_server_name on;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_read_timeout 310s;
      proxy_buffering off;
    }
  }
}
EOF

        destination   = "local/conf/nginx.conf"
        change_mode   = "signal"
        change_signal = "SIGHUP"
      }

      template {
        data = <<EOF
myuser:%dsFdsfg4a$#@cch#$%IvNykKW3d/
EOF

        destination   = "local/conf/.htpasswd"
        change_mode   = "signal"
        change_signal = "SIGHUP"
      }
    }
  }
}
nomad plan /etc/nomad.d/jobs/nginx_ingress.nomad                                                        
+ Job: "nginx"
+ Task Group: "nginx" (1 create)
  + Task: "nginx" (forces create)

Scheduler dry-run:
- All tasks successfully allocated.

Job Modify Index: 34024
To submit the job with version verification run:

nomad job run -check-index 34024 /etc/nomad.d/jobs/nginx_ingress.nomad

Hashicorp Nomad for NGINX Web Proxies

nomad status nginx                                                                                                   
ID            = nginx
Name          = nginx
Submit Date   = 2022-07-09T10:03:46+02:00

Allocations
ID        Node ID   Task Group  Version  Desired  Status   Created     Modified
c92a039c  005f708b  nginx       4        run      running  20s ago     6s ago
nomad alloc logs c92a039c                                                                                            
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up

I now have both my web services on port 55444 and 44555 proxied through ports 8080 and 8081 with the user login provided in .htpasswd