כיצד להגדיר פרוקסי הפוך קל ומאובטח באמצעות Docker, Nginx & Letsencrypt

מבוא

ניסית פעם להקים איזשהו שרת בבית? היכן עליכם לפתוח יציאה חדשה לכל שירות? וצריך לזכור איזה יציאה עוברת לאיזה שירות, ומה ה- ip הביתי שלך? זה בהחלט משהו שעובד, ואנשים עושים את זה זמן רב ביותר.

עם זאת, האם זה לא יהיה נחמד להקליד plex.example.com , ויש לך גישה מיידית לשרת המדיה שלך? זה בדיוק מה שפרוקסי הפוך יעשה עבורך, ושילוב זה עם Docker, זה קל מתמיד.

תנאים מוקדמים

Docker & Docker-Compose

אמורה להיות לך גרסת Docker 17.12.0+, ולהלחין גרסה 1.21.0+.

תְחוּם

אתה צריך להגדיר דומיין ולקבל אישור SSL המשויך אליו. אם אין לך אחד, עקוב אחר המדריך שלי כאן כיצד להשיג אחד חינם עם LetsEncrypt.

מה יכסה מאמר זה

אני מאמין בהחלט בהבנת מה שאתה עושה. היה זמן שבו הייתי עוקב אחר המדריכים, ואין לי מושג כיצד לפתור תקלות. אם ככה אתה רוצה לעשות את זה, הנה מדריך נהדר המכסה כיצד להגדיר אותו. בעוד המאמרים שלי ארוכים, אתה צריך בסופו של דבר להבין איך כל זה עובד.

מה שתלמד כאן, הוא מהו פרוקסי הפוך, כיצד להגדיר אותו וכיצד תוכל לאבטח אותו. אני עושה כמיטב יכולתי לחלק את הנושא לחלקים, מחולק לפי כותרות, אז אל תהסס לקפוץ מעל קטע, אם מתחשק לך. אני ממליץ לקרוא את המאמר כולו פעם ראשונה, לפני שמתחילים להגדיר אותו.

מהו פרוקסי הפוך?

פרוקסי רגיל

נתחיל ברעיון של פרוקסי רגיל. אמנם זהו מונח נפוץ מאוד בקהילת הטכנולוגיה, אך הוא אינו המקום היחיד בו השתמשו בו. פרוקסי פירושו שמידע עובר על ידי צד שלישי לפני ההגעה למיקום.

תגיד שאתה לא רוצה ששירות יכיר את ה- IP שלך, אתה יכול להשתמש ב- proxy. פרוקסי הוא שרת שהוקם במיוחד למטרה זו. אם שרת ה- proxy בו אתה משתמש נמצא למשל באמסטרדם, ה- IP שיוצג בפני העולם החיצוני הוא ה- IP מהשרת באמסטרדם. היחידים שיידעו את ה- IP שלך הם אלה ששולטים בשרת ה- proxy.

פרוקסי הפוך

כדי לפרק את זה למונחים פשוטים, פרוקסי יוסיף שכבת מיסוך. זה אותו הרעיון ב- proxy הפוך, למעט במקום להסוות חיבורים יוצאים (אתה ניגש לשרת אינטרנט), החיבורים הנכנסים (אנשים שניגשים לשרת האינטרנט שלך) יוסתרו. אתה פשוט מספק כתובת אתר כמו example.com , ובכל פעם שאנשים ניגשים לכתובת אתר זו, ה- proxy ההפוך שלך ידאג לאן הבקשה הזו מגיעה.

נניח שיש לך שני שרתים שהוגדרו ברשת הפנימית שלך. Server1 נמצא ב- 192.168.1.10 ו- Server2 ב- 192.168.1.20. כרגע ה- proxy ההפוך שלך שולח בקשות המגיעות מ- example.com אל Server1. יום אחד יש לך כמה עדכונים לדף האינטרנט. במקום להוריד את האתר לצורך תחזוקה, אתה פשוט מבצע את ההתקנה החדשה ב- Server2. לאחר שסיימת, אתה פשוט משנה שורה בודדת ב- proxy ההפוך שלך, וכעת בקשות נשלחות אל Server2. בהנחה שה- proxy ההפוך מוגדר כראוי, לא אמור להיות לך זמן השבתה.

אבל אולי היתרון הגדול ביותר שיש proxy הפוך, הוא שאתה יכול לקבל שירותים הפועלים על מספר יציאות, אבל אתה רק צריך לפתוח את היציאות 80 ו- 443, HTTP ו- HTTPS בהתאמה. כל הבקשות יגיעו לרשת שלך בשתי היציאות הללו, וה- proxy ההפוך ידאג לשאר. כל זה יהיה הגיוני ברגע שנתחיל להגדיר את ה- proxy.

הגדרת המכולה

מה לעשות

docker-compose.yaml:

version: '3' services: reverse: container_name: reverse hostname: reverse image: nginx ports: - 80:80 - 443:443 volumes: - :/etc/nginx - :/etc/ssl/private

קודם כל, עליך להוסיף שירות חדש לקובץ ה- docker-compose שלך. אתה יכול לקרוא לזה איך שאתה מעדיף, במקרה הזה בחרתי הפוך . כאן בחרתי זה עתה ב- nginx כתמונה, אולם בסביבת הפקה, בדרך כלל מומלץ לציין גרסה למקרה שיש אי פעם שינויים שבורים בעדכונים העתידיים.

אז אתה צריך לאגד נפח שתי תיקיות. / etc / nginx הוא המקום בו מאוחסנים כל קבצי התצורה שלך, ו / etc / ssl / private מאוחסנים אישורי ה- SSL שלך. חשוב מאוד שתיקיית התצורה שלך לא תהיה קיימת אצל המארח שלך בפעם הראשונה שאתה מפעיל את המיכל. כאשר אתה מפעיל את המיכל שלך באמצעות docker-compose, הוא ייצור את התיקיה באופן אוטומטי ויאוכלס בתוכן המכולה. אם יצרת תיקיית תצורה ריקה במארח שלך, היא תעלה אותה והתיקיה בתוך המיכל תהיה ריקה.

למה זה עובד

אין הרבה בחלק זה. בעיקר זה כמו להתחיל כל מיכל אחר עם docker-compose. מה שאתה צריך לשים לב כאן זה שאתה מחייב את יציאה 80 ו 443. זה המקום שבו כל הבקשות ייכנסו, והן יועברו לכל שירות שתציין.

הגדרת תצורה של Nginx

מה לעשות

עכשיו אתה צריך תיקיית תצורה על המארח שלך. כאשר אתה עובר לספרייה זו, אתה אמור לראות קבוצה של קבצים שונים ותיקיה בשם conf.d. בפנים conf.dכל קבצי התצורה שלך ימוקמו. כרגע יש default.confקובץ יחיד , אתה יכול להמשיך ולמחוק אותו.

Still inside conf.d, create two folders: sites-available and sites-enabled. Navigate into sites-available and create your first configuration file. Here we’re going to setup an entry for Plex, but feel free to use another service that you have set up if you like. It doesn’t really matter what the file is called, however I prefer to name it like plex.conf.

Now open the file, and enter the following:

upstream plex { server plex:32400; } server { listen 80; server_name plex.example.com; location / { proxy_pass //plex; } }

Go into the sites-enabled directory, and enter the following command:

ln -s ../sites-available/plex.conf .

This will create a symbolic link to the file in the other folder. Now there’s only one thing left, and that is to change the nginx.conf file in the config folder. If you open the file, you should see the following as the last line:

include /etc/nginx/conf.d/*.conf;

Change that to:

include /etc/nginx/conf.d/sites-enabled/*.conf;

In order to get the reverse proxy to actually work, we need to reload the nginx service inside the container. From the host, run docker exec nginx -t. This will run a syntax checker against your configuration files. This should output that the syntax is ok. Now run docker exec nginx -s reload. This will send a signal to the nginx process that it should reload, and congratulations! You now have a running reverse proxy, and should be able to access your server at plex.example.com (assuming that you have forwarded port 80 to your host in your router).

Even though your reverse proxy is working, you are running on HTTP, which provides no encryption whatsoever. The next part will be how to secure your proxy, and get a perfect score on SSL Labs.

Why it Works

The Configuration File

As you can see, the plex.conf file consists of two parts. An upstream part and a server part. Let’s start with the server part. This is where you are defining the port receiving the incoming requests, what domain this configuration should match, and where it should be sent to.

The way this server is being set up, you should make a file for each service that you want to proxy requests to, so obviously you need some way to distinguish which file to receive each request. This is what the server-name directive does. Below that we have the location directive.

In our case we only need one location, however you can have as many location directives as you want. Imagine you have a website with a frontend and a backend. Depending on the infrastructure you’re using, you’ll have the frontend as one container and the backend as another container. You could then have location / {} which will send requests to the frontend, and location /api/ {} which will send requests to the backend. Suddenly you have multiple services running on a single memorable domain.

As for the upstream part, that can be used for load-balancing. If you’re interested in learning more about how that works, you can look at the official docs here. For our simple case, you just define the hostname or ip address of the service you want to proxy to, and what port is should be proxied to, and then refer to the upstream name in the location directive.

Hostname Vs. IP Address

To understand what a hostname is, let’s make an example. Say you are on your home network 192.168.1.0. You then set up a server on 192.168.1.10 and run Plex on it. You can now access Plex on 192.168.1.10:32400, as long as you are still on the same network. Another possibility is to give the server a hostname. In this case we’ll give it the hostname plex. Now you can access Plex by entering plex:32400 in your browser!

This same concept was introduced to docker-compose in version 3. If you look at the docker-compose file earlier in this article, you’ll notice that I gave it a hostname: reverse directive. Now all other containers can access my reverse proxy by its hostname. One thing that’s very important to note, is that the service name has to be the same as the hostname. This is something that the creators of docker-compose chose to impose.

Another really important thing to remember, is that by default docker containers are put on their own network. This means that you won’t be able to access your container by it’s hostname, if you’re sitting on your laptop on your host network. It is only the containers that are able to access each other through their hostname.

So to sum it up and make it really clear. In your docker-compose file, add the hostname directive to your services. Most of the time your containers will get a new IP every time you restart the container, so referring to it via hostname, means it doesn’t matter what IP your container is getting.

Sites-available & Sites-enabled

Why are we creating the sites-available and sites-enabled directories? This is not something of my creation. If you install Nginx on a server, you will see that it comes with these folders. However because Docker is built with microservices in mind, where one container should only ever do one thing, these folders are omitted in the container. We’re recreating them again, because of how we’re using the container.

And yes, you could definitely just make a sites-enabled folder, or directly host your configuration files in conf.d. Doing it this way, enables you to have passive configuration laying around. Say that you are doing maintenance, and don’t want to have the service active; you simply remove the symbolic link, and put it back when you want the service active again.

Symbolic Links

Symbolic links are a very powerful feature of the operating system. I had personally never used them before setting up an Nginx server, but since then I’ve been using them everywhere I can. Say you are working on 5 different projects, but all these projects use the same file in some way. You can either copy the file into every project, and refer to it directly, or you can place the file in one place, and in those 5 projects make symlinks to that file.

This gives two advantages: you take up 4 times less space than you otherwise would have, and then the most powerful of them all; change the file in one place, and it changes in all 5 projects at once! This was a bit of a sidestep, but I think it’s worth mentioning.

Securing Nginx Proxy

What to Do

Go to your config folder, and create 3 files and fill them with the following input:

common.conf:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Frame-Options SAMEORIGIN; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block";

common_location.conf:

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_set_header Host $host; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port;

ssl.conf:

ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ecdh_curve secp384r1; ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384 OLD_TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 OLD_TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"; ssl_prefer_server_ciphers on; ssl_dhparam /etc/nginx/dhparams.pem; ssl_certificate /etc/ssl/private/fullchain.pem; ssl_certificate_key /etc/ssl/private/privkey.pem; ssl_session_timeout 10m; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on;

Now open the plex.conf file, and change it to the following (notice lines 6, 9, 10 & 14):

upstream plex { server plex:32400; } server { listen 443 ssl; server_name plex.example.com; include common.conf; include /etc/nginx/ssl.conf; location / { proxy_pass //plex; include common_location.conf; } }

Now go back to the root of your config folder, and run the following command:

openssl dhparam -out dhparams.pem 4096

This will take a long time to complete, even up to an hour in some cases.

If you followed my article on getting a LetsEncrypt SSL Certificate, your certificates should be located in /etc/letsencrypt/live// .

When I helped a friend set this up on his system, we ran into some problems where it couldn’t open the files when they were located in that directory. Most likely the cause of some permissions problems. The easy solution to this is to make an SSL directory, like /certs, and then mount that to the Nginx container’s /etc/ssl/private folder. In the newly created folder, you should then make symbolic links, to the certs in your LetsEncrypt’s config folder.

When the openssl command is done running, you should run the docker exec nginx -t to make sure that all the syntax is correct, and then reload it by running docker exec nginx -s reload. At this point everything should be running, and you now have a working and perfectly secure reverse proxy!

Why it Works

Looking in the plex.conf file, there is only one major change, and that is what port the reverse proxy is listening on, and telling it that it’s an ssl connection. Then there are 3 places where we’re including the 3 other files we made. While SSL is kind of secure by itself, these other files make it even more secure. However if for some reason you don’t want to include these files, you need to move the ssl-certificate and ssl-certificate-keyinside the .conf file. These are required to have, in order for an HTTPS connection to work.

Common.conf

Looking in the common.conf file, we add 4 different headers. Headers are something that the server sends to the browser on every response. These headers tell the browser to act a certain way, and it is then up to the browser to enforce these headers.

Strict-Transport-Security (HSTS)

This header tells the browser that connections should be made over HTTPS. When this header has been added, the browser won’t let you make plain HTTP connection to the server, ensuring that all communication is secure.

X-Frame-Options

When specifying this header, you are specifying whether or not other sites can embed your content into their sites. This can help avoid clickjacking attacks.

X-Content-Type-Options

Say you have a site where users can upload files. There’s not enough validation on the files, so a user successfully uploads a php file to the server, where the server is expecting an image to be uploaded. The attacker may then be able to access the uploaded file. Now the server responds with an image, however the file’s MIME-type is text/plain. The browser will ‘sniff’ the file, and then render the php script, allowing the attacker to do RCE (Remote Code Execution).

With this header set to ‘nosniff’, the browser will not look at the file, and simply render it as whatever the server tells the browser that it is.

X-XSS-Protection

While this header was more necessary in older browsers, it’s so easy to add that you might as well. Some XSS (Cross-site Scripting) attacks can be very intelligent, while some are very rudimentary. This header will tell browsers to scan for the simple vulnerabilities and block them.

Common_location.conf

X-Real-IP

Because your servers are behind a reverse proxy, if you try to look at the requesting IP, you will always see the IP of the reverse proxy. This header is added so you can see which IP is actually requesting your service.

X-Forwarded-For

Sometimes a users request will go through multiple clients before it reaches your server. This header includes an array of all those clients.

X-Forwarded-Proto

This header will show what protocol is being used between client and server.

Host

This ensures that it’s possible to do a reverse DNS lookup on the domain name. It’s used when the server_name directive is different than what you are proxying to.

X-Forwarded-Host

Shows what the real host of the request is instead of the reverse proxy.

X-Forwarded-Port

Helps identify what port the client requested the server on.

Ssl.conf

SSL is a huge topic in and of itself, and too big to start explaining in this article. There are many great tutorials out there on how SSL handshakes work, and so on. If you want to look into this specific file, I suggest looking at the protocols and ciphers being used, and what difference they make.

Redirecting HTTP to HTTPS

The observant ones have maybe noticed that we are only ever listening on port 443 in this secure version. This would mean that anyone trying to access the site via //* would get through, but trying to connect through //* would just get an error. Luckily there’s a really easy fix to this. Make a redirect.conf file with the following contents:

server { listen 80; server_name _; return 301 //$host$request_uri; }

Now just make sure that it appears in your sites-enabled folder, and when you’ve reloaded the Nginx process in the container, all requests to port 80 will be redirected to port 443 (HTTPS).

Final Thoughts

Now that your site is up and running, you can head over to SSL Labs and run a test to see how secure your site is. At the time of writing this, you should get a perfect score. However there is a big thing to notice about that.

There will always be a balance between security and convenience. In this case the weights are heavily on the side of security. If you run the test on SSL Labs and scroll down, you will see there are multiple devices that won’t be able to connect with your site, because they don’t support new standards.

So have this in mind when you are setting this up. Right now I am just running a server at home, where I don’t have to worry about that many people being able to access it. But if you do a scan on Facebook, you’ll see they won’t have as great a score, however their site can be accessed by more devices.