Major refactoring, updated config structure

This commit is contained in:
2026-02-28 00:59:11 +01:00
parent 2e52eae151
commit 2cbab1a0a2
18 changed files with 876 additions and 753 deletions

37
commands/basic.go Normal file
View File

@@ -0,0 +1,37 @@
package commands
import (
"fmt"
"log"
"git.nevets.tech/Keys/CertManager/internal"
"github.com/spf13/cobra"
)
func DevCmd(cmd *cobra.Command, args []string) {
testDomain := "lunamc.org"
err := internal.LoadConfig()
if err != nil {
log.Fatalf("Error loading configuration: %v\n", err)
}
err = internal.LoadDomainConfigs()
if err != nil {
log.Fatalf("Error loading configs: %v\n", err)
}
fmt.Println(testDomain)
}
func VersionCmd(cmd *cobra.Command, args []string) {
fmt.Printf("CertManager (certman) - Steven Tracey\nVersion: %s build-%s\n",
internal.Version, internal.Build,
)
}
func NewKeyCmd(cmd *cobra.Command, args []string) {
key, err := internal.GenerateKey()
if err != nil {
log.Fatalf("%v", err)
}
fmt.Printf(key)
}

266
commands/certs.go Normal file
View File

@@ -0,0 +1,266 @@
package commands
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"git.nevets.tech/Keys/CertManager/internal"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5/storage/memory"
)
var mgr *internal.ACMEManager
func RenewCertCmd(domain string, noPush bool, certmanMode internal.CertManMode) error {
err := internal.LoadConfig()
if err != nil {
return err
}
err = internal.LoadDomainConfigs()
if err != nil {
return err
}
switch internal.Config().GetString("App.mode") {
case "server":
mgr, err = internal.NewACMEManager()
if err != nil {
return err
}
err = renewCerts(domain, noPush, certmanMode)
if err != nil {
return err
}
return ReloadDaemonCmd()
case "client":
return pullCerts(domain, certmanMode)
default:
return fmt.Errorf("invalid operating mode %s", internal.Config().GetString("App.mode"))
}
}
func renewCerts(domain string, noPush bool, certmanMode internal.CertManMode) error {
_, err := mgr.RenewForDomain(domain)
if err != nil {
// if no existing cert, obtain instead
_, err = mgr.ObtainForDomain(domain)
if err != nil {
return fmt.Errorf("error obtaining domain certificates for domain %s: %v", domain, err)
}
}
domainConfig, exists := internal.DomainStore().Get(domain)
if !exists {
return fmt.Errorf("domain %s does not exist", domain)
}
domainConfig.Set("Internal.last_issued", time.Now().UTC().Unix())
err = internal.WriteDomainConfig(domainConfig)
if err != nil {
return fmt.Errorf("error saving domain config %s: %v", domain, err)
}
err = internal.EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), filepath.Join(mgr.CertsRoot, domain, domain+".crt"), filepath.Join(mgr.CertsRoot, domain, domain+".crt.crpt"), nil)
if err != nil {
return fmt.Errorf("error encrypting domain cert for domain %s: %v", domain, err)
}
err = internal.EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), filepath.Join(mgr.CertsRoot, domain, domain+".key"), filepath.Join(mgr.CertsRoot, domain, domain+".key.crpt"), nil)
if err != nil {
return fmt.Errorf("error encrypting domain key for domain %s: %v", domain, err)
}
if !noPush {
giteaClient := internal.CreateGiteaClient()
if giteaClient == nil {
return fmt.Errorf("error creating gitea client for domain %s: %v", domain, err)
}
gitWorkspace := &internal.GitWorkspace{
Storage: memory.NewStorage(),
FS: memfs.New(),
}
var repoUrl string
if !domainConfig.GetBool("Internal.repo_exists") {
repoUrl = internal.CreateGiteaRepo(domain, giteaClient)
if repoUrl == "" {
return fmt.Errorf("error creating Gitea repo for domain %s", domain)
}
domainConfig.Set("Internal.repo_exists", true)
err = internal.WriteDomainConfig(domainConfig)
if err != nil {
return fmt.Errorf("error saving domain config %s: %v", domain, err)
}
err = internal.InitRepo(repoUrl, gitWorkspace)
if err != nil {
return fmt.Errorf("error initializing repo for domain %s: %v", domain, err)
}
} else {
repoUrl = internal.Config().GetString("Git.server") + "/" + internal.Config().GetString("Git.org_name") + "/" + domain + domainConfig.GetString("Repo.repo_suffix") + ".git"
err = internal.CloneRepo(repoUrl, gitWorkspace, certmanMode)
if err != nil {
return fmt.Errorf("error cloning repo for domain %s: %v", domain, err)
}
}
err = internal.AddAndPushCerts(domain, gitWorkspace)
if err != nil {
return fmt.Errorf("error pushing certificates for domain %s: %v", domain, err)
}
fmt.Printf("Successfully pushed certificates for domain %s\n", domain)
}
return nil
}
func pullCerts(domain string, certmanMode internal.CertManMode) error {
gitWorkspace := &internal.GitWorkspace{
Storage: memory.NewStorage(),
FS: memfs.New(),
}
domainConfig, exists := internal.DomainStore().Get(domain)
if !exists {
return fmt.Errorf("domain %s does not exist", domain)
}
// Ex: https://git.example.com/Org/Repo-suffix.git
// Clones repo and stores in gitWorkspace, skip if clone fails (doesn't exist?)
repoUrl := internal.Config().GetString("Git.server") + "/" + internal.Config().GetString("Git.org_name") + "/" + domain + domainConfig.GetString("Repo.repo_suffix") + ".git"
err := internal.CloneRepo(repoUrl, gitWorkspace, certmanMode)
if err != nil {
return fmt.Errorf("Error cloning domain repo %s: %v\n", domain, err)
}
certsDir, err := internal.DomainCertsDirWConf(domain, domainConfig)
if err != nil {
return fmt.Errorf("Error getting certificates dir for domain %s: %v\n", domain, err)
}
// Get files in repo
fileInfos, err := gitWorkspace.FS.ReadDir("/")
if err != nil {
return fmt.Errorf("Error reading directory in memFS on domain %s: %v\n", domain, err)
}
// Iterate over files, filtering by .crpt (encrypted) files in case other files were accidentally added
for _, fileInfo := range fileInfos {
if strings.HasSuffix(fileInfo.Name(), ".crpt") {
filename, _ := strings.CutSuffix(fileInfo.Name(), ".crpt")
file, err := gitWorkspace.FS.Open(fileInfo.Name())
if err != nil {
fmt.Printf("Error opening file in memFS on domain %s: %v\n", domain, err)
continue
}
fileBytes, err := io.ReadAll(file)
if err != nil {
fmt.Printf("Error reading file in memFS on domain %s: %v\n", domain, err)
file.Close()
continue
}
err = file.Close()
if err != nil {
fmt.Printf("Error closing file on domain %s: %v\n", domain, err)
continue
}
err = internal.DecryptFileFromBytes(domainConfig.GetString("Certificates.crypto_key"), fileBytes, filepath.Join(certsDir, filename), nil)
if err != nil {
fmt.Printf("Error decrypting file %s in domain %s: %v\n", filename, domain, err)
continue
}
headRef, err := gitWorkspace.Repo.Head()
if err != nil {
fmt.Printf("Error getting head reference for domain %s: %v\n", domain, err)
continue
}
err = internal.WriteCommitHash(headRef.Hash().String(), domainConfig)
if err != nil {
fmt.Printf("Error writing commit hash: %v\n", err)
continue
}
certLinks := domainConfig.GetStringSlice("Certificates.cert_symlinks")
for _, certLink := range certLinks {
if certLink == "" {
continue
}
linkInfo, err := os.Stat(certLink)
if err != nil {
if !os.IsNotExist(err) {
fmt.Printf("Error stating cert link %s: %v\n", certLink, err)
continue
}
}
if linkInfo.IsDir() {
certLink = filepath.Join(certLink, domain+".crt")
}
err = os.Link(filepath.Join(certsDir, domain+".crt"), certLink)
if err != nil {
fmt.Printf("Error linking cert %s to %s: %v\n", certLink, domain, err)
continue
}
}
keyLinks := domainConfig.GetStringSlice("Certificates.key_symlinks")
for _, keyLink := range keyLinks {
if keyLink == "" {
continue
}
linkInfo, err := os.Stat(keyLink)
if err != nil {
if !os.IsNotExist(err) {
fmt.Printf("Error stating key link %s: %v\n", keyLink, err)
continue
}
}
if linkInfo.IsDir() {
keyLink = filepath.Join(keyLink, domain+".crt")
}
err = os.Link(filepath.Join(certsDir, domain+".crt"), keyLink)
if err != nil {
fmt.Printf("Error linking cert %s to %s: %v\n", keyLink, domain, err)
continue
}
}
}
}
return nil
}
func UpdateLinksCmd(domain string) error {
domainConfig, exists := internal.DomainStore().Get(domain)
if !exists {
return fmt.Errorf("domain %s does not exist", domain)
}
certsDir, err := internal.DomainCertsDirWConf(domain, domainConfig)
if err != nil {
return fmt.Errorf("error getting certificates dir for domain %s: %v", domain, err)
}
certLinks := domainConfig.GetStringSlice("Certificates.cert_symlinks")
for _, certLink := range certLinks {
err = internal.LinkFile(filepath.Join(certsDir, domain+".crt"), certLink, domain, ".crt")
if err != nil {
fmt.Printf("Error linking cert %s to %s: %v", certLink, domain, err)
continue
}
}
keyLinks := domainConfig.GetStringSlice("Certificates.key_symlinks")
for _, keyLink := range keyLinks {
err = internal.LinkFile(filepath.Join(certsDir, domain+".crt"), keyLink, domain, ".key")
if err != nil {
fmt.Printf("Error linking cert %s to %s: %v", keyLink, domain, err)
continue
}
}
return nil
}

164
commands/daemon.go Normal file
View File

@@ -0,0 +1,164 @@
package commands
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
"git.nevets.tech/Keys/CertManager/client"
"git.nevets.tech/Keys/CertManager/internal"
"git.nevets.tech/Keys/CertManager/server"
)
var (
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
)
func RunDaemonCmd() error {
err := internal.CreateOrUpdatePIDFile("/var/run/certman.pid")
if err != nil {
if errors.Is(err, internal.ErrorPIDInUse) {
return fmt.Errorf("daemon process is already running")
}
return fmt.Errorf("error creating pidfile: %v", err)
}
ctx, cancel = context.WithCancel(context.Background())
// Check if main config exists
if _, err := os.Stat("/etc/certman/certman.conf"); os.IsNotExist(err) {
return fmt.Errorf("main config file not found, please run 'certman --install', then properly configure /etc/certman/certman.conf")
} else if err != nil {
return fmt.Errorf("error opening /etc/certman/certman.conf: %v", err)
}
err = internal.LoadConfig()
if err != nil {
return fmt.Errorf("error loading configuration: %v", err)
}
// Setup SIGINT and SIGTERM listeners
sigChannel := make(chan os.Signal, 1)
signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(sigChannel)
reloadSigChan := make(chan os.Signal, 1)
signal.Notify(reloadSigChan, syscall.SIGHUP)
defer signal.Stop(reloadSigChan)
tickSigChan := make(chan os.Signal, 1)
signal.Notify(tickSigChan, syscall.SIGUSR1)
defer signal.Stop(tickSigChan)
tickRate := internal.Config().GetInt("App.tick_rate")
ticker := time.NewTicker(time.Duration(tickRate) * time.Hour)
defer ticker.Stop()
wg.Add(1)
if internal.Config().GetString("App.mode") == "server" {
fmt.Println("Starting CertManager in server mode...")
// Server Task loop
go func() {
server.Init()
defer wg.Done()
for {
select {
case <-ctx.Done():
server.Stop()
return
case <-reloadSigChan:
server.Reload()
case <-ticker.C:
server.Tick()
case <-tickSigChan:
server.Tick()
}
}
}()
} else if internal.Config().GetString("App.mode") == "client" {
fmt.Println("Starting CertManager in client mode...")
// Client Task loop
go func() {
client.Init()
defer wg.Done()
for {
select {
case <-ctx.Done():
client.Stop()
return
case <-reloadSigChan:
client.Reload()
case <-ticker.C:
client.Tick()
case <-tickSigChan:
client.Tick()
}
}
}()
} else {
return fmt.Errorf("invalid operating mode \"" + internal.Config().GetString("App.mode") + "\"")
}
// Cleanup on stop
sig := <-sigChannel
fmt.Printf("Program terminated with %v\n", sig.String())
stop()
wg.Wait()
return nil
}
func stop() {
cancel()
internal.ClearPIDFile()
}
func StopDaemonCmd() error {
proc, err := internal.DaemonProcess()
if err != nil {
return fmt.Errorf("error getting daemon process: %v", err)
}
err = proc.Signal(syscall.SIGTERM)
if err != nil {
return fmt.Errorf("error sending SIGTERM to daemon PID: %v", err)
}
return nil
}
func ReloadDaemonCmd() error {
proc, err := internal.DaemonProcess()
if err != nil {
return fmt.Errorf("error getting daemon process: %v", err)
}
err = proc.Signal(syscall.SIGHUP)
if err != nil {
return fmt.Errorf("error sending SIGHUP to daemon PID: %v", err)
}
return nil
}
func TickDaemonCmd() error {
proc, err := internal.DaemonProcess()
if err != nil {
return fmt.Errorf("error getting daemon process: %v", err)
}
err = proc.Signal(syscall.SIGUSR1)
if err != nil {
return fmt.Errorf("error sending SIGUSR1 to daemon PID: %v", err)
}
return nil
}
func DaemonStatusCmd() error {
fmt.Println("Not implemented :/")
return nil
}

98
commands/install.go Normal file
View File

@@ -0,0 +1,98 @@
package commands
import (
"fmt"
"os"
"os/exec"
"os/user"
"strconv"
"strings"
"git.nevets.tech/Keys/CertManager/internal"
)
func NewDomainCmd(domain, domainDir string, dirOverridden bool) error {
//TODO add config option for "overriden dir"
fmt.Printf("Creating new domain %s\n", domain)
err := internal.CreateDomainConfig(domain)
if err != nil {
return err
}
internal.CreateDomainCertsDir(domain, domainDir, dirOverridden)
certmanUser, err := user.Lookup("certman")
if err != nil {
return fmt.Errorf("error getting user certman: %v", err)
}
uid, err := strconv.Atoi(strings.TrimSpace(certmanUser.Uid))
if err != nil {
return err
}
gid, err := strconv.Atoi(strings.TrimSpace(certmanUser.Gid))
if err != nil {
return err
}
err = internal.ChownRecursive("/etc/certman/domains", uid, gid)
if err != nil {
return err
}
err = internal.ChownRecursive("/var/local/certman", uid, gid)
if err != nil {
return err
}
fmt.Println("Successfully created domain entry for " + domain + "\nUpdate config file as needed in /etc/certman/domains/" + domain + ".conf\n")
return nil
}
func InstallCmd(isThin bool, mode string) error {
if !isThin {
if os.Geteuid() != 0 {
return fmt.Errorf("installation must be run as root")
}
internal.MakeDirs()
internal.CreateConfig(mode)
f, err := os.OpenFile("/var/run/certman.pid", os.O_RDONLY|os.O_CREATE, 0755)
if err != nil {
return fmt.Errorf("error creating pid file: %v", err)
}
err = f.Close()
if err != nil {
return fmt.Errorf("error closing pid file: %v", err)
}
newUserCmd := exec.Command("useradd", "-d", "/var/local/certman", "-U", "-r", "-s", "/sbin/nologin", "certman")
if output, err := newUserCmd.CombinedOutput(); err != nil {
return fmt.Errorf("error creating user: %v: output %s", err, output)
}
certmanUser, err := user.Lookup("certman")
if err != nil {
return fmt.Errorf("error getting user certman: %v", err)
}
uid, err := strconv.Atoi(strings.TrimSpace(certmanUser.Uid))
if err != nil {
return err
}
gid, err := strconv.Atoi(strings.TrimSpace(certmanUser.Gid))
if err != nil {
return err
}
err = internal.ChownRecursive("/etc/certman", uid, gid)
if err != nil {
return fmt.Errorf("error changing uid/gid: %v", err)
}
err = internal.ChownRecursive("/var/local/certman", uid, gid)
if err != nil {
return fmt.Errorf("error changing uid/gid: %v", err)
}
err = os.Chown("/var/run/certman.pid", uid, gid)
if err != nil {
return fmt.Errorf("error changing uid/gid: %v", err)
}
} else {
internal.CreateConfig(mode)
}
return nil
}