Windows – How to programmatically cause a new Windows user’s profile to be created

powershellwindowswindows-service

I'm creating a (local) user for a Windows service to run as. I've got good reasons for not wanting to use NETWORK SERVICE, LOCAL SERVICE, or LOCAL SYSTEM.

I create the user via net user foobar "Abcd123!" /add – this works fine.

At this point, c:\users\foobar does not exist.

If I create the user's home directory, before the user either logs on (or, more pertinently) or the service that the user is for starts up, Windows creates a user-profile next-door called c:\users\foobar-{gibberish/SID/whatever} – this is not a predictable name.

I need the user's home directory to contain things like a .ssh directory, a .gitconfig – tools like that (not limited to those tools) that make assumptions that it'll be a person using them, and so user-configuration goes inside ~/.... Usually, tools from a Unix heritage.

Actual question

So – is there a programmatic (preferably, PowerShell, or out-of-the-box command-line) way to tell Windows to create the user-profile for a local user?

Or, any other workarounds?

Things I've yet to try:

  • An NSSM start/pre hook that copies files from elsewhere into the user-profile directory that hopefully exists at this point by virtue of Windows starting the service, creating the user-profile then handing control to the NSSM wrapper running the hook before startup.
  • Setting the USERPROFILE environment variable for the service to be somewhere other than the actual user-profile directory. This strikes me as dangerously off-piste but also might work fine.

Other context:

  • Windows Server 2016, desktop experience.
    • Can't use Core/Nano.
  • There is no active directory in play. There won't be.
  • These are local users.
  • I'm doing this via Ansible, which is using PowerShell under the hood for Windows things. Specifically the win_user module, with Ansible 2.7.5.
  • I don't want to create a C:\users\default (the equivalent of /etc/skel), because there are a few different service-users and one size won't fit all. This also doesn't affect when the user-profile is created, just what will be in it when it is.
  • I'm using NSSM to manage the services.

Things I've tried

  • starting the service and allowing Windows to create the directory
    • I don't want to do this, because the service requires secrets before starting up, and so if I do this inside my image-baking process I'll then need to clean them up, and also make sure my service doesn't do any work during the baking phase. I want to avoid both of those fiddly bits.

Best Answer

Windows can create a user-profile on-demand, using the CreateProfile API

However, if don't want to create an executable to perform this operation, you can call the API in PowerShell. Others have already done it: example on github.

Relevant part of the code:

$methodName = 'UserEnvCP'
$script:nativeMethods = @();

Register-NativeMethod "userenv.dll" "int CreateProfile([MarshalAs(UnmanagedType.LPWStr)] string pszUserSid,`
  [MarshalAs(UnmanagedType.LPWStr)] string pszUserName,`
  [Out][MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszProfilePath, uint cchProfilePath)";

Add-NativeMethods -typeName $MethodName;

$localUser = New-Object System.Security.Principal.NTAccount("$UserName");
$userSID = $localUser.Translate([System.Security.Principal.SecurityIdentifier]);
$sb = new-object System.Text.StringBuilder(260);
$pathLen = $sb.Capacity;

Write-Verbose "Creating user profile for $Username";
try
{
    [UserEnvCP]::CreateProfile($userSID.Value, $Username, $sb, $pathLen) | Out-Null;
}
catch
{
    Write-Error $_.Exception.Message;
    break;
}