diff --git a/command/ca/policy/actions/cn_regex.go b/command/ca/policy/actions/cn_regex.go new file mode 100644 index 00000000..5f2141b6 --- /dev/null +++ b/command/ca/policy/actions/cn_regex.go @@ -0,0 +1,123 @@ +package actions + +import ( + "context" + "errors" + "fmt" + + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/errs" + + "github.com/smallstep/cli/command/ca/policy/policycontext" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/internal/command" + "github.com/smallstep/cli/utils/cautils" +) + +// CommonNameRegexCommand returns the cn-regex policy subcommand. +func CommonNameRegexCommand(ctx context.Context) cli.Command { + commandName := policycontext.GetPrefixedCommandUsage(ctx, "cn-regex") + return cli.Command{ + Name: "cn-regex", + Usage: "add or remove common name regex patterns", + UsageText: fmt.Sprintf(`**%s** [**--remove**] [**--provisioner**=] +[**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] +[**--admin-provisioner**=] [**--admin-password-file**=] +[**--ca-url**=] [**--root**=] [**--context**=]`, commandName), + Description: fmt.Sprintf(`**%s** command manages common name regex patterns in policies. + +Common name regex patterns allow flexible matching of certificate Subject Common Names +using regular expressions. + +## EXAMPLES + +Allow any subdomain of example.com in Common Name +''' +$ step ca policy authority x509 allow cn-regex '^.*\.example\.com$' +''' + +Deny test certificates using regex +''' +$ step ca policy authority x509 deny cn-regex '^test-.*$' +''' + +Allow production server naming pattern +''' +$ step ca policy authority x509 allow cn-regex '^prod-[a-z]+-[0-9]+\.example\.com$' +''' + +Remove a regex pattern +''' +$ step ca policy authority x509 allow cn-regex '^.*\.example\.com$' --remove +'''`, commandName), + Action: command.InjectContext( + ctx, + commonNameRegexAction, + ), + Flags: []cli.Flag{ + flags.Provisioner, + cli.BoolFlag{ + Name: "remove", + Usage: `removes the provided common name regex patterns from the policy instead of adding them`, + }, + flags.AdminCert, + flags.AdminKey, + flags.AdminSubject, + flags.AdminProvisioner, + flags.AdminPasswordFile, + flags.CaURL, + flags.Root, + flags.Context, + }, + } +} + +func commonNameRegexAction(ctx context.Context) (err error) { + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + args = clictx.Args() + ) + + if len(args) == 0 { + return errs.TooFewArguments(clictx) + } + + client, err := cautils.NewAdminClient(clictx) + if err != nil { + return fmt.Errorf("error creating admin client: %w", err) + } + + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) + if err != nil { + return fmt.Errorf("error retrieving policy: %w", err) + } + + shouldRemove := clictx.Bool("remove") + + switch { + case policycontext.IsSSHHostPolicy(ctx): + return errors.New("SSH host policy does not support common name regex patterns") + case policycontext.IsSSHUserPolicy(ctx): + return errors.New("SSH user policy does not support common name regex patterns") + case policycontext.IsX509Policy(ctx): + switch { + case policycontext.IsAllow(ctx): + policy.X509.Allow.CommonNameRegex = addOrRemoveArguments(policy.X509.Allow.CommonNameRegex, args, shouldRemove) + case policycontext.IsDeny(ctx): + policy.X509.Deny.CommonNameRegex = addOrRemoveArguments(policy.X509.Deny.CommonNameRegex, args, shouldRemove) + default: + panic("no allow nor deny context set") + } + default: + panic("no SSH nor X.509 context set") + } + + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) + if err != nil { + return fmt.Errorf("error updating policy: %w", err) + } + + return prettyPrint(updatedPolicy) +} diff --git a/command/ca/policy/actions/dns_regex.go b/command/ca/policy/actions/dns_regex.go new file mode 100644 index 00000000..f260cadb --- /dev/null +++ b/command/ca/policy/actions/dns_regex.go @@ -0,0 +1,142 @@ +package actions + +import ( + "context" + "errors" + "fmt" + + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/errs" + + "github.com/smallstep/cli/command/ca/policy/policycontext" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/internal/command" + "github.com/smallstep/cli/utils/cautils" +) + +// DNSRegexCommand returns the dns-regex policy subcommand. +func DNSRegexCommand(ctx context.Context) cli.Command { + commandName := policycontext.GetPrefixedCommandUsage(ctx, "dns-regex") + return cli.Command{ + Name: "dns-regex", + Usage: "add or remove DNS regex patterns", + UsageText: fmt.Sprintf(`**%s** [**--remove**] [**--provisioner**=] +[**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] +[**--admin-provisioner**=] [**--admin-password-file**=] +[**--ca-url**=] [**--root**=] [**--context**=]`, commandName), + Description: fmt.Sprintf(`**%s** command manages DNS regex patterns in policies. + +DNS regex patterns allow flexible matching of DNS names using regular expressions. +When a DNS name matches a permitted regex pattern, it is allowed (and further +domain-based checks are skipped). When a DNS name matches an excluded regex +pattern, it is denied. + +## EXAMPLES + +Allow any subdomain of example.com using regex +''' +$ step ca policy authority x509 allow dns-regex '^.*\.example\.com$' +''' + +Deny internal domains using regex +''' +$ step ca policy authority x509 deny dns-regex '^internal\..*$' +''' + +Allow production server naming pattern +''' +$ step ca policy authority x509 allow dns-regex '^prod-[a-z0-9]+-[0-9]+\.example\.com$' +''' + +Allow multiple environments +''' +$ step ca policy authority x509 allow dns-regex '^(prod|staging|dev)-.*\.example\.com$' +''' + +Remove a regex pattern +''' +$ step ca policy authority x509 allow dns-regex '^.*\.example\.com$' --remove +''' + +Allow DNS regex in SSH host certificates +''' +$ step ca policy authority ssh host allow dns-regex '^.*\.internal\.example\.com$' +'''`, commandName), + Action: command.InjectContext( + ctx, + dnsRegexAction, + ), + Flags: []cli.Flag{ + flags.Provisioner, + cli.BoolFlag{ + Name: "remove", + Usage: `removes the provided DNS regex patterns from the policy instead of adding them`, + }, + flags.AdminCert, + flags.AdminKey, + flags.AdminSubject, + flags.AdminProvisioner, + flags.AdminPasswordFile, + flags.CaURL, + flags.Root, + flags.Context, + }, + } +} + +func dnsRegexAction(ctx context.Context) (err error) { + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + args = clictx.Args() + ) + + if len(args) == 0 { + return errs.TooFewArguments(clictx) + } + + client, err := cautils.NewAdminClient(clictx) + if err != nil { + return fmt.Errorf("error creating admin client: %w", err) + } + + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) + if err != nil { + return fmt.Errorf("error retrieving policy: %w", err) + } + + shouldRemove := clictx.Bool("remove") + + switch { + case policycontext.IsSSHHostPolicy(ctx): + switch { + case policycontext.IsAllow(ctx): + policy.Ssh.Host.Allow.DnsRegex = addOrRemoveArguments(policy.Ssh.Host.Allow.DnsRegex, args, shouldRemove) + case policycontext.IsDeny(ctx): + policy.Ssh.Host.Deny.DnsRegex = addOrRemoveArguments(policy.Ssh.Host.Deny.DnsRegex, args, shouldRemove) + default: + panic("no allow nor deny context set") + } + case policycontext.IsSSHUserPolicy(ctx): + return errors.New("SSH user policy does not support DNS regex patterns") + case policycontext.IsX509Policy(ctx): + switch { + case policycontext.IsAllow(ctx): + policy.X509.Allow.DnsRegex = addOrRemoveArguments(policy.X509.Allow.DnsRegex, args, shouldRemove) + case policycontext.IsDeny(ctx): + policy.X509.Deny.DnsRegex = addOrRemoveArguments(policy.X509.Deny.DnsRegex, args, shouldRemove) + default: + panic("no allow nor deny context set") + } + default: + panic("no SSH nor X.509 context set") + } + + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) + if err != nil { + return fmt.Errorf("error updating policy: %w", err) + } + + return prettyPrint(updatedPolicy) +} diff --git a/command/ca/policy/actions/email_regex.go b/command/ca/policy/actions/email_regex.go new file mode 100644 index 00000000..f0f80b0e --- /dev/null +++ b/command/ca/policy/actions/email_regex.go @@ -0,0 +1,134 @@ +package actions + +import ( + "context" + "errors" + "fmt" + + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/errs" + + "github.com/smallstep/cli/command/ca/policy/policycontext" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/internal/command" + "github.com/smallstep/cli/utils/cautils" +) + +// EmailRegexCommand returns the email-regex policy subcommand. +func EmailRegexCommand(ctx context.Context) cli.Command { + commandName := policycontext.GetPrefixedCommandUsage(ctx, "email-regex") + return cli.Command{ + Name: "email-regex", + Usage: "add or remove email regex patterns", + UsageText: fmt.Sprintf(`**%s** [**--remove**] [**--provisioner**=] +[**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] +[**--admin-provisioner**=] [**--admin-password-file**=] +[**--ca-url**=] [**--root**=] [**--context**=]`, commandName), + Description: fmt.Sprintf(`**%s** command manages email regex patterns in policies. + +Email regex patterns allow flexible matching of email addresses using regular expressions. + +## EXAMPLES + +Allow any email from example.com using regex +''' +$ step ca policy authority x509 allow email-regex '^.*@example\.com$' +''' + +Deny admin emails using regex +''' +$ step ca policy authority x509 deny email-regex '^admin@.*$' +''' + +Allow only specific email patterns +''' +$ step ca policy authority x509 allow email-regex '^[a-z]+\.[a-z]+@example\.com$' +''' + +Remove a regex pattern +''' +$ step ca policy authority x509 allow email-regex '^.*@example\.com$' --remove +''' + +Allow email regex in SSH user certificates +''' +$ step ca policy authority ssh user allow email-regex '^.*@example\.com$' +'''`, commandName), + Action: command.InjectContext( + ctx, + emailRegexAction, + ), + Flags: []cli.Flag{ + flags.Provisioner, + cli.BoolFlag{ + Name: "remove", + Usage: `removes the provided email regex patterns from the policy instead of adding them`, + }, + flags.AdminCert, + flags.AdminKey, + flags.AdminSubject, + flags.AdminProvisioner, + flags.AdminPasswordFile, + flags.CaURL, + flags.Root, + flags.Context, + }, + } +} + +func emailRegexAction(ctx context.Context) (err error) { + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + args = clictx.Args() + ) + + if len(args) == 0 { + return errs.TooFewArguments(clictx) + } + + client, err := cautils.NewAdminClient(clictx) + if err != nil { + return fmt.Errorf("error creating admin client: %w", err) + } + + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) + if err != nil { + return fmt.Errorf("error retrieving policy: %w", err) + } + + shouldRemove := clictx.Bool("remove") + + switch { + case policycontext.IsSSHHostPolicy(ctx): + return errors.New("SSH host policy does not support email regex patterns") + case policycontext.IsSSHUserPolicy(ctx): + switch { + case policycontext.IsAllow(ctx): + policy.Ssh.User.Allow.EmailRegex = addOrRemoveArguments(policy.Ssh.User.Allow.EmailRegex, args, shouldRemove) + case policycontext.IsDeny(ctx): + policy.Ssh.User.Deny.EmailRegex = addOrRemoveArguments(policy.Ssh.User.Deny.EmailRegex, args, shouldRemove) + default: + panic("no allow nor deny context set") + } + case policycontext.IsX509Policy(ctx): + switch { + case policycontext.IsAllow(ctx): + policy.X509.Allow.EmailRegex = addOrRemoveArguments(policy.X509.Allow.EmailRegex, args, shouldRemove) + case policycontext.IsDeny(ctx): + policy.X509.Deny.EmailRegex = addOrRemoveArguments(policy.X509.Deny.EmailRegex, args, shouldRemove) + default: + panic("no allow nor deny context set") + } + default: + panic("no SSH nor X.509 context set") + } + + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) + if err != nil { + return fmt.Errorf("error updating policy: %w", err) + } + + return prettyPrint(updatedPolicy) +} diff --git a/command/ca/policy/actions/principal_regex.go b/command/ca/policy/actions/principal_regex.go new file mode 100644 index 00000000..f7a5cf49 --- /dev/null +++ b/command/ca/policy/actions/principal_regex.go @@ -0,0 +1,136 @@ +package actions + +import ( + "context" + "errors" + "fmt" + + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/errs" + + "github.com/smallstep/cli/command/ca/policy/policycontext" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/internal/command" + "github.com/smallstep/cli/utils/cautils" +) + +// PrincipalRegexCommand returns the principal-regex policy subcommand. +func PrincipalRegexCommand(ctx context.Context) cli.Command { + commandName := policycontext.GetPrefixedCommandUsage(ctx, "principal-regex") + return cli.Command{ + Name: "principal-regex", + Usage: "add or remove principal regex patterns", + UsageText: fmt.Sprintf(`**%s** [**--remove**] [**--provisioner**=] +[**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] +[**--admin-provisioner**=] [**--admin-password-file**=] +[**--ca-url**=] [**--root**=] [**--context**=]`, commandName), + Description: fmt.Sprintf(`**%s** command manages principal regex patterns in SSH policies. + +Principal regex patterns allow flexible matching of SSH certificate principals +using regular expressions. + +## EXAMPLES + +Allow user principals matching a pattern in SSH user certificates +''' +$ step ca policy authority ssh user allow principal-regex '^user-.*$' +''' + +Deny root principal using regex in SSH user certificates +''' +$ step ca policy authority ssh user deny principal-regex '^root$' +''' + +Allow host principals matching a naming pattern in SSH host certificates +''' +$ step ca policy authority ssh host allow principal-regex '^server-[0-9]+\.example\.com$' +''' + +Allow service account principals +''' +$ step ca policy authority ssh user allow principal-regex '^svc-[a-z]+-[a-z]+$' +''' + +Remove a regex pattern +''' +$ step ca policy authority ssh user allow principal-regex '^user-.*$' --remove +'''`, commandName), + Action: command.InjectContext( + ctx, + principalRegexAction, + ), + Flags: []cli.Flag{ + flags.Provisioner, + cli.BoolFlag{ + Name: "remove", + Usage: `removes the provided principal regex patterns from the policy instead of adding them`, + }, + flags.AdminCert, + flags.AdminKey, + flags.AdminSubject, + flags.AdminProvisioner, + flags.AdminPasswordFile, + flags.CaURL, + flags.Root, + flags.Context, + }, + } +} + +func principalRegexAction(ctx context.Context) (err error) { + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + args = clictx.Args() + ) + + if len(args) == 0 { + return errs.TooFewArguments(clictx) + } + + client, err := cautils.NewAdminClient(clictx) + if err != nil { + return fmt.Errorf("error creating admin client: %w", err) + } + + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) + if err != nil { + return fmt.Errorf("error retrieving policy: %w", err) + } + + shouldRemove := clictx.Bool("remove") + + switch { + case policycontext.IsSSHHostPolicy(ctx): + switch { + case policycontext.IsAllow(ctx): + policy.Ssh.Host.Allow.PrincipalRegex = addOrRemoveArguments(policy.Ssh.Host.Allow.PrincipalRegex, args, shouldRemove) + case policycontext.IsDeny(ctx): + policy.Ssh.Host.Deny.PrincipalRegex = addOrRemoveArguments(policy.Ssh.Host.Deny.PrincipalRegex, args, shouldRemove) + default: + panic("no allow nor deny context set") + } + case policycontext.IsSSHUserPolicy(ctx): + switch { + case policycontext.IsAllow(ctx): + policy.Ssh.User.Allow.PrincipalRegex = addOrRemoveArguments(policy.Ssh.User.Allow.PrincipalRegex, args, shouldRemove) + case policycontext.IsDeny(ctx): + policy.Ssh.User.Deny.PrincipalRegex = addOrRemoveArguments(policy.Ssh.User.Deny.PrincipalRegex, args, shouldRemove) + default: + panic("no allow nor deny context set") + } + case policycontext.IsX509Policy(ctx): + return errors.New("X.509 policy does not support principal regex patterns") + default: + panic("no SSH nor X.509 context set") + } + + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) + if err != nil { + return fmt.Errorf("error updating policy: %w", err) + } + + return prettyPrint(updatedPolicy) +} + diff --git a/command/ca/policy/actions/uri_constraint.go b/command/ca/policy/actions/uri_constraint.go new file mode 100644 index 00000000..aa13e3ec --- /dev/null +++ b/command/ca/policy/actions/uri_constraint.go @@ -0,0 +1,142 @@ +package actions + +import ( + "context" + "errors" + "fmt" + + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/errs" + + "github.com/smallstep/cli/command/ca/policy/policycontext" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/internal/command" + "github.com/smallstep/cli/utils/cautils" +) + +// URIConstraintCommand returns the uri-constraint policy subcommand. +func URIConstraintCommand(ctx context.Context) cli.Command { + commandName := policycontext.GetPrefixedCommandUsage(ctx, "uri-constraint") + return cli.Command{ + Name: "uri-constraint", + Usage: "add or remove enhanced URI constraints (with scheme and path support)", + UsageText: fmt.Sprintf(`**%s** [**--remove**] [**--provisioner**=] +[**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] +[**--admin-provisioner**=] [**--admin-password-file**=] +[**--ca-url**=] [**--root**=] [**--context**=]`, commandName), + Description: fmt.Sprintf(`**%s** command manages enhanced URI constraints in policies. + +Enhanced URI constraints allow you to match URIs based on scheme, domain, and path, +providing more granular control than simple domain-based URI matching. + +## CONSTRAINT FORMAT + +The constraint can be specified in the following formats: + - **domain** - Match any URI with this domain (backwards compatible) + - **scheme://domain** - Match URIs with this scheme and domain + - **scheme://domain/path** - Match URIs with this scheme, domain, and exact path + - **scheme://domain/path/*** - Match URIs with this scheme, domain, and path prefix + - ***.domain** - Match any URI with a subdomain of this domain + +## EXAMPLES + +Allow HTTPS URIs for example.com in X.509 certificates on authority level +''' +$ step ca policy authority x509 allow uri-constraint "https://example.com" +''' + +Allow HTTPS API URIs with path prefix +''' +$ step ca policy authority x509 allow uri-constraint "https://api.example.com/api/*" +''' + +Allow SPIFFE URIs for a specific trust domain +''' +$ step ca policy authority x509 allow uri-constraint "spiffe://trust.domain" +''' + +Allow SPIFFE URIs with a specific workload path +''' +$ step ca policy authority x509 allow uri-constraint "spiffe://trust.domain/workload/*" +''' + +Deny HTTP (non-secure) URIs +''' +$ step ca policy authority x509 deny uri-constraint "http://" +''' + +Remove a constraint +''' +$ step ca policy authority x509 allow uri-constraint "https://example.com" --remove +'''`, commandName), + Action: command.InjectContext( + ctx, + uriConstraintAction, + ), + Flags: []cli.Flag{ + flags.Provisioner, + cli.BoolFlag{ + Name: "remove", + Usage: `removes the provided URI constraints from the policy instead of adding them`, + }, + flags.AdminCert, + flags.AdminKey, + flags.AdminSubject, + flags.AdminProvisioner, + flags.AdminPasswordFile, + flags.CaURL, + flags.Root, + flags.Context, + }, + } +} + +func uriConstraintAction(ctx context.Context) (err error) { + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + args = clictx.Args() + ) + + if len(args) == 0 { + return errs.TooFewArguments(clictx) + } + + client, err := cautils.NewAdminClient(clictx) + if err != nil { + return fmt.Errorf("error creating admin client: %w", err) + } + + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) + if err != nil { + return fmt.Errorf("error retrieving policy: %w", err) + } + + shouldRemove := clictx.Bool("remove") + + switch { + case policycontext.IsSSHHostPolicy(ctx): + return errors.New("SSH host policy does not support URI constraints") + case policycontext.IsSSHUserPolicy(ctx): + return errors.New("SSH user policy does not support URI constraints") + case policycontext.IsX509Policy(ctx): + switch { + case policycontext.IsAllow(ctx): + policy.X509.Allow.UriConstraints = addOrRemoveArguments(policy.X509.Allow.UriConstraints, args, shouldRemove) + case policycontext.IsDeny(ctx): + policy.X509.Deny.UriConstraints = addOrRemoveArguments(policy.X509.Deny.UriConstraints, args, shouldRemove) + default: + panic("no allow nor deny context set") + } + default: + panic("no SSH nor X.509 context set") + } + + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) + if err != nil { + return fmt.Errorf("error updating policy: %w", err) + } + + return prettyPrint(updatedPolicy) +} diff --git a/command/ca/policy/actions/uri_regex.go b/command/ca/policy/actions/uri_regex.go new file mode 100644 index 00000000..bf6b4b25 --- /dev/null +++ b/command/ca/policy/actions/uri_regex.go @@ -0,0 +1,128 @@ +package actions + +import ( + "context" + "errors" + "fmt" + + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/errs" + + "github.com/smallstep/cli/command/ca/policy/policycontext" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/internal/command" + "github.com/smallstep/cli/utils/cautils" +) + +// URIRegexCommand returns the uri-regex policy subcommand. +func URIRegexCommand(ctx context.Context) cli.Command { + commandName := policycontext.GetPrefixedCommandUsage(ctx, "uri-regex") + return cli.Command{ + Name: "uri-regex", + Usage: "add or remove URI regex patterns", + UsageText: fmt.Sprintf(`**%s** [**--remove**] [**--provisioner**=] +[**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] +[**--admin-provisioner**=] [**--admin-password-file**=] +[**--ca-url**=] [**--root**=] [**--context**=]`, commandName), + Description: fmt.Sprintf(`**%s** command manages URI regex patterns in policies. + +URI regex patterns allow flexible matching of full URIs using regular expressions. +The regex is matched against the complete URI string (scheme://host/path). + +## EXAMPLES + +Allow HTTPS API URIs using regex +''' +$ step ca policy authority x509 allow uri-regex '^https://.*\.example\.com/api/.*$' +''' + +Allow SPIFFE URIs for any workload in a trust domain +''' +$ step ca policy authority x509 allow uri-regex '^spiffe://trust\.domain/.*$' +''' + +Deny HTTP (non-secure) URIs using regex +''' +$ step ca policy authority x509 deny uri-regex '^http://.*$' +''' + +Allow specific URI path patterns +''' +$ step ca policy authority x509 allow uri-regex '^https://example\.com/api/v[0-9]+/.*$' +''' + +Remove a regex pattern +''' +$ step ca policy authority x509 allow uri-regex '^https://.*\.example\.com/api/.*$' --remove +'''`, commandName), + Action: command.InjectContext( + ctx, + uriRegexAction, + ), + Flags: []cli.Flag{ + flags.Provisioner, + cli.BoolFlag{ + Name: "remove", + Usage: `removes the provided URI regex patterns from the policy instead of adding them`, + }, + flags.AdminCert, + flags.AdminKey, + flags.AdminSubject, + flags.AdminProvisioner, + flags.AdminPasswordFile, + flags.CaURL, + flags.Root, + flags.Context, + }, + } +} + +func uriRegexAction(ctx context.Context) (err error) { + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + args = clictx.Args() + ) + + if len(args) == 0 { + return errs.TooFewArguments(clictx) + } + + client, err := cautils.NewAdminClient(clictx) + if err != nil { + return fmt.Errorf("error creating admin client: %w", err) + } + + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) + if err != nil { + return fmt.Errorf("error retrieving policy: %w", err) + } + + shouldRemove := clictx.Bool("remove") + + switch { + case policycontext.IsSSHHostPolicy(ctx): + return errors.New("SSH host policy does not support URI regex patterns") + case policycontext.IsSSHUserPolicy(ctx): + return errors.New("SSH user policy does not support URI regex patterns") + case policycontext.IsX509Policy(ctx): + switch { + case policycontext.IsAllow(ctx): + policy.X509.Allow.UriRegex = addOrRemoveArguments(policy.X509.Allow.UriRegex, args, shouldRemove) + case policycontext.IsDeny(ctx): + policy.X509.Deny.UriRegex = addOrRemoveArguments(policy.X509.Deny.UriRegex, args, shouldRemove) + default: + panic("no allow nor deny context set") + } + default: + panic("no SSH nor X.509 context set") + } + + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) + if err != nil { + return fmt.Errorf("error updating policy: %w", err) + } + + return prettyPrint(updatedPolicy) +} diff --git a/command/ca/policy/ssh/host/allow.go b/command/ca/policy/ssh/host/allow.go index ec6e70c7..ceb0814f 100644 --- a/command/ca/policy/ssh/host/allow.go +++ b/command/ca/policy/ssh/host/allow.go @@ -21,6 +21,9 @@ func allowCommand(ctx context.Context) cli.Command { actions.DNSCommand(ctx), actions.EmailCommand(ctx), actions.PrincipalsCommand(ctx), + // Regex pattern commands + actions.DNSRegexCommand(ctx), + actions.PrincipalRegexCommand(ctx), }, } } diff --git a/command/ca/policy/ssh/host/deny.go b/command/ca/policy/ssh/host/deny.go index 2277c806..1aec92ca 100644 --- a/command/ca/policy/ssh/host/deny.go +++ b/command/ca/policy/ssh/host/deny.go @@ -14,13 +14,16 @@ func denyCommand(ctx context.Context) cli.Command { ctx = policycontext.WithDeny(ctx) return cli.Command{ Name: "deny", - Usage: "manage denied dSSH host certificate principals", + Usage: "manage denied SSH host certificate principals", UsageText: "**step ca policy ssh host deny** [arguments] [global-flags] [subcommand-flags]", Description: `**step ca policy ssh host deny** command group provides facilities for managing SSH host certificate principals to be denied.`, Subcommands: cli.Commands{ actions.DNSCommand(ctx), actions.EmailCommand(ctx), actions.PrincipalsCommand(ctx), + // Regex pattern commands + actions.DNSRegexCommand(ctx), + actions.PrincipalRegexCommand(ctx), }, } } diff --git a/command/ca/policy/ssh/user/allow.go b/command/ca/policy/ssh/user/allow.go index 64015094..4e847488 100644 --- a/command/ca/policy/ssh/user/allow.go +++ b/command/ca/policy/ssh/user/allow.go @@ -20,6 +20,9 @@ func allowCommand(ctx context.Context) cli.Command { Subcommands: cli.Commands{ actions.EmailCommand(ctx), actions.PrincipalsCommand(ctx), + // Regex pattern commands + actions.EmailRegexCommand(ctx), + actions.PrincipalRegexCommand(ctx), }, } } diff --git a/command/ca/policy/ssh/user/deny.go b/command/ca/policy/ssh/user/deny.go index 9697d0fd..35ffebb8 100644 --- a/command/ca/policy/ssh/user/deny.go +++ b/command/ca/policy/ssh/user/deny.go @@ -20,6 +20,9 @@ func denyCommand(ctx context.Context) cli.Command { Subcommands: cli.Commands{ actions.EmailCommand(ctx), actions.PrincipalsCommand(ctx), + // Regex pattern commands + actions.EmailRegexCommand(ctx), + actions.PrincipalRegexCommand(ctx), }, } } diff --git a/command/ca/policy/x509/allow.go b/command/ca/policy/x509/allow.go index 6b10d0d2..48e4d58a 100644 --- a/command/ca/policy/x509/allow.go +++ b/command/ca/policy/x509/allow.go @@ -23,6 +23,13 @@ func allowCommand(ctx context.Context) cli.Command { actions.EmailCommand(ctx), actions.IPCommand(ctx), actions.URICommand(ctx), + // Enhanced URI constraints with scheme/path support + actions.URIConstraintCommand(ctx), + // Regex pattern commands + actions.DNSRegexCommand(ctx), + actions.EmailRegexCommand(ctx), + actions.URIRegexCommand(ctx), + actions.CommonNameRegexCommand(ctx), }, } } diff --git a/command/ca/policy/x509/deny.go b/command/ca/policy/x509/deny.go index b6742a2d..21422371 100644 --- a/command/ca/policy/x509/deny.go +++ b/command/ca/policy/x509/deny.go @@ -23,6 +23,13 @@ func denyCommand(ctx context.Context) cli.Command { actions.EmailCommand(ctx), actions.IPCommand(ctx), actions.URICommand(ctx), + // Enhanced URI constraints with scheme/path support + actions.URIConstraintCommand(ctx), + // Regex pattern commands + actions.DNSRegexCommand(ctx), + actions.EmailRegexCommand(ctx), + actions.URIRegexCommand(ctx), + actions.CommonNameRegexCommand(ctx), }, } } diff --git a/go.mod b/go.mod index 67218465..54de1d2a 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( cloud.google.com/go/auth v0.17.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/iam v1.5.3 // indirect cloud.google.com/go/longrunning v0.7.0 // indirect cloud.google.com/go/security v1.19.2 // indirect dario.cat/mergo v1.0.1 // indirect @@ -86,7 +86,7 @@ require ( github.com/google/go-tspi v0.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/googleapis/gax-go/v2 v2.16.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -128,18 +128,22 @@ require ( go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.39.0 // indirect - google.golang.org/api v0.256.0 // indirect - google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect - google.golang.org/grpc v1.77.0 // indirect - google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect + google.golang.org/api v0.258.0 // indirect + google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect howett.net/plist v1.0.0 // indirect ) + +replace github.com/smallstep/linkedca => ../linkedca + +replace github.com/smallstep/certificates => ../certificates diff --git a/go.sum b/go.sum index 03939dc7..c1116964 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= -cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k= cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= @@ -200,8 +200,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= +github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -291,16 +291,12 @@ github.com/slackhq/nebula v1.9.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE= github.com/slackhq/nebula v1.9.7/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= -github.com/smallstep/certificates v0.29.0 h1:f90szTKYTW62bmCc+qE5doGqIGPVxTQb8Ba37e/K8Zs= -github.com/smallstep/certificates v0.29.0/go.mod h1:27WI0od6gu84mvE4mYQ/QZGyYwHXvhsiSRNC+y3t+mo= github.com/smallstep/certinfo v1.15.0 h1:oxvuOr6KvwuXjgyg+gJEUJW6Gz9pm4uAGQ5tirpmTHg= github.com/smallstep/certinfo v1.15.0/go.mod h1:t5s4J23P3B/j68l2efuJFSZqCj0kBU8sa2FYbHRaffw= github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k= github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y= github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4= github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= -github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo= -github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc= github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE= github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU= github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= @@ -402,10 +398,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -471,18 +467,18 @@ golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= -google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= +google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc= +google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 h1:6Al3kEFFP9VJhRz3DID6quisgPnTeZVr4lep9kkxdPA= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0/go.mod h1:QLvsjh0OIR0TYBeiu2bkWGTJBUNQ64st52iWj/yA93I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=