mirror of
https://github.com/smallstep/certificates.git
synced 2024-10-31 03:20:16 +00:00
10f6a901ec
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
306 lines
8.7 KiB
Go
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)
|
|
}
|