OPENHAPD(8)

NAME
openhapd - HomeKit Accessory Protocol daemon

SYNOPSIS
openhapd [-fnv] [-c file]

DESCRIPTION
The openhapd daemon uses the HomeKit Accessory Protocol (HAP) so that Apple HomeKit can control MQTT-connected Tasmota devices on OpenBSD.

openhapd is a bridge between HomeKit controllers, for example, the iOS Home app, and devices that MQTT controls. It changes HAP requests into MQTT commands. It sends changes of the device state back to HomeKit.

Pairing and transport security obey the HAP specification:
•   Pair-setup with SRP-6a (Secure Remote Password)
•   Pair-verify with Curve25519 (X25519) key exchange
•   Ed25519 signatures for accessory and controller identity
•   HKDF-SHA-512 key derivation
•   Session encryption with ChaCha20-Poly1305 AEAD
•   Service advertisement over mDNS

openhapd advertises the service over mDNS. To do this, it connects directly to the control socket of mdnsd(8), /var/run/mdnsd.sock. openhapd keeps the socket open for its full lifetime and spawns no helper process. When openhapd stops, it closes the socket, and this removes the advertisement.

mdnsd(8) must run when openhapd starts. If it is not, HomeKit controllers cannot find the accessory. In that condition, the daemon starts, writes a warning to the log, and serves the paired controllers. Discovery is not available until you restart openhapd while mdnsd(8) runs.

On OpenBSD, the daemon drops privileges and advertises itself. Then it restricts its own syscall surface with pledge(2) to “stdio rpath wpath cpath fattr flock inet dns unix” before it receives any network input. The “proc”, “exec” and “prot_exec” promises are absent by design. These restrictions are in effect only on OpenBSD. On other systems, the daemon runs without restrictions.

The options are:

-c file
    Read the configuration from file. The default is /etc/openhapd.conf.

-f  Run in the foreground. By default, openhapd daemonizes and logs to syslog(3). With this option, openhapd stays in the foreground and logs to stderr.

-n  Do a check of the configuration file for syntax errors and exit. No daemon starts.

-v  Increase the log verbosity. You can give this option more than one time. Each use increases the log level: -v gives the debug messages, and -vv adds trace messages.

FILES
/etc/openhapd.conf
    The default configuration file.
/var/db/openhapd/
    The state directory that holds the pairing database and the cryptographic keys.
/var/db/openhapd/pairings.db
    The database of paired HomeKit controllers, mode 0600.
/var/db/openhapd/accessory_ltsk
    The long-term secret key (Ed25519 private key), mode 0600.
/var/db/openhapd/accessory_ltpk
    The long-term public key (Ed25519 public key), mode 0644.
/var/db/openhapd/state.json
    The configuration number, the configuration digest, and the counter of failed pairing attempts, mode 0600. An older installation kept each of these in a file of its own. The daemon reads those files one time and folds them into this one.
/var/run/openhapd/control.sock
    The control socket, mode 0600 in a directory of mode 0700. hapctl(8) asks the running daemon for its status over it. The daemon creates the directory while it is still root, because OpenBSD clears /var/run at boot, and it removes the socket at a clean shutdown. The control directive of openhapd.conf(5) moves the socket or turns it off.
/var/run/openhapd.pid
    The process ID file. The daemon takes it while it is still root, and holds the lock for its whole life. The file stays root-owned, so the unprivileged daemon cannot rewrite it. openhapd does not remove the file at exit: an unlink(2) in root-owned /var/run needs a directory permission that the daemon gives up at the privilege drop. A leftover file names a process that no longer runs, and hapctl(8) reports that as “not running”.

On OpenBSD, the daemon restricts its filesystem view with unveil(2). It can read nothing outside of the list that follows. If openhapd cannot see a file, examine this list first. If an optional path is absent, the daemon starts. If a necessary path is absent, this is a fatal configuration error.
db_path
    (necessary, read-write-create) the state directory above.
/dev/urandom
    (necessary, read) randomness for all key material.
perl library directories
    (necessary, read) the library tree of the interpreter, for the one module that the daemon loads late, on the MQTT reconnect path.
configuration file
    (optional, read) the file that the -c option names.
/var/log/openhapd.log
    (optional, write) the daemon-mode log; the daemon never creates this file in -f mode.
/var/run/mdnsd.sock
    (optional, read-write) after a pairing change, the daemon makes a new connection to mdnsd(8) to advertise again.
control socket directory
    (optional, read-write-create) the directory of the control socket, not the socket. bind(2) and unlink(2) both change a directory entry. The row is absent when control off turns the socket off.
/etc/resolv.conf, /etc/hosts, /etc/services, /etc/protocols
    (optional, read) the resolver files; the daemon uses them when mqtt_host is a name and not an address.
/etc/localtime
    (optional, read) timestamps.

SIGNALS

SIGTERM, SIGINT
    Shut down. The signal sets a flag. The event loop sees the flag at the end of the current pass, and the daemon then withdraws its mDNS advertisement and closes the listener and every client connection. The daemon exits 0.

    Nothing exits from inside the signal handler, thus a connection is never dropped in the middle of a response.

SIGHUP
    Reload. The daemon opens its log again and keeps serving. Use this after newsyslog(8) rotates /var/log/openhapd.log.

    rcctl reload openhapd sends this signal. In releases before 2026 the signal ended the daemon, which is the opposite of a reload.

The daemon does not remove /var/run/openhapd.pid at exit. See FILES.

EXAMPLES
Do a check of the configuration file:

    # openhapd -n

Run in the foreground with verbose logging:

    # openhapd -f -v

Start the daemon the usual way, through rc(8):

    # rcctl enable openhapd
    # rcctl start openhapd

DIAGNOSTICS
openhapd logs to syslog(3) with the “daemon” facility. When the daemon runs in the foreground with -f, it writes the messages to stderr.

These are some of the log messages:

“Starting OpenHAP server”
    The daemon started correctly and now listens for connections.

“Not paired - use Home app with PIN: XXX-XX-XXX”
    The accessory is not paired with a HomeKit controller. Use the PIN from the message to pair through the iOS Home app.

“Connected to MQTT broker at host:port”
    The daemon connected to the MQTT broker.

“MQTT broker not available, will retry in background”
    The daemon did not connect to the MQTT broker at startup. openhapd continues to try to connect in the background.

“Paired with controller: identifier”
    The pairing with a new HomeKit controller was successful.

“Secure session established”
    The daemon started an encrypted session with a paired controller.

A configuration file that does not parse is a fatal error. openhapd writes the file, the line number, and the reason to stderr, and it exits 1 before it opens the log:

    /etc/openhapd.conf:14: cannot parse: hap_prt 51827

The daemon does not fall back to the defaults, because the operator did not choose them. Use -n to check a file before you restart the daemon.

SEE ALSO
syslog(3), openhapd.conf(5), hapctl(8), mdnsd(8), rc(8), rc.conf(8)

HomeKit Accessory Protocol Specification (Non-Commercial Version): https://developer.apple.com/homekit/

STANDARDS
openhapd obeys the HomeKit Accessory Protocol Specification R2 for IP accessories. These documents specify the cryptography that it uses:
•   RFC 5054: Using the Secure Remote Password (SRP) Protocol for TLS Authentication
•   RFC 7539: ChaCha20 and Poly1305 for IETF Protocols
•   RFC 5869: HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
•   RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA)
•   RFC 7748: Elliptic Curves for Security (Curve25519)

HISTORY
The openhapd daemon was first available in 2025.

AUTHORS
Dick Olsson <hi@senzilla.io>

CAVEATS
For correct operation, openhapd needs an MQTT broker, for example, mosquitto(8), and mdnsd(8). Both must run. If one of them is not available, the daemon starts and does what it can. The daemon tries the MQTT connection again in the background. The mDNS discovery is not available until you restart the daemon.

At the first startup, if the accessory is not paired, the daemon writes the HAP PIN code to the log. Thus, limit the access to the system logs.

The pairing data in /var/db/openhapd/ holds cryptographic keys. The owner of the directory must be “_openhap”, and the mode must be 0700.

BUGS
openhapd operates only with Tasmota devices over MQTT. It does not implement HAP over Bluetooth Low Energy.

SECURITY CONSIDERATIONS
openhapd starts as root to set the correct ownership of /var/db/openhapd. Then it drops to the “_openhap” user and publishes its mDNS advertisement. Then it restricts itself: first the filesystem view with unveil(2), and then the syscall surface with pledge(2). The daemon accepts network connections only after these steps.

Because the restrictions come after the privilege drop, they apply to all code that receives untrusted input. They do not, and cannot, apply to the short root phase. But that phase receives no untrusted input.

The promise set is “stdio rpath wpath cpath fattr flock inet dns unix”:
stdio
    always necessary.
rpath
    /dev/urandom, state reads, and the MQTT module that the daemon loads late.
wpath, cpath
    to write, create, and remove the state files and the daemon log.
fattr
    the daemon sets the mode of the key material to 0600.
flock
    locks protect the state files.
inet
    the HAP listener and the MQTT connections.
dns to resolve mqtt_host when it is a name.
unix
    after a pairing change, the daemon makes a new connection to mdnsd(8) to advertise again.

The proc, exec and prot_exec promises are absent by design: the daemon spawns no processes and loads no shared objects after startup. The section FILES lists the unveiled paths and the access type of each path.

The mdnsd(8) control socket has owner root:wheel and mode 0660. Thus, the “_openhap” user must have read-write access to the socket. In the standard setup, the “_openhap” user is a member of the wheel group, and this gives the access.

The restrictions are in effect only on OpenBSD. Linux and Darwin are development platforms, and there the two mechanisms do nothing. On these platforms, the daemon runs without restrictions.

A pledge violation stops the daemon immediately with SIGABRT. The kernel then writes a line to dmesg(8) and /var/log/messages. This line names the process and the syscall that is not permitted. If openhapd stops and you do not know why, examine these logs first.

A path outside of the unveiled view does not stop the daemon. The operation on that path fails with ENOENT or EACCES. Usually, the daemon log then shows a warning.