We went though some "minimal" AZURE training last week. The trainer guy said we could organize and "bill" other departments using "TAGS". BUT, some Googling/Binging didn't turn anything up... so here is what I did to create a custom RBAC AZURE role:
Save this file (after you edit the last part, of course) as TAG_reader.json:
{
"Name": "Tag Reader",
"IsCustom": true,
"Description": "Can read tags.",
"Actions": [
"Microsoft.Resources/subscriptions/tagNames/read",
"Microsoft.Resources/subscriptions/tagNames/tagValues/read"
],
"NotActions": [
],
"AssignableScopes": [
"/subscriptions/PUTYOURSUBSCRIPTIONIDHERE"
]
}
then log into AZURE POSH, and run:
New-AzureRmRoleDefinition -inputfile TAG_reader.json
Then assign the person/people to that group.
If I spend some time figuring out something that might help others, and hasn't been found on the Internet by me, I will post about it. I hope to save some poor IT guy or gal some time.
Wednesday, November 15, 2017
Friday, July 7, 2017
Advanced Group Policy Management is such a control freak...
One of the things I like about my job is that I do lots of different "enterprisy" things with Microsoft Windows.
This week I had to solve an issue how to not have to create change requests every time someone edits a group policy. Thankfully, Microsoft has a solution that, while isn't perfect, is "good enough" called Advanced Group Policy Managment (AGPM).
The problem in our environment is we have over 600 GPOs that I needed to "import"/"control" into AGPM... and unless the owner and permissions are just right, they cannot be "controlled." I found a few people through some searches that were tackling either ownership, or permissions... but not both. So I present to you: Set-AGPMRights.ps1
Again, I am not a "full time programmer", so your mileage will vary, and I expect you to review my code before you use it on any production environment.
# Set-AGPMRights.ps1
# Created by Bryan Loveless # # Created June 2017
# This script will set the ownership and correct permissions/ownership for AGPM and
# will also "take control" of it/them.
# Just change the necessary variables, and away you go.
# References for borrowed code are in the script blocks where used, if they were.
# ONLY CHANGE THE ONE LINE BELOW!!! (After changing the users during initial config)
# It will support Wildcards (*)
$GPOTARGET = "*"
########################now the script parts not to configure########################################
# get list of all GPOs with that name
$allGPOnames = ((Get-Gpo -all | ? {$_.displayName -like $GPOTARGET }).DisplayName)
# cycle through each one
foreach ($gpo in $AllGPOnames){
# if you found this script online, below is where you would change the "AGPM archive account info"
set-gppermissions -name $gpo -TargetName "YOURDOMAN\YOURSERVICEACCOUNT" -TargetType user -PermissionLevel GpoEditDeleteModifySecurity
set-gppermissions -name $gpo -TargetName "YOURDOMAIN\Domain Admins" -TargetType group -PermissionLevel GpoEditDeleteModifySecurity
}
# now set owner
#Script to change stale or existing owner of GPO using AD DACL modules
# ref: https://gallery.technet.microsoft.com/scriptcenter/Script-to-Edit-Owner-on-bbba3562
$OwnerNew = "YOURSERVICEACCOUNT" #Name of the Object user or group to be updated
$GPOName = $GPOTARGET #GPO to be updated, This field accepts wildcards, "*" updates all GPO
#Get all GPOs, filter if required
$AllGPO = Get-GPO -All | ?{$_.DisplayName -like $GPOName}
#$AllGPO = Get-GPO -All
""
"GPO Name"+" "+ "OwnerBefore"+" "+ "OwnerAfter"
"--------"+" "+ "-----------"+" "+ "----------"
foreach ($gp in $AllGPO){
#"GPO Name: " + $gp.DisplayName
#Get the GUID and add wild*
#Get-GPO "TestGPO"
$gpId = "*"+($gp).Id+"*"
#Store the GPO AD Object in a variable
$Gpo1 = get-adobject -Filter {Name -like $gpId}
#Store the new Owner in a variable as well (Note changes for group and user accounts)
#$Ownr = New-Object System.Security.Principal.SecurityIdentifier (Get-ADGroup "Domain Admins").SID
#$Ownr = New-Object System.Security.Principal.SecurityIdentifier (Get-ADUser "USer1").SID
#Generic Cmdlet to get User or Group
$Ownr = New-Object System.Security.Principal.SecurityIdentifier (Get-ADObject -Filter {Name -like $OwnerNew} -Properties objectSid).objectSid
#$Ownr = New-Object System.Security.Principal.SecurityIdentifier (Get-ADObject -Filter {Name -like "User1"} -Properties objectSid).objectSid
#Copy the DACL for the GPO object to be modified in a variable
$Acl = Get-ACL -Path "ad:$($Gpo1.DistinguishedName)"
#Validate the currect owner (- can be skipped in when in a script)
#"Before:"
$aclBefore = $Acl.GetOwner([System.Security.Principal.NTAccount]).Value
#Edit Owner on a GPO using Powershell to new Owner
$Acl.SetOwner($Ownr)
#Note changes are not yet commited, we have made changes only to the variable data not the actual object
#"Ready:"
#$Acl.Owner
#Commit the changes on the variable to the -Path actual object
Set-ACL -Path "ad:$($Gpo1.DistinguishedName)" -ACLObject $Acl
#"After:"
#Get actual data, not from the old variable to confirm change has been made:
$aclafter = (Get-ACL -Path "ad:$($Gpo1.DistinguishedName)").Owner
$gp.DisplayName+" "+ $aclBefore+" "+ $aclafter
}
#Now add the GPO to the archive
#more on this command at https://technet.microsoft.com/itpro/powershell/mdop/agpm/add-controlledgpo
foreach ($gp in $AllGPO){
Add-ControlledGPO $gp
}
# let person know when script finished, and they need to wait for it to propagate
write-host ""
write-host "this script finshed:"
get-date
write-host ""
write-host "THIS MAY TAKE UP TO 15 MINUTES TO FINISH" -ForegroundColor Red
write-host ""
Tuesday, July 12, 2016
Splunk and Tor exit nodes
We wanted a way to figure out if a "bad actor" was using TOR to connect to our servers/resources. So I thought: "SPLUNK TO THE RESCUE!"
I am a Windows guy, so I wrote a Powershell script to retrieve a list of TOR exit nodes and write them to a file. Then I use SPLUNK to pick up that file, index it, and extract the interesting fields to then use in other SPLUNK dashboards/reports/whatever.
--------------------------------------------
automated task:
program/script: powershell.exe
Add arguments: -file "C:\SplunkInput\Scripts\Get-TorExitNodeList.ps1" -NoProfile
Start in: C:\SplunkInput\Scripts\
---------------------------------
POSH Script:
------------------------------------------------------------------
For the SPLUNK forwarder, input.conf:
[monitor://C:\SplunkInput\TorExitNode]
crcSalt = <SOURCE>
#initCrcLength = 4096
disabled = 0
sourcetype = TorExitNodeList
index = tor
------------------------------------------------------------
For the SPLUNK field extraction, props.conf:
[TorExitNodeList]
DATETIME_CONFIG =
NO_BINARY_CHECK = true
category = Custom
description = Tor Exit Node List
disabled = false
pulldown_type = true
HEADER_FIELD_LINE_NUMBER=14
#LINE_BREAKER = \bLastStatus\b
SHOULD_LINEMERGE = True
BREAK_ONLY_BEFORE = ExitNode
EXTRACT-ip-torexitnode = ^\w+\s+(?P<ip>[^ ]+)
EXTRACT-Last_Checkin_Date,Last_Checkin_Time = ^(?:[^ \n]* ){2}(?P<Last_Checkin_Date>[^ ]+)\s+(?P<Last_Checkin_Time>\d+:\d+:\d+)
References:
http://wiki.splunk.com/Community:Troubleshooting_Monitor_Inputs
http://docs.splunk.com/Documentation/Splunk/6.0.1/Data/Howlogfilerotationishandled
I am a Windows guy, so I wrote a Powershell script to retrieve a list of TOR exit nodes and write them to a file. Then I use SPLUNK to pick up that file, index it, and extract the interesting fields to then use in other SPLUNK dashboards/reports/whatever.
--------------------------------------------
automated task:
program/script: powershell.exe
Add arguments: -file "C:\SplunkInput\Scripts\Get-TorExitNodeList.ps1" -NoProfile
Start in: C:\SplunkInput\Scripts\
---------------------------------
POSH Script:
<#
.SYNOPSIS
Gets the list of tor exit nodes from the TOR project, put them in a file
.DESCRIPTION
Gets the list of tor exit nodes from the TOR project, put them in a file for other use (like SPLUNK).
Will by default write to "C:\SplunkInput\TorExitNode\TorExitNode.txt"
.EXAMPLE
./Get-TorExitNodeList.ps1
.NOTES
I recommend you run this as a scheduled task, every 5 min or so, as the list changes often.
I recommend you use a SPLUNK forwarder to take this file and index it.
Created by Bryan Loveless
Bryan.Loveless@gmail.com
July 2016
#>
#cleanup the old log files
remove-item C:\SplunkInput\TorExitNode\TorExitNode*.txt
#get the current date/time to create a new file
$now = (Get-Date).ToString("s").Replace(":","-")
$file = "C:\SplunkInput\TorExitNode\TorExitNode" + $now + ".txt"
#request the list of exit nodes, appending them to the file created above.
((invoke-webrequest -uri https://check.torproject.org/exit-addresses -UseBasicParsing).rawcontent) | out-file $file -Append
------------------------------------------------------------------
For the SPLUNK forwarder, input.conf:
[monitor://C:\SplunkInput\TorExitNode]
crcSalt = <SOURCE>
#initCrcLength = 4096
disabled = 0
sourcetype = TorExitNodeList
index = tor
------------------------------------------------------------
For the SPLUNK field extraction, props.conf:
[TorExitNodeList]
DATETIME_CONFIG =
NO_BINARY_CHECK = true
category = Custom
description = Tor Exit Node List
disabled = false
pulldown_type = true
HEADER_FIELD_LINE_NUMBER=14
#LINE_BREAKER = \bLastStatus\b
SHOULD_LINEMERGE = True
BREAK_ONLY_BEFORE = ExitNode
EXTRACT-ip-torexitnode = ^\w+\s+(?P<ip>[^ ]+)
EXTRACT-Last_Checkin_Date,Last_Checkin_Time = ^(?:[^ \n]* ){2}(?P<Last_Checkin_Date>[^ ]+)\s+(?P<Last_Checkin_Time>\d+:\d+:\d+)
-------------------------------------------------------------------
References:
http://wiki.splunk.com/Community:Troubleshooting_Monitor_Inputs
http://docs.splunk.com/Documentation/Splunk/6.0.1/Data/Howlogfilerotationishandled
Wednesday, May 18, 2016
Visual Studio POSH snippet, DocuComment
I am trying to get into more Visual Studio (as that is what "real" programmers use).... and to my delight, I found out that PowerShell is more supported than ever before in VS 2015. Perhaps I can finally put away my childish things (ISE) and move onto something much more unnecessarily complicated. So when I read about "code snippets" I was excited... but....
No matter how much I searched, I could not find one for a simple "documentation block." You know, the one with the ".Synopsis", and ".Example" that we are supposed to use in our scripts, but never do? Perhaps I can make the world a better place by figuring it out and allowing others to use it. Now you have no excuse to not document your code better, you are just three clicks away from having the "DocuComment" block created for you!
You will need to put the code below in to a file with an extension of ".snippet" and import it into VS.
(Details of this can be found at: https://msdn.microsoft.com/en-us/library/9ybhaktf(v=vs.100).aspx )
DocuComment.snippet :
No matter how much I searched, I could not find one for a simple "documentation block." You know, the one with the ".Synopsis", and ".Example" that we are supposed to use in our scripts, but never do? Perhaps I can make the world a better place by figuring it out and allowing others to use it. Now you have no excuse to not document your code better, you are just three clicks away from having the "DocuComment" block created for you!
You will need to put the code below in to a file with an extension of ".snippet" and import it into VS.
(Details of this can be found at: https://msdn.microsoft.com/en-us/library/9ybhaktf(v=vs.100).aspx )
DocuComment.snippet :
<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>DocuComment Block</Title>
<Shortcut>docucomment</Shortcut>
<Description>Code snippet for a comment block.</Description>
<Author>Bryan Loveless bryan.loveless@gmail.com</Author>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
<SnippetType>SurroundsWithStatement</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>param1</ID>
<ToolTip>DocuComment</ToolTip>
<Default>
.SYNOPSIS
synopsis of script, overall idea
.DESCRIPTION
description of overall script, more detail than synopsis
.PARAMETER parameternamehere
parameter description, if required, possible values
.PARAMETER path
parameter path
.EXAMPLE
example of script use, return behavior.
.EXAMPLE
another exmample if there are more. This can be repeated for as many examples as you want
.NOTES
other misc notes, perhaps permissions needed, dates of script.
</Default>
</Literal>
</Declarations>
<References />
<Code Language="PowerShell">
<![CDATA[<#
$param1$
$selected$ $end$
#>]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
Labels:
CocuComment,
Documentation,
POSH,
powershell,
VisualStudio,
work,
XML
Wednesday, December 16, 2015
The Netscaler is hiding stuff from you...
I have been thinking recently about how to hid my infrastructure info from the public, and one easy way is to stop telling the world what type of webserver you are running. Now I am not going to get into the discussion of whether or not "security through obscurity" works... but this is so easy, even if it hinders some script kiddies, I will be happy.
There are lots of ways to see the response headers from your webserver, and I found a scanner that will tell you that and a bit more: https://securityheaders.io I ran some of my URLs though the device, and sure enough, they are blabbing to the world what versions of whatever it has....
So instead of trying to figure out how to get all of my webservers to shut up, I decided to use the Netscaler to just remove the headers before they are presented to the client. I must admit, it was pretty easy... from the CLI. When I tried it from the GUI, I had a strange message and didn't want to fuss around with it much more.
-----------------------------
Remove "Server" header:
add rewrite action Delete_server_header_action delete_http_header Server -bypassSafetyCheck YES -comment "This will delete the Server Header field from Server's response before sending to client"
add rewrite policy Delete_server_header_policy "HTTP.RES.HEADER(\"Server\").EXISTS" Delete_server_header_action -comment "This will delete the Server header field from server\'s response before sending to client"
Now to remove "x-powered-by" header:
add rewrite action Delete_x-powered-by_header_action delete_http_header X-Powered-By -comment "This will delete the X-Powered-By Header field from Server's response before sending to client"
add rewrite policy Delete_x-powered-by_header_policy "HTTP.RES.HEADER(\"X-Powered-By\").EXISTS" Delete_x-powered-by_header_action -comment "This will delete the X-Powered-By header field from server\'s response before sending to client"
-------------------------
then bind them both to your Content Switching Virtual Server, give it priority of 85 (in my case I had a few others I want to run afterwards), and change "goto expression" to "NEXT"
Easy, right? Now run your test again, and those headers are now missing... of course, you could replace the headers with something fun, like "X-Powered-By: The Dark Side" or whatever.... but I am not sure my employer would appreciate the humor as much as I would.
Friday, October 16, 2015
OnBase isn't.
Executive summary: OnBase has terrible technical support, and the simplest things are either not supported or are not possible. (oh, but I found out they have a slide)
I had a ticket to push out using Group Policy a change to the ODBC settings for OnBase users, as the back-end database server was being moved and upgraded. Simple right?
This:
[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\ODBC\ODBC.INI\OnBaseProd]
"Driver"="C:\\Windows\\SysWOW64\\sqlncli10.dll"
"Description"="OnBaseProd"
"Server"="serverA.company.blah "
"Database"="OnBase"
Is replaced through group policy to:
[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\ODBC\ODBC.INI\OnBaseProd]
"Driver"="C:\\Windows\\SysWOW64\\sqlncli10.dll"
"Description"="OnBaseProd"
"Server"="serverB.company.blah "
"Database"="OnBase"
Seems pretty straightforward. Notice that only one line was changed, and it was the server name. (This was also done for the 32bit side too, after discovering that OnBase can ONLY use the 32bit drivers.)
(Disclaimer: I was not around for the initial installation of the OnBase, I am not the OnBase admin, I am not the OnBase DBA. I am a Server Admin who takes care of Active Directory, GPOs, and lots of other non-OnBase items. My views do not necessarily reflect those of my employer...)
A few hours later, users get the GPO, and OnBase doesn't work. They keep getting prompted for a username. We tested this numerous times for months, this hasn't happened in our test environment.
We look around the new DB server, all security settings, users, and roles are the same. It appears that OnBase security is based upon (at least at the DB connection level) the hostname of the connecting client. No prob we thought, as you can read above, has not changed. We added the AD user group to the same role as the machines... nothing.
After fusing around a bit, we decide to call OnBase Tech support. I was surprised by the lack of support... and I have worked with Adobe tech support.
Me: We changed ODBC server name, clients are prompted to log in... but it doesn't work, did we miss something?
OnBase: You have to log in with the user HSI.
Me: But the DB user HSI is a database owner.
OnBase: Yeah.
<a few min later as I clarify with him>
Me: So you want me to figure out how to push a database owner username and clear text password to 5,000 machines?
OnBase: Yes, that is the only way to make the initial connection to the DB for the Thick Clients.
<I put the phone on mute while my partner talks to the Tech support guy and I bang my head against the wall>
<Also, to get an idea of the type of security OnBase uses, their "network security" password that is hard coded into the server app is "ROMANZO" as documented.>
Me: So what about if we create a new user in the DB, one that is not the DB owner (and therefore cannot delete all the tables in the database, give that user permissions to only edit the necessary tables that are needed for the initial setup?
OnBase: That won't work, as the OnBase Thick client is hard coded to only accept the username HSI. You have to use that username or it wont work.
<I think to myself, are you effin' kidding me?>
Me: Ok, what about if I remove most of the permissions for HSI, so it is not an owner of the DB (And capable of destroying years of work), so that HSI is only used for the initial install for clients?
OnBase: Changing any of the Database backend will violate our contract with you and we will not support you anymore.
<I am not sure how we can get any less support at this point in time... time to frame it a different way, perhaps it is a communication problem... as sprinkled throughout this conversation, he kept saying it is a Microsoft ODBC problem>
Me: Ok, let's say I am a multinational corporation, and I want to install your product remotely to thousands of clients across the world.... how would I do that?
OnBase: We don't support installation of our clients.
<this was then confirmed by our OnBase sysadmin who said they had to write a custom installer to copy files, write registry keys, create shortcuts, etc..... I couldn't believe it.>
After trying a few more perspectives, I realized that every machine was going to have to be touched by a person who is trustworthy enough to know the password to the database owner user.
So like everyone else who is out of ideas, I decided to take to Twitter:
no, @OnBase, I don't want to distribute the Database Owner username and password to my entire domain. what, you don't have another solution?
— Bryan (@actualreverend) October 15, 2015
Hey, I recieved a message from "+OnBase by Hyland " asking me to direct message them, so I do.
I wrote them back: (Times are approx and local, timespans are not)
Bryan
Hello, Could we talk via phone, my number is xxx-xxx-xxxx.... have a meeting in about 30 min or so, then back after 1:30ish
(10:20AM Thurs)
OnBase by Hyland
Bryan, (Name of person who if find out is our sales rep) will be reaching out to you shortly. Thank you.
(11:20AM Thurs)
Bryan
great, it is 2:00 XX time as I write this
(2:00PM Thurs)
Bryan
or, you can e-mail me at mywork.e-mailaddresshere to schedule some time or start an e-mail thread
(2:00PM Thurs)
OnBase by Hyland
Sounds good! We will be in touch soon.
(2:00PM Thurs)
Bryan
I am leaving, as I have been here since 6am or so.... I am here normally 8:-4:30ish AZ time
(4:40PM Thurs)
OnBase by Hyland
(Name of person who if find out is our sales rep) has been in touch with your Sys Admin to resolve any issues. Thanks for reaching out!
(5:30AM Fri)
Bryan
I am the server admin. Was there a solution other than handing out the HSI username and password to every user?
(6:30AM Fri)
OnBase by Hyland
Let us look into this and get back to you. Thanks!
(6:30AM Fri)
So here is a multi-million dollar software company who has technical support reps who told us "we were not trained on that, " or "I don't see anything in the manual about that," requires normal users to log in using the database owner account, and doesn't have a way to deploy their "thick clients" using the number one business software in the world (Microsoft Active Directory)?
Hey, but at least they have a corporate slide I guess:
https://en.wikipedia.org/wiki/Hyland_Software#/media/File:Hyland_redslide.jpg
--Bryan
Labels:
group policy,
ODBC,
OnBase
Tuesday, August 4, 2015
Netscaler cert... damn thing hung up on me again
Netscaler has a strange GUI that I think was designed as an "afterthought" by the developers. The more you use it, the more you try to figure out why stuff is in the order it is, or the grouping it is in. Sometimes the Netscaler will perform an operation, and drop your connection without warning. So here is how to install Certificates, which might end in a dropped connection when associating a cert to the device management interface.
Request a certificate as you would normally do, using IIS. This has been documented plenty of other places, so skipped here.
Because of the HA pair, you will need one cert, but make it good for 2 DNS names, including NS.blah.com and NS-Otherlocation.blah.com
This is also good for moving a site's SSL certificate to the Netscaler for load balancing from an IIS host.
Visit http://www.derekseaman.com/2013/05/import-iis-ssl-certificate-to-citrix-netscaler.html on how to export this new certificate into the Netscaler UNTIL the section where you have to upload it to the NETSCALER
Most of Mr Seaman's instructions will still work, but things may be very slightly out of order, like the order to click "browse" or whatever... but it is much easier with his diagrams than I can explain here. Remember to use a good password manager to generate and store any passwords you use in this process.
When you are finished, the cert is ready to be used with your VIP.
Download the "X509 Certificate only, Base64 encoded" file and open it in a text editor.
blah_com_cert.cer
Download the "X509 Intermediates/root only Reverse, Base64 encoded: " file and open it as well.
blah_com_interm.cer
Create a new text file. Copy the X509 Certificate only, Base64 encoded cert to it first and then copy the NEXT two X509 Intermediates/root only Reverse, Base64 encoded certs from the file below the first. (They will be the first two in the blah_com_interm.cer file. The root is the last one in that file and you don't want it.) Now save the file with a meaningful name, like "blah_com-bun-noroot.crt". (bun=bundle)
Login into the NS and upload your cert bundle and private key. In this example they would be blah_com.key and blah_com-bun-noroot.crt.
Then under SSL/Certificates select the ns-server-certificate and update it. There's a check box on the Update Certificate window that says, Click to update Certificate/Key. Select that and then browse for the two files you just uploaded. Also check the box "no domain check" if you are switching domain suffixes .
You do not have to modify the other Node in the HA pair, the HA standby member gets updated automatically.
You can diagnose/view the certs you uploaded using "shell" and "openssl x509 -in NAMEOFFILE.cer -text -noout"
Happy balancing,
-_Bryan
Request a certificate as you would normally do, using IIS. This has been documented plenty of other places, so skipped here.
Because of the HA pair, you will need one cert, but make it good for 2 DNS names, including NS.blah.com and NS-Otherlocation.blah.com
This is also good for moving a site's SSL certificate to the Netscaler for load balancing from an IIS host.
Visit http://www.derekseaman.com/2013/05/import-iis-ssl-certificate-to-citrix-netscaler.html on how to export this new certificate into the Netscaler UNTIL the section where you have to upload it to the NETSCALER
(Mr Derek Seaman's instructions are good, but not for our NS version. You can probably figure it out with clicking around, but just in case:)At this point, on the Netscaler, you select Traffic management --> ssl and "import PKCS#12"
Most of Mr Seaman's instructions will still work, but things may be very slightly out of order, like the order to click "browse" or whatever... but it is much easier with his diagrams than I can explain here. Remember to use a good password manager to generate and store any passwords you use in this process.
When you are finished, the cert is ready to be used with your VIP.
IF YOU ARE INSTALLING THE CERT FOR THE NETSCALER DEVICE ITSELF:
Skip the step above where you upload the certificate, or remove it from the Netscaler if you have already uploaded it.Download the "X509 Certificate only, Base64 encoded" file and open it in a text editor.
blah_com_cert.cer
Download the "X509 Intermediates/root only Reverse, Base64 encoded: " file and open it as well.
blah_com_interm.cer
Create a new text file. Copy the X509 Certificate only, Base64 encoded cert to it first and then copy the NEXT two X509 Intermediates/root only Reverse, Base64 encoded certs from the file below the first. (They will be the first two in the blah_com_interm.cer file. The root is the last one in that file and you don't want it.) Now save the file with a meaningful name, like "blah_com-bun-noroot.crt". (bun=bundle)
Login into the NS and upload your cert bundle and private key. In this example they would be blah_com.key and blah_com-bun-noroot.crt.
Then under SSL/Certificates select the ns-server-certificate and update it. There's a check box on the Update Certificate window that says, Click to update Certificate/Key. Select that and then browse for the two files you just uploaded. Also check the box "no domain check" if you are switching domain suffixes .
After clicking "ok", wait a min or two... then you will have to reconnect your browser.
Check the certificate in the browser, it should list new certificate.
You can diagnose/view the certs you uploaded using "shell" and "openssl x509 -in NAMEOFFILE.cer -text -noout"
Happy balancing,
-_Bryan
Labels:
certificate,
Netscaler,
work
Friday, July 31, 2015
Splunkin' the Windows Firewall Log automagically
Splunk and Windows Firewall logging, gettin' the fields out.
I spent a a bit of time trying to figure out how to get SPLUNK to parse out our Window Firewall logs. I found several sites explaining how it should work.
I was able to get Transforms working by using the site http://answers.splunk.com/answers/107278/windows-firewall-log-extraction-transforms.html .
For example, after feeding the logs into Splunk with a simple file monitor and forwarder, I would enter: " sourcetype=pfirewall | extract Transform_Windows_FW " and it would parse out the fields.
I wanted this to happen automagically, so I tried the second part of the link above, where you edit the props.conf file.... fail. So long story short, I edited the props.conf file to contain:
"
[pfirewall]
EXTRACT-date,time,action,protocol,src_ip,dst_ip,src_port,dst_port,size,info = ^(?P<date>[^ ]+)\s+(?P<time>[^ ]+)\s+(?P<action>[^ ]+)\s+(?P<protocol>\w+)[^ \n]* (?P<src_ip>[^ ]+)\s+(?P<dst_ip>[^ ]+)\s+(?P<src_port>[^ ]+)\s+(?P<dst_port>[^ ]+)\s+(?P<size>[^ ]+)(?:[^ \n]* ){8}(?P<info>\w+)
p.s. You must edit the C:\Program Files\Splunk\etc\system\local\props.conf file, not the one in the default folder, or you might mess up some default functionality of Splunk.
I spent a a bit of time trying to figure out how to get SPLUNK to parse out our Window Firewall logs. I found several sites explaining how it should work.
I was able to get Transforms working by using the site http://answers.splunk.com/answers/107278/windows-firewall-log-extraction-transforms.html .
For example, after feeding the logs into Splunk with a simple file monitor and forwarder, I would enter: " sourcetype=pfirewall | extract Transform_Windows_FW " and it would parse out the fields.
I wanted this to happen automagically, so I tried the second part of the link above, where you edit the props.conf file.... fail. So long story short, I edited the props.conf file to contain:
"
[pfirewall]
EXTRACT-date,time,action,protocol,src_ip,dst_ip,src_port,dst_port,size,info = ^(?P<date>[^ ]+)\s+(?P<time>[^ ]+)\s+(?P<action>[^ ]+)\s+(?P<protocol>\w+)[^ \n]* (?P<src_ip>[^ ]+)\s+(?P<dst_ip>[^ ]+)\s+(?P<src_port>[^ ]+)\s+(?P<dst_port>[^ ]+)\s+(?P<size>[^ ]+)(?:[^ \n]* ){8}(?P<info>\w+)
"
and added it to my search head. Tested with IPv6 and it seems to work fine.
(Technically a co-worker tried this as I was trying other things, so he gets the credit)
p.s. You must edit the C:\Program Files\Splunk\etc\system\local\props.conf file, not the one in the default folder, or you might mess up some default functionality of Splunk.
Tuesday, July 21, 2015
Netscaler N00b no more?
My employer purchased a few Netscalers (NS) and put me and another dozen or so folks through training on how to configure and use it. But, nothing prepared me for the strange way Citrix and Citrix-fans write their documentation.
So here are a few things I learned in the last few weeks working with it:
1) AD and AAA - there are many articles on how to use AAA with Active Directory. (http://support.citrix.com/article/CTX111079 for example) I searched for quite some time trying to figure out how to "link" an AD group to a NS group. I assumed I would have to create a group on the NS, then tell the NS to associate that group with the AD group.... But no. I wanted to customize what the NS group was called, but you cannot. So here was my trick: Create a group on the NS EXACTLY the way it is spelled in AD and associate it with a policy ( I used a built-in NS policy). Then under "System, authentication, LDAP, Servers tab, the value I used was:
"memberOf=CN=ad_group_name_here,OU=ou_where_group_is_located,DC=domain_name,DC=dopmain_name,DC=domain_name,DC=com" . In short, a NS local group of the same name is associated with a local LDAP policy which is searched for using location in AD.
Also, here is another thing that I hope will save you some time. LDAP using FQDNs did not work for us. Instead, I had to create one server LDAP policy for each of our domain controllers using their individual IPs. DNS is setup, and seems to work great with everything else tested (ping, traceroute), but it appears the Nestcaler does not handle multiple A record responses from a DNS server in this circumstance.
2) AppExpert Templates - I was experimenting with SharePoint behind the NS, and I came across the AppExpert Template for it. It looked pretty neat, with lots of optimizations and promises of improved performance.
I tried using many walk-troughs to implement it, but none of them seemed to work:
https://www.citrix.com/content/dam/citrix/en_us/documents/products-solutions/microsoft-sharepoint-2013-with-citrix-netscaler.pdf
https://www.paloaltonetworks.com/content/dam/paloaltonetworks-com/en_US/assets/pdf/technology-solutions-briefs/citrix/panw-netscaler-sharepoint.pdf
https://www.citrix.com/content/dam/citrix/en_us/documents/products-solutions/citrix-netscaler-datasheet-microsoft-sharepoint-2013.pdf
So, here is what I had to know to get the template properly installed:
-- The NAME you first enter MUST NOT include any special characters. It will error towards the end with a strange message if you do.
-- You must not already have the resource of the "public endpoint" as a current Load balanced server or anywhere else if you can help it. The only thing you should have configured is the VIP configured on the NS itself. The template will create the server for you.
-- Only a N00b like me probably thought this, but the template does not create a "load balanced" server, it created a "content switching server."
-- If you need to delete the template, you will have to remove all of the Response, Rewrite, and all the other Policies and Actions it creates. Luckily they will start with the name you provided above.
3) Mac address filtering - We have some "real servers" and some VIPs behind and in front of the NS. We could not figure out why the traffic would disappear. It turned out that you must enable "
MAC based forwarding" under System, Settings, Configure Modes. The networking team here hated me for a few days, and they thought I was an idiot, as their NS instance worked fine... but they didn't have anything that had to traverse a firewall. This little checkbox was the reason it only half-worked for me.
4) Routes - For me, the GUI is really confusing, as they have a column that says "Gateway/Owned IP/Name." Long story short, add Routes using the CLI as it makes a hell of a lot more sense.
May your life and servers be forever balanced,
-_Bryan
So here are a few things I learned in the last few weeks working with it:
1) AD and AAA - there are many articles on how to use AAA with Active Directory. (http://support.citrix.com/article/CTX111079 for example) I searched for quite some time trying to figure out how to "link" an AD group to a NS group. I assumed I would have to create a group on the NS, then tell the NS to associate that group with the AD group.... But no. I wanted to customize what the NS group was called, but you cannot. So here was my trick: Create a group on the NS EXACTLY the way it is spelled in AD and associate it with a policy ( I used a built-in NS policy). Then under "System, authentication, LDAP, Servers tab, the value I used was:
"memberOf=CN=ad_group_name_here,OU=ou_where_group_is_located,DC=domain_name,DC=dopmain_name,DC=domain_name,DC=com" . In short, a NS local group of the same name is associated with a local LDAP policy which is searched for using location in AD.
Also, here is another thing that I hope will save you some time. LDAP using FQDNs did not work for us. Instead, I had to create one server LDAP policy for each of our domain controllers using their individual IPs. DNS is setup, and seems to work great with everything else tested (ping, traceroute), but it appears the Nestcaler does not handle multiple A record responses from a DNS server in this circumstance.
2) AppExpert Templates - I was experimenting with SharePoint behind the NS, and I came across the AppExpert Template for it. It looked pretty neat, with lots of optimizations and promises of improved performance.
I tried using many walk-troughs to implement it, but none of them seemed to work:
https://www.citrix.com/content/dam/citrix/en_us/documents/products-solutions/microsoft-sharepoint-2013-with-citrix-netscaler.pdf
https://www.paloaltonetworks.com/content/dam/paloaltonetworks-com/en_US/assets/pdf/technology-solutions-briefs/citrix/panw-netscaler-sharepoint.pdf
https://www.citrix.com/content/dam/citrix/en_us/documents/products-solutions/citrix-netscaler-datasheet-microsoft-sharepoint-2013.pdf
So, here is what I had to know to get the template properly installed:
-- The NAME you first enter MUST NOT include any special characters. It will error towards the end with a strange message if you do.
-- You must not already have the resource of the "public endpoint" as a current Load balanced server or anywhere else if you can help it. The only thing you should have configured is the VIP configured on the NS itself. The template will create the server for you.
-- Only a N00b like me probably thought this, but the template does not create a "load balanced" server, it created a "content switching server."
-- If you need to delete the template, you will have to remove all of the Response, Rewrite, and all the other Policies and Actions it creates. Luckily they will start with the name you provided above.
3) Mac address filtering - We have some "real servers" and some VIPs behind and in front of the NS. We could not figure out why the traffic would disappear. It turned out that you must enable "
MAC based forwarding" under System, Settings, Configure Modes. The networking team here hated me for a few days, and they thought I was an idiot, as their NS instance worked fine... but they didn't have anything that had to traverse a firewall. This little checkbox was the reason it only half-worked for me.
4) Routes - For me, the GUI is really confusing, as they have a column that says "Gateway/Owned IP/Name." Long story short, add Routes using the CLI as it makes a hell of a lot more sense.
May your life and servers be forever balanced,
-_Bryan
Labels:
Active Directory,
citrix,
load balancing,
Netscaler,
network,
sharepoint,
work
Tuesday, June 23, 2015
Simply log what VM is running and where.
We have an issue where randomly a node in our Hyper-V cluster will fail. We know it is related to an iSCSI event, but we cannot reproduce the error. Since we don't know what is running when it happens, I thought we could simply log what VMs are running, and on what hosts.
I looked around the internets (or BinGled so to speak) and didn't see anyone who was simply listing the Hyper-V virtual machines running on a host. (Some folks were doing it for Citrix for VMware, but not Hyper-V.)
So I wrote a simple script that will display it on the screen, along with writing it to a file (as we plan on running this script every 24 hours or so, waiting for a crash).
I looked around the internets (or BinGled so to speak) and didn't see anyone who was simply listing the Hyper-V virtual machines running on a host. (Some folks were doing it for Citrix for VMware, but not Hyper-V.)
So I wrote a simple script that will display it on the screen, along with writing it to a file (as we plan on running this script every 24 hours or so, waiting for a crash).
####################################################################################
####################################################################################
<#.Synopsis
List all VMs running in the cluster, along with what host it is running on and writes them to a location of your choosing.
.Description
List all VMs running in the cluster, along with what host it is running on and writes them to a location of your choosing.
.Parameter filetowrite
Path and file name to write the output to.
.Parameter clustergroupname
Cluster name itself, Fully qualified recommended
.Example
.\List-Vms -filetowrite c:\temp\vmlist.txt -clustergroupname YOURCLUSTERNAMEHERE-FQDN
.Notes
Author: Bryan Loveless bryan.loveless@gmail.com
Requires -Version 4.0
Version: 1.0
Updated: 22.June.2015
LEGAL: PUBLIC DOMAIN. SCRIPT PROVIDED "AS IS" WITH NO WARRANTIES OR GUARANTEES OF
ANY KIND, INCLUDING BUT NOT LIMITED TO MERCHANTABILITY AND/OR FITNESS FOR
A PARTICULAR PURPOSE. ALL RISKS OF DAMAGE REMAINS WITH THE USER, EVEN IF
THE AUTHOR, SUPPLIER OR DISTRIBUTOR HAS BEEN ADVISED OF THE POSSIBILITY OF
ANY SUCH DAMAGE. IF YOUR STATE DOES NOT PERMIT THE COMPLETE LIMITATION OF
LIABILITY, THEN DELETE THIS FILE SINCE YOU ARE NOW PROHIBITED TO HAVE IT.
#>
Function List-VMs
{
[CmdletBinding()]
Param (
[Parameter(
Position=1,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true,
#ValidateNotNullOrEmpty(),
Mandatory=$true,
HelpMessage="Filepath and name?"
)
]
[string]$filetowrite = ("c:\temp\VMlist.txt"),
[Parameter(
Position=2,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true,
#ValidateNotNullOrEmpty(),
Mandatory=$true,
HelpMessage="What is the name of the cluster?"
)
]
[string]$clustergroupname = ("YOURCLUSERNAMEHERE-FQDN")
)
get-date | out-file $filetowrite -Append
$OutArray = @()
$clusterNodes = Get-ClusterNode -cluster $clustergroupname;
ForEach($item in $clusterNodes)
{
write-host "RUNNING ON " ($item.name).ToString() ":" -foregroundcolor GREEN
$item | out-file $filetowrite -Append
$vms = Get-VM -ComputerName $item.Name
ForEach($vm in $vms)
{
write-host $vm.Name $vm.State "CPU used:" $vm.cpuusage "%" "Uptime:" $vm.uptime $vm.Status
$vm | Format-table name, state, cpuusage, memoryassigned, uptime, status | out-file $filetowrite -Append
}
}
}
#now to run it
List-VMs -filetowrite c:\temp\vmlist.txt -clustergroupname YOURCLUSTERNAMEHERE-FQDN
# FIN
Wednesday, April 22, 2015
Confuse your Local Admin, repeatedly. Part 2 - decrypt
Here we decrypt the password, using the private key stored in the local cert or user cert store.
####################################################################################
#.Synopsis
# Retrieve attribute from AD using powershell
#
#.Description
# Retrieve attribute from AD using powershell
#
#
#.Parameter ComputerNames
# One or more computer names that you are requesting the password for.
#
#.Example
# ./Get-EncryptedPasswordFromCarLicenseInAD.ps1 -computer machine1
# This will return the password for the listed machine.
#
#.Example
# ./Get-EncryptedPasswordFromCarLicenseInAD.ps1 -computernames machine1,machine2
# This will return the passwords for both machines listed
#
#
#Requires -Version 3.0
#
#.Notes
# Author: Bryan Loveless bryan.loveless@gmail.com
#
# Version: 1.0
# Updated: 17.April.2015
# LEGAL: PUBLIC DOMAIN. SCRIPT PROVIDED "AS IS" WITH NO WARRANTIES OR GUARANTEES OF
# ANY KIND, INCLUDING BUT NOT LIMITED TO MERCHANTABILITY AND/OR FITNESS FOR
# A PARTICULAR PURPOSE. ALL RISKS OF DAMAGE REMAINS WITH THE USER, EVEN IF
# THE AUTHOR, SUPPLIER OR DISTRIBUTOR HAS BEEN ADVISED OF THE POSSIBILITY OF
# ANY SUCH DAMAGE. IF YOUR STATE DOES NOT PERMIT THE COMPLETE LIMITATION OF
# LIABILITY, THEN DELETE THIS FILE SINCE YOU ARE NOW PROHIBITED TO HAVE IT.
####################################################################################
# Get-EncryptedPasswordFromCarLicenseInAD.ps1
[CmdletBinding()]
Param (
[Parameter(
Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$True,
Mandatory=$false,
HelpMessage="What is/are the computer names?"
)
]
[string[]]$ComputerNames = ("machine1","machine2") #change back to "$env:computername" to run on local machine
)
####################################################################################
# Decrypts TXT using public key
# Decrypt-Asymmetric -EncryptedBase64String $Base64String -CertThumbprint "thumbprintHere"
####################################################################################
Function Decrypt-Asymmetric
{
[CmdletBinding()]
[OutputType([System.String])]
param(
[Parameter(Position=0, Mandatory=$true)][ValidateNotNullOrEmpty()][System.String]
$EncryptedBase64String,
[Parameter(Position=1, Mandatory=$true)][ValidateNotNullOrEmpty()][System.String]
$CertThumbprint
)
# Decrypts cipher text using the private key
# Assumes the certificate is in the LocalMachine\My (Personal) Store
#below looks in local computer store for cert
#$Cert = Get-ChildItem cert:\LocalMachine\My | where { $_.Thumbprint -eq $CertThumbprint }
# below looks in current user store for cert
#$Cert = Get-ChildItem cert:\CurrentUser\my | where { $_.Thumbprint -eq $CertThumbprint }
# below looks in the current user's certificates and private keys AND CHECKS THEM.
# reference: Jason Fossen, Enclave Consulting (http://cyber-defense.sans.org/blog
try
{
$readonlyflag = [System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly
$currentuser = [System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser
$usercertstore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $currentuser
$usercertstore.Open($readonlyflag)
$usercertificates = $usercertstore.Certificates
}
catch
{
"`nERROR: Could not open your certificates store. `n"
exit
}
finally
{
$usercertstore.Close()
}
if ($usercertificates.count -eq 0) { "`nERROR: You have no certificates or private keys.`n" ; exit }
# Load the correct certificate and test for possession of private key.
$cert = $usercertificates | where { $_.thumbprint -eq $CertThumbprint }
if (-not $cert.hasprivatekey)
{
$output.StatusMessage = "ERROR: You do not have the private key for this certificate."
$output.Valid = $false
$output
continue
}
if($Cert) {
$EncryptedByteArray = [Convert]::FromBase64String($EncryptedBase64String)
$ClearText = [System.Text.Encoding]::UTF8.GetString($Cert.PrivateKey.Decrypt($EncryptedByteArray,$true))
}
Else {Write-Error "Certificate with thumbprint: $CertThumbprint not found!"}
Return $ClearText
#reference: http://jeffmurr.com/blog/?p=228
}
#Create the array to store all decrypted passwords
$AllPasswords = @()
ForEach ($ComputerName in $ComputerNames){
# retrieve where the machine lives in AD
$Filter = "(&(objectCategory=Computer)(Name=$ComputerName))"
$DirectorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$DirectorySearcher.Filter = $Filter
$SearcherPath = $DirectorySearcher.FindOne()
$machine = $SearcherPath.GetDirectoryEntry()
#get the "carLicense" attribute from AD
$carLicenseAttribute = ($machine.carLicense)
# get thumbprint of the certificate, create object for it, then set thumbprint for comparison
$output = ($output = " " | select-object Valid,StatusMessage,Password,Thumbprint)
$output.Valid = $false #Assume password recovery will fail.
#
$output.Thumbprint = "PUTYOURCERTIFICATETHUMBPRINTHEREOFTHEONEYOUEXPECT"
# Load the current user's certificates and private keys.
try
{
$readonlyflag = [System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly
$currentuser = [System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser
$usercertstore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $currentuser
$usercertstore.Open($readonlyflag)
$usercertificates = $usercertstore.Certificates
}
catch
{
"`nERROR: Could not open your certificates store. `n"
exit
}
finally
{
$usercertstore.Close()
}
if ($usercertificates.count -eq 0) { "`nERROR: You have no certificates or private keys.`n" ; exit }
# Load the correct certificate and test for possession of private key.
$thecert = $usercertificates | where { $_.thumbprint -eq $output.thumbprint }
if (-not $thecert.hasprivatekey)
{
$output.StatusMessage = "ERROR: You do not have the private key for this certificate."
$output.Valid = $false
$output
continue
}
# Test to confirm that the private key can be accessed, not just that it exists. The
# problem is that it is not a trivial task to allow .NET or PowerShell to use
# private keys managed by Crytography Next Generation (CNG) key storage providers, hence,
# these scripts are only compatible with the older Cryptographic Service Providers (CSPs), such
# as the "Microsoft Enhanced Cryptographic Provider", but not the newer CNG "Microsoft
# Software Key Storage Provider". Sorry...
if ($thecert.privatekey -eq $null)
{
$output.StatusMessage = "ERROR: This script is not compatible with CNG key storage providers."
$output.Valid = $false
$output
continue
}
#Remove Date information, Decrypt password using private key, and return vaule
$decryptedPassword = Decrypt-Asymmetric -EncryptedBase64String (($carLicenseAttribute.ToString()).SubString(19)) -CertThumbprint $output.Thumbprint
Write-Host $computername"'s" "password is" $decryptedPassword
$AllPasswords= $allpasswords + ($decryptedPassword)
Remove-Variable decryptedPassword
}
#returns all of the passwords in the array together, in case script was called expecting returns
Write-Host "Here are all of the passwords you requested:"
Return $AllPasswords
Remove-Variable AllPasswords
# FIN
Labels:
Active Directory,
certificate,
POSH,
powershell,
security,
work
Confuse your Local Admin, repeatedly. Part 1 - change and encrypt
We recently found out that some of our departmental desktop admin staff have been forgetting to change the password for the LocalAdmin account after imaging a machine. Our Information Security team found out this was so widespread, they asked my team to solve this issue. Politics aside, here is what we decided on:
1) Change Local Admin username to something else.
2) Change this new user's password to something unique between every device.
3) Encrypt this password using a public key, then store it in Active Directory.
4) Be able to control who can receive and decrypt this password, and how.
5) Be able to change this password as often as we want, by only issuing a new GPO.
6) Set how often to change the password, as every time it starts seems to harsh.
And now the solutions/background of solutions:
1) Bing how to change local admin username using GPOs. It is super easy.
2) Most password generators I found on the internet rely on Powershell's "Get-Random"... that is not truly random. It is based upon system up-time, so someone much smarter than me might be able to figure out the random seed. So I had help from a buddy at work, and we wrote another get-randompassword function that bases passwords on the .NET security provider. (see code)
3) With the borrowed code from the internet, I discovered this is much easier than I thought it would be. So I generate a "really strong" self-signed cert that is good for 10 years (because we had to take into account a machine that was taken off the domain, and how to access the data on it after it has been purged from AD). I put the public cert in the domain's NETLOGON area, so every authenticated object is able to access it. The private key is (obviously) stored somewhere else, very, very protected. Also, this enables us to flip out the public cert if we need to someday.
4) We are still discussing this, but I will post the code to decrypt the info in "Part 2". The code right now will work if the user logged in or the machine have a certificate that matches the public key, but the idea of a webpage has been discussed allowing our desktop admins to authenticate to a webpage and never have access to the private key itself.
5) With one digit in the POSH, easy-cheesy.
6) I write the value of the last time the password was changed in the AD attribute, then, if over a certain amount of days, I change it again. This prevents us from having the user who "will get around to migrating to the new machine" but doesn't do it within a week or two. Again, this is set in the code, very easy to change.
Now of course, I need to mention I am just a simple Sysadmin, so if this breaks your stuff, then don't blame me... you should have consulted a programmer.
1) Change Local Admin username to something else.
2) Change this new user's password to something unique between every device.
3) Encrypt this password using a public key, then store it in Active Directory.
4) Be able to control who can receive and decrypt this password, and how.
5) Be able to change this password as often as we want, by only issuing a new GPO.
6) Set how often to change the password, as every time it starts seems to harsh.
And now the solutions/background of solutions:
1) Bing how to change local admin username using GPOs. It is super easy.
2) Most password generators I found on the internet rely on Powershell's "Get-Random"... that is not truly random. It is based upon system up-time, so someone much smarter than me might be able to figure out the random seed. So I had help from a buddy at work, and we wrote another get-randompassword function that bases passwords on the .NET security provider. (see code)
3) With the borrowed code from the internet, I discovered this is much easier than I thought it would be. So I generate a "really strong" self-signed cert that is good for 10 years (because we had to take into account a machine that was taken off the domain, and how to access the data on it after it has been purged from AD). I put the public cert in the domain's NETLOGON area, so every authenticated object is able to access it. The private key is (obviously) stored somewhere else, very, very protected. Also, this enables us to flip out the public cert if we need to someday.
4) We are still discussing this, but I will post the code to decrypt the info in "Part 2". The code right now will work if the user logged in or the machine have a certificate that matches the public key, but the idea of a webpage has been discussed allowing our desktop admins to authenticate to a webpage and never have access to the private key itself.
5) With one digit in the POSH, easy-cheesy.
6) I write the value of the last time the password was changed in the AD attribute, then, if over a certain amount of days, I change it again. This prevents us from having the user who "will get around to migrating to the new machine" but doesn't do it within a week or two. Again, this is set in the code, very easy to change.
Now of course, I need to mention I am just a simple Sysadmin, so if this breaks your stuff, then don't blame me... you should have consulted a programmer.
####################################################################################
#.Synopsis
# Resets the password of a local user account with a random password which is
# then encrypted with your pubic key certificate and stored in AD attribute under
# the computer object. The plaintext password is
# displayed with the Get-EncryptedPasswordFromCarLicenseInAD.ps1
# script.
#
#.Description
# Resets the password of a local user account with a 16-25 character, random,
# complex password, which is encrypted with your own pubic key certificate and then
# stored under an Active Directory Attribute for the computer object.
# Recovery of the encrypted password from Active Directory requires possession of the
# private key corresponding to the chosen public key certificate. The password
# is never transmitted or stored in plaintext. The plaintext password
# is recovered with the companion
# Get-EncryptedPasswordFromCarLicenseInAD.ps1 script. The
# script must be run with administrative or local System privileges.
# The Private Key for decryption may be stored in either the Computer's Cert Store or the
# User's cert store.
#
#.Parameter CertificateFilePath
# The local or UNC path to the .CER file containing the public key
# certificate which will be used to encrypt the password. The .CER
# file can be DER- or Base64-encoded. (But note that the private
# key for the certificate cannot be managed by a Cryptography Next
# Generation (CNG) key storage provider, hence, do not use the Microsoft
# Software Key Storage Provider in the template for the certificate.)
#
#.Parameter LocalUserName
# Name of the local user account on the computer where this script is run
# whose password should be reset to a 16-25 character, complex, random password.
# Do not include a "\" or "@" character, only local accounts are supported.
# Defaults to "Guest", but any name can be specified.
#
#
#.Parameter MinimumPasswordLength
# The minimum length of the random password. Default is 16. The exact length
# used is randomly chosen to increase the workload of an attacker who can see
# the contents of this script. Maximum password length defaults to 25. The
# smallest acceptable minimum length is 4 due to complexity requirements.
#
#.Parameter MaximumPasswordLength
# The maximum length of the random password. Default is 16. Max is 127.
# The minimum and maximum values can be identical.
#
#.Example
# .\Set-EcryptedPasswordToCarLicenseInAD.ps1 -CertificateFilePath \\server\share\certificate.cer
#
# Resets the password of the default account, encrypts that password
# with the public key in the certificate.cer file, and saves the encrypted
# password in the AD attribute "carLicense". Choose a different account with -LocalUserName.
#
#
#.Example
# .\Set-EcryptedPasswordToCarLicenseInAD.ps1 -LocalUserName HelpDeskUser -CertificateFilePath \\server\share\certificate.cer
#
# The local account's password is reset by default, but any
# local user name can be specified instead.
#
#
#Requires -Version 2.0
#
#.Notes
# Author: Bryan Loveless bryan.loveless@gmail.com, based upon script by
# Jason Fossen, Enclave Consulting (http://cyber-defense.sans.org/blog)
# Password cryptographic method by Bryan Loveless bryan.loveless@gmail.com
# Version: 1.0
# Updated: 17.April.2015
# LEGAL: PUBLIC DOMAIN. SCRIPT PROVIDED "AS IS" WITH NO WARRANTIES OR GUARANTEES OF
# ANY KIND, INCLUDING BUT NOT LIMITED TO MERCHANTABILITY AND/OR FITNESS FOR
# A PARTICULAR PURPOSE. ALL RISKS OF DAMAGE REMAINS WITH THE USER, EVEN IF
# THE AUTHOR, SUPPLIER OR DISTRIBUTOR HAS BEEN ADVISED OF THE POSSIBILITY OF
# ANY SUCH DAMAGE. IF YOUR STATE DOES NOT PERMIT THE COMPLETE LIMITATION OF
# LIABILITY, THEN DELETE THIS FILE SINCE YOU ARE NOW PROHIBITED TO HAVE IT.
####################################################################################
# Set-EncryptedPasswordToCarLicenseInAD.ps1
Param ($CertificateFilePath = ".\LocalAdminPasswordChangePublicCert.cer", $LocalUserName = "bryan", $MinimumPasswordLength = 16, $MaximumPasswordLength = 25)
####################################################################################
# Below replaces the generate-randompassword used on a previous version of this script,
# as the older one used get-random and this new one uses the .net cryptographic provider
#####################################################################################
function Generate-Password() {
param(
[int] $len = 16,
[string] $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_!@#$%^&*()_"
)
$result = ""
for( $i=0; $i -lt $len; $i++ )
{
$bytes = new-object "System.Byte[]" 1
$rnd = new-object System.Security.Cryptography.RNGCryptoServiceProvider
$rnd.GetBytes($bytes)
if ($bytes[0] -gt (([int](256/$chars.Length))*$chars.length))
{
$i--
continue
}
$result += $chars[ $bytes[0] % $chars.Length ]
}
return $result
<#
.SYNOPSIS
Returns a secure password, based on the System's Crytpo Service Provider.
.EXAMPLE
Generate-Password
Returns a 16 character password, using A-Z, numbers, and the "easier to type" special characters
.EXAMPLE
Generate-Password -len 48 -chars "0123456789"
Returns a 48 length numeric password
.EXAMPLE
Generate-Password -len 1 -chars "01"
Returns a one character password that is either a 0 or a 1"
.NOTES
Modified by Bryan Loveless bryan.loveless@gmail.com Jan 2015
Based on http://www.peterprovost.org/blog/2007/06/22/Quick-n-Dirty-PowerShell-Password-Generator/
BUT modified to be a little bit more secure
#>
}
####################################################################################
# Returns true if password reset accepted, false if there is an error.
# Only works on local computer, but can be modified to work remotely too.
####################################################################################
Function Reset-LocalUserPassword ($UserName, $NewPassword)
{
Try
{
$ADSI = [ADSI]("WinNT://" + $env:ComputerName + ",computer")
$User = $ADSI.PSbase.Children.Find($UserName)
$User.PSbase.Invoke("SetPassword",$NewPassword)
$User.PSbase.CommitChanges()
$User = $null
$ADSI = $null
$True
}
Catch
{ $False }
}
####################################################################################
# Writes to console, writes to Application event log, optionally exits.
# Event log: Application, Source: "PasswordArchive", Event ID: 9013
####################################################################################
function Write-StatusLog ( $Message, [Switch] $Exit )
{
# Define the Source attribute for when this script writes to the Application event log.
New-EventLog -LogName Application -Source PasswordArchive -ErrorAction SilentlyContinue
"`n" + $Message + "`n"
#The following here-string is written to the Application log only when there is an error,
#but it contains information that could be useful to an attacker with access to the log.
#The data is written for troubleshooting purposes, but feel free change it if concerned.
#It does not contain any passwords of course.
$ErrorOnlyLogMessage = @"
$Message
CurrentPrincipal = $($CurrentPrincipal.Identity.Name)
CertificateFilePath = $CertificateFilePath
LocalUserName = $LocalUserName
PasswordArchivePath = $PasswordArchivePath
ArchiveFileName = $filename
"@
if ($Exit)
{ write-eventlog -logname Application -source PasswordArchive -eventID 9013 -message $ErrorOnlyLogMessage -EntryType Error }
else
{ write-eventlog -logname Application -source PasswordArchive -eventID 9013 -message $Message -EntryType Information }
if ($Exit) { exit }
}
####################################################################################
# Writes to AD attribute
#
####################################################################################
Function SendAttribute-ToAD
{
[CmdletBinding()]
Param (
[Parameter(
Position=1,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true,
#ValidateNotNullOrEmpty(),
HelpMessage="What is the field name that you would like to edit?"
)
]
[string]$PropertyToEdit,
[Parameter(
Position=2,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true,
#ValidateNotNullOrEmpty(),
Mandatory=$true,
HelpMessage="What do you want the new entry to be?"
)
]
[string]$EditedInfo
)
$ComputerName = $env:computername
$Filter = "(&(objectCategory=Computer)(Name=$ComputerName))"
$DirectorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$DirectorySearcher.Filter = $Filter
$SearcherPath = $DirectorySearcher.FindOne()
#---------------------------------------
$machine = $SearcherPath.GetDirectoryEntry()
write-host "editing property $PropertyToEdit to have $EditedInfo"
#edit the property
#$machine.$PropertyToEdit = $EditedInfo
$machine.InvokeSet(($PropertyToEdit),(($EditedInfo).tostring()))
write-host "committing changes"
#commit the edit on
$machine.CommitChanges()
}
####################################################################################
# Encrypts TXT using public key
# Encrypt-Asymmetric -ClearText "CLEAR TEXT DATA" -PublicCertFilePath "C:\Scripts\PowerShell\Asymmetrical-Encryption\PowerShellAsymmetricalTest.cer"
####################################################################################
Function Encrypt-Asymmetric {
[CmdletBinding()]
[OutputType([System.String])]
param(
[Parameter(Position=0, Mandatory=$true)][ValidateNotNullOrEmpty()][System.String]
$ClearText,
[Parameter(Position=1, Mandatory=$true)][ValidateNotNullOrEmpty()][ValidateScript({Test-Path $_ -PathType Leaf})][System.String]
$PublicCertFilePath
)
# Encrypts a string with a public key
$PublicCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($PublicCertFilePath)
$ByteArray = [System.Text.Encoding]::UTF8.GetBytes($ClearText)
$EncryptedByteArray = $PublicCert.PublicKey.Key.Encrypt($ByteArray,$true)
$Base64String = [Convert]::ToBase64String($EncryptedByteArray)
Return $Base64String
#reference: http://jeffmurr.com/blog/?p=228
}
# Sanity check the two password lengths:
if ($MinimumPasswordLength -le 3) { $MinimumPasswordLength = 4 }
if ($MaximumPasswordLength -gt 127) { $MaximumPasswordLength = 127 }
if ($MinimumPasswordLength -gt 127) { $MinimumPasswordLength = 127 }
if ($MaximumPasswordLength -lt $MinimumPasswordLength) { $MaximumPasswordLength = $MinimumPasswordLength }
# Confirm that this process has administrative privileges to reset a local password.
$CurrentWindowsID = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$CurrentPrincipal = new-object System.Security.Principal.WindowsPrincipal($CurrentWindowsID)
if (-not $? -or -not $CurrentPrincipal.IsInRole("Administrators"))
{ write-statuslog -m "ERROR: This process lacks the privileges necessary to reset a password." -exit }
# Confirm that the target local account exists and that ADSI is accessible.
if ($LocalUserName -match '[\\@]') { write-statuslog -m "ERROR: This script can only be used to reset the passwords of LOCAL user accounts, please specify a simple username without an '@' or '\' character in it." -exit }
try
{
$ADSI = [ADSI]("WinNT://" + $env:ComputerName + ",computer")
$User = $ADSI.PSbase.Children.Find($LocalUserName)
$User = $null
$ADSI = $null
}
catch
{ write-statuslog -m "ERROR: Local user does not exist: $LocalUserName" -exit }
# Generate and test new random password with min and max lengths.
$newpassword = "ConfirmThatNewPasswordIsRandom"
if ($MinimumPasswordLength -eq $MaximumPasswordLength)
{
# $newpassword = Generate-RandomPassword -Length $MaximumPasswordLength
$newpassword = Generate-Password -len $MaximumPasswordLength
}
else
{
# $newpassword = Generate-RandomPassword -Length $(Get-Random -Minimum $MinimumPasswordLength -Maximum $MaximumPasswordLength)
$newpassword = Generate-Password -len $(Get-Random -Minimum $MinimumPasswordLength -Maximum $MaximumPasswordLength)
}
# Users outside USA might modify the Generate-RandomPassword function, hence this check.
if ($newpassword -eq "ConfirmThatNewPasswordIsRandom")
{ write-statuslog -m "ERROR: Password generation failure, password not reset." -exit }
#encrypt password
$encryptedPassword = Encrypt-Asymmetric -ClearText "$newpassword" -PublicCertFilePath "$CertificateFilePath"
#if ($encryptedPassword -not $?) { write-statuslog -m "ERROR, password was not encrypted, password not reset." -exit }
# Attempt to reset the password.
if ( Reset-LocalUserPassword -UserName $LocalUserName -NewPassword $newpassword )
{
remove-variable -name newpassword #Just tidying up, not really necessary at this point...
write-statuslog -m "SUCCESS: $LocalUserName password reset."
}
else
{
# Write the RESET-FAILURE file to statuslog; these failure files are used by the other scripts too.
write-statuslog -m "ERROR: Failed to reset password:`n`n $error[0]" -exit
}
#now write the encrypted password($content) to the machine's AD attribute($Property), appending today's date on the front
$Property = "carLicense"
$TodaysDate=(GET-DATE -Format s)
SendAttribute-ToAD -PropertyToEdit $Property -EditedInfo "$TodaysDate $encryptedPassword"
# remove-variable -name encryptedPassword #Removing Variable, just in case.
# FIN
Labels:
Active Directory,
certificate,
POSH,
powershell,
security,
Windows,
work
Tuesday, January 20, 2015
Peoplesoft Roles and Active Directory group membership in harmony?
I recently had a request to synchronize a PeopleSoft role with an Active Directory group. I am not a programmer normally, so there might be an easier way to do this, but since I couldn't find anyone else on the internet doing it, here is the script I wrote to accomplish it. You will have to install the Oracle drivers, which was a pain and beyond the scope of this blog post. Also, our Oracle DBAs created a view for us containing only usernames in which I just dump the data from.
###############################################
###############################################
#
#
# Sync oracle (peoplesoft role(s)) results to an active directory group after purging group of current users
#
# Bryan Loveless bryan.loveless@gmail.com
# Jan 2015
#
#
# Prerequisites: Download and install the Oracle Data Access Components prior to accessing a database.
# Download the components here: bit.ly/1t2W790 or http://www.oracle.com/technetwork/topics/dotnet/downloads/index.html
# Select the appropriate architecture (x86/x86-64) and ensure the correct PowerShell program architecture is being executed with
# the corresponding Oracle component’s architecture.
# Failure to do so will lead to binary related errors while loading the assembly in PowerShell.
#
#
#
###############################################
#name of the active directory group you want to modify
$activeDirectoryGroup = 'group_NAME_HERE'
# username, password, server name for oracle DB:
$oracleusername = 'USERNAME'
#for dev purposes, ask for password. remove this bit of code when converted to nightly job
#prompt user with protected pin entering box:
$oraclepasswordencrypted = read-host -prompt "What is the password?" -AsSecureString
#extract plain text pin number from encrypted varible above
$BSTR = `
[System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($oraclepasswordencrypted)
$oraclepassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
# end of "prompt password" part
# $oraclepassword = 'password'
$oracleservername = 'SERVERNAME_AS_FQDN'
$servicename = 'SERVICENAME_AS_FQDN'
# default port 1521
#Load Oracle client
### try to load assembly, fail otherwise ###
$Assembly = [System.Reflection.Assembly]::LoadWithPartialName("System.Data.OracleClient")
if ( $Assembly ) {
write-output "System.Data.OracleClient Loaded!"
}
else {
write-output "System.Data.OracleClient could not be loaded! Exiting..."
Exit 1
}
#Setup Connection string and open DB connection
#borrowed from http://lvlnrd.com/oracle-database-queries-powershell-script-examples/
### connection string ###
$OracleConnectionString = "SERVER=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=$oracleservername)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=$servicename)));uid=$oracleusername;pwd=$oraclepassword;"
### open up oracle connection to database ###
$OracleConnection = New-Object System.Data.OracleClient.OracleConnection($OracleConnectionString);
$OracleConnection.Open()
#SQL select:
try {
### sql query command ###
# $OracleSQLQuery = "SELECT * FROM HOSTS"
$OracleSQLQuery = "select distinct(roleuser) from ps_roleuser_vw1 where rolename in('ROLENAME1','ROLENAME2','ROLENAME3')"
### create object ###
$SelectCommand = New-Object System.Data.OracleClient.OracleCommand;
$SelectCommand.Connection = $OracleConnection
$SelectCommand.CommandText = $OracleSQLQuery
$SelectCommand.CommandType = [System.Data.CommandType]::Text
### create datatable and load results into datatable ###
$SelectDataTable = New-Object System.Data.DataTable
$SelectDataTable.Load($SelectCommand.ExecuteReader())
}
catch {
echo $_.Exception.GetType().FullName, $_.Exception.Message
write-output "Error while retrieving data!"
}
finally{
$OracleConnection.close()
}
#output of SQL query above: $SelectDataTable
# list everyone in the group currenty, then remove everyone in the group
write-output "Removing current members from group now, this may take a minute or two"
Get-ADGroupMember "$activedirectorygroup" | ForEach-Object {Remove-ADGroupMember "$activedirectorygroup" $_ -Confirm:$false}
write-output "Done removing current members from group"
$memberoutput = ($selectdatatable | select-object)
# add-adgroupmember
foreach ($member in $memberoutput.roleuser) {
try{
Add-ADGroupMember $activeDirectoryGroup -member $member
write-output "Added $member to group $activeDirectoryGroup"
}
Catch {
}
}
Labels:
Active Directory,
oracle,
peoplesoft,
powershell
Thursday, January 8, 2015
Yubikey and Windows, Certifiably a pain no longer
At my new job, we are looking to use Yubikeys to store our personal certificates to log into Windows servers as a method of multifactor authentication. After searching for quite some time, it became apparent to me that there is little to no documentation that is easily read (at least for Windows machines), nor did I find any scripts to help the process of importing a certificate.
Here is my script that not only asks the user where the .PFX file is located, but also configures the management key and PUK based on a random number, asks the user for their preferred pin, and includes a function that allows you to reset the device easily. This script still requires Yubikey's PIV tool, found at https://developers.yubico.com/yubico-piv-tool/ . This script has been tested/used on version 0.1.3.
I am not primarily a programmer, so I apologize for any functionality that could have been done better or faster.
Here is my script that not only asks the user where the .PFX file is located, but also configures the management key and PUK based on a random number, asks the user for their preferred pin, and includes a function that allows you to reset the device easily. This script still requires Yubikey's PIV tool, found at https://developers.yubico.com/yubico-piv-tool/ . This script has been tested/used on version 0.1.3.
I am not primarily a programmer, so I apologize for any functionality that could have been done better or faster.
####################################################################################################
#
# Script to help out some of the management of YubiKeys in windows using powershell
# Bryan Loveless (bryan.loveless@gmail.com)
# January 2015
# http://actualreverend.blogspot.com/
#
# requires Yubikey PIV tool to be downloaded somewhere (0.1.3 was the verison tested)
# requires PFX to be exported in a pfx file, and not already assigned to a smartcard in use
# PFX must be protected with a password
#
# User must not use the pin numbers 12345 or 4711, as these are default and Reset-yubikey will fail
#
###################################################################################################
# Show an Open File Dialog and return the file selected by the user.
#Borrowed from http://blog.danskingdom.com/powershell-multi-line-input-box-dialog-open-file-dialog-folder-browser-dialog-input-box-and-message-box/
function Read-OpenFileDialog([string]$WindowTitle, [string]$InitialDirectory, [string]$Filter = "All files (*.*)|*.*", [switch]$AllowMultiSelect)
{
Add-Type -AssemblyName System.Windows.Forms
$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$openFileDialog.Title = $WindowTitle
if (![string]::IsNullOrWhiteSpace($InitialDirectory)) { $openFileDialog.InitialDirectory = $InitialDirectory }
$openFileDialog.Filter = $Filter
if ($AllowMultiSelect) { $openFileDialog.MultiSelect = $true }
$openFileDialog.ShowHelp = $true # Without this line the ShowDialog() function may hang depending on system configuration and running from console vs. ISE.
$openFileDialog.ShowDialog() > $null
if ($AllowMultiSelect) { return $openFileDialog.Filenames } else { return $openFileDialog.Filename }
}
# Show an Open Folder Dialog and return the directory selected by the user.
#Borrowed from http://blog.danskingdom.com/powershell-multi-line-input-box-dialog-open-file-dialog-folder-browser-dialog-input-box-and-message-box/
function Read-FolderBrowserDialog([string]$Message, [string]$InitialDirectory, [switch]$NoNewFolderButton)
{
$browseForFolderOptions = 0
if ($NoNewFolderButton) { $browseForFolderOptions += 512 }
$app = New-Object -ComObject Shell.Application
$folder = $app.BrowseForFolder(0, $Message, $browseForFolderOptions, $InitialDirectory)
if ($folder) { $selectedDirectory = $folder.Self.Path } else { $selectedDirectory = '' }
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($app) > $null
return $selectedDirectory
}
# reset the device easily
function Reset-Yubikey{
write-host "Are you sure you want to reset the yubikey? Ctrl-C to stop now!"
pause
#prompt user where the yubico-piv executable is:
$yubiPIVExecutable = Read-OpenFileDialog -InitialDirectory 'C:\Program Files (x86)' -WindowTitle "Select yubico-piv-tool.exe file" -filter "yubico-piv-exe files (yubico-piv-tool.exe)|yubico-piv-tool.exe"
# lock the pin an puk numbers on device
& "$yubiPIVExecutable" -a verify-pin -P 4711
& "$yubiPIVExecutable" -a verify-pin -P 4711
& "$yubiPIVExecutable" -a verify-pin -P 4711
& "$yubiPIVExecutable" -a verify-pin -P 4711
& "$yubiPIVExecutable" -a change-puk -P 4711 -N 67567
& "$yubiPIVExecutable" -a change-puk -P 4711 -N 67567
& "$yubiPIVExecutable" -a change-puk -P 4711 -N 67567
& "$yubiPIVExecutable" -a change-puk -P 4711 -N 67567
& "$yubiPIVExecutable" -a verify-pin -P 4711
& "$yubiPIVExecutable" -a verify-pin -P 4711
& "$yubiPIVExecutable" -a verify-pin -P 4711
& "$yubiPIVExecutable" -a verify-pin -P 4711
& "$yubiPIVExecutable" -a change-puk -P 4711 -N 67567
& "$yubiPIVExecutable" -a change-puk -P 4711 -N 67567
& "$yubiPIVExecutable" -a change-puk -P 4711 -N 67567
& "$yubiPIVExecutable" -a change-puk -P 4711 -N 67567
& "$yubiPIVExecutable" -a reset
}
# password generator, using it to get new managment key, borrowed from http://blog.morg.nl/2014/01/generate-a-random-strong-password-in-powershell/
function Generate-Password() {
Param (
[int]$length = 8,
[bool] $includeLowercaseLetters = $true,
[bool] $includeUppercaseLetters = $true,
[bool] $includeNumbers = $true,
[bool] $includeSpecialChars = $false,
[bool] $noSimilarCharacters = $true
)
<#
(c) Morgan de Jonge CC BY SA
Generates a random password. you're able to specify:
- The desired password length (minimum = 4)
- Whether or not to use lowercase characters
- Whether or not to use uppercase characters
- Whether or not to use numbers
- Whether or not to use special characters
- Whether or not to avoid using similar characters ( e.g. i, l, o, 1, 0, I)
#>
# Validate params
if($length -lt 4) {
$exception = New-Object Exception "The minimum password length is 4"
Throw $exception
}
if ($includeLowercaseLetters -eq $false -and
$includeUppercaseLetters -eq $false -and
$includeNumbers -eq $false -and
$includeSpecialChars -eq $false) {
$exception = New-Object Exception "At least one set of included characters must be specified"
Throw $exception
}
#Available characters
$CharsToSkip = [char]"i", [char]"l", [char]"o", [char]"1", [char]"0", [char]"I"
$AvailableCharsForPassword = $null;
$uppercaseChars = $null
for($a = 65; $a -le 90; $a++) { if($noSimilarCharacters -eq $false -or [char][byte]$a -notin $CharsToSkip) {$uppercaseChars += ,[char][byte]$a }}
$lowercaseChars = $null
for($a = 97; $a -le 122; $a++) { if($noSimilarCharacters -eq $false -or [char][byte]$a -notin $CharsToSkip) {$lowercaseChars += ,[char][byte]$a }}
$digitChars = $null
for($a = 48; $a -le 57; $a++) { if($noSimilarCharacters -eq $false -or [char][byte]$a -notin $CharsToSkip) {$digitChars += ,[char][byte]$a }}
$specialChars = $null
$specialChars += [char]"=", [char]"+", [char]"_", [char]"?", [char]"!", [char]"-", [char]"#", [char]"$", [char]"*", [char]"&", [char]"@"
$TemplateLetters = $null
if($includeLowercaseLetters) { $TemplateLetters += "L" }
if($includeUppercaseLetters) { $TemplateLetters += "U" }
if($includeNumbers) { $TemplateLetters += "N" }
if($includeSpecialChars) { $TemplateLetters += "S" }
$PasswordTemplate = @()
# Set password template, to ensure that required chars are included
do {
$PasswordTemplate.Clear()
for($loop = 1; $loop -le $length; $loop++) {
$PasswordTemplate += $TemplateLetters.Substring((Get-Random -Maximum $TemplateLetters.Length),1)
}
}
while ((
(($includeLowercaseLetters -eq $false) -or ($PasswordTemplate -contains "L")) -and
(($includeUppercaseLetters -eq $false) -or ($PasswordTemplate -contains "U")) -and
(($includeNumbers -eq $false) -or ($PasswordTemplate -contains "N")) -and
(($includeSpecialChars -eq $false) -or ($PasswordTemplate -contains "S"))) -eq $false
)
#$PasswordTemplate now contains an array with at least one of each included character type (uppercase, lowercase, number and/or special)
foreach($char in $PasswordTemplate) {
switch ($char) {
L { $Password += $lowercaseChars | Get-Random }
U { $Password += $uppercaseChars | Get-Random }
N { $Password += $digitChars | Get-Random }
S { $Password += $specialChars | Get-Random }
}
}
return $Password
}
#function configure-yubikey{
#change key, pin, and puk from default setting:
#generate a random number and store it in keepass as USER-YUBIKEY-ManagementKEY must be 16 characters (010203040506070801020304050607080102030405060708 is default)
#$randomkey = get-random -minimum 999999999999999 -maximum 9999999999999999
$randomkey = Generate-Password 48 $false $false $true $false $false
write-host "create a keepass entry named yourusername-YUBIKEY-ManagmentKEY and enter $randomkey in for the value" -ForeGroundColor Red
pause
#generate a random number and store it in keepass as USER-YUBIKEY-PUK, must be 8 characters
#$randompuk = get-random -minimum 9999999 -maximum 99999999
$randompuk = Generate-Password 8 $false $false $true $false $false
write-host "create a keepass entry named yourusername-YUBIKEY-PUK and enter $randompuk in for the value" -ForeGroundColor Red
pause
#prompt user with protected pin entering box:
$pinencrypted = read-host -prompt "What do you want your pin number to be?" -AsSecureString
#extract plain text pin number from encrypted varible above
$BSTR = `
[System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pinencrypted)
$pin = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
#prompt user where the yubico-piv executable is:
$yubiPIVExecutable = Read-OpenFileDialog -InitialDirectory 'C:\Program Files (x86)' -WindowTitle "Select yubico-piv-tool.exe file" -filter "yubico-piv-exe files (yubico-piv-tool.exe)|yubico-piv-tool.exe"
#ask user where PFX file is
$pfxfile = Read-OpenFileDialog -InitialDirectory c:\ -WindowTitle "Select PFX file" -filter "PFX files (*.pfx)|*.pfx"
#prompt user for PFX password:
$PFXpasswordencrypted = read-host -prompt "What is your PFX password?" -AsSecureString
#extract plain text pin number from encrypted varible above
$BSTR = `
[System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($PFXpasswordencrypted)
$PFXpassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
#changing pin from default(none) to random number generated above:
write-host "setting the yubikey managment key now"
& "$yubiPIVExecutable" -a set-mgm-key -n $randomkey
#changing PIN from default to user's seleciton
write-host "Changing the PIN number now"
& "$yubiPIVExecutable" -k $randomkey -a change-pin -P 123456 -N $pin
#Changing pin from default to random number generated above:
write-host "Changing the PUK number now"
& "$yubiPIVExecutable" -k $randomkey -a change-puk -P 12345678 -N $randompuk
# imports the key
& "$yubiPIVExecutable" --slot 9a --input=$pfxfile --password=$PFXpassword --key-format=PKCS12 --action=set-chuid --action=import-key --action=import-certificate -v2 --key=$randomkey
#}
Labels:
Active Directory,
certificate,
multifactor authentication,
PFX,
powershell,
Windows,
YubiKey
Friday, May 2, 2014
Be Like the NSA... on your Exchange server...
Long story short, we have a consultant who wants to create a report to figure out:
1) how often our customers (students) contact our dept
2) how long it takes for our employees to contact them back
3) who is responding to whom
4) some general e-mail numbers about productivity.
We tasked a student worker John ( Johnt5818@gmail.com )with figuring out if this was possible using Powershell and an Exchange module (with an active directory plugin too). I helped him stand up the trial VHD of Exchange for him to play with, and he certainly delivered.
He found another person's script that didn't seem to work right, then fixed it and customized it for what we needed. We needed all of the employees in our department to show up in the CSV, and we needed it placed on a remote server. We do not manage the production Exchange environment here, so we tested it to run as a scheduled task to run every day or so.
script:
------------------------
1) how often our customers (students) contact our dept
2) how long it takes for our employees to contact them back
3) who is responding to whom
4) some general e-mail numbers about productivity.
We tasked a student worker John ( Johnt5818@gmail.com )with figuring out if this was possible using Powershell and an Exchange module (with an active directory plugin too). I helped him stand up the trial VHD of Exchange for him to play with, and he certainly delivered.
He found another person's script that didn't seem to work right, then fixed it and customized it for what we needed. We needed all of the employees in our department to show up in the CSV, and we needed it placed on a remote server. We do not manage the production Exchange environment here, so we tested it to run as a scheduled task to run every day or so.
script:
------------------------
<#
|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
| Information added by John Thompson 4/30/2014 |
| Command to add exchange commands: add-pssnapin Microsoft.Exchange.Management.PowerShell.E2010 |
| Script is a modification from here: http://gallery.technet.microsoft.com/scriptcenter/bb94b422-eb9e-4c53-a454-f7da6ddfb5d6/view/Discussions#content |
| |
| When you run this script it will gather statistics on whatever switches below you have set to true. You need to specifiy what active Directory you want |
| the script to run on as well. It will save all the infromation to the save Location that you specify. |
|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
#>
<#
.NOTES (original)
Updated by Justin Beeden 11.25.2013
V1.0
I have updated the original script by parameterizing it and adding support for Exchange 2013, including the option to include 2013 Health mailboxes.
Original script written by Rob Campbell
http://blogs.technet.com/b/heyscriptingguy/archive/2011/03/02/use-powershell-to-track-email-messages-in-exchange-server.aspx
.SYNOPSIS
Checks Message Tracking Logs and pulls message statistics for an Exchange Organization. Works on Exchange 2007 and later.
By default does not include messages for the Exchange 2013 Health Mailboxes, use the Include2013HealthMailboxes to include them in report.
.DESCRIPTION
Checks Message Tracking Logs and pulls message statistics for an Exchange Organization. Works on Exchange 2007 and later.
By default does not include messages for the Exchange 2013 Health Mailboxes, use the Include2013HealthMailboxes to include them in report.
.PARAMETER DaysToGoBack
Specifies the amount of days to go back and search through the Message Tracking Logs.
.PARAMETER IncludeDistListStats
Optional parameter that will add an additional exported csv containing statistcics for Distribution List usage.
.PARAMETER Include2013HealthMailboxes
Optional parameter that will include the Exchange 2013 Health Mailboxes used by Managed Availabilty in the exported csv report.
.EXAMPLE
PS> .\GetMessageStatsToFile.ps1 -DaysToGoBack 30
Searches the past 30 days of Message Tracking Logs. Message Trackign logs are kept for 30 days by default on all transport servers.
.EXAMPLE
PS> .\GetMessageStatsToFile.ps1 -DaysToGoBack 15 -IncludeDistListStats
Searches the past 15 days of Message Tracking Logs. Also exports optional DistListStats csv.
#>
# Variables to change
#--------------------------------------------------------------------------------------------------------------------------------------------
[int]$DaysToGoBack = 365 # used to set how many days you want the script to look back on.
[switch]$IncludeDistListStats = $false # Not tested -Logic: line 304
[switch]$Include2013HealthMailboxes = $false # not tested -Logic: within $numberOfEmailsSent
[switch]$numberOfEmailsSent = $true # tracks the amount of emails sent and how much data with it. -Logic: line 156
[switch]$ToFromEmailStats = $true # Set to [true] to get statistics on who received/sent messages to whom. -Logic: line 337
$activeDirectory = "DLS_DL" #Set to the active directory group that you want to have the stats for. -Logic: line 139
$SaveLocation = "\\SLC-DC01\Email_log" #the network folder you want to have everything saved to.
#--------------------------------------------------------------------------------------------------------------------------------------------
#checks if you have already added the PSsnapin. if you have not it will add it for you.
if ((get-pssnapin -name Microsoft.exchange.management.powershell.e2010 -ErrorAction SilentlyContinue ) -eq $null)
{
add-pssnapin Microsoft.Exchange.Management.PowerShell.E2010
}
IF ($true) #General variables and set up script
{
$rundate = $(Get-Date).toshortdatestring()
$startdate = $((Get-Date).adddays(-$DaysToGoBack)).toshortdatestring()
$outfile_date = (Get-Date -Format "mm_dd_yyy-hh_mm").ToString()
$outfile = $SaveLocation + "\DaysMessageStats_" + $outfile_date + ".csv"
$dloutfile = "DistListStats_" + $outfile_date +".csv"
$ToFromOutFile = $SaveLocation + "\ToFromEmailStats_" + $outfile_date + ".csv"
$accepted_domains = Get-AcceptedDomain |Foreach {$_.domainname.domain}
[regex]$dom_rgx = "`(?i)(?:" + (($accepted_domains |% {"@" + [regex]::escape($_)}) -join "|") + ")$"
$mbx_servers = Get-ExchangeServer |Where {$_.serverrole -match "Mailbox"}|Foreach {$_.fqdn}
[regex]$mbx_rgx = "`(?i)(?:" + (($mbx_servers |Foreach {"@" + [regex]::escape($_)}) -join "|") + ")\>$"
$msgid_rgx = "^\<.+@.+\..+\>$"
$hts = Get-TransportServer -WarningAction SilentlyContinue | Foreach {$_.name}
$exch_addrs = @{}
$msgrec = @{}
$bytesrec = @{}
$msgrec_exch = @{}
$bytesrec_exch = @{}
$msgrec_smtpext = @{}
$bytesrec_smtpext = @{}
$total_msgsent = @{}
$total_bytessent = @{}
$unique_msgsent = @{}
$unique_bytessent = @{}
$total_msgsent_exch = @{}
$total_bytessent_exch = @{}
$unique_msgsent_exch = @{}
$unique_bytessent_exch = @{}
$total_msgsent_smtpext = @{}
$total_bytessent_smtpext = @{}
$unique_msgsent_smtpext=@{}
$unique_bytessent_smtpext = @{}
$dl = @{}
$obj_table = {@"
Date = $rundate
User = $($address.split("@")[0])
Domain = $($address.split("@")[1])
Sent Total = $(0 + $total_msgsent[$address])
Sent MB Total = $("{0:F2}" -f $($total_bytessent[$address]/1mb))
Received Total = $(0 + $msgrec[$address])
Received MB Total = $("{0:F2}" -f $($bytesrec[$address]/1mb))
Sent Internal = $(0 + $total_msgsent_exch[$address])
Sent Internal MB = $("{0:F2}" -f $($total_bytessent_exch[$address]/1mb))
Sent External = $(0 + $total_msgsent_smtpext[$address])
Sent External MB = $("{0:F2}" -f $($total_bytessent_smtpext[$address]/1mb))
Received Internal = $(0 + $msgrec_exch[$address])
Received Internal MB = $("{0:F2}" -f $($bytesrec_exch[$address]/1mb))
Received External = $(0 + $msgrec_smtpext[$address])
Received External MB = $("{0:F2}" -f $($bytesrec_smtpext[$address]/1mb))
Sent Unique Total = $(0 + $unique_msgsent[$address])
Sent Unique MB Total = $("{0:F2}" -f $($unique_bytessent[$address]/1mb))
Sent Internal Unique = $(0 + $unique_msgsent_exch[$address])
Sent Internal Unique MB = $("{0:F2}" -f $($unique_bytessent_exch[$address]/1mb))
Sent External Unique = $(0 + $unique_msgsent_smtpext[$address])
Sent External Unique MB = $("{0:F2}" -f $($unique_bytessent_smtpext[$address]/1mb))
"@
}
$props = $obj_table.ToString().Split("`n")|% {if ($_ -match "(.+)="){$matches[1].trim()}}
$stat_recs = @()
function time_pipeline
{
param ($increment = 1000)
begin{$i=0;$timer = [diagnostics.stopwatch]::startnew()}
process
{
$i++
if (!($i % $increment))
{
Write-host “`rProcessed $i in $($timer.elapsed.totalseconds) seconds” -nonewline
}
$_
}
end
{
write-host “`rProcessed $i log records in $($timer.elapsed.totalseconds) seconds”
Write-Host " Average rate: $([int]($i/$timer.elapsed.totalseconds)) log recs/sec."
}
}
}
#------Gets all the email accounts for the Active Directory that you chose to run the script against.-----
$emailArray = @()
$counter = 0
$accounts = Get-ADGroupMember $activeDirectory
foreach ($account in $accounts)
{
$principalName = get-ADUser $($account.SamAccountName) -Properties mail
$counter ++
$emailArray += $principalName.mail#$accountObject
}
Write-Host "Running script for $counter accounts" -ForegroundColor Green
Write-Host $emailArray -ForegroundColor Green
#-----------------------Mane body of logic--------------------------
If ($numberOfEmailsSent)#
{
foreach ($ht in $hts)
{
Write-Host "`nStarted processing $ht"
get-messagetrackinglog -Server $ht -Start "$startdate" -End "$rundate 11:59:59 PM" -resultsize unlimited | time_pipeline |%{
If ($Include2013HealthMailboxes)
{
if ($_.eventid -eq "DELIVER" -and $_.source -eq "STOREDRIVER")
{
if ($_.messageid -match $mbx_rgx -and $_.sender -match $dom_rgx)
{
$total_msgsent[$_.sender] += $_.recipientcount
$total_bytessent[$_.sender] += ($_.recipientcount * $_.totalbytes)
$total_msgsent_exch[$_.sender] += $_.recipientcount
$total_bytessent_exch[$_.sender] += ($_.totalbytes * $_.recipientcount)
foreach ($rcpt in $_.recipients)
{
$exch_addrs[$rcpt] ++
$msgrec[$rcpt] ++
$bytesrec[$rcpt] += $_.totalbytes
$msgrec_exch[$rcpt] ++
$bytesrec_exch[$rcpt] += $_.totalbytes
}
}
else
{
if ($_.messageid -match $msgid_rgx)
{
foreach ($rcpt in $_.recipients)
{
$msgrec[$rcpt] ++
$bytesrec[$rcpt] += $_.totalbytes
$msgrec_smtpext[$rcpt] ++
$bytesrec_smtpext[$rcpt] += $_.totalbytes
}
}
}
}
}
Else
{
if ($_.eventid -eq "DELIVER" -and $_.source -eq "STOREDRIVER" -and $_.Recipients -notmatch "HealthMailbox")
{
if ($_.messageid -match $mbx_rgx -and $_.sender -match $dom_rgx)
{
$total_msgsent[$_.sender] += $_.recipientcount
$total_bytessent[$_.sender] += ($_.recipientcount * $_.totalbytes)
$total_msgsent_exch[$_.sender] += $_.recipientcount
$total_bytessent_exch[$_.sender] += ($_.totalbytes * $_.recipientcount)
foreach ($rcpt in $_.recipients)
{
$exch_addrs[$rcpt] ++
$msgrec[$rcpt] ++
$bytesrec[$rcpt] += $_.totalbytes
$msgrec_exch[$rcpt] ++
$bytesrec_exch[$rcpt] += $_.totalbytes
}
}
else
{
if ($_.messageid -match $msgid_rgx)
{
foreach ($rcpt in $_.recipients)
{
$msgrec[$rcpt] ++
$bytesrec[$rcpt] += $_.totalbytes
$msgrec_smtpext[$rcpt] ++
$bytesrec_smtpext[$rcpt] += $_.totalbytes
}
}
}
}
}
If ($Include2013HealthMailboxes)
{
if ($_.eventid -eq "RECEIVE" -and $_.source -eq "STOREDRIVER")
{
$exch_addrs[$_.sender] ++
$unique_msgsent[$_.sender] ++
$unique_bytessent[$_.sender] += $_.totalbytes
if ($_.recipients -match $dom_rgx)
{
$unique_msgsent_exch[$_.sender] ++
$unique_bytessent_exch[$_.sender] += $_.totalbytes
}
if ($_.recipients -notmatch $dom_rgx)
{
$ext_count = ($_.recipients -notmatch $dom_rgx).count
$unique_msgsent_smtpext[$_.sender] ++
$unique_bytessent_smtpext[$_.sender] += $_.totalbytes
$total_msgsent[$_.sender] += $ext_count
$total_bytessent[$_.sender] += ($ext_count * $_.totalbytes)
$total_msgsent_smtpext[$_.sender] += $ext_count
$total_bytessent_smtpext[$_.sender] += ($ext_count * $_.totalbytes)
}
}
}
Else
{
if ($_.eventid -eq "RECEIVE" -and $_.source -eq "STOREDRIVER" -and $_.Recipients -notmatch "HealthMailbox")
{
$exch_addrs[$_.sender] ++
$unique_msgsent[$_.sender] ++
$unique_bytessent[$_.sender] += $_.totalbytes
if ($_.recipients -match $dom_rgx)
{
$unique_msgsent_exch[$_.sender] ++
$unique_bytessent_exch[$_.sender] += $_.totalbytes
}
if ($_.recipients -notmatch $dom_rgx)
{
$ext_count = ($_.recipients -notmatch $dom_rgx).count
$unique_msgsent_smtpext[$_.sender] ++
$unique_bytessent_smtpext[$_.sender] += $_.totalbytes
$total_msgsent[$_.sender] += $ext_count
$total_bytessent[$_.sender] += ($ext_count * $_.totalbytes)
$total_msgsent_smtpext[$_.sender] += $ext_count
$total_bytessent_smtpext[$_.sender] += ($ext_count * $_.totalbytes)
}
}
}
if ($_.eventid -eq "expand")
{
$dl[$_.relatedrecipientaddress] ++
}
}
}
foreach ($address in $exch_addrs.keys)
{
$stat_rec = (new-object psobject -property (ConvertFrom-StringData (&$obj_table)))
$stat_recs += $stat_rec | select $props
}
$stat_recs | export-csv $outfile -NoTypeInformation
Write-Host "Email stats file is $outfile" -ForegroundColor Green
}
If ($IncludeDistListStats)
{
If (Test-Path $dloutfile)
{
$DL_stats = Import-Csv $dloutfile
$dl_list = $dl_stats | Foreach {$_.address}
}
else
{
$dl_list = @()
$DL_stats = @()
}
$DL_stats | Foreach{
if ($dl[$_.address])
{
if ([datetime]$_.lastused -le [datetime]$rundate)
{
$_.used = [int]$_.used + [int]$dl[$_.address]
$_.lastused = $rundate
}
}
}
$dl.keys | Foreach {
if ($dl_list -notcontains $_)
{
$new_rec = "" | select Address,Used,Since,LastUsed
$new_rec.address = $_
$new_rec.used = $dl[$_]
$new_rec.Since = $rundate
$new_rec.lastused = $rundate
$dl_stats += @($new_rec)
}
}
$dl_stats | Export-Csv $dloutfile -NoTypeInformation
Write-Host "DL usage stats file is $dloutfile" -ForegroundColor Green
}
#gets emails from who Emails where sent from and to whom. it will also display the subject line.
If ($ToFromEmailStats)
{
$resultsarray = @()
foreach ($server in $hts)
{
foreach ($emailaddress in $emailArray)
{
#grabs the all the information in the MessageTrackingLog with the desired parameters.
#[if you need to limit what is being added into the result file filter it through this command]
get-messagetrackinglog -Server $ht -Start "$startdate" -End "$rundate 11:59:59 PM" -resultsize unlimited | Where-Object {$_.sender -imatch $emailaddress} | time_pipeline |
%{
foreach ($email in $_)
{
$resutlsObject = New-Object PSObject
$resutlsObject | Add-Member -membertype NoteProperty -name "Sender" -value $($email.Sender | Sort-Object)
$resutlsObject | Add-Member -membertype NoteProperty -name "Recipients" -value $($email.Recipients | Sort-Object)
$resutlsObject | Add-Member -membertype NoteProperty -name "Event ID" -value $($email.EventId | Sort-Object)
$resutlsObject | Add-Member -membertype NoteProperty -name "Time Stamp" -value $($email.Timestamp | Sort-Object)
$resutlsObject | Add-Member -membertype NoteProperty -name "Client Host Name" -value $($email.ClientHostname | Sort-Object)
$resutlsObject | Add-Member -membertype NoteProperty -name "Message Subject" -value $($email.MessageSubject | Sort-Object)
$resutlsObject | Add-Member -membertype NoteProperty -name "Total Bytes" -value $($email.totalbytes | Sort-Object)
$resultsarray += $resutlsObject
}
}
}
$resultsarray | Export-Csv $ToFromOutFile -NoTypeInformation
Write-Host "Email stats file is $ToFromOutFile" -ForegroundColor Green
}
}
-----------------------------------
Subscribe to:
Posts (Atom)