-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 0fa62a5
Showing
7 changed files
with
251 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.env | ||
certs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# certwatch | ||
|
||
Tool to watch a caddy certmagic redis store, implemented via https://github.com/gamalan/caddy-tlsredis |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"flag" | ||
"io/fs" | ||
"os" | ||
"os/exec" | ||
"path" | ||
"strings" | ||
"time" | ||
|
||
"github.com/redis/go-redis/v9" | ||
"golang.org/x/exp/slog" | ||
) | ||
|
||
type Config struct { | ||
RedisUrl string | ||
KeyPrefix string | ||
ValuePrefix string | ||
AcmeDirName string | ||
|
||
CertDir string | ||
Certs []string | ||
Cmd string | ||
Debug bool | ||
} | ||
|
||
var ( | ||
config Config | ||
client *redis.Client | ||
) | ||
|
||
func main() { | ||
flag.StringVar(&config.RedisUrl, "redisurl", "", "URL for redis instance") | ||
flag.StringVar(&config.KeyPrefix, "keyprefix", "caddytls", "prefix for keys") | ||
flag.StringVar(&config.ValuePrefix, "valueprefix", "caddy-storage-redis", "prefix for values") | ||
flag.StringVar(&config.AcmeDirName, "acmedir", "acme-v02.api.letsencrypt.org-directory", "subdir for ACME") | ||
flag.StringVar(&config.CertDir, "certdir", "/var/lib/certwatch", "directory for storing certificates locally") | ||
flag.StringVar(&config.Cmd, "cmd", "", "command to execute if certificates have been changed") | ||
flag.BoolVar(&config.Debug, "debug", false, "verbose debug output") | ||
flag.Parse() | ||
config.Certs = flag.Args() | ||
level := new(slog.LevelVar) // Info by default | ||
if config.Debug { | ||
level.Set(slog.LevelDebug) | ||
} | ||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ | ||
Level: level, | ||
})) | ||
slog.SetDefault(logger) | ||
slog.Debug("config", "config", config) | ||
if len(config.RedisUrl) == 0 || len(config.Certs) == 0 { | ||
flag.Usage() | ||
os.Exit(1) | ||
} | ||
err := os.MkdirAll(config.CertDir, 0700) | ||
if err != nil { | ||
slog.Error("MkdirAll", "err", err) | ||
os.Exit(1) | ||
} | ||
opt, err := redis.ParseURL(config.RedisUrl) | ||
if err != nil { | ||
slog.Error("redis.ParseURL", "err", err) | ||
os.Exit(1) | ||
} | ||
client = redis.NewClient(opt) | ||
ctx := context.Background() | ||
needExec := false | ||
for _, i := range config.Certs { | ||
didOne, err := handleCert(ctx, i) | ||
if err != nil { | ||
slog.Error("handleCert", "err", err) | ||
os.Exit(1) | ||
} | ||
if didOne { | ||
needExec = true | ||
} | ||
} | ||
if needExec { | ||
if len(config.Cmd) > 0 { | ||
cmd := exec.Command("sh", "-c", config.Cmd) | ||
outerr, err := cmd.CombinedOutput() | ||
if err != nil { | ||
slog.Error("exec", "err", err, "outerr", string(outerr)) | ||
} | ||
} | ||
} | ||
keypath := "__keyspace@0__:" + config.KeyPrefix + "/certificates/" + config.AcmeDirName + "/" | ||
pubsub := client.PSubscribe(ctx, keypath+"*") | ||
defer pubsub.Close() | ||
for { | ||
msg, err := pubsub.ReceiveMessage(ctx) | ||
if err != nil { | ||
slog.Error("ReceiveMessage", "err", err) | ||
os.Exit(1) | ||
} | ||
needExec := false | ||
key := strings.TrimPrefix(msg.Channel, keypath) | ||
slog.Debug("msg", "key", key, "payload", msg.Payload) | ||
for _, i := range config.Certs { | ||
if strings.HasPrefix(key, i) { | ||
switch msg.Payload { | ||
case "evicted": | ||
fallthrough | ||
case "expired": | ||
fallthrough | ||
case "del": | ||
fname := path.Join(config.CertDir, i+path.Ext(key)) | ||
err := os.Remove(fname) | ||
if err != nil { | ||
slog.Error("Remove", "err", err) | ||
} | ||
case "set": | ||
didOne, err := handleCert(ctx, i) | ||
if err != nil { | ||
slog.Error("handleCert", "err", err) | ||
continue | ||
} | ||
if didOne { | ||
needExec = true | ||
} | ||
default: | ||
slog.Warn("unhandled message", "msg", msg) | ||
} | ||
} | ||
} | ||
if needExec { | ||
if len(config.Cmd) > 0 { | ||
cmd := exec.Command("sh", "-c", config.Cmd) | ||
outerr, err := cmd.CombinedOutput() | ||
if err != nil { | ||
slog.Error("exec", "err", err, "outerr", string(outerr)) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
func handleCert(ctx context.Context, cert string) (bool, error) { | ||
didOne := false | ||
for _, suf := range []string{".key", ".crt"} { | ||
var value struct { | ||
Value []byte | ||
Modified time.Time | ||
} | ||
fname := path.Join(config.CertDir, cert+suf) | ||
key := config.KeyPrefix + "/certificates/" + config.AcmeDirName + "/" + cert + "/" + cert + suf | ||
val, err := client.Get(ctx, key).Result() | ||
if err != nil { | ||
if errors.Is(err, redis.Nil) { | ||
continue | ||
} | ||
return false, err | ||
} | ||
val = strings.TrimPrefix(val, config.ValuePrefix) | ||
err = json.Unmarshal([]byte(val), &value) | ||
if err != nil { | ||
return false, err | ||
} | ||
finfo, err := os.Stat(fname) | ||
if err == nil && finfo.ModTime() == value.Modified && finfo.Size() == int64(len(value.Value)) { | ||
continue | ||
} else if err != nil && !errors.Is(err, fs.ErrNotExist) { | ||
return false, err | ||
} | ||
f, err := os.OpenFile(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) | ||
if err != nil { | ||
return false, err | ||
} | ||
n, err := f.Write(value.Value) | ||
if n != len(value.Value) { | ||
f.Close() | ||
return false, err | ||
} | ||
err = f.Close() | ||
if err != nil { | ||
return false, err | ||
} | ||
err = os.Chtimes(fname, value.Modified, value.Modified) | ||
if err != nil { | ||
return false, err | ||
} | ||
didOne = true | ||
} | ||
return didOne, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
module github.com/jum/certwatch | ||
|
||
go 1.20 | ||
|
||
require ( | ||
github.com/cespare/reflex v0.3.1 | ||
github.com/redis/go-redis/v9 v9.0.5 | ||
golang.org/x/exp v0.0.0-20230810033253-352e893a4cad | ||
) | ||
|
||
require ( | ||
github.com/cespare/xxhash/v2 v2.2.0 // indirect | ||
github.com/creack/pty v1.1.11 // indirect | ||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | ||
github.com/fsnotify/fsnotify v1.4.7 // indirect | ||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect | ||
github.com/ogier/pflag v0.0.1 // indirect | ||
golang.org/x/sys v0.1.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= | ||
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= | ||
github.com/cespare/reflex v0.3.1 h1:N4Y/UmRrjwOkNT0oQQnYsdr6YBxvHqtSfPB4mqOyAKk= | ||
github.com/cespare/reflex v0.3.1/go.mod h1:I+0Pnu2W693i7Hv6ZZG76qHTY0mgUa7uCIfCtikXojE= | ||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= | ||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= | ||
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= | ||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= | ||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= | ||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | ||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= | ||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= | ||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= | ||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= | ||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||
github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= | ||
github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= | ||
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o= | ||
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= | ||
golang.org/x/exp v0.0.0-20230810033253-352e893a4cad h1:g0bG7Z4uG+OgH2QDODnjp6ggkk1bJDsINcuWmJN1iJU= | ||
golang.org/x/exp v0.0.0-20230810033253-352e893a4cad/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= | ||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= | ||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
#!/bin/bash | ||
go run github.com/cespare/reflex -d none -s -g .env -- bash -c ". .env; go run github.com/cespare/reflex -d none -s -G .env -G \$CERTDIR -- go run . -debug -redisurl=\$REDISURL -certdir=\$CERTDIR \$CERTS" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
//go:build tools | ||
|
||
// tools is a dummy package that will be ignored for builds, but included for dependencies | ||
package tools | ||
|
||
import ( | ||
_ "github.com/cespare/reflex" | ||
) |