Skip to content

Commit

Permalink
make azure storage account private
Browse files Browse the repository at this point in the history
  • Loading branch information
QuentinBisson committed Dec 2, 2024
1 parent e5a4d93 commit d59b29f
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 136 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Secure Azure Storage Account by making them private.
- Update Kyverno PolicyException to v2beta1.

## [0.9.0] - 2024-10-03
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ toolchain go1.23.3
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.1.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0
github.com/aquilax/truncate v1.0.0
github.com/aws/aws-sdk-go-v2 v1.32.5
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvUL
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.0.0 h1:Kb8eVvjdP6kZqYnER5w/PiGCFp91yVgaxve3d7kCEpY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.0.0/go.mod h1:lYq15QkJyEsNegz5EhI/0SXQ6spvGfgwBH/Qyzkoc/s=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.1.0 h1:Fd+iaEa+JBwzYo6OTWYSNqyvlPSLciMGsmsnYCKcXM0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.1.0/go.mod h1:ulHyBFJOI0ONiRL4vcJTmS7rx18jQQlEPmAgo80cRdM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
Expand Down
7 changes: 6 additions & 1 deletion internal/controller/bucket_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,12 @@ func (r BucketReconciler) reconcileNormal(ctx context.Context, objectStorageServ
return ctrl.Result{}, errors.WithStack(err)
}
} else {
logger.Info("Bucket exists and you already own it.")
logger.Info("Bucket exists and you already own it, let's update it")
err = objectStorageService.UpdateBucket(ctx, bucket)
if err != nil {
logger.Error(err, "Bucket could not be updated")
return ctrl.Result{}, errors.WithStack(err)
}
}

logger.Info("Configuring bucket settings")
Expand Down
5 changes: 5 additions & 0 deletions internal/pkg/service/objectstorage/cloud/aws/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ func (s S3ObjectStorageAdapter) CreateBucket(ctx context.Context, bucket *v1alph
return err
}

// UpdateBucket does nothing as we cannot update an s3 bucket
func (s S3ObjectStorageAdapter) UpdateBucket(ctx context.Context, bucket *v1alpha1.Bucket) error {
return nil
}

func (s S3ObjectStorageAdapter) DeleteBucket(ctx context.Context, bucket *v1alpha1.Bucket) error {
// First we need to empty the bucket
paginator := s3.NewListObjectsV2Paginator(s.s3Client, &s3.ListObjectsV2Input{
Expand Down
8 changes: 8 additions & 0 deletions internal/pkg/service/objectstorage/cloud/azure/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,11 @@ func (c AzureCluster) GetCredentials() cluster.Credentials {
func (c AzureCluster) GetResourceGroup() string {
return c.Credentials.ResourceGroup
}

func (c AzureCluster) GetSubscriptionID() string {
return c.Credentials.SubscriptionID
}

func (c AzureCluster) GetVNetName() string {
return c.GetName() + "-vnet"
}
17 changes: 16 additions & 1 deletion internal/pkg/service/objectstorage/cloud/azure/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage"
"github.com/go-logr/logr"
"github.com/pkg/errors"
Expand Down Expand Up @@ -57,9 +58,23 @@ func (s AzureObjectStorageService) NewObjectStorageService(ctx context.Context,
return nil, errors.WithStack(err)
}

var networkClientFactory *armnetwork.ClientFactory
networkClientFactory, err = armnetwork.NewClientFactory(azureCredentials.SubscriptionID, cred, nil)
if err != nil {
return nil, errors.WithStack(err)
}

azurecluster, ok := cluster.(AzureCluster)
if !ok {
return nil, errors.New("Impossible to cast cluster into Azure cluster")
}
return NewAzureStorageService(storageClientFactory.NewAccountsClient(), storageClientFactory.NewBlobContainersClient(), storageClientFactory.NewManagementPoliciesClient(), logger, azurecluster, client), nil
return NewAzureStorageService(
storageClientFactory.NewAccountsClient(),
storageClientFactory.NewBlobContainersClient(),
storageClientFactory.NewManagementPoliciesClient(),
networkClientFactory.NewPrivateEndpointsClient(),
logger,
azurecluster,
client,
), nil
}
202 changes: 125 additions & 77 deletions internal/pkg/service/objectstorage/cloud/azure/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage"
"github.com/aquilax/truncate"
"github.com/go-logr/logr"
Expand All @@ -30,10 +31,10 @@ type AzureObjectStorageAdapter struct {
storageAccountClient *armstorage.AccountsClient
blobContainerClient *armstorage.BlobContainersClient
managementPoliciesClient *armstorage.ManagementPoliciesClient
privateEndpointsClient *armnetwork.PrivateEndpointsClient
logger logr.Logger
cluster AzureCluster
client client.Client
listStorageAccountName []string
}

// NewAzureStorageService creates a new instance of AzureObjectStorageAdapter.
Expand All @@ -42,16 +43,22 @@ type AzureObjectStorageAdapter struct {
// The logger is used for logging purposes.
// The cluster represents the Azure cluster.
// The client is the Kubernetes client used for interacting with the Kubernetes API.
// The listStorageAccountName is a list of storage account names.
func NewAzureStorageService(storageAccountClient *armstorage.AccountsClient, blobContainerClient *armstorage.BlobContainersClient, managementPoliciesClient *armstorage.ManagementPoliciesClient, logger logr.Logger, cluster AzureCluster, client client.Client) AzureObjectStorageAdapter {
func NewAzureStorageService(
storageAccountClient *armstorage.AccountsClient,
blobContainerClient *armstorage.BlobContainersClient,
managementPoliciesClient *armstorage.ManagementPoliciesClient,
privateEndpointsClient *armnetwork.PrivateEndpointsClient,
logger logr.Logger,
cluster AzureCluster,
client client.Client) AzureObjectStorageAdapter {
return AzureObjectStorageAdapter{
storageAccountClient: storageAccountClient,
blobContainerClient: blobContainerClient,
managementPoliciesClient: managementPoliciesClient,
privateEndpointsClient: privateEndpointsClient,
logger: logger,
cluster: cluster,
client: client,
listStorageAccountName: []string{},
}
}

Expand Down Expand Up @@ -121,41 +128,80 @@ func (s AzureObjectStorageAdapter) CreateBucket(ctx context.Context, bucket *v1a
if err != nil {
return err
}
// If Storage Account does not exists, we need to create it first
if !existsStorageAccount {
// Create Storage Account
pollerResp, err := s.storageAccountClient.BeginCreate(
ctx,
s.cluster.GetResourceGroup(),
storageAccountName,
armstorage.AccountCreateParameters{
Kind: to.Ptr(armstorage.KindBlobStorage),
SKU: &armstorage.SKU{
Name: to.Ptr(armstorage.SKUNameStandardLRS),
},
Location: to.Ptr(s.cluster.GetRegion()),
Properties: &armstorage.AccountPropertiesCreateParameters{
AccessTier: to.Ptr(armstorage.AccessTierHot),
Encryption: &armstorage.Encryption{
Services: &armstorage.EncryptionServices{
Blob: &armstorage.EncryptionService{
KeyType: to.Ptr(armstorage.KeyTypeAccount),
Enabled: to.Ptr(true),
},

// Create or Update Storage Account
pollerStorageAccount, err := s.storageAccountClient.BeginCreate(
ctx,
s.cluster.GetResourceGroup(),
storageAccountName,
armstorage.AccountCreateParameters{
Kind: to.Ptr(armstorage.KindBlobStorage),
SKU: &armstorage.SKU{
Name: to.Ptr(armstorage.SKUNameStandardLRS),
},
Location: to.Ptr(s.cluster.GetRegion()),
Properties: &armstorage.AccountPropertiesCreateParameters{
AllowSharedKeyAccess: to.Ptr(true),
AccessTier: to.Ptr(armstorage.AccessTierHot),
Encryption: &armstorage.Encryption{
Services: &armstorage.EncryptionServices{
Blob: &armstorage.EncryptionService{
KeyType: to.Ptr(armstorage.KeyTypeAccount),
Enabled: to.Ptr(true),
},
KeySource: to.Ptr(armstorage.KeySourceMicrosoftStorage),
},
EnableHTTPSTrafficOnly: to.Ptr(true),
KeySource: to.Ptr(armstorage.KeySourceMicrosoftStorage),
},
}, nil)
if err != nil {
return err
}
_, err = pollerResp.PollUntilDone(ctx, nil)
if err != nil {
return err
}
EnableHTTPSTrafficOnly: to.Ptr(true),
// TODO make sure this is not the case on gaggle or public installations
PublicNetworkAccess: to.Ptr(armstorage.PublicNetworkAccessDisabled),
},
}, nil)
if err != nil {
return err
}
_, err = pollerStorageAccount.PollUntilDone(ctx, nil)
if err != nil {
return err
}

if !existsStorageAccount {
s.logger.Info(fmt.Sprintf("Storage Account %s created", storageAccountName))
} else {
s.logger.Info(fmt.Sprintf("Storage Account %s updated", storageAccountName))
}

// Create or Update Private endpoint
blobGroupID := "blob"
pollerPrivateEndpoint, err := s.privateEndpointsClient.BeginCreateOrUpdate(
ctx,
s.cluster.GetResourceGroup(),
bucket.Spec.Name,
armnetwork.PrivateEndpoint{
Location: to.Ptr(s.cluster.GetRegion()),
Properties: &armnetwork.PrivateEndpointProperties{
CustomNetworkInterfaceName: to.Ptr(fmt.Sprintf("%s-nodes-nic", bucket.Spec.Name)),
PrivateLinkServiceConnections: []*armnetwork.PrivateLinkServiceConnection{
{
Name: to.Ptr(bucket.Spec.Name),
Properties: &armnetwork.PrivateLinkServiceConnectionProperties{
PrivateLinkServiceID: to.Ptr(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Storage/storageAccounts/%s", s.cluster.GetSubscriptionID(), s.cluster.GetResourceGroup(), storageAccountName)),
GroupIDs: []*string{&blobGroupID},
},
},
},
Subnet: &armnetwork.Subnet{
ID: to.Ptr(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/%s/subnets/%s", s.cluster.GetSubscriptionID(), s.cluster.GetResourceGroup(), s.cluster.GetVNetName(), "node-subnet")),
},
},
Tags: s.getBucketTags(bucket),
}, nil)
if err != nil {
return err
}
_, err = pollerPrivateEndpoint.PollUntilDone(ctx, nil)
if err != nil {
return err
}

// Create Storage Container
Expand Down Expand Up @@ -187,43 +233,49 @@ func (s AzureObjectStorageAdapter) CreateBucket(ctx context.Context, bucket *v1a
if err != nil {
return fmt.Errorf("unable to retrieve access keys from storage account %s", storageAccountName)
}
// Then, we retrieve the Access Key for 'key1'
foundKey1 := false
for _, k := range listKeys.Keys {
if *k.KeyName == "key1" {
foundKey1 = true
// Finally, we create the Secret into the bucket namespace
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: bucket.Spec.Name,
Namespace: bucket.Namespace,
Labels: map[string]string{
"giantswarm.io/managed-by": "object-storage-operator",
},
Finalizers: []string{
v1alpha1.AzureSecretFinalizer,
},
},
Data: map[string][]byte{

secret := v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: bucket.Spec.Name,
Namespace: bucket.Namespace,
Labels: map[string]string{
"giantswarm.io/managed-by": "object-storage-operator",
},
Finalizers: []string{
v1alpha1.AzureSecretFinalizer,
},
},
}

_, err = controllerutil.CreateOrUpdate(ctx, s.client, &secret, func() error {
// Then, we retrieve the Access Key for 'key1'
for _, k := range listKeys.Keys {
if *k.KeyName == "key1" {
// Finally, we create the Secret into the bucket namespace
secret.Data = map[string][]byte{
"accountName": []byte(storageAccountName),
"accountKey": []byte(*k.Value),
},
}
err := s.client.Create(ctx, secret)
if err != nil {
return err
}
return nil
}
s.logger.Info(fmt.Sprintf("created secret %s", bucket.Spec.Name))
break
}
}
if !foundKey1 {

return fmt.Errorf("unable to retrieve access keys 'key1' from storage account %s", storageAccountName)
})

if err != nil {
return err
}

s.logger.Info(fmt.Sprintf("created secret %s", bucket.Spec.Name))
return nil
}

// UpdateBucket creates or updates the Storage Account if it not exists AND the Storage Container
func (s AzureObjectStorageAdapter) UpdateBucket(ctx context.Context, bucket *v1alpha1.Bucket) error {
return s.CreateBucket(ctx, bucket)
}

// DeleteBucket deletes the Storage Account (Storage Container will be deleted by cascade)
// Here, we decided to have a Storage Account dedicated to a Storage Container (relation 1 - 1)
// We want to prevent the Storage Account from being used by anyone
Expand Down Expand Up @@ -354,10 +406,8 @@ func sanitizeTagKey(tagName string) string {
return strings.ReplaceAll(tagName, "-", "_")
}

// setTags set cluster additionalTags and bucket tags into Storage Container Metadata
func (s AzureObjectStorageAdapter) setTags(ctx context.Context, bucket *v1alpha1.Bucket) error {
storageAccountName := s.getStorageAccountName(bucket.Spec.Name)
tags := map[string]*string{}
func (s AzureObjectStorageAdapter) getBucketTags(bucket *v1alpha1.Bucket) map[string]*string {
tags := make(map[string]*string)
for _, t := range bucket.Spec.Tags {
// We use this to avoid pointer issues in range loops.
tag := t
Expand All @@ -373,6 +423,12 @@ func (s AzureObjectStorageAdapter) setTags(ctx context.Context, bucket *v1alpha1
tags[sanitizeTagKey(key)] = &value
}
}
return tags
}

// setTags set cluster additionalTags and bucket tags into Storage Container Metadata
func (s AzureObjectStorageAdapter) setTags(ctx context.Context, bucket *v1alpha1.Bucket) error {
storageAccountName := s.getStorageAccountName(bucket.Spec.Name)

// Updating Storage Container Metadata with tags (cluster additionalTags + Bucket tags)
_, err := s.blobContainerClient.Update(
Expand All @@ -382,7 +438,7 @@ func (s AzureObjectStorageAdapter) setTags(ctx context.Context, bucket *v1alpha1
bucket.Spec.Name,
armstorage.BlobContainer{
ContainerProperties: &armstorage.ContainerProperties{
Metadata: tags,
Metadata: s.getBucketTags(bucket),
},
},
nil,
Expand All @@ -394,17 +450,9 @@ func (s AzureObjectStorageAdapter) setTags(ctx context.Context, bucket *v1alpha1
}

// getStorageAccountName returns the storage account name for the given bucket name.
// It sanitizes the bucket name and checks if it already exists in the list of storage account names.
// If it exists, it returns the sanitized name. Otherwise, it adds the sanitized name to the list and returns it.
// It sanitizes the bucket name and returns it.
func (s *AzureObjectStorageAdapter) getStorageAccountName(bucketName string) string {
sanitizeName := sanitizeAlphanumeric24(bucketName)
for _, name := range s.listStorageAccountName {
if sanitizeName == name {
return sanitizeName
}
}
s.listStorageAccountName = append(s.listStorageAccountName, sanitizeName)
return sanitizeName
return sanitizeAlphanumeric24(bucketName)
}

// sanitizeAlphanumeric24 sanitizes the given name by removing any non-alphanumeric characters and truncating it to a maximum length of 24 characters.
Expand Down
Loading

0 comments on commit d59b29f

Please sign in to comment.