cicada handles post-quantum identity and key material. It MUST:
-
Never touch external network - no egress, no ingress
-
Run in isolated container - separate network namespace
-
Communicate only via Unix socket - no TCP/IP even locally
┌─────────────────────────────────────────────────────────────────┐
│ Host System │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │
│ │ cicada container │ │ cloud-sync container │ │
│ │ │ │ │ │
│ │ network: none │ │ network: vpn-only │ │
│ │ no capabilities │ │ ┌─────────────────────────┐ │ │
│ │ read-only rootfs │ │ │ zig-wireguard │ │ │
│ │ │ │ │ zig-rclone │ │ │
│ │ ┌───────────────┐ │ │ │ zig-fuse-ext │ │ │
│ │ │ cicada daemon │ │ │ └─────────────────────────┘ │ │
│ │ └───────┬───────┘ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └──────────┼──────────┘ └──────────────┼──────────────────┘ │
│ │ Unix socket │ │
│ └──────────────────────────────┘ │
│ /run/cicada/cicada.sock │
│ (bind-mounted, 0600) │
└─────────────────────────────────────────────────────────────────┘# cicada-compose.yaml
services:
cicada:
image: ghcr.io/hyperpolymath/cicada:latest
container_name: cicada-vault
# CRITICAL: No network access whatsoever
network_mode: none
# Minimal capabilities
cap_drop:
- ALL
# Read-only root filesystem
read_only: true
# No privilege escalation
security_opt:
- no-new-privileges:true
# Seccomp profile - restrict syscalls
security_opt:
- seccomp=/etc/cicada/seccomp.json
# Unix socket for IPC only
volumes:
- type: bind
source: /run/cicada
target: /run/cicada
bind:
propagation: private
- type: bind
source: /var/lib/cicada/keys
target: /keys
read_only: false
# Tmpfs for runtime data
tmpfs:
- /tmp:size=10M,mode=1700
# Resource limits
deploy:
resources:
limits:
memory: 128M
cpus: '0.5'#!/usr/sbin/nft -f
# SPDX-License-Identifier: MPL-2.0-or-later
# Cicada isolation firewall rules
table inet cicada_isolation {
# Block any attempt to create network namespace escape
chain output {
type filter hook output priority 0; policy accept;
# If somehow cicada container gets network, block everything
meta skuid cicada drop
meta skgid cicada drop
}
chain input {
type filter hook input priority 0; policy accept;
# No incoming connections to cicada user
meta skuid cicada drop
}
}# Create socket directory with strict permissions
mkdir -p /run/cicada
chmod 0700 /run/cicada
chown cicada:cloud-sync /run/cicada
# Socket created by cicada daemon
# /run/cicada/cicada.sock - mode 0660, owner cicada:cloud-syncRequest: { "op": "get_key", "identity": "cloud-sync", "format": "wg" }
Response: { "key": "<base64>", "type": "x25519+kyber768" }
Request: { "op": "sign", "identity": "cloud-sync", "data": "<base64>" }
Response: { "signature": "<base64>", "algorithm": "dilithium3" }const std = @import("std");
pub const CicadaClient = struct {
socket: std.net.Stream,
pub fn connect() !CicadaClient {
// Unix socket only - no network
const socket = try std.net.connectUnixSocket("/run/cicada/cicada.sock");
return .{ .socket = socket };
}
pub fn getWireGuardKey(self: *CicadaClient, identity: []const u8) !Key {
const request = try std.json.stringifyAlloc(allocator, .{
.op = "get_key",
.identity = identity,
.format = "wg",
});
defer allocator.free(request);
try self.socket.writeAll(request);
var response_buf: [4096]u8 = undefined;
const len = try self.socket.read(&response_buf);
const response = try std.json.parseFromSlice(
KeyResponse,
allocator,
response_buf[0..len],
.{},
);
return Key.fromBase64(response.value.key);
}
pub fn close(self: *CicadaClient) void {
self.socket.close();
}
};#!/bin/bash
# SPDX-License-Identifier: MPL-2.0-or-later
# verify-cicada-isolation.sh
set -euo pipefail
# Verify cicada container has no network
verify_no_network() {
local netns=$(nerdctl inspect cicada-vault --format '{{.NetworkSettings.SandboxKey}}')
if [ -n "$netns" ] && [ "$netns" != "null" ]; then
echo "FATAL: cicada container has network namespace"
exit 1
fi
# Double-check with nsenter
if nerdctl exec cicada-vault ip link show 2>/dev/null | grep -v "^1: lo"; then
echo "FATAL: cicada container has non-loopback interfaces"
exit 1
fi
}
# Verify socket permissions
verify_socket_perms() {
local perms=$(stat -c %a /run/cicada/cicada.sock 2>/dev/null || echo "missing")
if [ "$perms" != "660" ]; then
echo "FATAL: cicada socket has wrong permissions: $perms (expected 660)"
exit 1
fi
}
# Verify no TCP listeners in cicada
verify_no_tcp() {
if nerdctl exec cicada-vault ss -tlnp 2>/dev/null | grep -q LISTEN; then
echo "FATAL: cicada is listening on TCP"
exit 1
fi
}
verify_no_network
verify_socket_perms
verify_no_tcp
echo "Cicada isolation verified"| Threat | Mitigation | Verification |
|---|---|---|
Network exfiltration of keys |
network_mode: none |
|
Container escape via capabilities |
cap_drop: ALL |
|
Privilege escalation |
no-new-privileges, read-only rootfs |
|
Side-channel via shared memory |
Separate tmpfs, no /dev/shm sharing |
|
Socket hijacking |
0660 permissions, cicada:cloud-sync ownership |
|
Syscall-based escape |
Seccomp profile |
|
The main cloud-sync container connects to cicada only for key retrieval at startup:
pub fn establishSdpTunnel() !void {
// 1. Get key from isolated cicada (Unix socket only)
var cicada = try CicadaClient.connect();
defer cicada.close();
const wg_key = try cicada.getWireGuardKey("cloud-sync");
// 2. cicada connection closed - no further contact needed
// Key material now in memory only
// 3. Establish WireGuard tunnel with retrieved key
var device = try wg.Device.create("wg0");
try device.setPrivateKey(wg_key);
try device.up();
}Key material exists in: 1. cicada container (encrypted at rest) 2. cloud-sync container memory (runtime only) 3. Kernel WireGuard module (runtime only)
Never on disk unencrypted, never on network.