提升安全性:Nginx 反向代理 RDP 和 IIS 服务

提升安全性:Nginx 反向代理 RDP 和 IIS 服务

最后修改于 2022-4-11 ⋅ 共 2.9k 字 ⋅ 6分钟 / #Tutorial / #Rclone, #Nginx, #Docker, #Guacamole, #Html5, #Rdp, #安全, #Hyper-V, #Linux, #Debian

后续:将提供 webdav 服务的 IIS 替换为 rclone
关联:使用 Zram 作为 Swap,将内存消耗从 750MB 降至 450MB,教程:how-to-enable-the-zram-module-for-faster-swapping-on-linux

Windows 远程桌面服务作为开放给外网的高权限系统组件存在很大的安全隐患,因此如果需要使用 RDP 服务,最好不要直接开放其对应的 3389 端口。通过Remote Desktop Gateway这个默认存在于rdp客户端的认证通道或者是通过代理来避免端口直接暴露。以上两种方式中前者较麻烦,后者则造成每次连接远程桌面都要开启代理,都不太方便。本文介绍如何用Guacamole将rdp内容转换为HTTP,再由Nginx代理给客户端来减少暴露额外的端口带来的安全风险。Nginx监听 443 端口传入的 HTTPS 协议内容,并按路径转发给 Hyper-v VM Docker 中的Guacamole和 Windows 上的 IIS 服务端。Guacamole将宿主机的 RDP 服务转换成 HTML5 内容,不再需要服务端向局域网开放 3389 端口。

本文基于如下环境和程序

EnvironmentContains
WindowsIIS Webserver 10.0Hyper-v (VM)
Debian10 VM (Hyper-v)Nginx 1.14.2Docker (Apache Guacamole 1.2.0)

各程序服务按外部访问流程排序

ProgramUsageEnvProtocolAddress
NginxHTTPS reverse proxyDebian10 VMHTTPS192.168.51.118:443 Lan 192.168.208.146 VM
IIS WebserverOther web app (Webdav)WindowsHTTP192.168.208.145:8088/S4f_/ VM
GuacamoleHTML5 RDP serverDebian10 VM (Docker)HTTP127.0.0.1:8080/rdp/ VM
RDPMicrosoft remote desktopWindowsRDP192.168.208.145:3389 VM

覆盖图

graph LR subgraph "Hyper-V" subgraph "VM (Debian10)" c1[Nginx]-->|127.0.0.1
:8080/rdp/|c2[Guacamole
& SSH to self] end subgraph "Windows10" a1["IIS Webserver
(WebDav)"] a2[RDP] end end b1[用户]-->|HTTPS
192.168.51.118|c1 c1-->|192.168.208.145
:8088/S4f_/|a1 c2-->|192.168.208.145
:3389|a2

No Need To Prepare Config File #

More offcial Docs Here

This Part is for the native build therefore SHOULD BE SKIPPED.1

As we use docker file here, the default user has been set to guacadmin with password guacadmin.

Open Firewall for IIS’ Unusual Port #

In Windows

After binding port 8088 to IIS server’s http service, there are addtional steps for unusual port other than 80 and 443. Open firewall advance settings, add allow rules for 8080. In Scope Remote IP address part, add ip range 192.168.208.0 to 192.168.208.255. In Advance Profiles part, check all three checkboxs and confirm.

Install Guacamole by Docker #

In Hyper-v (Debian10)

Docker Site and uploader’s Github

  1. Add those text to a Dockerfile which is used for changing base url.2
1
2
3
4
5
FROM oznu/guacamole:latest

RUN set -x \
&& rm -rf ${CATALINA_HOME}/webapps/ROOT \
&& mv ${CATALINA_HOME}/webapps/ROOT.war ${CATALINA_HOME}/webapps/rdp.war
  1. Run command at the place where you put the Dockerfile to bulid a new image.
1
docker build -t guapath .
  1. Run the container (replace /root/guacamole with your config directory which should mkdir first when new build) from your new-built image.
1
2
3
4
5
docker run -d \
  --name=win10rdp \
  -p 127.0.0.1:8080:8080 \
  -v /root/guacamole:/config \
  guapath

Nginx URL Rewrite #

In Hyper-v (Debian10)

Offcial Document

My original plan is to use IIS ARR3.0 module to achive the URL rewrite purpose as this IIS webserver also do the job of sharing my files in Lan via Webdav. But after a query online, I find out that the support of Websocket in IIS which Guacamole needed to communicate seems lack. Therefore I use Nginx to do reverse proxy both for Guacamole in Hyper-V and Webdav that host on IIS.3

Assign IPs to VM 4 #

To get double IPs in debian VM, first you need to add two network interfaces (here for example eth0 is external for Lan and the eth1 is internal to host) on hyper-v. Then login to the VM and add those lines to /etc/network/interfaces, eth1’s static ip is different from the host 192.168.208.145 (like one number after) and mask is same as host’s (you can find those in Windows Cmd ipconfig /all). This internal ip is related to firewall settings in Windows so you need to assign one manually to access the host Windows:

1
2
3
4
5
6
7
auto eth0
iface eth0 inet dhcp

auto eth1
iface eth1 inet static
    ip   192.168.208.146
    mask 255.255.255.240

Apply changes by systemctl restart networking. Type ip a in command to find your IPs.

In Windows

To add static IP in Windows, open Cmd(Administrator) and input:

1
netsh interface ip set address name="vEthernet (Default Switch)" static 192.168.208.145 255.255.255.240 none

Add this command to Manage->Scheduled Tasks->Create a Basic Task and set it run as administrator to get static ip rather than random one that Windows assign to on startup. Disable only start task when plug in power and add a delay for 30s after startup.

By default, Hyper-v will give VM a random Mac which would make your router assign new ip for your VM’s external network on boot. Open VM’s settings, expand your external Network Adapter and click on Advanced Features. In Mac Address select static and input a Mac address.

Install Nginx with Webdav module #

In Hyper-v (Debian10)

1
sudo apt-get install nginx-full -y

Remove Default Page

1
rm /etc/nginx/sites-enabled/default

Add Proxy Config #

Add those lines to your /etc/nginx/nginx.conf file inside the server section. Replace rdp with your guacamole’s url path.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    location /rdp/ {
        proxy_redirect off;
        proxy_buffering off;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $http_connection;
        access_log off;
        proxy_pass http://127.0.0.1:8080/rdp/;
    }

Replace S4f_ with your IIS Webdav url path, and 192.168.208.145 with your Windows’s internal ip. I also changed the query string from client to IIS to fix problems (it’s an Nginx bug when reverse proxy a webdav request, especially when use MOVE method, the Destination header would be set as nginx’s address rather than IIS’ 5) by if set.
add in http

1
2
3
4
    map $http_destination $destination {
        ~*(https?:\/\/)([a-z0-9\.]+\/)(.+)$ http://192.168.208.145:8088/$3;
        default $http_destination;
    }

add in server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    location /S4f_/ {
        # allow webdav to max upload 4G
        client_max_body_size 4096m;
        proxy_redirect      off;
        proxy_buffering     off;
        proxy_http_version  1.1;
        proxy_set_header    Destination          $destination;
        # don't set this header as webdav destination check needed
        # proxy_set_header    Host                 $http_host;
        proxy_set_header    X-Real-IP            $remote_addr;
        proxy_set_header    X-Forwarded-For      $proxy_add_x_forwarded_for;
        proxy_set_header    http_x_forwarded_for $remote_addr;
        proxy_set_header    remote_addr          $remote_addr;
        access_log          off;
        proxy_pass http://192.168.208.145:8088/S4f_/;
    }

Config HTTPS and Auth #

About how to generate self-signed certificate, you can refer to Here.
Copy your server.crt and server.key to nginx/ folder (use scp /path/to/file username@ip.ip.ip.ip:/path/to/destination or Winscp).
You can add extra auth for anyone who visit the site by nginx’s basic auth6.
Replace server config with following lines.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
server {
    listen       443 ssl http2;
    server_name  localhost;
    server_tokens off;
    ssl_certificate     server.crt;
    ssl_certificate_key server.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}

And to disable default nginx index page, add in server:

1
2
3
    location / {
        return 404;
    }

As a summary, your nginx.conf file should look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Default settings and others
http {
    # Default but remember to delete SSL-related settings
    # Because it's included in server
    map $http_destination $destination {
        # ..
    }
    server {
        # ...
        location / {
            return 404;
        }
        location /rdp/ {
            # ...
        }
        location /S4f_/ {
            # ...
        }
    }
}

When done, restart your Nginx service:

1
sudo systemctl restart nginx

Manage Nginx and Guacamole to Autostart #

For Nginx:

1
2
3
4
# nginx autostart (enable)/ disable (disable)
sudo systemctl enable nginx
# nginx start/stop/status
sudo systemctl start nginx

To check port use lsof -i -P.The Nginx log is in /var/log/nginx/ as access.log and error.log for debug.


For Guacamole, in /etc/systemd/system/docker-win10rdp.service add:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[Unit]
Description=Docker for win10rdp
Requires=docker.service
After=docker.service

[Service]
Restart=always
ExecStart=/usr/bin/docker start -a win10rdp
ExecStop=/usr/bin/docker stop -t 2 win10rdp

[Install]
WantedBy=default.target

Enable the service:

1
sudo systemctl enable docker-win10rdp.service

Edit Guacamole Settings #

In Browser

Add Account #

Access Guacamole’s web page using your VM’s external ip address https://192.168.51.118/rdp with guacadmin for both username and password.
After a successfully login, open settings and click on the Users. Create your own account with all permissions checked. Sign in your own account and delete the default guacadmin one.

Add Connections #

Click on the New Connection inside the Connections. Fill the Name, Protocol, Hostname (Hyper-v internal ip), Port, Username, Password, Security Mode and check the Ignore server certificate. Save the changes.

In Hyper-v (Debian10)

If you want to enhance VM server’s security, you can add a ssh connection from docker to host. Use ip a to check docker0 host’s ip. Also, change the settings in /etc/ssh/sshd_config by change port and listenAddress. Apply changes sudo systemctl restart sshd:

1
2
3
Port 25678
# same as docker0 in ip a
ListenAddress 172.18.0.1

Also add these command after sudo systemctl edit sshd to allow sshd start when docker0’s ready:

1
2
3
4
[Service]
Restart=on-failure
RestartSec=3
RestartPreventExitStatus=

Test on Connection #

In Browser

Back to Home, click on the connection you just added. Check if everything is fine.

More Security Stuff #

Expose 3389 to Hyper-v only #

In Windows

Open firewall advance settings, add two allow rules for TCP and UDP port3389. In Scope Remote IP address part, add ip range 192.168.208.0 to 192.168.208.255. In Advance Profiles part, check all three checkboxs and confirm. Disable old TCP and UDP port 3389 allow rules.

Fail x00/x03 to Ban #

In Hyper-v (Debian10)

Install fail2ban

1
sudo apt-get install fail2ban

Edit your config file from template

1
2
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local

Add your client ip to ignoreip

1
2
3
[DEFAULT]
# ...
ignoreip = 127.0.0.1/8 ::1 your-ip-address-here

Modify default bantime, findtime and maxretry

1
2
3
4
5
6
7
8
9
# "bantime" is the number of seconds that a host is banned.
bantime  = 48h # -1 means permanant

# A host is banned if it has generated "maxretry" during the last "findtime" 
# seconds.
findtime  = 10m

# "maxretry" is the number of failures before a host get banned.
maxretry = 10

Create your special jail in jail.local

1
2
3
4
5
[nginx-x0x] 
enabled   = true 
port      = http,https 
filter    = nginx-x0x
logpath   = /var/log/nginx/access.log

Create filter for Nginx x0x (like 403) error

1
sudo nano /etc/fail2ban/filter.d/nginx-x0x.conf

Add

1
2
3
[Definition] 
failregex = ^<HOST>.*"(GET|POST|HEAD).*" (404|444|403|400) .*$
ignoreregex =

You can test your regex here

1
fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-x0x.conf

Restart fail2ban

1
sudo systemctl restart fail2ban

To see status

1
sudo fail2ban-client status nginx-x0x 

To rescue from jail

1
sudo fail2ban-client set nginx-x0x unbanip <ip>

APPENDIX #


  1. Deprecated
    Changing the config of Guacamole is now in its GUI control panel instead of in its config file.

    First, prepare the config file that will be used by guacamole.
    Replace 192.168.208.145 with your Windows IP address or Hostname (not recommend) connected to the Hyper-v (as 192.168.208.146 is the VM’s IP that Hyper-v assigned to). Also don’t forget to change the username and password (md5).
    In Debian, you can use md5sum <File contains PASSWORD> to hash your plain password.
    /root/guacamole/user-mapping.xml ```xml

        <!-- Another user, but using md5 to hash the password
            (example below uses the md5 hash of "PASSWORD") -->
        <authorize 
                username="admin"
                password="**PASSWORD CONVERT TO MD5**"
                encoding="md5">
        <connection name="Unique Name">
            <protocol>rdp</protocol>
            <param name="hostname">192.168.208.145</param>
            <param name="port">3389</param>
        </connection>
        </authorize>
    
    </user-mapping>
    ```
    
     ↩︎
  2. Failed
    Failed on COPY root /.

    (This file is originated from oznu/guacamole, however the path in curl -SLo ${CATALINA_HOME}/webapps/ROOT.war has been changed to ${CATALINA_HOME}/webapps/rdp.war which is same to https://.../rdp).

    ```dockerfile
    FROM library/tomcat:9-jre11
    
    ENV ARCH=amd64 \
    GUAC_VER=1.2.0 \
    GUACAMOLE_HOME=/app/guacamole \
    PG_MAJOR=9.6 \
    PGDATA=/config/postgres \
    POSTGRES_USER=guacamole \
    POSTGRES_DB=guacamole_db
    
    # Apply the s6-overlay
    
    RUN curl -SLO "https://github.com/just-containers/s6-overlay/releases/download/v1.20.0.0/s6-overlay-${ARCH}.tar.gz" \
    && tar -xzf s6-overlay-${ARCH}.tar.gz -C / \
    && tar -xzf s6-overlay-${ARCH}.tar.gz -C /usr ./bin \
    && rm -rf s6-overlay-${ARCH}.tar.gz \
    && mkdir -p ${GUACAMOLE_HOME} \
        ${GUACAMOLE_HOME}/lib \
        ${GUACAMOLE_HOME}/extensions
    
    WORKDIR ${GUACAMOLE_HOME}
    
    # Install dependencies
    RUN apt-get update && apt-get install -y \
        libcairo2-dev libjpeg62-turbo-dev libpng-dev \
        libossp-uuid-dev libavcodec-dev libavutil-dev \
        libswscale-dev freerdp2-dev libfreerdp-client2-2 libpango1.0-dev \
        libssh2-1-dev libtelnet-dev libvncserver-dev \
        libpulse-dev libssl-dev libvorbis-dev libwebp-dev libwebsockets-dev \
        ghostscript postgresql-${PG_MAJOR} \
    && rm -rf /var/lib/apt/lists/*
    
    # Link FreeRDP to where guac expects it to be
    RUN [ "$ARCH" = "armhf" ] && ln -s /usr/local/lib/freerdp /usr/lib/arm-linux-gnueabihf/freerdp || exit 0
    RUN [ "$ARCH" = "amd64" ] && ln -s /usr/local/lib/freerdp /usr/lib/x86_64-linux-gnu/freerdp || exit 0
    
    # Install guacamole-server
    RUN curl -SLO "http://apache.org/dyn/closer.cgi?action=download&filename=guacamole/${GUAC_VER}/source/guacamole-server-${GUAC_VER}.tar.gz" \
    && tar -xzf guacamole-server-${GUAC_VER}.tar.gz \
    && cd guacamole-server-${GUAC_VER} \
    && ./configure \
    && make -j$(getconf _NPROCESSORS_ONLN) \
    && make install \
    && cd .. \
    && rm -rf guacamole-server-${GUAC_VER}.tar.gz guacamole-server-${GUAC_VER} \
    && ldconfig
    
    # Install guacamole-client and postgres auth adapter
    RUN set -x \
    && rm -rf ${CATALINA_HOME}/webapps/ROOT \
    && curl -SLo ${CATALINA_HOME}/webapps/rdp.war "http://apache.org/dyn/closer.cgi?action=download&filename=guacamole/${GUAC_VER}/binary/guacamole-${GUAC_VER}.war" \
    && curl -SLo ${GUACAMOLE_HOME}/lib/postgresql-42.1.4.jar "https://jdbc.postgresql.org/download/postgresql-42.1.4.jar" \
    && curl -SLO "http://apache.org/dyn/closer.cgi?action=download&filename=guacamole/${GUAC_VER}/binary/guacamole-auth-jdbc-${GUAC_VER}.tar.gz" \
    && tar -xzf guacamole-auth-jdbc-${GUAC_VER}.tar.gz \
    && cp -R guacamole-auth-jdbc-${GUAC_VER}/postgresql/guacamole-auth-jdbc-postgresql-${GUAC_VER}.jar ${GUACAMOLE_HOME}/extensions/ \
    && cp -R guacamole-auth-jdbc-${GUAC_VER}/postgresql/schema ${GUACAMOLE_HOME}/ \
    && rm -rf guacamole-auth-jdbc-${GUAC_VER} guacamole-auth-jdbc-${GUAC_VER}.tar.gz
    
    # Add optional extensions
    RUN set -xe \
    && mkdir ${GUACAMOLE_HOME}/extensions-available \
    && for i in auth-ldap auth-duo auth-header auth-cas auth-openid auth-quickconnect auth-totp; do \
        echo "http://apache.org/dyn/closer.cgi?action=download&filename=guacamole/${GUAC_VER}/binary/guacamole-${i}-${GUAC_VER}.tar.gz" \
        && curl -SLO "http://apache.org/dyn/closer.cgi?action=download&filename=guacamole/${GUAC_VER}/binary/guacamole-${i}-${GUAC_VER}.tar.gz" \
        && tar -xzf guacamole-${i}-${GUAC_VER}.tar.gz \
        && cp guacamole-${i}-${GUAC_VER}/guacamole-${i}-${GUAC_VER}.jar ${GUACAMOLE_HOME}/extensions-available/ \
        && rm -rf guacamole-${i}-${GUAC_VER} guacamole-${i}-${GUAC_VER}.tar.gz \
    ;done
    
    ENV PATH=/usr/lib/postgresql/${PG_MAJOR}/bin:$PATH
    ENV GUACAMOLE_HOME=/config/guacamole
    
    WORKDIR /config
    
    COPY root /
    
    EXPOSE 8080
    
    ENTRYPOINT [ "/init" ]
    ```
    
     ↩︎
  3. Deprecated
    Use nginx reverse proxy instead because ARR’s lack of Websocket support.

    Install Application Request Routing (ARR) Module
    Install ARR 3.0 for Reverse Proxy support. You can install the ARR module directly from the location: http://www.microsoft.com/web/gallery/install.aspx?appid=ARRv3_0.

    Change URL Rewrite Settings
    Open the IIS Manager and open the Default Web Site config pannel. Then double click on the URL Rewrite option.
    Add 2 rules for Inbound rules and another one for Outbound Rules by clicking on the Add Rule(s)... action and Choose the Blank Rule template. Contents that needed to be changed are in the following tables:

    Inbound Rules
    The first line of the table prevent address /S4f_/ (another web app that hosted on the IIS server) to be rewritten to the rdp sevice. Also need to check the Append query string checkbox.

    |Name|Input|Match|Pattern|Action Type|Action URL|Stop Processing|
    |-|-|-|-|-|-|-|
    |S4f| - |match|S4f_(/?)(.*)|Rewrite|{R:0}|True|
    |rdp| - |match|rdp(/?)(.*)|Rewrite|http://172.17.111.38:8080/{R:0}|True|
    
     ↩︎
  4. Deprecated
    The hostname that Hyper-v provided is not stable. It’s better set static ip both manually.

    The old host-accessed-by-hostname (HOSTNAME.mshome.net) plan isn’t unstable enough than assigning static ip to both host 192.168.208.145 and VM 192.168.208.146↩︎

  5. Important
    There is a bug when Nginx handle reverse proxy to a webdav server. Related discussion and webdav path requirement.

    In Nginx’s reverse proxy part S4f_, I add extra filter to change query uri for webdav service. I’m using rclone as my Webdav client, however, it contains bugs while sending requests to a proxyed server. For example, rclone sends webdav method MOVE and query uri https://<nginx>/S4f_/path/to/file to the server to rename a file while the correct query uri without proxy is http://<IIS>/S4f_/path/to/file. Therefore, to fix the problem, I add extra if-statement and regex set to convert path that contains scheme and server name to relative path.
    Edit1: It seems that it’s a common problem when use Nginx as reverse proxy and not just the bug of rclone. More information about this is in description above. The Webdav server will check the Destination header in query uri if it match the internal server.
    Edit2: Because of RFC2518, Destination header should be absolute URI. Convert it to relative is not the solution. The better way is to rewrite the scheme and path to match the internal communication when proxy to IIS backend.
    Edit3: if-state in Nginx will double escape() the query string (like convert already converted string %6E to %256E). So I use map (should be placed in http section) instead. For some reason, IIS Webdav server behind proxy will check if MOVE method’s query string Destination is match the proxyed Host. To avoid error, delete or comment the proxy_set_header host $http_host in location /S4f_/ {..}↩︎

  6. Optional
    Set basic auth for Nginx.

    Generate your conf/userpass.txt file by command htpasswd or online MD5 htpasswd generator. Text inside are like share:$apr1$AOW5qtOx$7D3rsVK/jIQuQYqd11/Xg0. Add those lines in your nginx.conf server section. nginx auth_basic "Auth"; auth_basic_user_file userpass.txt;  ↩︎