rationale Link to heading

  • hosting a lightweight caldav compatible server
  • podman w/ selinux enabled
  • caddy as a reverse proxy to handle dns/certs/multisite
  • several services on same domain.tld
  • being gnome/davx5 caldav compatible
  • share calendars with other users

prerequesites Link to heading

  • vps/cloud instance
  • rocky/alma/fedora server (other gnu/linux works)
  • non root/sudo/wheel user for podman
  • *domain.tld pointing to host ip

todo Link to heading

  • setup mail/signal/mattermost for reminders
  • sending reminders through cardav contacts
  • setup webdav in caddy for file sharing
  • implement simple protection corazawaf or reaction or anubis or ratelimit
  • having several scenarri of caldav/webdav sharing

setup firewall Link to heading

assuming you already setup your firewall for ssh/other services

sudo firewall-cmd --permanent --add-port=80/tcp --add-port=443/tcp
sudo firewall-cmd --complete-reload

install podman Link to heading

sudo dnf install epel-release -y
sudo dnf install -y podman podman-compose

check this file for search registries

/etc/containers/registries.conf

unqualified-search-registries = ["registry.access.redhat.com", "registry.redhat.io", "docker.io"]

create user without sudo/wheel rights

sudo useradd -m poduser
sudo passwd poduser
sudo loginctl enable-linger poduser

login with poduser through this command otherwise you can have problems

su --login poduser

create directory structure

mkdir -p web/{caldav.domain.tld,logs,radicale}
cd web

dockerbuild Link to heading

as latest version of caddy is not the ‘real’ latest we prefer specifying version

curl -s https://api.github.com/repos/caddyserver/caddy/releases/latest | jq -r .tag_name

2.10.0

caddy need to be build with dns plugins for tls/dns challenge or whatsoever

i use ovh/ionos and caddy-ratelimit

~/web/Dockerfile

ARG CADDY_VERSION=2.10.0
FROM caddy:builder AS builder


RUN xcaddy build \
  --with github.com/caddy-dns/ionos \
  --with github.com/caddy-dns/ovh \
  --with github.com/mholt/caddy-ratelimit

FROM caddy:${CADDY_VERSION}

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

before building caddy check if there’s no other image

podman images

if so you can remove it

podman rmi caddy.vps1

build caddy

podman build --no-cache -f Dockerfile -t caddy.vps1 .

podman compose Link to heading

  • to be docker compatible just take off the Z z occurences.

~/web/podman-compose.yml

---
version: "3.9"
services:
  caddy:
    image: caddy.vps1
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro,Z
      - caddy_data:/data
      - caddy_config:/config
      - ./logs:/srv/data:z
    environment:
      - ACME_AGREE=true
    restart: unless-stopped

  radicale:
    image: tomsquest/docker-radicale
    container_name: radicale
    ports:
      - "0.0.0.0:5232:5232"
    init: true
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - SETUID
      - SETGID
      - CHOWN
      - KILL
    deploy:
      resources:
        limits:
          memory: 256M
          pids: 50
    restart: unless-stopped
    environment:
      - TAKE_FILE_OWNERSHIP=true
      - TZ=Europe/Paris
    volumes:
      - "./calendar.domain.tld:/data:rw,Z"
      - "./radicale:/config:ro,Z"

volumes:
  caddy_data:
  caddy_config:
  calendar.domain.tld:

caddy configuration Link to heading

~/web/Caddyfile

calendar.domain.tld {
    tls {
        dns ovh {
            endpoint https://eu.api.ovh.com/v1
            application_key REDACTED
            application_secret REDACTED
            consumer_key REDACTED
    }
    handle /radicale/* {
        reverse_proxy radicale:5232 {
            header_up X-Script-Name /radicale/
        }
    }
    header {
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "no-referrer"
        Permissions-Policy "geolocation=(), microphone=(), camera=()"
        Cache-Control "no-store"
    }
    log {
        format json
        output file data/calendar.access.log {
            roll_local_time
            roll_keep     12
            roll_keep_for 365d
            }
        }
    }
}

radicale config Link to heading

now images are ready we need to add radicale configuration and users

  • switch logging level to debug if needed

it is quite a simple conf you can play with it

~/web/radicale/config

  • increase max_connections (i need only 2)
  • don’t need to expose web interface
  • htpasswd for internal auth
  • use bcrypt as hashing
  • delay of 300 seconds between failed attempts
  • etc …
[server]
hosts = 0.0.0.0:5232
max_connections = 2
max_content_length = 100000

[encoding]
request = utf-8
stock = utf-8

[auth]
type = htpasswd
htpasswd_filename = /config/users
htpasswd_encryption = bcrypt
delay = 300

[rights]
type = authenticated
permit_delete_collection = True
permit_overwrite_collection = True

[storage]
filesystem_folder = /data/collections

[web]
type = none

[logging]
level = info
mask_passwords = True

[headers]
[hook]
[reporting]

users list Link to heading

create bcrypt password with this command

mkpasswd -m bcrypt

~/multisite/radicale/users

luigi:$2b$05$8CBfV/S6zveENJK8Qq/Jz.ptSPoz1tApyRi9GtOJr66hrmCZo7T2W
brian:$2b$05$8CBfV/S6zveENJK8Qq/Jz.ptSPoz1tApyRi9GtOJr66hrmCZo7T2W

clients Link to heading

official docs pretend all those clients are compatible

  • Android with DAVx⁵ (formerly DAVdroid),
  • OneCalendar
  • GNOME Calendar, Contacts and Evolution
  • Mozilla Thunderbird with CardBook and Lightning
  • InfCloud, CalDavZAP and CardDavMATE

clients i tested

  • davx5 4.4.9 / etar 1.0.48

https://calendar.domain.tld/radicale/luigi

  • gnome calendar 47~rc-1.fc41

https://calendar.domain.tld/radicale/

troubleshooting Link to heading

curl diagnostics

curl -u 'luigi:realpassword' https://calendar.domain.tld/radicale/brian/ -X MOVE -H "Destination: https://calendar.domain.tld/radicale/luigi/
curl -X MKCOL -v -u 'brian:realpassword' https://calendar.domain.tld/brian/calendar/

find selinux problems

chcon -Rt svirt_sandbox_file_t /home/poduser/web/calendar.domain.tld

ressources Link to heading

  • official doc

https://radicale.org/v3.html

  • radicale howto

https://cheatsheets.stephane.plus/self-hosted/radicale/

https://radicale.org/v3.html#authentication-and-rights

https://github.com/Kozea/Radicale/issues/947

https://github.com/Kozea/Radicale/blob/master/config

  • storage

https://github.com/Kozea/Radicale/wiki/Collection-Storage

  • diags

https://github.com/Kozea/Radicale/wiki/Server-Diagnostics---Troubleshooting

  • rights

https://github.com/Kozea/Radicale/blob/master/rights

  • docker compose

https://github.com/tomsquest/docker-radicale/blob/master/docker-compose.yml

  • podman stuff

https://github.com/tomsquest/docker-radicale/issues/122

  • caddy reverse proxy

https://github.com/Kozea/Radicale/blob/master/contrib/caddy/radicale.caddyfile

https://caddy.community/t/radicale-reverse-proxy-caddy-2-0/8580

https://caddy.community/t/make-radicale-work-behind-caddy/3787

https://github.com/caddyserver/examples/blob/master/radicale/Caddyfile

https://github.com/tomsquest/docker-radicale?tab=readme-ov-file#running-behind-caddy

https://github.com/Kozea/Radicale/wiki/Reverse-Proxy-Diagnostics-Troubleshooting

  • security

https://github.com/Kozea/Radicale/wiki/Fail2Ban-Setup

https://github.com/mholt/caddy-ratelimit

  • sharing calendars

https://www.reddit.com/r/selfhosted/comments/raq3pa/how_do_you_share_calendars_in_radicale/