Compare commits

...

30 Commits
v0.0.6 ... main

Author SHA1 Message Date
0d184e27e8
Update workflow Go version 2025-03-21 22:56:35 +00:00
c737bab781
Update dependencies 2025-03-21 22:46:17 +00:00
dependabot[bot]
9e0ba09a84
Bump github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2
Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.1...v4.5.2)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-21 22:44:10 +00:00
dependabot[bot]
4bbeaa87e1
Bump golang.org/x/net from 0.35.0 to 0.36.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/net/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-19 21:16:16 +00:00
dependabot[bot]
35bc5ae4ab
Bump github.com/go-jose/go-jose/v4 from 4.0.4 to 4.0.5
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.4 to 4.0.5.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.0.4...v4.0.5)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-25 13:48:37 +00:00
02bfb4f62f
Update dependencies 2025-02-19 21:17:14 +00:00
56be812ff5
Only update the agent last sync when all copies were successful 2025-02-18 21:40:56 +00:00
8407f21090
Fix broken agent tests 2025-02-18 21:33:15 +00:00
fc2f8e34c6
Refactor agent to copy all files for an agent dir at the same time 2025-02-18 21:19:02 +00:00
a23155a827
Does this resolve issues with fullFilePath 2025-02-01 00:59:28 +00:00
432c907303
Add HostKeyAlgorithms field 2025-02-01 00:51:40 +00:00
b642957aaf
Only accept ed25519 kex 2025-01-31 23:23:30 +00:00
99c4c38bd5
Panic when this error occurs in testing 2025-01-31 23:23:14 +00:00
c4c8c33139
Add initial agent check when starting 2025-01-31 23:18:29 +00:00
939875ca4c
Add agent for certificate syncing 2025-01-31 20:22:29 +00:00
7e70331179
Add utils/name.go for cert and key file names 2025-01-31 19:05:49 +00:00
645d22b856
Move trySetup to setup.go 2025-01-31 19:05:49 +00:00
c373f18336
Change certificates.not_after and certificates.renew_retry to allow null values 2025-01-31 19:05:48 +00:00
c247a50472
Leave a comment suggesting to use a custom logger for lego 2025-01-31 19:05:48 +00:00
4105d14e63
Some further clean up of main and setup 2025-01-31 19:05:48 +00:00
bb7c4bcedc
Simplify serving and setup in main binary 2025-01-31 19:05:44 +00:00
0722b67969
Update to Go 1.23.5 and update dependencies 2025-01-29 18:29:37 +00:00
619f909767
Update dependencies 2025-01-26 22:42:12 +00:00
1f1db49160
Update Go to 1.23 and update dependencies 2024-12-24 15:28:05 +00:00
dependabot[bot]
f806c7d230
Bump golang.org/x/crypto from 0.26.0 to 0.31.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.26.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.26.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-12 18:39:38 +00:00
dependabot[bot]
7768a339c8
Bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1
Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.0...v4.5.1)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-05 17:53:22 +00:00
7f24d37e31
Disable stopping on first failure 2024-09-21 18:31:07 +01:00
d0fc76cd73
Retry certificate renewal after failure 2024-09-21 18:31:02 +01:00
7e65015b89
Update logging 2024-09-21 16:57:15 +01:00
37964ad546
Add more debug logging 2024-09-21 14:31:23 +01:00
32 changed files with 1278 additions and 328 deletions

View File

@ -4,7 +4,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.22.x]
go-version: [1.24.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3

233
agent/agent.go Normal file
View File

@ -0,0 +1,233 @@
package agent
import (
"context"
"database/sql"
_ "embed"
"fmt"
"github.com/1f349/orchid/database"
"github.com/1f349/orchid/utils"
"github.com/bramvdbogaerde/go-scp"
"golang.org/x/crypto/ssh"
"os"
"path/filepath"
"sync"
"time"
)
//go:embed agent_readme.md
var agentReadme []byte
type agentQueries interface {
FindAgentToSync(ctx context.Context) ([]database.FindAgentToSyncRow, error)
UpdateAgentLastSync(ctx context.Context, row database.UpdateAgentLastSyncParams) error
UpdateAgentCertNotAfter(ctx context.Context, arg database.UpdateAgentCertNotAfterParams) error
}
func NewAgent(wg *sync.WaitGroup, db agentQueries, sshKey ssh.Signer, certDir string, keyDir string) (*Agent, error) {
a := &Agent{
db: db,
ticker: time.NewTicker(time.Minute * 10),
done: make(chan struct{}),
syncLock: new(sync.Mutex),
sshKey: sshKey,
certDir: certDir,
keyDir: keyDir,
}
wg.Add(1)
go a.syncRoutine(wg)
return a, nil
}
type Agent struct {
db agentQueries
ticker *time.Ticker
done chan struct{}
syncLock *sync.Mutex
sshKey ssh.Signer
certDir string
keyDir string
}
func (a *Agent) Shutdown() {
Logger.Info("Shutting down agent syncing service")
close(a.done)
}
func (a *Agent) syncRoutine(wg *sync.WaitGroup) {
Logger.Debug("Starting syncRoutine")
// Upon leaving the function stop the ticker and clear the WaitGroup.
defer func() {
a.ticker.Stop()
Logger.Info("Stopped agent syncing service")
wg.Done()
}()
Logger.Info("Doing quick agent check before starting...")
a.syncCheck()
// Logging or something
Logger.Info("Initial check complete, continually checking every 10 minutes...")
// Main loop
for {
select {
case <-a.done:
// Exit if done has closed
return
case <-a.ticker.C:
Logger.Debug("Ticking agent syncing")
go a.syncCheck()
}
}
}
func (a *Agent) syncCheck() {
// if the lock is unavailable then ignore this cycle
if !a.syncLock.TryLock() {
return
}
defer a.syncLock.Unlock()
now := time.Now().UTC()
actions, err := a.db.FindAgentToSync(context.Background())
if err != nil {
panic(err)
}
a.syncCertPairs(now, actions)
}
type syncAgent struct {
agentId int64
address string
user string
fingerprint string
}
func (a *Agent) syncCertPairs(startTime time.Time, rows []database.FindAgentToSyncRow) {
agentMap := make(map[syncAgent][]database.FindAgentToSyncRow)
for _, row := range rows {
a := syncAgent{
agentId: row.AgentID,
address: row.Address,
user: row.User,
fingerprint: row.Fingerprint,
}
agentMap[a] = append(agentMap[a], row)
}
for agent, certPairs := range agentMap {
err := a.syncSingleAgentCertPairs(startTime, agent, certPairs)
if err != nil {
// This agent sync is allowed to fail without stopping other agent syncs from
// occurring.
Logger.Warn("Agent sync failed", "agent", agent.agentId, "err", err)
}
}
}
func (a *Agent) syncSingleAgentCertPairs(startTime time.Time, agent syncAgent, rows []database.FindAgentToSyncRow) error {
hostPubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(agent.fingerprint))
if err != nil {
return fmt.Errorf("failed to parse fingerprint: %w", err)
}
client, err := ssh.Dial("tcp", agent.address, &ssh.ClientConfig{
Config: ssh.Config{
KeyExchanges: []string{"curve25519-sha256"},
},
User: agent.user,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(a.sshKey),
},
HostKeyAlgorithms: []string{"ssh-ed25519"},
HostKeyCallback: ssh.FixedHostKey(hostPubKey),
Timeout: time.Second * 30,
})
if err != nil {
return fmt.Errorf("ssh dial: %w", err)
}
scpClient, err := scp.NewClientBySSH(client)
if err != nil {
return fmt.Errorf("scp client: %w", err)
}
hadError := false
for _, row := range rows {
err := a.copySingleCertPair(&scpClient, row)
if err != nil {
// This cert sync is allowed to fail without stopping other certs going to the
// same agent from copying.
err = fmt.Errorf("copySingleCertPair: %w", err)
Logger.Warn("Agent certificate sync failed", "agent", row.AgentID, "cert", row.CertID, "not after", row.CertNotAfter, "err", err)
hadError = true
continue
}
}
// The agent last sync will only update if all scp copies were successful.
if !hadError {
// Update last sync to the time when the database request happened. This ensures
// that certificates updated after the database request and before the agent
// syncing are updated properly.
err = a.db.UpdateAgentLastSync(context.Background(), database.UpdateAgentLastSyncParams{
LastSync: sql.NullTime{Time: startTime, Valid: true},
ID: agent.agentId,
})
if err != nil {
return fmt.Errorf("error updating agent last sync: %v", err)
}
}
return nil
}
func (a *Agent) copySingleCertPair(scpClient *scp.Client, row database.FindAgentToSyncRow) error {
certName := utils.GetCertFileName(row.CertID)
keyName := utils.GetKeyFileName(row.CertID)
certPath := filepath.Join(a.certDir, certName)
keyPath := filepath.Join(a.keyDir, keyName)
// open cert and key files
openCert, err := os.Open(certPath)
if err != nil {
return fmt.Errorf("open cert file: %w", err)
}
defer openCert.Close()
openKey, err := os.Open(keyPath)
if err != nil {
return fmt.Errorf("open key file: %w", err)
}
defer openKey.Close()
// copy cert and key to agent
err = scpClient.CopyFromFile(context.Background(), *openCert, filepath.Join(row.Dir, "certificates", certName), "0600")
if err != nil {
return fmt.Errorf("copy cert file: %w", err)
}
err = scpClient.CopyFromFile(context.Background(), *openKey, filepath.Join(row.Dir, "keys", keyName), "0600")
if err != nil {
return fmt.Errorf("copy cert file: %w", err)
}
err = a.db.UpdateAgentCertNotAfter(context.Background(), database.UpdateAgentCertNotAfterParams{
NotAfter: row.CertNotAfter,
AgentID: row.AgentID,
CertID: row.CertID,
})
if err != nil {
return fmt.Errorf("error updating agent last sync: %v", err)
}
return nil
}

5
agent/agent_readme.md Normal file
View File

@ -0,0 +1,5 @@
# Orchid Agent
This directory is controlled by Orchid agent configuration.
Certificates in this directory will be automatically updated when required.

391
agent/agent_test.go Normal file
View File

@ -0,0 +1,391 @@
package agent
import (
"bufio"
"bytes"
"context"
"crypto/x509/pkix"
"database/sql"
"encoding/binary"
"errors"
"fmt"
"github.com/1f349/orchid/database"
"github.com/1f349/orchid/logger"
"github.com/charmbracelet/log"
"github.com/mrmelon54/certgen"
"github.com/stretchr/testify/assert"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/ssh"
"io"
"math/big"
"net"
"net/netip"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
"time"
)
func TestAgentSyncing(t *testing.T) {
logger.Logger.SetLevel(log.DebugLevel)
if testing.Short() {
t.Skip("Skipping agent syncing tests in short mode")
}
t.Run("agent syncing test", func(t *testing.T) {
certDir, err := os.MkdirTemp("", "orchid-certs")
assert.NoError(t, err)
keyDir, err := os.MkdirTemp("", "orchid-keys")
assert.NoError(t, err)
defer func() {
assert.NoError(t, os.RemoveAll(certDir))
assert.NoError(t, os.RemoveAll(keyDir))
}()
_, privKey, err := ed25519.GenerateKey(nil)
if err != nil {
panic(err)
}
sshPrivKey, err := ssh.NewSignerFromKey(privKey)
if err != nil {
panic(err)
}
agent := &Agent{
db: &fakeAgentDb{},
ticker: nil,
done: nil,
syncLock: nil,
sshKey: sshPrivKey,
certDir: certDir,
keyDir: keyDir,
}
now := time.Now().UTC()
t.Run("missing cert file", func(t *testing.T) {
err = agent.copySingleCertPair(nil, database.FindAgentToSyncRow{
AgentID: 1337,
Address: "",
User: "test",
Dir: "~/hello/world",
Fingerprint: "",
CertID: 420,
CertNotAfter: sql.NullTime{Time: now, Valid: true},
})
assert.Contains(t, err.Error(), "open cert file:")
assert.Contains(t, err.Error(), "no such file or directory")
})
// generate example certificate
tlsCert, err := certgen.MakeServerTls(nil, 2048, pkix.Name{
Country: []string{"GB"},
Province: []string{"London"},
StreetAddress: []string{"221B Baker Street"},
PostalCode: []string{"NW1 6XE"},
SerialNumber: "test123456",
CommonName: "orchid-agent-test.local",
}, big.NewInt(1234567899), func(now time.Time) time.Time {
return now.Add(1 * time.Hour)
}, []string{"orchid-agent-test.local"}, []net.IP{
net.IPv6loopback,
net.IPv4(127, 0, 0, 1),
})
assert.NoError(t, err)
err = os.WriteFile(filepath.Join(certDir, "420.cert.pem"), tlsCert.GetCertPem(), 0600)
assert.NoError(t, err)
t.Run("missing key file", func(t *testing.T) {
err = agent.copySingleCertPair(nil, database.FindAgentToSyncRow{
AgentID: 1337,
Address: "",
User: "test",
Dir: "~/hello/world",
Fingerprint: "",
CertID: 420,
CertNotAfter: sql.NullTime{Time: now, Valid: true},
})
assert.Contains(t, err.Error(), "open key file:")
assert.Contains(t, err.Error(), "no such file or directory")
})
err = os.WriteFile(filepath.Join(keyDir, "420.key.pem"), tlsCert.GetKeyPem(), 0600)
assert.NoError(t, err)
t.Run("successful sync", func(t *testing.T) {
var wg sync.WaitGroup
server := setupFakeSSH(&wg, func(remoteAddrPort netip.AddrPort, remotePubKey ssh.PublicKey) {
println("Attempt agent syncing")
fingerprintStr := string(ssh.MarshalAuthorizedKey(remotePubKey))
err = agent.syncSingleAgentCertPairs(now, syncAgent{
agentId: 1337,
address: remoteAddrPort.String(),
user: "test",
fingerprint: fingerprintStr,
}, []database.FindAgentToSyncRow{
{
AgentID: 1337,
Address: remoteAddrPort.String(),
User: "test",
Dir: "~/hello/world",
Fingerprint: fingerprintStr,
CertID: 420,
CertNotAfter: sql.NullTime{Time: now, Valid: true},
},
})
assert.NoError(t, err)
})
server.Close()
println("Waiting for ssh server to exit")
server.Wait()
})
})
}
func setupFakeSSH(wg *sync.WaitGroup, call func(addrPort netip.AddrPort, pubKey ssh.PublicKey)) *ssh.ServerConn {
pubKey, privKey, err := ed25519.GenerateKey(nil)
if err != nil {
panic(err)
}
sshPubKey, err := ssh.NewPublicKey(pubKey)
if err != nil {
panic(err)
}
sshSigner, err := ssh.NewSignerFromKey(privKey)
if err != nil {
panic(err)
}
tcp, err := net.ListenTCP("tcp", net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.IPv6Loopback(), 0)))
if err != nil {
panic(err)
}
addrPort := tcp.Addr().(*net.TCPAddr).AddrPort()
var wg2 sync.WaitGroup
wg2.Add(1)
go func() {
defer wg2.Done()
call(addrPort, sshPubKey)
}()
tcpConn, err := tcp.AcceptTCP()
if err != nil {
panic(err)
}
serverConfig := &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
if conn.User() != "test" {
return nil, fmt.Errorf("invalid user")
}
if !conn.RemoteAddr().(*net.TCPAddr).AddrPort().Addr().IsLoopback() {
return nil, fmt.Errorf("invalid remote address")
}
return &ssh.Permissions{}, nil
},
ServerVersion: "SSH-2.0-OrchidTester",
}
serverConfig.AddHostKey(sshSigner)
sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, serverConfig)
if err != nil {
panic(err)
}
// The incoming Request channel must be serviced.
wg.Add(1)
go func() {
ssh.DiscardRequests(reqs)
wg.Done()
}()
wg.Add(1)
go func() {
defer wg.Done()
// Service the incoming channel.
for newChannel := range chans {
// Channels have a type, depending on the application level
// protocol intended. In the case of a shell, the type is
// "session" and ServerShell may be used to present a simple
// terminal interface.
if newChannel.ChannelType() != "session" {
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
channel, requests, err := newChannel.Accept()
if err != nil {
panic(err)
}
var fullFilePath *string
{
a := ""
fullFilePath = &a
}
// Sessions have out-of-band requests such as "shell",
// "pty-req" and "env". Here we handle only the
// "shell" request.
wg.Add(1)
go func(in <-chan *ssh.Request) {
for req := range in {
req.Reply(req.Type == "exec", nil)
if req.Type == "exec" {
length := binary.BigEndian.Uint32(req.Payload[:4])
if len(req.Payload) != int(length)+4 {
panic(fmt.Errorf("invalid exec payload (expected %d but got %d)", length, len(req.Payload)))
}
cmd := string(req.Payload[4:])
const scpStartStr = "scp -qt \""
if !strings.HasPrefix(cmd, scpStartStr) {
panic("invalid start")
}
if !strings.HasSuffix(cmd, "\"") {
panic("invalid end")
}
filePath := cmd[len(scpStartStr) : len(cmd)-1]
fmt.Println("Writing file:", filePath)
*fullFilePath = filePath
}
}
wg.Done()
}(requests)
wg.Add(1)
go func() {
defer func() {
channel.Close()
wg.Done()
}()
var b [1024]byte
read := must(channel.Read(b[:]))
if read < 1 {
panic("invalid read")
}
fmt.Println(string(b[:read]))
r := bufio.NewReader(bytes.NewReader(b[:read]))
if readByte(r) != 'C' {
panic("invalid scp command")
}
fileMode := readN(r, 4)
if string(fileMode) != "0600" {
panic("unexpected file mode")
}
if readByte(r) != ' ' {
panic("missing space")
}
fileSizeStr := must(r.ReadString(' '))
fileSize := must(strconv.Atoi(fileSizeStr[:len(fileSizeStr)-1]))
fileName := strings.TrimSpace(string(must(io.ReadAll(r))))
if fileName != filepath.Base(*fullFilePath) {
panic(fmt.Errorf("invalid file name (expected \"%s\" from full path \"%s\" but got \"%s\")", filepath.Base(*fullFilePath), *fullFilePath, fileName))
}
if fileName != "420.cert.pem" && fileName != "420.key.pem" {
panic("invalid file name")
}
channel.Write([]byte{0})
buf := new(bytes.Buffer)
_, err := io.CopyN(buf, channel, int64(fileSize))
if err != nil {
panic("Failed to copy channel")
}
fmt.Println("Copied file with size:", buf.Len())
fmt.Println(buf.String())
if readLastByte(r) != 0x00 {
panic("expected ending null byte")
}
channel.Write([]byte{0})
channel.SendRequest("exit-status", false, binary.BigEndian.AppendUint32(nil, 0))
}()
}
}()
wg2.Wait()
return sshConn
}
type fakeAgentDb struct{}
func (f *fakeAgentDb) FindAgentToSync(ctx context.Context) ([]database.FindAgentToSyncRow, error) {
panic("implement me")
}
func (f *fakeAgentDb) UpdateAgentLastSync(ctx context.Context, arg database.UpdateAgentLastSyncParams) error {
if arg.ID != 1337 {
return fmt.Errorf("invalid agent id")
}
if !arg.LastSync.Valid {
return fmt.Errorf("invalid last sync")
}
return nil
}
func (f *fakeAgentDb) UpdateAgentCertNotAfter(ctx context.Context, arg database.UpdateAgentCertNotAfterParams) error {
if arg.AgentID != 1337 {
return fmt.Errorf("invalid agent id")
}
if arg.CertID != 420 {
return fmt.Errorf("invalid cert id")
}
if !arg.NotAfter.Valid {
return fmt.Errorf("invalid not after")
}
return nil
}
func must[T any](t T, err error) T {
if err != nil {
panic(err)
}
return t
}
func readN(r io.Reader, n int) []byte {
b := make([]byte, n)
_, err := io.ReadFull(r, b)
if err != nil {
panic(err)
}
return b
}
func readByte(r io.Reader) byte {
b := readN(r, 1)
return b[0]
}
func readLastByte(r io.Reader) byte {
var b [1]byte
_, err := io.ReadFull(r, b[:])
if !errors.Is(err, io.EOF) {
panic("expected EOF")
}
return b[0]
}

5
agent/logger.go Normal file
View File

@ -0,0 +1,5 @@
package agent
import "github.com/1f349/orchid/logger"
var Logger = logger.Logger.WithPrefix("Orchid Agent")

65
cmd/orchid/agent.go Normal file
View File

@ -0,0 +1,65 @@
package main
import (
"encoding/pem"
"github.com/1f349/orchid/logger"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/ssh"
"os"
"path/filepath"
)
// loadAgentPrivateKey simply attempts to load the agent ssh private key and if
// it is missing generates a new key
func loadAgentPrivateKey(wd string) ssh.Signer {
// load or create a key for orchid agent
agentPrivKeyPath := filepath.Join(wd, "agent_id_ed25519")
agentPubKeyPath := filepath.Join(wd, "agent_id_ed25519.pub")
agentPrivKeyBytes, err := os.ReadFile(agentPrivKeyPath)
switch {
case err == nil:
break
case os.IsNotExist(err):
pubKey, privKey, err := ed25519.GenerateKey(nil)
if err != nil {
logger.Logger.Fatal("Failed to generate agent private key", "err", err)
}
marshalPrivKey, err := ssh.MarshalPrivateKey(privKey, "orchid-agent")
if err != nil {
logger.Logger.Fatal("Failed to encode private key", "err", err)
}
agentPrivKeyBytes = pem.EncodeToMemory(marshalPrivKey)
// public key
sshPubKey, err := ssh.NewPublicKey(pubKey)
if err != nil {
logger.Logger.Fatal("Failed to encode public key", "err", err)
}
marshalPubKey := ssh.MarshalAuthorizedKey(sshPubKey)
if err != nil {
logger.Logger.Fatal("Failed to encode public key", "err", err)
}
// write to files
err = os.WriteFile(agentPrivKeyPath, agentPrivKeyBytes, 0600)
if err != nil {
logger.Logger.Fatal("Failed to write agent private key", "path", agentPrivKeyPath, "err", err)
}
err = os.WriteFile(agentPubKeyPath, marshalPubKey, 0644)
if err != nil {
logger.Logger.Fatal("Failed to write agent public key", "path", agentPubKeyPath, "err", err)
}
// we can continue now
break
case err != nil:
logger.Logger.Fatal("Failed to read agent private key", "path", agentPrivKeyPath, "err", err)
}
privKey, err := ssh.ParsePrivateKey(agentPrivKeyBytes)
if err != nil {
logger.Logger.Fatal("Failed to parse agent private key file", "path", agentPrivKeyPath, "err", err)
}
return privKey
}

View File

@ -3,10 +3,11 @@ package main
import "github.com/1f349/orchid/renewal"
type startUpConfig struct {
Listen string `yaml:"listen"`
Acme acmeConfig `yaml:"acme"`
LE renewal.LetsEncryptConfig `yaml:"letsEncrypt"`
Domains []string `yaml:"domains"`
Listen string `yaml:"listen"`
Acme acmeConfig `yaml:"acme"`
LE renewal.LetsEncryptConfig `yaml:"letsEncrypt"`
Domains []string `yaml:"domains"`
AgentKey string `yaml:"agentKey"`
}
type acmeConfig struct {

View File

@ -1,20 +1,116 @@
package main
import (
"context"
"flag"
"github.com/google/subcommands"
"github.com/1f349/mjwt"
"github.com/1f349/orchid"
"github.com/1f349/orchid/agent"
httpAcme "github.com/1f349/orchid/http-acme"
"github.com/1f349/orchid/logger"
"github.com/1f349/orchid/renewal"
"github.com/1f349/orchid/servers"
"github.com/1f349/violet/utils"
_ "github.com/mattn/go-sqlite3"
exitReload "github.com/mrmelon54/exit-reload"
"gopkg.in/yaml.v3"
"os"
"path/filepath"
"sync"
)
func main() {
subcommands.Register(subcommands.HelpCommand(), "")
subcommands.Register(subcommands.FlagsCommand(), "")
subcommands.Register(subcommands.CommandsCommand(), "")
subcommands.Register(&serveCmd{}, "")
subcommands.Register(&setupCmd{}, "")
var configPath string
func main() {
flag.StringVar(&configPath, "conf", "", "/path/to/config.json : path to the config file")
flag.Parse()
ctx := context.Background()
os.Exit(int(subcommands.Execute(ctx)))
logger.Logger.Info("Starting...")
if configPath == "" {
logger.Logger.Error("Config flag is missing")
trySetup(configPath)
return
}
wd, err := getWD(configPath)
if err != nil {
logger.Logger.Fatal("Failed to find config directory", "err", err)
}
// try to open the config file
openConf, err := os.Open(configPath)
switch {
case err == nil:
break
case os.IsNotExist(err):
logger.Logger.Warn("Failed to open config file", "err", err)
trySetup(wd)
return
default:
logger.Logger.Fatal("Open config file", "err", err)
}
// config file opened with no errors
defer openConf.Close()
var config startUpConfig
err = yaml.NewDecoder(openConf).Decode(&config)
if err != nil {
logger.Logger.Fatal("Invalid config file", "err", err)
}
runDaemon(wd, config)
}
func runDaemon(wd string, conf startUpConfig) {
// load the MJWT RSA public key from a pem encoded file
mJwtVerify, err := mjwt.NewKeyStoreFromPath(filepath.Join(wd, "keys"))
if err != nil {
logger.Logger.Fatal("Failed to load MJWT verifier public key from file", "path", filepath.Join(wd, "keys"), "err", err)
}
// open sqlite database
db, err := orchid.InitDB(filepath.Join(wd, "orchid.db.sqlite"))
if err != nil {
logger.Logger.Fatal("Failed to open database", "err", err)
}
certDir := filepath.Join(wd, "renewal-certs")
keyDir := filepath.Join(wd, "renewal-keys")
wg := new(sync.WaitGroup)
acmeProv, err := httpAcme.NewHttpAcmeProvider(filepath.Join(wd, "tokens.yml"), conf.Acme.PresentUrl, conf.Acme.CleanUpUrl, conf.Acme.RefreshUrl)
if err != nil {
logger.Logger.Fatal("HTTP Acme Error", "err", err)
}
renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir)
if err != nil {
logger.Logger.Fatal("Service Error", "err", err)
}
certAgent, err := agent.NewAgent(wg, db, loadAgentPrivateKey(wd), certDir, keyDir)
if err != nil {
logger.Logger.Fatal("Failed to create agent", "err", err)
}
srv := servers.NewApiServer(conf.Listen, db, mJwtVerify, conf.Domains)
logger.Logger.Info("Starting API server", "listen", srv.Addr)
go utils.RunBackgroundHttp(logger.Logger, srv)
exitReload.ExitReload("Violet", func() {}, func() {
// stop renewal service and api server
renewalService.Shutdown()
certAgent.Shutdown()
srv.Close()
})
}
func getWD(configPath string) (string, error) {
if configPath == "" {
return os.Getwd()
}
wdAbs, err := filepath.Abs(configPath)
if err != nil {
return "", err
}
return filepath.Dir(wdAbs), nil
}

View File

@ -1,100 +0,0 @@
package main
import (
"context"
"flag"
"github.com/1f349/mjwt"
"github.com/1f349/orchid"
httpAcme "github.com/1f349/orchid/http-acme"
"github.com/1f349/orchid/logger"
"github.com/1f349/orchid/renewal"
"github.com/1f349/orchid/servers"
"github.com/1f349/violet/utils"
"github.com/google/subcommands"
_ "github.com/mattn/go-sqlite3"
"github.com/mrmelon54/exit-reload"
"gopkg.in/yaml.v3"
"log"
"os"
"path/filepath"
"sync"
)
type serveCmd struct{ configPath string }
func (s *serveCmd) Name() string { return "serve" }
func (s *serveCmd) Synopsis() string { return "Serve certificate renewal service" }
func (s *serveCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&s.configPath, "conf", "", "/path/to/config.json : path to the config file")
}
func (s *serveCmd) Usage() string {
return `serve [-conf <config file>]
Serve certificate renewal service using information from config file
`
}
func (s *serveCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
logger.Logger.Info("[Orchid] Starting...")
if s.configPath == "" {
logger.Logger.Info("[Orchid] Error: config flag is missing")
return subcommands.ExitUsageError
}
openConf, err := os.Open(s.configPath)
if err != nil {
if os.IsNotExist(err) {
logger.Logger.Info("[Orchid] Error: missing config file")
} else {
logger.Logger.Info("[Orchid] Error: open config file: ", "err", err)
}
return subcommands.ExitFailure
}
var conf startUpConfig
err = yaml.NewDecoder(openConf).Decode(&conf)
if err != nil {
logger.Logger.Info("[Orchid] Error: invalid config file: ", "err", err)
return subcommands.ExitFailure
}
wd := filepath.Dir(s.configPath)
normalLoad(conf, wd)
return subcommands.ExitSuccess
}
func normalLoad(conf startUpConfig, wd string) {
// load the MJWT RSA public key from a pem encoded file
mJwtVerify, err := mjwt.NewKeyStoreFromPath(filepath.Join(wd, "keys"))
if err != nil {
log.Fatalf("[Orchid] Failed to load MJWT verifier public key from file '%s': %s", filepath.Join(wd, "keys"), err)
}
// open sqlite database
db, err := orchid.InitDB(filepath.Join(wd, "orchid.db.sqlite"))
if err != nil {
log.Fatal("[Orchid] Failed to open database:", err)
}
certDir := filepath.Join(wd, "renewal-certs")
keyDir := filepath.Join(wd, "renewal-keys")
wg := &sync.WaitGroup{}
acmeProv, err := httpAcme.NewHttpAcmeProvider(filepath.Join(wd, "tokens.yml"), conf.Acme.PresentUrl, conf.Acme.CleanUpUrl, conf.Acme.RefreshUrl)
if err != nil {
log.Fatal("[Orchid] HTTP Acme Error:", err)
}
renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir)
if err != nil {
log.Fatal("[Orchid] Service Error:", err)
}
srv := servers.NewApiServer(conf.Listen, db, mJwtVerify, conf.Domains)
logger.Logger.Info("Starting API server", "listen", srv.Addr)
go utils.RunBackgroundHttp(logger.Logger, srv)
exit_reload.ExitReload("Violet", func() {}, func() {
// stop renewal service and api server
renewalService.Shutdown()
srv.Close()
})
}

View File

@ -2,16 +2,15 @@ package main
import (
"bytes"
"context"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"flag"
"errors"
"fmt"
httpAcme "github.com/1f349/orchid/http-acme"
"github.com/1f349/orchid/logger"
"github.com/1f349/orchid/renewal"
"github.com/AlecAivazis/survey/v2"
"github.com/google/subcommands"
"gopkg.in/yaml.v3"
"math/rand"
"net"
@ -22,37 +21,32 @@ import (
"time"
)
type setupCmd struct{ wdPath string }
var errExitSetup = errors.New("exit setup")
func (s *setupCmd) Name() string { return "setup" }
func (s *setupCmd) Synopsis() string { return "Setup certificate renewal service" }
func (s *setupCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&s.wdPath, "wd", ".", "Path to the directory to create config files in (defaults to the working directory)")
}
func (s *setupCmd) Usage() string {
return `setup [-wd <directory>]
Setup Orchid automatically by answering questions.
`
}
func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
// get absolute path to specify files
wdAbs, err := filepath.Abs(s.wdPath)
if err != nil {
fmt.Println("[Orchid] Failed to get full directory path: ", err)
return subcommands.ExitFailure
func trySetup(wd string) {
// handle potential errors during setup
err := runSetup(wd)
switch {
case errors.Is(err, errExitSetup):
// exit setup without questions
return
case err == nil:
return
default:
logger.Logger.Fatal("Failed to run setup", "err", err)
}
}
func runSetup(wd string) error {
// ask about running the setup steps
createFile := false
err = survey.AskOne(&survey.Confirm{Message: fmt.Sprintf("Create Orchid config files in this directory: '%s'?", wdAbs)}, &createFile)
err := survey.AskOne(&survey.Confirm{Message: fmt.Sprintf("Create Orchid config files in this directory: '%s'?", wd)}, &createFile)
if err != nil {
fmt.Println("[Orchid] Error: ", err)
return subcommands.ExitFailure
return err
}
if !createFile {
fmt.Println("[Orchid] Goodbye")
return subcommands.ExitSuccess
logger.Logger.Info("Goodbye")
return errExitSetup
}
var answers struct {
@ -64,7 +58,6 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
AcmeRefreshUrl string
LEEmail string
}
_ = answers
// ask main questions
err = survey.Ask([]*survey.Question{
@ -88,8 +81,7 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
},
}, &answers)
if err != nil {
fmt.Println("[Orchid] Error: ", err)
return subcommands.ExitFailure
return err
}
if answers.AcmeRefresh != "" {
@ -111,35 +103,31 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
},
}, &answers)
if err != nil {
fmt.Println("[Orchid] Error: ", err)
return subcommands.ExitFailure
return err
}
}
key, err := rsa.GenerateKey(rand.New(rand.NewSource(time.Now().UnixNano())), 4096)
if err != nil {
fmt.Println("[Orchid] Error: ", err)
return subcommands.ExitFailure
return fmt.Errorf("failed to generate private key: %w", err)
}
keyBytes := x509.MarshalPKCS1PrivateKey(key)
keyBuf := new(bytes.Buffer)
err = pem.Encode(keyBuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes})
if err != nil {
fmt.Println("[Orchid] Error: ", err)
return subcommands.ExitFailure
return fmt.Errorf("failed to PEM encode private key: %w", err)
}
// write config file
confFile := filepath.Join(wdAbs, "config.yml")
confFile := filepath.Join(wd, "config.yml")
createConf, err := os.Create(confFile)
if err != nil {
fmt.Println("[Orchid] Failed to create config file: ", err)
return subcommands.ExitFailure
return fmt.Errorf("failed to create config file: %w", err)
}
defer createConf.Close()
confEncode := yaml.NewEncoder(createConf)
confEncode.SetIndent(2)
err = confEncode.Encode(startUpConfig{
// this is the whole config structure
config := startUpConfig{
Listen: answers.ApiListen,
Acme: acmeConfig{
PresentUrl: answers.AcmePresentUrl,
@ -155,18 +143,20 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
Certificate: "default",
},
Domains: strings.Split(answers.ApiDomains, ","),
})
}
confEncode := yaml.NewEncoder(createConf)
confEncode.SetIndent(2)
err = confEncode.Encode(config)
if err != nil {
fmt.Println("[Orchid] Failed to write config file: ", err)
return subcommands.ExitFailure
return fmt.Errorf("failed to write config file: %w", err)
}
// write token file
tokenFile := filepath.Join(wdAbs, "tokens.yml")
tokenFile := filepath.Join(wd, "tokens.yml")
createTokens, err := os.Create(tokenFile)
if err != nil {
fmt.Println("[Orchid] Failed to create tokens file: ", err)
return subcommands.ExitFailure
return fmt.Errorf("failed to create tokens file: %w", err)
}
confEncode = yaml.NewEncoder(createTokens)
@ -176,14 +166,13 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
Refresh: answers.AcmeRefresh,
})
if err != nil {
fmt.Println("[Orchid] Failed to write tokens file: ", err)
return subcommands.ExitFailure
return fmt.Errorf("failed to write tokens file: %w", err)
}
fmt.Println("[Orchid] Setup complete")
fmt.Printf("[Orchid] Run the renewal service with `orchid serve -conf %s`\n", confFile)
logger.Logger.Info("Setup complete")
logger.Logger.Infof("Run the renewal service with `orchid-daemon -conf %s`", confFile)
return subcommands.ExitSuccess
return nil
}
func listenAddressValidator(ans interface{}) error {

98
database/agent.sql.go Normal file
View File

@ -0,0 +1,98 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// source: agent.sql
package database
import (
"context"
"database/sql"
)
const findAgentToSync = `-- name: FindAgentToSync :many
SELECT agents.id as agent_id, agents.address, agents.user, agents.dir, agents.fingerprint, cert.id as cert_id, cert.not_after as cert_not_after
FROM agents
INNER JOIN agent_certs
ON agent_certs.agent_id = agents.id
INNER JOIN certificates AS cert
ON cert.id = agent_certs.cert_id
WHERE (agents.last_sync IS NULL OR agents.last_sync < cert.updated_at)
AND (agent_certs.not_after IS NULL OR agent_certs.not_after IS NOT cert.not_after)
ORDER BY agents.last_sync NULLS FIRST
`
type FindAgentToSyncRow struct {
AgentID int64 `json:"agent_id"`
Address string `json:"address"`
User string `json:"user"`
Dir string `json:"dir"`
Fingerprint string `json:"fingerprint"`
CertID int64 `json:"cert_id"`
CertNotAfter sql.NullTime `json:"cert_not_after"`
}
func (q *Queries) FindAgentToSync(ctx context.Context) ([]FindAgentToSyncRow, error) {
rows, err := q.db.QueryContext(ctx, findAgentToSync)
if err != nil {
return nil, err
}
defer rows.Close()
var items []FindAgentToSyncRow
for rows.Next() {
var i FindAgentToSyncRow
if err := rows.Scan(
&i.AgentID,
&i.Address,
&i.User,
&i.Dir,
&i.Fingerprint,
&i.CertID,
&i.CertNotAfter,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateAgentCertNotAfter = `-- name: UpdateAgentCertNotAfter :exec
UPDATE agent_certs
SET not_after = ?
WHERE agent_id = ?
AND cert_id = ?
`
type UpdateAgentCertNotAfterParams struct {
NotAfter sql.NullTime `json:"not_after"`
AgentID int64 `json:"agent_id"`
CertID int64 `json:"cert_id"`
}
func (q *Queries) UpdateAgentCertNotAfter(ctx context.Context, arg UpdateAgentCertNotAfterParams) error {
_, err := q.db.ExecContext(ctx, updateAgentCertNotAfter, arg.NotAfter, arg.AgentID, arg.CertID)
return err
}
const updateAgentLastSync = `-- name: UpdateAgentLastSync :exec
UPDATE agents
SET last_sync = ?
WHERE agents.id = ?
`
type UpdateAgentLastSyncParams struct {
LastSync sql.NullTime `json:"last_sync"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateAgentLastSync(ctx context.Context, arg UpdateAgentLastSyncParams) error {
_, err := q.db.ExecContext(ctx, updateAgentLastSync, arg.LastSync, arg.ID)
return err
}

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.28.0
// source: certificate.sql
package database
@ -19,7 +19,7 @@ VALUES (?, ?, ?, ?)
type AddCertificateParams struct {
Owner string `json:"owner"`
Dns sql.NullInt64 `json:"dns"`
NotAfter time.Time `json:"not_after"`
NotAfter sql.NullTime `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
}
@ -75,7 +75,7 @@ FROM certificates AS cert
WHERE cert.active = 1
AND (cert.auto_renew = 1 OR cert.not_after IS NULL)
AND cert.renewing = 0
AND cert.renew_failed = 0
AND (cert.renew_retry IS NULL OR DATETIME() > DATETIME(cert.renew_retry))
AND (cert.not_after IS NULL OR DATETIME(cert.not_after, 'utc', '-30 days') < DATETIME())
ORDER BY cert.temp_parent, cert.not_after DESC NULLS FIRST
LIMIT 1
@ -83,7 +83,7 @@ LIMIT 1
type FindNextCertRow struct {
ID int64 `json:"id"`
NotAfter time.Time `json:"not_after"`
NotAfter sql.NullTime `json:"not_after"`
Type sql.NullString `json:"type"`
Token sql.NullString `json:"token"`
TempParent sql.NullInt64 `json:"temp_parent"`
@ -107,7 +107,7 @@ SELECT cert.id,
cert.auto_renew,
cert.active,
cert.renewing,
cert.renew_failed,
cert.renew_retry,
cert.not_after,
cert.updated_at,
certificate_domains.domain
@ -116,14 +116,14 @@ FROM certificates AS cert
`
type FindOwnedCertsRow struct {
ID int64 `json:"id"`
AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"`
Renewing bool `json:"renewing"`
RenewFailed bool `json:"renew_failed"`
NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
Domain string `json:"domain"`
ID int64 `json:"id"`
AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"`
Renewing bool `json:"renewing"`
RenewRetry sql.NullTime `json:"renew_retry"`
NotAfter sql.NullTime `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
Domain string `json:"domain"`
}
func (q *Queries) FindOwnedCerts(ctx context.Context) ([]FindOwnedCertsRow, error) {
@ -140,7 +140,7 @@ func (q *Queries) FindOwnedCerts(ctx context.Context) ([]FindOwnedCertsRow, erro
&i.AutoRenew,
&i.Active,
&i.Renewing,
&i.RenewFailed,
&i.RenewRetry,
&i.NotAfter,
&i.UpdatedAt,
&i.Domain,
@ -169,19 +169,30 @@ func (q *Queries) RemoveCertificate(ctx context.Context, id int64) error {
return err
}
const setRetryFlag = `-- name: SetRetryFlag :exec
UPDATE certificates
SET renew_retry = DATETIME('now', '+1 day')
WHERE id = ?
`
func (q *Queries) SetRetryFlag(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, setRetryFlag, id)
return err
}
const updateCertAfterRenewal = `-- name: UpdateCertAfterRenewal :exec
UPDATE certificates
SET renewing = 0,
renew_failed=0,
SET renewing = 0,
renew_retry=0,
not_after=?,
updated_at=?
WHERE id = ?
`
type UpdateCertAfterRenewalParams struct {
NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
ID int64 `json:"id"`
NotAfter sql.NullTime `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateCertAfterRenewal(ctx context.Context, arg UpdateCertAfterRenewalParams) error {
@ -191,18 +202,18 @@ func (q *Queries) UpdateCertAfterRenewal(ctx context.Context, arg UpdateCertAfte
const updateRenewingState = `-- name: UpdateRenewingState :exec
UPDATE certificates
SET renewing = ?,
renew_failed = ?
SET renewing = ?,
renew_retry = ?
WHERE id = ?
`
type UpdateRenewingStateParams struct {
Renewing bool `json:"renewing"`
RenewFailed bool `json:"renew_failed"`
ID int64 `json:"id"`
Renewing bool `json:"renewing"`
RenewRetry sql.NullTime `json:"renew_retry"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateRenewingState(ctx context.Context, arg UpdateRenewingStateParams) error {
_, err := q.db.ExecContext(ctx, updateRenewingState, arg.Renewing, arg.RenewFailed, arg.ID)
_, err := q.db.ExecContext(ctx, updateRenewingState, arg.Renewing, arg.RenewRetry, arg.ID)
return err
}

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.28.0
// source: certificate_domains.sql
package database

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.28.0
package database

View File

@ -0,0 +1,5 @@
ALTER TABLE certificates
DROP COLUMN renew_failed;
ALTER TABLE certificates
ADD COLUMN renew_retry DATETIME NOT NULL DEFAULT 0;

View File

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS agents
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
address TEXT NOT NULL,
user TEXT NOT NULL,
dir TEXT NOT NULL,
fingerprint TEXT NOT NULL,
last_sync DATETIME NULL DEFAULT NULL
);
CREATE TABLE IF NOT EXISTS agent_certs
(
agent_id INTEGER NOT NULL,
cert_id INTEGER NOT NULL,
not_after INTEGER NULL DEFAULT NULL,
PRIMARY KEY (agent_id, cert_id),
FOREIGN KEY (agent_id) REFERENCES agents (id),
FOREIGN KEY (cert_id) REFERENCES certificates (id)
);

View File

@ -0,0 +1,29 @@
-- null not after
ALTER TABLE certificates
RENAME COLUMN not_after TO not_after_2;
ALTER TABLE certificates
ADD COLUMN not_after DATETIME NULL;
UPDATE certificates
SET not_after = not_after_2
WHERE not_after IS NULL;
ALTER TABLE certificates
DROP COLUMN not_after_2;
-- null renew retry
ALTER TABLE certificates
RENAME COLUMN renew_retry TO renew_retry_2;
ALTER TABLE certificates
ADD COLUMN renew_retry DATETIME NULL;
UPDATE certificates
SET renew_retry = renew_retry_2
WHERE renew_retry IS NULL;
ALTER TABLE certificates
DROP COLUMN renew_retry_2;

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.28.0
package database
@ -9,17 +9,32 @@ import (
"time"
)
type Agent struct {
ID int64 `json:"id"`
Address string `json:"address"`
User string `json:"user"`
Dir string `json:"dir"`
Fingerprint string `json:"fingerprint"`
LastSync sql.NullTime `json:"last_sync"`
}
type AgentCert struct {
AgentID int64 `json:"agent_id"`
CertID int64 `json:"cert_id"`
NotAfter sql.NullTime `json:"not_after"`
}
type Certificate struct {
ID int64 `json:"id"`
Owner string `json:"owner"`
Dns sql.NullInt64 `json:"dns"`
AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"`
Renewing bool `json:"renewing"`
RenewFailed bool `json:"renew_failed"`
NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
TempParent sql.NullInt64 `json:"temp_parent"`
ID int64 `json:"id"`
Owner string `json:"owner"`
Dns sql.NullInt64 `json:"dns"`
AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"`
Renewing bool `json:"renewing"`
UpdatedAt time.Time `json:"updated_at"`
TempParent sql.NullInt64 `json:"temp_parent"`
NotAfter sql.NullTime `json:"not_after"`
RenewRetry sql.NullTime `json:"renew_retry"`
}
type CertificateDomain struct {

View File

@ -0,0 +1,21 @@
-- name: FindAgentToSync :many
SELECT agents.id as agent_id, agents.address, agents.user, agents.dir, agents.fingerprint, cert.id as cert_id, cert.not_after as cert_not_after
FROM agents
INNER JOIN agent_certs
ON agent_certs.agent_id = agents.id
INNER JOIN certificates AS cert
ON cert.id = agent_certs.cert_id
WHERE (agents.last_sync IS NULL OR agents.last_sync < cert.updated_at)
AND (agent_certs.not_after IS NULL OR agent_certs.not_after IS NOT cert.not_after)
ORDER BY agents.last_sync NULLS FIRST;
-- name: UpdateAgentLastSync :exec
UPDATE agents
SET last_sync = ?
WHERE agents.id = ?;
-- name: UpdateAgentCertNotAfter :exec
UPDATE agent_certs
SET not_after = ?
WHERE agent_id = ?
AND cert_id = ?;

View File

@ -5,7 +5,7 @@ FROM certificates AS cert
WHERE cert.active = 1
AND (cert.auto_renew = 1 OR cert.not_after IS NULL)
AND cert.renewing = 0
AND cert.renew_failed = 0
AND (cert.renew_retry IS NULL OR DATETIME() > DATETIME(cert.renew_retry))
AND (cert.not_after IS NULL OR DATETIME(cert.not_after, 'utc', '-30 days') < DATETIME())
ORDER BY cert.temp_parent, cert.not_after DESC NULLS FIRST
LIMIT 1;
@ -15,7 +15,7 @@ SELECT cert.id,
cert.auto_renew,
cert.active,
cert.renewing,
cert.renew_failed,
cert.renew_retry,
cert.not_after,
cert.updated_at,
certificate_domains.domain
@ -24,14 +24,19 @@ FROM certificates AS cert
-- name: UpdateRenewingState :exec
UPDATE certificates
SET renewing = ?,
renew_failed = ?
SET renewing = ?,
renew_retry = ?
WHERE id = ?;
-- name: SetRetryFlag :exec
UPDATE certificates
SET renew_retry = DATETIME('now', '+1 day')
WHERE id = ?;
-- name: UpdateCertAfterRenewal :exec
UPDATE certificates
SET renewing = 0,
renew_failed=0,
SET renewing = 0,
renew_retry=0,
not_after=?,
updated_at=?
WHERE id = ?;

View File

@ -3,12 +3,15 @@ package database
import (
"context"
"database/sql"
"errors"
)
var errMissingSqlDB = errors.New("cannot open transaction without sql.DB")
func (q *Queries) UseTx(ctx context.Context, cb func(tx *Queries) error) error {
sqlDB, ok := q.db.(*sql.DB)
if !ok {
panic("cannot open transaction without sql.DB")
return errMissingSqlDB
}
tx, err := sqlDB.BeginTx(ctx, nil)
if err != nil {

56
go.mod
View File

@ -1,23 +1,24 @@
module github.com/1f349/orchid
go 1.22
go 1.24.1
require (
github.com/1f349/mjwt v0.4.1
github.com/1f349/violet v0.0.14
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/log v0.4.0
github.com/go-acme/lego/v4 v4.17.3
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang-migrate/migrate/v4 v4.17.1
github.com/google/subcommands v1.2.0
github.com/bramvdbogaerde/go-scp v1.5.0
github.com/charmbracelet/log v0.4.1
github.com/go-acme/lego/v4 v4.22.2
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/golang-migrate/migrate/v4 v4.18.2
github.com/google/uuid v1.6.0
github.com/julienschmidt/httprouter v1.3.0
github.com/mattn/go-sqlite3 v1.14.22
github.com/miekg/dns v1.1.61
github.com/mattn/go-sqlite3 v1.14.24
github.com/miekg/dns v1.1.64
github.com/mrmelon54/certgen v0.0.2
github.com/mrmelon54/exit-reload v0.0.2
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.36.0
gopkg.in/yaml.v3 v3.0.1
)
@ -26,34 +27,37 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/becheran/wildmatch-go v1.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/charmbracelet/lipgloss v0.12.1 // indirect
github.com/charmbracelet/x/ansi v0.1.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.24.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.31.0 // indirect
)

108
go.sum
View File

@ -12,37 +12,44 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4=
github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM=
github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ=
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=
github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs=
github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk=
github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-acme/lego/v4 v4.17.3 h1:5our7Qdyik0abag40abdmQuytq97iweaNHFMT4pYDnQ=
github.com/go-acme/lego/v4 v4.17.3/go.mod h1:Ol6l04hnmavqVHKYS/ByhXXqE64x8yVYhomha82uAUk=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
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/go-acme/lego/v4 v4.22.2 h1:ck+HllWrV/rZGeYohsKQ5iKNnU/WAZxwOdiu6cxky+0=
github.com/go-acme/lego/v4 v4.22.2/go.mod h1:E2FndyI3Ekv0usNJt46mFb9LVpV/XBYT+4E3tz02Tzo=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -65,91 +72,92 @@ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ=
github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
github.com/mrmelon54/certgen v0.0.2 h1:4CMDkA/gGZu+E4iikU+5qdOWK7qOQrk58KtUfnmyYmY=
github.com/mrmelon54/certgen v0.0.2/go.mod h1:vwrWSXQmxZYqEyh+cf05IvDIFV2aYuxL4+O6ABIlN8M=
github.com/mrmelon54/exit-reload v0.0.2 h1:vqgfrMD/bF21HkDsWgg5+NLjFDrD3KGVEN/iTrMn9Ms=
github.com/mrmelon54/exit-reload v0.0.2/go.mod h1:aE3NhsqGMLUqmv6cJZRouC/8gXkZTvVSabRGOpI+Vjc=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -13,6 +13,7 @@ import (
"fmt"
"github.com/1f349/orchid/database"
"github.com/1f349/orchid/pebble"
"github.com/1f349/orchid/utils"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
@ -201,6 +202,8 @@ var ErrAlreadyRenewing = errors.New("already renewing")
// renewalRoutine is the main loop which makes used of certTicker to constantly
// check if the existing certificates are up-to-date.
func (s *Service) renewalRoutine(wg *sync.WaitGroup) {
Logger.Debug("Starting renewalRoutine")
// Upon leaving the function stop the ticker and clear the WaitGroup.
defer func() {
s.certTicker.Stop()
@ -212,8 +215,7 @@ func (s *Service) renewalRoutine(wg *sync.WaitGroup) {
Logger.Info("Doing quick certificate check before starting...")
err := s.renewalCheck()
if err != nil {
Logger.Info("Certificate check, should not error first try: ", "err", err)
return
Logger.Info("Certificate check, should not error first try", "err", err)
}
// Logging or something
@ -226,12 +228,14 @@ func (s *Service) renewalRoutine(wg *sync.WaitGroup) {
// Exit if certDone has closed
return
case <-s.certTicker.C:
Logger.Debug("Ticking certificate renewal")
// Start a new check in a separate routine
go func() {
// run a renewal check and log errors, but ignore ErrAlreadyRenewing
err := s.renewalCheck()
if err != nil && !errors.Is(err, ErrAlreadyRenewing) {
Logger.Info("Certificate check, an error occurred: ", "err", err)
Logger.Info("Certificate check, an error occurred", "err", err)
}
}()
}
@ -246,6 +250,8 @@ func (s *Service) renewalCheck() error {
}
defer s.renewLock.Unlock()
Logger.Debug("Running renewalCheck")
// check for running out certificates in the database
localData, err := s.findNextCertificateToRenew()
if err != nil {
@ -254,12 +260,14 @@ func (s *Service) renewalCheck() error {
// no certificates to update
if localData == nil {
Logger.Debug("No certificates to update")
return nil
}
// renew the certificate from the collected data
err = s.renewCert(localData)
if err != nil {
Logger.Debug("Failed to renew certificate", "err", err)
return err
}
@ -285,7 +293,7 @@ func (s *Service) findNextCertificateToRenew() (*localCertData, error) {
d.id = row.ID
d.dns.name = row.Type
d.dns.token = row.Token
d.notAfter = row.NotAfter
d.notAfter = row.NotAfter.Time
d.tempParent = row.TempParent
return d, nil
@ -315,6 +323,7 @@ func (s *Service) setupLegoClient() (*lego.Client, error) {
}
// create lego client from the config
// TODO: use a custom logger via `github.com/go-acme/lego/v4/log.Logger`
client, err := lego.NewClient(config)
if err != nil {
return nil, fmt.Errorf("failed to generate client: %w", err)
@ -360,7 +369,7 @@ func (s *Service) getDnsProvider(name, token string) (challenge.Provider, error)
// getPrivateKey reads the private key for the specified certificate id, or
// generates one is the file doesn't exist
func (s *Service) getPrivateKey(id int64) (*rsa.PrivateKey, error) {
fPath := filepath.Join(s.keyDir, fmt.Sprintf("%d.key.pem", id))
fPath := filepath.Join(s.keyDir, utils.GetKeyFileName(id))
pemBytes, err := os.ReadFile(fPath)
if err != nil {
if os.IsNotExist(err) {
@ -391,18 +400,20 @@ func (s *Service) getPrivateKey(id int64) (*rsa.PrivateKey, error) {
// certificate to the certDir directory.
func (s *Service) renewCert(localData *localCertData) error {
// database synchronous state
s.setRenewing(localData.id, true, false)
s.setRenewing(localData.id, true)
Logger.Debug("No certificates to update")
// run internal renewal code and log errors
cert, certBytes, err := s.renewCertInternal(localData)
if err != nil {
s.setRenewing(localData.id, false, true)
s.setRenewing(localData.id, false)
s.setRetry(localData.id)
return fmt.Errorf("failed to renew cert %d: %w", localData.id, err)
}
// set the NotAfter/NotBefore in the database
err = s.db.UpdateCertAfterRenewal(context.Background(), database.UpdateCertAfterRenewalParams{
NotAfter: cert.NotAfter,
NotAfter: sql.NullTime{Time: cert.NotAfter, Valid: true},
UpdatedAt: cert.NotBefore,
ID: localData.id,
})
@ -492,22 +503,28 @@ func (s *Service) renewCertInternal(localData *localCertData) (*x509.Certificate
// setRenewing sets the renewing and failed states in the database for a
// specified certificate id.
func (s *Service) setRenewing(id int64, renewing, failed bool) {
func (s *Service) setRenewing(id int64, renewing bool) {
err := s.db.UpdateRenewingState(context.Background(), database.UpdateRenewingStateParams{
Renewing: renewing,
RenewFailed: failed,
ID: id,
Renewing: renewing,
ID: id,
})
if err != nil {
Logger.Warn("Failed to set renewing/failed mode in database", "id", id, "err", err)
Logger.Warn("Failed to set renewing mode in database", "id", id, "err", err)
}
}
func (s *Service) setRetry(id int64) {
err := s.db.SetRetryFlag(context.Background(), id)
if err != nil {
Logger.Warn("Failed to set retry time in database", "id", id, "err", err)
}
}
// writeCertFile writes the output certificate file and renames the current one
// to include `-old` in the name.
func (s *Service) writeCertFile(id int64, certBytes []byte) error {
oldPath := filepath.Join(s.certDir, fmt.Sprintf("%d-old.cert.pem", id))
newPath := filepath.Join(s.certDir, fmt.Sprintf("%d.cert.pem", id))
oldPath := filepath.Join(s.certDir, utils.GetOldCertFileName(id))
newPath := filepath.Join(s.certDir, utils.GetCertFileName(id))
// move certificate file to old name
err := os.Rename(newPath, oldPath)

View File

@ -10,8 +10,10 @@ import (
"encoding/pem"
"fmt"
"github.com/1f349/orchid"
"github.com/1f349/orchid/logger"
"github.com/1f349/orchid/pebble"
"github.com/1f349/orchid/test"
"github.com/charmbracelet/log"
"github.com/go-acme/lego/v4/lego"
"github.com/google/uuid"
_ "github.com/mattn/go-sqlite3"
@ -71,7 +73,7 @@ func setupPebbleSuite(tb testing.TB) (*certgen.CertGen, func()) {
assert.NoError(tb, os.WriteFile(filepath.Join(pebbleTmp, "certs", "localhost", "cert.pem"), serverTls.GetCertPem(), os.ModePerm))
assert.NoError(tb, os.WriteFile(filepath.Join(pebbleTmp, "certs", "localhost", "key.pem"), serverTls.GetKeyPem(), os.ModePerm))
dnsServer := test.MakeFakeDnsProv("127.0.0.34:5053") // 127.0.0.34:53
dnsServer := test.MakeFakeDnsProv("127.243.243.34:5053") // 127.243.243.34:53
dnsServer.AddRecursiveSOA("example.test.")
go dnsServer.Start()
testDnsOptions = dnsServer
@ -140,6 +142,8 @@ func deconstructPebbleTest(t *testing.T, service *Service) {
}
func TestPebbleRenewal(t *testing.T) {
logger.Logger.SetLevel(log.DebugLevel)
if testing.Short() {
t.Skip("Skipping renewal tests in short mode")
}
@ -166,7 +170,7 @@ func TestPebbleRenewal(t *testing.T) {
_, err := db2.Exec("DELETE FROM certificate_domains")
assert.NoError(t, err)
_, err = db2.Exec(`INSERT INTO certificates (owner, dns, auto_renew, active, renewing, renew_failed, not_after, updated_at) VALUES (1, 1, 1, 1, 0, 0, "2000-01-01 00:00:00+00:00", "2000-01-01 00:00:00+00:00")`)
_, err = db2.Exec(`INSERT INTO certificates (owner, dns, auto_renew, active, renewing, renew_retry, not_after, updated_at) VALUES (1, 1, 1, 1, 0, '2000-01-01 00:00:00+00:00', '2000-01-01 00:00:00+00:00', '2000-01-01 00:00:00+00:00');`)
assert.NoError(t, err)
for _, j := range i.domains {
_, err = db2.Exec(`INSERT INTO certificate_domains (cert_id, domain) VALUES (1, ?)`, j)

View File

@ -25,14 +25,14 @@ type DomainStateValue struct {
}
type Certificate struct {
Id int64 `json:"id"`
AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"`
Renewing bool `json:"renewing"`
RenewFailed bool `json:"renew_failed"`
NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
Domains []string `json:"domains"`
Id int64 `json:"id"`
AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"`
Renewing bool `json:"renewing"`
RenewRetry time.Time `json:"renew_retry"`
NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
Domains []string `json:"domains"`
}
// NewApiServer creates and runs a http server containing all the API
@ -69,13 +69,13 @@ func NewApiServer(listen string, db *database.Queries, signer *mjwt.KeyStore, do
// loop over query rows
for _, row := range rows {
c := Certificate{
Id: row.ID,
AutoRenew: row.AutoRenew,
Active: row.Active,
Renewing: row.Renewing,
RenewFailed: row.RenewFailed,
NotAfter: row.NotAfter,
UpdatedAt: row.UpdatedAt,
Id: row.ID,
AutoRenew: row.AutoRenew,
Active: row.Active,
Renewing: row.Renewing,
RenewRetry: row.RenewRetry.Time,
NotAfter: row.NotAfter.Time,
UpdatedAt: row.UpdatedAt,
}
d := row.Domain
@ -136,7 +136,7 @@ func NewApiServer(listen string, db *database.Queries, signer *mjwt.KeyStore, do
err := db.AddCertificate(req.Context(), database.AddCertificateParams{
Owner: b.Subject,
Dns: sql.NullInt64{},
NotAfter: time.Now(),
NotAfter: sql.NullTime{Time: time.Now(), Valid: true},
UpdatedAt: time.Now(),
})
if err != nil {

View File

@ -8,3 +8,12 @@ sql:
package: "database"
out: "database"
emit_json_tags: true
overrides:
- column: certificates.not_after
go_type: database/sql.NullTime
- column: certificates.renew_retry
go_type: database/sql.NullTime
- column: agents.last_sync
go_type: database/sql.NullTime
- column: agent_certs.not_after
go_type: database/sql.NullTime

View File

@ -2,10 +2,10 @@ package test
import (
"fmt"
"github.com/1f349/orchid/logger"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/miekg/dns"
"log"
"strings"
)
@ -47,13 +47,13 @@ func (f *fakeDnsProv) AddRecursiveSOA(fqdn string) {
func (f *fakeDnsProv) Present(domain, _, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
f.mTxt[info.EffectiveFQDN] = info.Value
log.Printf("fakeDnsProv.Present(%s TXT %s)\n", info.EffectiveFQDN, info.Value)
logger.Logger.Infof("fakeDnsProv.Present(%s TXT %s)", info.EffectiveFQDN, info.Value)
return nil
}
func (f *fakeDnsProv) CleanUp(domain, _, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
delete(f.mTxt, info.EffectiveFQDN)
log.Printf("fakeDnsProv.CleanUp(%s TXT %s)\n", info.EffectiveFQDN, info.Value)
logger.Logger.Infof("fakeDnsProv.CleanUp(%s TXT %s)", info.EffectiveFQDN, info.Value)
return nil
}
func (f *fakeDnsProv) GetDnsAddrs() []string { return []string{f.Addr} }
@ -62,7 +62,7 @@ func (f *fakeDnsProv) parseQuery(m *dns.Msg) {
for _, q := range m.Question {
switch q.Qtype {
case dns.TypeTXT:
log.Printf("Looking up %s TXT record\n", q.Name)
logger.Logger.Info("Looking up TXT record", "name", q.Name)
txt := f.mTxt[q.Name]
if txt != "" {
rr, err := dns.NewRR(fmt.Sprintf("%s 32600 IN TXT \"%s\"", q.Name, txt))
@ -71,7 +71,7 @@ func (f *fakeDnsProv) parseQuery(m *dns.Msg) {
}
}
default:
log.Printf("Looking up %d for %s\n", q.Qtype, q.Name)
logger.Logger.Info("Looking up", "qtype", q.Qtype, "name", q.Name)
}
}
}
@ -95,10 +95,10 @@ func (f *fakeDnsProv) Start() {
// start server
f.srv = &dns.Server{Addr: f.Addr, Net: "udp"}
log.Printf("Starting fake dns service at %s\n", f.srv.Addr)
logger.Logger.Info("Starting fake dns service", "addr", f.srv.Addr)
err := f.srv.ListenAndServe()
if err != nil {
log.Fatalf("Failed to start server: %s\n ", err.Error())
logger.Logger.Fatal("Failed to start server", "err", err)
}
}

15
utils/name.go Normal file
View File

@ -0,0 +1,15 @@
package utils
import "fmt"
func GetCertFileName(id int64) string {
return fmt.Sprintf("%d.cert.pem", id)
}
func GetOldCertFileName(id int64) string {
return fmt.Sprintf("%d-old.cert.pem", id)
}
func GetKeyFileName(id int64) string {
return fmt.Sprintf("%d.key.pem", id)
}