Major refactoring, updated config structure
This commit is contained in:
7
Makefile
7
Makefile
@@ -1,12 +1,13 @@
|
|||||||
VERSION := 1.0.0
|
VERSION := 1.0.0-beta
|
||||||
|
BUILD := $(shell git rev-parse --short HEAD)
|
||||||
|
|
||||||
GO := go
|
GO := go
|
||||||
|
|
||||||
BUILD_FLAGS := -buildmode=pie -trimpath
|
BUILD_FLAGS := -buildmode=pie -trimpath
|
||||||
LDFLAGS := -linkmode=external -extldflags="-Wl,-z,relro,-z,now"
|
LDFLAGS := -linkmode=external -extldflags="-Wl,-z,relro,-z,now" -X git.nevets.tech/Keys/CertManager/internal.Version=$(VERSION) -X git.nevets.tech/Keys/CertManager/internal.Build=$(BUILD)
|
||||||
|
|
||||||
build:
|
build:
|
||||||
$(GO) build $(BUILD_FLAGS) -ldflags='$(LDFLAGS)' -o ./certman .
|
$(GO) build $(BUILD_FLAGS) -ldflags="$(LDFLAGS)" -o ./certman .
|
||||||
@cp ./certman ./certman-$(VERSION)-amd64
|
@cp ./certman ./certman-$(VERSION)-amd64
|
||||||
|
|
||||||
stage: build
|
stage: build
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -7,24 +7,26 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.nevets.tech/Keys/CertManager/internal"
|
||||||
"github.com/go-git/go-billy/v5/memfs"
|
"github.com/go-git/go-billy/v5/memfs"
|
||||||
"github.com/go-git/go-git/v5/storage/memory"
|
"github.com/go-git/go-git/v5/storage/memory"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initClient() {
|
func Init() {
|
||||||
err := LoadDomainConfigs()
|
err := internal.LoadDomainConfigs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error loading domain configs: %v", err)
|
log.Fatalf("Error loading domain configs: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
clientTick()
|
Tick()
|
||||||
}
|
}
|
||||||
|
|
||||||
func clientTick() {
|
func Tick() {
|
||||||
fmt.Println("Tick!")
|
fmt.Println("Tick!")
|
||||||
|
|
||||||
// Get local copy of domain configs
|
// Get local copy of configs
|
||||||
localDomainConfigs := domainStore.Snapshot()
|
config := internal.Config()
|
||||||
|
localDomainConfigs := internal.DomainStore().Snapshot()
|
||||||
|
|
||||||
// Loop over all domain configs (domains)
|
// Loop over all domain configs (domains)
|
||||||
for domainStr, domainConfig := range localDomainConfigs {
|
for domainStr, domainConfig := range localDomainConfigs {
|
||||||
@@ -37,16 +39,16 @@ func clientTick() {
|
|||||||
// If the repo doesn't exist, we can't check for a remote commit, so stop the rest of the check
|
// If the repo doesn't exist, we can't check for a remote commit, so stop the rest of the check
|
||||||
repoExists := domainConfig.GetBool("Internal.repo_exists")
|
repoExists := domainConfig.GetBool("Internal.repo_exists")
|
||||||
if repoExists {
|
if repoExists {
|
||||||
localHash, err := getLocalCommitHash(domainStr)
|
localHash, err := internal.LocalCommitHash(domainStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("No local commit hash found for domain %s\n", domainStr)
|
fmt.Printf("No local commit hash found for domain %s\n", domainStr)
|
||||||
}
|
}
|
||||||
gitSource, err := strToGitSource(config.GetString("Git.host"))
|
gitSource, err := internal.StrToGitSource(internal.Config().GetString("Git.host"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error getting git source for domain %s: %v\n", domainStr, err)
|
fmt.Printf("Error getting git source for domain %s: %v\n", domainStr, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
remoteHash, err := getRemoteCommitHash(domainStr, gitSource)
|
remoteHash, err := internal.RemoteCommitHash(domainStr, gitSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error getting remote commit hash for domain %s: %v\n", domainStr, err)
|
fmt.Printf("Error getting remote commit hash for domain %s: %v\n", domainStr, err)
|
||||||
}
|
}
|
||||||
@@ -58,20 +60,20 @@ func clientTick() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gitWorkspace := &GitWorkspace{
|
gitWorkspace := &internal.GitWorkspace{
|
||||||
Storage: memory.NewStorage(),
|
Storage: memory.NewStorage(),
|
||||||
FS: memfs.New(),
|
FS: memfs.New(),
|
||||||
}
|
}
|
||||||
// Ex: https://git.example.com/Org/Repo-suffix.git
|
// Ex: https://git.example.com/Org/Repo-suffix.git
|
||||||
// Clones repo and stores in gitWorkspace, skip if clone fails (doesn't exist?)
|
// Clones repo and stores in gitWorkspace, skip if clone fails (doesn't exist?)
|
||||||
repoUrl := config.GetString("Git.server") + "/" + config.GetString("Git.org_name") + "/" + domainStr + domainConfig.GetString("Repo.repo_suffix") + ".git"
|
repoUrl := internal.Config().GetString("Git.server") + "/" + config.GetString("Git.org_name") + "/" + domainStr + domainConfig.GetString("Repo.repo_suffix") + ".git"
|
||||||
err := cloneRepo(repoUrl, gitWorkspace)
|
err := internal.CloneRepo(repoUrl, gitWorkspace, internal.Client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error cloning domain repo %s: %v\n", domainStr, err)
|
fmt.Printf("Error cloning domain repo %s: %v\n", domainStr, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
certsDir, err := getDomainCertsDirWConf(domainStr, domainConfig)
|
certsDir, err := internal.DomainCertsDirWConf(domainStr, domainConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error getting certificates dir for domain %s: %v\n", domainStr, err)
|
fmt.Printf("Error getting certificates dir for domain %s: %v\n", domainStr, err)
|
||||||
continue
|
continue
|
||||||
@@ -104,7 +106,7 @@ func clientTick() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = DecryptFileFromBytes(domainConfig.GetString("Certificates.crypto_key"), fileBytes, filepath.Join(certsDir, filename), nil)
|
err = internal.DecryptFileFromBytes(domainConfig.GetString("Certificates.crypto_key"), fileBytes, filepath.Join(certsDir, filename), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error decrypting file %s in domain %s: %v\n", filename, domainStr, err)
|
fmt.Printf("Error decrypting file %s in domain %s: %v\n", filename, domainStr, err)
|
||||||
continue
|
continue
|
||||||
@@ -116,7 +118,7 @@ func clientTick() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = writeCommitHash(headRef.Hash().String(), domainConfig)
|
err = internal.WriteCommitHash(headRef.Hash().String(), domainConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error writing commit hash: %v\n", err)
|
fmt.Printf("Error writing commit hash: %v\n", err)
|
||||||
continue
|
continue
|
||||||
@@ -124,7 +126,7 @@ func clientTick() {
|
|||||||
|
|
||||||
certLinks := domainConfig.GetStringSlice("Certificates.cert_symlinks")
|
certLinks := domainConfig.GetStringSlice("Certificates.cert_symlinks")
|
||||||
for _, certLink := range certLinks {
|
for _, certLink := range certLinks {
|
||||||
err = linkFile(filepath.Join(certsDir, domainStr+".crt"), certLink, domainStr, ".crt")
|
err = internal.LinkFile(filepath.Join(certsDir, domainStr+".crt"), certLink, domainStr, ".crt")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error linking cert %s to %s: %v\n", certLink, domainStr, err)
|
fmt.Printf("Error linking cert %s to %s: %v\n", certLink, domainStr, err)
|
||||||
continue
|
continue
|
||||||
@@ -133,7 +135,7 @@ func clientTick() {
|
|||||||
|
|
||||||
keyLinks := domainConfig.GetStringSlice("Certificates.key_symlinks")
|
keyLinks := domainConfig.GetStringSlice("Certificates.key_symlinks")
|
||||||
for _, keyLink := range keyLinks {
|
for _, keyLink := range keyLinks {
|
||||||
err = linkFile(filepath.Join(certsDir, domainStr+".crt"), keyLink, domainStr, ".key")
|
err = internal.LinkFile(filepath.Join(certsDir, domainStr+".crt"), keyLink, domainStr, ".key")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error linking cert %s to %s: %v\n", keyLink, domainStr, err)
|
fmt.Printf("Error linking cert %s to %s: %v\n", keyLink, domainStr, err)
|
||||||
continue
|
continue
|
||||||
@@ -144,16 +146,16 @@ func clientTick() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadClient() {
|
func Reload() {
|
||||||
fmt.Println("Reloading configs...")
|
fmt.Println("Reloading configs...")
|
||||||
|
|
||||||
err := LoadDomainConfigs()
|
err := internal.LoadDomainConfigs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error loading domain configs: %v\n", err)
|
fmt.Printf("Error loading domain configs: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopClient() {
|
func Stop() {
|
||||||
fmt.Println("Shutting down client")
|
fmt.Println("Shutting down client")
|
||||||
}
|
}
|
||||||
494
commands.go
494
commands.go
@@ -1,494 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"os/user"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-git/go-billy/v5/memfs"
|
|
||||||
"github.com/go-git/go-git/v5/storage/memory"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func devFunc(cmd *cobra.Command, args []string) {
|
|
||||||
testDomain := "lunamc.org"
|
|
||||||
//config, err = ezconf.LoadConfiguration("/etc/certman/certman.conf")
|
|
||||||
err := LoadConfig("/etc/certman/certman.conf")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error loading configuration: %v\n", err)
|
|
||||||
}
|
|
||||||
err = LoadDomainConfigs()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error loading configs: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(testDomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
func versionResponse(cmd *cobra.Command, args []string) {
|
|
||||||
fmt.Println("CertManager (certman) - Steven Tracey\nVersion: " + version + " build-" + build)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newKey(cmd *cobra.Command, args []string) {
|
|
||||||
key, err := GenerateKey()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("%v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDomain(domain, domainDir string, dirOverridden bool) error {
|
|
||||||
//TODO add config option for "overriden dir"
|
|
||||||
fmt.Printf("Creating new domain %s\n", domain)
|
|
||||||
err := createNewDomainConfig(domain)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
createNewDomainCertsDir(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 = ChownRecursive("/etc/certman/domains", uid, gid)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = 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 install(isThin bool, mode string) error {
|
|
||||||
if !isThin {
|
|
||||||
if os.Geteuid() != 0 {
|
|
||||||
return fmt.Errorf("installation must be run as root")
|
|
||||||
}
|
|
||||||
|
|
||||||
makeDirs()
|
|
||||||
createNewConfig(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 = ChownRecursive("/etc/certman", uid, gid)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error changing uid/gid: %v", err)
|
|
||||||
}
|
|
||||||
err = 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 {
|
|
||||||
createNewConfig(mode)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func renewCertFunc(domain string, noPush bool) error {
|
|
||||||
err := LoadConfig("/etc/certman/certman.conf")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = LoadDomainConfigs()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch config.GetString("App.mode") {
|
|
||||||
case "server":
|
|
||||||
mgr, err = NewACMEManager()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = renewCerts(domain, noPush)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return reloadDaemon()
|
|
||||||
case "client":
|
|
||||||
return pullCerts(domain)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid operating mode %s", config.GetString("App.mode"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func renewCerts(domain string, noPush bool) 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 := domainStore.Get(domain)
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("domain %s does not exist", domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
domainConfig.Set("Internal.last_issued", time.Now().UTC().Unix())
|
|
||||||
err = WriteDomainConfig(domainConfig)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error saving domain config %s: %v", domain, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = 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 = 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 := createGiteaClient()
|
|
||||||
if giteaClient == nil {
|
|
||||||
return fmt.Errorf("error creating gitea client for domain %s: %v", domain, err)
|
|
||||||
}
|
|
||||||
gitWorkspace := &GitWorkspace{
|
|
||||||
Storage: memory.NewStorage(),
|
|
||||||
FS: memfs.New(),
|
|
||||||
}
|
|
||||||
|
|
||||||
var repoUrl string
|
|
||||||
if !domainConfig.GetBool("Internal.repo_exists") {
|
|
||||||
repoUrl = createGiteaRepo(domain, giteaClient)
|
|
||||||
if repoUrl == "" {
|
|
||||||
return fmt.Errorf("error creating Gitea repo for domain %s", domain)
|
|
||||||
}
|
|
||||||
domainConfig.Set("Internal.repo_exists", true)
|
|
||||||
err = WriteDomainConfig(domainConfig)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error saving domain config %s: %v", domain, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = initRepo(repoUrl, gitWorkspace)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error initializing repo for domain %s: %v", domain, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
repoUrl = config.GetString("Git.server") + "/" + config.GetString("Git.org_name") + "/" + domain + domainConfig.GetString("Repo.repo_suffix") + ".git"
|
|
||||||
err = cloneRepo(repoUrl, gitWorkspace)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error cloning repo for domain %s: %v", domain, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = 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) error {
|
|
||||||
gitWorkspace := &GitWorkspace{
|
|
||||||
Storage: memory.NewStorage(),
|
|
||||||
FS: memfs.New(),
|
|
||||||
}
|
|
||||||
|
|
||||||
domainConfig, exists := 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 := config.GetString("Git.server") + "/" + config.GetString("Git.org_name") + "/" + domain + domainConfig.GetString("Repo.repo_suffix") + ".git"
|
|
||||||
err := cloneRepo(repoUrl, gitWorkspace)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error cloning domain repo %s: %v\n", domain, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
certsDir, err := getDomainCertsDirWConf(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 = 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 = 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 runDaemon() error {
|
|
||||||
err := createOrUpdatePIDFile("/var/run/certman.pid")
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, 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(configFile); 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 %s: %v", configFile, err)
|
|
||||||
}
|
|
||||||
err = LoadConfig(configFile)
|
|
||||||
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 := config.GetInt("App.tick_rate")
|
|
||||||
ticker := time.NewTicker(time.Duration(tickRate) * time.Hour)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
if config.GetString("App.mode") == "server" {
|
|
||||||
fmt.Println("Starting CertManager in server mode...")
|
|
||||||
// Server Task loop
|
|
||||||
go func() {
|
|
||||||
initServer()
|
|
||||||
defer wg.Done()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
stopServer()
|
|
||||||
return
|
|
||||||
case <-reloadSigChan:
|
|
||||||
reloadServer()
|
|
||||||
case <-ticker.C:
|
|
||||||
serverTick()
|
|
||||||
case <-tickSigChan:
|
|
||||||
serverTick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
} else if config.GetString("App.mode") == "client" {
|
|
||||||
fmt.Println("Starting CertManager in client mode...")
|
|
||||||
// Client Task loop
|
|
||||||
go func() {
|
|
||||||
initClient()
|
|
||||||
defer wg.Done()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
stopClient()
|
|
||||||
return
|
|
||||||
case <-reloadSigChan:
|
|
||||||
reloadClient()
|
|
||||||
case <-ticker.C:
|
|
||||||
clientTick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("invalid operating mode \"" + 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()
|
|
||||||
clearPIDFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopDaemon() error {
|
|
||||||
proc, err := getDaemonProcess()
|
|
||||||
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 reloadDaemon() error {
|
|
||||||
proc, err := getDaemonProcess()
|
|
||||||
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 tickDaemon() error {
|
|
||||||
proc, err := getDaemonProcess()
|
|
||||||
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 statusDaemon() error {
|
|
||||||
fmt.Println("Not implemented :/")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
37
commands/basic.go
Normal file
37
commands/basic.go
Normal 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
266
commands/certs.go
Normal 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
164
commands/daemon.go
Normal 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
98
commands/install.go
Normal 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
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
[App]
|
|
||||||
mode = {mode}
|
|
||||||
|
|
||||||
[Git]
|
|
||||||
host = gitea
|
|
||||||
server = https://gitea.instance.com
|
|
||||||
username = user
|
|
||||||
org_name = org
|
|
||||||
template_name = template
|
|
||||||
|
|
||||||
[Certificates]
|
|
||||||
email = user@example.com
|
|
||||||
data_root = /var/local/certman
|
|
||||||
request_method = dns
|
|
||||||
|
|
||||||
[Cloudflare]
|
|
||||||
cf_email = email@example.com
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
[Domain]
|
|
||||||
domain_name = {domain}
|
|
||||||
; default (use system dns) or IPv4 Address (1.1.1.1)
|
|
||||||
dns_server = default
|
|
||||||
; optionally use /path/to/directory
|
|
||||||
file_location = default
|
|
||||||
|
|
||||||
[Certificates]
|
|
||||||
subdomains =
|
|
||||||
expiry = 90
|
|
||||||
cert_symlink =
|
|
||||||
key_symlink =
|
|
||||||
|
|
||||||
[Repo]
|
|
||||||
repo_suffix = -certificates
|
|
||||||
|
|
||||||
; Don't change setting below here unless you know what you're doing!
|
|
||||||
[Internal]
|
|
||||||
last_issued = never
|
|
||||||
4
go.mod
4
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module main
|
module git.nevets.tech/Keys/CertManager
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
@@ -7,7 +7,6 @@ require (
|
|||||||
github.com/go-acme/lego/v4 v4.32.0
|
github.com/go-acme/lego/v4 v4.32.0
|
||||||
github.com/go-git/go-billy/v5 v5.8.0
|
github.com/go-git/go-billy/v5 v5.8.0
|
||||||
github.com/go-git/go-git/v5 v5.17.0
|
github.com/go-git/go-git/v5 v5.17.0
|
||||||
github.com/google/go-github/v55 v55.0.0
|
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
@@ -30,7 +29,6 @@ require (
|
|||||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/google/go-querystring v1.2.0 // indirect
|
|
||||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// acme_manager.go
|
package internal
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
@@ -28,14 +27,14 @@ import (
|
|||||||
// Thread safety for your domain config map
|
// Thread safety for your domain config map
|
||||||
// (assumes you already have these globals elsewhere)
|
// (assumes you already have these globals elsewhere)
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
// var mu sync.RWMutex
|
// var MU sync.RWMutex
|
||||||
// var domainConfigs map[string]*ezconf.Configuration
|
// var domainConfigs map[string]*ezconf.Configuration
|
||||||
// var config *ezconf.Configuration
|
// var config *ezconf.Configuration
|
||||||
//
|
//
|
||||||
// func getDomainConfig(domain string) (*ezconf.Configuration, bool) { ... }
|
// func getDomainConfig(domain string) (*ezconf.Configuration, bool) { ... }
|
||||||
|
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
// ACME account user (file-backed)
|
// ACME account User (file-backed)
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
type fileUser struct {
|
type fileUser struct {
|
||||||
@@ -54,14 +53,14 @@ func (u *fileUser) GetPrivateKey() crypto.PrivateKey { return u.privateKe
|
|||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
type ACMEManager struct {
|
type ACMEManager struct {
|
||||||
mu sync.Mutex // serializes lego client ops + account writes
|
MU sync.Mutex // serializes lego Client ops + account writes
|
||||||
client *lego.Client
|
Client *lego.Client
|
||||||
user *fileUser
|
User *fileUser
|
||||||
|
|
||||||
// root dirs
|
// root dirs
|
||||||
dataRoot string // e.g. /var/local/certman
|
dataRoot string // e.g. /var/local/certman
|
||||||
accountRoot string // e.g. /var/local/certman/accounts
|
accountRoot string // e.g. /var/local/certman/accounts
|
||||||
certsRoot string // e.g. /var/local/certman/certificates
|
CertsRoot string // e.g. /var/local/certman/certificates
|
||||||
}
|
}
|
||||||
|
|
||||||
// DomainRuntimeConfig has domain-specific runtime settings derived from main+domain config.
|
// DomainRuntimeConfig has domain-specific runtime settings derived from main+domain config.
|
||||||
@@ -89,7 +88,7 @@ type StoredCertMeta struct {
|
|||||||
// Public API
|
// Public API
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
// NewACMEManager initializes a long-lived lego client using:
|
// NewACMEManager initializes a long-lived lego Client using:
|
||||||
// - file-backed account
|
// - file-backed account
|
||||||
// - persistent ECDSA P-256 account key
|
// - persistent ECDSA P-256 account key
|
||||||
// - Let’s Encrypt production by default (from config fallback)
|
// - Let’s Encrypt production by default (from config fallback)
|
||||||
@@ -104,20 +103,20 @@ func NewACMEManager() (*ACMEManager, error) {
|
|||||||
mgr := &ACMEManager{
|
mgr := &ACMEManager{
|
||||||
dataRoot: dataRoot,
|
dataRoot: dataRoot,
|
||||||
accountRoot: filepath.Join(dataRoot, "accounts"),
|
accountRoot: filepath.Join(dataRoot, "accounts"),
|
||||||
certsRoot: filepath.Join(dataRoot, "certificates"),
|
CertsRoot: filepath.Join(dataRoot, "certificates"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(mgr.accountRoot, 0o700); err != nil {
|
if err := os.MkdirAll(mgr.accountRoot, 0o700); err != nil {
|
||||||
return nil, fmt.Errorf("create account root: %w", err)
|
return nil, fmt.Errorf("create account root: %w", err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(mgr.certsRoot, 0o700); err != nil {
|
if err := os.MkdirAll(mgr.CertsRoot, 0o700); err != nil {
|
||||||
return nil, fmt.Errorf("create certs root: %w", err)
|
return nil, fmt.Errorf("create certs root: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create/load file-backed account user
|
// Create/load file-backed account User
|
||||||
user, err := loadOrCreateACMEUser(mgr.accountRoot, email)
|
user, err := loadOrCreateACMEUser(mgr.accountRoot, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("load/create acme user: %w", err)
|
return nil, fmt.Errorf("load/create acme User: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cloudflare provider (DNS-01 only).
|
// Cloudflare provider (DNS-01 only).
|
||||||
@@ -147,20 +146,20 @@ func NewACMEManager() (*ACMEManager, error) {
|
|||||||
return nil, fmt.Errorf("set dns-01 provider: %w", err)
|
return nil, fmt.Errorf("set dns-01 provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mgr.client = client
|
mgr.Client = client
|
||||||
mgr.user = user
|
mgr.User = user
|
||||||
|
|
||||||
// Register account only on first run
|
// Register account only on first run
|
||||||
if mgr.user.Registration == nil {
|
if mgr.User.Registration == nil {
|
||||||
reg, err := mgr.client.Registration.Register(registration.RegisterOptions{
|
reg, err := mgr.Client.Registration.Register(registration.RegisterOptions{
|
||||||
TermsOfServiceAgreed: true,
|
TermsOfServiceAgreed: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("acme registration: %w", err)
|
return nil, fmt.Errorf("acme registration: %w", err)
|
||||||
}
|
}
|
||||||
mgr.user.Registration = reg
|
mgr.User.Registration = reg
|
||||||
if err := saveACMEUser(mgr.accountRoot, mgr.user); err != nil {
|
if err := saveACMEUser(mgr.accountRoot, mgr.User); err != nil {
|
||||||
return nil, fmt.Errorf("save acme user registration: %w", err)
|
return nil, fmt.Errorf("save acme User registration: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,10 +183,10 @@ func (m *ACMEManager) ObtainForDomain(domainKey string) (*certificate.Resource,
|
|||||||
Bundle: true,
|
Bundle: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
m.mu.Lock()
|
m.MU.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.MU.Unlock()
|
||||||
|
|
||||||
res, err := m.client.Certificate.Obtain(req)
|
res, err := m.Client.Certificate.Obtain(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("obtain %q: %w", domainKey, err)
|
return nil, fmt.Errorf("obtain %q: %w", domainKey, err)
|
||||||
}
|
}
|
||||||
@@ -201,8 +200,8 @@ func (m *ACMEManager) ObtainForDomain(domainKey string) (*certificate.Resource,
|
|||||||
|
|
||||||
// RenewForDomain renews an existing stored cert for a domain key.
|
// RenewForDomain renews an existing stored cert for a domain key.
|
||||||
func (m *ACMEManager) RenewForDomain(domainKey string) (*certificate.Resource, error) {
|
func (m *ACMEManager) RenewForDomain(domainKey string) (*certificate.Resource, error) {
|
||||||
m.mu.Lock()
|
m.MU.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.MU.Unlock()
|
||||||
|
|
||||||
existing, err := m.loadStoredResource(domainKey)
|
existing, err := m.loadStoredResource(domainKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -210,7 +209,7 @@ func (m *ACMEManager) RenewForDomain(domainKey string) (*certificate.Resource, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RenewWithOptions is preferred in newer lego versions.
|
// RenewWithOptions is preferred in newer lego versions.
|
||||||
renewed, err := m.client.Certificate.RenewWithOptions(*existing, &certificate.RenewOptions{
|
renewed, err := m.Client.Certificate.RenewWithOptions(*existing, &certificate.RenewOptions{
|
||||||
Bundle: true,
|
Bundle: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -226,8 +225,8 @@ func (m *ACMEManager) RenewForDomain(domainKey string) (*certificate.Resource, e
|
|||||||
|
|
||||||
// GetCertPaths returns disk paths for the domain's cert material.
|
// GetCertPaths returns disk paths for the domain's cert material.
|
||||||
func (m *ACMEManager) GetCertPaths(domainKey string) (certPEM, keyPEM string) {
|
func (m *ACMEManager) GetCertPaths(domainKey string) (certPEM, keyPEM string) {
|
||||||
base := sanitizeDomainKey(domainKey)
|
base := SanitizeDomainKey(domainKey)
|
||||||
dir := filepath.Join(m.certsRoot, base)
|
dir := filepath.Join(m.CertsRoot, base)
|
||||||
return filepath.Join(dir, base+".crt"),
|
return filepath.Join(dir, base+".crt"),
|
||||||
filepath.Join(dir, base+".key")
|
filepath.Join(dir, base+".key")
|
||||||
}
|
}
|
||||||
@@ -450,7 +449,7 @@ func loadACMEUser(accountRoot string) (*fileUser, error) {
|
|||||||
|
|
||||||
func saveACMEUser(accountRoot string, u *fileUser) error {
|
func saveACMEUser(accountRoot string, u *fileUser) error {
|
||||||
if u == nil {
|
if u == nil {
|
||||||
return errors.New("nil user")
|
return errors.New("nil User")
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(accountRoot, 0o700); err != nil {
|
if err := os.MkdirAll(accountRoot, 0o700); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -525,8 +524,8 @@ func (m *ACMEManager) saveCertFiles(domainKey string, res *certificate.Resource,
|
|||||||
return errors.New("nil certificate resource")
|
return errors.New("nil certificate resource")
|
||||||
}
|
}
|
||||||
|
|
||||||
base := sanitizeDomainKey(domainKey)
|
base := SanitizeDomainKey(domainKey)
|
||||||
dir := filepath.Join(m.certsRoot, base)
|
dir := filepath.Join(m.CertsRoot, base)
|
||||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -599,8 +598,8 @@ func (m *ACMEManager) saveCertFiles(domainKey string, res *certificate.Resource,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *ACMEManager) loadStoredResource(domainKey string) (*certificate.Resource, error) {
|
func (m *ACMEManager) loadStoredResource(domainKey string) (*certificate.Resource, error) {
|
||||||
base := sanitizeDomainKey(domainKey)
|
base := SanitizeDomainKey(domainKey)
|
||||||
dir := filepath.Join(m.certsRoot, base)
|
dir := filepath.Join(m.CertsRoot, base)
|
||||||
raw, err := os.ReadFile(filepath.Join(dir, base+".json"))
|
raw, err := os.ReadFile(filepath.Join(dir, base+".json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -623,8 +622,8 @@ func (m *ACMEManager) loadStoredResource(domainKey string) (*certificate.Resourc
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *ACMEManager) loadMeta(domainKey string) (*StoredCertMeta, error) {
|
func (m *ACMEManager) loadMeta(domainKey string) (*StoredCertMeta, error) {
|
||||||
base := sanitizeDomainKey(domainKey)
|
base := SanitizeDomainKey(domainKey)
|
||||||
dir := filepath.Join(m.certsRoot, base)
|
dir := filepath.Join(m.CertsRoot, base)
|
||||||
raw, err := os.ReadFile(filepath.Join(dir, base+".meta.json"))
|
raw, err := os.ReadFile(filepath.Join(dir, base+".meta.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
6
internal/buildinfo.go
Normal file
6
internal/buildinfo.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
Build = "local"
|
||||||
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -67,19 +67,48 @@ func (s *DomainConfigStore) Snapshot() map[string]*viper.Viper {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
config *viper.Viper
|
config *viper.Viper
|
||||||
|
configMu sync.RWMutex
|
||||||
domainStore = NewDomainConfigStore()
|
domainStore = NewDomainConfigStore()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func Config() *viper.Viper {
|
||||||
|
configMu.RLock()
|
||||||
|
defer configMu.RUnlock()
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func DomainStore() *DomainConfigStore {
|
||||||
|
domainStore.mu.RLock()
|
||||||
|
defer domainStore.mu.RUnlock()
|
||||||
|
return domainStore
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Loading
|
// Loading
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// LoadConfig reads the main certman.conf into config.
|
// LoadConfig reads the main certman.conf into config.
|
||||||
func LoadConfig(path string) error {
|
func LoadConfig() error {
|
||||||
config = viper.New()
|
config = viper.New()
|
||||||
config.SetConfigFile(path)
|
config.SetConfigFile("/etc/certman/certman.conf")
|
||||||
config.SetConfigType("toml")
|
config.SetConfigType("toml")
|
||||||
return config.ReadInConfig()
|
err := config.ReadInConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch config.GetString("App.mode") {
|
||||||
|
case "server":
|
||||||
|
config.SetConfigType("toml")
|
||||||
|
config.SetConfigFile("server.conf")
|
||||||
|
return config.MergeInConfig()
|
||||||
|
case "Client":
|
||||||
|
config.SetConfigType("toml")
|
||||||
|
config.SetConfigFile("Client.conf")
|
||||||
|
return config.MergeInConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadDomainConfigs reads every .conf file in the domains directory.
|
// LoadDomainConfigs reads every .conf file in the domains directory.
|
||||||
@@ -223,7 +252,7 @@ func EffectiveBool(domainCfg *viper.Viper, key string) bool {
|
|||||||
// Directory bootstrapping
|
// Directory bootstrapping
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func makeDirs() {
|
func MakeDirs() {
|
||||||
dirs := []struct {
|
dirs := []struct {
|
||||||
path string
|
path string
|
||||||
perm os.FileMode
|
perm os.FileMode
|
||||||
@@ -240,31 +269,49 @@ func makeDirs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createNewConfig(mode string) {
|
func CreateConfig(mode string) {
|
||||||
content := strings.NewReplacer(
|
content := strings.NewReplacer(
|
||||||
"{mode}", mode,
|
"{mode}", mode,
|
||||||
"{uuid}", uuid.New().String(),
|
|
||||||
).Replace(defaultConfig)
|
).Replace(defaultConfig)
|
||||||
createFile("/etc/certman/certman.conf", 0640, []byte(content))
|
createFile("/etc/certman/certman.conf", 0640, []byte(content))
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case "server":
|
||||||
|
content = strings.NewReplacer(
|
||||||
|
"{uuid}", uuid.New().String(),
|
||||||
|
).Replace(defaultServerConfig)
|
||||||
|
createFile("/etc/certman/server.conf", 640, []byte(content))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createNewDomainConfig(domain string) error {
|
func CreateDomainConfig(domain string) error {
|
||||||
key, err := GenerateKey()
|
key, err := GenerateKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to generate key: %v", err)
|
return fmt.Errorf("unable to generate key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
content := strings.NewReplacer(
|
var content string
|
||||||
"{domain}", domain,
|
switch Config().GetString("App.mode") {
|
||||||
"{key}", key,
|
case "server":
|
||||||
).Replace(defaultDomainConfig)
|
content = strings.NewReplacer(
|
||||||
|
"{domain}", domain,
|
||||||
|
"{key}", key,
|
||||||
|
).Replace(defaultServerDomainConfig)
|
||||||
|
case "Client":
|
||||||
|
content = strings.NewReplacer(
|
||||||
|
"{domain}", domain,
|
||||||
|
"{key}", key,
|
||||||
|
).Replace(defaultClientDomainConfig)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown certman mode: %v", Config().GetString("App.mode"))
|
||||||
|
}
|
||||||
|
|
||||||
path := filepath.Join("/etc/certman/domains", domain+".conf")
|
path := filepath.Join("/etc/certman/domains", domain+".conf")
|
||||||
createFile(path, 0640, []byte(content))
|
createFile(path, 0640, []byte(content))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createNewDomainCertsDir(domain string, dir string, dirOverride bool) {
|
func CreateDomainCertsDir(domain string, dir string, dirOverride bool) {
|
||||||
var target string
|
var target string
|
||||||
if dirOverride {
|
if dirOverride {
|
||||||
target = filepath.Join(dir, domain)
|
target = filepath.Join(dir, domain)
|
||||||
@@ -286,49 +333,68 @@ func createNewDomainCertsDir(domain string, dir string, dirOverride bool) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const defaultConfig = `[App]
|
const defaultConfig = `[App]
|
||||||
mode = "{mode}"
|
mode = '{mode}'
|
||||||
tick_rate = 2
|
tick_rate = 2
|
||||||
uuid = "{uuid}"
|
|
||||||
|
|
||||||
[Git]
|
[Git]
|
||||||
host = "gitea"
|
host = 'gitea'
|
||||||
server = "https://gitea.instance.com"
|
server = 'https://gitea.instance.com'
|
||||||
username = "user"
|
username = 'User'
|
||||||
api_token = "xxxxxxxxxxxxxxxxxxxxxxxxx"
|
api_token = 'xxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||||
org_name = "org"
|
org_name = 'org'
|
||||||
|
|
||||||
[Certificates]
|
[Certificates]
|
||||||
email = "user@example.com"
|
data_root = '/var/local/certman'
|
||||||
data_root = "/var/local/certman"
|
|
||||||
ca_dir_url = "https://acme-v02.api.letsencrypt.org/directory"
|
|
||||||
|
|
||||||
[Cloudflare]
|
|
||||||
cf_email = "email@example.com"
|
|
||||||
cf_api_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const defaultDomainConfig = `[Domain]
|
const defaultServerConfig = `[App]
|
||||||
domain_name = "{domain}"
|
uuid = '{uuid}'
|
||||||
enabled = true
|
|
||||||
dns_server = "default"
|
|
||||||
|
|
||||||
[Certificates]
|
[Certificates]
|
||||||
data_root = ""
|
email = 'User@example.com'
|
||||||
|
data_root = '/var/local/certman'
|
||||||
|
ca_dir_url = 'https://acme-v02.api.letsencrypt.org/directory'
|
||||||
|
|
||||||
|
[Cloudflare]
|
||||||
|
cf_email = 'email@example.com'
|
||||||
|
cf_api_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx'`
|
||||||
|
|
||||||
|
const defaultClientConfig = ``
|
||||||
|
|
||||||
|
const defaultServerDomainConfig = `[Domain]
|
||||||
|
domain_name = '{domain}'
|
||||||
|
enabled = true
|
||||||
|
dns_server = 'default'
|
||||||
|
|
||||||
|
[Certificates]
|
||||||
|
data_root = ''
|
||||||
expiry = 90
|
expiry = 90
|
||||||
request_method = "dns-01"
|
request_method = 'dns-01'
|
||||||
renew_period = 30
|
renew_period = 30
|
||||||
subdomains = []
|
subdomains = []
|
||||||
cert_symlinks = []
|
crypto_key = '{key}'
|
||||||
key_symlinks = []
|
|
||||||
crypto_key = "{key}"
|
|
||||||
|
|
||||||
[Repo]
|
[Repo]
|
||||||
repo_suffix = "-certificates"
|
repo_suffix = '-certificates'
|
||||||
|
|
||||||
[Internal]
|
[Internal]
|
||||||
last_issued = 0
|
last_issued = 0
|
||||||
repo_exists = false
|
repo_exists = false
|
||||||
status = "clean"
|
status = 'clean'
|
||||||
|
`
|
||||||
|
|
||||||
|
const defaultClientDomainConfig = `[Certificates]
|
||||||
|
data_root = ''
|
||||||
|
cert_symlinks = []
|
||||||
|
key_symlinks = []
|
||||||
|
crypto_key = '{key}'
|
||||||
|
|
||||||
|
[Domain]
|
||||||
|
domain_name = '{domain}'
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[Repo]
|
||||||
|
repo_suffix = '-certificates'
|
||||||
`
|
`
|
||||||
|
|
||||||
const readme = ``
|
const readme = ``
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -17,10 +16,16 @@ import (
|
|||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||||
"github.com/go-git/go-git/v5/storage/memory"
|
"github.com/go-git/go-git/v5/storage/memory"
|
||||||
"github.com/google/go-github/v55/github"
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CertManMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Server CertManMode = iota
|
||||||
|
Client
|
||||||
|
)
|
||||||
|
|
||||||
type GitWorkspace struct {
|
type GitWorkspace struct {
|
||||||
Repo *git.Repository
|
Repo *git.Repository
|
||||||
Storage *memory.Storage
|
Storage *memory.Storage
|
||||||
@@ -48,7 +53,7 @@ var GitSourceName = map[GitSource]string{
|
|||||||
CodeCommit: "code-commit",
|
CodeCommit: "code-commit",
|
||||||
}
|
}
|
||||||
|
|
||||||
func strToGitSource(s string) (GitSource, error) {
|
func StrToGitSource(s string) (GitSource, error) {
|
||||||
for k, v := range GitSourceName {
|
for k, v := range GitSourceName {
|
||||||
if v == s {
|
if v == s {
|
||||||
return k, nil
|
return k, nil
|
||||||
@@ -57,11 +62,11 @@ func strToGitSource(s string) (GitSource, error) {
|
|||||||
return GitSource(0), errors.New("invalid gitsource name")
|
return GitSource(0), errors.New("invalid gitsource name")
|
||||||
}
|
}
|
||||||
|
|
||||||
func createGithubClient() *github.Client {
|
//func createGithubClient() *github.Client {
|
||||||
return github.NewClient(nil).WithAuthToken(config.GetString("Git.api_token"))
|
// return github.NewClient(nil).WithAuthToken(config.GetString("Git.api_token"))
|
||||||
}
|
//}
|
||||||
|
|
||||||
func createGiteaClient() *gitea.Client {
|
func CreateGiteaClient() *gitea.Client {
|
||||||
client, err := gitea.NewClient(config.GetString("Git.server"), gitea.SetToken(config.GetString("Git.api_token")))
|
client, err := gitea.NewClient(config.GetString("Git.server"), gitea.SetToken(config.GetString("Git.api_token")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error connecting to gitea instance: %v\n", err)
|
fmt.Printf("Error connecting to gitea instance: %v\n", err)
|
||||||
@@ -70,30 +75,30 @@ func createGiteaClient() *gitea.Client {
|
|||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
func createGithubRepo(domain *Domain, client *github.Client) string {
|
//func createGithubRepo(domain *Domain, Client *github.Client) string {
|
||||||
name := domain.name
|
// name := domain.name
|
||||||
owner := domain.config.GetString("Repo.owner")
|
// owner := domain.config.GetString("Repo.owner")
|
||||||
description := domain.description
|
// description := domain.description
|
||||||
private := true
|
// private := true
|
||||||
includeAllBranches := false
|
// includeAllBranches := false
|
||||||
|
//
|
||||||
|
// ctx := context.Background()
|
||||||
|
// template := &github.TemplateRepoRequest{
|
||||||
|
// Name: name,
|
||||||
|
// Owner: &owner,
|
||||||
|
// Description: description,
|
||||||
|
// Private: &private,
|
||||||
|
// IncludeAllBranches: &includeAllBranches,
|
||||||
|
// }
|
||||||
|
// repo, _, err := Client.Repositories.CreateFromTemplate(ctx, config.GetString("Git.org_name"), config.GetString("Git.template_name"), template)
|
||||||
|
// if err != nil {
|
||||||
|
// fmt.Println("Error creating repository from template,", err)
|
||||||
|
// return ""
|
||||||
|
// }
|
||||||
|
// return *repo.CloneURL
|
||||||
|
//}
|
||||||
|
|
||||||
ctx := context.Background()
|
func CreateGiteaRepo(domain string, giteaClient *gitea.Client) string {
|
||||||
template := &github.TemplateRepoRequest{
|
|
||||||
Name: name,
|
|
||||||
Owner: &owner,
|
|
||||||
Description: description,
|
|
||||||
Private: &private,
|
|
||||||
IncludeAllBranches: &includeAllBranches,
|
|
||||||
}
|
|
||||||
repo, _, err := client.Repositories.CreateFromTemplate(ctx, config.GetString("Git.org_name"), config.GetString("Git.template_name"), template)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error creating repository from template,", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return *repo.CloneURL
|
|
||||||
}
|
|
||||||
|
|
||||||
func createGiteaRepo(domain string, giteaClient *gitea.Client) string {
|
|
||||||
domainConfig, exists := domainStore.Get(domain)
|
domainConfig, exists := domainStore.Get(domain)
|
||||||
if !exists {
|
if !exists {
|
||||||
fmt.Printf("Domain %s config does not exist\n", domain)
|
fmt.Printf("Domain %s config does not exist\n", domain)
|
||||||
@@ -121,7 +126,7 @@ func createGiteaRepo(domain string, giteaClient *gitea.Client) string {
|
|||||||
return giteaRepo.CloneURL
|
return giteaRepo.CloneURL
|
||||||
}
|
}
|
||||||
|
|
||||||
func initRepo(url string, ws *GitWorkspace) error {
|
func InitRepo(url string, ws *GitWorkspace) error {
|
||||||
var err error
|
var err error
|
||||||
ws.Repo, err = git.Init(ws.Storage, ws.FS)
|
ws.Repo, err = git.Init(ws.Storage, ws.FS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -147,7 +152,7 @@ func initRepo(url string, ws *GitWorkspace) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneRepo(url string, ws *GitWorkspace) error {
|
func CloneRepo(url string, ws *GitWorkspace, certmanMode CertManMode) error {
|
||||||
creds := &http.BasicAuth{
|
creds := &http.BasicAuth{
|
||||||
Username: config.GetString("Git.username"),
|
Username: config.GetString("Git.username"),
|
||||||
Password: config.GetString("Git.api_token"),
|
Password: config.GetString("Git.api_token"),
|
||||||
@@ -163,33 +168,35 @@ func cloneRepo(url string, ws *GitWorkspace) error {
|
|||||||
fmt.Printf("Error getting worktree from cloned repo: %v\n", err)
|
fmt.Printf("Error getting worktree from cloned repo: %v\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
serverIdFile, err := ws.FS.OpenFile("/SERVER_ID", os.O_RDWR, 0640)
|
if certmanMode == Server {
|
||||||
if err != nil {
|
serverIdFile, err := ws.FS.OpenFile("/SERVER_ID", os.O_RDWR, 0640)
|
||||||
if os.IsNotExist(err) {
|
if err != nil {
|
||||||
fmt.Printf("Server ID file not found for %s, adopting domain\n", url)
|
if os.IsNotExist(err) {
|
||||||
return nil
|
fmt.Printf("Server ID file not found for %s, adopting domain\n", url)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
serverIdBytes, err := io.ReadAll(serverIdFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
serverId := strings.TrimSpace(string(serverIdBytes))
|
||||||
|
if serverId != config.GetString("App.uuid") {
|
||||||
|
return fmt.Errorf("domain is already managed by server with uuid %s", serverId)
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
|
||||||
serverIdBytes, err := io.ReadAll(serverIdFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
serverId := strings.TrimSpace(string(serverIdBytes))
|
|
||||||
if serverId != config.GetString("App.uuid") {
|
|
||||||
return fmt.Errorf("domain is already managed by server with uuid %s", serverId)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addAndPushCerts(domain string, ws *GitWorkspace) error {
|
func AddAndPushCerts(domain string, ws *GitWorkspace) error {
|
||||||
domainConfig, exists := domainStore.Get(domain)
|
domainConfig, exists := domainStore.Get(domain)
|
||||||
if !exists {
|
if !exists {
|
||||||
fmt.Printf("Domain %s config does not exist\n", domain)
|
fmt.Printf("Domain %s config does not exist\n", domain)
|
||||||
return ErrConfigNotFound
|
return ErrConfigNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
certsDir, err := getDomainCertsDirWConf(domain, domainConfig)
|
certsDir, err := DomainCertsDirWConf(domain, domainConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrConfigNotFound) {
|
if errors.Is(err, ErrConfigNotFound) {
|
||||||
fmt.Printf("Domain %s config not found: %v\n", domain, err)
|
fmt.Printf("Domain %s config not found: %v\n", domain, err)
|
||||||
@@ -301,8 +308,8 @@ func addAndPushCerts(domain string, ws *GitWorkspace) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeCommitHash(hash string, domainConfig *viper.Viper) error {
|
func WriteCommitHash(hash string, domainConfig *viper.Viper) error {
|
||||||
certsDir, err := getDomainCertsDirWOnlyConf(domainConfig)
|
certsDir, err := DomainCertsDirWOnlyConf(domainConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrConfigNotFound) {
|
if errors.Is(err, ErrConfigNotFound) {
|
||||||
return err
|
return err
|
||||||
@@ -318,8 +325,8 @@ func writeCommitHash(hash string, domainConfig *viper.Viper) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLocalCommitHash(domain string) (string, error) {
|
func LocalCommitHash(domain string) (string, error) {
|
||||||
certsDir, err := getDomainCertsDir(domain)
|
certsDir, err := DomainCertsDir(domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrConfigNotFound) {
|
if errors.Is(err, ErrConfigNotFound) {
|
||||||
fmt.Printf("Domain %s config not found: %v\n", domain, err)
|
fmt.Printf("Domain %s config not found: %v\n", domain, err)
|
||||||
@@ -330,15 +337,17 @@ func getLocalCommitHash(domain string) (string, error) {
|
|||||||
|
|
||||||
data, err := os.ReadFile(filepath.Join(certsDir, "hash"))
|
data, err := os.ReadFile(filepath.Join(certsDir, "hash"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error reading file for domain %s: %v\n", domain, err)
|
if !os.IsNotExist(err) {
|
||||||
return "", err
|
fmt.Printf("Error reading file for domain %s: %v\n", domain, err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.TrimSpace(string(data)), nil
|
return strings.TrimSpace(string(data)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRemoteCommitHash(domain string, gitSource GitSource) (string, error) {
|
func RemoteCommitHash(domain string, gitSource GitSource) (string, error) {
|
||||||
domainConfig, exists := domainStore.Get(domain)
|
domainConfig, exists := DomainStore().Get(domain)
|
||||||
if !exists {
|
if !exists {
|
||||||
fmt.Printf("Domain %s config does not exist\n", domain)
|
fmt.Printf("Domain %s config does not exist\n", domain)
|
||||||
return "", ErrConfigNotFound
|
return "", ErrConfigNotFound
|
||||||
@@ -354,7 +363,7 @@ func getRemoteCommitHash(domain string, gitSource GitSource) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getRemoteCommitHashGitea(org, repo, branchName string) (string, error) {
|
func getRemoteCommitHashGitea(org, repo, branchName string) (string, error) {
|
||||||
giteaClient := createGiteaClient()
|
giteaClient := CreateGiteaClient()
|
||||||
branch, _, err := giteaClient.GetRepoBranch(org, repo, branchName)
|
branch, _, err := giteaClient.GetRepoBranch(org, repo, branchName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error getting repo branch: %v\n", err)
|
fmt.Printf("Error getting repo branch: %v\n", err)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
"github.com/google/go-github/v55/github"
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +24,6 @@ type Domain struct {
|
|||||||
name *string
|
name *string
|
||||||
config *viper.Viper
|
config *viper.Viper
|
||||||
description *string
|
description *string
|
||||||
ghClient *github.Client
|
|
||||||
gtClient *gitea.Client
|
gtClient *gitea.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +42,7 @@ func createPIDFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 0x02
|
// 0x02
|
||||||
func clearPIDFile() {
|
func ClearPIDFile() {
|
||||||
file, err := os.OpenFile("/var/run/certman.pid", os.O_RDWR|os.O_CREATE, 0644)
|
file, err := os.OpenFile("/var/run/certman.pid", os.O_RDWR|os.O_CREATE, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("0x02: Error opening PID file: %v\n", err)
|
fmt.Printf("0x02: Error opening PID file: %v\n", err)
|
||||||
@@ -60,7 +58,7 @@ func clearPIDFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 0x03
|
// 0x03
|
||||||
func createOrUpdatePIDFile(filename string) error {
|
func CreateOrUpdatePIDFile(filename string) error {
|
||||||
pidBytes, err := os.ReadFile(filename)
|
pidBytes, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("0x03: Error reading PID file: %v\n", err)
|
fmt.Printf("0x03: Error reading PID file: %v\n", err)
|
||||||
@@ -142,7 +140,7 @@ func isProcessActive(pid int) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 0x05
|
// 0x05
|
||||||
func getDaemonProcess() (*os.Process, error) {
|
func DaemonProcess() (*os.Process, error) {
|
||||||
pidBytes, err := os.ReadFile("/var/run/certman.pid")
|
pidBytes, err := os.ReadFile("/var/run/certman.pid")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("0x05: Error getting PID from /var/run/certman.pid: %v\n", err)
|
fmt.Printf("0x05: Error getting PID from /var/run/certman.pid: %v\n", err)
|
||||||
@@ -209,15 +207,20 @@ func createFile(fileName string, filePermission os.FileMode, data []byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkFile(source, target, domain, extension string) error {
|
func LinkFile(source, target, domain, extension string) error {
|
||||||
if target == "" {
|
if target == "" {
|
||||||
return ErrBlankCert
|
return ErrBlankCert
|
||||||
}
|
}
|
||||||
linkInfo, err := os.Stat(target)
|
linkInfo, err := os.Stat(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return err
|
err = os.Symlink(source, target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
if linkInfo.IsDir() {
|
if linkInfo.IsDir() {
|
||||||
target = filepath.Join(target, domain+extension)
|
target = filepath.Join(target, domain+extension)
|
||||||
@@ -252,24 +255,24 @@ func insert(a []string, index int, value string) []string {
|
|||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitizeDomainKey(s string) string {
|
func SanitizeDomainKey(s string) string {
|
||||||
s = strings.TrimSpace(strings.ToLower(s))
|
s = strings.TrimSpace(strings.ToLower(s))
|
||||||
r := strings.NewReplacer("/", "_", "\\", "_", " ", "_", ":", "_")
|
r := strings.NewReplacer("/", "_", "\\", "_", " ", "_", ":", "_")
|
||||||
return r.Replace(s)
|
return r.Replace(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDomainCertsDir Can return ErrBlankConfigEntry, ErrConfigNotFound, or other errors
|
// DomainCertsDir Can return ErrBlankConfigEntry, ErrConfigNotFound, or other errors
|
||||||
func getDomainCertsDir(domain string) (string, error) {
|
func DomainCertsDir(domain string) (string, error) {
|
||||||
domainConfig, exists := domainStore.Get(domain)
|
domainConfig, exists := domainStore.Get(domain)
|
||||||
if !exists {
|
if !exists {
|
||||||
return "", ErrConfigNotFound
|
return "", ErrConfigNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return getDomainCertsDirWConf(domain, domainConfig)
|
return DomainCertsDirWConf(domain, domainConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDomainCertsDir Can return ErrBlankConfigEntry or other errors
|
// DomainCertsDirWConf Can return ErrBlankConfigEntry or other errors
|
||||||
func getDomainCertsDirWConf(domain string, domainConfig *viper.Viper) (string, error) {
|
func DomainCertsDirWConf(domain string, domainConfig *viper.Viper) (string, error) {
|
||||||
effectiveDataRoot, err := EffectiveString(domainConfig, "Certificates.data_root")
|
effectiveDataRoot, err := EffectiveString(domainConfig, "Certificates.data_root")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -278,9 +281,9 @@ func getDomainCertsDirWConf(domain string, domainConfig *viper.Viper) (string, e
|
|||||||
return filepath.Join(effectiveDataRoot, "certificates", domain), nil
|
return filepath.Join(effectiveDataRoot, "certificates", domain), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDomainCertsDirWOnlyConf(domainConfig *viper.Viper) (string, error) {
|
func DomainCertsDirWOnlyConf(domainConfig *viper.Viper) (string, error) {
|
||||||
domain := domainConfig.GetString("Domain.domain_name")
|
domain := domainConfig.GetString("Domain.domain_name")
|
||||||
return getDomainCertsDirWConf(domain, domainConfig)
|
return DomainCertsDirWConf(domain, domainConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ChownRecursive(path string, uid, gid int) error {
|
func ChownRecursive(path string, uid, gid int) error {
|
||||||
46
main.go
46
main.go
@@ -1,24 +1,16 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
|
||||||
|
|
||||||
|
"git.nevets.tech/Keys/CertManager/commands"
|
||||||
|
"git.nevets.tech/Keys/CertManager/internal"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.0.0"
|
var configFile string
|
||||||
var build = "1"
|
|
||||||
|
|
||||||
var (
|
|
||||||
configFile string
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
wg sync.WaitGroup
|
|
||||||
)
|
|
||||||
|
|
||||||
var fqdnRegex = regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$`)
|
var fqdnRegex = regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$`)
|
||||||
|
|
||||||
@@ -36,9 +28,9 @@ func main() {
|
|||||||
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "/etc/certman/certman.conf", "Configuration file")
|
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "/etc/certman/certman.conf", "Configuration file")
|
||||||
|
|
||||||
rootCmd.AddCommand(basicCmd("version", "Show version", versionResponse))
|
rootCmd.AddCommand(basicCmd("version", "Show version", commands.VersionCmd))
|
||||||
rootCmd.AddCommand(basicCmd("gen-key", "Generates encryption key", newKey))
|
rootCmd.AddCommand(basicCmd("gen-key", "Generates encryption key", commands.NewKeyCmd))
|
||||||
rootCmd.AddCommand(basicCmd("dev", "Dev Function", devFunc))
|
rootCmd.AddCommand(basicCmd("dev", "Dev Function", commands.DevCmd))
|
||||||
|
|
||||||
var domainCertDir string
|
var domainCertDir string
|
||||||
newDomainCmd := &cobra.Command{
|
newDomainCmd := &cobra.Command{
|
||||||
@@ -49,7 +41,7 @@ func main() {
|
|||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
dirOverridden := cmd.Flags().Changed("dir")
|
dirOverridden := cmd.Flags().Changed("dir")
|
||||||
return newDomain(args[0], domainCertDir, dirOverridden)
|
return commands.NewDomainCmd(args[0], domainCertDir, dirOverridden)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
newDomainCmd.Flags().StringVar(&domainCertDir, "dir", "/var/local/certman/certificates/", "Alternate directory for certificates")
|
newDomainCmd.Flags().StringVar(&domainCertDir, "dir", "/var/local/certman/certificates/", "Alternate directory for certificates")
|
||||||
@@ -65,7 +57,7 @@ func main() {
|
|||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
switch modeFlag {
|
switch modeFlag {
|
||||||
case "server", "client":
|
case "server", "client":
|
||||||
return install(thinInstallFlag, modeFlag)
|
return commands.InstallCmd(thinInstallFlag, modeFlag)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid --mode %q (must be server or client)", modeFlag)
|
return fmt.Errorf("invalid --mode %q (must be server or client)", modeFlag)
|
||||||
}
|
}
|
||||||
@@ -89,12 +81,22 @@ func main() {
|
|||||||
Short: "Renews a domains certificate",
|
Short: "Renews a domains certificate",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return renewCertFunc(args[0], noPush)
|
return commands.RenewCertCmd(args[0], noPush, internal.Server)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
renewCertCmd.Flags().BoolVar(&noPush, "no-push", false, "Don't push certs to repo, renew locally only [server mode only]")
|
renewCertCmd.Flags().BoolVar(&noPush, "no-push", false, "Don't push certs to repo, renew locally only [server mode only]")
|
||||||
certCmd.AddCommand(renewCertCmd)
|
certCmd.AddCommand(renewCertCmd)
|
||||||
|
|
||||||
|
updateCertLinkCmd := &cobra.Command{
|
||||||
|
Use: "update-link",
|
||||||
|
Short: "Update linked certificates",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return commands.UpdateLinksCmd(args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
certCmd.AddCommand(updateCertLinkCmd)
|
||||||
|
|
||||||
rootCmd.AddCommand(certCmd)
|
rootCmd.AddCommand(certCmd)
|
||||||
|
|
||||||
daemonCmd := &cobra.Command{
|
daemonCmd := &cobra.Command{
|
||||||
@@ -110,7 +112,7 @@ func main() {
|
|||||||
Short: "Start the daemon",
|
Short: "Start the daemon",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runDaemon()
|
return commands.RunDaemonCmd()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -119,7 +121,7 @@ func main() {
|
|||||||
Short: "Stop the daemon",
|
Short: "Stop the daemon",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return stopDaemon()
|
return commands.StopDaemonCmd()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -128,7 +130,7 @@ func main() {
|
|||||||
Short: "Reload daemon configs",
|
Short: "Reload daemon configs",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return reloadDaemon()
|
return commands.ReloadDaemonCmd()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -137,7 +139,7 @@ func main() {
|
|||||||
Short: "Manually triggers daemon tick",
|
Short: "Manually triggers daemon tick",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return tickDaemon()
|
return commands.TickDaemonCmd()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -146,7 +148,7 @@ func main() {
|
|||||||
Short: "Show daemon status",
|
Short: "Show daemon status",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return statusDaemon()
|
return commands.DaemonStatusCmd()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -7,23 +7,24 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.nevets.tech/Keys/CertManager/internal"
|
||||||
"github.com/go-git/go-billy/v5/memfs"
|
"github.com/go-git/go-billy/v5/memfs"
|
||||||
"github.com/go-git/go-git/v5/storage/memory"
|
"github.com/go-git/go-git/v5/storage/memory"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tickMu sync.Mutex
|
tickMu sync.Mutex
|
||||||
mgr *ACMEManager
|
mgr *internal.ACMEManager
|
||||||
mgrMu sync.Mutex
|
mgrMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func getACMEManager() (*ACMEManager, error) {
|
func getACMEManager() (*internal.ACMEManager, error) {
|
||||||
mgrMu.Lock()
|
mgrMu.Lock()
|
||||||
defer mgrMu.Unlock()
|
defer mgrMu.Unlock()
|
||||||
|
|
||||||
if mgr == nil {
|
if mgr == nil {
|
||||||
var err error
|
var err error
|
||||||
mgr, err = NewACMEManager()
|
mgr, err = internal.NewACMEManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -31,21 +32,22 @@ func getACMEManager() (*ACMEManager, error) {
|
|||||||
return mgr, nil
|
return mgr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initServer() {
|
func Init() {
|
||||||
err := LoadDomainConfigs()
|
err := internal.LoadDomainConfigs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error loading domain configs: %v", err)
|
log.Fatalf("Error loading domain configs: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
serverTick()
|
Tick()
|
||||||
}
|
}
|
||||||
|
|
||||||
func serverTick() {
|
func Tick() {
|
||||||
tickMu.Lock()
|
tickMu.Lock()
|
||||||
defer tickMu.Unlock()
|
defer tickMu.Unlock()
|
||||||
fmt.Println("Tick!")
|
fmt.Println("Tick!")
|
||||||
|
|
||||||
mgr, err := getACMEManager()
|
var err error
|
||||||
|
mgr, err = getACMEManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error getting acme manager: %v\n", err)
|
fmt.Printf("Error getting acme manager: %v\n", err)
|
||||||
return
|
return
|
||||||
@@ -53,7 +55,7 @@ func serverTick() {
|
|||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
localDomainConfigs := domainStore.Snapshot()
|
localDomainConfigs := internal.DomainStore().Snapshot()
|
||||||
|
|
||||||
for domainStr, domainConfig := range localDomainConfigs {
|
for domainStr, domainConfig := range localDomainConfigs {
|
||||||
if !domainConfig.GetBool("Domain.enabled") {
|
if !domainConfig.GetBool("Domain.enabled") {
|
||||||
@@ -75,62 +77,62 @@ func serverTick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
domainConfig.Set("Internal.last_issued", time.Now().UTC().Unix())
|
domainConfig.Set("Internal.last_issued", time.Now().UTC().Unix())
|
||||||
err = WriteDomainConfig(domainConfig)
|
err = internal.WriteDomainConfig(domainConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error saving domain config %s: %v\n", domainStr, err)
|
fmt.Printf("Error saving domain config %s: %v\n", domainStr, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".crt"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".crt.crpt"), nil)
|
err = internal.EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), filepath.Join(mgr.CertsRoot, domainStr, domainStr+".crt"), filepath.Join(mgr.CertsRoot, domainStr, domainStr+".crt.crpt"), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error encrypting domain cert for domain %s: %v\n", domainStr, err)
|
fmt.Printf("Error encrypting domain cert for domain %s: %v\n", domainStr, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err = EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".key"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".key.crpt"), nil)
|
err = internal.EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), filepath.Join(mgr.CertsRoot, domainStr, domainStr+".key"), filepath.Join(mgr.CertsRoot, domainStr, domainStr+".key.crpt"), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error encrypting domain key for domain %s: %v\n", domainStr, err)
|
fmt.Printf("Error encrypting domain key for domain %s: %v\n", domainStr, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
giteaClient := createGiteaClient()
|
giteaClient := internal.CreateGiteaClient()
|
||||||
if giteaClient == nil {
|
if giteaClient == nil {
|
||||||
fmt.Printf("Error creating gitea client for domain %s: %v\n", domainStr, err)
|
fmt.Printf("Error creating gitea client for domain %s: %v\n", domainStr, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
gitWorkspace := &GitWorkspace{
|
gitWorkspace := &internal.GitWorkspace{
|
||||||
Storage: memory.NewStorage(),
|
Storage: memory.NewStorage(),
|
||||||
FS: memfs.New(),
|
FS: memfs.New(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var repoUrl string
|
var repoUrl string
|
||||||
if !domainConfig.GetBool("Internal.repo_exists") {
|
if !domainConfig.GetBool("Internal.repo_exists") {
|
||||||
repoUrl = createGiteaRepo(domainStr, giteaClient)
|
repoUrl = internal.CreateGiteaRepo(domainStr, giteaClient)
|
||||||
if repoUrl == "" {
|
if repoUrl == "" {
|
||||||
fmt.Printf("Error creating Gitea repo for domain %s\n", domainStr)
|
fmt.Printf("Error creating Gitea repo for domain %s\n", domainStr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
domainConfig.Set("Internal.repo_exists", true)
|
domainConfig.Set("Internal.repo_exists", true)
|
||||||
err = WriteDomainConfig(domainConfig)
|
err = internal.WriteDomainConfig(domainConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error saving domain config %s: %v\n", domainStr, err)
|
fmt.Printf("Error saving domain config %s: %v\n", domainStr, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = initRepo(repoUrl, gitWorkspace)
|
err = internal.InitRepo(repoUrl, gitWorkspace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error initializing repo for domain %s: %v\n", domainStr, err)
|
fmt.Printf("Error initializing repo for domain %s: %v\n", domainStr, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
repoUrl = config.GetString("Git.server") + "/" + config.GetString("Git.org_name") + "/" + domainStr + domainConfig.GetString("Repo.repo_suffix") + ".git"
|
repoUrl = internal.Config().GetString("Git.server") + "/" + internal.Config().GetString("Git.org_name") + "/" + domainStr + domainConfig.GetString("Repo.repo_suffix") + ".git"
|
||||||
err = cloneRepo(repoUrl, gitWorkspace)
|
err = internal.CloneRepo(repoUrl, gitWorkspace, internal.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error cloning repo for domain %s: %v\n", domainStr, err)
|
fmt.Printf("Error cloning repo for domain %s: %v\n", domainStr, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = addAndPushCerts(domainStr, gitWorkspace)
|
err = internal.AddAndPushCerts(domainStr, gitWorkspace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error pushing certificates for domain %s: %v\n", domainStr, err)
|
fmt.Printf("Error pushing certificates for domain %s: %v\n", domainStr, err)
|
||||||
continue
|
continue
|
||||||
@@ -138,15 +140,15 @@ func serverTick() {
|
|||||||
fmt.Printf("Successfully pushed certificates for domain %s\n", domainStr)
|
fmt.Printf("Successfully pushed certificates for domain %s\n", domainStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = SaveDomainConfigs()
|
err = internal.SaveDomainConfigs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error saving domain configs: %v\n", err)
|
fmt.Printf("Error saving domain configs: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadServer() {
|
func Reload() {
|
||||||
fmt.Println("Reloading configs...")
|
fmt.Println("Reloading configs...")
|
||||||
err := LoadDomainConfigs()
|
err := internal.LoadDomainConfigs()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error loading domain configs: %v\n", err)
|
fmt.Printf("Error loading domain configs: %v\n", err)
|
||||||
@@ -160,6 +162,6 @@ func reloadServer() {
|
|||||||
fmt.Println("Successfully reloaded configs")
|
fmt.Println("Successfully reloaded configs")
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopServer() {
|
func Stop() {
|
||||||
fmt.Println("Shutting down server")
|
fmt.Println("Shutting down server")
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user