#!/bin/bash # Reading a secret the WebUI collected, without it ever touching a command line. # # The problem this exists for: the browser cannot run restic, so a repository # password typed in the WebUI has to reach the host. The two channels that # already existed both leak it — # # * as part of a task's command string, which lands in # frontend/data/tasks/*.json (0644, in a world-readable directory) and is # visible in `ps` while the task runs # * as a plain file in that same directory, which is either 0644 and # world-readable, or 0640 and unreadable by the manager # # and a backup repository password is the key to every backup the user has. # # So the container writes it into a directory root prepared for exactly this: # owned :, mode 2730. Setgid gives the file the manager's # group, the container writes it 0640, and the directory is unlistable. The # manager reads it once and unlinks it; the value never appears in argv. # # Callers pass a REFERENCE (the id) wherever they would have passed the secret, # and resolve it here at the last moment. # Where the drop lives. Created by `libreportal-ownership secret-dir`. webuiSecretDir() { printf '%s' "$(webuiDir)/frontend/data/.secrets" } # Read a secret and consume it. Prints the value on stdout; nothing on failure. # # Single use by construction: a reference that has already been redeemed, or was # never written, is indistinguishable from a wrong one, which is what we want. webuiSecretConsume() { local id="$1" # Names come from the browser, so nothing that could escape the directory. if [[ ! "$id" =~ ^[A-Za-z0-9_-]{8,64}$ ]]; then isError "Invalid secret reference." return 1 fi local f; f="$(webuiSecretDir)/$id" if [[ ! -f "$f" ]]; then isError "That secret is not available (already used, or expired)." return 1 fi cat -- "$f" 2>/dev/null local rc=$? # Unlink even if the read failed — a secret that could not be delivered must # not sit around waiting to be delivered to someone else. rm -f -- "$f" 2>/dev/null return $rc } # Drop anything left behind. A secret is written immediately before the action # that redeems it, so anything older than a few minutes belongs to a flow that # was abandoned — a browser tab closed mid-form — and should not linger. webuiSecretSweep() { local d; d="$(webuiSecretDir)" [[ -d "$d" ]] || return 0 find "$d" -maxdepth 1 -type f -mmin +15 -delete 2>/dev/null return 0 } # True when a value is a reference rather than the secret itself. Lets a caller # accept either during the transition without guessing. webuiSecretIsRef() { [[ "$1" == secret:* ]] } # Resolve a value that may be a reference. Anything else is returned unchanged, # so call sites stay readable. webuiSecretResolve() { local v="$1" if webuiSecretIsRef "$v"; then webuiSecretConsume "${v#secret:}" return $? fi printf '%s' "$v" }