azuredns: pipeline credential support (#2621)

This commit is contained in:
Ludovic Fernandez 2025-08-19 17:44:47 +02:00 committed by GitHub
commit 8a11af149f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 172 additions and 100 deletions

View file

@ -229,6 +229,10 @@ This authentication method can be specifically used by setting the `AZURE_AUTH_M
Open ID Connect is a mechanism that establish a trust relationship between a running environment and the Azure AD identity provider.
It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `oidc`.
### Azure DevOps Pipelines
It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `pipeline`.

View file

@ -3,19 +3,13 @@
package azuredns
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
)
@ -33,10 +27,21 @@ const (
EnvClientID = envNamespace + "CLIENT_ID"
EnvClientSecret = envNamespace + "CLIENT_SECRET"
EnvOIDCToken = envNamespace + "OIDC_TOKEN"
EnvOIDCTokenFilePath = envNamespace + "OIDC_TOKEN_FILE_PATH"
EnvOIDCRequestURL = envNamespace + "OIDC_REQUEST_URL"
EnvOIDCRequestToken = envNamespace + "OIDC_REQUEST_TOKEN"
EnvOIDCToken = envNamespace + "OIDC_TOKEN"
EnvOIDCTokenFilePath = envNamespace + "OIDC_TOKEN_FILE_PATH"
EnvOIDCRequestURL = envNamespace + "OIDC_REQUEST_URL"
EnvGitHubOIDCRequestURL = "ACTIONS_ID_TOKEN_REQUEST_URL"
altEnvArmOIDCRequestURL = "ARM_OIDC_REQUEST_URL"
EnvOIDCRequestToken = envNamespace + "OIDC_REQUEST_TOKEN"
EnvGitHubOIDCRequestToken = "ACTIONS_ID_TOKEN_REQUEST_TOKEN"
altEnvArmOIDCRequestToken = "ARM_OIDC_REQUEST_TOKEN"
EnvServiceConnectionID = envNamespace + "SERVICE_CONNECTION_ID"
altEnvServiceConnectionID = "SERVICE_CONNECTION_ID"
altEnvArmAdoPipelineServiceConnectionID = "ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID"
altEnvArmOIDCAzureServiceConnectionID = "ARM_OIDC_AZURE_SERVICE_CONNECTION_ID"
EnvSystemAccessToken = envNamespace + "SYSTEM_ACCESS_TOKEN"
altEnvSystemAccessToken = "SYSTEM_ACCESSTOKEN"
EnvAuthMethod = envNamespace + "AUTH_METHOD"
EnvAuthMSITimeout = envNamespace + "AUTH_MSI_TIMEOUT"
@ -46,9 +51,6 @@ const (
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvGitHubOIDCRequestURL = "ACTIONS_ID_TOKEN_REQUEST_URL"
EnvGitHubOIDCRequestToken = "ACTIONS_ID_TOKEN_REQUEST_TOKEN"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
@ -73,6 +75,9 @@ type Config struct {
OIDCRequestURL string
OIDCRequestToken string
ServiceConnectionID string
SystemAccessToken string
AuthMethod string
AuthMSITimeout time.Duration
@ -134,13 +139,22 @@ func NewDNSProvider() (*DNSProvider, error) {
config.ServiceDiscoveryFilter = env.GetOrFile(EnvServiceDiscoveryFilter)
oidcValues, _ := env.GetWithFallback(
[]string{EnvOIDCRequestURL, EnvGitHubOIDCRequestURL},
[]string{EnvOIDCRequestToken, EnvGitHubOIDCRequestToken},
[]string{EnvOIDCRequestURL, EnvGitHubOIDCRequestURL, altEnvArmOIDCRequestURL},
[]string{EnvOIDCRequestToken, EnvGitHubOIDCRequestToken, altEnvArmOIDCRequestToken},
)
config.OIDCRequestURL = oidcValues[EnvOIDCRequestURL]
config.OIDCRequestToken = oidcValues[EnvOIDCRequestToken]
// https://registry.terraform.io/providers/hashicorp/Azurerm/latest/docs/guides/service_principal_oidc
pipelineValues, _ := env.GetWithFallback(
[]string{EnvServiceConnectionID, altEnvServiceConnectionID, altEnvArmAdoPipelineServiceConnectionID, altEnvArmOIDCAzureServiceConnectionID},
[]string{EnvSystemAccessToken, altEnvArmOIDCRequestToken, altEnvSystemAccessToken},
)
config.ServiceConnectionID = pipelineValues[EnvServiceConnectionID]
config.SystemAccessToken = pipelineValues[EnvSystemAccessToken]
config.AuthMethod = env.GetOrFile(EnvAuthMethod)
config.AuthMSITimeout = env.GetOrDefaultSecond(EnvAuthMSITimeout, 2*time.Second)
@ -193,88 +207,3 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return d.provider.CleanUp(domain, token, keyAuth)
}
func getCredentials(config *Config) (azcore.TokenCredential, error) {
clientOptions := azcore.ClientOptions{Cloud: config.Environment}
switch strings.ToLower(config.AuthMethod) {
case "env":
if config.ClientID != "" && config.ClientSecret != "" && config.TenantID != "" {
return azidentity.NewClientSecretCredential(config.TenantID, config.ClientID, config.ClientSecret,
&azidentity.ClientSecretCredentialOptions{ClientOptions: clientOptions})
}
return azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{ClientOptions: clientOptions})
case "wli":
return azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{ClientOptions: clientOptions})
case "msi":
cred, err := azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ClientOptions: clientOptions})
if err != nil {
return nil, err
}
return &timeoutTokenCredential{cred: cred, timeout: config.AuthMSITimeout}, nil
case "cli":
var credOptions *azidentity.AzureCLICredentialOptions
if config.TenantID != "" {
credOptions = &azidentity.AzureCLICredentialOptions{TenantID: config.TenantID}
}
return azidentity.NewAzureCLICredential(credOptions)
case "oidc":
err := checkOIDCConfig(config)
if err != nil {
return nil, err
}
return azidentity.NewClientAssertionCredential(config.TenantID, config.ClientID, getOIDCAssertion(config), &azidentity.ClientAssertionCredentialOptions{ClientOptions: clientOptions})
default:
return azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ClientOptions: clientOptions})
}
}
// timeoutTokenCredential wraps a TokenCredential to add a timeout.
type timeoutTokenCredential struct {
cred azcore.TokenCredential
timeout time.Duration
}
// GetToken implements the azcore.TokenCredential interface.
func (w *timeoutTokenCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if w.timeout <= 0 {
return w.cred.GetToken(ctx, opts)
}
ctxTimeout, cancel := context.WithTimeout(ctx, w.timeout)
defer cancel()
tk, err := w.cred.GetToken(ctxTimeout, opts)
if ce := ctxTimeout.Err(); errors.Is(ce, context.DeadlineExceeded) {
return tk, azidentity.NewCredentialUnavailableError("managed identity timed out")
}
w.timeout = 0
return tk, err
}
func getZoneName(config *Config, fqdn string) (string, error) {
if config.ZoneName != "" {
return config.ZoneName, nil
}
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return "", fmt.Errorf("could not find zone for %s: %w", fqdn, err)
}
if authZone == "" {
return "", errors.New("empty zone name")
}
return authZone, nil
}

View file

@ -174,6 +174,10 @@ This authentication method can be specifically used by setting the `AZURE_AUTH_M
Open ID Connect is a mechanism that establish a trust relationship between a running environment and the Azure AD identity provider.
It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `oidc`.
### Azure DevOps Pipelines
It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `pipeline`.
'''
[Configuration]

View file

@ -0,0 +1,135 @@
package azuredns
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/go-acme/lego/v4/challenge/dns01"
)
const (
authMethodEnv = "env"
authMethodWLI = "wli"
authMethodMSI = "msi"
authMethodCLI = "cli"
authMethodOIDC = "oidc"
authMethodPipeline = "pipeline"
)
//nolint:gocyclo // The complexity is related to the number of possible configurations.
func getCredentials(config *Config) (azcore.TokenCredential, error) {
clientOptions := azcore.ClientOptions{Cloud: config.Environment}
switch strings.ToLower(config.AuthMethod) {
case authMethodEnv:
if config.ClientID != "" && config.ClientSecret != "" && config.TenantID != "" {
return azidentity.NewClientSecretCredential(config.TenantID, config.ClientID, config.ClientSecret,
&azidentity.ClientSecretCredentialOptions{ClientOptions: clientOptions})
}
return azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{ClientOptions: clientOptions})
case authMethodWLI:
return azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{ClientOptions: clientOptions})
case authMethodMSI:
cred, err := azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ClientOptions: clientOptions})
if err != nil {
return nil, err
}
return &timeoutTokenCredential{cred: cred, timeout: config.AuthMSITimeout}, nil
case authMethodCLI:
var credOptions *azidentity.AzureCLICredentialOptions
if config.TenantID != "" {
credOptions = &azidentity.AzureCLICredentialOptions{TenantID: config.TenantID}
}
return azidentity.NewAzureCLICredential(credOptions)
case authMethodOIDC:
err := checkOIDCConfig(config)
if err != nil {
return nil, err
}
return azidentity.NewClientAssertionCredential(config.TenantID, config.ClientID, getOIDCAssertion(config), &azidentity.ClientAssertionCredentialOptions{ClientOptions: clientOptions})
case authMethodPipeline:
err := checkPipelineConfig(config)
if err != nil {
return nil, err
}
// Uses the env var `SYSTEM_OIDCREQUESTURI`,
// but the constant is not exported,
// and there is no way to set it programmatically.
// https://github.com/Azure/azure-sdk-for-go/blob/aae2fb75ffccafc669db72bebc3c1a66332f48d7/sdk/azidentity/azure_pipelines_credential.go#L22
// https://github.com/Azure/azure-sdk-for-go/blob/aae2fb75ffccafc669db72bebc3c1a66332f48d7/sdk/azidentity/azure_pipelines_credential.go#L79
return azidentity.NewAzurePipelinesCredential(config.TenantID, config.ClientID, config.ServiceConnectionID, config.SystemAccessToken, &azidentity.AzurePipelinesCredentialOptions{ClientOptions: clientOptions})
default:
return azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ClientOptions: clientOptions})
}
}
// timeoutTokenCredential wraps a TokenCredential to add a timeout.
type timeoutTokenCredential struct {
cred azcore.TokenCredential
timeout time.Duration
}
// GetToken implements the azcore.TokenCredential interface.
func (w *timeoutTokenCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if w.timeout <= 0 {
return w.cred.GetToken(ctx, opts)
}
ctxTimeout, cancel := context.WithTimeout(ctx, w.timeout)
defer cancel()
tk, err := w.cred.GetToken(ctxTimeout, opts)
if ce := ctxTimeout.Err(); errors.Is(ce, context.DeadlineExceeded) {
return tk, azidentity.NewCredentialUnavailableError("managed identity timed out")
}
w.timeout = 0
return tk, err
}
func getZoneName(config *Config, fqdn string) (string, error) {
if config.ZoneName != "" {
return config.ZoneName, nil
}
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return "", fmt.Errorf("could not find zone for %s: %w", fqdn, err)
}
if authZone == "" {
return "", errors.New("empty zone name")
}
return authZone, nil
}
func checkPipelineConfig(config *Config) error {
if config.ServiceConnectionID == "" {
return errors.New("azuredns: ServiceConnectionID is missing")
}
if config.SystemAccessToken == "" {
return errors.New("azuredns: SystemAccessToken is missing")
}
return nil
}