├── README.md └── create-internal-constrained-pki.sh /README.md: -------------------------------------------------------------------------------- 1 | ## Just want simple TLS for your [`.internal`](https://en.wikipedia.org/wiki/.internal) network? 2 | 3 | Run 4 | 5 | ```sh 6 | ./create-internal-constrained-pki.sh mydomain.internal 7 | ``` 8 | 9 | It creates a root CA certificate that your users (colleagues/friends/family) can **_safely_** add to their devices' trust store because it uses X.509 [`Name Constraints`](https://netflixtechblog.com/bettertls-c9915cd255c0) to provably restrict it to the chosen domain. 10 | 11 | The CA cannot be used to [MitM](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) all traffic. 12 | 13 | Result: 14 | 15 | ``` 16 | certs-and-keys/ 17 | ca-mydomain.internal.crt <- root CA certificate to give to your users 18 | to _safely_ add to their devices' trust store 19 | 20 | wildcard.mydomain.internal.crt <- certificate and key to use for hosting services 21 | wildcard.mydomain.internal.key.pem under mydomain.internal and *.mydomain.internal 22 | ``` 23 | 24 | 25 | ## Verification 26 | 27 | Your users can run 28 | 29 | ```sh 30 | openssl x509 -noout -text -in ca-mydomain.internal.crt 31 | ``` 32 | 33 | to verify which domains the root CA allows; it should show: 34 | 35 | ``` 36 | X509v3 Name Constraints: critical 37 | Permitted: 38 | DNS:mydomain.internal 39 | DNS:.mydomain.internal 40 | ``` 41 | 42 | ## Important 43 | 44 | * Read the code of `create-internal-constrained-pki.sh` to see if it suites your goals: 45 | * Default `VALIDITY_DAYS="3650"` 46 | * **No passphrases:** The generated keys will be unencrypted (no passphrase) to allow the script to run without prompts. **Generate them directly onto at-rest encrypted storage.** If you want passphrases instead, add e.g. `-aes256` to the `openssl genrsa` invocations. 47 | 48 | 49 | ## Literature 50 | 51 | * Security StackExchange: [Can I restrict a Certification Authority to signing certain domains only?](https://security.stackexchange.com/questions/31376/can-i-restrict-a-certification-authority-to-signing-certain-domains-only/130674#130674) 52 | 53 | * https://systemoverlord.com/2020/06/14/private-ca-with-x-509-name-constraints.html 54 | with `openssl` instructions 55 | 56 | * https://utcc.utoronto.ca/~cks/space/blog/tech/TLSInternalCANameConstraints 57 | 58 | * https://utcc.utoronto.ca/~cks/space/blog/tech/TLSInternalCANameConstraintsII?showcomments 59 | 60 | * [`step-ca`](https://smallstep.com/docs/step-ca/) is easier than `openssl`, but apparently can use `Name Constraints` only in intermediate certificates: 61 | https://smallstep.com/docs/step-ca/templates/#adding-name-constraints 62 | So this does not meet our goal. 63 | https://smallstep.com/docs/step-ca/#limitations also says: 64 | 65 | > Its root CA is always offline; a single-tier PKI is not supported 66 | 67 | Further, see https://github.com/caddyserver/caddy/issues/5759 68 | 69 | * Support in clients was originally bad, where many would allow to bypass the Name Constraints: 70 | https://news.ycombinator.com/item?id=37537689 71 | 72 | * The spec does not even require that Name Constraint be enforced on Root CAs, only on intermediates: 73 | https://issues.chromium.org/issues/40685439 74 | 75 | That creates the same problem as above, not meeting the goal. 76 | 77 | However, Chrome now [supports it](https://issues.chromium.org/issues/40685439) properly, and OpenSSL and Firefox already did before. 78 | 79 | https://bettertls.com tracks which implementations support it how well. 80 | Good write-up: https://netflixtechblog.com/bettertls-c9915cd255c0 81 | 82 | * Important point from https://github.com/caddyserver/caddy/issues/5759#issuecomment-1690700681: 83 | 84 | > People using name constraints should know what they exactly mean, as some cases are not obvious. For example, **adding just `permittedDNSDomains` as above does not exclude creating domains with IP addresses or any other type of SAN**. Name constraints are defined in [RFC5280#4.2.1.10](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10) 85 | 86 | * See also: 87 | https://github.com/FiloSottile/mkcert/pull/309/commits/922158ed6856077c8b07478d67e0a7a930b90510#r1805672121 88 | -------------------------------------------------------------------------------- /create-internal-constrained-pki.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -eu -o pipefail 4 | 5 | # Creates an .internal X.509 PKI whose root CA is safe to include 6 | # system-wide for others. 7 | # 8 | # This makes it easy to add TLS to local networks or VPNs. 9 | # 10 | # Safe because it uses `nameConstraints` so it can only be used 11 | # for `.mydomain.internal` domains and it cannot be used 12 | # to MITM other DNS names (but potentially IP addresses, see 13 | # https://github.com/caddyserver/caddy/issues/5759#issuecomment-1690700681). 14 | # 15 | # If you set 16 | # BASE_DOMAIN="mydomain.internal" 17 | # the script will generate a wildcard certificate: 18 | # mydomain.internal 19 | # *.mydomain.internal 20 | # 21 | # The generated keys will be unencrypted (no passphrase) 22 | # to allow the script to run without prompts. 23 | # Generate them directly onto at-rest encrypted storage. 24 | # If you want passphrases, add e.g. `-aes256` to the 25 | # `openssl genrsa` invocations. 26 | # 27 | # Requires `openssl` on `$PATH`. 28 | # 29 | # Based on: 30 | # https://systemoverlord.com/2020/06/14/private-ca-with-x-509-name-constraints.html 31 | 32 | 33 | BASE_DOMAIN="${1:-"mydomain.internal"}" # change `mydomain` to a name of your choice 34 | echo $BASE_DOMAIN 35 | 36 | VALIDITY_DAYS="3650" # 10 years 37 | 38 | mkdir -p certs-and-keys/ 39 | cd certs-and-keys/ 40 | 41 | 42 | # Create CA 43 | if [ -f "ca-${BASE_DOMAIN}.key.pem" ]; then 44 | echo >&2 "Will not overwrite existing: "ca-${BASE_DOMAIN}.key.pem"" 45 | else 46 | 47 | # Create CA key 48 | 49 | set -x 50 | openssl genrsa -out "ca-${BASE_DOMAIN}.key.pem" 4096 51 | openssl req -new -key "ca-${BASE_DOMAIN}.key.pem" -batch -out "ca-${BASE_DOMAIN}.csr" -utf8 -subj '/O=Internal' 52 | { set +x; } 2> /dev/null 53 | 54 | 55 | # Create CA cert 56 | 57 | cat <caext-${BASE_DOMAIN}.ini 58 | basicConstraints = critical, CA:TRUE 59 | keyUsage = critical, keyCertSign, cRLSign 60 | subjectKeyIdentifier = hash 61 | nameConstraints = critical, permitted;DNS:${BASE_DOMAIN} , permitted;DNS:.${BASE_DOMAIN} 62 | EOF 63 | set -x 64 | openssl x509 -req -sha256 -days "$VALIDITY_DAYS" -in "ca-${BASE_DOMAIN}.csr" -signkey "ca-${BASE_DOMAIN}.key.pem" -extfile "caext-${BASE_DOMAIN}.ini" -out "ca-${BASE_DOMAIN}.crt" 65 | { set +x; } 2> /dev/null 66 | 67 | # Create serial counter 68 | echo 1000 > ""ca-${BASE_DOMAIN}.srl"" 69 | fi 70 | 71 | 72 | 73 | # Create certificate for the desired domains. 74 | 75 | # *.${BASE_DOMAIN} 76 | if [ -f wildcard.${BASE_DOMAIN}.key.pem ]; then 77 | echo >&2 "Will not overwrite existing: wildcard.${BASE_DOMAIN}.key.pem" 78 | else 79 | set -x 80 | openssl genrsa -out wildcard.${BASE_DOMAIN}.key.pem 2048 81 | openssl req -new -key wildcard.${BASE_DOMAIN}.key.pem -batch -out "wildcard.${BASE_DOMAIN}.csr" -utf8 -subj "/CN=*.${BASE_DOMAIN}" 82 | { set +x; } 2> /dev/null 83 | 84 | cat <<'EOF' >"certext-wildcard.${BASE_DOMAIN}.ini" 85 | basicConstraints = critical, CA:FALSE 86 | subjectKeyIdentifier = hash 87 | authorityKeyIdentifier = keyid:always 88 | nsCertType = server 89 | authorityKeyIdentifier = keyid, issuer:always 90 | keyUsage = critical, digitalSignature, keyEncipherment 91 | extendedKeyUsage = serverAuth 92 | subjectAltName = ${ENV::CERT_SAN} 93 | EOF 94 | set -x 95 | CERT_SAN="DNS:${BASE_DOMAIN},DNS:*.${BASE_DOMAIN}" openssl x509 -req -sha256 -days "$VALIDITY_DAYS" -in "wildcard.${BASE_DOMAIN}.csr" -CAkey "ca-${BASE_DOMAIN}.key.pem" -CA "ca-${BASE_DOMAIN}.crt" -CAserial "ca-${BASE_DOMAIN}.srl" -out "wildcard.${BASE_DOMAIN}.crt" -extfile "certext-wildcard.${BASE_DOMAIN}.ini" 96 | { set +x; } 2> /dev/null 97 | fi 98 | 99 | # Check 100 | set -x 101 | openssl verify -CAfile "ca-${BASE_DOMAIN}.crt" "wildcard.${BASE_DOMAIN}.crt" 102 | { set +x; } 2> /dev/null 103 | --------------------------------------------------------------------------------