Hello, friends!
There are many ways to connect from home to your workplace in the office. One of them is to use Microsoft Remote Desktop Gateway. This is RDP over HTTP. I don't want to discuss the configuration of RDGW here, nor do I want to debate whether it is good or bad; let's treat it as one of the remote access tools. I want to talk about securing your RDGW server from the evil internet. When I set up the RDGW server, I immediately focused on security, especially protection against password guessing. I was surprised that I couldn't find any articles on the internet about how to do this. Well, I guess I'll have to figure it out myself.
RDGW itself does not have any protections. Yes, you can leave it exposed with a bare interface on the public network, and it will work perfectly. But a proper administrator or security professional would be uneasy about it. Moreover, this will prevent the situation of account blocking, when an employee forgets the corporate account password on their home computer and then changes their password.
A good way to protect internal resources from the external environment is through various proxies, publication systems, and other WAFs. Remember that RDGW is still HTTP, so it makes sense to insert a specialized solution between internal servers and the internet.
I know there are great solutions like F5, A10, Netscaler (ADC). As an administrator of one of these systems, I can say that it's also possible to set up protection against password guessing on these systems. And yes, these systems will also protect you from various SYN floods.
However, not every company can afford to purchase such a solution (and find an administrator for the system :), but security can be taken care of!
It is entirely possible to install the free version of HAProxy on a free operating system. I tested it on Debian 10, and in the stable repository, the version of haproxy is 1.8.19. I also tested version 2.0.xx from the testing repository.
We will leave the setup of Debian beyond the scope of this article. In short: on the public interface, close everything except port 443; on the gray interface, do so according to your policy, for example, close everything except port 22. Open only what is necessary for operation (such as VRRP for floating IP).
First, I configured HAProxy in SSL bridging mode (also known as HTTP mode) and enabled logging to see what traffic is going through RDP. So to speak, I positioned myself in the middle. However, the path /RDWeb mentioned in 'all' the RDGateway configuration articles is missing. All that's available is /rpc/rpcproxy.dll and /remoteDesktopGateway/. Furthermore, standard GET/POST requests are not used; instead, a custom request type RDG_IN_DATA, RDG_OUT_DATA is utilized.
Not much, but at least something.
Let's start testing.
I launch mstsc, navigate to the server, and see four 401 (unauthorized) errors in the logs. Then I enter my login/password and see a 200 response.
I disconnect, restart, and see the same four 401 errors in the logs. I enter incorrect login/password and see four 401 errors again. That's what we need. We will track this.
Since I couldn't determine the login URL, and I also don't know how to catch the 401 error in HAProxy, I will track (actually count) all 4xx errors. That will also suffice for solving the task.
The purpose of the protection will be to count the number of 4xx errors (on the backend) in a given time frame, and if it exceeds the specified limit, deny (on the frontend) all further connections from that IP for a specified duration.
Technically, this will not protect against password brute-forcing; it will protect against 4xx errors. For example, if a non-existent URL (404) is frequently requested, the protection will also trigger.
The simplest and most effective way is to count and block on the backend if anything excessive appears:
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
...
# create a string table with 1000 entries, expires after 15 seconds, record the error count for the last 10 seconds
stick-table type string len 128 size 1k expire 15s store http_err_rate(10s)
# remember IP
http-request track-sc0 src
# deny with HTTP error 429 if more than 4 errors in the last 10 seconds
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; let's complicate it. We will count on the backend and block on the frontend.
We will take a rough approach with the attacker by dropping their TCP connection.
frontend fe_rdp_tsc
bind *:443 ssl crt /etc/haproxy/cert/ertelecom_ru_2020_06_11.pem
mode http
...
#create a table of IP addresses, 1000 entries, expires after 15 seconds, to store from global counter
stick-table type ip size 1k expire 15s store gpc0
#take the source
tcp-request connection track-sc0 src
#reject the TCP connection if the global counter >0
tcp-request connection reject if { sc0_get_gpc0 gt 0 }
...
default_backend be_rdp_tsc
backend be_rdp_tsc
...
mode http
...
#create a table of IP addresses, 1000 entries, expires after 15 seconds, to store error count for 10 seconds
stick-table type ip size 1k expire 15s store http_err_rate(10s)
#many errors if the number of errors for 10 seconds exceeds 8
acl errors_too_fast sc1_http_err_rate gt 8
#mark the attack in the global counter (increment the counter)
acl mark_as_abuser sc0_inc_gpc0(fe_rdp_tsc) gt 0
#reset the global counter
acl clear_as_abuser sc0_clr_gpc0(fe_rdp_tsc) ge 0
#take the source
tcp-request content track-sc1 src
#reject, mark as an attack
tcp-request content reject if errors_too_fast mark_as_abuser
#allow, reset attack flag
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 thing, but politely, we will return an HTTP 429 (Too Many Requests) error
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 randomly entering passwords. After the third attempt within 10 seconds, I'm kicked out, and mstsc reports an error. This is also visible in the logs.
Explanations. I am by no means an expert on haproxy. I do not understand, for example
http-request deny deny_status 429 if { sc_http_err_rate(0) gt 4 }
allows for about 10 errors before it triggers.
I am confused about the counter numbering. Haproxy experts, I would be glad if you could add, correct, or improve it.
In the comments, feel free to suggest other ways to protect the RD Gateway, it would 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; it does not support current ciphers, so I also had to leave the old ones.
For those who are just learning and want to do things well, I will provide 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 have two servers in the backend? Because it allows for fault tolerance. You can also set up HAProxy with two floating public IPs.
Computing resources: you can start with 'two gigs, two cores, a gaming PC.' According to this will be more than enough.
Links:
Source: habr.com
