The official PHP SDK for Locker Passwords & Secrets Management.
This package is a typed PHP client for the stable Locker SDK protocol. It
starts the Locker CLI as locker sdk, sends one JSON-RPC 2.0 request over
stdin, reads one response from stdout, and never depends on human-facing CLI
output.
- PHP 8.2 or later
- The JSON and Sodium extensions
- Linux or macOS with the
pcntlandposixextensions - The Locker CLI with SDK protocol v1 support
- A Locker access key ID and secret access key
The package has no third-party runtime dependency. Sodium verifies the Locker release signatures with the public key embedded in the SDK package.
Upgrading from the unfinished legacy package line is a breaking change. See
MIGRATION.md.
composer require lockerpm/locker-secretsInstall the Locker CLI through the signed release channel, or configure the exact path to a trusted binary:
use Locker\ClientOptions;
use Locker\Locker;
$locker = new Locker(
accessKeyId: $accessKeyId,
secretAccessKey: $secretAccessKey,
options: new ClientOptions(cliPath: '/opt/locker/bin/locker'),
);Binary resolution is deterministic:
ClientOptions::$cliPathLOCKER_CLI_PATH- the managed binary from Locker's signed release channel
Explicit paths intentionally bypass managed updates, but must be absolute,
regular, executable non-link files. Bare and relative values are rejected
instead of being searched through ambient PATH. In managed mode, the SDK
checks for a compatible release on first use and at most once every six hours.
A network failure may reuse a previously verified cache; invalid signatures,
mutated releases, and rollback attempts always fail closed. A missing or
invalid bundled trust root also fails closed; the SDK never executes an
ambient locker from PATH.
Verified releases, state, and the interprocess lock are isolated below
~/.locker/sdk-cli/php/, so another language SDK cannot overwrite this cache.
On POSIX, every managed cache ancestor must be owned by the effective user and
is revalidated at mode 0700 before updater state is used.
Legacy names such as locker_secret are never selected automatically. An
explicit absolute path may use any filename, but protocol-v1 capability
negotiation remains mandatory.
The Locker scanner generates code against this public contract:
<?php
use Locker\Locker;
$locker = Locker::fromEnvironment();
$databasePassword = $locker->getRequired('DATABASE_PASSWORD');fromEnvironment() reads the canonical variables:
| Environment variable | Purpose |
|---|---|
LOCKER_ACCESS_KEY_ID |
Project access key ID as a UUIDv4 |
LOCKER_SECRET_ACCESS_KEY |
Non-empty canonical standard Base64 project secret access key |
LOCKER_API_BASE |
Cloud or self-hosted API base URL |
LOCKER_CLI_PATH |
Absolute caller-owned CLI path |
For migration only, ACCESS_KEY_ID, SECRET_ACCESS_KEY,
LOCKER_ACCESS_KEY_SECRET, and ACCESS_KEY_SECRET are accepted after the
canonical names. Outer credential whitespace is trimmed once, then the pair is
validated before any managed download, binary resolution, capability
negotiation, or CLI launch. Malformed input therefore fails locally with a
typed, non-retryable AuthenticationException; it is never sent to the CLI.
Credentials are removed from the environment inherited by the child process.
$secrets = $locker->secrets();
$secret = $secrets->get('DATABASE_PASSWORD', 'production');
$all = $secrets->list('production');
$page = $secrets->listPage(
environment: 'production',
pageSize: 100,
);
while ($page->nextCursor !== null) {
$page = $secrets->listPage(
environment: 'production',
pageSize: 100,
cursor: $page->nextCursor,
);
}
$created = $secrets->create(
key: 'API_TOKEN',
value: $token,
environment: 'production',
description: 'Token used by the worker',
);
$updated = $secrets->update(
key: 'API_TOKEN',
changes: [
'value' => $rotatedToken,
'description' => '',
'environment' => null,
],
environment: 'production',
);Empty secret values and descriptions are valid. In an update,
'environment' => null clears the environment association.
list() remains available for compatibility. Prefer listPage() for vaults
that could exceed the CLI's negotiated response-size limit. Cursors are
opaque; pass nextCursor back unchanged.
getRequired() returns the secret value and throws when the secret does not
exist:
$token = $locker->getRequired('API_TOKEN', 'production');Use getOrDefault() only when a missing secret has a safe fallback. It catches
only Locker's numeric not_found error; authentication, permission, network,
storage, server, and protocol failures still fail closed:
$region = $locker->getOrDefault('REGION', 'us-east-1');$environments = $locker->environments();
$production = $environments->get('production');
$all = $environments->list();
$page = $environments->listPage(pageSize: 100);
$created = $environments->create(
name: 'staging',
externalUrl: 'https://staging.example.com',
description: '',
);
$updated = $environments->update(
name: 'staging',
changes: [
'name' => 'staging-eu',
'external_url' => 'https://eu.staging.example.com',
],
);As with secrets, use listPage() for potentially large environment
collections.
use Locker\ClientOptions;
use Locker\Locker;
$options = new ClientOptions(
cliPath: '/opt/locker/bin/locker',
timeoutSeconds: 15.0,
apiBase: 'https://api.locker.io/locker_secrets',
headers: ['CF-Access-Client-Id' => $clientId],
forceRefresh: false,
cacheMaxAgeSeconds: 120,
);
$locker = new Locker($accessKeyId, $secretAccessKey, $options);Custom headers, credentials, and mutation values are sent only in the JSON request body. They never appear in argv, child environment variables, debug output, or transport exception messages.
apiBase accepts an absolute HTTP or HTTPS base URL. Credential-bearing,
query-bearing, fragmented, ambiguous, and control-character URLs are rejected;
put custom access data in headers instead.
The SDK negotiates system.capabilities before its first vault operation and
caches one capability result for a bounded time. Use
$locker->capabilities(forceRefresh: true) to renegotiate explicitly.
Negotiated request, response, and JSON-depth limits are enforced locally.
timeoutSeconds is one total initialization or operation budget. On POSIX,
timeout terminates the CLI process group. The public synchronous API has no
cooperative cancellation token; use the timeout and cancel the supervising
request/process when application-level cancellation is required.
The SDK does not retry API or JSON-RPC failures. Create and update are issued
once because a lost response can leave the remote commit outcome unknown. One
internal rebind attempt is allowed only when the executable changes before a
protocol exchange. Applications may inspect isRetryable() and add bounded
retry only around read-only operations.
The SDK does not retain plaintext secret values. forceRefresh and
cacheMaxAgeSeconds are delegated to the CLI's encrypted, revision-aware
cache. A zero max age disables offline reuse; force refresh requires a
successful server response. Authentication, authorization, TLS, integrity,
malformed-response, and local-storage failures always fail closed.
All SDK exceptions implement Locker\Exception\ExceptionInterface. JSON-RPC
codes map to these typed operation errors:
use Locker\Exception\AlreadyExistsException;
use Locker\Exception\ConflictException;
try {
$locker->secrets()->create(
key: 'PAYMENT_API_KEY',
value: 'value',
);
} catch (AlreadyExistsException $error) {
// The secret or environment already exists.
// AlreadyExistsException is also a ConflictException.
}| Code | Exception | Canonical kind |
|---|---|---|
-32700 |
ProtocolException |
parse_error |
-32600 |
ProtocolException |
invalid_request |
-32601 |
ProtocolException |
method_not_found |
-32602 |
ProtocolException |
invalid_params |
-32603 |
ProtocolException |
internal_protocol_error |
-32000 |
OperationException and legacy subtypes |
operation_error, request_rejected, response_too_large, cancelled |
-32001 |
AuthenticationException |
missing_credentials, invalid_access_key_id, malformed_secret_access_key, invalid_secret_access_key, unauthorized |
-32003 |
PermissionDeniedException |
forbidden; legacy permission_denied |
-32004 |
NotFoundException |
secret_not_found, environment_not_found; legacy not-found aliases |
-32009 |
ConflictException / AlreadyExistsException |
conflict, secret_already_exists, environment_already_exists |
-32022 |
ValidationException |
validation_error |
-32029 |
RateLimitedException |
rate_limited |
-32050 |
NetworkException |
network_error, network_timeout; legacy http_error |
-32051 |
ServerException |
service_unavailable, internal_error; legacy server_error |
-32060 |
StorageException |
database_error, file_error, path_error |
-32070 |
IntegrityException |
integrity, transport-integrity, and data-integrity kinds |
Every RPC exception retains rpcCode(), requestId(), kind(), and
isRetryable(). Protocol and transport failures use ProtocolException and
TransportException. Classification is numeric-first. Distinctive kinds from
older CLI releases (duplicate_hash, *_already_exists, conflict,
validation_error, and the integrity aliases) also retain their typed mapping
when the legacy code is -32000. request_rejected, response_too_large,
and cancelled have explicit OperationException subtypes but are never
guessed to be conflicts. Every -32000 error and known authentication,
permission, not-found, conflict, validation, storage, integrity, protocol,
cancellation, and internal-server error is non-retryable. Only rate-limit,
network, service-unavailable, or an unknown server-range code may preserve a
true hint. RateLimitedException::retryAfterSeconds() exposes an optional
validated 0..86400 delay; the SDK does not retry vault RPCs automatically.
Authentication errors use stable, safe messages without echoing credentials
or backend details. missing_credentials, invalid_access_key_id, and
malformed_secret_access_key are detected locally before the CLI is resolved.
A well-formed but mismatched pair is reported as
invalid_secret_access_key; an upstream HTTP 401 is unauthorized. Unknown
future authentication kinds remain an AuthenticationException with the
generic authentication failed message.
The SDK sends context.error_contract = 'typed-v1' only after that exact
contract is advertised in capability error_contracts. An absent list and
unknown valid contracts remain compatible and do not opt in.
serverRequestId() exposes a separately validated upstream correlation ID;
it never replaces local JSON-RPC requestId() and is not included in default
exception text.
The SDK embeds Locker's reviewed production Ed25519 release public key. To install or force-check the latest compatible CLI explicitly:
use Locker\Distribution\CliInstaller;
$path = (new CliInstaller())->installLatest();The SDK accepts a managed download only after its signed release metadata, manifest, binary digest, detached signature, and executable platform header all verify. Verified versions are stored privately and activated atomically. A temporary release-channel outage may reuse a previously verified compatible binary; signature, schema, hash, platform, rollback, and local-integrity failures always fail closed.
- The only Locker CLI argv is
[locker, sdk]. - Request and response buffers, JSON depth, and process lifetime are bounded.
- stdout and stderr are drained independently.
- timeout handling terminates the full process tree. Linux and macOS run the
CLI in a dedicated POSIX session and signal its process group. POSIX
execution fails closed with a clear error unless the PHP
pcntlandposixextensions are enabled. timeoutSecondsis one end-to-end budget for signed managed resolution, update-lock waits, capability negotiation, and the vault operation. Managed cache contents are cryptographically rebound to the signed manifest before every CLI process starts. Tampering fails closed. Explicit absolute paths remain caller-owned and use the documented regular-file policy.- PHP's native
proc_open()transport is fail-closed on Windows because PHP cannot provide bounded non-blocking pipes there. The SDK never persists protocol requests or responses, which contain credentials and secret values, to temporary files. Use the PHP SDK on Linux/macOS until a memory-only Windows transport is available. - JSON responses reject invalid UTF-8, duplicate object fields, trailing values, mismatched IDs, and malformed DTOs.
- Unknown response fields are ignored for protocol-v1 forward compatibility.
Secretredacts its value from JSON, debug output, and string conversion.- SDK exception debug output excludes traces and previous exceptions. Set
zend.exception_ignore_args=Onin PHP configuration so PHP-generated stack traces also omit application call arguments that may contain secret values. - No CLI binary is bundled in the Composer package.
- No network request occurs during Composer install or autoload. Client construction performs only the bounded periodic release check described above when managed resolution is active.
- The managed updater pins a release public key, not a CLI version. Signed versions older than the verified installed release are rejected.
The SDK follows Semantic Versioning. Packagist versions use
MAJOR.MINOR.PATCH; matching source releases use
vMAJOR.MINOR.PATCH. See the release notes when upgrading across a major
version.
- Help: https://support.locker.io
- Security: Locker Bug Bounty program
For managed-install failures, check system time, access to the
Locker Secrets download service,
required POSIX extensions, and private ownership below
~/.locker/sdk-cli/php. For protocol errors, upgrade the SDK and CLI together
or remove an incompatible explicit LOCKER_CLI_PATH. Never loosen cache
permissions or log a secret-bearing request while troubleshooting.
Apache-2.0