Skip to content

Commit

Permalink
Use Client Cert Auth for ARO HCP deployments
Browse files Browse the repository at this point in the history
Use Client Certificate Authentication for ARO HCP deployments.
HyperShift will pass the needed environment variables for this
authentication method: ARO_HCP_MI_CLIENT_ID, ARO_HCP_TENANT_ID, and
ARO_HCP_CLIENT_CERTIFICATE_PATH.

Signed-off-by: Bryan Cox <[email protected]>
  • Loading branch information
bryan-cox committed Nov 5, 2024
1 parent 042d588 commit 5be01e8
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 6 deletions.
33 changes: 27 additions & 6 deletions pkg/cloudprovider/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/jongio/azidext/go/azidext"
v1 "github.com/openshift/api/cloudnetwork/v1"
configv1 "github.com/openshift/api/config/v1"
"github.com/openshift/cloud-network-config-controller/pkg/filewatcher"
corev1 "k8s.io/api/core/v1"
"k8s.io/klog/v2"
utilnet "k8s.io/utils/net"
Expand Down Expand Up @@ -586,17 +587,37 @@ func (a *Azure) getAuthorizer(env azureapi.Environment, cfg *azureCredentialsCon
err error
)

// MSI Override for ARO HCP
msi := os.Getenv("AZURE_MSI_AUTHENTICATION")
if msi == "true" {
options := azidentity.ManagedIdentityCredentialOptions{
// Managed Identity Override for ARO HCP
managedIdentityClientID := os.Getenv("ARO_HCP_MI_CLIENT_ID")
if managedIdentityClientID != "" {
klog.Info("Using client certification Azure authentication for ARO HCP")
options := &azidentity.ClientCertificateCredentialOptions{
ClientOptions: azcore.ClientOptions{
Cloud: cloudConfig,
},
SendCertificateChain: true,
}

var err error
cred, err = azidentity.NewManagedIdentityCredential(&options)
tenantID := os.Getenv("ARO_HCP_TENANT_ID")
certPath := os.Getenv("ARO_HCP_CLIENT_CERTIFICATE_PATH")

certData, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf(`failed to read certificate file "%s": %v`, certPath, err)
}

certs, key, err := azidentity.ParseCertificates(certData, []byte{})
if err != nil {
return nil, fmt.Errorf(`failed to parse certificate data "%s": %v`, certPath, err)
}

// Watch the certificate for changes; if the certificate changes, the pod will be restarted
err = filewatcher.WatchFileForChanges(certPath, 30*time.Minute)
if err != nil {
return nil, err
}

cred, err = azidentity.NewClientCertificateCredential(tenantID, managedIdentityClientID, certs, key, options)
if err != nil {
return nil, err
}
Expand Down
88 changes: 88 additions & 0 deletions pkg/filewatcher/filewatcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package filewatcher

import (
"fmt"
"hash/fnv"
"os"
"sync"
"time"

"k8s.io/klog/v2"
)

var (
initialFileHash string
watchCertificateFileOnce sync.Once
)

// WatchFileForChanges watches the file, fileToWatch, for changes based on interval passed into the function. This is
// accomplished through starting only one file watcher goroutine and recording a hash of the file contents prior to
// starting the goroutine. The goroutine will compare a hash of the file contents against the initial hash of the file
// contents at the interval specified. If the file contents have changed, the pod this function is running on will be
// restarted.
func WatchFileForChanges(fileToWatch string, interval time.Duration) error {
var fileContents []byte
var err error

// This starts only one occurrence of the file watcher, which watches the file, fileToWatch, for changes every interval.
// In addition, it also captures an initial hash of the file contents to use to monitor the file for changes.
watchCertificateFileOnce.Do(func() {
klog.Infof("Starting the file change watcher on file, %s", fileToWatch)
fileContents, err = os.ReadFile(fileToWatch)
if err != nil {
err = fmt.Errorf("failed to read file on initialization: %v", err)
return
}

// Record a hash of the initial file contents to compare against later
initialFileHash = hashSimple(fileContents)

// Start the file watcher to monitor file changes
go checkForFileChanges(fileToWatch, interval)
})
return err
}

// checkForFileChanges starts a new ticker based on the interval to watch a file for changes. When the ticker timer is
// up, a hash of the file contents being watch will be made and checked against an initial hash of the file contents.
// If the file changed, the pod running this function will exit.
func checkForFileChanges(fileToWatch string, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()

done := make(chan bool)

go func() {
for {
select {
case <-done:
return
case <-ticker.C:
klog.Infof("Checking file for changes, %s", fileToWatch)
fileContents, err := os.ReadFile(fileToWatch)
if err != nil {
klog.Errorf("failed to read the file: %v", err)
done <- true
return
}
fileContentsHash := hashSimple(fileContents)

if initialFileHash != fileContentsHash {
klog.Infof("The file contents changed... exiting pod, %s", fileToWatch)
done <- true
os.Exit(0)
}
}
}
}()

<-done
}

// hashSimple takes a value, typically a string, and returns a 32-bit FNV-1a hashed version of the value as a string
func hashSimple(o interface{}) string {
hash := fnv.New32a()
_, _ = hash.Write([]byte(fmt.Sprintf("%v", o)))
intHash := hash.Sum32()
return fmt.Sprintf("%08x", intHash)
}

0 comments on commit 5be01e8

Please sign in to comment.