├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── requirements.txt ├── resources ├── certificate-hierarchy-basic.png ├── certificate-hierarchy-intermediate.png ├── certificate-hierarchy.graffle └── certificate-panel.png ├── src ├── client.py ├── namer.py ├── proto │ ├── .gitignore │ ├── __init__.py │ └── namer.proto ├── server.py └── test_namer.py └── ssl ├── README.md ├── ca-config.json ├── ca-csr.json ├── client-csr.json └── server-csr.json /.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | .cache 3 | .pants* 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | ###################### 3 | .DS_Store 4 | .DS_Store? 5 | ._* 6 | .Spotlight-V100 7 | .Trashes 8 | ehthumbs.db 9 | Thumbs.db 10 | 11 | # Virtualenv 12 | .venv* 13 | venv* 14 | 15 | # Byte-compiled / optimized / DLL files 16 | __pycache__/ 17 | *.py[cod] 18 | *$py.class 19 | 20 | # C extensions 21 | *.so 22 | 23 | # Vim temp files 24 | *.swp 25 | 26 | # Cert and CSR files 27 | *.pem 28 | *.csr 29 | *.key 30 | *.crt 31 | 32 | # Pantsbuild 33 | .pants* 34 | .cache 35 | dist 36 | 37 | # Editor-specific # 38 | ################### 39 | .vscode 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine3.10 as compile 2 | RUN mkdir /app 3 | WORKDIR /app 4 | RUN apk add --no-cache curl 5 | RUN curl -O -fssL https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v0.3.0/grpc_health_probe-linux-amd64 && \ 6 | mv grpc_health_probe-linux-amd64 /usr/local/bin/grpc_health_probe && \ 7 | chmod 755 /usr/local/bin/grpc_health_probe 8 | RUN apk add --no-cache libstdc++ 9 | RUN python -m venv /opt/venv 10 | ENV PATH="/opt/venv/bin:$PATH" 11 | COPY requirements.txt /app 12 | RUN apk add --no-cache --virtual build-deps build-base && \ 13 | pip install --no-cache-dir -r requirements.txt && \ 14 | apk del build-deps 15 | 16 | FROM python:3.8-alpine3.10 17 | RUN apk add --no-cache libstdc++ 18 | RUN mkdir /app 19 | WORKDIR /app 20 | COPY src/ /app/ 21 | COPY --from=compile /opt/venv /opt/venv 22 | ENV PATH="/opt/venv/bin:$PATH" 23 | RUN python -m grpc_tools.protoc \ 24 | -I. \ 25 | --python_out=. \ 26 | --grpc_python_out=. \ 27 | ./proto/namer.proto 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Joe Kottke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TLS authentication in Python gRPC 2 | 3 | ## Intro 4 | 5 | I've always had a fascination with network programming; its what got me into SRE and DevOps work originally. In a previous job, we built all of our services (micro and otherwise) around HTTP, REST, and JSON. This worked well: all languages had an HTTP client (even a crappy one) and all languages had a JSON parser (even a crappy one). But it always meant you had to serialize and marshal your data by hand, and each language handled the client/server contract just a bit differently. 6 | 7 | We also had a need to send data and events between geographically disparate regions to keep the large system in sync. Obviously we had to encrypt everything going over the public Internet, and we had to identify clients to servers and servers to clients using SSL/TLS. We did the TLS processing at the front-end load-balancers; it was effective if a bit clumsy. 8 | 9 | gRPC has pretty much solved all of these issues by creating a strong API contract between clients and servers through the use of Protocol Buffers, implementing the network programming semantics across multiple languages, and using TLS to secure the whole thing. 10 | 11 | There are some great examples of doing Server authentication and identification in Python gRPC (like the one at [Sandtable](https://www.sandtable.com/using-ssl-with-grpc-in-python/), and I'd found some decent examples of doing mutual TLS authentication in other languages (like this [Go example](https://bbengfort.github.io/programmer/2017/03/03/secure-grpc.html)), so I decided to just extrapolate this into Python. 12 | 13 | ## TLS Basics 14 | 15 | ![Basic Certificate Hierarchy](resources/certificate-hierarchy-basic.png) 16 | 17 | A quick refresher: TLS/SSL works through chains of trust, or transitive trust. If I (or my machine, or process) trust a particular certificate authority, I therefor trust the certificates that it has generated. This trust is implicit in browsers on operating systems: every browser and/or operating system has a 'Trusted Roots' certificate store that it uses to confirm the trust of HTTPS servers on the internet. If you received an SSL/TLS server certificate from, say, [Let's Encrypt](https://letsencrypt.org), [GoDaddy](https://godaddy.com), or other public certificate authorities, browsers and operating systems will automatically trust the veracity of that server certificate. 18 | 19 | In our example here, we are creating our own certificate authority (CA), and have inform to the client about the CA certificate so that it can trust the server certificate presented by our server process. 20 | 21 | In terms of server certificates, we also have to see that the server name that we connect to is also the server name mentioned in the server certificate. 22 | 23 | ## Generate Certificates 24 | 25 | For the purpose of this example, we will be creating an extremely basic PKI Infrastructure using CloudFlare's [CFSSL](https://cfssl.org). Specifically, we will be using the `cfssl` and `cfssljson` tools, which can be downloaded [here](https://pkg.cfssl.org). 26 | 27 | The config files in the `ssl` directory intended to be modified, but they can also be used as-is for demonstration purposes. 28 | 29 | ### Generate CA Certificate and Config 30 | 31 | ```sh 32 | cd ./ssl 33 | cfssl gencert -initca ca-csr.json | cfssljson -bare ca 34 | ``` 35 | 36 | This generates the `ca.pem` and `ca-key.pem` files. The `ca.pem` file will be used by both the client and the server to verify each other. 37 | 38 | ### Generate Server and Client certificates 39 | 40 | #### Server Certificate 41 | 42 | ```sh 43 | cd ./ssl 44 | cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -hostname='127.0.0.1,localhost' server-csr.json | cfssljson -bare server 45 | ``` 46 | 47 | This creates the certificate and key pair to be used by the server. 48 | 49 | **Note:** You can change the `hostname` parameter to the name or IP address of a server on your network, it just needs to match the server name that you connect to with the client. 50 | 51 | #### Client Certificate 52 | 53 | ```sh 54 | cd ./ssl 55 | cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json client-csr.json | cfssljson -bare client 56 | ``` 57 | 58 | When generating the client certificate and key pair, you will see the warning: 59 | 60 | ``` 61 | [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for 62 | websites. For more information see the Baseline Requirements for the Issuance and Management 63 | of Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org); 64 | specifically, section 10.2.3 ("Information Requirements"). 65 | ``` 66 | 67 | This is expected and acceptable as the client certificate won't be used for server identification, only client identification (see note above). 68 | 69 | ## TLS Server Identification and Encryption 70 | 71 | ### Client trusts the certificate authority certificate, and thus the server certificate. 72 | 73 | This is similar to the browser use-case, where the browser has (pre-installed) all of the public Certificate Authority certificates installed in the browser or system trust store. 74 | 75 | In our case, we are generating our own CA certificate, and distributing it to both the client and the server. But when we are only doing one-way trust verification (the client verifies the identity of the server, but the server doesn't care about the identity of the client), the server does not necessarily need to present the CA certificate as part of its certificate chain. In general, a server only needs to present enough of a certificate chain so that the client can ascend up the certificate to a certificate that is signed by one of the CA certificates trusted by the client already. 76 | 77 | We can configure our server to use SSL with something similar to the following code snippet 78 | 79 | ```python 80 | # Server snippet 81 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) 82 | namer_pb2_grpc.add_NamerServicer_to_server(Namer(), server) 83 | port = 9999 84 | keyfile = 'server-key.pem' 85 | certfile = 'server.pem' 86 | private_key = open(keyfile).read() 87 | certificate_chain = open(certfile).read() 88 | credentials = grpc.ssl_server_credentials( 89 | [(private_key, certificate_chain)] 90 | ) 91 | server.add_secure_port('[::]:' + str(port), credentials) 92 | 93 | print('Starting server. Listening on port {}...'.format(port)) 94 | server.start() 95 | ``` 96 | 97 | And the client would look something like this: 98 | 99 | ```python 100 | # Client snippet 101 | server_port = 9999 102 | server_host = 'localhost' 103 | ca_cert = 'ca.pem' 104 | root_certs = open(ca_cert).read() 105 | credentials = grpc.ssl_channel_credentials(root_certs) 106 | channel = grpc.secure_channel(server_host + ':' + str(server_port), credentials) 107 | stub = namer_pb2_grpc.NamerStub(channel) 108 | ``` 109 | 110 | [Sandtable](https://www.sandtable.com/using-ssl-with-grpc-in-python/) has a well written post about building this kind of TLS gRPC server and client. 111 | 112 | ## TLS Client Identification and Authentication 113 | 114 | ### Client and Server trust the certificate authority, and therefor, each other 115 | 116 | When the client connects to the server, it presents its own certificate during the TLS handshake with the server. The client verifies the server certificate by confirming that the certificate was signed and generated using our certificate authority. The server, in turn, does the same thing, and confirms that the client is presenting a certificate that is signed and generated by our certificate authority. 117 | 118 | #### Mind the CA bundle 119 | 120 | Note that you can pass a CA bundle (multiple CA certificates concatenated in a single file) to `grpc.ssl_server_credentials()`, and that means that your server will trust any client certificates signed by those CAs. If you put a Public CA certificate in that bundle (like one from GoDaddy, Symantec, GeoTrust, etc.) any certificate signed by one of those CAs will be acceptable to the server. 121 | 122 | #### TO BE COMPLETED 123 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | grpcio==1.23.0 2 | grpcio-health-checking==1.23.0 3 | grpcio-tools==1.23.0 4 | prometheus-client==0.7.1 5 | protobuf==3.15.0 6 | six==1.12.0 7 | -------------------------------------------------------------------------------- /resources/certificate-hierarchy-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joekottke/python-grpc-ssl/2e750ec39151d9bca0c565f06aa417e83cf35b69/resources/certificate-hierarchy-basic.png -------------------------------------------------------------------------------- /resources/certificate-hierarchy-intermediate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joekottke/python-grpc-ssl/2e750ec39151d9bca0c565f06aa417e83cf35b69/resources/certificate-hierarchy-intermediate.png -------------------------------------------------------------------------------- /resources/certificate-hierarchy.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joekottke/python-grpc-ssl/2e750ec39151d9bca0c565f06aa417e83cf35b69/resources/certificate-hierarchy.graffle -------------------------------------------------------------------------------- /resources/certificate-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joekottke/python-grpc-ssl/2e750ec39151d9bca0c565f06aa417e83cf35b69/resources/certificate-panel.png -------------------------------------------------------------------------------- /src/client.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import random 3 | import time 4 | 5 | import grpc 6 | 7 | from proto import namer_pb2 8 | from proto import namer_pb2_grpc 9 | 10 | prefixes = ['Dr.', 'Honorary', 'Judge'] 11 | suffixes = ['III', 'II', 'The Great', 'BDFL', 'Esq'] 12 | first_names = ['Jim', 'Henry', 'Michael', 'Robert', 'Ronald', 13 | 'Sarah', 'Alison', 'Kimberly', 'Sasha', 'Norma'] 14 | last_names = ['Smith', 'Jones', 'Cooper', 'Hunter', 'Baker', 15 | 'Watson', 'Redmond', 'Williams', 'Crick', 'Moore'] 16 | 17 | 18 | def command_arguments(): 19 | parser = argparse.ArgumentParser(description='GRPC-based namer client.') 20 | parser.add_argument( 21 | '--host', 22 | type=str, 23 | required=True, 24 | help='The server hostname or address.' 25 | ) 26 | parser.add_argument( 27 | '--port', 28 | type=int, 29 | required=True, 30 | help='The server port' 31 | ) 32 | parser.add_argument( 33 | '--ca_cert', 34 | type=str, 35 | required=False, 36 | help='CA cert or bundle.' 37 | ) 38 | parser.add_argument( 39 | '--client_cert', 40 | type=str, 41 | required=False, 42 | help='Client certificate used for client identification and auth.' 43 | ) 44 | parser.add_argument( 45 | '--client_key', 46 | type=str, 47 | required=False, 48 | help='Client certificate key.' 49 | ) 50 | return parser.parse_args() 51 | 52 | 53 | def build_client_stub(cli_args): 54 | cert = None 55 | key = None 56 | if cli_args.client_cert: 57 | cert = open(cli_args.client_cert, 'rb').read() 58 | key = open(cli_args.client_key, 'rb').read() 59 | 60 | if cli_args.ca_cert: 61 | ca_cert = open(cli_args.ca_cert, 'rb').read() 62 | credentials = grpc.ssl_channel_credentials(ca_cert, key, cert) 63 | channel = grpc.secure_channel( 64 | cli_args.host + ':' + str(cli_args.port), credentials) 65 | else: 66 | channel = grpc.insecure_channel( 67 | cli_args.host + ':' + str(cli_args.port)) 68 | 69 | return namer_pb2_grpc.NamerStub(channel) 70 | 71 | 72 | def main(): 73 | args = command_arguments() 74 | stub = build_client_stub(args) 75 | 76 | start_time = time.time() 77 | for _ in range(1000): 78 | prefix = None 79 | suffix = None 80 | middle = None 81 | first = random.choice(first_names) 82 | last = random.choice(last_names) 83 | if random.randint(0, 1): 84 | middle = random.choice(first_names) 85 | if random.randint(0, 1): 86 | prefix = random.choice(prefixes) 87 | if random.randint(0, 1): 88 | suffix = random.choice(suffixes) 89 | name_request = namer_pb2.NameRequest( 90 | first_name=first, last_name=last, 91 | middle_name=middle, prefix=prefix, suffix=suffix 92 | ) 93 | name_response = stub.EnglishFullName(name_request) 94 | #print("Got response: '{}'".format(name_response.full_name)) 95 | time_total = time.time() - start_time 96 | print("Total time: {}\nTotal QPS: {}".format( 97 | time_total, 1000 / time_total)) 98 | 99 | 100 | if __name__ == '__main__': 101 | main() 102 | -------------------------------------------------------------------------------- /src/namer.py: -------------------------------------------------------------------------------- 1 | def english_full_name(first=None, last=None, middle=None, 2 | prefix=None, suffix=None): 3 | fullname = None 4 | 5 | if first is None or last is None: 6 | raise ValueError("first and last must be specified") 7 | 8 | if middle: 9 | fullname = first + " " + middle + " " + last 10 | else: 11 | fullname = "{} {}".format(first, last) 12 | 13 | if prefix: 14 | fullname = prefix + " " + fullname 15 | 16 | if suffix: 17 | fullname = fullname + " " + suffix 18 | 19 | return fullname 20 | -------------------------------------------------------------------------------- /src/proto/.gitignore: -------------------------------------------------------------------------------- 1 | namer_pb2_grpc.py 2 | namer_pb2.py -------------------------------------------------------------------------------- /src/proto/__init__.py: -------------------------------------------------------------------------------- 1 | # Generated *pb2.py and *pb2_grpc.py files go here with the following command 2 | # run at the top level of the repo: 3 | # 4 | # python -m grpc_tools.protoc -I./protos --python_out=./src/proto --grpc_python_out=./src/proto/ ./protos/namer.proto 5 | -------------------------------------------------------------------------------- /src/proto/namer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | service Namer { 4 | rpc EnglishFullName (NameRequest) returns (NameResponse) {} 5 | } 6 | 7 | message NameRequest { 8 | string first_name = 1; 9 | string last_name = 2; 10 | string middle_name = 3; 11 | string prefix = 4; 12 | string suffix = 5; 13 | } 14 | 15 | message NameResponse { 16 | string full_name = 1; 17 | } 18 | -------------------------------------------------------------------------------- /src/server.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import time 3 | from concurrent import futures 4 | 5 | import grpc 6 | 7 | from prometheus_client import start_http_server, Summary 8 | from proto import namer_pb2 9 | from proto import namer_pb2_grpc 10 | 11 | import namer 12 | 13 | from grpc_health.v1 import health, health_pb2, health_pb2_grpc 14 | 15 | request_metric = Summary( 16 | 'english_full_name_seconds', 17 | 'Time processing EnglishFullName requests') 18 | 19 | 20 | class Namer(namer_pb2_grpc.NamerServicer): 21 | @request_metric.time() 22 | def EnglishFullName(self, request, context): 23 | response = namer_pb2.NameResponse() 24 | first = request.first_name 25 | last = request.last_name 26 | middle = request.middle_name 27 | prefix = request.prefix 28 | suffix = request.suffix 29 | response.full_name = namer.english_full_name( 30 | first=first, last=last, 31 | middle=middle, prefix=prefix, 32 | suffix=suffix) 33 | # print('Peer: {}\nPeerIdentityKey: {}\nMetadata: {}'.format( 34 | # context.peer(), context.peer_identity_key(), 35 | # context.invocation_metadata())) 36 | return response 37 | 38 | 39 | def command_args(): 40 | parser = argparse.ArgumentParser(description='GRPC-based namer server.') 41 | parser.add_argument( 42 | '--port', 43 | type=int, 44 | required=True, 45 | help='The server listen port' 46 | ) 47 | parser.add_argument( 48 | '--metrics_port', 49 | type=int, 50 | required=False, 51 | help='The server metrics http port' 52 | ) 53 | parser.add_argument( 54 | '--ca_cert', 55 | type=str, 56 | required=False, 57 | help='CA cert or bundle.' 58 | ) 59 | parser.add_argument( 60 | '--server_cert', 61 | type=str, 62 | required=False, 63 | help='Server certificate.' 64 | ) 65 | parser.add_argument( 66 | '--server_key', 67 | type=str, 68 | required=False, 69 | help='Server certificate key.' 70 | ) 71 | return parser.parse_args() 72 | 73 | 74 | def serve(args): 75 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) 76 | 77 | health_servicer = health.HealthServicer() 78 | health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server) 79 | 80 | namer_pb2_grpc.add_NamerServicer_to_server(Namer(), server) 81 | 82 | ca_cert = None 83 | client_auth = False 84 | if args.ca_cert: 85 | ca_cert = open(args.ca_cert, 'rb').read() 86 | client_auth = True 87 | 88 | if args.server_cert and args.server_key: 89 | private_key = open(args.server_key, 'rb').read() 90 | certificate_chain = open(args.server_cert, 'rb').read() 91 | 92 | credentials = grpc.ssl_server_credentials( 93 | [(private_key, certificate_chain)], 94 | root_certificates=ca_cert, 95 | require_client_auth=client_auth 96 | ) 97 | server.add_secure_port('[::]:' + str(args.port), credentials) 98 | else: 99 | server.add_insecure_port('[::]:' + str(args.port)) 100 | 101 | if args.metrics_port: 102 | print('Starting metrics server. Listening on port {}...'.format( 103 | args.metrics_port)) 104 | start_http_server(args.metrics_port) 105 | 106 | print('Starting server. Listening on port {}...'.format(args.port)) 107 | server.start() 108 | health_servicer.set('', health_pb2.HealthCheckResponse.SERVING) 109 | try: 110 | while True: 111 | time.sleep(86400) 112 | except KeyboardInterrupt: 113 | health_servicer.set('', health_pb2.HealthCheckResponse.NOT_SERVING) 114 | time.sleep(10) 115 | server.stop(1) 116 | 117 | 118 | if __name__ == '__main__': 119 | args = command_args() 120 | serve(args) 121 | -------------------------------------------------------------------------------- /src/test_namer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import namer 4 | 5 | 6 | class TestNamerEnglishFullName(unittest.TestCase): 7 | 8 | def testFirstLast(self): 9 | first = 'Robert' 10 | last = 'Jones' 11 | self.assertEqual(namer.english_full_name( 12 | first=first, last=last), first + ' ' + last) 13 | 14 | def testFirstLastMiddle(self): 15 | first = 'Robert' 16 | middle = 'Thomas' 17 | last = 'Jones' 18 | self.assertEqual(namer.english_full_name( 19 | first=first, last=last, middle=middle), 20 | first + ' ' + middle + ' ' + last) 21 | 22 | def testPrefixFirstLast(self): 23 | prefix = 'Dr' 24 | first = 'Robert' 25 | last = 'Jones' 26 | self.assertEqual(namer.english_full_name( 27 | first=first, last=last, prefix=prefix), 28 | prefix + ' ' + first + ' ' + last) 29 | 30 | def testPrefixFirstMiddleLast(self): 31 | prefix = 'Dr' 32 | first = 'Robert' 33 | middle = 'Thomas' 34 | last = 'Jones' 35 | self.assertEqual(namer.english_full_name( 36 | first=first, last=last, middle=middle, prefix=prefix), 37 | prefix + ' ' + first + ' ' + middle + ' ' + last) 38 | 39 | def testMissingFirstLast(self): 40 | self.assertRaises(ValueError, namer.english_full_name) 41 | 42 | def testMissingLast(self): 43 | self.assertRaises(ValueError, namer.english_full_name, first='John') 44 | 45 | def testMissingFirst(self): 46 | self.assertRaises(ValueError, namer.english_full_name, last='Doe') 47 | 48 | 49 | if __name__ == '__main__': 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /ssl/README.md: -------------------------------------------------------------------------------- 1 | # SSL setup 2 | 3 | ## CFSSL 4 | 5 | Files in this directory are meant to be used with the CFSSL toolkit, which can be found at the [CFSSL Website](https://cfssl.org/) 6 | 7 | ## Files 8 | 9 | While it is not required for basic testing, it is otherwise assumed that files in this directory will be modified with your own details, particularly the `ca-config.json` and `ca-csr.json` files. 10 | 11 | ## Generate Certificate Authority 12 | 13 | ```sh 14 | cfssl gencert -initca ca-csr.json | cfssljson -bare ca 15 | ``` 16 | 17 | This will generate the files `ca.pem` and `ca-key.pem`. These files will be used to generate the client and server certificates, and the `ca.pem` file will be used to verify the client and server to each other, respectively. 18 | 19 | ## Generate client certificate 20 | 21 | ```sh 22 | cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json client-csr.json | cfssljson -bare client 23 | ``` 24 | 25 | This will generate the files `client.pem` and `client-key.pem`. 26 | 27 | **_Note:_** You will get a warning from this command: 28 | 29 | ``` 30 | [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for 31 | websites. For more information see the Baseline Requirements for the Issuance and Management 32 | of Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org); 33 | specifically, section 10.2.3 ("Information Requirements"). 34 | ``` 35 | 36 | As this certificate is to be used for a client, and not a server, it is acceptable that the hosts field is missing. 37 | 38 | ## Generate server certificate 39 | 40 | ```sh 41 | cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -hostname= server-csr.json | cfssljson -bare server 42 | ``` 43 | 44 | This will generate the files `server.pem` and `server-key.pem`. 45 | -------------------------------------------------------------------------------- /ssl/ca-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "signing": { 3 | "profiles": { 4 | "default": { 5 | "usages": ["signing", "key encipherment", "server auth", "client auth"], 6 | "expiry": "8760h" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ssl/ca-csr.json: -------------------------------------------------------------------------------- 1 | { 2 | "CN": "Example CA", 3 | "key": { 4 | "algo": "rsa", 5 | "size": 2048 6 | }, 7 | "names": [ 8 | { 9 | "C": "US", 10 | "L": "San Francisco", 11 | "O": "Example", 12 | "OU": "CertificateAuthority", 13 | "ST": "California" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /ssl/client-csr.json: -------------------------------------------------------------------------------- 1 | { 2 | "CN": "TestClient", 3 | "key": { 4 | "algo": "rsa", 5 | "size": 2048 6 | }, 7 | "names": [ 8 | { 9 | "C": "US", 10 | "L": "San Francisco", 11 | "O": "Example", 12 | "OU": "SRE-Operations", 13 | "ST": "California" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /ssl/server-csr.json: -------------------------------------------------------------------------------- 1 | { 2 | "CN": "server.example.com", 3 | "key": { 4 | "algo": "rsa", 5 | "size": 2048 6 | }, 7 | "names": [ 8 | { 9 | "C": "US", 10 | "L": "San Francisco", 11 | "O": "Example", 12 | "OU": "SRE-Operations", 13 | "ST": "California" 14 | } 15 | ] 16 | } 17 | --------------------------------------------------------------------------------