May 28, 2018 • SysadminEditsPermalink

Syncing Contacts Without Exposing Them to the Cloud

I finally have a setup that I am happy with for syncing contacts between my phone and my laptop. Most would probably consider that a solved problem, but I have an extra requirement that rules out most existing solutions:

The data of my contacts must not be exposed in clear text to any machine I do not physically control.

I’m not happy about my own personal data being shared with third parties, and consequently, I will not share the personal data others entrust to me with third parties. This obviously rules out all of these “free” cloud services out there that Apple, Google and others offer—services that are paid with data, and in the case of contact sync frequently paid with the data of others. (This leaves me wonder whether under the GDPR, it is even legal for someone else to consent to my contact data being shared with a cloud provider. I certainly never consented and still my data sits in multiple synced address books. But that’s a discussion for another day.) Moreover, this also rules out putting them e.g. into an ownCloud or Nextcloud hosted on, because that is a VPS that I do not have any physical control over.

I could host the contact server on a Raspberry Pi at home and set up dynamic DNS and port forwarding “as usual”, but today we are going to look at another solution: Hosting the contact server on my laptop, and making that accessible from my phone even when my laptop is behind a non-cooperating NAT or another kind of firewall.

To do this, I need a server with an internet-facing IP. However, that server will just serve as a relay between my phone and my laptop, and the data is going to be encrypted, so I can just use for that. The plan is to forward the HTTP and HTTPS ports from that IP to the laptop using inverse SSH port forwarding, therefore making the server running on the laptop available to the internet.

I assume you are already familiar with setting up ownCloud or Nextcloud or whatever you want to use (personally, I am using Radicale), and have it set up on your laptop. If not, all of these projects come with the required documentation and the internet is full of tutorials. The remaining task, then, is to make this server, which is running on your laptop, available to the entire internet so your phone can access it. This requires some configuration on the server, and some configuration on the laptop.

Setting Up the Server

First we need an IP $IP where the HTTP and HTTPS ports are still free, and a hostname $HOST pointing to that IP. My server has a second IP for various reasons, so that was fairly easy to do. If yours does not, you should still be able to achieve this kind of setup through a reverse SNI proxy running on your server; I will come back to that variant later.

We want to use the -R option of SSH to forward port 80 and 443 from the server to the laptop. However, these are privileged ports that only root can open, so doing this naively would require logging in as root via SSH. For security reasons, I have root logins disabled, and I’d rather not change that, so we proceed differently instead: We forward ports 8053 and 44353, and then we have NAT rules on the server that forward packages from ports 80 and 443 to ports 8053 and 44353, respectively.

With ferm, such an iptables rule would look as follows (here and in the following, replacing $IP by whatever IP you are using for this purpose):

table nat {
    # PREROUTING applies to packages coming from the outside, OUTPUT
    # for localhost connections.
        daddr $IP proto tcp dport 80 DNAT to $IP:8053;
        daddr $IP proto tcp dport 443 DNAT to $IP:44353;

The plain iptables equivalent is

-A PREROUTING -d $IP -p tcp -m tcp --dport 80 -j DNAT --to-destination $IP:8053
-A PREROUTING -d $IP -p tcp -m tcp --dport 443 -j DNAT --to-destination $IP:44353
-A OUTPUT -d $IP -p tcp -m tcp --dport 80 -j DNAT --to-destination $IP:8053
-A OUTPUT -d $IP -p tcp -m tcp --dport 443 -j DNAT --to-destination $IP:44353

Next, we have to configure the SSH daemon to permit reverse port forwarding to be configured by the client. There is no reason to do this for all users, so instead we create a new user that is used specifically for this purpose:

adduser --system inverse-cloud --home /var/lib/inverse-cloud

Next, we configure SSH for this user by editing /etc/ssh/sshd_config:

Match User inverse-cloud
	ClientAliveInterval 15
	GatewayPorts clientspecified

The second option lets the client control the reverse port forwarding; ClientAliveInterval is useful because it makes the server kill the connection if the client does not respond for 45 seconds (three alive intervals). When the laptop goes offline, we want to kill the connection on both ends quickly so that the laptop can open a new connection; as long as the old one is still around the ports are still blocked.

Now you just need to put a public SSH key (I suggest you create a new one for this purpose, and it should have no passphrase) into /var/lib/inverse-cloud/.ssh/authorized_keys. That’s already it on the server side! You can test if this all works by running

ssh -R $IP:44353:localhost:443 -R $IP:8053:localhost:80 inverse-cloud@your-server -i ~/.ssh/inverse-cloud

and then accessing $HOST in your browser. If it all worked, that should connect to the webserver on your laptop.

What If the Ports Are Already Used?

If you don’t have an IP where ports 80 and 443 are still free, the approach above will not work. Installing the NAT rules will make your main webserver unavailable. However, you can still use reverse port forwarding, you just need a reverse proxy in front of it that decides if a connection goes to the local webserver or the ports opened by the laptop.

On port 80, that can be just done with an apache vhost or using nginx (or any other webserver, really) that forwards all requests for $HOST to the local port 8053. However, if we did the same with port 443 then nginx on your server would terminate the HTTPS tunnel, which violates our goal of not exposing the unencrypted contacts to the server. Instead, we use something like sniproxy. I have not actually done this setup, but the rough idea is to make sniproxy listen on port 443, to forward $HOST to $IP:44353 and forward everything else to your main webserver (which has to be configured to listen on a different port). Consult the docs of sniproxy and whatever web server you are using for more details.

Setting up the laptop

Next, we need to set up the laptop. First of all, we don’t want to manually run ssh -R ... all the time, and we also need to take care of reconnecting when the connection fails. It turns out there already is a tool for this purpose: autossh! After installing it, all you need to do is automatically run autossh and it will keep the connection to your server, and therefore the reverse port forwarding, alive. If you are using systemd on your laptop, the following service file will do it:


User=$USER # use your username here
# -M 0 --> no monitoring
# -N Just open the connection and do nothing (not interactive)
ExecStart=/usr/bin/autossh -M 0 -N -o "ServerAliveInterval 30" -R $IP:44353:localhost:443 -R $IP:8053:localhost:80 inverse-cloud@your-server -i /home/$USER/.ssh/inverse-cloud


I have set AUTOSSH_GATETIME to 0 because I had trouble with autossh quitting after ssh failed due to a lack of DNS resolution when the laptop is offline. The ServerAliveInterval serves the same purpose as the ClientAliveInterval on the server side: The SSH connection is automatically killed after 90 seconds, which will trigger autossh to try again.

Now make sure you terminate the test SSH session from the last section, and start your new service. If this works, you should still be able to reach $HOST in your browser.

All we still need to do is set up some crypto. We are going to obtain an SSL certificate for $HOST for your laptop, and use that to secure the connection to https://$HOST. Because only the laptop has the key to this certificate, the server at $IP cannot actually decipher the connection, it just forwards the encrypted bytes to the laptop where they are decrypted. The easiest way to obtain such a certificate is using Let’s Encrypt. I am using my own Let’s Encrypt Tiny for this purpose, but you can use any other Let’s Encrypt client as well. Since $HOST:80 legitimately is your laptop at this point, the laptop should be able to obtain a certificate just fine.

If you are using Radicale like me, just putting Radicale on port 80 is not going to work though as that provides no way to serve the ACME challenge file needed for Let’s Encrypt. So, I have set up an nginx reverse proxy with the following configuration

server {
    server_name $HOST;
    listen [::]:80;
# Enable these once you have a certificate
#    listen [::]:443 ssl;
#    ssl_certificate ...;
#    ssl_certificate_key ...;
#    ssl_dhparam ...;

    # This directory should be empty.
    root /var/www/$HOST;

    location /.well-known/acme-challenge/ {
        # You may have to change this directory to match your Let's Encrypt client.
        alias /var/www/acme-challenge/;

    # This is the actual reverse proxy.
    location /radicale/ {
        proxy_pass http://localhost:35232/;

That’s It

That’s it! You can now access your laptop via https://$HOST, and you can set up your devices to use that server for contact synchronization. On my phone, I am using DAVdroid, and on the laptop Thunderbird with the Inverse SOGo Connector. Whenever both my laptop and my phone are up, they synchronize contacts automatically over a properly end-to-end encrypted channel. When my laptop is offline, I get a warning on my phone about not being able to sync, but the sync will resume automatically when the laptop is online again.

Happy hacking!

Posted on Ralf's Ramblings on May 28, 2018.
Comments? Drop me a mail!