Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PSWSMan and document PowerShell remoting to Windows hosts #32

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion examples/demo-container-images-shared/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
#FROM public.ecr.aws/lambda/provided:al2023
## INSTALL POWERSHELL RUNTIME
FROM 978558897928.dkr.ecr.us-east-1.amazonaws.com/powershell-runtime:latest as runtime-files
## Install gss-ntlmssp and related packages for NTLM authentication
# FROM 978558897928.dkr.ecr.us-east-1.amazonaws.com/powershell-remoting-ntlm:latest as remoting-files
## INSTALL AWS SDK
FROM 978558897928.dkr.ecr.us-east-1.amazonaws.com/powershell-modules-aws-tools:latest as module-files

## Build final image
FROM public.ecr.aws/lambda/provided:al2023
## Copy PowerShell runtime files
COPY --from=runtime-files . /
## Copy NTLM auth files
# COPY --from=remoting-files . /
## Copy Module files
COPY --from=module-files . /
## Function files
COPY /function/ /var/task
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
WORKDIR /var/task
ENTRYPOINT [ "/var/runtime/bootstrap" ]
CMD [ "examplehandler.ps1::handler" ]
CMD [ "examplehandler.ps1::handler" ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# syntax=docker/dockerfile:1

# This image is based on the existing powershell-runtime image.
# Set any of the arguments as needed in case you customized the image details.
ARG REGISTRY=<account>.dkr.ecr.<region>.amazonaws.com
ARG RUNTIME_IMAGE=${REGISTRY}/powershell-remoting
ARG RUNTIME_TAG=latest
ARG RUNTIME=${RUNTIME_IMAGE}:${RUNTIME_TAG}

FROM ${RUNTIME} as build

WORKDIR /tmp

# These build dependencies are documented here:
# https://github.com/gssapi/gss-ntlmssp/blob/main/contrib/gssntlmssp.spec.in#L13
RUN dnf install -y \
autoconf automake docbook-style-xsl doxygen findutils krb5-devel \
libtool libxml2 libxslt libunistring-devel m4 pkgconfig openssl-devel

# These build dependencies are also needed but not in the spec file.
# It's easier to iterate on these packages when they are in their own RUN step.
RUN dnf install -y rpm-build gettext-devel libwbclient-devel zlib-devel

# Build gssntlmssp
RUN <<EOF
git clone https://github.com/gssapi/gss-ntlmssp
cd /tmp/gss-ntlmssp
autoreconf -f -i
./configure
make rpms
mkdir -p /tmp/gssntlmssp
mv /tmp/gss-ntlmssp/rpmbuild/RPMS/x86_64/gssntlmssp-[0-9]*.x86_64.rpm /tmp/gssntlmssp/gssntlmssp.rpm
rm -rf /tmp/gss-ntlmssp
EOF

# Start a new build stage since we don't need all the build dependencies and intermediate build output.
FROM ${RUNTIME} as target

# Get the final RPM we built out of the build stage.
COPY --from=build /tmp/gssntlmssp /tmp/

# libwbclient is required to install the RPM, but it does not seem to declare it as a runtime dependency
# so it won't be installed automatically.
RUN dnf install -y libwbclient

# This RPM is quite small but it brings in around 45 other packages.
RUN rpm --install /tmp/gssntlmssp.rpm
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@ RUN rm -Rf $ARTIFACTS_DIR/modules/Private
# Make bootstrap script executable
RUN chmod +x /var/runtime/bootstrap

# https://github.com/jborean93/PSWSMan
# This fixes WSMan/OMI so that PowerShell remoting is possible to Windows hosts.
RUN DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 /opt/powershell/pwsh -C '\
Install-PSResource -Name PSWSMAN -Version "[2.3.1,3.0.0]" -Scope AllUsers -TrustRepository ; \
Install-WSMan -Verbose ; \
Get-WSManVersion | Format-List \
'

## Build final image
FROM public.ecr.aws/lambda/provided:al2023
## Copy PowerShell runtime files
COPY --from=runtime-files . /
COPY --from=runtime-files . /
151 changes: 151 additions & 0 deletions examples/demo-container-images-shared/powershell-runtime/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# About PowerShell remoting

With the help of [`PSWSMAN`](https://github.com/jborean93/PSWSMan) PowerShell remoting into Active Directory domain-joined Windows hosts is possible, using Kerberos authentication. You will need credentials that have permission to connect to the host and session configuration.

## Lambda considerations

### Network access

Since your lambda needs to have access to your domain, it will need network access to perform the authentication and remoting, so it will usually need to be in a VPC and given a security group that allows it connect to those resources.

### Memory

Recommend at least 512 MB, as it seems to use just over 256 even without doing much of anything.

### Timeout

Consider that the authentication and connection process over WinRM/PSRP is kind of slow. On a cold start, it could take ~30 seconds until your code gets to the point where a command is executing against the remote host. On a warm start, that will be less but not instant. Consider starting with a timeout measured in minutes and pare down after you get a feel for how long things are taking. It will be even slower with less than 512 MB of memory.

## Examples

### Credential

You **must** use UPN format (`user@REALM`), and the realm **is case sensitive**.

For the Actice Directory domain `ad.contoso.com` and the user `account`, you must set the user name to `[email protected]`.

For all of the following examples, we'll assume the use of a `PSCredential` object in the variable `$credential`.

Here's an example of retrieving it from AWS Secrets Manager, but it could come from anywhere. It is not recommended to accept Active Directory credentials as direct function input since it is not encrypted and likely to be logged.

This example assumes that the lambda already has IAM permission to read from the secret, and the ARN is set via environment variable `ConnectorSecretARN`.

It assumes that the secret is formatted as JSON with the following fields:

```json
{
"user": "account",
"domain_dns": "ad.contoso.com",
"password": "correct horse battery staple"
}
```

```powershell
Import-Module -Name AWS.Tools.SecretsManager

$connector_secret = Get-SECSecretValue -SecretId $env:ConnectorSecretARN | ConvertFrom-Json
$user = "{0}@{1}" -f $connector_secret.user, $connector_secret.domain_dns.ToUpper()
$pass = ConvertTo-SecureString -String $connector_secret.password -AsPlainText -Force
$credential = [PSCredential]::new($user, $pass)
```

### Simple use

Using `Invoke-Command` implicitly creates a PSSession to run a script in and removes it (closes it) when the script is done executing.

```powershell
Invoke-Command -ComputerName server01.ad.contoso.com -Credential $credential -ScriptBlock {
$env:COMPUTERNAME
& whoami.exe
Get-ChildItem
}
```

### Passing data to a remote session

Variables in the local scope are not available in the remote scope. There are two ways to pass values into the remote command.

#### The `using:` scope modifier

See also: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes#the-using-scope-modifier

The `using:` method is a little more straightforward in many cases where you want to use the same variable names in the local and remote side.

```powershell
$a, $b, $c = @(1, 2, 3)

Invoke-Command -ComputerName server01.ad.contoso.com -Credential $credential -ScriptBlock {
Write-Host $using:a
Write-Host $using:b
Write-Host $using:c
}
```

### Arguments

The arguments method requires declaring a `param()` block in the scriptblock, but that can also be versatile. It allows full parameter declaration like `[Parameter()]` and validation attributes, and it lets you give different names to the variables for use in the remote scriptblock which can help distinguish them.

However there is no way to pass them in by name, so you have to match up your values to your parameters positionally.

```powershell
$a, $b, $c = @(1, 2, 3)

Invoke-Command -ComputerName server01.ad.contoso.com -Credential $credential -ScriptBlock {
param($x, $y, $z)

Write-Host $x
Write-Host $y
Write-Host $z
} -ArgumentList @($a, $b, $c)

# Passing a single object requires wrapping it in a single element list
Invoke-Command -ComputerName server01.ad.contoso.com -Credential $credential -ScriptBlock {
param($x)

Write-Host $x
} -ArgumentList @(,$a)
```

### Reusing a session

Creating the session object separately lets you use it for several commands at different points in the execution. Be sure to close the session when finished otherwise it may stay open on the remote end.

```powershell
$session = New-PSSession -ComputerName server01.ad.contoso.com -Credential $credential

try {
$remote_output = Invoke-Command -Session $session -ScriptBlock { Invoke-CustomFunction }
$converted_data = $remote_output | Convert-DataToSomething
Invoke-Command -Session $session -ScriptBlock { Update-AppData -Data $using:converted_data }
}
finally {
if ($session) {
Remove-PSSession -InputObject $session -ErrorAction SilentlyContinue
}
}
```

### Using JEA and custom session configurations

Using [Just Enough Administration](https://learn.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/overview) you can provide very restricted endpoints and remote sessions, with different local and remote identities. The documentation above has the details on how to set this up on the remote (Windows) side. For the purposes of a connecting lambda, all you need to know is the name of the Session Configuration to connect to.

```powershell
Invoke-Command `
-ComputerName server01.ad.contoso.com `
-Credential $credential `
-SessionConfiguration My.Custom.Configuration `
-ScriptBlock { Get-Command -Module Microsoft.PowerShell.Archive }
```

```powershell
$session = New-PSSession -ComputerName server01.ad.contoso.com -Credential $credential -SessionConfiguration My.Custom.Configuration

try {
Invoke-Command -Session $session -ScriptBlock { Invoke-CustomFunction }
}
finally {
if ($session) {
Remove-PSSession -InputObject $session -ErrorAction SilentlyContinue
}
}
```