Some time back I
created a PowerShell script that goes through all the computers you have in a
list, searches those computer's certificate store for expired/expiring
certificates and emails you reminders.
The script was designed to run nightly on one central computer.
Side
note: I have a quite a few certificates from StartSSL installed on a bunch of
computers. If you've never heard of
StartSSL, I highly recommend checking them out!
You can get free SSL certificates and they're trusted by pretty much all
computers these days.
I had a problem with
the script I wrote though. It required
that the security context in which the PowerShell script ran had access to all
the remote computer's certificate store.
I didn't like this for a couple reasons.
First, there was no central AD group that allows for this. Which makes sense if you think about it. Sure you could run it with domain
administrator access but you shouldn't be doing that. Following the Principal of Least Privilege,
you want to limit access to the most essential.
So I thought add the user account that runs the PowerShell script to the
"Certificate Service DCOM Access" on each individual machine that I
wanted to get certificate reminders for.
This seems bad to me. If that
user account gets compromised the attacker has access to all the certificates
on all the other computers. Also the
attacker would have access to modify all those certificate stores.
After talking it
over with some people, I decided the best route was to modify the PowerShell
script slightly and have it execute on every computer instead of one central
computer. The script would just check
its own certificate store and email about its own certificates. This way the user account running the script
doesn't need access to all the other computer's certificate store. The problems I saw with this approach was
that we'd need to change the PowerShell execution policy on all the computers
from the default of not allowing scripts to either unrestricted or signed. Unrestricted execution was out of the
question, AllSigned it was. So off I set
to figure out how to sign PowerShell scripts and deploy everything via GPO to
make like easier.
I've concluded that
there are three approaches to signing scripts.
Using an internal certificate server, using a 3rd party certificate
server and manually creating your own certificates.
There are a ton of
articles on how to do it with your own internal CA or a 3rd party CA but I
couldn't find one about the manual method.
I didn't have an internal certificate server, so that right away was
kind of out of the question. I wasn't
looking to implement one at this time.
Although I'd like to in the future.
So I started looking
at 3rd party CAs. Specifically I started
looking at using StartSSL code signing cert that you get with their class 2
validation level. I got things working
with this but I was worried about what would happen after two years when the
code signing certificate expires. I knew
that I could use a timestamp server when signing my code to extend the validity
of the script past the certificate's lifetime but when I tried testing it, it
didn't seem to work? Exploring this some
more, it turns out that I would need to get StartSSL's extended validation to
be able to time stamp with a code signing certificate from StartSSL. Long story short, the certs created by
StartSSL have the Lifetime Signing (1.3.6.1.4.1.311.10.3.13) OID added to them
unless you get the extended validation and this OID prevents time stamping from
working. Check out this forum
if you'd like a further explanation.
Side
note: They should have called it "No LifeTime Signer" or something a little
more intuitive.
I didn't want to
spend the money to upgrade to the extended validation level and I didn't want
to spend much more time on this. So I
decided to explore the manual route quickly and it turned out to be the best
option. The certificate expires in 2039
and everything can get deployed via GPO to make life easy. So here are the steps I took. I'll break it down into 4 broad parts:
- Modifying My Existing PowerShell Script and create Batch File
- Creating the Certificates
- Signing the PowerShell Scripts
- Deploying Everything
Modify the Existing PowerShell Script and Create a
Batch File
The original
PowerShell script that emails certificate reminders can be found here.
I changed a couple lines to have it run on all the computers instead of one.
Change
the line:
$Computers = "server1",
"server2", "server3" #list of servers to check
To:
$Computers
= $env:COMPUTERNAME
This basically keeps
the script as is but runs it with only itself in the list of computers. I also changed the email subjects and body
slightly to include friendly names on the certificates. Here is the modified script, copy it and save
it as ExecuteCertReminder.ps1. Be sure
to set your own SMTP server and email addresses in the script.
#this
script is intended to be run on one computer, it should then be pushed out with
GPO and scheduled to run on each computer
$Computers = $env:COMPUTERNAME #list of computer names "server01",
"server02"
$SmtpServer = "smtpserver.domain.com"
$MailFrom = "mailfrom@domain.com"
$MailTo = "mailto@domain.com"
#
Change above settings. No need to change anything below.
$DaysToExpire = 30
#
increase $DaysToExpire if you want more of a warning, decrease it if you would
like less
$StoreName = "My"
#
change $StoreName above to one of the valuse below if you wish to check a store
other then the personal store, for example if you're worried about intermediate
CA certs expiring
#
"AddressBook", "AuthRoot",
"CertificateAuthority", "Disallowed", "Root",
TrustedPeople", "TrustedPublisher"
$deadline = (Get-Date).AddDays($DaysToExpire)
ForEach ($c in $Computers) {
Try {
$store=new-object System.Security.Cryptography.X509Certificates.X509Store("\\$c\$StoreName","LocalMachine")
$store.open("ReadOnly")
# all certs
#$store.certificates | Select *
# will expire certs
$store.Certificates | ? {$_.NotAfter -le ($deadline) -and $_.NotAfter -ge (Get-Date)} | Select *, @{Label="ExpiresIn"; Expression={($_.NotAfter - (Get-Date)).Days}} | % {
$ExpiresIn = $_.ExpiresIn
$NotAfter = $_.NotAfter
$friendlyNAme = $_.FriendlyName
$emailBody = "The following certificate on $c will expire in
$ExpiresIn days on $NotAfter" + [System.Environment]::NewLine
$emailBody += $_ | select * | Out-String
Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "Certificate about to Expire on $c -
$friendlyNAme" -Body $emailBody
Write-Host $emailBody
}
# expired certs
$store.Certificates | ? {$_.NotAfter -lt (Get-Date)} | Select *, @{Label="ExpiredOn";Expression={$_.NotAfter}} | % {
$ExpiredOn = $_.ExpiredOn
$friendlyNAme = $_.FriendlyName
$emailBody = "The following certificate on $c expired on
$ExpiredOn" + [System.Environment]::NewLine
$emailBody += $_ | select * | Out-String
Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "CERTIFICATE EXPIRED on $c - $friendlyNAme" -Body $emailBody
Write-Host $emailBody
}
}
Catch {
$emailBody = "Errors detected during certification reminder
powershell script execution. " + [System.Environment]::NewLine + [System.Environment]::NewLine + "$($c): $($error[0])"
Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "Certification Reminder Powershell Script Execution
Error" -Body $emailBody
Write-Host -foregroundcolor Yellow $emailBody #"$($c): $($error[0])"
}
Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "ExecuteCertReminder
ran on $c " -Body "ExecuteCertReminder ran on $c "
}
Next create a batch
file called ExecuteCertReminder.bat containing the following:
powershell.exe
-ExecutionPolicy Bypass -Command ". '\\network
path will be determined later\ExecuteCertReminder.ps1'"
We'll use this batch
file later when we deploy everything with GPO and create a scheduled task. Note that we're going to deploy
ExecuteCertReminder.ps1 and ExecuteCertReminder.bat with the GPO we create
later. They'll be stored in the domain's
SysVol alongside the GPO we create later.
Thus we'll be coming back and editing this batch file once we know the
path.
Creating the Certificates
I followed Scott
Hanselman's blog about
signing PowerShell scripts to create the certificates. I modified his stuff slightly.
First go download
the Windows SDK
and install it on your workstation to gain access to the makecert executable.
On your workstation,
open up the SDK Command Prompt with administrator
privileges and run the slightly modified command from Scott's blog:
makecert -n "CN=Your Company Name PowerShell
Certificate Root" -a sha256 -eku 1.3.6.1.5.5.7.3.3 -r -sv root.pvk
root.cer -ss Root -sr localMachine
A dialog should pop
up asking for a password. Create a
password and remember it.
Enter the password
you created in the next dialog box.
This creates a
public and private key root cert pair that can only be used to make code
signing certs and puts the public key in your localmachine root store. It creates two files. One with the public key and one with the
private key. Keep these two files some
place secure. You will need them to sign
other certificates in the future. Later
we will take this certificate and deploy it to the root certificate store on
all your computers in your domain. Do
not lose the private key file if you intend to create other certs later on with
this root cert.
Next run the
slightly modified command from Scott's blog:
makecert -pe -n "CN=Your company Name PowerShell
Signing Cert" -ss MY -a sha256 -eku 1.3.6.1.5.5.7.3.3 -iv root.pvk -ic
root.cer
A dialog will pop up
asking for the password you created in the previous step.
This creates the
certificate pair that you will using for signing your PowerShell scripts and
puts it in your personal certificate store.
It's marked as exportable. You'll
want to export it to a PFX file and keep it secure.
Open the MMC by
tying "mmc" and hitting enter at a command prompt. Once loaded, select File, Add/Remove Snap-In.
A dialog opens.
Select
"Certificates" and click Add.
Make sure the
"My user account" radio is selected and click Finish. Click Ok on the "Add or Remove
Snap-Ins" dialog.
Expand
"Certificates - Current User", Expand "Personal" and Select
"Certificates".
Find the certificate
you created in the right hand pane, select it and right click. Select "All Tasks",
"Export".
An export wizard
comes up, click Next.
Select "Yes,
export the private key" radio and click next.
Check the
"Export all extended properties" check-box and click next.
Check the
"Password" check box, enter a password and click Next.
Enter a name for the
exported certificate file with a .pfx extension and click Next. A summary dialog will appear, click Finish
and the export should be complete. You
should not have three files. The root
private key, the root public key and the .pfx file containing both the signing
certificates key pair. Key these three
files some place safe where no one will have access to them.
Signing the PowerShell Scripts
You could sign the
scripts manually but I found a link
that showed how to add a menu item to the PowerShell Integrated Scripting
Environment. For me, this simplifies the
process of signing scripts. Once it's
setup it's easy to use. I won't repost
the information, please follow the link and read the blog about how to do
it.
I will summarize the
process though.
- The file has a txt extension. Rename the file to Sign-ISEScript.ps1.
- If you have multiple code signing certs you may need to modify this file to select the correct certificate.
- To check if you have multiple code signing certificates, run the following from the interactive shell at the bottom of the ISE application:
Get-ChildItem -Path cert:\CurrentUser\My -CodeSigningCert
- If only one certificate is listed, you can go on to step 4.
- If more than one certificate is listed, make note of the thumbprint of the certificate you want to use.
- You will need to change a
line in Sign-ISEScript.ps1 to select the proper certificate. Find the following line in
Sign-ISEScript.ps1:
$cert=get-childitem
-Path cert:\currentuser\my -CodeSigningCert
And
replace it with
$cert=Get-ChildItem
-Path cert:\CurrentUser\My -CodeSigningCert
| where {$_.Thumbprint -eq 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' }
Replace
the F's with the thumbprint that you noted earlier.
- Edit your ISE profile
- In the interactive shell at the bottom, type "psedit $profile". A tab will open called Microsoft.PowerShellISE_profile.ps1.
- Add the following to this
tab
. C:\<path to downloaded file>\Sign-ISEScript.ps1
$psISE.CurrentPowerShellTab.AddOnsMenu.submenus.Add(“Sign Script”,{Sign-ISEScript},$null) | Out-Null - Save the file and reopen ISE. There should now be a menu item called "Sign Script" under the "Add-ons" menu.
- Open the file you want to sign (ExecuteCertReminder.ps1 in this case), select Add-ons, sign-script. It should sign your script by adding a signature to the end of the file, save it and reopen.
Deploying Everything
The final major
hurdle is to push everything out to the computers you want the script to run
on. I'm not going to get into GPO
organization strategies or anything. I'm
just going to setup one GPO with all the settings required. You can then link this GPO however you want
to any OU you want. Also, you can break
this out into multiple GPOs if you so desire.
There are three basic things this GPO needs to do.
- Push out the appropriate certificates
- Set the PowerShell execution policy to AllSigned
- Setup a scheduled task to execute the PowerShell script
Start by opening the
Group Policy Management snap-in either on a domain controller or your
workstation if you have the remote admin tools installed. I assume you know how to make a new GPO. You'll want to edit The GPO. In the Group Policy Management Editor:
Right click Policy Object Name/Computer
Configuration/Policies/Windows Settings/Security Settings/Public Key
Policies/Trusted Root Certification Authorities and select import. Use the root.cer file you created earlier.
Right click Policy Object Name/Computer
Configuration/Policies/Windows Settings/Security Settings/Public Key
Policies/Trusted Publishers and select import.
Use the "signing cert.pfx" file you exported earlier.
That's all you need
to do to push out the certificates.
Continue editing the
GPO. Navigate to Policy Object Name/Computer
Configuration/Policies/Administrative Templates/Windows Components/Windows
PowerShell. Double click "Turn on
Script Execution" in the right hang pane.
Check the Enabled radio and change the drop down to "Allow only
signed scripts". Click Ok.
Now you'll need to
setup the scheduled task to execute nightly.
First we'll copy ExecuteCertReminder.ps1 and ExecuteCertReminder.bat to
the corresponding GPO in the SysVol on the domain. In the Group Policy Management Snap-in find
your new GPO and click on the details tab in the right hand pane.
Note the GUID used
for the Unique ID, yours will be different than the one above. Open windows explorer and navigate to the
SysVol share on one of your domain controllers.
There should be a subfolder with your domain name and under that a
Policies folder. Open this policies
folder then find and open the folder with the GUID noted previously. Then navigate farther to
Machine\Scripts. Copy
ExecuteCertReminder.ps1 and ExecuteCertReminder.bat this this network
location.
Make note of the
full path to the PowerShell script.
You'll need to edit the batch file and enter the full network path to
the PowerShell script.
Make note of the
full path to the batch file. We'll need
the full path when creating the scheduled task in GPO.
Back in the In the
Group Policy Management Editor navigate to Policy
Object Name/Computer Configuration/Preferences/Control Panel
Settings/Scheduled Tasks. Right click
-> New -> Scheduled Task. A dialog
opens up.
Set
Action to Replace.
Enter
a Name of ExecuteCertReminder.
Enter
the entire path to the batch file in the Run textbox.
Click on the
schedule tab and setup whatever schedule you would like. Hit Ok and you should be done.
No comments:
Post a Comment