It watches ACME certs, detects new files, syncs them to your other servers, and bounces them.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
cert-bouncer/main.go

167 lines
3.3 KiB

package main
import (
"github.com/fsnotify/fsnotify"
"flag"
"log"
"os"
"io"
"encoding/json"
"time"
"path"
"path/filepath"
)
type Cert struct {
PrivateKey string
PublicKey string
Owner string
}
type Config struct {
Source Cert
Target Cert
Reload string
WatchDelay string
watcher *fsnotify.Watcher
delay_time time.Duration
}
func MustExist(p string) {
_, err := os.Stat(p)
if os.IsNotExist(err) {
log.Fatalf("path %s does not exist: %v", p, err)
}
}
func AbsPath(p string) string {
p, err := filepath.Abs(p)
if err != nil {
log.Fatalf("can't convert %s to absolute p: %v", p, err)
}
return p
}
func LoadConfig(config_path string) Config {
var config Config
config_data, err := os.ReadFile(config_path)
if err != nil {
log.Fatal("invalid config path %s: %v", config_path, err)
}
err = json.Unmarshal(config_data, &config)
if err != nil {
log.Fatal(err, "json format error")
}
config.delay_time, err = time.ParseDuration(config.WatchDelay)
if err != nil {
log.Fatalf("can't parse watch_delay setting %s: %v", config.WatchDelay, err)
}
config.Source.PrivateKey = AbsPath(config.Source.PrivateKey)
MustExist(config.Source.PrivateKey)
config.Source.PublicKey = AbsPath(config.Source.PublicKey)
MustExist(config.Source.PublicKey)
config.Target.PrivateKey = AbsPath(config.Target.PrivateKey)
MustExist(path.Dir(config.Target.PrivateKey))
config.Target.PublicKey = AbsPath(config.Target.PublicKey)
MustExist(path.Dir(config.Target.PublicKey))
return config
}
func ParseOpts() Config {
var config_file string
flag.StringVar(&config_file, "config", "cert-bouncer.json", ".json config to use.")
flag.Parse()
return LoadConfig(config_file)
}
func Copy(from string, to string) {
log.Println("copying from=", from, "to=", to)
src, err := os.Open(from)
if err != nil { log.Fatalf("%s: %v", from, err) }
defer src.Close()
dst, err := os.OpenFile(to, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0700)
if err != nil { log.Fatalf("%s: %v", to, err) }
defer dst.Close()
_, err = io.Copy(dst, src)
if err != nil {
log.Fatalf("failed to copy: %v", err)
}
}
func (cfg *Config) SyncCerts() {
log.Println("SYNC CERTS CALLED")
// copy the files
Copy(cfg.Source.PrivateKey, cfg.Target.PrivateKey)
Copy(cfg.Source.PublicKey, cfg.Target.PublicKey)
// change the ownership
// restart the service
}
func (cfg *Config) HandleEvents() {
doit := time.NewTimer(cfg.delay_time)
doit.Stop()
for {
select {
case event, ok := <-cfg.watcher.Events:
if !ok {
return
}
log.Println("EVENT", event)
if event.Name == cfg.Source.PrivateKey {
doit.Reset(cfg.delay_time)
}
case <-doit.C:
cfg.SyncCerts()
case err, ok := <-cfg.watcher.Errors:
if !ok {
return
}
log.Println("failed to watch", err)
}
}
}
func (cfg *Config) WatchFiles() {
var err error
cfg.watcher, err = fsnotify.NewWatcher()
if err != nil {
log.Fatal(err, "Can't watch files.")
}
defer cfg.watcher.Close()
go cfg.HandleEvents()
err = cfg.watcher.Add(cfg.Source.PrivateKey)
if err != nil {
log.Fatalf("can't watch %s: %v", err, cfg.Source.PrivateKey)
}
<-make(chan struct{})
}
func main() {
config := ParseOpts()
config.WatchFiles()
}