From b1104cc00422b4465604107bfc16bf5696b5818a Mon Sep 17 00:00:00 2001 From: Tevildo Date: Sun, 14 Apr 2024 21:07:24 +0100 Subject: [PATCH 01/14] add auth package to hash requests --- auth/auth.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 auth/auth.go diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..b864192 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,57 @@ +package auth + +import ( + "crypto/rand" + "crypto/sha1" + "encoding/hex" + "fmt" + "math/big" + "strconv" + "strings" + "time" +) + +var ( + saltChars = "0123456789ABCDEFGHIJKLMNOPQRTSUVWXYZabcdefghijklmnopqrstuvwxyz" + baseUrl = "https://api.nearlyfreespeech.net" + authHeaderName = "X-NFSN-Authentication" + maxSalt big.Int +) + +func init() { + maxSalt.SetInt64(int64(len(saltChars))) +} + +func Salt() (string, error) { + salt := make([]string, 16) + for i := 0; i < 16; i++ { + rand_index, err := rand.Int(rand.Reader, &maxSalt) + if err != nil { + return "", err + } + + salt[i] = string(saltChars[rand_index.Int64()]) + } + + return strings.Join(salt, ""), nil +} + +func Timestamp() string { + return strconv.FormatInt(time.Now().Unix(), 10) +} + +func ComputeHash(data string) string { + result := sha1.Sum([]byte(data)) + return hex.EncodeToString(result[:]) +} + +func GetAuthHeader(login string, apiKey string, url string, body string) (string, error) { + timestamp := Timestamp() + salt, err := Salt() + if err != nil { + return "", err + } + bodyHash := ComputeHash(body) + hash := ComputeHash(fmt.Sprintf("%s;%s;%s;%s;%s;%s", login, timestamp, salt, apiKey, url, bodyHash)) + return hash, nil +} From b67b987b07dae6604249ad6889b9541366ee982b Mon Sep 17 00:00:00 2001 From: Tevildo Date: Sun, 14 Apr 2024 21:08:00 +0100 Subject: [PATCH 02/14] add dns package with func to set TXT record --- dns/dns.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 dns/dns.go diff --git a/dns/dns.go b/dns/dns.go new file mode 100644 index 0000000..136b83a --- /dev/null +++ b/dns/dns.go @@ -0,0 +1,44 @@ +package dns + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/cert-manager/webhook-example/auth" +) + +var ( + baseUrl = "https://api.nearlyfreespeech.net" + authHeaderName = "X-NFSN-Authentication" +) + +func SetTXTRecord(domain string, resolvedFqdn string, key string, login string, apiKey string) error { + resolvedFqdn = strings.TrimSuffix(resolvedFqdn, domain) + requestUrl := fmt.Sprintf("%s/dns/%s", baseUrl, domain) + + values := url.Values{"name": {resolvedFqdn}, "type": {"TXT"}, "data": {key}} + body := values.Encode() + authHeader, err := auth.GetAuthHeader(login, apiKey, requestUrl, body) + if err != nil { + return err + } + + bodyReader := strings.NewReader(body) + req, err := http.NewRequest("POST", requestUrl, bodyReader) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set(authHeaderName, authHeader) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} From 8e6eff385555f0499652616a2e9152f76fdfed35 Mon Sep 17 00:00:00 2001 From: Tevildo Date: Sun, 14 Apr 2024 21:08:34 +0100 Subject: [PATCH 03/14] configure main provider with api key secret and k8s client --- main.go | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/main.go b/main.go index 969e6d2..2e5359a 100644 --- a/main.go +++ b/main.go @@ -6,10 +6,12 @@ import ( "os" extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" - "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" + acme_v1alpha1 "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" "github.com/cert-manager/cert-manager/pkg/acme/webhook/cmd" + meta_v1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" ) var GroupName = os.Getenv("GROUP_NAME") @@ -40,7 +42,7 @@ type customDNSProviderSolver struct { // 3. uncomment the relevant code in the Initialize method below // 4. ensure your webhook's service account has the required RBAC role // assigned to it for interacting with the Kubernetes APIs you need. - //client kubernetes.Clientset + client *kubernetes.Clientset } // customDNSProviderConfig is a structure that is used to decode into when @@ -64,7 +66,8 @@ type customDNSProviderConfig struct { // `issuer.spec.acme.dns01.providers.webhook.config` field. //Email string `json:"email"` - //APIKeySecretRef v1alpha1.SecretKeySelector `json:"apiKeySecretRef"` + Login string `json:"login"` + APIKeySecretRef meta_v1.SecretKeySelector `json:"apiKeySecretRef"` } // Name is used as the name for this DNS solver when referencing it on the ACME @@ -74,7 +77,7 @@ type customDNSProviderConfig struct { // within a single webhook deployment**. // For example, `cloudflare` may be used as the name of a solver. func (c *customDNSProviderSolver) Name() string { - return "my-custom-solver" + return "NearlyFreeSpeech" } // Present is responsible for actually presenting the DNS record with the @@ -82,7 +85,7 @@ func (c *customDNSProviderSolver) Name() string { // This method should tolerate being called multiple times with the same value. // cert-manager itself will later perform a self check to ensure that the // solver has correctly configured the DNS provider. -func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { +func (c *customDNSProviderSolver) Present(ch *acme_v1alpha1.ChallengeRequest) error { cfg, err := loadConfig(ch.Config) if err != nil { return err @@ -92,6 +95,7 @@ func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { fmt.Printf("Decoded configuration %v", cfg) // TODO: add code that sets a record in the DNS provider's console + return nil } @@ -101,7 +105,7 @@ func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { // value provided on the ChallengeRequest should be cleaned up. // This is in order to facilitate multiple DNS validations for the same domain // concurrently. -func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { +func (c *customDNSProviderSolver) CleanUp(ch *acme_v1alpha1.ChallengeRequest) error { // TODO: add code that deletes a record from the DNS provider's console return nil } @@ -119,12 +123,12 @@ func (c *customDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stop ///// UNCOMMENT THE BELOW CODE TO MAKE A KUBERNETES CLIENTSET AVAILABLE TO ///// YOUR CUSTOM DNS PROVIDER - //cl, err := kubernetes.NewForConfig(kubeClientConfig) - //if err != nil { - // return err - //} - // - //c.client = cl + cl, err := kubernetes.NewForConfig(kubeClientConfig) + if err != nil { + return err + } + + c.client = cl ///// END OF CODE TO MAKE KUBERNETES CLIENTSET AVAILABLE return nil From 9c8aeffea3bab0027db2ddbc58407488e28777da Mon Sep 17 00:00:00 2001 From: Tevildo Date: Tue, 16 Apr 2024 21:12:08 +0100 Subject: [PATCH 04/14] include loging, timestamp, and salt in auth header --- auth/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/auth.go b/auth/auth.go index b864192..3018f3c 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -53,5 +53,5 @@ func GetAuthHeader(login string, apiKey string, url string, body string) (string } bodyHash := ComputeHash(body) hash := ComputeHash(fmt.Sprintf("%s;%s;%s;%s;%s;%s", login, timestamp, salt, apiKey, url, bodyHash)) - return hash, nil + return fmt.Sprintf("%s;%s;%s;%s", login, timestamp, salt, hash), nil } From 23ae00ce0df0ffe3e6ac4f6202785661abfdd2b6 Mon Sep 17 00:00:00 2001 From: Tevildo Date: Tue, 16 Apr 2024 21:13:24 +0100 Subject: [PATCH 05/14] hash url path (including leading /) instead of full url --- dns/dns.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dns/dns.go b/dns/dns.go index 136b83a..6507489 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -15,12 +15,12 @@ var ( ) func SetTXTRecord(domain string, resolvedFqdn string, key string, login string, apiKey string) error { - resolvedFqdn = strings.TrimSuffix(resolvedFqdn, domain) - requestUrl := fmt.Sprintf("%s/dns/%s", baseUrl, domain) + urlPath := fmt.Sprintf("/dns/%s/addRR", domain) + requestUrl := fmt.Sprintf("%s%s", baseUrl, urlPath) values := url.Values{"name": {resolvedFqdn}, "type": {"TXT"}, "data": {key}} body := values.Encode() - authHeader, err := auth.GetAuthHeader(login, apiKey, requestUrl, body) + authHeader, err := auth.GetAuthHeader(login, apiKey, urlPath, body) if err != nil { return err } From 1a821c664364f32e1fa832f688615a521bcd4d3b Mon Sep 17 00:00:00 2001 From: Tevildo Date: Tue, 16 Apr 2024 21:14:01 +0100 Subject: [PATCH 06/14] rename param to SetTXTRecord --- dns/dns.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dns/dns.go b/dns/dns.go index 6507489..6f9a1af 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -14,11 +14,11 @@ var ( authHeaderName = "X-NFSN-Authentication" ) -func SetTXTRecord(domain string, resolvedFqdn string, key string, login string, apiKey string) error { +func SetTXTRecord(domain string, dnsName string, key string, login string, apiKey string) error { urlPath := fmt.Sprintf("/dns/%s/addRR", domain) requestUrl := fmt.Sprintf("%s%s", baseUrl, urlPath) - values := url.Values{"name": {resolvedFqdn}, "type": {"TXT"}, "data": {key}} + values := url.Values{"name": {dnsName}, "type": {"TXT"}, "data": {key}} body := values.Encode() authHeader, err := auth.GetAuthHeader(login, apiKey, urlPath, body) if err != nil { From 9b0a8f8440cdd744c561bdbf5f2145514e2ec569 Mon Sep 17 00:00:00 2001 From: Tevildo Date: Tue, 16 Apr 2024 21:14:27 +0100 Subject: [PATCH 07/14] rename mod --- dns/dns.go | 2 +- go.mod | 2 +- main_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dns/dns.go b/dns/dns.go index 6f9a1af..65da002 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -6,7 +6,7 @@ import ( "net/url" "strings" - "github.com/cert-manager/webhook-example/auth" + "github.com/MartinWilkerson/cert-manager-webhook-nearlyfreespeech/auth" ) var ( diff --git a/go.mod b/go.mod index c62945a..76ff6fb 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/cert-manager/webhook-example +module github.com/MartinWilkerson/cert-manager-webhook-nearlyfreespeech go 1.20 diff --git a/main_test.go b/main_test.go index 1d7d5ff..99c42b3 100644 --- a/main_test.go +++ b/main_test.go @@ -6,7 +6,7 @@ import ( acmetest "github.com/cert-manager/cert-manager/test/acme" - "github.com/cert-manager/webhook-example/example" + "github.com/MartinWilkerson/cert-manager-webhook-nearlyfreespeech/example" ) var ( From 7bc82c179960b5852951b7e800ba8a969aad4e26 Mon Sep 17 00:00:00 2001 From: Tevildo Date: Tue, 16 Apr 2024 21:14:49 +0100 Subject: [PATCH 08/14] read api key from secret and use to set txt record --- main.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 2e5359a..377ecee 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,18 @@ package main import ( + "context" "encoding/json" "fmt" "os" + "strings" extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "github.com/MartinWilkerson/cert-manager-webhook-nearlyfreespeech/dns" acme_v1alpha1 "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" "github.com/cert-manager/cert-manager/pkg/acme/webhook/cmd" meta_v1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" @@ -95,8 +99,16 @@ func (c *customDNSProviderSolver) Present(ch *acme_v1alpha1.ChallengeRequest) er fmt.Printf("Decoded configuration %v", cfg) // TODO: add code that sets a record in the DNS provider's console + apiKeySecret, err := c.client.CoreV1().Secrets("").Get(context.TODO(), cfg.APIKeySecretRef.Name, v1.GetOptions{}) + if err != nil { + return err + } + apiKeyData := apiKeySecret.Data[cfg.APIKeySecretRef.Key] + apiKey := string(apiKeyData) - return nil + dnsName := strings.TrimSuffix(ch.ResolvedFQDN, "."+ch.ResolvedZone) + + return dns.SetTXTRecord(ch.ResolvedZone, dnsName, ch.Key, cfg.Login, apiKey) } // CleanUp should delete the relevant TXT record from the DNS provider console. From 4a6c8920370187048448db374a79daa63a623876 Mon Sep 17 00:00:00 2001 From: Tevildo Date: Tue, 16 Apr 2024 21:30:44 +0100 Subject: [PATCH 09/14] add function to remove a TXT record --- dns/dns.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dns/dns.go b/dns/dns.go index 65da002..2dd1672 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -20,6 +20,19 @@ func SetTXTRecord(domain string, dnsName string, key string, login string, apiKe values := url.Values{"name": {dnsName}, "type": {"TXT"}, "data": {key}} body := values.Encode() + return send(login, apiKey, urlPath, body, requestUrl) +} + +func ClearTXTRecord(domain string, dnsName string, key string, login string, apiKey string) error { + urlPath := fmt.Sprintf("/dns/%s/removeRR", domain) + requestUrl := fmt.Sprintf("%s%s", baseUrl, urlPath) + + values := url.Values{"name": {dnsName}, "type": {"TXT"}, "data": {key}} + body := values.Encode() + return send(login, apiKey, urlPath, body, requestUrl) +} + +func send(login string, apiKey string, urlPath string, body string, requestUrl string) error { authHeader, err := auth.GetAuthHeader(login, apiKey, urlPath, body) if err != nil { return err From b911dcf170dfeb27d91ce422675045a5ce8156e2 Mon Sep 17 00:00:00 2001 From: Tevildo Date: Tue, 16 Apr 2024 21:30:59 +0100 Subject: [PATCH 10/14] implement CleanUp method --- main.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 377ecee..87da8e5 100644 --- a/main.go +++ b/main.go @@ -118,8 +118,21 @@ func (c *customDNSProviderSolver) Present(ch *acme_v1alpha1.ChallengeRequest) er // This is in order to facilitate multiple DNS validations for the same domain // concurrently. func (c *customDNSProviderSolver) CleanUp(ch *acme_v1alpha1.ChallengeRequest) error { - // TODO: add code that deletes a record from the DNS provider's console - return nil + cfg, err := loadConfig(ch.Config) + if err != nil { + return err + } + + apiKeySecret, err := c.client.CoreV1().Secrets("").Get(context.TODO(), cfg.APIKeySecretRef.Name, v1.GetOptions{}) + if err != nil { + return err + } + apiKeyData := apiKeySecret.Data[cfg.APIKeySecretRef.Key] + apiKey := string(apiKeyData) + + dnsName := strings.TrimSuffix(ch.ResolvedFQDN, "."+ch.ResolvedZone) + + return dns.ClearTXTRecord(ch.ResolvedZone, dnsName, ch.Key, cfg.Login, apiKey) } // Initialize will be called when the webhook first starts. From 604c8b02d07a683f1a7c8fc005c0a334f31b8446 Mon Sep 17 00:00:00 2001 From: Tevildo Date: Tue, 16 Apr 2024 22:18:02 +0100 Subject: [PATCH 11/14] make solver name lowercase --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 87da8e5..db2b733 100644 --- a/main.go +++ b/main.go @@ -81,7 +81,7 @@ type customDNSProviderConfig struct { // within a single webhook deployment**. // For example, `cloudflare` may be used as the name of a solver. func (c *customDNSProviderSolver) Name() string { - return "NearlyFreeSpeech" + return "nearlyfreespeech" } // Present is responsible for actually presenting the DNS record with the From 78539b35e24344becb6130a778bdaeeab94b8b3d Mon Sep 17 00:00:00 2001 From: Tevildo Date: Thu, 18 Apr 2024 23:32:04 +0100 Subject: [PATCH 12/14] report non-200 status codes --- dns/dns.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dns/dns.go b/dns/dns.go index 2dd1672..237321c 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -2,6 +2,8 @@ package dns import ( "fmt" + "io" + "log" "net/http" "net/url" "strings" @@ -17,6 +19,7 @@ var ( func SetTXTRecord(domain string, dnsName string, key string, login string, apiKey string) error { urlPath := fmt.Sprintf("/dns/%s/addRR", domain) requestUrl := fmt.Sprintf("%s%s", baseUrl, urlPath) + log.Printf("Request URL: %v", requestUrl) values := url.Values{"name": {dnsName}, "type": {"TXT"}, "data": {key}} body := values.Encode() @@ -53,5 +56,13 @@ func send(login string, apiKey string, urlPath string, body string, requestUrl s } defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + return fmt.Errorf("HTTP error code %v: %v", resp.StatusCode, string(bytes)) + } + return nil } From 9d36b956256420fa10211b44e36858ef726c2af1 Mon Sep 17 00:00:00 2001 From: Tevildo Date: Thu, 18 Apr 2024 23:32:59 +0100 Subject: [PATCH 13/14] Trim trailing . from resolved zone --- main.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index db2b733..eb2c3a8 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "log" "os" "strings" @@ -96,10 +97,11 @@ func (c *customDNSProviderSolver) Present(ch *acme_v1alpha1.ChallengeRequest) er } // TODO: do something more useful with the decoded configuration - fmt.Printf("Decoded configuration %v", cfg) + log.Printf("Decoded configuration %v", cfg) + log.Printf("ResolvedZone: '%v'. ResolvedFQDN: '%v'", ch.ResolvedZone, ch.ResolvedFQDN) // TODO: add code that sets a record in the DNS provider's console - apiKeySecret, err := c.client.CoreV1().Secrets("").Get(context.TODO(), cfg.APIKeySecretRef.Name, v1.GetOptions{}) + apiKeySecret, err := c.client.CoreV1().Secrets(ch.ResourceNamespace).Get(context.TODO(), cfg.APIKeySecretRef.Name, v1.GetOptions{}) if err != nil { return err } @@ -107,8 +109,9 @@ func (c *customDNSProviderSolver) Present(ch *acme_v1alpha1.ChallengeRequest) er apiKey := string(apiKeyData) dnsName := strings.TrimSuffix(ch.ResolvedFQDN, "."+ch.ResolvedZone) + domain := strings.TrimSuffix(ch.ResolvedZone, ".") - return dns.SetTXTRecord(ch.ResolvedZone, dnsName, ch.Key, cfg.Login, apiKey) + return dns.SetTXTRecord(domain, dnsName, ch.Key, cfg.Login, apiKey) } // CleanUp should delete the relevant TXT record from the DNS provider console. @@ -123,7 +126,7 @@ func (c *customDNSProviderSolver) CleanUp(ch *acme_v1alpha1.ChallengeRequest) er return err } - apiKeySecret, err := c.client.CoreV1().Secrets("").Get(context.TODO(), cfg.APIKeySecretRef.Name, v1.GetOptions{}) + apiKeySecret, err := c.client.CoreV1().Secrets(ch.ResourceNamespace).Get(context.TODO(), cfg.APIKeySecretRef.Name, v1.GetOptions{}) if err != nil { return err } From eb0bf91863649dc374868561801a764e03765d72 Mon Sep 17 00:00:00 2001 From: Tevildo Date: Fri, 26 Apr 2024 10:40:43 +0100 Subject: [PATCH 14/14] set default version to 0.0.7 --- deploy/example-webhook/values.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/example-webhook/values.yaml b/deploy/example-webhook/values.yaml index 31eb151..fe72b0a 100644 --- a/deploy/example-webhook/values.yaml +++ b/deploy/example-webhook/values.yaml @@ -13,8 +13,8 @@ certManager: serviceAccountName: cert-manager image: - repository: mycompany/webhook-image - tag: latest + repository: tevildo/cert-manager-webhook-nearlyfreespeech + tag: 0.0.7 pullPolicy: IfNotPresent nameOverride: ""