Hook the CA RPC endpoint into the provider interface
This commit is contained in:
parent
a40db26ffe
commit
fc9ef9741b
|
@ -1,22 +1,13 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/mitchellh/go-testing-interface"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// ConnectCA manages the Connect CA.
|
||||
|
@ -25,81 +16,54 @@ type ConnectCA struct {
|
|||
srv *Server
|
||||
}
|
||||
|
||||
// ConfigurationSet updates the configuration for the CA.
|
||||
//
|
||||
// NOTE(mitchellh): This whole implementation is temporary until the real
|
||||
// CA plugin work comes in. For now, this is only used to configure a single
|
||||
// static CA root.
|
||||
func (s *ConnectCA) ConfigurationSet(
|
||||
args *structs.CAConfiguration,
|
||||
reply *interface{}) error {
|
||||
// NOTE(mitchellh): This is the temporary hardcoding of a static CA
|
||||
// provider. This will allow us to test agent implementations and so on
|
||||
// with an incomplete CA for now.
|
||||
if args.Provider != "static" {
|
||||
return fmt.Errorf("The CA provider can only be 'static' for now")
|
||||
}
|
||||
|
||||
// Config is the configuration allowed for our static provider
|
||||
var config struct {
|
||||
Name string
|
||||
CertPEM string
|
||||
PrivateKeyPEM string
|
||||
Generate bool
|
||||
}
|
||||
if err := mapstructure.Decode(args.Config, &config); err != nil {
|
||||
return fmt.Errorf("error decoding config: %s", err)
|
||||
}
|
||||
|
||||
// Basic validation so demos aren't super jank
|
||||
if config.Name == "" {
|
||||
return fmt.Errorf("Name must be set")
|
||||
}
|
||||
if config.CertPEM == "" || config.PrivateKeyPEM == "" {
|
||||
if !config.Generate {
|
||||
return fmt.Errorf(
|
||||
"CertPEM and PrivateKeyPEM must be set, or Generate must be true")
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience to auto-generate the cert
|
||||
if config.Generate {
|
||||
ca := connect.TestCA(&testing.RuntimeT{}, nil)
|
||||
config.CertPEM = ca.RootCert
|
||||
config.PrivateKeyPEM = ca.SigningKey
|
||||
}
|
||||
|
||||
// TODO(mitchellh): verify that the private key is valid for the cert
|
||||
|
||||
// Generate an ID for this
|
||||
id, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
// ConfigurationGet returns the configuration for the CA.
|
||||
func (s *ConnectCA) ConfigurationGet(
|
||||
args *structs.DCSpecificRequest,
|
||||
reply *structs.CAConfiguration) error {
|
||||
if done, err := s.srv.forward("ConnectCA.ConfigurationGet", args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the highest index
|
||||
// This action requires operator read access.
|
||||
rule, err := s.srv.resolveToken(args.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rule != nil && !rule.OperatorRead() {
|
||||
return acl.ErrPermissionDenied
|
||||
}
|
||||
|
||||
state := s.srv.fsm.State()
|
||||
idx, _, err := state.CARoots(nil)
|
||||
_, config, err := state.CAConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*reply = *config
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigurationSet updates the configuration for the CA.
|
||||
func (s *ConnectCA) ConfigurationSet(
|
||||
args *structs.CARequest,
|
||||
reply *interface{}) error {
|
||||
if done, err := s.srv.forward("ConnectCA.ConfigurationSet", args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
|
||||
// This action requires operator read access.
|
||||
rule, err := s.srv.resolveToken(args.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rule != nil && !rule.OperatorWrite() {
|
||||
return acl.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// Commit
|
||||
resp, err := s.srv.raftApply(structs.ConnectCARequestType, &structs.CARequest{
|
||||
Op: structs.CAOpSet,
|
||||
Index: idx,
|
||||
Roots: []*structs.CARoot{
|
||||
&structs.CARoot{
|
||||
ID: id,
|
||||
Name: config.Name,
|
||||
RootCert: config.CertPEM,
|
||||
SigningKey: config.PrivateKeyPEM,
|
||||
Active: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
args.Op = structs.CAOpSetConfig
|
||||
resp, err := s.srv.raftApply(structs.ConnectCARequestType, args)
|
||||
if err != nil {
|
||||
s.srv.logger.Printf("[ERR] consul.test: Apply failed %v", err)
|
||||
return err
|
||||
}
|
||||
if respErr, ok := resp.(error); ok {
|
||||
|
@ -157,13 +121,13 @@ func (s *ConnectCA) Roots(
|
|||
}
|
||||
|
||||
// Sign signs a certificate for a service.
|
||||
//
|
||||
// NOTE(mitchellh): There is a LOT missing from this. I do next to zero
|
||||
// validation of the incoming CSR, the way the cert is signed probably
|
||||
// isn't right, we're not using enough of the CSR fields, etc.
|
||||
func (s *ConnectCA) Sign(
|
||||
args *structs.CASignRequest,
|
||||
reply *structs.IssuedCert) error {
|
||||
if done, err := s.srv.forward("ConnectCA.Sign", args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse the CSR
|
||||
csr, err := connect.ParseCSR(args.CSR)
|
||||
if err != nil {
|
||||
|
@ -180,93 +144,15 @@ func (s *ConnectCA) Sign(
|
|||
return fmt.Errorf("SPIFFE ID in CSR must be a service ID")
|
||||
}
|
||||
|
||||
// Get the currently active root
|
||||
state := s.srv.fsm.State()
|
||||
_, root, err := state.CARootActive(nil)
|
||||
// todo(kyhavlov): more validation on the CSR before signing
|
||||
|
||||
cert, err := s.srv.signConnectCert(serviceId, csr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if root == nil {
|
||||
return fmt.Errorf("no active CA found")
|
||||
}
|
||||
|
||||
// Determine the signing certificate. It is the set signing cert
|
||||
// unless that is empty, in which case it is identically to the public
|
||||
// cert.
|
||||
certPem := root.SigningCert
|
||||
if certPem == "" {
|
||||
certPem = root.RootCert
|
||||
}
|
||||
|
||||
// Parse the CA cert and signing key from the root
|
||||
caCert, err := connect.ParseCert(certPem)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing CA cert: %s", err)
|
||||
}
|
||||
signer, err := connect.ParseSigner(root.SigningKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing signing key: %s", err)
|
||||
}
|
||||
|
||||
// The serial number for the cert. NOTE(mitchellh): in the final
|
||||
// implementation this should be monotonically increasing based on
|
||||
// some raft state.
|
||||
sn, err := rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating serial number: %s", err)
|
||||
}
|
||||
|
||||
// Create the keyId for the cert from the signing public key.
|
||||
keyId, err := connect.KeyId(signer.Public())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cert template for generation
|
||||
template := x509.Certificate{
|
||||
SerialNumber: sn,
|
||||
Subject: pkix.Name{CommonName: serviceId.Service},
|
||||
URIs: csr.URIs,
|
||||
Signature: csr.Signature,
|
||||
SignatureAlgorithm: csr.SignatureAlgorithm,
|
||||
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
|
||||
PublicKey: csr.PublicKey,
|
||||
BasicConstraintsValid: true,
|
||||
KeyUsage: x509.KeyUsageDataEncipherment |
|
||||
x509.KeyUsageKeyAgreement |
|
||||
x509.KeyUsageDigitalSignature |
|
||||
x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
x509.ExtKeyUsageServerAuth,
|
||||
},
|
||||
NotAfter: time.Now().Add(3 * 24 * time.Hour),
|
||||
NotBefore: time.Now(),
|
||||
AuthorityKeyId: keyId,
|
||||
SubjectKeyId: keyId,
|
||||
}
|
||||
|
||||
// Create the certificate, PEM encode it and return that value.
|
||||
var buf bytes.Buffer
|
||||
bs, err := x509.CreateCertificate(
|
||||
rand.Reader, &template, caCert, signer.Public(), signer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating certificate: %s", err)
|
||||
}
|
||||
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error encoding private key: %s", err)
|
||||
}
|
||||
|
||||
// Set the response
|
||||
*reply = structs.IssuedCert{
|
||||
SerialNumber: connect.HexString(template.SerialNumber.Bytes()),
|
||||
CertPEM: buf.String(),
|
||||
Service: serviceId.Service,
|
||||
ServiceURI: template.URIs[0].String(),
|
||||
ValidAfter: template.NotBefore,
|
||||
ValidBefore: template.NotAfter,
|
||||
}
|
||||
*reply = *cert
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/consul/autopilot"
|
||||
"github.com/hashicorp/consul/agent/consul/fsm"
|
||||
|
@ -96,6 +98,10 @@ type Server struct {
|
|||
// autopilotWaitGroup is used to block until Autopilot shuts down.
|
||||
autopilotWaitGroup sync.WaitGroup
|
||||
|
||||
// caProvider is the current CA provider in use for Connect.
|
||||
caProvider connect.CAProvider
|
||||
caProviderLock sync.RWMutex
|
||||
|
||||
// Consul configuration
|
||||
config *Config
|
||||
|
||||
|
|
|
@ -96,7 +96,8 @@ type IssuedCert struct {
|
|||
type CAOp string
|
||||
|
||||
const (
|
||||
CAOpSet CAOp = "set"
|
||||
CAOpSetRoots CAOp = "set-roots"
|
||||
CAOpSetConfig CAOp = "set-config"
|
||||
)
|
||||
|
||||
// CARequest is used to modify connect CA data. This is used by the
|
||||
|
@ -106,14 +107,33 @@ type CARequest struct {
|
|||
// other fields are required.
|
||||
Op CAOp
|
||||
|
||||
// Datacenter is the target for this request.
|
||||
Datacenter string
|
||||
|
||||
// Index is used by CAOpSet for a CAS operation.
|
||||
Index uint64
|
||||
|
||||
// Roots is a list of roots. This is used for CAOpSet. One root must
|
||||
// always be active.
|
||||
Roots []*CARoot
|
||||
|
||||
// Config is the configuration for the current CA plugin.
|
||||
Config *CAConfiguration
|
||||
|
||||
// WriteRequest is a common struct containing ACL tokens and other
|
||||
// write-related common elements for requests.
|
||||
WriteRequest
|
||||
}
|
||||
|
||||
// RequestDatacenter returns the datacenter for a given request.
|
||||
func (q *CARequest) RequestDatacenter() string {
|
||||
return q.Datacenter
|
||||
}
|
||||
|
||||
const (
|
||||
ConsulCAProvider = "consul"
|
||||
)
|
||||
|
||||
// CAConfiguration is the configuration for the current CA plugin.
|
||||
type CAConfiguration struct {
|
||||
// Provider is the CA provider implementation to use.
|
||||
|
@ -123,4 +143,8 @@ type CAConfiguration struct {
|
|||
// should only contain primitive values and containers (such as lists
|
||||
// and maps).
|
||||
Config map[string]interface{}
|
||||
|
||||
// CreateIndex/ModifyIndex store the create/modify indexes of this configuration.
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue