mirror of https://github.com/linkerd/linkerd2.git
298 lines
8.9 KiB
Go
298 lines
8.9 KiB
Go
package identity
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
v1 "k8s.io/api/core/v1"
|
|
|
|
"github.com/golang/protobuf/ptypes"
|
|
pb "github.com/linkerd/linkerd2-proxy-api/go/identity"
|
|
"github.com/linkerd/linkerd2/pkg/tls"
|
|
log "github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
const (
|
|
// DefaultIssuanceLifetime is the default lifetime of certificates issued by
|
|
// the identity service.
|
|
DefaultIssuanceLifetime = 24 * time.Hour
|
|
|
|
// EnvTrustAnchors is the environment variable holding the trust anchors for
|
|
// the proxy identity.
|
|
EnvTrustAnchors = "LINKERD2_PROXY_IDENTITY_TRUST_ANCHORS"
|
|
eventTypeSkipped = "IssuerUpdateSkipped"
|
|
eventTypeUpdated = "IssuerUpdated"
|
|
eventTypeFailed = "IssuerValidationFailed"
|
|
)
|
|
|
|
type (
|
|
// Service implements the gRPC service in terms of a Validator and Issuer.
|
|
Service struct {
|
|
validator Validator
|
|
trustAnchors *x509.CertPool
|
|
issuer *tls.Issuer
|
|
issuerMutex *sync.RWMutex
|
|
validity *tls.Validity
|
|
recordEvent func(eventType, reason, message string)
|
|
expectedName, issuerPathCrt, issuerPathKey string
|
|
}
|
|
|
|
// Validator implementors accept a bearer token, validates it, and returns a
|
|
// DNS-form identity.
|
|
Validator interface {
|
|
// Validate takes an opaque authentication token, attempts to validate its
|
|
// authenticity, and produces a DNS-like identifier.
|
|
//
|
|
// An InvalidToken error should be returned if the provided token was not in a
|
|
// correct form.
|
|
//
|
|
// A NotAuthenticated error should be returned if the authenticity of the
|
|
// token cannot be validated.
|
|
Validate(context.Context, []byte) (string, error)
|
|
}
|
|
|
|
// InvalidToken is an error type returned by Validators to indicate that the
|
|
// provided authentication token was not valid.
|
|
InvalidToken struct{ Reason string }
|
|
|
|
// NotAuthenticated is an error type returned by Validators to indicate that the
|
|
// provided authentication token could not be authenticated.
|
|
NotAuthenticated struct{}
|
|
)
|
|
|
|
// Initialize loads the issuer certs from disk so it can start service CSRs to proxies
|
|
func (svc *Service) Initialize() error {
|
|
credentials, err := svc.loadCredentials()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
svc.updateIssuer(credentials)
|
|
return nil
|
|
}
|
|
|
|
func (svc *Service) updateIssuer(newIssuer tls.Issuer) {
|
|
svc.issuerMutex.Lock()
|
|
svc.issuer = &newIssuer
|
|
log.Debug("Issuer has been updated")
|
|
svc.issuerMutex.Unlock()
|
|
}
|
|
|
|
// Run reads from the issuer and error channels and reloads the issuer certs when necessary
|
|
func (svc *Service) Run(issuerEvent <-chan struct{}, issuerError <-chan error) {
|
|
for {
|
|
select {
|
|
case <-issuerEvent:
|
|
if err := svc.Initialize(); err != nil {
|
|
message := fmt.Sprintf("Skipping issuer update as certs could not be read from disk: %s", err)
|
|
log.Warn(message)
|
|
svc.recordEvent(v1.EventTypeWarning, eventTypeSkipped, message)
|
|
} else {
|
|
message := "Updated identity issuer"
|
|
log.Infof(message)
|
|
svc.recordEvent(v1.EventTypeNormal, eventTypeUpdated, message)
|
|
}
|
|
case err := <-issuerError:
|
|
log.Warnf("Received error from fs watcher: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (svc *Service) loadCredentials() (tls.Issuer, error) {
|
|
creds, err := tls.ReadPEMCreds(
|
|
svc.issuerPathKey,
|
|
svc.issuerPathCrt,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read CA from disk: %s", err)
|
|
}
|
|
|
|
if err := creds.Crt.Verify(svc.trustAnchors, svc.expectedName, time.Time{}); err != nil {
|
|
return nil, fmt.Errorf("failed to verify issuer credentials for '%s' with trust anchors: %s", svc.expectedName, err)
|
|
}
|
|
|
|
log.Debugf("Loaded issuer cert: %s", creds.EncodeCertificatePEM())
|
|
return tls.NewCA(*creds, *svc.validity), nil
|
|
}
|
|
|
|
// NewService creates a new identity service.
|
|
func NewService(validator Validator, trustAnchors *x509.CertPool, validity *tls.Validity, recordEvent func(eventType, reason, message string), expectedName, issuerPathCrt, issuerPathKey string) *Service {
|
|
return &Service{
|
|
validator,
|
|
trustAnchors,
|
|
nil,
|
|
&sync.RWMutex{},
|
|
validity,
|
|
recordEvent,
|
|
expectedName,
|
|
issuerPathCrt,
|
|
issuerPathKey,
|
|
}
|
|
}
|
|
|
|
// Register registers an identity service implementation in the provided gRPC
|
|
// server.
|
|
func Register(g *grpc.Server, s *Service) {
|
|
pb.RegisterIdentityServer(g, s)
|
|
}
|
|
|
|
// ensureIssuerStillValid should check that the CA is still good time wise
|
|
// and verifies just fine with the provided trust anchors
|
|
func (svc *Service) ensureIssuerStillValid() error {
|
|
issuer := *svc.issuer
|
|
switch is := issuer.(type) {
|
|
case *tls.CA:
|
|
return is.Cred.Verify(svc.trustAnchors, svc.expectedName, time.Time{})
|
|
default:
|
|
return fmt.Errorf("unsupported issuer type. Expected *tls.CA, got %v", is)
|
|
}
|
|
}
|
|
|
|
// Certify validates identity and signs certificates.
|
|
func (svc *Service) Certify(ctx context.Context, req *pb.CertifyRequest) (*pb.CertifyResponse, error) {
|
|
svc.issuerMutex.RLock()
|
|
defer svc.issuerMutex.RUnlock()
|
|
|
|
if svc.issuer == nil {
|
|
log.Warn("Certificate issuer is not ready")
|
|
return nil, status.Error(codes.Unavailable, "cert issuer not ready yet")
|
|
}
|
|
|
|
// Extract the relevant info from the request.
|
|
reqIdentity, tok, csr, err := checkRequest(req)
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
}
|
|
|
|
if err := svc.ensureIssuerStillValid(); err != nil {
|
|
log.Errorf("could not process CSR because of CA cert validation failure: %s - CSR Identity : %s", err, reqIdentity)
|
|
message := fmt.Sprintf("%s - CSR Identity : %s", err.Error(), reqIdentity)
|
|
svc.recordEvent(v1.EventTypeWarning, eventTypeFailed, message)
|
|
return nil, err
|
|
}
|
|
|
|
if err = checkCSR(csr, reqIdentity); err != nil {
|
|
log.Debugf("requester sent invalid CSR: %s", err)
|
|
return nil, status.Error(codes.FailedPrecondition, err.Error())
|
|
}
|
|
|
|
// Authenticate the provided token against the Kubernetes API.
|
|
log.Debugf("Validating token for %s", reqIdentity)
|
|
tokIdentity, err := svc.validator.Validate(ctx, tok)
|
|
if err != nil {
|
|
switch e := err.(type) {
|
|
case NotAuthenticated:
|
|
log.Infof("authentication failed for %s: %s", reqIdentity, e)
|
|
return nil, status.Error(codes.FailedPrecondition, e.Error())
|
|
case InvalidToken:
|
|
log.Debugf("invalid token provided for %s: %s", reqIdentity, e)
|
|
return nil, status.Error(codes.InvalidArgument, e.Error())
|
|
default:
|
|
msg := fmt.Sprintf("error validating token for %s: %s", reqIdentity, e)
|
|
log.Error(msg)
|
|
return nil, status.Error(codes.Internal, msg)
|
|
}
|
|
}
|
|
|
|
// Ensure the requested identity matches the token's identity.
|
|
if reqIdentity != tokIdentity {
|
|
msg := fmt.Sprintf("requested identity did not match provided token: requested=%s; found=%s",
|
|
reqIdentity, tokIdentity)
|
|
log.Debug(msg)
|
|
return nil, status.Error(codes.FailedPrecondition, msg)
|
|
}
|
|
|
|
// Create a certificate
|
|
issuer := *svc.issuer
|
|
crt, err := issuer.IssueEndEntityCrt(csr)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
crts := crt.ExtractRaw()
|
|
if len(crts) == 0 {
|
|
log.Fatal("the issuer provided a certificate without key material")
|
|
}
|
|
|
|
// Bundle issuer crt with certificate so the trust path to the root can be verified.
|
|
log.Infof("certifying %s until %s", tokIdentity, crt.Certificate.NotAfter)
|
|
validUntil, err := ptypes.TimestampProto(crt.Certificate.NotAfter)
|
|
if err != nil {
|
|
log.Errorf("invalid expiry time: %s", err)
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
|
|
rsp := &pb.CertifyResponse{
|
|
LeafCertificate: crts[0],
|
|
IntermediateCertificates: crts[1:],
|
|
|
|
ValidUntil: validUntil,
|
|
}
|
|
return rsp, nil
|
|
}
|
|
|
|
func checkRequest(req *pb.CertifyRequest) (string, []byte, *x509.CertificateRequest, error) {
|
|
reqIdentity := req.GetIdentity()
|
|
if reqIdentity == "" {
|
|
return "", nil, nil, errors.New("missing identity")
|
|
}
|
|
|
|
tok := req.GetToken()
|
|
if len(tok) == 0 {
|
|
return "", nil, nil, errors.New("missing token")
|
|
}
|
|
|
|
der := req.GetCertificateSigningRequest()
|
|
if len(der) == 0 {
|
|
return "", nil, nil,
|
|
errors.New("missing certificate signing request")
|
|
}
|
|
csr, err := x509.ParseCertificateRequest(der)
|
|
if err != nil {
|
|
return "", nil, nil, err
|
|
}
|
|
|
|
return reqIdentity, tok, csr, nil
|
|
}
|
|
|
|
func checkCSR(csr *x509.CertificateRequest, identity string) error {
|
|
if len(csr.DNSNames) != 1 {
|
|
return errors.New("CSR must have exactly one DNSName")
|
|
}
|
|
if csr.DNSNames[0] != identity {
|
|
return fmt.Errorf("CSR name does not match requested identity: csr=%s; req=%s", csr.DNSNames[0], identity)
|
|
}
|
|
|
|
switch csr.Subject.CommonName {
|
|
case "", identity:
|
|
default:
|
|
return fmt.Errorf("invalid CommonName: %s", csr.Subject.CommonName)
|
|
}
|
|
|
|
if len(csr.EmailAddresses) > 0 {
|
|
return errors.New("cannot validate email addresses")
|
|
}
|
|
if len(csr.IPAddresses) > 0 {
|
|
return errors.New("cannot validate IP addresses")
|
|
}
|
|
if len(csr.URIs) > 0 {
|
|
return errors.New("cannot validate URIs")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (NotAuthenticated) Error() string {
|
|
return "authentication token could not be authenticated"
|
|
}
|
|
|
|
func (e InvalidToken) Error() string {
|
|
return e.Reason
|
|
}
|