Skip to content

This document is a WORK IN PROGRESS.
This is just a quick personal cheat sheet: treat its contents with caution!


Radicale

Radicale is a small but powerful CalDAV (calendars, todo lists) and CardDAV (contacts) server.

Reference(s)

Table of contents


Install

Install Radicale with Python (v3!) in /root/.local/bin/radicale:

$ sudo python -m pip install radicale --user

Create a folder, wherever you want, to store your CalDAV and CardDAV collections:

$ sudo mkdir -p /path/to/radicale/collections

Check the installation by running the Radicale server, then you can access it by searching localhost:5232 in your web browser:

$ sudo python -m radicale --storage-filesystem-folder=/path/to/radicale/collections
$ firefox localhost:5232 # e.g. with firefox


Config

Radicale config file

Create the Radicale configuration file with a basic config:

$ sudo mkdir /etc/radicale
$ sudo vi /etc/radicale/config
  + > [server]
  + > hosts = 0.0.0.0:5232, [::]:5232
  + >
  + > [storage]
  + > filesystem_folder = /path/to/radicale/collections

Authentication

Create the following script, it will allow you to define a user (and associated password) for your Radicale server:

$ cd /path/to/radicale/
    > #!/bin/sh
    >
    > echo "Info  - This script is a 'htpasswd' compatile alternative for this command:"
    > echo "        'htpasswd -c /path/to/file username'"
    > echo "        by running './passwd.sh /path/to/file' instead."
    > echo ""
    >
    > if [ $# -ne 1 ]
    > then
    >     echo "Error - Illegal number of parameter(s), one paramater must be provided: "
    >     echo "        the path to fhe file where you want to append the result of this script."
    >     exit 1
    > fi
    >
    > if [ ! -f $1 ]
    > then
    >     echo "Error - The path to fhe file where you want to append the result of this script"
    >     echo "        does not exist!"
    >     exit 2
    > fi
    >
    > if [ ! -w $1 ]
    > then
    >     echo "Error - The path to fhe file where you want to append the result of this script"
    >     echo "        is not writable by the current user!"
    >     echo "        (you might want to run this script with 'sudo')"
    >     exit 3
    > fi
    >
    > username=""
    > password=""
    >
    > echo "[passwd.sh] enter the name of the user you want to create:"
    > read username
    >
    > echo "[passwd.sh] enter the password of the user you want to create:"
    > get_pwd () {
    >     # Safe and POSIX compliant way of asking password:
    >     stty -echo
    >     read password
    >     stty echo
    >     printf "\n"
    > }
    > get_pwd
    >
    > echo "$username":$(openssl passwd -apr1 "$password") >> $1

$ chmod +x passwd.sh

Create a user and it's password:

$ ./passwd.sh /path/to/radicale/users

You can create second user/password just by running the same command again.

Then update the Radicale config file:

$ sudo vi /etc/radicale/config
    > [server]
    > hosts = 0.0.0.0:5232, [::]:5232
  + >
  + > [auth]
  + > type = htpasswd
  + > htpasswd_filename = /path/to/radicale/users
  + > # encryption method used in the htpasswd file:
  + > htpasswd_encryption = md5
    >
    > [storage]
    > filesystem_folder = /path/to/radicale/collections

Domain name and router configuration

Getting a domain name is optional, but recommended if you don't want to enter your server's public IP address every time you want to access your Radicale server, or if you don't have a fixed public IP address.

Configuring your router in order to forward your port 80 and port 443 is optional, but recommended if you want to access your Radicale server outside your local network.

You can refer to DuckDNS for a free and open source service which will point a DNS (sub domains of duckdns.org) to an IP of your choice, but you can choose the domain name provider of your choice. In any cases, this cheat sheet will assume that your domain name is yourhostname.tld

You might want to check if your router does support NAT loopback (see https://en.wikipedia.org/wiki/Network_address_translation#NAT_hairpinning and see https://en.wikipedia.org/wiki/Hairpinning). If it doesn't, you will have to look for a work around in order to access your server by it's domain name from your local network.

Nginx server

  • For reference my default /etc/nginx/nginx.conf can be found here, you just have to modify it like so:

    $ sudo vi /etc/nginx/nginx.conf
        > ...
      ~ >         #include /etc/nginx/conf.d/*.conf;
      + >         include /etc/nginx/conf.d/radicale.conf;
        > }
    

  • And create the following configuration file:

    $ sudo vi /etc/nginx/conf.d/radicale.conf
    
        > server {
        >     listen 80;
        >     listen [::]:80;
        >     server_name yourhostname.tld;
        >
        >     return 301 https://$server_name:5232;
        > }
    

Check the Nginx server

  • Test your configuration:

    # nginx -t
    

  • (re)start nginx:

$ sudo rc-service nginx restart
$ sudo sv restart service_name
$ sudo service nginx restart
$ sudo systemctl restart nginx
  • (re)start Radicale:

    $ sudo python -m radicale
    

  • Now open a web browser and navigate to yourhostname.tld, you should see the login menu of Radicale.

Basic SSL certs config

  • Create basic self signed SSL certificate and key:

    $ sudo mkdir /etc/ssl/radicale
    $ sudo openssl req -x509 -nodes -days 9999 -newkey rsa:4096 -keyout /etc/ssl/radicale/yourhostname.tld.key -out /etc/ssl/radicale/yourhostname.tld.crt # ⚠️ don't forget to replace "yourhostname.tld" in this cmd ⚠️
    

  • Add the SSL cert and key to the Nginx Nextcloud config:

    $ sudo vi /etc/nginx/conf.d/radicale.conf
        > server {
        >     listen 80;
        >     listen [::]:80;
        >     server_name yourhostname.tld;
        >
      + >     # enforce https
      ~ >     return 301 https://$server_name:443$request_uri;
        > }
      + >
      + > server {
      + >     listen 443 ssl http2;
      + >     listen [::]:443 ssl http2;
      + >     server_name yourhostname.tld;
      + >
      + >     ## Basic ssl config
      + >     ###################
      + >     ssl_certificate         /etc/ssl/radicale/yourhostname.tld.crt;
      + >     ssl_certificate_key     /etc/ssl/radicale/yourhostname.tld.key;
      + >     ###################
      + >
      + >     return 301 https://$server_name:5232/;
      + > }
    

  • Then update the Radicale config file:

    $ sudo vi /etc/radicale/config
        > [server]
        > hosts = 0.0.0.0:5232, [::]:5232
      + > ssl = True
      + > certificate = /etc/ssl/radicale/yourhostname.tld.crt
      + > key = /etc/ssl/radicale/yourhostname.tld.key
        >
        > [auth]
        > type = htpasswd
        > htpasswd_filename = /path/to/radicale/users
        > # encryption method used in the htpasswd file:
        > htpasswd_encryption = md5
        >
        > [storage]
        > filesystem_folder = /path/to/radicale/collections
    

  • Check the Nginx server

Advanced SSL certs config

  • Install acme-tiny:

  • Create a folder for the acme-challenge and another one for letsencrypt:

    $ sudo mkdir -p /path/to/radicale/acme-challenge
    $ sudo mkdir -p /path/to/radicale/letsencrypt
    

  • Create the account and domain private keys and the domain certificate:

    $ cd /path/to/letsencrypt
    $ sudo openssl genrsa 4096 | sudo tee account.key # account private key: DO NOT SHARE
    $ sudo openssl genrsa 4096 | sudo tee domain.key  # domain  private key: DO NOT SHARE
    $ sudo openssl req -new -sha256 -key domain.key -subj "/CN=yourhostname.tld" | sudo tee domain.csr
    

  • Edit your radicale.conf file like below:

    $ sudo vi /etc/nginx/conf.d/radicale.conf # add acm-challenge location
        > server {
        >     listen 80;
        >     listen [::]:80;
        >     server_name yourhostname.tld;
        >
      + >     # Match best non-regular expression for "/.well-known/acme-challenge/"
      + >     location ^~ /.well-known/acme-challenge/ {
      + >         #alias /var/www/stephane-radicale.duckdns.org/acme-challenge/;
      + >         alias /media/raid/radicale/acme-challenge/;
      + >         try_files $uri =404;
      + >     }
      + >
      + >     # Match everything else to enforce https
      + >     location / {
      ~ >         return 301 https://$server_name:443$request_uri;
      + >     }
        > }
        >
        > server {
        >     listen 443 ssl http2;
        >     listen [::]:443 ssl http2;
        >     server_name yourhostname.tld;
        >
        >     ## Basic ssl config
        >     ###################
        >     ssl_certificate         /etc/ssl/radicale/yourhostname.tld.crt;
        >     ssl_certificate_key     /etc/ssl/radicale/yourhostname.tld.key;
        >     ###################
        >
        >     return 301 https://$server_name:5232;
        > }
    

  • Check the Nginx server

  • Get the certificate from Let’s Encrypt:

    $ cd /path/to/radicale/letsencrypt
    $ sudo acme-tiny --account-key account.key --csr domain.csr --acme-dir /path/to/radicale/acme-challenge/ | sudo tee signed_chain.crt
    
    $ sudo cp signed_chain.crt signed_chain.crt.back # backup signed_chain.crt just in case
    

    Note: signed_chain.crt is the root CA certificate, and it is self signed.

  • Generate your Diffie-Hellman parameter file (this will take a long time):

    $ sudo openssl dhparam -out dhparam4096.pem 4096
    

  • Download Let’s Encrypt intermediate cert and create a fullchain file with it and the signed cert:

    $ sudo wget -O - https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem | sudo tee chain.pem
    $ sudo cat signed_chain.crt chain.pem | sudo tee fullchain.pem
    

    Note: chain.pem is the intermediate certificate.

  • Just to make sure, you can verify the cert with this command:

    ?$ sudo openssl verify -CAfile signed_chain.crt chain.pem
    ?$ sudo openssl verify -CAfile fullchain.pem signed_chain.crt
    ?$ sudo openssl verify -CAfile signed_chain.crt fullchain.pem
    

  • Then update the Radicale config file:

    $ sudo vi /etc/radicale/config
        > [server]
        > hosts = 0.0.0.0:5232, [::]:5232
        > ssl = True
      ~ > #certificate = /etc/ssl/radicale/yourhostname.tld.crt
      ~ > #key = /etc/ssl/radicale/yourhostname.tld.key
      + > certificate = /path/to/radicale/letsencrypt/signed_chain.crt
      + > key = /path/to/radicale/letsencrypt/domain.key
        >
        > [auth]
        > type = htpasswd
        > htpasswd_filename = /path/to/radicale/users
        > # encryption method used in the htpasswd file:
        > htpasswd_encryption = md5
        >
        > [storage]
        > filesystem_folder = /path/to/radicale/collections
    

  • Now your radicale.conf file should look like this:

    $ sudo vi /etc/nginx/conf.d/radicale.conf
        > server {
        >     listen 80;
        >     listen [::]:80;
        >     server_name yourhostname.tld;
        >
        >     # Match best non-regular expression for "/.well-known/acme-challenge/"
        >     location ^~ /.well-known/acme-challenge/ {
        >         #alias /var/www/stephane-radicale.duckdns.org/acme-challenge/;
        >         alias /media/raid/radicale/acme-challenge/;
        >         try_files $uri =404;
        >     }
        >
        >     # Match everything else to enforce https
        >     location / {
        >         return 301 https://$server_name:443$request_uri;
        >     }
        > }
        >
        > server {
        >     listen 443 ssl http2;
        >     listen [::]:443 ssl http2;
        >     server_name yourhostname.tld;
        >
        >     ## Basic ssl config
        >     ###################
      ~ >     #ssl_certificate         /etc/ssl/radicale/yourhostname.tld.crt;
      ~ >     #ssl_certificate_key     /etc/ssl/radicale/yourhostname.tld.key;
        >     ###################
      + >
      + >     ## Advanced ssl config
      + >     ######################
      + >     ssl_certificate         /path/to/radicale/letsencrypt/signed_chain.crt;
      + >     ssl_certificate_key     /path/to/radicale/letsencrypt/domain.key;
      + >
      + >     ssl_dhparam             /path/to/radicale/letsencrypt/dhparam4096.pem;
      + >
      + >     ssl_stapling on;
      + >     ssl_stapling_verify on;
      + >
      + >     ssl_trusted_certificate /path/to/radicale/letsencrypt/fullchain.pem;
      + >
      + >     #add_header Strict-Transport-Security "max-age=63072000" always;
      + >
      + >     ssl_session_timeout 1d;
      + >     ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
      + >     ssl_session_tickets off;
      + >
      + >     ssl_protocols TLSv1.2 TLSv1.3;
      + >     ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
      + >     ssl_prefer_server_ciphers off;
      + >     ######################
        >
        >     return 301 https://$server_name:5232/;
        > }
    

    Note: see https://ssl-config.mozilla.org for a secure SSL config.

  • Check the Nginx server

Now you can test your config at this addresses:

With the previous configurations, you should get A+ grades.

Create a Radicale service

With OpenRC

An OpenRC service can be created in order to start the Radicale server at startup, before login and starting the graphical server:

# sudo vi /etc/init.d/radicale # create an openrc service
    > #!/sbin/openrc-run
    >
    > # See <https://github.com/Kozea/Radicale/issues/737>
    >
    > command="python"
    > command_args="-m radicale"
    > #command_user="root"
    > command_background="yes"
    > pidfile="/var/run/radicale.pid"
    > description="A Free and Open-Source CalDAV and CardDAV Server"
    >
    > depend() {
    >         need localmount
    >         need net
    >         need nginx
    > }

$ sudo chmod +x /etc/init.d/radicale
$ rc-update add radicale default

Auto renew certificates Cron job config

Reference(s)
$ sudo vi /opt/renew_cert_for_yourhostname.tld.sh
    > #!/bin/sh
    >
    > rm -f /path/to/letsencrypt/signed_chain.crt.tmp
    >
    > acme-tiny --disable-check --account-key /path/to/radicale/letsencrypt/account.key --csr /path/to/radicale/letsencrypt/domain.csr --acme-dir /path/to/radicale/acme-challenge/ > /path/to/radicale/letsencrypt/signed_chain.crt.tmp || exit
    >
    > mv -f /path/to/letsencrypt/signed_chain.crt.tmp /path/to/letsencrypt/signed_chain.crt
    >
    > rc-service nginx reload

$ sudo chmod 700 /opt/renew_cert_for_yourhostname.tld.sh

Note that this script won't work if the current certificate is already expired!

$ sudo EDITOR=vi crontab -e # edit the crontab of the root user
    > ...
    > 0 0 1 * * /opt/renew_cert_for_yourhostname.tld.sh 2>> /var/log/acme-tiny-yourhostname.tld.log # run once per month

Use

Import an existing address book

  • export
    • e.g. in android -> Go into your android "Contacts" app -> menu -> settings -> export -> share all contacts -> send the .vcf file to your PC -> from your PC: open yourhostname.tld in a web browser and login -> upload address book or calendar -> select the .vcf file

Import an existing ics calendar

DAVx

"+" add account -> login with URL and user name -> base URL: yourhostname.tld ; username: your-user-name ; password: your-password -> login -> account name: your-davx-account-name -> create account

Go into your android "Contacts" app -> menu -> settings -> accounts -> add account -> DAVx address book -> your-davx-account-name

Go back into your android "Contacts" app -> menu -> settings -> default account for new contacts -> select the new DAVx address book

Troubleshooting

Can't upload an address book: Error: 400 Bad Request

When trying to upload an address book, you might get the following error message: Error: 400 Bad Request. Server side you might get a log like this one:

[2021-01-09 17:42:22 +0100] [30265/Thread-35] [WARNING] Bad PUT request on '/username/9b900a16-b9ff-99f2-3608-64b2a94ab306/': At line 394: Failed to parse line: =38=68=20=C3=A0=20=32=30=68

You can resolve this error by editing the .vcf file you are trying to upload. Search for the NOTE fields, and make sure every NOTE field is on one line (with line wrapping), not on several lines.

E.g. from this:

NOTE;ENCODING=QUOTED-PRINTABLE:=41=64=72=65=73=73=65=20=3A=20=31=33=20=72=75=65=20=52=
=69=63=68=61=72=64=20=4C=65=6E=6F=69=72=
=0A=43=6F=64=65=20=3A=20=37=39=33=31

To this:

NOTE;ENCODING=QUOTED-PRINTABLE:=41=64=72=65=73=73=65=20=3A=20=31=33=20=72=75=65=20=52=69=63=68=61=72=64=20=4C=65=6E=6F=69=72=0A=43=6F=64=65=20=3A=20=37=39=33=31

Note that the first = character of each line is deleted before merging them.


If this cheat sheet has been useful to you, then please consider leaving a star here.