Ubuntu 18.04 set up Shadowsocks server with fail2ban

Here I use both Shadowsocks-libev and Shadowsock for no reason

Shadowsocks

Shadowsocks is written in Python

install and config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sudo apt install python-pip
sudo pip install git+https://github.com/shadowsocks/shadowsocks.git@master
sudo ufw allow 8388

sudo tee /etc/shadowsocks/config.json > /dev/null<<EOF
{
"server":"0.0.0.0",
"server_port":8388,
"local_address": "127.0.0.1",
"local_port":1080,
"password":"$(openssl rand -base64 12)",
"timeout":300,
"method":"aes-256-cfb",
"fast_open": false,
"workers": 1,
"prefer_ipv6": false
}
EOF

autostart with systemd [1]

1
2
3
4
5
6
7
8
9
10
11
12
sudo tee /lib/systemd/system/shadowsocks.service <<EOF
[Unit]
Description=Shadowsocks Server
After=network.target

[Service]
ExecStart=/usr/local/bin/ssserver -c /etc/shadowsocks/config.json
Restart=on-abort

[Install]
WantedBy=multi-user.target
EOF

start

1
2
3
4
5
6
7
8
sudo systemctl enable shadowsocks
sudo systemctl start shadowsocks
# sudo systemctl stop shadowsocks
# sudo systemctl restart shadowsocks

# sudo ssserver -c /etc/shadowsocks.json -d start
# sudo ssserver -c /etc/shadowsocks.json -d stop
# sudo ssserver -c /etc/shadowsocks.json -d restart

Shadowsocks-libev

Shadowsocks-libev is written in pure C and depends on libev.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# install
sudo apt install shadowsocks-libev
sudo ufw allow 8389

# config
sudo tee /etc/shadowsocks-libev/config.json > /dev/null<<EOF
{
"server":"0.0.0.0",
"server_port":8389,
"local_port":1081,
"password":"$(openssl rand -base64 12)",
"timeout":60,
"method":"chacha20-ietf-poly1305"
}
EOF

# start
sudo systemctl restart shadowsocks-libev
sudo systemctl start shadowsocks-libev.service
sudo systemctl enable shadowsocks-libev.service

# check
sudo systemctl status shadowsocks-libev.service

Problem

systemctl status shadowsocks-libev.service status shows following error:[2]

This system doesn’t provide enough entropy to quickly generate high-quality random numbers. The service will not start until enough entropy has been collected.

Solution

1
2
sudo apt-get install rng-tools
sudo rngd -r /dev/urandom

fail2ban

There are also other options to secure the Shadowsocks Server[3]

But since my server is only uesd by some acquaintances, I just care about brute force password cracking.

Intro

fail2ban is used to ban IP addresses conducting too many failed login attempts.

How fail2ban works?[4]
fail2ban use date pattern to capture and remove the date from log, and then use failregex to parse the log to get the IP. After that, fail2ban would update the firewall rule to block the IP addresses

log file

Both Shadowsocks and Shadowsocks-libev output log to /var/log/syslog, Shadowsocks also output some INFO and DEBUG level log to /var/log/shadowsocks.log

Shadowsocks can customize log file location but Shadowsocks-libev cannot[5]

I would use ufw rather than iptables to modify my firewall sudo vim /etc/fail2ban/jail.local

1
2
[DEFAULT]
banaction = ufw

Shadowsocks

log sample

Aug 15 08:30:39 <hostname> ssserver[1377]: 2018-08-15 08:30:39 ERROR can not parse header when handling connection from <HOST>:<PORT>

create filter[6][7]

1
2
3
4
5
6
7
8
9
10
11
12
13
sudo tee /etc/fail2ban/filter.d/shadowsocks.conf > /dev/null <<EOF
[INCLUDES]
before = common.conf

[Definition]
_daemon = ssserver

failregex = ^\w+\s+\d+ \d+:\d+:\d+\s+%(__prefix_line)sERROR\s+can not parse header when handling connection from <HOST>:\d+$

ignoreregex =

datepattern = %%Y-%%m-%%d %%H:%%M:%%S
EOF

test
fail2ban-regex /var/log/syslog /etc/fail2ban/filter.d/shadowsocks.conf --print-all-matched

update jail config

sudo vim /etc/fail2ban/jail.local

1
2
3
4
5
6
7
8
9
[shadowsocks]
enabled = true
filter = shadowsocks
port = 8838
logpath = /var/log/syslog

maxretry = 3
findtime = 3600
bantime = 3600

Shadowsocks-libev

log sample

Aug 15 08:59:07 <hostname> ss-server[1382]: 2018-08-15 08:59:07 ERROR: failed to handshake with <HOST>: authentication error

create filter

1
2
3
4
5
6
7
8
9
10
11
12
13
sudo tee /etc/fail2ban/filter.d/shadowsocks-libev.conf > /dev/null <<EOF
[INCLUDES]
before = common.conf

[Definition]
_daemon = ss-server

failregex = ^\w+\s+\d+ \d+:\d+:\d+\s+%(__prefix_line)sERROR:\s+failed to handshake with <HOST>: authentication error$

ignoreregex =

datepattern = %%Y-%%m-%%d %%H:%%M:%%S
EOF

sudo vim /etc/fail2ban/jail.local

test
fail2ban-regex /var/log/syslog /etc/fail2ban/filter.d/shadowsocks-libev.conf --print-all-matched

update jail config

sudo vim /etc/fail2ban/jail.local

1
2
3
4
5
6
7
8
9
[shadowsocks-libev]
enabled = true
filter = shadowsocks-libev
port = 8839
logpath = /var/log/syslog

maxretry = 3
findtime = 3600
bantime = 3600

start fail2ban

1
2
3
4
5
6
sudo systemctl restart fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
sudo systemctl status fail2ban
sudo fail2ban-client status shadowsocks
sudo fail2ban-client status shadowsocks-libev

Slack webhook notifications[8][9]

I use bot-user here so I can select different channel with same token. That means I can use different channel for each application with one bot-user.

Of course you can use webhook[10], which could be used only for one channel. In other word, if you want multiple channels, you have to create one webhook for each channel and then modify the related curl argument.

Generate a bot-user

just follow the official instructions

create action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
sudo tee /etc/fail2ban/action.d/slack-notify.conf > /dev/null <<EOF
#
# Author: Yue (Zed) Yang
#

[Definition]

# Option: actionstart
# Notes.: command executed once at the start of Fail2Ban.
# Values: CMD
#
actionstart = curl -s -o /dev/null 'https://slack.com/api/chat.postMessage' -d 'token=<slack_api_token>' -d 'channel=#<slack_channel>' -d 'text=Fail2Ban (<name>) jail has started'

# Option: actionstop
# Notes.: command executed once at the end of Fail2Ban
# Values: CMD
#
actionstop = curl -s -o /dev/null 'https://slack.com/api/chat.postMessage' -d 'token=<slack_api_token>' -d 'channel=#<slack_channel>' -d 'text=Fail2Ban (<name>) jail has stopped'

# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck =

# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#

actionban = curl -s -o /dev/null 'https://slack.com/api/chat.postMessage' -d 'token=<slack_api_token>' -d 'channel=#<slack_channel>' -d 'text=Fail2Ban (<name>) banned IP *<ip>* for <failures> failure(s)'

# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
actionunban = curl -s -o /dev/null 'https://slack.com/api/chat.postMessage' -d 'token=<slack_api_token>' -d 'channel=#<slack_channel>' -d 'text=Fail2Ban (<name>) unbanned IP *<ip>*'
EOF

update jail config

sudo vim /etc/fail2ban/jail.local

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#
# ACTIONS
#

slack_api_token = <slack_api_token>
slack_channel = general

action_with_slack_notification = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
slack-notify[name=%(__name__)s, slack_api_token=%(slack_api_token), slack_channel=%(slack_channel)]

action = %(action_with_slack_notification)s

[shadowsocks]
slack_channel = supervise-shadowsocks

[shadowsocks-libev]
slack_channel = supervise-shadowsocks

Just fill your token and channel

restart fail2ban

1
2
sudo systemctl restart fail2ban
sudo systemctl status fail2ban

Notes

  • use ufw rather than iptables to modify firewall, it would make life easier

    UFW (Uncomplicated Firewall) is a front-end for iptables and is particularly well-suited for host-based firewalls.https://help.ubuntu.com/community/Firewall

  • Can't assign requested address[11]
    In config file, set the server_ip as 0.0.0.0

Reference

[1] https://novnan.github.io/Shadowsocks/setup-Shadowsocks-on-ubuntu-1604/
[2] https://www.linuxbabe.com/ubuntu/shadowsocks-libev-proxy-server-ubuntu-16-04-17-10
[3] https://github.com/shadowsocks/shadowsocks/wiki/Securing-Public-Shadowsocks-Server
[4] https://github.com/fail2ban/fail2ban/issues/2201#issuecomment-413155557
[5] https://github.com/shadowsocks/shadowsocks/issues/1242
[6] https://fail2ban.readthedocs.io/en/latest/filters.html
[7] https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Filters
[8] https://github.com/coleturner/fail2ban-slack-action
[9] https://api.slack.com/methods/chat.postMessage
[10] https://api.slack.com/incoming-webhooks
[11] https://github.com/shadowsocks/shadowsocks/issues/961