ca_rotation.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. package controlapi
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/tls"
  6. "crypto/x509"
  7. "errors"
  8. "net"
  9. "net/url"
  10. "time"
  11. "google.golang.org/grpc"
  12. "google.golang.org/grpc/codes"
  13. "github.com/cloudflare/cfssl/helpers"
  14. "github.com/docker/swarmkit/api"
  15. "github.com/docker/swarmkit/ca"
  16. "github.com/docker/swarmkit/log"
  17. )
  18. var minRootExpiration = 1 * helpers.OneYear
  19. // determines whether an api.RootCA, api.RootRotation, or api.CAConfig has a signing key (local signer)
  20. func hasSigningKey(a interface{}) bool {
  21. switch b := a.(type) {
  22. case *api.RootCA:
  23. return len(b.CAKey) > 0
  24. case *api.RootRotation:
  25. return b != nil && len(b.CAKey) > 0
  26. case *api.CAConfig:
  27. return len(b.SigningCACert) > 0 && len(b.SigningCAKey) > 0
  28. default:
  29. panic("needsExternalCAs should be called something of type *api.RootCA, *api.RootRotation, or *api.CAConfig")
  30. }
  31. }
  32. // Creates a cross-signed intermediate and new api.RootRotation object.
  33. // This function assumes that the root cert and key and the external CAs have already been validated.
  34. func newRootRotationObject(ctx context.Context, securityConfig *ca.SecurityConfig, apiRootCA *api.RootCA, newCARootCA ca.RootCA, extCAs []*api.ExternalCA, version uint64) (*api.RootCA, error) {
  35. var (
  36. rootCert, rootKey, crossSignedCert []byte
  37. newRootHasSigner bool
  38. err error
  39. )
  40. rootCert = newCARootCA.Certs
  41. if s, err := newCARootCA.Signer(); err == nil {
  42. rootCert, rootKey = s.Cert, s.Key
  43. newRootHasSigner = true
  44. }
  45. // we have to sign with the original signer, not whatever is in the SecurityConfig's RootCA (which may have an intermediate signer, if
  46. // a root rotation is already in progress)
  47. switch {
  48. case hasSigningKey(apiRootCA):
  49. var oldRootCA ca.RootCA
  50. oldRootCA, err = ca.NewRootCA(apiRootCA.CACert, apiRootCA.CACert, apiRootCA.CAKey, ca.DefaultNodeCertExpiration, nil)
  51. if err == nil {
  52. crossSignedCert, err = oldRootCA.CrossSignCACertificate(rootCert)
  53. }
  54. case !newRootHasSigner: // the original CA and the new CA both require external CAs
  55. return nil, grpc.Errorf(codes.InvalidArgument, "rotating from one external CA to a different external CA is not supported")
  56. default:
  57. // We need the same credentials but to connect to the original URLs (in case we are in the middle of a root rotation already)
  58. externalCA := securityConfig.ExternalCA().Copy()
  59. var urls []string
  60. for _, c := range extCAs {
  61. if c.Protocol == api.ExternalCA_CAProtocolCFSSL {
  62. urls = append(urls, c.URL)
  63. }
  64. }
  65. if len(urls) == 0 {
  66. return nil, grpc.Errorf(codes.InvalidArgument,
  67. "must provide an external CA for the current external root CA to generate a cross-signed certificate")
  68. }
  69. externalCA.UpdateURLs(urls...)
  70. crossSignedCert, err = externalCA.CrossSignRootCA(ctx, newCARootCA)
  71. }
  72. if err != nil {
  73. log.G(ctx).WithError(err).Error("unable to generate a cross-signed certificate for root rotation")
  74. return nil, grpc.Errorf(codes.Internal, "unable to generate a cross-signed certificate for root rotation")
  75. }
  76. copied := apiRootCA.Copy()
  77. copied.RootRotation = &api.RootRotation{
  78. CACert: rootCert,
  79. CAKey: rootKey,
  80. CrossSignedCACert: ca.NormalizePEMs(crossSignedCert),
  81. }
  82. copied.LastForcedRotation = version
  83. return copied, nil
  84. }
  85. // Checks that a CA URL is connectable using the credentials we have and that its server certificate is signed by the
  86. // root CA that we expect. This uses a TCP dialer rather than an HTTP client; because we have custom TLS configuration,
  87. // if we wanted to use an HTTP client we'd have to create a new transport for every connection. The docs specify that
  88. // Transports cache connections for future re-use, which could cause many open connections.
  89. func validateExternalCAURL(dialer *net.Dialer, tlsOpts *tls.Config, caURL string) error {
  90. parsed, err := url.Parse(caURL)
  91. if err != nil {
  92. return err
  93. }
  94. if parsed.Scheme != "https" {
  95. return errors.New("invalid HTTP scheme")
  96. }
  97. host, port, err := net.SplitHostPort(parsed.Host)
  98. if err != nil {
  99. // It either has no port or is otherwise invalid (e.g. too many colons). If it's otherwise invalid the dialer
  100. // will error later, so just assume it's no port and set the port to the default HTTPS port.
  101. host = parsed.Host
  102. port = "443"
  103. }
  104. conn, err := tls.DialWithDialer(dialer, "tcp", net.JoinHostPort(host, port), tlsOpts)
  105. if conn != nil {
  106. conn.Close()
  107. }
  108. return err
  109. }
  110. // Validates that there is at least 1 reachable, valid external CA for the given CA certificate. Returns true if there is, false otherwise.
  111. // Requires that the wanted cert is already normalized.
  112. func validateHasAtLeastOneExternalCA(ctx context.Context, externalCAs map[string][]*api.ExternalCA, securityConfig *ca.SecurityConfig,
  113. wantedCert []byte, desc string) ([]*api.ExternalCA, error) {
  114. specific, ok := externalCAs[string(wantedCert)]
  115. if ok {
  116. pool := x509.NewCertPool()
  117. pool.AppendCertsFromPEM(wantedCert)
  118. dialer := net.Dialer{Timeout: 5 * time.Second}
  119. opts := tls.Config{
  120. RootCAs: pool,
  121. Certificates: securityConfig.ClientTLSCreds.Config().Certificates,
  122. }
  123. for i, ca := range specific {
  124. if ca.Protocol == api.ExternalCA_CAProtocolCFSSL {
  125. if err := validateExternalCAURL(&dialer, &opts, ca.URL); err != nil {
  126. log.G(ctx).WithError(err).Warnf("external CA # %d is unreachable or invalid", i+1)
  127. } else {
  128. return specific, nil
  129. }
  130. }
  131. }
  132. }
  133. return nil, grpc.Errorf(codes.InvalidArgument, "there must be at least one valid, reachable external CA corresponding to the %s CA certificate", desc)
  134. }
  135. // validates that the list of external CAs have valid certs associated with them, and produce a mapping of subject/pubkey:external
  136. // for later validation of required external CAs
  137. func getNormalizedExtCAs(caConfig *api.CAConfig, normalizedCurrentRootCACert []byte) (map[string][]*api.ExternalCA, error) {
  138. extCAs := make(map[string][]*api.ExternalCA)
  139. for _, extCA := range caConfig.ExternalCAs {
  140. associatedCert := normalizedCurrentRootCACert
  141. // if no associated cert is provided, assume it's the current root cert
  142. if len(extCA.CACert) > 0 {
  143. associatedCert = ca.NormalizePEMs(extCA.CACert)
  144. }
  145. certKey := string(associatedCert)
  146. extCAs[certKey] = append(extCAs[certKey], extCA)
  147. }
  148. return extCAs, nil
  149. }
  150. // validateAndUpdateCA validates a cluster's desired CA configuration spec, and returns a RootCA value on success representing
  151. // current RootCA as it should be. Validation logic and return values are as follows:
  152. // 1. Validates that the contents are complete - e.g. a signing key is not provided without a signing cert, and that external
  153. // CAs are not removed if they are needed. Otherwise, returns an error.
  154. // 2. If no desired signing cert or key are provided, then either:
  155. // - we are happy with the current CA configuration (force rotation value has not changed), and we return the current RootCA
  156. // object as is
  157. // - we want to generate a new internal CA cert and key (force rotation value has changed), and we return the updated RootCA
  158. // object
  159. // 3. Signing cert and key have been provided: validate that these match (the cert and key match). Otherwise, return an error.
  160. // 4. Return the updated RootCA object according to the following criteria:
  161. // - If the desired cert is the same as the current CA cert then abort any outstanding rotations. The current signing key
  162. // is replaced with the desired signing key (this could lets us switch between external->internal or internal->external
  163. // without an actual CA rotation, which is not needed because any leaf cert issued with one CA cert can be validated using
  164. // the second CA certificate).
  165. // - If the desired cert is the same as the current to-be-rotated-to CA cert then a new root rotation is not needed. The
  166. // current to-be-rotated-to signing key is replaced with the desired signing key (this could lets us switch between
  167. // external->internal or internal->external without an actual CA rotation, which is not needed because any leaf cert
  168. // issued with one CA cert can be validated using the second CA certificate).
  169. // - Otherwise, start a new root rotation using the desired signing cert and desired signing key as the root rotation
  170. // signing cert and key. If a root rotation is already in progress, just replace it and start over.
  171. func validateCAConfig(ctx context.Context, securityConfig *ca.SecurityConfig, cluster *api.Cluster) (*api.RootCA, error) {
  172. newConfig := cluster.Spec.CAConfig.Copy()
  173. newConfig.SigningCACert = ca.NormalizePEMs(newConfig.SigningCACert) // ensure this is normalized before we use it
  174. if len(newConfig.SigningCAKey) > 0 && len(newConfig.SigningCACert) == 0 {
  175. return nil, grpc.Errorf(codes.InvalidArgument, "if a signing CA key is provided, the signing CA cert must also be provided")
  176. }
  177. normalizedRootCA := ca.NormalizePEMs(cluster.RootCA.CACert)
  178. extCAs, err := getNormalizedExtCAs(newConfig, normalizedRootCA) // validate that the list of external CAs is not malformed
  179. if err != nil {
  180. return nil, err
  181. }
  182. var oldCertExtCAs []*api.ExternalCA
  183. if !hasSigningKey(&cluster.RootCA) {
  184. oldCertExtCAs, err = validateHasAtLeastOneExternalCA(ctx, extCAs, securityConfig, normalizedRootCA, "current")
  185. if err != nil {
  186. return nil, err
  187. }
  188. }
  189. // if the desired CA cert and key are not set, then we are happy with the current root CA configuration, unless
  190. // the ForceRotate version has changed
  191. if len(newConfig.SigningCACert) == 0 {
  192. if cluster.RootCA.LastForcedRotation != newConfig.ForceRotate {
  193. newRootCA, err := ca.CreateRootCA(ca.DefaultRootCN)
  194. if err != nil {
  195. return nil, grpc.Errorf(codes.Internal, err.Error())
  196. }
  197. return newRootRotationObject(ctx, securityConfig, &cluster.RootCA, newRootCA, oldCertExtCAs, newConfig.ForceRotate)
  198. }
  199. // we also need to make sure that if the current root rotation requires an external CA, those external CAs are
  200. // still valid
  201. if cluster.RootCA.RootRotation != nil && !hasSigningKey(cluster.RootCA.RootRotation) {
  202. _, err := validateHasAtLeastOneExternalCA(ctx, extCAs, securityConfig, ca.NormalizePEMs(cluster.RootCA.RootRotation.CACert), "next")
  203. if err != nil {
  204. return nil, err
  205. }
  206. }
  207. return &cluster.RootCA, nil // no change, return as is
  208. }
  209. // A desired cert and maybe key were provided - we need to make sure the cert and key (if provided) match.
  210. var signingCert []byte
  211. if hasSigningKey(newConfig) {
  212. signingCert = newConfig.SigningCACert
  213. }
  214. newRootCA, err := ca.NewRootCA(newConfig.SigningCACert, signingCert, newConfig.SigningCAKey, ca.DefaultNodeCertExpiration, nil)
  215. if err != nil {
  216. return nil, grpc.Errorf(codes.InvalidArgument, err.Error())
  217. }
  218. if len(newRootCA.Pool.Subjects()) != 1 {
  219. return nil, grpc.Errorf(codes.InvalidArgument, "the desired CA certificate cannot contain multiple certificates")
  220. }
  221. parsedCert, err := helpers.ParseCertificatePEM(newConfig.SigningCACert)
  222. if err != nil {
  223. return nil, grpc.Errorf(codes.InvalidArgument, "could not parse the desired CA certificate")
  224. }
  225. // The new certificate's expiry must be at least one year away
  226. if parsedCert.NotAfter.Before(time.Now().Add(minRootExpiration)) {
  227. return nil, grpc.Errorf(codes.InvalidArgument, "CA certificate expires too soon")
  228. }
  229. if !hasSigningKey(newConfig) {
  230. if _, err := validateHasAtLeastOneExternalCA(ctx, extCAs, securityConfig, newConfig.SigningCACert, "desired"); err != nil {
  231. return nil, err
  232. }
  233. }
  234. // check if we can abort any existing root rotations
  235. if bytes.Equal(normalizedRootCA, newConfig.SigningCACert) {
  236. copied := cluster.RootCA.Copy()
  237. copied.CAKey = newConfig.SigningCAKey
  238. copied.RootRotation = nil
  239. copied.LastForcedRotation = newConfig.ForceRotate
  240. return copied, nil
  241. }
  242. // check if this is the same desired cert as an existing root rotation
  243. if r := cluster.RootCA.RootRotation; r != nil && bytes.Equal(ca.NormalizePEMs(r.CACert), newConfig.SigningCACert) {
  244. copied := cluster.RootCA.Copy()
  245. copied.RootRotation.CAKey = newConfig.SigningCAKey
  246. copied.LastForcedRotation = newConfig.ForceRotate
  247. return copied, nil
  248. }
  249. // ok, everything's different; we have to begin a new root rotation which means generating a new cross-signed cert
  250. return newRootRotationObject(ctx, securityConfig, &cluster.RootCA, newRootCA, oldCertExtCAs, newConfig.ForceRotate)
  251. }