Compare commits

...

39 commits
0.1.1 ... main

Author SHA1 Message Date
0cea3f282b
set disableExplicitIndexURLs
All checks were successful
ci/woodpecker/push/deploy-docs Pipeline was successful
2024-07-31 15:34:25 +02:00
527c038f02
update docs
All checks were successful
ci/woodpecker/push/deploy-docs Pipeline was successful
2024-07-30 14:09:53 +02:00
7098a6c5eb
update Docs with latest version
All checks were successful
ci/woodpecker/push/deploy-docs Pipeline was successful
2024-07-30 09:01:42 +02:00
3a0a739fae
add git as dependency to CI pipeline
All checks were successful
ci/woodpecker/tag/build-and-deploy/3 Pipeline was successful
ci/woodpecker/tag/build-and-deploy/2 Pipeline was successful
ci/woodpecker/tag/build-and-deploy/1 Pipeline was successful
2024-07-30 08:57:48 +02:00
146a2800cb
minor docs change
All checks were successful
ci/woodpecker/push/deploy-docs Pipeline was successful
ci/woodpecker/tag/build-and-deploy/2 Pipeline was successful
ci/woodpecker/tag/build-and-deploy/1 Pipeline was successful
ci/woodpecker/tag/build-and-deploy/3 Pipeline was successful
2024-07-30 08:54:08 +02:00
20cbb33778 Merge pull request 'add Makefile for easier builds' (#3) from feature-makefile into main
Reviewed-on: #3
2024-07-30 08:53:09 +02:00
6f20779714
bump version to 0.2.2 2024-07-30 08:49:00 +02:00
b8ec72877d
update CI 2024-07-30 08:48:48 +02:00
05a536a17d
add makefile 2024-07-30 08:42:51 +02:00
8d4f92f7be Merge pull request 'Change Docs theme to relearn' (#2) from feature-new-docs into main
All checks were successful
ci/woodpecker/push/deploy-docs Pipeline was successful
Reviewed-on: #2
2024-07-30 08:28:16 +02:00
eb367df0f9
add favicon and page logo 2024-07-30 08:27:49 +02:00
abc1ef0a79
describe configuration options 2024-07-30 08:11:08 +02:00
2fe2b980be
enable search feature 2024-07-29 20:30:56 +02:00
f40bdd5cbf
describe installation process 2024-07-29 20:30:42 +02:00
fcdc147dd2
small wording changes 2024-07-29 20:30:09 +02:00
74c32b87b9
begin installation page 2024-07-29 12:47:08 +02:00
dd104386a7
add sidbar items 2024-07-29 12:46:57 +02:00
5c80f88608
change to relearn theme 2024-07-28 17:58:52 +02:00
e46d924907
update contribution documenation
All checks were successful
ci/woodpecker/push/deploy-docs Pipeline was successful
2024-07-28 14:36:06 +02:00
345b2b10f7
clean up old docs
All checks were successful
ci/woodpecker/push/deploy-docs Pipeline was successful
2024-07-28 14:28:21 +02:00
6c2b04e6b9
update quickstart docs
All checks were successful
ci/woodpecker/push/deploy-docs Pipeline was successful
2024-07-28 08:11:49 +02:00
b988e4cf51
refactor logger init function 2024-07-12 17:41:50 +02:00
60aea576dc
remove sentry remnants 2024-07-12 17:31:44 +02:00
5e0e263ff8
use dev mail
All checks were successful
ci/woodpecker/push/deploy-docs Pipeline was successful
2024-07-12 14:37:05 +02:00
08cb6e27bc
update readme 2024-07-12 14:31:47 +02:00
9ee54b6c2a
add header
All checks were successful
ci/woodpecker/push/deploy-docs Pipeline was successful
2024-07-12 13:07:50 +02:00
5a6e51463a
add docs to repo 2024-07-12 13:04:33 +02:00
99c5a497f0
add vscode debug config 2024-07-12 12:54:25 +02:00
4997114458
bump version to 0.2.1
All checks were successful
ci/woodpecker/tag/build-and-deploy/3 Pipeline was successful
ci/woodpecker/tag/build-and-deploy/2 Pipeline was successful
ci/woodpecker/tag/build-and-deploy/1 Pipeline was successful
2024-07-12 11:18:50 +02:00
d1ac68c4f7
fix configuration validation 2024-07-12 11:18:32 +02:00
1746653453
update example config file 2024-07-12 10:58:27 +02:00
29af4dada3
change text in 0.2.0
All checks were successful
ci/woodpecker/tag/build-and-deploy/2 Pipeline was successful
ci/woodpecker/tag/build-and-deploy/3 Pipeline was successful
ci/woodpecker/tag/build-and-deploy/1 Pipeline was successful
2024-07-11 16:26:45 +02:00
f0ad7c021e
Bump version to 0.2.0 2024-07-11 16:25:03 +02:00
b1626cd198
refactor a lot 2024-07-11 16:22:58 +02:00
102fc2a056
add generic Certificate model 2024-07-11 16:22:26 +02:00
62803d451d
remove sentry 2024-07-11 16:11:11 +02:00
8ff8a63299
remove source logging because it annoys me 2024-07-11 16:10:34 +02:00
7c7fe0ba7c
show if certificate was changed or not 2024-07-08 10:26:49 +02:00
3821477c34
add CONTRIBUTING and MAINTAINERS files 2024-07-04 10:11:51 +02:00
40 changed files with 880 additions and 215 deletions

41
.gitignore vendored
View file

@ -1,31 +1,28 @@
# ---> Go
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
# Allowlisting gitignore template for GO projects prevents us
# from adding various unwanted local files, such as generated
# files, developer configurations or IDE-specific files etc.
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Recommended: Go.AllowList.gitignore
# Test binary, built with `go test -c`
*.test
# Ignore everything
*
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# But not these files...
!/.gitignore
# Dependency directories (remove the comment below to include it)
# vendor/
!*.go
!go.sum
!go.mod
# Go workspace file
go.work
!examples/*
bin/
!*.md
!LICENSE
examples/testing/
!Makefile
*.yaml
!examples/*.yaml
# Woodpecker CI
!.woodpecker/*
test/
# ...even if they are in subdirectories
!*/

24
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,24 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug Dry Run",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/certwarden-deploy/main.go",
"args": ["--config", "${workspaceFolder}/config.yaml", "--dry-run"]
},
{
"name": "Debug",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/certwarden-deploy/main.go",
"args": ["--config", "${workspaceFolder}/config.yaml", "--verbose"]
}
]
}

View file

@ -20,13 +20,13 @@ steps:
- APP_NAME=certwarden-deploy
- FORGE=https://code.lila.network
commands:
- apk add --update --no-cache xz curl jq
- go mod download
- go build -o output/$APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM} main.go
- cd output
- xz --keep --compress $APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM}
- sha256sum $APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM} >> $APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM}.sha256
- sha256sum $APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM}.xz >> $APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM}.xz.sha256
- apk add --update --no-cache xz curl jq make git
- make build
- cd bin/
- mv $APP_NAME $APP_NAME-${GOOS}-${GOARCH}${GOARM}
- xz --keep --compress $APP_NAME-${GOOS}-${GOARCH}${GOARM}
- sha256sum $APP_NAME-${GOOS}-${GOARCH}${GOARM} >> $APP_NAME-${GOOS}-${GOARCH}${GOARM}.sha256
- sha256sum $APP_NAME-${GOOS}-${GOARCH}${GOARM}.xz >> $APP_NAME-${GOOS}-${GOARCH}${GOARM}.xz.sha256
- |-
export RELEASE_ID=`curl --location "$FORGE/api/v1/repos/$CI_REPO/releases?limit=10" \
--header 'Accept: application/json' -s -S \
@ -35,23 +35,23 @@ steps:
curl --location "$FORGE/api/v1/repos/$CI_REPO/releases/$RELEASE_ID/assets" \
--header "Authorization: token $FORGEJO_APIKEY" \
--header 'Content-Type: multipart/form-data' -s -S \
--form "attachment=@$APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM};type=application/octet-stream" \
--form "attachment=@$APP_NAME-${GOOS}-${GOARCH}${GOARM};type=application/octet-stream" \
--fail-with-body
- |-
curl --location "$FORGE/api/v1/repos/$CI_REPO/releases/$RELEASE_ID/assets" \
--header "Authorization: token $FORGEJO_APIKEY" \
--header 'Content-Type: multipart/form-data' -s -S \
--form "attachment=@$APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM}.xz;type=application/octet-stream" \
--form "attachment=@$APP_NAME-${GOOS}-${GOARCH}${GOARM}.xz;type=application/octet-stream" \
--fail-with-body
- |-
curl --location "$FORGE/api/v1/repos/$CI_REPO/releases/$RELEASE_ID/assets" \
--header "Authorization: token $FORGEJO_APIKEY" \
--header 'Content-Type: multipart/form-data' -s -S \
--form "attachment=@$APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM}.sha256;type=application/octet-stream" \
--form "attachment=@$APP_NAME-${GOOS}-${GOARCH}${GOARM}.sha256;type=application/octet-stream" \
--fail-with-body
- |-
curl --location "$FORGE/api/v1/repos/$CI_REPO/releases/$RELEASE_ID/assets" \
--header "Authorization: token $FORGEJO_APIKEY" \
--header 'Content-Type: multipart/form-data' -s -S \
--form "attachment=@$APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM}.xz.sha256;type=application/octet-stream" \
--form "attachment=@$APP_NAME-${GOOS}-${GOARCH}${GOARM}.xz.sha256;type=application/octet-stream" \
--fail-with-body

View file

@ -0,0 +1,38 @@
when:
- event: push
branch: main
path:
include:
- 'docs/**'
- '.woodpecker/deploy-docs.yml'
ignore_message: '[ALL]'
steps:
build:
image: golang:1.22-bookworm
environment:
- HUGO_VERSION=0.128.1
- TZ=Europe/Berlin
commands:
- cd docs/
- wget https://github.com/gohugoio/hugo/releases/download/v$${HUGO_VERSION}/hugo_extended_$${HUGO_VERSION}_linux-amd64.deb && apt install ./hugo_extended_$${HUGO_VERSION}_linux-amd64.deb && rm -f hugo_extended_$${HUGO_VERSION}_linux-amd64.deb
- hugo --minify --destination ./public
upload:
image: alpine:latest
secrets:
- RSYNC_SSHKEY
- RSYNC_TARGET_SERVER
- RSYNC_TARGET_USER
environment:
- TARGET_PATH=/webroot/certwarden-deploy.adora.codes
- RSYNC_TARGET_PORT=2003
commands:
- cd docs/
- apk add --update --no-cache openssh rsync git
- mkdir -p $HOME/.ssh
- echo "$RSYNC_SSHKEY" > $HOME/.ssh/id_ed25519
- chmod 0600 $HOME/.ssh/id_ed25519
- ssh-keyscan -t ed25519 -p $RSYNC_TARGET_PORT $RSYNC_TARGET_SERVER >> $HOME/.ssh/known_hosts
- rsync -avh -e "ssh -p $RSYNC_TARGET_PORT" --delete ./public/ $RSYNC_TARGET_USER@$RSYNC_TARGET_SERVER:$TARGET_PATH

View file

@ -7,7 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.2.2] - 2024-07-30
### Changed
- changed the way the version string is handled internally
- CI pipeline changed
- documentation is now more sophisticated and has a new theme
### Added
- Makefile
## [0.2.1] - 2024-07-12
### Fixed
- Configuration validation did not work as intended
### Changed
- updated example config file
## [0.2.0] - 2024-07-11
### ⚠️ Breaking Changes
- Config file syntax was changed to accomodate both private and public key deployment for certificates.
This change is __NOT__ backwards compatible!
The following yaml keys were changed/added:
- `api_key`: changed to `cert_secret`
- `file_path`: changed to `cert_path`
- added keys: `key_secret`, `key_path`
### Changed
- config file syntax to enable deployment of private keys too
- refactor code
## [0.1.1] - 2024-07-03
@ -21,6 +50,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- some documentation
[unreleased]: https://code.lila.network/adoralaura/certwarden-deploy/compare/0.1.1...HEAD
[unreleased]: https://code.lila.network/adoralaura/certwarden-deploy/compare/0.2.2...HEAD
[0.2.2]: https://code.lila.network/adoralaura/certwarden-deploy/compare/0.2.1...0.2.2
[0.2.1]: https://code.lila.network/adoralaura/certwarden-deploy/compare/0.2.0...0.2.1
[0.2.0]: https://code.lila.network/adoralaura/certwarden-deploy/compare/0.1.1...0.2.0
[0.1.1]: https://code.lila.network/adoralaura/certwarden-deploy/compare/0.1.0...0.1.1
[0.1.0]: https://code.lila.network/adoralaura/certwarden-deploy/releases/tag/0.1.0

17
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,17 @@
# Contributing
I use my own [Forgejo Instance](https://code.lila.network) to manage issues and pull requests.
* If you have a trivial fix or improvement, go ahead and create a pull request,
addressing (with `@...`) the maintainer of this repository (see
[MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request.
* If you plan to do something more involved, first please [send me a mail]( mailto:dev@lauka.net?subject=%5Bcertwarden-deploy%5D).
# What to contribute
The best way to help without speaking a lot of Go would be to share your
configuration, alerts, dashboards, and recording rules. If you have something
that works and is not in the repository, please pay it forward and
share what works.

1
MAINTAINERS.md Normal file
View file

@ -0,0 +1 @@
* Adora Laura Kalb <dev@lauka.net> @adoralaura

13
Makefile Normal file
View file

@ -0,0 +1,13 @@
# Set the default Go build flags
GOFLAGS = -ldflags='-w -s -X constants.Version=$(VERSION)'
# Build the application
build:
go build $(GOFLAGS) -o bin/certwarden-deploy cmd/certwarden-deploy/main.go
# Clean the build artifacts
clean:
rm -rf bin
# Set a version for the build
VERSION := $(shell git describe --tags --always)

View file

@ -2,10 +2,81 @@
![status-badge](https://ci.lila.network/api/badges/22/status.svg)
[![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page)
This is a tool to deploy certificates from a [CertWarden](https://www.certwarden.com/) instance.
This is a simple binary to deploy certificates from a [CertWarden](https://www.certwarden.com/) instance.
## Quick Start
Installation of the required CertWarden instance is out of scope of this documentation. For detailed instructions regarding CertWarden, please visit [it's documentation](https://www.certwarden.com/docs/introduction/)
To quickly get started with `certwarden-deploy`, just download the binary...
```shell
# this downloads certwarden-deploy version 0.2.2
# to /usr/local/bin/certwarden-deploy
sudo wget https://code.lila.network/adoralaura/certwarden-deploy/releases/download/0.2.2/certwarden-deploy-linux-amd64 -O /usr/local/bin/certwarden-deploy
sudo chmod +x /usr/local/bin/certwarden-deploy
```
... fill out the config file...
```shell
vi /etc/certwarden-deploy/config.yaml
```
```yaml
# Base URL of the CertWarden instance
# required
base_url: "https://certwarden.example.com"
# Set this to true if your CertWarden instance does not have a publicly trusted
# TLS certificate (e.g. it has a self signed one)
# default is false
disable_certificate_validation: false
# define all managed certificates here
certificates:
# name is a unique identifier that must start and end with an alphanumeric character,
# and can contain the following characters: a-zA-Z0-9._-
# required
- name: test-certificate.example.com
# Contains the API-Key to fetch the certificate from the server
# required
cert_secret: examplekey_notvalid_hrzjGDDw8z
# path where to save the certificate
# required
cert_path: "/path/to/test-certificate.example.com-cert.pem"
# Contains the API-Key to fetch the private key from the server
# required
key_secret: examplekey_notvalid_hrzbbDDw8z
# path where to save the private key
# required
key_path: "/path/to/test-certificate.example.com-key.pem"
# action to run when certificate was updated or --force is on
action: "/usr/bin/systemd reload caddy"
```
... and run it!
```shell
certwarden-deploy -v
```
## Contributing
I use my own [Forgejo](https://forgejo.org/) Instance [code.lila.network](https://code.lila.network) to manage issues, pull requests and CI/CD.
* If you have a trivial fix or improvement, go ahead and send a diff to the maintainer(s) of this repository (see
[MAINTAINERS.md](https://code.lila.network/adoralaura/certwarden-deploy/src/branch/main/MAINTAINERS.md)).
* If you plan to do something more involved, first please [send me a mail]( mailto:dev@lauka.net?subject=%5Bcertwarden-deploy%5D)mso I can create an account for you.
### Non-development Contibutions
The best way to help without speaking a lot of Go would be to share your
configuration, setup, and tips. If you have something
that works and is not in the repository, please pay it forward and
share what works.
## Changelog
You can find the Changelog here: [Changelog](https://code.lila.network/adoralaura/certwarden-deploy/src/branch/main/CHANGELOG.md)
## License
`certwarden-deploy` is available under the MIT license. See the LICENSE file for more info.
`certwarden-deploy` is available under the MIT license. See the [LICENSE](https://code.lila.network/adoralaura/certwarden-deploy/src/branch/main/LICENSE) file for more info.

View file

@ -2,25 +2,20 @@
Copyright © 2024 Laura Kalb <dev@lauka.net>
The code of this project is available under the MIT license. See the LICENSE file for more info.
*/
package cmd
package main
import (
"os"
"time"
"code.lila.network/adoralaura/certwarden-deploy/internal/cli"
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
"github.com/getsentry/sentry-go"
)
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
func main() {
err := cli.RootCmd.Execute()
if err != nil {
os.Exit(1)
}
defer sentry.Flush(2 * time.Second)
}
func init() {

15
docs/.gitignore vendored Normal file
View file

@ -0,0 +1,15 @@
# Generated files by hugo
/public/
/resources/_gen/
/assets/jsconfig.json
hugo_stats.json
# Executable may be added to repository
hugo.exe
hugo.darwin
hugo.linux
# Temporary lock file while building
/.hugo_build.lock
!**.html

9
docs/LICENSE Normal file
View file

@ -0,0 +1,9 @@
MIT License
Copyright 2024 Adora Laura Kalb <dev@lauka.net>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

1
docs/README.md Normal file
View file

@ -0,0 +1 @@
# certwarden-deploy Documentation

View file

@ -0,0 +1,5 @@
+++
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
date = {{ .Date }}
draft = true
+++

54
docs/config.toml Normal file
View file

@ -0,0 +1,54 @@
baseURL = 'https://certwarden-deploy.adora.codes/'
languageCode = 'en-us'
title = 'certwarden-deploy'
author = ""
theme = "github.com/McShelby/hugo-theme-relearn"
repo = "https://code.lila.network/adoralaura/certwarden-deploy"
enableGitInfo = true
enableRobotsTXT = true
uniqueHomePage = false # change to false to add sidebar to homepage
[params]
disableLandingPageButton = false
disableLanguageSwitchingButton = false
editURL = "https://code.lila.network/adoralaura/certwarden-deploy/_edit/main/docs/content/${FilePath}"
externalLinkTarget = "_blank"
headingPre = '<script defer src="https://esseles.adora.codes/script.js" data-website-id="fe4ec517-25b2-4e0d-b502-6bd3a7420849"></script>'
disableExplicitIndexURLs = true
[params.author]
name = 'Adora Laura Kalb'
[outputs]
home = ['html', 'rss', 'search']
[menu]
[[menu.shortcuts]]
identifier = 'ds'
name = "<i class='fa-fw fas fa-code-branch'></i> certwarden-deploy Git Repository"
url = 'https://code.lila.network/adoralaura/certwarden-deploy'
weight = 10
[[menu.shortcuts]]
name = "<i class='fa-fw fas fa-arrow-up-right-from-square'></i> CertWarden GitHub"
url = 'showcase/'
weight = 11
[[menu.shortcuts]]
identifier = 'hugodoc'
name = "<i class='fa-fw fas fa-arrow-up-right-from-square'></i> CertWarden Website"
url = 'https://gohugo.io/'
weight = 20
# [[menu.shortcuts]]
# name = "<i class='fa-fw fas fa-bullhorn'></i> Credits"
# url = 'more/credits/'
# weight = 30
#
# [[menu.shortcuts]]
# name = "<i class='fa-fw fas fa-tags'></i> Tags"
# url = 'tags/'
# weight = 40

93
docs/content/_index.md Normal file
View file

@ -0,0 +1,93 @@
---
title: CertWarden-Deploy
type: docs
---
[CertWarden](https://www.certwarden.com/) is a self-hosted Centralized ACME Certificate Management platform. With it you can manage and aquire Let's Encrypt certificates.
However, to deploy them to your hosts, for now there only was a docker client, and that was too bloated for me.
So I built `certwarden-deploy`, a dependency-less binary that can run via crontab/systemd timers and that can fetch new certificates and run actions after new certificates got rolled out.
## Quick Start
Installation of the required CertWarden instance is out of scope of this documentation. For detailed instructions regarding CertWarden, please visit [it's documentation](https://www.certwarden.com/docs/introduction/)
To quickly get started with `certwarden-deploy`, just download the binary...
```shell
# this downloads certwarden-deploy version 0.2.2
# to /usr/local/bin/certwarden-deploy
sudo wget https://code.lila.network/adoralaura/certwarden-deploy/releases/download/0.2.2/certwarden-deploy-linux-amd64 -O /usr/local/bin/certwarden-deploy
sudo chmod +x /usr/local/bin/certwarden-deploy
```
... fill out the config file...
`vi /etc/certwarden-deploy/config.yaml`
```yaml
# Base URL of the CertWarden instance
# required
base_url: "https://certwarden.example.com"
# Set this to true if your CertWarden instance does not have a publicly trusted
# TLS certificate (e.g. it has a self signed one)
# default is false
disable_certificate_validation: false
# define all managed certificates here
certificates:
# name is a unique identifier that must start and end with an alphanumeric character,
# and can contain the following characters: a-zA-Z0-9._-
# required
- name: test-certificate.example.com
# Contains the API-Key to fetch the certificate from the server
# required
cert_secret: examplekey_notvalid_hrzjGDDw8z
# path where to save the certificate
# required
cert_path: "/path/to/test-certificate.example.com-cert.pem"
# Contains the API-Key to fetch the private key from the server
# required
key_secret: examplekey_notvalid_hrzbbDDw8z
# path where to save the private key
# required
key_path: "/path/to/test-certificate.example.com-key.pem"
# action to run when certificate was updated or --force is on
action: "/usr/bin/systemd reload caddy"
```
... and run it!
```shell
certwarden-deploy -v
```
## Contributing
I use my own [Forgejo](https://forgejo.org/) Instance [code.lila.network](https://code.lila.network) to manage issues, pull requests and CI/CD.
* If you have a trivial fix or improvement, go ahead and send a diff to the maintainer(s) of this repository (see
[MAINTAINERS.md](https://code.lila.network/adoralaura/certwarden-deploy/src/branch/main/MAINTAINERS.md)).
* If you plan to do something more involved, first please [send me a mail](mailto:dev@lauka.net?subject=%5Bcertwarden-deploy%5D) so I can create an account for you.
### Non-development Contibutions
The best way to help without speaking a lot of Go would be to share your
configuration, setup, and tips. If you have something
that works and is not in the repository, please pay it forward and
share what works.
## Changelog
You can find the Changelog here: [Changelog](https://code.lila.network/adoralaura/certwarden-deploy/src/branch/main/CHANGELOG.md)
## License
`certwarden-deploy` is available under the MIT license. See the [License page](/license/) for more info.

View file

@ -0,0 +1,85 @@
---
title: Configuration
weight: 20
---
This document describes how to configure `certwarden-deploy` and which certificates should be managed by it. The configuration file uses the [YAML format](https://yaml.org/) for a human-readable and easy-to-maintain structure.
## certwarden-deploy CLI Options
```plaintext
$ ./certwarden-deploy --help
certwarden-deploy is a CLI utility to deploy certificates managed by CertWarden.
Configuration is handled by a single YAML file, so you can get started quickly.
For more information on how to configure this tool, visit the docs at https://certwarden-deploy.adora.codes
Usage:
certwarden-deploy [flags]
Flags:
-c, --config string Path to config file (default is /etc/certwarden-deploy/config.yaml) (default "/etc/certwarden-deploy/config.yaml")
-d, --dry-run Just show the would-be changes without changing the file system (turns on verbose logging)
-f, --force Force overwriting and execution action to occur, regardless if certificate already exists
-h, --help help for certwarden-deploy
-q, --quiet Disable any logging (if both -q and -v are set, quiet wins)
-v, --verbose Enable verbose logging
--version version for certwarden-deploy
```
## Configuration File Options
`base_url` (required):
This string specifies the base URL of your CertWarden instance.
`disable_certificate_validation` (optional, default: false):
This boolean flag indicates whether to disable certificate validation for the CertWarden instance. Set this to true only if your CertWarden instance uses a self-signed certificate and you trust it explicitly. **Disabling validation weakens security, so use it with caution.**
`certificates:` (required):
This is a list that defines each certificate to be managed.
Each certificate definition is a nested YAML block with the following properties:
Each certificate configuration consists of:
`name` (required):
This string is a unique identifier for the certificate and must be the same as in you CertWarden instance.
It must start and end with an alphanumeric character and can contain letters (a-zA-Z), numbers (0-9), underscore (_), hyphen (-), and period (.).
`cert_secret` (required):
This string holds the API key used to fetch the certificate data from the CertWarden server.
`cert_path` (required):
This string defines the file path where the downloaded certificate will be saved.
`key_secret` (required):
This string holds the API key used to fetch the private key data from the CertWarden server.
`key_path` (required):
This string defines the file path where the downloaded private key will be saved.
`action` (optional):
This string specifies a command to run after a certificate is updated or when the --force flag is used during execution.
The example uses a systemd reload command for the popular reverse proxy named "caddy".
Example Configuration:
```yaml
# Base URL of the CertWarden instance
base_url: "https://certwarden.example.com"
# Disable certificate validation (not recommended for production)
disable_certificate_validation: false
# Define all managed certificates here
certificates:
- name: test-certificate.example.com
cert_secret: examplekey_notvalid_hrzjGDDw8z # Replace with your actual key
cert_path: "/path/to/test-certificate.example.com-cert.pem"
key_secret: examplekey_notvalid_hrzbbDDw8z # Replace with your actual key
key_path: "/path/to/test-certificate.example.com-key.pem"
action: "/usr/bin/systemctl reload caddy"
```
Use code with caution.
## Notes
- This documentation assumes you have a basic understanding of YAML syntax. Resources for learning YAML are readily available online.
- Replace placeholder values like `examplekey_notvalid_hrzjGDDw8z` with your actual API keys.

View file

@ -0,0 +1,30 @@
---
title: Installation
weight: 10
---
## Prerequisites
Before building the project, ensure you have the following installed:
- make: A build automation tool
- Go: Version 1.22 or later
## Building the Project from Source
To build the project, first clone the projects git repository, then navigate to the project's root directory and run the following command:
```shell
make build
```
This command will generate the `certwarden-deploy` binary in the `bin/` folder.
## Getting pre-built Binaries
You can also get pre-built binaries from the [releases page](https://code.lila.network/adoralaura/certwarden-deploy/releases). Make sure you get the binaries fitting your architecture!
## Setting up automatic Certificate Renewals
Although not required for `certwarden-deploy` to work, it's highly rrecommended to set up automatic renewals for `certwarden-deploy`, so that you don't need to worry about rolling out your certificates every time they get renewed by CertWarden.
To do that, there are example `systemd` Service and Timer files included in the `examples/` directory of the `certwarden-deploy` repository.
Please make sure to customize them to your requirements (path to `certwarden-deploy` binary, user and group, execution interval...) and then drop them into the `/etc/systemd/system/` directory, then enable the timer with `systemctl enable --now certwarden-deploy.timer`
If you kept the example schedule, `certwarden-deploy` will run every saturday at ~4am.

33
docs/content/license.md Normal file
View file

@ -0,0 +1,33 @@
---
title: License
weight: 99
---
## Documentation
This documentation is available under the [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
## Source Code
The source code of `certwarden-deploy` is available under the MIT license:
```plaintext
MIT License
Copyright © 2024 Adora Laura Kalb <dev@lauka.net>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

5
docs/go.mod Normal file
View file

@ -0,0 +1,5 @@
module code.lila.network/adoralaura/certwarden-deploy-docs
go 1.22.2
require github.com/McShelby/hugo-theme-relearn v0.0.0-20240721222908-7aec99b38dc2 // indirect

2
docs/go.sum Normal file
View file

@ -0,0 +1,2 @@
github.com/McShelby/hugo-theme-relearn v0.0.0-20240721222908-7aec99b38dc2 h1:022HGVq2CBuTftLgNRiU3rxqh+w3M3ZcschnXbjgomc=
github.com/McShelby/hugo-theme-relearn v0.0.0-20240721222908-7aec99b38dc2/go.mod h1:mKQQdxZNIlLvAj8X3tMq+RzntIJSr9z7XdzuMomt0IM=

View file

@ -0,0 +1,2 @@
{{ .Params.headingPost | safeHTML }}
<script defer src="https://esseles.adora.codes/script.js" data-website-id="fe4ec517-25b2-4e0d-b502-6bd3a7420849"></script>

View file

@ -0,0 +1 @@
<img src="/images/logo.svg"/>

BIN
docs/static/images/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
docs/static/images/logo.svg vendored Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 3246 924" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="BG"></g><rect id="Artboard1" x="0" y="0" width="3245.03" height="923.786" style="fill:none;"/><rect x="142.173" y="142.589" width="513.711" height="659.756" style="fill:#96c8d1;fill-rule:nonzero;"/><rect x="207.956" y="204.786" width="380.629" height="533.018" style="fill:#cde9e3;fill-rule:nonzero;"/><path d="M518.389,552.593l-55.026,-0l0,109.913l27.444,-24.548l27.582,24.548l-0,-109.913Z" style="fill:#96c96c;fill-rule:nonzero;"/><path d="M536.731,543.491c-0,25.237 -20.549,45.785 -45.924,45.785c-25.375,0 -45.924,-20.548 -45.924,-45.785c0,-25.376 20.549,-45.786 45.924,-45.786c25.375,-0 45.924,20.548 45.924,45.786Z" style="fill:#fbba22;fill-rule:nonzero;"/><path d="M655.884,124.385l-513.711,0c-10.067,0 -18.342,8.275 -18.342,18.342l0,659.756c0,10.068 8.275,18.342 18.342,18.342l513.711,0c10.067,0 18.342,-8.274 18.342,-18.342l-0,-659.894c-0,-10.067 -8.275,-18.204 -18.342,-18.204Zm-18.342,659.757l-477.027,-0l0,-623.211l477.027,0l0,623.211Z" style="fill:#211f1e;fill-rule:nonzero;"/><path d="M206.439,747.458l385.179,-0c5.103,-0 9.24,-4.137 9.24,-9.102l0,-531.501c0,-5.103 -4.137,-9.102 -9.24,-9.102l-385.179,-0c-5.103,-0 -9.24,4.137 -9.24,9.102l-0,531.501c-0,4.965 4.137,9.102 9.24,9.102Zm9.102,-531.501l366.838,-0l-0,513.159l-366.838,-0l-0,-513.159Z" style="fill:#211f1e;fill-rule:nonzero;"/><path d="M341.728,600.723l-55.026,-0c-5.103,-0 -9.24,4.137 -9.24,9.102c-0,5.102 4.137,9.102 9.24,9.102l55.026,-0c5.102,-0 9.239,-4.138 9.239,-9.102c-0.138,-4.965 -4.275,-9.102 -9.239,-9.102Zm-61.922,-201.899l238.445,0c5.103,0 9.24,-4.137 9.24,-9.102c-0,-5.102 -4.137,-9.102 -9.24,-9.102l-238.445,0c-5.102,0 -9.239,4.137 -9.239,9.102c-0,4.965 4.137,9.102 9.239,9.102Zm119.154,36.684l-119.292,-0c-5.102,-0 -9.239,4.137 -9.239,9.102c-0,5.103 4.137,9.102 9.239,9.102l119.292,-0c5.102,-0 9.24,-4.137 9.24,-9.102c-0,-5.103 -4.138,-9.102 -9.24,-9.102Zm-119.154,-91.71l238.445,0c5.103,0 9.24,-4.137 9.24,-9.102c-0,-5.102 -4.137,-9.102 -9.24,-9.102l-238.445,0c-5.102,0 -9.239,4.138 -9.239,9.102c-0,4.965 4.137,9.102 9.239,9.102Zm0,-54.887l238.445,-0c5.103,-0 9.24,-4.138 9.24,-9.102c-0,-5.103 -4.137,-9.102 -9.24,-9.102l-238.445,-0c-5.102,-0 -9.239,4.137 -9.239,9.102c-0,4.964 4.137,9.102 9.239,9.102Zm201.899,201.485c-30.34,-0 -55.026,24.685 -55.026,55.025c0,16.274 7.172,30.754 18.342,40.821l0,78.195c0,3.585 2.069,6.895 5.379,8.412c3.31,1.517 7.171,0.828 9.929,-1.517l21.376,-19.169l21.376,19.169c1.655,1.517 3.861,2.345 6.068,2.345c1.241,-0 2.482,-0.276 3.723,-0.828c3.31,-1.517 5.379,-4.689 5.379,-8.412l-0,-78.195c11.171,-10.067 18.342,-24.685 18.342,-40.821c0.138,-30.34 -24.548,-55.025 -54.888,-55.025Zm18.342,153.492l-12.274,-10.895c-3.448,-3.171 -8.826,-3.171 -12.274,0l-12.274,10.895l0,-46.751c5.792,2.069 11.86,3.172 18.342,3.172c6.482,0 12.55,-1.103 18.342,-3.172l0,46.751l0.138,0Zm-18.342,-61.783c-20.273,0 -36.684,-16.411 -36.684,-36.684c0,-20.272 16.411,-36.683 36.684,-36.683c20.273,-0 36.684,16.411 36.684,36.683c-0,20.273 -16.411,36.684 -36.684,36.684Z" style="fill:#211f1e;fill-rule:nonzero;"/><g transform="matrix(283.53,0,0,283.53,3117.47,579.164)"></g><text x="784.992px" y="579.164px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:283.53px;fill:#fff;">certwarden-deploy</text></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -17,12 +17,20 @@ certificates:
# Contains the API-Key to fetch the certificate from the server
# required
cert_secret: examplekey_notvalid_hrzjGDDw8z
api_key: examplekey_notvalid_hrzjGDDw8z
# path where to save the certificate
# required
cert_path: "/path/to/test-certificate.example.com-cert.pem"
# Contains the API-Key to fetch the private key from the server
# required
key_secret: examplekey_notvalid_hrzbbDDw8z
# path where to save the private key
# required
key_path: "/path/to/test-certificate.example.com-key.pem"
# action to run when certificate was updated or --force is on
action: "/usr/bin/systemd reload caddy"
# path where to save the certificate
# required
file_path: "/path/to/test-certificate.example.com-cert.pem"

3
go.mod
View file

@ -8,11 +8,8 @@ require (
)
require (
github.com/getsentry/sentry-go v0.28.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)

6
go.sum
View file

@ -1,7 +1,5 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k=
github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -16,10 +14,6 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -17,133 +17,203 @@ import (
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
"code.lila.network/adoralaura/certwarden-deploy/internal/constants"
"github.com/getsentry/sentry-go"
)
func HandleCertificates(logger *slog.Logger, config *configuration.ConfigFileData) {
for _, cert := range config.Certificates {
certBytes, err := getCertFromServer(
certInfos := GenericCertificate{
Name: cert.Name,
FilePath: cert.CertificatePath,
Secret: cert.CertificateSecret,
IsKey: false,
}
keyInfos := GenericCertificate{
Name: cert.Name,
FilePath: cert.KeyPath,
Secret: cert.KeySecret,
IsKey: true,
}
// Rollout Certificate
certOnDiskChanged, err := certInfos.Rollout(logger, config.BaseURL, config.DisableCertificateValidation)
if err != nil {
logger.Error(
"Failed to roll out Certificate", "path",
certInfos.FilePath, "name", cert.Name, "error", err,
)
continue
}
// Rollout Key
keyOnDiskChanged, err := keyInfos.Rollout(logger, config.BaseURL, config.DisableCertificateValidation)
if err != nil {
logger.Error(
"Failed to roll out Key", "path",
keyInfos.FilePath, "name", cert.Name, "error", err,
)
continue
}
// if cert OR key changed OR --force
if (certOnDiskChanged || keyOnDiskChanged) || configuration.Force {
if configuration.Force {
logger.Info("Forcing file system change due to --force", "name", cert.Name)
}
err = handleCertificateAction(cert.Action)
if err != nil {
logger.Error("Failed to execute post-rollout action", "name", cert.Name, "error", err)
}
}
}
}
// Rollout handles getting the certificate/key data from the
// server and writing it to disk if the data differs.
//
// Returns error on error, true if certificate action needs to be executed, false if not
func (c *GenericCertificate) Rollout(logger *slog.Logger, baseUrl string, skipInsecure bool) (bool, error) {
err := c.fetchFromServer(
logger,
cert.Name,
cert.ApiKey,
config.BaseURL,
config.DisableCertificateValidation,
baseUrl,
skipInsecure,
)
if err != nil {
logger.Error("Failed to get certificate from server", "cert-id", cert.Name, "error", err)
return
return false, fmt.Errorf("failed to get certificate from server: %w", err)
}
certIsDifferent, err := checkCertIsDifferent(logger, cert.FilePath, certBytes)
fileNeedsRollout, err := c.needsRollout(logger)
if err != nil {
logger.Error("failed to handle certificate", "cert-id", cert.Name, "error", err)
return
return false, fmt.Errorf("failed to check certificate on disk: %w", err)
}
if certIsDifferent || configuration.Force {
if fileNeedsRollout || configuration.Force {
if configuration.Force {
logger.Info("Forcing file system change due to --force", "cert-id", cert.Name)
logger.Info("Forcing file system change due to --force", "name", c.Name)
}
err = updateCertOnFS(logger, cert.FilePath, certBytes)
err = c.writeToDisk(logger)
if err != nil {
logger.Error("failed to handle certificate", "cert-id", cert.Name, "error", err)
return
return false, fmt.Errorf("failed to handle certificate: %w", err)
}
if configuration.Force {
logger.Info("Forcing file system change due to --force", "cert-id", cert.Name)
}
err = handleCertificateAction(cert)
if err != nil {
logger.Error("post certificate change command failed", "cert-id", cert.Name, "error", err)
if fileNeedsRollout {
logger.Info("New file deployed", "path", c.FilePath)
return true, nil
} else if configuration.Force {
logger.Info("File deployed", "path", c.FilePath)
return true, nil
} else {
logger.Info("File not changed, skipping...", "path", c.FilePath)
return false, nil
}
}
logger.Info("Certificate updated successfully", "cert-id", cert.Name)
}
}
func getCertFromFile(path string) ([]byte, error) {
filebytes, err := os.ReadFile(path)
// readFromDisk reads file data from disk and populates the data []byte field.
//
// Returns error or nil on success
func (c *GenericCertificate) readFromDisk() error {
filebytes, err := os.ReadFile(c.FilePath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return []byte{}, err
return err
} else {
return []byte{}, fmt.Errorf("failed to read certificate file on disk: %w", err)
return fmt.Errorf("failed to read file from disk: %w", err)
}
}
return filebytes, nil
}
func checkCertIsDifferent(logger *slog.Logger, path string, data []byte) (bool, error) {
filebytes, err := getCertFromFile(path)
c.diskBytes = filebytes
return nil
}
// needsRollout checks the data []bytes against the data on disk.
//
// Returns true if file needs rollout, false if not
func (c *GenericCertificate) needsRollout(logger *slog.Logger) (bool, error) {
err := c.readFromDisk()
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return true, nil
} else {
return false, fmt.Errorf("failed to compare certificates: %w", err)
return false, fmt.Errorf("failed to compare data to file on disk: %w", err)
}
}
existingSha256 := sha256.Sum256(filebytes)
newSha256 := sha256.Sum256(data)
diskHash := sha256.Sum256(c.diskBytes)
serverHash := sha256.Sum256(c.serverBytes)
sumsAreDifferent := existingSha256 != newSha256
if sumsAreDifferent {
logger.Debug("Certificate on file differs from the certificate on the server", "cert-path", path)
hashesAreDifferent := diskHash != serverHash
if hashesAreDifferent {
logger.Debug("File on disk differs from server source", "path", c.FilePath)
} else {
logger.Debug("Certificate on file is identical to the certificate on the server", "cert-path", path)
logger.Debug("File on disk is identical to server source", "path", c.FilePath)
}
return sumsAreDifferent, nil
return hashesAreDifferent, nil
}
func updateCertOnFS(logger *slog.Logger, path string, data []byte) error {
// writeToDisk flushes the certificate data to disk.
//
// Returns error or nil on success.
func (c *GenericCertificate) writeToDisk(logger *slog.Logger) error {
if configuration.DryRun {
logger.Debug("DRY-RUN: writing certificate data to file", "cert-path", path)
logger.Debug("DRY-RUN: writing data to file", "path", c.FilePath)
return nil
}
file, err := os.Create(path)
file, err := os.Create(c.FilePath)
if err != nil {
return fmt.Errorf("failed to open certificate for writing: %w", err)
return fmt.Errorf("failed to open file for writing: %w", err)
}
defer func(l *slog.Logger) {
if err := file.Close(); err != nil {
l.Error("failed to close file", "file-path", path, "error", err)
l.Error("failed to close file", "path", c.FilePath, "error", err)
}
}(logger)
w := bufio.NewWriter(file)
if _, err := w.Write(data); err != nil {
return fmt.Errorf("failed to write certificate data to file: %w", err)
if _, err := w.Write(c.serverBytes); err != nil {
return fmt.Errorf("failed to write data to file: %w", err)
}
if err = w.Flush(); err != nil {
return fmt.Errorf("failed to flush certificate data to file: %w", err)
return fmt.Errorf("failed to flush data to file: %w", err)
}
logger.Debug("wrote certificate to file", "file-path", path)
logger.Debug("Successfully wrote to file", "path", c.FilePath)
return nil
}
func getCertFromServer(logger *slog.Logger, certName string, certKey string, baseUrl string, skipInsecure bool) ([]byte, error) {
url := baseUrl + constants.CertificateApiPath + certName
logger.Debug("Certificate request URL: " + url)
// fetchFromServer fetches the cert/key data from the CertWarden server and
// fills the serverBytes field.
//
// Returns error or nil on success.
func (c *GenericCertificate) fetchFromServer(logger *slog.Logger, baseUrl string, skipInsecure bool) error {
var url string
var fileType string
if c.IsKey {
url = baseUrl + constants.KeyApiPath + c.Name
fileType = "privatekey"
} else {
url = baseUrl + constants.CertificateApiPath + c.Name
fileType = "certificate"
}
logger.Debug("Data request URL: "+url, "file-type", fileType)
var transport http.RoundTripper
if skipInsecure {
logger.Debug("TLS Certificate Validation is disabled")
logger.Debug("Upstream Server TLS Certificate Validation is disabled")
transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
} else {
logger.Debug("TLS Certificate Validation is enabled")
logger.Debug("Upstream Server HTTP TLS Certificate Validation is enabled")
}
client := &http.Client{
@ -152,17 +222,15 @@ func getCertFromServer(logger *slog.Logger, certName string, certKey string, bas
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return []byte{}, fmt.Errorf("failed to prepare to request certificate from server: %w", err)
return fmt.Errorf("failed to prepare to request data from server: %w", err)
}
req.Header.Set("User-Agent", constants.UserAgent)
req.Header.Add(constants.ApiKeyHeaderName, certKey)
req.Header.Add(constants.ApiKeyHeaderName, c.Secret)
res, err := client.Do(req)
if err != nil {
e := fmt.Errorf("failed to request certificate from server: %w", err)
sentry.CaptureException(e)
return []byte{}, e
return fmt.Errorf("failed to request data from server: %w", err)
}
defer func(l *slog.Logger) {
@ -172,29 +240,26 @@ func getCertFromServer(logger *slog.Logger, certName string, certKey string, bas
}(logger)
if res.StatusCode == http.StatusUnauthorized {
logger.Error("API-Key for Certificate is invalid, skipping certificate!", "cert-id", certName)
return []byte{}, errors.New("API-Key invalid")
logger.Error("API-Key for request is invalid, skipping certificate!", "name", c.Name, "file-type", fileType)
return errors.New("API-Key invalid")
} else if res.StatusCode != http.StatusOK {
logger.Error("failed to get certificate from server", "cert-id", certName, "http-response", res.Status)
logger.Error("failed to get data from server", "name", c.Name, "http-response", res.Status, "file-type", fileType)
}
body, err := io.ReadAll(res.Body)
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
e := fmt.Errorf("failed to read certificate response from server: %w", err)
sentry.CaptureException(e)
return []byte{}, e
return fmt.Errorf("failed to read response from server: %w", err)
}
return body, nil
}
func handleCertificateAction(cert configuration.CertificateData) error {
if cert.Action == "" {
c.serverBytes = bodyBytes
return nil
}
action := strings.ReplaceAll(cert.Action, "{name}", cert.Name)
action = strings.ReplaceAll(action, "{path}", cert.FilePath)
// handleCertificateAction executes the user-defined action after successful certificate deployment
func handleCertificateAction(action string) error {
if action == "" {
return nil
}
sargs := strings.Split(action, " ")

View file

@ -1 +0,0 @@
package certificates

View file

@ -0,0 +1,18 @@
package certificates
// GenericCertificate is a generic container to enable us to
// handle both certificates and keys with one function
type GenericCertificate struct {
Name string
FilePath string
Secret string
// True if key, false if certificate
IsKey bool
// Bytes fetched from the server
serverBytes []byte
// Bytes fetched from disk
diskBytes []byte
}

View file

@ -7,7 +7,6 @@ import (
"code.lila.network/adoralaura/certwarden-deploy/internal/certificates"
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
"code.lila.network/adoralaura/certwarden-deploy/internal/constants"
"code.lila.network/adoralaura/certwarden-deploy/internal/errlog"
"code.lila.network/adoralaura/certwarden-deploy/internal/logger"
"github.com/spf13/cobra"
)
@ -31,13 +30,15 @@ func handleRootCmd(cmd *cobra.Command, args []string) {
slog.Error("failed to initialize config", "error", err)
os.Exit(1)
}
log := logger.InitializeLogger()
err = errlog.SetupSentry(log, config.Sentry.DSN)
if err != nil {
slog.Error("failed to initialize sentry", "error", err)
}
log := logger.Initialize()
config.SubstituteKeys(log)
configuration.ValidateConfig(log, *config)
validation := config.IsValid()
if validation.HasMessages() {
validation.Print(log)
slog.Error("The configuration file has errors! Application cannot start unless all errors are corrected!")
os.Exit(1)
}
certificates.HandleCertificates(log, config)
}

View file

@ -1,5 +1,7 @@
package configuration
import "log/slog"
// Config file gets read into here
var Config *ConfigFileData
@ -22,18 +24,37 @@ var Force bool
type ConfigFileData struct {
BaseURL string `yaml:"base_url"`
DisableCertificateValidation bool `yaml:"disable_certificate_validation"`
Sentry SentryData `yaml:"sentry,omitempty"`
Certificates []CertificateData `yaml:"certificates"`
}
// Struct that holds the details of a single managed certificate
type CertificateData struct {
Name string `yaml:"name"`
ApiKey string `yaml:"api_key"`
CertificateSecret string `yaml:"cert_secret"`
CertificatePath string `yaml:"cert_path"`
KeySecret string `yaml:"key_secret"`
KeyPath string `yaml:"key_path"`
Action string `yaml:"action"`
FilePath string `yaml:"file_path"`
}
type SentryData struct {
DSN string `yaml:"dsn"`
type ConfigValidationError struct {
ErrorMessages []string
}
func (e *ConfigValidationError) Error() string {
return "Configuration file has errors! Application cannot start unless the errors are corrected."
}
func (e *ConfigValidationError) Add(msg string) {
e.ErrorMessages = append(e.ErrorMessages, msg)
}
func (e *ConfigValidationError) HasMessages() bool {
return len(e.ErrorMessages) != 0
}
func (e *ConfigValidationError) Print(logger *slog.Logger) {
for _, line := range e.ErrorMessages {
logger.Error(line)
}
}

View file

@ -0,0 +1,17 @@
package configuration
import (
"log/slog"
"strings"
)
func (c *ConfigFileData) SubstituteKeys(logger *slog.Logger) {
for index, cert := range c.Certificates {
c.Certificates[index].CertificatePath = strings.ReplaceAll(cert.CertificatePath, "{name}", c.Certificates[index].Name)
c.Certificates[index].KeyPath = strings.ReplaceAll(cert.KeyPath, "{name}", c.Certificates[index].Name)
c.Certificates[index].Action = strings.ReplaceAll(cert.Action, "{name}", c.Certificates[index].Name)
c.Certificates[index].Action = strings.ReplaceAll(c.Certificates[index].Action, "{cert_path}", c.Certificates[index].CertificatePath)
c.Certificates[index].Action = strings.ReplaceAll(c.Certificates[index].Action, "{key_path}", c.Certificates[index].KeyPath)
}
}

View file

@ -0,0 +1,65 @@
package configuration
import (
"testing"
)
// TestStringSubstitutionWithPlaceholders tests the string substitution feature.
// It ensures that {name}, {cert_path} and {key_path} get substituted correctly.
func TestStringSubstitutionWithPlaceholders(t *testing.T) {
cert := CertificateData{
Name: "qwer",
CertificatePath: "/fake/path/{name}",
KeyPath: "/fake/path/{name}-key",
Action: "./fake action {cert_path} {key_path}",
}
cfg := ConfigFileData{
Certificates: []CertificateData{cert},
}
cfg.SubstituteKeys(nil)
if cfg.Certificates[0].CertificatePath != "/fake/path/qwer" {
t.Fail()
t.Logf(`CertificatePath = %q, want "/fake/path/qwer"`, cfg.Certificates[0].CertificatePath)
}
if cfg.Certificates[0].KeyPath != "/fake/path/qwer-key" {
t.Fail()
t.Logf(`KeyPath = %q, want "/fake/path/qwer-key"`, cfg.Certificates[0].KeyPath)
}
if cfg.Certificates[0].Action != "./fake action /fake/path/qwer /fake/path/qwer-key" {
t.Fail()
t.Logf(`Action = %q, want "./fake action /fake/path/qwer /fake/path/qwer-key"`, cfg.Certificates[0].Action)
}
}
// TestStringSubstitutionWithPlaceholders tests the string substitution feature.
// It ensures that if no substitutes are present, the config values are not changed.
func TestStringSubstitutionWithoutPlaceholders(t *testing.T) {
cert := CertificateData{
Name: "qwer",
CertificatePath: "/fake/path/asd",
KeyPath: "/fake/path/asdf-key",
Action: "./fake action abcd efgh",
}
cfg := ConfigFileData{
Certificates: []CertificateData{cert},
}
cfg.SubstituteKeys(nil)
if cfg.Certificates[0].CertificatePath != "/fake/path/asd" {
t.Fail()
t.Logf(`CertificatePath = %q, want "/fake/path/asd"`, cfg.Certificates[0].CertificatePath)
}
if cfg.Certificates[0].KeyPath != "/fake/path/asdf-key" {
t.Fail()
t.Logf(`KeyPath = %q, want "/fake/path/asdf-key"`, cfg.Certificates[0].KeyPath)
}
if cfg.Certificates[0].Action != "./fake action abcd efgh" {
t.Fail()
t.Logf(`Action = %q, want "./fake action abcd efgh"`, cfg.Certificates[0].Action)
}
}

View file

@ -1,45 +1,38 @@
package configuration
import (
"log/slog"
"os"
"regexp"
)
func ValidateConfig(logger *slog.Logger, config ConfigFileData) {
validationFailed := false
// IsValid tests if the config read from file has all required parameters set.
//
// Exits the app if errors are detected
func (c *ConfigFileData) IsValid() ConfigValidationError {
err := ConfigValidationError{}
if config.BaseURL == "" {
logger.Error(`Field 'base_url' in config file is required!`)
validationFailed = true
if c.BaseURL == "" {
err.Add(`Field 'base_url' in config file is required!`)
}
for _, cert := range config.Certificates {
for _, cert := range c.Certificates {
if cert.Name == "" {
cert.Name = "unnamed_certificate"
logger.Error(`Field 'name' for certificates cannot be blank!`)
validationFailed = true
err.Add(`Field 'name' for certificates cannot be blank!`)
}
if cert.ApiKey == "" {
logger.Error(`Field 'api_key' for certificate ` + cert.Name + " cannot be blank!")
validationFailed = true
if cert.CertificateSecret == "" {
err.Add(`Field 'cert_secret' for certificate ` + cert.Name + " cannot be blank!")
}
if cert.FilePath == "" {
logger.Error(`Field 'file_path' for certificate ` + cert.Name + " cannot be blank!")
validationFailed = true
if cert.CertificatePath == "" {
err.Add(`Field 'cert_path' for certificate ` + cert.Name + " cannot be blank!")
}
re := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
if !re.MatchString(cert.Name) {
logger.Error(`Field 'name' for certificate may only contain -_. and alphanumeric characters!`)
validationFailed = true
err.Add(`Field 'name' for certificate may only contain -_. and alphanumeric characters!`)
}
}
if validationFailed {
logger.Error("Config file has errors! Please fix errors above! Exiting...", "config-path", ConfigFile)
os.Exit(1)
}
return err
}

View file

@ -1,6 +1,9 @@
package constants
const Version = "0.1.1"
var Version string
var UserAgent = "certwarden-deploy/" + Version + " +https://code.lila.network/adoralaura/certwarden-deploy"
const CertificateApiPath = "/certwarden/api/v1/download/certificates/"
const KeyApiPath = "/certwarden/api/v1/download/privatekeys/"
const ApiKeyHeaderName = "X-API-Key"
const UserAgent = "certwarden-deploy/" + Version + " +https://code.lila.network/adoralaura/certwarden-deploy"

View file

@ -1,27 +0,0 @@
package errlog
import (
"log/slog"
"github.com/getsentry/sentry-go"
)
func SetupSentry(logger *slog.Logger, dsn string) error {
if dsn == "" {
return nil
}
err := sentry.Init(sentry.ClientOptions{
Dsn: dsn,
// Set TracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production,
TracesSampleRate: 1.0,
})
if err != nil {
logger.Error("failed to set up sentry")
}
// Flush buffered events before the program terminates.
return nil
}

View file

@ -8,13 +8,12 @@ import (
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
)
func InitializeLogger() *slog.Logger {
// Initialize initializes a *slog.Logger with the right log level and options.
func Initialize() *slog.Logger {
logLevel := slog.LevelInfo
sourceLogging := false
if configuration.VerboseLogging {
logLevel = slog.LevelDebug
sourceLogging = true
}
if configuration.QuietLogging {
logLevel = slog.LevelError
@ -25,7 +24,6 @@ func InitializeLogger() *slog.Logger {
opts := &slog.HandlerOptions{
Level: logLevel,
AddSource: sourceLogging,
}
handler := slog.NewTextHandler(os.Stdout, opts)

11
main.go
View file

@ -1,11 +0,0 @@
/*
Copyright © 2024 Laura Kalb <dev@lauka.net>
The code of this project is available under the MIT license. See the LICENSE file for more info.
*/
package main
import cmd "code.lila.network/adoralaura/certwarden-deploy/cmd/certwarden-deploy"
func main() {
cmd.Execute()
}