Copyright (C) 2020 RUI Ni (ranqus@gmail.com)
This is a document on how to depoly a reasonably secured proxy server which is capable of relaying non-critical communications and few other tasks.
Given the fact that this document is largely a personal note and memo, information and method provided in this document may NOT be useful for your use case. Do NOT follow the content if you don't fully understand the information and instruction.
The document also assumes the reader has decent amont of knownledge about Linux server. Because of that, step-by-step guide will not be provided, the reader must install and fine tune the softwares by themselves.
WARNING: If you decided to continue, please keep in mind that the author of this document does not provide any warranty. You have to proceed at your own risk.
Deploy a proxy server can be really easy. A typical HTTP or Socks5 proxy only takes one or two commands to setup and operate.
However, sometime, just to get the proxy software up & running is not enough. In many situation, you want to have more control over the server so it can be safely shared with your friend and even the public without been abused, or you maybe just want to make the server to be more secured against hackers who may hijack the server.
In order to achieve that, extra step is needed. And sometime those steps can be really troublesome and hard to figure out.
The document will provide few advices to readers who wants to create a decently reinforced proxy server, so it can be shared relately safely with other people.
This document will not touch topics about DDoS protection, software bug, update policy, as well as anything that is unrelated to building and reinforcing a proxy.
This document will also not to recommend any actual proxy software, as there are too many of them. The reader can make the decision by themselves and apply necessary restrictions on those softwares.
The author will assume all softwares on the proxy is and will continue be bug-free, so the configuration given by this document will be applied correctly, and the softwares is working normally.
By the end of the document, the reader should receive some tips on creating a proxy server that:
- Can automatically recover from software crash
- Can be restricted from sending outbound traffic
- Is isolated enough to reduce potential chance of danger that could effect the rest of the system
To achieve that, following method will be used:
- Run the main proxy software inside a Docker container, or at least run it as a dedicated
nologin
'd user viasystemd
- Use
iptables
to restrict traffic going out from the proxy program - Use
squid
or other transparent proxy application to filter unencrypted HTTP traffic
Normally you want to have a server with cleanly installed OS. Make sure the OS setup image is acquired from an official source to avoid tamper.
After the OS is ready, make sure following softwares is installed:
iptables
openssh
openssl
squid
or another transparent proxy of your choice- A text editor like
vi
ornano
If you decided to go with Docker, install it as well.
This can normally be done by using adduser
command.
$ adduser proxy_server_user
$ passwd proxy_server_user
<Setup password for the user>
$ mkdir -p /home/proxy_server_user
$ chown proxy_server_user:proxy_server_user /home/proxy_server_user
Notice: Do NOT pick a common username such as admin
, user
or proxy
. As the auto scanner used by some attackers can easily guess out those usernames.
Do NOT add the user to sudoers. The only purpose of this user is to allow remote SSH login.
After the login, user can switch to root
via su
command.
Edit the file /etc/ssh/sshd_config
, change Port 22
to another number of your liking, we'll use 2222
for the rest of the document as an example, and enable following settings:
LoginGraceTime 1m
PermitRootLogin no
StrictModes yes
MaxAuthTries 2
MaxSessions 5
It is also strongly recommended to enable PubkeyAuthentication yes
and disable PasswordAuthentication yes
. Though this document will not cover the detail. User may implement the recommendation by themselves after the proxy is completely ready.
Notice: If you decided to use PubkeyAuthentication
, do NOT directly enable it on root
. Use a median account instead and perform su
after each login
After the configuration is done, restart the openssh
server to apply the change.
Keep in mind: You want to prevent unrelated user from accessing anything that is not suppose to be accessed. That is, a user should NOT be able to use anything that is unrelated to the proxy feature of the server.
A typical iptables rule set will by default, disallow all accesses, by
$ iptables -P INPUT DROP
and then selectively enable accesses, by
$ iptables -A INPUT -p tcp --dport 2222 -j ACCEPT
When it come together, the rules should be similar to:
$ iptables -P INPUT DROP
$ iptables -P FORWARD ACCEPT
$ iptables -P OUTPUT ACCEPT
$ iptables -A INPUT -i lo -j ACCEPT
$ iptables -A INPUT -m conntrack ! --ctstate NEW -j ACCEPT
$ iptables -A INPUT -p tcp --dport 2222 -j ACCEPT
This will allow only access to the OpenSSH
host on our server for now.
Tip: You can also future limit the SSH port by employ methods such as source IP verification. However, the detail will not be covered by this document.
Depends on the use case, your user may want to access different ports. For example, 80 and 443 to browse the web, and 53 for DNS. You may have to consolidate the decision with your user.
Once the allowed egress ports is determined, an iptables
whitelist chain can then be created to contain the traffic verification items.
There is an example ruleset which allows egress only to 80/tcp, 443/tcp and 53 ports:
# Create guest_output_redirect
$ iptables -t nat -N guest_output_redirect
$ iptables -t nat -A guest_output_redirect -p tcp --dport 80 -j REDIRECT --to-port 3128
# Create guest_output_limit
$ iptables -t filter -N guest_output_limit
$ iptables -t filter -A guest_output_limit -p tcp --dport 53 -j RETURN
$ iptables -t filter -A guest_output_limit -p udp --dport 53 -m limit --limit 16/s --limit-burst 32 -j RETURN
$ iptables -t filter -A guest_output_limit -p tcp --dport 443 -j RETURN
Notice we use NAT to redirect traffic from 80/tcp port to a Squid server so we can filter the HTTP traffic.
net.core.default_qdisc=fq
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
net.ipv4.conf.docker0.route_localnet=1
net.ipv4.ip_default_ttl=129
net.ipv4.icmp_echo_ignore_all=1
net.ipv4.tcp_fastopen=3
net.ipv4.tcp_keepalive_time=96
net.ipv4.tcp_keepalive_probes=3
net.ipv4.tcp_keepalive_intvl=45
net.ipv4.tcp_retries1=3
net.ipv4.tcp_retries2=9
vm.oom_kill_allocating_task=1
vm.min_free_kbytes=32768
vm.admin_reserve_kbytes=16384
You may have to change 32768
to your actual application.
Then sysctl -p
to apply.
WARNING: If your system is not drived by systemd
, skip this section.
Please follow the instruction provided by the software vendor to install the software. This document will not cover that.
Many of those softwares can be installed via a simple apt
or yum
command, others needs binary download and manual install or even compile from source.
If the software does not come with it's own <PROXY_SERVICE>.service
file for systemd
, you may have to create one by hand. Here is an example:
[Unit]
Description=<PROXY_SERVICE_NAME>
After=network.target
[Service]
ExecStart=</path/to/the/executable>
Restart=always
RestartSec=5
User=<PROXY_SERVICE_RUN_USER>
Group=<PROXY_SERVICE_RUN_GROUP>
[Install]
WantedBy=multi-user.target
You need to replace <PROXY_SERVICE>
, <PROXY_SERVICE_NAME>
, </path/to/the/executable>
, <PROXY_SERVICE_RUN_USER>
and <PROXY_SERVICE_RUN_GROUP>
above to the actual value that fits your application.
When your <PROXY_SERVICE>.service
file is ready, put it into /usr/lib/systemd/system
or other appreciate folder of your system, then run systemctl daemon-reload
.
Watch out the <PROXY_SERVICE_RUN_USER>
. This will be the dedicated user which the application will be running as, you need to create the user with adduser
command, make it nologin
so nobody can login to your server with this user.
Once the user creation is done, we can then apply the iptables
whitelist chain guest_output_limit
and guest_output_redirect
to this user, so the user is limited by those iptables
rules.
To do that, run:
$ iptables -t filter -I guest_output_limit -p tcp --sport <PROXY_SERVICE_PORT> -j RETURN
$ iptables -t filter -I guest_output_limit -p udp --sport <PROXY_SERVICE_PORT> -j RETURN
$ iptables -t filter -I OUTPUT -m owner --uid-owner <PROXY_SERVICE_RUN_USER> -j guest_output_limit
$ iptables -t nat -I OUTPUT -m owner --uid-owner <PROXY_SERVICE_RUN_USER> -j guest_output_redirect
$ iptables -A INPUT -p tcp --dport <PROXY_SERVICE_PORT> -j ACCEPT
Notice you need to replace <PROXY_SERVICE_PORT>
and <PROXY_SERVICE_RUN_USER>
with the actual value used on your server.
If the proxy software vendor provides a Docker image, follow their instruction to install the proxy in a Docker container. This document will not cover that.
However, because of how Docker is operated, the method used to restrict it's outbound traffic is different compare to the case above.
In Docker, when a container is using bridged network, the traffic in and out of the container is routed through a virtual interface, called docker0
by default.
To put limit on traffic in and out the container, we need to apply the restriction on the interface, and it will effect ALL containers which are routing their traffic through the interface.
The command is:
$ iptables -N DOCKER-USER # It's OK if the command reports duplication
$ iptables -t filter -I DOCKER-USER -i docker0 ! -o docker0 -m conntrack --ctstate NEW -j guest_output_limit
$ iptables -t nat -A PREROUTING -i docker0 -j guest_output_redirect
$ iptables -A INPUT -i docker0 -p tcp --dport 3128 -j ACCEPT
Keep in mind if the name of the interface is not docker0
, you need to change the command accordingly.
As you may have noticed, we didn't allow the <PROXY_SERVICE_PORT>
in the command above unlike what we did during the Setup directly onto the server section. This is because Docker manages iptables
by itself.
By specify the -p
or --publish
parameter for docker run
, it opens the port on iptables
automatically, so we don't have to do that with iptables
ourselves.
If you don't want to expose the container to the public, then publish the port to lo
: --publish 127.0.0.1:8080:8080/tcp
instead.
Edit the config file /etc/squid/squid.conf
(maybe different on your system), change the http_port 3128
to http_port 127.0.0.1:3128 intercept
, and http_access deny all
to http_access allow all
. Then add filter settings above the http_access allow all
line:
acl limit5conn maxconn 16
http_access deny limit5conn
acl bad_urls urlpath_regex -i "/etc/squid/bad_urls"
acl bad_domains dstdomain "/etc/squid/bad_domains"
http_access deny bad_urls
http_access deny bad_domains
Create /etc/squid/bad_urls
to contain "bad" urls such as:
\.torrent$
\/stat.*
\/announce.*
\/gtag.*
\/ga\.js.*
\/ga\.js$
\/hm\.js.*
Then create /etc/squid/bad_domains
to contain "bad" domains such as:
.cn
.baidu.com
.360.com
.360.cn
.bdstatic.com
.alimama.com
.baidustatic.com
.jb51.net
.googleleadservices.com
.xunlei.com
.51la.com
.google-analytics.com
.atdmt.com
.doubleclick.net
.pointroll.com
.ard.yahoo.com
.yimg.com
.tribalfusion.com
.googlesyndication.com
.nozonedata.com
.fastclick.net
For some version of Squid, you must have a non-intercept http_port
in order for it to start up. If that's the case, use http_port 127.0.0.1:3127
and never expose this port to the public.
If you want to setup the transparent HTTP filtering proxy via Nginx, make sure the default site is disabled by overwriting it with:
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
ssl_certificate /etc/nginx/ssl/dummy.crt;
ssl_certificate_key /etc/nginx/ssl/dummy.key;
access_log off;
server_name _;
return 444;
}
in /etc/nginx/nginx.conf
. The dummy certificate is a must, create them with command:
$ mkdir -p /etc/nginx/ssl
$ chmod 0600 /etc/nginx/ssl
$ openssl req -x509 -newkey rsa:4096 -keyout /etc/nginx/ssl/dummy.key -out /etc/nginx/ssl/dummy.crt -days 3650 --nodes
#!/bin/sh
#
# IPtables rules for proxy server
DOCKER_IF="docker0"
DOCKER_CHAIN="DOCKER-USER"
SSH_PORT=2222
SQUID_HOST=127.0.0.1
SQUID_PORT=3128
# Initialize default states
iptables -P INPUT DROP
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack ! --ctstate NEW -j ACCEPT
# Create guest_output_redirect
iptables -t nat -N guest_output_redirect
iptables -t nat -A guest_output_redirect -p tcp --dport 80 -j DNAT --to-destination $SQUID_HOST:$SQUID_PORT # HTTP
# Create guest_output_limit
iptables -t filter -N guest_output_limit
iptables -t filter -A guest_output_limit -p tcp --dport 53 -j RETURN # DNS
iptables -t filter -A guest_output_limit -p udp --dport 53 -m limit --limit 16/s --limit-burst 32 -j RETURN
iptables -t filter -A guest_output_limit -p tcp --dport 443 -j RETURN # Web
iptables -t filter -A guest_output_limit -p tcp --dport 8080 -j RETURN
iptables -t filter -A guest_output_limit -p tcp --dport 993 -j RETURN # Mail
iptables -t filter -A guest_output_limit -p tcp --dport 995 -j RETURN
iptables -t filter -A guest_output_limit -p tcp --dport 587 -j RETURN
iptables -t filter -A guest_output_limit -p tcp --dport 465 -j RETURN
iptables -t filter -A guest_output_limit -p tcp --dport 1080 -j RETURN # Other proxies
iptables -t filter -A guest_output_limit -p tcp --dport 9050 -j RETURN
iptables -t filter -A guest_output_limit -p tcp -m multiport --dports 5228:5230 -j RETURN # Android
iptables -t filter -A guest_output_limit -p udp -m multiport --dports 5228:5230 -j RETURN
iptables -t filter -A guest_output_limit -p udp -m multiport --dports 3000:9000 -j RETURN # Extra UDP ports
iptables -t filter -A guest_output_limit -p tcp --dport 21 -j RETURN # Misc
iptables -t filter -A guest_output_limit -p tcp --dport 22 -j RETURN
iptables -t filter -A guest_output_limit -p tcp --dport 23 -j RETURN
iptables -t filter -A guest_output_limit -p tcp --dport 2302 -j RETURN
iptables -t filter -A guest_output_limit -p udp --dport 2302 -m limit --limit 16/s --limit-burst 32 -j RETURN
iptables -t filter -A guest_output_limit -p tcp -j REJECT --reject-with tcp-reset # Deny the reset
iptables -t filter -A guest_output_limit -p udp -j DROP
# Allowing SSH port to be access from the public network
iptables -A INPUT -p tcp --dport $SSH_PORT -j ACCEPT
# Apply guest_output_limit to the default Docker if: docker0
iptables -N $DOCKER_CHAIN
iptables -t filter -I $DOCKER_CHAIN -i $DOCKER_IF ! -o $DOCKER_IF -m conntrack --ctstate NEW -j guest_output_limit
# Apply guest_output_redirect to the default Docker if: docker0
iptables -t nat -A PREROUTING -i $DOCKER_IF -j guest_output_redirect
iptables -A INPUT -i $DOCKER_IF -p tcp --dport $SQUID_PORT -j ACCEPT
$ docker run \
--name shadowsocks \
--detach \
--restart always \
--publish <PUBLIC_PORT>:8388/tcp \
--publish <PUBLIC_PORT>:8388/udp \
--env PASSWORD=******* \
--env METHOD=******* \
--env TIMEOUT=60 \
--env DNS_ADDRS=1.1.1.1 \
shadowsocks/shadowsocks-libev