Bor v0.10.1 released

Bor v0.10.1 is a patch release, three days after v0.10.0. It fixes disk encryption on a default Fedora Workstation install, where the root volume was invisible to the agent; it keeps the policy stream alive across a suspend; it accepts constrain in custom SELinux modules, which is what the agent self-protection policy needs; and it publishes signed SHA256SUMS with every release from now on. There are no database migrations and no configuration changes. The complete changelog is on the GitHub release page.

Disk encryption on a default Fedora Workstation

Most of this release is one scenario: install Fedora Workstation with the default disk layout, enroll the node, and look at the Disk Encryption page. Almost nothing worked, for several independent reasons.

  • LUKS volumes behind a btrfs root are discovered again. btrfs, and bcachefs, mounts carry an anonymous device number in mountinfo, so /sys/dev/block has no node for them and the walk down to the dm-crypt container never started. On a default Fedora install the root volume was simply not there: bor-agent luks status printed no volumes, bor-agent luks adopt found nothing, and the encryption check reported an encrypted root as unencrypted. The agent now resolves the mount source to its block node when the device number is anonymous.
  • zram swap is compliant. RAM-backed swap never reaches a disk, yet the system-scope encryption check counted it as an unencrypted swap device, which made every default Fedora install non-compliant on its own. Such devices are recognised during discovery and reported as compliant, with the reason.
  • The LUKS inventory flag reaches agents. AgentConfig.disk_encryption_inventory is documented as on by default, but the server never set it, so a node without a bound DiskEncryption policy reported no volumes at all and the Disk Encryption page stayed empty until a policy was bound. The server now sends it as true, which is what makes “are we actually encrypted?” answerable before any policy exists.
  • bor-agent luks adopt prompts on the terminal. systemd-ask-password only uses the TTY when stdin is one, and the generic command runner detaches the child, so adoption fell back to the password-agent protocol: a wall broadcast, with the typed passphrase echoed back by the terminal. The command now runs with the caller’s own stdin and stderr, and prints a hint when stdin is not a terminal. It also reports the inventory before asking for the passphrase, so adopting a freshly enrolled node no longer fails with “LUKS volume not found” because the server had not yet learned about the volume.
  • A recovery-key rotation no longer breaks the protector binds that follow it. Rotation wipes the old Bor recovery slots once the new key is confirmed. Where the credential for the sync came from a server release - the active recovery key, handed out for a pending rotation - it stopped opening the volume the moment the rotation finished, and the Tang bind right behind it failed with “Error adding new binding”. A reveal with rotate-after-reveal on a Tang-bound node hit this every time. Rotation now returns the key it enrolled, and the sync continues with that credential for TPM 2.0, Tang and bootstrap retirement.
  • Adding a Tang server works. Settings -> Tang Servers -> Add failed with an internal error, because a new record was inserted with a nil advertised_thumbprints array and PostgreSQL rejects that for a NOT NULL column. A nil list is stored as an empty one.

A policy stream that notices it is dead

After a suspend and resume, a NAT reset or a server restart, a node’s TCP socket to the policy port can stay established while the server has long since dropped the connection: the server’s TCP user timeout fires as soon as a push goes unacknowledged for 20 seconds. The agent then sat in Recv on a stream that no longer existed and saw no new bindings until something restarted it.

The agent now sends HTTP/2 keepalive pings every 60 seconds with a 20 second timeout. A dead connection is closed and the existing reconnect loop, with its exponential backoff, takes over from there. The server’s enforcement policy permits pings as often as every 10 seconds, so an agent configured more aggressively is not disconnected for it.

SELinux: constrain, and a category for the agent

Securing the Bor agent with Bor, published yesterday, ends with a caveat: the policy it builds needs a CIL constrain statement, and v0.10.0 refused it with statement “constrain” is not permitted in a Bor module. This is the release that accepts it.

  • Custom modules may use constrain. The statement allowlist ruled out the one statement that can take a permission away from the unconfined domains, which hold allow rules for everything - so a module that keeps a service from being killed from a root shell could not be authored at all. Constraints restructure nothing in the base policy, and a bad one is undone by removing the module. The statements that do restructure the base policy - classes, SIDs, users and roles, MLS, policy capabilities and kernel object contexts - are still refused.
  • The agent runs with an MCS category of its own. The packaged unit now sets SELinuxContext=-system_u:system_r:unconfined_service_t:s0:c1023, so the agent keeps its unconfined service domain but gets the level s0:c1023. The category confines nothing by itself; on the targeted policy MCS binds only container domains for file access. What it does is let a policy single the agent process out without a module per node - for instance with a constrain statement that refuses signals from senders at a lower level, so that kill -9 from a root shell is denied while systemctl restart bor-agent keeps working. The leading - makes systemd ignore a policy that does not have the type, and the directive is skipped where SELinux is disabled. It is documented in the hardening guide and on the SELinux policy type page.

Together these two mean the self-protection policy works on a v0.10.1 node with nothing hand-written on the node itself.

Signed checksums, now automatic

v0.10.0 introduced SHA256SUMS and the detached SHA256SUMS.asc next to the packages. A publish-checksums job now does it for every release, once the agent and server packages are uploaded: it assembles SHA256SUMS from the digests GitHub already records for the assets, downloading and hashing only where a digest is missing, signs it with the release key that also signs the package repositories a Bor server serves at /agent/, verifies the signature and uploads both files to the release.

The key, its fingerprint and the two commands to check a download are on the Download page.

Upgrade notes

  • No database migrations and no new settings. Server and agent configuration from v0.10.0 is unchanged.
  • Server and agent can be upgraded independently. The Tang server, SELinux constrain and LUKS inventory fixes are server-side; volume discovery, zram, luks adopt, the rotation credential and the stream keepalive are agent-side. Upgrade both to get all of them.
  • The agent package replaces bor-agent.service to add the SELinuxContext= directive. Local changes belong in a drop-in under /etc/systemd/system/bor-agent.service.d/; a modified unit file in /usr/lib/systemd/system/ is overwritten by the package.
  • Nodes whose LUKS volumes were invisible report them after the agent upgrade. Once discovery works, the inventory arrives within 15 minutes and the previously missing volumes appear on the Disk Encryption page, where they can be adopted.

Download

Packages for Debian/Ubuntu, RHEL/Fedora/SUSE, Alpine Linux and Arch Linux across x86_64, aarch64 and ppc64le are on the Download page, together with the checksums and their signature. The demo instance runs the current release.