fedora

Home server with Docker containers via linuxserver.io

After a few years of meticulously maintaining a large shell script that setup my Fedora home server, finally got around to containerizing a good portion of it thanks to the fine team at linuxserver.io.

As the software set I tried to maintain grew, there were a few challenges with dependencies and I ended up having to install/compile a few software titles myself, which I generally try to avoid at all costs (since that means I'm on the hook for regularly checking for security updates, addressing compatibility issues with OS upgrades, etc).

After getting my docker-compose file right, it's been wonderful - a simple docker-compose pull updates everything and a small systemd service keeps the docker images running at boot. Mapped volumes mean none of the data is outside my host, and I can also use the host networking mode for images that I want auto-discovery for (e.g. Plex or SMB).

Plus, seeing as I've implemented docker-compose as a systemd service, I am able depend on zfs-keyvault to ensure that any dependent filesystem are mounted and available. Hurray!

You check out a sample config for my setup in this GitHub gist.

Using OpenVPN connection to play games while abroad using Stream's In-Home Streaming

Introduction

Steam has a great (albeit, a little glitchy) feature called In-Home Streaming that allows you to stream games from running Steam clients on your local network, effectively turning your gaming PC into a little render farm and allowing you to play from low-power devices like a laptop.

With the help of OpenVPN, it's possible to enable playback of games from your home PC seamlessly while away from your home network too, provided you have a decent Internet connection. This tutorial will demonstrate how to setup an OpenVPN server on Fedora 23 with a bridged network connection in let you VPN into your home network and stream Steam games from PCs on your LAN.

To make this work, we need to use OpenVPN in bridged mode with a tap network device. Bridging the ethernet and tap interfaces will allow VPN clients to receive an IP address on the LAN's subnet.

The default (and simpler) tun devices are not bridged, and function on a separate subnet - something which will break in-home streaming. We need to be on the same LAN so that the UDP broadcast packets sent by Steam for auto-discovery will be received by the VPN clients.

Creating a network bridge

Let's start by setting up the network bridge with NetworkManager and enslaving the ethernet interface. Check the name of your active network interface by running nmcli d, and replace the value ofETH_IFACE with that name below:

ETH_IFACE=enp3s0
nmcli con add type bridge ifname br0
nmcli c modify bridge-br0 bridge.stp no
nmcli con add type bridge-slave ifname $ETH_IFACE master bridge-br0
nmcli c up "bridge-slave-${ETH_IFACE}"
nmcli c up bridge-br0

Installing the OpenVPN server

Next, in order to run an OpenVPN server, one needs to set up a certificate authority (CA) signs client certificates and authorizes them for login. In our case, we'll be using password authentication (for convenience) -- but OpenVPN still wants a CA setup and the server's certificate signed. Let's set up the CA for the OpenVPN server:

dnf install easy-rsa
cp -a /usr/share/easy-rsa/3 /root/openvpn-bridged-rsa
cd /root/openvpn-bridged-rsa
./easyrsa init-pki
./easyrsa build-ca
# Enter the CA password, then accept the defaults

Now we need to create and sign the certificate for the server (set the value of SERVER_ALIAS to an alias of your choice):

SERVER_ALIAS=homelab
./easyrsa gen-dh
./easyrsa gen-req $SERVER_ALIAS nopass
./easyrsa sign-req server $SERVER_ALIAS
# Enter 'yes', then CA password

Finally, we copy the keys and certificates to a dedicated folder for OpenVPN:

mkdir /etc/openvpn/keys
chmod 700 /etc/openvpn/keys
cp pki/ca.crt pki/dh.pem "pki/issued/${SERVER_ALIAS}.crt" "pki/private/${SERVER_ALIAS}.key" /etc/openvpn/keys

We are now ready to configure OpenVPN. Set the variables based on your LAN's configuration (see ifconfig $ETH_IFACE output if unsure):

BRIDGE_IP=192.168.1.1
NETMASK=255.255.255.0
IP_POOL_START=192.168.1.241
IP_POOL_END=192.168.1.254

dnf install openvpn
firewall-cmd --permanent --add-service openvpn
firewall-cmd --reload

cat << EOF > /etc/openvpn/bridged.conf
port 1194
dev tap0
tls-server
ca /etc/openvpn/keys/ca.crt
cert /etc/openvpn/keys/$SERVER_ALIAS.crt
key /etc/openvpn/keys/$SERVER_ALIAS.key # This file should be kept secret
dh /etc/openvpn/keys/dh.pem
server-bridge $BRIDGE_IP $NETMASK $IP_POOL_START $IP_POOL_END

# Password authentication
client-cert-not-required
username-as-common-name
plugin /usr/lib64/openvpn/plugins/openvpn-plugin-auth-pam.so openvpn

# Allow multiple client connections from the same user
duplicate-cn

# Client should attempt reconnection on link failure.
keepalive 10 120

# The server doesn't need root privileges
user openvpn
group openvpn

# Logging levels & prevent repeated messages
verb 4
mute 20
log-append /var/log/openvpn.log
status /var/log/openvpn-status.log

# Set some other options
comp-lzo
persist-key
persist-tun
push persist-key
push persist-tun

# Brings up tap0 since NetworkManager won't do it automatically (yet?)
script-security 2
up up.sh
EOF

OpenVPN will create the tap0 interface automatically when the OpenVPN server starts. NetworkManager is able to enslave the interface to the bridge, but won't bring tap0 online. For that, we install a simple script:

cat << EOF > /etc/openvpn/up.sh
#!/bin/bash
br=br0
dev=\$1
mtu=\$2
link_mtu=\$3
local_ip=\$4
local_netmask=\$5

# This should be done by NetworkManager... but it can't hurt.
/sbin/brctl addif \$br \$dev

# NetworkManager appears to be capable of enslaving tap0 to the bridge automatically, but won't bring up the interface.
/sbin/ifconfig \$dev 0.0.0.0 promisc up
EOF
chmod +x /etc/openvpn/up.sh

Next, we need to create the PAM authentication configuration file for the OpenVPN password plugin:

cat << EOF > /etc/pam.d/openvpn
#%PAM-1.0
auth       substack     system-auth
auth       include      postlogin
auth       requisite    pam_succeed_if.so user ingroup openvpn_pw quiet
account    required     pam_nologin.so
account    include      system-auth
password   include      system-auth
EOF

This configuration file requires that the users logging in be a member of the openvpn_pw group. You can adjust the file as you see fit.

Finally open the OpenVPN port in the firewall and start the service:

firewall-cmd --permanent --add-service openvpn
firewall-cmd --reload
systemctl enable openvpn@bridged
systemctl start openvpn@bridged

Configure the firewall

By default, packets are filtered through iptables which can cause issues, as packets won't freely through between the interfaces. We can disable that behavior:

cat << EOF > /etc/modules-load.d/bridge.conf
br_netfilter
EOF

cat << EOF > /etc/sysctl.d/bridge.conf
net.bridge.bridge-nf-call-ip6tables=0
net.bridge.bridge-nf-call-iptables=0
net.bridge.bridge-nf-call-arptables=0
EOF
sysctl -p /etc/sysctl.d/bridge.conf

cat << EOF > /etc/udev/rules.d/99-bridge.rules
ACTION=="add", SUBSYSTEM=="module", KERNEL=="br_netfilter", RUN+="/sbin/sysctl -p /etc/sysctl.d/bridge.conf"
EOF

Note that I assume that net.ipv4.ip_forward=1 (having libvirt seems to configure this automatically). If not, you'll want to tune the sysctl parameter net.ipv4.ip_forward to a value of 1.

OpenVPN client configuration

That's it! Send a copy of /etc/openvpn/keys/ca.crt on the server to your clients, and you should now be able to connect to your OpenVPN server using this very simple client configuration (don't forget to replace your.server.fqdn with your server's IP address or FQDN):

client
dev tap
proto udp
remote your.server.fqdn 1194
resolv-retry infinite
nobind
persist-key
persist-tun
ca ca.crt
auth-user-pass
comp-lzo
verb 3
mute 20

Once connected, you should be able to ping any machine on the LAN as well as fire up Stream for a remote gaming session.

Appendix A: Steam on OS X

Small note, if you're running the Steam client on OS X there's a bug where the client only sends its UDP broadcast packets for in-home streaming discovery on the machine's primary (i.e. Ethernet or Wi-FI) interface. This nifty command captures those and re-broadcasts them over the VPN's interface (once again, substitute the value of BROADCAST_ADDR per your LAN settings):

BROADCAST_ADDR=192.168.1.255
sudo tshark -T fields -e data -l 'udp and dst port 27036' | script -q /dev/null xxd -r -p | socat - UDP-DATAGRAM:${BROADCAST_ADDR}:27036,broadcast

Special thanks to Larry Land for pointing that out in his blog post Run your own high-end cloud gaming service on EC2 (which is awesome and deserves a read, by the way).

The above command requires the Wireshark and socat utilities to be installed, which you can grab using homebrew:

brew install wireshark socat

and if you don't know your subnet's broadcast address, verify it with:

ifconfig tap0 | grep broadcast

Appendix B: Troubleshooting tips

Whenever possible, I like to use the most modern tooling available. This tends to bite me because documentation might not be as good or the feature set in the replacement tools might be lacking compared to the older tried and true tooling, but I try to always look forward. 'new' tooling like systemd, NetworkManager and firewalld might be rough around the edges, but I like modern feature set and consistency they bring. Most importantly, using them (instead of dropping various custom shell scripts here and there) feels a lot less like my server is held together with glue, which I like.

While trying different configurations, I discovered a few tricks or debugging commands that proved very useful to me - particularly while migrating commands from online resources intended for the older tooling to the tools mentioned above. Hopefully, you'll find them useful too!

It's always the firewall

The blame for most of the issues you will experience generally fall under firewall or routing issues.

First steps in testing should always be disabling the firewall (systemctl stop firewalld) and if that doesn't fix it, then move to checking the routes (route -n or netstat -rn).
If you've identified the firewall is to blame, re-enable it and identifying the root cause by adjusting your configuration while listening for packets to see when packets start flowing again.

Listening for packets

This will be your most used tool. If things don't go as expected, listen on each of the tap0 (client), tap0 (server) and br0 (server) interfaces and then generate some traffic to see how far the packets:

tcpdump -i IFNAME PATTERN

where PATTERN can select for hosts, ports or traffic type. In this case, a particular favorite of mine was icmp or udp port 27036 as this let me test by trying to ping a machine on the LAN from the VPN client, as well as see if the Steam UDP traffic was making it in/out.

Send UDP traffic

The iperf utility can be used to test if UDP traffic makes it to the OpenVPN server (run iperf -c server.ip -u -T 32 -t 3 -i 1 -p 27036) from the OpenVPN clients/LAN machines (run iperf -s -u -i 1 -p 27036).

Changing the zone of an interface

firewalld has different 'zones', each with different rules (see firewall-cmd --list-all-zones). Interfaces will have the rules from the default zone applied to them unless otherwise configured, which you can do as follows:

firewall-cmd --permanent --change-interface=IFNAME --zone=NEW_ZONE
firewall-cmd --reload

Adding raw IPTables rules to firewalld

e.g. to accept all packet forwarding on the bridge interface:

firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -i br0 -j ACCEPT

or to allow all packets flowing over br0 and tap0:

firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 -i tap0 -j ACCEPT
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 -i br0 -j ACCEPT

Neither of these commands should be necessary given the netfilter sysctl parameters tweaked earlier, but certain OpenVPN options (such as client2client) change the packet flow and cause packets to flow over the interface, get filters, then re-injected which could cause them to suddenly be affected by the iptables rules.

Recall that firewall-cmd --reload needs to be called before the permanent rules will take effect.

Debugging IPTables

The default iptables -L listing isn't very helpful when inserting/deleting rules by their chain offset. The following command lists all rules, numerically, and displays line numbers:

iptables --line-numbers -L -n -v

You can also log dropped packets for further troubleshooting (here limited to 1/s, inserted at position 15 which was the position before the DROP rules on my machine):

iptables -I INPUT 15 -j LOG -m limit --limit 60/min --log-prefix "iptables dropped: " --log-level 4

Configuring a client VPN connection using only NetworkManager

VPN_IFNAME=homelab
nmcli c add type vpn ifname $VPN_IFNAME vpn-type openvpn
nmcli c modify $VPN_IFNAME
set vpn.data dev = tap
set vpn.data ca = /path/to/copy/of/ca.crt
set vpn.data connection-type = password
set vpn.data remote = your.server.fqdn
set vpn.data comp-lzo = yes
set vpn.data username = your_user
set vpn.data connection-type = password
set vpn.secrets password = your_pw

'waiting for password' when connecting using Tunnelblock

When I was attempting to test my VPN connections using Tunnelblick on OS X, I experienced an annoying bug: When trying to connect, Tunnelblick would enter a 'Waiting for password' state, but never 'get' the password nor prompt for one. Log were misleading:

Tunnelblick: Obtained VPN username and password from the Keychain

No VPN password was stored in my keychain (verified using Keychain Access.app). Fortunately, this post on the Sophos community forums correctly identified the issue as a bug after having copied/renamed a Tunnelblick connection.

Tunnelblick's preference file needs to be adjusted in order to correctly prompt for a password again:

# from https://community.sophos.com/products/xg-firewall/f/124/t/75819
conname="homelab"
defaults delete net.tunnelblick.tunnelblick "${conname}-keychainHasPrivateKey"
defaults delete net.tunnelblick.tunnelblick "${conname}-keychainHasUsername"
defaults delete net.tunnelblick.tunnelblick "${conname}-keychainHasUsernameAndPassword"

Additional reading

Building a home media server with ZFS and a gaming virtual machine

Work has kept me busy lately so it's been a while since my last post... I have been doing lots of research and collecting lots of information over the holiday break and I'm happy to say that in the coming days I will be posting a new server setup guide, this time for a server that is capable of running redundant storage (ZFS RAIDZ2), sharing home media (Plex Media Server, SMB, AFP) as well as a full Windows 7 gaming rig simultaneously!

Windows runs in a virtual machine and is assigned it's own real graphics card from the host's hardware using the using the brand-new VFIO PCI passthrough technique with the VGA quirks enabled. This does require a motherboard and CPU with support for IOMMU, more commonly known as VT-d or AMD-Vi.

How to Convert a GPT disk layout to a MS-DOS/MBR layout without data loss (and Gigabyte Hybrid EFI)

If you're coming here from Google searching for how to convert a GPT disk layout to MS-DOS/MBR and don't want to read through my (probably boring) story, click here ;)

Adventures with Hybrid EFI

My gaming PC has been long overdue due for a reformat. I naively allocated only 30GB to the Windows partition (and the other 120GB to 3 flavours of Linux) thinking I wouldn't use Windows for much other than Starcraft 2, but a few months back I had the urge to play Battlefield 2 again. Ever since installing and fully patching it disk space has been running pretty tight. I had to disable sleep, hibernation as well as system restore and still only had 4GB of free space, so my filesystem became fragmented easily. With the release of Windows 8 Customer Preview (download it free here), I figured it was a good time to reformat my disk and reinstall all my OSs from scratch.

I figured while I'm at it, I would make all of the big changes at once and enabled EFI booting on my Gigabyte GA-Z68A-D3H board. Little did I know that when the BIOS says "EFI," it really means Gigabyte's "Hybrid EFI" implementation and not UEFI (although in retrospect, the fact that I made the change in the BIOS should have been enough of a hint, right?). With Hybrid EFI enabled, Windows 7 and Windows 8CP installed perfectly and even created a nice GPT disk layout so reinstalled my games and activated Windows 7. Then I rebooted to play around in Windows 8CP for a bit (I do not like it, btw).

I then tried installing Fedora 16. To my surprise EFI booting failed every time, despite the all of the Fedora 16 installation media being EFI-capable. When attempting to boot from my Fedora 16 Live (x86_64) USB key I just would get a black screen with "........." printed one dot a time and then it would proceed to fall back to the next boot device (Windows boot manager on the hard disk). Upon re-examining my BIOS settings, I was disappointed to find that the setting was actually called "CD/DVD EFI Boot Option" indicating that perhaps USB EFI booting was not supported. Fair enough, I burnt the same F16 image I was using on the USB key to a CD and tried again. The same "........." text appeared.

It was then as I went back to boot Windows 7 that I discovered my attempts to set it as the default OS from Windows 8CP removed my capability of booting Windows 7 somehow. At this point it was 2AM and I was fed up with this stupid Hybrid EFI. I looked for a way to revert to a good old MS-DOS/MBR partition layout. After some Googling I stumbled across Rod Smith's website. He has extensive documentation on EFI booting, including with Gigabyte's implementation of Hybrid EFI. He says that it shares a large amount of code with EFI DUET (tianocore) and although it does work natively with Windows 7, it is not a full UEFI implementation. That would explain the problems I was having with Fedora, then.

The actual GPT to MBR conversion

Through the Rod Smith's guidance and a few dirty tricks, I was successfully able to convert my GPT partition - without data loss or deleting any partitions - and then boot Windows 7 in legacy/MBR mode. In order to do this you'll need your Windows installation media at hand as well as a copy of the Fedora 16 Live media. If you don't have a copy of Fedora 16 Live handy, you can download the Live media ISO (64-bit) from a local mirror here. See the Fedora 16 Installation Guide for details on burning this image to a CD or on creating a bootable USB key.

Keep in mind that at this point I only had 3 partitions and a bunch of unpartitioned space on the disk, so conversion was a rather straightforward process (all GPT partitions mapped directly to primary partitions). Although it is theoretically possible to convert GPT partitions with >4 partitions by defining which ones are to be logical partitions after conversion, I have not tested this.

  1. Boot your Fedora 16 Live media and wait for your session to start. If you're having troubles booting, press Tab at the boot loader screen and try booting with the nomodeset parameter added.
  2. Depending on your graphics card, you'll either be presented with the new Gnome 3 Shell or with the traditional interface. Start a terminal session by putting your mouse in the top right corner of the screen and typing "terminal" in the search (Gnome Shell) or by selecting Applications > System Tools > Terminal (traditional interface)
  3. Install gdisk:
    su -
    yum -y install gdisk

    This may take a few moments.

  4. Make a backup of your current GPT scheme:
    gdisk -b sda-preconvert.gpt /dev/sda
  5. Now we will attempt to convert your GPT disk layout to MS-DOS/MBR. Start gdisk:
    gdisk /dev/sda

    You should be prompted with:

    Command (? for help):
  6. Press r to start recovery/transformation.
  7. Press g to convert GPT to MBR.
  8. Press p to preview the converted MBR partition table.
  9. Make any modification necessary to the partition layout. See Rod Smith's Converting to or from GPT page for more details on this.
  10. When you're happy with the MS-DOS/MBR layout, press w to write changes to the disk.
  11. Shutdown Fedora 16 and boot from the Windows 7 installation media
  12. Enter your language & keyboard layout and then select the option to repair your computer in the bottom left corner.
  13. From the available options, select Startup Repair. Windows will ask for a reboot.
  14. Follow the previous three steps again to boot the Windows 7 installation and run startup repair
  15. Once again, boot the Windows 7 installation media but this time opt to open a command prompt instead of choosing startup repair. Type:
    bootrec /scanos
    bootrec /rebuildbcd
    bootrec /fixmbr
    bootrec /fixboot
  16. Close the command prompt and run Startup Repair one last time.

That's it! You should now have a bootable installation of Windows 7 on a MBR partition layout.

References

Installing the Darwin Calendar Server 2.4 on Fedora 13 or Fedora 14

As I mentioned in my last post, I've been playing with the Darwin Calendar Server (DCS) on Linux... Today I was able to re-test my setup notes to see if they worked properly, so below I've written a tutorial on how to get your own DCS server going on Fedora 13 or 14.

Installing Dependencies

Since we will be installing CalendarServer directly from the 2.4 branch subversion repository, the first thing to do is to install subversion and the dependencies for DCS:

su -
# Required to check out the source code from the repository
yum install subversion
# Dependencies
yum install patch memcached krb5-devel python-zope-interface PyXML pyOpenSSL python-kerberos
# Requirements for compiling xattr
yum install python-setuptools gcc gcc-c++ python-devel

Enable extended file attributes (xattrs)

DCS requires user extended file attributes so the user_xattr mount option must be enabled for the partition on which CalendarServer will be storing its documents and data (in this case, /srv). If you have not already enabled this option (it is disabled by default), edit /etc/fstab and add the user_xattr mount option after defaults, for example:

/dev/mapper/VolGroup-lv_root /                       ext4    defaults,user_xattr        1 1

Grab DCS from SVN and run auto-setup

Once these packages have been installed and extended file attributes have been enabled, we will begin setting up the CalendarServer as your regular, non-root user.

# Directory to hold CalendarServer checkout and its dependencies
mkdir CalendarServer
cd CalendarServer
# Checkout the code from the repo
svn checkout http://svn.calendarserver.org/repository/calendarserver/CalendarServer/tags/release/CalendarServer-2.4 CalendarServer-2.4
cd CalendarServer-2.4
# Start auto-setup
./run -s

Auto-setup will now attempt to grab any missing dependencies for CalendarServer an will unpack and patch them accordingly. You may find that the download for PyDirector stalls - if so, hit to abort setup and download it manually:

pushd ..
wget http://downloads.sourceforge.net/pythondirector/pydirector-1.0.0.tar.gz
tar xfz pydirector-1.0.0.tar.gz
popd
# Resume unpacking
./run -s

Prepare for installation

Since DCS bundles a modified version of Twisted as well as a few other projects (such as pydirector), we will now prepare an installation root folder to avoid conflicts with system libraries (i.e., Twisted if it has been installed from the Fedora repos). This code will be run as root.

su -
# setup data & document roots
mkdir -p /srv/CalendarServer/{Data,Documents}
chown -R daemon:daemon /srv/CalendarServer/
# setup installation root
mkdir -p /opt/CalendarServer/etc/caldavd
mkdir -p /opt/CalendarServer/var/run/caldavd
mkdir -p /opt/CalendarServer/var/log/caldavd

Install DCS and configure the server instance

The last step is to install DCS from the Subversion checkout we made earlier into the installation root. Replace /home/regularuser with the actual path to the home directory of your regular user.

# install DCS to installation root
cd /home/regularuser/CalendarServer/CalendarServer-2.4
./run -i /opt/CalendarServer
rm -rf /opt/CalendarServer/usr/caldavd/caldavd.plist
# copy sample configuration files
cp conf/servertoserver-test.xml /opt/CalendarServer/etc/caldavd/servertoserver.xml
cp conf/auth/accounts.xml /opt/CalendarServer/etc/caldavd/accounts.xml
cp conf/caldavd-test.plist /opt/CalendarServer/etc/caldavd/caldavd.plist
cp conf/sudoers.plist /opt/CalendarServer/etc/caldavd/sudoers.plist
# change permissions; passwords are stored plaintext!
chmod 600 /opt/CalendarServer/etc/caldavd/*

I have reported bugs #390 and #391 about problems with the setup script on 64-bit machines as well as a problem if a custom destination installation directory is used (which we did). This bit of code works around both of the bugs:

# 64-bit fix - see https://trac.calendarserver.org/ticket/391
sitelib="$(python -c 'from distutils.sysconfig import get_python_lib; print(get_python_lib())')"
sitearch="$(python -c 'from distutils.sysconfig import get_python_lib; print(get_python_lib(1))')"
if [ "$sitelib" != "$sitearch" ];then
  mv /opt/CalendarServer"${sitelib}"/twisted/plugins/caldav.py* /opt/CalendarServer"${sitearch}"/twisted/plugins
  # PYTHONPATH fix for 64-bit - see https://trac.calendarserver.org/ticket/390
  sed -i.orig 's|PYTHONPATH="'"${sitelib}"'|DESTDIR=/opt/CalendarServer\nPYTHONPATH="${DESTDIR}'"${sitelib}"':${DESTDIR}'"${sitearch}"':|' /opt/CalendarServer/usr/bin/caldavd
else
  # PYTHONPATH fix for 32-bit - see https://trac.calendarserver.org/ticket/390
  sed -i.orig 's|PYTHONPATH="'"${sitelib}"'|DESTDIR=/opt/CalendarServer\nPYTHONPATH="${DESTDIR}'"${sitelib}"':|' /opt/CalendarServer/usr/bin/caldavd
fi

If you would like your server to use SSL (highly recommended), you will need to generate a certificate. If you have a certificate and key ready to install, place it in /opt/CalendarServer/etc/tls. If not, you can easily generate a free self-signed one:

# Generate SSL keys
mkdir /opt/CalendarServer/etc/tls
openssl req -new -newkey rsa:1024 -days 365 -nodes -x509 -keyout www.example.com.key -out www.example.com.crt

Now, edit /opt/CalendarServer/etc/caldavd/caldavd.plist in your favorite editor and configure the server as follows:

    <!-- Network host name [empty = system host name] -->
    <key>ServerHostName</key>
    <string>example.com</string> <!-- The hostname clients use when connecting -->

# Data roots
    <!-- Data root -->
    <key>DataRoot</key>
    <string>/srv/CalendarServer/Data/</string>
        
    <!-- Document root -->
    <key>DocumentRoot</key>
    <string>/srv/CalendarServer/Documents/</string>

# Test accounts configuration
    <!-- XML File Directory Service -->
    <key>DirectoryService</key>
    <dict>
      <key>type</key>
      <string>twistedcaldav.directory.xmlfile.XMLDirectoryService</string>
         
      <key>params</key>
      <dict>
        <key>xmlFile</key>
        <string>/opt/CalendarServer/etc/caldavd/accounts.xml</string>
      </dict>
    </dict>

# Sudoers configuration
    <!-- Principals that can pose as other principals -->
    <key>SudoersFile</key>
    <string>/opt/CalendarServer/etc/caldavd/sudoers.plist</string>

# Delete this section
<!-- Wikiserver authentication (Mac OS X) -->
      <key>Wiki</key>
      <dict>
        <key>Enabled</key>
        <true/>
        <key>Cookie</key>
        <string>sessionID</string>
        <key>URL</key>
        <string>http://127.0.0.1/RPC2</string>
        <key>UserMethod</key>
        <string>userForSession</string>
        <key>WikiMethod</key>
        <string>accessLevelForUserWikiCalendar</string>
      </dict>

# logging
    <!--
        Logging
      -->

    <!-- Apache-style access log -->
    <key>AccessLogFile</key>
    <string>/opt/CalendarServer/var/log/caldavd/access.log</string>
    <key>RotateAccessLog</key>
    <false/>

    <!-- Server activity log -->
    <key>ErrorLogFile</key>
    <string>/opt/CalendarServer/var/log/caldavd/error.log</string>

    <!-- Log levels -->
    <key>DefaultLogLevel</key>
    <string>info</string> <!-- debug, info, warn, error -->
# a bit further down…
    <!-- Global server stats -->
    <key>GlobalStatsSocket</key>
    <string>/opt/CalendarServer/var/run/caldavd/caldavd-stats.sock</string>
# <snip>
    <!-- Server statistics file -->
    <key>ServerStatsFile</key>
    <string>/opt/CalendarServer/var/log/caldavd/stats.plist</string>
       
    <!-- Server process ID file -->
    <key>PIDFile</key>
    <string>/opt/CalendarServer/var/run/caldavd/caldavd.pid</string>

# SSL 
    <!-- Public key -->
    <key>SSLCertificate</key>
    <string>/opt/CalendarServer/etc/tls/www.example.com.crt</string>
           
    <!-- Private key -->
    <key>SSLPrivateKey</key> 
    <string>/opt/CalendarServer/etc/tls/www.example.com.key</string>

# Privilege drop
    <!--
        Process management
      -->
   
    <key>UserName</key>
    <string>daemon</string>
   
    <key>GroupName</key>
    <string>daemon</string>
       
    <key>ProcessType</key>
    <string>Combined</string>

# iSchedule server-to-server settings
      <!-- iSchedule protocol options -->
      <key>iSchedule</key>
      <dict>
        <key>Enabled</key>
        <false/>
        <key>AddressPatterns</key>
        <array>
        </array>
        <key>Servers</key>
        <string>/opt/CalendarServer/etc/caldavd/servertoserver.xml</string>
      </dict>

# Communication socket
    <!-- A unix socket used for communication between the child and master processes.
         An empty value tells the server to use a tcp socket instead. -->
    <key>ControlSocket</key>
    <string>/opt/CalendarServer/var/run/caldavd/caldavd.sock</string>

# Twisted
    <!--
        Twisted
      -->
        
    <key>Twisted</key>
    <dict>
      <key>twistd</key>
      <string>/opt/CalendarServer/usr/bin/twistd</string>
    </dict>

# Load balancer
    <!--
        Python Director
      -->

    <key>PythonDirector</key>
    <dict>
      <key>pydir</key>
      <string>/opt/CalendarServer/usr/bin/pydir.py</string>

      <key>ConfigFile</key>
      <string>/opt/CalendarServer/etc/pydir.xml</string>

      <key>ControlSocket</key>
      <string>/opt/CalendarServer/var/run/caldavd/caldavd-pydir.sock</string>
    </dict>

...Profit!

Try starting the server!

/opt/CalendarServer/usr/bin/caldavd -T /opt/CalendarServer/usr/bin/twistd -f /opt/CalendarServer/etc/caldavd/caldavd.plist -X

If all goes well, press to kill the process and then daemonize it:

/opt/CalendarServer/usr/bin/caldavd -T /opt/CalendarServer/usr/bin/twistd -f /opt/CalendarServer/etc/caldavd/caldavd.plist