Simplifying Self-Signed Certificates for Multi-Host Environments

:shield: Simplifying Self-Signed Certificates for Multi-Host Environments

:double_exclamation_mark: Please note: This post is to aide in quickly deploying proof-of-concept or test environments. Netwrix does not recommend the use of self-signed certificates in production.

:magnifying_glass_tilted_left: The Challenge

We’re setting up a multi-host Proof of Concept (PoC) for Netwrix Privilege Secure, including:

  • Secure Remote Access
  • Remote Proxy
  • Remote Action Service

The environment includes 6 TLS-enabled hosts:

β€’ portal.domain.com  
β€’ gateway.domain.com  
β€’ guacd.domain.com  
β€’ npsa.domain.com  
β€’ remoteproxy.domain.com  
β€’ remoteactionservice.domain.com

Managing self-signed certificates across all these hosts can be a real pain:

  • Manual certificate generation for each hostname
  • Properly configuring SANs (Subject Alternative Names)
  • Creating and managing full certificate chains
  • Installing multiple self-signed certs on every client machine
  • Generating different file formats based on platform requirements

:backhand_index_pointing_right: Result: Complex, error-prone, and time-consuming.


:white_check_mark: The Solution

To solve this, I used vibe coding to generate simple automated Bash script that:

  • :locked_with_key: Generates a dedicated Root CA for the PoC
  • :brain: Only one Root CA needs to be installed in trust stores β€” all issued certs are trusted automatically
  • :label: Lets you name the Root CA based on the customer or PoC
  • :receipt: Outputs all common formats: .key, .crt, .cer, .pem, .pfx, and -chain.crt
  • :desktop_computer: Installs the Root CA into the Linux trust store (ideal for WSL or Guacamole hosting)
  • :hourglass_not_done: Supports custom validity durations and password-protected .pfx files
  • :window: Generates Windows-ready PFX files for use with IIS, RDP, etc.
  • :repeat_button: Reusable - Re-run the script to generate a new Root CA and new Certificates with Keys for Hosts either in your lab or customer environment

:gear: How It Works

  1. Prompt for:
  • Number of hosts
  • Hostnames
  • Root CA name
  • PFX password
  • Certificate validity (in days)
  1. Automatically generates all certs, keys, and formats
  2. Installs the Root CA to the Linux trust store
  3. Delivers PFX bundles for Windows platforms

:outbox_tray: What to Do After Generating Certificates

On Linux

  • Root CA is added to /usr/local/share/ca-certificates/
  • Run update-ca-certificates (done by the script)
  • Use .crt + .key or -chain.crt in:
    • NGINX
    • Apache
    • Guacamole Daemon
  • .cer and .pem formats are also available for tools and APIs

On Windows

  • Import the .pfx file:
    • Installs the cert + key in Local Machine > Personal
    • Installs the CA in Trusted Root Certification Authorities
  • Use in:
    • IIS (bindings)
    • RDP
    • Remote Proxy and Action Service
  • :light_bulb: Keep the private key secure β€” do not share it!

:package: Prerequisites

  • Linux VM or WSL with OpenSSL installed (should be present by default)
  • Root or sudo privileges to install the Root CA
  • Basic comfort with the terminal

:package: The Script

#!/bin/bash

echo "πŸ” Certificate Generator for Multiple Hosts"

# Prompt for number of hosts
read -p "How many hostnames do you want to generate certificates for? " HOST_COUNT

# Prompt for Root CA name
read -p "Enter the desired Root CA Name: " CA_NAME

# Prompt for certificate validity period
read -p "Enter number of days the certificates should be valid for [Default: 365]: " CERT_VALIDITY
CERT_VALIDITY=${CERT_VALIDITY:-365}

# Prompt for PFX password (hidden input)
read -s -p "Enter password for PFX files: " PFX_PASSWORD
echo ""

# Set base directories
BASE_CERT_DIR="/opt/poc_certs"
CA_DIR="$BASE_CERT_DIR/ca"
HOSTS_DIR="$BASE_CERT_DIR/hosts"

# Create directories
mkdir -p "$CA_DIR" "$HOSTS_DIR"

CA_KEY="$CA_DIR/${CA_NAME// /_}_rootCA.key"
CA_CRT="$CA_DIR/${CA_NAME// /_}_rootCA.crt"

# Generate Root CA if not already exists
if [[ ! -f "$CA_KEY" ]]; then
    echo "πŸ” Generating Root CA..."
    openssl req -x509 -newkey rsa:4096 -nodes \
        -keyout "$CA_KEY" \
        -out "$CA_CRT" \
        -days 1825 -subj "/CN=$CA_NAME"
fi

# Process hostnames
for ((i=1; i<=HOST_COUNT; i++)); do
    read -p "Enter hostname #$i: " HOSTNAME
    CERT_DIR="$HOSTS_DIR/$HOSTNAME"
    mkdir -p "$CERT_DIR"

    CERT_KEY="$CERT_DIR/$HOSTNAME.key"
    CERT_CSR="$CERT_DIR/$HOSTNAME.csr"
    CERT_CRT="$CERT_DIR/$HOSTNAME.crt"
    CERT_CHAIN="$CERT_DIR/$HOSTNAME-chain.crt"
    CERT_PFX="$CERT_DIR/$HOSTNAME.pfx"
    CERT_CER="$CERT_DIR/$HOSTNAME.cer"
    CERT_PEM="$CERT_DIR/$HOSTNAME.pem"
    SAN_CONFIG="$CERT_DIR/$HOSTNAME-san.cnf"

    echo "πŸ”§ Generating certificate for $HOSTNAME..."

    # Create SAN config
    cat > "$SAN_CONFIG" <<EOF
[req]
distinguished_name=req_distinguished_name
req_extensions=v3_req
[req_distinguished_name]
[v3_req]
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = $HOSTNAME
EOF

    # Generate private key
    openssl genrsa -out "$CERT_KEY" 4096

    # Create CSR with SAN
    openssl req -new -key "$CERT_KEY" -out "$CERT_CSR" -subj "/CN=$HOSTNAME" -config "$SAN_CONFIG"

    # Generate cert with SAN and extended key usage
    openssl x509 -req -in "$CERT_CSR" -CA "$CA_CRT" -CAkey "$CA_KEY" -CAcreateserial \
        -out "$CERT_CRT" -days "$CERT_VALIDITY" -extfile "$SAN_CONFIG" -extensions v3_req

    if [[ -f "$CERT_CRT" ]]; then
        cat "$CERT_CRT" "$CA_CRT" > "$CERT_CHAIN"
        echo "βœ… Certificate created: $CERT_CRT"
    else
        echo "❌ Failed to generate certificate for $HOSTNAME"
        continue
    fi

    # Generate PFX
    openssl pkcs12 -export -out "$CERT_PFX" \
        -inkey "$CERT_KEY" \
        -in "$CERT_CHAIN" \
        -certfile "$CA_CRT" \
        -password pass:$PFX_PASSWORD

    echo "πŸ“¦ PFX created: $CERT_PFX"

    # Generate .cer (DER format)
    openssl x509 -in "$CERT_CRT" -outform DER -out "$CERT_CER"
    echo "πŸ“„ CER file created: $CERT_CER"

    # Generate .pem (full chain)
    cat "$CERT_CHAIN" > "$CERT_PEM"
    echo "πŸ“„ PEM file created: $CERT_PEM"

    # Cleanup
    rm -f "$SAN_CONFIG"
done

# Install CA cert into Linux system
echo "πŸ”§ Installing Root CA on Linux..."
CA_INSTALL_PATH="/usr/local/share/ca-certificates/${CA_NAME// /_}_rootCA.crt"
cp "$CA_CRT" "$CA_INSTALL_PATH"
update-ca-certificates

echo ""
echo "πŸŽ‰ All certificates and PFX files generated under $HOSTS_DIR"
echo "πŸ” Root CA: $CA_CRT"

:test_tube: How to Use the Certificate Generator Script

Follow these simple steps to generate self-signed certificates for multiple hosts in just a few minutes.


:white_check_mark: Step 1: Create the Script File in Linux

Open a terminal and create a new file called generate_certs:

bash

CopyEdit

nano generate_certs

:white_check_mark: Step 2: Paste the Script

Copy the full certificate generation script into and paste it into the file you created using nano.

Then save and exit:

  • Press CTRL + O to write (save) the file
  • Press ENTER to confirm
  • Press CTRL + X to exit

:white_check_mark: Step 3: Make the Script Executable & Run It

Now, make the script executable and run it:

bash

CopyEdit

chmod +x generate_certs
./generate_certs

Alternatively, you can run it directly using:

bash

CopyEdit

bash generate_certs

:white_check_mark: Step 4: Follow the Prompts

The script will walk you through the setup interactively:

  • :1234: Enter the number of hostnames (e.g., 6 for a PoC)
  • :globe_with_meridians: Provide each hostname one by one (e.g., portal.domain.com)
  • :label: Set a name for your Root CA (e.g., CustomerPoC Root CA)
  • :locked_with_key: Enter a password to protect .pfx files
  • :hourglass_not_done: Choose how long the certificates should remain valid (in days)

That’s it β€” the script takes care of everything from SANs to full chains!


:white_check_mark: Step 5: View the Generated Certificates

After completion, all certificates will be available under:

bash

CopyEdit

/opt/poc_certs/

Each hostname will have its own folder containing:

  • :key: hostname.key – Private Key
  • :page_facing_up: hostname.crt – Certificate
  • :paperclip: hostname-chain.crt – Certificate with Root CA
  • :package: hostname.pfx – Password-protected Windows-ready file
  • :receipt: hostname.cer, hostname.pem – Alternate formats

:rocket: Benefits

  • :three_o_clock: Massive time-saver vs. manual cert generation
  • :puzzle_piece: Handles SANs and multi-host use cases cleanly
  • :light_bulb: Provides all formats, cross-platform
  • :hammer_and_wrench: One-click Linux trust store integration
  • :locked: Secure, customized cert generation for every PoC or environment

:brain: Final Thoughts

Self-signed certificates often get a bad reputation β€” but they don’t have to be painful.

This solution brings order, automation, and cross-platform support to the process, making it ideal for multi-host PoCs like Netwrix Privilege Secure deployments.

:open_mailbox_with_raised_flag: Need help? Please reach out to me!

:package: Bonus


:light_bulb: Update: Windows Support Added!

You can now generate the same multi-host certificates using PowerShell on Windows.

:white_check_mark: Same prompts: number of hosts, Root CA name, validity (days), PFX password, hostnames
:white_check_mark: Outputs .key, .crt, .cer, .pem, .pfx, full chains β€” all under C:\poc_certs

:warning: Requires Shining Light Productions OpenSSL installed on the Windows Host

If you’re working entirely from a Windows host.

:package: The Script

# generate-certs.ps1
Write-Host "πŸ” Certificate Generator for Multiple Hosts (PowerShell Edition)"

# Prompt for inputs
$hostCount = Read-Host "Enter number of hostnames"
$caName = Read-Host "Enter Root CA name"
$validDays = Read-Host "Enter certificate validity in days"
$pfxPassword = Read-Host "Enter password for PFX files" -AsSecureString

# Setup directories
$baseDir = "C:\poc_certs"
$caDir = Join-Path $baseDir "ca"
$hostsDir = Join-Path $baseDir "hosts"
New-Item -Path $caDir, $hostsDir -ItemType Directory -Force | Out-Null

$caKey = Join-Path $caDir "$caName`_rootCA.key"
$caCert = Join-Path $caDir "$caName`_rootCA.crt"

# Generate Root CA if it doesn't exist
if (-not (Test-Path $caKey)) {
    Write-Host "πŸ”§ Generating Root CA..."
    openssl req -x509 -newkey rsa:4096 -nodes `
        -keyout $caKey -out $caCert -days $validDays `
        -subj "/C=US/ST=State/L=City/O=MyOrg/CN=$caName"
}

# Generate certs per host
for ($i = 1; $i -le $hostCount; $i++) {
    $hostname = Read-Host "Enter hostname #$i"
    $certDir = Join-Path $hostsDir $hostname
    New-Item -Path $certDir -ItemType Directory -Force | Out-Null

    $keyFile = Join-Path $certDir "$hostname.key"
    $csrFile = Join-Path $certDir "$hostname.csr"
    $crtFile = Join-Path $certDir "$hostname.crt"
    $chainFile = Join-Path $certDir "$hostname-chain.crt"
    $pfxFile = Join-Path $certDir "$hostname.pfx"
    $cerFile = Join-Path $certDir "$hostname.cer"
    $pemFile = Join-Path $certDir "$hostname.pem"
    $sanFile = Join-Path $certDir "$hostname-san.cnf"

    @"
[req]
distinguished_name=req_distinguished_name
req_extensions=v3_req
[req_distinguished_name]
[v3_req]
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = $hostname
"@ | Set-Content $sanFile

    # Generate private key and CSR
    openssl genrsa -out $keyFile 4096
    openssl req -new -key $keyFile -out $csrFile -subj "/CN=$hostname" -config $sanFile

    # Sign certificate with Root CA and SAN extension
    openssl x509 -req -in $csrFile -CA $caCert -CAkey $caKey -CAcreateserial -out $crtFile -days $validDays -extfile $sanFile -extensions v3_req

    if (Test-Path $crtFile) {
        # Create full chain file
        Get-Content $crtFile, $caCert | Set-Content $chainFile
        # Copy cert as .cer file
        Copy-Item $crtFile $cerFile
        # Create PEM combining key + chain
        Get-Content $keyFile, $chainFile | Set-Content $pemFile

        # Convert SecureString password to plain text
        $pfxPasswordPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
            [Runtime.InteropServices.Marshal]::SecureStringToBSTR($pfxPassword)
        )

        # Generate password protected PFX
        openssl pkcs12 -export -out $pfxFile -inkey $keyFile -in $chainFile -certfile $caCert -password pass:$pfxPasswordPlain
        Write-Host "βœ… Certificate for $hostname generated."
    } else {
        Write-Host "❌ Failed to generate certificate for $hostname"
    }

    # Cleanup SAN config file
    Remove-Item $sanFile
}

Write-Host "πŸŽ‰ Certificates generated at: $baseDir"




:open_mailbox_with_raised_flag: Need help? Please reach out to me!

7 Likes

Nice writeup, Niraj!

Going for the over achiever award with the video - nice touch :smiley:

3 Likes

This is amazing, Niraj! I’m a big fan of the way you presented this information, especially the emojis. It makes following along so much easier :+1:

2 Likes

Since this is using openssl on a linux host I wanted to provide a pwsh script that can be used on windows and the NPS-AM application servers

#This will create a self signed cert for the server you are on. It uses the FQDN and allows for SAN names to be entered as well for your other applications or service servers
#This will automatically install the cert to IIS and bind it to the NPS website. It also installs it to the root store and creates a pfx.
#Use the pfx to install to other servers local machine personal store and to the root store (so it will be valid) Then change the IIS bindings to use the new cert.


# Import IIS Module
Import-Module IISAdministration

# Get FQDN of current server
$FQDN = [System.Net.Dns]::GetHostByName($env:computerName).HostName

# Prompt for additional SAN names
$SANList = New-Object System.Collections.ArrayList
$SANList.Add($FQDN) | Out-Null  # Add FQDN as first SAN

Write-Host "Enter additional SAN names (DNS names) one at a time. Press Enter without input when done."
while ($true) {
    $input = Read-Host "Enter SAN name (or press Enter to finish)"
    if ([string]::IsNullOrWhiteSpace($input)) {
        break
    }
    $SANList.Add($input) | Out-Null
}

# Create certificate
Write-Host "Creating certificate with the following DNS names:"
$SANList | ForEach-Object { Write-Host "- $_" }

$cert = New-SelfSignedCertificate `
    -DnsName $SANList `
    -CertStoreLocation "cert:\LocalMachine\My" `
    -FriendlyName "SbPAM Web Service Certificate" `
    -NotAfter (Get-Date).AddYears(5) `
    -KeyAlgorithm RSA `
    -KeyLength 2048 `
    -KeyExportPolicy Exportable `
    -KeyUsage DigitalSignature, KeyEncipherment `
    -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.1")

# Export certificate to PFX file
$certPassword = Read-Host -Prompt "Enter password for PFX file" -AsSecureString
$pfxPath = Join-Path $PSScriptRoot "SbPAM_WebService_Cert.pfx"
$cert | Export-PfxCertificate -FilePath $pfxPath -Password $certPassword

# Copy certificate to Trusted Root store
$rootStore = New-Object System.Security.Cryptography.X509Certificates.X509Store("Root", "LocalMachine")
$rootStore.Open("ReadWrite")
$rootStore.Add($cert)
$rootStore.Close()

try {
    # Connect to IIS
    $iisManager = Get-IISServerManager
    $siteName = "SbPAM Web Service"
    
    # Get the site
    $site = $iisManager.Sites[$siteName]
    if (-not $site) {
        Write-Error "Site '$siteName' not found in IIS"
        exit 1
    }

    # Remove existing HTTPS binding if it exists
    $existingBinding = $site.Bindings | Where-Object { 
        $_.Protocol -eq "https" -and $_.BindingInformation -like "*:6500:*" 
    }
    if ($existingBinding) {
        $site.Bindings.Remove($existingBinding)
    }

    # Add new HTTPS binding
    $binding = $site.Bindings.Add("*:6500:", $cert.GetCertHash(), "My")
    $binding.Protocol = "https"
    
    # Save changes
    $iisManager.CommitChanges()

    Write-Host "`nCertificate creation and installation complete!"
    Write-Host "Certificate Thumbprint: $($cert.Thumbprint)"
    Write-Host "PFX file exported to: $pfxPath"
    Write-Host "`nSAN names included:"
    $SANList | ForEach-Object { Write-Host "- $_" }
    Write-Host "`nTo install this certificate on other servers:"
    Write-Host "1. Copy the PFX file"
    Write-Host "2. Use the following PowerShell commands to import (as administrator):"
    Write-Host "   `$pfxPassword = Read-Host -Prompt 'Enter PFX Password' -AsSecureString"
    Write-Host "   Import-PfxCertificate -FilePath 'path\to\SbPAM_WebService_Cert.pfx' -CertStoreLocation Cert:\LocalMachine\My -Password `$pfxPassword"
    Write-Host "3. Import to Trusted Root store:"
    Write-Host "   Import-PfxCertificate -FilePath 'path\to\SbPAM_WebService_Cert.pfx' -CertStoreLocation Cert:\LocalMachine\Root -Password `$pfxPassword"
    Write-Host "4. Configure IIS binding using the certificate thumbprint shown above"

} catch {
    Write-Error "Error configuring IIS: $_"
    Write-Error "Full Error Details: $($_.Exception.Message)"
    exit 1
}
5 Likes

Kudos! What a great concept!

1 Like

Thanks @adam.sneed for posting this, not all may have access to WSL or Linux, hence I also generated the PowerShell Edition :slight_smile:

Have updated the original post.

Kindly test and let me know if this helps

1 Like