mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-08-16 10:40:31 -04:00
support to declaratively set attestation policy
This commit is contained in:
parent
b25228d175
commit
dbc495f164
15 changed files with 82 additions and 188 deletions
|
@ -4,11 +4,11 @@ load("//bazel/go:go_test.bzl", "go_test")
|
|||
go_library(
|
||||
name = "cloudcmd",
|
||||
srcs = [
|
||||
"attestationpolicy.go",
|
||||
"clients.go",
|
||||
"cloudcmd.go",
|
||||
"create.go",
|
||||
"iam.go",
|
||||
"patch.go",
|
||||
"rollback.go",
|
||||
"terminate.go",
|
||||
"validators.go",
|
||||
|
@ -29,9 +29,6 @@ go_library(
|
|||
"//internal/config",
|
||||
"//internal/constants",
|
||||
"//internal/imagefetcher",
|
||||
"@com_github_azure_azure_sdk_for_go//profiles/latest/attestation/attestation",
|
||||
"@com_github_azure_azure_sdk_for_go_sdk_azcore//policy",
|
||||
"@com_github_azure_azure_sdk_for_go_sdk_azidentity//:azidentity",
|
||||
"@com_github_hashicorp_terraform_json//:terraform-json",
|
||||
"@com_github_spf13_cobra//:cobra",
|
||||
],
|
||||
|
@ -40,10 +37,10 @@ go_library(
|
|||
go_test(
|
||||
name = "cloudcmd_test",
|
||||
srcs = [
|
||||
"attestationpolicy_test.go",
|
||||
"clients_test.go",
|
||||
"create_test.go",
|
||||
"iam_test.go",
|
||||
"patch_test.go",
|
||||
"rollback_test.go",
|
||||
"terminate_test.go",
|
||||
"validators_test.go",
|
||||
|
|
54
cli/internal/cloudcmd/attestationpolicy.go
Normal file
54
cli/internal/cloudcmd/attestationpolicy.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// maaAttestationPolicy is the default attestation policy for Azure VMs on Constellation.
|
||||
const maaAttestationPolicy = `
|
||||
version= 1.0;
|
||||
authorizationrules
|
||||
{
|
||||
[type=="x-ms-azurevm-default-securebootkeysvalidated", value==false] => deny();
|
||||
[type=="x-ms-azurevm-debuggersdisabled", value==false] => deny();
|
||||
// The line below was edited by the Constellation CLI. Do not edit manually.
|
||||
//[type=="secureboot", value==false] => deny();
|
||||
[type=="x-ms-azurevm-signingdisabled", value==false] => deny();
|
||||
[type=="x-ms-azurevm-dbvalidated", value==false] => deny();
|
||||
[type=="x-ms-azurevm-dbxvalidated", value==false] => deny();
|
||||
=> permit();
|
||||
};
|
||||
issuancerules
|
||||
{
|
||||
};`
|
||||
|
||||
// NewAzureMaaAttestationPolicy returns a new AzureAttestationPolicy to use with MAA.
|
||||
func NewAzureMaaAttestationPolicy() AzureAttestationPolicy {
|
||||
return AzureAttestationPolicy{
|
||||
policy: maaAttestationPolicy,
|
||||
}
|
||||
}
|
||||
|
||||
// AzureAttestationPolicy patches attestation policies on Azure.
|
||||
type AzureAttestationPolicy struct {
|
||||
policy string
|
||||
}
|
||||
|
||||
// Encode encodes the base64-encoded attestation policy in the JWS format specified here:
|
||||
// https://learn.microsoft.com/en-us/azure/attestation/author-sign-policy#creating-the-policy-file-in-json-web-signature-format
|
||||
func (p AzureAttestationPolicy) Encode() string {
|
||||
encodedPolicy := base64.RawURLEncoding.EncodeToString([]byte(p.policy))
|
||||
const header = `{"alg":"none"}`
|
||||
payload := fmt.Sprintf(`{"AttestationPolicy":"%s"}`, encodedPolicy)
|
||||
|
||||
encodedHeader := base64.RawURLEncoding.EncodeToString([]byte(header))
|
||||
encodedPayload := base64.RawURLEncoding.EncodeToString([]byte(payload))
|
||||
|
||||
return fmt.Sprintf("%s.%s.", encodedHeader, encodedPayload)
|
||||
}
|
|
@ -11,12 +11,12 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEncodeAttestationPolicy(t *testing.T) {
|
||||
func TestAzureMaaAttestationPolicyEncode(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
p := AzurePolicyPatcher{}
|
||||
p := NewAzureMaaAttestationPolicy()
|
||||
|
||||
// taken from <resource group url in the azure portal>/providers/Microsoft.Attestation/attestationProviders/<attestation provider name>/mrsg_item2
|
||||
expected := "eyJhbGciOiJub25lIn0.eyJBdHRlc3RhdGlvblBvbGljeSI6IkNpQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNCMlpYSnphVzl1UFNBeExqQTdDaUFnSUNBZ0lDQWdJQ0FnSUNBZ0lDQmhkWFJvYjNKcGVtRjBhVzl1Y25Wc1pYTUtJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lIc0tJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0JiZEhsd1pUMDlJbmd0YlhNdFlYcDFjbVYyYlMxa1pXWmhkV3gwTFhObFkzVnlaV0p2YjNSclpYbHpkbUZzYVdSaGRHVmtJaXdnZG1Gc2RXVTlQV1poYkhObFhTQTlQaUJrWlc1NUtDazdDaUFnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnVzNSNWNHVTlQU0o0TFcxekxXRjZkWEpsZG0wdFpHVmlkV2RuWlhKelpHbHpZV0pzWldRaUxDQjJZV3gxWlQwOVptRnNjMlZkSUQwLUlHUmxibmtvS1RzS0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQXZMeUJVYUdVZ2JHbHVaU0JpWld4dmR5QjNZWE1nWldScGRHVmtJR0o1SUhSb1pTQkRiMjV6ZEdWc2JHRjBhVzl1SUVOTVNTNGdSRzhnYm05MElHVmthWFFnYldGdWRXRnNiSGt1Q2lBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0x5OWJkSGx3WlQwOUluTmxZM1Z5WldKdmIzUWlMQ0IyWVd4MVpUMDlabUZzYzJWZElEMC1JR1JsYm5rb0tUc0tJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0JiZEhsd1pUMDlJbmd0YlhNdFlYcDFjbVYyYlMxemFXZHVhVzVuWkdsellXSnNaV1FpTENCMllXeDFaVDA5Wm1Gc2MyVmRJRDAtSUdSbGJua29LVHNLSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNCYmRIbHdaVDA5SW5ndGJYTXRZWHAxY21WMmJTMWtZblpoYkdsa1lYUmxaQ0lzSUhaaGJIVmxQVDFtWVd4elpWMGdQVDRnWkdWdWVTZ3BPd29nSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUZ0MGVYQmxQVDBpZUMxdGN5MWhlblZ5WlhadExXUmllSFpoYkdsa1lYUmxaQ0lzSUhaaGJIVmxQVDFtWVd4elpWMGdQVDRnWkdWdWVTZ3BPd29nSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUQwLUlIQmxjbTFwZENncE93b2dJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ2ZUc0tJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lHbHpjM1ZoYm1ObGNuVnNaWE1LSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJSHNLSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJSDA3In0."
|
||||
assert.Equal(expected, p.encodeAttestationPolicy())
|
||||
assert.Equal(expected, p.Encode())
|
||||
}
|
|
@ -35,7 +35,6 @@ type Creator struct {
|
|||
newTerraformClient func(ctx context.Context) (terraformClient, error)
|
||||
newLibvirtRunner func() libvirtRunner
|
||||
newRawDownloader func() rawDownloader
|
||||
policyPatcher policyPatcher
|
||||
}
|
||||
|
||||
// NewCreator creates a new creator.
|
||||
|
@ -52,7 +51,6 @@ func NewCreator(out io.Writer) *Creator {
|
|||
newRawDownloader: func() rawDownloader {
|
||||
return imagefetcher.NewDownloader()
|
||||
},
|
||||
policyPatcher: NewAzurePolicyPatcher(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -226,6 +224,7 @@ func (c *Creator) createAzure(ctx context.Context, cl terraformClient, opts Crea
|
|||
ImageID: opts.image,
|
||||
SecureBoot: *opts.Config.Provider.Azure.SecureBoot,
|
||||
CreateMAA: opts.Config.GetAttestationConfig().GetVariant().Equal(variant.AzureSEVSNP{}),
|
||||
MAAPolicy: NewAzureMaaAttestationPolicy().Encode(),
|
||||
Debug: opts.Config.IsDebugCluster(),
|
||||
}
|
||||
|
||||
|
@ -243,13 +242,6 @@ func (c *Creator) createAzure(ctx context.Context, cl terraformClient, opts Crea
|
|||
return clusterid.File{}, err
|
||||
}
|
||||
|
||||
if vars.CreateMAA {
|
||||
// Patch the attestation policy to allow the cluster to boot while having secure boot disabled.
|
||||
if err := c.policyPatcher.Patch(ctx, tfOutput.AttestationURL); err != nil {
|
||||
return clusterid.File{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return clusterid.File{
|
||||
CloudProvider: cloudprovider.Azure,
|
||||
IP: tfOutput.IP,
|
||||
|
@ -259,11 +251,6 @@ func (c *Creator) createAzure(ctx context.Context, cl terraformClient, opts Crea
|
|||
}, nil
|
||||
}
|
||||
|
||||
// policyPatcher interacts with the CSP (currently only applies for Azure) to update the attestation policy.
|
||||
type policyPatcher interface {
|
||||
Patch(ctx context.Context, attestationURL string) error
|
||||
}
|
||||
|
||||
// The azurerm Terraform provider enforces its own convention of case sensitivity for Azure URIs which Azure's API itself does not enforce or, even worse, actually returns.
|
||||
// Let's go loco with case insensitive Regexp here and fix the user input here to be compliant with this arbitrary design decision.
|
||||
var (
|
||||
|
|
|
@ -33,7 +33,6 @@ func TestCreator(t *testing.T) {
|
|||
libvirt *stubLibvirtRunner
|
||||
provider cloudprovider.Provider
|
||||
config *config.Config
|
||||
policyPatcher *stubPolicyPatcher
|
||||
wantErr bool
|
||||
wantRollback bool // Use only together with stubClients.
|
||||
wantTerraformRollback bool // When libvirt fails, don't call into Terraform.
|
||||
|
@ -65,7 +64,6 @@ func TestCreator(t *testing.T) {
|
|||
cfg.RemoveProviderAndAttestationExcept(cloudprovider.Azure)
|
||||
return cfg
|
||||
}(),
|
||||
policyPatcher: &stubPolicyPatcher{},
|
||||
},
|
||||
"azure trusted launch": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
|
@ -77,18 +75,6 @@ func TestCreator(t *testing.T) {
|
|||
}
|
||||
return cfg
|
||||
}(),
|
||||
policyPatcher: &stubPolicyPatcher{},
|
||||
},
|
||||
"azure new policy patch error": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
provider: cloudprovider.Azure,
|
||||
config: func() *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.RemoveProviderAndAttestationExcept(cloudprovider.Azure)
|
||||
return cfg
|
||||
}(),
|
||||
policyPatcher: &stubPolicyPatcher{someErr},
|
||||
wantErr: true,
|
||||
},
|
||||
"azure newTerraformClient error": {
|
||||
newTfClientErr: someErr,
|
||||
|
@ -98,8 +84,7 @@ func TestCreator(t *testing.T) {
|
|||
cfg.RemoveProviderAndAttestationExcept(cloudprovider.Azure)
|
||||
return cfg
|
||||
}(),
|
||||
policyPatcher: &stubPolicyPatcher{},
|
||||
wantErr: true,
|
||||
wantErr: true,
|
||||
},
|
||||
"azure create cluster error": {
|
||||
tfClient: &stubTerraformClient{createClusterErr: someErr},
|
||||
|
@ -109,7 +94,6 @@ func TestCreator(t *testing.T) {
|
|||
cfg.RemoveProviderAndAttestationExcept(cloudprovider.Azure)
|
||||
return cfg
|
||||
}(),
|
||||
policyPatcher: &stubPolicyPatcher{},
|
||||
wantErr: true,
|
||||
wantRollback: true,
|
||||
wantTerraformRollback: true,
|
||||
|
@ -213,7 +197,6 @@ func TestCreator(t *testing.T) {
|
|||
destination: "some-destination",
|
||||
}
|
||||
},
|
||||
policyPatcher: tc.policyPatcher,
|
||||
}
|
||||
|
||||
opts := CreateOptions{
|
||||
|
@ -247,14 +230,6 @@ func TestCreator(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type stubPolicyPatcher struct {
|
||||
patchErr error
|
||||
}
|
||||
|
||||
func (s stubPolicyPatcher) Patch(_ context.Context, _ string) error {
|
||||
return s.patchErr
|
||||
}
|
||||
|
||||
func TestNormalizeAzureURIs(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
in terraform.AzureClusterVariables
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/attestation/attestation"
|
||||
azpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
)
|
||||
|
||||
// NewAzurePolicyPatcher returns a new AzurePolicyPatcher.
|
||||
func NewAzurePolicyPatcher() AzurePolicyPatcher {
|
||||
return AzurePolicyPatcher{}
|
||||
}
|
||||
|
||||
// AzurePolicyPatcher patches attestation policies on Azure.
|
||||
type AzurePolicyPatcher struct{}
|
||||
|
||||
// Patch updates the attestation policy to the base64-encoded attestation policy JWT for the given attestation URL.
|
||||
// https://learn.microsoft.com/en-us/azure/attestation/author-sign-policy#next-steps
|
||||
func (p AzurePolicyPatcher) Patch(ctx context.Context, attestationURL string) error {
|
||||
// hacky way to update the MAA attestation policy. This should be changed as soon as either the Terraform provider supports it
|
||||
// or the Go SDK gets updated to a recent API version.
|
||||
// https://github.com/hashicorp/terraform-provider-azurerm/issues/20804
|
||||
cred, err := azidentity.NewDefaultAzureCredential(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving default Azure credentials: %w", err)
|
||||
}
|
||||
token, err := cred.GetToken(ctx, azpolicy.TokenRequestOptions{
|
||||
Scopes: []string{"https://attest.azure.net/.default"},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving token from default Azure credentials: %w", err)
|
||||
}
|
||||
|
||||
client := attestation.NewPolicyClient()
|
||||
|
||||
// azureGuest is the id for the "Azure VM" attestation type. Other types are documented here:
|
||||
// https://learn.microsoft.com/en-us/rest/api/attestation/policy/set
|
||||
req, err := client.SetPreparer(ctx, attestationURL, "azureGuest", p.encodeAttestationPolicy())
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.Token))
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Send(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending request: %w", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("updating attestation policy: unexpected status code: %s", resp.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeAttestationPolicy encodes the base64-encoded attestation policy in the JWS format specified here:
|
||||
// https://learn.microsoft.com/en-us/azure/attestation/author-sign-policy#creating-the-policy-file-in-json-web-signature-format
|
||||
func (p AzurePolicyPatcher) encodeAttestationPolicy() string {
|
||||
const policy = `
|
||||
version= 1.0;
|
||||
authorizationrules
|
||||
{
|
||||
[type=="x-ms-azurevm-default-securebootkeysvalidated", value==false] => deny();
|
||||
[type=="x-ms-azurevm-debuggersdisabled", value==false] => deny();
|
||||
// The line below was edited by the Constellation CLI. Do not edit manually.
|
||||
//[type=="secureboot", value==false] => deny();
|
||||
[type=="x-ms-azurevm-signingdisabled", value==false] => deny();
|
||||
[type=="x-ms-azurevm-dbvalidated", value==false] => deny();
|
||||
[type=="x-ms-azurevm-dbxvalidated", value==false] => deny();
|
||||
=> permit();
|
||||
};
|
||||
issuancerules
|
||||
{
|
||||
};`
|
||||
encodedPolicy := base64.RawURLEncoding.EncodeToString([]byte(policy))
|
||||
const header = `{"alg":"none"}`
|
||||
payload := fmt.Sprintf(`{"AttestationPolicy":"%s"}`, encodedPolicy)
|
||||
|
||||
encodedHeader := base64.RawURLEncoding.EncodeToString([]byte(header))
|
||||
encodedPayload := base64.RawURLEncoding.EncodeToString([]byte(payload))
|
||||
|
||||
return fmt.Sprintf("%s.%s.", encodedHeader, encodedPayload)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue