mirror of
https://github.com/cert-manager/webhook-example.git
synced 2026-03-16 18:02:51 +01:00
Merge pull request #1 from MartinWilkerson/nearlyfreespeech
Nearlyfreespeech
This commit is contained in:
commit
6531333f20
6 changed files with 177 additions and 20 deletions
57
auth/auth.go
Normal file
57
auth/auth.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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
68
dns/dns.go
Normal 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
2
go.mod
|
|
@ -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
64
main.go
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue