Full SSL, HOSTS, and IIS Dev Setup via PowerShell

I'm all about people having their own setup and way of doing things. If you like Resharper, cool. If you like Notepad++, fine. Problems arise when you are forced into a standardized setup. Focus on the interface, not the implementation. With standardization you lose the natural QA you get from diverse environments.

Getting to specifics: HTTP is great, but times have changed. While we all know and love it (whatever), it needs to be taken out back. You can throw a site into the wild without protection from the beasts. This is NOT an after-the-fact production implementation detail. You need this in development. You can't have HTTPS surprises at the last second. That's simply irresponsible.

You should use IISExpress with SSL where possible. My preference is definitely for a full-IIS dev box setup with full HTTPS on everywhere.

What goes into this setup?

  • Dev IP Addresses
  • Updating HOSTS/DNS
  • SSL Certs
  • WebSites in IIS
  • Getting Visual Studio to like it

There is all where PowerShell comes in.

Prerequisite: Make sure you can run ps1 files in PowerShell. Here's one suggestion: Set-ExecutionPolicy RemoteSigned.

Adding IP addresses and updating HOSTS

While I sometimes teach kids, this explanation is for adults. I'll assume you can read and can deduce what does what. Code is self-documenting.

Do this in PowerShell as Administrator. I'd recommend the ISE.

10..30 | % {
    $ip = "10.1.111.$_"
    New-NetIPAddress -InterfaceAlias "Ethernet" -IPAddress $ip -PrefixLength 16
    #++ for Hyper-V
    #New-NetIPAddress -InterfaceAlias "vEthernet (External Virtual Switch)" -IPAddress $ip -PrefixLength 16
}

"
10.1.111.13 api.domain.local
10.1.111.12 viewer.domain.local
10.1.111.11 admin.domain.local
10.1.111.10 domain.local
" | ac "$($env:systemroot)\system32\drivers\etc\hosts"

Obviously, this will add the following IP Addresses and assign them .local domain names in the hosts file (don't hijack a top level domain; use .local!)

Next...

Creating SSL certs

Below is a PowerShell script that will:

  • create a master certificate and
  • create SSL certificates 1.

First, it uses makecert.exe to create the cert 2. Then, it uses pvk2pfx.exe to create a pfx file. Finally, it uses Import-PfxCertificate to import this pfx file into the certificate store.

Look at the "##+ run" section for examples of how to run this:

  • First, create a master certificate.
  • Then, create the others.

Uncomment and comment as needed. It just makes life easier. No need for fancy PowerShell modules.

Troubleshooting: Go to run (Win-R) and type MMC to get to a place where you can delete the certs. File->Add/Remove Snap-in. Add "Certificates". Add. Select Local Computer. Look in Trusted Root Certificate Authorities for master and Personal for other others. Use your eyes. You'll see it.

$virtualenv = {

##+ environment
$apiFolder = 'C:\Program Files (x86)\Windows Kits\10\bin\x64'
$target = 'E:\Drive\Code\Security\Cert'

if(!(Test-Path $apiFolder)) {
    "$apiFolder not found; check SDK installation"
    exit
}

##+ /environment

$root = {
    param([string]$name)

    $certFile = Join-Path $target $name
    $rootBaseName = "$certFile`Root"

    if(Test-Path "$rootBaseName.cer") {
        "$rootBaseName.cer already exists. ABORTING."
        exit
    }

    &"$apiFolder\makecert.exe" -r -n "CN=$rootName`Root" -pe -sv "$rootBaseName.pvk" -a sha1 -len 2048 -b 01/01/2010 -e 01/01/2030 -cy authority "$rootBaseName.cer"
    &"$apiFolder\pvk2pfx.exe" -pvk "$rootBaseName.pvk" -spc "$rootBaseName.cer" -pfx "$rootBaseName.pfx"

    Import-Certificate -FilePath "$rootBaseName.cer" -CertStoreLocation Cert:\LocalMachine\Root > $null
}

$ssl = {
    param([string]$name, [string]$rootName, [boolean]$isClientCert = $false)

    $rootCertFile = Join-Path $target $rootName
    $rootBaseName = "$rootCertFile`Root"

    if(!(Test-Path "$rootBaseName.cer")) {
        "$rootBaseName.cer does not exist. Stopping."
        return
    }

    $siteBaseName = Join-Path $target $name
    $siteBaseName

    if($isClientCert) {
        &"$apiFolder\makecert.exe" -iv "$rootBaseName.pvk" -ic "$rootBaseName.cer" -n "CN=$name" -pe -sv "$siteBaseName.pvk" -a sha1 -len 2048 -b 01/01/2010 -e 01/01/2030 -sky exchange "$siteBaseName.cer" -eku 1.3.6.1.5.5.7.3.2
    }
    else {
        &"$apiFolder\makecert.exe" -iv "$rootBaseName.pvk" -ic "$rootBaseName.cer" -n "CN=$name" -pe -sv "$siteBaseName.pvk" -a sha1 -len 2048 -b 01/01/2010 -e 01/01/2030 -sky exchange "$siteBaseName.cer" -eku 1.3.6.1.5.5.7.3.1
    }

    &"$apiFolder\pvk2pfx.exe" -pvk "$siteBaseName.pvk" -spc "$siteBaseName.cer" -pfx "$siteBaseName.pfx"

    ##++ cer is only public key; need private key for this one; that's pfx
    Import-PfxCertificate -FilePath "$siteBaseName.pfx" Cert:\LocalMachine\My > $null
}

##+ run
$rootName = 'Master'

#&$root $rootName
&$ssl 'identity.jampad.local' -rootName $rootName
#&$ssl 'devworkerrole01.local' -rootName $rootName
#&$ssl 'client.ssl' -rootName $rootName -isClientCert $true
##+ /run

}

&$virtualenv

Now for IIS...

Setting up IIS

If you are on Windows Server, just check go here and run that one line: IIS PowerShell Installation.

Now for setup...

This is somewhat of a monster: it creates the site, adds the IP addresses, and adds the certs. IIS doesn't let you create a binding-less WebSite, so HTTP came first. Remove it. It's obsolete and shouldn't be used.

$virtualenv = {

$networkInterfaceName = 'Ethernet'
##+ hyper-v
#$networkInterfaceName = 'vEthernet (External Virtual Switch)'

$ipCheck = {
    param([string]$ip)
    $addressData = Get-NetIPAddress | where { $_.InterfaceAlias -eq $networkInterfaceName } | select -expand IPAddress

    if(($addressData | where { $_ -eq $ip }).Count -eq 0) {
        Write-Host "IP address $ip not found. Run the following command. ABORTING."
        Write-Host "    New-NetIPAddress -InterfaceAlias '$networkInterfaceName' -IPAddress $ip -PrefixLength 16"
        exit
    }
}

$addSslBinding = {
    param([string]$name, [string]$ip, [string] $sslCert)
    if((Get-WebBinding  -name $name -IP $ip -Port 443 -Protocol https).Count -eq 0) {
        Write-Host "Adding SSL for address $ip..."

        New-WebBinding -name $name -IP $ip -Port 443 -Protocol https

        try {
            Write-Host "Setting SSL cert $sslCert..."
            $subjectName = "CN=$sslCert"
            $cert = ls Cert:\LocalMachine\My | where { $_.Subject -eq "CN=$sslCert" }
            $cert | ni "IIS:\SslBindings\$ip!443" > $null
        }
        catch {
            Write-Host ("`t{0}" -f $_.Exception.Message)
        }
    }
}

$setup = {
    param (
        [Parameter(Mandatory=$True)]
        [string] $name,
        [string] $path,
        [string] $ip,
        [int32] $httpPort = 80,
        [string] $sslCert,
        $ipHostMap,
        [boolean] $assignSslForEachHost,
        [boolean] $sslOnly
    )

    if($ip) {
        &$ipCheck $ip
    }
    elseif($ipHostMap) {
        foreach($ip in $ipHostMap.Keys) {
            # Write-Host ("$ip => {0}" -f $ipHostMap[$ip])
            &$ipCheck $ip
        }
        $ip = '127.0.0.1'
        $tempIp = $true
    }

    try {
        if((ls IIS:\AppPools | where name -eq $name).Count -eq 0) {
            Write-Host 'Creating application pool...'
            pushd IIS:\AppPools
            New-Item $name > $null
        }
        if((ls IIS:\Sites | where name -eq $name).Count -eq 0) {
            cd IIS:\Sites
            Write-Host "Creating web site ($name)..."
            Write-Host "`nNOTE: Adding temporary HTTP binding (required). If SSL-only, will try to remove HTTP in a moment.`n" -f Yellow
            New-Item $name -bindings @{protocol="http"; bindingInformation=$ip + ":" + $httpPort + ":"} -physicalPath $path > $null
            Set-ItemProperty $name -name applicationPool -value $name > $null
        }
        if($sslOnly -and !$sslCert) {
            $sslCert = $name
        }
        if($sslCert -and !$assignSslForEachHost) {
            &$addSslBinding $name $ip $sslCert
        }
        elseif($assignSslForEachHost) {
            foreach($ip in $ipHostMap.Keys) {
                $sslCert = $ipHostMap[$ip]
                &$addSslBinding $name $ip $sslCert
            }
        }
        if($sslOnly -and ($sslCert -or $ipHostMap)) {
            Write-Host "Removing HTTP..."
            Remove-WebBinding -Name $name -Protocol 'http'
            Write-Host "`nNOTE: You should double check to make sure HTTP was removed." -f Yellow
        }
    }
    finally {
        popd
    }
}

Import-Module WebAdministration

$ipHostMap = @{
  '10.1.111.13' = 'api.domain.local'
  '10.1.111.12' = 'viewer.domain.local'
  '10.1.111.11' = 'admin.domain.local'
  '10.1.111.10' = 'domain.local'
}

$name = 'domain.local'
if((ls IIS:\Sites | where name -eq $name).Count -gt 0) {
    cd IIS:\Sites
    Write-Host 'Deleting web site...'
    rm -r $name
}

# single-side multi-tenancy
&$setup -name 'domain.local' -path 'E:\_GIT\Domain.Project\Domain.Project.WebSite' -ipHostMap $ipHostMap -assignSslForEachHost $true -sslOnly $true


# single-side multi-tenancy
&$setup -name 'domain.local' -path 'E:\_GIT\Domain.Project\Domain.Project.WebSite' -ipHostMap $ipHostMap -assignSslForEachHost $true -sslOnly $true

&$setup -name 'anotherdomain.local' -ip '10.1.111.111' -path 'E:\_GIT\anotherdomain\anotherdomain.WebSite' -sslOnly $true
&$setup -name 'yetanotherdomain.local' -ip '10.1.111.112' -path 'E:\_GIT\yetanotherdomain\yetanotherdomain.WebSite' -sslOnly $true
&$setup -name 'another.local' -ip '10.1.111.113' -path 'E:\_GIT\another\another.WebSite' -sslOnly $true
&$setup -name 'onemore.local' -ip '10.1.111.113' -path 'E:\_GIT\onemore\onemore.WebSite' -sslOnly $true

}

&$virtualenv

Your question: "What the heck is that &$virtualenv?" I do everything in the ISE. It shares variables between tabs. This &$virtualenv runs the entire thing in it's own scope. The name isn't magic. I used to call it "scope". Whatever.

At this point the server is setup. Do this enough, it will take only the time it takes to type in the addresses and host names.

Removing annoying Visual Studio message

One problem: Visual Studio hates attaching to IIS. First, you have to be admin. Get over it. Be admin. I have my Visual Studio to always run as admin. Second, it complains when attaching to IIS.

Visual Studio Image

The fix is simple.

#vs 13
sp -path HKCU:\Software\Microsoft\VisualStudio\12.0\Debugger -Name DisableAttachSecurityWarning -Value 1

#vs 15
sp -path HKCU:\Software\Microsoft\VisualStudio\14.0\Debugger -Name DisableAttachSecurityWarning -Value 1

That's it. Now you have a nice server system.

Also, no more F5 nonsense. No more "run app". What "app"? It's not a console app. It's a WebSite. It's running anyway. F5 doesn't "start it". So... no more of that nonsense.

Quick HowTos

How do you debug Startup? Because Global.asax is obsolete and you're now using OWIN/Startup, you know that startup is Startup. Things now make sense. To debug this, throw in the following code:

System.Diagnostics.Debugger.Launch();

How do I restart the WebSite? Touch web.config (open and save it).

How do I restart the WebSite while attached? Yeah, for ABSOLUTELY NO REASON Visual Studio wants to detach when you touch web.config. The solution shouldn't be a surprise: PowerShell. When I'm developing I have a lot of PowerShell stuff open. Actually, when I'm doing anything I have it open.

Here's what I actually use to touch all my sites to reset them all at once. No F5-on-each. No AppPool recycle.

$touch = {
    param([string]$base)
    $config = Join-Path $base 'web.config'
    (ls $config).LastWriteTime = [DateTime]::Now
}

&$touch 'E:\_GIT\jampad.content\Content.Sample.WebSite'
&$touch 'E:\_GIT\jampad.content\Content.Sample.WebSite.Api'
&$touch 'E:\_GIT\jampad.content\Jampad.Content.WebSite.Api'
&$touch 'E:\_GIT\netfxharmonics\NetFXHarmonics.WebSite'


1
It will also create client certificates. See the 'client.ssl' example.
2
You can use pure PowerShell to create a self-signed certificate, but, apparently, makecert.exe goes above and beyond the call of duty.