Added Azure DNS support for DNS01 challange

This commit is contained in:
mwieczorek 2017-12-29 12:15:50 +01:00
parent 6722b74551
commit cc89fe59aa
7 changed files with 301 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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"`

View File

@ -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

View 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)
}

View 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)
}

View File

@ -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)
}