# Samba Share

Every {{productName}} install provisions a per-brand SMB network share so you can read and write the brand's install folder from Finder, File Explorer, the Files app on iOS, or any SMB-capable client on Android or Linux. No client install required — every modern OS speaks SMB natively.

The share lives next to the rest of the brand. On a device that runs more than one brand, each brand gets its own stanza, its own credentials, and its own lifecycle. Tearing one brand down never touches another brand's share.

## What gets provisioned

The installer runs the same Samba step on every supported footprint — Raspberry Pi, Hetzner Cloud server, and self-hosted Linux laptop. Four sub-steps emit `[install-invariant] samba-provision-<step>` markers in order:

1. **apt** — installs the `samba` package (skipped if `dpkg -s samba` already reports installed, so re-runs don't fight `unattended-upgrades` for the dpkg lock).
2. **conf** — writes `/etc/samba/smb.conf` with a `[global]` section plus a `[<brand>]` stanza pointing at the brand's install directory. The stanza is owned by the install owner (see below) and is marked `read only = no`, `browseable = yes`. The `[global]` bind posture depends on the host's interfaces (see "Interface binding" below); the marker records which posture fired — `bind=lan iface=<name>` or `bind=loopback-only`.
3. **user** — deferred at install time on a fresh Pi or Hetzner box because there is no PIN to hand to `smbpasswd` yet. The user is created the moment the operator sets a PIN in the admin UI (see "PIN rotation" below).
4. **units** — `systemctl enable --now smbd` so the share is reachable as soon as the install finishes. `nmbd` (NetBIOS name service) is never enabled: nothing mounts the share by NetBIOS name, and on a public-facing host it answers on `:137`/`:138` as a DDoS-reflection vector.

macOS install is a no-op for this step — the installer logs `samba-provision skipped: platform=darwin` and returns. Mac operators do not get an SMB share against their laptop.

## Share path

| Client | Address |
|---|---|
| macOS Finder | `smb://<hostname>.local` then pick the `<brand>` share |
| Windows Explorer | `\\<hostname>.local\<brand>` |
| Linux (`mount.cifs`, Nautilus, KDE) | `//<hostname>.local/<brand>` |
| iOS Files | `smb://<hostname>.local` |
| Android (Solid Explorer, CX File Explorer) | Host `<hostname>.local`, share `<brand>` |

`<hostname>` is whatever the installer printed at the end of `npx @rubytech/create-<brand>-code install` — usually the brand name on a fresh Pi (`maxy-code.local`, `realagent-code.local`). `<brand>` is the same string — it is also the install folder name under the install owner's home.

If `<hostname>.local` does not resolve from your client (some networks do not route mDNS), fall back to the LAN IP: `smb://192.168.1.50` on macOS, `\\192.168.1.50\<brand>` on Windows.

## Credentials

- **Username** — the Unix user that owns the install on the device. On a Pi or Hetzner box this is `admin`; on a self-hosted Linux laptop it is whatever Linux user ran the installer (for example `neo`). The installer persists this value to `~/.<brand>/.install-owner` so every later read uses the same identity the installer wrote.
- **Password** — your current {{productName}} PIN. The same PIN that unlocks the admin UI unlocks the SMB share.

Both halves are required. SMB never accepts a guest connection; `map to guest = bad user` is set in the global stanza.

## PIN rotation

There is no separate "SMB password." When you set or rotate the PIN in the admin UI, the platform's `set-pin` route runs `sudo -n smbpasswd -a -s <install-owner>` inline with the new PIN, behind a `NOPASSWD` sudoers grant written at install time and scoped to that exact command.

So:

- On a fresh Pi or Hetzner box, the share is reachable as soon as you set the first PIN. Before that point the `smbpasswd` entry does not exist and the mount fails with a logon error — that is expected.
- Rotating the PIN re-syncs the SMB password to the new value on the next set-pin request. Mounts using the old PIN start failing immediately; remount with the new PIN.
- If `set-pin` cannot read `~/.<brand>/.install-owner` (file missing or empty), it logs `[set-pin] smbpasswd sync failed owner=<unknown> rc=-1 reason=install-owner-file-missing` and skips the sync. The PIN still writes to the admin UI, but the SMB mount keeps refusing the new password until the install-owner file is restored.

## Interface binding

`bind interfaces only = yes` is always set, which makes the `interfaces` line an allow-list rather than a hint. Smbd binds only to the interfaces named there. The installer classifies the host's interfaces and writes one of two postures:

- **Private LAN host (Raspberry Pi).** At least one non-loopback interface carries a private address — RFC1918 (`10/8`, `172.16/12`, `192.168/16`), CGNAT (`100.64/10`), or link-local (`169.254/16`). The installer binds loopback plus that interface (`wlan0` preferred, then `eth0`, then the first other private interface):

  ```
  interfaces = lo <lan>
  bind interfaces only = yes
  ```

  The share is reachable on the LAN and nowhere else.

- **Public-only host (Hetzner Cloud).** The only non-loopback interface carries a routable public address (typically `eth0` with a `/32`, no private LAN). Binding `lo eth0` here would put smbd directly on the public internet, so the installer binds **loopback only**:

  ```
  interfaces = lo
  bind interfaces only = yes
  ```

  The share is reachable solely from the box itself. Operators reach it by forwarding loopback over SSH — `ssh -L 4445:localhost:445 admin@<tunnel-host>` and then mounting `smb://localhost:4445` — or by joining the box to a private network and re-installing, which makes the private interface the chosen LAN bind.

If the device has no non-loopback IPv4 interface at all, the installer refuses to provision and exits — there is nothing safe to bind to. This is distinct from the public-only case, which still provisions a working (host-local) share.

This classification is the structural guarantee that SMB never leaves the box (loopback-only) or the LAN (LAN-only), even if upstream firewall rules are misconfigured. The Cloudflare tunnel that fronts the admin UI carries HTTPS only; it does not route SMB.

## Peer-brand lifecycle

A device that hosts more than one brand carries one stanza per brand in `/etc/samba/smb.conf`. The provisioner is idempotent and peer-safe:

- **Install a second brand** — the new brand's stanza is appended next to the existing one. The shared `[global]` section is rewritten to keep the LAN-only directives current but is otherwise unchanged. Peer-brand stanzas are preserved byte-for-byte.
- **Re-run the installer on an existing brand** — the brand's own stanza is replaced in place. Peer stanzas are not touched.
- **Uninstall one brand** — only that brand's stanza is stripped. `smbd` is then `reload`ed so the brand share disappears from the running config without dropping connections to peer shares.
- **Uninstall the last brand** — after the stanza is removed, the uninstaller checks `hasAnyBrandStanza()` and, if false, stops and disables `smbd`/`nmbd`, runs `smbpasswd -x <install-owner>` to drop the smbpasswd entry, and `apt-purge samba`. If any peer stanza remains, the units stay running and the package stays installed — the uninstaller logs `Leaving smbd/nmbd + samba package in place — other brand stanza remains`.

The brand-stanza name is the only identifier the uninstaller matches on, so two brands with different `BRAND.hostname` values cannot collide.

## Troubleshooting

- **"Logon failure" on mount.** The PIN you typed does not match the current `smbpasswd` entry. Set a new PIN in the admin UI and remount. If the PIN was just rotated and the mount still fails, check `~/.<brand>/.install-owner` exists and is non-empty.
- **Share does not show up in Finder / network browser.** mDNS may not be routed on your network. Mount by LAN IP instead of `<hostname>.local`.
- **`smbd` not running after install.** Check the install log for the four `[install-invariant] samba-provision-<step>` markers. The `units` step running `systemctl enable --now smbd` is the last to fire; if it failed the marker prints `fail: <reason>`.
- **Hetzner share not reachable from outside the box.** This is by design — see "Interface binding" above. A public-only host binds loopback only; use SSH port forwarding (`ssh -L 4445:localhost:445 …`).

Also see [Deployment Guide](./deployment.md) for the surrounding install flow, and [Access Control](./access-control.md) for how the brand isolation extends from the admin UI to the SMB share.
