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 --service for running as a windows service for renewal and rekeying #877

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
15 changes: 15 additions & 0 deletions command/ca/rekey.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ configuration and load the new certificate. Default value is SIGHUP (1)`,
periodically. By default the daemon will rekey a certificate before 2/3 of the
time to expiration has elapsed. The period can be configured using the
**--rekey-period** or **--expires-in** flags.`,
},
cli.BoolFlag{
Name: "service",
Usage: `Run the rekey command as a windows service, rekeying and overwriting the certificate
periodically. By default the service will rekey a certificate before 2/3 of the
time to expiration has elapsed. The period can be configured using the
**--rekey-period** or **--expires-in** flags. You must install this as a service first using **sc.exe create step-renew binPath= "path_to_step_cli.exe ca rekey --service --ca-url=your_ca_url --root=path_to_root_ca.crt other_arguments"** `,
},
cli.StringFlag{
Name: "rekey-period",
Expand Down Expand Up @@ -216,6 +223,7 @@ func rekeyCertificateAction(ctx *cli.Context) error {
keyFile := args.Get(1)
passFile := ctx.String("password-file")
isDaemon := ctx.Bool("daemon")
isService := ctx.Bool("service")
execCmd := ctx.String("exec")
givenPrivate := ctx.String("private-key")

Expand Down Expand Up @@ -312,6 +320,13 @@ func rekeyCertificateAction(ctx *cli.Context) error {
return renewer.Daemon(outCert, next, expiresIn, rekeyPeriod, afterRekey)
}

if isService {
// Force is always enabled when daemon mode is used
ctx.Set("force", "true")
next := nextRenewDuration(leaf, expiresIn, rekeyPeriod)
return renewer.Service(outCert, next, expiresIn, rekeyPeriod, afterRekey)
}

// Do not rekey if (cert.notAfter - now) > (expiresIn + jitter)
if expiresIn > 0 {
//nolint:gosec // The random number below is not being used for crypto.
Expand Down
17 changes: 16 additions & 1 deletion command/ca/renew.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ configuration and load the new certificate. Default value is SIGHUP (1)`,
periodically. By default the daemon will renew a certificate before 2/3 of the
time to expiration has elapsed. The period can be configured using the
**--renew-period** or **--expires-in** flags.`,
},
cli.BoolFlag{
Name: "service",
Usage: `Run the renew command as a windows service, renewing and overwriting the certificate
periodically. By default the service will renew a certificate before 2/3 of the
time to expiration has elapsed. The period can be configured using the
**--renew-period** or **--expires-in** flags. You must install this as a service first using **sc.exe create step-renew binPath= "path_to_step_cli.exe ca renew --service --ca-url=your_ca_url --root=path_to_root_ca.crt other_arguments"** `,
},
cli.StringFlag{
Name: "renew-period",
Expand All @@ -225,6 +232,7 @@ func renewCertificateAction(ctx *cli.Context) error {
keyFile := args.Get(1)
passFile := ctx.String("password-file")
isDaemon := ctx.Bool("daemon")
isService := ctx.Bool("service")
execCmd := ctx.String("exec")

outFile := ctx.String("out")
Expand Down Expand Up @@ -256,7 +264,7 @@ func renewCertificateAction(ctx *cli.Context) error {
if expiresIn > 0 && renewPeriod > 0 {
return errs.IncompatibleFlagWithFlag(ctx, "expires-in", "renew-period")
}
if renewPeriod > 0 && !isDaemon {
if renewPeriod > 0 && !isDaemon && !isService {
return errs.RequiredWithFlag(ctx, "renew-period", "daemon")
}

Expand Down Expand Up @@ -316,6 +324,13 @@ func renewCertificateAction(ctx *cli.Context) error {
return renewer.Daemon(outFile, next, expiresIn, renewPeriod, afterRenew)
}

if isService {
// Force is always enabled when daemon mode is used
ctx.Set("force", "true")
next := nextRenewDuration(cert.Leaf, expiresIn, renewPeriod)
return renewer.Service(outFile, next, expiresIn, renewPeriod, afterRenew)
}

// Do not renew if (cert.notAfter - now) > (expiresIn + jitter)
if expiresIn > 0 {
//nolint:gosec // The random number below is not being used for crypto.
Expand Down
8 changes: 8 additions & 0 deletions command/ca/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build !windows

package ca

func (r *renewer) Service(outFile string, next, expiresIn, renewPeriod time.Duration, afterRenew func() error) error {
errLog := log.New(os.Stderr, "ERROR: ", log.LstdFlags)
errLog.Fatalf("running as a service is only supported on windows, use --daemon instead.")
}
85 changes: 85 additions & 0 deletions command/ca/service_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//go:build windows

package ca

import (
"fmt"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/eventlog"
"log"
"os"
"time"
)

type windowsRenewer struct {
eLog *eventlog.Log
next, expiresIn, renewPeriod time.Duration
afterRenew func() error
renewer *renewer
outFile string
}

func (r *renewer) Service(outFile string, next, expiresIn, renewPeriod time.Duration, afterRenew func() error) error {
errLog := log.New(os.Stderr, "ERROR: ", log.LstdFlags)
inService, err := svc.IsWindowsService()
if err != nil {
errLog.Fatalf("failed to determine if we are running in service: %v", err)
}

if !inService {
errLog.Fatalf("--service requires running as a service, see step ca renew --help for an example of how to install the service")
}

eventlog.InstallAsEventCreate("step-renew", eventlog.Info|eventlog.Warning|eventlog.Error)

// Loggers
eLog, err := eventlog.Open("step-renew")
if err != nil {
return err
}
defer eLog.Close()

wr := windowsRenewer{
eLog: eLog,
next: next,
expiresIn: expiresIn,
renewPeriod: renewPeriod,
afterRenew: afterRenew,
renewer: r,
outFile: outFile,
}

eLog.Info(100, fmt.Sprintf("starting step certificate renewal service. First renewal in %s", next.Round(time.Second)))

return svc.Run("step-renew", &wr)
}

func (wr *windowsRenewer) Execute(args []string, cr <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
var err error
changes <- svc.Status{State: svc.StartPending}

changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
loop:
for {
select {
case <-time.After(wr.next):
if wr.next, err = wr.renewer.RenewAndPrepareNext(wr.outFile, wr.expiresIn, wr.renewPeriod); err != nil {
wr.eLog.Warning(1, err.Error())
} else if err := wr.afterRenew(); err != nil {
wr.eLog.Warning(1, err.Error())
}
case c := <-cr:
switch c.Cmd {
case svc.Interrogate:
changes <- c.CurrentStatus
case svc.Stop, svc.Shutdown:
break loop
default:
wr.eLog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
}
}
}
changes <- svc.Status{State: svc.StopPending}
return
}