Merge branch 'master' into crl-support
# Conflicts: # api/api.go # authority/config/config.go # cas/softcas/softcas.go # db/db.gopull/731/head
commit
60671b07d7
@ -0,0 +1,56 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "needs triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Tell us how to reproduce this issue.
|
||||
placeholder: These are the steps!
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: your-env
|
||||
attributes:
|
||||
label: Your Environment
|
||||
value: |-
|
||||
* OS -
|
||||
* `step-ca` Version -
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: What did you expect to happen?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: What happens instead?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: contributing
|
||||
attributes:
|
||||
label: Contributing
|
||||
value: |
|
||||
Vote on this issue by adding a 👍 reaction.
|
||||
To contribute a fix for this issue, leave a comment (and link to your pull request, if you've opened one already).
|
||||
validations:
|
||||
required: false
|
@ -1,27 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug, needs triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Subject of the issue
|
||||
Describe your issue here.
|
||||
|
||||
### Your environment
|
||||
* OS -
|
||||
* Version -
|
||||
|
||||
### Steps to reproduce
|
||||
Tell us how to reproduce this issue. Please provide a working demo, you can use [this template](https://plnkr.co/edit/XorWgI?p=preview) as a base.
|
||||
|
||||
### Expected behaviour
|
||||
Tell us what should happen
|
||||
|
||||
### Actual behaviour
|
||||
Tell us what happens instead
|
||||
|
||||
### Additional context
|
||||
Add any other context about the problem here.
|
@ -1,13 +1,24 @@
|
||||
---
|
||||
name: Enhancement
|
||||
about: Suggest an enhancement to step certificates
|
||||
about: Suggest an enhancement to step-ca
|
||||
title: ''
|
||||
labels: enhancement, needs triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### What would you like to be added
|
||||
## Hello!
|
||||
<!-- Please leave this section as-is,
|
||||
it's designed to help others in the community know how to interact with our GitHub issues. -->
|
||||
|
||||
- Vote on this issue by adding a 👍 reaction
|
||||
- If you want to implement this feature, comment to let us know (we'll work with you on design, scheduling, etc.)
|
||||
|
||||
### Why this is needed
|
||||
## Issue details
|
||||
|
||||
<!-- Enhancement requests are most helpful when they describe the problem you're having
|
||||
as well as articulating the potential solution you'd like to see built. -->
|
||||
|
||||
## Why is this needed?
|
||||
|
||||
<!-- Let us know why you think this enhancement would be good for the project or community. -->
|
||||
|
@ -1,4 +1,20 @@
|
||||
### Description
|
||||
Please describe your pull request.
|
||||
<!---
|
||||
Please provide answers in the spaces below each prompt, where applicable.
|
||||
Not every PR requires responses for each prompt.
|
||||
Use your discretion.
|
||||
-->
|
||||
#### Name of feature:
|
||||
|
||||
#### Pain or issue this feature alleviates:
|
||||
|
||||
#### Why is this important to the project (if not answered above):
|
||||
|
||||
#### Is there documentation on how to use this feature? If so, where?
|
||||
|
||||
#### In what environments or workflows is this feature supported?
|
||||
|
||||
#### In what environments or workflows is this feature explicitly NOT supported (if any)?
|
||||
|
||||
#### Supporting links/other PRs/issues:
|
||||
|
||||
💔Thank you!
|
||||
|
@ -0,0 +1,79 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client is the interface used to verify ACME challenges.
|
||||
type Client interface {
|
||||
// Get issues an HTTP GET to the specified URL.
|
||||
Get(url string) (*http.Response, error)
|
||||
|
||||
// LookupTXT returns the DNS TXT records for the given domain name.
|
||||
LookupTxt(name string) ([]string, error)
|
||||
|
||||
// TLSDial connects to the given network address using net.Dialer and then
|
||||
// initiates a TLS handshake, returning the resulting TLS connection.
|
||||
TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error)
|
||||
}
|
||||
|
||||
type clientKey struct{}
|
||||
|
||||
// NewClientContext adds the given client to the context.
|
||||
func NewClientContext(ctx context.Context, c Client) context.Context {
|
||||
return context.WithValue(ctx, clientKey{}, c)
|
||||
}
|
||||
|
||||
// ClientFromContext returns the current client from the given context.
|
||||
func ClientFromContext(ctx context.Context) (c Client, ok bool) {
|
||||
c, ok = ctx.Value(clientKey{}).(Client)
|
||||
return
|
||||
}
|
||||
|
||||
// MustClientFromContext returns the current client from the given context. It will
|
||||
// return a new instance of the client if it does not exist.
|
||||
func MustClientFromContext(ctx context.Context) Client {
|
||||
c, ok := ClientFromContext(ctx)
|
||||
if !ok {
|
||||
return NewClient()
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type client struct {
|
||||
http *http.Client
|
||||
dialer *net.Dialer
|
||||
}
|
||||
|
||||
// NewClient returns an implementation of Client for verifying ACME challenges.
|
||||
func NewClient() Client {
|
||||
return &client{
|
||||
http: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
dialer: &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) Get(url string) (*http.Response, error) {
|
||||
return c.http.Get(url)
|
||||
}
|
||||
|
||||
func (c *client) LookupTxt(name string) ([]string, error) {
|
||||
return net.LookupTXT(name)
|
||||
}
|
||||
|
||||
func (c *client) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) {
|
||||
return tls.DialWithDialer(c.dialer, network, addr, config)
|
||||
}
|
@ -1,56 +1,117 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
)
|
||||
|
||||
// Handler is the Admin API request handler.
|
||||
type Handler struct {
|
||||
adminDB admin.DB
|
||||
auth adminAuthority
|
||||
acmeDB acme.DB
|
||||
acmeResponder acmeAdminResponderInterface
|
||||
acmeResponder ACMEAdminResponder
|
||||
policyResponder PolicyAdminResponder
|
||||
}
|
||||
|
||||
// Route traffic and implement the Router interface.
|
||||
//
|
||||
// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder)
|
||||
func (h *Handler) Route(r api.Router) {
|
||||
Route(r, h.acmeResponder, h.policyResponder)
|
||||
}
|
||||
|
||||
// NewHandler returns a new Authority Config Handler.
|
||||
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface) api.RouterHandler {
|
||||
//
|
||||
// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder)
|
||||
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) api.RouterHandler {
|
||||
return &Handler{
|
||||
auth: auth,
|
||||
adminDB: adminDB,
|
||||
acmeDB: acmeDB,
|
||||
acmeResponder: acmeResponder,
|
||||
acmeResponder: acmeResponder,
|
||||
policyResponder: policyResponder,
|
||||
}
|
||||
}
|
||||
|
||||
var mustAuthority = func(ctx context.Context) adminAuthority {
|
||||
return authority.MustFromContext(ctx)
|
||||
}
|
||||
|
||||
// Route traffic and implement the Router interface.
|
||||
func (h *Handler) Route(r api.Router) {
|
||||
authnz := func(next nextHTTP) nextHTTP {
|
||||
return h.extractAuthorizeTokenAdmin(h.requireAPIEnabled(next))
|
||||
func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) {
|
||||
authnz := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return extractAuthorizeTokenAdmin(requireAPIEnabled(next))
|
||||
}
|
||||
|
||||
enabledInStandalone := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return checkAction(next, true)
|
||||
}
|
||||
|
||||
disabledInStandalone := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return checkAction(next, false)
|
||||
}
|
||||
|
||||
requireEABEnabled := func(next nextHTTP) nextHTTP {
|
||||
return h.requireEABEnabled(next)
|
||||
acmeEABMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return authnz(loadProvisionerByName(requireEABEnabled(next)))
|
||||
}
|
||||
|
||||
authorityPolicyMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return authnz(enabledInStandalone(next))
|
||||
}
|
||||
|
||||
provisionerPolicyMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return authnz(disabledInStandalone(loadProvisionerByName(next)))
|
||||
}
|
||||
|
||||
acmePolicyMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return authnz(disabledInStandalone(loadProvisionerByName(requireEABEnabled(loadExternalAccountKey(next)))))
|
||||
}
|
||||
|
||||
// Provisioners
|
||||
r.MethodFunc("GET", "/provisioners/{name}", authnz(h.GetProvisioner))
|
||||
r.MethodFunc("GET", "/provisioners", authnz(h.GetProvisioners))
|
||||
r.MethodFunc("POST", "/provisioners", authnz(h.CreateProvisioner))
|
||||
r.MethodFunc("PUT", "/provisioners/{name}", authnz(h.UpdateProvisioner))
|
||||
r.MethodFunc("DELETE", "/provisioners/{name}", authnz(h.DeleteProvisioner))
|
||||
r.MethodFunc("GET", "/provisioners/{name}", authnz(GetProvisioner))
|
||||
r.MethodFunc("GET", "/provisioners", authnz(GetProvisioners))
|
||||
r.MethodFunc("POST", "/provisioners", authnz(CreateProvisioner))
|
||||
r.MethodFunc("PUT", "/provisioners/{name}", authnz(UpdateProvisioner))
|
||||
r.MethodFunc("DELETE", "/provisioners/{name}", authnz(DeleteProvisioner))
|
||||
|
||||
// Admins
|
||||
r.MethodFunc("GET", "/admins/{id}", authnz(h.GetAdmin))
|
||||
r.MethodFunc("GET", "/admins", authnz(h.GetAdmins))
|
||||
r.MethodFunc("POST", "/admins", authnz(h.CreateAdmin))
|
||||
r.MethodFunc("PATCH", "/admins/{id}", authnz(h.UpdateAdmin))
|
||||
r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin))
|
||||
|
||||
// ACME External Account Binding Keys
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", authnz(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys)))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys)))
|
||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.CreateExternalAccountKey)))
|
||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(h.acmeResponder.DeleteExternalAccountKey)))
|
||||
r.MethodFunc("GET", "/admins/{id}", authnz(GetAdmin))
|
||||
r.MethodFunc("GET", "/admins", authnz(GetAdmins))
|
||||
r.MethodFunc("POST", "/admins", authnz(CreateAdmin))
|
||||
r.MethodFunc("PATCH", "/admins/{id}", authnz(UpdateAdmin))
|
||||
r.MethodFunc("DELETE", "/admins/{id}", authnz(DeleteAdmin))
|
||||
|
||||
// ACME responder
|
||||
if acmeResponder != nil {
|
||||
// ACME External Account Binding Keys
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.CreateExternalAccountKey))
|
||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(acmeResponder.DeleteExternalAccountKey))
|
||||
}
|
||||
|
||||
// Policy responder
|
||||
if policyResponder != nil {
|
||||
// Policy - Authority
|
||||
r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(policyResponder.GetAuthorityPolicy))
|
||||
r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(policyResponder.CreateAuthorityPolicy))
|
||||
r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(policyResponder.UpdateAuthorityPolicy))
|
||||
r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(policyResponder.DeleteAuthorityPolicy))
|
||||
|
||||
// Policy - Provisioner
|
||||
r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.GetProvisionerPolicy))
|
||||
r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.CreateProvisionerPolicy))
|
||||
r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.UpdateProvisionerPolicy))
|
||||
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.DeleteProvisionerPolicy))
|
||||
|
||||
// Policy - ACME Account
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy))
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,496 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api/read"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/certificates/authority/policy"
|
||||
)
|
||||
|
||||
// PolicyAdminResponder is the interface responsible for writing ACME admin
|
||||
// responses.
|
||||
type PolicyAdminResponder interface {
|
||||
GetAuthorityPolicy(w http.ResponseWriter, r *http.Request)
|
||||
CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request)
|
||||
UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request)
|
||||
DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request)
|
||||
GetProvisionerPolicy(w http.ResponseWriter, r *http.Request)
|
||||
CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request)
|
||||
UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request)
|
||||
DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request)
|
||||
GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
|
||||
CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
|
||||
UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
|
||||
DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// policyAdminResponder implements PolicyAdminResponder.
|
||||
type policyAdminResponder struct{}
|
||||
|
||||
// NewACMEAdminResponder returns a new PolicyAdminResponder.
|
||||
func NewPolicyAdminResponder() PolicyAdminResponder {
|
||||
return &policyAdminResponder{}
|
||||
}
|
||||
|
||||
// GetAuthorityPolicy handles the GET /admin/authority/policy request
|
||||
func (par *policyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := blockLinkedCA(ctx); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
auth := mustAuthority(ctx)
|
||||
authorityPolicy, err := auth.GetAuthorityPolicy(r.Context())
|
||||
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
if authorityPolicy == nil {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
render.ProtoJSONStatus(w, authorityPolicy, http.StatusOK)
|
||||
}
|
||||
|
||||
// CreateAuthorityPolicy handles the POST /admin/authority/policy request
|
||||
func (par *policyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := blockLinkedCA(ctx); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
auth := mustAuthority(ctx)
|
||||
authorityPolicy, err := auth.GetAuthorityPolicy(ctx)
|
||||
|
||||
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
if authorityPolicy != nil {
|
||||
adminErr := admin.NewError(admin.ErrorConflictType, "authority already has a policy")
|
||||
render.Error(w, adminErr)
|
||||
return
|
||||
}
|
||||
|
||||
var newPolicy = new(linkedca.Policy)
|
||||
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
newPolicy.Deduplicate()
|
||||
|
||||
if err := validatePolicy(newPolicy); err != nil {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
adm := linkedca.MustAdminFromContext(ctx)
|
||||
|
||||
var createdPolicy *linkedca.Policy
|
||||
if createdPolicy, err = auth.CreateAuthorityPolicy(ctx, adm, newPolicy); err != nil {
|
||||
if isBadRequest(err) {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error storing authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
render.Error(w, admin.WrapErrorISE(err, "error storing authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
render.ProtoJSONStatus(w, createdPolicy, http.StatusCreated)
|
||||
}
|
||||
|
||||
// UpdateAuthorityPolicy handles the PUT /admin/authority/policy request
|
||||
func (par *policyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := blockLinkedCA(ctx); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
auth := mustAuthority(ctx)
|
||||
authorityPolicy, err := auth.GetAuthorityPolicy(ctx)
|
||||
|
||||
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
if authorityPolicy == nil {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
var newPolicy = new(linkedca.Policy)
|
||||
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
newPolicy.Deduplicate()
|
||||
|
||||
if err := validatePolicy(newPolicy); err != nil {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
adm := linkedca.MustAdminFromContext(ctx)
|
||||
|
||||
var updatedPolicy *linkedca.Policy
|
||||
if updatedPolicy, err = auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil {
|
||||
if isBadRequest(err) {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
render.Error(w, admin.WrapErrorISE(err, "error updating authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
render.ProtoJSONStatus(w, updatedPolicy, http.StatusOK)
|
||||
}
|
||||
|
||||
// DeleteAuthorityPolicy handles the DELETE /admin/authority/policy request
|
||||
func (par *policyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := blockLinkedCA(ctx); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
auth := mustAuthority(ctx)
|
||||
authorityPolicy, err := auth.GetAuthorityPolicy(ctx)
|
||||
|
||||
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
if authorityPolicy == nil {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth.RemoveAuthorityPolicy(ctx); err != nil {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error deleting authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
|
||||
}
|
||||
|
||||
// GetProvisionerPolicy handles the GET /admin/provisioners/{name}/policy request
|
||||
func (par *policyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := blockLinkedCA(ctx); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
provisionerPolicy := prov.GetPolicy()
|
||||
if provisionerPolicy == nil {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
render.ProtoJSONStatus(w, provisionerPolicy, http.StatusOK)
|
||||
}
|
||||
|
||||
// CreateProvisionerPolicy handles the POST /admin/provisioners/{name}/policy request
|
||||
func (par *policyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := blockLinkedCA(ctx); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
provisionerPolicy := prov.GetPolicy()
|
||||
if provisionerPolicy != nil {
|
||||
adminErr := admin.NewError(admin.ErrorConflictType, "provisioner %s already has a policy", prov.Name)
|
||||
render.Error(w, adminErr)
|
||||
return
|
||||
}
|
||||
|
||||
var newPolicy = new(linkedca.Policy)
|
||||
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
newPolicy.Deduplicate()
|
||||
|
||||
if err := validatePolicy(newPolicy); err != nil {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating provisioner policy"))
|
||||
return
|
||||
}
|
||||
|
||||
prov.Policy = newPolicy
|
||||
auth := mustAuthority(ctx)
|
||||
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
|
||||
if isBadRequest(err) {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner policy"))
|
||||
return
|
||||
}
|
||||
|
||||
render.Error(w, admin.WrapErrorISE(err, "error creating provisioner policy"))
|
||||
return
|
||||
}
|
||||
|
||||
render.ProtoJSONStatus(w, newPolicy, http.StatusCreated)
|
||||
}
|
||||
|
||||
// UpdateProvisionerPolicy handles the PUT /admin/provisioners/{name}/policy request
|
||||
func (par *policyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := blockLinkedCA(ctx); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
provisionerPolicy := prov.GetPolicy()
|
||||
if provisionerPolicy == nil {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
var newPolicy = new(linkedca.Policy)
|
||||
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
newPolicy.Deduplicate()
|
||||
|
||||
if err := validatePolicy(newPolicy); err != nil {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating provisioner policy"))
|
||||
return
|
||||
}
|
||||
|
||||
prov.Policy = newPolicy
|
||||
auth := mustAuthority(ctx)
|
||||
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
|
||||
if isBadRequest(err) {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner policy"))
|
||||
return
|
||||
}
|
||||
|
||||
render.Error(w, admin.WrapErrorISE(err, "error updating provisioner policy"))
|
||||
return
|
||||
}
|
||||
|
||||
render.ProtoJSONStatus(w, newPolicy, http.StatusOK)
|
||||
}
|
||||
|
||||
// DeleteProvisionerPolicy handles the DELETE /admin/provisioners/{name}/policy request
|
||||
func (par *policyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := blockLinkedCA(ctx); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
if prov.Policy == nil {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
// remove the policy
|
||||
prov.Policy = nil
|
||||
|
||||
auth := mustAuthority(ctx)
|
||||
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error deleting provisioner policy"))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
|
||||
}
|
||||
|
||||
func (par *policyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := blockLinkedCA(ctx); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
eak := linkedca.MustExternalAccountKeyFromContext(ctx)
|
||||
eakPolicy := eak.GetPolicy()
|
||||
if eakPolicy == nil {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
render.ProtoJSONStatus(w, eakPolicy, http.StatusOK)
|
||||
}
|
||||
|
||||
func (par *policyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := blockLinkedCA(ctx); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
eak := linkedca.MustExternalAccountKeyFromContext(ctx)
|
||||
eakPolicy := eak.GetPolicy()
|
||||
if eakPolicy != nil {
|
||||
adminErr := admin.NewError(admin.ErrorConflictType, "ACME EAK %s already has a policy", eak.Id)
|
||||
render.Error(w, adminErr)
|
||||
return
|
||||
}
|
||||
|
||||
var newPolicy = new(linkedca.Policy)
|
||||
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
newPolicy.Deduplicate()
|
||||
|
||||
if err := validatePolicy(newPolicy); err != nil {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating ACME EAK policy"))
|
||||
return
|
||||
}
|
||||
|
||||
eak.Policy = newPolicy
|
||||
|
||||
acmeEAK := linkedEAKToCertificates(eak)
|
||||
acmeDB := acme.MustDatabaseFromContext(ctx)
|
||||
if err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error creating ACME EAK policy"))
|
||||
return
|
||||
}
|
||||
|
||||
render.ProtoJSONStatus(w, newPolicy, http.StatusCreated)
|
||||
}
|
||||
|
||||
func (par *policyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := blockLinkedCA(ctx); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
eak := linkedca.MustExternalAccountKeyFromContext(ctx)
|
||||
eakPolicy := eak.GetPolicy()
|
||||
if eakPolicy == nil {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
var newPolicy = new(linkedca.Policy)
|
||||
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
newPolicy.Deduplicate()
|
||||
|
||||
if err := validatePolicy(newPolicy); err != nil {
|
||||
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating ACME EAK policy"))
|
||||
return
|
||||
}
|
||||
|
||||
eak.Policy = newPolicy
|
||||
acmeEAK := linkedEAKToCertificates(eak)
|
||||
acmeDB := acme.MustDatabaseFromContext(ctx)
|
||||
if err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error updating ACME EAK policy"))
|
||||
return
|
||||
}
|
||||
|
||||
render.ProtoJSONStatus(w, newPolicy, http.StatusOK)
|
||||
}
|
||||
|
||||
func (par *policyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := blockLinkedCA(ctx); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
eak := linkedca.MustExternalAccountKeyFromContext(ctx)
|
||||
eakPolicy := eak.GetPolicy()
|
||||
if eakPolicy == nil {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
// remove the policy
|
||||
eak.Policy = nil
|
||||
|
||||
acmeEAK := linkedEAKToCertificates(eak)
|
||||
acmeDB := acme.MustDatabaseFromContext(ctx)
|
||||
if err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error deleting ACME EAK policy"))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
|
||||
}
|
||||
|
||||
// blockLinkedCA blocks all API operations on linked deployments
|
||||
func blockLinkedCA(ctx context.Context) error {
|
||||
// temporary blocking linked deployments
|
||||
adminDB := admin.MustFromContext(ctx)
|
||||
if a, ok := adminDB.(interface{ IsLinkedCA() bool }); ok && a.IsLinkedCA() {
|
||||
return admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isBadRequest checks if an error should result in a bad request error
|
||||
// returned to the client.
|
||||
func isBadRequest(err error) bool {
|
||||
var pe *authority.PolicyError
|
||||
isPolicyError := errors.As(err, &pe)
|
||||
return isPolicyError && (pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure)
|
||||
}
|
||||
|
||||
func validatePolicy(p *linkedca.Policy) error {
|
||||
|
||||
// convert the policy; return early if nil
|
||||
options := policy.LinkedToCertificates(p)
|
||||
if options == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// Initialize a temporary x509 allow/deny policy engine
|
||||
if _, err = policy.NewX509PolicyEngine(options.GetX509Options()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize a temporary SSH allow/deny policy engine for host certificates
|
||||
if _, err = policy.NewSSHHostPolicyEngine(options.GetSSHOptions()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize a temporary SSH allow/deny policy engine for user certificates
|
||||
if _, err = policy.NewSSHUserPolicyEngine(options.GetSSHOptions()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,339 @@
|
||||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
type dbX509Policy struct {
|
||||
Allow *dbX509Names `json:"allow,omitempty"`
|
||||
Deny *dbX509Names `json:"deny,omitempty"`
|
||||
AllowWildcardNames bool `json:"allow_wildcard_names,omitempty"`
|
||||
}
|
||||
|
||||
type dbX509Names struct {
|
||||
CommonNames []string `json:"cn,omitempty"`
|
||||
DNSDomains []string `json:"dns,omitempty"`
|
||||
IPRanges []string `json:"ip,omitempty"`
|
||||
EmailAddresses []string `json:"email,omitempty"`
|
||||
URIDomains []string `json:"uri,omitempty"`
|
||||
}
|
||||
|
||||
type dbSSHPolicy struct {
|
||||
// User contains SSH user certificate options.
|
||||
User *dbSSHUserPolicy `json:"user,omitempty"`
|
||||
// Host contains SSH host certificate options.
|
||||
Host *dbSSHHostPolicy `json:"host,omitempty"`
|
||||
}
|
||||
|
||||
type dbSSHHostPolicy struct {
|
||||
Allow *dbSSHHostNames `json:"allow,omitempty"`
|
||||
Deny *dbSSHHostNames `json:"deny,omitempty"`
|
||||
}
|
||||
|
||||
type dbSSHHostNames struct {
|
||||
DNSDomains []string `json:"dns,omitempty"`
|
||||
IPRanges []string `json:"ip,omitempty"`
|
||||
Principals []string `json:"principal,omitempty"`
|
||||
}
|
||||
|
||||
type dbSSHUserPolicy struct {
|
||||
Allow *dbSSHUserNames `json:"allow,omitempty"`
|
||||
Deny *dbSSHUserNames `json:"deny,omitempty"`
|
||||
}
|
||||
|
||||
type dbSSHUserNames struct {
|
||||
EmailAddresses []string `json:"email,omitempty"`
|
||||
Principals []string `json:"principal,omitempty"`
|
||||
}
|
||||
|
||||
type dbPolicy struct {
|
||||
X509 *dbX509Policy `json:"x509,omitempty"`
|
||||
SSH *dbSSHPolicy `json:"ssh,omitempty"`
|
||||
}
|
||||
|
||||
type dbAuthorityPolicy struct {
|
||||
ID string `json:"id"`
|
||||
AuthorityID string `json:"authorityID"`
|
||||
Policy *dbPolicy `json:"policy,omitempty"`
|
||||
}
|
||||
|
||||
func (dbap *dbAuthorityPolicy) convert() *linkedca.Policy {
|
||||
if dbap == nil {
|
||||
return nil
|
||||
}
|
||||
return dbToLinked(dbap.Policy)
|
||||
}
|
||||
|
||||
func (db *DB) getDBAuthorityPolicyBytes(ctx context.Context, authorityID string) ([]byte, error) {
|
||||
data, err := db.db.Get(authorityPoliciesTable, []byte(authorityID))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, admin.NewError(admin.ErrorNotFoundType, "authority policy not found")
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("error loading authority policy: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (db *DB) unmarshalDBAuthorityPolicy(data []byte) (*dbAuthorityPolicy, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var dba = new(dbAuthorityPolicy)
|
||||
if err := json.Unmarshal(data, dba); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling policy bytes into dbAuthorityPolicy: %w", err)
|
||||
}
|
||||
return dba, nil
|
||||
}
|
||||
|
||||
func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*dbAuthorityPolicy, error) {
|
||||
data, err := db.getDBAuthorityPolicyBytes(ctx, authorityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbap, err := db.unmarshalDBAuthorityPolicy(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dbap == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if dbap.AuthorityID != authorityID {
|
||||
return nil, admin.NewError(admin.ErrorAuthorityMismatchType,
|
||||
"authority policy is not owned by authority %s", authorityID)
|
||||
}
|
||||
return dbap, nil
|
||||
}
|
||||
|
||||
func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||
|
||||
dbap := &dbAuthorityPolicy{
|
||||
ID: db.authorityID,
|
||||
AuthorityID: db.authorityID,
|
||||
Policy: linkedToDB(policy),
|
||||
}
|
||||
|
||||
if err := db.save(ctx, dbap.ID, dbap, nil, "authority_policy", authorityPoliciesTable); err != nil {
|
||||
return admin.WrapErrorISE(err, "error creating authority policy")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
|
||||
dbap, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dbap.convert(), nil
|
||||
}
|
||||
|
||||
func (db *DB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||
old, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbap := &dbAuthorityPolicy{
|
||||
ID: db.authorityID,
|
||||
AuthorityID: db.authorityID,
|
||||
Policy: linkedToDB(policy),
|
||||
}
|
||||
|
||||
if err := db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable); err != nil {
|
||||
return admin.WrapErrorISE(err, "error updating authority policy")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) DeleteAuthorityPolicy(ctx context.Context) error {
|
||||
old, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.save(ctx, old.ID, nil, old, "authority_policy", authorityPoliciesTable); err != nil {
|
||||
return admin.WrapErrorISE(err, "error deleting authority policy")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dbToLinked(p *dbPolicy) *linkedca.Policy {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
r := &linkedca.Policy{}
|
||||
if x509 := p.X509; x509 != nil {
|
||||
r.X509 = &linkedca.X509Policy{}
|
||||
if allow := x509.Allow; allow != nil {
|
||||
r.X509.Allow = &linkedca.X509Names{}
|
||||
r.X509.Allow.Dns = allow.DNSDomains
|
||||
r.X509.Allow.Emails = allow.EmailAddresses
|
||||
r.X509.Allow.Ips = allow.IPRanges
|
||||
r.X509.Allow.Uris = allow.URIDomains
|
||||
r.X509.Allow.CommonNames = allow.CommonNames
|
||||
}
|
||||
if deny := x509.Deny; deny != nil {
|
||||
r.X509.Deny = &linkedca.X509Names{}
|
||||
r.X509.Deny.Dns = deny.DNSDomains
|
||||
r.X509.Deny.Emails = deny.EmailAddresses
|
||||
r.X509.Deny.Ips = deny.IPRanges
|
||||
r.X509.Deny.Uris = deny.URIDomains
|
||||
r.X509.Deny.CommonNames = deny.CommonNames
|
||||
}
|
||||
r.X509.AllowWildcardNames = x509.AllowWildcardNames
|
||||
}
|
||||
if ssh := p.SSH; ssh != nil {
|
||||
r.Ssh = &linkedca.SSHPolicy{}
|
||||
if host := ssh.Host; host != nil {
|
||||
r.Ssh.Host = &linkedca.SSHHostPolicy{}
|
||||
if allow := host.Allow; allow != nil {
|
||||
r.Ssh.Host.Allow = &linkedca.SSHHostNames{}
|
||||
r.Ssh.Host.Allow.Dns = allow.DNSDomains
|
||||
r.Ssh.Host.Allow.Ips = allow.IPRanges
|
||||
r.Ssh.Host.Allow.Principals = allow.Principals
|
||||
}
|
||||
if deny := host.Deny; deny != nil {
|
||||
r.Ssh.Host.Deny = &linkedca.SSHHostNames{}
|
||||
r.Ssh.Host.Deny.Dns = deny.DNSDomains
|
||||
r.Ssh.Host.Deny.Ips = deny.IPRanges
|
||||
r.Ssh.Host.Deny.Principals = deny.Principals
|
||||
}
|
||||
}
|
||||
if user := ssh.User; user != nil {
|
||||
r.Ssh.User = &linkedca.SSHUserPolicy{}
|
||||
if allow := user.Allow; allow != nil {
|
||||
r.Ssh.User.Allow = &linkedca.SSHUserNames{}
|
||||
r.Ssh.User.Allow.Emails = allow.EmailAddresses
|
||||
r.Ssh.User.Allow.Principals = allow.Principals
|
||||
}
|
||||
if deny := user.Deny; deny != nil {
|
||||
r.Ssh.User.Deny = &linkedca.SSHUserNames{}
|
||||
r.Ssh.User.Deny.Emails = deny.EmailAddresses
|
||||
r.Ssh.User.Deny.Principals = deny.Principals
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func linkedToDB(p *linkedca.Policy) *dbPolicy {
|
||||
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// return early if x509 nor SSH is set
|
||||
if p.GetX509() == nil && p.GetSsh() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
r := &dbPolicy{}
|
||||
// fill x509 policy configuration
|
||||
if x509 := p.GetX509(); x509 != nil {
|
||||
r.X509 = &dbX509Policy{}
|
||||
if allow := x509.GetAllow(); allow != nil {
|
||||
r.X509.Allow = &dbX509Names{}
|
||||
if allow.Dns != nil {
|
||||
r.X509.Allow.DNSDomains = allow.Dns
|
||||
}
|
||||
if allow.Ips != nil {
|
||||
r.X509.Allow.IPRanges = allow.Ips
|
||||
}
|
||||
if allow.Emails != nil {
|
||||
r.X509.Allow.EmailAddresses = allow.Emails
|
||||
}
|
||||
if allow.Uris != nil {
|
||||
r.X509.Allow.URIDomains = allow.Uris
|
||||
}
|
||||
if allow.CommonNames != nil {
|
||||
r.X509.Allow.CommonNames = allow.CommonNames
|
||||
}
|
||||
}
|
||||
if deny := x509.GetDeny(); deny != nil {
|
||||
r.X509.Deny = &dbX509Names{}
|
||||
if deny.Dns != nil {
|
||||
r.X509.Deny.DNSDomains = deny.Dns
|
||||
}
|
||||
if deny.Ips != nil {
|
||||
r.X509.Deny.IPRanges = deny.Ips
|
||||
}
|
||||
if deny.Emails != nil {
|
||||
r.X509.Deny.EmailAddresses = deny.Emails
|
||||
}
|
||||
if deny.Uris != nil {
|
||||
r.X509.Deny.URIDomains = deny.Uris
|
||||
}
|
||||
if deny.CommonNames != nil {
|
||||
r.X509.Deny.CommonNames = deny.CommonNames
|
||||
}
|
||||
}
|
||||
|
||||
r.X509.AllowWildcardNames = x509.GetAllowWildcardNames()
|
||||
}
|
||||
|
||||
// fill ssh policy configuration
|
||||
if ssh := p.GetSsh(); ssh != nil {
|
||||
r.SSH = &dbSSHPolicy{}
|
||||
if host := ssh.GetHost(); host != nil {
|
||||
r.SSH.Host = &dbSSHHostPolicy{}
|
||||
if allow := host.GetAllow(); allow != nil {
|
||||
r.SSH.Host.Allow = &dbSSHHostNames{}
|
||||
if allow.Dns != nil {
|
||||
r.SSH.Host.Allow.DNSDomains = allow.Dns
|
||||
}
|
||||
if allow.Ips != nil {
|
||||
r.SSH.Host.Allow.IPRanges = allow.Ips
|
||||
}
|
||||
if allow.Principals != nil {
|
||||
r.SSH.Host.Allow.Principals = allow.Principals
|
||||
}
|
||||
}
|
||||
if deny := host.GetDeny(); deny != nil {
|
||||
r.SSH.Host.Deny = &dbSSHHostNames{}
|
||||
if deny.Dns != nil {
|
||||
r.SSH.Host.Deny.DNSDomains = deny.Dns
|
||||
}
|
||||
if deny.Ips != nil {
|
||||
r.SSH.Host.Deny.IPRanges = deny.Ips
|
||||
}
|
||||
if deny.Principals != nil {
|
||||
r.SSH.Host.Deny.Principals = deny.Principals
|
||||
}
|
||||
}
|
||||
}
|
||||
if user := ssh.GetUser(); user != nil {
|
||||
r.SSH.User = &dbSSHUserPolicy{}
|
||||
if allow := user.GetAllow(); allow != nil {
|
||||
r.SSH.User.Allow = &dbSSHUserNames{}
|
||||
if allow.Emails != nil {
|
||||
r.SSH.User.Allow.EmailAddresses = allow.Emails
|
||||
}
|
||||
if allow.Principals != nil {
|
||||
r.SSH.User.Allow.Principals = allow.Principals
|
||||
}
|
||||
}
|
||||
if deny := user.GetDeny(); deny != nil {
|
||||
r.SSH.User.Deny = &dbSSHUserNames{}
|
||||
if deny.Emails != nil {
|
||||
r.SSH.User.Deny.EmailAddresses = deny.Emails
|
||||
}
|
||||
if deny.Principals != nil {
|
||||
r.SSH.User.Deny.Principals = deny.Principals
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,265 @@
|
||||
package authority
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
authPolicy "github.com/smallstep/certificates/authority/policy"
|
||||
policy "github.com/smallstep/certificates/policy"
|
||||
)
|
||||
|
||||
type policyErrorType int
|
||||
|
||||
const (
|
||||
AdminLockOut policyErrorType = iota + 1
|
||||
StoreFailure
|
||||
ReloadFailure
|
||||
ConfigurationFailure
|
||||
EvaluationFailure
|
||||
InternalFailure
|
||||
)
|
||||
|
||||
type PolicyError struct {
|
||||
Typ policyErrorType
|
||||
Err error
|
||||
}
|
||||
|
||||
func (p *PolicyError) Error() string {
|
||||
return p.Err.Error()
|
||||
}
|
||||
|
||||
func (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
|
||||
a.adminMutex.Lock()
|
||||
defer a.adminMutex.Unlock()
|
||||
|
||||
p, err := a.adminDB.GetAuthorityPolicy(ctx)
|
||||
if err != nil {
|
||||
return nil, &PolicyError{
|
||||
Typ: InternalFailure,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (a *Authority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) (*linkedca.Policy, error) {
|
||||
a.adminMutex.Lock()
|
||||
defer a.adminMutex.Unlock()
|
||||
|
||||
if err := a.checkAuthorityPolicy(ctx, adm, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := a.adminDB.CreateAuthorityPolicy(ctx, p); err != nil {
|
||||
return nil, &PolicyError{
|
||||
Typ: StoreFailure,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.reloadPolicyEngines(ctx); err != nil {
|
||||
return nil, &PolicyError{
|
||||
Typ: ReloadFailure,
|
||||
Err: fmt.Errorf("error reloading policy engines when creating authority policy: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) (*linkedca.Policy, error) {
|
||||
a.adminMutex.Lock()
|
||||
defer a.adminMutex.Unlock()
|
||||
|
||||
if err := a.checkAuthorityPolicy(ctx, adm, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := a.adminDB.UpdateAuthorityPolicy(ctx, p); err != nil {
|
||||
return nil, &PolicyError{
|
||||
Typ: StoreFailure,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.reloadPolicyEngines(ctx); err != nil {
|
||||
return nil, &PolicyError{
|
||||
Typ: ReloadFailure,
|
||||
Err: fmt.Errorf("error reloading policy engines when updating authority policy: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error {
|
||||
a.adminMutex.Lock()
|
||||
defer a.adminMutex.Unlock()
|
||||
|
||||
if err := a.adminDB.DeleteAuthorityPolicy(ctx); err != nil {
|
||||
return &PolicyError{
|
||||
Typ: StoreFailure,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.reloadPolicyEngines(ctx); err != nil {
|
||||
return &PolicyError{
|
||||
Typ: ReloadFailure,
|
||||
Err: fmt.Errorf("error reloading policy engines when deleting authority policy: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *linkedca.Admin, p *linkedca.Policy) error {
|
||||
|
||||
// no policy and thus nothing to evaluate; return early
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// get all current admins from the database
|
||||
allAdmins, err := a.adminDB.GetAdmins(ctx)
|
||||
if err != nil {
|
||||
return &PolicyError{
|
||||
Typ: InternalFailure,
|
||||
Err: fmt.Errorf("error retrieving admins: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
return a.checkPolicy(ctx, currentAdmin, allAdmins, p)
|
||||
}
|
||||
|
||||
func (a *Authority) checkProvisionerPolicy(ctx context.Context, provName string, p *linkedca.Policy) error {
|
||||
|
||||
// no policy and thus nothing to evaluate; return early
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// get all admins for the provisioner; ignoring case in which they're not found
|
||||
allProvisionerAdmins, _ := a.admins.LoadByProvisioner(provName)
|
||||
|
||||
// check the policy; pass in nil as the current admin, as all admins for the
|
||||
// provisioner will be checked by looping through allProvisionerAdmins. Also,
|
||||
// the current admin may be a super admin not belonging to the provisioner, so
|
||||
// can't be blocked, but is not required to be in the policy, either.
|
||||
return a.checkPolicy(ctx, nil, allProvisionerAdmins, p)
|
||||
}
|
||||
|
||||
// checkPolicy checks if a new or updated policy configuration results in the user
|
||||
// locking themselves or other admins out of the CA.
|
||||
func (a *Authority) checkPolicy(ctx context.Context, currentAdmin *linkedca.Admin, otherAdmins []*linkedca.Admin, p *linkedca.Policy) error {
|
||||
|
||||
// convert the policy; return early if nil
|
||||
policyOptions := authPolicy.LinkedToCertificates(p)
|
||||
if policyOptions == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
engine, err := authPolicy.NewX509PolicyEngine(policyOptions.GetX509Options())
|
||||
if err != nil {
|
||||
return &PolicyError{
|
||||
Typ: ConfigurationFailure,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// when an empty X.509 policy is provided, the resulting engine is nil
|
||||
// and there's no policy to evaluate.
|
||||
if engine == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(hs): Provide option to force the policy, even when the admin subject would be locked out?
|
||||
|
||||
// check if the admin user that instructed the authority policy to be
|
||||
// created or updated, would still be allowed when the provided policy
|
||||
// would be applied. This case is skipped when current admin is nil, which
|
||||
// is the case when a provisioner policy is checked.
|
||||
if currentAdmin != nil {
|
||||
sans := []string{currentAdmin.GetSubject()}
|
||||
if err := isAllowed(engine, sans); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// loop through admins to verify that none of them would be
|
||||
// locked out when the new policy were to be applied. Returns
|
||||
// an error with a message that includes the admin subject that
|
||||
// would be locked out.
|
||||
for _, adm := range otherAdmins {
|
||||
sans := []string{adm.GetSubject()}
|
||||
if err := isAllowed(engine, sans); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(hs): mask the error message for non-super admins?
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reloadPolicyEngines reloads x509 and SSH policy engines using
|
||||
// configuration stored in the DB or from the configuration file.
|
||||
func (a *Authority) reloadPolicyEngines(ctx context.Context) error {
|
||||
var (
|
||||
err error
|
||||
policyOptions *authPolicy.Options
|
||||
)
|
||||
|
||||
if a.config.AuthorityConfig.EnableAdmin {
|
||||
|
||||
// temporarily disable policy loading when LinkedCA is in use
|
||||
if _, ok := a.adminDB.(*linkedCaClient); ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
linkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx)
|
||||
if err != nil {
|
||||
var ae *admin.Error
|
||||
if isAdminError := errors.As(err, &ae); (isAdminError && ae.Type != admin.ErrorNotFoundType.String()) || !isAdminError {
|
||||
return fmt.Errorf("error getting policy to (re)load policy engines: %w", err)
|
||||
}
|
||||
}
|
||||
policyOptions = authPolicy.LinkedToCertificates(linkedPolicy)
|
||||
} else {
|
||||
policyOptions = a.config.AuthorityConfig.Policy
|
||||
}
|
||||
|
||||
engine, err := authPolicy.New(policyOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// only update the policy engine when no error was returned
|
||||
a.policyEngine = engine
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isAllowed(engine authPolicy.X509Policy, sans []string) error {
|
||||
if err := engine.AreSANsAllowed(sans); err != nil {
|
||||
var policyErr *policy.NamePolicyError
|
||||
isNamePolicyError := errors.As(err, &policyErr)
|
||||
if isNamePolicyError && policyErr.Reason == policy.NotAllowed {
|
||||
return &PolicyError{
|
||||
Typ: AdminLockOut,
|
||||
Err: fmt.Errorf("the provided policy would lock out %s from the CA. Please update your policy to include %s as an allowed name", sans, sans),
|
||||
}
|
||||
}
|
||||
return &PolicyError{
|
||||
Typ: EvaluationFailure,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// Engine is a container for multiple policies.
|
||||
type Engine struct {
|
||||
x509Policy X509Policy
|
||||
sshUserPolicy UserPolicy
|
||||
sshHostPolicy HostPolicy
|
||||
}
|
||||
|
||||
// New returns a new Engine using Options.
|
||||
func New(options *Options) (*Engine, error) {
|
||||
|
||||
// if no options provided, return early
|
||||
if options == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
x509Policy X509Policy
|
||||
sshHostPolicy HostPolicy
|
||||
sshUserPolicy UserPolicy
|
||||
err error
|
||||
)
|
||||
|
||||
// initialize the x509 allow/deny policy engine
|
||||
if x509Policy, err = NewX509PolicyEngine(options.GetX509Options()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// initialize the SSH allow/deny policy engine for host certificates
|
||||
if sshHostPolicy, err = NewSSHHostPolicyEngine(options.GetSSHOptions()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// initialize the SSH allow/deny policy engine for user certificates
|
||||
if sshUserPolicy, err = NewSSHUserPolicyEngine(options.GetSSHOptions()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Engine{
|
||||
x509Policy: x509Policy,
|
||||
sshHostPolicy: sshHostPolicy,
|
||||
sshUserPolicy: sshUserPolicy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsX509CertificateAllowed evaluates an X.509 certificate against
|
||||
// the X.509 policy (if available) and returns an error if one of the
|
||||
// names in the certificate is not allowed.
|
||||
func (e *Engine) IsX509CertificateAllowed(cert *x509.Certificate) error {
|
||||
|
||||
// return early if there's no policy to evaluate
|
||||
if e == nil || e.x509Policy == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// return result of X.509 policy evaluation
|
||||
return e.x509Policy.IsX509CertificateAllowed(cert)
|
||||
}
|
||||
|
||||
// AreSANsAllowed evaluates the slice of SANs against the X.509 policy
|
||||
// (if available) and returns an error if one of the SANs is not allowed.
|
||||
func (e *Engine) AreSANsAllowed(sans []string) error {
|
||||
|
||||
// return early if there's no policy to evaluate
|
||||
if e == nil || e.x509Policy == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// return result of X.509 policy evaluation
|
||||
return e.x509Policy.AreSANsAllowed(sans)
|
||||
}
|
||||
|
||||
// IsSSHCertificateAllowed evaluates an SSH certificate against the
|
||||
// user or host policy (if configured) and returns an error if one of the
|
||||
// principals in the certificate is not allowed.
|
||||
func (e *Engine) IsSSHCertificateAllowed(cert *ssh.Certificate) error {
|
||||
|
||||
// return early if there's no policy to evaluate
|
||||
if e == nil || (e.sshHostPolicy == nil && e.sshUserPolicy == nil) {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch cert.CertType {
|
||||
case ssh.HostCert:
|
||||
// when no host policy engine is configured, but a user policy engine is
|
||||
// configured, the host certificate is denied.
|
||||
if e.sshHostPolicy == nil && e.sshUserPolicy != nil {
|
||||
return errors.New("authority not allowed to sign ssh host certificates")
|
||||
}
|
||||
|
||||
// return result of SSH host policy evaluation
|
||||
return e.sshHostPolicy.IsSSHCertificateAllowed(cert)
|
||||
case ssh.UserCert:
|
||||
// when no user policy engine is configured, but a host policy engine is
|
||||
// configured, the user certificate is denied.
|
||||
if e.sshUserPolicy == nil && e.sshHostPolicy != nil {
|
||||
return errors.New("authority not allowed to sign ssh user certificates")
|
||||
}
|
||||
|
||||
// return result of SSH user policy evaluation
|
||||
return e.sshUserPolicy.IsSSHCertificateAllowed(cert)
|
||||
default:
|
||||
return fmt.Errorf("unexpected ssh certificate type %q", cert.CertType)
|
||||
}
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
package policy
|
||||
|
||||
// Options is a container for authority level x509 and SSH
|
||||
// policy configuration.
|
||||
type Options struct {
|
||||
X509 *X509PolicyOptions `json:"x509,omitempty"`
|
||||
SSH *SSHPolicyOptions `json:"ssh,omitempty"`
|
||||
}
|
||||
|
||||
// GetX509Options returns the x509 authority level policy
|
||||
// configuration
|
||||
func (o *Options) GetX509Options() *X509PolicyOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.X509
|
||||
}
|
||||
|
||||
// GetSSHOptions returns the SSH authority level policy
|
||||
// configuration
|
||||
func (o *Options) GetSSHOptions() *SSHPolicyOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.SSH
|
||||
}
|
||||
|
||||
// X509PolicyOptionsInterface is an interface for providers
|
||||
// of x509 allowed and denied names.
|
||||
type X509PolicyOptionsInterface interface {
|
||||
GetAllowedNameOptions() *X509NameOptions
|
||||
GetDeniedNameOptions() *X509NameOptions
|
||||
AreWildcardNamesAllowed() bool
|
||||
}
|
||||
|
||||
// X509PolicyOptions is a container for x509 allowed and denied
|
||||
// names.
|
||||
type X509PolicyOptions struct {
|
||||
// AllowedNames contains the x509 allowed names
|
||||
AllowedNames *X509NameOptions `json:"allow,omitempty"`
|
||||
|
||||
// DeniedNames contains the x509 denied names
|
||||
DeniedNames *X509NameOptions `json:"deny,omitempty"`
|
||||
|
||||
// AllowWildcardNames indicates if literal wildcard names
|
||||
// like *.example.com are allowed. Defaults to false.
|
||||
AllowWildcardNames bool `json:"allowWildcardNames,omitempty"`
|
||||
}
|
||||
|
||||
// X509NameOptions models the X509 name policy configuration.
|
||||
type X509NameOptions struct {
|
||||
CommonNames []string `json:"cn,omitempty"`
|
||||
DNSDomains []string `json:"dns,omitempty"`
|
||||
IPRanges []string `json:"ip,omitempty"`
|
||||
EmailAddresses []string `json:"email,omitempty"`
|
||||
URIDomains []string `json:"uri,omitempty"`
|
||||
}
|
||||
|
||||
// HasNames checks if the AllowedNameOptions has one or more
|
||||
// names configured.
|
||||
func (o *X509NameOptions) HasNames() bool {
|
||||
return len(o.CommonNames) > 0 ||
|
||||
len(o.DNSDomains) > 0 ||
|
||||
len(o.IPRanges) > 0 ||
|
||||
len(o.EmailAddresses) > 0 ||
|
||||
len(o.URIDomains) > 0
|
||||
}
|
||||
|
||||
// GetAllowedNameOptions returns x509 allowed name policy configuration
|
||||
func (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.AllowedNames
|
||||
}
|
||||
|
||||
// GetDeniedNameOptions returns the x509 denied name policy configuration
|
||||
func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.DeniedNames
|
||||
}
|
||||
|
||||
// AreWildcardNamesAllowed returns whether the authority allows
|
||||
// literal wildcard names to be signed.
|
||||
func (o *X509PolicyOptions) AreWildcardNamesAllowed() bool {
|
||||
if o == nil {
|
||||
return true
|
||||
}
|
||||
return o.AllowWildcardNames
|
||||
}
|
||||
|
||||
// SSHPolicyOptionsInterface is an interface for providers of
|
||||
// SSH user and host name policy configuration.
|
||||
type SSHPolicyOptionsInterface interface {
|
||||
GetAllowedUserNameOptions() *SSHNameOptions
|
||||
GetDeniedUserNameOptions() *SSHNameOptions
|
||||
GetAllowedHostNameOptions() *SSHNameOptions
|
||||
GetDeniedHostNameOptions() *SSHNameOptions
|
||||
}
|
||||
|
||||
// SSHPolicyOptions is a container for SSH user and host policy
|
||||
// configuration
|
||||
type SSHPolicyOptions struct {
|
||||
// User contains SSH user certificate options.
|
||||
User *SSHUserCertificateOptions `json:"user,omitempty"`
|
||||
// Host contains SSH host certificate options.
|
||||
Host *SSHHostCertificateOptions `json:"host,omitempty"`
|
||||
}
|
||||
|
||||
// GetAllowedUserNameOptions returns the SSH allowed user name policy
|
||||
// configuration.
|
||||
func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions {
|
||||
if o == nil || o.User == nil {
|
||||
return nil
|
||||
}
|
||||
return o.User.AllowedNames
|
||||
}
|
||||
|
||||
// GetDeniedUserNameOptions returns the SSH denied user name policy
|
||||
// configuration.
|
||||
func (o *SSHPolicyOptions) GetDeniedUserNameOptions() *SSHNameOptions {
|
||||
if o == nil || o.User == nil {
|
||||
return nil
|
||||
}
|
||||
return o.User.DeniedNames
|
||||
}
|
||||
|
||||
// GetAllowedHostNameOptions returns the SSH allowed host name policy
|
||||
// configuration.
|
||||
func (o *SSHPolicyOptions) GetAllowedHostNameOptions() *SSHNameOptions {
|
||||
if o == nil || o.Host == nil {
|
||||
return nil
|
||||
}
|
||||
return o.Host.AllowedNames
|
||||
}
|
||||
|
||||
// GetDeniedHostNameOptions returns the SSH denied host name policy
|
||||
// configuration.
|
||||
func (o *SSHPolicyOptions) GetDeniedHostNameOptions() *SSHNameOptions {
|
||||
if o == nil || o.Host == nil {
|
||||
return nil
|
||||
}
|
||||
return o.Host.DeniedNames
|
||||
}
|
||||
|
||||
// SSHUserCertificateOptions is a collection of SSH user certificate options.
|
||||
type SSHUserCertificateOptions struct {
|
||||
// AllowedNames contains the names the provisioner is authorized to sign
|
||||
AllowedNames *SSHNameOptions `json:"allow,omitempty"`
|
||||
// DeniedNames contains the names the provisioner is not authorized to sign
|
||||
DeniedNames *SSHNameOptions `json:"deny,omitempty"`
|
||||
}
|
||||
|
||||
// SSHHostCertificateOptions is a collection of SSH host certificate options.
|
||||
// It's an alias of SSHUserCertificateOptions, as the options are the same
|
||||
// for both types of certificates.
|
||||
type SSHHostCertificateOptions SSHUserCertificateOptions
|
||||
|
||||
// SSHNameOptions models the SSH name policy configuration.
|
||||
type SSHNameOptions struct {
|
||||
DNSDomains []string `json:"dns,omitempty"`
|
||||
IPRanges []string `json:"ip,omitempty"`
|
||||
EmailAddresses []string `json:"email,omitempty"`
|
||||
Principals []string `json:"principal,omitempty"`
|
||||
}
|
||||
|
||||
// GetAllowedNameOptions returns the AllowedSSHNameOptions, which models the
|
||||
// names that a provisioner is authorized to sign SSH certificates for.
|
||||
func (o *SSHUserCertificateOptions) GetAllowedNameOptions() *SSHNameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.AllowedNames
|
||||
}
|
||||
|
||||
// GetDeniedNameOptions returns the DeniedSSHNameOptions, which models the
|
||||
// names that a provisioner is NOT authorized to sign SSH certificates for.
|
||||
func (o *SSHUserCertificateOptions) GetDeniedNameOptions() *SSHNameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.DeniedNames
|
||||
}
|
||||
|
||||
// HasNames checks if the SSHNameOptions has one or more
|
||||
// names configured.
|
||||
func (o *SSHNameOptions) HasNames() bool {
|
||||
return len(o.DNSDomains) > 0 ||
|
||||
len(o.IPRanges) > 0 ||
|
||||
len(o.EmailAddresses) > 0 ||
|
||||
len(o.Principals) > 0
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestX509PolicyOptions_IsWildcardLiteralAllowed(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options *X509PolicyOptions
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil-options",
|
||||
options: nil,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not-set",
|
||||
options: &X509PolicyOptions{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "set-true",
|
||||
options: &X509PolicyOptions{
|
||||
AllowWildcardNames: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "set-false",
|
||||
options: &X509PolicyOptions{
|
||||
AllowWildcardNames: false,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.options.AreWildcardNamesAllowed(); got != tt.want {
|
||||
t.Errorf("X509PolicyOptions.IsWildcardLiteralAllowed() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,256 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/policy"
|
||||
)
|
||||
|
||||
// X509Policy is an alias for policy.X509NamePolicyEngine
|
||||
type X509Policy policy.X509NamePolicyEngine
|
||||
|
||||
// UserPolicy is an alias for policy.SSHNamePolicyEngine
|
||||
type UserPolicy policy.SSHNamePolicyEngine
|
||||
|
||||
// HostPolicy is an alias for policy.SSHNamePolicyEngine
|
||||
type HostPolicy policy.SSHNamePolicyEngine
|
||||
|
||||
// NewX509PolicyEngine creates a new x509 name policy engine
|
||||
func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, error) {
|
||||
|
||||
// return early if no policy engine options to configure
|
||||
if policyOptions == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
options := []policy.NamePolicyOption{}
|
||||
|
||||
allowed := policyOptions.GetAllowedNameOptions()
|
||||
if allowed != nil && allowed.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithPermittedCommonNames(allowed.CommonNames...),
|
||||
policy.WithPermittedDNSDomains(allowed.DNSDomains...),
|
||||
policy.WithPermittedIPsOrCIDRs(allowed.IPRanges...),
|
||||
policy.WithPermittedEmailAddresses(allowed.EmailAddresses...),
|
||||
policy.WithPermittedURIDomains(allowed.URIDomains...),
|
||||
)
|
||||
}
|
||||
|
||||
denied := policyOptions.GetDeniedNameOptions()
|
||||
if denied != nil && denied.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithExcludedCommonNames(denied.CommonNames...),
|
||||
policy.WithExcludedDNSDomains(denied.DNSDomains...),
|
||||
policy.WithExcludedIPsOrCIDRs(denied.IPRanges...),
|
||||
policy.WithExcludedEmailAddresses(denied.EmailAddresses...),
|
||||
policy.WithExcludedURIDomains(denied.URIDomains...),
|
||||
)
|
||||
}
|
||||
|
||||
// ensure no policy engine is returned when no name options were provided
|
||||
if len(options) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// check if configuration specifies that wildcard names are allowed
|
||||
if policyOptions.AreWildcardNamesAllowed() {
|
||||
options = append(options, policy.WithAllowLiteralWildcardNames())
|
||||
}
|
||||
|
||||
// enable subject common name verification by default
|
||||
options = append(options, policy.WithSubjectCommonNameVerification())
|
||||
|
||||
return policy.New(options...)
|
||||
}
|
||||
|
||||
type sshPolicyEngineType string
|
||||
|
||||
const (
|
||||
UserPolicyEngineType sshPolicyEngineType = "user"
|
||||
HostPolicyEngineType sshPolicyEngineType = "host"
|
||||
)
|
||||
|
||||
// newSSHUserPolicyEngine creates a new SSH user certificate policy engine
|
||||
func NewSSHUserPolicyEngine(policyOptions SSHPolicyOptionsInterface) (UserPolicy, error) {
|
||||
policyEngine, err := newSSHPolicyEngine(policyOptions, UserPolicyEngineType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policyEngine, nil
|
||||
}
|
||||
|
||||
// newSSHHostPolicyEngine create a new SSH host certificate policy engine
|
||||
func NewSSHHostPolicyEngine(policyOptions SSHPolicyOptionsInterface) (HostPolicy, error) {
|
||||
policyEngine, err := newSSHPolicyEngine(policyOptions, HostPolicyEngineType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policyEngine, nil
|
||||
}
|
||||
|
||||
// newSSHPolicyEngine creates a new SSH name policy engine
|
||||
func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) {
|
||||
|
||||
// return early if no policy engine options to configure
|
||||
if policyOptions == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
allowed *SSHNameOptions
|
||||
denied *SSHNameOptions
|
||||
)
|
||||
|
||||
switch typ {
|
||||
case UserPolicyEngineType:
|
||||
allowed = policyOptions.GetAllowedUserNameOptions()
|
||||
denied = policyOptions.GetDeniedUserNameOptions()
|
||||
case HostPolicyEngineType:
|
||||
allowed = policyOptions.GetAllowedHostNameOptions()
|
||||
denied = policyOptions.GetDeniedHostNameOptions()
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown SSH policy engine type %s provided", typ)
|
||||
}
|
||||
|
||||
options := []policy.NamePolicyOption{}
|
||||
|
||||
if allowed != nil && allowed.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithPermittedDNSDomains(allowed.DNSDomains...),
|
||||
policy.WithPermittedIPsOrCIDRs(allowed.IPRanges...),
|
||||
policy.WithPermittedEmailAddresses(allowed.EmailAddresses...),
|
||||
policy.WithPermittedPrincipals(allowed.Principals...),
|
||||
)
|
||||
}
|
||||
|
||||
if denied != nil && denied.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithExcludedDNSDomains(denied.DNSDomains...),
|
||||
policy.WithExcludedIPsOrCIDRs(denied.IPRanges...),
|
||||
policy.WithExcludedEmailAddresses(denied.EmailAddresses...),
|
||||
policy.WithExcludedPrincipals(denied.Principals...),
|
||||
)
|
||||
}
|
||||
|
||||
// ensure no policy engine is returned when no name options were provided
|
||||
if len(options) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return policy.New(options...)
|
||||
}
|
||||
|
||||
func LinkedToCertificates(p *linkedca.Policy) *Options {
|
||||
|
||||
// return early
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// return early if x509 nor SSH is set
|
||||
if p.GetX509() == nil && p.GetSsh() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
opts := &Options{}
|
||||
|
||||
// fill x509 policy configuration
|
||||
if x509 := p.GetX509(); x509 != nil {
|
||||
opts.X509 = &X509PolicyOptions{}
|
||||
if allow := x509.GetAllow(); allow != nil {
|
||||
opts.X509.AllowedNames = &X509NameOptions{}
|
||||
if allow.Dns != nil {
|
||||
opts.X509.AllowedNames.DNSDomains = allow.Dns
|
||||
}
|
||||
if allow.Ips != nil {
|
||||
opts.X509.AllowedNames.IPRanges = allow.Ips
|
||||
}
|
||||
if allow.Emails != nil {
|
||||
opts.X509.AllowedNames.EmailAddresses = allow.Emails
|
||||
}
|
||||
if allow.Uris != nil {
|
||||
opts.X509.AllowedNames.URIDomains = allow.Uris
|
||||
}
|
||||
if allow.CommonNames != nil {
|
||||
opts.X509.AllowedNames.CommonNames = allow.CommonNames
|
||||
}
|
||||
}
|
||||
if deny := x509.GetDeny(); deny != nil {
|
||||
opts.X509.DeniedNames = &X509NameOptions{}
|
||||
if deny.Dns != nil {
|
||||
opts.X509.DeniedNames.DNSDomains = deny.Dns
|
||||
}
|
||||
if deny.Ips != nil {
|
||||
opts.X509.DeniedNames.IPRanges = deny.Ips
|
||||
}
|
||||
if deny.Emails != nil {
|
||||
opts.X509.DeniedNames.EmailAddresses = deny.Emails
|
||||
}
|
||||
if deny.Uris != nil {
|
||||
opts.X509.DeniedNames.URIDomains = deny.Uris
|
||||
}
|
||||
if deny.CommonNames != nil {
|
||||
opts.X509.DeniedNames.CommonNames = deny.CommonNames
|
||||
}
|
||||
}
|
||||
|
||||
opts.X509.AllowWildcardNames = x509.GetAllowWildcardNames()
|
||||
}
|
||||
|
||||
// fill ssh policy configuration
|
||||
if ssh := p.GetSsh(); ssh != nil {
|
||||
opts.SSH = &SSHPolicyOptions{}
|
||||
if host := ssh.GetHost(); host != nil {
|
||||
opts.SSH.Host = &SSHHostCertificateOptions{}
|
||||
if allow := host.GetAllow(); allow != nil {
|
||||
opts.SSH.Host.AllowedNames = &SSHNameOptions{}
|
||||
if allow.Dns != nil {
|
||||
opts.SSH.Host.AllowedNames.DNSDomains = allow.Dns
|
||||
}
|
||||
if allow.Ips != nil {
|
||||
opts.SSH.Host.AllowedNames.IPRanges = allow.Ips
|
||||
}
|
||||
if allow.Principals != nil {
|
||||
opts.SSH.Host.AllowedNames.Principals = allow.Principals
|
||||
}
|
||||
}
|
||||
if deny := host.GetDeny(); deny != nil {
|
||||
opts.SSH.Host.DeniedNames = &SSHNameOptions{}
|
||||
if deny.Dns != nil {
|
||||
opts.SSH.Host.DeniedNames.DNSDomains = deny.Dns
|
||||
}
|
||||
if deny.Ips != nil {
|
||||
opts.SSH.Host.DeniedNames.IPRanges = deny.Ips
|
||||
}
|
||||
if deny.Principals != nil {
|
||||
opts.SSH.Host.DeniedNames.Principals = deny.Principals
|
||||
}
|
||||
}
|
||||
}
|
||||
if user := ssh.GetUser(); user != nil {
|
||||
opts.SSH.User = &SSHUserCertificateOptions{}
|
||||
if allow := user.GetAllow(); allow != nil {
|
||||
opts.SSH.User.AllowedNames = &SSHNameOptions{}
|
||||
if allow.Emails != nil {
|
||||
opts.SSH.User.AllowedNames.EmailAddresses = allow.Emails
|
||||
}
|
||||
if allow.Principals != nil {
|
||||
opts.SSH.User.AllowedNames.Principals = allow.Principals
|
||||
}
|
||||
}
|
||||
if deny := user.GetDeny(); deny != nil {
|
||||
opts.SSH.User.DeniedNames = &SSHNameOptions{}
|
||||
if deny.Emails != nil {
|
||||
opts.SSH.User.DeniedNames.EmailAddresses = deny.Emails
|
||||
}
|
||||
if deny.Principals != nil {
|
||||
opts.SSH.User.DeniedNames.Principals = deny.Principals
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
func TestPolicyToCertificates(t *testing.T) {
|
||||
type args struct {
|
||||
policy *linkedca.Policy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Options
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
args: args{
|
||||
policy: nil,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "no-policy",
|
||||
args: args{
|
||||
&linkedca.Policy{},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "partial-policy",
|
||||
args: args{
|
||||
&linkedca.Policy{
|
||||
X509: &linkedca.X509Policy{
|
||||
Allow: &linkedca.X509Names{
|
||||
Dns: []string{"*.local"},
|
||||
},
|
||||
AllowWildcardNames: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &Options{
|
||||
X509: &X509PolicyOptions{
|
||||
AllowedNames: &X509NameOptions{
|
||||
DNSDomains: []string{"*.local"},
|
||||
},
|
||||
AllowWildcardNames: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "full-policy",
|
||||
args: args{
|
||||
&linkedca.Policy{
|
||||
X509: &linkedca.X509Policy{
|
||||
Allow: &linkedca.X509Names{
|
||||
Dns: []string{"step"},
|
||||
Ips: []string{"127.0.0.1/24"},
|
||||
Emails: []string{"*.example.com"},
|
||||
Uris: []string{"https://*.local"},
|
||||
CommonNames: []string{"some name"},
|
||||
},
|
||||
Deny: &linkedca.X509Names{
|
||||
Dns: []string{"bad"},
|
||||
Ips: []string{"127.0.0.30"},
|
||||
Emails: []string{"badhost.example.com"},
|
||||
Uris: []string{"https://badhost.local"},
|
||||
CommonNames: []string{"another name"},
|
||||
},
|
||||
AllowWildcardNames: true,
|
||||
},
|
||||
Ssh: &linkedca.SSHPolicy{
|
||||
Host: &linkedca.SSHHostPolicy{
|
||||
Allow: &linkedca.SSHHostNames{
|
||||
Dns: []string{"*.localhost"},
|
||||
Ips: []string{"127.0.0.1/24"},
|
||||
Principals: []string{"user"},
|
||||
},
|
||||
Deny: &linkedca.SSHHostNames{
|
||||
Dns: []string{"badhost.localhost"},
|
||||
Ips: []string{"127.0.0.40"},
|
||||
Principals: []string{"root"},
|
||||
},
|
||||
},
|
||||
User: &linkedca.SSHUserPolicy{
|
||||
Allow: &linkedca.SSHUserNames{
|
||||
Emails: []string{"@work"},
|
||||
Principals: []string{"user"},
|
||||
},
|
||||
Deny: &linkedca.SSHUserNames{
|
||||
Emails: []string{"root@work"},
|
||||
Principals: []string{"root"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &Options{
|
||||
X509: &X509PolicyOptions{
|
||||
AllowedNames: &X509NameOptions{
|
||||
DNSDomains: []string{"step"},
|
||||
IPRanges: []string{"127.0.0.1/24"},
|
||||
EmailAddresses: []string{"*.example.com"},
|
||||
URIDomains: []string{"https://*.local"},
|
||||
CommonNames: []string{"some name"},
|
||||
},
|
||||
DeniedNames: &X509NameOptions{
|
||||
DNSDomains: []string{"bad"},
|
||||
IPRanges: []string{"127.0.0.30"},
|
||||
EmailAddresses: []string{"badhost.example.com"},
|
||||
URIDomains: []string{"https://badhost.local"},
|
||||
CommonNames: []string{"another name"},
|
||||
},
|
||||
AllowWildcardNames: true,
|
||||
},
|
||||
SSH: &SSHPolicyOptions{
|
||||
Host: &SSHHostCertificateOptions{
|
||||
AllowedNames: &SSHNameOptions{
|
||||
DNSDomains: []string{"*.localhost"},
|
||||
IPRanges: []string{"127.0.0.1/24"},
|
||||
Principals: []string{"user"},
|
||||
},
|
||||
DeniedNames: &SSHNameOptions{
|
||||
DNSDomains: []string{"badhost.localhost"},
|
||||
IPRanges: []string{"127.0.0.40"},
|
||||
Principals: []string{"root"},
|
||||
},
|
||||
},
|
||||
User: &SSHUserCertificateOptions{
|
||||
AllowedNames: &SSHNameOptions{
|
||||
EmailAddresses: []string{"@work"},
|
||||
Principals: []string{"user"},
|
||||
},
|
||||
DeniedNames: &SSHNameOptions{
|
||||
EmailAddresses: []string{"root@work"},
|
||||
Principals: []string{"root"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := LinkedToCertificates(tt.args.policy)
|
||||
if !cmp.Equal(tt.want, got) {
|
||||
t.Errorf("policyToCertificates() diff=\n%s", cmp.Diff(tt.want, got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue