diff --git a/command/ca/rekey.go b/command/ca/rekey.go index dfb9b9e94..ed5c90a88 100644 --- a/command/ca/rekey.go +++ b/command/ca/rekey.go @@ -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", @@ -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") @@ -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. diff --git a/command/ca/renew.go b/command/ca/renew.go index cbe4ef51b..6a561432e 100644 --- a/command/ca/renew.go +++ b/command/ca/renew.go @@ -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", @@ -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") @@ -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") } @@ -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. diff --git a/command/ca/service.go b/command/ca/service.go new file mode 100644 index 000000000..e7f7b5ee8 --- /dev/null +++ b/command/ca/service.go @@ -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.") +} diff --git a/command/ca/service_windows.go b/command/ca/service_windows.go new file mode 100644 index 000000000..1fe632d5d --- /dev/null +++ b/command/ca/service_windows.go @@ -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 +}