You probably have experience with servers to authenticate users. Client connect with your website, click on the link to sign in form and fills it up with own username and password. Browser encrypt credentials and send those secrets to Server. But how to authenticate client when it is a machine and you have to verify both sides of communication? But one by one. You must first understand how regular tls works.
Github repo with examples: https://github.com/gawsoftpl/gawsoft.com/tree/main/blog/mtls
Standard TLS
Standard encrypted connection between server and client. This type of connection happens when you visit facebook, twitter or bank.
In short, it can be described that:
-
Browser(Client) send request to Server,
-
Endpoint will send back to browser, signed by Certificate Authorities (CA), public certificate which browser should encrypt data transmitted to Server.
-
Browser check that Certificate is signed by known to her CA. Most browsers has list of approved of trusted authorities.
Most popular are:- Verisign
- DigiCert
- GeoTrust
- Entrust
- etc
- Please note that you can use own CA
-
Browser create secret key, which will be only use in this session, encrypted with public certificate received from Server.
-
Server receive encrypted payload and decrypt secret key with own private key. Botch machines Server and Client have unique session key, thanks to which communication will be encrypted.
This type of connection is good when a human connects with a machine. But to connect machine - machine (B2B) you have to verify that the connected machines are the one he claims to be.
Two way encryption (mTLS)
The solution is mutual Transport Layer Security. This is an extension of regular TLS. In additional between points 1 and 2 from previous chapter Client will send own signed certificate. Thanks that Server can verify that client is from verified source.
The above solution makes it possible to authenticate and verify clients, without a prompt username and password. For sign certificates on both sides of connection you can use a paid version of CA, but in this case, we will generate own CA public and private key for sign certificates.
A real life example
Imagine that we have an organization ABC inc. In this small company we have two machines:
- Invoice server
- Mail Server
We want to Mail server connect with Invoice server and download invoice data. For that both server have to encrypt connection.
Additional invoice server have to authorize mail server. We don't want to send our client's data to not verified machine.
For this we will check that certificate of mail server is signed by our company CA. In situation when Mail is not signed by our CA connection will be dropped.
Connection diagram
Ok so much theory, now let's move to bash commands which you will generate own CA, Certificate and Key. At the end of this article you find NodeJS scripts for server and client.
First, create a directory hierarchy for all codes, which you will generate in this article
mkdir ca ca-wrong invoice-server mail-server
Generate certificates
1. Generate ABC Inc. CA
This command will generate private key and certificate, which will be valid for 365 days.
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=ABC Inc.' -keyout ca/ca.key.pem -out ca/ca.cert.pem
After executed command in ca directory should have two files.
ca
├── ca.cert.pem
└── ca.key.pem
- ca.cert.pem is a Public CA certificate for verify InvoiceServer and MailServer certificates
- ca.key.pem this is a Private key, this key will sign all certificate in organization ABC. In production this key is stored in offline machine (for example you can use Hashicorp Vault)
2. InvoiceServer Certificate and private key
Generate private key for InvoiceServer and prepare Certificate Signing Request (CSR). This file has information about the entity for which the certificate is to be issued by our CA.
This command has line: /CN=localhost/O=InvoiceServer.
CN means CommonName or FQDN (Fully qualified domain name). For https we use FQDN. Because this is example we will start our nodejs server in localhost, so we have to issue a certificate for localhost.
In production, if you have a server hosted as: invoice.abc.com you should use the parameter: /CN=invoice.abc.com/O=InvoiceServer
openssl req -out invoice-server/server.csr -newkey rsa:2048 -nodes -keyout invoice-server/private.key -subj "/CN=localhost/O=InvoiceServer"
Now we have to issue our certificate request. For this we will use CA generated in step 1
openssl x509 -req -sha256 -days 365 -CA ca/ca.cert.pem -CAkey ca/ca.key.pem -set_serial 0 -in invoice-server/server.csr -out invoice-server/server.cert
Now invoice-server directory looks like this:
invoice-server
├── private.key
├── server.cert # Issued by ABC CA certificate for invoice-server
└── server.csr
For verify that certificate is signed by ABC CA, please use below commands. First will show information about certificate. Second just verify that certificate is signed by ABC Inc CA
openssl x509 -noout -text -in invoice-server/server.cert
openssl verify -CAfile ca/ca.cert.pem invoice-server/server.cert
# invoice-server/server.cert: OK
Great now we have certificates for InvoiceServer, we have to create certificates for MailServer. Skaffold for that process is same as in step 2.
3. MailServer Certificate and private key
openssl req -out mail-server/server.csr -newkey rsa:2048 -nodes -keyout mail-server/private.key -subj "/CN=localhost/O=MailServer"
openssl x509 -req -sha256 -days 365 -CA ca/ca.cert.pem -CAkey ca/ca.key.pem -set_serial 0 -in mail-server/server.csr -out mail-server/server.cert
MailServer directory:
mail-server
├── private.key
├── server.cert
└── server.csr
For check please verify that certificate is signed correctly.
openssl verify -CAfile ca/ca.cert.pem mail-server/server.cert
# Output: mail-server/server.cert: OK
4. Prepare NodeJS scripts
invoice_server.js
cat <<\EOF > invoice_server.js
const https = require('https');
const fs = require('fs');
const hostname = '0.0.0.0';
const port = 3100;
const options = {
ca: fs.readFileSync('ca/ca.cert.pem'),
cert: fs.readFileSync('invoice-server/server.cert'),
key: fs.readFileSync('invoice-server/private.key'),
rejectUnauthorized: true, // if certificate is not signed by ca/ca.cert.pem, drop connection
requestCert: true, // mail_server.js have to send own Cert
};
const server = https.createServer(options, (req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
const userData = JSON.stringify({
firstname: 'John',
lastname: 'Ji',
email: '[email protected]',
invoice_number: "00095234",
invoice_price: "10USD",
});
res.end(userData);
});
server.on('error',(e)=>console.log(e));
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
EOF
mail_server.js
cat <<\EOF > mail_server.js
var fs = require('fs');
var https = require('https');
var options = {
hostname: 'localhost',
port: 3100,
path: '/',
method: 'GET',
key: fs.readFileSync('mail-server/private.key'),
cert: fs.readFileSync('mail-server/server.cert'),
ca: fs.readFileSync('ca/ca.cert.pem'),
};
var req = https.request(options, function(res) {
res.on('data', function(data) {
process.stdout.write(data);
});
});
req.end();
req.on('error', function(e) {
console.error(e);
});
EOF
Start invoice_server
node invoice_server.js
# Output: Server running at http://0.0.0.0:3100/
Send request from mail_server
node mail_server.js
# Output: {"firstname":"John","lastname":"Ji","email":"[email protected]","invoice_number":"00095234","invoice_price":"10USD"}
The scheme of above scripts can be described as follow
- Mail_server.js send request to invoice_server.js with own certificate
- Invoice_server, check that mail_server send certificate and is signed by ABC .inc CA
- If certificate is signed, return user invoice to mail_server.js
5. Unauthorized request
What happen when we send request with certificate signed by CA belongs to other company?
Generate CA for company ZXC (wrong ca)
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=ZXC Inc./CN=zxc.com' -keyout ca-wrong/ca.key.pem -out ca-wrong/ca.cert.pem
Sign mail_server certificate by CA belongs to ZXC (not ABC)
openssl x509 -req -sha256 -days 365 -CA ca-wrong/ca.cert.pem -CAkey ca-wrong/ca.key.pem -set_serial 0 -in mail-server/server.csr -out mail-server/server-wrong.cert
mail_server_wrong_cert.js
cat <<\EOF > mail_server_wrong_cert.js
var fs = require('fs');
var https = require('https');
var options = {
hostname: 'localhost',
port: 3100,
path: '/',
method: 'GET',
key: fs.readFileSync('mail-server/private.key'),
cert: fs.readFileSync('mail-server/server-wrong.cert'),
ca: fs.readFileSync('ca/ca.cert.pem'),
};
var req = https.request(options, function(res) {
res.on('data', function(data) {
process.stdout.write(data);
});
});
req.end();
req.on('error', function(e) {
console.error(e);
});
EOF
Execute test request:
node mail_server_wrong_cert.js
#Error: socket hang up
# at connResetException (node:internal/errors:704:14)
# at TLSSocket.socketOnEnd (node:_http_client:505:23)
# at TLSSocket.emit (node:events:525:35)
# at endReadableNT (node:internal/streams/readable:1358:12)
# at processTicksAndRejections (node:internal/process/task_queues:83:21) {
# code: 'ECONNRESET'
#}
Invoice_server.js block connection from mail_server_wrong_cert.js because, certificate is issued by other CA. Very imporatnt are options which have been set in invoice_server.js
const options = {
rejectUnauthorized: true, // if certificate is not signed by ca/ca.cert.pem, drop connection
requestCert: true, // mail_server.js have to send own Cert
};
That's it. You successfully set up mTLS connection in NodeJS and authenticate client which sent request to invoice_server.js
Output directory structure should look like below:
.
├── ca
│ ├── ca.cert.pem
│ └── ca.key.pem
├── ca-wrong
│ ├── ca.cert.pem
│ └── ca.key.pem
├── invoice-server
│ ├── private.key
│ ├── server.cert
│ └── server.csr
├── invoice_server.js
├── mail-server
│ ├── private.key
│ ├── server.cert
│ ├── server.csr
│ └── server-wrong.cert
├── mail_server.js
└── mail_server_wrong_cert.js
References
- Github examples: https://github.com/gawsoftpl/gawsoft.com/tree/main/blog/mtls
- https://www.cloudflare.com/learning/access-management/what-is-mutual-tls/
- https://en.wikipedia.org/wiki/Mutual_authentication
- https://www.f5.com/labs/articles/education/what-is-mtls