Lots of progress

This commit is contained in:
2026-02-19 22:49:13 +01:00
parent 9ea5b8668f
commit d09c81da5c
15 changed files with 328 additions and 163 deletions

11
.idea/go.imports.xml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="github.com/pkg/errors" />
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>

2
Makefile Normal file
View File

@@ -0,0 +1,2 @@
build:
@go build -o ./certman .

View File

@@ -1,5 +0,0 @@
@echo off
set GOARCH=amd64
set GOOS=linux
go build -o ./certman .

View File

@@ -6,10 +6,11 @@ import (
"crypto/elliptic"
"crypto/rand"
"fmt"
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
"log"
"os"
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
@@ -25,7 +26,7 @@ type User struct {
func (u *User) GetEmail() string {
return u.Email
}
func (u User) GetRegistration() *registration.Resource {
func (u *User) GetRegistration() *registration.Resource {
return u.Registration
}
func (u *User) GetPrivateKey() crypto.PrivateKey {

View File

@@ -4,10 +4,14 @@ import (
"fmt"
"os"
"strings"
"git.nevets.tech/Steven/ezconf"
)
var domainConfCache map[string]*ezconf.Configuration
func makeDirs() {
err := os.MkdirAll("/etc/certman", 0775)
err := os.MkdirAll("/etc/certman", 0644)
if err != nil {
if !os.IsExist(err) {
fmt.Println("Unable to create config directory")
@@ -15,7 +19,7 @@ func makeDirs() {
}
}
err = os.Mkdir("/etc/certman/conf", 0775)
err = os.Mkdir("/etc/certman/domains", 0644)
if err != nil {
if !os.IsExist(err) {
fmt.Println("Unable to create config directory")
@@ -23,7 +27,7 @@ func makeDirs() {
}
}
err = os.Mkdir("/var/local/certman", 0660)
err = os.Mkdir("/var/local/certman", 0640)
if err != nil {
if !os.IsExist(err) {
fmt.Printf("Unable to create certman directory: %v\n", err)
@@ -34,11 +38,20 @@ func makeDirs() {
func createNewDomainConfig(domain string) {
data := []byte(strings.ReplaceAll(defaultDomainConfig, "{domain}", domain))
createFile("/etc/certman/conf/"+domain+".conf", 0755, data)
createFile("/etc/certman/domains/"+domain+".conf", 0755, data)
}
func createNewDomainCertsDir(domain string) {
err := os.Mkdir("/var/local/certman/"+domain, 0660)
func createNewDomainCertsDir(domain string, dir string) {
var err error
if dir == "/opt/certs/example.com" {
err = os.Mkdir("/var/local/certman/"+domain, 0640)
} else {
if strings.HasSuffix(dir, "/") {
err = os.MkdirAll(dir+domain, 0640)
} else {
err = os.Mkdir(dir+"/"+domain, 0640)
}
}
if err != nil {
if os.IsExist(err) {
fmt.Println("Directory already exists...")
@@ -48,3 +61,22 @@ func createNewDomainCertsDir(domain string) {
}
}
}
func getDomainConfig(domain string) *ezconf.Configuration {
if domainConfCache == nil {
domainConfCache = make(map[string]*ezconf.Configuration)
domainConf := ezconf.LoadConfiguration("/etc/certman/domains/" + domain + ".conf")
domainConfCache[domain] = domainConf
return domainConf
}
if domainConfCache[domain] == nil {
domainConf := ezconf.LoadConfiguration("/etc/certman/domains/" + domain + ".conf")
domainConfCache[domain] = domainConf
return domainConf
}
return domainConfCache[domain]
}
func clearDomainConfCache() {
domainConfCache = nil
}

128
crypto.go
View File

@@ -1,17 +1,15 @@
package main
import (
"bufio"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"os"
"strings"
_ "filippo.io/age"
"filippo.io/age/armor"
"golang.org/x/crypto/chacha20poly1305"
)
//var cert *x509.Certificate
@@ -75,86 +73,80 @@ func GenerateKey() (string, error) {
return string(out), nil
}
// LoadKeyFromFile reads a key file that contains either a raw base64 string or
// "AGE_SYM_KEY=<base64>" (handy for dotenv). Whitespace is trimmed.
func LoadKeyFromFile(path string) (string, error) {
b, err := os.ReadFile(path)
func decodeKey(b64 string) ([]byte, error) {
key, err := base64.StdEncoding.DecodeString(b64) // standard padded
if err != nil {
return "", err
return nil, err
}
s := strings.TrimSpace(string(b))
if i := strings.Index(s, "AGE_SYM_KEY="); i >= 0 {
s = strings.TrimSpace(strings.TrimPrefix(s, "AGE_SYM_KEY="))
if len(key) != chacha20poly1305.KeySize {
return nil, fmt.Errorf("bad key length: got %d, want %d", len(key), chacha20poly1305.KeySize)
}
if s == "" {
return "", errors.New("empty symmetric key")
}
// Quick sanity check that its base64 and ~32 bytes after decode.
if _, err := base64.StdEncoding.DecodeString(s); err != nil {
return "", fmt.Errorf("invalid base64 key: %w", err)
}
return s, nil
return key, nil
}
// Encrypt streams plaintext from r to w using a symmetric passphrase.
// If armorOut is true, output is ASCII-armored (BEGIN AGE ENCRYPTED FILE).
func Encrypt(r io.Reader, w io.Writer, passphrase string, armorOut bool) error {
passphrase = strings.TrimSpace(passphrase)
if passphrase == "" {
return errors.New("missing passphrase")
}
var out io.WriteCloser
var err error
if armorOut {
aw := armor.NewWriter(w)
defer aw.Close()
//out, err = age.Encrypt(aw, age.NewScryptRecipient(passphrase))
} else {
//out, err = age.Encrypt(w, age.NewScryptRecipient(passphrase))
}
func EncryptFileXChaCha(keyB64, inPath, outPath string, aad []byte) error {
key, err := decodeKey(keyB64)
if err != nil {
return err
}
_, copyErr := io.Copy(out, bufio.NewReader(r))
closeErr := out.Close()
if copyErr != nil {
return copyErr
}
return closeErr
}
// Decrypt streams ciphertext from r to w using the same symmetric passphrase.
// It auto-detects armored vs binary ciphertext.
func Decrypt(r io.Reader, w io.Writer, passphrase string) error {
passphrase = strings.TrimSpace(passphrase)
if passphrase == "" {
return errors.New("missing passphrase")
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return fmt.Errorf("new aead: %w", err)
}
br := bufio.NewReader(r)
peek, _ := br.Peek(32)
//var in io.Reader = br
if strings.HasPrefix(string(peek), "-----BEGIN AGE ENCRYPTED FILE-----") {
// in = armor.NewReader(br)
plaintext, err := os.ReadFile(inPath)
if err != nil {
return fmt.Errorf("read input: %w", err)
}
//dr, err := age.Decrypt(in, age.NewScryptIdentity(passphrase))
//if err != nil {
// return err
//}
//_, err = io.Copy(w, bufio.NewWriter(wrap0600(w)))
//return err
nonce := make([]byte, chacha20poly1305.NonceSizeX) // 24 bytes
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return fmt.Errorf("nonce: %w", err)
}
ciphertext := aead.Seal(nil, nonce, plaintext, aad)
// Write: nonce || ciphertext
out := make([]byte, 0, len(nonce)+len(ciphertext))
out = append(out, nonce...)
out = append(out, ciphertext...)
if err := os.WriteFile(outPath, out, 0600); err != nil {
return fmt.Errorf("write output: %w", err)
}
return nil
}
// wrap0600 ensures that when w is an *os.File newly created by caller,
// its perms are 0600. If its not an *os.File, its returned unchanged.
func wrap0600(w io.Writer) io.Writer {
if f, ok := w.(*os.File); ok {
_ = f.Chmod(0600)
func DecryptFileXChaCha(keyB64, inPath, outPath string, aad []byte) error {
key, err := decodeKey(keyB64)
if err != nil {
return err
}
return w
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return fmt.Errorf("new aead: %w", err)
}
in, err := os.ReadFile(inPath)
if err != nil {
return fmt.Errorf("read input: %w", err)
}
if len(in) < chacha20poly1305.NonceSizeX {
return errors.New("ciphertext too short")
}
nonce := in[:chacha20poly1305.NonceSizeX]
ciphertext := in[chacha20poly1305.NonceSizeX:]
plaintext, err := aead.Open(nil, nonce, ciphertext, aad)
if err != nil {
return fmt.Errorf("decrypt/auth failed: %w", err)
}
if err := os.WriteFile(outPath, plaintext, 0640); err != nil {
return fmt.Errorf("write output: %w", err)
}
return nil
}

View File

@@ -1,14 +1,17 @@
[App]
mode = {mode}
[Git]
host = gitea
server = https://gitea.instance.com
username = user
api_token = xxxxxxxxxxxxxxxx
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
cf_api_token = xxxxxxxxxxxxxxxx
[Certificates]
data_root = /var/local/certman

19
example.domainconfig.conf Normal file
View File

@@ -0,0 +1,19 @@
[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

View File

@@ -1,6 +0,0 @@
[Domain]
domain_name = example.com
[Certificates]
subdomains =
expiry = 90

View File

@@ -1,6 +0,0 @@
@echo off
set GOARCH=amd64
set GOOS=linux
go install -v -a std
go build -o ./certman .

28
git.go
View File

@@ -1,15 +1,16 @@
package main
import (
"code.gitea.io/sdk/gitea"
"context"
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/google/go-github/v55/github"
"os"
"strings"
"time"
"code.gitea.io/sdk/gitea"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/google/go-github/v55/github"
)
func createGithubClient() *github.Client {
@@ -49,13 +50,14 @@ func createGithubRepo(domain *Domain, client *github.Client) string {
}
func createGiteaRepo() string {
domainConfig := getDomainConfig(domain)
options := gitea.CreateRepoFromTemplateOption{
Avatar: true,
Description: "Certificates storage for " + domain,
GitContent: true,
GitHooks: true,
Labels: true,
Name: domain + "-certificates",
Name: domain + domainConfig.GetAsString("Repo.repo_suffix"),
Owner: config.GetAsString("Git.org_name"),
Private: true,
Topics: true,
@@ -85,19 +87,19 @@ func cloneRepo(url string) (*git.Repository, *git.Worktree) {
}
func addAndPushCerts() {
certs, err := os.ReadDir(config.GetAsString("Certificates.certs_path") + "/certificates")
certFiles, err := os.ReadDir(config.GetAsString("Certificates.certs_path") + "/certificates")
if err != nil {
fmt.Printf("Error reading from directory: %v\n", err)
os.Exit(1)
}
for _, cert := range certs {
if strings.HasPrefix(cert.Name(), domain) {
file, err := fs.Create(cert.Name())
for _, file := range certFiles {
if strings.HasPrefix(file.Name(), domain) {
file, err := fs.Create(file.Name())
if err != nil {
fmt.Printf("Error copying cert to memfs: %v\n", err)
fmt.Printf("Error copying file to memfs: %v\n", err)
os.Exit(1)
}
certFile, err := os.ReadFile(config.GetAsString("Certificates.certs_path") + "/certificates/" + cert.Name())
certFile, err := os.ReadFile(config.GetAsString("Certificates.certs_path") + "/certificates/" + file.Name())
//certFile = encryptBytes(certFile)
_, err = file.Write(certFile)
err = file.Close()
@@ -105,9 +107,9 @@ func addAndPushCerts() {
fmt.Printf("Error writing to memfs: %v\n", err)
os.Exit(1)
}
_, err = workTree.Add(cert.Name())
_, err = workTree.Add(file.Name())
if err != nil {
fmt.Printf("Error adding certificate %v: %v", cert.Name(), err)
fmt.Printf("Error adding file %v: %v", file.Name(), err)
os.Exit(1)
}
}

3
go.mod
View File

@@ -12,6 +12,8 @@ require (
github.com/go-git/go-billy/v5 v5.4.1
github.com/go-git/go-git/v5 v5.7.0
github.com/google/go-github/v55 v55.0.0
github.com/makifdb/pidfile v0.0.0-20231129022650-50ec86392313
golang.org/x/crypto v0.42.0
)
require (
@@ -34,7 +36,6 @@ require (
github.com/sergi/go-diff v1.3.1 // indirect
github.com/skeema/knownhosts v1.1.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect

13
go.sum
View File

@@ -1,4 +1,5 @@
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0=
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w=
code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
code.gitea.io/sdk/gitea v0.15.1 h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M=
code.gitea.io/sdk/gitea v0.15.1/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA=
@@ -13,7 +14,9 @@ github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
@@ -24,10 +27,13 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/go-acme/lego/v4 v4.26.0 h1:521aEQxNstXvPQcFDDPrJiFfixcCQuvAvm35R4GbyYA=
github.com/go-acme/lego/v4 v4.26.0/go.mod h1:BQVAWgcyzW4IT9eIKHY/RxYlVhoyKyOMXOkq7jK1eEQ=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@@ -35,6 +41,7 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmS
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE=
github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
@@ -43,6 +50,7 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v55 v55.0.0 h1:4pp/1tNMB9X/LuAhs5i0KQAE40NmiR/y6prLNb9x9cg=
github.com/google/go-github/v55 v55.0.0/go.mod h1:JLahOTA1DnXzhxEymmFF5PP2tSS9JVNj68mSZNDwskA=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
@@ -63,6 +71,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/makifdb/pidfile v0.0.0-20231129022650-50ec86392313 h1:5/CjuZQWnRALu4hkEDRg4fA5lWDSfjlKg+koRDRuotQ=
github.com/makifdb/pidfile v0.0.0-20231129022650-50ec86392313/go.mod h1:nm72+BE0Z1PcotR9soX+NnGyEs1iQ0b1Ot0IhL2Nwwk=
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
@@ -74,6 +84,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -83,6 +94,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -136,6 +148,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

186
main.go
View File

@@ -2,9 +2,19 @@ package main
import (
"bufio"
"code.gitea.io/sdk/gitea"
"context"
"flag"
"fmt"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
"code.gitea.io/sdk/gitea"
"git.nevets.tech/Steven/ezconf"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
@@ -12,12 +22,7 @@ import (
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/google/go-github/v55/github"
"io"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"github.com/makifdb/pidfile"
)
var config *ezconf.Configuration
@@ -33,57 +38,175 @@ var creds *http.BasicAuth
var repo *git.Repository
var ctx context.Context
var cancel context.CancelFunc
var wg sync.WaitGroup
//TODO create logic for gh vs gt repos
func main() {
devFlag := flag.Bool("dev", false, "Developer Mode")
configFile := flag.String("config", "/etc/certman/certman.conf", "Configuration file")
newDomainFlag := flag.String("new-domain", "example.com", "Domain to create new configs and directories for")
newDomainDirFlag := flag.String("new-domain-dir", "/opt/certs/example.com", "Directory that certs will be stored in")
installFlag := flag.Bool("install", false, "Install Certman")
modeFlag := flag.String("mode", "client", "CertManager Mode [server, client]")
thinInstallFlag := flag.Bool("t", false, "Thin Install (skip creating dirs)")
newKeyFlag := flag.Bool("newkey", false, "Generate new encryption key")
reloadFlag := flag.Bool("reload", false, "Reload configs")
daemonFlag := flag.Bool("d", false, "Daemon Mode")
flag.Parse()
if *devFlag {
os.Exit(0)
}
if *newDomainFlag != "example.com" {
fmt.Printf("Creating new domain %s\n", *newDomainFlag)
createNewDomainConfig(*newDomainFlag)
createNewDomainCertsDir(*newDomainFlag)
createNewDomainCertsDir(*newDomainFlag, *newDomainDirFlag)
fmt.Println("Successfully created domain entry for " + *newDomainFlag + "\nUpdate config file as needed in /etc/certman/domains/" + *newDomainFlag + ".conf")
os.Exit(0)
}
if *installFlag {
if !*thinInstallFlag {
makeDirs()
config = ezconf.NewConfiguration("/etc/certman/certman.conf", defaultConfig)
}
config = ezconf.NewConfiguration(*configFile, strings.ReplaceAll(defaultConfig, "{mode}", *modeFlag))
os.Exit(0)
}
if *newKeyFlag {
key, err := GenerateKey()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Printf(key)
os.Exit(0)
}
if *reloadFlag {
pidBytes, err := os.ReadFile("/var/run/certman.pid")
if err != nil {
fmt.Printf("Error getting PID from /var/run/certman.pid: %v\n", err)
os.Exit(1)
}
pidStr := strings.TrimSpace(string(pidBytes))
daemonPid, err := strconv.Atoi(pidStr)
if err != nil {
fmt.Printf("Error converting PID string to int (%s): %v\n", pidStr, err)
os.Exit(1)
}
proc, err := os.FindProcess(daemonPid)
if err != nil {
fmt.Printf("Error finding process with PID %d: %v\n", daemonPid, err)
os.Exit(1)
}
err = proc.Signal(syscall.SIGHUP)
if err != nil {
fmt.Printf("Error sending SIGHUP to PID %d: %v\n", daemonPid, err)
os.Exit(1)
}
os.Exit(0)
}
if *daemonFlag {
err := pidfile.CreateOrUpdatePIDFile("/var/run/certman.pid")
if err != nil {
fmt.Println("Error creating pidfile")
os.Exit(1)
}
ctx, cancel = context.WithCancel(context.Background())
// Check if main config exists
if _, err := os.Stat("/etc/certman/certman.conf"); os.IsNotExist(err) {
if _, err := os.Stat(*configFile); os.IsNotExist(err) {
fmt.Println("Main config file not found, please run 'certman --install', then properly configure /etc/certman/certman.conf.")
os.Exit(1)
} else if err != nil {
fmt.Printf("Error opening /etc/certman/certman.conf: %v\n", err)
fmt.Printf("Error opening %s: %v\n", *configFile, err)
}
config = ezconf.LoadConfiguration(*configFile)
// Setup SIGINT and SIGTERM listeners
sigChannel := make(chan os.Signal, 1)
signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(sigChannel)
// Task loop
reloadSigChan := make(chan os.Signal, 1)
signal.Notify(reloadSigChan, syscall.SIGHUP)
defer signal.Stop(reloadSigChan)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
wg.Add(1)
if config.GetAsString("App.mode") == "server" {
fmt.Println("Starting CertManager in server mode...")
// Server Task loop
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("Shutting down server")
return
case <-reloadSigChan:
{
fmt.Println("Reloading configs...")
}
case <-ticker.C:
{
fmt.Println("Tick!")
}
}
}
}()
} else if config.GetAsString("App.mode") == "client" {
fmt.Println("Starting CertManager in client mode...")
// Client Task loop
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("Shutting down client")
return
case <-reloadSigChan:
{
fmt.Println("Reloading configs...")
}
case <-ticker.C:
{
fmt.Println("Tick!")
}
}
}
}()
} else {
fmt.Println("Invalid operating mode \"" + config.GetAsString("App.mode") + "\"")
}
// Cleanup on stop
sig := <-sigChannel
fmt.Printf("Program terminated with %v\n", sig)
close()
stop()
wg.Wait()
}
}
func close() {
func stop() {
cancel()
}
func maindis() {
@@ -141,7 +264,6 @@ func maindis() {
{
url := createGiteaRepo()
repo, workTree = cloneRepo(url)
fixUpdateSh()
cmd = exec.Command("lego", legoNewSiteArgs...)
}
case "renew":
@@ -161,7 +283,6 @@ func maindis() {
{
url := createGiteaRepo()
repo, workTree = cloneRepo(url)
fixUpdateSh()
addAndPushCerts()
os.Exit(0)
}
@@ -202,30 +323,3 @@ func maindis() {
}
addAndPushCerts()
}
func fixUpdateSh() {
oldUpdateSh, err := fs.Open("update.sh")
if err != nil {
fmt.Printf("Error opening update.sh: %v\n", err)
os.Exit(1)
}
contentBytes, err := io.ReadAll(oldUpdateSh)
if err != nil {
fmt.Printf("Error reading update.sh: %v\n", err)
os.Exit(1)
}
content := string(contentBytes)
strings.ReplaceAll(content, "<>", domain)
updateSh, err := fs.Create("update.sh")
_, err = updateSh.Write([]byte(content))
err = updateSh.Close()
if err != nil {
fmt.Printf("Error writing update.sh: %v\n", err)
os.Exit(1)
}
_, err = workTree.Add("update.sh")
if err != nil {
fmt.Printf("Error adding update.sh: %v\n", err)
os.Exit(1)
}
}

18
util.go
View File

@@ -1,12 +1,13 @@
package main
import (
"code.gitea.io/sdk/gitea"
"errors"
"fmt"
"os"
"code.gitea.io/sdk/gitea"
"git.nevets.tech/Steven/ezconf"
"github.com/google/go-github/v55/github"
"os"
)
type Domain struct {
@@ -99,7 +100,10 @@ func insert(a []string, index int, value string) []string {
return a
}
const defaultConfig = `[Git]
const defaultConfig = `[App]
mode = {mode}
[Git]
host = gitea
server = https://gitea.instance.com
username = user
@@ -120,12 +124,20 @@ const defaultDomainConfig = `[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
`