Public/Deploy-LinuxConfiguration.ps1

#!/usr/bin/env pwsh
Set-StrictMode -Version Latest

if ($IsLinux) {
    function Deploy-LinuxConfiguration {
        <#
        .SYNOPSIS
            Copies the PSSecurity Linux hardening files from Config/Linux into place.

        .DESCRIPTION
            Config/Linux mirrors the root of the file system, so each file is copied to the
            same path under /. For example, Config/Linux/etc/sysctl.d/99-pssecurity-hardening.conf
            is copied to /etc/sysctl.d/99-pssecurity-hardening.conf. Missing directories are
            created. Every copied file gets mode 0644, whatever the mode of its source. A file
            whose destination already has the same content and mode 0644 is skipped, so a
            run also repairs the mode of files deployed earlier.

            The files are drop-ins in standard locations: kernel parameters in sysctl.d,
            the kernel module blacklist in modprobe.d, and the core dump policy in
            security/limits.d and systemd/coredump.conf.d. Edit the files in Config/Linux to
            change the policy.

            After a sysctl.d file is copied, runs sysctl --system to apply it without a
            reboot. Keys the running kernel does not support, common in containers and WSL,
            are reported as a warning; the file is still deployed and applies at boot on
            kernels that support them.

            Requires root. Supports -WhatIf and -Confirm. Linux only.

        .PARAMETER Name
            File names to deploy, such as 99-pssecurity-hardening.conf. Wildcards are
            accepted. Defaults to every file in Config/Linux.

        .PARAMETER PassThru
            Returns each file that was copied.

        .INPUTS
            None. You can't pipe objects to this function.

        .OUTPUTS
            System.IO.FileInfo. With -PassThru, one object per copied file. Otherwise
            nothing.

        .EXAMPLE
            Deploy-LinuxConfiguration -WhatIf

            Lists the files that would be copied and where.

        .EXAMPLE
            Invoke-Sudo { Import-Module ./PSSecurity.psd1; Deploy-LinuxConfiguration -PassThru }

            Deploys every file as root and lists the files that were copied.

        .EXAMPLE
            Deploy-LinuxConfiguration -Name '99-pssecurity-hardening.conf', '99-pssecurity-core.conf'

            Deploys only the kernel parameters and the core dump policy, leaving the kernel
            module blacklist out.
        #>

        [CmdletBinding(SupportsShouldProcess)]
        [OutputType([System.IO.FileInfo])]
        param(
            [Parameter(HelpMessage = 'Enter the file names to deploy. Wildcards are accepted.')]
            [ValidateNotNullOrEmpty()]
            [string[]]$Name = '*',

            [Parameter(HelpMessage = 'Return each file that was copied.')]
            [switch]$PassThru
        )

        $configRoot = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Config' -AdditionalChildPath 'Linux'
        $sysctlChanged = $false

        # 0644. Copy-Item keeps the source mode, and /mnt/c shows every Windows file as
        # 0777, which would leave world-writable files in /etc.
        $fileMode = [System.IO.UnixFileMode]'UserRead, UserWrite, GroupRead, OtherRead'

        foreach ($source in Get-ChildItem -LiteralPath $configRoot -File -Recurse -Include $Name) {
            $destination = '/' + [System.IO.Path]::GetRelativePath($configRoot, $source.FullName)

            $upToDate = $false
            if (Test-Path -LiteralPath $destination) {
                $sameContent = (Get-FileHash -LiteralPath $destination).Hash -eq (Get-FileHash -LiteralPath $source.FullName).Hash
                $upToDate = $sameContent -and (Get-Item -LiteralPath $destination).UnixFileMode -eq $fileMode
            }
            if ($upToDate) {
                Write-Verbose -Message "$destination is already up to date."
                continue
            }

            if ($PSCmdlet.ShouldProcess($destination, "Copy from $($source.FullName) with mode 0644")) {
                [void](New-Item -ItemType Directory -Path (Split-Path -Path $destination -Parent) -Force -ErrorAction Stop)
                Copy-Item -LiteralPath $source.FullName -Destination $destination -Force -ErrorAction Stop
                [System.IO.File]::SetUnixFileMode($destination, $fileMode)

                if ($destination -like '/etc/sysctl.d/*') {
                    $sysctlChanged = $true
                }
                if ($PassThru) {
                    Get-Item -LiteralPath $destination
                }
            }
        }

        if ($sysctlChanged) {
            $sysctlOutput = (sysctl --system 2>&1)
            if ($LASTEXITCODE -ne 0) {
                $failures = @($sysctlOutput | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] })
                Write-Warning -Message "sysctl --system could not apply every key: $($failures -join '; ')"
            }
        }
    }
}