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

Support running SPIRE as a Windows service #3625

Merged
merged 5 commits into from
Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/spire-agent/cli/cli.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"context"
stdlog "log"

"github.com/mitchellh/cli"
Expand All @@ -17,7 +18,7 @@ type CLI struct {
AllowUnknownConfig bool
}

func (cc *CLI) Run(args []string) int {
func (cc *CLI) Run(ctx context.Context, args []string) int {
c := cli.NewCLI("spire-agent", version.Version())
c.Args = args
c.Commands = map[string]cli.CommandFactory{
Expand All @@ -37,7 +38,7 @@ func (cc *CLI) Run(args []string) int {
return &api.WatchCLI{}, nil
},
"run": func() (cli.Command, error) {
return run.NewRunCommand(cc.LogOptions, cc.AllowUnknownConfig), nil
return run.NewRunCommand(ctx, cc.LogOptions, cc.AllowUnknownConfig), nil
},
"healthcheck": func() (cli.Command, error) {
return healthcheck.NewHealthCheckCommand(), nil
Expand Down
14 changes: 10 additions & 4 deletions cmd/spire-agent/cli/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,19 @@ type experimentalConfig struct {
}

type Command struct {
ctx context.Context
logOptions []log.Option
env *common_cli.Env
allowUnknownConfig bool
}

func NewRunCommand(logOptions []log.Option, allowUnknownConfig bool) cli.Command {
return newRunCommand(common_cli.DefaultEnv, logOptions, allowUnknownConfig)
func NewRunCommand(ctx context.Context, logOptions []log.Option, allowUnknownConfig bool) cli.Command {
return newRunCommand(ctx, common_cli.DefaultEnv, logOptions, allowUnknownConfig)
}

func newRunCommand(env *common_cli.Env, logOptions []log.Option, allowUnknownConfig bool) *Command {
func newRunCommand(ctx context.Context, env *common_cli.Env, logOptions []log.Option, allowUnknownConfig bool) *Command {
return &Command{
ctx: ctx,
env: env,
logOptions: logOptions,
allowUnknownConfig: allowUnknownConfig,
Expand Down Expand Up @@ -183,7 +185,11 @@ func (cmd *Command) Run(args []string) int {

a := agent.New(c)

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
ctx := cmd.ctx
if ctx == nil {
ctx = context.Background()
}
ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer stop()

err = a.Run(ctx)
Expand Down
3 changes: 2 additions & 1 deletion cmd/spire-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"os"

"github.com/spiffe/spire/cmd/spire-agent/cli"
"github.com/spiffe/spire/pkg/common/entrypoint"
)

func main() {
os.Exit(new(cli.CLI).Run(os.Args[1:]))
os.Exit(entrypoint.NewEntryPoint(new(cli.CLI).Run).Main())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried that with this approach, any command will try to verify if our binary is running as a service,
but that is only true for "run" commands

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this and we agreed that in order to avoid blocking SPIRE to run only because the svc.IsWindowsService() fails, we will fallback to run SPIRE as a regular process if IsWindowsService() returns an error.

}
5 changes: 3 additions & 2 deletions cmd/spire-server/cli/cli.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"context"
stdlog "log"

"github.com/mitchellh/cli"
Expand All @@ -25,7 +26,7 @@ type CLI struct {
}

// Run configures the server CLI commands and subcommands.
func (cc *CLI) Run(args []string) int {
func (cc *CLI) Run(ctx context.Context, args []string) int {
c := cli.NewCLI("spire-server", version.Version())
c.Args = args
c.Commands = map[string]cli.CommandFactory{
Expand Down Expand Up @@ -93,7 +94,7 @@ func (cc *CLI) Run(args []string) int {
return federation.NewUpdateCommand(), nil
},
"run": func() (cli.Command, error) {
return run.NewRunCommand(cc.LogOptions, cc.AllowUnknownConfig), nil
return run.NewRunCommand(ctx, cc.LogOptions, cc.AllowUnknownConfig), nil
},
"token generate": func() (cli.Command, error) {
return token.NewGenerateCommand(), nil
Expand Down
14 changes: 10 additions & 4 deletions cmd/spire-server/cli/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,13 @@ type rateLimitConfig struct {
UnusedKeys []string `hcl:",unusedKeys"`
}

func NewRunCommand(logOptions []log.Option, allowUnknownConfig bool) cli.Command {
return newRunCommand(common_cli.DefaultEnv, logOptions, allowUnknownConfig)
func NewRunCommand(ctx context.Context, logOptions []log.Option, allowUnknownConfig bool) cli.Command {
return newRunCommand(ctx, common_cli.DefaultEnv, logOptions, allowUnknownConfig)
}

func newRunCommand(env *common_cli.Env, logOptions []log.Option, allowUnknownConfig bool) *Command {
func newRunCommand(ctx context.Context, env *common_cli.Env, logOptions []log.Option, allowUnknownConfig bool) *Command {
return &Command{
ctx: ctx,
env: env,
logOptions: logOptions,
allowUnknownConfig: allowUnknownConfig,
Expand All @@ -182,6 +183,7 @@ func newRunCommand(env *common_cli.Env, logOptions []log.Option, allowUnknownCon

// Run Command struct
type Command struct {
ctx context.Context
logOptions []log.Option
env *common_cli.Env
allowUnknownConfig bool
Expand Down Expand Up @@ -241,7 +243,11 @@ func (cmd *Command) Run(args []string) int {

s := server.New(*c)

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
ctx := cmd.ctx
if ctx == nil {
ctx = context.Background()
}
Comment on lines +246 to +249
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may this initialization be moved to newRunCommand or NewRunCommand?
I know this is a double check... but sounds like if we require a context it must be set on struct initialization

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if there is a real benefit of doing that. Currently, the context is being initialized here also, there is no change with that (https://github.com/spiffe/spire/blob/main/cmd/spire-server/cli/run/run.go#L244).
The cost of moving this to newRunCommand would be that if the struct is being initialized without calling the newRunCommand function (like we currently do in unit tests), we would need to set ctx there. The intention of having it here is that a context is always provided if not specified.
What do you think?

ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer stop()

err = s.Run(ctx)
Expand Down
3 changes: 2 additions & 1 deletion cmd/spire-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"os"

"github.com/spiffe/spire/cmd/spire-server/cli"
"github.com/spiffe/spire/pkg/common/entrypoint"
)

func main() {
os.Exit(new(cli.CLI).Run(os.Args[1:]))
os.Exit(entrypoint.NewEntryPoint(new(cli.CLI).Run).Main())
}
19 changes: 19 additions & 0 deletions doc/spire_agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,25 @@ the following flags are available:
| `-trustBundleUrl` | URL to download the SPIRE server CA bundle | |
| `-trustDomain` | The trust domain that this agent belongs to (should be no more than 255 characters) | |

#### Running SPIRE Agent as a Windows service

On Windows platform, SPIRE Agent can optionally be run as a Windows service. When running as a Windows service, the only command supported is the `run` command.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is this validation done? may we prevent it programmatically? what happens if a user try to create a service that run ./bin/spire-server bundle show (it is unrealistic example)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a validation to fail with ERROR_BAD_ARGUMENTS if an argument indicating a command different to "run" is used when running as a service.


_Note: SPIRE does not automatically create the service in the system, it must be created by the user.
When starting the service, all the arguments to execute SPIRE Agent with the `run` command must be passed as service arguments._
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is something useful to provide an example here of a configuration that allows this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I'll add an example of how the service can be created and how the service can be run.


##### Example to create the SPIRE Agent Windows service

```bash
sc.exe create spire-agent binpath=c:\spire\bin\spire-agent.exe
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since it is a shell may we add $ at the start?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

> probably fits better for a Windows terminal / command prompt?

```

##### Example to run the SPIRE Agent Windows service

```bash
sc.exe start spire-agent run -config c:\spire\conf\agent\agent.conf
```

### `spire-agent api fetch`

Calls the workload API to fetch an X509-SVID. This command is aliased to `spire-agent api fetch x509`.
Expand Down
19 changes: 19 additions & 0 deletions doc/spire_server.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,25 @@ Most of the configuration file above options have identical command-line counter
| `-socketPath` | Path to bind the SPIRE Server API socket to | |
| `-trustDomain` | The trust domain that this server belongs to (should be no more than 255 characters) | |

#### Running SPIRE Server as a Windows service

On Windows platform, SPIRE Server can optionally be run as a Windows service. When running as a Windows service, the only command supported is the `run` command.

_Note: SPIRE does not automatically create the service in the system, it must be created by the user.
When starting the service, all the arguments to execute SPIRE Server with the `run` command must be passed as service arguments._

##### Example to create the SPIRE Server Windows service

```bash
sc.exe create spire-server binpath=c:\spire\bin\spire-server.exe
```

##### Example to run the SPIRE Server Windows service

```bash
sc.exe start spire-server run -config c:\spire\conf\server\server.conf
```

### `spire-server token generate`

Generates one node join token and creates a registration entry for it. This token can be used to
Expand Down
23 changes: 23 additions & 0 deletions pkg/common/entrypoint/entrypoint_posix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//go:build !windows
// +build !windows

package entrypoint

import (
"context"
"os"
)

type EntryPoint struct {
runCmdFn func(ctx context.Context, args []string) int
}

func NewEntryPoint(runFn func(ctx context.Context, args []string) int) *EntryPoint {
return &EntryPoint{
runCmdFn: runFn,
}
}

func (e *EntryPoint) Main() int {
return e.runCmdFn(context.Background(), os.Args[1:])
}
21 changes: 21 additions & 0 deletions pkg/common/entrypoint/entrypoint_posix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//go:build !windows
// +build !windows

package entrypoint

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
)

func TestEntryPoint(t *testing.T) {
assert.Equal(t,
NewEntryPoint(func(ctx context.Context, args []string) int { return 0 }).Main(),
0)

assert.Equal(t,
NewEntryPoint(func(ctx context.Context, args []string) int { return 1 }).Main(),
1)
}
73 changes: 73 additions & 0 deletions pkg/common/entrypoint/entrypoint_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//go:build windows
// +build windows

package entrypoint

import (
"context"
"fmt"
"os"

"golang.org/x/sys/windows/svc"
)

type systemCaller interface {
IsWindowsService() (bool, error)
Run(name string, handler svc.Handler) error
}

type systemCall struct {
}

func (s *systemCall) IsWindowsService() (bool, error) {
return svc.IsWindowsService()
}

func (s *systemCall) Run(name string, handler svc.Handler) error {
return svc.Run(name, handler)
}

type EntryPoint struct {
handler svc.Handler
runCmdFn func(ctx context.Context, args []string) int
sc systemCaller
}

func NewEntryPoint(runCmdFn func(ctx context.Context, args []string) int) *EntryPoint {
return &EntryPoint{
runCmdFn: runCmdFn,
handler: &service{
executeServiceFn: func(ctx context.Context, stop context.CancelFunc, args []string) int {
defer stop()
retCode := runCmdFn(ctx, args[1:])
return retCode
},
},
sc: &systemCall{},
}
}

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()
if err != nil {
fmt.Fprintf(os.Stderr, "Could not determine if running as a Windows service: %v", err)
}
if isWindowsService {
errChan := make(chan error)
go func() {
// Since the service runs in its own process, the service name is ignored.
// https://learn.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-startservicectrldispatcherw
errChan <- e.sc.Run("", e.handler)
}()
err = <-errChan
if err != nil {
Comment on lines +65 to +66
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we be sure that returned error was logged?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errors will be logged as part of the normal execution of the "run" command, in the same way that is done when running as a regular process.

return 1
}
return 0
}

return e.runCmdFn(context.Background(), os.Args[1:])
}