smallstep-certificates/commands/app.go
Mariano Cano 10f6a901ec
Let the CA determine the RA lifetime
When the RA mode with StepCAS is used, let the CA decide which lifetime
the RA should get instead of requiring always 24h.

This commit also fixes linter warnings.

Related to #1094
2024-03-12 14:29:55 -07:00

306 lines
8.7 KiB
Go

package commands
import (
"bytes"
"context"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"unicode"
"github.com/pkg/errors"
"github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/ca"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/pki"
"github.com/urfave/cli"
"go.step.sm/cli-utils/errs"
"go.step.sm/cli-utils/step"
)
// AppCommand is the action used as the top action.
var AppCommand = cli.Command{
Name: "start",
Action: appAction,
UsageText: `**step-ca** <config> [**--password-file**=<file>]
[**--ssh-host-password-file**=<file>] [**--ssh-user-password-file**=<file>]
[**--issuer-password-file**=<file>] [**--pidfile**=<file>] [**--resolver**=<addr>]`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "password-file",
Usage: `path to the <file> containing the password to decrypt the
intermediate private key.`,
},
cli.StringFlag{
Name: "ssh-host-password-file",
Usage: `path to the <file> containing the password to decrypt the
private key used to sign SSH host certificates. If the flag is not passed it
will default to --password-file.`,
},
cli.StringFlag{
Name: "ssh-user-password-file",
Usage: `path to the <file> containing the password to decrypt the
private key used to sign SSH user certificates. If the flag is not passed it
will default to --password-file.`,
},
cli.StringFlag{
Name: "issuer-password-file",
Usage: `path to the <file> containing the password to decrypt the
certificate issuer private key used in the RA mode.`,
},
cli.StringFlag{
Name: "resolver",
Usage: "address of a DNS resolver to be used instead of the default.",
},
cli.StringFlag{
Name: "token",
Usage: "token used to enable the linked ca.",
EnvVar: "STEP_CA_TOKEN",
},
cli.BoolFlag{
Name: "quiet",
Usage: "disable startup information",
EnvVar: "STEP_CA_QUIET",
},
cli.StringFlag{
Name: "context",
Usage: "the <name> of the authority's context.",
EnvVar: "STEP_CA_CONTEXT",
},
cli.IntFlag{
Name: "acme-http-port",
Usage: `the <port> used on http-01 challenges. It can be changed for testing purposes.
Requires **--insecure** flag.`,
},
cli.IntFlag{
Name: "acme-tls-port",
Usage: `the <port> used on tls-alpn-01 challenges. It can be changed for testing purposes.
Requires **--insecure** flag.`,
},
cli.StringFlag{
Name: "pidfile",
Usage: "the path to the <file> to write the process ID.",
},
cli.BoolFlag{
Name: "insecure",
Usage: "enable insecure flags.",
},
},
}
var pidfile string
// AppAction is the action used when the top command runs.
func appAction(ctx *cli.Context) error {
passFile := ctx.String("password-file")
sshHostPassFile := ctx.String("ssh-host-password-file")
sshUserPassFile := ctx.String("ssh-user-password-file")
issuerPassFile := ctx.String("issuer-password-file")
resolver := ctx.String("resolver")
token := ctx.String("token")
quiet := ctx.Bool("quiet")
if ctx.NArg() > 1 {
return errs.TooManyArguments(ctx)
}
// Allow custom ACME ports with insecure
if acmePort := ctx.Int("acme-http-port"); acmePort != 0 {
if ctx.Bool("insecure") {
acme.InsecurePortHTTP01 = acmePort
} else {
return fmt.Errorf("flag '--acme-http-port' requires the '--insecure' flag")
}
}
if acmePort := ctx.Int("acme-tls-port"); acmePort != 0 {
if ctx.Bool("insecure") {
acme.InsecurePortTLSALPN01 = acmePort
} else {
return fmt.Errorf("flag '--acme-tls-port' requires the '--insecure' flag")
}
}
// Allow custom contexts.
if caCtx := ctx.String("context"); caCtx != "" {
if _, ok := step.Contexts().Get(caCtx); ok {
if err := step.Contexts().SetCurrent(caCtx); err != nil {
return err
}
} else if token == "" {
return fmt.Errorf("context %q not found", caCtx)
} else if err := createContext(caCtx); err != nil {
return err
}
}
var configFile string
if ctx.NArg() > 0 {
configFile = ctx.Args().Get(0)
} else {
configFile = step.CaConfigFile()
}
cfg, err := config.LoadConfiguration(configFile)
if err != nil && token == "" {
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("step-ca can't find or open the configuration file for your CA.")
fmt.Println("You may need to create a CA first by running `step ca init`.")
fmt.Println("Documentation: https://u.step.sm/docs/ca")
os.Exit(1)
}
fatal(err)
}
// Initialize a basic configuration to be used with an automatically
// configured linked RA. Default configuration includes:
// * badgerv2 on $(step path)/db
// * JSON logger
// * Default TLS options
if cfg == nil {
cfg = &config.Config{
SkipValidation: true,
Logger: []byte(`{"format":"json"}`),
DB: &db.Config{
Type: "badgerv2",
DataSource: filepath.Join(step.Path(), "db"),
},
AuthorityConfig: &config.AuthConfig{
DeploymentType: pki.LinkedDeployment.String(),
Provisioners: provisioner.List{},
Template: &config.ASN1DN{},
Backdate: &provisioner.Duration{
Duration: config.DefaultBackdate,
},
},
TLS: &config.DefaultTLSOptions,
}
}
if cfg.AuthorityConfig != nil {
if token == "" && strings.EqualFold(cfg.AuthorityConfig.DeploymentType, pki.LinkedDeployment.String()) {
return errors.New(`'step-ca' requires the '--token' flag for linked deploy type.
To get a linked authority token:
1. Log in or create a Certificate Manager account at ` + "\033[1mhttps://u.step.sm/linked\033[0m" + `
2. Add a new authority and select "Link a step-ca instance"
3. Follow instructions in browser to start 'step-ca' using the '--token' flag
`)
}
}
var password []byte
if passFile != "" {
if password, err = os.ReadFile(passFile); err != nil {
fatal(errors.Wrapf(err, "error reading %s", passFile))
}
password = bytes.TrimRightFunc(password, unicode.IsSpace)
}
var sshHostPassword []byte
if sshHostPassFile != "" {
if sshHostPassword, err = os.ReadFile(sshHostPassFile); err != nil {
fatal(errors.Wrapf(err, "error reading %s", sshHostPassFile))
}
sshHostPassword = bytes.TrimRightFunc(sshHostPassword, unicode.IsSpace)
}
var sshUserPassword []byte
if sshUserPassFile != "" {
if sshUserPassword, err = os.ReadFile(sshUserPassFile); err != nil {
fatal(errors.Wrapf(err, "error reading %s", sshUserPassFile))
}
sshUserPassword = bytes.TrimRightFunc(sshUserPassword, unicode.IsSpace)
}
var issuerPassword []byte
if issuerPassFile != "" {
if issuerPassword, err = os.ReadFile(issuerPassFile); err != nil {
fatal(errors.Wrapf(err, "error reading %s", issuerPassFile))
}
issuerPassword = bytes.TrimRightFunc(issuerPassword, unicode.IsSpace)
}
if filename := ctx.String("pidfile"); filename != "" {
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
//nolint:gosec // 0644 (-rw-r--r--) are common permissions for a pid file
if err := os.WriteFile(filename, pid, 0644); err != nil {
fatal(errors.Wrap(err, "error writing pidfile"))
}
pidfile = filename
}
// replace resolver if requested
if resolver != "" {
net.DefaultResolver.PreferGo = true
net.DefaultResolver.Dial = func(_ context.Context, network, _ string) (net.Conn, error) {
return net.Dial(network, resolver)
}
}
srv, err := ca.New(cfg,
ca.WithConfigFile(configFile),
ca.WithPassword(password),
ca.WithSSHHostPassword(sshHostPassword),
ca.WithSSHUserPassword(sshUserPassword),
ca.WithIssuerPassword(issuerPassword),
ca.WithLinkedCAToken(token),
ca.WithQuiet(quiet),
)
if err != nil {
fatal(err)
}
go ca.StopReloaderHandler(srv)
if err = srv.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) {
fatal(err)
}
if pidfile != "" {
os.Remove(pidfile)
}
return nil
}
// createContext creates a new context using the given name for the context,
// authority and profile.
func createContext(name string) error {
if err := step.Contexts().Add(&step.Context{
Name: name, Authority: name, Profile: name,
}); err != nil {
return fmt.Errorf("error adding context: %w", err)
}
if err := step.Contexts().SaveCurrent(name); err != nil {
return fmt.Errorf("error saving context: %w", err)
}
if err := step.Contexts().SetCurrent(name); err != nil {
return fmt.Errorf("error setting context: %w", err)
}
if err := os.MkdirAll(step.Path(), 0700); err != nil {
return fmt.Errorf("error creating directory: %w", err)
}
return nil
}
// fatal writes the passed error on the standard error and exits with the exit
// code 1. If the environment variable STEPDEBUG is set to 1 it shows the
// stack trace of the error.
func fatal(err error) {
if os.Getenv("STEPDEBUG") == "1" {
fmt.Fprintf(os.Stderr, "%+v\n", err)
} else {
fmt.Fprintln(os.Stderr, err)
}
if pidfile != "" {
os.Remove(pidfile)
}
os.Exit(2)
}