Support for two-way TLS/HTTPS with ELB

You can switch to single instance on Elastic Beanstalk, and use ebextensions to upload the certs and configure nginx for mutual TLS.

Example

.ebextensions/setup.config

files:
  "/etc/nginx/conf.d/00_elastic_beanstalk_ssl.conf":
    mode: "000755"
    owner: root
    group: root
    content: |
      server {
        listen 443;
        server_name example.com;

        ssl on;
        ssl_certificate /etc/nginx/conf.d/server.crt;
        ssl_certificate_key /etc/nginx/conf.d/server.key;
        ssl_client_certificate /etc/nginx/conf.d/ca.crt;
        ssl_verify_client on;

        gzip on;

        send_timeout            300s;
        client_body_timeout     300s;
        client_header_timeout   300s;
        keepalive_timeout       300s;

        location / {
          proxy_pass  http://127.0.0.1:5000;
          proxy_http_version  1.1;
          proxy_set_header  Connection "";
          proxy_set_header  Upgrade $http_upgrade;
          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-SSL-client-serial $ssl_client_serial;
          proxy_set_header X-SSL-client-s-dn $ssl_client_s_dn;
          proxy_set_header X-SSL-client-i-dn $ssl_client_i_dn;
          proxy_set_header X-SSL-client-session-id $ssl_session_id;
          proxy_set_header X-SSL-client-verify $ssl_client_verify;

          proxy_connect_timeout   300s;
          proxy_send_timeout      300s;
          proxy_read_timeout      300s;
        }
      }
  "/etc/nginx/conf.d/server.crt":
    mode: "000400"
    owner: root
    group: root
    content: |
      -----BEGIN CERTIFICATE-----
      MIJDkzCCAvygAwIBAgIJALrlDwddAmnYMA0GCSqGSIb3DQEBBQUAMIGJMQswCQYD
      ...
      LqGyLiCzbVtg97mcvqAmVcJ9TtUoabtzsRJt3fhbZ0KKIlzqkeZr+kmn8TqtMpGn
      r6oVDizulA==
      -----END CERTIFICATE-----
  "/etc/nginx/conf.d/server.key":
    mode: "000400"
    owner: root
    group: root
    content: |
      -----BEGIN RSA PRIVATE KEY-----
      MIJCXQIBAAKBgQCvnu08hroXwnbgsBOYOt+ipinBWNDZRtJHrH1Cbzu/j5KxyTWF
      ...
      f92RjCvuqdc17CYbjo9pmanaLGNSKf0rLx77WXu+BNCZ
      -----END RSA PRIVATE KEY-----
  "/etc/nginx/conf.d/ca.crt":
    mode: "000400"
    owner: root
    group: root
    content: |
      -----BEGIN CERTIFICATE-----
      MIJCizCCAfQCCQChmTtNzd2fhDANBgkqhkiG9w0BAQUFADCBiTELMAkGA1UEBhMC
      ...
      4nCavUiq9CxhCzLmT6o/74t4uCDHjB+2+sIxo2zbfQ==
      -----END CERTIFICATE-----

In case your back end can support client authenticated HTTPS connections itself, you may use ELB as TCP on port 443 to TCP on port your back end listens to. This will make ELB just to resend unencrypted request directly to your back end. This config also doesn't require installation of SSL certificate to a load balancer.

Update: with this solution x-forwarded-* headers are not set.


I don't see how it could, in double-ended HTTPS mode, because the ELB is establishing a second TCP connection to the back-end server, and internally it's decrypting/encrypting the payload to/from the client and server... so the server wouldn't see the client certificate directly, and there are no documented X-Forwarded-* headers other than -For, -Proto, and -Port.

With an ELB running in TCP mode, on the other hand, the SSL negotiation is done directly between the client and server with ELB blindly tying the streams together. If the server supports the PROXY protocol, you could enable that functionality in the ELB so that you could identify the client's originating IP and port at the server, as well as identifying the client certificate directly because the client would be negotiating directly with you... though this means you are no longer offloading SSL to the ELB, which may be part of the point of what you are trying to do.


Update:

It doesn't look like there's a way to do everything you want to do -- offload SSL and identify the client certificatite -- with ELB alone. The information below is presented “for what it’s worth.”

Apparently HAProxy has support for client-side certificates in version 1.5, and passes the certificate information in X- headers. Since HAProxy also supports the PROXY protocol via configuration (something along the lines of tcp-request connection expect-proxy) ... so it seems conceivable that you could use HAProxy behind a TCP-mode ELB, with HAProxy terminating the SSL connection and forwarding both the client IP/port information from ELB (via the PROXY protocol) and the client cert information to the application server... thus allowing you to still maintain SSL offload.

I mention this because it seems to be a complementary solution, perhaps more feature-complete than either platform alone, and, at least in 1.4, the two products work flawlessly together -- I am using HAProxy 1.4 behind ELB successfully for all requests in my largest web platform (in my case, ELB is offloading the SSL -- there aren't client certs) and it seems to be a solid combination in spite of the apparent redundancy of cascaded load balancers. I like having ELB being the only thing out there on the big bad Internet, though I have no reason to think that directly-exposed HAProxy would be problematic on its own. In my application, the ELBs are there to balance between the HAProxies in the A/Z's (which I had originally intended to also auto-scale, but the CPU utilization stayed so low even during our busy season that I never had more than one per Availability Zone, and I've never lost one, yet...) which can then do some filtering, forwarding, and and munging of headers before delivering the traffic to the actual platform in addition to giving me some logging, rewriting, and traffic-splitting control that I don't have with ELB on its own.