Vortos
Deploy

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 configured

Providers

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

Capabilities

CapabilityMeaning
short_lived_certThe provider issues a certificate with a bounded TTL rather than a static credential
oidc_federationThe provider authenticates by exchanging an OIDC token, not a stored secret
no_inbound_networkThe 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.

On this page