PowerShell Script to Create Self-Signed Certificate for Entra App Registration

Create PFX, CER, cert.pem, key.pem, and combined.pem for PnP PowerShell app-only authentication with Windows PowerShell 5.1 and PowerShell 7 compatibility.

If your automation uses Connect-PnPOnline with Entra app registration, certificate-based authentication is usually the most stable option. This guide gives you a practical script that generates all common certificate outputs used in real projects: .pfx, .cer, cert.pem, key.pem, and combined.pem. It also explains Entra app registration setup and the difference between delegated and application permissions so you can choose the correct model.

Compatibility scope: this script is built for Windows PowerShell 5.1 and PowerShell 7 on Windows. The New-SelfSignedCertificate cmdlet is Windows-specific.

Entra app registration basics

Before running any script, create an app registration in Microsoft Entra admin center, note the Application (client) ID and Directory (tenant) ID, and upload the generated .cer file under Certificates & secrets. Never upload the .pfx file.

Need the full registration walkthrough? Use our PnP PowerShell Entra app registration guide. If your app is already registered and you already have the required IDs and API permissions, skip app creation and continue from certificate upload in this guide.

Where to upload the certificate in Entra (exact path)

  1. Open Microsoft Entra admin center.
  2. Navigate to Identity > Applications > App registrations, then open your app registration.
  3. From the left menu, select Certificates & secrets.
  4. Open the Certificates tab (not Client secrets).
  5. Select Upload certificate, choose pnp-apponly.cer, then select Add.
  6. Confirm you can see Thumbprint, Start date, and Expires values after upload.
Microsoft Entra App registrations Certificates and secrets pane showing where certificate credentials are managed
Microsoft Docs screenshot of the App registrations credentials area. Use this blade to go to Certificates, then choose Upload certificate and add your .cer file.

Important: upload only the public .cer file to Entra. Keep the private .pfx file in a secure location and use it only from your automation runtime.

Post-upload check: copy the uploaded certificate thumbprint from Entra and compare it with your script output thumbprint. If they do not match, the wrong certificate was uploaded.

Delegated vs application API permissions

ModelHow it runsBest forCommon PnP pattern
DelegatedActs as signed-in user identityInteractive admin tasks and testingConnect-PnPOnline -Interactive -ClientId ...
ApplicationActs as app identity (no user)Scheduled jobs, runbooks, CI/CDConnect-PnPOnline -CertificatePath ...

For unattended scripts, use application permissions. For one-off manual administration, delegated can be fine. Mixing them without design is a common root cause of "connected but access denied" problems.

PowerShell 5 and 7 compatible script

This version keeps your original intent but makes private-key PEM export safe across runtimes. The PFX and CER files always generate on both Windows PowerShell 5.1 and PowerShell 7. For key.pem and combined.pem, the script uses native .NET export on PowerShell 7, falls back to OpenSSL on Windows PowerShell 5.1, and simply warns (instead of failing) if neither path is available — because Entra only needs the .cer and PnP uses the .pfx.

Why the fallback exists: the managed methods ExportPkcs8PrivateKey() and ExportRSAPrivateKey() only exist on the .NET runtime used by PowerShell 7. Windows PowerShell 5.1 runs on .NET Framework, which cannot export the private key in managed code, so OpenSSL (or PowerShell 7) is required to produce PEM key files.

PowerShell - create PFX/CER/PEM for Entra app registration
# ===== Generate PFX/CER and PEMs for Entra App Registration =====
# Compatible with Windows PowerShell 5.1 and PowerShell 7 (Windows)

# --- Hardcoded settings
$OutDir      = "C:\Secure\Certs"
$SubjectCN   = "PnP-App-registration"
$YearsValid  = 10
$PfxPassword = "ChangeMeBeforeUse!"

if ([string]::IsNullOrWhiteSpace($PfxPassword)) {
    throw "Set a non-empty PFX password before running this script."
}

if (-not $IsWindows -and $PSVersionTable.PSVersion.Major -ge 6) {
    throw "This script uses New-SelfSignedCertificate, which requires Windows."
}

# --- Prep output
if (-not (Test-Path $OutDir)) {
    New-Item -ItemType Directory -Path $OutDir -Force | Out-Null
}

$pfxPath     = Join-Path $OutDir "pnp-apponly.pfx"
$cerPath     = Join-Path $OutDir "pnp-apponly.cer"
$certPemPath = Join-Path $OutDir "cert.pem"
$keyPemPath  = Join-Path $OutDir "key.pem"
$combinedPem = Join-Path $OutDir "combined.pem"

function To-PemBlock {
    param(
        [Parameter(Mandatory)][string]$Label,
        [Parameter(Mandatory)][byte[]]$Bytes
    )
    $b64 = [Convert]::ToBase64String($Bytes, [System.Base64FormattingOptions]::InsertLineBreaks)
    return "-----BEGIN $Label-----`n$b64`n-----END $Label-----`n"
}

function Get-PrivateKeyPem {
    param([Parameter(Mandatory)]$Certificate)

    $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)
    $ecdsa = $null
    if (-not $rsa) {
        $ecdsa = [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::GetECDsaPrivateKey($Certificate)
    }

    if ($rsa) {
        if ($rsa.GetType().GetMethod("ExportPkcs8PrivateKey")) {
            return To-PemBlock -Label "PRIVATE KEY" -Bytes ($rsa.ExportPkcs8PrivateKey())
        }
        if ($rsa.GetType().GetMethod("ExportRSAPrivateKey")) {
            return To-PemBlock -Label "RSA PRIVATE KEY" -Bytes ($rsa.ExportRSAPrivateKey())
        }
        return $null   # .NET Framework (Windows PowerShell 5.1): no managed key export
    }

    if ($ecdsa) {
        if ($ecdsa.GetType().GetMethod("ExportPkcs8PrivateKey")) {
            return To-PemBlock -Label "PRIVATE KEY" -Bytes ($ecdsa.ExportPkcs8PrivateKey())
        }
        if ($ecdsa.GetType().GetMethod("ExportECPrivateKey")) {
            return To-PemBlock -Label "EC PRIVATE KEY" -Bytes ($ecdsa.ExportECPrivateKey())
        }
        return $null
    }

    return $null
}

# --- Create self-signed certificate (CurrentUser\My)
$notAfter = (Get-Date).AddYears($YearsValid)
$cert = New-SelfSignedCertificate `
    -Subject "CN=$SubjectCN" `
    -KeyAlgorithm RSA -KeyLength 3072 `
    -KeyExportPolicy Exportable `
    -KeyUsage DigitalSignature, KeyEncipherment `
    -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" `
    -CertStoreLocation "Cert:\CurrentUser\My" `
    -NotAfter $notAfter `
    -HashAlgorithm SHA256

if (-not $cert) {
    throw "Failed to create self-signed certificate."
}
Write-Host "Created cert. Thumbprint: $($cert.Thumbprint)" -ForegroundColor Green

# --- Export PFX & CER
$secPwd = ConvertTo-SecureString -String $PfxPassword -AsPlainText -Force
Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $secPwd -Force | Out-Null
Export-Certificate -Cert $cert -FilePath $cerPath -Force | Out-Null
Write-Host "Exported PFX: $pfxPath" -ForegroundColor Cyan
Write-Host "Exported CER: $cerPath" -ForegroundColor Cyan

# --- Reopen PFX with private key (exportable) for PEM creation
$pfxBytes = [IO.File]::ReadAllBytes($pfxPath)
$x509 = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
    $pfxBytes,
    $PfxPassword,
    [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
)

# --- cert.pem (certificate)
$derCert = $x509.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
$certPem = To-PemBlock -Label "CERTIFICATE" -Bytes $derCert
[IO.File]::WriteAllText($certPemPath, $certPem, [Text.Encoding]::ASCII)
Write-Host "Exported cert.pem" -ForegroundColor Cyan

# --- key.pem (private key)
# Native managed export works on PowerShell 7+. Windows PowerShell 5.1
# (.NET Framework) cannot export the key in managed code, so it falls back
# to OpenSSL when available; otherwise key.pem is skipped (PFX/CER are all
# Entra needs).
$keyPem = Get-PrivateKeyPem -Certificate $x509

if ($keyPem) {
    [IO.File]::WriteAllText($keyPemPath, $keyPem, [Text.Encoding]::ASCII)
}
elseif (Get-Command openssl -ErrorAction SilentlyContinue) {
    Write-Host "Using OpenSSL fallback for key.pem (PowerShell 5.1)..." -ForegroundColor Yellow
    & openssl pkcs12 -in $pfxPath -nocerts -nodes -out $keyPemPath -passin "pass:$PfxPassword" 2>$null
    if (-not (Test-Path $keyPemPath)) {
        # OpenSSL 3.x often needs -legacy for a Windows-exported PFX
        & openssl pkcs12 -in $pfxPath -nocerts -nodes -legacy -out $keyPemPath -passin "pass:$PfxPassword" 2>$null
    }
}

if (Test-Path $keyPemPath) {
    Write-Host "Exported key.pem" -ForegroundColor Cyan

    # --- combined.pem (cert + key)
    $keyPemText = Get-Content -Raw -Path $keyPemPath
    [IO.File]::WriteAllText($combinedPem, ($certPem + $keyPemText), [Text.Encoding]::ASCII)
    Write-Host "Exported combined.pem" -ForegroundColor Cyan
}
else {
    Write-Warning "key.pem/combined.pem skipped. Run on PowerShell 7 or install OpenSSL to generate PEM private keys. PFX and CER are ready for Entra."
}

# --- Summary
Write-Host "`n==== Summary ====" -ForegroundColor Yellow
Write-Host ("PFX:      {0}" -f $pfxPath)
Write-Host ("CER:      {0}" -f $cerPath)
Write-Host ("cert.pem: {0}" -f $certPemPath)
if (Test-Path $keyPemPath)  { Write-Host ("key.pem:  {0}" -f $keyPemPath) }
if (Test-Path $combinedPem) { Write-Host ("combined: {0}" -f $combinedPem) }
Write-Host ("Thumbprint: {0}" -f $cert.Thumbprint)
Write-Host ("Subject:  {0}" -f $SubjectCN)
Write-Host ("Valid to: {0:yyyy-MM-dd}" -f $notAfter)

Install the certificate on the automation device

If your runbook or scheduled task uses -Thumbprint, the certificate must be installed in the certificate store of the account that runs the job. If you use -CertificatePath, you can keep the PFX as a file instead.

Option A - keep PFX as a file (simple and common)

  1. Store pnp-apponly.pfx in a protected folder on the automation machine.
  2. Grant read access only to the service account running the script.
  3. Use Connect-PnPOnline -CertificatePath with a secure password source.

Option B - import to Windows certificate store (thumbprint mode)

Mandatory for thumbprint mode: -Thumbprint works only when that certificate is installed in the local certificate store of the same Windows identity running the script (user account, service account, or task account).

PowerShell - import PFX into CurrentUser\My (same account that runs automation)
$pfxPath = "C:\Secure\Certs\pnp-apponly.pfx"
$pfxPassword = ConvertTo-SecureString -String "ChangeMeBeforeUse!" -AsPlainText -Force

Import-PfxCertificate `
  -FilePath $pfxPath `
  -CertStoreLocation "Cert:\CurrentUser\My" `
  -Password $pfxPassword | Out-Null

# Verify it exists
Get-ChildItem "Cert:\CurrentUser\My" |
  Where-Object Subject -like "*PnP-App-registration*" |
  Select-Object Subject, Thumbprint, NotAfter
PowerShell - get thumbprint to use in Connect-PnPOnline
$cert = Get-ChildItem "Cert:\CurrentUser\My" |
  Where-Object Subject -like "*PnP-App-registration*" |
  Sort-Object NotAfter -Descending |
  Select-Object -First 1

$cert.Thumbprint

For service or machine-wide jobs, import to Cert:\LocalMachine\My and ensure the runtime identity has permission to use the private key.

How to use certificate info in your connection settings

After app registration and certificate upload, you need four values for reliable app-only auth:

  • Tenant: your tenant domain (for example contoso.onmicrosoft.com) or tenant ID.
  • ClientId: Application (client) ID from the Entra app overview page.
  • Certificate: either CertificatePath + password, or Thumbprint.
  • Target URL: SharePoint tenant or site URL you are automating.
PowerShell - app-only with CertificatePath (PFX file)
$pfxPassword = ConvertTo-SecureString -String "ChangeMeBeforeUse!" -AsPlainText -Force

Connect-PnPOnline `
  -Url "https://contoso.sharepoint.com" `
  -ClientId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
  -Tenant "contoso.onmicrosoft.com" `
  -CertificatePath "C:\Secure\Certs\pnp-apponly.pfx" `
  -CertificatePassword $pfxPassword
PowerShell - app-only with Thumbprint (store-installed cert)
Connect-PnPOnline `
  -Url "https://contoso.sharepoint.com" `
  -ClientId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
  -Tenant "contoso.onmicrosoft.com" `
  -Thumbprint "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"

Operational tip: if scheduled automation fails with certificate errors, check that the cert is installed for the same identity running the job and that the thumbprint exactly matches the uploaded Entra certificate.

How to use this certificate with Entra app registration

  1. Run the script and keep pnp-apponly.pfx private.
  2. Upload only pnp-apponly.cer to Entra app registration.
  3. Grant required API permissions, then click Grant admin consent.
  4. Use app-only authentication in PnP PowerShell.
PowerShell - app-only PnP connection with generated certificate
$pfxPassword = ConvertTo-SecureString -String "ChangeMeBeforeUse!" -AsPlainText -Force

Connect-PnPOnline `
  -Url "https://contoso.sharepoint.com" `
  -ClientId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
  -Tenant "contoso.onmicrosoft.com" `
  -CertificatePath "C:\Secure\Certs\pnp-apponly.pfx" `
  -CertificatePassword $pfxPassword

Security recommendation: avoid hardcoding certificate passwords in scripts. For production, load secrets from environment variables, Azure Key Vault, or your secure secret manager.

Permission design checklist

  • Use delegated permissions for interactive admin tasks.
  • Use application permissions for unattended jobs.
  • Prefer least privilege over broad *.FullControl.All where possible.
  • Document which scripts use which app registration and certificate.
  • Set a certificate renewal reminder at least 60 days before expiry.

Need help hardening your PnP PowerShell authentication model?

OceanCloud can help you standardize app registration, certificate rotation, and least-privilege API permissions for production automation.

Book a Security Review