| package ca |
| |
| import ( |
| "crypto/tls" |
| "crypto/x509/pkix" |
| "strings" |
| |
| "github.com/sirupsen/logrus" |
| |
| "github.com/docker/swarmkit/api" |
| "github.com/docker/swarmkit/log" |
| "golang.org/x/net/context" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/credentials" |
| "google.golang.org/grpc/peer" |
| "google.golang.org/grpc/status" |
| ) |
| |
| type localRequestKeyType struct{} |
| |
| // LocalRequestKey is a context key to mark a request that originating on the |
| // local node. The associated value is a RemoteNodeInfo structure describing the |
| // local node. |
| var LocalRequestKey = localRequestKeyType{} |
| |
| // LogTLSState logs information about the TLS connection and remote peers |
| func LogTLSState(ctx context.Context, tlsState *tls.ConnectionState) { |
| if tlsState == nil { |
| log.G(ctx).Debugf("no TLS Chains found") |
| return |
| } |
| |
| peerCerts := []string{} |
| verifiedChain := []string{} |
| for _, cert := range tlsState.PeerCertificates { |
| peerCerts = append(peerCerts, cert.Subject.CommonName) |
| } |
| for _, chain := range tlsState.VerifiedChains { |
| subjects := []string{} |
| for _, cert := range chain { |
| subjects = append(subjects, cert.Subject.CommonName) |
| } |
| verifiedChain = append(verifiedChain, strings.Join(subjects, ",")) |
| } |
| |
| log.G(ctx).WithFields(logrus.Fields{ |
| "peer.peerCert": peerCerts, |
| // "peer.verifiedChain": verifiedChain}, |
| }).Debugf("") |
| } |
| |
| // getCertificateSubject extracts the subject from a verified client certificate |
| func getCertificateSubject(tlsState *tls.ConnectionState) (pkix.Name, error) { |
| if tlsState == nil { |
| return pkix.Name{}, status.Errorf(codes.PermissionDenied, "request is not using TLS") |
| } |
| if len(tlsState.PeerCertificates) == 0 { |
| return pkix.Name{}, status.Errorf(codes.PermissionDenied, "no client certificates in request") |
| } |
| if len(tlsState.VerifiedChains) == 0 { |
| return pkix.Name{}, status.Errorf(codes.PermissionDenied, "no verified chains for remote certificate") |
| } |
| |
| return tlsState.VerifiedChains[0][0].Subject, nil |
| } |
| |
| func tlsConnStateFromContext(ctx context.Context) (*tls.ConnectionState, error) { |
| peer, ok := peer.FromContext(ctx) |
| if !ok { |
| return nil, status.Errorf(codes.PermissionDenied, "Permission denied: no peer info") |
| } |
| tlsInfo, ok := peer.AuthInfo.(credentials.TLSInfo) |
| if !ok { |
| return nil, status.Errorf(codes.PermissionDenied, "Permission denied: peer didn't not present valid peer certificate") |
| } |
| return &tlsInfo.State, nil |
| } |
| |
| // certSubjectFromContext extracts pkix.Name from context. |
| func certSubjectFromContext(ctx context.Context) (pkix.Name, error) { |
| connState, err := tlsConnStateFromContext(ctx) |
| if err != nil { |
| return pkix.Name{}, err |
| } |
| return getCertificateSubject(connState) |
| } |
| |
| // AuthorizeOrgAndRole takes in a context and a list of roles, and returns |
| // the Node ID of the node. |
| func AuthorizeOrgAndRole(ctx context.Context, org string, blacklistedCerts map[string]*api.BlacklistedCertificate, ou ...string) (string, error) { |
| certSubj, err := certSubjectFromContext(ctx) |
| if err != nil { |
| return "", err |
| } |
| // Check if the current certificate has an OU that authorizes |
| // access to this method |
| if intersectArrays(certSubj.OrganizationalUnit, ou) { |
| return authorizeOrg(certSubj, org, blacklistedCerts) |
| } |
| |
| return "", status.Errorf(codes.PermissionDenied, "Permission denied: remote certificate not part of OUs: %v", ou) |
| } |
| |
| // authorizeOrg takes in a certificate subject and an organization, and returns |
| // the Node ID of the node. |
| func authorizeOrg(certSubj pkix.Name, org string, blacklistedCerts map[string]*api.BlacklistedCertificate) (string, error) { |
| if _, ok := blacklistedCerts[certSubj.CommonName]; ok { |
| return "", status.Errorf(codes.PermissionDenied, "Permission denied: node %s was removed from swarm", certSubj.CommonName) |
| } |
| |
| if len(certSubj.Organization) > 0 && certSubj.Organization[0] == org { |
| return certSubj.CommonName, nil |
| } |
| |
| return "", status.Errorf(codes.PermissionDenied, "Permission denied: remote certificate not part of organization: %s", org) |
| } |
| |
| // AuthorizeForwardedRoleAndOrg checks for proper roles and organization of caller. The RPC may have |
| // been proxied by a manager, in which case the manager is authenticated and |
| // so is the certificate information that it forwarded. It returns the node ID |
| // of the original client. |
| func AuthorizeForwardedRoleAndOrg(ctx context.Context, authorizedRoles, forwarderRoles []string, org string, blacklistedCerts map[string]*api.BlacklistedCertificate) (string, error) { |
| if isForwardedRequest(ctx) { |
| _, err := AuthorizeOrgAndRole(ctx, org, blacklistedCerts, forwarderRoles...) |
| if err != nil { |
| return "", status.Errorf(codes.PermissionDenied, "Permission denied: unauthorized forwarder role: %v", err) |
| } |
| |
| // This was a forwarded request. Authorize the forwarder, and |
| // check if the forwarded role matches one of the authorized |
| // roles. |
| _, forwardedID, forwardedOrg, forwardedOUs := forwardedTLSInfoFromContext(ctx) |
| |
| if len(forwardedOUs) == 0 || forwardedID == "" || forwardedOrg == "" { |
| return "", status.Errorf(codes.PermissionDenied, "Permission denied: missing information in forwarded request") |
| } |
| |
| if !intersectArrays(forwardedOUs, authorizedRoles) { |
| return "", status.Errorf(codes.PermissionDenied, "Permission denied: unauthorized forwarded role, expecting: %v", authorizedRoles) |
| } |
| |
| if forwardedOrg != org { |
| return "", status.Errorf(codes.PermissionDenied, "Permission denied: organization mismatch, expecting: %s", org) |
| } |
| |
| return forwardedID, nil |
| } |
| |
| // There wasn't any node being forwarded, check if this is a direct call by the expected role |
| nodeID, err := AuthorizeOrgAndRole(ctx, org, blacklistedCerts, authorizedRoles...) |
| if err == nil { |
| return nodeID, nil |
| } |
| |
| return "", status.Errorf(codes.PermissionDenied, "Permission denied: unauthorized peer role: %v", err) |
| } |
| |
| // intersectArrays returns true when there is at least one element in common |
| // between the two arrays |
| func intersectArrays(orig, tgt []string) bool { |
| for _, i := range orig { |
| for _, x := range tgt { |
| if i == x { |
| return true |
| } |
| } |
| } |
| return false |
| } |
| |
| // RemoteNodeInfo describes a node sending an RPC request. |
| type RemoteNodeInfo struct { |
| // Roles is a list of roles contained in the node's certificate |
| // (or forwarded by a trusted node). |
| Roles []string |
| |
| // Organization is the organization contained in the node's certificate |
| // (or forwarded by a trusted node). |
| Organization string |
| |
| // NodeID is the node's ID, from the CN field in its certificate |
| // (or forwarded by a trusted node). |
| NodeID string |
| |
| // ForwardedBy contains information for the node that forwarded this |
| // request. It is set to nil if the request was received directly. |
| ForwardedBy *RemoteNodeInfo |
| |
| // RemoteAddr is the address that this node is connecting to the cluster |
| // from. |
| RemoteAddr string |
| } |
| |
| // RemoteNode returns the node ID and role from the client's TLS certificate. |
| // If the RPC was forwarded, the original client's ID and role is returned, as |
| // well as the forwarder's ID. This function does not do authorization checks - |
| // it only looks up the node ID. |
| func RemoteNode(ctx context.Context) (RemoteNodeInfo, error) { |
| // If we have a value on the context that marks this as a local |
| // request, we return the node info from the context. |
| localNodeInfo := ctx.Value(LocalRequestKey) |
| |
| if localNodeInfo != nil { |
| nodeInfo, ok := localNodeInfo.(RemoteNodeInfo) |
| if ok { |
| return nodeInfo, nil |
| } |
| } |
| |
| certSubj, err := certSubjectFromContext(ctx) |
| if err != nil { |
| return RemoteNodeInfo{}, err |
| } |
| |
| org := "" |
| if len(certSubj.Organization) > 0 { |
| org = certSubj.Organization[0] |
| } |
| |
| peer, ok := peer.FromContext(ctx) |
| if !ok { |
| return RemoteNodeInfo{}, status.Errorf(codes.PermissionDenied, "Permission denied: no peer info") |
| } |
| |
| directInfo := RemoteNodeInfo{ |
| Roles: certSubj.OrganizationalUnit, |
| NodeID: certSubj.CommonName, |
| Organization: org, |
| RemoteAddr: peer.Addr.String(), |
| } |
| |
| if isForwardedRequest(ctx) { |
| remoteAddr, cn, org, ous := forwardedTLSInfoFromContext(ctx) |
| if len(ous) == 0 || cn == "" || org == "" { |
| return RemoteNodeInfo{}, status.Errorf(codes.PermissionDenied, "Permission denied: missing information in forwarded request") |
| } |
| return RemoteNodeInfo{ |
| Roles: ous, |
| NodeID: cn, |
| Organization: org, |
| ForwardedBy: &directInfo, |
| RemoteAddr: remoteAddr, |
| }, nil |
| } |
| |
| return directInfo, nil |
| } |