diff --git a/manifest.json b/manifest.json index 956de5a7..2c461c31 100644 --- a/manifest.json +++ b/manifest.json @@ -93,6 +93,10 @@ "title": "Install the Smallstep Agent", "path": "/platform/smallstep-agent.mdx" }, + { + "title": "Credential Files", + "path": "/platform/credential-files.mdx" + }, { "title": "Troubleshooting Guide", "path": "/platform/troubleshooting-agent.mdx" diff --git a/platform/credential-files.mdx b/platform/credential-files.mdx new file mode 100644 index 00000000..70c7d2c9 --- /dev/null +++ b/platform/credential-files.mdx @@ -0,0 +1,160 @@ +--- +updated_at: October 23, 2026 +title: Credential Files +html_title: Write Credential Files Where Applications Expect Them +description: Configure where the Smallstep Agent writes a credential's key, certificate, and root CA, with per-file owners, modes, POSIX ACLs, and Windows access lists. +--- + +A managed credential can tell the Smallstep Agent where to write its private key, certificate, and root CA certificate, and who may read each one. +Applications such as PostgreSQL, Elasticsearch, or SQL Server then read their credential directly, without a hook that copies files and fixes their permissions after every renewal. + + +
+Credential files need Smallstep Agent 0.72.0 or later, and API version `2026-11-01`. +Older agents don't apply most of these settings; update the agent before you use them. +
+
+ +## How it works + +A credential's `files` object holds one block per platform: `linux`, `macos`, and `windows`. +Each device receives only the block for its own operating system. +Inside a block, the `key`, `certificate`, and `root` entries each take their own path and permissions. + +Every field is optional. +A platform without a block, or a file without an entry, keeps the agent's default path and permissions. + +## PostgreSQL on Linux + +PostgreSQL refuses to start when its key is readable by anyone but its own account. +This credential writes the key for `postgres` alone, the certificate chain readable to all, and the root CA readable to a `monitoring` group: + +```bash +curl -X POST https://gateway.smallstep.com/api/credentials \ + -H "Authorization: Bearer $SMALLSTEP_API_TOKEN" \ + -H "X-Smallstep-Api-Version: 2026-11-01" \ + -H "Content-Type: application/json" \ + -d @postgres-credential.json +``` + +```json +{ + "slug": "postgres-server", + "certificate": { + "type": "X509", + "authorityID": "3c2e1115-49b7-4657-9b0f-301d7111bc6e", + "duration": "24h0m0s", + "fields": { "commonName": { "static": "db.example.internal" } } + }, + "key": { "type": "ECDSA_P256", "protection": "NONE", "store": "MACHINE" }, + "policy": { "operatingSystem": ["Linux"] }, + "files": { + "linux": { + "key": { + "path": "/var/lib/postgresql/16/main/server.key", + "format": "PKCS8", + "owner": "postgres", "group": "postgres", "mode": "0600" + }, + "certificate": { + "path": "/var/lib/postgresql/16/main/server.crt", + "contents": "CHAIN", + "owner": "postgres", "group": "postgres", "mode": "0644" + }, + "root": { + "path": "/var/lib/postgresql/16/main/root.crt", + "mode": "0640", + "acl": [{ "type": "GROUP", "name": "monitoring", "access": "r" }] + } + } + } +} +``` + +## A service account on Windows + +On Windows, each file takes an access list of principals instead of an owner and mode. +This credential lets the `CORP\svc-sql` domain account read the key, and nobody else besides `SYSTEM` and the Administrators: + +```json +{ + "slug": "sql-server", + "certificate": { "type": "X509", "authorityID": "3c2e1115-49b7-4657-9b0f-301d7111bc6e", "fields": { "commonName": { "static": "sql.example.internal" } } }, + "key": { "type": "ECDSA_P256", "protection": "NONE", "store": "MACHINE" }, + "policy": { "operatingSystem": ["Windows"] }, + "files": { + "windows": { + "key": { + "path": "D:\\certs\\sql\\server.key", + "acl": { + "inherit": false, + "entries": [{ "principal": "CORP\\svc-sql", "access": "READ" }] + } + }, + "certificate": { "path": "D:\\certs\\sql\\server.crt" }, + "root": { "path": "D:\\certs\\sql\\root.crt" } + } + } +} +``` + +A principal can be a local or domain account or group, a virtual service account such as `NT SERVICE\MSSQLSERVER`, a group managed service account such as `CORP\gmsa-sql$`, or a SID string such as `S-1-5-21-…`. + +By default, a Windows credential with a software key in the machine scope is also imported into the Windows key store and `LocalMachine\My`, next to its files. +Set `"windowsStore": false` on the `key` to keep it to its files alone, for software that reads the files and shouldn't find the credential in the stores. +It requires `"protection": "NONE"` and `"store": "MACHINE"`. + +## Field reference + +| Field | Applies to | Values | Default | +| --- | --- | --- | --- | +| `path` | key, certificate, root | Absolute path in the platform's style | The agent's directory for the credential | +| `format` | key | `DEFAULT`, `PKCS8`, `CLASSIC`, `TSS2` | `DEFAULT` | +| `contents` | certificate | `CHAIN` (leaf and intermediates) or `LEAF` | `CHAIN` | +| `owner`, `group` | Linux, macOS | A name or a numeric ID, as a string | The agent's user and group | +| `mode` | Linux, macOS | Octal string, `"0000"` to `"0777"` | `"0640"`; `"0600"` for SSH user credentials | +| `acl` | Linux, macOS | List of `{type: USER or GROUP, name, access}`, `access` a combination of `r`, `w`, `x` | No extra entries | +| `acl.inherit` | Windows | `true` keeps the access the folder grants | `false` once `acl` is set | +| `acl.entries` | Windows | List of `{principal, access}`, `access` one of `READ`, `MODIFY`, `FULL_CONTROL` | See [Windows defaults](#windows-defaults) | +| `key.windowsStore` | Windows | `false` keeps the credential out of the Windows stores | `true` | + +The root CA is always written to its own file and never into the certificate file. +SSH credentials take `key` and `certificate` only. + +## Rules + +The API refuses a credential that breaks these rules, with an error that names the field: + +- **A custom key path needs a software key.** `key.path` is accepted only with `"protection": "NONE"` and without `key.pubFile`. The agent writes the key there and nowhere else. +- **Paths are absolute** in the platform's style (`/…` on Linux and macOS, `C:\…` or `\\server\share\…` on Windows), and distinct within a block. +- **Settings stay on their platform.** `owner`, `group`, and `mode` can't appear under `windows`, and a Windows access list can't appear under `linux` or `macos`. A block for an operating system the credential's `policy.operatingSystem` excludes is refused. +- **`SYSTEM` keeps full control on Windows.** An access list that gives `SYSTEM` (`NT AUTHORITY\SYSTEM`, `S-1-5-18`) less than `FULL_CONTROL` is refused, and each principal can be listed once. + +The API doesn't check owners, groups, or principals: they exist only on the device. +The agent reports a name it can't find when it writes the file. + +### Windows defaults + +- The key file always grants `SYSTEM` and `BUILTIN\Administrators` full control, with inheritance blocked; `acl.entries` add principals to that. + Listing the Administrators with `READ` or `MODIFY` lowers their access. They can't be removed. +- The certificate and root files inherit their folder's permissions unless they have an `acl`. With one, `SYSTEM` gets full control in addition to the listed entries. +- A virtual service account (`NT SERVICE\`) resolves only once its service is installed. Until then the agent reports the principal as not found and retries; the service's SID works before the service exists. +- Domain names are resolved when the file is written, so the device needs to reach a domain controller. + +## How the agent writes files + +- **Atomically.** The agent writes each file to a temporary file in the same folder, with its permissions already applied, and renames it over the target. An app never reads a half-written file, and a failed write leaves the previous file in place. +- **Key first.** The key is written before the certificate, and the certificate before the root. If the key can't be written, the other files aren't either, so a new certificate never sits next to an old key. +- **Open files on Windows.** A file another program holds open without delete sharing can't be replaced. The agent retries for about two seconds, then reports the error and tries again later. +- **No folders.** The agent never creates a folder for a custom path. Create it with the ownership and permissions your app expects. +- **Old files stay.** When a path changes, the agent writes the new path and leaves the file at the old one, including an old key. Delete it yourself. +- **No restart.** Writing the files doesn't restart the app. Use the workload's reload settings or hooks for that. + +If a file can't be written as configured, the agent reports it on the credential's status for that device, and on Windows in the event log. +See [Credential files not written](./troubleshooting-agent.mdx#credential-files-not-written). + +## Compatibility + +- **API versions.** The per-platform `files` object is available in API version `2026-11-01` and later. Earlier versions keep the flat `files` object (`crtFile`, `keyFile`, `rootFile`, `uid`, `gid`, `mode`), which applies the same settings on every device; `2026-11-01` refuses it. + Earlier versions show a credential's per-platform settings in the flat shape when they can be expressed that way, and omit them otherwise. They refuse an update that would drop them. +- **Workloads on `2023-11-01`.** A workload's `certificateInfo.files` takes the same per-platform blocks. It can't be combined with the flat fields, and a save without `files` that changes the flat fields is refused with `412`. Send `"files": {}` to go back to the flat fields. +- **Agents.** Agents older than 0.72.0 apply the paths, the key format, and the key file's owner, group, mode, and ACL entries to all three files. They ignore everything else: per-file permissions, `contents`, Windows access lists, and `windowsStore`. diff --git a/platform/troubleshooting-agent.mdx b/platform/troubleshooting-agent.mdx index 0246d643..65764399 100644 --- a/platform/troubleshooting-agent.mdx +++ b/platform/troubleshooting-agent.mdx @@ -1,5 +1,5 @@ --- -updated_at: September 8, 2026 +updated_at: October 23, 2026 title: Troubleshooting Guide html_title: Smallstep Troubleshooting Guide description: Troubleshoot Smallstep Device Identity issues. Diagnose platform, MDM, and endpoint problems with step-by-step guidance. @@ -543,6 +543,32 @@ If you ran `modutil -add` before exporting `P11_KIT_SERVER_ADDRESS` (see [PKCS#1 - **If a certificate exists**: The issue is between the Smallstep Agent and the client application. Check the client's certificate configuration. See [Platform-Specific Reference](#platform-specific-reference) for certificate locations. - **If no certificate exists**: The resource may not be assigned to this device, or there may be an agent issue. Check the device's **Resources** tab and run `doctor`. +#### Credential files not written + +**Symptoms:** +- A credential with [custom files](./credential-files.mdx) shows an error or warning status on the device +- An app can't find or read its key or certificate +- Event ID 801 from `SmallstepAgent` in the Windows Application log + +**Troubleshooting steps:** +1. Run `step-agent status` on the device. Under a credential that reported an error or warning, a `last report:` line gives the file, its path, and the reason. +2. On Windows, read the agent's file events: + ```powershell + Get-WinEvent -FilterHashtable @{ LogName = 'Application'; ProviderName = 'SmallstepAgent'; ID = 800, 801 } + ``` + Event 801 (Error) carries `file` (`key`, `certificate`, or `root`), `path`, `fault`, `principal`, and `error`. Event 800 (Information) means the files were written as configured again. +3. Fix the cause the `fault` names: + + | `fault` | Cause | Fix | + | --- | --- | --- | + | `directory-missing` | The folder of a custom path doesn't exist. The agent never creates it. | Create the folder with the ownership and permissions the app expects. | + | `principal-not-found` | An owner, group, or Windows principal doesn't exist on the device. A domain account also fails while no domain controller is reachable. | Create the account, check its spelling, or use its numeric ID or SID. An `NT SERVICE\` account exists only once its service is installed. | + | `acl-unsupported` | The file system can't hold the configured ACL entries. | Move the file to a file system with ACL support, or remove the `acl` entries. | + | `write-failed` | Anything else, such as a file another program holds open on Windows, or no space left. | Read `error` for the reason. | + +The agent writes a credential's files again at its next issuance, renewal, or restart, so a fix made on the device alone is picked up then. To retry right away, run `step-agent doctor --renew`. +Until a write succeeds, the previous files stay in place, and the agent never writes a file with broader permissions than configured. + --- ## Getting help