LAN SHARE Download
Docs · For organisations

For organisations

One file decides how LAN Share behaves on every computer you manage: which computers everyone sees, what may be received, which features are on, and where a record of every transfer goes. Deploy it with the tools you already use, or from the self-hosted Hub.

Plans and prices are on LAN Share for business. This page is the technical reference.

What each plan adds#

TierHow you get itAdds
ProA personal key, or a one-time seat pack (10, 25 or 50 computers)The Pro features on each computer; no organisation features
TeamYearly Team subscriptionPro, plus the central policy: managed computers and groups, receive rules, switched-off features, hot folders, a command on arrival, the local API, the Hub
BusinessYearly Business subscriptionTeam, plus the audit log (folder, syslog, the Hub's audit search), signed webhooks and the audit export

The tier comes from the licence key activated on the computer; lanshare license status shows it, for example LAN Share Pro: active (Team). When a subscription ends, the policy simply stops being applied; nothing is deleted. The full edition (built without the licence check) counts as Business. The Mac App Store version is for personal use and has no organisation features (no policy, Hub, audit, webhooks, hot folders or local API), so manage Macs with the DMG from lanshare.io; see editions.

Deploying keys and the policy#

LAN Share reads two plain files from a system folder, so MDM, Group Policy, Ansible, Intune or a login script can put them in place. No server of ours is involved.

FilemacOSLinuxWindows
Licence key (one line)/Library/Application Support/LAN Share/license-key/etc/lanshare/license-key%ProgramData%\LAN Share\license-key
Policy/Library/Application Support/LAN Share/policy.json/etc/lanshare/policy.json%ProgramData%\LAN Share\policy.json
  • The key activates itself the next time LAN Share starts. The environment variable LANSHARE_LICENSE_KEY works too.
  • Receive rules (receive, disable, require_encryption) are read again for every offer, so a changed file counts from the next transfer. The computers, groups and locked settings it puts into the user's list are synced when the receiver starts (the app, or lanshare receive), when a Hub delivers a new version, and at once with lanshare policy apply.
  • LANSHARE_POLICY=/path/to/policy.json points to a policy file elsewhere; then nothing else is read. Handy for testing. It is ignored when the system policy.json exists, so a user can't swap the administrator's rules for their own.
  • The system file applies whatever the licence: only an administrator can put it there, so when a subscription lapses its protective rules stay (the receive rules, require_encryption, disable, speed_limit, lock_settings, the managed computers). The paid extras (audit, webhook, on_receive, hot folders, the API, the Hub) need Team or Business. A per-user policy (also the Hub's) needs Team or Business.
  • Both files may also sit in the user's own settings folder (next to config.json). That is where the Hub puts its policy. The system file always wins, see which policy wins.

Install LAN Share as usual (the download page), then put the files in place, as root or administrator:

Jamf · Kandji · Intune script (root)
$ mkdir -p "/Library/Application Support/LAN Share"
$ echo "YOUR-KEY" > "/Library/Application Support/LAN Share/license-key"
$ cp policy.json "/Library/Application Support/LAN Share/policy.json"

If activation cannot happen right away (every seat taken, no network), it tries again the next day without bothering the user.

policy.json#

Every field is optional. Unknown fields are an error, so a typo is caught instead of silently ignored. Check a file before you deploy it, and see what is in force on a computer:

Terminal
$ lanshare policy check policy.json
$ lanshare policy show
$ lanshare policy show --json      # {"tier", "file", "in_force", "policy", "organisation", "error", "using_last_good"}

A broken policy file fails closed. When policy.json exists but can't be read (a typo, an unknown field, no permission), LAN Share never switches the rules off. It keeps the last copy that could be read (policy.last-good.json next to config.json, written at every good read) in force and says the file is broken. Without such a copy, every incoming transfer is refused until the file is fixed; Settings → Organisation says so (Every transfer is refused until the policy file is fixed). lanshare policy show and policy check then exit with status 1, and policy show --json carries the problem in error and using_last_good.

Output of policy check (shortened)
  computers         File server, Design iMac
  encryption        required
  receive folder    /Volumes/Shared/Inbox
  max size          18.6 GiB
  blocked types     exe, msi, sh
  only from         10.0.0.0/16, File server
  switched off      phone, mcp
  speed limit       200 MiB/s
✓ policy.json is a valid policy
policy.json · everything at once (comments are for reading; JSON has none)
{
  // Team: computers everyone gets, in groups; users can't delete them
  "hosts": [
    { "name": "File server", "address": "10.0.0.10", "group": "Servers", "trusted": true },
    { "name": "Design iMac", "address": "design.local:45821", "group": "Studio" }
  ],
  "require_encryption": true,
  "receive": {
    "folder": "/Volumes/Shared/Inbox",
    "max_bytes": 20000000000,
    "blocked_extensions": ["exe", "msi", "sh"],
    "only_from": ["10.0.0.0/16", "fd00::/8", "File server"]
  },
  "disable": ["phone", "mcp"],
  "speed_limit": 200,
  "lock_settings": true,

  // Team: automation
  "on_receive": ["/usr/local/bin/scan-and-file"],
  "hot_folders": [{ "path": "/Users/Shared/Outbox/Accounting", "to": "Accounting", "after": "move" }],
  "api": { "port": 45822, "token": "at least 16 characters" },

  // Business: a record of every transfer
  "audit": { "dir": "/Volumes/Audit/lanshare", "syslog": "siem.example.com:514" },
  "webhook": { "url": "https://hooks.example.com/lanshare", "secret": "a long random string", "events": ["received", "rejected"] }
}

Every field#

FieldTypeEffect
hostsobject[]Computers every user gets in their list, marked as managed: they cannot be removed. Groups named here are created (also managed). Dropping a computer from the policy takes it out of the lists again. A computer the user had saved already at the same address is adopted (it becomes the managed one), and when the policy drops it, it goes back to the user with its old name and group.
hosts[].namestringThe name shown.
hosts[].addressstringhost, IP, or either with :port. Required.
hosts[].groupstringThe group it goes in.
hosts[].trustedbooleanTransfers from it are accepted without asking. From the Hub, only the owner can set it.
require_encryptionbooleanRefuse unencrypted connections (very old versions), and keep encryption switched on.
receive.folderpathEveryone receives here; the user's own choice no longer applies. A full path; ~/ is each user's home folder; a relative path makes the policy file broken (the last good one stays). The same goes for audit.dir and hot_folders[].path. From the Hub, only the owner can set it.
receive.max_bytesintegerThe largest transfer accepted, in bytes (the total of an offer).
receive.blocked_extensionsstring[]File endings never accepted, with or without the dot, any case: ["exe", "dmg", "sh"]. Every file in an offer is checked, also inside folders.
receive.only_fromstring[]Only these senders: IP addresses, IPv4 or IPv6 ranges (10.0.0.0/8, fd00::/8) or names of computers in hosts. A name is matched through that host's address (looked up when it is a host name) against the address the offer comes from, never against the name the sender gives itself. Empty: anyone on the network, who is then asked about.
disablestring[]Features switched off: phone (the phone and browser page), sync (sync transfers are neither sent nor accepted), text (text is neither sent nor accepted), mcp (lanshare mcp will not start).
speed_limitintegerSending speed limit in MiB/s, for everyone. 0 means no limit (policy check flags it: leave the field out instead).
lock_settingsbooleanLocks the settings: in the apps the Receiving, Trusted, Compression, Exclusions and Speed pages are read-only with "Set by your organisation", and lanshare config … / lanshare trust add|remove refuse with "settings are locked by your organisation's policy". Combine it with the values you want everyone to have (receive.folder, speed_limit, require_encryption). Removing the policy unlocks them.
on_receivestring[]Team. A program and its arguments, run after files arrive. Details
hot_foldersobject[]Team. {path, to, after, message}: folders sent automatically by lanshare automate. Details
apiobjectTeam. {port, token} for lanshare api; the token needs 16 characters or more. Details
audit.dirpathBusiness. A folder (often a network share) that gets one JSON line per transfer. Format
audit.syslogstringBusiness. A syslog server, host:port, over UDP.
webhook.urlstringBusiness. An https:// (or http://) address that gets a POST for each transfer. Format
webhook.secretstringBusiness. Signs each request.
webhook.eventsstring[]Business. Which of received, sent, rejected, failed; empty means all.

lanshare policy check also catches what the format alone cannot: a host without an address, an unknown feature in disable or event in webhook.events, a webhook address that is not http(s), a hot folder without to or with an unknown after, and an API token shorter than 16 characters.

How receive rules apply#

Rules are checked on the receiving computer for every offer, before the user is asked and before anything is written; uploads from the phone page follow the same rules. The phone page is plain HTTP: with require_encryption uploads from it are refused, and a share that is running stops when a policy switches phone off. In order: encryption (if required), text (if switched off), sync (if switched off), the sender (only_from), the size, then the file types. A refused offer is answered as declined, and recorded in the history (and the audit log) as rejected with the reason, for example refused by policy: setup.exe: this type of file is not accepted here.

Audit log and syslog#

With audit.dir, each computer appends one JSON line per transfer (sent, received, rejected, failed) to <computer>-<YYYY-MM>.jsonl in that folder: one file per computer and month. Each line is the history record plus who and where:

studio-mac-2026-11.jsonl (one line, wrapped here)
{
  "event": "rejected",
  "at": "2026-11-03T09:14:02+00:00",
  "computer": "studio-mac",
  "user": "anna",
  "id": 1793697242516678000,
  "time": 1793697242,
  "direction": "received",
  "kind": "files",
  "peer": "unknown-laptop",
  "address": "192.168.1.88",
  "items": ["setup.exe"],
  "paths": [],
  "files": 1,
  "bytes": 5242880,
  "wire_bytes": 0,
  "compressed": false,
  "duration_ms": 0,
  "status": "rejected",
  "error": "refused by policy: setup.exe: this type of file is not accepted here",
  "encrypted": true
}
event
sent, received, rejected (declined, also by policy) or failed (also cancelled).
at
When it finished, RFC 3339 in UTC. time is the same in Unix seconds.
computer, user
This computer's name and the user LAN Share runs as.
peer, address
The other computer: its name and the address used.
items, paths
Top-level names; where they were saved (received) or sent from (sent).
files, bytes
Count and total size. wire_bytes: what crossed the network.
encrypted, fingerprint
encrypted appears only when the connection was encrypted (true); fingerprint, the other computer's key, when it is known: not on an offer refused before the transfer, as above.
message, error
The sender's note; why it failed or was refused (each only when there is one).
also possible
skipped_bytes, versioned (sync kept old versions), auto_accepted, key_changed.

With audit.syslog the same line goes to your syslog server over UDP as an RFC 5424 message, facility local0, severity informational, app name lanshare, the event as message id:

syslog message
<134>1 2026-11-03T09:14:02+00:00 studio-mac lanshare - rejected - {"event":"rejected","at":"2026-11-03T09:14:02+00:00",…}

Writing the log never slows down or fails a transfer: a folder that cannot be written, or a server that does not answer, is reported on stderr and the transfer goes on.

Webhooks#

With webhook.url, every transfer (or the events you chose) is POSTed as the same JSON as an audit line. Headers:

Content-Type
application/json
X-LanShare-Event
received, sent, rejected or failed
X-LanShare-Signature
sha256= and the hex HMAC-SHA256 of the request body with the secret as key: hmac_sha256(secret, body), the same scheme GitHub and Stripe-style webhooks use. Only when secret is set.
User-Agent
LAN Share/<version>

Each computer sends its own requests, right after the transfer, with a 10-second time limit and no retries: a failure is reported on that computer's stderr. For a complete record, use the audit log as well.

Verify the raw body

The signature is over the exact bytes received. Compute it before parsing the JSON, compare in constant time, and refuse requests without it. Every language has HMAC in its standard library, as below.

webhook.py · Python 3, standard library only
import hashlib, hmac, json, os
from http.server import BaseHTTPRequestHandler, HTTPServer

SECRET = os.environ["LANSHARE_WEBHOOK_SECRET"].encode()

def valid(body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")

class Hook(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
        if not valid(body, self.headers.get("X-LanShare-Signature")):
            self.send_response(401); self.end_headers(); return
        event = json.loads(body)
        print(event["event"], event["computer"], event["peer"], event["items"], event.get("error", ""))
        self.send_response(204); self.end_headers()

HTTPServer(("0.0.0.0", 8080), Hook).serve_forever()

Test your endpoint by hand, signing the body the same way:

Terminal
$ body='{"event":"received","computer":"studio-mac","peer":"NAS","items":["report.pdf"]}'
$ sig="sha256=$(printf '%s' "$body" | openssl dgst -sha256 -hmac "$LANSHARE_WEBHOOK_SECRET" -r | cut -d' ' -f1)"
$ curl -i -H "Content-Type: application/json" -H "X-LanShare-Event: received" \
    -H "X-LanShare-Signature: $sig" -d "$body" http://localhost:8080/

Audit export#

Every transfer in a computer's history, for an auditor or a spreadsheet (Business):

Terminal
$ lanshare audit export --days 90 -o transfers-q4.csv
$ lanshare audit export --format json --days 30 | jq 'select(.event == "rejected")'
OptionDescription
--days <N>Only the last N days. Default: everything in the history.
--format csv|jsonCSV (default) or JSON lines in the audit format.
-o, --out <FILE>Write to a file instead of the terminal.
CSV columns
event,at,computer,user,peer,address,items,files,bytes,encrypted,fingerprint,status,error,message

Several items in one transfer are joined with | in the items column.

LAN Share Hub#

The Hub is a small console you run yourself, on a VM, a NAS or in Docker. It is part of the lanshare command (Team and Business); nothing goes through a server of ours. Computers join it once and then check in every 5 minutes, so you can change the policy for all of them from one place, without MDM.

  • Overview: the fleet in numbers, transfers per day, policy rollout per group, recent transfers and recent changes (who changed what).
  • Fleet: every computer that checked in, with its user, system, version and build, tier, last seen (online within 10 minutes), last transfer and the policy version it applied. Filter by status, group, tier and system; choose the columns; give a computer a display name, tags and notes (kept in the Hub, never sent to the computer); select several to move them to a group, export them or forget them; computers not seen for 14 days are marked stale and can be forgotten in one go. A computer used by several people (each with their own login) has a row for each user. A computer's panel shows whether it has its secret and has Re-key and Forget.
  • Groups (a dialog from the Fleet page): put computers in groups (a name and a colour). Each group follows the default policy or has its own.
  • Policy: edit policy.json in a form (managed computers, receive rules, features, security, audit, webhook, automation) or as JSON; both stay in sync and are checked exactly like lanshare policy check while you type. Saved as numbered versions with history, a diff between any two and restore.
  • Audit (Business): every transfer of every computer, searchable by text, event and date, downloadable as CSV.
  • Onboarding: the join command, a join link, hub.json for MDM (all with the fingerprint of the Hub's signing key), and a new fleet token when you need one.
  • Settings: the Hub's name, how long audit lines are kept, limits against floods (new computers per address and hour, new computers a day, audit lines a day), backup and restore, alerts, admin accounts and the activity log.

Press ⌘K / Ctrl-K anywhere to jump to a page, a computer, a group or an action, ? for all shortcuts, G then O/F/P/A/S to go to a page.

Run it#

Terminal
$ lanshare hub serve --listen 0.0.0.0:45830 --data /var/lib/lanshare-hub
OptionDescription
--data <DIR>requiredThe Hub's folder. Everything is files there (the fleet, groups, the policy versions, the audit lines, admin accounts, settings); back it up (Settings → General → Download backup gives a zip of it; restore it there too), there is no database. Folders of older Hub versions are read as they are.
--listen <ADDR>Address and port. Default 0.0.0.0:45830.
--trusted-proxy <IP|CIDR,…>The reverse proxies whose X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host are believed (also LANSHARE_HUB_TRUSTED_PROXY). Default: none. X-Forwarded-For is then believed from no one; X-Forwarded-Proto and -Host are still believed from this machine (a proxy on the same host gets Secure cookies). When a request from this machine carries X-Forwarded-For without the flag, the Hub warns in its log at every start and Settings shows Set --trusted-proxy: until then every client counts as the server itself.
--admin-token <T>Sets or replaces the owner's admin token (also LANSHARE_HUB_ADMIN_TOKEN): at least 24 characters, at least 10 different ones (openssl rand -hex 24 makes one). Without it, a token is made on the first start and printed once; only a keyed hash of it (HMAC-SHA256) is kept. Setting a new one is also how you reset a lost token.

Docker: there is no published image. Build one from hub/Dockerfile in the LAN Share source (docker build -f hub/Dockerfile -t lanshare-hub .), or use hub/docker-compose.yml, which builds it and puts Caddy in front for HTTPS (on a fixed network, 172.30.83.0/24, with Caddy at 172.30.83.10 as the trusted proxy). On the first start the admin token is in the log (docker compose logs hub).

HTTPS

The Hub speaks plain HTTP. Put a reverse proxy with a certificate in front of it (Caddy, nginx, Traefik) before computers join over anything but a trusted LAN. It believes X-Forwarded-For only from a --trusted-proxy: with the proxy on the same host, start it with --trusted-proxy 127.0.0.1. Without the flag it still believes X-Forwarded-Proto and X-Forwarded-Host from this host, so its sign-in cookie (12 hours) is marked Secure behind HTTPS. See the security model.

Caddyfile
hub.example.com {
	encode gzip
	reverse_proxy 127.0.0.1:45830
}

Join computers#

A joining computer needs a Team or Business subscription (the full edition counts as Business).

On each computer
$ lanshare hub join https://hub.example.com --token <fleet token> --key <key fingerprint>   # both on the Onboarding page
$ lanshare hub join 'https://hub.example.com/join#token=…&key=…'   # or the join link alone, in quotes
$ printf '%s' "$FLEET_TOKEN" | lanshare hub join https://hub.example.com --token-stdin --key <key fingerprint>   # keeps the token out of the process list
$ lanshare hub status        # the Hub, the last check-in, the policy version, any warning
$ lanshare hub checkin       # now, instead of waiting
$ lanshare hub leave         # forget the Hub and remove the policy it delivered (hub-identity.json stays)
  • The join link from the Onboarding page (also as a QR code) carries the fleet token and the key fingerprint. Give it to hub join in quotes: a shell cuts it at # and & (in Command Prompt use double quotes). The apps take the same link: paste it into Settings → Organisation → Join a Hub. A --token or --key next to the link must be the link's own, else the join is refused.
  • join checks in once before it saves anything (--no-check skips that), then writes hub.json ({"url", "token", "key"}) to the user's settings folder.
  • --key (key in hub.json) is the fingerprint of the Hub's signing key, on the Onboarding page and in the message hub serve prints at start. With it, the computer takes only answers signed with that key from the very first one. Without it (older join instructions) it trusts the key the Hub answers with first, and hub status warns about that.
  • An http:// address that is public is refused unless you add --insecure; a private one joins with a warning. The apps refuse public http without an override. Use https://; see the security model.
  • The first check-in gives the computer its own secret (in hub-identity.json) and pins the Hub's signing key (in hub-state.json), both in the user's settings folder and readable only by the user, like the Hub policy and the audit queue. Each user of a computer has their own. hub-state.json belongs to one Hub address: joining another Hub starts it clean.
  • With MDM, deploy the same hub.json in the system folder (the folders above, where a system policy.json would go; you don't need one). Users can't leave a system hub.json: hub leave then removes only their own one, keeps the Hub's policy and says Still managed by ….
  • While LAN Share receives (the app or lanshare receive), it checks in at start and every 5 minutes. A new policy version is written to the user's settings folder as policy.json and applied; if the Hub has no policy any more, the file is removed.
  • Business: every audit line also goes to the Hub. While the Hub cannot be reached, lines wait in a queue (up to 50 MiB) and are sent with the next transfer or check-in.

Groups and their policy#

A group gets the default policy until you press Use own policy on the Policy page (you start from a copy of the default); Back to default undoes that and keeps the group's versions. A computer gets the policy of its group, or the default when it has none. All versions share one counter, so a computer that moves to another group, or whose group goes back to the default, always sees a different version number and applies it on its next check-in. A number is never given out twice, also after a restore or an undo. Deleting a group puts its computers back on the default.

Admins and roles#

The token the Hub was started with belongs to the owner. In Settings → Access the owner adds people with their own token (shown once; only a keyed hash of it, HMAC-SHA256, is stored) as viewer or admin, changes their role or revokes them (their sessions end at once).

WhatViewerAdminOwner
Overview, fleet, groups, policies and their versions, audit search, the bell✓✓✓
Check a policy without saving it✓✓✓
Computers: display name, tags, notes, group, re-key, forget–✓✓
Create, change and delete groups–✓✓
Save, restore and inherit policies–✓, keeping or removing the fields below, not adding or changing them✓
Policy fields that run programs, move files, write or send data out: on_receive, hot_folders, api, webhook, receive.folder, audit.dir, audit.syslog, trusted computers (hosts[].trusted)–keep or remove✓
Onboarding page (join command, hub.json, the fleet token)–✓✓
Settings (name, retention), alerts, the activity log–✓✓
A new fleet token––✓
Download a backup, restore, undo a restore––✓
Admin accounts––✓

Roles are checked by the Hub on every call (403 otherwise), not only hidden in the console, which shows the owner-only fields locked as Owner only for admins, from the start, with a Remove button for each one the policy has. An admin may keep those fields unchanged or remove them; saving or restoring a policy that adds or changes one answers 403 with owner_fields. So does moving computers to another group (or none), sending a group back to the default or deleting a group, when that gives computers owner-only fields they don't have now. Viewers see a policy's api.token and webhook.secret as (hidden). Every change (policies, groups, computers, tokens, admins, settings, alerts, backups, sign-ins) is recorded with who and when in Settings → Activity.

Security model#

  • HTTPS, strongly recommended. The fleet token, admin tokens and every policy travel over this connection, and a policy can run programs on every computer that joined. Put the Hub behind a reverse proxy with a certificate.
  • Plain http only on a trusted LAN. lanshare hub join refuses an http:// address that is public unless you add --insecure; a private address (10/8, 172.16/12, 192.168/16, IPv6 ULA, link-local) joins with a warning. The apps' join refuses public http, with no override. lanshare hub status (and Settings → Organisation in the apps) shows a warning line for a Hub on plain http to anything but this computer, right after joining too.
  • Over plain http the dangerous fields are ignored. A computer that checks in with a non-loopback Hub over http does not apply the owner-only fields: on_receive, hot_folders, api, webhook, receive.folder, audit.dir, audit.syslog, and hosts[].trusted (those computers become untrusted). The rest of the policy applies.
  • Signed policies, made for each check-in. The Hub has its own Ed25519 key (signing-key.pk8 in the data folder). A computer sends a random nonce with every check-in, and the Hub signs its reply together with that nonce, the computer's id, the policy version and the policy (or a removed mark when it has none for it); for a current computer also the fleet entry it filed the check-in under (the computer and its user) and the secret it gives out with the reply. So a reply recorded on the way can't be played back later or to another computer, nobody can swap the user to get another user's policy, and nobody can plant another secret. Joined with the key fingerprint, a computer takes only replies signed with that key from the first one; otherwise it remembers the key at the first signed reply. From then on it refuses unsigned replies, forged ones and replies signed with another key. Replies in an older format (from an older Hub) are taken only until the first one in the newer format, and may not lower the policy version or take the policy away. So don't roll the Hub's program back to an older version: computers that have had the newer replies refuse the older ones until they leave and join again. Restoring a backup from a different Hub changes the key: those computers must leave and join again.
  • A secret per computer. At its first check-in a computer gets its own secret (kept in hub-identity.json, readable only by the user); the Hub stores only its SHA-256. Later check-ins and audit uploads without the right secret are refused (401), so a stolen fleet token alone cannot pose as an existing computer. A computer from before the secrets gets one at its next check-in only within 14 days of the Hub's upgrade; after that an admin clicks Re-key in its panel first (every secret given to a computer already in the fleet is noted in the activity log and raises an alert). After those 14 days the Hub also refuses check-ins and audit lines that come without a secret at all, from such a computer (ask an admin to re-key; it needs a current LAN Share) and from a new one. Re-key is also the way back for a computer that lost hub-identity.json (for example after a reinstall): for a week, its next check-in takes a new secret; its old secret keeps working until then. hub leave keeps that file, so rejoining works.
  • A Hub policy changed on the computer. At every check-in the computer compares the Hub's policy.json with what it wrote; when it was edited (or deleted), it is written again, and the Fleet flags the computer modified locally (the activity log says so too).
  • Roles. Viewers read; admins run the fleet, groups and policies; only the owner sets the owner-only fields, downloads or restores backups, makes a new fleet token and manages admins (table).
  • Webhooks. The alert webhook and a policy's webhook may not go to this server, a link-local or a cloud-metadata address unless the owner allows local addresses; checked when saving and, for alerts, when sending, where the address looked up is the one connected to. Every failed alert delivery reads the same in the console; the details are in the Hub's log.
  • Sign-in and the console. The token is checked first, so the right one always gets in. After 5 wrong tokens from one address the Hub answers further wrong ones only after 1, 2, 4… up to 60 seconds (also after 50 failures overall), with 429 and Retry-After; failures are recorded in the activity log with the address. A script's wrong Bearer token is answered after a short delay, and while its address waits, its requests get 429 without the token being looked at. Requests that use the session cookie and change something must come from the console itself (matching Origin) with a JSON body; scripts using Authorization: Bearer are not affected.
  • Behind a proxy. X-Forwarded-For is believed only from the addresses in --trusted-proxy (default: none); X-Forwarded-Proto and -Host also from this machine. Behind a proxy on the same host, set --trusted-proxy 127.0.0.1: the Hub's log (at every start) and Settings remind you when it is missing.
  • Files. The data folder is readable only by the Hub's user (folder 0700, files 0600, fixed at every start). Its admin token hashes are keyed HMAC-SHA256. The owner's key (owner-key) never goes into a backup; the admins' key (token-key) does, or the admins couldn't sign in after a restore (see backup).
  • Limits. 8 MiB per request (413), request headers 16 KiB and 100 lines (431), 10 seconds to send them and 30 seconds for the body (408), the token checked before a body is read, a body on a request that takes none refused before it is read (400), 256 connections at once, 32 from one address (an IPv6 one by its /64) and 128 from this machine itself (503), 10 000 computers (403), 256 characters per text field of a check-in, 30 new computers per address and hour and 500 a day, 50 MiB of audit lines per computer and 1024 MiB for the whole fleet per UTC day (429; the limits in Settings, 0 for none).
  • A system policy wins. A policy.json deployed by MDM always beats the Hub's (which policy wins).

Backup and restore#

Download backup (Settings → General, owner) gives the whole data folder as one zip: the fleet, groups, every policy version, audit lines, settings, alerts, the activity log, admin accounts (their token hashes and the key those are made with), the signing key and the fleet token. Keep it like a password: whoever has it can pose as the Hub to its computers and join computers to it. The owner's token hash is in it but not its key, so it can't be guessed at. It is written as it is read, so a large Hub needs no extra memory; backups over 4 GiB use zip64, which every current unzip tool reads.

The owner restores a backup on the same page:

  1. Restore from a backup: choose or drop the zip. The Hub checks it first and changes nothing: the page shows how many computers, groups, policy versions and admins it holds, the audit size and when it was made.
  2. Replace the Hub's data with this backup, then confirm. The current data is set aside in the data folder (.before-restore-…, the last 3 are kept), the backup's files take its place, and the Hub carries on with them at once.
  3. Undo last restore puts back the data from just before it.
  • Computers keep their current secrets: for a computer in both the backup and the Hub, the secret it has now stays (a secret isn't policy), so one re-keyed after the backup keeps working. The summary says how many.
  • The activity log keeps everything: a restore adds the backup's lines to the current ones (each once, by time) and then its own; an undo takes nothing away either and adds restore undone.
  • A backup from a different Hub brings that Hub's signing key, so computers that joined this Hub refuse its policies: they must leave and join again. A backup from this version does not restore on an older Hub.
  • Your owner token stays: a backup's owner token is never restored. Everyone else is signed out (their sign-in page says why); computers carry on with the backup's fleet token.
  • Only a zip as the Hub made it is accepted: every file is checked (its checksum, its name, its contents) before anything changes. A zip that was unpacked and compressed again by another tool is refused; use the original.
  • Up to 16 GiB from the console. Behind a reverse proxy, allow bodies that large for /api/admin/restore (Caddy has no limit by default; in nginx, client_max_body_size 16g;). For a bigger backup, stop the Hub and unzip it as the --data folder.

Alerts#

Settings → Alerts: a computer not seen for N hours, more than N rejected transfers in an hour, a computer still behind on policy N hours after a new version, a new computer joining. Checked every minute; an alert fires once and again only after its condition cleared. Alerts appear under the bell in the console and/or go to a webhook as a JSON POST {"event": "alert", "hub", "at", "alerts": [{"rule", "key", "title", "text", "machine"}]} (hub: the Hub's name, else the server's host name) with the header X-LanShare-Event: alert, signed like transfer webhooks but in X-LanShare-Hub-Signature: sha256=<hex HMAC-SHA256 of the body>.

Which policy wins#

  1. The system policy.json (deployed by MDM). When it exists, LANSHARE_POLICY is ignored.
  2. LANSHARE_POLICY, when set (and there is no system file): nothing else is read.
  3. The user's policy.json: where the Hub writes its policy.

So a policy from MDM always beats the Hub's; the Fleet page shows such a computer's policy as system instead of a version number. The user's folder is writable by the user: when people must not be able to change the rules, deploy the policy as the system file.

Hub API#

For scripts against the console, send Authorization: Bearer <admin token> (the owner's or an admin's; the role decides what it may do, as in the table above), or sign in for a session cookie. With the cookie, a request that changes something must send a matching Origin (else 403) and Content-Type: application/json (application/zip for a restore; else 415). Every answer is JSON; an error is {"error": "…"} with 400, 401 (not signed in), 403 (the role is not enough) or 404.

CallWhat it doesRole
POST /api/admin/login{"token"} → {"ok", "role", "name"} and the session cookie (12 hours); 429 with Retry-After after repeated failures. POST /api/admin/logout ends the session.Any
GET /api/admin/me{"ok", "id", "name", "role", "hub_name", "now"}: who you are, and the Hub's clock (Unix seconds).Viewer
GET /api/admin/statsThe Overview numbers: ?days=14&tz=<minutes east of UTC> (1–90 days, counted in that time zone).Viewer
GET /api/admin/fleetEvery computer that checked in (one entry per computer and user: machine is the entry, machine_id the computer, users how many it has), with its group, display name, tags, notes, expected_version, stale, has_secret and rekey_until; ?format=csv[&machines=a,b] for CSV (tags joined with ; , cells that could run as spreadsheet formulas are prefixed with ').Viewer
POST /api/admin/forget{"machine"} or {"machines": [...]}: removes computers from the fleet list; POST /api/admin/forget-stale {"days": 14} every one not seen for that long.Admin
POST /api/admin/computer{"machine", "display_name", "notes", "tags"}: the Hub's own fields of a computer.Admin
POST /api/admin/rekey{"machine"}: the computer may take a new secret at its next check-in (within a week); its old one stops working.Admin
GET /api/admin/groupsThe groups.Viewer
POST /api/admin/groupsCreate ({"name", "colour"}); PUT/DELETE /api/admin/groups/<id>; POST /api/admin/assign {"machines": [...], "group": id | null}.Admin
GET /api/admin/policyThe current policy and its history (?group=<id> for a group's); GET /api/admin/policy/<N> one version.Viewer
POST /api/admin/policy/validateBody: the policy JSON (?group=<id> to compare with a group's). Checks it and saves nothing: {"ok", "error", "problems", "line", "column", "owner_fields", "owner_only"}; owner_fields lists the owner-only fields it would add or change.Viewer
PUT /api/admin/policyBody: the policy JSON (?group=<id>: the group's own). 400 with problems when it is invalid; the same policy again keeps its version.Admin; owner for the owner-only fields
POST /api/admin/policy/restore{"version": N, "group"?}: saves that version again as a new one → {"version", "changed", "restored"}. POST /api/admin/policy/inherit {"group"} puts a group back on the default.Admin; owner for the owner-only fields
GET /api/admin/audit?q=&event=&from=YYYY-MM-DD&to=YYYY-MM-DD&tz=&machine=&limit=500&offset=0, newest first → {"total", "offset", "more", "counts", "machines", "lines"}; &format=csv for CSV.Viewer
GET /api/admin/onboarding{"fleet_token", "key"}: the token computers join with and the fingerprint of the signing key (the console builds the join command and hub.json from them).Admin
POST /api/admin/fleet-tokenMakes a new fleet token; computers must join again.Owner
GET/POST /api/admin/adminsList, or add {"name", "role": "viewer" | "admin"} (the reply has the token, once); PUT/DELETE /api/admin/admins/<id>.Owner
GET /api/admin/activityEvery change, newest first: ?q=&action=&limit=100&offset=0 → {"total", "offset", "more", "lines"}.Admin
GET/PUT /api/admin/settings{"hub_name", "audit_retention_days", "new_computers_per_ip_hour", "new_computers_per_day", "audit_mb_per_day"} (0 keeps audit lines forever, or means no limit) and, on GET, version, uptime, data folder size and key_fingerprint.Admin
GET/PUT /api/admin/alertsThe alert rules and delivery. GET returns has_secret and allow_local, never the webhook secret; PUT merges the fields you send ("webhook_secret": "" clears it). Webhooks to loopback, link-local or cloud-metadata addresses are refused, when saving and when sending, unless allow_local, which only the owner may set; redirects are not followed. POST /api/admin/alerts/test sends a test.Admin; owner for allow_local
GET /api/admin/notificationsThe bell: {"unread", "last_read", "items"}; POST /api/admin/notifications/read marks them read.Viewer
GET /api/admin/backupThe data folder as a zip (zip64 over 4 GiB).Owner
POST /api/admin/restoreBody: a backup zip (Content-Type: application/zip). Replaces the Hub's data (the owner token stays); ?dry_run=1 only checks it and returns what it holds. GET /api/admin/restore says whether the last restore can be undone; POST /api/admin/restore/undo undoes it.Owner
POST /api/v1/checkin, POST /api/v1/auditWhat computers call, with Authorization: Bearer <fleet token>, X-LanShare-Machine-Secret and (check-in) X-LanShare-Nonce; not for scripts. The check-in reply carries policy, policy_version, public_key, signature, signature_version (2), removed and, the first time, machine_secret. Audit for an unknown computer: 404.Fleet token
GET /healthzFor your monitoring; no token.Any
Example: put a new policy live
$ curl -s -X PUT -H "Authorization: Bearer $HUB_ADMIN_TOKEN" \
    --data-binary @policy.json https://hub.example.com/api/admin/policy

Something missing or wrong? Write to [email protected].