Skip to content

Commit c8246e3

Browse files
authored
feat: Add Azure instance identitity authentication (#1064)
This enables zero-trust authentication for Azure instances. Now we support the three major clouds: AWS, Azure, and GCP 😎.
1 parent 118a47e commit c8246e3

13 files changed

+348
-15
lines changed

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"trimprefix",
5656
"unconvert",
5757
"Untar",
58+
"VMID",
5859
"webrtc",
5960
"xerrors",
6061
"yamux"

cli/agent.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,19 @@ func workspaceAgent() *cobra.Command {
7676
return client.AuthWorkspaceAWSInstanceIdentity(ctx)
7777
}
7878
case "azure-instance-identity":
79-
return xerrors.Errorf("not implemented")
79+
// This is *only* done for testing to mock client authentication.
80+
// This will never be set in a production scenario.
81+
var azureClient *http.Client
82+
azureClientRaw := cmd.Context().Value("azure-client")
83+
if azureClientRaw != nil {
84+
azureClient, _ = azureClientRaw.(*http.Client)
85+
if azureClient != nil {
86+
client.HTTPClient = azureClient
87+
}
88+
}
89+
exchangeToken = func(ctx context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error) {
90+
return client.AuthWorkspaceAzureInstanceIdentity(ctx)
91+
}
8092
}
8193

8294
if exchangeToken != nil {

cli/agent_test.go

+56-2
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,66 @@ import (
1515

1616
func TestWorkspaceAgent(t *testing.T) {
1717
t.Parallel()
18+
t.Run("Azure", func(t *testing.T) {
19+
t.Parallel()
20+
instanceID := "instanceidentifier"
21+
certificates, metadataClient := coderdtest.NewAzureInstanceIdentity(t, instanceID)
22+
client := coderdtest.New(t, &coderdtest.Options{
23+
AzureCertificates: certificates,
24+
})
25+
user := coderdtest.CreateFirstUser(t, client)
26+
coderdtest.NewProvisionerDaemon(t, client)
27+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
28+
Parse: echo.ParseComplete,
29+
Provision: []*proto.Provision_Response{{
30+
Type: &proto.Provision_Response_Complete{
31+
Complete: &proto.Provision_Complete{
32+
Resources: []*proto.Resource{{
33+
Name: "somename",
34+
Type: "someinstance",
35+
Agents: []*proto.Agent{{
36+
Auth: &proto.Agent_InstanceId{
37+
InstanceId: instanceID,
38+
},
39+
}},
40+
}},
41+
},
42+
},
43+
}},
44+
})
45+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
46+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
47+
workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID)
48+
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
49+
50+
cmd, _ := clitest.New(t, "agent", "--auth", "azure-instance-identity", "--url", client.URL.String())
51+
ctx, cancelFunc := context.WithCancel(context.Background())
52+
defer cancelFunc()
53+
go func() {
54+
// A linting error occurs for weakly typing the context value here,
55+
// but it seems reasonable for a one-off test.
56+
// nolint
57+
ctx = context.WithValue(ctx, "azure-client", metadataClient)
58+
err := cmd.ExecuteContext(ctx)
59+
require.NoError(t, err)
60+
}()
61+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
62+
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
63+
require.NoError(t, err)
64+
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
65+
require.NoError(t, err)
66+
defer dialer.Close()
67+
_, err = dialer.Ping()
68+
require.NoError(t, err)
69+
cancelFunc()
70+
})
71+
1872
t.Run("AWS", func(t *testing.T) {
1973
t.Parallel()
2074
instanceID := "instanceidentifier"
2175
certificates, metadataClient := coderdtest.NewAWSInstanceIdentity(t, instanceID)
2276
client := coderdtest.New(t, &coderdtest.Options{
23-
AWSInstanceIdentity: certificates,
77+
AWSCertificates: certificates,
2478
})
2579
user := coderdtest.CreateFirstUser(t, client)
2680
coderdtest.NewProvisionerDaemon(t, client)
@@ -74,7 +128,7 @@ func TestWorkspaceAgent(t *testing.T) {
74128
instanceID := "instanceidentifier"
75129
validator, metadata := coderdtest.NewGoogleInstanceIdentity(t, instanceID, false)
76130
client := coderdtest.New(t, &coderdtest.Options{
77-
GoogleInstanceIdentity: validator,
131+
GoogleTokenValidator: validator,
78132
})
79133
user := coderdtest.CreateFirstUser(t, client)
80134
coderdtest.NewProvisionerDaemon(t, client)

coderd/azureidentity/azureidentity.go

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package azureidentity
2+
3+
import (
4+
"context"
5+
"crypto/x509"
6+
"encoding/base64"
7+
"encoding/json"
8+
"io"
9+
"net/http"
10+
"regexp"
11+
12+
"go.mozilla.org/pkcs7"
13+
"golang.org/x/xerrors"
14+
)
15+
16+
// allowedSigners matches valid common names listed here:
17+
// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#tabgroup_14
18+
var allowedSigners = regexp.MustCompile(`^(.*\.)?metadata\.(azure\.(com|us|cn)|microsoftazure\.de)$`)
19+
20+
type metadata struct {
21+
VMID string `json:"vmId"`
22+
}
23+
24+
// Validate ensures the signature was signed by an Azure certificate.
25+
// It returns the associated VM ID if successful.
26+
func Validate(ctx context.Context, signature string, options x509.VerifyOptions) (string, error) {
27+
data, err := base64.StdEncoding.DecodeString(signature)
28+
if err != nil {
29+
return "", xerrors.Errorf("decode base64: %w", err)
30+
}
31+
pkcs7Data, err := pkcs7.Parse(data)
32+
if err != nil {
33+
return "", xerrors.Errorf("parse pkcs7: %w", err)
34+
}
35+
signer := pkcs7Data.GetOnlySigner()
36+
if signer == nil {
37+
return "", xerrors.New("no signers for signature")
38+
}
39+
if !allowedSigners.MatchString(signer.Subject.CommonName) {
40+
return "", xerrors.Errorf("unmatched common name of signer: %q", signer.Subject.CommonName)
41+
}
42+
if options.Intermediates == nil {
43+
options.Intermediates = x509.NewCertPool()
44+
for _, certURL := range signer.IssuingCertificateURL {
45+
req, err := http.NewRequestWithContext(ctx, "GET", certURL, nil)
46+
if err != nil {
47+
return "", xerrors.Errorf("new request %q: %w", certURL, err)
48+
}
49+
res, err := http.DefaultClient.Do(req)
50+
if err != nil {
51+
return "", xerrors.Errorf("perform request %q: %w", certURL, err)
52+
}
53+
data, err := io.ReadAll(res.Body)
54+
if err != nil {
55+
return "", xerrors.Errorf("read body %q: %w", certURL, err)
56+
}
57+
cert, err := x509.ParseCertificate(data)
58+
if err != nil {
59+
return "", xerrors.Errorf("parse certificate %q: %w", certURL, err)
60+
}
61+
options.Intermediates.AddCert(cert)
62+
}
63+
}
64+
65+
_, err = signer.Verify(options)
66+
if err != nil {
67+
return "", xerrors.Errorf("verify certificates: %w", err)
68+
}
69+
70+
var metadata metadata
71+
err = json.Unmarshal(pkcs7Data.Content, &metadata)
72+
if err != nil {
73+
return "", xerrors.Errorf("unmarshal metadata: %w", err)
74+
}
75+
return metadata.VMID, nil
76+
}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package azureidentity_test
2+
3+
import (
4+
"context"
5+
"crypto/x509"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/coderd/azureidentity"
11+
)
12+
13+
const (
14+
signature = `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`
15+
)
16+
17+
func TestValidate(t *testing.T) {
18+
t.Parallel()
19+
vm, err := azureidentity.Validate(context.Background(), signature, x509.VerifyOptions{})
20+
require.NoError(t, err)
21+
require.Equal(t, "bd8e7443-24a0-41f3-b949-8baf4fd1c573", vm)
22+
}

coderd/coderd.go

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package coderd
22

33
import (
44
"context"
5+
"crypto/x509"
56
"fmt"
67
"net/http"
78
"net/url"
@@ -35,6 +36,7 @@ type Options struct {
3536

3637
AgentConnectionUpdateFrequency time.Duration
3738
AWSCertificates awsidentity.Certificates
39+
AzureCertificates x509.VerifyOptions
3840
GoogleTokenValidator *idtoken.Validator
3941
ICEServers []webrtc.ICEServer
4042
SecureAuthCookie bool
@@ -172,6 +174,7 @@ func New(options *Options) (http.Handler, func()) {
172174
})
173175
})
174176
r.Route("/workspaceagents", func(r chi.Router) {
177+
r.Post("/azure-instance-identity", api.postWorkspaceAuthAzureInstanceIdentity)
175178
r.Post("/aws-instance-identity", api.postWorkspaceAuthAWSInstanceIdentity)
176179
r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity)
177180
r.Route("/me", func(r chi.Router) {

coderd/coderdtest/coderdtest.go

+70-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"crypto/rsa"
99
"crypto/sha256"
1010
"crypto/x509"
11+
"crypto/x509/pkix"
1112
"database/sql"
1213
"encoding/base64"
1314
"encoding/json"
@@ -24,6 +25,7 @@ import (
2425
"time"
2526

2627
"cloud.google.com/go/compute/metadata"
28+
"github.com/fullsailor/pkcs7"
2729
"github.com/golang-jwt/jwt"
2830
"github.com/google/uuid"
2931
"github.com/moby/moby/pkg/namesgenerator"
@@ -49,9 +51,10 @@ import (
4951
)
5052

5153
type Options struct {
52-
AWSInstanceIdentity awsidentity.Certificates
53-
GoogleInstanceIdentity *idtoken.Validator
54-
SSHKeygenAlgorithm gitsshkey.Algorithm
54+
AWSCertificates awsidentity.Certificates
55+
AzureCertificates x509.VerifyOptions
56+
GoogleTokenValidator *idtoken.Validator
57+
SSHKeygenAlgorithm gitsshkey.Algorithm
5558
}
5659

5760
// New constructs an in-memory coderd instance and returns
@@ -60,11 +63,11 @@ func New(t *testing.T, options *Options) *codersdk.Client {
6063
if options == nil {
6164
options = &Options{}
6265
}
63-
if options.GoogleInstanceIdentity == nil {
66+
if options.GoogleTokenValidator == nil {
6467
ctx, cancelFunc := context.WithCancel(context.Background())
6568
t.Cleanup(cancelFunc)
6669
var err error
67-
options.GoogleInstanceIdentity, err = idtoken.NewValidator(ctx, option.WithoutAuthentication())
70+
options.GoogleTokenValidator, err = idtoken.NewValidator(ctx, option.WithoutAuthentication())
6871
require.NoError(t, err)
6972
}
7073

@@ -117,8 +120,9 @@ func New(t *testing.T, options *Options) *codersdk.Client {
117120
Database: db,
118121
Pubsub: pubsub,
119122

120-
AWSCertificates: options.AWSInstanceIdentity,
121-
GoogleTokenValidator: options.GoogleInstanceIdentity,
123+
AWSCertificates: options.AWSCertificates,
124+
AzureCertificates: options.AzureCertificates,
125+
GoogleTokenValidator: options.GoogleTokenValidator,
122126
SSHKeygenAlgorithm: options.SSHKeygenAlgorithm,
123127
TURNServer: turnServer,
124128
})
@@ -414,6 +418,65 @@ func NewAWSInstanceIdentity(t *testing.T, instanceID string) (awsidentity.Certif
414418
}
415419
}
416420

421+
// NewAzureInstanceIdentity returns a metadata client and ID token validator for faking
422+
// instance authentication for Azure.
423+
func NewAzureInstanceIdentity(t *testing.T, instanceID string) (x509.VerifyOptions, *http.Client) {
424+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
425+
require.NoError(t, err)
426+
427+
rawCertificate, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{
428+
SerialNumber: big.NewInt(2022),
429+
NotAfter: time.Now().AddDate(1, 0, 0),
430+
Subject: pkix.Name{
431+
CommonName: "metadata.azure.com",
432+
},
433+
}, &x509.Certificate{}, &privateKey.PublicKey, privateKey)
434+
require.NoError(t, err)
435+
436+
certificate, err := x509.ParseCertificate(rawCertificate)
437+
require.NoError(t, err)
438+
439+
signed, err := pkcs7.NewSignedData([]byte(`{"vmId":"` + instanceID + `"}`))
440+
require.NoError(t, err)
441+
err = signed.AddSigner(certificate, privateKey, pkcs7.SignerInfoConfig{})
442+
require.NoError(t, err)
443+
signatureRaw, err := signed.Finish()
444+
require.NoError(t, err)
445+
signature := make([]byte, base64.StdEncoding.EncodedLen(len(signatureRaw)))
446+
base64.StdEncoding.Encode(signature, signatureRaw)
447+
448+
payload, err := json.Marshal(codersdk.AzureInstanceIdentityToken{
449+
Signature: string(signature),
450+
Encoding: "pkcs7",
451+
})
452+
require.NoError(t, err)
453+
454+
certPool := x509.NewCertPool()
455+
certPool.AddCert(certificate)
456+
457+
return x509.VerifyOptions{
458+
Intermediates: certPool,
459+
Roots: certPool,
460+
}, &http.Client{
461+
Transport: roundTripper(func(r *http.Request) (*http.Response, error) {
462+
// Only handle metadata server requests.
463+
if r.URL.Host != "169.254.169.254" {
464+
return http.DefaultTransport.RoundTrip(r)
465+
}
466+
switch r.URL.Path {
467+
case "/metadata/attested/document":
468+
return &http.Response{
469+
StatusCode: http.StatusOK,
470+
Body: io.NopCloser(bytes.NewReader(payload)),
471+
Header: make(http.Header),
472+
}, nil
473+
default:
474+
panic("unhandled route: " + r.URL.Path)
475+
}
476+
}),
477+
}
478+
}
479+
417480
func randomUsername() string {
418481
return strings.ReplaceAll(namesgenerator.GetRandomName(0), "_", "-")
419482
}

coderd/workspaceresourceauth.go

+18
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,31 @@ import (
88
"net/http"
99

1010
"github.com/coder/coder/coderd/awsidentity"
11+
"github.com/coder/coder/coderd/azureidentity"
1112
"github.com/coder/coder/coderd/database"
1213
"github.com/coder/coder/coderd/httpapi"
1314
"github.com/coder/coder/codersdk"
1415

1516
"github.com/mitchellh/mapstructure"
1617
)
1718

19+
// Azure supports instance identity verification:
20+
// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#tabgroup_14
21+
func (api *api) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
22+
var req codersdk.AzureInstanceIdentityToken
23+
if !httpapi.Read(rw, r, &req) {
24+
return
25+
}
26+
instanceID, err := azureidentity.Validate(r.Context(), req.Signature, api.AzureCertificates)
27+
if err != nil {
28+
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
29+
Message: fmt.Sprintf("validate: %s", err),
30+
})
31+
return
32+
}
33+
api.handleAuthInstanceID(rw, r, instanceID)
34+
}
35+
1836
// AWS supports instance identity verification:
1937
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
2038
// Using this, we can exchange a signed instance payload for an agent token.

0 commit comments

Comments
 (0)