├── .gitignore ├── .github └── workflows │ ├── etc │ └── ext.conf │ └── sign.yaml ├── new-ca.sh ├── new-csr.sh ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.key 2 | *.pem 3 | *.csr 4 | *.crt 5 | *.b64 6 | -------------------------------------------------------------------------------- /.github/workflows/etc/ext.conf: -------------------------------------------------------------------------------- 1 | basicConstraints = critical, CA:FALSE 2 | extendedKeyUsage = critical, serverAuth 3 | -------------------------------------------------------------------------------- /new-ca.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | openssl req -x509 -newkey rsa:4096 -sha256 -keyout "$CA_NAME.combined.pem" -noenc \ 3 | -subj "/CN=$CA_NAME" -days 3650 | tee "$CA_NAME.cert.pem" >> "$CA_NAME.combined.pem" 4 | -------------------------------------------------------------------------------- /new-csr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | openssl req -new -utf8 -newkey 2048 -noenc -keyout "$DNS_NAME.key" \ 3 | -subj "/CN=" -addext "subjectAltName = critical, DNS:$DNS_NAME" | tee "$DNS_NAME.csr" | base64 -w0 > "$DNS_NAME.csr.b64" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright Andrew Benton 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 | -------------------------------------------------------------------------------- /.github/workflows/sign.yaml: -------------------------------------------------------------------------------- 1 | name: sign 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | csr: 6 | description: "A base64-encoded PEM-encoded CSR" 7 | type: string 8 | required: true 9 | days: 10 | description: "Days until the certificate expires" 11 | type: number 12 | required: false 13 | default: 30 14 | 15 | jobs: 16 | sign: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - run: echo "${{ secrets.CA_KEY_AND_CERT }}" > ca.pem 21 | - run: echo "${{ inputs.csr }}" | base64 -d | openssl x509 -req -CA ca.pem -days ${{ inputs.days }} -copy_extensions copy -ext "subjectAltName" -extfile .github/workflows/etc/ext.conf -out cert.pem 22 | - id: certInfo 23 | run: openssl x509 -noout -serial -in cert.pem >> "$GITHUB_OUTPUT" 24 | - run: mv cert.pem ${{ steps.certInfo.outputs.serial }}.pem 25 | - uses: actions/upload-artifact@v4 26 | with: 27 | name: ${{ steps.certInfo.outputs.serial }}.pem 28 | path: ${{ steps.certInfo.outputs.serial }}.pem 29 | if-no-files-found: error 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A GitHub-native private certificate authority 2 | 3 | For local development environments that need TLS certificates, it's useful to mimic public 4 | certificate authority behavior with a private CA that belongs to your organization. 5 | 6 | This repo is a very simple wrapper around the `openssl` certificate signing command, which 7 | runs without dependencies inside of GitHub actions. You store your CA's private key as a 8 | repository secret, and signed certificates are stored as workflow run artifacts. 9 | 10 | > [!IMPORTANT] 11 | > Before continuing, create a new private repository in your GitHub organization using 12 | > this repository as a template. Click the green "Use this template" button above. 13 | 14 | If you've already completed setup, skip to the [Get a signed cert](#get-a-signed-cert) 15 | section. 16 | 17 | The following steps assume you have a POSIX-ish shell and `openssl` available on your 18 | local machine so make sure it's installed before proceeding. The included shell scripts 19 | also depend on `tee` and `base64` with the flag `-w0` but can be easily adapted if 20 | those aren't available. 21 | 22 | ## Setup 23 | 24 | You only need to do this once. Skip to the [Get a signed cert](#get-a-signed-cert) 25 | section if you completed setup previously. 26 | 27 | #### Create your CA key and cert 28 | 29 | On your local machine, create a CA key and certificate, combined into a single file. You 30 | only need to do this once. Use the provided `new-ca.sh` shell script for convenience: 31 | 32 | ```sh 33 | CA_NAME="Acme, Inc. Private CA" ./new-ca.sh 34 | ``` 35 | 36 | #### Configure GitHub 37 | 38 | Create a repository secret named `CA_KEY_AND_CERT` with the contents of your combined CA key 39 | and certificate PEM file. 40 | 41 | 1. In GitHub, navigate to the settings page for [actions secrets and variables](../../settings/secrets/actions) 42 | 3. In the "Secrets" tab, select "New repository secret" 43 | 4. Name your secret `CA_KEY_AND_CERT` 44 | 5. Paste the contents of your combined CA key and certificate PEM file as the "Secret" value 45 | 46 | Note that once you've stored the private key as a GitHub repository secret you can destroy 47 | the key on your local machine (just be sure to keep a copy of the CA cert). Your CA will 48 | then only exist inside of your GitHub repo. 49 | 50 | #### Add your CA certificate to trust stores (optional but recommended) 51 | 52 | Presuming you want your leaf ("server") certificates to be accepted as valid by TLS clients 53 | (i.e. web browsers, curl, etc.) you'll need to add the CA certificate created in step one 54 | (just the cert, not the combined key and cert) to the relevant "trust stores" in your 55 | development environment. 56 | 57 | ## Get a signed cert 58 | 59 | #### Create a private key and certificate signing request 60 | 61 | On your local machine, create a new private key and use it to generate a CSR for whatever 62 | DNS name you want to appear in your signed cert. Use the provided `new-csr.sh` shell 63 | script for convenience: 64 | 65 | ```sh 66 | DNS_NAME=www.example.com ./new-csr.sh 67 | ``` 68 | 69 | You'll need the private key to configure TLS for your server later, so make a note of its 70 | location. It's the file ending in `.key`. 71 | 72 | #### Request a signed certificate 73 | 74 | Certificates are signed using GitHub Actions. This repository includes a workflow named "sign" 75 | that you can initiate directly from the [GitHub UI](../../actions/workflows/sign.yaml) or API. 76 | In the UI, at the top of the workflow runs list, you should see a banner that reads 77 | "This workflow has a workflow_dispatch event trigger." To the right of that is a button named 78 | "Run workflow" which you can use to initiate a new signing request. 79 | 80 | The only required input is a base64-encoded certificate signing request. If you used the 81 | provided shell script in the previous step then the CSR file ending in `.b64` contains the 82 | data you need. 83 | 84 | Run the workflow with your base64-encoded CSR as input. Assuming the run completes successfully 85 | your signed cert is available as a downloadable artifact from the workflow run details page. 86 | 87 | ## Use the signed cert 88 | 89 | Pop the private key and signed cert from the previous step into any server's TLS configuration 90 | and it should work. 91 | --------------------------------------------------------------------------------