Compare commits

..

No commits in common. "main" and "0.1.0" have entirely different histories.
main ... 0.1.0

42 changed files with 214 additions and 928 deletions

41
.gitignore vendored
View file

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

24
.vscode/launch.json vendored
View file

@ -1,24 +0,0 @@
{
// 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 - APP_NAME=certwarden-deploy
- FORGE=https://code.lila.network - FORGE=https://code.lila.network
commands: commands:
- apk add --update --no-cache xz curl jq make git - apk add --update --no-cache xz curl jq
- make build - go mod download
- cd bin/ - go build -o output/$APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM} main.go
- mv $APP_NAME $APP_NAME-${GOOS}-${GOARCH}${GOARM} - cd output
- xz --keep --compress $APP_NAME-${GOOS}-${GOARCH}${GOARM} - xz --keep --compress $APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM}
- sha256sum $APP_NAME-${GOOS}-${GOARCH}${GOARM} >> $APP_NAME-${GOOS}-${GOARCH}${GOARM}.sha256 - 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}.xz >> $APP_NAME-${GOOS}-${GOARCH}${GOARM}.xz.sha256 - sha256sum $APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM}.xz >> $APP_NAME-${CI_COMMIT_TAG##v}-${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-${GOOS}-${GOARCH}${GOARM};type=application/octet-stream" \ --form "attachment=@$APP_NAME-${CI_COMMIT_TAG##v}-${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-${GOOS}-${GOARCH}${GOARM}.xz;type=application/octet-stream" \ --form "attachment=@$APP_NAME-${CI_COMMIT_TAG##v}-${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-${GOOS}-${GOARCH}${GOARM}.sha256;type=application/octet-stream" \ --form "attachment=@$APP_NAME-${CI_COMMIT_TAG##v}-${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-${GOOS}-${GOARCH}${GOARM}.xz.sha256;type=application/octet-stream" \ --form "attachment=@$APP_NAME-${CI_COMMIT_TAG##v}-${GOOS}-${GOARCH}${GOARM}.xz.sha256;type=application/octet-stream" \
--fail-with-body --fail-with-body

View file

@ -1,38 +0,0 @@
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

@ -1,58 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [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
### Fixed
- Fixed handling of the post certificate action
## [0.1.0] - 2024-07-03
### Added
- Minimal viable application
- some documentation
[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

View file

@ -1,17 +0,0 @@
# 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.

View file

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

View file

@ -1,13 +0,0 @@
# 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,81 +2,10 @@
![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 simple binary to deploy certificates from a [CertWarden](https://www.certwarden.com/) instance. This is a tool 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](https://code.lila.network/adoralaura/certwarden-deploy/src/branch/main/LICENSE) file for more info. `certwarden-deploy` is available under the MIT license. See the LICENSE file for more info.

View file

@ -2,20 +2,25 @@
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 main package cmd
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"
) )
func main() { // 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() {
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
View file

@ -1,15 +0,0 @@
# 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

View file

@ -1,9 +0,0 @@
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.

View file

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

View file

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

View file

@ -1,54 +0,0 @@
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

View file

@ -1,93 +0,0 @@
---
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

@ -1,85 +0,0 @@
---
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

@ -1,30 +0,0 @@
---
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.

View file

@ -1,33 +0,0 @@
---
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.
```

View file

@ -1,5 +0,0 @@
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

View file

@ -1,2 +0,0 @@
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

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -1,13 +0,0 @@
[Unit]
Description=CertWarden Deployer binary
Documentation=https://code.lila.network/adoralaura/certwarden-deploy
[Service]
# uncomment if you want to use a different user than root
# User=certwarden-deploy
# Group=certwarden-deploy
ExecStart=/usr/local/bin/certwarden-deploy
[Install]
WantedBy=multi-user.target

View file

@ -1,10 +0,0 @@
[Unit]
Description=Timer for certwarden-deploy
[Timer]
Persistent=true
OnCalendar=Sat *-*-* 04:00:00
RandomizedDelaySec=2h
[Install]
WantedBy=timers.target

View file

@ -17,20 +17,12 @@ 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
# path where to save the certificate api_key: examplekey_notvalid_hrzjGDDw8z
# 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
View file

@ -8,8 +8,11 @@ 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
View file

@ -1,5 +1,7 @@
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=
@ -14,6 +16,10 @@ 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=

View file

@ -17,203 +17,133 @@ 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 {
certInfos := GenericCertificate{ certBytes, err := getCertFromServer(
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,
baseUrl, cert.Name,
skipInsecure, cert.ApiKey,
config.BaseURL,
config.DisableCertificateValidation,
) )
if err != nil { if err != nil {
return false, fmt.Errorf("failed to get certificate from server: %w", err) logger.Error("Failed to get certificate from server", "cert-id", cert.Name, "error", err)
return
} }
fileNeedsRollout, err := c.needsRollout(logger) certIsDifferent, err := checkCertIsDifferent(logger, cert.FilePath, certBytes)
if err != nil { if err != nil {
return false, fmt.Errorf("failed to check certificate on disk: %w", err) logger.Error("failed to handle certificate", "cert-id", cert.Name, "error", err)
return
} }
if fileNeedsRollout || configuration.Force { if certIsDifferent || configuration.Force {
if configuration.Force { if configuration.Force {
logger.Info("Forcing file system change due to --force", "name", c.Name) logger.Info("Forcing file system change due to --force", "cert-id", cert.Name)
} }
err = c.writeToDisk(logger) err = updateCertOnFS(logger, cert.FilePath, certBytes)
if err != nil { if err != nil {
return false, fmt.Errorf("failed to handle certificate: %w", err) logger.Error("failed to handle certificate", "cert-id", cert.Name, "error", err)
return
} }
if configuration.Force {
logger.Info("Forcing file system change due to --force", "cert-id", cert.Name)
} }
if fileNeedsRollout { err = handleCertificateAction(cert)
logger.Info("New file deployed", "path", c.FilePath) if err != nil {
return true, nil logger.Error("post certificate change command failed", "cert-id", cert.Name, "error", err)
} 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
} }
} }
// readFromDisk reads file data from disk and populates the data []byte field. logger.Info("Certificate updated successfully", "cert-id", cert.Name)
//
// Returns error or nil on success }
func (c *GenericCertificate) readFromDisk() error { }
filebytes, err := os.ReadFile(c.FilePath)
func getCertFromFile(path string) ([]byte, error) {
filebytes, err := os.ReadFile(path)
if err != nil { if err != nil {
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
return err return []byte{}, err
} else { } else {
return fmt.Errorf("failed to read file from disk: %w", err) return []byte{}, fmt.Errorf("failed to read certificate file on disk: %w", err)
} }
} }
return filebytes, nil
}
c.diskBytes = filebytes func checkCertIsDifferent(logger *slog.Logger, path string, data []byte) (bool, error) {
return nil filebytes, err := getCertFromFile(path)
}
// 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 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 data to file on disk: %w", err) return false, fmt.Errorf("failed to compare certificates: %w", err)
} }
} }
diskHash := sha256.Sum256(c.diskBytes) existingSha256 := sha256.Sum256(filebytes)
serverHash := sha256.Sum256(c.serverBytes) newSha256 := sha256.Sum256(data)
hashesAreDifferent := diskHash != serverHash sumsAreDifferent := existingSha256 != newSha256
if hashesAreDifferent { if sumsAreDifferent {
logger.Debug("File on disk differs from server source", "path", c.FilePath) logger.Debug("Certificate on file differs from the certificate on the server", "cert-path", path)
} else { } else {
logger.Debug("File on disk is identical to server source", "path", c.FilePath) logger.Debug("Certificate on file is identical to the certificate on the server", "cert-path", path)
} }
return hashesAreDifferent, nil return sumsAreDifferent, nil
} }
// writeToDisk flushes the certificate data to disk. func updateCertOnFS(logger *slog.Logger, path string, data []byte) error {
//
// 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 data to file", "path", c.FilePath) logger.Debug("DRY-RUN: writing certificate data to file", "cert-path", path)
return nil return nil
} }
file, err := os.Create(c.FilePath) file, err := os.Create(path)
if err != nil { if err != nil {
return fmt.Errorf("failed to open file for writing: %w", err) return fmt.Errorf("failed to open certificate 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", "path", c.FilePath, "error", err) l.Error("failed to close file", "file-path", path, "error", err)
} }
}(logger) }(logger)
w := bufio.NewWriter(file) w := bufio.NewWriter(file)
if _, err := w.Write(c.serverBytes); err != nil { if _, err := w.Write(data); err != nil {
return fmt.Errorf("failed to write data to file: %w", err) return fmt.Errorf("failed to write certificate data to file: %w", err)
} }
if err = w.Flush(); err != nil { if err = w.Flush(); err != nil {
return fmt.Errorf("failed to flush data to file: %w", err) return fmt.Errorf("failed to flush certificate data to file: %w", err)
} }
logger.Debug("Successfully wrote to file", "path", c.FilePath) logger.Debug("wrote certificate to file", "file-path", path)
return nil return nil
} }
// fetchFromServer fetches the cert/key data from the CertWarden server and func getCertFromServer(logger *slog.Logger, certName string, certKey string, baseUrl string, skipInsecure bool) ([]byte, error) {
// fills the serverBytes field. url := baseUrl + constants.CertificateApiPath + certName
// 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("Upstream Server TLS Certificate Validation is disabled") logger.Debug("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("Upstream Server HTTP TLS Certificate Validation is enabled") logger.Debug("TLS Certificate Validation is enabled")
} }
client := &http.Client{ client := &http.Client{
@ -222,15 +152,17 @@ func (c *GenericCertificate) fetchFromServer(logger *slog.Logger, baseUrl string
} }
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
return fmt.Errorf("failed to prepare to request data from server: %w", err) return []byte{}, fmt.Errorf("failed to prepare to request certificate from server: %w", err)
} }
req.Header.Set("User-Agent", constants.UserAgent) req.Header.Set("User-Agent", constants.UserAgent)
req.Header.Add(constants.ApiKeyHeaderName, c.Secret) req.Header.Add(constants.ApiKeyHeaderName, certKey)
res, err := client.Do(req) res, err := client.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("failed to request data from server: %w", err) e := fmt.Errorf("failed to request certificate from server: %w", err)
sentry.CaptureException(e)
return []byte{}, e
} }
defer func(l *slog.Logger) { defer func(l *slog.Logger) {
@ -240,30 +172,33 @@ func (c *GenericCertificate) fetchFromServer(logger *slog.Logger, baseUrl string
}(logger) }(logger)
if res.StatusCode == http.StatusUnauthorized { if res.StatusCode == http.StatusUnauthorized {
logger.Error("API-Key for request is invalid, skipping certificate!", "name", c.Name, "file-type", fileType) logger.Error("API-Key for Certificate is invalid, skipping certificate!", "cert-id", certName)
return errors.New("API-Key invalid") return []byte{}, errors.New("API-Key invalid")
} else if res.StatusCode != http.StatusOK { } else if res.StatusCode != http.StatusOK {
logger.Error("failed to get data from server", "name", c.Name, "http-response", res.Status, "file-type", fileType) logger.Error("failed to get certificate from server", "cert-id", certName, "http-response", res.Status)
} }
bodyBytes, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
return fmt.Errorf("failed to read response from server: %w", err) e := fmt.Errorf("failed to read certificate response from server: %w", err)
sentry.CaptureException(e)
return []byte{}, e
} }
c.serverBytes = bodyBytes return body, nil
}
func handleCertificateAction(cert configuration.CertificateData) error {
if cert.Action == "" {
return nil return nil
} }
// handleCertificateAction executes the user-defined action after successful certificate deployment action := strings.ReplaceAll(cert.Action, "{name}", cert.Name)
func handleCertificateAction(action string) error { action = strings.ReplaceAll(action, "{path}", cert.FilePath)
if action == "" {
return nil
}
sargs := strings.Split(action, " ") sargs := strings.Split(action, " ")
cmd := exec.Command(sargs[0], sargs[1:]...) cmd := exec.Command(sargs[0], sargs...)
err := cmd.Run() err := cmd.Run()
return err return err
} }

View file

@ -0,0 +1 @@
package certificates

View file

@ -1,18 +0,0 @@
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,6 +7,7 @@ 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"
) )
@ -30,15 +31,13 @@ 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.Initialize() log := logger.InitializeLogger()
config.SubstituteKeys(log) err = errlog.SetupSentry(log, config.Sentry.DSN)
if err != nil {
validation := config.IsValid() slog.Error("failed to initialize sentry", "error", err)
if validation.HasMessages() {
validation.Print(log)
slog.Error("The configuration file has errors! Application cannot start unless all errors are corrected!")
os.Exit(1)
} }
configuration.ValidateConfig(log, *config)
certificates.HandleCertificates(log, config) certificates.HandleCertificates(log, config)
} }

View file

@ -1,7 +1,5 @@
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
@ -24,37 +22,18 @@ 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"`
CertificateSecret string `yaml:"cert_secret"` ApiKey string `yaml:"api_key"`
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 ConfigValidationError struct { type SentryData struct {
ErrorMessages []string DSN string `yaml:"dsn"`
}
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

@ -1,17 +0,0 @@
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

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

View file

@ -1,9 +1,6 @@
package constants package constants
var Version string const Version = "0.1.0"
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"

27
internal/errlog/sentry.go Normal file
View file

@ -0,0 +1,27 @@
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,12 +8,13 @@ import (
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration" "code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
) )
// Initialize initializes a *slog.Logger with the right log level and options. func InitializeLogger() *slog.Logger {
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
@ -24,6 +25,7 @@ func Initialize() *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 Normal file
View file

@ -0,0 +1,11 @@
/*
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()
}