Signed policy#
A signed policy is one file an IT team pushes to every machine with the tool it already uses, such as Intune, Jamf or Ansible. It sets floors that the machine’s own settings can only tighten. This page is for the platform and security admins who write, sign and deliver it.
How it works#
Lockpin reads the policy at start and within five seconds of a change. What the file sets is a
floor: the flags, lockpin.json, the dashboard, an agent’s headers and a capability’s caveats
can only tighten it, and nothing on the machine can loosen it without an administrator.
A file that is unsigned, signed by a key the machine does not pin, edited after signing or damaged is refused and logged, never ignored. The last good policy stays in force.
The file has no field for a provider key or a token. Nothing in it is uploaded.
While a policy is in force, Lockpin forwards only calls it meters, the model list and token counts, so nothing spends outside the policy’s limits. Fallbacks and keep-alive pings follow its model rules too.
What the file can set#
Key |
Effect |
|---|---|
|
The budget a new task gets, if lower than |
|
A hard ceiling per task. An approval at the cap, the meter or a budget header cannot pass it. |
|
Dollar limits per day, week, month or total for every call through this Lockpin, each team key, each user of a key, and each tag value ( |
|
|
|
A shorter wait at the cap. |
|
|
|
|
|
|
|
|
|
|
|
The private root CA file, used instead of |
|
Turns the audit export on, to an absolute folder, from the next start. |
|
The signed price feed, read from the machine policy only. See The price feed setting. |
A file with any key not in this table is refused, as is one with a key written twice or not
in lower case. A provider rule covers the provider’s family: bedrock covers
bedrock-invoke.
{
"lockpin_policy": 1,
"name": "acme-engineering",
"revision": 4,
"policy": {
"default_task_budget_usd": 2.00,
"max_task_budget_usd": 25,
"limits": { "machine": { "day": 50, "month": 600 }, "user_day_usd": 10 },
"models": { "deny": ["*opus*"] },
"ask_at_cap": "0s",
"on_fault": "block",
"reporting": { "allowed": false }
}
}
Bump revision on every change. A running Lockpin refuses a lower revision than the one it
holds, so an old signed copy cannot be put back.
Note
Point ca_file at a file only administrators can change, since every outbound connection
trusts what is in it. See Corporate proxies and custom CAs.
The price feed setting#
price_feed is read from the machine policy only. With auto_update: true the proxy fetches
the signed price feed every refresh (default 6h), off the call path.
Field |
What it does |
|---|---|
|
|
|
How often to fetch. Default |
|
An organisation’s mirror of the feed: |
|
Up to 8 extra base64 public keys the mirror’s feed may be signed by. |
|
Refuse a feed issued longer ago than this. Default |
A failed fetch keeps the last good prices. See Prices and the price feed.
Before you begin#
Lockpin is installed on IT’s own machine, to make the key and sign the file.
You have a way to put files on managed machines, in a folder only administrators can write.
Sign a policy#
Do this on IT’s machine, never on a managed one.
Make a signing key pair:
lockpin policy keygen -out ./lockpin-keys -name acme
acme.keyis the private key: keep it with your code-signing material and never copy it to a managed machine.acme.pubis what the machines pin. On Windows the.keyfile takes the permissions of its folder, so make it in a folder only you can read.Pin the public key in a folder you will push:
lockpin policy trust -policy-dir ./mdm acme.pub
Write
lockpin-policy.json, then sign it into that folder:lockpin policy sign -key ./lockpin-keys/acme.key -out ./mdm/lockpin-policy.json lockpin-policy.json
signchecks the file first and refuses one the machines would refuse.Check it as a machine would:
lockpin policy verify -policy-dir ./mdm ./mdm/lockpin-policy.json
The mdm folder now holds the two files to push: lockpin-policy-trust.json (the pinned key)
and lockpin-policy.json (the signed policy).
Deliver it#
Put both files in the machine’s policy folder. Write each to a temporary name and rename it, so Lockpin never reads half a file.
System |
Folder (administrators write, everyone reads) |
|---|---|
Windows |
|
macOS |
|
Linux |
|
- name: Lockpin policy folder
ansible.builtin.file:
path: /etc/lockpin
state: directory
owner: root
group: root
mode: "0755"
- name: Pinned policy key
ansible.builtin.copy:
src: mdm/lockpin-policy-trust.json
dest: /etc/lockpin/lockpin-policy-trust.json
owner: root
group: root
mode: "0644"
- name: Signed policy
ansible.builtin.copy:
src: mdm/lockpin-policy.json
dest: /etc/lockpin/lockpin-policy.json
owner: root
group: root
mode: "0644"
validate: /usr/local/libexec/lockpin/lockpin policy verify -policy-dir /etc/lockpin %s
validate keeps a file the machine would refuse from replacing a good one. The path is a
node’s copy of lockpin; use wherever lockpin is on your machines.
Run as root from a Jamf policy:
#!/bin/sh
set -e
dir="/Library/Application Support/Lockpin/policy"
mkdir -p "$dir"
chown root:wheel "/Library/Application Support/Lockpin" "$dir"
chmod 755 "/Library/Application Support/Lockpin" "$dir"
cat > "$dir/.trust.tmp" <<'JSON'
PASTE lockpin-policy-trust.json HERE
JSON
cat > "$dir/.policy.tmp" <<'JSON'
PASTE lockpin-policy.json HERE
JSON
chmod 644 "$dir/.trust.tmp" "$dir/.policy.tmp"
mv -f "$dir/.trust.tmp" "$dir/lockpin-policy-trust.json"
mv -f "$dir/.policy.tmp" "$dir/lockpin-policy.json"
A package that lays down the same two files, root-owned and mode 644, works as well.
Run as a platform script or a Win32 app, as SYSTEM, 64-bit. A user may create folders under
ProgramData, so the script refuses a folder that is a link and makes the folders the
administrators’ before it writes.
$ErrorActionPreference = "Stop"
$dir = "$env:ProgramData\Lockpin\policy"
New-Item -ItemType Directory -Force $dir | Out-Null
foreach ($p in @("$env:ProgramData\Lockpin", $dir) + (Get-ChildItem -Recurse -Force $dir | ForEach-Object FullName)) {
if ((Get-Item -Force $p).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "$p is a link; remove it first" }
}
icacls "$env:ProgramData\Lockpin" /setowner "*S-1-5-32-544" | Out-Null
icacls "$env:ProgramData\Lockpin" /inheritance:r /grant:r "*S-1-5-32-544:(OI)(CI)F" "*S-1-5-18:(OI)(CI)F" "*S-1-5-32-545:(OI)(CI)RX" | Out-Null
icacls $dir /setowner "*S-1-5-32-544" /T | Out-Null
icacls $dir /inheritance:r /grant:r "*S-1-5-32-544:(OI)(CI)F" "*S-1-5-18:(OI)(CI)F" "*S-1-5-32-545:(OI)(CI)RX" | Out-Null
$files = [ordered]@{
"lockpin-policy-trust.json" = @'
PASTE lockpin-policy-trust.json HERE
'@
"lockpin-policy.json" = @'
PASTE lockpin-policy.json HERE
'@
}
foreach ($name in $files.Keys) {
$tmp = Join-Path $dir ".$name.tmp"
[IO.File]::WriteAllText($tmp, $files[$name]) # UTF-8 without a BOM
Move-Item $tmp (Join-Path $dir $name) -Force
}
icacls "$dir\*" /reset /T | Out-Null
Windows PowerShell’s Set-Content and Out-File add a byte-order mark or change the
encoding, and Lockpin refuses such a file as not JSON, so the script uses WriteAllText.
A stricter policy for some users#
A per-user file, lockpin-policy.json in the user’s Lockpin data folder, is read too. It is
verified against the same pinned keys and can only tighten the machine’s.
System |
User data folder |
|---|---|
Windows |
|
macOS |
|
Linux |
|
On a team node#
lockpin node install -policy-trust acme.pub pins the key in the machine’s policy folder and
locks the folder to administrators. -policy-required makes the node refuse every metered
call (503 policy_unavailable) while no signed machine policy is in force. A per-user file
alone does not count, since the user can write there. See Run a team node.
Change the policy or the key#
A change. Edit, raise
revision, sign, push. Machines apply it within five seconds of the file landing.A mistake. A refused file changes nothing: the last good policy stays until a good one arrives. A removed file also leaves the last good policy in force until Lockpin restarts.
Approvals and pauses. A yes at the cap, a budget set in the meter and
lockpin pausecan raise a task abovedefault_task_budget_usd, but cannot pass the policy’s limits ormax_task_budget_usd.A new key. Pin the new key everywhere, sign with it, push, then remove the old key with
lockpin policy trust -remove KEY_ID.A key that leaked. Unpinning a key revokes it: a policy signed with it is dropped at the next check, whatever its revision. Push the new trust file and a policy signed with the new key together. With
-policy-required, calls are refused in between.
Honest limits#
An administrator on the machine can replace the pinned key and the policy. The file protects against users and agents, not against local administrators.
The per-user proxy is the user’s own process. It honours the machine policy, but a user can run another program, or Lockpin with their own provider key. The floor is a wall where the provider keys are sealed in a team node the user cannot read.
The per-user policy file is in the user’s folder, so the user can delete it. It takes effect at the next restart.
A lower revision is refused while Lockpin runs. After a restart it loads whatever the folder holds, so the folder’s permissions keep an old file out.
The limits count spend from when the policy is first applied, in each Lockpin process’s own ledger. A node and a person’s own Lockpin each get the whole limit.
Tag, user and task limits are only as good as the names calls carry. The machine limit, the key limit and
max_task_budget_usdare the walls.default_task_budget_usdapplies to tasks first seen after it. Usemax_task_budget_usdfor a ceiling on every task.ca_fileandaudit_export_dirapply from the next start.lockpin pauselifts this machine’s own limits, not the policy’s.The dashboard does not show which policy a machine applies.
reporting.allowed: falsestops uploads; it does not delete what a dashboard already holds.
Verify#
On a managed machine, show the settings in force and where each one comes from:
lockpin policy show
A line reads like
default-budget 2.00 (machine policy acme-engineering rev 4; local 5.00).Run
lockpin doctorand check itspolicyline. The proxy also logs one line when a policy is applied and one when a file is refused, with the reason.