Uploaded on Dec. 12, 2025

Last modified on Dec. 12, 2025

Build a CA with Vault & issue EAP-TLS Certificates for FreeRADIUS

A short tutorial on how to build a CA with Vault & issue (server & client) certificates for EAP-TLS authentication powered by FreeRADIUS. Part of my "upinem" project to stream-line the implementation of EAP-TLS enabled eduroam systems.

Related project:

Tech

Build a CA with Vault & issue EAP-TLS Certificates for FreeRADIUS

You can find more code and technical stuff on how to use Vault with FreeRADIUS from the following repo.
GitHub: joshua-liew/up-in-em

Step 1 (Vault): Generate Root CA

  • On the default installation of FreeRADIUS, test certificates for EAP-TLS are provided. These can be found within the /etc/freeradius/certs directory. Within that directory is the ca.cnf file, which contains the configuration used to generate the self-signed CA for the test certificates. Using this as a reference, we can go about generating the CA in Vault.

    GitHub: /raddb/certs/ca.cnf

[ ca ]
default_ca      = CA_default

[ CA_default ]
dir         = ./
certs           = $dir
crl_dir         = $dir/crl
database        = $dir/index.txt
new_certs_dir       = $dir
certificate     = $dir/ca.pem
serial          = $dir/serial
crl         = $dir/crl.pem
private_key     = $dir/ca.key
RANDFILE        = $dir/.rand
name_opt        = ca_default
cert_opt        = ca_default
default_days        = 60
default_crl_days    = 30
default_md      = sha256
preserve        = no
policy          = policy_match
crlDistributionPoints   = URI:http://www.example.org/example_ca.crl

[ policy_match ]
countryName     = match
stateOrProvinceName = match
organizationName    = match
organizationalUnitName  = optional
commonName      = supplied
emailAddress        = optional

[ policy_anything ]
countryName     = optional
stateOrProvinceName = optional
localityName        = optional
organizationName    = optional
organizationalUnitName  = optional
commonName      = supplied
emailAddress        = optional

[ req ]
prompt          = no
distinguished_name  = certificate_authority
default_bits        = 2048
input_password      = whatever
output_password     = whatever
x509_extensions     = v3_ca

[certificate_authority]
countryName     = FR
stateOrProvinceName = Radius
localityName        = Somewhere
organizationName    = Example Inc.
emailAddress        = admin@example.org
commonName      = "Example Certificate Authority"

[v3_ca]
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always,issuer:always
basicConstraints    = critical,CA:true
crlDistributionPoints   = URI:http://www.example.org/example_ca.crl
  1. Enable the pki secrets engine at pki_root path using the sys/mounts endpoint. The pki_root engine will be used to generate the Root CA & subsequently sign the Intermediate CA.

    API: [POST] /sys/mounts/:path

curl --header "X-Vault-Token: $VAULT_TOKEN" \
   --request POST \
   --data '{"type":"pki"}' \
   $VAULT_ADDR/v1/sys/mounts/pki_root
  1. Tune the pki_root engine to issue certificates with max TTL of 87600 hours. This corresponds to the max TTL of the Root CA generated by pki_root engine.

    NOTE: set the max_lease_ttl as necessary.
    API: [POST] /sys/mounts/:path/tune

curl --header "X-Vault-Token: $VAULT_TOKEN" \
   --request POST \
   --data '{"max_lease_ttl":"87600h"}' \
   $VAULT_ADDR/v1/sys/mounts/pki_root/tune
  1. Create an API request payload (payload-gen-root-ca.json) containing information to generate the root certificate. Refer to the API to understand which parameters can be used.
    • max_path_length: Security Best Practice. Set to 1 to prevent the Intermediate CA from issuing another Intermediate CA.
    • issuer_name: This parameter will be used as the issue_ref parameter when creating a Root CA role.

      NOTE: change parameters as necessary. Some of the parameters set here will be seen in the EAP-TLS process in FreeRADIUS later on in attributes such as TLS-Cert-Subject and TLS-Cert-Issuer.
      API: [POST] /pki/root/generate/:type

{
  "common_name": "Upinem AOU",
  "issuer_name": "root-ca",
  "ttl": "87600h",
  "organization": "Aomori University",
  "country": "JP",
  "province": "Aomori-ken",
  "locality": "Aomori-shi",
  "max_path_length": 1,
  "key_bits": 4096,
  "key_type": "rsa"
}
  1. Generate the root certificate and extract it. This generates a new self-signed CA certificate and private key.

    API: [POST] /pki/root/generate/:type

curl --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data @payload-gen-root-ca.json \
  $VAULT_ADDR/v1/pki_root/root/generate/internal \
  | jq -r ".data.certificate" > root_ca.pem
  1. List the issuers and extract the issuer ID.

    API: [LIST] /pki/issuers

curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
  --request LIST \
  $VAULT_ADDR/v1/pki_root/issuers \
  | jq -r ".data.keys[]"

# Extract and export it as a variable
export ISSUER_ROOT_ID=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" --request LIST $VAULT_ADDR/v1/pki_root/issuers | jq -r ".data.keys[]")
  1. Read the issuer with the ID to get the certificates and other metadata about the issuer.

    API: [GET] /pki/issuer/:issuer_ref(/json)

curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
  $VAULT_ADDR/v1/pki_root/issuer/$ISSUER_ROOT_ID | jq
  1. Create an API request payload (payload-role-root-ca.json) to create a role for the root CA. Here are the list of important parameters.
    • allow_any_name: Required for flexibility to sign the Intermediate CA's CSR, which will contain its own CN and subject fields. Rely on max_path_length for security.
    • issuer_ref: Required to link this policy role to the Root CA certificate generated (above).
    • max_path_length: CRITICAL Security Constraint. This ensures that any CA issued by this role (your Intermediate CA) can only sign one more layer of certificates (the final leaf/client certificates). This prevents unauthorized sub-CAs.
    • key_usage: Required. "CertSign" and "CRLSign" are the minimum key usages for a CA certificate.

      API: [POST] /pki/roles/:name

{
  "allow_any_name": true,
  "issuer_ref": "root-ca",
  "max_ttl": "43800h",
  "key_usage": ["CertSign", "CRLSign"],
  "organization": "Aomori University",
  "country": "JP",
  "province": "Aomori-ken",
  "locality": "Aomori-shi"
}
  1. Create a role root-ca-role for the Root CA.

    API: [POST] /pki/roles/:name

curl -s --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data @payload-role-root-ca.json \
  $VAULT_ADDR/v1/pki_root/roles/root-ca-role
  1. Create an API request payload (payload-url-ca.json) containing the URLs to set for the Root CA.
    • crl_distribution_points: URL set here is seen in the TLS-Cert-CRL-Distribution-Points attribute during the EAP-TLS handshake in FreeRADIUS.

      API: [POST] /pki/config/urls

{
  "issuing_certificates": "http://127.0.0.1:8200/v1/pki_root/ca",
  "crl_distribution_points": "http://127.0.0.1:8200/v1/pki_root/crl"
}
  1. Configure the URLs for the Root CA.

    API: [POST] /pki/config/urls

curl --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data @payload-url-ca.json \
  $VAULT_ADDR/v1/pki_root/config/urls

Step 2 (Vault): Generate Intermediate CA

  1. Enable the pki secrets engine at pki_int path. Create a separate engine as best practice.

    API: [POST] /sys/mounts/:path

curl --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data '{"type":"pki"}' \
  $VAULT_ADDR/v1/sys/mounts/pki_int
  1. Tune the pki_int secrets engine to issue certificates with a maximum TTL of 43800 hours. This corresponds to the max TTL of the Intermediate CA generated by pki_int engine.

    NOTE: set the max_lease_ttl as necessary.
    API: [POST] /sys/mounts/:path/tune

curl --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data '{"max_lease_ttl":"43800h"}' \
  $VAULT_ADDR/v1/sys/mounts/pki_int/tune
  1. Create an API request payload (payload-gen-int-csr.json) used to generate the Intermediate CA's CSR.

    API: [POST] /pki/intermediate/generate/:type

{
  "common_name": "Upinem AOU Intermediate CA",
  "organization": "Aomori University",
  "country": "JP",
  "province": "Aomori-ken",
  "locality": "Aomori-shi",
  "key_bits": 4096,
  "key_type": "rsa"
}
  1. Generate an intermediate using the /pki_int/intermediate/generate/internal endpoint and save it as pki_intermediate.csr.

    API: [POST] /pki/intermediate/generate/:type

curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data @payload-gen-int-csr.json \
  $VAULT_ADDR/v1/pki_int/intermediate/generate/internal \
  | jq -c '.data | .csr' >> pki_intermediate.csr
  1. Create an API request payload (payload-gen-int-ca.json) to sign the CSR. Use a script (payload-gen-int-ca.sh to generate the payload. This is a CRITICAL STEP. Pay close attention to the parameters set here!
    • common_name: Required to match the value set in the CSR. Will be seen in the EAP-TLS handshake within FreeRADIUS as the attributes TLS-Cert-Subject and TLS-Cert-Common-Name.
    • permitted_email_addresses: CRITICAL for this use case. This embeds the Name Constraint into the Intermediate CA certificate, authorizing it to sign certificates for the example.com and example.org email domain.
    • max_path_length: Security Hardening. Ensures the Intermediate CA can only issue leaf (end-entity) certificates and cannot sign another subordinate CA. (This overrides the max_path_length=1 set on the (signing) Root CA role).

      API: [POST] /pki/root/sign-intermediate

#!/bin/bash

tee payload-gen-int-ca.json <<EOF
{
  "common_name": "Upinem AOU Intermediate CA",
  "ttl": "43800h",
  "max_path_length": 0,
  "permitted_email_addresses": "example.com, example.org",
  "format": "pem_bundle",
  "use_csr_values": true,
  "csr": $(cat pki_intermediate.csr)
}
EOF
  1. Sign the intermediate certificate with the Root CA (notice the pki_root path) private key, and save the certificate as intermediate.cert.pem.

    API: [POST] /pki/root/sign-intermediate

curl --silent --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data @payload-gen-int-ca.json \
  $VAULT_ADDR/v1/pki_root/issuer/root-ca/sign-intermediate \
  | jq '.data | .certificate' >> intermediate.cert.pem
  1. After signing the CSR the root CA returns a certificate, it can be imported back into the pki_int secrets engine using the /pki_int/intermediate/set-signed endpoint. Create an API request payload containing the certificate you obtained. Perform this with a script - payload-signed.sh.

    API: [POST] /pki/intermediate/set-signed

#!/bin/bash

tee payload-signed.json <<EOF
{
  "certificate": $(cat intermediate.cert.pem)
}
EOF
  1. Submit the signed certificate back into the pki_int engine.

    API: [POST] /pki/intermediate/set-signed

curl --silent --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data @payload-signed.json \
  $VAULT_ADDR/v1/pki_int/intermediate/set-signed \
  | jq
  1. Create an API request payload (payload-url-int.json) containing the URLs to set for the Intermediate CA.
    • crl_distribution_points: Seen in the TLS-Client-Cert-CRL-Distribution-Points attribute during the EAP-TLS handshake within FreeRADIUS.

      NOTE: the URLs set here are for the Intermediate CA. The Root CA has separate URLs as denoted by the paths - pki_root/crl and pki_int/crl.
      API: [POST] /pki/config/urls

{
  "issuing_certificates": "http://127.0.0.1:8200/v1/pki_int/ca",
  "crl_distribution_points": "http://127.0.0.1:8200/v1/pki_int/crl"
}
  1. Configure the URLs for the Intermediate CA.

    API: [POST] /pki/config/urls

curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data @payload-url-int.json \
  $VAULT_ADDR/v1/pki_int/config/urls

Step 3 (Vault): Create Server Role & Generate Server Certificates

  • On a standard installation of FreeRADIUS, test certificates are provided to test an EAP-TLS setup. Also provided is the configuration options used to create those test certificates. We can use the server.cnf file as a reference to create a role in Vault that issues server certificates.

    GitHub: /raddb/certs/server.cnf

[ ca ]
default_ca      = CA_default

[ CA_default ]
dir         = ./
certs           = $dir
crl_dir         = $dir/crl
database        = $dir/index.txt
new_certs_dir       = $dir
certificate     = $dir/server.pem
serial          = $dir/serial
crl         = $dir/crl.pem
private_key     = $dir/server.key
RANDFILE        = $dir/.rand
name_opt        = ca_default
cert_opt        = ca_default
default_days        = 60
default_crl_days    = 30
default_md      = sha256
preserve        = no
policy          = policy_match
copy_extensions     = copy

[ policy_match ]
countryName     = match
stateOrProvinceName = match
organizationName    = match
organizationalUnitName  = optional
commonName      = supplied
emailAddress        = optional

[ policy_anything ]
countryName     = optional
stateOrProvinceName = optional
localityName        = optional
organizationName    = optional
organizationalUnitName  = optional
commonName      = supplied
emailAddress        = optional

[ req ]
prompt          = no
distinguished_name  = server
default_bits        = 2048
input_password      = whatever
output_password     = whatever
#req_extensions     = v3_req

[server]
countryName     = FR
stateOrProvinceName = Radius
localityName        = Somewhere
organizationName    = Example Inc.
emailAddress        = admin@example.org
commonName      = "Example Server Certificate"

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer:always

#  This should be a host name of the RADIUS server.
#  Note that the host name is exchanged in EAP *before*
#  the user machine has network access.  So the host name
#  here doesn't really have to match anything in DNS.
[alt_names]
DNS.1 = radius.example.com

# NAIRealm from RFC 7585
otherName.0 = 1.3.6.1.5.5.7.8.8;FORMAT:UTF8,UTF8:*.example.com
  1. Create an API request payload (payload-role-server.json) containing the role information.
    • issuer_ref: Points to the Intermediate CA you just imported, ensuring it signs all leaf certs.
    • allowed_domains: Limits the hostnames that can be requested i.e. only radius.example.com and example.com.
    • cn_validations: CRITICAL. Enforces that the CN must be a hostname (e.g., radius.example.com).
    • server_flag: CRITICAL. true enables the required TLS Web Server Authentication extended key usage.
    • client_flag: Security. false prevents the certificate from being misused for client authentication.
    • ext_key_usage: Explicitly sets the necessary Extended Key Usage. For TLS Web Server Authentication use "ServerAuth".
    • allowed_other_sans: CRITICAL. This is required to allow the NAI Realm OID (1.3.6.1.5.5.7.8.8) seen in your server.cnf to be included in the server certificate.

      API: [POST] /pki/roles/:name

{
  "issuer_ref": "default",
  "max_ttl": "26280h",
  "allowed_domains": "radius.example.com, example.com",
  "allow_subdomains": true,
  "allow_bare_domains": true,
  "allow_glob_domains": true,
  "cn_validations": "hostname",
  "require_cn": true,
  "enforce_hostnames": true,
  "server_flag": true,
  "client_flag": false,
  "key_usage": ["DigitalSignature", "KeyEncipherment"],
  "ext_key_usage": ["ServerAuth"],
  "allowed_other_sans": "1.3.6.1.5.5.7.8.8;UTF8:*",
  "key_type": "rsa",
  "key_bits": 4096,
  "organization": "Aomori University",
  "country": "JP",
  "province": "Aomori-ken",
  "locality": "Aomori-shi"
}
  1. Create a role named eap-tls-server to issue server certificates for EAP-TLS usage.

    API: [POST] /pki/roles/:name

curl -s --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data @payload-role-server.json \
  $VAULT_ADDR/v1/pki_int/roles/eap-tls-server \
  | jq
  1. Create an API request payload (payload-gen-server.json) to issue the server certificate.
    • common_name: This must be the FQDN of your RADIUS server, matching the hostname constraint in the role.
    • alt_names: Specifies the requested Subject Alternative Names, in a comma-delimited list. These can be host names or email addresses; they will be parsed into their respective fields. Ensures the FQDN is included as a DNS Subject Alternative Name (SAN), which is best practice for TLS validation.
    • ip_sans: Replace this with the actual IP Address of your RADIUS server if clients will connect via IP.
    • other_sans: CRITICAL. This directly includes the required NAI Realm SAN from your FreeRADIUS server.cnf, which the eap-tls-server role is configured to allow.

      API: [POST] /pki/issue/:name

{
  "common_name": "radius.example.com",
  "alt_names": "radius.example.com, upinem.example.com",
  "ip_sans": "192.168.1.100",
  "other_sans": "1.3.6.1.5.5.7.8.8;UTF8:*.example.com",
  "ttl": "8760h",
  "format": "pem_bundle"
}
  1. Issue the server certificate and save the details (certificate, private key, etc.) into a separate result-server-cert.json file. We will extract the necessary information from the json file with jq later on.

    API: [POST] /pki/issue/:name

curl -s --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data @payload-gen-server.json \
  $VAULT_ADDR/v1/pki_int/issue/eap-tls-server \
  | jq > result-server-cert.json
  1. Extract the private key into server.key. Will be ported for use in FreeRADIUS.
jq -r '.data.private_key' result-server-cert.json > server.key
  1. Extract the server certificate into server.pem. Will be ported for use in FreeRADIUS.
jq -r '.data.certificate' result-server-cert.json | \
grep -E 'BEGIN CERTIFICATE|END CERTIFICATE|' | \
awk '
    BEGIN {cert_found=0}
    /BEGIN CERTIFICATE/ { cert_found=1; print; next }
    cert_found { print }
    /END CERTIFICATE/ { exit }
' > server.pem
  1. Extract the CA chain into ca.pem. This contains the Intermediate and Root CAs for client trust. Will be ported for use in FreeRADIUS.
jq -r '.data.ca_chain[]' result-server-cert.json > ca.pem

Step 4 (Vault): Create Client Role & Generate Client Certificates

  • On a standard installation of FreeRADIUS, test certificates are provided to test an EAP-TLS setup. Also provided is the configuration options used to create those test certificates. We can use the client.cnf file as a reference to create a role in Vault that issues client certificates.

    GitHub: /raddb/certs/client.cnf

[ ca ]
default_ca      = CA_default

[ CA_default ]
dir         = ./
certs           = $dir
crl_dir         = $dir/crl
database        = $dir/index.txt
new_certs_dir       = $dir
certificate     = $dir/ca.pem
serial          = $dir/serial
crl         = $dir/crl.pem
private_key     = $dir/ca.key
RANDFILE        = $dir/.rand
name_opt        = ca_default
cert_opt        = ca_default
default_days        = 60
default_crl_days    = 30
default_md      = sha256
preserve        = no
policy          = policy_match

[ policy_match ]
countryName     = match
stateOrProvinceName = match
organizationName    = match
organizationalUnitName  = optional
commonName      = supplied
emailAddress        = optional

[ policy_anything ]
countryName     = optional
stateOrProvinceName = optional
localityName        = optional
organizationName    = optional
organizationalUnitName  = optional
commonName      = supplied
emailAddress        = optional

[ req ]
prompt          = no
distinguished_name  = client
default_bits        = 2048
input_password      = whatever
output_password     = whatever

[client]
countryName     = FR
stateOrProvinceName = Radius
localityName        = Somewhere
organizationName    = Example Inc.
emailAddress        = user@example.org
commonName      = user@example.org
  1. Create an API request payload (payload-role-client.json) for the role used to create client certificates.
    • issuer_ref: Points to the Intermediate CA you just imported, ensuring it signs all leaf certs.
    • allowed_domains: Constrains the domains to match the Intermediate CA's Name Constraints.
    • cn_validations: CRITICAL. Forces the Common Name to be an email address (e.g., user@example.com).
    • client_flag: CRITICAL. true enables the clientAuth Extended Key Usage used for client authentication.
    • server_flag: Security. false prevents the client certificate from being misused to impersonate a server.
    • key_usage: Standard usages ("DigitalSignature", "KeyAgreement") for client authentication, focusing on signature/key exchange.
    • ext_key_usage: Specifies the exact EKU OID to be included in the certificate (e.g., ClientAuth).
{
  "issuer_ref": "default",
  "max_ttl": "26280h",
  "allowed_domains": "example.com, example.org",
  "allow_subdomains": true,
  "allow_bare_domains": true,
  "allow_glob_domains": false,
  "cn_validations": "email",
  "require_cn": true,
  "enforce_hostnames": false,
  "server_flag": false,
  "client_flag": true,
  "key_usage": ["DigitalSignature", "KeyAgreement"],
  "ext_key_usage": ["ClientAuth"],
  "key_type": "rsa",
  "key_bits": 4096,
  "organization": "Aomori University",
  "country": "JP",
  "province": "Aomori-ken",
  "locality": "Aomori-shi"
}
  1. Create a role named eap-tls-client to issue client certificates for EAP-TLS usage.

    API: [POST] /pki/roles/:name

curl -s --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data @payload-role-client.json \
  $VAULT_ADDR/v1/pki_int/roles/eap-tls-client \
  | jq
  1. Create payloads for the client certificates to be issued for EAP-TLS usage. In this example, I create 2 payloads one with different common names.

    API: [POST] /pki/issue/:name

# payload-gen-client-1.json
{
  "common_name": "user1@example.com",
  "alt_names": "user1@example.com",
  "ip_sans": "192.168.1.200",
  "ttl": "8760h"
}

# payload-gen-client-2.json
{
  "common_name": "user2@example.org",
  "alt_names": "user2@example.org",
  "ip_sans": "192.168.1.201",
  "ttl": "8760h"
}
  1. Issue the client certificate and save the details (certificate, private key, etc.) into a separate json files. We will extract the necessary information from the json file with jq later on.

    API: [POST] /pki/issue/:name

# Create client cert with CN 'user1@example.com'
curl --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data @payload-gen-client-1.json \
  $VAULT_ADDR/v1/pki_int/issue/eap-tls-client \
  | jq > result-client-1.json

# Create client cert with CN 'user2@example.com'
curl --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data @payload-gen-client-2.json \
  $VAULT_ADDR/v1/pki_int/issue/eap-tls-client \
  | jq > result-client-2.json
  1. Extract the necessary information (certificate & private key) from the json files.

    You can use jq -r '.data.ca_chain[]' result-client-1.json > client-1-ca.pem to extract the CA chain. However this is unnecessary as the CA chain of the server certificate is identical to those of the client certificates. Using the CA chain of the server certificate will suffice.

jq -r '.data.private_key' result-client-1.json > client-1.key
jq -r '.data.certificate' result-client-1.json > client-1.pem

jq -r '.data.private_key' result-client-2.json > client-2.key
jq -r '.data.certificate' result-client-2.json > client-2.pem

Step 5 (FreeRADIUS): Configure EAP-TLS & Proxy Settings

  1. Remove the default EAP module.
sudo rm /etc/freeradius/mods-enabled/eap
  1. Create a new EAP module (upinem-eap.conf) with minimum necessary settings configured for EAP-TLS. Save the EAP module to the /etc/freeradius/mods-available directory where modules are defined. Required to set the proper permissions so that the FreeRADIUS process can eventually read the module config.
    • private_key_file: Point to the private key (.key) of the server certificate created above (i.e. server.key in Step 3).
    • certificate_file: Point to the server certificate (.pem) created above (i.e. server.pem in Step 3)
    • ca_file: Point to the CA chain file created above (i.e. ca.pem in Step 3)
    • private_key_password: Comment out. Redundant to encrypt private_key_file; unnecessary.

      GitHub: upinem/mods-available/upinem-eap.conf

##
## upinem-eap.conf -- UPINEM EAP module
##   configuration file for FreeRADIUS - 3.2.*
##
## Find out more about UPINEM below.
## https://github.com/joshua-liew/up-in-em

######################################################################

eap {
    default_eap_type = tls
    timer_expire = 60
    ignore_unknown_eap_types = no
    max_sessions = ${max_requests}

    # Common TLS configuration for TLS-based EAP types
    tls-config tls-common {
        #private_key_password = whatever
        private_key_file = ${certdir}/server.key

        certificate_file = ${certdir}/server.pem
        ca_file = ${cadir}/ca.pem
        ca_path = ${cadir}
        auto_chain = yes

        # check_cert_issuer = "<CERT_ISSUER>"
        # check_cert_cn = %{User-Name}

        cipher_list = "DEFAULT"
        cipher_server_preference = no

        tls_min_version = "1.2"
        tls_max_version = "1.2"
        ecdh_curve = ""
    }

    # EAP-TLS configuration
    tls {
        tls = tls-common
        #virtual_server = check-eap-tls
    }
}
  1. Enable the upinem-eap module created. Symbolic links are used for this purpose.
sudo ln -sr /etc/freeradius/mods-available/upinem-eap.conf /etc/freeradius/mods-enabled/upinem-eap

# You may need to set permissions for the symbolic link.
sudo chown freeradius:freeradius -h /etc/freeradius/mods-enabled/upinem-eap
  1. Add the following realms to the proxy.file to ensure that the corresponding EAP identities are handled locally. Any empty realm or realm with no server pool specified causes packets to be processed locally. These realms must correspond to the CN constraints (allowed_domains, allow_subdomains, etc.) for client authentication in EAP-TLS set in Step 4.
realm example.com {
}

realm example.org {
}
  1. Run the FreeRADIUS server in debug mode. You should be able to confirm that your custom EAP module was successfully loaded.
sudo -u freeradius radiusd -X
FreeRADIUS Version 3.2.8
...
Starting - reading configuration files ...
...
including configuration file /etc/freeradius/mods-enabled/upinem-eap
...
radiusd: #### Instantiating modules ####
...
  # Loaded module rlm_eap
  # Loading module "eap" from file /etc/freeradius/mods-enabled/upinem-eap
  eap {
        default_eap_type = "tls"
        timer_expire = 60
        max_eap_type = 52
        ignore_unknown_eap_types = no
        cisco_accounting_username_bug = no
        max_sessions = 16384
        dedup_key = ""
  }
...
  # Instantiating module "eap" from file /etc/freeradius/mods-enabled/upinem-eap
   # Linked to sub-module rlm_eap_tls
   tls {
        tls = "tls-common"
   }
   tls-config tls-common {
        verify_depth = 0
        ca_path = "/etc/freeradius/certs"
        pem_file_type = yes
        private_key_file = "/etc/freeradius/certs/server.key"
        certificate_file = "/etc/freeradius/certs/server.pem"
        ca_file = "/etc/freeradius/certs/ca.pem"
        fragment_size = 1024
        include_length = yes
        auto_chain = yes
        check_crl = no
        check_all_crl = no
        ca_path_reload_interval = 0
        cipher_list = "DEFAULT"
        cipher_server_preference = no
        reject_unknown_intermediate_ca = no
        ecdh_curve = ""
        tls_max_version = "1.2"
        tls_min_version = "1.2"
    cache {
        enable = no
        lifetime = 24
        max_entries = 255
    }
    verify {
        skip_if_ocsp_ok = no
    }
    ocsp {
        enable = no
        override_cert_url = no
        use_nonce = yes
        timeout = 0
        softfail = no
    }
   }
...
  1. Create an eapol test file to conduct testing. Use the client certificates generated in Step 4.

    You can install the eapol_test tool with apt-get: sudo apt-get install eapoltest

# test-client-1.conf
network={
        ssid="test"
        key_mgmt=WPA-EAP
        eap=TLS
        identity="user1@example.com"
        ca_cert="/tmp/certs/ca.pem"
        client_cert="/tmp/certs/clients/client-1.pem"
        private_key="/tmp/certs/clients/client-1.key"
        eapol_flags=3
}
  1. With radiusd -X running in one terminal, open another terminal and perform the eapol test. You should see a SUCCESS message in the terminal.
eapol_test -c test-client-1.conf -s testing123

# Example output below
Reading configuration file 'test-client-1.conf'
Line: 1 - start of a new network block
ssid - hexdump_ascii(len=4):
     74 65 73 74                                       test
key_mgmt: 0x1
eap methods - hexdump(len=16): 00 00 00 00 0d 00 00 00 00 00 00 00 00 00 00 00
identity - hexdump_ascii(len=21):
     ...    user1@example.com
ca_cert - hexdump_ascii(len=17):
     ...    /tmp/certs/ca.pem
client_cert - hexdump_ascii(len=31):
     ...    /tmp/certs/clients/client-1.pem
private_key - hexdump_ascii(len=31):
     ...    /tmp/certs/clients/client-1.key
Priority group 0
   id=0 ssid='test'
...
MPPE keys OK: 1  mismatch: 0
SUCCESS
  1. Within the FreeRADIUS debug output you should be able to confirm that EAP-TLS with certificates generated by Vault is successful. Many TLS attributes correspond to the values of parameters set between Step 1 - 4. You should see Sent Access-Accept logged within the output.
...
(9) eap_tls:   TLS-Cert-Serial := "5b477fe9fd13eeed3b043572ce23fa3a4f0d80d0"
(9) eap_tls:   TLS-Cert-Expiration := "301126100938Z"
(9) eap_tls:   TLS-Cert-Valid-Since := "251127100908Z"
(9) eap_tls:   TLS-Cert-Subject := "/C=JP/ST=Aomori-ken/L=Aomori-shi/O=Aomori University/CN=Upinem AOU Intermediate CA"
(9) eap_tls:   TLS-Cert-Issuer := "/C=JP/ST=Aomori-ken/L=Aomori-shi/O=Aomori University/CN=Upinem AOU"
(9) eap_tls:   TLS-Cert-Common-Name := "Upinem AOU Intermediate CA"
(9) eap_tls:   TLS-Cert-CRL-Distribution-Points += "http://127.0.0.1:8200/v1/pki_root/crl"
(9) eap_tls: (TLS) TLS - Creating attributes from certificate 1 in chain
(9) eap_tls:   TLS-Client-Cert-Serial := "084cf7653d49b706801c38f820125ace298a1788"
(9) eap_tls:   TLS-Client-Cert-Expiration := "261128072323Z"
(9) eap_tls:   TLS-Client-Cert-Valid-Since := "251128072254Z"
(9) eap_tls:   TLS-Client-Cert-Subject := "/C=JP/ST=Aomori-ken/L=Aomori-shi/O=Aomori University/CN=user1@sub.example.com"
(9) eap_tls:   TLS-Client-Cert-Issuer := "/C=JP/ST=Aomori-ken/L=Aomori-shi/O=Aomori University/CN=Upinem AOU Intermediate CA"
(9) eap_tls:   TLS-Client-Cert-Common-Name := "user1@example.com"
(9) eap_tls:   TLS-Client-Cert-CRL-Distribution-Points += "http://127.0.0.1:8200/v1/pki_int/crl"
(9) eap_tls:   TLS-Client-Cert-Subject-Alt-Name-Email := "user1@example.com"
(9) eap_tls:   TLS-Client-Cert-X509v3-Extended-Key-Usage += "TLS Web Client Authentication"
(9) eap_tls:   TLS-Client-Cert-X509v3-Subject-Key-Identifier += "DD:3B:59:A3:E2:AD:B3:0D:F2:42:9F:83:39:F4:59:71:51:F2:7F:6A"
(9) eap_tls:   TLS-Client-Cert-X509v3-Authority-Key-Identifier += "7C:CE:86:95:50:B8:E5:DE:1F:40:1D:AE:C3:0B:55:72:C2:7A:1D:7F"
(9) eap_tls:   TLS-Client-Cert-X509v3-Extended-Key-Usage-OID += "1.3.6.1.5.5.7.3.2"
...
(10) Sent Access-Accept Id 10 from 127.0.0.1:1812 to 127.0.0.1:60161 length 189
(10)   MS-MPPE-Recv-Key = 0x7447441801becf79d243bf3d08df60dd307bebcbd756fb4f8e0fdfcec7a82207
(10)   MS-MPPE-Send-Key = 0xc6b811229731c1e2f6ecde0fbcf03bed01237456b03c90dbe90329bec1ce845e
(10)   EAP-Message = 0x03e90004
(10)   Message-Authenticator = 0x00000000000000000000000000000000
(10)   User-Name = "user1@example.com"
(10)   Framed-MTU += 1014
(10) Finished request

Congratulations. You have successfully configured FreeRADIUS to perform EAP-TLS authentication with certificates generated by Vault's PKI Secrets Engine.