PhysKey

Security at PhysKey

Last updated: October 28, 2026

Report a vulnerability

Email [email protected] with "Security" in the subject. Include what you found, how to reproduce it, and how we can reach you. We will confirm that we received your report and keep you informed while we work on a fix.

Please do not access or change data that is not yours, do not degrade the service for others, and give us reasonable time to fix an issue before you publish it. We do not offer a paid bug bounty.

In scope: physkey.ai (including the console), the edge agent, the verifier and our firmware builds. Out of scope for testing: our identity provider and other Nameless services, Cloudflare, and social engineering of our staff. If you find an issue in one of them that affects PhysKey, please tell us. Please also report issues in upstream OpenSK to Google.

Machine-readable contact details: https://physkey.ai/.well-known/security.txt

What we store

For each signed inspection verdict, the control plane stores the signed payload (time, camera or PC name, decision, model name and score, image file name, a SHA-256 hash of the image, a random nonce, and any extra fields the edge agent adds), the key's signature and authenticator data, the clientDataJSON (only for records signed through the Windows WebAuthn API), the time we received it, and the hash-chain links. PhysKey never receives the image itself.

For each enrolled key, it stores the public key, the employee ID it is bound to, a label, the key model (AAGUID), who enrolled it and when, and when and by whom it was revoked. Private keys are not sent to us. At enrollment we ask the key not to keep the credential in its own storage (a non-discoverable credential); a key may ignore this, and then it also stores the employee ID and label we send at enrollment. On PhysKey keys, which run OpenSK, the credential ID we store is the private key encrypted under a secret held on the key. We do not verify attestation (see Known limits), so we cannot confirm this for every enrolled credential.

It also stores rejected submissions (the credential ID submitted, the matching key if one is enrolled, the reason and status code, and the time) and the audit reports staff generate, together with the facts each report was written from. We have not set a retention period yet; today no database records are deleted automatically, including unfinished enrollments (employee ID, label and the staff member who started them). Server logs record request paths, including query strings, and are rotated by size (the oldest entries are discarded). We do not store the IP addresses that submit records; the rate limiter keeps each address in memory until one to two minutes after its last submission. For pilot requests from this website, see our privacy policy: https://physkey.ai/en/privacy

How the service is protected

  • Connections to physkey.ai use TLS and end at Cloudflare, which decrypts them and can see their contents. Cloudflare forwards requests to us through an encrypted Cloudflare Tunnel. Inside our network, traffic from the tunnel connector to our services is not encrypted. The servers that run PhysKey accept no inbound connections from the internet; Cloudflare Tunnel connects out from our network.
  • The control plane API is not reachable from the internet. The website forwards only pilot requests, signed-record submissions, console requests from signed-in staff, and a health check. Anyone can submit a record, but only records signed by an enrolled key are accepted.
  • Console access requires sign-in through our identity provider (authentik), with a password and a second factor, or with a Nameless Google account (subject to that account's own sign-in policy), and is limited to a staff group. The website forwards the signed-in identity to the control plane with a shared secret; the control plane refuses to start without it and rejects identity headers that lack it.
  • Every record must carry a valid signature from an enrolled, unrevoked key, with the key's user-presence (touch) flag set. For keys that keep a signature counter, the counter must advance. Each payload carries a random nonce, so signing the same inspection result again creates a new payload, and that one is accepted. A key cannot record the same signed payload twice: for keys that keep a counter, a new signature over a payload we already hold is refused and logged; for keys without one, it is treated as a resend and not logged. A different enrolled key can still sign and record a copy of the same payload.
  • Submissions are rate-limited (120 a minute per IP address, 60 per key). Anyone who knows a key's credential ID can use up that key's limit for as long as they keep sending; the edge agent keeps the records and sends them again once requests are allowed. Requests that are rate-limited, larger than 64 KB, or whose request body is not JSON with the expected top-level fields are not stored, and neither are resends of a record we already hold.
  • Rejected submissions, including those from unknown or revoked keys, are stored with the reason. Anyone can create rejections for unknown keys. Most other rejections need no valid signature, so anyone who knows a key's credential ID can create them under that key's employee ID. Only rejections for a counter that did not advance or a payload signed again need a signature from the key that we do not already hold, for example one copied from an edge PC before it was sent. A rejection for a signed payload in the wrong format can also be created by anyone holding a copy of a record signed through the Windows WebAuthn API. No one is alerted when this happens; they show up as findings when staff review them.
  • Data is stored in PostgreSQL on servers we operate in Japan, reachable only from our internal network. Connections from the control plane to the database are not encrypted. The server also hosts other Nameless services' databases.
  • Containers run as non-root users. Every change to main runs automated tests, type checks and linting in CI (deployment does not wait for them to finish), and Dependabot proposes dependency updates (weekly for Python and npm packages, monthly for base images and CI actions).
  • Backend error monitoring uses our self-hosted Sentry, set up for errors only: no performance tracing, no request bodies, query strings, cookies or local variables, and no headers except User-Agent.

AI and your data

Findings come from fixed rules in our code. A language model on GPU servers that Nameless operates writes the explanations and audit-report text. The service is configured to use only that model, so these facts, including employee IDs and camera names, are not sent to an outside AI provider. Requests to the model server travel unencrypted over our internal network.

The model sees only counted facts: the reporting period, record totals by decision and by camera or PC name, rejection counts by reason, the number of active, revoked and signing keys, the employee ID, label and chain status of each key that signed in the period, and the rule-based findings. It never sees signed payloads or images.

If its text contains a number written in digits that the check cannot find in those facts, or the model is unavailable, the report uses fixed text instead. The check is loose: it accepts any digits that occur anywhere in the facts, including inside dates, times and IDs, and a decimal whose whole-number part occurs there. Numbers written out in words are not checked, the check does not confirm that each number is attached to the right item, and names and causes in the text are limited only by the model's instructions. Camera and PC names and decisions are set by the edge PC and reach the model as text, so text placed in them can steer what it writes. Below its text, each report lists the main counts it was written from, so you can check the text against them. Some details the model saw, such as dates and delay times in findings, are not listed. The report also shows a SHA-256 hash of the full set of facts we keep with it; it lets us show later which stored facts a report came from, but you cannot recompute it from the printed report.

Check records independently

On request, we export an audit bundle for a period, for one key or for all keys. It contains the signed records, the hash chain, the username of the staff member who exported it, and, for each key that signed a record in the period (or the one key you asked for), its public key and fingerprint, credential ID, key model (AAGUID), status, enrollment time, label, the employee ID it is bound to, and its latest chain hash. The period is whole days in UTC. Records are selected by the signing time the edge PC wrote into them, but the chain follows the order we received them. A record delivered late across the edge of the period, or signed with a wrong clock, therefore shows up as a gap; export a slightly wider period if the verifier reports missing records. Our verifier is one Python file (about 250 lines, one direct dependency: fido2) that we give to your auditors. It rechecks every signature, user presence, counter order and chain link on their own machine, without contacting PhysKey. By default, it fails a bundle whose RP ID is not physkey.ai or that allows a Windows agent origin other than https://agent.physkey.ai.

The public keys, employee IDs and chain hashes in a bundle come from our database, so a bundle alone cannot reveal changes made there. To detect later changes, ask us for each key's device ID, fingerprint and employee ID when the key is enrolled (all three are shown on the key's page in our console). From time to time, also save each key's latest chain hash; every bundle file lists them. Keep these outside PhysKey. When you verify a bundle, pass the fingerprint and saved chain hash of every key you expect to have signed in the exported period, and export a period that includes each saved chain hash. The chain hash a bundle lists for a key is its latest when the bundle was exported, which can belong to a record signed after the exported period. The verifier fails if any of those keys is missing from the bundle, if a key's public key differs from its fingerprint, or if a saved chain hash is not found. The verifier does not check employee IDs, and for keys you pass nothing for, it trusts the public key in the bundle. Compare the bundle's key list and the employee IDs it prints with your own records. It cannot tell whether records before the first or after the last one in the bundle were left out.

Known limits

  • A signature needs a touch, not a PIN. It shows that someone touched the key, not who. The agent can be set to supply the key's PIN, but the PIN is then stored on the edge PC, so it does not identify the operator either. Anyone holding a key can sign as the employee it is bound to until it is revoked, from any computer and any network, with any camera name and time.
  • The edge PC supplies the time and camera name in each record. The signature stops anyone from changing them afterwards, but does not prove they were right when signed. Keys are not bound to stations.
  • Only the verdicts the edge agent is set to sign (for example NG) are signed. The key has no screen, so the operator cannot see what a touch signs. Software on the edge PC that changes a verdict before signing, never asks for a signature, or deletes a signed record before it is sent is not detected. The hash chain starts when we receive a record, so a record that never reaches us leaves no gap.
  • The nRF52840 has no secure element. Our current firmware build does not turn on the chip's readout protection, and a key accepts new firmware over USB while keeping its stored secrets. Assume that anyone with physical access to a key can read its flash, including the secret that decrypts its credential IDs. Even with readout protection turned on, a published fault-injection attack (CVE-2020-27211) reads the flash of earlier chip revisions. Credential IDs are not secret (bundles and edge PCs hold them), so this yields the private key. On keys that keep a counter, a copy is detected only when a submission arrives with a counter no higher than the last one we stored. A copy that signs while the genuine key is unused, or that uses counter values the genuine key skips (its counter also advances when it is used to sign in elsewhere), can go undetected. This raises a finding, but no one is notified: findings appear only when staff open the console's findings view or generate a report. The key keeps working until staff revoke it. Records the clone signed before the conflict are accepted, and afterwards records from whichever copy has the lower counter, which may be the genuine key, are refused.
  • Attestation certificates are not verified, and the key model (AAGUID) is whatever the key reports. Enrollment currently accepts any FIDO2 authenticator model, including passkeys synced through a phone or cloud account, which have no signature counter. The service therefore cannot prove that an enrolled credential lives on a PhysKey key rather than on another authenticator or in software. The user-presence check shows that a person touched a key only if staff enrolled a real hardware key; a software authenticator can set that flag without any touch.
  • Nameless staff enroll and revoke keys in our console; customers cannot. The console is served at physkey.ai/console, on the same origin as this public website. Our Content Security Policy allows inline scripts on every page, and the public pages load an analytics script from telemetry.namelessjp.com, a separate Nameless server. Anyone who can change that script, or inject a script into any physkey.ai page, could act with the session of a staff member who is signed in to the console. Console access has no separate roles: every staff member who can sign in can enroll and revoke keys, export bundles, generate reports and read pilot requests. Staff can enroll a key under any employee ID, and a list of key fingerprints kept outside PhysKey reveals such a key. We record who enrolled and who revoked each key and who requested each report, but not who exported a bundle or viewed records. Console sessions cannot be revoked one at a time; the only way to end them early is to change the session secret, which signs out all staff. Signing out of PhysKey does not sign you out of our identity provider. Signing out deletes the session cookie from that browser, but a copy of it stays valid for up to 8 hours after sign-in, and neither disabling the account nor removing it from the staff group ends sessions already open.
  • Nameless staff who administer our servers or can push to our main branch (which deploys automatically), or anyone else with write access to our database, could add a key or replace one and sign records with it, delete the newest records, change the employee ID a key is bound to, undo a revocation, edit the log of rejected submissions, change the time we received a record, or edit stored reports and the record of who enrolled, revoked or requested what. A key fingerprint and employee ID saved outside PhysKey reveal a replaced or reassigned key. A chain hash saved outside PhysKey reveals a deleted or rewritten record that we received before that hash, but only if the bundle you verify starts at or before that record. Records signed after the last hash you saved can still be removed without trace. Revocations and rejected submissions are not covered by the chain.
  • PhysKey keys run Google OpenSK unmodified, which Google publishes as a research project, not a production product. Our build pins a specific OpenSK commit and has not yet been tested on hardware.
  • Signing through the Windows WebAuthn API has not yet been tested on real Windows hardware with a key. Our automated tests check that the control plane verifies records in this format, but no test signs through the Windows API.
  • The control plane does not yet keep customers apart. All keys and records are stored together. Every audit report counts every enrolled key and every rejected submission, including those from unknown keys, and the model that writes it sees them all. An exported bundle covers either one key or every key that signed a record in the period, including keys of other customers.
  • PhysKey is in a pilot stage. It holds no certifications (no SOC 2, no IEC 62443-4-2), and the service runs on servers at a single location in Japan, with no second site.