Guide: NGINX Reverse Proxy for Lync 2013

Standard

Introduction

Setting up a reverse proxy for an on premises Lync 2013 (aka Skype for Business) environment is fairly straightforward but the technical details are not very well documented, and there is very little out there for the excellent (and my favourite) web server and reverse proxy, nginx.

There are plenty of guides that talk about ISS APR, Apache or other products and few outline the required features of the proxy itself, namely HTTP Proxy 1.1 support, the keepalive parameters and proxy buffers.

This guide’s focus is on the reverse proxy component of the Lync 2013 architecture, you need to make sure that all other architecture requirements are met including DNS records. For help with that see Jack Stromberg’s excellent guide.

Without too much fuss, here is the reverse proxy configuration we are using in production with Lync 2013, it runs on Ubuntu 14.04 and nginx 1.8.0 but will work on most standard nginx environments. This configuration enables remote meeting and mobile application (both iOS and Android).

 

Nginx Configuration (/etc/nginx/sites-enabled/webext.yourdomain.com.conf)

Change yourdomain.com to your public domain.


# =====================================================
# LYNC 2013 Reverse Proxy
# MW 2015
# =====================================================

upstream lync_backend {
	server your-lync-server.DOMAIN.local:4443;
	keepalive 180;
}

server {
	listen 443;
	server_name webext.yourdomain.com lyncdiscover.yourdomain.com dialin.yourdomain.com meet.yourdomain.com;

	# SSL settings
	ssl on;
	
	# use strong protocols and ciphers with acceptable fallback
	ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
	ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
	ssl_session_cache shared:SSL:10m;
	ssl_prefer_server_ciphers on;

	# ssl certificate chain and private key
	ssl_certificate /etc/ssl/certs/webext.yourdomain.com_BUNDLED.pem;
	ssl_certificate_key /etc/ssl/private/webext.yourdomain.com.key;

	error_log /var/www/webext.yourdomain.com_error_log warn;

	root /var/www/webext.yourdomain.com;

	# reverse proxy to Lync with HTTP Proxy 1.1
	location / {
	
		# reset connection to 1.1
		proxy_http_version 	1.1;
		proxy_set_header 	Connection "";
		
		# buffers
		proxy_buffering 	on;
		proxy_buffers		128 4k;
		
		# headers
		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 it to lync!
		proxy_pass 		https://lync_backend;
		proxy_redirect 		default;
	}
}

# redirect port 80 traffic to 443 without passing it to Lync backend
server {
	listen 80;
	server_name webext.yourdomain.com lyncdiscover.yourdomain.com dialin.yourdomain.com meet.yourdomain.com;

	return 301 https://$host$request_uri;
}

Nginx Configuration (/etc/nginx/nginx.conf)


# =====================================================
# NGINX Config w/ compression and SSL tweaks
# MW 2015
# =====================================================
user www-data;

error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

worker_rlimit_nofile 8192;

events {
    worker_connections 4096;
    # Accept as many connections as possible.
    multi_accept on;
}

http {
    # MIME types.
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Default log and error files.
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # Use sendfile() syscall to speed up I/O operations and speed up static file serving.
    sendfile on;

    # Timeouts.
    client_body_timeout 60;
    client_header_timeout 60;
    keepalive_timeout 10 10;
    send_timeout 60;

    # Reset lingering timed out connections. Deflect DDoS.
    reset_timedout_connection on;

    # Body size.
    client_max_body_size 10m;

    # TCP options.
    tcp_nodelay on;
    ## Optimization of socket handling when using sendfile.
    tcp_nopush on;

    # Compression.
    gzip on;
    gzip_buffers 16 8k;
    gzip_comp_level 1;
    gzip_http_version 1.1;
    gzip_min_length 10;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/x-icon application/vnd.ms-fontobject font/opentype application/x-font-ttf;
    gzip_vary on;
    gzip_proxied any; # Compression for all requests.
    gzip_disable msie6;

    # Hide the Nginx version number.
    server_tokens off;

    # SSL Settings
    ssl_session_cache shared:SSL:10m;
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/nginx/dh_param.pem;
    
    # use strong protocols and ciphers with acceptable fallback
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 
    ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";    

    # Enable OCSP stapling. A better way to revocate server certificates.
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.4.4 8.8.8.8 valid=300s;
    resolver_timeout 10s;

    # security headers
    add_header Strict-Transport-Security max-age=63072000;
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;

    # indexes
    index index.html;

    # Include all vhosts.
    include /etc/nginx/sites-enabled/*;
}

 

Generate Diffie-Hellman parameters for Forward Secrecy (/etc/nginx/dh_param.pem)

Execute the following in your terminal:
openssl dhparam -out /etc/nginx/dh_param.pem 2048

 

Restart nginx

Execute the following in your terminal:
nginx -s reload

 

Test Lync 2013 Remote Authentication

Navigate to the following in your browser from outside your network (change yourdomain.com to your public domain):
https://webext.yourdomain.com/WebTicket/WebTicketService.svc

Enter your credentials when prompted:

Authentication Prompt
image-179

If all is working you should see something like this:

WebTicketService Success
image-180

If not, you’ll likely get a HTTP 401. Check that you have “Negotiate” authentication enabled in your IIS “Lync Server External Web Site” (see below).

 

Enabling IIS Negotiate Authentication

  1. On your Lync Server, open IIS Manager.
  2. Click the “Lync Server External Web Site”
  3. In the main panel, double click the “Authentication” icon.IIS Step 1
  4. Click “Windows Authentication”, then click “Providers…” from the “Actions” panel.
  5. From the “Available Providers” drop down, select “Negotiate” and click “Add”.
  6. Move Negotiate to the top of the provider list using the “Move Up” button.
    IIS Step 2
    image-181

 

Thoughts?

Please comment below if this worked for you, or if you have some improvements on this configuration and I’ll update this post. I’d love to hear from you if you’re using nginx with Lync 2013!

Guide: NGINX Reverse Proxy for Lync 2013
How to set up an nginx reverse proxy for Lync 2013 (Skype for Business), enabling mobile application and remote meeting support.

13 Comments

  1. Hey Mike, this is a great write up and very helpful. There is only one thing I can’t follow and that is to do with the SSL certs. Nginx throws an error when started because the certs you reference in your configuration are not there.

    I’m presuming we need to import these or create them from somewhere. If that is the case could you maybe expand on what is needed to get this fully working.

    Thanks,

    Charles

    • Thanks Charles, glad you find this useful.

      You’ll need to generate an SSL private key and Certificate Signing Request in the usual way, then get your CSR signed by a trusted issuer (I use RapidSSL, reseller here) which generates your signed certificate.

      Remember to keep your private key absolutely private!

      [Quick steps]

      1. Generate private key:
      openssl genrsa -out webext.yourdomain.com.key 2048

      2. Generate CSR using your private key:
      openssl req -new -key webext.yourdomain.com.key -out webext.yourdomain.com.csr;

      3. Get your CSR signed by issuer

      4. Get a copy of your issuers CA chain.

      5. Copy the cert to a new file, then in a text editor copy the contents of your issuers CA chain at the top of your cert file, above your cert string. The CA chain will usually be one file, with several cert strings inside of it.

      You could also do something like:
      cat name-of-your-signed-cert.pem >> webext.yourdomain.com_BUNDLED.pem

      cat name-of-cert-ca-file.pem >> webext.yourdomain.com_BUNDLED.pem

      6. Copy bundled cert to your location specified in your nginx config.

      I’ll add these steps to the guide.

      Thank you for your feedback!

  2. Hi Mike,

    I’m working on my first Nginx configuration and I’m not a Lync administrator so this is probably a complete newbie question.

    FYI, my project is to conifgure Nginx to load balance our internal traffic between two internal Lync servers.

    Looking at your config, I’m guessing its purpose is to allow client access from outside via HTTPS only. Is that the case?

    My second question is what is the purpose of proxying the SSL connection (i.e. breaking the encryption and then sending it on to the server)? Is this to keep the details of the server hidden and/or to relieve the server of encrypt/decrypt duties?

    For what I’m doing, I’m thinking of simply using the stream 443 directive at the load balancer rather than having it do the decryption because I’m not sure if the LB really needs to see inside the packet. Plus it’s easier that way.

    • Hi Stacy,

      Thanks for commenting.

      Yes, the idea of this proxy is to provide a fast, lightweight reverse proxy to Lync without the need for additional Microsoft IIS servers (which I personally never really like making accessible to the internet).

      For Lync, the reverse proxy component of the architecture allows use of the mobile and web apps / external client access (from home or out of office), federation with Skype, federation with trusted partners and a few other things. Most of that it is listed over on on Technet.

      In my setup, the nginx server does handle the external Lync user’s SSL session and encryption (not sure what you mean by breaking it?). Once encrypted over SSL, nginx then passes all traffic between the external client and Lync backend (lync_backend) silently. The reason for this was primarily that I did not want an IIS server running as remote proxy, and did not want to expose my internal Lync server to the internet which goes against Microsoft’s recommendation and makes Lync configuration a bit more complicated.

      For your setup, to load balance you could make a minor adjustment to the config changing the lync_backend block to:

      upstream lync_backend {
      least_conn;
      server your-lync-server1.DOMAIN.local:4443;
      server your-lync-server2.DOMAIN.local:4443;
      keepalive 180;
      }

      That will send traffic to the server with the least proxy connections using the least_conn directive. For more info and other options check out nginx’s load balancing guide.

      Simple as that!

      Love your idea, let us know how you go with that or the streamed approach!!

  3. Hi Mike, very usefull guide.
    I don’t understand if with this configuration works only on kerberos mode, because with NTLM it seems that nginx doesn’t works …
    I experience a lot of problem with sharepoint with Negotiate + NTLM authentication, do you have some issue?

  4. Hi Mike,
    First at all, thank you for the post, I’ve spent some some hours trying configure SFB2015 mobile access and your config did the last step, now it’s working!!
    I’m running the nginx in a docker container within a Hyper-V VM and work perfectly, is a very simple config, I saying this for if this config is usefull for someone.
    I have a question, Do you know way Microsoft recomend use reverse proxy instead use a simple NAT, is just for security reasons or exist any other reason?
    Thank very much.

    Tomás from Argentina.

    http://blog.powershell-es.com

  5. Hi Mike

    Thank you for documenting your nginx configuration. I have one little thing I had to add to your config. It is proxy_read_timeout. This needs to be higher than the default 60s. Because otherwise the UCWA P-GET (long polling) does not work. This is especially useful if one has his own client like a chatbot.

  6. Hi Mike – thanks for putting this together. I’m just curious if you had confirmed if this also works with Web App clients. I used this config for Skype for Business 2015 and it seems to work except when Web App clients join meetings they get an error about PPT, whiteboard, etc. not working. Has anyone else tried this with SFB and/or Web App clients?

    • Hi SP,

      Not sure on that one for 2015, it definitely worked for me on 2013 but I no longer administer either.

      Could you post the front end error details here, and perhaps any more detailed error logs from he server side/s?

Leave a Reply