Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rotate SQL User credentials #856

Merged
merged 14 commits into from
Apr 7, 2020
Merged
3 changes: 3 additions & 0 deletions api/v1alpha1/azuresqlaction_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ type AzureSqlActionSpec struct {
ServerName string `json:"serverName"`
ServerAdminSecretName string `json:"serverAdminSecretName,omitempty"`
ServerSecretKeyVault string `json:"serverSecretKeyVault,omitempty"`
UserSecretKeyVault string `json:"userSecretKeyVault,omitempty"`
DbUser string `json:"dbUser,omitempty"`
DbName string `json:"dbName,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha1/azuresqlaction_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ var _ = Describe("AzureSqlAction", func() {
Spec: AzureSqlActionSpec{
ResourceGroup: "foo-action",
ServerName: "sqlsrvsample",
ActionName: "rollcreds",
ActionName: "rolladmincreds",
}}

By("creating an API obj")
Expand Down
19 changes: 13 additions & 6 deletions config/samples/azure_v1alpha1_azuresqlaction.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ metadata:
name: azuresqlaction-sample
spec:
resourceGroup: resourcegroup-azure-operators
actionName: rolladmincreds
serverName: sqlserver-sample-777

# actionName: rolladmincreds
## Optionally specify the secretname and keyvault for the admin credentials secret
# serverAdminSecretName: sqlserver-sample-777-secret
# serverSecretKeyVault: asoSecretKeyVault

# Optionally specify the secretname and keyvault where the admin credentials secret
# of the SQL server is stored. If not specified we fallback to the global secret
# client and the default name for the secret
#serverAdminSecretName: sqlserver-sample-777-secret
#serverSecretKeyVault: asoSecretKeyVault
# actionName: rollusercreds
# dbName: azuresqldatabase-sample
# dbUser: sqluser-sample
## Optionally specify the keyvault for the user credentials secret
# userSecretKeyVault: asoSecretKeyVault
## Optionally specify the secretname and keyvault for the admin credentials secret
# serverAdminSecretName: sqlserver-sample-777-secret
# serverSecretKeyVault: asoSecretKeyVault
42 changes: 41 additions & 1 deletion controllers/azuresql_combined_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func TestAzureSqlServerCombinedHappyPath(t *testing.T) {
var kvSqlUser1 *azurev1alpha1.AzureSQLUser
var kvSqlUser2 *azurev1alpha1.AzureSQLUser

// run sub tests that require 2 servers or have to be run after rollcreds test ------------------
// run sub tests that require 2 servers or have to be run after rolladmincreds test ------------------
t.Run("group2", func(t *testing.T) {

t.Run("set up user in first db", func(t *testing.T) {
Expand Down Expand Up @@ -276,6 +276,46 @@ func TestAzureSqlServerCombinedHappyPath(t *testing.T) {
})
})

t.Run("deploy sql action and roll user credentials", func(t *testing.T) {
keyNamespace := "azuresqluser-" + sqlServerName + "-" + sqlDatabaseName
key := types.NamespacedName{Name: kvSqlUser1.ObjectMeta.Name, Namespace: keyNamespace}

keyVaultName := tc.keyvaultName
keyVaultSecretClient := kvsecrets.New(keyVaultName)
var oldSecret, _ = keyVaultSecretClient.Get(ctx, key)

sqlActionName := GenerateTestResourceNameWithRandom("azuresqlaction-dev", 10)
sqlActionInstance := &azurev1alpha1.AzureSqlAction{
ObjectMeta: metav1.ObjectMeta{
Name: sqlActionName,
Namespace: "default",
},
Spec: azurev1alpha1.AzureSqlActionSpec{
ResourceGroup: rgName,
ServerName: sqlServerName,
ActionName: "rollusercreds",
DbName: sqlDatabaseName,
DbUser: kvSqlUser1.ObjectMeta.Name,
UserSecretKeyVault: keyVaultName,
},
}

err := tc.k8sClient.Create(ctx, sqlActionInstance)
assert.Equal(nil, err, "create sqlaction in k8s")

sqlActionInstanceNamespacedName := types.NamespacedName{Name: sqlActionName, Namespace: "default"}

assert.Eventually(func() bool {
_ = tc.k8sClient.Get(ctx, sqlActionInstanceNamespacedName, sqlActionInstance)
return sqlActionInstance.Status.Provisioned
}, tc.timeout, tc.retry, "wait for sql action to be submitted")

var newSecret, _ = keyVaultSecretClient.Get(ctx, key)

assert.NotEqual(oldSecret["password"], newSecret["password"], "password should have been updated")
assert.Equal(oldSecret["username"], newSecret["username"], "usernames should be the same")
})

var sqlFailoverGroupInstance *azurev1alpha1.AzureSqlFailoverGroup
sqlFailoverGroupName := GenerateTestResourceNameWithRandom("sqlfog-dev", 10)

Expand Down
13 changes: 11 additions & 2 deletions controllers/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"runtime/debug"
Expand Down Expand Up @@ -731,8 +732,16 @@ func setup() error {

log.Println("Creating KV:", keyvaultName)
_, err = resourcemanagerkeyvaults.AzureKeyVaultManager.CreateVaultWithAccessPolicies(context.Background(), resourceGroupName, keyvaultName, resourcegroupLocation, resourcemanagerconfig.ClientID())
if err != nil {
return err
// Key Vault needs to be in "Suceeded" state
finish = time.Now().Add(tc.timeout)
for {
if finish.Before(time.Now()) {
return fmt.Errorf("time out waiting for keyvault")
}
result, _ := tc.keyVaultManager.GetVault(context.Background(), resourceGroupName, keyvaultName)
if result.Response.StatusCode == http.StatusOK {
break
}
}

log.Println(fmt.Sprintf("finished common controller test setup"))
Expand Down
29 changes: 25 additions & 4 deletions docs/azuresql/azuresql.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,34 @@ The `server` indicates the SQL server on which you want to configure the new SQL

### SQL Action

The SQL Action operator is used to trigger an action on the SQL server. Right now, the only action supported is `rollcreds` which rolls the password for the SQL server to a new one.
The SQL Action operator is used to trigger an action on the SQL server. Right now, the supported actions are `rolladmincreds`, which rolls the password for the SQL server to a new one, and `rollusercreds`, which rolls the password for the SQL Database User.

Here is a [sample YAML](/config/samples/azure_v1alpha1_azuresqlaction.yaml) for rolling the admin password of the SQL server
The `name` is a name for the action that we want to trigger. The type of action is determined by the value of `actionname` in the spec which is `rolladmincreds` or `rollusercreds` if you want to roll the password (Note: This action name should be exact for the password to be rolled). The `resourcegroup` and `servername` identify the SQL server on which the action should be triggered on.

The `name` is a name for the action that we want to trigger. The type of action is determined by the value of `actionname` in the spec which is `rollcreds` if you want to roll the password (Note: This action name should be exactly `rollcreds` for the password to be rolled). The `resourcegroup` and `servername` identify the SQL server on which the action should be triggered on.
Here is a [sample YAML](/config/samples/azure_v1alpha1_azuresqlaction.yaml) for rolling the admin or user password of the SQL server

Once you apply this, the kube secret with the same name as the SQL server is updated with the rolled password.
Once you apply this, the kube or Key Vault secret with the same name as the SQL server is updated with the rolled password.

#### rolladmincreds

This action will roll the password for the SQL Server.

Optional parameters:
* `serverSecretKeyVault` - specify a Key Vault to store the admin secret credentials in. If there is no Key Vault specified, it will default to the global secret client.
* `serverAdminSecretName` - specify a secret name for the admin secret. If there is no secret name specified, it will fallback to the default name.

#### rollusercreds

This action will roll the password for the SQL Database User.

Required parameters:
* `dbName` - name of the database
* `dbUser` - name of the user

Optional parameters:
* `userSecretKeyVault` - specify a Key Vault to store the user secret credentials in. If there is no Key Vault specified, it will default to the global secret client.
* `serverSecretKeyVault` - specify a Key Vault to store the admin secret credentials in. If there is no Key Vault specified, it will default to the global secret client.
* `serverAdminSecretName` - specify a secret name for the admin secret. If there is no secret name specified, it will fallback to the default name.

### SQL failover group

Expand Down
63 changes: 63 additions & 0 deletions pkg/resourcemanager/azuresql/azuresqlaction/azuresqlaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ package azuresqlaction

import (
"context"
"fmt"
"strings"

azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1"
"github.com/Azure/azure-service-operator/pkg/errhelp"
"github.com/Azure/azure-service-operator/pkg/helpers"
azuresqlserver "github.com/Azure/azure-service-operator/pkg/resourcemanager/azuresql/azuresqlserver"
"github.com/Azure/azure-service-operator/pkg/resourcemanager/azuresql/azuresqlshared"
azuresqluser "github.com/Azure/azure-service-operator/pkg/resourcemanager/azuresql/azuresqluser"
"github.com/Azure/azure-service-operator/pkg/secrets"
"github.com/Azure/go-autorest/autorest/to"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
)
Expand All @@ -29,6 +33,65 @@ func NewAzureSqlActionManager(secretClient secrets.SecretClient, scheme *runtime
}
}

func (s *AzureSqlActionManager) UpdateUserPassword(ctx context.Context, groupName string, serverName string, dbUser string, dbName string,
adminSecretKey types.NamespacedName, adminSecretClient secrets.SecretClient, userSecretClient secrets.SecretClient) error {
data, err := adminSecretClient.Get(ctx, adminSecretKey)
if err != nil {
return err
}

azuresqluserManager := azuresqluser.NewAzureSqlUserManager(userSecretClient, s.Scheme)
db, err := azuresqluserManager.ConnectToSqlDb(ctx, "sqlserver", serverName, dbName, 1433, string(data["username"]), string(data["password"]))
if err != nil {
return err
}

instance := &azurev1alpha1.AzureSQLUser{
ObjectMeta: metav1.ObjectMeta{
Name: dbUser,
Namespace: adminSecretKey.Namespace,
},
Spec: azurev1alpha1.AzureSQLUserSpec{
Server: serverName,
DbName: dbName,
},
}

DBSecret := azuresqluserManager.GetOrPrepareSecret(ctx, instance, userSecretClient)
// reset user from secret in case it was loaded
userExists, err := azuresqluserManager.UserExists(ctx, db, string(DBSecret["username"]))
if err != nil {
return fmt.Errorf("failed checking for user, err: %v", err)
}

if !userExists {
return fmt.Errorf("user does not exist")
}

password := helpers.NewPassword()
DBSecret["password"] = []byte(password)

err = azuresqluserManager.UpdateUser(ctx, DBSecret, db)
if err != nil {
return fmt.Errorf("error updating user credentials: %v", err)
}

secretKey := azuresqluser.GetNamespacedName(instance, userSecretClient)
key := types.NamespacedName{Namespace: secretKey.Namespace, Name: dbUser}
err = userSecretClient.Upsert(
ctx,
key,
DBSecret,
secrets.WithOwner(instance),
secrets.WithScheme(s.Scheme),
)
if err != nil {
return fmt.Errorf("failed to update secret: %v", err)
}

return nil
}

// UpdateAdminPassword gets the server instance from Azure, updates the admin password
// for the server and stores the new password in the secret
func (s *AzureSqlActionManager) UpdateAdminPassword(ctx context.Context, groupName string, serverName string, secretKey types.NamespacedName, secretClient secrets.SecretClient) error {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,20 @@ func (s *AzureSqlActionManager) Ensure(ctx context.Context, obj runtime.Object,
if err != nil {
return false, err
}
var key types.NamespacedName
var adminKey types.NamespacedName
var adminSecretClient secrets.SecretClient
var userSecretClient secrets.SecretClient
serverName := instance.Spec.ServerName
groupName := instance.Spec.ResourceGroup

if strings.ToLower(instance.Spec.ActionName) == "rolladmincreds" {

if !instance.Status.Provisioned {

// Determine the secret key based on the spec
if len(instance.Spec.ServerAdminSecretName) == 0 {
key = types.NamespacedName{Name: instance.Spec.ServerName, Namespace: instance.Namespace}
adminKey = types.NamespacedName{Name: instance.Spec.ServerName, Namespace: instance.Namespace}
} else {
key = types.NamespacedName{Name: instance.Spec.ServerAdminSecretName}
adminKey = types.NamespacedName{Name: instance.Spec.ServerAdminSecretName}
}

// Determine secretclient based on Spec. If Keyvault name isn't specified, fall back to
Expand All @@ -56,7 +56,7 @@ func (s *AzureSqlActionManager) Ensure(ctx context.Context, obj runtime.Object,
}

// Roll SQL server's admin password
err := s.UpdateAdminPassword(ctx, groupName, serverName, key, adminSecretClient)
err := s.UpdateAdminPassword(ctx, groupName, serverName, adminKey, adminSecretClient)
if err != nil {
instance.Status.Message = err.Error()
catch := []string{
Expand All @@ -75,6 +75,50 @@ func (s *AzureSqlActionManager) Ensure(ctx context.Context, obj runtime.Object,
instance.Status.Message = resourcemanager.SuccessMsg
}

} else if strings.ToLower(instance.Spec.ActionName) == "rollusercreds" {
if !instance.Status.Provisioned {

// Determine the admin secret key based on the spec
if len(instance.Spec.ServerAdminSecretName) == 0 {
adminKey = types.NamespacedName{Name: instance.Spec.ServerName, Namespace: instance.Namespace}
} else {
adminKey = types.NamespacedName{Name: instance.Spec.ServerAdminSecretName}
}

// Determine secretclient based on Spec. If Keyvault name isn't specified, fall back to
// global secret client
if len(instance.Spec.ServerSecretKeyVault) == 0 {
adminSecretClient = s.SecretClient
} else {
adminSecretClient = keyvaultsecretlib.New(instance.Spec.ServerSecretKeyVault)
if !keyvaultsecretlib.IsKeyVaultAccessible(adminSecretClient) {
instance.Status.Message = "InvalidKeyVaultAccess: Keyvault not accessible yet"
return false, nil
}
}

// Determine userSecretclient based on Spec. If Keyvault name isn't specified, fall back to
// global secret client
if len(instance.Spec.UserSecretKeyVault) == 0 {
userSecretClient = s.SecretClient
} else {
userSecretClient = keyvaultsecretlib.New(instance.Spec.UserSecretKeyVault)
if !keyvaultsecretlib.IsKeyVaultAccessible(userSecretClient) {
instance.Status.Message = "InvalidKeyVaultAccess: Keyvault not accessible yet"
return false, nil
}
}

err := s.UpdateUserPassword(ctx, groupName, serverName, instance.Spec.DbUser, instance.Spec.DbName, adminKey, adminSecretClient, userSecretClient)
if err != nil {
instance.Status.Message = err.Error()
return true, nil // unrecoverable error
}

instance.Status.Provisioned = true
instance.Status.Provisioning = false
instance.Status.Message = resourcemanager.SuccessMsg
}
} else {
instance.Status.Message = "Unrecognized action"
}
Expand Down
Loading