ca_rotation.go 12 KB

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