Repository: deis/deis

Issue:
Application SSL does not support SAN certificates
When I register a SAN certificate using `deis certs:add` only the common name is picked up, not the other SANs (which can be entierly different domain names)

An example certificate can have: `CN=example.com` and `SAN=DNS:example.co.uk,DNS:example.no,DNS:example.io,DNS:my-aweseome-example.com,DNS:other-website.com`

See this for a guide on how to create SAN certificates: http://apetec.com/support/GenerateSAN-CSR.htm

My (hopefully temporary) workaround is to just use platform SSL certificates for now and remove the application SSL solution from my `deis-router`.



Discussion:
Thanks for reporting this, @Blystad. SAN support is definitely something we want. We're going to look at adding a flag for `deis certs:add` like `--domains` that would allow you to specify which domains a cert should be added to, bypassing our inspection for "normal" SSL certs. This should enable us to support SANs with minimal code changes. Does that make sense?

Sounds good to me.

So to gather requirements here with an example:
- We have an SSL certificate with common name `foo.deisapp.com`
- A certificate contains SAN entries `bar.deisapp.com`, `car.deisapp.com`, `star.deisapp.com`
- `deis certs:add <cert>` by default should attach all SAN entries
- if `--subjaltnames bar.deisapp.com,car.deisapp.com` is supplied, only those two alt names are read in
- after creating the certificate, no other certificate may claim those names

In that case, this could be an easy PR by just creating additional Certificate objects in the database for each subject alt name. We already have the infrastructure in the controller for this, so it'd just be a client change. This has the effect of having to manually delete each entry for each SAN, but for some users this may be a benefit. Then again, SSL is not my strong point.

@Blystad does this seem good to you?

Sounds good to me.

What about wildcard SSL certificates? (Unrelated isse, but semi relevant)
On 21 Apr 2015 19:23, "Matthew Fisher" notifications@github.com wrote:

> So to gather requirements here with an example:
> - We have an SSL certificate with common name foo.deisapp.com
> - A certificate contains SAN entries bar.deisapp.com, car.deisapp.com,
>   star.deisapp.com
> - deis certs:add <cert> by default should attach all SAN entries
> - if --subjaltnames bar.deisapp.com,car.deisapp.com is supplied, only
>   those two alt names are read in
> - after creating the certificate, no other certificate may claim those
>   names
> 
> In that case, this could be an easy PR by just creating additional
> Certificate objects in the database for each subject alt name. We already
> have the infrastructure in the controller for this, so it'd just be a
> client change. This has the effect of having to manually delete each entry
> for each SAN, but for some users this may be a benefit. Then again, SSL is
> not my strong point.
> 
> @Blystad https://github.com/Blystad does this seem good to you?
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/deis/deis/issues/3441#issuecomment-94878849.

We don't currently support them at this time. We need to address the workflow for that: https://github.com/deis/deis/issues/1608

There is currently some infrastructure in place where you can attach a certificate and a key, but give it a different common name so a wildcard certificate _could_ be attached to a specific subdomain, but that means that you'd have to attach the wildcard certificate to every domain which is something that I'd imagine users wouldn't want at this time :)


Resolving patch:
diff --git a/client/deis.py b/client/deis.py
--- a/client/deis.py
+++ b/client/deis.py
@@ -1043,18 +1043,36 @@ def certs_add(self, args):
         """
         Binds a certificate/key pair to an application.
 
-        Usage: deis certs:add <cert> <key>
+        Usage: deis certs:add <cert> <key> [options]
 
         Arguments:
           <cert>
             The public key of the SSL certificate.
           <key>
             The private key of the SSL certificate.
+
+        Options:
+          --common-name=<cname>
+            The common name of the certificate. If none is provided, the controller will
+            interpret the common name from the certificate.
+          --subject-alt-names=<sans>
+            The subject alternate names (SAN) of the certificate, separated by commas. This will
+            create multiple Certificate objects in the controller, one for each SAN.
         """
         cert = args.get('<cert>')
         key = args.get('<key>')
+        self._certs_add(cert, key, args.get('--common-name'))
+        sans = args.get('--subject-alt-names')
+        if sans:
+            [self._certs_add(cert, key, san) for san in sans.split(',')]
+
+    def _certs_add(self, cert, key, common_name=None):
         body = {'certificate': file(cert).read().strip(), 'key': file(key).read().strip()}
-        sys.stdout.write("Adding SSL endpoint... ")
+        if common_name:
+            body['common_name'] = common_name
+            sys.stdout.write("Adding SSL endpoint {}...".format(common_name))
+        else:
+            sys.stdout.write("Adding SSL endpoint... ")
         sys.stdout.flush()
         try:
             progress = TextProgress()
@@ -1066,7 +1084,8 @@ def certs_add(self, args):
         if response.status_code == requests.codes.created:
             self._logger.info("done")
             data = response.json()
-            self._logger.info("{common_name}".format(**data))
+            if not common_name:
+                self._logger.info("{common_name}".format(**data))
         else:
             raise ResponseError(response)
 
diff --git a/controller/api/serializers.py b/controller/api/serializers.py
--- a/controller/api/serializers.py
+++ b/controller/api/serializers.py
@@ -275,8 +275,9 @@ class Meta:
         """Metadata options for a DomainCertSerializer."""
         model = models.Certificate
         extra_kwargs = {'certificate': {'write_only': True},
-                        'key': {'write_only': True}}
-        read_only_fields = ['common_name', 'expires', 'created', 'updated']
+                        'key': {'write_only': True},
+                        'common_name': {'required': False}}
+        read_only_fields = ['expires', 'created', 'updated']
 
 
 class PushSerializer(ModelSerializer):


Test patch:
diff --git a/controller/api/tests/test_certificate.py b/controller/api/tests/test_certificate.py
--- a/controller/api/tests/test_certificate.py
+++ b/controller/api/tests/test_certificate.py
@@ -80,6 +80,20 @@ def test_create_certificate_with_domain(self):
                                     HTTP_AUTHORIZATION='token {}'.format(self.token))
         self.assertEqual(response.status_code, 201)
 
+    def test_create_certificate_with_different_common_name(self):
+        """
+        In some cases such as with SAN certificates, the certificate can cover more
+        than a single domain. In that case, we want to be able to specify the common
+        name for the certificate/key.
+        """
+        body = {'certificate': self.autotest_example_com_cert,
+                'key': self.key,
+                'common_name': 'foo.example.com'}
+        response = self.client.post(self.url, json.dumps(body), content_type='application/json',
+                                    HTTP_AUTHORIZATION='token {}'.format(self.token))
+        self.assertEqual(response.status_code, 201)
+        self.assertEqual(response.data['common_name'], 'foo.example.com')
+
     def test_get_certificate_screens_data(self):
         """
         When a user retrieves a certificate, only the common name and expiry date should be