Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions cmd/kube-etcd-signer-server/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ var (
serveOpts struct {
caCrtFile string
caKeyFile string
mCACrtFile string
mCAKeyFile string
mCASigner bool
sCrtFile string
sKeyFile string
addr string
peerCertDur string
serverCertDur string
metricCertDur string
csrDir string
}
)
Expand All @@ -36,17 +40,28 @@ func init() {
serveCmd.PersistentFlags().StringVar(&serveOpts.caKeyFile, "cakey", "", "CA private key file for signer")
serveCmd.PersistentFlags().StringVar(&serveOpts.sCrtFile, "servcrt", "", "Server certificate file for signer")
serveCmd.PersistentFlags().StringVar(&serveOpts.sKeyFile, "servkey", "", "Server private key file for signer")
serveCmd.PersistentFlags().StringVar(&serveOpts.mCACrtFile, "metric-cacrt", "", "CA certificate file for metrics signer")
serveCmd.PersistentFlags().StringVar(&serveOpts.mCAKeyFile, "metric-cakey", "", "CA private key file for metrics signer")
serveCmd.PersistentFlags().StringVar(&serveOpts.addr, "address", "0.0.0.0:6443", "Address on which the signer listens for requests")
serveCmd.PersistentFlags().StringVar(&serveOpts.metricCertDur, "metriccertdur", "8760h", "Certificate duration for etcd metrics certs (defaults to 365 days)")
serveCmd.PersistentFlags().StringVar(&serveOpts.peerCertDur, "peercertdur", "8760h", "Certificate duration for etcd peer certs (defaults to 365 days)")
serveCmd.PersistentFlags().StringVar(&serveOpts.serverCertDur, "servercertdur", "8760h", "Certificate duration for etcd server certs (defaults to 365 days)")
serveCmd.PersistentFlags().StringVar(&serveOpts.csrDir, "csrdir", "", "Directory location where signer will save CSRs.")
}

// validateServeOpts validates the user flag values given to the signer server
func validateServeOpts(cmd *cobra.Command, args []string) error {
if serveOpts.caCrtFile == "" || serveOpts.caKeyFile == "" {
return errors.New("both --cacrt and --cakey are required flags")
caPair := 0
if serveOpts.caCrtFile != "" && serveOpts.caKeyFile != "" {
caPair++
}
if serveOpts.mCACrtFile != "" && serveOpts.mCAKeyFile != "" {
caPair++
}
if caPair == 0 {
return errors.New("no signer CA flags passed one cert/key pair is required")
}

if serveOpts.sCrtFile == "" || serveOpts.sKeyFile == "" {
return errors.New("both --servcrt and --servkey are required flags")
}
Expand All @@ -67,13 +82,24 @@ func runCmdServe(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("error parsing duration for etcd server cert: %v", err)
}
mCertDur, err := time.ParseDuration(serveOpts.metricCertDur)
if err != nil {
return fmt.Errorf("error parsing duration for etcd metric cert: %v", err)
}

ca := signer.SignerCAFiles{
CACert: serveOpts.caCrtFile,
CAKey: serveOpts.caKeyFile,
MetricCACert: serveOpts.mCACrtFile,
MetricCAKey: serveOpts.mCAKeyFile,
}

c := signer.Config{
CACertFile: serveOpts.caCrtFile,
CAKeyFile: serveOpts.caKeyFile,
SignerCAFiles: ca,
ServerCertFile: serveOpts.sCrtFile,
ServerKeyFile: serveOpts.sKeyFile,
ListenAddress: serveOpts.addr,
EtcdMetricCertDuration: mCertDur,
EtcdPeerCertDuration: pCertDur,
EtcdServerCertDuration: sCertDur,
CSRDir: serveOpts.csrDir,
Expand Down
185 changes: 140 additions & 45 deletions pkg/certsigner/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
const (
etcdPeer = "EtcdPeer"
etcdServer = "EtcdServer"
etcdMetric = "EtcdMetric"
)

var (
Expand All @@ -39,6 +40,8 @@ var (
ErrInvalidOrg = errors.New("invalid organization")
// ErrInvalidCN defines a global error for invalid subject common name
ErrInvalidCN = errors.New("invalid subject Common Name")
// ErrProfileSupport defines a global error for a profile which was not backed by a CA signer cert..
ErrProfileSupport = errors.New("csr profile is not currently supported")
)

// CertServer is the object that handles the HTTP requests and responses.
Expand All @@ -52,6 +55,10 @@ type CertServer struct {
csrDir string
// signer is the object that handles the approval of the CSRs
signer *CertSigner
// policy
policy *config.Signing
// caFiles
caFiles *SignerCAFiles
}

// CertSigner signs a certiifcate using a `cfssl` Signer.
Expand All @@ -72,16 +79,16 @@ type CertSigner struct {

// Config holds the configuration values required to start a new signer
type Config struct {
// CACertFile is the file location of the Certificate Authority certificate
CACertFile string
// CAKeyFile is the file location of the Certificate Authority private key
CAKeyFile string
// SignerCAFiles
SignerCAFiles
// ServerCertFile is the file location of the server certificate
ServerCertFile string
// ServerKeyFile is the file location of the server private key
ServerKeyFile string
// ListenAddress is the address at which the server listens for requests
ListenAddress string
// EtcdMetricCertDuration
EtcdMetricCertDuration time.Duration
// EtcdPeerCertDuration is the cert duration for the `EtcdPeer` profile
EtcdPeerCertDuration time.Duration
// EtcdServerCertDuration is the cert duration for the `EtcdServer` profile
Expand All @@ -90,6 +97,28 @@ type Config struct {
CSRDir string
}

// SignerCAFiles holds the file paths to the signer CA assets
type SignerCAFiles struct {
// CACert is the file location of the Certificate Authority certificate
CACert string
// CAKey is the file location of the Certificate Authority private key
CAKey string
// MetricCACert is the file location of the metrics Certificate Authority certificate
MetricCACert string
// MetricCAKey is the file location of the metrics Certificate Authority private key
MetricCAKey string
}

// SignerCA stores the PEM encoded cert and key blocks.
type SignerCA struct {
// caCert is the x509 PEM encoded certificate of the CA used for the
// cfssl signer
caCert *x509.Certificate
// caCert is the x509 PEM encoded private key of the CA used for the
// cfssl signer
caKey crypto.Signer
}

// loggingHandler is the HTTP handler that logs information about requests received by the server
type loggingHandler struct {
h http.Handler
Expand All @@ -103,16 +132,14 @@ func (l *loggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// NewServer returns a CertServer object that has a CertSigner object
// as a part of it
func NewServer(c Config) (*CertServer, error) {
signer, err := NewSigner(c)
if err != nil {
return nil, fmt.Errorf("error setting up a signer: %v", err)
}

policy := signerPolicy(c)
mux := mux.NewRouter()
server := &CertServer{
mux: mux,
csrDir: c.CSRDir,
signer: signer,
policy: &policy,

caFiles: &c.SignerCAFiles,
}

mux.HandleFunc("/apis/certificates.k8s.io/v1beta1/certificatesigningrequests", server.HandlePostCSR).Methods("POST")
Expand All @@ -125,29 +152,58 @@ func (s *CertServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.mux.ServeHTTP(w, r)
}

// NewSigner returns a CertSigner object after filling in its attibutes
// from the `Config` provided.
func NewSigner(c Config) (*CertSigner, error) {
ca, err := ioutil.ReadFile(c.CACertFile)
// newSignerCA returns a SignerCA object of PEM encoded CA cert and keys based on the profile passed.
func newSignerCA(sc *SignerCAFiles, csr *capi.CertificateSigningRequest) (*SignerCA, error) {
var caCert, caKey string

profile, err := getProfile(csr)
if err != nil {
return nil, fmt.Errorf("error reading CA cert file %q: %v", c.CACertFile, err)
return nil, err
}
cakey, err := ioutil.ReadFile(c.CAKeyFile)
if err != nil {
return nil, fmt.Errorf("error reading CA key file %q: %v", c.CAKeyFile, err)
switch profile {
case "EtcdMetric":
if sc.MetricCAKey != "" && sc.MetricCACert != "" {
caCert = sc.MetricCACert
caKey = sc.MetricCAKey
break
}
return nil, ErrProfileSupport
case "EtcdServer", "EtcdPeer":
if sc.CAKey != "" && sc.CACert != "" {
caCert = sc.CACert
caKey = sc.CAKey
break
}
return nil, ErrProfileSupport
default:
return nil, ErrInvalidOrg
}

ca, err := ioutil.ReadFile(caCert)
if err != nil {
return nil, fmt.Errorf("error reading CA cert file %q: %v", caCert, err)
}
cakey, err := ioutil.ReadFile(caKey)
if err != nil {
return nil, fmt.Errorf("error reading CA key file %q: %v", caKey, err)
}
parsedCA, err := helpers.ParseCertificatePEM(ca)
if err != nil {
return nil, fmt.Errorf("error parsing CA cert file %q: %v", c.CACertFile, err)
return nil, fmt.Errorf("error parsing CA cert file %q: %v", caCert, err)
}

privateKey, err := helpers.ParsePrivateKeyPEM(cakey)
if err != nil {
return nil, fmt.Errorf("Malformed private key %v", err)
}

// policy is the signature configuration policy for the signer.
return &SignerCA{
caCert: parsedCA,
caKey: privateKey,
}, nil
}

// signerPolicy
func signerPolicy(c Config) config.Signing {
policy := config.Signing{
Profiles: map[string]*config.SigningProfile{
etcdPeer: &config.SigningProfile{
Expand All @@ -169,6 +225,16 @@ func NewSigner(c Config) (*CertSigner, error) {
Expiry: c.EtcdServerCertDuration,
ExpiryString: c.EtcdServerCertDuration.String(),
},
etcdMetric: &config.SigningProfile{
Usage: []string{
string(capi.UsageKeyEncipherment),
string(capi.UsageDigitalSignature),
string(capi.UsageClientAuth),
string(capi.UsageServerAuth),
},
Expiry: c.EtcdMetricCertDuration,
ExpiryString: c.EtcdMetricCertDuration.String(),
},
},
Default: &config.SigningProfile{
Usage: []string{
Expand All @@ -180,14 +246,20 @@ func NewSigner(c Config) (*CertSigner, error) {
},
}

cfs, err := local.NewSigner(privateKey, parsedCA, signer.DefaultSigAlgo(privateKey), &policy)
return policy
}

// NewSigner returns a CertSigner object after filling in its attibutes
// from the `Config` provided.
func NewSigner(s *SignerCA, policy *config.Signing) (*CertSigner, error) {
cfs, err := local.NewSigner(s.caKey, s.caCert, signer.DefaultSigAlgo(s.caKey), policy)
if err != nil {
return nil, fmt.Errorf("error setting up local cfssl signer: %v", err)
}

return &CertSigner{
caCert: parsedCA,
caKey: privateKey,
caCert: s.caCert,
caKey: s.caKey,
cfsslSigner: cfs,
}, nil
}
Expand All @@ -198,18 +270,9 @@ func NewSigner(c Config) (*CertSigner, error) {
//
// Note: A signed certificate is issued only for etcd profiles.
func (s *CertSigner) Sign(csr *capi.CertificateSigningRequest) (*capi.CertificateSigningRequest, error) {
x509CSR, err := csrutil.ParseCSR(csr)
if err != nil {
return nil, fmt.Errorf("error parsing CSR, %v", err)
}

if err := x509CSR.CheckSignature(); err != nil {
return nil, fmt.Errorf("error validating signature of CSR: %v", err)
}

// the following step ensures that the signer server only signs CSRs from etcd nodes
// that have a specific profile. All other requests are denied immediately.
profile, err := getProfile(x509CSR)
profile, err := getProfile(csr)
if err != nil {
csr.Status.Conditions = []capi.CertificateSigningRequestCondition{
capi.CertificateSigningRequestCondition{
Expand Down Expand Up @@ -245,18 +308,36 @@ func (s *CertSigner) Sign(csr *capi.CertificateSigningRequest) (*capi.Certificat

// getProfile returns the profile corresponding to the CSR Subject. For now only
// `etcd-peers` and `etcd-servers` are considered valid profiles.
func getProfile(csr *x509.CertificateRequest) (string, error) {
if csr.Subject.Organization != nil && len(csr.Subject.Organization) == 1 && csr.Subject.Organization[0] == "system:etcd-peers" {
if !strings.HasPrefix(csr.Subject.CommonName, "system:etcd-peer:") {
return "", ErrInvalidCN
}
return etcdPeer, nil
func getProfile(csr *capi.CertificateSigningRequest) (string, error) {
x509CSR, err := csrutil.ParseCSR(csr)
if err != nil {
return "", fmt.Errorf("error parsing CSR, %v", err)
}
if err := x509CSR.CheckSignature(); err != nil {
return "", fmt.Errorf("error validating signature of CSR: %v", err)
}
if csr.Subject.Organization != nil && len(csr.Subject.Organization) == 1 && csr.Subject.Organization[0] == "system:etcd-servers" {
if !strings.HasPrefix(csr.Subject.CommonName, "system:etcd-server:") {
return "", ErrInvalidCN
if x509CSR.Subject.Organization == nil || len(x509CSR.Subject.Organization) == 0 {
return "", ErrInvalidOrg
}

org := x509CSR.Subject.Organization[0]
cn := fmt.Sprintf(org[:len(org)-1]+"%s", ":")
switch org {
case "system:etcd-peers":
if strings.HasPrefix(x509CSR.Subject.CommonName, cn) {
return etcdPeer, nil
}
break
case "system:etcd-servers":
if strings.HasPrefix(x509CSR.Subject.CommonName, cn) {
return etcdServer, nil
}
break
case "system:etcd-metrics":
if strings.HasPrefix(x509CSR.Subject.CommonName, cn) {
return etcdMetric, nil
}
return etcdServer, nil
break
}
return "", ErrInvalidOrg
}
Expand Down Expand Up @@ -287,7 +368,21 @@ func (s *CertServer) HandlePostCSR(w http.ResponseWriter, r *http.Request) {
return
}

signedCSR, err := s.signer.Sign(csr)
signerCA, err := newSignerCA(s.caFiles, csr)
if err != nil {
glog.Errorf("Error signing CSR provided in request from agent: %v", err)
http.Error(w, "Error signing csr", http.StatusBadRequest)
return
}

signer, err := NewSigner(signerCA, s.policy)
if err != nil {
glog.Errorf("Error signing CSR provided in request from agent: %v", err)
http.Error(w, "Error signing csr", http.StatusBadRequest)
return
}

signedCSR, err := signer.Sign(csr)
if err != nil {
glog.Errorf("Error signing CSR provided in request from agent: %v", err)
http.Error(w, "Error signing csr", http.StatusBadRequest)
Expand Down
Loading