Putting web-facing software on the public internet is hard
My sysadmin persona is meek and easily intimidated, which is why I’m close to panic when I have to set up anything that needs to be reachable from the internet. Over the holidays I set up a private build server which should be accessible by a small group of trusted developers and testers only. The server would run a few home-grown web applications and a build chain consisting of Gitlab, Jenkins, Nexus and Mediawiki. All these tools run their own embedded web servers of various flavours ranging from Nginx to Apache, they have their own web interfaces and countless binary dependencies… I honestly don’t trust myself operating this setup safely on the internet. Our servers are scanned daily by hundreds of bots for known exploits, if any of these applications were left unpatched for a few days then most likely the entire server would be compromised.
The action plan
3. get an SSL certificate for the domain or self-sign one
4. distribute personal, client certificates to developers which they need to install in their browsers and tools like Git
6. HAProxy forwards HTTP requests to the firewalled applications
Why is this set-up secure enough?
Setting up the firewall
This is the first time I don’t need a complicated firewall set-up with port forwarding, so UFW [13] will do just fine:
ufw allow 22/tcp ufw allow 80/tcp ufw allow 443/tcp
Fail2ban bans misbehaving IP addresses
Fail2ban [11] scans logs for suspicious activities and blocks IP addresses it can associate with these activities; a great way to keep nosey bots that scan for known exploits away. In its default setup it already blocks IP addresses that attempt too many failed SSH logins.
An overly simple rule for HAProxy (/etc/fail2ban/filter.d/haproxy.conf) might look like:
# Haproxy Fail2Ban Regex using CLF format [Definition] failregex =^.*?haproxy.*?: -.* ignoreregex =
And /etc/fail2ban/jail.local :
# 360 requests in 2 min :Ban for 10 min [haproxy] enabled = true port = http,https filter = haproxy logpath = /var/log/haproxy.log maxretry = 60 findtime = 60 action = iptables[name=HTTP, port=http, protocol=tcp] mail-whois-lines[name=%(__name__)s, dest=%(destemail)s, logpath=%(logpath)s] bantime = 600
HAProxy
The interesting parts of the HAProxy configuration:
# SSL termination for build server frontend www-https bind 78.47.203.76:8443 name https ssl crt /etc/ssl/private/server.pem ca-file /etc/ssl/private/server.pem verify required reqadd X-Forwarded-Proto:\ https use_backend gitlab if { hdr_beg(Host) -i gitlab.example.com } use_backend jenkins if { hdr_beg(Host) -i jenkins.example.com } default_backend nobackend backend gitlab http-response del-header Server server gitlab 127.0.0.1:10080 check backend jenkins http-response del-header Server server jenkins 127.0.0.1:18080 check backend nobackend server nobackend 127.0.0.1:65000 check
Docker
Server and client certificates
We ended up installing client certificates in users’ browsers and command line tools, which is a bit of a hassle to get right, but once done it works reliably.
Automatically installing security updates
Appendix
Appendix: on the need for a sub-domain per application
Most web applications we looked at require that their public URLs are mapped to the root path, e.g. https://example.com:8080/ for Jenkins. If one maps them (i.e. via URL rewriting) to a sub-path like https://example.com:8080/jenkins/ then they break in various subtle ways. Thus, hosting multiple applications that all need to be mapped to the root path on the same server is possible only when they run at either different TPC ports or at different public (sub) domains.
Since we’re trying to run a tight ship, opening more TCP ports doesn’t seem like a good idea. Instead, we have HAProxy map an individual sub-domains for every application to the respective application’s back-end.
Appendix: Docker containers talking to each other
That one popped up late. After having set up all tools we started integrating them and found out that e.g. Jenkins couldn’t talk to Gitlab over HTTP, mainly because it didn’t know how to resolve the Gitlab’s container IP. That is easily done with container linking [9]: when you link container A to container B, Docker puts container B’s name and IP address in container’s A /etc/hosts. As an added benefit, linked containers expose to each other their original TCP ports and not the ones mapped at runtime which makes for “prettier” host names and ports. For example, if the jenkins container is accessible at http://jenkins.example.com:8080 I still can map it via linking in such a way that other (linked) containers see it at http://jenkins (with port 80 implied).
Appendix: Gitlab
Gitlab doesn’t correctly work with SSL termination out of the box [8]. When it detects that its own public URL is HTTPS then the bundled Nginx will enable SSL termination and change the TCP port it is listening to, causing two problems: a) Gitlab will try to decrypt the already decrypted HTTP traffic that comes in from HAProxy and b) it is listening to a different port than agreed in the configuration. [8] tells how to fix that. Gitlab also consumes a lot of memory [9] which doesn’t exactly predestine it for containerization, but a glimpse into /etc/gitlab/gitlab.rb reveals several switches that reduce memory footprint.
Appendinx: Jenkins
The official Jenkins image (1.652.3) can’t clone git repositories because the linux user it runs under isn’t set up “correctly” (my words). When Jenkins runs Git it fails with:
docker jenkins unable to look up current user in the passwd file: no such user
The minimalistic image set-up lacks many useful tools (ifconfig, vi etc). So we’re using our own Dockerfile and fixing the Jenkins user:
FROM jenkins:1.625.3 USER root RUN chmod 644 /etc/nsswitch.conf RUN adduser consoleuser --uid 1002 --home /home/consoleuser --disabled-password USER jenkins
Resources
[1] Unattended upgrades for Ubuntu server
http://askubuntu.com/questions/325998/how-to-enable-auto-security-update-in-ubuntu-12-04-server
[2] HAproxy: client side ssl certificates
https://raymii.org/s/tutorials/haproxy_client_side_ssl_certificates.html
[3] Using client certificates with haproxy
https://serversforhackers.com/using-ssl-certificates-with-haproxy
[4] OpenSSL
https://help.ubuntu.com/community/OpenSSL#Using_PKCS.2312_Certificates_in_Client_Applications
[5] Creating self-signed certificates
https://help.ubuntu.com/12.04/serverguide/certificates-and-security.html#creating-a-self-signed-certificate
[7] Port knocking
https://en.wikipedia.org/wiki/Port_knocking
[8] Gitlab behind proxy
https://github.com/gitlabhq/gitlabhq/issues/9911
[9] Docker container linking
https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/
[10] Gitlab high memory footprint
https://github.com/gitlabhq/gitlabhq/issues/903
[11] Fail2ban
http://www.fail2ban.org/wiki/index.php/Main_Page
[12] HAProxyhttp://www.haproxy.org/
[13] UFW
https://en.wikipedia.org/wiki/Uncomplicated_Firewall
[14] Docker
https://www.docker.com/
[16] Let’s encrypt
https://letsencrypt.org/
Thanking you for sharing a nice and detailed setup !
LikeLike
I'm glad you liked it. I am learning more about operations with Docker these days, there will be a follow-up on some problems we faced with Docker and how we solved them.
LikeLike
Hi, fantastic writeup, I was wondering if you had any more suggestions or recommendations with Fail2Ban and HAProxy? Im currently trying to configure some custom filters to block bots/hackers trying to access URL's which are not present (WordPress Login URL's when the site is not even a WordPress site.) Do you have any other custom filters that you recommend? (RegEx is like witchcraft and I haven't been able to suss it out.)
LikeLike
Fail2ban has the badbots filter (https://github.com/fail2ban/fail2ban/blob/master/config/filter.d/apache-badbots.conf) that works on Apache logs, but I can't say how current it is. You probably could adapt it for other type of logs too. Haproxy can be configured to write Apache-style logs (the CLF option). The point of using client certificates is so that you don't have to worry about bots any more since they can't authenticate without certificates anyway.
LikeLike