Free Script: Fixing Hyper-V Folder Security

My standing recommendation for NTFS security when it comes to Hyper-V is: leave it alone. Unfortunately, most people don’t learn why until they’ve already broken something. It’s not fun to fix. I’ve produced a script for fixing Hyper-V folder security that should make things much easier for you.

Fixing Hyper-V Folder Security – The Script

Here’s the script in all its glory. The straight-through run of it just needs a Path. It will add the necessary privileges for “NT VIRTUAL MACHINE\Virtual Machine” to that path. If you specify -FullReset, then it removes inheritance and sets up a default set of permissions. You can read what they are in the help text. I had toyed around with resetting ownership as well, but doing so in PowerShell is just plain painful. If you want to modify this script to allow it, grab Lee Holmes’ instructions.

The script kind of has WhatIf support. It will only really work if -FullReset is specified. That’s because I didn’t want it over-confirming if you’re using FullReset and I couldn’t find a reliable way to trap if -WhatIf was specified without always triggering the prompt or writing my own WhatIf handler.

If you use -FullReset and it finds anything it thinks is a virtual machine file, it will make you double-confirm that you want to perform the reset. You should really only be using FullReset on the parent folder, not “Virtual Hard Disks” folders or anything like that. If you wind up ripping off the permissions of the VM to its VHD files, well, I warned you. Twice. The script should only operate on the folder and not its files, but, well, scripting NTFS permissions sometimes leads to unexpected results.

All you have to do is copy and paste the contents of this box into your own .ps1 file. I’ve written it with the expectation that you’ll use the name Reset-VMFolderSecurity.

<#
.SYNOPSIS
    Corrects broken security on a folder that contains Hyper-V virtual machines.

.DESCRIPTION
    The NT VIRTUAL MACHINES\Virtual Machine account is much easier to remove from a folder than it is to replace. This script returns that account's permissions.
    If the FullReset parameter is not used, the Virtual Machines account is given its expected rights and nothing else is modified. 

.PARAMETER Path
    The root path of the location that contains VMs. Avoid selecting a subfolder, such as "Virtual Hard Disks"

.PARAMETER FullReset
    Removes existing permissions (owner is not changed) and inheritance and replaces with a known working set:

    CREATOR OWNER is given Full Control on subfolder and files.
    Virtual Machines account is given its standard permissions.
    SYSTEM is given Full Control.
    The local Administrators group is given Full Control.
    The local Users group is given Read & Execute.
    The local Users group is given Create Files/Write Data and Create Folders/Append Data on the folder and subfolders.

.PARAMETER Force
    Use to bypass confirmation messages.

.EXAMPLE
    Add "Virtual Machines" to the ACL for C:\LocalVMs.
    C:\PS> Reset-VMFolderSecurity C:\LocalVMs

.EXAMPLE
    Completely resets the security for Hyper-V on C:\LocalVMs
    C:\PS> Reset-VMFolderSecurity C:\LocalVMs -FullReset

.NOTES
    Author: Eric Siron
    Copyright: (C) 2014 Altaro Software
    Version 1.0
    Authored Date: August 29, 2014

.LINK
    https://www.altaro.com/hyper-v/free-script-fixing-hyper-v-folder-security
#>

#requires -Version 3

[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="High")]
param(
        [Parameter(Position=0, HelpMessage="Specify the path to reset security on", ValueFromPipelineByPropertyName=$true, Mandatory=$true)]
        [String]$Path,

        [Parameter()][Switch]$FullReset,

        [Parameter()][Switch]$Force
)

BEGIN {
    $VirtualMachinesACE = "(A;;0x12008f;;;S-1-5-83-0)(A;CIIO;DCLCGR;;;S-1-5-83-0)"
    $FullResetACEs = "(A;OICIIO;FA;;;CO)(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;0x1200af;;;BU)"
}

PROCESS {
    if(!(Test-Path -Path $Path))
    {
        Write-Error -Message "$Path does not exist."
        return
    }

    $PathACL = Get-Acl -Path $Path

    if($FullReset)
    {
        if(-not $Force)
        {
            if($PSCmdlet.ShouldProcess($Path, "Full permissions reset"))
            {
                if((Get-ChildItem -Path $Path -File | where { $_.Name -match "(.)bin|vsv|xml|a?vhdx?|slp$" }).Count)
                {
                    if(-not $PSCmdlet.ShouldContinue("$Path, which contains at least one possible Hyper-V related file. It is recommended that you only reset permissions on the parent container.", "Full permissions reset"))
                    {
                        return
                    }
                }
            }
            else { return }
        }
        $SDDLPreamble = $PathACL.Sddl -replace "\(.*", ""
        $SDDL = $SDDLPreamble + $VirtualMachinesACE + $FullResetACEs
    }
    else
    {
        $SDDL = $PathACL.Sddl + $VirtualMachinesACE
    }
    $PathACL.SetSecurityDescriptorSddlForm($SDDL)
    if($FullReset)
    {
        $PathAcl.SetAccessRuleProtection($true,$false)
    }
    Set-Acl -Path $Path -AclObject $PathAcl
}

END {}

 

The Discovery Process

The impetus to create this script is not entirely altruistic. Most of my lab work is done without backups. I’m probably not alone in the general attitude that if it was really important, it wouldn’t be in the lab. But then, I suffer some kind of self-inflicted data loss and wish I’d had backups. So, I went about setting up Altaro Hyper-V Backup on all my lab nodes and let it go. I had a few problems, mostly because of situations I created. I sent in an e-mail to Altaro support, and, as always, the initial issues were quickly resolved. But then, I noticed that I was getting some VSS errors. I probably could have dug through the logs, but Altaro support already had them and I’m kind of lazy, so I asked if they could tell me what was wrong.

I get an e-mail back telling me that I’m getting access denied errors and asking if I would check permissions. So, I thought, “I know better than to tinker with the permissions on my VM folders. How could security possibly be wrong?” But, Altaro support is rarely wrong, and checking is pretty easy even for a lazy person like me, so I did. And, sure enough, the “NT VIRTUAL MACHINE\Virtual Machine” account was not present on my virtual machines folder. I’m still not sure how that happened, but in case you’re lucky enough to be one of those people who can learn by reading, don’t ever remove the Virtual Machines account from a security list.

Setting it back is kind of a pain. For a long time, I’ve just been referring people to a blog I found that shows how to do it. The problem I have with that blog is that it sets security on the root of a drive, so I have to always couch my recommendation with a warning against doing that. It’s horrible practice and Windows really doesn’t like it. Furthermore, his instructions grant Full Control to the account. There’s nothing blatantly wrong about that, but it’s not necessary and it’s not what Hyper-V does when it builds out folder ACLs. But, as long as I personally don’t know how to do something, I have to refer everyone to someone that does. So, my goal became to figure out the process on my own and do it the right way.

The first thing I thought was that I could take some screenshots of a properly set folder and tell everyone running a GUI to duplicate those, then find some PowerShell way to set them as well. I learned pretty quickly that the GUI method would be incomplete and the PowerShell method is excessively difficult. So, the first thing to be aware of is that if you use the GUI screen to transcribe properties, it might work, but it won’t be complete.

The issue with PowerShell isn’t directly PowerShell’s fault. It relies on the .Net Framework, and as it just so happens, the .Net Framework’s file and folder security commands, at least up through v4.5, just aren’t quite complete.

What I did first was retrieve the ACL for a working folder, then narrow down its access entries to just the Virtual Machines object. I did that like so:

(Get-Acl C:\LocalVMs).Access | ? { $_.IdentityReference -like "*virtual machines*" }

That produced the following:

FileSystemRights  : CreateFiles, AppendData, Read, Synchronize
AccessControlType : Allow
IdentityReference : NT VIRTUAL MACHINE\Virtual Machines
IsInherited       : False
InheritanceFlags  : None
PropagationFlags  : None

FileSystemRights  : -2147483642
AccessControlType : Allow
IdentityReference : NT VIRTUAL MACHINE\Virtual Machines
IsInherited       : False
InheritanceFlags  : ContainerInherit
PropagationFlags  : InheritOnly

The same folder has two separate access rules for the Virtual Machines account. The first is legible. It can be understood and manipulated. As for the second… well… What the heck is “-2147483642”?

To answer that, we need to look at something that will seem scary at first:

PS C:\Windows\system32> (Get-Acl C:\LocalVMs).Sddl
O:SYG:SYD:AI(A;OICIIO;GA;;;CO)(A;;0x12008f;;;S-1-5-83-0)(A;CIIO;DCLCGR;;;S-1-5-83-0)(A;OICIID;FA;;;SY)(A;OICIID;FA;;;BA)(A;OICIID;0x1200a9;;;BU)(A;CIID;LC;;;BU)(A;CIID;DC;;;BU)(A;OICIIOID;GA;;;CO)

This mass of seemingly incomprehensible jumble is called Security Descriptor Definition Language. It’s actually not so bad if you have an SDDL lexicon. To discover what matters, we need to find the items here that represent the “Virtual Machines” account. I’ve seen it enough in the past to recognize its SID: S-1-5-83-0. There are two items here that contain that SID:

  • (A;;0x12008f;;;S-1-5-83-0)
  • (A;CIIO;DCLCGR;;;S-1-5-83-0)

The first one really is unintelligible to the human eye, but the second one isn’t. We’ll start with the first one though. We want to match them both to the more readable entries from the Get-Acl results. They’re actually in the same order, so that’s our first clue. But, from the lexicon, we know that the second grouping (after the first semicolon), references the inheritance model. The first SDDL entry has nothing between the first and second semicolons, which means it has no inheritance, which aligns it with the first object from the Get-Acl pull. We can match the CI and IO of the second item to the Container Inherit and InheritOnly of the second item from Get-Acl. So we know that “0x12008f” means “CreateFiles, AppendData, Read, Synchronize”. That’s easy enough. If I wanted to apply that to a folder, I would do this:

$TargetFolder = "C:\LocalVMs"
$UserAccount = "NT VIRTUAL MACHINE\Virtual Machines"
$ACLAccessRights = `
	[System.Security.AccessControl.FileSystemRights]::CreateFiles -bor
	[System.Security.AccessControl.FileSystemRights]::AppendData -bor
	[System.Security.AccessControl.FileSystemRights]::Read -bor
	[System.Security.AccessControl.FileSystemRights]::Synchronize
$ACLInheritanceFlags = [System.Security.AccessControl.InheritanceFlags]::None
$ACLPropagationFlags = [System.Security.AccessControl.PropagationFlags]::None
$AccessControlType = [System.Security.AccessControl.AccessControlType]::Allow

$AccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($UserAccount, $ACLAccessRights, $ACLInheritanceFlags, $ACLPropagationFlags, $AccessControlType)

$Acl = Get-Acl $TargetFolder
$Acl.AddAccessRule($AccessRule)

Set-Acl -Path $TargetFolder -AclObject $Acl

If I do that, the folder will get the same permissions that you find when you look at the security properties of a properly configured folder in Windows Explorer. But, if I run that Get-Acl cmdlet against it, I’m not going to get the strange entry with “-2147483642”.

To solve that, I naturally began with the lexicon. Using that, we find that “(A;CIIO;DCLCGR;;;S-1-5-83-0)” parses out to:

Access Allowed;
Container Inherit, Inherit Only;
Delete Child Objects, List Contents, Generic Read

Next, I looked at the options I had available in System.Security.AccessControl.FileSystemRights:

FileSystemRights OptionsThey’re not all visible in that screenshot, but it doesn’t matter. I tried various combinations from that list that I thought might work, but none did. I actually wasn’t surprised, because if .Net were able to parse that number as a FileSystemRights object, then it wouldn’t show us that number.

My next step was to figure out what the real number was. Obviously, a negative number doesn’t work as a group of flags. In case you’re not familiar with how flags work, here’s a visual representation:

Mailboxes, source http://www.uspostmailbox.com/photo_franklin.gif

Mailboxes, source http://www.uspostmailbox.com/photo_franklin.gif

In the picture, we have six mailboxes. All these boxes co-exist in the same structure. Each has its own flag. If it’s up, the postal worker knows that the box contains outbound mail. At a glance, it’s easy to tell which, if any, of the six need attention. Windows uses a similar scheme for flagging various true/false properties on items. Rather than use up a lot of boolean variables which really require more than a single bit to store, it can use a more efficient variable, like an integer, to hold many at once. So, for a six mailbox stretch like this, Windows could use an 8-bit variable to represent them all. It would store them like this:

  • Bit 1 is box 770 – binary value 1
  • Bit 2 is box 768 – binary value 10
  • Bit 3 is box 766 – binary value 100
  • Bit 4 is box 764 – binary value 1000
  • Bit 5 is box 762 – binary value 10000
  • Bit 6 is box 760 – binary value 100000
  • Bit 7 is unused  – binary value 1000000
  • Bit 8 is unused  – binary value 10000000

In the picture, all the flags are up, so that would be represented by 00111111. Converted to decimal, that would be 63; hex would be 3f. If it wants to know if a particular flag is up, it uses a boolean AND against that column. For example, it wants to check whether box 764 has a message waiting. Box 764 is bit 4, so the mask to use puts a 1 in bit 4 and a zero everywhere else: 00001000. It performs a binary AND against the condition of the entire variable, which in this case is 00111111. It then checks the result to see if bit 4 is set. If it is, then it knows that box 764 has something waiting.

Condition|0|0|1|1|1|1|1|1|
Check    |0|0|0|0|1|0|0|0|
--------------------------
Result   |0|0|0|0|1|0|0|0|

If bit 4 had held a 0, the result would have been all 0s, or false, and it would have known there was no message waiting.

To go the other way, that is, to take all the values of the flags and put them together, we use a boolean OR. So, if we need to set that box 768 and 762 have messages waiting, the formula is 10 OR 10000 which is 10010.

So now that we know how flags work, we can decipher the number that represents the access rights. Let’s look at the one we already know, which showed up as 0x12008f. In binary, that’s 100100000000010001111. So, if we knew all the flags, we could visually determine what was set and what wasn’t. We’ve already figured that one out though, so let’s move to the “-2147483642” item. As I said before, that’s not valid because it’s negative. No amount of ORing 0s and 1s will ever result in a negative number. What probably happened here is that a really big unsigned number found its way into a signed number’s slot. Just looking at the contents of memory, you can’t tell if a number is signed or not. That’s decided by how the variable that owns that memory is defined. If it’s defined as a signed number, then when the leftmost bit is set, it’s negative. When it’s empty, the number is positive. If it’s an unsigned number, then that leftmost bit is part of the number. If it’s set, it’s a big number. Anyway, whatever is going on here, that number is not usable.

Knowing that, I set off to find out what the actual number is. For that, I need to find out what numbers represent the letters that we saw in the SDDL (DCLCGR, if you forgot). For that, I found myself on this page. I downloaded the linked SDDL parser and fed it both sets:

C:\Source\Software>sddlparse.exe O:SYG:SYD:AI(A;;0x12008f;;;S-1-5-83-0)(A;CIIO;DCLCGR;;;S-1-5-83-0)
SDDL: O:SYG:SYD:AI(A;;0x12008f;;;S-1-5-83-0)(A;CIIO;DCLCGR;;;S-1-5-83-0)
Ace count: 2
**** ACE 1 of 2 ****
ACE Type: ACCESS_ALLOWED_ACE_TYPE
Trustee: NT VIRTUAL MACHINE\Virtual Machines
AccessMask:
  ADS_RIGHT_READ_CONTROL
  ADS_RIGHT_DS_CREATE_CHILD
  ADS_RIGHT_DS_DELETE_CHILD
  ADS_RIGHT_ACTRL_DS_LIST
  ADS_RIGHT_DS_SELF
  ADS_RIGHT_DS_LIST_OBJECT
Inheritance flags: 0
**** ACE 2 of 2 ****
ACE Type: ACCESS_ALLOWED_ACE_TYPE
Trustee: NT VIRTUAL MACHINE\Virtual Machines
AccessMask:
  ADS_RIGHT_GENERIC_READ
  ADS_RIGHT_DS_DELETE_CHILD
  ADS_RIGHT_ACTRL_DS_LIST
Inheritance flags: 10

ACE 1 is the one we figured out. ACE 2 is the mystery set. As you can see, they only have “ADS_RIGHT_SD_DELETE_CHILD” in common. That by itself doesn’t tell us what the numbers should be, though. To the Internet! This time, we landed in the Active Directory C++ API documentation. With this, we can get the values of the flags:

  • ADS_RIGHT_GENERIC_READ is 0x80000000
  • ADS_RIGHT_DS_DELETE_CHILD is 0x2
  • ADS_RIGHT_ACTRL_DS_LIST is 0x4

If you weren’t aware, the “0x” means that the number is given in hexadecimal. I guess its obvious which one put us over the edge. Converted to binary, 0x80000000 is 10000000000000000000000000000000. Our flag calculation is 0x80000000 OR 0x2 OR 0x4 = 0x80000006. In binary, that’s 10000000000000000000000000000110. So, mostly for academic purposes, I assigned that number to the $ACLAccessRights variable in the small script above and tried to execute, and as expected, the system threw it out as not being a valid value for System.Security.AccessControl.FileSystemRights.

Well, now what? I tried stepping through all the other types I found under the AccessControl .Net class. Most of them just won’t let a programmer create an object. I worked my way up to the root AuthorizationRule, and I was told that there weren’t any constructors; it seems that .Net can make one of those objects but I can’t. Out of the listed rule object types, programmers can make FileSystemRights and RegistrySystemRights, and that’s it. For anything else, such as “2147483654” (the actual decimal representation of the number we want), your only options are SDDL or going deep with API calls. I have the capability of doing C++ API work, but I’m just not going to do it unless I really have to. So, SDDL it is.

As we saw before, we have the ability to set the first part of this using nice, clean, legible PowerShell. But, we do know the entire SDDL that we need. So, since we can’t have legibility across the board, we’ll just go with the super short SDDL and do it all quickly:

$TargetDir = "C:\TestVMs"

# retrieves the ACL that's already on the folder
$TargetDirACL = Get-ACL -Path $TargetDir

# SDDL is just a string, and PowerShell can "add" strings, which really just glues them end-to-end. we take the existing SDDL and append the two entries that we want
$TargetDirACL.SetSecurityDescriptorSddlForm($TargetDirACL.Sddl + '(A;;0x12008f;;;S-1-5-83-0)(A;CIIO;DCLCGR;;;S-1-5-83-0)')

# the ACL looks the way we want it to, so now we apply it to the folder
Set-Acl -Path $TargetDir -AclObject $TargetDirACL

That’s really all there is to it. It’s not easy to read, but once you understand the components, it does make sense in its own way. It would be nice if the .Net framework, and by extension PowerShell, caught up to the rest of it, because setting file and folder permissions is definitely in line with what a sysop does every day but what I just showed you most definitely should not be. It’s because of frustrations from complications like this that you find “Everyone/Full Control” being set on critical file shares. It’s also the only thing I still dread doing when I connect to Hyper-V Server or Windows Server in Core mode.

 

 

Altaro Hyper-V Backup
Share this post

Not a DOJO Member yet?

Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!

11 thoughts on "Free Script: Fixing Hyper-V Folder Security"

  • Juri says:

    Eric, great post for mass fixing wrong ACLs on VHD(X)!
    My usual fix for a single disk is a simple 2 step process:

    Make sure you have full NTFS access with the current user on the VHD you are just about to fix

    1. Have the VHD mounted to a VM and create a snapshot and delete it right after its creation

    Thats brings back the GUID Read/Write on the ACL of the VHD

    2. Now open an administrative Power Shell and run icacls X:filename.vhdx /grant “*S-1-5-83-0:(R)”

    That brings back the “Virtual Machines” read access on the VHD

    For a lab environment you can just set “Everyone” Full Access on the NTFS tab of the disk, which renders the 2 steps above needless. Definitely not recommended for production VHDs though 🙂

    Cheers
    Juri

    • Eric Siron says:

      Hi Juri,
      Thanks for the information!
      To be clear, my script is meant for the folder that holds all the VMs, not for the VHDX files. The virtual hard disk files should also have an ACE for the VM that connects to them. I didn’t write it to check for that.

  • Juri says:

    Eric, great post for mass fixing wrong ACLs on VHD(X)!
    My usual fix for a single disk is a simple 2 step process:

    Make sure you have full NTFS access with the current user on the VHD you are just about to fix

    1. Have the VHD mounted to a VM and create a snapshot and delete it right after its creation

    Thats brings back the GUID Read/Write on the ACL of the VHD

    2. Now open an administrative Power Shell and run icacls X:filename.vhdx /grant “*S-1-5-83-0:(R)”

    That brings back the “Virtual Machines” read access on the VHD

    For a lab environment you can just set “Everyone” Full Access on the NTFS tab of the disk, which renders the 2 steps above needless. Definitely not recommended for production VHDs though 🙂

    Cheers
    Juri

  • Attila says:

    Thanks for the solution Eric!
    It fixed well the folder, which somehow lost the correct ACL entries during the import of a whole virtual environment.

  • Attila says:

    Thanks for the solution Eric!
    It fixed well the folder, which somehow lost the correct ACL entries during the import of a whole virtual environment.

  • Eleftherios says:

    Hi Eric,

    I tried to run the script but I am the getting the below error.
    Set-Acl : The security identifier is not allowed to be the owner of this object.
    The virtual machines group is deleted from C:ClusterStorageVolume1. This is storage drive. Under the volume1 are all the vhds which are having the virtual machines group in the security tab. It is only missing in C:ClusterStorageVolume1.

    Please let me know how we can fix the issue.

    Regards,
    Lefteris

    • Eric Siron says:

      That folder isn’t going to take the permissions, but I’ve never run into a condition where it was needed so I’m not sure what to tell you. I think I’d put the VHDs into a “Virtual Hard Disks” subfolder like the default, and apply permissions to that.

  • Eleftherios says:

    Hi Eric,

    I tried to run the script but I am the getting the below error.
    Set-Acl : The security identifier is not allowed to be the owner of this object.
    The virtual machines group is deleted from C:ClusterStorageVolume1. This is storage drive. Under the volume1 are all the vhds which are having the virtual machines group in the security tab. It is only missing in C:ClusterStorageVolume1.

    Please let me know how we can fix the issue.

    Regards,
    Lefteris

Leave a comment or ask a question

Your email address will not be published. Required fields are marked *

Your email address will not be published.

Notify me of follow-up replies via email

Yes, I would like to receive new blog posts by email

What is the color of grass?

Please note: If you’re not already a member on the Dojo Forums you will create a new account and receive an activation email.