update repo
This commit is contained in:
12
traefik/traefik-certs-dumper/dumper/config.go
Normal file
12
traefik/traefik-certs-dumper/dumper/config.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package dumper
|
||||
|
||||
// BaseConfig Base dump command configuration.
|
||||
type BaseConfig struct {
|
||||
DumpPath string
|
||||
CrtInfo FileInfo
|
||||
KeyInfo FileInfo
|
||||
DomainSubDir bool
|
||||
Clean bool
|
||||
Watch bool
|
||||
Hook string
|
||||
}
|
129
traefik/traefik-certs-dumper/dumper/dumper.go
Normal file
129
traefik/traefik-certs-dumper/dumper/dumper.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package dumper
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-acme/lego/certcrypto"
|
||||
)
|
||||
|
||||
const (
|
||||
certsSubDir = "certs"
|
||||
keysSubDir = "private"
|
||||
)
|
||||
|
||||
// FileInfo File information.
|
||||
type FileInfo struct {
|
||||
Name string
|
||||
Ext string
|
||||
}
|
||||
|
||||
// Dump Dumps data to certificates.
|
||||
func Dump(data *StoredData, baseConfig *BaseConfig) error {
|
||||
if baseConfig.Clean {
|
||||
err := cleanDir(baseConfig.DumpPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !baseConfig.DomainSubDir {
|
||||
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, certsSubDir), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, keysSubDir), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privateKeyPem := extractPEMPrivateKey(data.Account)
|
||||
err := ioutil.WriteFile(filepath.Join(baseConfig.DumpPath, keysSubDir, "letsencrypt"+baseConfig.KeyInfo.Ext), privateKeyPem, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cert := range data.Certificates {
|
||||
err := writeCert(baseConfig.DumpPath, cert, baseConfig.CrtInfo, baseConfig.DomainSubDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = writeKey(baseConfig.DumpPath, cert, baseConfig.KeyInfo, baseConfig.DomainSubDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeCert(dumpPath string, cert *Certificate, info FileInfo, domainSubDir bool) error {
|
||||
certPath := filepath.Join(dumpPath, certsSubDir, safeName(cert.Domain.Main+info.Ext))
|
||||
if domainSubDir {
|
||||
certPath = filepath.Join(dumpPath, safeName(cert.Domain.Main), info.Name+info.Ext)
|
||||
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(certPath, cert.Certificate, 0666)
|
||||
}
|
||||
|
||||
func writeKey(dumpPath string, cert *Certificate, info FileInfo, domainSubDir bool) error {
|
||||
keyPath := filepath.Join(dumpPath, keysSubDir, safeName(cert.Domain.Main+info.Ext))
|
||||
if domainSubDir {
|
||||
keyPath = filepath.Join(dumpPath, safeName(cert.Domain.Main), info.Name+info.Ext)
|
||||
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(keyPath, cert.Key, 0600)
|
||||
}
|
||||
|
||||
func extractPEMPrivateKey(account *Account) []byte {
|
||||
var block *pem.Block
|
||||
switch account.KeyType {
|
||||
case certcrypto.RSA2048, certcrypto.RSA4096, certcrypto.RSA8192:
|
||||
block = &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: account.PrivateKey,
|
||||
}
|
||||
case certcrypto.EC256, certcrypto.EC384:
|
||||
block = &pem.Block{
|
||||
Type: "EC PRIVATE KEY",
|
||||
Bytes: account.PrivateKey,
|
||||
}
|
||||
default:
|
||||
panic("unsupported key type")
|
||||
}
|
||||
|
||||
return pem.EncodeToMemory(block)
|
||||
}
|
||||
|
||||
func cleanDir(dumpPath string) error {
|
||||
_, errExists := os.Stat(dumpPath)
|
||||
if os.IsNotExist(errExists) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if errExists != nil {
|
||||
return errExists
|
||||
}
|
||||
|
||||
dir, err := ioutil.ReadDir(dumpPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range dir {
|
||||
if err := os.RemoveAll(filepath.Join(dumpPath, f.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
167
traefik/traefik-certs-dumper/dumper/file/file.go
Normal file
167
traefik/traefik-certs-dumper/dumper/file/file.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/ldez/traefik-certs-dumper/v2/dumper"
|
||||
"github.com/ldez/traefik-certs-dumper/v2/hook"
|
||||
)
|
||||
|
||||
// Dump Dumps "acme.json" file to certificates.
|
||||
func Dump(acmeFile string, baseConfig *dumper.BaseConfig) error {
|
||||
err := dump(acmeFile, baseConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if baseConfig.Watch {
|
||||
return watch(acmeFile, baseConfig)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dump(acmeFile string, baseConfig *dumper.BaseConfig) error {
|
||||
data, err := readFile(acmeFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dumper.Dump(data, baseConfig)
|
||||
}
|
||||
|
||||
func readFile(acmeFile string) (*dumper.StoredData, error) {
|
||||
source, err := os.Open(acmeFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := &dumper.StoredData{}
|
||||
if err = json.NewDecoder(source).Decode(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func watch(acmeFile string, baseConfig *dumper.BaseConfig) error {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = watcher.Close() }()
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
var previousHash []byte
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if isDebug() {
|
||||
log.Println("event:", event)
|
||||
}
|
||||
|
||||
hash, errW := manageEvent(watcher, event, acmeFile, previousHash, baseConfig)
|
||||
if errW != nil {
|
||||
log.Println("error:", errW)
|
||||
done <- true
|
||||
return
|
||||
}
|
||||
|
||||
previousHash = hash
|
||||
|
||||
case errW, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("error:", errW)
|
||||
done <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = watcher.Add(acmeFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
<-done
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func manageEvent(watcher *fsnotify.Watcher, event fsnotify.Event, acmeFile string, previousHash []byte, baseConfig *dumper.BaseConfig) ([]byte, error) {
|
||||
err := manageRename(watcher, event, acmeFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hash, err := calculateHash(acmeFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(previousHash, hash) {
|
||||
if isDebug() {
|
||||
log.Println("detected changes on file:", event.Name)
|
||||
}
|
||||
|
||||
if errD := dump(acmeFile, baseConfig); errD != nil {
|
||||
return nil, errD
|
||||
}
|
||||
|
||||
if isDebug() {
|
||||
log.Println("Dumped new certificate data.")
|
||||
}
|
||||
|
||||
hook.Exec(baseConfig.Hook)
|
||||
}
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
func manageRename(watcher *fsnotify.Watcher, event fsnotify.Event, acmeFile string) error {
|
||||
if event.Op&fsnotify.Rename != fsnotify.Rename {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := watcher.Remove(acmeFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return watcher.Add(acmeFile)
|
||||
}
|
||||
|
||||
func calculateHash(acmeFile string) ([]byte, error) {
|
||||
file, err := os.Open(acmeFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
h := md5.New()
|
||||
_, err = io.Copy(h, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
|
||||
func isDebug() bool {
|
||||
return strings.EqualFold(os.Getenv("TCD_DEBUG"), "true")
|
||||
}
|
7
traefik/traefik-certs-dumper/dumper/filename.go
Normal file
7
traefik/traefik-certs-dumper/dumper/filename.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// +build !windows
|
||||
|
||||
package dumper
|
||||
|
||||
func safeName(filename string) string {
|
||||
return filename
|
||||
}
|
9
traefik/traefik-certs-dumper/dumper/filename_windows.go
Normal file
9
traefik/traefik-certs-dumper/dumper/filename_windows.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// +build windows
|
||||
|
||||
package dumper
|
||||
|
||||
import "strings"
|
||||
|
||||
func safeName(filename string) string {
|
||||
return strings.ReplaceAll(filename, "*", "_")
|
||||
}
|
35
traefik/traefik-certs-dumper/dumper/info.go
Normal file
35
traefik/traefik-certs-dumper/dumper/info.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package dumper
|
||||
|
||||
import (
|
||||
"github.com/go-acme/lego/certcrypto"
|
||||
"github.com/go-acme/lego/registration"
|
||||
)
|
||||
|
||||
// StoredData represents the data managed by the Store
|
||||
type StoredData struct {
|
||||
Account *Account
|
||||
Certificates []*Certificate
|
||||
HTTPChallenges map[string]map[string][]byte
|
||||
TLSChallenges map[string]*Certificate
|
||||
}
|
||||
|
||||
// Certificate is a struct which contains all data needed from an ACME certificate
|
||||
type Certificate struct {
|
||||
Domain Domain
|
||||
Certificate []byte
|
||||
Key []byte
|
||||
}
|
||||
|
||||
// Domain holds a domain name with SANs
|
||||
type Domain struct {
|
||||
Main string
|
||||
SANs []string
|
||||
}
|
||||
|
||||
// Account is used to store lets encrypt registration info
|
||||
type Account struct {
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
PrivateKey []byte
|
||||
KeyType certcrypto.KeyType
|
||||
}
|
11
traefik/traefik-certs-dumper/dumper/kv/config.go
Normal file
11
traefik/traefik-certs-dumper/dumper/kv/config.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package kv
|
||||
|
||||
import "github.com/abronan/valkeyrie/store"
|
||||
|
||||
// Config KV configuration.
|
||||
type Config struct {
|
||||
Backend store.Backend
|
||||
Prefix string
|
||||
Endpoints []string
|
||||
Options *store.Config
|
||||
}
|
65
traefik/traefik-certs-dumper/dumper/kv/convert.go
Normal file
65
traefik/traefik-certs-dumper/dumper/kv/convert.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"github.com/go-acme/lego/certcrypto"
|
||||
"github.com/go-acme/lego/registration"
|
||||
"github.com/ldez/traefik-certs-dumper/v2/dumper"
|
||||
)
|
||||
|
||||
// CertificateV1 is used to store certificate info
|
||||
type CertificateV1 struct {
|
||||
Domain string
|
||||
CertURL string
|
||||
CertStableURL string
|
||||
PrivateKey []byte
|
||||
Certificate []byte
|
||||
}
|
||||
|
||||
// AccountV1 is used to store lets encrypt registration info
|
||||
type AccountV1 struct {
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
PrivateKey []byte
|
||||
KeyType certcrypto.KeyType
|
||||
DomainsCertificate DomainsCertificates
|
||||
ChallengeCerts map[string]*ChallengeCert
|
||||
HTTPChallenge map[string]map[string][]byte
|
||||
}
|
||||
|
||||
// DomainsCertificates stores a certificate for multiple domains
|
||||
type DomainsCertificates struct {
|
||||
Certs []*DomainsCertificate
|
||||
}
|
||||
|
||||
// ChallengeCert stores a challenge certificate
|
||||
type ChallengeCert struct {
|
||||
Certificate []byte
|
||||
PrivateKey []byte
|
||||
}
|
||||
|
||||
// DomainsCertificate contains a certificate for multiple domains
|
||||
type DomainsCertificate struct {
|
||||
Domains dumper.Domain
|
||||
Certificate *CertificateV1
|
||||
}
|
||||
|
||||
// convertAccountV1ToV2 converts account information from version 1 to 2
|
||||
func convertAccountV1ToV2(account *AccountV1) *dumper.StoredData {
|
||||
storedData := &dumper.StoredData{}
|
||||
storedData.Account = &dumper.Account{
|
||||
PrivateKey: account.PrivateKey,
|
||||
Registration: account.Registration,
|
||||
Email: account.Email,
|
||||
KeyType: account.KeyType,
|
||||
}
|
||||
var certs []*dumper.Certificate
|
||||
for _, oldCert := range account.DomainsCertificate.Certs {
|
||||
certs = append(certs, &dumper.Certificate{
|
||||
Certificate: oldCert.Certificate.Certificate,
|
||||
Domain: oldCert.Domains,
|
||||
Key: oldCert.Certificate.PrivateKey,
|
||||
})
|
||||
}
|
||||
storedData.Certificates = certs
|
||||
return storedData
|
||||
}
|
99
traefik/traefik-certs-dumper/dumper/kv/kv.go
Normal file
99
traefik/traefik-certs-dumper/dumper/kv/kv.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/abronan/valkeyrie"
|
||||
"github.com/abronan/valkeyrie/store"
|
||||
"github.com/ldez/traefik-certs-dumper/v2/dumper"
|
||||
"github.com/ldez/traefik-certs-dumper/v2/hook"
|
||||
)
|
||||
|
||||
const storeKeySuffix = "/acme/account/object"
|
||||
|
||||
// Dump Dumps KV content to certificates.
|
||||
func Dump(config *Config, baseConfig *dumper.BaseConfig) error {
|
||||
kvStore, err := valkeyrie.NewStore(config.Backend, config.Endpoints, config.Options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create client of the store: %v", err)
|
||||
}
|
||||
|
||||
storeKey := config.Prefix + storeKeySuffix
|
||||
|
||||
if baseConfig.Watch {
|
||||
return watch(kvStore, storeKey, baseConfig)
|
||||
}
|
||||
|
||||
pair, err := kvStore.Get(storeKey, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve %s value: %v", storeKey, err)
|
||||
}
|
||||
|
||||
return dumpPair(pair, baseConfig)
|
||||
}
|
||||
|
||||
func watch(kvStore store.Store, storeKey string, baseConfig *dumper.BaseConfig) error {
|
||||
stopCh := make(<-chan struct{})
|
||||
|
||||
pairs, err := kvStore.Watch(storeKey, stopCh, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
pair := <-pairs
|
||||
if pair == nil {
|
||||
return fmt.Errorf("could not fetch Key/Value pair for key %v", storeKey)
|
||||
}
|
||||
|
||||
err = dumpPair(pair, baseConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isDebug() {
|
||||
log.Println("Dumped new certificate data.")
|
||||
}
|
||||
|
||||
hook.Exec(baseConfig.Hook)
|
||||
}
|
||||
}
|
||||
|
||||
func dumpPair(pair *store.KVPair, baseConfig *dumper.BaseConfig) error {
|
||||
data, err := getStoredDataFromGzip(pair)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dumper.Dump(data, baseConfig)
|
||||
}
|
||||
|
||||
func getStoredDataFromGzip(pair *store.KVPair) (*dumper.StoredData, error) {
|
||||
reader, err := gzip.NewReader(bytes.NewBuffer(pair.Value))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to create GZip reader: %v", err)
|
||||
}
|
||||
|
||||
acmeData, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read the pair content: %v", err)
|
||||
}
|
||||
|
||||
account := &AccountV1{}
|
||||
if err := json.Unmarshal(acmeData, &account); err != nil {
|
||||
return nil, fmt.Errorf("unable marshal AccountV1: %v", err)
|
||||
}
|
||||
|
||||
return convertAccountV1ToV2(account), nil
|
||||
}
|
||||
|
||||
func isDebug() bool {
|
||||
return strings.EqualFold(os.Getenv("TCD_DEBUG"), "true")
|
||||
}
|
Reference in New Issue
Block a user