valpackett / freshcerts

ACME certificate protocol (Let's Encrypt) proxy client with a dashboard and monitoring
The Unlicense
60 stars 11 forks source link

Multi-domain certs? #5

Closed joegsn closed 7 years ago

joegsn commented 7 years ago

Hi,

I was trying to figure out if it's possible to create the multi-domain certs for which LetsEncrypt introduced support. It seems like they're doing something outside of the CN entries in the openssl CSR, so I'm thinking that freshcerts does not yet support this functionality. Please let me know if it's currently implemented. There are quite a few things I like about the freshcerts design, but the multi-domain certs one is an upcoming need.

valpackett commented 7 years ago

@joegsn Multi-domain means Subject Alternative Name. If you look at the client script, you can see that you can pass any CSR fields to openssl. Should work.

However, it only monitors one domain per certificate for now.

joegsn commented 7 years ago

@myfreeweb I've tried two different subject fields in the CSR creation, and cannot get either to work. One uses the subjectAltName, which returns a certificate, but the altname doesn't seem to be anywhere on it, and the other uses multiple CN fields, which the LetsEncrypt service throws an error back at me. (TLD has been search/replaced in this comment.)

env FRESHCERTS_HOST="http://cert.mydomain.com:9393" ./freshcerts-client ep1.mydomain.com "/CN=ep1.mydomain.com/subjectAltName=DNS.1=ep2.mydomain.com" 443 "echo hello" "eyJ0eXA<snip>LtS6ysMQ" This returns a working cert, but it's only valid for ep1.mydomain.com.

env FRESHCERTS_HOST="http://cert.mydomain.com:9393" ./freshcerts-client ep1.mydomain.com "/CN=ep1.mydomain.com/CN=ep2.mydomain.com" 443 "echo hello" "eyJ0eXA<snip>QhC2KIOA" For this one, it fails to issue a cert. On the server console, it kicks back an error:

10.135.13.23 - - [31/Jan/2017:08:01:33 -0600] "GET /v1/cert/ep1.mydomain.com/should_reissue HTTP/1.1" 200 36 0.0175
I, [2017-01-31T08:01:35.525803 #33289]  INFO -- : make_challenge domain=ep1.mydomain.com id=wjrTjeIgXXrIGFBNNsz0j03cjSSUwFY6ThfD-2Q7SpI
I, [2017-01-31T08:01:36.090621 #33289]  INFO -- : verify_challenge domain=ep1.mydomain.com id=wjrTjeIgXXrIGFBNNsz0j03cjSSUwFY6ThfD-2Q7SpI status=valid
2017-01-31 08:01:36 - Acme::Client::Error::Unauthorized - Error creating new cert :: Authorizations for these names not found or expired: ep2.mydomain.com:
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/acme-client-0.5.0/lib/acme/client/faraday_middleware.rb:43:in `raise_on_error!'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/acme-client-0.5.0/lib/acme/client/faraday_middleware.rb:33:in `on_complete'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/acme-client-0.5.0/lib/acme/client/faraday_middleware.rb:18:in `block in call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/faraday-0.11.0/lib/faraday/response.rb:61:in `on_complete'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/acme-client-0.5.0/lib/acme/client/faraday_middleware.rb:18:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/faraday-0.11.0/lib/faraday/rack_builder.rb:139:in `build_response'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/faraday-0.11.0/lib/faraday/connection.rb:377:in `run_request'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/faraday-0.11.0/lib/faraday/connection.rb:177:in `post'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/acme-client-0.5.0/lib/acme/client.rb:73:in `new_certificate'
    /usr/home/joegsn/freshcerts/app.rb:116:in `issue'
    /usr/home/joegsn/freshcerts/app.rb:68:in `block in <class:App>'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1611:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1611:in `block in compile!'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:975:in `block (3 levels) in route!'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:994:in `route_eval'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:975:in `block (2 levels) in route!'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1015:in `block in process_route'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1013:in `catch'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1013:in `process_route'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:973:in `block in route!'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:972:in `each'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:972:in `route!'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1085:in `block in dispatch!'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1067:in `block in invoke'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1067:in `catch'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1067:in `invoke'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1082:in `dispatch!'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:907:in `block in call!'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1067:in `block in invoke'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1067:in `catch'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1067:in `invoke'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:907:in `call!'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:895:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-protection-1.5.3/lib/rack/protection/xss_header.rb:18:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-protection-1.5.3/lib/rack/protection/path_traversal.rb:16:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-protection-1.5.3/lib/rack/protection/json_csrf.rb:18:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-protection-1.5.3/lib/rack/protection/base.rb:49:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-protection-1.5.3/lib/rack/protection/base.rb:49:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-protection-1.5.3/lib/rack/protection/frame_options.rb:31:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-1.6.5/lib/rack/logger.rb:15:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:212:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-1.6.5/lib/rack/head.rb:13:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:182:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:2013:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1487:in `block in call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1787:in `synchronize'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:1487:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-attack-4.3.1/lib/rack/attack.rb:106:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-1.6.5/lib/rack/tempfile_reaper.rb:15:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-1.6.5/lib/rack/lint.rb:49:in `_call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-1.6.5/lib/rack/lint.rb:37:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-1.6.5/lib/rack/showexceptions.rb:24:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-1.6.5/lib/rack/commonlogger.rb:33:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/sinatra-1.4.7/lib/sinatra/base.rb:219:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-1.6.5/lib/rack/chunked.rb:54:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/rack-1.6.5/lib/rack/content_length.rb:15:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/puma-3.6.2/lib/puma/configuration.rb:225:in `call'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/puma-3.6.2/lib/puma/server.rb:578:in `handle_request'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/puma-3.6.2/lib/puma/server.rb:415:in `process_client'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/puma-3.6.2/lib/puma/server.rb:275:in `block in run'
    /usr/home/joegsn/freshcerts/vendor/bundle/ruby/2.3/gems/puma-3.6.2/lib/puma/thread_pool.rb:116:in `block in spawn_thread'
10.135.13.23 - - [31/Jan/2017:08:01:36 -0600] "POST /v1/cert/ep1.mydomain.com/issue HTTP/1.1" 500 30 1.3509
valpackett commented 7 years ago

Is .well-known/acme-challenge available on both domains? Authorizations for these names not found or expired: ep2.mydomain.com is an error from Let's Encrypt, it can't verify the second domain. Maybe that's why the SAN CSR doesn't contain the second domain — LE couldn't verify it, so it just excluded it from the certificate.

joegsn commented 7 years ago

Yes, .well-known/acme-challenge is in the nginx configuration for both domains (or, in this case, both domains use the same configuration in nginx). Log entries on the freshcert server are create when doing http://ep1.mydomain.com/.well-known/acme-challenge as well as http://ep2.mydomain.com/.well-known/acme-challenge . (404's in both cases, as they need an :id, obviously).

valpackett commented 7 years ago

Oh damn, right, the challenge is tied to the domain.

valpackett commented 7 years ago

Please test the fix! I haven't tested it…

Use comma-separated domains ep1.mydomain.com,ep2.mydomain.com in place of the single domain, and subjectAltName.

joegsn commented 7 years ago

I do see that with this patch the LetsEncrypt service does do validations on both subdomains.

It does not seem to generate a cert which has SAN entries. env FRESHCERTS_HOST="http://cert.mydomain.com:9393" ./freshcerts-client ep1.mydomain.com,ep2.mydomain.com "/CN=ep1.mydomain.com/subjectAltName=DNS.1=ep2.mydomain.com" 443 "echo hello" "eyJ0eXA<snip>WM4EQ"

I've taken a look at the CSR that's generated from the key openssl req -new -batch -subj "/CN=ep1.mydomain.com/subjectAltName=DNS.1=ep2.mydomain.com" -key "/usr/local/etc/certs/ep1.mydomain.com,ep2.mydomain.com.key.pem.new" -out /dev/stdout | openssl req -text -noout and here's the start of it:

Certificate Request:
    Data:
        Version: 0 (0x0)
        Subject: CN=ep1.mydomain.com/subjectAltName=DNS.1=ep2.mydomain.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:be:1f:35:1e:4a:d5:eb:24:54:fd:4e:e0:c4:8f:
<snip>

The certificate returned by LetsEncrypt does not seem to have the SAN fields:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            03:cc:dd:93:6b:03:d4:dd:61:1d:45:45:31:97:0b:63:16:87
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X3
        Validity
            Not Before: Jan 31 14:23:00 2017 GMT
            Not After : May  1 14:23:00 2017 GMT
        Subject: CN=ep1.mydomain.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:e2:83:d9:cb:24:5b:7a:8b:b7:be:56:c5:2d:24:
                    <snip>
                    c3:b7
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Subject Key Identifier:
                44:FC:43:9F:D1:6C:FE:BE:6C:4F:BC:F9:47:C4:3F:38:8F:14:38:F5
            X509v3 Authority Key Identifier:
                keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1

            Authority Information Access:
                OCSP - URI:http://ocsp.int-x3.letsencrypt.org/
                CA Issuers - URI:http://cert.int-x3.letsencrypt.org/

            X509v3 Subject Alternative Name:
                DNS:ep1.mydomain.com
            X509v3 Certificate Policies:
                Policy: 2.23.140.1.2.1
                Policy: 1.3.6.1.4.1.44947.1.1.1
                  CPS: http://cps.letsencrypt.org
                  User Notice:
                    Explicit Text: This Certificate may only be relied upon by Relying Parties and only in accordance with the Certificate Policy found at https://letsencrypt.org/repository/

    Signature Algorithm: sha256WithRSAEncryption
         2d:9a:98:c5:26:bb:f9:fb:46:93:0c:86:fe:d0:a7:f6:c9:25:
         <snip>
         ca:40:54:b2

You can see that it has the X509v3 SAN field filled in, but it's lacking the second entry.

valpackett commented 7 years ago

Apparently the CN also needs to be included as one of the SAN entries

joegsn commented 7 years ago

Just tried: env FRESHCERTS_HOST="http://cert.mydomain.com:9393" ./freshcerts-client ep1.mydomain.com,ep2.mydomain.com "/CN=ep1.mydomain.com/subjectAltName=DNS.1=ep1.mydomain.com,DNS.2=ep2.mydomain.com" 443 "echo hello" "eyJ0eXA<snip>e9QSoEQ"

That still doesn't generate a cert with multiple SAN entries:

> openssl x509 -in /usr/local/etc/certs/ep1.mydomain.com,ep2.mydomain.com.cert.fullchain.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            fa:63:cd:b8:10:96:6e:59:0c:a1:10:ae:31:6c:a3:46:84:60
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=Fake LE Intermediate X1
        Validity
            Not Before: Jan 31 15:06:00 2017 GMT
            Not After : May  1 15:06:00 2017 GMT
        Subject: CN=ep1.mydomain.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:df:96:29:4a:49:2d:97:af:7c:61:dc:b9:0e:a9:
                    <snip>
                    15:61
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Subject Key Identifier:
                BB:D8:69:C1:35:BA:65:F3:EA:ED:AC:36:5D:87:DA:D3:D1:5E:85:17
            X509v3 Authority Key Identifier:
                keyid:C0:CC:03:46:B9:58:20:CC:5C:72:70:F3:E1:2E:CB:20:A6:F5:68:3A

            Authority Information Access:
                OCSP - URI:http://ocsp.stg-int-x1.letsencrypt.org/
                CA Issuers - URI:http://cert.stg-int-x1.letsencrypt.org/

            X509v3 Subject Alternative Name:
                DNS:ep1.mydomain.com
            X509v3 Certificate Policies:
                Policy: 2.23.140.1.2.1
                Policy: 1.3.6.1.4.1.44947.1.1.1
                  CPS: http://cps.letsencrypt.org
                  User Notice:
                    Explicit Text: This Certificate may only be relied upon by Relying Parties and only in accordance with the Certificate Policy found at https://letsencrypt.org/repository/

    Signature Algorithm: sha256WithRSAEncryption
         56:b0:ec:3d:13:80:15:26:d1:7a:f6:b1:74:96:56:78:5f:23:
         <snip>
         d0:dd:f5:cb
valpackett commented 7 years ago

Oh. So you need to use the weird openssl config-based thing for SAN. The subjectAltName field in the subject is not actually a standard syntax, it is supported by some CAs. What the hell even.

joegsn commented 7 years ago

Yikes! I'll give that a try in a little bit of a hardcoded client edit, and see what happens.

valpackett commented 7 years ago

I think I'll just write a second client in Ruby that supports SAN easily. The openssl binary is total garbage >_<

joegsn commented 7 years ago

Well, with a little hardcoding, it definitely generated the certificate with SAN entries for both subdomains. Thanks for doing all that work, and trumping my research on openssl.

valpackett commented 7 years ago

I have added freshcerts-multi-client, please try it! (See README for usage, it's a bit different)

joegsn commented 7 years ago

I have tried it, and it works. Thanks for doing that so very quickly!