Rust 2019
[web.git] / personal / _posts / 2018-05-28-cloudless-contact-sync.md
1 ---
2 title: "Syncing Contacts Without Exposing Them to the Cloud"
3 categories: sysadmin
4 ---
5
6 I finally have a setup that I am happy with for syncing contacts between my phone and my laptop.
7 Most would probably consider that a solved problem, but I have an extra requirement that rules out most existing solutions:
8
9 > *The data of my contacts must not be exposed in clear text to any machine I do not physically control*.
10
11 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.
12 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.
13 (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.)
14 Moreover, this also rules out putting them e.g. into an ownCloud or Nextcloud hosted on ralfj.de, because that is a VPS that I do not have any physical control over.
15
16 <!-- MORE -->
17
18 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:
19 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.
20
21 To do this, I need a server with an internet-facing IP.
22 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 ralfj.de for that.
23 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.
24
25 I assume you are already familiar with setting up ownCloud or Nextcloud or whatever you want to use (personally, I am using [Radicale](https://github.com/Kozea/Radicale/)), and have it set up on your laptop.
26 If not, all of these projects come with the required documentation and the internet is full of tutorials.
27 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.
28 This requires some configuration on the server, and some configuration on the laptop.
29
30 ## Setting Up the Server
31
32 First we need an IP `$IP` where the HTTP and HTTPS ports are still free, and a hostname `$HOST` pointing to that IP.
33 My server has a second IP for various reasons, so that was fairly easy to do.
34 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.
35
36 We want to use the `-R` option of SSH to forward port 80 and 443 from the server to the laptop.
37 However, these are privileged ports that only root can open, so doing this naively would require logging in as root via SSH.
38 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.
39
40 With [ferm](http://ferm.foo-projects.org/), such an iptables rule would look as follows (here and in the following, replacing `$IP` by whatever IP you are using for this purpose):
41 ```
42 table nat {
43     # PREROUTING applies to packages coming from the outside, OUTPUT
44     # for localhost connections.
45     chain (PREROUTING OUTPUT) {
46         daddr $IP proto tcp dport 80 DNAT to $IP:8053;
47         daddr $IP proto tcp dport 443 DNAT to $IP:44353;
48     }
49 }
50 ```
51 The plain iptables equivalent is
52 ```
53 -A PREROUTING -d $IP -p tcp -m tcp --dport 80 -j DNAT --to-destination $IP:8053
54 -A PREROUTING -d $IP -p tcp -m tcp --dport 443 -j DNAT --to-destination $IP:44353
55 -A OUTPUT -d $IP -p tcp -m tcp --dport 80 -j DNAT --to-destination $IP:8053
56 -A OUTPUT -d $IP -p tcp -m tcp --dport 443 -j DNAT --to-destination $IP:44353
57 ```
58
59 Next, we have to configure the SSH daemon to permit reverse port forwarding to be configured by the client.
60 There is no reason to do this for all users, so instead we create a new user that is used specifically for this purpose:
61 ```
62 adduser --system inverse-cloud --home /var/lib/inverse-cloud
63 ```
64 Next, we configure SSH for this user by editing `/etc/ssh/sshd_config`:
65 ```
66 Match User inverse-cloud
67         ClientAliveInterval 15
68         GatewayPorts clientspecified
69 ```
70 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).
71 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.
72
73 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`.
74 That's already it on the server side!
75 You can test if this all works by running
76 ```
77 ssh -R $IP:44353:localhost:443 -R $IP:8053:localhost:80 inverse-cloud@your-server -i ~/.ssh/inverse-cloud
78 ```
79 and then accessing `$HOST` in your browser. If it all worked, that should connect to the webserver on your laptop.
80
81 ### What If the Ports Are Already Used?
82
83 If you don't have an IP where ports 80 and 443 are still free, the approach above will not work.
84 Installing the NAT rules will make your main webserver unavailable.
85 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.
86
87 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`.
88 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.
89 Instead, we use something like [sniproxy](https://github.com/dlundquist/sniproxy).
90 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).
91 Consult the docs of sniproxy and whatever web server you are using for more details.
92
93 ## Setting up the laptop
94
95 Next, we need to set up the laptop.
96 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.
97 It turns out there already is a tool for this purpose: [autossh](http://www.harding.motd.ca/autossh/)!
98 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.
99 If you are using systemd on your laptop, the following service file will do it:
100 ```
101 [Unit]
102 Description=inverse-cloud
103 After=network-online.target
104
105 [Service]
106 User=$USER # use your username here
107 KillSignal=SIGINT
108 Environment="AUTOSSH_GATETIME=0"
109 # -M 0 --> no monitoring
110 # -N Just open the connection and do nothing (not interactive)
111 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
112 Restart=on-failure
113
114 [Install]
115 WantedBy=multi-user.target
116 ```
117 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.
118 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.
119
120 Now make sure you terminate the test SSH session from the last section, and start your new service.
121 If this works, you should still be able to reach `$HOST` in your browser.
122
123 All we still need to do is set up some crypto.
124 We are going to obtain an SSL certificate for `$HOST` *for your laptop*, and use that to secure the connection to `https://$HOST`.
125 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.
126 The easiest way to obtain such a certificate is using [Let's Encrypt](https://letsencrypt.org/).
127 I am using my own [Let's Encrypt Tiny]({% post_url 2017-12-26-lets-encrypt %}) for this purpose, but you can use any other Let's Encrypt client as well.
128 Since `$HOST:80` legitimately *is* your laptop at this point, the laptop should be able to obtain a certificate just fine.
129
130 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.
131 So, I have set up an nginx reverse proxy with the following configuration
132 ```
133 server {
134     server_name $HOST;
135     listen [::]:80;
136 # Enable these once you have a certificate
137 #    listen [::]:443 ssl;
138 #    ssl_certificate ...;
139 #    ssl_certificate_key ...;
140 #    ssl_dhparam ...;
141
142     # This directory should be empty.
143     root /var/www/$HOST;
144
145     location /.well-known/acme-challenge/ {
146         # You may have to change this directory to match your Let's Encrypt client.
147         alias /var/www/acme-challenge/;
148     }
149
150     # This is the actual reverse proxy.
151     location /radicale/ {
152         proxy_pass http://localhost:35232/;
153     }
154 }
155 ```
156
157 ## That's It
158
159 That's it!
160 You can now access your laptop via `https://$HOST`, and you can set up your devices to use that server for contact synchronization.
161 On my phone, I am using [DAVdroid](https://f-droid.org/packages/at.bitfire.davdroid/), and on the laptop Thunderbird with the [Inverse SOGo Connector](http://sogo.nu/downloads/frontends.html).
162 Whenever both my laptop and my phone are up, they synchronize contacts automatically over a properly end-to-end encrypted channel.
163 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.
164
165 Happy hacking!