2024-07-03 10:00:21 +02:00
|
|
|
package certificates
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"crypto/sha256"
|
|
|
|
"crypto/tls"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/fs"
|
|
|
|
"log/slog"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
2024-07-03 11:56:04 +02:00
|
|
|
"os/exec"
|
|
|
|
"strings"
|
2024-07-03 10:00:21 +02:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
|
|
|
|
"code.lila.network/adoralaura/certwarden-deploy/internal/constants"
|
|
|
|
)
|
|
|
|
|
|
|
|
func HandleCertificates(logger *slog.Logger, config *configuration.ConfigFileData) {
|
|
|
|
for _, cert := range config.Certificates {
|
2024-07-11 16:22:58 +02:00
|
|
|
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,
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
// Rollout Certificate
|
|
|
|
certOnDiskChanged, err := certInfos.Rollout(logger, config.BaseURL, config.DisableCertificateValidation)
|
2024-07-03 10:00:21 +02:00
|
|
|
if err != nil {
|
2024-07-11 16:22:58 +02:00
|
|
|
logger.Error(
|
|
|
|
"Failed to roll out Certificate", "path",
|
|
|
|
certInfos.FilePath, "name", cert.Name, "error", err,
|
|
|
|
)
|
|
|
|
continue
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
// 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
|
|
|
|
}
|
2024-07-03 11:56:04 +02:00
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
// if cert OR key changed OR --force
|
|
|
|
if (certOnDiskChanged || keyOnDiskChanged) || configuration.Force {
|
2024-07-03 11:56:04 +02:00
|
|
|
|
|
|
|
if configuration.Force {
|
2024-07-11 16:22:58 +02:00
|
|
|
logger.Info("Forcing file system change due to --force", "name", cert.Name)
|
2024-07-03 11:56:04 +02:00
|
|
|
}
|
2024-07-11 16:22:58 +02:00
|
|
|
err = handleCertificateAction(cert.Action)
|
2024-07-03 11:56:04 +02:00
|
|
|
if err != nil {
|
2024-07-11 16:22:58 +02:00
|
|
|
logger.Error("Failed to execute post-rollout action", "name", cert.Name, "error", err)
|
2024-07-03 11:56:04 +02:00
|
|
|
}
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
2024-07-11 16:22:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
baseUrl,
|
|
|
|
skipInsecure,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("failed to get certificate from server: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fileNeedsRollout, err := c.needsRollout(logger)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("failed to check certificate on disk: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if fileNeedsRollout || configuration.Force {
|
|
|
|
if configuration.Force {
|
|
|
|
logger.Info("Forcing file system change due to --force", "name", c.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = c.writeToDisk(logger)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("failed to handle certificate: %w", err)
|
2024-07-08 10:26:49 +02:00
|
|
|
}
|
2024-07-03 11:56:04 +02:00
|
|
|
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
2024-07-11 16:22:58 +02:00
|
|
|
if fileNeedsRollout {
|
|
|
|
logger.Info("New file deployed", "path", c.FilePath)
|
|
|
|
return true, nil
|
2024-07-12 11:18:32 +02:00
|
|
|
} else if configuration.Force {
|
|
|
|
logger.Info("File deployed", "path", c.FilePath)
|
|
|
|
return true, nil
|
2024-07-11 16:22:58 +02:00
|
|
|
} else {
|
|
|
|
logger.Info("File not changed, skipping...", "path", c.FilePath)
|
|
|
|
return false, nil
|
|
|
|
}
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
// readFromDisk reads file data from disk and populates the data []byte field.
|
|
|
|
//
|
|
|
|
// Returns error or nil on success
|
|
|
|
func (c *GenericCertificate) readFromDisk() error {
|
|
|
|
filebytes, err := os.ReadFile(c.FilePath)
|
2024-07-03 11:56:04 +02:00
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
2024-07-11 16:22:58 +02:00
|
|
|
return err
|
2024-07-03 11:56:04 +02:00
|
|
|
} else {
|
2024-07-11 16:22:58 +02:00
|
|
|
return fmt.Errorf("failed to read file from disk: %w", err)
|
2024-07-03 11:56:04 +02:00
|
|
|
}
|
|
|
|
}
|
2024-07-11 16:22:58 +02:00
|
|
|
|
|
|
|
c.diskBytes = filebytes
|
|
|
|
return nil
|
2024-07-03 11:56:04 +02:00
|
|
|
}
|
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
// 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()
|
2024-07-03 11:56:04 +02:00
|
|
|
|
2024-07-03 10:00:21 +02:00
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
|
|
return true, nil
|
|
|
|
} else {
|
2024-07-11 16:22:58 +02:00
|
|
|
return false, fmt.Errorf("failed to compare data to file on disk: %w", err)
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
diskHash := sha256.Sum256(c.diskBytes)
|
|
|
|
serverHash := sha256.Sum256(c.serverBytes)
|
2024-07-03 10:00:21 +02:00
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
hashesAreDifferent := diskHash != serverHash
|
|
|
|
if hashesAreDifferent {
|
|
|
|
logger.Debug("File on disk differs from server source", "path", c.FilePath)
|
2024-07-03 10:00:21 +02:00
|
|
|
} else {
|
2024-07-11 16:22:58 +02:00
|
|
|
logger.Debug("File on disk is identical to server source", "path", c.FilePath)
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
return hashesAreDifferent, nil
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
// writeToDisk flushes the certificate data to disk.
|
|
|
|
//
|
|
|
|
// Returns error or nil on success.
|
|
|
|
func (c *GenericCertificate) writeToDisk(logger *slog.Logger) error {
|
2024-07-03 10:00:21 +02:00
|
|
|
if configuration.DryRun {
|
2024-07-11 16:22:58 +02:00
|
|
|
logger.Debug("DRY-RUN: writing data to file", "path", c.FilePath)
|
2024-07-03 10:00:21 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
file, err := os.Create(c.FilePath)
|
2024-07-03 10:00:21 +02:00
|
|
|
if err != nil {
|
2024-07-11 16:22:58 +02:00
|
|
|
return fmt.Errorf("failed to open file for writing: %w", err)
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
defer func(l *slog.Logger) {
|
|
|
|
if err := file.Close(); err != nil {
|
2024-07-11 16:22:58 +02:00
|
|
|
l.Error("failed to close file", "path", c.FilePath, "error", err)
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
}(logger)
|
|
|
|
|
|
|
|
w := bufio.NewWriter(file)
|
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
if _, err := w.Write(c.serverBytes); err != nil {
|
|
|
|
return fmt.Errorf("failed to write data to file: %w", err)
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if err = w.Flush(); err != nil {
|
2024-07-11 16:22:58 +02:00
|
|
|
return fmt.Errorf("failed to flush data to file: %w", err)
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
logger.Debug("Successfully wrote to file", "path", c.FilePath)
|
2024-07-03 10:00:21 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
// fetchFromServer fetches the cert/key data from the CertWarden server and
|
|
|
|
// fills the serverBytes field.
|
|
|
|
//
|
|
|
|
// Returns error or nil on success.
|
|
|
|
func (c *GenericCertificate) fetchFromServer(logger *slog.Logger, baseUrl string, skipInsecure bool) error {
|
|
|
|
var url string
|
2024-07-12 11:18:32 +02:00
|
|
|
var fileType string
|
2024-07-11 16:22:58 +02:00
|
|
|
if c.IsKey {
|
2024-07-12 11:18:32 +02:00
|
|
|
url = baseUrl + constants.KeyApiPath + c.Name
|
|
|
|
fileType = "privatekey"
|
2024-07-11 16:22:58 +02:00
|
|
|
} else {
|
|
|
|
url = baseUrl + constants.CertificateApiPath + c.Name
|
2024-07-12 11:18:32 +02:00
|
|
|
fileType = "certificate"
|
2024-07-11 16:22:58 +02:00
|
|
|
}
|
|
|
|
|
2024-07-12 11:18:32 +02:00
|
|
|
logger.Debug("Data request URL: "+url, "file-type", fileType)
|
2024-07-03 10:00:21 +02:00
|
|
|
var transport http.RoundTripper
|
|
|
|
|
|
|
|
if skipInsecure {
|
2024-07-12 11:18:32 +02:00
|
|
|
logger.Debug("Upstream Server TLS Certificate Validation is disabled")
|
2024-07-03 10:00:21 +02:00
|
|
|
transport = &http.Transport{
|
|
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
|
|
}
|
|
|
|
} else {
|
2024-07-12 11:18:32 +02:00
|
|
|
logger.Debug("Upstream Server HTTP TLS Certificate Validation is enabled")
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
client := &http.Client{
|
|
|
|
Timeout: 10 * time.Second,
|
|
|
|
Transport: transport,
|
|
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
|
|
if err != nil {
|
2024-07-12 11:18:32 +02:00
|
|
|
return fmt.Errorf("failed to prepare to request data from server: %w", err)
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("User-Agent", constants.UserAgent)
|
2024-07-11 16:22:58 +02:00
|
|
|
req.Header.Add(constants.ApiKeyHeaderName, c.Secret)
|
2024-07-03 10:00:21 +02:00
|
|
|
|
|
|
|
res, err := client.Do(req)
|
|
|
|
if err != nil {
|
2024-07-12 11:18:32 +02:00
|
|
|
return fmt.Errorf("failed to request data from server: %w", err)
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
defer func(l *slog.Logger) {
|
|
|
|
if err := res.Body.Close(); err != nil {
|
|
|
|
l.Error("failed to close http response body", "error", err)
|
|
|
|
}
|
|
|
|
}(logger)
|
|
|
|
|
|
|
|
if res.StatusCode == http.StatusUnauthorized {
|
2024-07-12 11:18:32 +02:00
|
|
|
logger.Error("API-Key for request is invalid, skipping certificate!", "name", c.Name, "file-type", fileType)
|
2024-07-11 16:22:58 +02:00
|
|
|
return errors.New("API-Key invalid")
|
2024-07-03 10:00:21 +02:00
|
|
|
} else if res.StatusCode != http.StatusOK {
|
2024-07-12 11:18:32 +02:00
|
|
|
logger.Error("failed to get data from server", "name", c.Name, "http-response", res.Status, "file-type", fileType)
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
bodyBytes, err := io.ReadAll(res.Body)
|
2024-07-03 10:00:21 +02:00
|
|
|
if err != nil {
|
2024-07-12 11:18:32 +02:00
|
|
|
return fmt.Errorf("failed to read response from server: %w", err)
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
c.serverBytes = bodyBytes
|
|
|
|
return nil
|
2024-07-03 10:00:21 +02:00
|
|
|
}
|
2024-07-03 11:56:04 +02:00
|
|
|
|
2024-07-11 16:22:58 +02:00
|
|
|
// handleCertificateAction executes the user-defined action after successful certificate deployment
|
|
|
|
func handleCertificateAction(action string) error {
|
|
|
|
if action == "" {
|
2024-07-03 11:56:04 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
sargs := strings.Split(action, " ")
|
|
|
|
|
2024-07-03 14:34:12 +02:00
|
|
|
cmd := exec.Command(sargs[0], sargs[1:]...)
|
2024-07-03 11:56:04 +02:00
|
|
|
err := cmd.Run()
|
|
|
|
return err
|
|
|
|
}
|