You've already forked step-ca-cli
mirror of
https://github.com/smallstep/cli.git
synced 2026-10-24 11:19:19 +03:00
Add fallback=-context poc
This commit adds a fallback context that can be used on some commands to retry a command using a different context.
This commit is contained in:
@@ -20,7 +20,7 @@ func bootstrapCommand() cli.Command {
|
||||
UsageText: `**step ca bootstrap**
|
||||
[**--ca-url**=<uri>] [**--fingerprint**=<fingerprint>] [**--install**]
|
||||
[**--team**=<name>] [**--authority**=<name>] [**--team-url**=<uri>] [**--redirect-url**=<uri>]
|
||||
[**--context**=<name>] [**--profile**=<name>]
|
||||
[**--context**=<name>] [**--fallback-context**=<name>] [**--profile**=<name>]
|
||||
[**--authority**=<name>] [**--team-authority**=<sub-domain>]`,
|
||||
Description: `**step ca bootstrap** downloads the root certificate from the certificate
|
||||
authority and sets up the current environment to use it.
|
||||
@@ -77,6 +77,7 @@ $ step ca bootstrap --team superteam --team-url https://config.example.com/<>
|
||||
flags.Context,
|
||||
flags.ContextProfile,
|
||||
flags.ContextAuthority,
|
||||
flags.FallbackContext,
|
||||
flags.HiddenNoContext,
|
||||
},
|
||||
}
|
||||
@@ -90,12 +91,18 @@ func bootstrapAction(ctx *cli.Context) error {
|
||||
fingerprint := strings.TrimSpace(ctx.String("fingerprint"))
|
||||
team := ctx.String("team")
|
||||
teamAuthority := ctx.String("team-authority")
|
||||
contextName := ctx.String("context")
|
||||
fallbackContextName := ctx.String("fallback-context")
|
||||
|
||||
switch {
|
||||
case team != "" && caURL != "":
|
||||
return errs.IncompatibleFlagWithFlag(ctx, "team", "ca-url")
|
||||
case team != "" && fingerprint != "":
|
||||
return errs.IncompatibleFlagWithFlag(ctx, "team", "fingerprint")
|
||||
case fallbackContextName != "" && contextName == "":
|
||||
return errs.RequiredWithFlag(ctx, "fallback-context", "context")
|
||||
case fallbackContextName != "" && contextName == fallbackContextName:
|
||||
return errs.IncompatibleFlagValues(ctx, "fallback-context", fallbackContextName, "context", contextName)
|
||||
case team != "" && teamAuthority != "":
|
||||
return cautils.BootstrapTeamAuthority(ctx, team, teamAuthority)
|
||||
case team != "":
|
||||
|
||||
@@ -54,6 +54,7 @@ $ step ssh check-host internal.smallstep.com
|
||||
flags.CaURL,
|
||||
flags.Root,
|
||||
flags.Context,
|
||||
flags.FallbackContext,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -71,7 +72,7 @@ func checkHostAction(ctx *cli.Context) error {
|
||||
}
|
||||
version, err := client.Version()
|
||||
if err != nil {
|
||||
return contactAdminErr(errors.Wrap(err, "error retrieving client version info"))
|
||||
return runOnFallbackContext(ctx, contactAdminErr(errors.Wrap(err, "error retrieving client version info")))
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -101,8 +102,7 @@ func checkHostAction(ctx *cli.Context) error {
|
||||
|
||||
resp, err := client.SSHCheckHost(hostname, tok)
|
||||
if err != nil {
|
||||
return caErrs.Wrap(http.StatusInternalServerError, err,
|
||||
"error checking ssh host eligibility")
|
||||
return runOnFallbackContext(ctx, caErrs.Wrap(http.StatusInternalServerError, err, "error checking ssh host eligibility"))
|
||||
}
|
||||
|
||||
if isVerbose {
|
||||
|
||||
@@ -34,7 +34,7 @@ func configCommand() cli.Command {
|
||||
[**--team**=<name>] [**--team-authority**=<sub-domain>] [**--host**]
|
||||
[**--set**=<key=value>] [**--set-file**=<file>] [**--dry-run**] [**--roots**]
|
||||
[**--federation**] [**--console**] [**--force**] [**--offline**] [**--ca-config**=<file>]
|
||||
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>]
|
||||
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>] [**--fallback-context**=<name>]
|
||||
[**--authority**=<name>] [**--profile**=<name>]`,
|
||||
Description: `**step ssh config** configures SSH to be used with certificates. It also supports
|
||||
flags to inspect the root certificates used to sign the certificates.
|
||||
@@ -104,6 +104,7 @@ times to set multiple variables.`,
|
||||
},
|
||||
flags.ContextProfile,
|
||||
flags.ContextAuthority,
|
||||
flags.FallbackContext,
|
||||
flags.HiddenNoContext,
|
||||
},
|
||||
}
|
||||
@@ -115,6 +116,8 @@ func configAction(ctx *cli.Context) (recoverErr error) {
|
||||
isRoots := ctx.Bool("roots")
|
||||
isFederation := ctx.Bool("federation")
|
||||
sets := ctx.StringSlice("set")
|
||||
contextName := ctx.String("context")
|
||||
fallbackContextName := ctx.String("fallback-context")
|
||||
|
||||
switch {
|
||||
case team != "" && isHost:
|
||||
@@ -131,6 +134,10 @@ func configAction(ctx *cli.Context) (recoverErr error) {
|
||||
return errs.IncompatibleFlagWithFlag(ctx, "roots", "set")
|
||||
case isFederation && len(sets) > 0:
|
||||
return errs.IncompatibleFlagWithFlag(ctx, "federation", "set")
|
||||
case fallbackContextName != "" && contextName == "":
|
||||
return errs.RequiredWithFlag(ctx, "fallback-context", "context")
|
||||
case fallbackContextName != "" && contextName == fallbackContextName:
|
||||
return errs.IncompatibleFlagValues(ctx, "fallback-context", fallbackContextName, "context", contextName)
|
||||
}
|
||||
|
||||
// Bootstrap Authority
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/ca"
|
||||
"github.com/smallstep/cli-utils/command"
|
||||
"github.com/smallstep/cli-utils/errs"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
|
||||
"github.com/smallstep/cli/exec"
|
||||
"github.com/smallstep/cli/flags"
|
||||
@@ -36,7 +30,8 @@ func proxycommandCommand() cli.Command {
|
||||
UsageText: `**step ssh proxycommand** <user> <host> <port>
|
||||
[**--provisioner**=<name>] [**--set**=<key=value>] [**--set-file**=<file>]
|
||||
[**--console**] [**--offline**] [**--ca-config**=<file>]
|
||||
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>]`,
|
||||
[**--ca-url**=<uri>] [**--root**=<file>]
|
||||
[**--context**=<name>] [**--fallback-context**=<name>]`,
|
||||
Description: `**step ssh proxycommand** looks into the host registry
|
||||
and proxies the ssh connection according to its configuration. This command
|
||||
is used in the ssh client config with <ProxyCommand> keyword.
|
||||
@@ -64,6 +59,7 @@ This command will add the user to the ssh-agent if necessary.
|
||||
flags.CaURL,
|
||||
flags.Root,
|
||||
flags.Context,
|
||||
flags.FallbackContext,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -99,121 +95,7 @@ func proxycommandAction(ctx *cli.Context) error {
|
||||
// doLoginIfNeeded check if the user is logged in looking at the ssh agent, if
|
||||
// it's not it will do the login flow.
|
||||
func doLoginIfNeeded(ctx *cli.Context, subject string) error {
|
||||
templateData, err := flags.ParseTemplateData(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
agent, err := sshutil.DialAgent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := cautils.NewClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if a user key exists
|
||||
if roots, err := client.SSHRoots(); err == nil && len(roots.UserKeys) > 0 {
|
||||
userKeys := make([]ssh.PublicKey, len(roots.UserKeys))
|
||||
for i, uk := range roots.UserKeys {
|
||||
userKeys[i] = uk.PublicKey
|
||||
}
|
||||
exists, err := agent.HasKeys(sshutil.WithSignatureKey(userKeys), sshutil.WithRemoveExpiredCerts(time.Now()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Do login flow
|
||||
flow, err := cautils.NewCertificateFlow(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// There's not need to sanitize the principal, it should come from ssh.
|
||||
principals := []string{subject}
|
||||
|
||||
// Make sure the validAfter is in the past. It avoids `Certificate
|
||||
// invalid: not yet valid` errors if the times are not in sync
|
||||
// perfectly.
|
||||
validAfter := provisioner.NewTimeDuration(time.Now().Add(-1 * time.Minute))
|
||||
validBefore := provisioner.TimeDuration{}
|
||||
|
||||
token, err := flow.GenerateSSHToken(ctx, subject, cautils.SSHUserSignType, principals, validAfter, validBefore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// NOTE: For OIDC tokens the subject should always be the email. The
|
||||
// provisioner is responsible for loading and setting the principals with
|
||||
// the application of an Identity function.
|
||||
if email, ok := tokenEmail(token); ok {
|
||||
subject = email
|
||||
}
|
||||
|
||||
caClient, err := flow.GetClient(ctx, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
version, err := caClient.Version()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate identity certificate (x509) if necessary
|
||||
var identityCSR api.CertificateRequest
|
||||
var identityKey crypto.PrivateKey
|
||||
if version.RequireClientAuthentication {
|
||||
csr, key, err := ca.CreateIdentityRequest(subject)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
identityCSR = *csr
|
||||
identityKey = key
|
||||
}
|
||||
|
||||
// Generate keypair
|
||||
pub, priv, err := keyutil.GenerateDefaultKeyPair()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sshPub, err := ssh.NewPublicKey(pub)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating public key")
|
||||
}
|
||||
|
||||
// Sign certificate in the CA
|
||||
resp, err := caClient.SSHSign(&api.SSHSignRequest{
|
||||
PublicKey: sshPub.Marshal(),
|
||||
OTT: token,
|
||||
Principals: principals,
|
||||
CertType: provisioner.SSHUserCert,
|
||||
KeyID: subject,
|
||||
ValidAfter: validAfter,
|
||||
ValidBefore: validBefore,
|
||||
IdentityCSR: identityCSR,
|
||||
TemplateData: templateData,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write x509 identity certificate
|
||||
if version.RequireClientAuthentication {
|
||||
if err := ca.WriteDefaultIdentity(resp.IdentityCertificate, identityKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Add certificate and private key to agent
|
||||
return agent.AddCertificate(subject, resp.Certificate.Certificate, priv)
|
||||
return loginIfNeeded(ctx, subject, withRetryFunc(runOnFallbackContext))
|
||||
}
|
||||
|
||||
func getBastion(ctx *cli.Context, user, host string) (*api.SSHBastionResponse, error) {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
@@ -13,9 +18,11 @@ import (
|
||||
"github.com/smallstep/certificates/ca"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/cli-utils/command"
|
||||
"github.com/smallstep/cli-utils/step"
|
||||
"github.com/smallstep/cli-utils/ui"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
|
||||
"github.com/smallstep/cli/exec"
|
||||
"github.com/smallstep/cli/flags"
|
||||
"github.com/smallstep/cli/internal/sshutil"
|
||||
"github.com/smallstep/cli/token"
|
||||
@@ -161,6 +168,171 @@ private key so that the pair can be added to an SSH Agent.`,
|
||||
}
|
||||
)
|
||||
|
||||
type loginOptions struct {
|
||||
retryFunc func(*cli.Context, error) error
|
||||
}
|
||||
|
||||
type loginOption func(*loginOptions)
|
||||
|
||||
func withRetryFunc(fn func(*cli.Context, error) error) loginOption {
|
||||
return func(lo *loginOptions) {
|
||||
lo.retryFunc = fn
|
||||
}
|
||||
}
|
||||
|
||||
func runOnFallbackContext(ctx *cli.Context, err error) error {
|
||||
var (
|
||||
contexts = step.Contexts()
|
||||
current = contexts.GetCurrent()
|
||||
)
|
||||
|
||||
if name := ctx.String("fallback-context"); name != "" && name != current.Name {
|
||||
if _, ok := contexts.Get(name); !ok {
|
||||
return fmt.Errorf("error loading fallback context %q", name)
|
||||
}
|
||||
|
||||
arg0, err := os.Executable()
|
||||
if err != nil || arg0 == "" {
|
||||
arg0 = os.Args[0]
|
||||
}
|
||||
|
||||
args := slices.Clone(os.Args[1:])
|
||||
args = append(args, "--context", name)
|
||||
exec.Exec(arg0, args...)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// loginIfNeeded check if the user is logged in looking at the ssh agent, if
|
||||
// it's not it will do the login flow.
|
||||
func loginIfNeeded(ctx *cli.Context, subject string, opts ...loginOption) error {
|
||||
o := &loginOptions{
|
||||
retryFunc: func(_ *cli.Context, err error) error {
|
||||
return err
|
||||
},
|
||||
}
|
||||
for _, fn := range opts {
|
||||
fn(o)
|
||||
}
|
||||
|
||||
templateData, err := flags.ParseTemplateData(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
agent, err := sshutil.DialAgent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := cautils.NewClient(ctx)
|
||||
if err != nil {
|
||||
return o.retryFunc(ctx, err)
|
||||
}
|
||||
|
||||
// Check if a user key exists
|
||||
if roots, err := client.SSHRoots(); err == nil && len(roots.UserKeys) > 0 {
|
||||
userKeys := make([]ssh.PublicKey, len(roots.UserKeys))
|
||||
for i, uk := range roots.UserKeys {
|
||||
userKeys[i] = uk.PublicKey
|
||||
}
|
||||
exists, err := agent.HasKeys(sshutil.WithSignatureKey(userKeys), sshutil.WithRemoveExpiredCerts(time.Now()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Do login flow
|
||||
flow, err := cautils.NewCertificateFlow(ctx)
|
||||
if err != nil {
|
||||
return o.retryFunc(ctx, err)
|
||||
}
|
||||
|
||||
// There's not need to sanitize the principal, it should come from ssh.
|
||||
principals := []string{subject}
|
||||
|
||||
// Make sure the validAfter is in the past. It avoids `Certificate
|
||||
// invalid: not yet valid` errors if the times are not in sync
|
||||
// perfectly.
|
||||
validAfter := provisioner.NewTimeDuration(time.Now().Add(-1 * time.Minute))
|
||||
validBefore := provisioner.TimeDuration{}
|
||||
|
||||
token, err := flow.GenerateSSHToken(ctx, subject, cautils.SSHUserSignType, principals, validAfter, validBefore)
|
||||
if err != nil {
|
||||
return o.retryFunc(ctx, err)
|
||||
}
|
||||
|
||||
// NOTE: For OIDC tokens the subject should always be the email. The
|
||||
// provisioner is responsible for loading and setting the principals with
|
||||
// the application of an Identity function.
|
||||
if email, ok := tokenEmail(token); ok {
|
||||
subject = email
|
||||
}
|
||||
|
||||
caClient, err := flow.GetClient(ctx, token)
|
||||
if err != nil {
|
||||
return o.retryFunc(ctx, err)
|
||||
}
|
||||
|
||||
version, err := caClient.Version()
|
||||
if err != nil {
|
||||
return o.retryFunc(ctx, err)
|
||||
}
|
||||
|
||||
// Generate identity certificate (x509) if necessary
|
||||
var identityCSR api.CertificateRequest
|
||||
var identityKey crypto.PrivateKey
|
||||
if version.RequireClientAuthentication {
|
||||
csr, key, err := ca.CreateIdentityRequest(subject)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
identityCSR = *csr
|
||||
identityKey = key
|
||||
}
|
||||
|
||||
// Generate keypair
|
||||
pub, priv, err := keyutil.GenerateDefaultKeyPair()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sshPub, err := ssh.NewPublicKey(pub)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating public key")
|
||||
}
|
||||
|
||||
// Sign certificate in the CA
|
||||
resp, err := caClient.SSHSign(&api.SSHSignRequest{
|
||||
PublicKey: sshPub.Marshal(),
|
||||
OTT: token,
|
||||
Principals: principals,
|
||||
CertType: provisioner.SSHUserCert,
|
||||
KeyID: subject,
|
||||
ValidAfter: validAfter,
|
||||
ValidBefore: validBefore,
|
||||
IdentityCSR: identityCSR,
|
||||
TemplateData: templateData,
|
||||
})
|
||||
if err != nil {
|
||||
return o.retryFunc(ctx, err)
|
||||
}
|
||||
|
||||
// Write x509 identity certificate
|
||||
if version.RequireClientAuthentication {
|
||||
if err := ca.WriteDefaultIdentity(resp.IdentityCertificate, identityKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Add certificate and private key to agent
|
||||
return agent.AddCertificate(subject, resp.Certificate.Certificate, priv)
|
||||
}
|
||||
|
||||
func loginOnUnauthorized(ctx *cli.Context) (ca.RetryFunc, error) {
|
||||
templateData, err := flags.ParseTemplateData(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -261,7 +261,7 @@ generating key.`,
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
// Context is a cli.Flag used to select a a context name.
|
||||
// Context is a cli.Flag used to select a context name.
|
||||
Context = cli.StringFlag{
|
||||
Name: "context",
|
||||
Usage: "The context <name> to apply for the given command.",
|
||||
@@ -279,6 +279,12 @@ generating key.`,
|
||||
Usage: `The <name> that will serve as the authority name for the context.`,
|
||||
}
|
||||
|
||||
// FallbackContext is a cli.flat to select a fallback context.
|
||||
FallbackContext = cli.StringFlag{
|
||||
Name: "fallback-context",
|
||||
Usage: "The context <name> to use as a fallback for the given command.",
|
||||
}
|
||||
|
||||
// Offline is a cli.Flag used to activate the offline flow.
|
||||
Offline = cli.BoolFlag{
|
||||
Name: "offline",
|
||||
|
||||
@@ -91,6 +91,7 @@ type bootstrapConfig struct {
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Root string `json:"root"`
|
||||
Redirect string `json:"redirect-url,omitempty"`
|
||||
FallbackContext string `json:"fallback-context,omitempty"`
|
||||
Provisioner string `json:"provisioner,omitempty"`
|
||||
MinPasswordLength int `json:"min-password-length,omitempty"`
|
||||
}
|
||||
@@ -112,6 +113,7 @@ func bootstrap(ctx *cli.Context, caURL, fingerprint string, opts ...bootstrapOpt
|
||||
return errors.Wrap(err, "error downloading root certificate")
|
||||
}
|
||||
|
||||
var fallbackContext string
|
||||
if UseContext(ctx) {
|
||||
ctxName := ctx.String("context")
|
||||
if ctxName == "" {
|
||||
@@ -125,6 +127,7 @@ func bootstrap(ctx *cli.Context, caURL, fingerprint string, opts ...bootstrapOpt
|
||||
if ctxProfile == "" {
|
||||
ctxProfile = ctxName
|
||||
}
|
||||
fallbackContext = ctx.String("fallback-context")
|
||||
if err := step.Contexts().Add(&step.Context{
|
||||
Name: ctxName,
|
||||
Profile: ctxProfile,
|
||||
@@ -168,10 +171,11 @@ func bootstrap(ctx *cli.Context, caURL, fingerprint string, opts ...bootstrapOpt
|
||||
|
||||
// Serialize defaults.json
|
||||
bootConf := bootstrapConfig{
|
||||
CA: caURL,
|
||||
Fingerprint: fingerprint,
|
||||
Root: pki.GetRootCAPath(),
|
||||
Redirect: bc.redirectURL,
|
||||
CA: caURL,
|
||||
Fingerprint: fingerprint,
|
||||
Root: pki.GetRootCAPath(),
|
||||
Redirect: bc.redirectURL,
|
||||
FallbackContext: fallbackContext,
|
||||
}
|
||||
if bc.minPasswordLength > 0 {
|
||||
bootConf.MinPasswordLength = bc.minPasswordLength
|
||||
|
||||
Reference in New Issue
Block a user