diff --git a/pkg/common/entrypoint/entrypoint_windows.go b/pkg/common/entrypoint/entrypoint_windows.go index a7694c1ce3..eb9d3697cb 100644 --- a/pkg/common/entrypoint/entrypoint_windows.go +++ b/pkg/common/entrypoint/entrypoint_windows.go @@ -5,8 +5,13 @@ package entrypoint import ( "context" + "errors" "fmt" "os" + "strings" + "unsafe" + + "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc" ) @@ -20,7 +25,10 @@ type systemCall struct { } func (s *systemCall) IsWindowsService() (bool, error) { - return svc.IsWindowsService() + // We are using a custom function because the svc.IsWindowsService() one still has an open issue in which it states + // that it is not working properly in Windows containers: https://github.com/golang/go/issues/56335. Soon as we have + // a fix for that, we can use the original function. + return isWindowsService() } func (s *systemCall) Run(name string, handler svc.Handler) error { @@ -51,11 +59,11 @@ func (e *EntryPoint) Main() int { // Determining if SPIRE is running as a Windows service is done // with a best-effort approach. If there is an error, just fallback // to the behavior of not running as a Windows service. - isWindowsService, err := e.sc.IsWindowsService() + isWindowsSvc, err := e.sc.IsWindowsService() if err != nil { fmt.Fprintf(os.Stderr, "Could not determine if running as a Windows service: %v", err) } - if isWindowsService { + if isWindowsSvc { errChan := make(chan error) go func() { // Since the service runs in its own process, the service name is ignored. @@ -71,3 +79,38 @@ func (e *EntryPoint) Main() int { return e.runCmdFn(context.Background(), os.Args[1:]) } + +// isWindowsService is a copy of the svc.IsWindowsService() function, but without the parentProcess.SessionID == 0 check +// that is causing the issue in Windows containers, this logic is exactly the same from .NET runtime (>= 6.0.10). +func isWindowsService() (bool, error) { + // The below technique looks a bit hairy, but it's actually + // exactly what the .NET runtime (>= 6.0.10) does for the similarly named function: + // https://github.com/dotnet/runtime/blob/36bf84fc4a89209f4fdbc1fc201e81afd8be49b0/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceHelpers.cs#L20-L33 + // Specifically, it looks up whether the parent process is called "services". + + var currentProcess windows.PROCESS_BASIC_INFORMATION + infoSize := uint32(unsafe.Sizeof(currentProcess)) + err := windows.NtQueryInformationProcess(windows.CurrentProcess(), windows.ProcessBasicInformation, unsafe.Pointer(¤tProcess), infoSize, &infoSize) + if err != nil { + return false, err + } + var parentProcess *windows.SYSTEM_PROCESS_INFORMATION + for infoSize = uint32((unsafe.Sizeof(*parentProcess) + unsafe.Sizeof(uintptr(0))) * 1024); ; { + parentProcess = (*windows.SYSTEM_PROCESS_INFORMATION)(unsafe.Pointer(&make([]byte, infoSize)[0])) + err = windows.NtQuerySystemInformation(windows.SystemProcessInformation, unsafe.Pointer(parentProcess), infoSize, &infoSize) + if err == nil { + break + } else if !errors.Is(err, windows.STATUS_INFO_LENGTH_MISMATCH) { + return false, err + } + } + for ; ; parentProcess = (*windows.SYSTEM_PROCESS_INFORMATION)(unsafe.Pointer(uintptr(unsafe.Pointer(parentProcess)) + uintptr(parentProcess.NextEntryOffset))) { + if parentProcess.UniqueProcessID == currentProcess.InheritedFromUniqueProcessId { + return strings.EqualFold("services.exe", parentProcess.ImageName.String()), nil + } + if parentProcess.NextEntryOffset == 0 { + break + } + } + return false, nil +} diff --git a/test/integration/README.md b/test/integration/README.md index db59f00c3f..34bc1bee91 100644 --- a/test/integration/README.md +++ b/test/integration/README.md @@ -89,3 +89,4 @@ The following environment variables are available to the teardown script: * [Self Test](suites/self-test/README.md) * [SPIRE Server CLI](suites/spire-server-cli/README.md) * [Upgrade](suites/upgrade/README.md) +* [Windows Service](suites-windows/windows-service/README.md) diff --git a/test/integration/suites-windows/windows-service/00-setup.sh b/test/integration/suites-windows/windows-service/00-setup.sh new file mode 100644 index 0000000000..49c69db23c --- /dev/null +++ b/test/integration/suites-windows/windows-service/00-setup.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +"${ROOTDIR}/setup/x509pop/setup.sh" conf/server conf/agent diff --git a/test/integration/suites-windows/windows-service/01-start-server-service b/test/integration/suites-windows/windows-service/01-start-server-service new file mode 100644 index 0000000000..a00e0458d3 --- /dev/null +++ b/test/integration/suites-windows/windows-service/01-start-server-service @@ -0,0 +1,8 @@ +#!/bin/bash +source ./common + +docker-up spire-server + +create-service spire-server C:/spire/bin/spire-server.exe +start-service spire-server run -config C:/spire/conf/server/server.conf +assert-service-status spire-server RUNNING diff --git a/test/integration/suites-windows/windows-service/02-bootstrap-agent b/test/integration/suites-windows/windows-service/02-bootstrap-agent new file mode 100644 index 0000000000..3ff7ec941b --- /dev/null +++ b/test/integration/suites-windows/windows-service/02-bootstrap-agent @@ -0,0 +1,5 @@ +#!/bin/bash + +log-debug "bootstrapping agent..." +docker-compose exec -T spire-server \ + c:/spire/bin/spire-server bundle show > conf/agent/bootstrap.crt || fail-now "failed to bootstrap agent" diff --git a/test/integration/suites-windows/windows-service/03-start-agent-service b/test/integration/suites-windows/windows-service/03-start-agent-service new file mode 100644 index 0000000000..d024e8aad5 --- /dev/null +++ b/test/integration/suites-windows/windows-service/03-start-agent-service @@ -0,0 +1,8 @@ +#!/bin/bash +source ./common + +docker-up spire-agent + +create-service spire-agent C:/spire/bin/spire-agent.exe +start-service spire-agent run -config C:/spire/conf/agent/agent.conf +assert-service-status spire-agent RUNNING diff --git a/test/integration/suites-windows/windows-service/04-create-registration-entries b/test/integration/suites-windows/windows-service/04-create-registration-entries new file mode 100644 index 0000000000..890c9385f2 --- /dev/null +++ b/test/integration/suites-windows/windows-service/04-create-registration-entries @@ -0,0 +1,12 @@ +#!/bin/bash +source ./common + +log-debug "creating regular registration entry..." +docker-compose exec -T spire-server \ + c:/spire/bin/spire-server entry create \ + -parentID "spiffe://domain.test/spire/agent/x509pop/$(fingerprint conf/agent/agent.crt.pem)" \ + -spiffeID "spiffe://domain.test/workload" \ + -selector "windows:user_name:User Manager\ContainerUser" \ + -ttl 0 + +assert-synced-entry "spiffe://domain.test/workload" diff --git a/test/integration/suites-windows/windows-service/05-test-fetch-svid b/test/integration/suites-windows/windows-service/05-test-fetch-svid new file mode 100644 index 0000000000..83168a2b63 --- /dev/null +++ b/test/integration/suites-windows/windows-service/05-test-fetch-svid @@ -0,0 +1,9 @@ +#!/bin/bash + +log-debug "test fetch x509 SVID..." +docker-compose exec -T -u ContainerUser spire-agent \ + c:/spire/bin/spire-agent api fetch x509 || fail-now "failed to fetch x509" + +log-debug "test fetch JWT SVID..." +docker-compose exec -T -u ContainerUser spire-agent \ + c:/spire/bin/spire-agent api fetch jwt -audience mydb || fail-now "failed to fetch JWT" diff --git a/test/integration/suites-windows/windows-service/06-test-graceful-shutdown b/test/integration/suites-windows/windows-service/06-test-graceful-shutdown new file mode 100644 index 0000000000..934c052a8a --- /dev/null +++ b/test/integration/suites-windows/windows-service/06-test-graceful-shutdown @@ -0,0 +1,10 @@ +#!/bin/bash +source ./common + +stop-service spire-agent +assert-service-status spire-agent STOPPED +assert-graceful-shutdown agent + +stop-service spire-server +assert-service-status spire-server STOPPED +assert-graceful-shutdown server diff --git a/test/integration/suites-windows/windows-service/07-test-service-failing-to-start b/test/integration/suites-windows/windows-service/07-test-service-failing-to-start new file mode 100644 index 0000000000..91516615d9 --- /dev/null +++ b/test/integration/suites-windows/windows-service/07-test-service-failing-to-start @@ -0,0 +1,8 @@ +#!/bin/bash +source ./common + +start-service spire-server run -config invalid-config-path +assert-service-status spire-server STOPPED + +start-service spire-agent run -config invalid-config-path +assert-service-status spire-agent STOPPED diff --git a/test/integration/suites-windows/windows-service/README.md b/test/integration/suites-windows/windows-service/README.md new file mode 100644 index 0000000000..0a001f6f79 --- /dev/null +++ b/test/integration/suites-windows/windows-service/README.md @@ -0,0 +1,19 @@ +# SPIRE Server CLI Suite + +## Description + +This suite validates that we can run both spire agent and spire server natively on Windows OS, asserting that spire components +can run as a [windows service application](https://learn.microsoft.com/en-us/dotnet/framework/windows-services/introduction-to-windows-service-applications#service-applications-vs-other-visual-studio-applications), +and perform [service state transitions](https://learn.microsoft.com/en-us/windows/win32/services/service-status-transitions). + +The suite steps are structured as follows: + +1. Spire server and agent are installed as Windows services. +2. Spire server and agent services starts, their respective status is asserted as **_RUNNING_**, and the node attestation +is performed with x509pop. +3. Workload registration entries are created. +4. The feature of fetching SVIDs (x509 and JWT) is asserted with the running spire agent service. +5. Spire server and agent services are stopped, their respective status is asserted as **_STOPPED_**, and graceful +shutdown is verified via application logs. +6. Spire server and agent services are started again, but this time with an invalid config; their respective status is +asserted as **_STOPPED_**. diff --git a/test/integration/suites-windows/windows-service/common b/test/integration/suites-windows/windows-service/common new file mode 100644 index 0000000000..d2918ce8e9 --- /dev/null +++ b/test/integration/suites-windows/windows-service/common @@ -0,0 +1,66 @@ +#!/bin/bash + +assert-synced-entry() { + # Check at most 30 times (with one second in between) that the agent has + # successfully synced down the workload entry. + MAXCHECKS=30 + CHECKINTERVAL=1 + for ((i=1;i<=MAXCHECKS;i++)); do + log-info "checking for synced entry ($i of $MAXCHECKS max)..." + if grep -wq "$1" conf/agent/logs.txt; then + return 0 + fi + sleep "${CHECKINTERVAL}" + done + + fail-now "timed out waiting for agent to sync down entry" +} + +assert-service-status() { + MAXCHECKS=10 + CHECKINTERVAL=1 + for ((i=1;i<=MAXCHECKS;i++)); do + log-info "checking for $1 service $2 ($i of $MAXCHECKS max)..." + scCommand=$([ "$2" == "STOPPED" ] && echo "query" || echo "interrogate") + if docker-compose exec -T -u ContainerAdministrator "$1" sc "$scCommand" "$1" | grep -wq "$2"; then + log-info "$1 is in $2 state" + return 0 + fi + sleep "${CHECKINTERVAL}" + done + + fail-now "$1 service failed to reach $2 state" +} + +assert-graceful-shutdown() { + MAXCHECKS=10 + CHECKINTERVAL=1 + for ((i=1;i<=MAXCHECKS;i++)); do + log-info "checking for graceful shutdown ($i of $MAXCHECKS max)..." + if grep -wq "stopped gracefully" conf/"$1"/logs.txt; then + log-info "$1 stopped gracefully" + return 0 + fi + sleep "${CHECKINTERVAL}" + done + + fail-now "timed out waiting for $1 graceful shutdown" +} + +create-service() { + log-info "creating $1 service..." + docker-compose exec -T -u ContainerAdministrator "$1" \ + sc create "$1" binPath="$2" || grep "STOPPED" fail-now "failed to create $1 service" +} + +stop-service() { + log-info "stopping $1 service..." + docker-compose exec -T -u ContainerAdministrator "$1" \ + sc stop "$1" || fail-now "failed to stop $1 service" +} + +start-service(){ + log-info "starting $1 service..." + docker-compose exec -T -u ContainerAdministrator "$1" \ + sc start "$@" | grep -wq "START_PENDING" || fail-now "failed to start $2 service" +} diff --git a/test/integration/suites-windows/windows-service/conf/agent/agent.conf b/test/integration/suites-windows/windows-service/conf/agent/agent.conf new file mode 100644 index 0000000000..ad67ee6328 --- /dev/null +++ b/test/integration/suites-windows/windows-service/conf/agent/agent.conf @@ -0,0 +1,25 @@ +agent { + data_dir = "c:/spire/data/agent" + log_level = "DEBUG" + server_address = "spire-server" + log_file ="c:/spire/conf/agent/logs.txt" + server_port = "8081" + trust_bundle_path = "c:/spire/conf/agent/bootstrap.crt" + trust_domain = "domain.test" +} + +plugins { + NodeAttestor "x509pop" { + plugin_data { + private_key_path = "c:/spire/conf/agent/agent.key.pem" + certificate_path = "c:/spire/conf/agent/agent.crt.pem" + } + } + KeyManager "disk" { + plugin_data { + directory = "c:/spire/data/agent" + } + } + WorkloadAttestor "windows" { + } +} diff --git a/test/integration/suites-windows/windows-service/conf/server/server.conf b/test/integration/suites-windows/windows-service/conf/server/server.conf new file mode 100644 index 0000000000..600d5ad85d --- /dev/null +++ b/test/integration/suites-windows/windows-service/conf/server/server.conf @@ -0,0 +1,25 @@ +server { + bind_address = "0.0.0.0" + bind_port = "8081" + trust_domain = "domain.test" + log_file ="c:/spire/conf/server/logs.txt" + data_dir = "c:/spire/data/server" + log_level = "DEBUG" +} + +plugins { + DataStore "sql" { + plugin_data { + database_type = "sqlite3" + connection_string = "c:/spire/data/server/datastore.sqlite3" + } + } + NodeAttestor "x509pop" { + plugin_data { + ca_bundle_path = "c:/spire/conf/server/agent-cacert.pem" + } + } + KeyManager "memory" { + plugin_data = {} + } +} diff --git a/test/integration/suites-windows/windows-service/docker-compose.yaml b/test/integration/suites-windows/windows-service/docker-compose.yaml new file mode 100644 index 0000000000..a74aef2684 --- /dev/null +++ b/test/integration/suites-windows/windows-service/docker-compose.yaml @@ -0,0 +1,24 @@ +version: '3' + +services: + spire-server: + image: spire-server-windows:latest-local + hostname: spire-server + volumes: + - ./conf/server:c:/spire/conf/server + user: ContainerAdministrator + entrypoint: + - cmd + command: + - cmd /c ping -t localhost > NUL + spire-agent: + image: spire-agent-windows:latest-local + hostname: spire-agent + depends_on: ["spire-server"] + volumes: + - ./conf/agent:c:/spire/conf/agent + user: ContainerAdministrator + entrypoint: + - cmd + command: + - cmd /c ping -t localhost > NUL diff --git a/test/integration/suites-windows/windows-service/teardown b/test/integration/suites-windows/windows-service/teardown new file mode 100644 index 0000000000..9953dcd3f9 --- /dev/null +++ b/test/integration/suites-windows/windows-service/teardown @@ -0,0 +1,6 @@ +#!/bin/bash + +if [ -z "$SUCCESS" ]; then + docker-compose logs +fi +docker-down