Credentials
Zero-standing-secrets credential providers — SSH-CA-OIDC, SSH key, and pull-agent — and the non-mutating preflight every one of them supports.
Credentials
A standing secret — an SSH key sitting on disk, a registry password baked into an image — is a secret that exists whether or not anyone is using it right now, which means it's a secret that can be stolen whether or not anyone is using it right now. Deploy's credential providers exist to avoid that: every credential is minted on demand, scoped narrowly, and expires quickly.
interface CredentialProviderInterface extends DriverInterface
{
public function issue(EnvironmentName $env): IssuedCredential;
public function assertIssuable(EnvironmentName $env): void;
}assertIssuable — preflight without minting
assertIssuable() is the method that makes zero-standing-secrets safe to verify. It proves a provider could mint a working credential right now — config present, signer or OIDC source reachable, backing secret available — without actually minting one and without leaving any artifact behind. deploy:doctor calls this, not issue(). A health check should never itself create a secret just to confirm the path works.
php bin/console deploy:doctor --env=production[OK] credential.ssh-ca-oidc OIDC token source reachable, CA signer configuredProviders
ssh-ca-oidc
Exchanges a short-lived OIDC token for an SSH certificate signed by an internal certificate authority, capped at a 300-second TTL:
final class SshCaOidcCredentialProvider extends AbstractCredentialProvider
{
public function capabilities(): CapabilityDescriptor
{
return CapabilityDescriptor::create(
[
CredentialCapability::ShortLivedCert->value => true,
CredentialCapability::OidcFederation->value => true,
CredentialCapability::NoInboundNetwork->value => false,
],
['cert_ttl_seconds' => 300],
);
}
}This is the default for SSH-reachable targets — the certificate is valid for the duration of one deploy and nothing more. There's no long-lived private key to rotate or leak, because there's no long-lived private key at all.
ssh-key
The default provider, and the simplest to stand up: it issues credentials from an SSH keypair held in the encrypted secrets store (deploy_ssh_private_key) rather than minting them from a CA. It also requires a deploy_known_hosts secret — strict host-key verification is mandatory and there is no trust-on-first-use fallback, so deploy:doctor fails closed up front if either secret is missing.
The provider never writes the key to disk. It returns the key material inside the lease; the transport layer materializes a 0600 key and known_hosts file only for the duration of the deploy and unlinks them the moment the lease is wiped. Nothing survives the deploy that leased it.
Graduate to ssh-ca-oidc wherever your CI can produce an OIDC token — it gives you shorter-lived, narrower-scoped credentials with no standing keypair to rotate. Because both providers speak the same CredentialUse contract, switching is a config change (credential('ssh-ca-oidc')), not a rewrite.
pull-agent
For environments where you'd rather not open inbound network access to the deploy target at all — pull-agent declares NoInboundNetwork: true. Instead of the deploy pipeline pushing to the target over SSH, an agent running on the target polls for a signed desired-state manifest and pulls it:
final class PullAgentCredentialProvider extends AbstractCredentialProvider
{
public function capabilities(): CapabilityDescriptor
{
return CapabilityDescriptor::create(
[
CredentialCapability::NoInboundNetwork->value => true,
CredentialCapability::OidcFederation->value => true,
CredentialCapability::ShortLivedCert->value => false,
],
['manifest_sig_required' => true],
);
}
}manifest_sig_required: true means the agent only ever applies a manifest signed by ManifestSignerInterface — a compromised network path between the publisher and the agent can't smuggle in an unsigned desired-state change. Use deploy:agent to run the reconciliation loop on the target:
php bin/console deploy:agentCapabilities
| Capability | Meaning |
|---|---|
short_lived_cert | The provider issues a certificate with a bounded TTL rather than a static credential |
oidc_federation | The provider authenticates by exchanging an OIDC token, not a stored secret |
no_inbound_network | The target never needs an inbound connection accepted — it pulls instead of being pushed to |
Governance — approval gates
For environments that need a human sign-off before a credential can even be issued (production, anything regulated), DeployApprovalGateInterface sits in front of credential issuance:
interface DeployApprovalGateInterface
{
// ...
}ChangeRequestDeployApprovalGate ties this to a DeployChangeRequest record — a deploy to a protected environment is blocked until a matching change request has been approved. NullDeployApprovalGate is the default no-op for environments that don't need this. IssuedCredentialAudit records every credential actually issued — who, when, for which environment — independent of whether the deploy that requested it succeeded.
Governance is opt-in per environment
Configure EnvironmentProtectionConfig per environment. Development and staging typically run with NullDeployApprovalGate; production wires ChangeRequestDeployApprovalGate so a credential can't be minted for a protected environment without an approved change request on file.