Baikal 0.11
Overview
Baikal is a lightweight, self-hosted CalDAV and CardDAV server built on the sabre/dav library. It syncs calendars, tasks and contacts across phones, laptops and mail clients using the open standards they already speak (iOS and macOS Calendar and Contacts, Android with DAVx5, Thunderbird, Evolution and others). It also gives you a small web admin console for users, calendars and address books. Everything is stored in SQLite, so there is no database server to run.
This guide covers launching and using the SecureAnyCloud Baikal 0.11 on Ubuntu 24.04 AMI on Amazon EC2. It is one version of the SecureAnyCloud office suites and workspaces listing on AWS Marketplace. You pick it as the software version when you subscribe and launch.
What you get:
- HTTPS from the first boot. nginx serves Baikal on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS, and requests for a host name the instance does not serve are dropped.
- A password unique to your instance. The web
adminand the CalDAV/CardDAV account admin share one password, generated on first boot and written to/root/baikal-credentials.txt(root only). The image ships with random password hashes that match no password, and nginx and PHP-FPM refuse to start until first boot has finished. - No setup wizard. Upstream Baikal's
/admin/install/database step answers without a login until the installer is disabled. On this image, the configuration is written in advance, the installer is disabled (Specific/INSTALL_DISABLED), and nginx returns 404 for/admin/install/. - An upstream security fix, backported. Baikal 0.12.0 and earlier let any signed-in CalDAV user take over the admin console by renaming a calendar to HTML (advisory GHSA-j44x-cj7p-vx2w, fixed in Baikal 0.12.1). The same escaping is applied to this 0.11.1 build. The build proves it by renaming a calendar to an HTML payload and checking that the admin pages show it as text.
- Hardened defaults. nginx and PHP-FPM are blocked from the instance metadata service, and the AMI requires IMDSv2. PHP is handed the configured host name and the real client address from the connection, never values a client sends. Cross-site form posts are refused.
- A dedicated data volume.
/var/www/baikal(code, configuration and the SQLite database) is its own 20 GiB EBS volume that you can grow or snapshot on its own. - A fully patched base. Ubuntu 24.04 LTS is fully patched at build time, with unattended security upgrades switched on.
What is on the image: Baikal 0.11.1 (the official release zip, checksum verified, sabre/dav 4.7.0), nginx, PHP 8.3 FPM with OPcache, SQLite, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu. Architecture: x86_64. Recommended instance type: m5.large.
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair, and a VPC subnet with a route to the internet (or a private route you use for management)
- A security group that allows TCP 22 (SSH) from your admin network, and TCP 443 (Baikal) and TCP 80 (redirects to HTTPS) from the networks your calendar and contact clients use
Launch the instance
In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick Baikal 0.11 on Ubuntu 24.04 as the software version, choose your Region, then choose Launch through EC2.
Choose m5.large, your key pair and a security group as described above. Baikal itself is light, so a smaller size is fine for a handful of users. Keep the second EBS volume, because it holds Baikal and its data.
To launch with the AWS CLI instead, replace the values in angle brackets:
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=baikal}]'First boot takes well under a minute after the instance is running.
Connect to your instance
Version SSH user
Baikal 0.11 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
First boot generates the certificate and the password, and then nginx and PHP-FPM start:
systemctl is-active nginx php8.3-fpm baikal-firstboot
sudo test -f /var/lib/sac/data/baikal-firstboot.done && echo FIRSTBOOT_DONEAll three units are active and the first-boot marker is present:
active active active FIRSTBOOT_DONEOnly SSH and nginx listen on network ports. PHP-FPM uses a local socket:
sudo ss -lntp | awk 'NR>1 {print $4, $6}' | grep -v '127.0.0.5' | sort0.0.0.0:22 users:(("sshd",...)) 0.0.0.0:443 users:(("nginx",...)) 0.0.0.0:80 users:(("nginx",...)) [::]:22 users:(("sshd",...)) [::]:443 users:(("nginx",...)) [::]:80 users:(("nginx",...))Baikal and its SQLite database live on the dedicated data volume:
df -h /var/www/baikal | tail -1/dev/nvme1n1 20G 9.9M 19G 1% /var/www/baikalGet your URLs and password
Read the credentials file as root:
sudo cat /root/baikal-credentials.txtThe file is readable by root only:
# Baikal 0.11 - generated on first boot by baikal-firstboot.service
# These credentials are unique to this instance; none was baked into the image. Keep this file secret.
# The same password signs in to the web admin and to CalDAV/CardDAV as user admin. baikal.admin.url=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/admin/ baikal.dav.url=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/dav.php/ baikal.admin.user=admin baikal.admin.pass=<BAIKAL_ADMIN_PASSWORD>
# The certificate is self signed (minted on this instance's first boot); port 80 redirects to https. # Change the name Baikal is served as:
sudo baikal-set-hostname <your-dns-name>
# User guide: https://secureanycloud.com/stacks/baikal-0-11/The URLs use the instance's public DNS name from EC2 instance metadata. The certificate also names the public and private IP addresses, so https://<public-ip>/admin/ works too.
To serve Baikal under your own DNS name, see Step 9.
Sign in to the web admin
Open the baikal.admin.url in your browser. The certificate is self-signed, so your browser asks you to confirm it the first time. You can install your own certificate instead (Step 9).
Sign in as admin with the password from Step 4.
The dashboard shows the Baikal version, which services are on (web admin, CalDAV, CardDAV), and counts of users, calendars, events, address books and contacts.
Users, calendars and address books
Open Users and resources. The image ships with one account, admin, with a default calendar and a default address book.
Choose + Add user to create an account for each person who will sync. Each user signs in to CalDAV and CardDAV with the user name and password you set here.
The Calendars and Address Books links on each user row manage that user's collections.
System Settings controls CalDAV and CardDAV, the time zone, the invite sender address, the authentication type and the web admin password.
A CalDAV and CardDAV round trip from the command line
The blocks below sign in with the instance's password over HTTPS, list the admin calendars, create an event and a contact, read them back, delete them, and show that a wrong password is refused.
The password is handed to curl on standard input, so it never appears in a process list or your shell history.
sudo bash -c ' set -e F=/root/baikal-credentials.txt P=$(grep "^baikal.admin.pass=" $F | cut -d= -f2-) H=$(grep "^baikal.dav.url=" $F | cut -d= -f2- | sed -E "s#https://([^/]+)/.*#\1#") D="https://$H/dav.php"; R="--resolve $H:443:127.0.0.1" dav() { printf "user = \"%s\"\n" "$1" | curl -K - --digest -sk $R "${@:2}"; } want() { printf "%-16s %s\n" "$1" "$2"; [ "$2" = "$3" ] || { echo "FAIL: expected $3"; exit 1; }; } has() { case "$2" in *"$3"*) printf "%-16s %s\n" "$1" "$3";; *) echo "FAIL: $1"; exit 1;; esac; } printf "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//sac//guide//EN\r\nBEGIN:VEVENT\r\nUID:guide-event\r\nDTSTAMP:20261019T120000Z\r\nDTSTART:20261020T090000Z\r\nDTEND:20261020T100000Z\r\nSUMMARY:Guide test event\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n" > /root/guide-event.ics printf "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:guide-card\r\nFN:Guide Test Contact\r\nEND:VCARD\r\n" > /root/guide-card.vcf want "no login:" "HTTP $(curl -sk $R -o /dev/null -w %{http_code} -X PROPFIND -H "Depth: 0" $D/)" "HTTP 401" want "wrong password:" "HTTP $(dav "admin:not-the-password" -o /dev/null -w %{http_code} -X PROPFIND -H "Depth: 0" $D/)" "HTTP 401" has "calendar home:" "$(dav "admin:$P" -X PROPFIND -H "Depth: 1" $D/calendars/admin/)" "/dav.php/calendars/admin/default/" want "PUT event:" "HTTP $(dav "admin:$P" -X PUT -H "Content-Type: text/calendar" --data-binary @/root/guide-event.ics -o /dev/null -w %{http_code} $D/calendars/admin/default/guide-event.ics)" "HTTP 201" has "GET event:" "$(dav "admin:$P" $D/calendars/admin/default/guide-event.ics)" "SUMMARY:Guide test event" want "PUT contact:" "HTTP $(dav "admin:$P" -X PUT -H "Content-Type: text/vcard" --data-binary @/root/guide-card.vcf -o /dev/null -w %{http_code} $D/addressbooks/admin/default/guide-card.vcf)" "HTTP 201" has "GET contact:" "$(dav "admin:$P" $D/addressbooks/admin/default/guide-card.vcf)" "FN:Guide Test Contact" want "DELETE event:" "HTTP $(dav "admin:$P" -X DELETE -o /dev/null -w %{http_code} $D/calendars/admin/default/guide-event.ics)" "HTTP 204" want "DELETE contact:" "HTTP $(dav "admin:$P" -X DELETE -o /dev/null -w %{http_code} $D/addressbooks/admin/default/guide-card.vcf)" "HTTP 204" rm -f /root/guide-event.ics /root/guide-card.vcf 'no login: HTTP 401 wrong password: HTTP 401 calendar home: /dav.php/calendars/admin/default/ PUT event: HTTP 201 GET event: SUMMARY:Guide test event PUT contact: HTTP 201 GET contact: FN:Guide Test Contact DELETE event: HTTP 204 DELETE contact: HTTP 204Connect your calendar and contacts apps
Use the baikal.dav.url from Step 4: enter it as the server address, with the Baikal user name and password:
Client Where to add it Server address
iOS / iPadOS Settings, Calendar (or Contacts), Accounts, Add Account, Other, Add CalDAV (or CardDAV) Account <your-domain> or the public DNS name
macOS Calendar (or Contacts), Settings, Accounts, +, Other CalDAV (CardDAV) Account, Manual https://<your-domain>/dav.php/
Android DAVx5, Login with URL and user name https://<your-domain>/dav.php/
Thunderbird New Calendar, On the Network https://<your-domain>/dav.php/
Clients that discover the server automatically use the well-known addresses, which redirect to the DAV endpoint:
H=$(sudo grep '^baikal.dav.url=' /root/baikal-credentials.txt | cut -d= -f2- | sed -E 's#https://([^/]+)/.*#\1#')
curl -sk -o /dev/null -w '.well-known/caldav -> %{http_code} %{redirect_url}\n' --resolve "$H:443:127.0.0.1" "https://$H/.well-known/caldav"
curl -sk -o /dev/null -w '.well-known/carddav -> %{http_code} %{redirect_url}\n' --resolve "$H:443:127.0.0.1" "https://$H/.well-known/carddav"
curl -s -o /dev/null -w 'http:// -> %{http_code} %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/admin/" [ "$(curl -sk -o /dev/null -w '%{redirect_url}' --resolve "$H:443:127.0.0.1" "https://$H/.well-known/caldav")" = "https://$H/dav.php/" ].well-known/caldav -> 301 https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/dav.php/ .well-known/carddav -> 301 https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/dav.php/ http:// -> 301 https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/admin/Most phones and desktop clients refuse a self-signed certificate for sync, or ask you to trust it once. For everyday use, give the instance a DNS name and a certificate your devices trust (Step 9).
Your own DNS name and certificate
Point a DNS record at the instance. An Elastic IP keeps the address stable.
Then set the name that Baikal is served as. The command below mints a new self-signed certificate for the name, updates nginx and the URLs in /root/baikal-credentials.txt, and keeps the name across reboots:
sudo baikal-set-hostname <your-domain>To use a certificate from a public or internal certificate authority instead, copy it over the generated pair and reload nginx. An example is a certificate issued by Let's Encrypt for <your-domain>. Running baikal-set-hostname again replaces it with a self-signed one.
sudo install -m 0644 <your-domain>.crt /etc/ssl/baikal/baikal.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/baikal/baikal.key
sudo nginx -t && sudo systemctl reload nginxSecurity checks
The setup wizard cannot be reached, and the backported fix is recorded on the image:
H=$(sudo grep '^baikal.admin.url=' /root/baikal-credentials.txt | cut -d= -f2- | sed -E 's#https://([^/]+)/.*#\1#')
curl -sk -o /dev/null -w '/admin/install/ -> HTTP %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/admin/install/" ls /var/www/baikal/Specific/INSTALL_DISABLED grep -E 'BAIKAL_VERSION"' /var/www/baikal/Core/Distrib.php cat /var/www/baikal/SAC-SECURITY-BACKPORTS.txt [ "$(curl -sk -o /dev/null -w '%{http_code}' --resolve "$H:443:127.0.0.1" "https://$H/admin/install/")" = "404" ]/admin/install/ -> HTTP 404 /var/www/baikal/Specific/INSTALL_DISABLED define("BAIKAL_VERSION", "0.11.1"); sac: Baikal 0.11.1 with the GHSA-j44x-cj7p-vx2w admin XSS fix (Baikal 0.12.1, sabre-io/Baikal#1430) backported.The certificate was generated on this instance and names its own addresses:
H=$(sudo grep '^baikal.admin.url=' /root/baikal-credentials.txt | cut -d= -f2- | sed -E 's#https://([^/]+)/.*#\1#')
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltNamesubject=CN = ec2-<public-ip-dashed>.compute-1.amazonaws.com, O = sac X509v3 Subject Alternative Name: DNS:ec2-<public-ip-dashed>.compute-1.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhostNeither nginx nor PHP-FPM can reach the instance metadata service:
systemctl show nginx php8.3-fpm -p Id -p IPAddressDenyIPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 Id=nginx.service IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 Id=php8.3-fpm.servicenginx hands PHP the configured host name and the client address from the connection, never values a client sends:
grep -E 'fastcgi_param (HTTP_HOST|HTTP_X_FORWARDED_FOR|HTTP_X_REAL_IP|HTTP_CLIENT_IP)' /etc/nginx/sites-available/sac-baikalfastcgi_param HTTP_HOST $baikal_host; fastcgi_param HTTP_X_FORWARDED_FOR $remote_addr; fastcgi_param HTTP_X_REAL_IP $remote_addr; fastcgi_param HTTP_CLIENT_IP $remote_addr;nginx also refuses a form post or a DAV change whose Origin is another website (HTTP 403). CalDAV and CardDAV apps send no Origin, so they are unaffected.
Changing passwords
Baikal keeps two passwords, and first boot set both to the same value:
- Web admin password: open System Settings, enter a new Admin password and its confirmation, then choose Save changes.
- CalDAV/CardDAV password of the
adminaccount: open Users and resources, choose Edit on theadminrow, then enter a new password.
After you change them, update or delete baikal.admin.pass in /root/baikal-credentials.txt so that it does not hold an old password. Give each person their own Baikal user instead of sharing admin.
Backup and maintenance
All Baikal state is under /var/www/baikal on the data volume: the configuration in config/baikal.yaml and the database in Specific/db/db.sqlite. The simplest backup is an EBS snapshot of that volume.
For a file-level copy that is safe while Baikal is running, use SQLite's online backup:
sudo sqlite3 /var/www/baikal/Specific/db/db.sqlite ".backup '<backup-dir>/baikal-$(date +%F).sqlite'"
sudo cp /var/www/baikal/config/baikal.yaml <backup-dir>/Operating system security updates install automatically. Run sudo apt-get update && sudo apt-get upgrade to apply everything else. nginx and PHP-FPM restart cleanly, and Baikal keeps all of its data.
Component Path
Baikal /var/www/baikal (data volume)
Configuration /var/www/baikal/config/baikal.yaml
Database /var/www/baikal/Specific/db/db.sqlite
nginx site /etc/nginx/sites-available/sac-baikal
Certificate and key /etc/ssl/baikal/
First boot script /usr/local/sbin/baikal-firstboot.sh
Host name helper /usr/local/sbin/baikal-set-hostname
Credentials /root/baikal-credentials.txt (mode 0600)
Security recommendations
- Allow port 22 from your admin network only, and ports 80 and 443 only from the networks your devices sync from.
- Use your own DNS name and a certificate your devices trust (Step 9).
- Change the passwords if they may have been shared, and give each person their own Baikal user (Step 11).
- Keep the operating system patched, and snapshot the data volume regularly.
Support and licensing
Baikal is open-source software under the GNU General Public License version 3. There is no per-user or per-CPU licence fee. The AMI price covers SecureAnyCloud's hardening, first-boot setup and support.
- Email: sales@secureanycloud.com
- Website: https://secureanycloud.com
- Support: 24/7, with a 24 hour response SLA
Deploy on AWS
Launch Baikal 0.11 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com