DEV Community

Chris White
Chris White

Posted on

Create Your Own Local Root CA With Yubikey Signing

Major Security Warning

Setting up your own certificate authority needs to be handled with extreme caution. If either the root or intermediate certificates are leaked, someone could impersonate the HTTPS certificate of any site you visit (like google/gmail/etc.). In the case you're uncertain about things, just accepting self signed certs for your local development servers and taking the padlock warning is probably a better idea.

If you plan to setup a corporate security authority please seek professional consultation. This setup is too risky for that type of environment.

Preparation

The root certificate is best created external to the system. I recommend an encrypted USB drive. The one I ended up using was an Apricorn USB drive. It allows me to have a physical pin protect to keep the root cert safe. I also recommend one more USB key. With the setup I'll be showing having in encrypted isn't as much of an issue. For Yubikey you'll want one that supports PIV. The one I have is a YubiKey 5 NFC FIPS, but Yubikey 4 and 5 are supported. I also assume your Yubikey has touch support which supplements PIN protection. That way you'll need to know the pin if you manage physical access, and you'll need to have physical access if you manage to get the pin.

First I recommend doing a scan of the system using something like Microsoft Safety Scanner for Windows or scanning tool of choice for other systems. Anything related to the root key or any time the USB drive that the root key is on is accessed, I make sure to unplug the Ethernet cable first. You can also setup a basic system with the proper Yubikey and openssl related software and handle most of the process in a more secure and offline environment.

I recommend having a browser tab open with this guide before going offline in a private/incognito window, then close out any other network related apps. Having another device with internet connectivity (other PC, smart phone, tablet, etc.) is good if you need to look something up without re-connecting to the internet.

CA Folder Structure

This is the time to disconnect your Ethernet cable or turn off your Wifi. Some laptops even come with an Airplane mode to handle this easily. Plugin your encrypted USB key you plan to use for holding the root key. All commands should be run from the encrypted USB drive.

$ mkdir rootCA
$ mkdir intermediateCA
$ mkdir rootCA/certs
$ mkdir intermediateCA/certs
$ mkdir rootCA/crl
$ mkdir intermediateCA/crl
$ mkdir rootCA/newcerts
$ mkdir intermediateCA/newcerts
$ mkdir rootCA/private
$ mkdir intermediateCA/private
$ mkdir rootCA/csr
$ mkdir intermediateCA/csr
Enter fullscreen mode Exit fullscreen mode

Next are initial serial numbers used to identify certificates in the CA:

$ echo 1000 > rootCA/serial
$ echo 1000 > intermediateCA/serial
$ echo 0100 > rootCA/crlnumber
$ echo 0100 > intermediateCA/crlnumber
Enter fullscreen mode Exit fullscreen mode

And now the database itself:

$ touch rootCA/index.txt
$ touch intermediateCA/index.txt
Enter fullscreen mode Exit fullscreen mode

Note: ni can be used in place of touch in powershell

Now it's time to make some configuration files. These will store path information for our CAs so we don't have to enter it manually, as well as setup defaults for various certificate properties.

openssl_root.cnf

[ ca ]                                                   # The default CA section
default_ca = CA_default                                  # The default CA name

[ CA_default ]                                           # Default settings for the CA
dir               = ./rootCA                             # CA directory
certs             = $dir/certs                           # Certificates directory
crl_dir           = $dir/crl                             # CRL directory
new_certs_dir     = $dir/newcerts                        # New certificates directory
database          = $dir/index.txt                       # Certificate index file
serial            = $dir/serial                          # Serial number file
RANDFILE          = $dir/private/.rand                   # Random number file
private_key       = $dir/private/cakey.pem               # Root CA private key
certificate       = $dir/certs/ca.cert.pem               # Root CA certificate
crl               = $dir/crl/ca.crl.pem                  # Root CA CRL
crlnumber         = $dir/crlnumber                       # Root CA CRL number
crl_extensions    = crl_ext                              # CRL extensions
default_crl_days  = 30                                   # Default CRL validity days
default_md        = sha256                               # Default message digest
preserve          = no                                   # Preserve existing extensions
email_in_dn       = no                                   # Exclude email from the DN
name_opt          = ca_default                           # Formatting options for names
cert_opt          = ca_default                           # Certificate output options
policy            = policy_strict                        # Certificate policy
unique_subject    = no                                   # Allow multiple certs with the same DN

[ policy_strict ]                                        # Policy for stricter validation
countryName             = match                          # Must match the issuer's country
stateOrProvinceName     = match                          # Must match the issuer's state
organizationName        = match                          # Must match the issuer's organization
organizationalUnitName  = optional                       # Organizational unit is optional
commonName              = supplied                       # Must provide a common name
emailAddress            = optional                       # Email address is optional

[ req ]                                                  # Request settings
default_bits        = 2048                               # Default key size
distinguished_name  = req_distinguished_name             # Default DN template
string_mask         = utf8only                           # UTF-8 encoding
default_md          = sha256                             # Default message digest
prompt              = no                                 # Non-interactive mode

[ req_distinguished_name ]                               # Template for the DN in the CSR
countryName                     = US
stateOrProvinceName             = Texas
localityName                    = Dallas
0.organizationName              = cwprogram
organizationalUnitName          = cwprogram Cert Authorization
commonName                      = cwprogram Root Cert Authority
emailAddress                    = me@nospam.com

[ v3_ca ]                                           # Root CA certificate extensions
subjectKeyIdentifier = hash                         # Subject key identifier
authorityKeyIdentifier = keyid:always,issuer        # Authority key identifier
basicConstraints = critical, CA:true                # Basic constraints for a CA
keyUsage = critical, keyCertSign, cRLSign           # Key usage for a CA

[ crl_ext ]                                         # CRL extensions
authorityKeyIdentifier = keyid:always,issuer        # Authority key identifier

[ v3_intermediate_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
Enter fullscreen mode Exit fullscreen mode

The use of 2048 bits along with sha256 is meant to be a balance between security and performance. Anything more complex and it can require additional processor power and may have compatibility issues. req_distinguished_name should be filled out with your appropriate information.

openssl_intermediate.cnf

[ ca ]                           # The default CA section
default_ca = CA_default          # The default CA name

[ CA_default ]                                           # Default settings for the intermediate CA
dir               = ./intermediateCA                     # Intermediate CA directory
certs             = $dir/certs                           # Certificates directory
crl_dir           = $dir/crl                             # CRL directory
new_certs_dir     = $dir/newcerts                        # New certificates directory
database          = $dir/index.txt                       # Certificate index file
serial            = $dir/serial                          # Serial number file
RANDFILE          = $dir/private/.rand                   # Random number file
private_key       = $dir/private/intermediate.key.pem    # Intermediate CA private key
certificate       = $dir/certs/intermediate.cert.pem     # Intermediate CA certificate
crl               = $dir/crl/intermediate.crl.pem        # Intermediate CA CRL
crlnumber         = $dir/crlnumber                       # Intermediate CA CRL number
crl_extensions    = crl_ext                              # CRL extensions
default_crl_days  = 30                                   # Default CRL validity days
default_md        = sha256                               # Default message digest
preserve          = no                                   # Preserve existing extensions
email_in_dn       = no                                   # Exclude email from the DN
name_opt          = ca_default                           # Formatting options for names
cert_opt          = ca_default                           # Certificate output options
policy            = policy_loose                         # Certificate policy

[ policy_loose ]                                         # Policy for less strict validation
countryName             = optional                       # Country is optional
stateOrProvinceName     = optional                       # State or province is optional
localityName            = optional                       # Locality is optional
organizationName        = optional                       # Organization is optional
organizationalUnitName  = optional                       # Organizational unit is optional
commonName              = supplied                       # Must provide a common name
emailAddress            = optional                       # Email address is optional

[ req ]                                                  # Request settings
default_bits        = 2048                               # Default key size
distinguished_name  = req_distinguished_name             # Default DN template
string_mask         = utf8only                           # UTF-8 encoding
default_md          = sha256                             # Default message digest
x509_extensions     = v3_intermediate_ca                 # Extensions for intermediate CA certificate

[ req_distinguished_name ]                               # Template for the DN in the CSR
countryName                     = US
stateOrProvinceName             = Texas
localityName                    = Dallas
0.organizationName              = cwprogram
organizationalUnitName          = cwprogram Cert Authorization
commonName                      = cwprogram Intermediate Cert Authority
emailAddress                    = me@nospam.com

[ v3_intermediate_ca ]                                      # Intermediate CA certificate extensions
subjectKeyIdentifier = hash                                 # Subject key identifier
authorityKeyIdentifier = keyid:always,issuer                # Authority key identifier
basicConstraints = critical, CA:true, pathlen:0             # Basic constraints for a CA
keyUsage = critical, digitalSignature, cRLSign, keyCertSign # Key usage for a CA

[ crl_ext ]                                                 # CRL extensions
authorityKeyIdentifier=keyid:always                         # Authority key identifier

[ server_cert ]                                             # Server certificate extensions
basicConstraints = CA:FALSE                                 # Not a CA certificate
nsCertType = server                                         # Server certificate type
keyUsage = critical, digitalSignature, keyEncipherment      # Key usage for a server cert
extendedKeyUsage = serverAuth                               # Extended key usage for server authentication purposes (e.g., TLS/SSL servers).
authorityKeyIdentifier = keyid,issuer                       # Authority key identifier linking the certificate to the issuer's public key.
Enter fullscreen mode Exit fullscreen mode

This is for the intermediate certificate authority. It will be used to handle most of the signing to reduce the probability of the root key getting compromised. Note that you want to make sure commonName is different for each of your certificates.

Root Certificate Generation

Now it's time to start making the certificates. First we'll need a private key used for signing purposes as well as generating the root certificate:

$ openssl ecparam -genkey -name prime256v1 -out rootCA/private/cakey.pem
$ sudo chmod 400 rootCA/private/cakey.pem
Enter fullscreen mode Exit fullscreen mode

If you're in Windows:

> icacls.exe cakey.pem /reset
> icacls.exe cakey.pem /grant:r "$($env:username):(r)"
> icacls.exe cakey.pem /inheritance:r
Enter fullscreen mode Exit fullscreen mode

Is a reasonable chmod 400 equivalent. Now it's time sign the certificate:

$ openssl req -config openssl_root.cnf -key ./rootCA/private/cakey.pem -new -x509 -days 7300 -sha256 -extensions v3_ca -out ./rootCA/certs/ca.cert.pem -subj "/C=US/ST=Texas/L=Dallas/O=cwprogram/OU=cwprogram Cert Authorization/CN=Root CA"
$ chmod 444 ./rootCA/certs/ca.cert.pem
Enter fullscreen mode Exit fullscreen mode

This creates a self-signed certificate where the issuer of the certificate and the subject of the certificate are the same. It's what defines a certificate as root. You'll want a local copy of ./rootCA/certs/ca.cert.pem somewhere so you can upload it to various trust stores when the time comes.

Intermediate Certificate Generation

The intermediate certificate will handle the actual signing of certificates. First the key is generated:

$ openssl ecparam -genkey -name prime256v1 -out ./intermediateCA/private/intermediate.key.pem
$ sudo chmod 400 ~/myCA/intermediateCA/private/intermediate.key.pem
Enter fullscreen mode Exit fullscreen mode

Next the certificate itself is generated similar to the root cert:

$ openssl req -config openssl_intermediate.cnf -key ./intermediateCA/private/intermediate.key.pem -new -sha256 -out ./intermediateCA/certs/intermediate.csr.pem -subj "/C=US/ST=Texas/L=Dallas/O=cwprogram/OU=cwprogram Cert Authorization/CN=Intermediate CA"
Enter fullscreen mode Exit fullscreen mode

What's different in this case is we'll be using the root certificate to sign the intermediate one. This begins the overall certificate chain of trust:

$ openssl ca -config openssl_root.cnf -extensions v3_intermediate_ca -days 3650 -notext -md sha256 -in ./intermediateCA/certs/intermediate.csr.pem -out ./intermediateCA/certs/intermediate.cert.pem
$ openssl verify -CAfile ./rootCA/certs/ca.cert.pem ./intermediateCA/certs/intermediate.cert.pem
./intermediateCA/certs/intermediate.cert.pem: OK
Enter fullscreen mode Exit fullscreen mode

This shows that the intermediate is properly linked to the root CA. We'll also want to create a certificate bundle linking the intermediate and root certificate together. While you should be fine in most cases it's good to have this around for compatibility reasons. It also prevents needing to access your root key later should a bundle become necessary. The actual process is simply combining the certs as so:

$ cat ./intermediateCA/certs/intermediate.cert.pem ./rootCA/certs/ca.cert.pem > ./intermediateCA/certs/ca-chain.cert.pem
Enter fullscreen mode Exit fullscreen mode

type can be used in Powershell if you're on Windows:

> type ./intermediateCA/certs/intermediate.cert.pem,./rootCA/certs/ca.cert.pem > ./intermediateCA/certs/ca-chain.cert.pem
Enter fullscreen mode Exit fullscreen mode

So now that we have the root CA requirement out of the way it's time to finalize putting the intermediate signing key into Yubikey.

Yubikey Setup

To start out the yubikey-manger will need to be installed. An example on Ubuntu:

$ sudo apt-get install -y yubikey-manager
Enter fullscreen mode Exit fullscreen mode

For Windows you'll need first obtain the Yubikey Manager GUI which comes with the ykman CLI tool necessary. to add to path or create a profile entry as documented. Once everything is setup you'll want to setup pins:

$ ykman piv access change-pin
Enter the current PIN:
Enter the new PIN:
Repeat for confirmation:
New PIN set.
Enter fullscreen mode Exit fullscreen mode

This is for the pin that will be used for general access. The factory default pin is 123456 when it asks for current PIN. Next is the PUK:

$ ykman piv access change-puk
Enter the current PUK:
Enter the new PUK:
Repeat for confirmation:
New PUK set.
Enter fullscreen mode Exit fullscreen mode

This is used for the case that you forgot your pin as a backup of sorts. When it asks for current PUK the factory default is 12345678. For the management key, I tend to use one that's derived from the pin like so:

$ ykman piv access change-management-key --generate --protect
Enter the current management key [blank to use default key]:
Enter fullscreen mode Exit fullscreen mode

Note that you'll want to check the Yubikey site to understand the implications and decide what step you want to take here.

Intermediate Signing Yubikey Import

Now that the setup of Yubikey is done it's time to import the intermediate certificate and key into Yubikey:

$ ykman piv certificates import 9c ./intermediateCA/certs/intermediate.cert.pem
$ ykman piv keys import 9c --pin-policy ALWAYS --touch-policy CACHED  ./intermediateCA/private/intermediate.key.pem
Enter fullscreen mode Exit fullscreen mode

The signing key is now protected by both pin and touch. A touch policy of cached means that you won't have to touch the Yubikey again if it's been touched within 15 seconds. Note that pin policy and touch policy can only be set at import. If you fail to do so and decide to change it later you'll have to essentially re-import the key again. Now that the intermediate key is somewhere safe, we can pull the intermediate CA to another device so the encrypted drive with the root key on it doesn't need to be accessed anymore:

  1. move ./intermediateCA/private/intermediate.key.pem to the root of the encrypted drive. This is to have a backup in case something happens with the Yubikey and it needs to be re-imported.
  2. Now put in the other USB drive
  3. Copy the entirety of intermediateCA over to it
  4. Copy ./rootCA/certs/ca.cert.pem over to it (this is just the certificate, not the signing key, and will be needed for setting up trust as a root certificate authority)
  5. Copy openssl_intermediate.cnf over to it
  6. Safely remove the encrypted drive with the root CA information on it through umount/eject
  7. Store it somewhere safe

Once the encrypted USB drive with root CA information on it has been removed, it's safe to connect to your network again / move the USB drive with the intermediate CA on it to a network connected system. Please note that everything from here on out will be done using the intermediate CA USB drive.

Root Trust Store

Next up is setting our root certificate as trusted. Without this we'll get warnings from browsers trying to validate certificates. Here are how some operating systems handle this:

Some other places you might have to chang include the Java Runtime with its own store for root certs:

$ keytool -import -trustcacerts -cacerts -alias 'cwprogram Root CA' -storepass changeit -file ca.cert.pem
Enter fullscreen mode Exit fullscreen mode

For Windows, git will need to be changed to use the schannel SSL backend so it uses the Windows Trust Store to validate certificates:

> git config --global http.sslBackend schannel
Enter fullscreen mode Exit fullscreen mode

Firefox will need to either have the certificate added to its own store or use the system's via security.enterprise_roots.enabled as per their documentation.

OpenSSL pkcs11 Setup

Now we need to make some changes to OpenSSL so it works with Yubikey when doing signing. For this we'll need two packages:

$ sudo apt-get install -y opensc libengine-pkcs11-openssl
Enter fullscreen mode Exit fullscreen mode

This installs opensc, a library for dealing with Smart Card (essentially what a Yubikey is recognized as) access in a programmatic way. It also installs OpenSSL bindings that interact using the pkcs11 standard. Basically, we won't get very far using a Yubikey for signing without this. The intermediate CA configuration will also need to be updated:

openssl_intermediate.cnf

openssl_conf = openssl_init

[openssl_init]
engines = engine_section

[engine_section]
pkcs11 = pkcs11_section

[pkcs11_section]
engine_id = pkcs11
init = 0

[ ca ]                           # The default CA section
default_ca = CA_default          # The default CA name

[ CA_default ]                                           # Default settings for the intermediate CA
dir               = ./intermediateCA                     # Intermediate CA directory
certs             = $dir/certs                           # Certificates directory
crl_dir           = $dir/crl                             # CRL directory
new_certs_dir     = $dir/newcerts                        # New certificates directory
database          = $dir/index.txt                       # Certificate index file
serial            = $dir/serial                          # Serial number file
RANDFILE          = $dir/private/.rand                   # Random number file
private_key       = pkcs11:manufacturer=piv_II;id=%02    # Intermediate CA private key
certificate       = $dir/certs/intermediate.cert.pem     # Intermediate CA certificate
crl               = $dir/crl/intermediate.crl.pem        # Intermediate CA CRL
crlnumber         = $dir/crlnumber                       # Intermediate CA CRL number
crl_extensions    = crl_ext                              # CRL extensions
default_crl_days  = 30                                   # Default CRL validity days
default_md        = sha256                               # Default message digest
preserve          = no                                   # Preserve existing extensions
email_in_dn       = no                                   # Exclude email from the DN
name_opt          = ca_default                           # Formatting options for names
cert_opt          = ca_default                           # Certificate output options
policy            = policy_loose                         # Certificate policy

[ policy_loose ]                                         # Policy for less strict validation
countryName             = optional                       # Country is optional
stateOrProvinceName     = optional                       # State or province is optional
localityName            = optional                       # Locality is optional
organizationName        = optional                       # Organization is optional
organizationalUnitName  = optional                       # Organizational unit is optional
commonName              = supplied                       # Must provide a common name
emailAddress            = optional                       # Email address is optional

[ req ]                                                  # Request settings
default_bits        = 2048                               # Default key size
distinguished_name  = req_distinguished_name             # Default DN template
string_mask         = utf8only                           # UTF-8 encoding
default_md          = sha256                             # Default message digest
x509_extensions     = v3_intermediate_ca                 # Extensions for intermediate CA certificate

[ req_distinguished_name ]                               # Template for the DN in the CSR
countryName                     = US
stateOrProvinceName             = Texas
localityName                    = Dallas
0.organizationName              = cwprogram
organizationalUnitName          = cwprogram Cert Authorization
commonName                      = cwprogram Intermediate Cert Authority
emailAddress                    = me@nospam.com

[ v3_intermediate_ca ]                                      # Intermediate CA certificate extensions
subjectKeyIdentifier = hash                                 # Subject key identifier
authorityKeyIdentifier = keyid:always,issuer                # Authority key identifier
basicConstraints = critical, CA:true, pathlen:0             # Basic constraints for a CA
keyUsage = critical, digitalSignature, cRLSign, keyCertSign # Key usage for a CA

[ crl_ext ]                                                 # CRL extensions
authorityKeyIdentifier=keyid:always                         # Authority key identifier

[ server_cert ]                                             # Server certificate extensions
basicConstraints = CA:FALSE                                 # Not a CA certificate
nsCertType = server                                         # Server certificate type
keyUsage = critical, digitalSignature, keyEncipherment      # Key usage for a server cert
extendedKeyUsage = serverAuth                               # Extended key usage for server authentication purposes (e.g., TLS/SSL servers).
authorityKeyIdentifier = keyid,issuer                       # Authority key identifier linking the certificate to the issuer's public key.
Enter fullscreen mode Exit fullscreen mode

This will now setup OpenSSL to use the pkcs11 OpenSSL backend. For the private_key value it should be fine as is if you're private key is in 9c. Otherwise you'll need to use pkcs11-tool to find the proper ID:

$ pkcs11-tool --list-objects
Using slot 0 with a present token (0x0)
Public Key Object; EC  EC_POINT 256 bits
  label:      SIGN pubkey
  ID:         02
  Usage:      verify
  Access:     none
Enter fullscreen mode Exit fullscreen mode

As mine is in 9c it shows 02 as the ID. If the ID ends up being different just put it after the pkcs11:manufacturer=piv_II;id=% part.

Example HTTPS Setup: Gitea

To test if this works I'll show how to add HTTPS to a Gitea install I wrote about. The first thing we'll do is create a nice little config for the server certificate:

gitea.cnf

[req]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no

[req_distinguished_name]
C = US
ST = Texas
L = Dallas
O = cwprogram
OU = cwprogram Cert Authorization
CN = gitea

[req_ext]
subjectAltName = @alt_names

[alt_names]
IP.1 = 172.18.139.193
DNS.1 = gitea
Enter fullscreen mode Exit fullscreen mode

I recommend making a config like this customized for each cert you wish to deploy. The important thing to note is the subjectAltName or SAN. It used to be the common name was used for verifying the host. The modern way now is to use the SAN extension to provide host name info when the certificate is validated.

[alt_names]
IP.1 = 172.18.139.193
DNS.1 = gitea
Enter fullscreen mode Exit fullscreen mode

For here I'm using /etc/hosts to bind a gitea hostname to the IP present. Technically it works fine as-is without the IP.1 part. I just tend to like being specific as possible to keep things safe. If you're not sure of the IP or there's too many just ignore and use DNS instead. The items under req_distinguised_name (especially the CN value) will need to be changed if the hostname is not gitea and to match the values you've been using in the root and intermediate cert setup. Now create the private key for the gitea cert first:

$ openssl ecparam -genkey -name prime256v1 -out gitea_private.pem
$ sudo chmod 400 gitea_private.pem
Enter fullscreen mode Exit fullscreen mode

Make the certificate request:

$ openssl req -config ./gitea.cnf -key gitea_private.pem -new -sha256 -out ./intermediateCA/csr/gitea.csr
Enter fullscreen mode Exit fullscreen mode

Then the intermediate will sign it:

$ openssl ca -config ./openssl_intermediate.cnf -engine pkcs11 -keyform engine -extensions server_cert -extensions req_ext -extfile gitea.cnf -days 375 -notext -md sha256 -in ./intermediateCA/csr/gitea.csr -out ./gitea.cert.pem

Engine "pkcs11" set.
Using configuration from ./openssl_intermediate.cnf
Enter PKCS#11 token PIN for Intermediate CA:
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number: 4098 (0x1002)
        Validity
            Not Before: Aug  6 14:59:52 2023 GMT
            Not After : Aug 15 14:59:52 2024 GMT
        Subject:
            countryName               = US
            stateOrProvinceName       = Texas
            localityName              = Dallas
            organizationName          = cwprogram
            organizationalUnitName    = cwprogram Cert Authorization
            commonName                = gitea
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                IP Address:172.18.139.193, DNS:gitea
Certificate is to be certified until Aug 15 14:59:52 2024 GMT (375 days)
Sign the certificate? [y/n]:y
Enter PKCS#11 key PIN for SIGN key:
Enter fullscreen mode Exit fullscreen mode

The PIN is entered twice to validate against the intermediate cert stored, and then do the signing. If you have the touch policy enabled as per instructions you should notice a hang after you enter the PIN a second time to access the key. This is because it's waiting on you to touch authenticate with the Yubikey. Obtaining the private key from the Yubikey is handled by this:

-engine pkcs11 -keyform engine
Enter fullscreen mode Exit fullscreen mode

This tells OpenSSL the value of private_key we set in the config comes from our Yubikey instead of a file. Also of importance:

-extensions req_ext -extfile gitea.cnf
Enter fullscreen mode Exit fullscreen mode

As-is the signing will not pass over the absolutely essential SAN value. So the above forces it to be included with the values we had setup during the client signing request session. Now we need to have gitea recognize the certificate we have. Before copying things over gitea expects a cert bundle with the actual gitea cert and the intermediate cert included. This can be done via:

$ cat gitea.cert.pem ./intermediateCA/certs/intermediate.cert.pem > gitea.cert.bundle.pem
Enter fullscreen mode Exit fullscreen mode

I'll make a folder to store the cert info:

$ sudo mkdir /home/git/certs
$ sudo cp gitea.cert.bundle.pem /home/git/certs/
$ sudo cp gitea_private.pem /home/git/certs/
$ chown -R git:git /home/git/certs
$ sudo chmod 400 /home/git/certs/gitea.cert.bundle.pem
$ sudo chmod 400 /home/git/certs/gitea_private.pem
Enter fullscreen mode Exit fullscreen mode

The git user is what I have setup as gitea's service user (as per recommendation). Now the app configuration needs to be updated:

$ sudo su git -
$ vim /etc/gitea/app.ini
Enter fullscreen mode Exit fullscreen mode

Now I change the server block to something like this:

[server]
SSH_DOMAIN = gitea
DOMAIN = gitea
PROTOCOL = https
CERT_FILE = /home/git/certs/gitea.cert.bundle.pem
KEY_FILE  = /home/git/certs/gitea_private.pem
HTTP_PORT = 3000
ROOT_URL = https://gitea:3000/
DISABLE_SSH = false
SSH_PORT = 22
LFS_START_SERVER = true
LFS_JWT_SECRET = [censored]
OFFLINE_MODE = false
Enter fullscreen mode Exit fullscreen mode

Then simply restart gitea:

$ sudo systemctl restart gitea.service
$ sudo systemctl status gitea.service
● gitea.service - Gitea (Git with a cup of tea)
     Loaded: loaded (/etc/systemd/system/gitea.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-08-08 19:54:13 CDT; 5s ago
   Main PID: 33939 (gitea)
      Tasks: 16 (limit: 18997)
     Memory: 117.2M
     CGroup: /system.slice/gitea.service
             └─33939 /usr/local/bin/gitea web --config /etc/gitea/app.ini
Enter fullscreen mode Exit fullscreen mode

Then check the have the wonderful secure padlock:

Firefox showing site secure

While this is still somewhat of a warning about the source of the root cert (being from Windows store instead of Mozilla's store) the padlock we want is present.

Conclusion

This concludes a look at setting up a root certificate authority locally in a secure manner. Root CA information is stored on an encrypted USB key and the signing key is managed via Yubikey and a separate USB drive. I hope this helps those who really do want to get rid of the annoying cert warnings. Also I hope it means you don't have to deal with piecing together of 10 different sources to try to figure out how this all works!

Top comments (2)

Collapse
 
phlash profile image
Phil Ashby

Thanks for this comprehensive guide Chris!

I'm interested to know in what scenario(s) you think someone would wish to set up a local CA, especially one to this level of protection/paranoia?

What are your thoughts on simply using LetsEncrypt instead?

Collapse
 
cwprogram profile image
Chris White

Thanks for your comment Phil! Just to clarify this is meant to be a local CA just for locally run services like a nginx, jenkins, gitea, etc. Part of it was that doing all this along with making Yubikey signing work wasn't really well documented. So I got it working and documented how I did it in case someone else comes along. It's also not too bad once you know what commands to run, it's just the finding what commands to run is the hard part (and why I wrote the article).

Anything for actual public facing infrastructure needs to be done through an legitimate certificate authority such as let's encrypt ( unless you wan to try and become a public trusted root CA yourself which I wouldn't recommend ( cabforum.org/network-security-requ... ) ). I did notice a few solutions to make local CA setup easier, but many of them weren't really great at handling Yubikey based private key signing.

As for as Let's Encrypt is concerned they've pretty much stated they're not quite what you'd want for local systems with /etc/hosts like resolution ( letsencrypt.org/docs/certificates-... ). It does look like you could do it locally but due to DNS challenges the solution seems a bit much ( blog.heckel.io/2018/08/05/issuing-... ). Not to mention trying to figure out how to manage a local DNS server is its own challenge.

While I do make this seem really scary, as long as you're aware of common security practices and aren't opening up suspicious websites all the time you should be fine (oh and the encrypted drive for root CA of course). This is more a warning towards someone who may be in their early development learning phase that this is something to be taken more seriously than putting up an nginx server. As long as people aren't breaking into houses all the time in your neighborhood (which if they are you should probably move) physical security should be mostly fine.