Merge pull request #1 from MartinWilkerson/nearlyfreespeech

Nearlyfreespeech
This commit is contained in:
MartinWilkerson 2026-01-24 20:37:51 +00:00 committed by GitHub
commit 6531333f20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 177 additions and 20 deletions

57
auth/auth.go Normal file
View file

@ -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 fmt.Sprintf("%s;%s;%s;%s", login, timestamp, salt, hash), nil
}

View file

@ -13,8 +13,8 @@ certManager:
serviceAccountName: cert-manager serviceAccountName: cert-manager
image: image:
repository: mycompany/webhook-image repository: tevildo/cert-manager-webhook-nearlyfreespeech
tag: latest tag: 0.0.7
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
nameOverride: "" nameOverride: ""

68
dns/dns.go Normal file
View file

@ -0,0 +1,68 @@
package dns
import (
"fmt"
"io"
"log"
"net/http"
"net/url"
"strings"
"github.com/MartinWilkerson/cert-manager-webhook-nearlyfreespeech/auth"
)
var (
baseUrl = "https://api.nearlyfreespeech.net"
authHeaderName = "X-NFSN-Authentication"
)
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()
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
}
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()
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
}

2
go.mod
View file

@ -1,4 +1,4 @@
module github.com/cert-manager/webhook-example module github.com/MartinWilkerson/cert-manager-webhook-nearlyfreespeech
go 1.20 go 1.20

64
main.go
View file

@ -1,15 +1,22 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"os" "os"
"strings"
extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 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" "k8s.io/client-go/rest"
"github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" "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" "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") var GroupName = os.Getenv("GROUP_NAME")
@ -40,7 +47,7 @@ type customDNSProviderSolver struct {
// 3. uncomment the relevant code in the Initialize method below // 3. uncomment the relevant code in the Initialize method below
// 4. ensure your webhook's service account has the required RBAC role // 4. ensure your webhook's service account has the required RBAC role
// assigned to it for interacting with the Kubernetes APIs you need. // 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 // customDNSProviderConfig is a structure that is used to decode into when
@ -64,7 +71,8 @@ type customDNSProviderConfig struct {
// `issuer.spec.acme.dns01.providers.webhook.config` field. // `issuer.spec.acme.dns01.providers.webhook.config` field.
//Email string `json:"email"` //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 // Name is used as the name for this DNS solver when referencing it on the ACME
@ -74,7 +82,7 @@ type customDNSProviderConfig struct {
// within a single webhook deployment**. // within a single webhook deployment**.
// For example, `cloudflare` may be used as the name of a solver. // For example, `cloudflare` may be used as the name of a solver.
func (c *customDNSProviderSolver) Name() string { func (c *customDNSProviderSolver) Name() string {
return "my-custom-solver" return "nearlyfreespeech"
} }
// Present is responsible for actually presenting the DNS record with the // Present is responsible for actually presenting the DNS record with the
@ -82,17 +90,28 @@ func (c *customDNSProviderSolver) Name() string {
// This method should tolerate being called multiple times with the same value. // 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 // cert-manager itself will later perform a self check to ensure that the
// solver has correctly configured the DNS provider. // 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) cfg, err := loadConfig(ch.Config)
if err != nil { if err != nil {
return err return err
} }
// TODO: do something more useful with the decoded configuration // 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 // TODO: add code that sets a record in the DNS provider's console
return nil apiKeySecret, err := c.client.CoreV1().Secrets(ch.ResourceNamespace).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)
domain := strings.TrimSuffix(ch.ResolvedZone, ".")
return dns.SetTXTRecord(domain, dnsName, ch.Key, cfg.Login, apiKey)
} }
// CleanUp should delete the relevant TXT record from the DNS provider console. // CleanUp should delete the relevant TXT record from the DNS provider console.
@ -101,9 +120,22 @@ func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error {
// value provided on the ChallengeRequest should be cleaned up. // value provided on the ChallengeRequest should be cleaned up.
// This is in order to facilitate multiple DNS validations for the same domain // This is in order to facilitate multiple DNS validations for the same domain
// concurrently. // 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 cfg, err := loadConfig(ch.Config)
return nil if err != nil {
return err
}
apiKeySecret, err := c.client.CoreV1().Secrets(ch.ResourceNamespace).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. // Initialize will be called when the webhook first starts.
@ -119,12 +151,12 @@ func (c *customDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stop
///// UNCOMMENT THE BELOW CODE TO MAKE A KUBERNETES CLIENTSET AVAILABLE TO ///// UNCOMMENT THE BELOW CODE TO MAKE A KUBERNETES CLIENTSET AVAILABLE TO
///// YOUR CUSTOM DNS PROVIDER ///// YOUR CUSTOM DNS PROVIDER
//cl, err := kubernetes.NewForConfig(kubeClientConfig) cl, err := kubernetes.NewForConfig(kubeClientConfig)
//if err != nil { if err != nil {
// return err return err
//} }
//
//c.client = cl c.client = cl
///// END OF CODE TO MAKE KUBERNETES CLIENTSET AVAILABLE ///// END OF CODE TO MAKE KUBERNETES CLIENTSET AVAILABLE
return nil return nil

View file

@ -6,7 +6,7 @@ import (
acmetest "github.com/cert-manager/cert-manager/test/acme" 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 ( var (