From 51cf0d931c7c44218c50ca3cd55fbdcbd6727073 Mon Sep 17 00:00:00 2001 From: "Eric R. Rath" Date: Fri, 31 Jul 2020 13:17:28 -0700 Subject: [PATCH 1/4] OCI provider: add support for OCI IAM instance principal authentication Oracle Cloud Infrastructure (OCI) supports "instance princpal" authentication. From : > After you set up the required resources and policies, an application running > on an instance can call Oracle Cloud Infrastructure public services, removing > the need to configure user credentials or a configuration file. This change adds support to the OCI provider for instance principal authentication when external-dns is run on an OCI instance (e.g. in OCI OKE). Existing support for key/fingerprint-based authentication is unchanged. --- main.go | 15 ++++++++++++- pkg/apis/externaldns/types.go | 4 ++++ provider/oci/oci.go | 41 +++++++++++++++++++++++------------ provider/oci/oci_test.go | 15 ++++++++++++- 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/main.go b/main.go index 60bf72977..142c31a73 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ package main import ( "context" + "fmt" "net/http" "os" "os/signal" @@ -271,7 +272,19 @@ func main() { ) case "oci": var config *oci.OCIConfig - config, err = oci.LoadOCIConfig(cfg.OCIConfigFile) + // if the instance-principals flag was set, and a compartment OCID was provided, then ignore the + // OCI config file, and provide a config that uses instance principal authentication. + if cfg.OCIAuthInstancePrincipal { + if len(cfg.OCICompartmentOCID) == 0 { + err = fmt.Errorf("OCI IAM instance principal authentication requested, but no compartment OCID provided!") + } else { + authConfig := oci.OCIAuthConfig{UseInstancePrincipal: true} + config = &oci.OCIConfig{Auth: authConfig, CompartmentID: cfg.OCICompartmentOCID} + } + } else { + config, err = oci.LoadOCIConfig(cfg.OCIConfigFile) + } + if err == nil { p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun) } diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index be557ef1e..1787ec8b6 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -97,6 +97,8 @@ type Config struct { DynPassword string `secure:"yes"` DynMinTTLSeconds int OCIConfigFile string + OCICompartmentOCID string + OCIAuthInstancePrincipal bool InMemoryZones []string OVHEndpoint string OVHApiRateLimit int @@ -358,6 +360,8 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("dyn-password", "When using the Dyn provider, specify the pasword").Default("").StringVar(&cfg.DynPassword) app.Flag("dyn-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.DynMinTTLSeconds) app.Flag("oci-config-file", "When using the OCI provider, specify the OCI configuration file (required when --provider=oci").Default(defaultConfig.OCIConfigFile).StringVar(&cfg.OCIConfigFile) + app.Flag("oci-compartment-ocid", "When using the OCI provider, specify the OCID of the OCI compartment containing all managed zones and records. Required when using OCI IAM instance principal authentication.").StringVar(&cfg.OCICompartmentOCID) + app.Flag("oci-auth-instance-principal", "When using the OCI provider, specify whether OCI IAM instance principal authentication should be used (instead of key-based auth via the OCI config file).").Default(strconv.FormatBool(defaultConfig.OCIAuthInstancePrincipal)).BoolVar(&cfg.OCIAuthInstancePrincipal) app.Flag("rcodezero-txt-encrypt", "When using the Rcodezero provider with txt registry option, set if TXT rrs are encrypted (default: false)").Default(strconv.FormatBool(defaultConfig.RcodezeroTXTEncrypt)).BoolVar(&cfg.RcodezeroTXTEncrypt) app.Flag("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.InMemoryZones) app.Flag("ovh-endpoint", "When using the OVH provider, specify the endpoint (default: ovh-eu)").Default(defaultConfig.OVHEndpoint).StringVar(&cfg.OVHEndpoint) diff --git a/provider/oci/oci.go b/provider/oci/oci.go index 759caebf1..627e29467 100644 --- a/provider/oci/oci.go +++ b/provider/oci/oci.go @@ -22,6 +22,7 @@ import ( "strings" "github.com/oracle/oci-go-sdk/common" + "github.com/oracle/oci-go-sdk/common/auth" "github.com/oracle/oci-go-sdk/dns" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -36,12 +37,13 @@ const ociRecordTTL = 300 // OCIAuthConfig holds connection parameters for the OCI API. type OCIAuthConfig struct { - Region string `yaml:"region"` - TenancyID string `yaml:"tenancy"` - UserID string `yaml:"user"` - PrivateKey string `yaml:"key"` - Fingerprint string `yaml:"fingerprint"` - Passphrase string `yaml:"passphrase"` + Region string `yaml:"region"` + TenancyID string `yaml:"tenancy"` + UserID string `yaml:"user"` + PrivateKey string `yaml:"key"` + Fingerprint string `yaml:"fingerprint"` + Passphrase string `yaml:"passphrase"` + UseInstancePrincipal bool `yaml:"useInstancePrincipal"` } // OCIConfig holds the configuration for the OCI Provider. @@ -87,14 +89,25 @@ func LoadOCIConfig(path string) (*OCIConfig, error) { // NewOCIProvider initializes a new OCI DNS based Provider. func NewOCIProvider(cfg OCIConfig, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) (*OCIProvider, error) { var client ociDNSClient - client, err := dns.NewDnsClientWithConfigurationProvider(common.NewRawConfigurationProvider( - cfg.Auth.TenancyID, - cfg.Auth.UserID, - cfg.Auth.Region, - cfg.Auth.Fingerprint, - cfg.Auth.PrivateKey, - &cfg.Auth.Passphrase, - )) + var err error + var configProvider common.ConfigurationProvider + if cfg.Auth.UseInstancePrincipal { + configProvider, err = auth.InstancePrincipalConfigurationProvider() + if err != nil { + return nil, errors.Wrap(err, "error creating OCI instance principal config provider") + } + } else { + configProvider = common.NewRawConfigurationProvider( + cfg.Auth.TenancyID, + cfg.Auth.UserID, + cfg.Auth.Region, + cfg.Auth.Fingerprint, + cfg.Auth.PrivateKey, + &cfg.Auth.Passphrase, + ) + } + + client, err = dns.NewDnsClientWithConfigurationProvider(configProvider) if err != nil { return nil, errors.Wrap(err, "initializing OCI DNS API client") } diff --git a/provider/oci/oci_test.go b/provider/oci/oci_test.go index 5f890a4b5..437745b62 100644 --- a/provider/oci/oci_test.go +++ b/provider/oci/oci_test.go @@ -19,6 +19,7 @@ package oci import ( "context" "sort" + "strings" "testing" "github.com/oracle/oci-go-sdk/common" @@ -166,6 +167,17 @@ hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K }, }, }, + "instance-principal": { + // testing the InstancePrincipalConfigurationProvider is tricky outside of an OCI context, because it tries + // to request a token from the internal OCI systems; this test-case just confirms that the expected error is + // observed, confirming that the instance-principal provider was instantiated. + config: OCIConfig{ + Auth: OCIAuthConfig{ + UseInstancePrincipal: true, + }, + }, + err: errors.New("error creating OCI instance principal config provider: failed to create a new key provider for instance principal"), + }, "invalid": { config: OCIConfig{ Auth: OCIAuthConfig{ @@ -191,7 +203,8 @@ hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K if err == nil { require.NoError(t, err) } else { - require.Equal(t, tc.err.Error(), err.Error()) + // have to use prefix testing because the expected instance-principal error strings vary after a known prefix + require.Truef(t, strings.HasPrefix(err.Error(), tc.err.Error()), "observed: %s", err.Error()) } }) } From 9d90d082bdb3bb42e696d787c1e475a53eae112c Mon Sep 17 00:00:00 2001 From: "Eric R. Rath" Date: Fri, 31 Jul 2020 13:56:31 -0700 Subject: [PATCH 2/4] OCI provider: updated tutorial with info about instance principal auth --- docs/tutorials/oracle.md | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/oracle.md b/docs/tutorials/oracle.md index 98918816c..920e11395 100644 --- a/docs/tutorials/oracle.md +++ b/docs/tutorials/oracle.md @@ -6,16 +6,25 @@ Make sure to use the latest version of ExternalDNS for this tutorial. ## Creating an OCI DNS Zone -Create a DNS zone which will contain the managed DNS records. Let's use `example.com` as an reference here. +Create a DNS zone which will contain the managed DNS records. Let's use +`example.com` as a reference here. Make note of the OCID of the compartment +in which you created the zone; you'll need to provide that later. For more information about OCI DNS see the documentation [here][1]. ## Deploy ExternalDNS Connect your `kubectl` client to the cluster you want to test ExternalDNS with. +The OCI provider supports two authentication options: key-based and instance +principals. + +### Key-based + We first need to create a config file containing the information needed to connect with the OCI API. -Create a new file (oci.yaml) and modify the contents to match the example below. Be sure to adjust the values to match your own credentials: +Create a new file (oci.yaml) and modify the contents to match the example +below. Be sure to adjust the values to match your own credentials, and the OCID +of the compartment containing the zone: ```yaml auth: @@ -35,7 +44,29 @@ Create a secret using the config file above: $ kubectl create secret generic external-dns-config --from-file=oci.yaml ``` -### Manifest (for clusters with RBAC enabled) +### OCI IAM Instance Principal + +If you're running ExternalDNS within OCI, you can use OCI IAM instance +principals to authenticate with OCI. This obviates the need to create the +secret with your credentials. You'll need to ensure an OCI IAM policy exists +with a statement granting the `manage dns` permission on zones and records in +the target compartment to the dynamic group covering your instance running +ExternalDNS. +E.g.: + +``` +Allow dynamic-group to manage dns in compartment id +``` + +You'll also need to add the `--oci-instance-principals=true` flag to enable +this type of authentication. Finally, you'll need to add the +`--oci-compartment-ocid=ocid1.compartment.oc1...` flag to provide the OCID of +the compartment containing the zone to be managed. + +For more information about OCI IAM instance principals, see the documentation [here][2]. +For more information about OCI IAM policy details for the DNS service, see the documentation [here][3]. + +## Manifest (for clusters with RBAC enabled) Apply the following manifest to deploy ExternalDNS. @@ -157,3 +188,6 @@ $ kubectl apply -f nginx.yaml ``` [1]: https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm +[2]: https://docs.cloud.oracle.com/iaas/Content/Identity/Reference/dnspolicyreference.htm +[3]: https://docs.cloud.oracle.com/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm + From c5ddb1e1a98de5641898516cdddb488f3274137f Mon Sep 17 00:00:00 2001 From: "Eric R. Rath" Date: Fri, 31 Jul 2020 14:02:50 -0700 Subject: [PATCH 3/4] added entry to changelog under 'unreleased' --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0893ab6d..5caddf947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Fix: add serviceaccount name in kustomize deployment (#1689) @jmthvt - Updates Oracle OCI SDK to latest (#1687) @ericrrath - UltraDNS Provider (#1635) @kbhandari +- Oracle OCI provider: add support for instance principal authentication (#1700) @ericrrath ## v0.7.2 - 2020-06-03 From 07cfb7fdfbc7c256fd9b7058aea95bbda9d8f8b7 Mon Sep 17 00:00:00 2001 From: "Eric R. Rath" Date: Fri, 31 Jul 2020 14:18:37 -0700 Subject: [PATCH 4/4] fixed linting error - no punc in errors --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 142c31a73..185fbc3b5 100644 --- a/main.go +++ b/main.go @@ -276,7 +276,7 @@ func main() { // OCI config file, and provide a config that uses instance principal authentication. if cfg.OCIAuthInstancePrincipal { if len(cfg.OCICompartmentOCID) == 0 { - err = fmt.Errorf("OCI IAM instance principal authentication requested, but no compartment OCID provided!") + err = fmt.Errorf("instance principal authentication requested, but no compartment OCID provided") } else { authConfig := oci.OCIAuthConfig{UseInstancePrincipal: true} config = &oci.OCIConfig{Auth: authConfig, CompartmentID: cfg.OCICompartmentOCID}