From ab737ef0f8dd12b794c0fa2247a8d928a161394c Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Sun, 8 Apr 2018 21:58:31 -0700 Subject: [PATCH] Hook the CA RPC endpoint into the provider interface --- agent/consul/connect_ca_endpoint.go | 192 ++++++---------------------- agent/consul/server.go | 6 + agent/structs/connect_ca.go | 26 +++- 3 files changed, 70 insertions(+), 154 deletions(-) diff --git a/agent/consul/connect_ca_endpoint.go b/agent/consul/connect_ca_endpoint.go index 4efdafc067..84cffc85d1 100644 --- a/agent/consul/connect_ca_endpoint.go +++ b/agent/consul/connect_ca_endpoint.go @@ -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) +// 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 } - // Basic validation so demos aren't super jank - if config.Name == "" { - return fmt.Errorf("Name must be set") + // This action requires operator read access. + rule, err := s.srv.resolveToken(args.Token) + if err != nil { + return err } - if config.CertPEM == "" || config.PrivateKeyPEM == "" { - if !config.Generate { - return fmt.Errorf( - "CertPEM and PrivateKeyPEM must be set, or Generate must be true") - } + if rule != nil && !rule.OperatorRead() { + return acl.ErrPermissionDenied } - // Convenience to auto-generate the cert - if config.Generate { - ca := connect.TestCA(&testing.RuntimeT{}, nil) - config.CertPEM = ca.RootCert - config.PrivateKeyPEM = ca.SigningKey + state := s.srv.fsm.State() + _, config, err := state.CAConfig() + if err != nil { + return err } + *reply = *config - // TODO(mitchellh): verify that the private key is valid for the cert + return nil +} - // Generate an ID for this - id, err := uuid.GenerateUUID() - if err != 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 } - // Get the highest index - state := s.srv.fsm.State() - idx, _, err := state.CARoots(nil) + // 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) - 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) - } + // todo(kyhavlov): more validation on the CSR before signing - // 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()) + cert, err := s.srv.signConnectCert(serviceId, csr) 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 } diff --git a/agent/consul/server.go b/agent/consul/server.go index 23fbf337c3..fef016829d 100644 --- a/agent/consul/server.go +++ b/agent/consul/server.go @@ -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 diff --git a/agent/structs/connect_ca.go b/agent/structs/connect_ca.go index 5ac8a0fc23..af8f826531 100644 --- a/agent/structs/connect_ca.go +++ b/agent/structs/connect_ca.go @@ -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 }