Skip to content

Commit

Permalink
feat: Pebble Notices (#20)
Browse files Browse the repository at this point in the history
Co-authored-by: Ghislain Bourgeois <[email protected]>
Co-authored-by: Guillaume Belanger <[email protected]>
  • Loading branch information
3 people authored Jun 7, 2024
1 parent fce043d commit 2142819
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 43 deletions.
54 changes: 50 additions & 4 deletions .github/workflows/build-rock.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
sudo skopeo --insecure-policy copy oci-archive:${{ steps.rockcraft.outputs.rock }} docker-daemon:gocert:latest
- name: Create files required by GoCert
run: |
printf 'keypath: "/etc/config/key.pem"\ncertpath: "/etc/config/cert.pem"\ndbpath: "/etc/config/certs.db"\nport: 3000\n' > config.yaml
printf 'key_path: "/etc/config/key.pem"\ncert_path: "/etc/config/cert.pem"\ndb_path: "/etc/config/certs.db"\nport: 3000\npebble_notifications: true\n' > config.yaml
openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 1 -out cert.pem -subj "/CN=githubaction.example"
- name: Run the image
Expand All @@ -33,15 +33,61 @@ jobs:
docker cp cert.pem gocert:/etc/config/cert.pem
docker cp config.yaml gocert:/etc/config/config.yaml
docker restart gocert
- name: Check if GoCert is successfully running
id: test_image
- name: Check if GoCert frontend is loaded
run: |
sleep 30
docker logs gocert
curl -k https://localhost:3000/certificate_requests.html 2>&1 | grep "Certificate Requests"
- name: Test if pebble notify fires correctly
id: test_notify
run : |
curl -XPOST -k -d '-----BEGIN CERTIFICATE REQUEST-----
MIIC5zCCAc8CAQAwRzEWMBQGA1UEAwwNMTAuMTUyLjE4My41MzEtMCsGA1UELQwk
MzlhY2UxOTUtZGM1YS00MzJiLTgwOTAtYWZlNmFiNGI0OWNmMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjM5Wz+HRtDveRzeDkEDM4ornIaefe8d8nmFi
pUat9qCU3U9798FR460DHjCLGxFxxmoRitzHtaR4ew5H036HlGB20yas/CMDgSUI
69DyAsyPwEJqOWBGO1LL50qXdl5/jOkO2voA9j5UsD1CtWSklyhbNhWMpYqj2ObW
XcaYj9Gx/TwYhw8xsJ/QRWyCrvjjVzH8+4frfDhBVOyywN7sq+I3WwCbyBBcN8uO
yae0b/q5+UJUiqgpeOAh/4Y7qI3YarMj4cm7dwmiCVjedUwh65zVyHtQUfLd8nFW
Kl9775mNBc1yicvKDU3ZB5hZ1MZtpbMBwaA1yMSErs/fh5KaXwIDAQABoFswWQYJ
KoZIhvcNAQkOMUwwSjBIBgNVHREEQTA/hwQKmLc1gjd2YXVsdC1rOHMtMC52YXVs
dC1rOHMtZW5kcG9pbnRzLnZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqGSIb3
DQEBCwUAA4IBAQCJt8oVDbiuCsik4N5AOJIT7jKsMb+j0mizwjahKMoCHdx+zv0V
FGkhlf0VWPAdEu3gHdJfduX88WwzJ2wBBUK38UuprAyvfaZfaYUgFJQNC6DH1fIa
uHYEhvNJBdFJHaBvW7lrSFi57fTA9IEPrB3m/XN3r2F4eoHnaJJqHZmMwqVHck87
cAQXk3fvTWuikHiCHqqdSdjDYj/8cyiwCrQWpV245VSbOE0WesWoEnSdFXVUfE1+
RSKeTRuuJMcdGqBkDnDI22myj0bjt7q8eqBIjTiLQLnAFnQYpcCrhc8dKU9IJlv1
H9Hay4ZO9LRew3pEtlx2WrExw/gpUcWM8rTI
-----END CERTIFICATE REQUEST-----' 'https://localhost:3000/api/v1/certificate_requests'
curl -XPOST -k -d '-----BEGIN CERTIFICATE-----
MIIDrDCCApSgAwIBAgIURKr+jf7hj60SyAryIeN++9wDdtkwDQYJKoZIhvcNAQEL
BQAwOTELMAkGA1UEBhMCVVMxKjAoBgNVBAMMIXNlbGYtc2lnbmVkLWNlcnRpZmlj
YXRlcy1vcGVyYXRvcjAeFw0yNDAzMjcxMjQ4MDRaFw0yNTAzMjcxMjQ4MDRaMEcx
FjAUBgNVBAMMDTEwLjE1Mi4xODMuNTMxLTArBgNVBC0MJDM5YWNlMTk1LWRjNWEt
NDMyYi04MDkwLWFmZTZhYjRiNDljZjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAIzOVs/h0bQ73kc3g5BAzOKK5yGnn3vHfJ5hYqVGrfaglN1Pe/fBUeOt
Ax4wixsRccZqEYrcx7WkeHsOR9N+h5RgdtMmrPwjA4ElCOvQ8gLMj8BCajlgRjtS
y+dKl3Zef4zpDtr6APY+VLA9QrVkpJcoWzYVjKWKo9jm1l3GmI/Rsf08GIcPMbCf
0EVsgq7441cx/PuH63w4QVTsssDe7KviN1sAm8gQXDfLjsmntG/6uflCVIqoKXjg
If+GO6iN2GqzI+HJu3cJoglY3nVMIeuc1ch7UFHy3fJxVipfe++ZjQXNconLyg1N
2QeYWdTGbaWzAcGgNcjEhK7P34eSml8CAwEAAaOBnTCBmjAhBgNVHSMEGjAYgBYE
FN/vgl9cAapV7hH9lEyM7qYS958aMB0GA1UdDgQWBBRJJDZkHr64VqTC24DPQVld
Ba3iPDAMBgNVHRMBAf8EAjAAMEgGA1UdEQRBMD+CN3ZhdWx0LWs4cy0wLnZhdWx0
LWs4cy1lbmRwb2ludHMudmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWyHBAqYtzUwDQYJ
KoZIhvcNAQELBQADggEBAEH9NTwDiSsoQt/QXkWPMBrB830K0dlwKl5WBNgVxFP+
hSfQ86xN77jNSp2VxOksgzF9J9u/ubAXvSFsou4xdP8MevBXoFJXeqMERq5RW3gc
WyhXkzguv3dwH+n43GJFP6MQ+n9W/nPZCUQ0Iy7ueAvj0HFhGyZzAE2wxNFZdvCs
gCX3nqYpp70oZIFDrhmYwE5ij5KXlHD4/1IOfNUKCDmQDgGPLI1tVtwQLjeRq7Hg
XVelpl/LXTQawmJyvDaVT/Q9P+WqoDiMjrqF6Sy7DzNeeccWVqvqX5TVS6Ky56iS
Mvo/+PAJHkBciR5Xn+Wg2a+7vrZvT6CBoRSOTozlLSM=
-----END CERTIFICATE-----' 'https://localhost:3000/api/v1/certificate_requests/1/certificate'
docker exec gocert /usr/bin/pebble notices
docker exec gocert /usr/bin/pebble notices | grep gocert\\.com/certificate/update
docker exec gocert /usr/bin/pebble notice 3
- uses: actions/upload-artifact@v4
if: steps.test_image.outcome == 'success'
if: steps.test_notify.outcome == 'success'
with:

name: rock
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
1. Install Go and Nodejs
2. Fork the repository on GitHub
3. Clone the forked repository to your local machine
4. Build the frontend: `cd ui && npm build build`
4. Build the frontend: `npm i --prefix ui && npm run build --prefix ui`
5. Install the project: `go install ./...`
6. Create a `config.yaml` file:
6. Create a `config.yaml` file as described in README.md
7. Run the project: `gocert -config config.yaml`

Commands for go need to be run from the project directory, and commands for the frontend need to be run from the `ui/` directory
Commands assume you're running them from the top level git repo directory
## Testing

### Unit Tests
Expand Down
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,30 @@ GoCert requires 3 files to operate:
* A private key
* A TLS certificate with that private key
* A YAML config file with the required parameters
as an example:

You can generate the cert and the associated key by running:
```
openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 1 -out cert.pem -subj "/CN=example.com"
```

GoCert does not support insecure http connections.

### Config File
The config file requires the following parameters:
| Key | Type | Description |
|----------------------|---------|----------|
| key_path | string | path to the private key for enabling HTTPS connections |
| cert_path | string | path to a PEM formatted certificate for enabling HTTPS connections |
| db_path | string | path to a sqlite database file. If the file does not exist GoCert will attempt to create it. |
| port | integer (0-65535) | port number on which GoCert will listen for all incoming API and frontend connections. |
| pebble_notifications | boolean | Allow GoCert to send pebble notices on certificate events (create, update, delete). Pebble needs to be running on the same system as GoCert. Read more about Pebble Notices [here](https://github.com/canonical/pebble?tab=readme-ov-file#notices). |

An example config file may look like:

```yaml
keypath: "./key.pem"
certpath: "./cert.pem"
dbpath: "./certs.db"
key_path: "./key.pem"
cert_path: "./cert.pem"
db_path: "./certs.db"
port: 3000
pebble_notifications: true
```
18 changes: 10 additions & 8 deletions cmd/gocert/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,18 @@ Q53tuiWQeoxNOjHiWstBPELxGbW6447JyVVbNYGUk+VFU7okzA6sRTJ/5Ysda4Sf
auNQc2hruhr/2plhFUYoZHPzGz7d5zUGKymhCoS8BsFVtD0WDL4srdtY/W2Us7TD
D7DC34n8CH9+avz9sCRwxpjxKnYW/BeyK0c4n9uZpjI8N4sOVqy6yWBUseww
-----END RSA PRIVATE KEY-----`
validConfig = `keypath: "./key_test.pem"
certpath: "./cert_test.pem"
dbpath: "./certs.db"
port: 8000`
validConfig = `key_path: "./key_test.pem"
cert_path: "./cert_test.pem"
db)path: "./certs.db"
port: 8000
pebble_notices: false`
invalidConfig = `hello: "world"
goodbye: "world"`
invalidDBConfig = `keypath: "./key_test.pem"
certpath: "./cert_test.pem"
dbpath: "/etc/hosts"
port: 8000`
invalidDBConfig = `key_path: "./key_test.pem"
cert_path: "./cert_test.pem"
db_path: "/etc/hosts"
port: 8000
pebble_notices: false`
)

func TestMain(m *testing.M) {
Expand Down
27 changes: 24 additions & 3 deletions internal/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,15 @@ func PostCertificate(env *Environment) http.HandlerFunc {
logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w)
return
}
insertIdStr := strconv.FormatInt(insertId, 10)
if env.SendPebbleNotifications {
err := SendPebbleNotification("gocert.com/certificate/update", insertIdStr)
if err != nil {
log.Printf("pebble notify failed: %s. continuing silently.", err.Error())
}
}
w.WriteHeader(http.StatusCreated)
if _, err := w.Write([]byte(strconv.FormatInt(insertId, 10))); err != nil {
if _, err := w.Write([]byte(insertIdStr)); err != nil {
logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w)
}
}
Expand All @@ -191,8 +198,15 @@ func RejectCertificate(env *Environment) http.HandlerFunc {
logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w)
return
}
insertIdStr := strconv.FormatInt(insertId, 10)
if env.SendPebbleNotifications {
err := SendPebbleNotification("gocert.com/certificate/update", insertIdStr)
if err != nil {
log.Printf("pebble notify failed: %s. continuing silently.", err.Error())
}
}
w.WriteHeader(http.StatusAccepted)
if _, err := w.Write([]byte(strconv.FormatInt(insertId, 10))); err != nil {
if _, err := w.Write([]byte(insertIdStr)); err != nil {
logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w)
}
}
Expand All @@ -212,8 +226,15 @@ func DeleteCertificate(env *Environment) http.HandlerFunc {
logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w)
return
}
insertIdStr := strconv.FormatInt(insertId, 10)
if env.SendPebbleNotifications {
err := SendPebbleNotification("gocert.com/certificate/update", insertIdStr)
if err != nil {
log.Printf("pebble notify failed: %s. continuing silently.", err.Error())
}
}
w.WriteHeader(http.StatusAccepted)
if _, err := w.Write([]byte(strconv.FormatInt(insertId, 10))); err != nil {
if _, err := w.Write([]byte(insertIdStr)); err != nil {
logErrorAndWriteResponse(err.Error(), http.StatusInternalServerError, w)
}
}
Expand Down
32 changes: 23 additions & 9 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,32 @@ import (
"log"
"net/http"
"os"
"os/exec"
"time"

"github.com/canonical/gocert/internal/certdb"
"gopkg.in/yaml.v3"
)

type ConfigYAML struct {
KeyPath string
CertPath string
DBPath string
Port int
KeyPath string `yaml:"key_path"`
CertPath string `yaml:"cert_path"`
DBPath string `yaml:"db_path"`
Port int `yaml:"port"`
Pebblenotificationsenabled bool `yaml:"pebble_notifications"`
}

type Config struct {
Key []byte
Cert []byte
DBPath string
Port int
Key []byte
Cert []byte
DBPath string
Port int
PebbleNotificationsEnabled bool
}

type Environment struct {
DB *certdb.CertificateRequestsRepository
DB *certdb.CertificateRequestsRepository
SendPebbleNotifications bool
}

// validateConfigFile opens and processes the given yaml file, and catches errors in the process
Expand Down Expand Up @@ -65,9 +69,18 @@ func validateConfigFile(filePath string) (Config, error) {
config.Key = key
config.DBPath = c.DBPath
config.Port = c.Port
config.PebbleNotificationsEnabled = c.Pebblenotificationsenabled
return config, nil
}

func SendPebbleNotification(key, request_id string) error {
cmd := exec.Command("pebble", "notify", key, fmt.Sprintf("request_id=%s", request_id))
if err := cmd.Run(); err != nil {
return errors.Join(errors.New("couldn't execute a pebble notify: "), err)
}
return nil
}

// NewServer creates an environment and an http server with handlers that Go can start listening to
func NewServer(configFile string) (*http.Server, error) {
config, err := validateConfigFile(configFile)
Expand All @@ -85,6 +98,7 @@ func NewServer(configFile string) (*http.Server, error) {

env := &Environment{}
env.DB = db
env.SendPebbleNotifications = config.PebbleNotificationsEnabled
router := NewGoCertRouter(env)

s := &http.Server{
Expand Down
24 changes: 12 additions & 12 deletions internal/api/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,23 +85,23 @@ Q53tuiWQeoxNOjHiWstBPELxGbW6447JyVVbNYGUk+VFU7okzA6sRTJ/5Ysda4Sf
auNQc2hruhr/2plhFUYoZHPzGz7d5zUGKymhCoS8BsFVtD0WDL4srdtY/W2Us7TD
D7DC34n8CH9+avz9sCRwxpjxKnYW/BeyK0c4n9uZpjI8N4sOVqy6yWBUseww
-----END RSA PRIVATE KEY-----`
validConfig = `keypath: "./key_test.pem"
certpath: "./cert_test.pem"
dbpath: "./certs.db"
validConfig = `key_path: "./key_test.pem"
cert_path: "./cert_test.pem"
db_path: "./certs.db"
port: 8000`
wrongCertConfig = `keypath: "./key_test.pem"
certpath: "./cert_test_wrong.pem"
dbpath: "./certs.db"
wrongCertConfig = `key_path: "./key_test.pem"
cert_path: "./cert_test_wrong.pem"
db_path: "./certs.db"
port: 8000`
wrongKeyConfig = `keypath: "./key_test_wrong.pem"
certpath: "./cert_test.pem"
dbpath: "./certs.db"
wrongKeyConfig = `key_path: "./key_test_wrong.pem"
cert_path: "./cert_test.pem"
db_path: "./certs.db"
port: 8000`
invalidYAMLConfig = `wrong: fields
every: where`
invalidFileConfig = `keypath: "./nokeyfile.pem"
certpath: "./nocertfile.pem"
dbpath: "./certs.db"
invalidFileConfig = `key_path: "./nokeyfile.pem"
cert_path: "./nocertfile.pem"
db_path: "./certs.db"
port: 8000`
)

Expand Down

0 comments on commit 2142819

Please sign in to comment.