The public endpoint exposes S3 only. Private access uses Tailscale. Do not enable router forwarding, UPnP, NAT-PMP, PCP, Tailscale Funnel, or a public SSH/SMB tunnel. Do not change an existing Cloudflare or Tailscale account without its owner's approval. Prepare the exact DNS/tunnel/grant changes for review first.
Use the Homebrew CLI/daemon variant on the server for operation before user login. The GUI variants have different login behavior. Do not run two variants simultaneously; switching an existing installation needs a deliberate migration. Other Macs can use the normal Tailscale app.
brew install tailscale
sudo install -d -o root -g wheel -m 0700 '/Library/Application Support/bin-tailnet'
sudo install -d -o root -g wheel -m 0700 /Library/Logs/bin-tailnetCopy config/tailscaled.service.toml.example to
/Library/Application Support/bin-tailnet/tailscaled.service.toml, mode 0600,
root-owned. Render config/service.plist.example as dev.bin.tailscaled.plist:
service name tailscaled, UserName root, and the config path above. Install
and bootstrap it as shown in macOS setup. The supervisor from that guide
must already be installed. Do not also start the Homebrew tailscale service.
TS_DISABLE_PORTMAPPER=true disables Tailscale's automatic router mappings. It can
still use outbound-established direct connectivity or encrypted relays. Do not
open a UDP router port to improve performance. This environment knob is upstream
implementation detail: recheck it when upgrading Tailscale.
After approval, enroll the node with the interactive login printed by:
sudo /opt/homebrew/bin/tailscale --socket=/var/run/bin-tailscaled.sock upUse the same --socket for subsequent CLI commands. Add a server tag and grants in
the tailnet admin console after reviewing existing policy. A fragment, to merge
with an existing policy rather than replacing it:
{
"groups": { "group:bin-operators": ["you@example.com"] },
"tagOwners": { "tag:bin": ["group:bin-operators"] },
"grants": [
{ "src": ["group:bin-operators"], "dst": ["tag:bin"], "ip": ["tcp:22", "tcp:445"] }
]
}Apply tag:bin to this node. Review overlapping rules: grants are additive, so an
existing allow-all rule defeats the intended restriction. Plan device key expiry
and rotation explicitly for an unattended server. This recipe uses ordinary macOS
SSH through Tailscale, not the separate Tailscale SSH feature.
Do this at the local console before enabling File Sharing or Remote Login. Tailscale grants do not protect the Mac's ordinary LAN interfaces. macOS File Sharing listens on those too. Application Firewall's simple allow/deny UI is not a per-interface restriction.
Use a dedicated PF anchor. Back up the current /etc/pf.conf outside Git and
inspect any existing VPN/firewall rules. Keep Apple's existing rules. Copy
config/pf.anchor.example to /etc/pf.anchors/bin, root-owned 0644. Add the
following to the filter rules section, before other filter anchors/rules:
anchor "bin" quick
load anchor "bin" from "/etc/pf.anchors/bin"
Do not put filter rules before existing normalization/NAT rules. Validate before loading, and stop if syntax validation fails:
sudo pfctl -nf /etc/pf.conf
sudo pfctl -f /etc/pf.conf
sudo pfctl -E
sudo pfctl -a bin -srKeep the PF reference token privately if you plan to release your enable request later; never disable PF globally as a shortcut. Existing established connections may survive a policy change; close them and test fresh connections. Do this before any SMB/SSH sessions exist. Do not flush unrelated firewall state blindly.
The initial anchor denies TCP 22/139/445 and UDP 137/138 except loopback. The helper
identifies tailscaled's actual utun interface, adds an IPv4-only SMB/SSH allowance,
and leaves LAN, IPv6, and legacy NetBIOS access denied. Use the server's Tailscale
IPv4 explicitly in Finder/SSH; MagicDNS may choose IPv6, which is deliberately
not permitted by this minimal policy.
sudo install -o root -g wheel -m 0755 tools/tailnet-firewall.py /usr/local/libexec/bin/Copy config/firewall.service.toml.example to
/Library/Application Support/bin-tailnet/firewall.service.toml, root-owned 0600.
Render service.plist.example as dev.bin.firewall.plist with service firewall,
UserName root, and that config path; install and bootstrap. Also validate,
install, and bootstrap config/pf-enable.plist.example under
/Library/LaunchDaemons/dev.bin.pf.plist for PF initialization at boot.
The helper updates only the bin anchor, follows interface-number changes, and
falls back to deny rules when the interface cannot be identified. PF must remain
enabled and the main anchor installed. Other software can change PF state; test
again after OS/VPN/firewall upgrades. Keep all helper files/configuration that run
as root owned by root and unwritable by binstore/binshare.
Boot isolation remains an acceptance requirement. macOS starts sharing and PF as independent services; these templates do not guarantee their startup ordering. Test fresh LAN connections continuously during reboot. If any succeed, leave File Sharing/Remote Login disabled until a locally verified service-start gate is in place. Do not describe that machine as Tailscale-only based on a post-login test. No host firewall rules are installed by the test suite.
With PF active and verified, enable File Sharing in System Settings → General →
Sharing. Add only /Volumes/bin/files, name the share bin. Give binshare
Read & Write, set Everyone to No Access, disable guest access, and in SMB Options
enable only the dedicated sharing user. Remove unrelated shares if this machine
is dedicated to bin, after checking they are not in use. Do not share the drive
root, metadata, or the service account's directories. Verify SMB encryption is
negotiated where available; Tailscale also encrypts the network path.
On another authorized Mac, Finder → Go → Connect to Server:
smb://TAILSCALE_IPV4/bin
Sign in as Registered User binshare, never Guest. Test creating, reading, and
deleting a synthetic file. Test wrong credentials and a fresh LAN-IP connection
with Tailscale disabled; both must fail.
For Remote Login, permit only the intended operator account. Configure SSH keys
first, then disable SSH password/keyboard-interactive authentication if it fits
your recovery procedure. binshare and binstore must not have shell access.
Use ssh operator@TAILSCALE_IPV4. Garage administration runs locally through SSH;
there is no public admin HTTP service or web browser.
Prepare and approve these account changes: a named tunnel for bin, one DNS route for the chosen S3 hostname, and a hostname-scoped cache bypass rule. Preserve existing records and policies for other services. Verify the hostname is unused before replacing anything.
After approval, the operator can run:
cloudflared tunnel login
cloudflared tunnel create bin
cloudflared tunnel route dns bin s3.example.comThe login certificate is account-management material: keep it private and do not
give it to the service. Install only that tunnel's generated credentials JSON at
/Library/Application Support/bin/tunnel-credentials.json, readable by binstore
only. Copy config/cloudflared.yml.example outside Git, set the tunnel UUID and
hostname, and validate with cloudflared tunnel --config /private/path/config.yml ingress validate before installing it as the path in the service template.
Render cloudflared.service.toml.example and service.plist.example for
dev.bin.cloudflared, UserName binstore, using the same directory/permission
pattern as Garage. Bootstrap the system LaunchDaemon. Keep cloudflared auto-update
disabled; update the Homebrew package deliberately. Only 127.0.0.1:3900 is routed.
The catch-all ingress returns 404; metrics bind only to loopback.
For this hostname:
- Bypass Cloudflare caching for all paths, including presigned downloads. Do not apply Cache Everything or cache private responses.
- Preserve Host, raw path, query parameters, and Authorization. URL rewrites and normalization can invalidate S3 signatures. Test spaces, plus signs, and Unicode.
- Avoid browser challenges or interactive Cloudflare Access login in front of S3; standard AWS clients authenticate with S3 keys. Make only scoped rule adjustments demonstrated necessary by tests; do not disable protection for the whole zone.
- Keep HTTPS required and no anonymous bucket/website endpoint enabled.
Free/Pro proxy uploads are limited to 100 MB per request (a zone may set a lower limit). Larger objects require multipart upload; a single large presigned PUT is still one request. Home upstream speed, request timeouts, and Cloudflare policies remain constraints. Media/large-file traffic is subject to its delivery terms; cache bypass is not an exemption. This is not an unlimited free CDN replacement.
References: Tailscale macOS variants, port mapping switch, Cloudflare macOS services, upload limits, media delivery policy.