UUCP TLS Network — Setup Status

Three hosts, all links working, all six directions verified by checksum. Taylor UUCP 1.07, stunnel-terminated TLS, mutual certificate authentication. Managed by the ansible role roles/uucp; the playbook is idempotent.

Architecture

uucico never speaks to the network. It binds loopback only; stunnel terminates TLS and forwards to it. The UUCP password is a second factor inside an already-authenticated tunnel, not the security boundary.

Inbound: peer connects to TCP 10540, stunnel verifies the client certificate against the shared CA, forwards to 127.0.0.1:540, where systemd socket activation starts uucico per connection.

Outbound: uucico connects to a loopback port named in /etc/uucp/port, stunnel encrypts and forwards to peer:10540.

Port scheme

Uniform on every host, so a port number means the same thing everywhere.

UUCP port names follow the destination: port to-gravlax has service 5411 on every host. Inbound needs no port entry because uucico takes the accepted connection on stdin.

Why socket activation rather than uucico -e -p

Taylor UUCP has no bind-address option. A -p listener binds INADDR_ANY, which would expose a cleartext password-auth UUCP service to the network and defeat the point of the tunnel. systemd ListenStream=127.0.0.1:540 with Accept=yes binds loopback exactly. uucico with -l and no -p reads the connection from stdin, which is the classic inetd mode, so the FreeBSD equivalent is an ordinary inetd line.

Authentication

Per-direction logins and passwords, no shared secret. A caller authenticates as U<its own nodename>: pomelo logs into gravlax as Upomelo, kelp logs into gravlax as Ukelp.

Outbound credentials live in /etc/uucp/call, inbound in /etc/uucp/passwd, both 0600 uucp:uucp. The sys file uses call-login and call-password so no secret appears in a world-readable file.

Passwords are in ~/ansible/secrets.yml as uucp_pw_<caller>_to_<callee>, six of them. That file is gitignored and copied between controllers by scp.

TLS

One self-signed John Critchley CA signs everything. The UUCP sections live in each host's main /etc/stunnel/stunnel.conf alongside notes, postgres and socks, inheriting the global verify = 2 and CAfile.

kelp is the exception: its stunnel globals are its server identity, so its client sections override with kelp-client-cert.pem, kelp-client-key.pem and kelp-client-CA.pem. The role handles this through client_cert/client_key/client_cafile in the topology file.

stunnel connects by FQDN, not short name. Short names exist only in pomelo's /etc/hosts; gravlax and kelp cannot resolve each other otherwise.

Verified: connecting to a peer's 10540 without a client certificate gets a TLS certificate required alert and never reaches uucico. With the certificate, the login prompt appears.

Topology

gravlax (EC2 eu-north-1) and kelp (EC2 eu-central-1) are both callable and link to each other and to pomelo. Security groups open TCP 10540 to the specific peer addresses only.

pomelo is behind home NAT and cannot be called, so it is configured outbound-only: no [uucp] stunnel section, and uucico.socket installed but not enabled. Work queued on gravlax or kelp for pomelo is collected when pomelo calls, because UUCP turns the connection around mid-session. That is the normal polling model and needs no inbound reachability.

Gotchas worth remembering

/var/spool/uucppublic must be mode 1777. Taylor UUCP checks the destination's accessibility as the requesting user, not as uucp, so a 0700 public directory fails every inbound transfer with permission denied by remote on the caller and not permitted to receive on the receiver, even though the uucp user can write there. This is the most likely thing to break if someone tightens permissions later.

A transfer refused by the remote is dropped from the queue, so re-queue before retrying rather than expecting the old job to still be there.

The lock directory is compiled into policy.h as /usr/spool/uucp and cannot be set from the config file. All hosts are built the same way so they agree; changing it means rebuilding everywhere.

Taylor UUCP 1.07 is C89 from 2003 and will not build on Debian 13 / gcc 14 without -std=gnu89 and the implicit-declaration warnings turned back down. Its configure predates autoconf 2.50, so CFLAGS must be passed in the environment; as an argument it is read as a host type.

Validation runs uuchk as the uucp user via su -s /bin/sh, not as root. Root can read anything, so validating as root would not notice that uucp cannot read its own 0600 call and passwd files.

uucp/mail — deployed internal mail, short addresses, configurable hub, polling, Ansible setup and host onboarding.

Mail deployment 2026-09-20: inbound uucico@.service now uses Type=oneshot with -l -D -q and runs uuxqt in ExecStartPost, so received jobs finish before systemd closes the cgroup. command-path includes /usr/sbin for Postfix rmail. Polling is enabled only on pomelo, every 15 minutes to gravlax. New transport-role nodes default to non-callable.

version 4  ·  updated 2026-09-20