Hook the CA RPC endpoint into the provider interface

pull/4275/head
Kyle Havlovitz 7 years ago committed by Mitchell Hashimoto
parent 1f6501895f
commit ab737ef0f8
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A

@ -1,22 +1,13 @@
package consul package consul
import ( import (
"bytes"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt" "fmt"
"math/big"
"time"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/go-memdb" "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. // ConnectCA manages the Connect CA.
@ -25,81 +16,54 @@ type ConnectCA struct {
srv *Server srv *Server
} }
// ConfigurationSet updates the configuration for the CA. // ConfigurationGet returns the configuration for the CA.
// func (s *ConnectCA) ConfigurationGet(
// NOTE(mitchellh): This whole implementation is temporary until the real args *structs.DCSpecificRequest,
// CA plugin work comes in. For now, this is only used to configure a single reply *structs.CAConfiguration) error {
// static CA root. if done, err := s.srv.forward("ConnectCA.ConfigurationGet", args, args, reply); done {
func (s *ConnectCA) ConfigurationSet( return err
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 // This action requires operator read access.
if config.Name == "" { rule, err := s.srv.resolveToken(args.Token)
return fmt.Errorf("Name must be set") if err != nil {
return err
} }
if config.CertPEM == "" || config.PrivateKeyPEM == "" { if rule != nil && !rule.OperatorRead() {
if !config.Generate { return acl.ErrPermissionDenied
return fmt.Errorf(
"CertPEM and PrivateKeyPEM must be set, or Generate must be true")
}
} }
// Convenience to auto-generate the cert state := s.srv.fsm.State()
if config.Generate { _, config, err := state.CAConfig()
ca := connect.TestCA(&testing.RuntimeT{}, nil) if err != nil {
config.CertPEM = ca.RootCert return err
config.PrivateKeyPEM = ca.SigningKey
} }
*reply = *config
// TODO(mitchellh): verify that the private key is valid for the cert return nil
}
// Generate an ID for this // ConfigurationSet updates the configuration for the CA.
id, err := uuid.GenerateUUID() func (s *ConnectCA) ConfigurationSet(
if err != nil { args *structs.CARequest,
reply *interface{}) error {
if done, err := s.srv.forward("ConnectCA.ConfigurationSet", args, args, reply); done {
return err return err
} }
// Get the highest index // This action requires operator read access.
state := s.srv.fsm.State() rule, err := s.srv.resolveToken(args.Token)
idx, _, err := state.CARoots(nil)
if err != nil { if err != nil {
return err return err
} }
if rule != nil && !rule.OperatorWrite() {
return acl.ErrPermissionDenied
}
// Commit // Commit
resp, err := s.srv.raftApply(structs.ConnectCARequestType, &structs.CARequest{ args.Op = structs.CAOpSetConfig
Op: structs.CAOpSet, resp, err := s.srv.raftApply(structs.ConnectCARequestType, args)
Index: idx,
Roots: []*structs.CARoot{
&structs.CARoot{
ID: id,
Name: config.Name,
RootCert: config.CertPEM,
SigningKey: config.PrivateKeyPEM,
Active: true,
},
},
})
if err != nil { if err != nil {
s.srv.logger.Printf("[ERR] consul.test: Apply failed %v", err)
return err return err
} }
if respErr, ok := resp.(error); ok { if respErr, ok := resp.(error); ok {
@ -157,13 +121,13 @@ func (s *ConnectCA) Roots(
} }
// Sign signs a certificate for a service. // 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( func (s *ConnectCA) Sign(
args *structs.CASignRequest, args *structs.CASignRequest,
reply *structs.IssuedCert) error { reply *structs.IssuedCert) error {
if done, err := s.srv.forward("ConnectCA.Sign", args, args, reply); done {
return err
}
// Parse the CSR // Parse the CSR
csr, err := connect.ParseCSR(args.CSR) csr, err := connect.ParseCSR(args.CSR)
if err != nil { if err != nil {
@ -180,93 +144,15 @@ func (s *ConnectCA) Sign(
return fmt.Errorf("SPIFFE ID in CSR must be a service ID") return fmt.Errorf("SPIFFE ID in CSR must be a service ID")
} }
// Get the currently active root // todo(kyhavlov): more validation on the CSR before signing
state := s.srv.fsm.State()
_, root, err := state.CARootActive(nil)
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 cert, err := s.srv.signConnectCert(serviceId, csr)
// 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 { if err != nil {
return err 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 // Set the response
*reply = structs.IssuedCert{ *reply = *cert
SerialNumber: connect.HexString(template.SerialNumber.Bytes()),
CertPEM: buf.String(),
Service: serviceId.Service,
ServiceURI: template.URIs[0].String(),
ValidAfter: template.NotBefore,
ValidBefore: template.NotAfter,
}
return nil return nil
} }

@ -17,6 +17,8 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/autopilot" "github.com/hashicorp/consul/agent/consul/autopilot"
"github.com/hashicorp/consul/agent/consul/fsm" "github.com/hashicorp/consul/agent/consul/fsm"
@ -96,6 +98,10 @@ type Server struct {
// autopilotWaitGroup is used to block until Autopilot shuts down. // autopilotWaitGroup is used to block until Autopilot shuts down.
autopilotWaitGroup sync.WaitGroup autopilotWaitGroup sync.WaitGroup
// caProvider is the current CA provider in use for Connect.
caProvider connect.CAProvider
caProviderLock sync.RWMutex
// Consul configuration // Consul configuration
config *Config config *Config

@ -96,7 +96,8 @@ type IssuedCert struct {
type CAOp string type CAOp string
const ( 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 // CARequest is used to modify connect CA data. This is used by the
@ -106,14 +107,33 @@ type CARequest struct {
// other fields are required. // other fields are required.
Op CAOp Op CAOp
// Datacenter is the target for this request.
Datacenter string
// Index is used by CAOpSet for a CAS operation. // Index is used by CAOpSet for a CAS operation.
Index uint64 Index uint64
// Roots is a list of roots. This is used for CAOpSet. One root must // Roots is a list of roots. This is used for CAOpSet. One root must
// always be active. // always be active.
Roots []*CARoot 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. // CAConfiguration is the configuration for the current CA plugin.
type CAConfiguration struct { type CAConfiguration struct {
// Provider is the CA provider implementation to use. // 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 // should only contain primitive values and containers (such as lists
// and maps). // and maps).
Config map[string]interface{} Config map[string]interface{}
// CreateIndex/ModifyIndex store the create/modify indexes of this configuration.
CreateIndex uint64
ModifyIndex uint64
} }

Loading…
Cancel
Save