Locking Down Liteshort
The Watcamp newsletter contains a lot of links to Google Calendar entries. These links are long and ugly, so I wanted to shorten them for my newsletters. Thus I needed a link shortener.
First I used Google's built-in goo.gl shortener, but naturally it was too useful to live and was replaced by some inscrutible thing called Firebase. Then I switched to the https://da.gd service, because it was gratis, FLOSS, light on surveillance, and maintained by somebody who seemed to share some of my values. Unfortunately because da.gd is open, it is used by spammers, so a bunch of corporate content filters block it.
I looked around for alternatives. There were a couple of other mostly-open offerings (https://www.is.gd/ does not seem bad, and neither does the venerable https://tinyurl.com) but any public service that would be free for me to use would also be usable by spammers, and thus would be targeted by the corporate content filters.
Going with one of the popular commercial link shorteners (bit.ly, for example) was another possibility, but they are all gross, and their free plans are fairly restrictive.
In the end I decided that trying to self-host something off of kwlug.org would be the way to go. Here were my requirements:
- FLOSS, of course
- Reasonably easy to self-host
- Reasonably simple, with the ability to create shortlinks and expand them, and that is about it
- Written so it is possible to disable the surveillance
- Secure so I could feel good about self-hosting on somebody else's site
- Related to the above, relatively easy to lock down
- Related to the above, preferably written in Python or another language I had a chance of understanding, as opposed to some PHP monstrosity
- Preferably written by some people whose values aligned with mine. This does not seem important but it is critically important -- depending on some commercial funnelware or "community edition" software is a great way to be disappointed down the road.
After trawling through the Awesome Self-hosted List I decided to try liteshort. It was written in Python, had relatively few features, and the author emphasized simplicity and configurability over surveillance.
Unfortunately, the author does not prioritize security. As asserted on the Nginx configuration page, "Security changes too often for it to be documented here." That is not a great sign, but I was able to lock down the service enough that I feel reasonably confident in running it on the Internet. This blog post documents how I did that.
Initial setup
I chose to run the link shortener on the subdomain s.kwlug.org
.
I installed liteshort system-wide using PyPi, following the installation instructions.
I set up Nginx to be my web server, and used a reverse proxy so liteshort could talk to the webserver.
I encoded my configuration changes using Saltstack, but I will ignore that here.
Enabling SSL
This was relatively easy. As is standard these days, I used Let's
Encrypt. I created an HTTP and an HTTPS server configuration in Nginx,
putting them in /etc/nginx/sites-enabled
. Here is the HTTP configuration, which just redirects to the HTTPS site:
server {
listen 80;
server_name s.kwlug.org;
# Dummy page for http
root /var/www/html/liteshort;
index index.html;
location ^~ /.well-known/acme-challenge/ {
default_type "text/plain";
root /var/www/html/certbot/;
}
location / {
return 301 https://$server_name$request_uri;
}
}
The index.html
was just a dummy page I added for testing. Users should never see it.
The HTTPS configuration looked something like:
server {
server_name s.kwlug.org;
location ^~ /static/ {
include /etc/nginx/mime.types;
root /usr/local/lib/python3.7/dist-packages/liteshort;
}
location / {
include uwsgi_params;
uwsgi_pass unix:///run/uwsgi/liteshort.sock;
}
location ^~ /.well-known/acme-challenge/ {
default_type "text/plain";
alias /var/www/html/certbot/;
allow all;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/s.kwlug.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/s.kwlug.org/privkey.pem;
include /usr/lib/python3/dist-packages/certbot_nginx/options-ssl-nginx.conf
}
Most of this is standard configuration for setting up Let's Encrypt. The second and third stanzas are specific to liteshort. Note that I changed the location of the socket fron /run/liteshort.sock
to /run/uwsgi/liteshort.sock
. I had to make a corresponding change in /etc/liteshort/liteshort.ini
:
socket = /run/uwsgi/liteshort.sock
chmod-socket = 660
The reason for chmod-socket
will become clearer in the next section.
Running as non-root
By default the liteshort service wants to run as root, which is way more powerful than I thought was necessary. So I created a system user called liteshort
with a shell of /usr/sbin/nologin
.
Next I made a folder for liteshort to store its database, which was
owned by this user. /var/lib/liteshort
is used by the default install, so I made a folder /var/lib/liteshort-local
. I configured liteshort to store its database in this location by setting the following parameter in /etc/liteshort/config.yml
:
# String: Filename of the URL database without extension
# Default: 'urls'
database_name: '/var/lib/liteshort-local/liteshort-urls'
liteshort runs as a service, using uWSGI. This is configured in the file /etc/liteshort/liteshort.ini
. To the end of that file I added the lines:
uid = liteshort
gid = liteshort
(I am not sure whether these lines were necessary. I think systemd
might override them.)
Speaking of our favorite and in no way troublesome init system, we need to add lines to specify the user and group liteshort will use. My /etc/systemd/system/liteshort.service
looks like this:
[Unit]
Description=uWSGI instance to serve liteshort
After=network.target
[Service]
RuntimeDirectory=uwsgi
User=liteshort
Group=www-data
ExecStart=/usr/bin/uwsgi --ini /etc/liteshort/liteshort.ini
[Install]
WantedBy=multi-user.target
The User
and Group
lines deserve some elaboration. Nginx runs as
the www-data
user. We want liteshort to run as a different
unprivileged user. They communicate via a UNIX socket. Both
liteshort
and www-data
need to be able to read and write to this
socket, which is why we needed the chmod-socket = 660
in /etc/liteshort/liteshort.ini
above.
Setting up the service this way means the socket will be created with the proper permissions so that the two services can still communicate. This is not as locked down as I would like, but I hope there is some real security improvement here.
The RuntimeDirectory=uwsgi
line dynamically creates entries in /run/uwsgi
for the liteshort service when it is running, and destroys them when
it is not.
Disabling the API
On the surface, this seems easy. In /etc/liteshort/config.yml
set the disable_api
option:
# Boolean: Disables API. If set to true, admin_password/admin_hashed_password do not need to be set.
# Default: false
disable_api: True
However, as of the current writing this does not work the way you might think. Disabling the API disables operations that require a username and API password, but anonymous users will still be able to create links by making POST requests to the service! As it turns out I only want to create links and expand them, so disabling the API makes sense for me, but there is more work to do.
Limiting who can shorten links
I am running liteshort because I want to shorten links in my newsletters. The scripts that generate these newsletters run from a computer with a fixed IP address (say 1.2.3.4
). Thus I would like only this computer to create shortlinks. On the other hand, I would like users from any computer to follow those shortlinks.
Fortunately, liteshort uses POST requests to create shortlinks and GET requests to follow them. We can restrict POST requests in the Nginx config. The relevant snippet for the location
block becomes;
location / {
limit_except GET {
allow 1.2.3.4;
allow 2.3.4.5;
deny all;
}
include uwsgi_params;
uwsgi_pass unix:///run/uwsgi/liteshort.sock;
}
This would allow computers with IP addresses 1.2.3.4
and 2.3.4.5
to create links. Everybody else would just be able to follow shortened links.
This makes me feel much more confident in running liteshort on the Internet. I do not trust the security setup of liteshort, but I do trust that Nginx has good security, because many eyes make shallow bugs.
One problem with this solution is that it returns a status 403 when a bad person tries to POST and not a status 405. I tried getting Nginx to return the proper status instead of deny all;
, but I could not get it to work.