lib/core/icingaagent/installer/Install-IcingaAgentCertificates.psm1

<#
.SYNOPSIS
    Installs the required certificates for the Icinga Agent including the entire
    signing process either by using the CA-Proxy, the CA-Server directly or
    by manually signing the request on the CA master
.DESCRIPTION
    Installs the required certificates for the Icinga Agent including the entire
    signing process either by using the CA-Proxy, the CA-Server directly or
   by manually signing the request on the CA master
.FUNCTIONALITY
    Creates, installs and signs required certificates for the Icinga Agent
.EXAMPLE
    # Connect to the CA server with a ticket to fully complete the request
    PS>Install-IcingaAgentCertificates -Hostname 'windows.example.com' -Endpoint 'icinga2.example.com' -Ticket 'my_secret_ticket';
.EXAMPLE
    # Connect to the CA server without a ticket, to create the sign request on the master
    PS>Install-IcingaAgentCertificates -Hostname 'windows.example.com' -Endpoint 'icinga2.example.com';
.EXAMPLE
    # Uses the Icinga ca.crt from a local filesystem and prepares the Icinga Agent for receiving connections from the Master/Satellite for signing
    PS>Install-IcingaAgentCertificates -Hostname 'windows.example.com' -CACert 'C:\users\public\icinga2\ca.crt';
.EXAMPLE
    # Uses the Icinga ca.crt from a web resource and prepares the Icinga Agent for receiving connections from the Master/Satellite for signing
    PS>Install-IcingaAgentCertificates -Hostname 'windows.example.com' -CACert 'https://example.com/icinga2/ca.crt';
.PARAMETER Hostname
    The hostname of the local system. Has to match the object name within the Icinga configuration
.PARAMETER Endpoint
    The address of either the Icinga CA master or a parent node of the Agent to transmit the request to the CA master
.PARAMETER Port
    The port used for Icinga communication. Uses 5665 as default
.PARAMETER CACert
    Allows to specify the path to the ca.crt from the Icinga CA master on a local, network or web share to allow certificate generation
    in case the Icinga Agent is not able to connect to it's parent hosts
.PARAMETER Ticket
    The ticket number for the signing request which is either generated by Icinga 2 or the Icinga Director
.PARAMETER Force
    Ignores existing certificates and will force the creation, overriding existing certificates
.INPUTS
   System.String
.OUTPUTS
   System.Boolean
.LINK
   https://github.com/Icinga/icinga-powershell-framework
#>


function Install-IcingaAgentCertificates()
{
    param(
        [string]$Hostname,
        [string]$Endpoint,
        [int]$Port        = 5665,
        [string]$CACert,
        [string]$Ticket,
        [switch]$Force    = $FALSE
    );

    if ([string]::IsNullOrEmpty($Hostname)) {
        Write-IcingaConsoleError 'Failed to install Icinga Agent certificates. Please provide a hostname';
        return $FALSE;
    }

    # Default for Icinga 2.8.0 and above
    [string]$NewCertificateDirectory = (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs\');
    [string]$OldCertificateDirectory = (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\etc\icinga2\pki\');
    [string]$CertificateDirectory    = $NewCertificateDirectory;
    if ((Compare-IcingaVersions -RequiredVersion '2.8.0') -eq $FALSE) {
        # Certificate path for versions older than 2.8.0
        $CertificateDirectory = $OldCertificateDirectory;
        Move-IcingaAgentCertificates -Source $NewCertificateDirectory -Destination $OldCertificateDirectory;
    } else {
        Move-IcingaAgentCertificates -Source $OldCertificateDirectory -Destination $NewCertificateDirectory;
    }

    if (-Not (Test-IcingaAgentCertificates -CertDirectory $CertificateDirectory -Hostname $Hostname -Force $Force)) {
        Write-IcingaConsoleNotice ([string]::Format('Generating host certificates for host "{0}"', $Hostname));

        $arguments = [string]::Format('pki new-cert --cn {0} --key {1}{0}.key --cert {1}{0}.crt',
            $Hostname,
            $CertificateDirectory
        );

        if ((Start-IcingaAgentCertificateProcess -Arguments $arguments) -eq $FALSE) {
            Write-IcingaConsoleError 'Failed to generate host certificate';
            return $FALSE;
        }

        # Once we generated new host certificates, we always require to sign them if possible
        $Force = $TRUE;
    }

    if ([string]::IsNullOrEmpty($Endpoint) -And [string]::IsNullOrEmpty($CACert)) {
        Write-IcingaConsoleWarning 'Your host certificates were generated successfully. Please either specify an endpoint to connect to or provide the path to a valid ca.crt';
        return $FALSE;
    }

    if (-Not [string]::IsNullOrEmpty($Endpoint)) {
        if (-Not (Test-IcingaAgentCertificates -CertDirectory $CertificateDirectory -Hostname $Hostname -TestTrustedParent -Force $Force)) {

            Write-IcingaConsoleNotice ([string]::Format('Fetching trusted master certificate from "{0}"', $Endpoint));

            # Argument --key for save-cert is deprecated starting with Icinga 2.12.0
            if (Compare-IcingaVersions -RequiredVersion '2.12.0') {
                $arguments = [string]::Format('pki save-cert --trustedcert {0}trusted-parent.crt --host {1} --port {2}',
                    $CertificateDirectory,
                    $Endpoint,
                    $Port
                );
            } else {
                $arguments = [string]::Format('pki save-cert --key {0}{1}.key --trustedcert {0}trusted-parent.crt --host {2} --port {3}',
                    $CertificateDirectory,
                    $Hostname,
                    $Endpoint,
                    $Port
                );
            }

            if ((Start-IcingaAgentCertificateProcess -Arguments $arguments) -eq $FALSE) {
                Write-IcingaConsoleError -Message 'Unable to connect to your provided Icinga CA. Please verify the entered configuration is correct.' `
                    'If you are not able to connect to your Icinga CA from this machine, you will have to provide the path' `
                    'to your Icinga ca.crt and use the CA-Proxy certificate handling.';
                return $FALSE;
            }
        }

        if (-Not (Test-IcingaAgentCertificates -CertDirectory $CertificateDirectory -Hostname $Hostname -TestCACert -Force $Force)) {
            [string]$PKIRequest = 'pki request --host {0} --port {1} --ticket {4} --key {2}{3}.key --cert {2}{3}.crt --trustedcert {2}trusted-parent.crt --ca {2}ca.crt';

            if ([string]::IsNullOrEmpty($Ticket)) {
                $PKIRequest = 'pki request --host {0} --port {1} --key {2}{3}.key --cert {2}{3}.crt --trustedcert {2}trusted-parent.crt --ca {2}ca.crt';
            }

            $arguments = [string]::Format($PKIRequest,
                $Endpoint,
                $Port,
                $CertificateDirectory,
                $Hostname,
                $Ticket
            );

            if ((Start-IcingaAgentCertificateProcess -Arguments $arguments) -eq $FALSE) {
                Write-IcingaConsoleError 'Failed to sign Icinga certificate';
                return $FALSE;
            }

            if ([string]::IsNullOrEmpty($Ticket)) {
                Write-IcingaConsoleNotice 'Your certificates were generated successfully. Please sign the certificate now on your Icinga CA master. You can lookup open requests with "icinga2 ca list"';
            } else {
                Write-IcingaConsoleNotice 'Icinga certificates successfully installed';
            }
        }

        return $TRUE;
    } elseif (-Not [string]::IsNullOrEmpty($CACert)) {
        if (-Not (Copy-IcingaAgentCACertificate -CAPath $CACert -Destination $CertificateDirectory)) {
            return $FALSE;
        }
        Write-IcingaConsoleNotice 'Host-Certificates and ca.crt are present. Please start your Icinga Agent now and manually sign your certificate request on your CA master. You can lookup open requests with "icinga2 ca list"';
    }

    return $TRUE;
}

function Start-IcingaAgentCertificateProcess()
{
    param(
        $Arguments
    );

    $Binary  = Get-IcingaAgentBinary;
    $Process = Start-IcingaProcess -Executable $Binary -Arguments $Arguments;

    if ($Process.ExitCode -ne 0) {
        Write-IcingaConsoleError ([string]::Format('Failed to create certificate.{0}Arguments: {1}{0}Error:{2} {3}', "`r`n", $Arguments, $Process.Message, $Process.Error));
        return $FALSE;
    }

    Write-IcingaConsoleNotice $Process.Message;
    return $TRUE;
}

function Move-IcingaAgentCertificates()
{
    param(
        [string]$Source,
        [string]$Destination
    );

    $SourceDir = Join-Path -Path $Source -ChildPath '\*';
    $TargetDir = Join-Path -Path $Destination -ChildPath '\';

    Move-Item -Path $SourceDir -Destination $TargetDir;
}

function Test-IcingaAgentCertificates()
{
    param(
        [string]$CertDirectory,
        [string]$Hostname,
        [switch]$TestCACert,
        [switch]$TestTrustedParent,
        [bool]$Force
    );

    if ($Force) {
        return $FALSE;
    }

    if ($TestCACert) {
        if (Test-Path (Join-Path -Path $CertDirectory -ChildPath 'ca.crt')) {
            Write-IcingaConsoleNotice 'Your ca.crt is present. No generation or fetching required';
            return $TRUE;
        } else {
            Write-IcingaConsoleWarning 'Your ca.crt is not present. Manually copy or fetching from your Icinga CA is required.';
            return $FALSE;
        }
    }

    if ($TestTrustedParent) {
        if (Test-Path (Join-Path -Path $CertDirectory -ChildPath 'trusted-parent.crt')) {
            Write-IcingaConsoleNotice 'Your trusted-parent.crt is present. No fetching or generation required';
            return $TRUE;
        } else {
            Write-IcingaConsoleWarning 'Your trusted master certificate is not present. Fetching from your CA server is required';
            return $FALSE;
        }
    }

    if ((-Not (Test-Path ((Join-Path -Path $CertDirectory -ChildPath $Hostname) + '.key'))) `
            -Or -Not (Test-Path ((Join-Path -Path $CertDirectory -ChildPath $Hostname) + '.crt'))) {
        return $FALSE;
    }

    [string]$hostCRT       = [string]::Format('{0}.crt', $Hostname);
    [string]$hostKEY       = [string]::Format('{0}.key', $Hostname);
    [bool]$CertNameInvalid = $FALSE;

    $certificates = Get-ChildItem -Path $CertDirectory;
    # Now loop each file and match their name with our hostname
    foreach ($cert in $certificates) {
        if ($cert.Name.toLower() -eq $hostCRT.toLower() -Or $cert.Name.toLower() -eq $hostKEY.toLower()) {
            $file = $cert.Name.Replace('.key', '').Replace('.crt', '');
            if (-Not ($file -clike $Hostname)) {
                Write-IcingaConsoleWarning ([string]::Format('Certificate file {0} is not matching the hostname {1}. Certificate generation is required.', $cert.Name, $Hostname));
                $CertNameInvalid = $TRUE;
                break;
            }
        }
    }

    if ($CertNameInvalid) {
        Remove-Item -Path (Join-Path -Path $CertDirectory -ChildPath $hostCRT) -Force;
        Remove-Item -Path (Join-Path -Path $CertDirectory -ChildPath $hostKEY) -Force;
        return $FALSE;
    }

    Write-IcingaConsoleNotice 'Icinga host certificates are present and valid. No generation required';

    return $TRUE;
}

function Copy-IcingaAgentCACertificate()
{
    param(
        [string]$CAPath,
        [string]$Destination
    );

    # Copy ca.crt from local path or network share to certificate path
    if ((Test-Path $CAPath)) {
        Copy-Item -Path $CAPath -Destination (Join-Path -Path $Destination -ChildPath 'ca.crt') | Out-Null;
        Write-IcingaConsoleNotice ([string]::Format('Copied ca.crt from "{0}" to "{1}', $CAPath, $Destination));
    } else {
        Set-IcingaTLSVersion;
        # It could also be a web resource
        try {
            $response   = Invoke-IcingaWebRequest $CAPath -UseBasicParsing;
            [int]$Index = $response.RawContent.IndexOf("`r`n`r`n") + 4;

            [string]$CAContent = $response.RawContent.SubString(
                $Index,
                $response.RawContent.Length - $Index
            );
            Write-IcingaFileSecure -File (Join-Path $Destination -ChildPath 'ca.crt') -Value $CAContent;
            Write-IcingaConsoleNotice ([string]::Format('Downloaded ca.crt from "{0}" to "{1}', $CAPath, $Destination))
        } catch {
            Write-IcingaConsoleError 'Failed to load any provided ca.crt resource';
            return $FALSE;
        }
    }

    return $TRUE;
}

Export-ModuleMember -Function @('Install-IcingaAgentCertificates');