Compare commits
39 commits
Author | SHA1 | Date | |
---|---|---|---|
0cea3f282b | |||
527c038f02 | |||
7098a6c5eb | |||
3a0a739fae | |||
146a2800cb | |||
20cbb33778 | |||
6f20779714 | |||
b8ec72877d | |||
05a536a17d | |||
8d4f92f7be | |||
eb367df0f9 | |||
abc1ef0a79 | |||
2fe2b980be | |||
f40bdd5cbf | |||
fcdc147dd2 | |||
74c32b87b9 | |||
dd104386a7 | |||
5c80f88608 | |||
e46d924907 | |||
345b2b10f7 | |||
6c2b04e6b9 | |||
b988e4cf51 | |||
60aea576dc | |||
5e0e263ff8 | |||
08cb6e27bc | |||
9ee54b6c2a | |||
5a6e51463a | |||
99c5a497f0 | |||
4997114458 | |||
d1ac68c4f7 | |||
1746653453 | |||
29af4dada3 | |||
f0ad7c021e | |||
b1626cd198 | |||
102fc2a056 | |||
62803d451d | |||
8ff8a63299 | |||
7c7fe0ba7c | |||
3821477c34 |
40 changed files with 880 additions and 215 deletions
41
.gitignore
vendored
41
.gitignore
vendored
|
@ -1,31 +1,28 @@
|
||||||
# ---> Go
|
# Allowlisting gitignore template for GO projects prevents us
|
||||||
# If you prefer the allow list template instead of the deny list, see community template:
|
# from adding various unwanted local files, such as generated
|
||||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
# files, developer configurations or IDE-specific files etc.
|
||||||
#
|
#
|
||||||
# Binaries for programs and plugins
|
# Recommended: Go.AllowList.gitignore
|
||||||
*.exe
|
|
||||||
*.exe~
|
|
||||||
*.dll
|
|
||||||
*.so
|
|
||||||
*.dylib
|
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Ignore everything
|
||||||
*.test
|
*
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# But not these files...
|
||||||
*.out
|
!/.gitignore
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
!*.go
|
||||||
# vendor/
|
!go.sum
|
||||||
|
!go.mod
|
||||||
|
|
||||||
# Go workspace file
|
!examples/*
|
||||||
go.work
|
|
||||||
|
|
||||||
bin/
|
!*.md
|
||||||
|
!LICENSE
|
||||||
|
|
||||||
examples/testing/
|
!Makefile
|
||||||
|
|
||||||
*.yaml
|
# Woodpecker CI
|
||||||
!examples/*.yaml
|
!.woodpecker/*
|
||||||
|
|
||||||
test/
|
# ...even if they are in subdirectories
|
||||||
|
!*/
|
||||||
|
|
24
.vscode/launch.json
vendored
Normal file
24
.vscode/launch.json
vendored
Normal 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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -20,13 +20,13 @@ steps:
|
||||||
- APP_NAME=certwarden-deploy
|
- APP_NAME=certwarden-deploy
|
||||||
- FORGE=https://code.lila.network
|
- FORGE=https://code.lila.network
|
||||||
commands:
|
commands:
|
||||||
- apk add --update --no-cache xz curl jq
|
- apk add --update --no-cache xz curl jq make git
|
||||||
- go mod download
|
- make build
|
||||||
- go build -o output/$APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM} main.go
|
- cd bin/
|
||||||
- cd output
|
- mv $APP_NAME $APP_NAME-${GOOS}-${GOARCH}${GOARM}
|
||||||
- xz --keep --compress $APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM}
|
- xz --keep --compress $APP_NAME-${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-${GOOS}-${GOARCH}${GOARM} >> $APP_NAME-${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
|
- 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" \
|
export RELEASE_ID=`curl --location "$FORGE/api/v1/repos/$CI_REPO/releases?limit=10" \
|
||||||
--header 'Accept: application/json' -s -S \
|
--header 'Accept: application/json' -s -S \
|
||||||
|
@ -35,23 +35,23 @@ steps:
|
||||||
curl --location "$FORGE/api/v1/repos/$CI_REPO/releases/$RELEASE_ID/assets" \
|
curl --location "$FORGE/api/v1/repos/$CI_REPO/releases/$RELEASE_ID/assets" \
|
||||||
--header "Authorization: token $FORGEJO_APIKEY" \
|
--header "Authorization: token $FORGEJO_APIKEY" \
|
||||||
--header 'Content-Type: multipart/form-data' -s -S \
|
--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
|
--fail-with-body
|
||||||
- |-
|
- |-
|
||||||
curl --location "$FORGE/api/v1/repos/$CI_REPO/releases/$RELEASE_ID/assets" \
|
curl --location "$FORGE/api/v1/repos/$CI_REPO/releases/$RELEASE_ID/assets" \
|
||||||
--header "Authorization: token $FORGEJO_APIKEY" \
|
--header "Authorization: token $FORGEJO_APIKEY" \
|
||||||
--header 'Content-Type: multipart/form-data' -s -S \
|
--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
|
--fail-with-body
|
||||||
- |-
|
- |-
|
||||||
curl --location "$FORGE/api/v1/repos/$CI_REPO/releases/$RELEASE_ID/assets" \
|
curl --location "$FORGE/api/v1/repos/$CI_REPO/releases/$RELEASE_ID/assets" \
|
||||||
--header "Authorization: token $FORGEJO_APIKEY" \
|
--header "Authorization: token $FORGEJO_APIKEY" \
|
||||||
--header 'Content-Type: multipart/form-data' -s -S \
|
--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
|
--fail-with-body
|
||||||
- |-
|
- |-
|
||||||
curl --location "$FORGE/api/v1/repos/$CI_REPO/releases/$RELEASE_ID/assets" \
|
curl --location "$FORGE/api/v1/repos/$CI_REPO/releases/$RELEASE_ID/assets" \
|
||||||
--header "Authorization: token $FORGEJO_APIKEY" \
|
--header "Authorization: token $FORGEJO_APIKEY" \
|
||||||
--header 'Content-Type: multipart/form-data' -s -S \
|
--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
|
--fail-with-body
|
||||||
|
|
38
.woodpecker/deploy-docs.yml
Normal file
38
.woodpecker/deploy-docs.yml
Normal 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
|
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -7,7 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [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
|
- 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.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
|
[0.1.0]: https://code.lila.network/adoralaura/certwarden-deploy/releases/tag/0.1.0
|
||||||
|
|
17
CONTRIBUTING.md
Normal file
17
CONTRIBUTING.md
Normal 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
1
MAINTAINERS.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
* Adora Laura Kalb <dev@lauka.net> @adoralaura
|
13
Makefile
Normal file
13
Makefile
Normal 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)
|
75
README.md
75
README.md
|
@ -2,10 +2,81 @@
|
||||||
![status-badge](https://ci.lila.network/api/badges/22/status.svg)
|
![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)
|
[![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
|
## Changelog
|
||||||
You can find the Changelog here: [Changelog](https://code.lila.network/adoralaura/certwarden-deploy/src/branch/main/CHANGELOG.md)
|
You can find the Changelog here: [Changelog](https://code.lila.network/adoralaura/certwarden-deploy/src/branch/main/CHANGELOG.md)
|
||||||
|
|
||||||
## License
|
## 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.
|
||||||
|
|
|
@ -2,25 +2,20 @@
|
||||||
Copyright © 2024 Laura Kalb <dev@lauka.net>
|
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.
|
The code of this project is available under the MIT license. See the LICENSE file for more info.
|
||||||
*/
|
*/
|
||||||
package cmd
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/cli"
|
"code.lila.network/adoralaura/certwarden-deploy/internal/cli"
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
|
"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.
|
func main() {
|
||||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
|
||||||
func Execute() {
|
|
||||||
err := cli.RootCmd.Execute()
|
err := cli.RootCmd.Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer sentry.Flush(2 * time.Second)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
15
docs/.gitignore
vendored
Normal file
15
docs/.gitignore
vendored
Normal 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
9
docs/LICENSE
Normal 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
1
docs/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# certwarden-deploy Documentation
|
5
docs/archetypes/default.md
Normal file
5
docs/archetypes/default.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
+++
|
||||||
|
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||||
|
date = {{ .Date }}
|
||||||
|
draft = true
|
||||||
|
+++
|
54
docs/config.toml
Normal file
54
docs/config.toml
Normal 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
93
docs/content/_index.md
Normal 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.
|
85
docs/content/configuration.md
Normal file
85
docs/content/configuration.md
Normal 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.
|
30
docs/content/installation.md
Normal file
30
docs/content/installation.md
Normal 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
33
docs/content/license.md
Normal 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
5
docs/go.mod
Normal 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
2
docs/go.sum
Normal 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=
|
2
docs/layouts/partials/heading-post.html
Normal file
2
docs/layouts/partials/heading-post.html
Normal 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>
|
1
docs/layouts/partials/logo.html
Normal file
1
docs/layouts/partials/logo.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<img src="/images/logo.svg"/>
|
BIN
docs/static/images/favicon.ico
vendored
Normal file
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
1
docs/static/images/logo.svg
vendored
Normal 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 |
|
@ -17,12 +17,20 @@ certificates:
|
||||||
|
|
||||||
# Contains the API-Key to fetch the certificate from the server
|
# Contains the API-Key to fetch the certificate from the server
|
||||||
# required
|
# 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 to run when certificate was updated or --force is on
|
||||||
action: "/usr/bin/systemd reload caddy"
|
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
3
go.mod
|
@ -8,11 +8,8 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/getsentry/sentry-go v0.28.1 // indirect
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/kr/pretty v0.3.1 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // 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
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||||
)
|
)
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -1,7 +1,5 @@
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
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/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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
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/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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
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 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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
|
@ -17,133 +17,203 @@ import (
|
||||||
|
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
|
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/constants"
|
"code.lila.network/adoralaura/certwarden-deploy/internal/constants"
|
||||||
"github.com/getsentry/sentry-go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleCertificates(logger *slog.Logger, config *configuration.ConfigFileData) {
|
func HandleCertificates(logger *slog.Logger, config *configuration.ConfigFileData) {
|
||||||
for _, cert := range config.Certificates {
|
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,
|
logger,
|
||||||
cert.Name,
|
baseUrl,
|
||||||
cert.ApiKey,
|
skipInsecure,
|
||||||
config.BaseURL,
|
|
||||||
config.DisableCertificateValidation,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to get certificate from server", "cert-id", cert.Name, "error", err)
|
return false, fmt.Errorf("failed to get certificate from server: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
certIsDifferent, err := checkCertIsDifferent(logger, cert.FilePath, certBytes)
|
fileNeedsRollout, err := c.needsRollout(logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to handle certificate", "cert-id", cert.Name, "error", err)
|
return false, fmt.Errorf("failed to check certificate on disk: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if certIsDifferent || configuration.Force {
|
if fileNeedsRollout || configuration.Force {
|
||||||
if 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 {
|
if err != nil {
|
||||||
logger.Error("failed to handle certificate", "cert-id", cert.Name, "error", err)
|
return false, fmt.Errorf("failed to handle certificate: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if configuration.Force {
|
|
||||||
logger.Info("Forcing file system change due to --force", "cert-id", cert.Name)
|
|
||||||
}
|
}
|
||||||
err = handleCertificateAction(cert)
|
if fileNeedsRollout {
|
||||||
if err != nil {
|
logger.Info("New file deployed", "path", c.FilePath)
|
||||||
logger.Error("post certificate change command failed", "cert-id", cert.Name, "error", err)
|
return true, nil
|
||||||
}
|
} else if configuration.Force {
|
||||||
}
|
logger.Info("File deployed", "path", c.FilePath)
|
||||||
|
return true, nil
|
||||||
logger.Info("Certificate updated successfully", "cert-id", cert.Name)
|
} else {
|
||||||
|
logger.Info("File not changed, skipping...", "path", c.FilePath)
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCertFromFile(path string) ([]byte, error) {
|
// readFromDisk reads file data from disk and populates the data []byte field.
|
||||||
filebytes, err := os.ReadFile(path)
|
//
|
||||||
|
// Returns error or nil on success
|
||||||
|
func (c *GenericCertificate) readFromDisk() error {
|
||||||
|
filebytes, err := os.ReadFile(c.FilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
return []byte{}, err
|
return err
|
||||||
} else {
|
} 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
|
|
||||||
|
c.diskBytes = filebytes
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkCertIsDifferent(logger *slog.Logger, path string, data []byte) (bool, error) {
|
// needsRollout checks the data []bytes against the data on disk.
|
||||||
filebytes, err := getCertFromFile(path)
|
//
|
||||||
|
// 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 err != nil {
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
return true, nil
|
return true, nil
|
||||||
} else {
|
} 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)
|
diskHash := sha256.Sum256(c.diskBytes)
|
||||||
newSha256 := sha256.Sum256(data)
|
serverHash := sha256.Sum256(c.serverBytes)
|
||||||
|
|
||||||
sumsAreDifferent := existingSha256 != newSha256
|
hashesAreDifferent := diskHash != serverHash
|
||||||
if sumsAreDifferent {
|
if hashesAreDifferent {
|
||||||
logger.Debug("Certificate on file differs from the certificate on the server", "cert-path", path)
|
logger.Debug("File on disk differs from server source", "path", c.FilePath)
|
||||||
} else {
|
} 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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create(path)
|
file, err := os.Create(c.FilePath)
|
||||||
if err != nil {
|
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) {
|
defer func(l *slog.Logger) {
|
||||||
if err := file.Close(); err != nil {
|
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)
|
}(logger)
|
||||||
|
|
||||||
w := bufio.NewWriter(file)
|
w := bufio.NewWriter(file)
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
if _, err := w.Write(c.serverBytes); err != nil {
|
||||||
return fmt.Errorf("failed to write certificate data to file: %w", err)
|
return fmt.Errorf("failed to write data to file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = w.Flush(); err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCertFromServer(logger *slog.Logger, certName string, certKey string, baseUrl string, skipInsecure bool) ([]byte, error) {
|
// fetchFromServer fetches the cert/key data from the CertWarden server and
|
||||||
url := baseUrl + constants.CertificateApiPath + certName
|
// fills the serverBytes field.
|
||||||
logger.Debug("Certificate request URL: " + url)
|
//
|
||||||
|
// 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
|
var transport http.RoundTripper
|
||||||
|
|
||||||
if skipInsecure {
|
if skipInsecure {
|
||||||
logger.Debug("TLS Certificate Validation is disabled")
|
logger.Debug("Upstream Server TLS Certificate Validation is disabled")
|
||||||
transport = &http.Transport{
|
transport = &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Debug("TLS Certificate Validation is enabled")
|
logger.Debug("Upstream Server HTTP TLS Certificate Validation is enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
|
@ -152,17 +222,15 @@ func getCertFromServer(logger *slog.Logger, certName string, certKey string, bas
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != 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.Set("User-Agent", constants.UserAgent)
|
||||||
req.Header.Add(constants.ApiKeyHeaderName, certKey)
|
req.Header.Add(constants.ApiKeyHeaderName, c.Secret)
|
||||||
|
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := fmt.Errorf("failed to request certificate from server: %w", err)
|
return fmt.Errorf("failed to request data from server: %w", err)
|
||||||
sentry.CaptureException(e)
|
|
||||||
return []byte{}, e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func(l *slog.Logger) {
|
defer func(l *slog.Logger) {
|
||||||
|
@ -172,30 +240,27 @@ func getCertFromServer(logger *slog.Logger, certName string, certKey string, bas
|
||||||
}(logger)
|
}(logger)
|
||||||
|
|
||||||
if res.StatusCode == http.StatusUnauthorized {
|
if res.StatusCode == http.StatusUnauthorized {
|
||||||
logger.Error("API-Key for Certificate is invalid, skipping certificate!", "cert-id", certName)
|
logger.Error("API-Key for request is invalid, skipping certificate!", "name", c.Name, "file-type", fileType)
|
||||||
return []byte{}, errors.New("API-Key invalid")
|
return errors.New("API-Key invalid")
|
||||||
} else if res.StatusCode != http.StatusOK {
|
} 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 {
|
if err != nil {
|
||||||
e := fmt.Errorf("failed to read certificate response from server: %w", err)
|
return fmt.Errorf("failed to read response from server: %w", err)
|
||||||
sentry.CaptureException(e)
|
|
||||||
return []byte{}, e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return body, nil
|
c.serverBytes = bodyBytes
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCertificateAction(cert configuration.CertificateData) error {
|
// handleCertificateAction executes the user-defined action after successful certificate deployment
|
||||||
if cert.Action == "" {
|
func handleCertificateAction(action string) error {
|
||||||
|
if action == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
action := strings.ReplaceAll(cert.Action, "{name}", cert.Name)
|
|
||||||
action = strings.ReplaceAll(action, "{path}", cert.FilePath)
|
|
||||||
|
|
||||||
sargs := strings.Split(action, " ")
|
sargs := strings.Split(action, " ")
|
||||||
|
|
||||||
cmd := exec.Command(sargs[0], sargs[1:]...)
|
cmd := exec.Command(sargs[0], sargs[1:]...)
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
package certificates
|
|
18
internal/certificates/models.go
Normal file
18
internal/certificates/models.go
Normal 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
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/certificates"
|
"code.lila.network/adoralaura/certwarden-deploy/internal/certificates"
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
|
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/constants"
|
"code.lila.network/adoralaura/certwarden-deploy/internal/constants"
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/errlog"
|
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/logger"
|
"code.lila.network/adoralaura/certwarden-deploy/internal/logger"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
@ -31,13 +30,15 @@ func handleRootCmd(cmd *cobra.Command, args []string) {
|
||||||
slog.Error("failed to initialize config", "error", err)
|
slog.Error("failed to initialize config", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
log := logger.InitializeLogger()
|
log := logger.Initialize()
|
||||||
err = errlog.SetupSentry(log, config.Sentry.DSN)
|
config.SubstituteKeys(log)
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to initialize sentry", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
certificates.HandleCertificates(log, config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package configuration
|
package configuration
|
||||||
|
|
||||||
|
import "log/slog"
|
||||||
|
|
||||||
// Config file gets read into here
|
// Config file gets read into here
|
||||||
var Config *ConfigFileData
|
var Config *ConfigFileData
|
||||||
|
|
||||||
|
@ -22,18 +24,37 @@ var Force bool
|
||||||
type ConfigFileData struct {
|
type ConfigFileData struct {
|
||||||
BaseURL string `yaml:"base_url"`
|
BaseURL string `yaml:"base_url"`
|
||||||
DisableCertificateValidation bool `yaml:"disable_certificate_validation"`
|
DisableCertificateValidation bool `yaml:"disable_certificate_validation"`
|
||||||
Sentry SentryData `yaml:"sentry,omitempty"`
|
|
||||||
Certificates []CertificateData `yaml:"certificates"`
|
Certificates []CertificateData `yaml:"certificates"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Struct that holds the details of a single managed certificate
|
// Struct that holds the details of a single managed certificate
|
||||||
type CertificateData struct {
|
type CertificateData struct {
|
||||||
Name string `yaml:"name"`
|
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"`
|
Action string `yaml:"action"`
|
||||||
FilePath string `yaml:"file_path"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SentryData struct {
|
type ConfigValidationError struct {
|
||||||
DSN string `yaml:"dsn"`
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
17
internal/configuration/substitutions.go
Normal file
17
internal/configuration/substitutions.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
65
internal/configuration/substitutions_test.go
Normal file
65
internal/configuration/substitutions_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,45 +1,38 @@
|
||||||
package configuration
|
package configuration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ValidateConfig(logger *slog.Logger, config ConfigFileData) {
|
// IsValid tests if the config read from file has all required parameters set.
|
||||||
validationFailed := false
|
//
|
||||||
|
// Exits the app if errors are detected
|
||||||
|
func (c *ConfigFileData) IsValid() ConfigValidationError {
|
||||||
|
err := ConfigValidationError{}
|
||||||
|
|
||||||
if config.BaseURL == "" {
|
if c.BaseURL == "" {
|
||||||
logger.Error(`Field 'base_url' in config file is required!`)
|
err.Add(`Field 'base_url' in config file is required!`)
|
||||||
validationFailed = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cert := range config.Certificates {
|
for _, cert := range c.Certificates {
|
||||||
if cert.Name == "" {
|
if cert.Name == "" {
|
||||||
cert.Name = "unnamed_certificate"
|
cert.Name = "unnamed_certificate"
|
||||||
logger.Error(`Field 'name' for certificates cannot be blank!`)
|
err.Add(`Field 'name' for certificates cannot be blank!`)
|
||||||
validationFailed = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if cert.ApiKey == "" {
|
if cert.CertificateSecret == "" {
|
||||||
logger.Error(`Field 'api_key' for certificate ` + cert.Name + " cannot be blank!")
|
err.Add(`Field 'cert_secret' for certificate ` + cert.Name + " cannot be blank!")
|
||||||
validationFailed = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if cert.FilePath == "" {
|
if cert.CertificatePath == "" {
|
||||||
logger.Error(`Field 'file_path' for certificate ` + cert.Name + " cannot be blank!")
|
err.Add(`Field 'cert_path' for certificate ` + cert.Name + " cannot be blank!")
|
||||||
validationFailed = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
re := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
re := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||||
if !re.MatchString(cert.Name) {
|
if !re.MatchString(cert.Name) {
|
||||||
logger.Error(`Field 'name' for certificate may only contain -_. and alphanumeric characters!`)
|
err.Add(`Field 'name' for certificate may only contain -_. and alphanumeric characters!`)
|
||||||
validationFailed = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if validationFailed {
|
return err
|
||||||
logger.Error("Config file has errors! Please fix errors above! Exiting...", "config-path", ConfigFile)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package constants
|
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 CertificateApiPath = "/certwarden/api/v1/download/certificates/"
|
||||||
|
const KeyApiPath = "/certwarden/api/v1/download/privatekeys/"
|
||||||
const ApiKeyHeaderName = "X-API-Key"
|
const ApiKeyHeaderName = "X-API-Key"
|
||||||
const UserAgent = "certwarden-deploy/" + Version + " +https://code.lila.network/adoralaura/certwarden-deploy"
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -8,13 +8,12 @@ import (
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
|
"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
|
logLevel := slog.LevelInfo
|
||||||
sourceLogging := false
|
|
||||||
|
|
||||||
if configuration.VerboseLogging {
|
if configuration.VerboseLogging {
|
||||||
logLevel = slog.LevelDebug
|
logLevel = slog.LevelDebug
|
||||||
sourceLogging = true
|
|
||||||
}
|
}
|
||||||
if configuration.QuietLogging {
|
if configuration.QuietLogging {
|
||||||
logLevel = slog.LevelError
|
logLevel = slog.LevelError
|
||||||
|
@ -25,7 +24,6 @@ func InitializeLogger() *slog.Logger {
|
||||||
|
|
||||||
opts := &slog.HandlerOptions{
|
opts := &slog.HandlerOptions{
|
||||||
Level: logLevel,
|
Level: logLevel,
|
||||||
AddSource: sourceLogging,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := slog.NewTextHandler(os.Stdout, opts)
|
handler := slog.NewTextHandler(os.Stdout, opts)
|
||||||
|
|
11
main.go
11
main.go
|
@ -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()
|
|
||||||
}
|
|
Loading…
Reference in a new issue