Added Azure DNS support for DNS01 challange
This commit is contained in:
parent
6722b74551
commit
cc89fe59aa
@ -15,6 +15,7 @@ spec:
|
||||
- cm-dns-clouddns.k8s.group
|
||||
- cm-dns-cloudflare.k8s.group
|
||||
- cm-dns-route53.k8s.group
|
||||
- cm-dns-azuredns.k8s.group
|
||||
acme:
|
||||
config:
|
||||
- http01:
|
||||
@ -38,3 +39,8 @@ spec:
|
||||
provider: route53
|
||||
domains:
|
||||
- cm-dns-route53.k8s.group
|
||||
- dns-01:
|
||||
provider: azuredns
|
||||
domains:
|
||||
- cm-dns-azuredns.k8s.group
|
||||
|
||||
|
||||
@ -46,3 +46,21 @@ spec:
|
||||
# This field is optional for overriding the Route53 hosted zone ID
|
||||
# It is required to use it if the cert-manager cannot disambiguate between two different hosted zones for the same zone name
|
||||
hostedZoneID: DIKER8JPL21PSA
|
||||
- name: azuredns
|
||||
azuredns:
|
||||
# Service principal clientId (also called appId)
|
||||
clientID: 8ff041f4-a14f-4753-80c2-101b35db5879
|
||||
# A secretKeyRef to a service principal ClientSecret (password)
|
||||
# ref: https://docs.microsoft.com/en-us/azure/container-service/kubernetes/container-service-kubernetes-service-principal
|
||||
clientSecretSecretRef:
|
||||
name: azuredns-config
|
||||
key: client-secret
|
||||
# Azure subscription Id
|
||||
subscriptionID: 0933cdcc-0cd0-4fb3-9f26-dac4fdc2154b
|
||||
# Azure AD tenant Id
|
||||
tenantID: 9581f7ad-8f4f-4f07-92df-12c821981ce8
|
||||
# ResourceGroup name where dns zone is provisioned
|
||||
resourceGroupName: resource-group
|
||||
# Name of the hosted zone, if ommited it will be computed from domain provided during certificate creation
|
||||
# hosted zone name is always part of domain name from certificate request
|
||||
hostedZoneName: k8s.group
|
||||
@ -114,6 +114,7 @@ type ACMEIssuerDNS01Provider struct {
|
||||
CloudDNS *ACMEIssuerDNS01ProviderCloudDNS `json:"clouddns,omitempty"`
|
||||
Cloudflare *ACMEIssuerDNS01ProviderCloudflare `json:"cloudflare,omitempty"`
|
||||
Route53 *ACMEIssuerDNS01ProviderRoute53 `json:"route53,omitempty"`
|
||||
AzureDNS *ACMEIssuerDNS01ProviderAzureDNS `json:"azuredns,omitempty"`
|
||||
}
|
||||
|
||||
// ACMEIssuerDNS01ProviderCloudDNS is a structure containing the DNS
|
||||
@ -139,6 +140,19 @@ type ACMEIssuerDNS01ProviderRoute53 struct {
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
// ACMEIssuerDNS01ProviderAzureDNS is a structure containing the
|
||||
// configuration for Azure DNS
|
||||
type ACMEIssuerDNS01ProviderAzureDNS struct {
|
||||
ClientID string `json:"clientID"`
|
||||
ClientSecret SecretKeySelector `json:"clientSecretSecretRef"`
|
||||
SubscriptionID string `json:"subscriptionID"`
|
||||
TenantID string `json:"tenantID"`
|
||||
ResourceGroupName string `json:"resourceGroupName"`
|
||||
|
||||
// + optional
|
||||
HostedZoneName string `json:"hostedZoneName"`
|
||||
}
|
||||
|
||||
// IssuerStatus contains status information about an Issuer
|
||||
type IssuerStatus struct {
|
||||
Conditions []IssuerCondition `json:"conditions"`
|
||||
|
||||
@ -231,6 +231,15 @@ func (in *ACMEIssuerDNS01Provider) DeepCopyInto(out *ACMEIssuerDNS01Provider) {
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
if in.AzureDNS != nil {
|
||||
in, out := &in.AzureDNS, &out.AzureDNS
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(ACMEIssuerDNS01ProviderAzureDNS)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -244,6 +253,23 @@ func (in *ACMEIssuerDNS01Provider) DeepCopy() *ACMEIssuerDNS01Provider {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ACMEIssuerDNS01ProviderAzureDNS) DeepCopyInto(out *ACMEIssuerDNS01ProviderAzureDNS) {
|
||||
*out = *in
|
||||
out.ClientSecret = in.ClientSecret
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ACMEIssuerDNS01ProviderAzureDNS.
|
||||
func (in *ACMEIssuerDNS01ProviderAzureDNS) DeepCopy() *ACMEIssuerDNS01ProviderAzureDNS {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ACMEIssuerDNS01ProviderAzureDNS)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ACMEIssuerDNS01ProviderCloudDNS) DeepCopyInto(out *ACMEIssuerDNS01ProviderCloudDNS) {
|
||||
*out = *in
|
||||
|
||||
159
pkg/issuer/acme/dns/azuredns/azuredns.go
Normal file
159
pkg/issuer/acme/dns/azuredns/azuredns.go
Normal file
@ -0,0 +1,159 @@
|
||||
// Package azuredns implements a DNS provider for solving the DNS-01 challenge
|
||||
// using Azure DNS.
|
||||
package azuredns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/arm/dns"
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/Azure/go-autorest/autorest/to"
|
||||
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util"
|
||||
)
|
||||
|
||||
// DNSProvider implements the util.ChallengeProvider interface
|
||||
type DNSProvider struct {
|
||||
recordClient dns.RecordSetsClient
|
||||
zoneClient dns.ZonesClient
|
||||
resourceGroupName string
|
||||
zoneName string
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for the Azure
|
||||
// DNS service.
|
||||
// Credentials are automatically detected from environment variables
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
|
||||
clientID := os.Getenv("AZURE_CLIENT_ID")
|
||||
clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
|
||||
subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID")
|
||||
tenantID := os.Getenv("AZURE_TENANT_ID")
|
||||
resourceGroupName := ("AZURE_RESOURCE_GROUP")
|
||||
zoneName := ("AZURE_ZONE_NAME")
|
||||
|
||||
return NewDNSProviderCredentials(clientID, clientSecret, subscriptionID, tenantID, resourceGroupName, zoneName)
|
||||
}
|
||||
|
||||
// NewDNSProviderCredentials returns a DNSProvider instance configured for the Azure
|
||||
// DNS service using static credentials from its parameters
|
||||
func NewDNSProviderCredentials(clientID, clientSecret, subscriptionID, tenantID, resourceGroupName, zoneName string) (*DNSProvider, error) {
|
||||
oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spt, err := adal.NewServicePrincipalToken(*oauthConfig, clientID, clientSecret, azure.PublicCloud.ResourceManagerEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rc := dns.NewRecordSetsClient(subscriptionID)
|
||||
rc.Authorizer = autorest.NewBearerAuthorizer(spt)
|
||||
|
||||
zc := dns.NewZonesClient(subscriptionID)
|
||||
zc.Authorizer = autorest.NewBearerAuthorizer(spt)
|
||||
|
||||
return &DNSProvider{
|
||||
recordClient: rc,
|
||||
zoneClient: zc,
|
||||
resourceGroupName: resourceGroupName,
|
||||
zoneName: zoneName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record using the specified parameters
|
||||
func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value, ttl := util.DNS01Record(domain, keyAuth)
|
||||
|
||||
return c.createRecord(fqdn, value, ttl)
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters
|
||||
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, _, _ := util.DNS01Record(domain, keyAuth)
|
||||
|
||||
z, err := c.getHostedZoneName(fqdn)
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting hosted zone name for: %s, %v", fqdn, err)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.recordClient.Delete(
|
||||
c.resourceGroupName,
|
||||
z,
|
||||
c.trimFqdn(fqdn),
|
||||
dns.TXT, "")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Timeout returns the timeout and interval to use when checking for DNS
|
||||
// propagation. Adjusting here to cope with spikes in propagation times.
|
||||
func (c *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return 120 * time.Second, 2 * time.Second
|
||||
}
|
||||
|
||||
func (c *DNSProvider) createRecord(fqdn, value string, ttl int) error {
|
||||
rparams := &dns.RecordSet{
|
||||
RecordSetProperties: &dns.RecordSetProperties{
|
||||
TTL: to.Int64Ptr(int64(ttl)),
|
||||
TxtRecords: &[]dns.TxtRecord{
|
||||
{Value: &[]string{value}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
z, err := c.getHostedZoneName(fqdn)
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting hosted zone name for: %s, %v", fqdn, err)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.recordClient.CreateOrUpdate(
|
||||
c.resourceGroupName,
|
||||
z,
|
||||
c.trimFqdn(fqdn),
|
||||
dns.TXT,
|
||||
*rparams, "", "")
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating TXT: %s, %v", c.zoneName, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DNSProvider) getHostedZoneName(fqdn string) (string, error) {
|
||||
if c.zoneName != "" {
|
||||
return c.zoneName, nil
|
||||
}
|
||||
z, err := util.FindZoneByFqdn(fqdn, util.RecursiveNameservers)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(z) == 0 {
|
||||
return "", fmt.Errorf("Zone %s not found for domain %s", z, fqdn)
|
||||
}
|
||||
|
||||
_, err = c.zoneClient.Get(c.resourceGroupName, util.UnFqdn(z))
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Zone %s not found in AzureDNS for domain %s. Err: %v", z, fqdn, err)
|
||||
}
|
||||
|
||||
return util.UnFqdn(z), nil
|
||||
}
|
||||
|
||||
func (c *DNSProvider) trimFqdn(fqdn string) string {
|
||||
return strings.TrimSuffix(strings.TrimSuffix(fqdn, "."), "."+c.zoneName)
|
||||
}
|
||||
58
pkg/issuer/acme/dns/azuredns/azuredns_test.go
Normal file
58
pkg/issuer/acme/dns/azuredns/azuredns_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
package azuredns
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
azureLiveTest bool
|
||||
azureClientID string
|
||||
azureClientSecret string
|
||||
azuresubscriptionID string
|
||||
azureTenantID string
|
||||
azureResourceGroupName string
|
||||
azureHostedZoneName string
|
||||
azureDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
azureClientID = os.Getenv("AZURE_CLIENT_ID")
|
||||
azureClientSecret = os.Getenv("AZURE_CLIENT_SECRET")
|
||||
azuresubscriptionID = os.Getenv("AZURE_SUBSCRIPTION_ID")
|
||||
azureTenantID = os.Getenv("AZURE_TENANT_ID")
|
||||
azureResourceGroupName = os.Getenv("AZURE_RESOURCE_GROUP")
|
||||
azureHostedZoneName = os.Getenv("AZURE_ZONE_NAME")
|
||||
azureDomain = os.Getenv("AZURE_DOMAIN")
|
||||
if len(azureClientID) > 0 && len(azureClientSecret) > 0 && len(azureDomain) > 0 {
|
||||
azureLiveTest = true
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveAzureDnsPresent(t *testing.T) {
|
||||
if !azureLiveTest {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
provider, err := NewDNSProviderCredentials(azureClientID, azureClientSecret, azuresubscriptionID, azureTenantID, azureResourceGroupName, azureHostedZoneName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = provider.Present(azureDomain, "", "123d==")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveAzureDnsCleanUp(t *testing.T) {
|
||||
if !azureLiveTest {
|
||||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
|
||||
provider, err := NewDNSProviderCredentials(azureClientID, azureClientSecret, azuresubscriptionID, azureTenantID, azureResourceGroupName, azureHostedZoneName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = provider.CleanUp(azureDomain, "", "123d==")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@ -10,6 +10,7 @@ import (
|
||||
corev1listers "k8s.io/client-go/listers/core/v1"
|
||||
|
||||
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
|
||||
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/azuredns"
|
||||
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/clouddns"
|
||||
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/cloudflare"
|
||||
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/route53"
|
||||
@ -155,6 +156,25 @@ func (s *Solver) solverFor(crt *v1alpha1.Certificate, domain string) (solver, er
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error instantiating route53 challenge solver: %s", err.Error())
|
||||
}
|
||||
case providerConfig.AzureDNS != nil:
|
||||
clientSecret, err := s.secretLister.Secrets(s.resourceNamespace).Get(providerConfig.AzureDNS.ClientSecret.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting azuredns client secret: %s", err.Error())
|
||||
}
|
||||
|
||||
clientSecretBytes, ok := clientSecret.Data[providerConfig.AzureDNS.ClientSecret.Key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error getting azure dns client secret: key '%s' not found in secret", providerConfig.AzureDNS.ClientSecret.Key)
|
||||
}
|
||||
|
||||
impl, err = azuredns.NewDNSProviderCredentials(
|
||||
providerConfig.AzureDNS.ClientID,
|
||||
string(clientSecretBytes),
|
||||
providerConfig.AzureDNS.SubscriptionID,
|
||||
providerConfig.AzureDNS.TenantID,
|
||||
providerConfig.AzureDNS.ResourceGroupName,
|
||||
providerConfig.AzureDNS.HostedZoneName,
|
||||
)
|
||||
default:
|
||||
return nil, fmt.Errorf("no dns provider config specified for domain '%s'", domain)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user