spatie/ssl-certificate
A PHP package from Spatie that makes it easy to query SSL certificate properties. Fetch a certificate from a hostname, file, or string, then check validity, expiration dates, issuer, and key details.
Architecture fit
spatie/ssl-certificate is a focused, framework-agnostic value-object library, not a Laravel package in the traditional sense: no service provider, no config file, no facade registration. It depends only on nesbot/carbon and spatie/macroable, both already present in most Laravel codebases. Architecturally it occupies a narrow seam — "given a hostname, file, or DER/PEM string, return a typed certificate object" — which makes it easy to wrap behind our own CertificateInspector interface and swap out later. That is the right shape for us: it should be an implementation detail behind a domain port, never called directly from controllers or Blade views.
Its capabilities are metadata introspection, not validation: isValid(), isExpired(), isValidUntil(), daysUntilExpirationDate(), getDomains(), getAdditionalDomains(), getIssuer(), getSubjectOrganization(), getFingerprintSha256(), getPublicKeyAlgorithm()/Size(), getSignatureAlgorithm(), isSelfSigned(), usesSha1Hash(), getSerialNumber(), plus toArray()/createFromArray() for cache-friendly round-tripping.
Integration feasibility
Low friction. composer require spatie/ssl-certificate, PHP 8+ only (v2.x line), MIT licensed, actively released with a documented changelog back to 1.3.x. The fluent downloader (download()->usingPort()->fromIpAddress()->withSocketContextOptions()->withVerifyPeer()->withVerifyPeerName()->forHost()) covers the awkward cases we actually hit: non-443 ports, pinning a check to a specific A/AAAA record before DNS cutover, IPv6 targets, and custom socket context for SNI or proxy egress. createFromFile() now handles DER as of 2.4.0, which matters if we ingest certificates from an internal CA or an asset inventory rather than the network.
Technical risk
The dominant risk is semantic, not technical: the README explicitly states the package does not check whether a certificate is signed by a trusted authority. isValid() means "now is between notBefore and notAfter, and the hostname matches the SANs" — it does not mean "this certificate is trustworthy." Any code path that treats a true from this library as a security clearance is a defect. Trust decisions must stay with openssl/cURL/Guzzle with peer verification enabled; this package is for reporting, expiry alerting, drift detection, and inventory.
Secondary risks: synchronous TLS handshakes are blocking network I/O with a caller-supplied timeout, so an unreachable host stalls the calling process; wildcard semantics have had real bugs (2.6.10 corrected wildcards incorrectly matching the root domain), so hostname-matching results should be treated as advisory; there are no OCSP/CRL or revocation checks; and "0 dependents" reported by the source data is a weak adoption signal that argues for a thin internal wrapper rather than deep coupling.
Key questions
isValid() implies a verified chain?2026-06-05 release date correspond to a real, pinnable tag in our registry mirror, or is it a feed artifact?Stack fit
Near-perfect for a Laravel estate. It slots naturally into three places: (1) a scheduled php artisan command that sweeps a domains table nightly and writes expiry/issuer/fingerprint into a certificate_checks table; (2) an on-demand admin panel action to inspect a tenant's custom domain during onboarding or support triage; (3) a health-check endpoint or dashboard widget for our own infrastructure endpoints. Because toArray()/createFromArray() exist, we can persist the parsed certificate snapshot and diff it release-over-release to detect unexpected issuer or key-size changes — that is where the real product value sits, not in a single isExpired() call.
Migration path
Greenfield — there is nothing to migrate away from. The only "migration" is behavioural: teams currently shelling out to openssl s_client in ad-hoc scripts or copy-pasting expiry dates into a spreadsheet should be redirected to the internal service. Introduce a single CertificateInspector contract in our codebase with one inspect(string $host, int $port = 443): CertificateSnapshot method, implement it with this package, and register the binding in a service provider. Everything else in the application depends on the contract, not on Spatie\SslCertificate\SslCertificate.
Compatibility
PHP 8.0+ required; verify against our matrix. nesbot/carbon is already a Laravel transitive dependency, so no version conflict expected. No Laravel version constraint is imposed, which is good — but it also means no framework integration is provided, so scheduling, queueing, caching, and notification are all ours to wire. Pin the major version (^2.6) and let patch/minor float; the changelog shows a disciplined semver history with no breaking changes inside 2.x.
Sequencing
CertificateInspector contract; validate against a fixture set of known-good, expired, self-signed, wildcard, and multi-SAN certificates plus badssl.com-style targets. Confirm DER-file ingestion and IPv6 paths.Maintenance
Very low. MIT license, single maintainer organisation (Spatie) with a long, consistent release cadence and a readable changelog. The API surface we depend on is small and stable, which caps our exposure to upstream churn. The real maintenance cost is not the dependency — it's the monitoring product we build on top of it: alert thresholds, the domain inventory, and the snapshot retention policy. Budget accordingly.
Support
Spatie supports this as open source with no SLA; the "postcardware" framing in the README is a fair signal of expected support posture. Escalation path for a bug is a GitHub issue, and realistically a fork or a local patch if we hit an edge case (wildcard matching is the historically fragile area — see 1.3.2 and 2.6.10). Because it's MIT, forking is a legitimate fallback; keep the wrapper thin so a fork swap is a one-file change.
Scaling
The library does not scale itself — it does one blocking TLS handshake per call. At a few hundred domains nightly, that is trivially fine. At tens of thousands, the constraint is outbound connection concurrency and DNS resolution, not CPU. Design for queue workers with bounded concurrency (say 20–50 in flight), a 5–10 second connect timeout, and exponential backoff on transient failures. Persist snapshots so dashboards and alerts read from the database, never from live handshakes. Cache per host for at least an hour to absorb repeated dashboard loads.
Failure modes
CouldNotDownloadCertificate — DNS failure, refused connection, closed port, or TLS handshake rejected. Note that with verifyCertificate = false (third argument to createForHostName) you can retrieve expired or otherwise invalid certificates, which is exactly what you want for alerting: an expired cert must still be reportable, not silently a connection error. This distinction must be deliberate in the implementation.InvalidUrl / InvalidIpAddress — thrown on malformed input; validate at the boundary and treat as a validation error, not a system failure.isValid() will report healthy for a self-signed or wrong-CA certificate. Pair every trust-sensitive display with a genuine verification check, and label isValid() results as "expiry and hostname match," not "secure."Ramp-up
Effectively zero for a PHP developer — the README is complete and example-driven, and the entire API is discoverable in an afternoon. The learning curve is not the library; it's the TLS domain literacy required to interpret what it returns. Invest half a day in making sure the team understands the difference between "this certificate is currently within its validity window and names this host" and "this certificate is trusted by this client," because that misunderstanding is the only way this integration causes real harm.
How can I help you explore Laravel packages today?