Friends, hello!
There are many ways to connect from home to the workplace in the office. One of them is to use the Microsoft Remote Desktop Gateway. It's RDP over HTTP. I do not want to touch on the configuration of the RDGW itself here, I do not want to argue why it is good or bad, let's treat it as one of the remote access tools. I want to talk about protecting your RDGW server from the evil internet. When I set up the RDGW servers, I immediately became concerned about security, especially password protection. I was surprised that I did not find articles on the Internet on how to do this. Well, you'll have to do it yourself.
By itself, RDGW has no protections. Yes, you can put it with a bare interface in a white network and it will work fine. But the right administrator or IB'shnik will be restless from this. In addition, it will allow you to avoid the situation of account blocking, when a negligent employee remembers the corporate account password on his home computer, and then changes his password.
A good way to protect internal resources from the external environment is through various proxies, publishing systems, and other WAFs. Recall that RDGW is still http, then it just begs to stick a specialized solution between internal servers and the Internet.
I know that there are cool F5, A10, Netscaler(ADC). As an administrator of one of these systems, I can say that it is also possible to set up brute force protection on these systems. And yes, these systems will protect you from any syn flood along the way.
But not every company can afford to purchase such a solution (and find an administrator for such a system :), but at the same time, security can be taken care of!
It is entirely possible to install the free version of HAProxy on a free operating system. I tested on Debian 10, in the stable repository the haproxy version is 1.8.19. I also checked for version 2.0.xx from the testing repository.
We will leave the configuration of debian itself outside the article. Briefly: on the white interface, close everything except port 443, on the gray interface - according to your policy, for example, also close everything except port 22. Open only what is necessary for work (VRRP for example, for a floating ip).
First of all, I configured haproxy to SSL bridging mode (aka http mode) and enabled logging to see what goes inside RDP. So to speak, got in the middle. So, the /RDWeb path specified in "all" articles on configuring RDGateway is missing. All there is /rpc/rpcproxy.dll and /remoteDesktopGateway/. This does not use standard GET / POST requests, it uses its own type of request RDG_IN_DATA, RDG_OUT_DATA.
Not much, but at least something.
Let's test.
I run mstsc, go to the server, I see four 401 (unauthorized) errors in the logs, then I enter my login / password and I see a 200 response.
I turn it off, restart it, I see the same four 401 errors in the logs. I enter the wrong username / password and see four 401 errors again. That's what I need. This is what we will capture.
Since it was not possible to determine the login url, and besides, I don’t know how to catch the 401 error in haproxy, I will catch (actually not catch, but count) all 4xx errors. Also suitable for solving the problem.
The essence of protection will be that we will count the number of 4xx errors (on the backend) per unit of time and if it exceeds the specified limit, then reject (on the frontend) all further connections from this ip within the specified time.
Technically, it won't be brute force protection, it will be 4xx error protection. For example, if you frequently request a non-existent url (404), then the protection will also work.
The easiest and most effective way is to count and beat back on the backend, if something extra has appeared:
frontend fe_rdp_tsc
bind *:443 ssl crt /etc/haproxy/cert/desktop.example.com.pem
mode http
...
default_backend be_rdp_tsc
backend be_rdp_tsc
...
mode http
...
#создать таблицу, строковую, 1000 элементов, протухает через 15 сек, записать кол-во ошибок за последние 10 сек
stick-table type string len 128 size 1k expire 15s store http_err_rate(10s)
#запомнить ip
http-request track-sc0 src
#запретить с http ошибкой 429, если за последние 10 сек больше 4 ошибок
http-request deny deny_status 429 if { sc_http_err_rate(0) gt 4 }
...
server rdgw01 192.168.1.33:443 maxconn 1000 weight 10 ssl check cookie rdgw01
server rdgw02 192.168.2.33:443 maxconn 1000 weight 10 ssl check cookie rdgw02
Not the best option, complicate. We will count on the backend, and block on the frontend.
We will act rudely with the attacker, we will throw off a tcp connection to him.
frontend fe_rdp_tsc
bind *:443 ssl crt /etc/haproxy/cert/ertelecom_ru_2020_06_11.pem
mode http
...
#создать таблицу ip адресов, 1000 элементов, протухнет через 15 сек, сохрянять из глобального счётчика
stick-table type ip size 1k expire 15s store gpc0
#взять источник
tcp-request connection track-sc0 src
#отклонить tcp соединение, если глобальный счётчик >0
tcp-request connection reject if { sc0_get_gpc0 gt 0 }
...
default_backend be_rdp_tsc
backend be_rdp_tsc
...
mode http
...
#создать таблицу ip адресов, 1000 элементов, протухнет через 15 сек, сохранять кол-во ошибок за 10 сек
stick-table type ip size 1k expire 15s store http_err_rate(10s)
#много ошибок, если кол-во ошибок за 10 сек превысило 8
acl errors_too_fast sc1_http_err_rate gt 8
#пометить атаку в глобальном счётчике (увеличить счётчик)
acl mark_as_abuser sc0_inc_gpc0(fe_rdp_tsc) gt 0
#обнулить глобальный счётчик
acl clear_as_abuser sc0_clr_gpc0(fe_rdp_tsc) ge 0
#взять источник
tcp-request content track-sc1 src
#отклонить, пометить, что атака
tcp-request content reject if errors_too_fast mark_as_abuser
#разрешить, сбросить флажок атаки
tcp-request content accept if !errors_too_fast clear_as_abuser
...
server rdgw01 192.168.1.33:443 maxconn 1000 weight 10 ssl check cookie rdgw01
server rdgw02 192.168.2.33:443 maxconn 1000 weight 10 ssl check cookie rdgw02
the same, but politely, we will return an error http 429 (Too Many Requests)
frontend fe_rdp_tsc
...
stick-table type ip size 1k expire 15s store gpc0
http-request track-sc0 src
http-request deny deny_status 429 if { sc0_get_gpc0 gt 0 }
...
default_backend be_rdp_tsc
backend be_rdp_tsc
...
stick-table type ip size 1k expire 15s store http_err_rate(10s)
acl errors_too_fast sc1_http_err_rate gt 8
acl mark_as_abuser sc0_inc_gpc0(fe_rdp_tsc) gt 0
acl clear_as_abuser sc0_clr_gpc0(fe_rdp_tsc) ge 0
http-request track-sc1 src
http-request allow if !errors_too_fast clear_as_abuser
http-request deny deny_status 429 if errors_too_fast mark_as_abuser
...
I check: I run mstsc and start typing passwords randomly. After the third attempt, it kicks me in 10 seconds, and mstsc gives an error. As you can see in the logs.
Explanations. I'm far from a haproxy master. I don't understand why, for example
http-request deny deny_status 429 if { sc_http_err_rate(0) gt 4 }
allows you to make about 10 mistakes before it works.
I'm confused about the numbering of counters. Haproxy masters, I will be glad if you add me, correct me, make it better.
In the comments, you can throw in other ways to protect the RD Gateway, it will be interesting to study.
Regarding the Windows Remote Desktop Client (mstsc), it is worth noting that it does not support TLS1.2 (at least in Windows 7), so I had to leave TLS1; does not support current ciphers, so I also had to leave the old ones.
For those who do not understand anything, just learning, and already want to do well, I will give the entire config.
haproxy.conf
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
# Default SSL material locations
ca-base /etc/ssl/certs
crt-base /etc/ssl/private
# See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate
#ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE
-RSA-AES256-GCM-SHA384
ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
#ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
ssl-default-bind-options no-sslv3
ssl-server-verify none
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 15m
timeout server 15m
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
frontend fe_rdp_tsc
bind *:443 ssl crt /etc/haproxy/cert/dektop.example.com.pem
mode http
capture request header Host len 32
log global
option httplog
timeout client 300s
maxconn 1000
stick-table type ip size 1k expire 15s store gpc0
tcp-request connection track-sc0 src
tcp-request connection reject if { sc0_get_gpc0 gt 0 }
acl rdweb_domain hdr(host) -i beg dektop.example.com
http-request deny deny_status 400 if !rdweb_domain
default_backend be_rdp_tsc
backend be_rdp_tsc
balance source
mode http
log global
stick-table type ip size 1k expire 15s store http_err_rate(10s)
acl errors_too_fast sc1_http_err_rate gt 8
acl mark_as_abuser sc0_inc_gpc0(fe_rdp_tsc) gt 0
acl clear_as_abuser sc0_clr_gpc0(fe_rdp_tsc) ge 0
tcp-request content track-sc1 src
tcp-request content reject if errors_too_fast mark_as_abuser
tcp-request content accept if !errors_too_fast clear_as_abuser
option forwardfor
http-request add-header X-CLIENT-IP %[src]
option httpchk GET /
cookie RDPWEB insert nocache
default-server inter 3s rise 2 fall 3
server rdgw01 192.168.1.33:443 maxconn 1000 weight 10 ssl check cookie rdgw01
server rdgw02 192.168.2.33:443 maxconn 1000 weight 10 ssl check cookie rdgw02
frontend fe_stats
mode http
bind *:8080
acl ip_allow_admin src 192.168.66.66
stats enable
stats uri /stats
stats refresh 30s
#stats admin if LOCALHOST
stats admin if ip_allow_admin
Why two servers on the backend? Because this is how you can make fault tolerance. Haproxy can also make two with floating white ip.
Computing resources: you can start with "two gigs, two cores, a gaming PC." According to
Links:
Source: habr.com