Skip to content
C.W.K.
Lesson 04 of 05 · published

Loopback Is Not Identity

~13 min · loopback, local-token, file-permissions, multi-account, constant-time

Level 0Loose File
0 XP0/43 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

127.0.0.1 Is a Place, Not a Person

Track 1 moved every app's network traffic onto loopback: an app asks its own Mac's Delta on 127.0.0.1, and only Delta dials the LAN. It is tempting to finish the thought with "and therefore anything on loopback is trusted". That sentence is wrong on every Mac with more than one account, and the family has one where it matters most.

Pippa's home Mac has two accounts. Delta runs in her pippa account and serves that account's home. Dad's account exists there only to administer the machine. Both accounts' processes can open a TCP connection to 127.0.0.1; loopback does not care which user a process belongs to. If "came from loopback" meant "is this Mac", then any process in any account (or any tool that a stray script launched) could read the home Delta serves, as that Mac, with full fleet standing.

So loopback is a place. To be treated as "this Mac", a caller there has to prove something only the account that runs Delta can prove.

The Proof Is a File Permission

The proof is the local token: 32 random bytes (secrets.token_urlsafe(32)) in <state root>/local.token, created with mode 0600. Among ordinary accounts, macOS lets only one read that file: the one Delta runs as. (Root reads any file, so an administrator who chooses sudo can read it too; the token keeps ordinary accounts and stray processes apart, not an administrator who deliberately becomes root.) An app in that account reads it and sends it as X-Delta-Local-Token, beside X-Delta-App naming itself. A process in another account cannot open the file, so it cannot show the token, so it is refused with 401 and a sentence that says what is missing.

The kit's client, kit_delta, does this for every app: it reads the token from Delta's state folder (again at most once a minute, so a rewritten token is picked up) and adds both headers to every request. No app keeps its own copy, and the token never appears in a URL, a log line or an environment variable.

ListenerWho reaches itWhat it demands
engine, 127.0.0.1:5200loopback only; any other source is closed unansweredthe local token on every route except health and version
gateway, :5201, from loopbacka process on this Mac that dialled the gateway directlythe local token, or it should ask the engine
gateway, from the tailnetanother Macnothing more: the roster names it (lesson 1)
gateway, Thunderbolt or LANanother Maca ticket (lessons 2 and 3)

The Hole the Review Found

The engine asked for the token from the first day. The gateway did not. In the first version, a connection that reached the gateway from loopback was classified as a tailnet connection carrying this Mac's own label, with no token asked. The engine's front door was locked; the side door into the same house stood open. Any account on the Mac could skip the engine, call the gateway on its port, and read the home share as the Mac itself.

The 1110-7 review found it, and the fix is one more connection kind. Loopback callers at the gateway are now loopback, not tailnet, and _authenticate holds them to the engine's rule: show the token, or be told to ask the engine. A test pins it: the same request to the gateway answers 401 without the header and 200 with it.

The same review fixed two smaller things in the same few lines. The token comparison became hmac.compare_digest, which takes the same time whether the first byte or the last byte differs, instead of !=. And token creation became crash-safe. The first version created the file with O_EXCL and then wrote the token into it. A stop between those two steps left an empty file, every later start read the empty string, and an empty token matches nothing: every app on the Mac refused, permanently, by a file that looked present. Now an empty file counts as missing, and the token is written whole under a temporary name, then moved into place with os.replace, which is atomic.

What the App Name Is Worth

X-Delta-App is honest bookkeeping, not proof. Share policies list the apps that may use them (apps), and Delta holds a request to that list. But the header is whatever the calling process says, on a Mac whose token it holds. It separates well-behaved apps from each other; it does not stop a process in Delta's own account from naming a different app. The architecture says so in plain words: a writer that must tell two apps on one Mac apart needs more than that header. Knowing exactly what a credential proves, and writing down what it does not, is as much a part of the design as the credential itself.

Code

Delta's loopback rules and the local token (trimmed excerpt)·python
# delta/server.py and delta/identity.py (trimmed excerpt)

def admit(self, sock, client_address):             # Listener: classify a new connection
    peer_ip = client_address[0]
    if self.role == "engine":
        if not ipaddress.ip_address(peer_ip).is_loopback:
            log.warning("engine refused a non-loopback caller %s", peer_ip)
            return None
        return Conn("engine", peer_ip, local_ip, sock, self.delta.label)
    if ipaddress.ip_address(peer_ip).is_loopback:
        # A process on this Mac: this Mac's label, once it shows the local token
        # (the engine's rule; without it any account here read the home share).
        return Conn("loopback", peer_ip, local_ip, sock)
    ...


def _local_token_ok(self) -> bool:
    token = self.headers.get("X-Delta-Local-Token", "")
    return bool(token) and bool(self.delta.local_token) and \
        hmac.compare_digest(token.encode(), self.delta.local_token.encode())


def ensure_local_token() -> str:
    path = local_token_path()                      # <state root>/local.token
    try:
        token = path.read_text().strip()
    except FileNotFoundError:
        token = ""
    if token:
        return token
    # Absent, or empty (a stop between creating the file and writing it): an empty
    # token would refuse every app on this Mac for good, so it is written again,
    # whole, under a name of its own first.
    token = secrets.token_urlsafe(32)
    tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
    fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w") as handle:
        handle.write(token + "\n")
    os.replace(tmp, path)
    return token
A loopback service that asks for a 0600 token, crash case included·python
#!/usr/bin/env python3
"""A loopback service that asks callers for a 0600 token file, the way Delta's engine does."""
import hmac
import http.client
import http.server
import os
import secrets
import tempfile
import threading
from pathlib import Path


def ensure_token(path: Path) -> str:
    try:
        token = path.read_text().strip()
    except FileNotFoundError:
        token = ""
    if token:
        return token
    # Missing or empty: write it whole under a temporary name, then rename into place.
    token = secrets.token_urlsafe(32)
    tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
    fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w") as handle:
        handle.write(token + "\n")
    os.replace(tmp, path)
    return token


class Handler(http.server.BaseHTTPRequestHandler):
    token = ""

    def do_GET(self):
        if self.path == "/health":                     # the one route anyone on loopback may ask
            return self.answer(200, b"ok\n")
        shown = self.headers.get("X-Local-Token", "")
        if not shown or not hmac.compare_digest(shown.encode(), self.token.encode()):
            return self.answer(401, b"show X-Local-Token (the 0600 file only this account can read)\n")
        self.answer(200, f"hello, {self.headers.get('X-App', '?')}\n".encode())

    def answer(self, status: int, body: bytes):
        self.send_response(status)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *args):
        pass


with tempfile.TemporaryDirectory() as tmp:
    token_file = Path(tmp) / "local.token"
    token_file.write_text("")                          # the crash case: an empty file left behind
    Handler.token = ensure_token(token_file)
    print("mode", oct(token_file.stat().st_mode & 0o777), "length", len(Handler.token))

    server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
    threading.Thread(target=server.serve_forever, daemon=True).start()

    def get(path, headers=None):
        conn = http.client.HTTPConnection("127.0.0.1", server.server_address[1], timeout=5)
        conn.request("GET", path, headers=headers or {})
        resp = conn.getresponse()
        out = resp.status, resp.read().decode().strip()
        conn.close()
        return out

    print(get("/health"))
    print(get("/data"))
    print(get("/data", {"X-Local-Token": token_file.read_text().strip(), "X-App": "my-app"}))
    server.shutdown()

External links

Exercise

Find a service on one of your Macs that listens on 127.0.0.1 (lsof -nP -iTCP -sTCP:LISTEN | grep 127.0.0.1). For each one, decide what a process in a different account on that Mac could do by connecting to it. Then run the token script above and add a second check: refuse any request whose token file is readable by group or others (st_mode & 0o077), with a message that names the fix.
Hint
Many developer servers (notebooks, dev servers, local databases) accept any loopback caller. If you have a second macOS account, log in to it and try curl http://127.0.0.1:<port>/ from there: whatever answers is answering a stranger.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.