1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
|
package ca
import (
"context"
"crypto/tls"
"crypto/x509/pkix"
"strings"
"github.com/moby/swarmkit/v2/api"
"github.com/moby/swarmkit/v2/log"
"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(log.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
}
|