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 }