agent: /v1/connect/ca/leaf/:service_id

pull/4275/head
Mitchell Hashimoto 2018-03-21 10:55:39 -07:00
parent 571d9aa785
commit c2588262b7
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
6 changed files with 145 additions and 14 deletions

View File

@ -10,6 +10,7 @@ import (
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/checks" "github.com/hashicorp/consul/agent/checks"
"github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/ipaddr" "github.com/hashicorp/consul/ipaddr"
@ -21,6 +22,9 @@ import (
"github.com/hashicorp/serf/serf" "github.com/hashicorp/serf/serf"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
// NOTE(mitcehllh): This is temporary while certs are stubbed out.
"github.com/mitchellh/go-testing-interface"
) )
type Self struct { type Self struct {
@ -844,3 +848,40 @@ func (s *HTTPServer) AgentConnectCARoots(resp http.ResponseWriter, req *http.Req
// behavior will differ. // behavior will differ.
return s.ConnectCARoots(resp, req) return s.ConnectCARoots(resp, req)
} }
// AgentConnectCALeafCert returns the certificate bundle for a service
// instance. This supports blocking queries to update the returned bundle.
func (s *HTTPServer) AgentConnectCALeafCert(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Test the method
if req.Method != "GET" {
return nil, MethodNotAllowedError{req.Method, []string{"GET"}}
}
// Get the service ID. Note that this is the ID of a service instance.
id := strings.TrimPrefix(req.URL.Path, "/v1/agent/connect/ca/leaf/")
// Retrieve the service specified
service := s.agent.State.Service(id)
if service == nil {
return nil, fmt.Errorf("unknown service ID: %s", id)
}
// Create a CSR.
// TODO(mitchellh): This is obviously not production ready!
csr, pk := connect.TestCSR(&testing.RuntimeT{}, &connect.SpiffeIDService{
Host: "1234.consul",
Namespace: "default",
Datacenter: s.agent.config.Datacenter,
Service: service.Service,
})
// Request signing
var reply structs.IssuedCert
args := structs.CASignRequest{CSR: csr}
if err := s.agent.RPC("ConnectCA.Sign", &args, &reply); err != nil {
return nil, err
}
reply.PrivateKeyPEM = pk
return &reply, nil
}

View File

@ -2,6 +2,7 @@ package agent
import ( import (
"bytes" "bytes"
"crypto/x509"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -2074,3 +2075,58 @@ func TestAgentConnectCARoots_list(t *testing.T) {
assert.Equal("", r.SigningKey) assert.Equal("", r.SigningKey)
} }
} }
func TestAgentConnectCALeafCert_good(t *testing.T) {
t.Parallel()
assert := assert.New(t)
a := NewTestAgent(t.Name(), "")
defer a.Shutdown()
// Set CAs
var reply interface{}
ca1 := connect.TestCA(t, nil)
assert.Nil(a.RPC("Test.ConnectCASetRoots", []*structs.CARoot{ca1}, &reply))
{
// Register a local service
args := &structs.ServiceDefinition{
ID: "foo",
Name: "test",
Address: "127.0.0.1",
Port: 8000,
Check: structs.CheckType{
TTL: 15 * time.Second,
},
}
req, _ := http.NewRequest("PUT", "/v1/agent/service/register", jsonReader(args))
resp := httptest.NewRecorder()
_, err := a.srv.AgentRegisterService(resp, req)
assert.Nil(err)
if !assert.Equal(200, resp.Code) {
t.Log("Body: ", resp.Body.String())
}
}
// List
req, _ := http.NewRequest("GET", "/v1/agent/connect/ca/leaf/foo", nil)
resp := httptest.NewRecorder()
obj, err := a.srv.AgentConnectCALeafCert(resp, req)
assert.Nil(err)
// Get the issued cert
issued, ok := obj.(*structs.IssuedCert)
assert.True(ok)
// Verify that the cert is signed by the CA
roots := x509.NewCertPool()
assert.True(roots.AppendCertsFromPEM([]byte(ca1.RootCert)))
leaf, err := connect.ParseCert(issued.CertPEM)
assert.Nil(err)
_, err = leaf.Verify(x509.VerifyOptions{
Roots: roots,
})
assert.Nil(err)
// TODO(mitchellh): verify the private key matches the cert
}

View File

@ -187,29 +187,47 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) string {
return buf.String() return buf.String()
} }
// TestCSR returns a CSR to sign the given service. // TestCSR returns a CSR to sign the given service along with the PEM-encoded
func TestCSR(t testing.T, id SpiffeID) string { // private key for this certificate.
func TestCSR(t testing.T, id SpiffeID) (string, string) {
template := &x509.CertificateRequest{ template := &x509.CertificateRequest{
URIs: []*url.URL{id.URI()}, URIs: []*url.URL{id.URI()},
SignatureAlgorithm: x509.ECDSAWithSHA256, SignatureAlgorithm: x509.ECDSAWithSHA256,
} }
// Result buffers
var csrBuf, pkBuf bytes.Buffer
// Create the private key we'll use // Create the private key we'll use
signer := testPrivateKey(t, nil) signer := testPrivateKey(t, nil)
// Create the CSR itself {
bs, err := x509.CreateCertificateRequest(rand.Reader, template, signer) // Create the private key PEM
if err != nil { bs, err := x509.MarshalECPrivateKey(signer.(*ecdsa.PrivateKey))
t.Fatalf("error creating CSR: %s", err) if err != nil {
t.Fatalf("error marshalling PK: %s", err)
}
err = pem.Encode(&pkBuf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: bs})
if err != nil {
t.Fatalf("error encoding PK: %s", err)
}
} }
var buf bytes.Buffer {
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs}) // Create the CSR itself
if err != nil { bs, err := x509.CreateCertificateRequest(rand.Reader, template, signer)
t.Fatalf("error encoding CSR: %s", err) if err != nil {
t.Fatalf("error creating CSR: %s", err)
}
err = pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs})
if err != nil {
t.Fatalf("error encoding CSR: %s", err)
}
} }
return buf.String() return csrBuf.String(), pkBuf.String()
} }
// testKeyID returns a KeyID from the given public key. The "raw" must be // testKeyID returns a KeyID from the given public key. The "raw" must be

View File

@ -171,7 +171,7 @@ func (s *ConnectCA) Sign(
// Set the response // Set the response
*reply = structs.IssuedCert{ *reply = structs.IssuedCert{
SerialNumber: template.SerialNumber, SerialNumber: template.SerialNumber,
Cert: buf.String(), CertPEM: buf.String(),
} }
return nil return nil

View File

@ -30,6 +30,7 @@ func init() {
registerEndpoint("/v1/agent/check/fail/", []string{"PUT"}, (*HTTPServer).AgentCheckFail) registerEndpoint("/v1/agent/check/fail/", []string{"PUT"}, (*HTTPServer).AgentCheckFail)
registerEndpoint("/v1/agent/check/update/", []string{"PUT"}, (*HTTPServer).AgentCheckUpdate) registerEndpoint("/v1/agent/check/update/", []string{"PUT"}, (*HTTPServer).AgentCheckUpdate)
registerEndpoint("/v1/agent/connect/ca/roots", []string{"GET"}, (*HTTPServer).AgentConnectCARoots) registerEndpoint("/v1/agent/connect/ca/roots", []string{"GET"}, (*HTTPServer).AgentConnectCARoots)
registerEndpoint("/v1/agent/connect/ca/leaf/", []string{"GET"}, (*HTTPServer).AgentConnectCALeafCert)
registerEndpoint("/v1/agent/service/register", []string{"PUT"}, (*HTTPServer).AgentRegisterService) registerEndpoint("/v1/agent/service/register", []string{"PUT"}, (*HTTPServer).AgentRegisterService)
registerEndpoint("/v1/agent/service/deregister/", []string{"PUT"}, (*HTTPServer).AgentDeregisterService) registerEndpoint("/v1/agent/service/deregister/", []string{"PUT"}, (*HTTPServer).AgentDeregisterService)
registerEndpoint("/v1/agent/service/maintenance/", []string{"PUT"}, (*HTTPServer).AgentServiceMaintenance) registerEndpoint("/v1/agent/service/maintenance/", []string{"PUT"}, (*HTTPServer).AgentServiceMaintenance)

View File

@ -2,6 +2,7 @@ package structs
import ( import (
"math/big" "math/big"
"time"
) )
// IndexedCARoots is the list of currently trusted CA Roots. // IndexedCARoots is the list of currently trusted CA Roots.
@ -72,9 +73,23 @@ type IssuedCert struct {
// SerialNumber is the unique serial number for this certificate. // SerialNumber is the unique serial number for this certificate.
SerialNumber *big.Int SerialNumber *big.Int
// Cert is the PEM-encoded certificate. This should not be stored in the // CertPEM and PrivateKeyPEM are the PEM-encoded certificate and private
// key for that cert, respectively. This should not be stored in the
// state store, but is present in the sign API response. // state store, but is present in the sign API response.
Cert string `json:",omitempty"` CertPEM string `json:",omitempty"`
PrivateKeyPEM string
// Service is the name of the service for which the cert was issued.
// ServiceURI is the cert URI value.
Service string
ServiceURI string
// ValidAfter and ValidBefore are the validity periods for the
// certificate.
ValidAfter time.Time
ValidBefore time.Time
RaftIndex
} }
// CAOp is the operation for a request related to intentions. // CAOp is the operation for a request related to intentions.