Register for part 2 of our Device Identity Webinar Series!

Credential Files

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.

Example: PostgreSQL on Linux

PostgreSQL won't start if anyone other than its own account can read its key. In this example, the agent writes a server credential straight into the PostgreSQL data directory, with the permissions PostgreSQL expects:

  • The key, readable only by postgres (0600).
  • The certificate chain, readable by everyone (0644).
  • The root CA, readable by root and, through a POSIX ACL entry, by a monitoring group (0640).

1. Prepare the device

The agent writes files into existing folders, for existing accounts. The PostgreSQL package creates the postgres account and the data directory; create the monitoring group if the device doesn't have one:

sudo groupadd --system monitoring

POSIX ACLs need a file system that supports them, such as ext4 or XFS.

2. Create the credential

Save the credential as postgres-credential.json. Replace authorityID with your authority's ID, and the paths with your PostgreSQL version's data directory:

{
  "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" }]
      }
    }
  }
}

Then create it with the API:

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

The root entry has no owner or group, so it keeps the agent's (root).

3. Check the files

Once the device has picked up the credential, check who owns each file and its mode:

$ cd /var/lib/postgresql/16/main
$ sudo stat -c '%U %G %a %n' server.key server.crt root.crt
postgres postgres 600 server.key
postgres postgres 644 server.crt
root root 640 root.crt

The root CA's access list includes the monitoring group:

$ sudo getfacl --omit-header root.crt | grep monitoring
group:monitoring:r--

If a file is missing, run sudo step-agent status: it names the file and the reason, such as an owner that doesn't exist yet. See Credential files not written.

4. Point PostgreSQL at the files

In postgresql.conf:

ssl = on
ssl_cert_file = '/var/lib/postgresql/16/main/server.crt'
ssl_key_file = '/var/lib/postgresql/16/main/server.key'
ssl_ca_file = '/var/lib/postgresql/16/main/root.crt'

ssl_ca_file is needed only if PostgreSQL verifies client certificates. Reload PostgreSQL to load the certificate:

sudo systemctl reload postgresql

The agent renews the certificate before it expires, but it doesn't reload PostgreSQL. To have PostgreSQL pick up each renewed certificate, add systemctl reload postgresql to the workload's reload settings or to an after-renew hook.

Example: 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:

{
  "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

FieldApplies toValuesDefault
pathkey, certificate, rootAbsolute path in the platform's styleThe agent's directory for the credential
formatkeyDEFAULT, PKCS8, CLASSIC, TSS2DEFAULT
contentscertificateCHAIN (leaf and intermediates) or LEAFCHAIN
owner, groupLinux, macOSA name or a numeric ID, as a stringThe agent's user and group
modeLinux, macOSOctal string, "0000" to "0777""0640"; "0600" for SSH user credentials
aclLinux, macOSList of {type: USER or GROUP, name, access}, access a combination of r, w, xNo extra entries
acl.inheritWindowstrue keeps the access the folder grantsfalse once acl is set
acl.entriesWindowsList of {principal, access}, access one of READ, MODIFY, FULL_CONTROLSee Windows defaults
key.windowsStoreWindowsfalse keeps the credential out of the Windows storestrue

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\<name>) 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.

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.

Last updated on October 26, 2026

Write Credential Files Where Applications Expect Them | Smallstep