DisplaySync

Tailscale integration

Tailscale gives you a private overlay network that reaches every kiosk regardless of what venue network it landed on. With it, you can RDP/VNC into a misbehaving sign from anywhere; without it, you're calling someone at the venue.

This step is optional but strongly recommended for any fleet you don't physically supervise day-of. Most of the work is one-time thinking about how clones enroll themselves; the build steps are short.

Why a tagged, pre-authorized auth key

Each kiosk needs to log itself into your tailnet on its own, with no human interaction — including the ones that don't exist yet, because they'll be cloned off this image weeks from now. The way to do that:

  1. Create an ACL tag in Tailscale, e.g., tag:displaysync-sign
  2. Mint a reusable, pre-authorized auth key assigned to that tag
  3. Stage the key on the image, where a boot-time task uses it to enroll each machine (step 4)
  4. Use ACLs to restrict what tag:displaysync-sign devices can reach and who can reach them

A tagged device has no human owner — it's a machine identity. That matters for two reasons:

  • No expiry games: untagged devices on the free plan re-authenticate every 6 months. Tagged ones don't.
  • ACL clarity: ACLs that target the tag are simple and stable, regardless of which person originally generated the key.

A pre-authorized key is added to the tailnet without admin approval. A reusable key can onboard many devices from one secret. Treat that secret like a production credential — store it in your secrets manager, rotate it periodically, and never commit it to a public repo.

1. Create the tag and ACL

In the Tailscale admin console, edit your tailnet's ACL policy. A starting-point grant set:

{
  "tagOwners": {
    "tag:displaysync-sign": ["autogroup:admin"]
  },
  "grants": [
    // Admins can reach everything (signs, peer devices, etc.)
    { "src": ["autogroup:admin"], "dst": ["*"], "ip": ["*"] },

    // Members (your support team) can reach signs + their own devices
    { "src": ["autogroup:member"], "dst": ["tag:displaysync-sign"], "ip": ["*"] },
    { "src": ["autogroup:member"], "dst": ["autogroup:self"],       "ip": ["*"] },

    // Signs can reach NOTHING on the tailnet — outbound-only by default.
    // (No grant for tag:displaysync-sign as src.)
  ]
}

This gives you:

  • Admins reach anything.
  • Members (techs) reach signs and their own devices.
  • Signs reach nothing — they're outbound-only beachheads.

If you want signs to talk to a specific service on the tailnet (e.g., a logging endpoint), add a narrow grant. Default-deny is the right starting point.

Free plan ACL note

Tailscale's free plan limits the grants DSL features available. If your tailnet is on the free plan and the JSON above won't compile, fall back to the older acls syntax — same intent, different syntax. The Tailscale docs cover the migration.

2. Mint the auth key

Admin console → Settings → Keys → Generate auth key:

  • Reusable:
  • Pre-approved:
  • Ephemeral: ✗ (we want devices to persist)
  • Tags: tag:displaysync-sign
  • Expiration: 1–90 days (the lower the better; rotate when needed)

Copy the key. You'll stage it on the image in step 4. The key won't be shown again — store it.

Check your plan's device cap before a big fleet

Tailscale's free plan allows 100 devices and 3 users. Enrollment hard-stops at the device cap: clone #101 boots, tries to join, and is refused — and the symptom on your side is a missing Tailscale IP on the sign, exactly like every other kind of join failure. If the event fleet is anywhere near 100 signs, raise the cap before clone #100 boots, not after you notice stragglers.

On inviting other people into your tailnet: a contractor or venue tech whose employer runs its own tailnet may hit "your organization has restricted you from joining external tailnets" when they accept. That restriction belongs to their organization and cannot be lifted from your side — have them join with a personal account instead. Also worth telling them up front: the Tailscale client holds one active profile at a time, so switching between their work tailnet and yours drops whatever session they had open — including an in-flight RDP session into one of your signs.

3. Install Tailscale on the build machine

Download the latest Windows installer from https://pkgs.tailscale.com/stable/. The installer accepts MSI properties for unattended setup.

Silent install:

Start-Process msiexec.exe -ArgumentList `
  "/i tailscale-setup.msi /quiet /norestart" -Wait

After install, configure it as a system service that starts at boot:

Set-Service -Name "Tailscale" -StartupType Automatic
Start-Service -Name "Tailscale"

4. Decide how each machine joins

You could run tailscale up --authkey ... by hand right now, and the build machine would join. That is exactly the approach that falls apart the moment you clone the disk.

Here's why. A successful join doesn't keep the auth key — it keeps the node identity it earned, under C:\ProgramData\Tailscale\. Nothing in the capture flow clears that: Sysprep generalize handles Windows' own machine-specific data and leaves third-party service state untouched. So a one-shot join leaves you with one of two broken outcomes:

  • Capture while joined → every clone carries the build machine's identity. The tailnet shows one device entry flapping between machines instead of one entry per sign.
  • Log out before capture → the image carries nothing that could join, because the key isn't on disk anymore.

What works is to stop treating enrollment as a build step and make it a boot-time decision. Three ingredients, all of which survive Sysprep and cloning:

  1. The reusable auth key staged somewhere on the image — a file under C:\ProgramData\, since that's outside any user profile and comes through generalize intact.
  2. A scheduled task that runs at startup as SYSTEM, on every boot, for the life of the machine. Scheduled tasks survive Sysprep too.
  3. A record of which hardware the current identity belongs to, written when the machine joins.

That third one is what separates a reboot from a clone. Fingerprint the machine with values that are copied bit-for-bit by disk imaging but differ on every physical unit — the BIOS serial plus the lowest physical MAC works well — and store the fingerprint alongside the key when you enroll.

Then the task decides, on every boot:

What it findsWhat it does
Joined, and the fingerprint matches the stored oneNothing. This is every normal reboot.
Not joinedJoins with the staged key, then stores the current fingerprint.
Joined, but the fingerprint doesn't matchThis disk was cloned onto new hardware: clear the copied Tailscale state, restart the service, join fresh, store the new fingerprint.

Roughly:

state       = current Tailscale backend state
fingerprint = BIOS serial + lowest physical MAC

if state is "joined" and fingerprint == stored fingerprint:
    exit                      # healthy machine, healthy reboot

if state is "joined":         # ...but on different hardware: a clone
    stop the Tailscale service
    clear C:\ProgramData\Tailscale\
    start the Tailscale service

wait for the network, then:
    tailscale up --authkey <staged key> --hostname <computername> --unattended
    on success, store the current fingerprint

The join itself is the same command you'd type by hand:

& "C:\Program Files\Tailscale\tailscale.exe" up `
  --authkey "tskey-auth-XXXXXXXXXXXXXX" `
  --hostname $env:COMPUTERNAME `
  --unattended
FlagPurpose
--authkeyThe pre-authorized key from step 2, read from wherever you staged it
--hostnameShow the device under its Windows hostname in the admin console
--unattendedKeep the connection up as a system service with nobody logged in (critical for kiosks)

Because the task is a permanent fixture rather than a one-shot first-boot script, it doesn't matter how many times the build machine boots before you capture — it just stays enrolled as itself. And the same logic covers both cloning workflows: the Sysprep capture documented on this site, and straight disk duplication from a booted-state disk.

Considerations you'll hit while building this

  • Wait for a definitive answer at boot before deciding anything. The task and the Tailscale service start at the same time, and an early "no state" reading looks exactly like "this machine has never joined" — which makes a healthy sign re-enroll (and burn a key use) on every single boot. Poll until Tailscale reports a state you can trust rather than acting on the first thing you read; a minute or two of patience is enough.
  • A first join can lag boot by several minutes. The task also has to wait for the venue network. One to five minutes on a fresh clone is normal; treat it as a failure only past that on a machine that definitely has connectivity.
  • Suppress the Tailscale tray app's autostart. A logged-out Windows session pops a Tailscale login window at every logon, which is unacceptable on a wall — and nobody must ever sign in through it, because that would enroll the machine under a personal account instead of your tag, outside the ACLs from step 1. Remove its Run registry entries (machine-wide and in each user hive) and any Startup-folder shortcut during the build, and close the tray app if it's running.
  • Log what the task decided. A one-line-per-boot log next to the staged key is the difference between "this clone didn't join" and "this clone couldn't reach the network at 06:14 and retried at 06:31." You will want it during provisioning.
  • Make it idempotent. You'll re-run the setup on the build machine more than once. Registering the task with a replace-if-exists flag and overwriting the staged key keeps that safe.

The staged key is a credential sitting on every sign

Whatever you stage on the image ships to every clone in plaintext, readable by anyone who gets a keyboard on any one of them. That's a deliberate trade — unattended enrollment needs the key present — and it's why revoking the key when provisioning ends is mandatory, not hygiene.

5. Reboot and verify the build machine enrolled

Reboot. The task should enroll this machine — and unlike the old one-shot approach, the build machine staying enrolled is expected and fine. There's no log-out-before-capture step in this design.

# Device is logged in and tagged
& "C:\Program Files\Tailscale\tailscale.exe" status

# Tail-IP is assigned
& "C:\Program Files\Tailscale\tailscale.exe" ip -4

# Service is running and set to auto-start
Get-Service Tailscale | Select-Object Status, StartType

You want the device logged in, tagged tag:displaysync-sign, holding a tail-IP that answers ping from your laptop, and visible in the admin console.

Then reboot once more and confirm it did nothing the second time — no new join, no new device entry, whatever log your task writes gaining no new lines. That silence is the healthy no-op path working, and it's the check that catches the boot-race problem before it multiplies across a fleet.

6. Verify the fleet joined

Once clones are rolling, the join receipt is in your own dashboard. The sign app reads tailscale ip -4 on every heartbeat and reports it; the dashboard shows it as the Tailscale IP row on the sign's detail page, under Network. No extra configuration — if Tailscale is up on the kiosk, the dashboard shows it.

  • Per sign: a populated Tailscale IP on the sign means that machine enrolled. The row is absent while there's nothing to report — which is what a sign that didn't join looks like, and also what one looks like in the first minutes after boot, per the lag above.
  • Fleet-wide: the device count in the Tailscale admin console should equal the number of clones booted, plus the build machine. A mismatch tells you how many stragglers there are; the blank dashboard fields tell you which.

Validate clone #1 before mass-cloning: it should appear in the admin console as a new device with the build machine's entry untouched, and show its own Tailscale IP on its sign detail page.

If the field stays blank but tailscale status works on the kiosk itself, check that the kiosk user can read the Tailscale CLI and named pipe, then restart the sign app. Tailscale issues has the symptom-indexed version of this.

7. Connect VNC, RDP, or SSH (optional)

For hands-on access from a tagged support device, install VNC server during the image build. Common picks:

  • TightVNC (free, simple, sufficient for occasional support)
  • MeshCentral (heavier, includes audit logging — overkill for most)

Whatever you pick, configure it to:

  • Bind only to the Tailscale interface (don't listen on the LAN)
  • Require a password
  • Auto-start as a service

The Tailscale ACLs from step 1 are what actually gate access — VNC's password is a defense-in-depth layer.

RDP (built-in) is also a solid choice: enable Remote Desktop, ensure the kiosk user has the Remote Desktop Users role, and rely on Tailscale ACLs for the network gate.

Hostnames on clones

Clones boot with the random names Windows generates at OOBE, so the admin console fills with machine names that mean nothing to you. That's cosmetic: the dashboard maps each sign to its Tailscale IP, so you find a machine by the sign you care about, not by hostname. Tailscale also uniquifies duplicate names on its own (name-1, name-2) if you take the no-Sysprep route.

If you do rename a deployed machine, follow it with:

& "C:\Program Files\Tailscale\tailscale.exe" set --hostname NEW-NAME

That aligns the console name with the Windows name and needs no auth key.

After the event: revoke the key

The staged auth key is on every deployed sign, in plaintext. Anyone with a keyboard on any one of those machines can read it and enroll a device of their own into your tailnet under your sign tag.

So revoking it in the admin console once provisioning is done — or the event ends — is mandatory, not hygiene. Already-joined signs keep working on their node identities; only the staged copies die with the key. The short expiry you set in step 2 is the automatic backstop for the times you forget.

The trade-off is deliberate: after revocation nothing can enroll, so a sign that loses its identity later (a wiped disk, a swapped network adapter changing its fingerprint) needs a fresh key staged before it can rejoin. That's the USB recovery drive's job.

If you skip Tailscale

The dashboard's Remote Control commands still work over the regular WebSocket — you only lose the ability to open a desktop session into the kiosk. Worth the trade-off only for very small fleets you can physically reach.

What's next

Continue to Crash recovery — the Task Scheduler watchdog that restarts the sign app if it crashes.

If a clone comes up without a Tailscale IP, or a device behaves strangely after cloning, see Tailscale issues for the symptom-indexed playbook.