NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

The published config/certificates.php in full, comments removed:

return [
    'default' => env('CERTIFICATES_DRIVER', 'kubernetes'),
    'connection' => env('CERTIFICATES_DB_CONNECTION'),

    'model' => RoundlyConsulting\Certificates\Models\Certificate::class,
    'table' => 'certificates',
    'key_type' => env('CERTIFICATES_KEY_TYPE', 'bigint'),

    'renewal' => [
        'threshold_days' => env('CERTIFICATES_RENEW_THRESHOLD_DAYS', 21),
        'queue' => env('CERTIFICATES_RENEW_QUEUE'),
    ],

    'name_prefix' => env('CERTIFICATES_NAME_PREFIX', 'generated-tls-'),

    'lock' => [
        'name' => env('CERTIFICATES_LOCK_NAME', 'certificates:generate'),
        'locked_for_seconds' => env('CERTIFICATES_LOCK_SECONDS', 5),
    ],

    'status_cache' => [
        'enabled' => env('CERTIFICATES_STATUS_CACHE', true),
        'store' => env('CERTIFICATES_STATUS_CACHE_STORE'),
        'ttl' => env('CERTIFICATES_STATUS_CACHE_TTL', 300),
    ],

    'alerts' => [
        'enabled' => env('CERTIFICATES_ALERTS', false),
        'notifiable' => env('CERTIFICATES_ALERTS_NOTIFIABLE'),
        'thresholds' => [
            'warning_days' => env('CERTIFICATES_ALERTS_WARNING_DAYS', 30),
            'critical_days' => env('CERTIFICATES_ALERTS_CRITICAL_DAYS', 7),
        ],
        'channels' => ['mail'],
        'register_check' => env('CERTIFICATES_ALERTS_REGISTER_CHECK', false),
    ],

    'drivers' => [
        'kubernetes' => [
            'base_url' => env('CERTIFICATES_K8S_BASE_URL', 'https://kubernetes.default.svc'),
            'token' => env('CERTIFICATES_K8S_TOKEN'),
            'token_path' => env('CERTIFICATES_K8S_TOKEN_PATH', '/var/run/secrets/kubernetes.io/serviceaccount/token'),
            'ca_path' => env('CERTIFICATES_K8S_CA_PATH', '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'),
            'namespace' => env('CERTIFICATES_K8S_NAMESPACE', 'default'),
            'issuer' => env('CERTIFICATES_K8S_ISSUER', 'letsencrypt'),
            'issuer_kind' => env('CERTIFICATES_K8S_ISSUER_KIND', 'ClusterIssuer'),
            'ingress' => [
                'name' => env('CERTIFICATES_K8S_INGRESS_NAME'),
                'class' => env('CERTIFICATES_K8S_INGRESS_CLASS', 'nginx'),
            ],
            'service' => [
                'name' => env('CERTIFICATES_K8S_SERVICE_NAME'),
                'port' => env('CERTIFICATES_K8S_SERVICE_PORT', 80),
            ],
        ],

        'acme' => [
            'directory' => env('CERTIFICATES_ACME_DIRECTORY', 'https://acme-v02.api.letsencrypt.org/directory'),
            // Staging: https://acme-staging-v02.api.letsencrypt.org/directory
            'contact' => env('CERTIFICATES_ACME_CONTACT'),
            'account' => [
                'key_type' => env('CERTIFICATES_ACME_KEY_TYPE', 'EC'),
                'disk' => env('CERTIFICATES_ACME_ACCOUNT_DISK', 'local'),
                'key_path' => env('CERTIFICATES_ACME_ACCOUNT_KEY', 'acme/account.pem'),
                'auto_register' => env('CERTIFICATES_ACME_AUTO_REGISTER', true),
            ],
            'solver' => env('CERTIFICATES_ACME_SOLVER'),
            'http' => [
                'disk' => env('CERTIFICATES_ACME_HTTP_DISK', 'local'),
                'path' => env('CERTIFICATES_ACME_HTTP_PATH', 'acme-challenge'),
            ],
            'store' => [
                'disk' => env('CERTIFICATES_ACME_STORE_DISK', 'local'),
                'path' => env('CERTIFICATES_ACME_STORE_PATH', 'certificates'),
            ],
            'poll' => [
                'attempts' => env('CERTIFICATES_ACME_POLL_ATTEMPTS', 30),
                'seconds' => env('CERTIFICATES_ACME_POLL_SECONDS', 2),
            ],
            'verify' => env('CERTIFICATES_ACME_VERIFY', true),
        ],

        'filesystem' => [
            'disk' => env('CERTIFICATES_FS_DISK', 'local'),
            'path' => env('CERTIFICATES_FS_PATH', 'certificates'),
            'self_signed' => env('CERTIFICATES_FS_SELF_SIGNED', false),
            'self_signed_days' => env('CERTIFICATES_FS_SELF_SIGNED_DAYS', 90),
        ],

        'null' => [],
        'array' => [],
    ],
];

General keys

KeyEnvDefaultPurpose
defaultCERTIFICATES_DRIVERkubernetesDriver used when none is named — a drivers key or one registered with extend().
connectionCERTIFICATES_DB_CONNECTIONnullRegistry DB connection; null uses the model’s default. Override per call with on().
model—Models\Certificate::classRegistry model; a subclass swaps it everywhere, any other class throws.
table—certificatesRegistry table name.
key_typeCERTIFICATES_KEY_TYPEbigintKey type of the certifiable owner morph: bigint, uuid or ulid (case-insensitive; anything else throws).
renewal.threshold_daysCERTIFICATES_RENEW_THRESHOLD_DAYS21Days before expiry a certificate counts as expiring.
renewal.queueCERTIFICATES_RENEW_QUEUEnullQueue RenewCertificateJob is dispatched onto.
name_prefixCERTIFICATES_NAME_PREFIXgenerated-tls-Prefix of the derived, lowercase DNS-1123 certificate name. '' is a valid prefix — names are then the bare host.
lock.nameCERTIFICATES_LOCK_NAMEcertificates:generatePrefix of the per-certificate provisioning lock ({lock.name}:{certificate name}).
lock.locked_for_secondsCERTIFICATES_LOCK_SECONDS5The lock’s safety expiry in seconds — keep it above your slowest issuance.
status_cache.enabledCERTIFICATES_STATUS_CACHEtrueCache live provider status reports.
status_cache.storeCERTIFICATES_STATUS_CACHE_STOREnullCache store; null uses the default store.
status_cache.ttlCERTIFICATES_STATUS_CACHE_TTL300Status cache lifetime in seconds.
alerts.enabledCERTIFICATES_ALERTSfalseRaise alerts from certificates:check and on lifecycle failures.
alerts.notifiableCERTIFICATES_ALERTS_NOTIFIABLEnullNotifiable FQCN resolved from the container — it must resolve to a stored Eloquent model, so bind the class to its record (an unbound class builds an unsaved model, which throws); beats the certificate’s owner.
alerts.thresholds.warning_daysCERTIFICATES_ALERTS_WARNING_DAYS30Warning band of the expiry check.
alerts.thresholds.critical_daysCERTIFICATES_ALERTS_CRITICAL_DAYS7Critical band — the check fails inside it.
alerts.channels—['mail']Channels of the registry-wide check.
alerts.register_checkCERTIFICATES_ALERTS_REGISTER_CHECKfalseRegister one registry-wide expiry check at boot.

Driver keys are covered on the ACME & Let’s Encrypt, Kubernetes & cert-manager and Filesystem driver pages. The null and array drivers take no options.

Environment

The common knobs are env-driven, so you rarely need to publish the file:

CERTIFICATES_DRIVER=acme
CERTIFICATES_RENEW_THRESHOLD_DAYS=21
CERTIFICATES_RENEW_QUEUE=certificates
CERTIFICATES_STATUS_CACHE_TTL=300
CERTIFICATES_ALERTS=true
CERTIFICATES_ALERTS_WARNING_DAYS=30
CERTIFICATES_ALERTS_CRITICAL_DAYS=7

Every bool switch accepts the usual env spellings — true/false, 1/0, on/off, yes/no — so CERTIFICATES_ALERTS=1 turns alerts on and CERTIFICATES_STATUS_CACHE=off turns the cache off. Not set — absent, null or blank ('', what KEY= in .env gives) — reads as the default; anything else (a typo such as disabled) throws InvalidConfigurationException naming the key instead of quietly reading as the default.

Strict config reads

Every other value is read just as strictly. Not set (absent, null or blank) reads as the default; a present value of the wrong shape throws InvalidConfigurationException naming the key:

  • Integers take an int or a canonical integer string (env values arrive as strings), so CERTIFICATES_RENEW_THRESHOLD_DAYS=five, '1.5' or a value out of range throws — never a silent 0. Bounds: renewal.threshold_days, lock.locked_for_seconds, status_cache.ttl, drivers.acme.poll.* and drivers.filesystem.self_signed_days ≥ 1; alerts.thresholds.* ≥ 0; drivers.kubernetes.service.port 1–65535.
  • String settings (default, table, lock.name, disks, paths, namespace, issuer, …) must be strings, and a blank one is not set and takes its default. The optional ones (connection, queues, stores, tokens, contact, solver, notifiable) must be strings when set, and a blank value is not set, so it stays unset. name_prefix may be '' — it is not read as unset, so names are the bare host.
  • drivers.acme.account.key_type is EC or RSA; alerts.channels is a list of channel names; alerts.notifiable must resolve to a stored Eloquent model (bind the class in the container: $this->app->bind(OpsTeam::class, fn () => OpsTeam::query()->firstOrFail())) and drivers.acme.solver to an AcmeChallengeSolver.
  • key_type is bigint, uuid or ulid, case-insensitive — unset or blank reads as bigint, anything else throws. The migration reads it, so set it before you migrate.

Locking and TLS verification

lock.* configures a cache lock taken per certificate ({lock.name}:{certificate name}) while it is provisioned — other certificates are never blocked. While it is held, issue() throws ProvisioningInProgressException (the registry row is left untouched) and generate() returns false. Keep lock.locked_for_seconds above your slowest issuance: an ACME order polls for up to poll.attempts × poll.seconds.

drivers.kubernetes.ca_path and drivers.acme.verify read the same way: a CA bundle path, null or an empty value (or true, 1, on, yes) for the system CA bundle, and only a false value (false, 0, off, no) disables TLS verification — not recommended. Any other string is a bundle path (a typo fails the TLS handshake — it never disables verification), and a value that is neither a string nor a boolean (an array, a float) throws InvalidConfigurationException.

Derived certificate names

Every certificate gets a deterministic name: name_prefix plus the primary domain, lowercased and folded into a DNS-1123 subdomain — only a–z, 0–9 and hyphens, a leading *. becomes wildcard-, and an over-long name is cut to 253 characters with a hash suffix. Domains are case-insensitive, so App.Example.com and app.example.com share one name and one registry row. The name is the Kubernetes secret name, the directory name on a store disk and half of the registry’s unique (driver, name) key:

Certificates::certificateName('app.example.com'); // "generated-tls-app-example-com"
Certificates::certificateName('App.Example.com'); // "generated-tls-app-example-com" — the same certificate
Certificates::certificateName('*.Example.com');   // "generated-tls-wildcard-example-com"

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.