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

default_task_budget_usd

The budget a new task gets, if lower than -default-budget.

max_task_budget_usd

A hard ceiling per task. An approval at the cap, the meter or a budget header cannot pass it.

limits.machine, limits.key, limits.user_day_usd, limits.tags

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 ("env=prod").

providers, models

allow and deny lists. Model patterns use * and match model or provider/model.

ask_at_cap

A shorter wait at the cap. "0s" never asks.

on_fault

block: calls are refused when the ledger or keys file fails.

compact

off (no compaction, and X-Lockpin-Compact: on is ignored) or on.

reporting

allowed: false stops every upload to the Lockpin dashboard, including lockpin enroll. max_detail lowers -report-detail.

allow_unpriced

false turns -allow-unpriced off.

downgrades

false forbids serving a call that does not fit its budget on a cheaper model.

ca_file

The private root CA file, used instead of -ca-file from the next start.

audit_export_dir

Turns the audit export on, to an absolute folder, from the next start.

price_feed

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

auto_update

true turns the automatic fetch on.

refresh

How often to fetch. Default 6h.

url

An organisation’s mirror of the feed: https, with no user, password, query or fragment.

keys

Up to 8 extra base64 public keys the mirror’s feed may be signed by.

max_age

Refuse a feed issued longer ago than this. Default 720h.

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.

  1. Make a signing key pair:

    lockpin policy keygen -out ./lockpin-keys -name acme
    

    acme.key is the private key: keep it with your code-signing material and never copy it to a managed machine. acme.pub is what the machines pin. On Windows the .key file takes the permissions of its folder, so make it in a folder only you can read.

  2. Pin the public key in a folder you will push:

    lockpin policy trust -policy-dir ./mdm acme.pub
    
  3. 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
    

    sign checks the file first and refuses one the machines would refuse.

  4. 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

%ProgramData%\Lockpin\policy

macOS

/Library/Application Support/Lockpin/policy

Linux

/etc/lockpin

- 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

%LOCALAPPDATA%\Lockpin

macOS

~/Library/Application Support/Lockpin

Linux

~/.local/share/lockpin

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 pause can raise a task above default_task_budget_usd, but cannot pass the policy’s limits or max_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_usd are the walls.

  • default_task_budget_usd applies to tasks first seen after it. Use max_task_budget_usd for a ceiling on every task.

  • ca_file and audit_export_dir apply from the next start.

  • lockpin pause lifts this machine’s own limits, not the policy’s.

  • The dashboard does not show which policy a machine applies. reporting.allowed: false stops uploads; it does not delete what a dashboard already holds.

Verify#

  1. 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).

  2. Run lockpin doctor and check its policy line. The proxy also logs one line when a policy is applied and one when a file is refused, with the reason.

Next steps#