Skip to main content

Checks when the specified hostname's SSL/TLS certificate expires.

# Checks when the specified hostname's SSL/TLS certificate expires:
$hostname = "example.com"
$port = 443
$tcpClient = New-Object Net.Sockets.TcpClient($hostname, $port)
$sslStream = New-Object Net.Security.SslStream($tcpClient.GetStream(), $false, ({ $true }))
$sslStream.AuthenticateAsClient($hostname)
$cert = $sslStream.RemoteCertificate
$cert2 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $cert
$cert2.NotAfter
$sslStream.Close()
$tcpClient.Close()

# One-liner:
$hostname="example.com";$port=443;$tcpClient=New-Object Net.Sockets.TcpClient($hostname,$port);$sslStream=New-Object Net.Security.SslStream($tcpClient.GetStream(),$false,({$true}));$sslStream.AuthenticateAsClient($hostname);(New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $sslStream.RemoteCertificate).NotAfter;$sslStream.Close();$tcpClient.Close()

# OpenSSL CLI command equivalent:
# openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -enddate

# ---------------

function Get-CertificateExpiration
{
    <#
    .SYNOPSIS
        Gets the expiration date of an SSL/TLS certificate from a remote host.

    .DESCRIPTION
        This function connects to a remote host via SSL/TLS and retrieves the certificate's expiration date.
        It can check multiple hosts and ports, and provides options for timeout configuration and
        detailed certificate information. The function returns the certificate's NotAfter date which
        indicates when the certificate expires.

    .PARAMETER ComputerName
        The hostname or IP address to check for SSL certificate expiration.
        Accepts an array of computer names. Defaults to 'localhost' if not specified.

    .PARAMETER Port
        The port number to connect to for SSL certificate retrieval.
        Defaults to 443 (standard HTTPS port).

    .PARAMETER Timeout
        Connection timeout in milliseconds. Defaults to 10000 (10 seconds).

    .PARAMETER IncludeCertificateDetails
        If specified, returns detailed certificate information including subject, issuer,
        thumbprint, and other certificate properties instead of just the expiration date.

    .PARAMETER WarnIfExpiresSoon
        If specified, displays a warning if the certificate expires within the specified number of days.
        Defaults to 30 days if the switch is used without a value.

    .PARAMETER DaysToWarn
        Number of days before expiration to show warning. Only used when WarnIfExpiresSoon is specified.
        Defaults to 30 days.

    .EXAMPLE
        PS > Get-CertificateExpiration -ComputerName 'google.com'

        Gets the SSL certificate expiration date for google.com on port 443.

    .EXAMPLE
        PS > Get-CertificateExpiration -ComputerName 'example.com' -Port 8443

        Gets the SSL certificate expiration date for example.com on port 8443.

    .EXAMPLE
        PS > @('google.com', 'github.com', 'stackoverflow.com') | Get-CertificateExpiration

        Gets SSL certificate expiration dates for multiple hosts using pipeline input.

    .EXAMPLE
        PS > Get-CertificateExpiration -ComputerName 'expired.badssl.com' -WarnIfExpiresSoon -DaysToWarn 90

        Gets the certificate expiration date and warns if it expires within 90 days.

    .EXAMPLE
        PS > Get-CertificateExpiration -ComputerName 'google.com' -IncludeCertificateDetails

        Gets detailed certificate information including expiration date, subject, issuer, and thumbprint.

    .EXAMPLE
        PS > Get-CertificateExpiration -ComputerName 'internal.company.com' -Port 8443 -Timeout 5000

        Gets certificate expiration with a 5-second timeout for internal servers.

    .OUTPUTS
        System.DateTime
        Returns the certificate expiration date when IncludeCertificateDetails is not specified.

    .OUTPUTS
        System.Management.Automation.PSCustomObject
        Returns detailed certificate information when IncludeCertificateDetails is specified.

    .LINK
        https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2

    .LINK
        https://docs.microsoft.com/en-us/dotnet/api/system.net.security.sslstream
    #>

    [CmdletBinding()]
    [OutputType([System.DateTime], [System.Management.Automation.PSCustomObject])]
    param
    (
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('HostName', 'Server', 'Name')]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $ComputerName = 'localhost',

        [Parameter()]
        [ValidateRange(1, 65535)]
        [Int32]
        $Port = 443,

        [Parameter()]
        [ValidateRange(1000, 300000)]
        [Int32]
        $Timeout = 10000,

        [Parameter()]
        [Switch]
        $IncludeCertificateDetails,

        [Parameter()]
        [Switch]
        $WarnIfExpiresSoon,

        [Parameter()]
        [ValidateRange(1, 365)]
        [Int32]
        $DaysToWarn = 30
    )

    begin
    {
        Write-Verbose 'Starting SSL certificate expiration check'

        function Get-CertificateFromHost
        {
            param
            (
                [String]$HostName,
                [Int32]$PortNumber,
                [Int32]$TimeoutMs
            )

            $tcpClient = $null
            $sslStream = $null

            try
            {
                Write-Verbose "Connecting to $HostName`:$PortNumber with timeout ${TimeoutMs}ms"

                # Create TCP client with timeout
                $tcpClient = New-Object System.Net.Sockets.TcpClient
                $tcpClient.ReceiveTimeout = $TimeoutMs
                $tcpClient.SendTimeout = $TimeoutMs

                # Connect to the host
                $connectTask = $tcpClient.ConnectAsync($HostName, $PortNumber)
                if (-not $connectTask.Wait($TimeoutMs))
                {
                    throw "Connection to $HostName`:$PortNumber timed out after ${TimeoutMs}ms"
                }

                # Create SSL stream with certificate validation callback that always returns true
                $sslStream = New-Object System.Net.Security.SslStream(
                    $tcpClient.GetStream(),
                    $false,
                    { param($senderObject, $certificate, $chain, $sslPolicyErrors) return $true }
                )

                # Authenticate as client to get the certificate
                $sslStream.AuthenticateAsClient($HostName)

                # Get the certificate
                $certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $sslStream.RemoteCertificate

                return $certificate
            }
            catch
            {
                Write-Error "Failed to retrieve SSL certificate from $HostName`:$PortNumber - $($_.Exception.Message)"
                return $null
            }
            finally
            {
                # Clean up connections
                if ($sslStream)
                {
                    $sslStream.Close()
                    $sslStream.Dispose()
                }
                if ($tcpClient)
                {
                    $tcpClient.Close()
                    $tcpClient.Dispose()
                }
            }
        }
    }

    process
    {
        foreach ($computer in $ComputerName)
        {
            Write-Verbose "Processing host: $computer"

            $certificate = Get-CertificateFromHost -HostName $computer -PortNumber $Port -TimeoutMs $Timeout

            if ($certificate)
            {
                $expirationDate = $certificate.NotAfter
                $daysUntilExpiration = ($expirationDate - (Get-Date)).Days

                # Check if warning should be displayed
                if ($WarnIfExpiresSoon -and $daysUntilExpiration -le $DaysToWarn)
                {
                    if ($daysUntilExpiration -lt 0)
                    {
                        Write-Warning "Certificate for $computer`:$Port EXPIRED $([Math]::Abs($daysUntilExpiration)) days ago on $($expirationDate.ToString('yyyy-MM-dd HH:mm:ss'))"
                    }
                    else
                    {
                        Write-Warning "Certificate for $computer`:$Port expires in $daysUntilExpiration days on $($expirationDate.ToString('yyyy-MM-dd HH:mm:ss'))"
                    }
                }

                if ($IncludeCertificateDetails)
                {
                    # Return detailed certificate information
                    [PSCustomObject]@{
                        ComputerName = $computer
                        Port = $Port
                        Subject = $certificate.Subject
                        Issuer = $certificate.Issuer
                        NotBefore = $certificate.NotBefore
                        NotAfter = $certificate.NotAfter
                        Thumbprint = $certificate.Thumbprint
                        SerialNumber = $certificate.SerialNumber
                        DaysUntilExpiration = $daysUntilExpiration
                        IsExpired = $daysUntilExpiration -lt 0
                        SignatureAlgorithm = $certificate.SignatureAlgorithm.FriendlyName
                        Version = $certificate.Version
                        HasPrivateKey = $certificate.HasPrivateKey
                    }
                }
                else
                {
                    # Return just the expiration date
                    $expirationDate
                }
            }
        }
    }

    end
    {
        Write-Verbose 'SSL certificate expiration check completed'
    }
}