Moved from ini to toml, fixed installation and new-domain permissions issues
This commit is contained in:
329
commands.go
329
commands.go
@@ -4,16 +4,24 @@ 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 devCmd(cmd *cobra.Command, args []string) {
|
||||
func devFunc(cmd *cobra.Command, args []string) {
|
||||
testDomain := "lunamc.org"
|
||||
//config, err = ezconf.LoadConfiguration("/etc/certman/certman.conf")
|
||||
err := LoadConfig("/etc/certman/certman.conf")
|
||||
@@ -28,11 +36,11 @@ func devCmd(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(testDomain)
|
||||
}
|
||||
|
||||
func versionCmd(cmd *cobra.Command, args []string) {
|
||||
func versionResponse(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("CertManager (certman) - Steven Tracey\nVersion: " + version + " build-" + build)
|
||||
}
|
||||
|
||||
func newKeyCmd(cmd *cobra.Command, args []string) {
|
||||
func newKey(cmd *cobra.Command, args []string) {
|
||||
key, err := GenerateKey()
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
@@ -48,20 +56,304 @@ func newDomain(domain, domainDir string, dirOverridden bool) error {
|
||||
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)
|
||||
}
|
||||
//config, err = ezconf.NewConfiguration(configFile, strings.ReplaceAll(defaultConfig, "{mode}", mode))
|
||||
createNewConfig(mode)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runDaemonCmd() error {
|
||||
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) {
|
||||
@@ -92,6 +384,10 @@ func runDaemonCmd() error {
|
||||
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()
|
||||
@@ -112,6 +408,8 @@ func runDaemonCmd() error {
|
||||
reloadServer()
|
||||
case <-ticker.C:
|
||||
serverTick()
|
||||
case <-tickSigChan:
|
||||
serverTick()
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -151,7 +449,7 @@ func stop() {
|
||||
clearPIDFile()
|
||||
}
|
||||
|
||||
func stopDaemonCmd() error {
|
||||
func stopDaemon() error {
|
||||
proc, err := getDaemonProcess()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting daemon process: %v", err)
|
||||
@@ -164,7 +462,7 @@ func stopDaemonCmd() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func reloadDaemonCmd() error {
|
||||
func reloadDaemon() error {
|
||||
proc, err := getDaemonProcess()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting daemon process: %v", err)
|
||||
@@ -177,7 +475,20 @@ func reloadDaemonCmd() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func statusDaemonCmd() error {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user