mirror of https://github.com/coder/coder.git
feat: Add AWS instance identity authentication (#570)
* feat: Add AWS instance identity authentication This allows zero-trust authentication for all AWS instances. Prior to this, AWS instances could be used by passing `CODER_TOKEN` as an environment variable to the startup script. AWS explicitly states that secrets should not be passed in startup scripts because it's user-readable. * Fix sha256 verbosity * Fix HTTP client being exposed on auth
This commit is contained in:
parent
01957da040
commit
a502a5fa14
1
Makefile
1
Makefile
|
@ -90,6 +90,5 @@ site/out:
|
|||
.PHONY: site/out
|
||||
|
||||
lint:
|
||||
@echo "--- golangci-lint"
|
||||
golangci-lint run
|
||||
.PHONY: lint
|
||||
|
|
|
@ -36,7 +36,7 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
|
|||
signal.Notify(interrupt, os.Interrupt)
|
||||
defer signal.Stop(interrupt)
|
||||
|
||||
errCh := make(chan error)
|
||||
errCh := make(chan error, 1)
|
||||
lineCh := make(chan string)
|
||||
go func() {
|
||||
var line string
|
||||
|
|
|
@ -2,6 +2,7 @@ package cli
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
|
@ -39,6 +40,11 @@ func workspaceAgent() *cobra.Command {
|
|||
}
|
||||
logger := slog.Make(sloghuman.Sink(cmd.OutOrStdout())).Leveled(slog.LevelDebug)
|
||||
client := codersdk.New(coderURL)
|
||||
|
||||
// exchangeToken returns a session token.
|
||||
// This is abstracted to allow for the same looping condition
|
||||
// regardless of instance identity auth type.
|
||||
var exchangeToken func(context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error)
|
||||
switch auth {
|
||||
case "token":
|
||||
if token == "" {
|
||||
|
@ -53,29 +59,51 @@ func workspaceAgent() *cobra.Command {
|
|||
if gcpClientRaw != nil {
|
||||
gcpClient, _ = gcpClientRaw.(*metadata.Client)
|
||||
}
|
||||
exchangeToken = func(ctx context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error) {
|
||||
return client.AuthWorkspaceGoogleInstanceIdentity(ctx, "", gcpClient)
|
||||
}
|
||||
case "aws-instance-identity":
|
||||
// This is *only* done for testing to mock client authentication.
|
||||
// This will never be set in a production scenario.
|
||||
var awsClient *http.Client
|
||||
awsClientRaw := cmd.Context().Value("aws-client")
|
||||
if awsClientRaw != nil {
|
||||
awsClient, _ = awsClientRaw.(*http.Client)
|
||||
if awsClient != nil {
|
||||
client.HTTPClient = awsClient
|
||||
}
|
||||
}
|
||||
exchangeToken = func(ctx context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error) {
|
||||
return client.AuthWorkspaceAWSInstanceIdentity(ctx)
|
||||
}
|
||||
case "azure-instance-identity":
|
||||
return xerrors.Errorf("not implemented")
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithTimeout(cmd.Context(), 30*time.Second)
|
||||
if exchangeToken != nil {
|
||||
// Agent's can start before resources are returned from the provisioner
|
||||
// daemon. If there are many resources being provisioned, this time
|
||||
// could be significant. This is arbitrarily set at an hour to prevent
|
||||
// tons of idle agents from pinging coderd.
|
||||
ctx, cancelFunc := context.WithTimeout(cmd.Context(), time.Hour)
|
||||
defer cancelFunc()
|
||||
for retry.New(100*time.Millisecond, 5*time.Second).Wait(ctx) {
|
||||
var response codersdk.WorkspaceAgentAuthenticateResponse
|
||||
|
||||
response, err = client.AuthWorkspaceGoogleInstanceIdentity(ctx, "", gcpClient)
|
||||
response, err = exchangeToken(ctx)
|
||||
if err != nil {
|
||||
logger.Warn(ctx, "authenticate workspace with Google Instance Identity", slog.Error(err))
|
||||
logger.Warn(ctx, "authenticate workspace", slog.F("method", auth), slog.Error(err))
|
||||
continue
|
||||
}
|
||||
client.SessionToken = response.SessionToken
|
||||
logger.Info(ctx, "authenticated with Google Instance Identity")
|
||||
logger.Info(ctx, "authenticated", slog.F("method", auth))
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return xerrors.Errorf("agent failed to authenticate in time: %w", err)
|
||||
}
|
||||
case "aws-instance-identity":
|
||||
return xerrors.Errorf("not implemented")
|
||||
case "azure-instance-identity":
|
||||
return xerrors.Errorf("not implemented")
|
||||
}
|
||||
|
||||
closer := agent.New(client.ListenWorkspaceAgent, &peer.ConnOptions{
|
||||
Logger: logger,
|
||||
})
|
||||
|
|
|
@ -14,12 +14,66 @@ import (
|
|||
|
||||
func TestWorkspaceAgent(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("AWS", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
instanceID := "instanceidentifier"
|
||||
certificates, metadataClient := coderdtest.NewAWSInstanceIdentity(t, instanceID)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
AWSInstanceIdentity: certificates,
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
Provision: []*proto.Provision_Response{{
|
||||
Type: &proto.Provision_Response_Complete{
|
||||
Complete: &proto.Provision_Complete{
|
||||
Resources: []*proto.Resource{{
|
||||
Name: "somename",
|
||||
Type: "someinstance",
|
||||
Agent: &proto.Agent{
|
||||
Auth: &proto.Agent_InstanceId{
|
||||
InstanceId: instanceID,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
})
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
cmd, _ := clitest.New(t, "workspaces", "agent", "--auth", "aws-instance-identity", "--url", client.URL.String())
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
go func() {
|
||||
// A linting error occurs for weakly typing the context value here,
|
||||
// but it seems reasonable for a one-off test.
|
||||
// nolint
|
||||
ctx = context.WithValue(ctx, "aws-client", metadataClient)
|
||||
err := cmd.ExecuteContext(ctx)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
|
||||
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
|
||||
require.NoError(t, err)
|
||||
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].ID, nil, nil)
|
||||
require.NoError(t, err)
|
||||
defer dialer.Close()
|
||||
_, err = dialer.Ping()
|
||||
require.NoError(t, err)
|
||||
cancelFunc()
|
||||
})
|
||||
|
||||
t.Run("GoogleCloud", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
instanceID := "instanceidentifier"
|
||||
validator, metadata := coderdtest.NewGoogleInstanceIdentity(t, instanceID, false)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
GoogleTokenValidator: validator,
|
||||
GoogleInstanceIdentity: validator,
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
package awsidentity
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Region represents the AWS locations a public-key covers.
|
||||
type Region string
|
||||
|
||||
const (
|
||||
Other Region = "other"
|
||||
HongKong Region = "hongkong"
|
||||
Bahrain Region = "bahrain"
|
||||
CapeTown Region = "capetown"
|
||||
Milan Region = "milan"
|
||||
China Region = "china"
|
||||
GovCloud Region = "govcloud"
|
||||
)
|
||||
|
||||
var (
|
||||
All = []Region{Other, HongKong, Bahrain, CapeTown, Milan, China, GovCloud}
|
||||
)
|
||||
|
||||
// Certificates hold public keys for various AWS regions. See:
|
||||
type Certificates map[Region]string
|
||||
|
||||
// Identity represents a validated document and signature.
|
||||
type Identity struct {
|
||||
InstanceID string
|
||||
Region Region
|
||||
}
|
||||
|
||||
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
|
||||
type awsInstanceIdentityDocument struct {
|
||||
InstanceID string `json:"instanceId"`
|
||||
}
|
||||
|
||||
// Validate ensures the document was signed by an AWS public key.
|
||||
// Regions that aren't provided in certificates will use defaults.
|
||||
func Validate(signature, document string, certificates Certificates) (Identity, error) {
|
||||
if certificates == nil {
|
||||
certificates = Certificates{}
|
||||
}
|
||||
for _, region := range All {
|
||||
if _, ok := certificates[region]; ok {
|
||||
continue
|
||||
}
|
||||
defaultCertificate, exists := defaultCertificates[region]
|
||||
if !exists {
|
||||
panic("dev error: no certificate exists for region " + region)
|
||||
}
|
||||
certificates[region] = defaultCertificate
|
||||
}
|
||||
|
||||
var instanceIdentity awsInstanceIdentityDocument
|
||||
err := json.Unmarshal([]byte(document), &instanceIdentity)
|
||||
if err != nil {
|
||||
return Identity{}, xerrors.Errorf("parse document: %w", err)
|
||||
}
|
||||
rawSignature, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
return Identity{}, xerrors.Errorf("decode signature: %w", err)
|
||||
}
|
||||
hashedDocument := sha256.Sum256([]byte(document))
|
||||
|
||||
for region, certificate := range certificates {
|
||||
regionBlock, rest := pem.Decode([]byte(certificate))
|
||||
if len(rest) != 0 {
|
||||
return Identity{}, xerrors.Errorf("invalid certificate for %q. %d bytes remain", region, len(rest))
|
||||
}
|
||||
regionCert, err := x509.ParseCertificate(regionBlock.Bytes)
|
||||
if err != nil {
|
||||
return Identity{}, xerrors.Errorf("parse certificate: %w", err)
|
||||
}
|
||||
regionPublicKey, valid := regionCert.PublicKey.(*rsa.PublicKey)
|
||||
if !valid {
|
||||
return Identity{}, xerrors.Errorf("certificate for %q was not an rsa key", region)
|
||||
}
|
||||
err = rsa.VerifyPKCS1v15(regionPublicKey, crypto.SHA256, hashedDocument[:], rawSignature)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return Identity{
|
||||
InstanceID: instanceIdentity.InstanceID,
|
||||
Region: region,
|
||||
}, nil
|
||||
}
|
||||
return Identity{}, rsa.ErrVerification
|
||||
}
|
||||
|
||||
// Default AWS certificates for regions.
|
||||
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/verify-signature.html
|
||||
var defaultCertificates = Certificates{
|
||||
Other: `-----BEGIN CERTIFICATE-----
|
||||
MIIDIjCCAougAwIBAgIJAKnL4UEDMN/FMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV
|
||||
BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgw
|
||||
FgYDVQQKEw9BbWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3Mu
|
||||
Y29tMB4XDTE0MDYwNTE0MjgwMloXDTI0MDYwNTE0MjgwMlowajELMAkGA1UEBhMC
|
||||
VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1NlYXR0bGUxGDAWBgNV
|
||||
BAoTD0FtYXpvbi5jb20gSW5jLjEaMBgGA1UEAxMRZWMyLmFtYXpvbmF3cy5jb20w
|
||||
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAIe9GN//SRK2knbjySG0ho3yqQM3
|
||||
e2TDhWO8D2e8+XZqck754gFSo99AbT2RmXClambI7xsYHZFapbELC4H91ycihvrD
|
||||
jbST1ZjkLQgga0NE1q43eS68ZeTDccScXQSNivSlzJZS8HJZjgqzBlXjZftjtdJL
|
||||
XeE4hwvo0sD4f3j9AgMBAAGjgc8wgcwwHQYDVR0OBBYEFCXWzAgVyrbwnFncFFIs
|
||||
77VBdlE4MIGcBgNVHSMEgZQwgZGAFCXWzAgVyrbwnFncFFIs77VBdlE4oW6kbDBq
|
||||
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2Vh
|
||||
dHRsZTEYMBYGA1UEChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1h
|
||||
em9uYXdzLmNvbYIJAKnL4UEDMN/FMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
|
||||
BQADgYEAFYcz1OgEhQBXIwIdsgCOS8vEtiJYF+j9uO6jz7VOmJqO+pRlAbRlvY8T
|
||||
C1haGgSI/A1uZUKs/Zfnph0oEI0/hu1IIJ/SKBDtN5lvmZ/IzbOPIJWirlsllQIQ
|
||||
7zvWbGd9c9+Rm3p04oTvhup99la7kZqevJK0QRdD/6NpCKsqP/0=
|
||||
-----END CERTIFICATE-----`,
|
||||
HongKong: `-----BEGIN CERTIFICATE-----
|
||||
MIICSzCCAbQCCQDtQvkVxRvK9TANBgkqhkiG9w0BAQsFADBqMQswCQYDVQQGEwJV
|
||||
UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2VhdHRsZTEYMBYGA1UE
|
||||
ChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1hem9uYXdzLmNvbTAe
|
||||
Fw0xOTAyMDMwMzAwMDZaFw0yOTAyMDIwMzAwMDZaMGoxCzAJBgNVBAYTAlVTMRMw
|
||||
EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgwFgYDVQQKEw9B
|
||||
bWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3MuY29tMIGfMA0G
|
||||
CSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1kkHXYTfc7gY5Q55JJhjTieHAgacaQkiR
|
||||
Pity9QPDE3b+NXDh4UdP1xdIw73JcIIG3sG9RhWiXVCHh6KkuCTqJfPUknIKk8vs
|
||||
M3RXflUpBe8Pf+P92pxqPMCz1Fr2NehS3JhhpkCZVGxxwLC5gaG0Lr4rFORubjYY
|
||||
Rh84dK98VwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAA6xV9f0HMqXjPHuGILDyaNN
|
||||
dKcvplNFwDTydVg32MNubAGnecoEBtUPtxBsLoVYXCOb+b5/ZMDubPF9tU/vSXuo
|
||||
TpYM5Bq57gJzDRaBOntQbX9bgHiUxw6XZWaTS/6xjRJDT5p3S1E0mPI3lP/eJv4o
|
||||
Ezk5zb3eIf10/sqt4756
|
||||
-----END CERTIFICATE-----`,
|
||||
Bahrain: `-----BEGIN CERTIFICATE-----
|
||||
MIIDPDCCAqWgAwIBAgIJAMl6uIV/zqJFMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNV
|
||||
BAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMSAw
|
||||
HgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzEaMBgGA1UEAwwRZWMyLmFt
|
||||
YXpvbmF3cy5jb20wIBcNMTkwNDI2MTQzMjQ3WhgPMjE5ODA5MjkxNDMyNDdaMHIx
|
||||
CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0
|
||||
dGxlMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzEaMBgGA1UEAwwR
|
||||
ZWMyLmFtYXpvbmF3cy5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALVN
|
||||
CDTZEnIeoX1SEYqq6k1BV0ZlpY5y3KnoOreCAE589TwS4MX5+8Fzd6AmACmugeBP
|
||||
Qk7Hm6b2+g/d4tWycyxLaQlcq81DB1GmXehRkZRgGeRge1ePWd1TUA0I8P/QBT7S
|
||||
gUePm/kANSFU+P7s7u1NNl+vynyi0wUUrw7/wIZTAgMBAAGjgdcwgdQwHQYDVR0O
|
||||
BBYEFILtMd+T4YgH1cgc+hVsVOV+480FMIGkBgNVHSMEgZwwgZmAFILtMd+T4YgH
|
||||
1cgc+hVsVOV+480FoXakdDByMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGlu
|
||||
Z3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEgMB4GA1UECgwXQW1hem9uIFdlYiBTZXJ2
|
||||
aWNlcyBMTEMxGjAYBgNVBAMMEWVjMi5hbWF6b25hd3MuY29tggkAyXq4hX/OokUw
|
||||
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQBhkNTBIFgWFd+ZhC/LhRUY
|
||||
4OjEiykmbEp6hlzQ79T0Tfbn5A4NYDI2icBP0+hmf6qSnIhwJF6typyd1yPK5Fqt
|
||||
NTpxxcXmUKquX+pHmIkK1LKDO8rNE84jqxrxRsfDi6by82fjVYf2pgjJW8R1FAw+
|
||||
mL5WQRFexbfB5aXhcMo0AA==
|
||||
-----END CERTIFICATE-----`,
|
||||
CapeTown: `-----BEGIN CERTIFICATE-----
|
||||
MIICNjCCAZ+gAwIBAgIJAKumfZiRrNvHMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
|
||||
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
|
||||
dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0xOTExMjcw
|
||||
NzE0MDVaGA8yMTk5MDUwMjA3MTQwNVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgT
|
||||
EFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0Ft
|
||||
YXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
|
||||
gQDFd571nUzVtke3rPyRkYfvs3jh0C0EMzzG72boyUNjnfw1+m0TeFraTLKb9T6F
|
||||
7TuB/ZEN+vmlYqr2+5Va8U8qLbPF0bRH+FdaKjhgWZdYXxGzQzU3ioy5W5ZM1VyB
|
||||
7iUsxEAlxsybC3ziPYaHI42UiTkQNahmoroNeqVyHNnBpQIDAQABMA0GCSqGSIb3
|
||||
DQEBCwUAA4GBAAJLylWyElEgOpW4B1XPyRVD4pAds8Guw2+krgqkY0HxLCdjosuH
|
||||
RytGDGN+q75aAoXzW5a7SGpxLxk6Hfv0xp3RjDHsoeP0i1d8MD3hAC5ezxS4oukK
|
||||
s5gbPOnokhKTMPXbTdRn5ZifCbWlx+bYN/mTYKvxho7b5SVg2o1La9aK
|
||||
-----END CERTIFICATE-----`,
|
||||
Milan: `-----BEGIN CERTIFICATE-----
|
||||
MIICNjCCAZ+gAwIBAgIJAOZ3GEIaDcugMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
|
||||
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
|
||||
dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0xOTEwMjQx
|
||||
NTE5MDlaGA8yMTk5MDMyOTE1MTkwOVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgT
|
||||
EFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0Ft
|
||||
YXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
|
||||
gQCjiPgW3vsXRj4JoA16WQDyoPc/eh3QBARaApJEc4nPIGoUolpAXcjFhWplo2O+
|
||||
ivgfCsc4AU9OpYdAPha3spLey/bhHPRi1JZHRNqScKP0hzsCNmKhfnZTIEQCFvsp
|
||||
DRp4zr91/WS06/flJFBYJ6JHhp0KwM81XQG59lV6kkoW7QIDAQABMA0GCSqGSIb3
|
||||
DQEBCwUAA4GBAGLLrY3P+HH6C57dYgtJkuGZGT2+rMkk2n81/abzTJvsqRqGRrWv
|
||||
XRKRXlKdM/dfiuYGokDGxiC0Mg6TYy6wvsR2qRhtXW1OtZkiHWcQCnOttz+8vpew
|
||||
wx8JGMvowtuKB1iMsbwyRqZkFYLcvH+Opfb/Aayi20/ChQLdI6M2R5VU
|
||||
-----END CERTIFICATE-----`,
|
||||
China: `-----BEGIN CERTIFICATE-----
|
||||
MIICSzCCAbQCCQCQu97teKRD4zANBgkqhkiG9w0BAQUFADBqMQswCQYDVQQGEwJV
|
||||
UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2VhdHRsZTEYMBYGA1UE
|
||||
ChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1hem9uYXdzLmNvbTAe
|
||||
Fw0xMzA4MjExMzIyNDNaFw0yMzA4MjExMzIyNDNaMGoxCzAJBgNVBAYTAlVTMRMw
|
||||
EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgwFgYDVQQKEw9B
|
||||
bWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3MuY29tMIGfMA0G
|
||||
CSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GFQ2WoBl1xZYH85INUMaTc4D30QXM6f+
|
||||
YmWZyJD9fC7Z0UlaZIKoQATqCO58KNCre+jECELYIX56Uq0lb8LRLP8tijrQ9Sp3
|
||||
qJcXiH66kH0eQ44a5YdewcFOy+CSAYDUIaB6XhTQJ2r7bd4A2vw3ybbxTOWONKdO
|
||||
WtgIe3M3iwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAHzQC5XZVeuD9GTJTsbO5AyH
|
||||
ZQvki/jfARNrD9dgBRYZzLC/NOkWG6M9wlrmks9RtdNxc53nLxKq4I2Dd73gI0yQ
|
||||
wYu9YYwmM/LMgmPlI33Rg2Ohwq4DVgT3hO170PL6Fsgiq3dMvctSImJvjWktBQaT
|
||||
bcAgaZLHGIpXPrWSA2d+
|
||||
-----END CERTIFICATE-----`,
|
||||
GovCloud: `-----BEGIN CERTIFICATE-----
|
||||
MIIDCzCCAnSgAwIBAgIJAIe9Hnq82O7UMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
|
||||
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
|
||||
dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0yMTA3MTQx
|
||||
NDI3NTdaFw0yNDA3MTMxNDI3NTdaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBX
|
||||
YXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6
|
||||
b24gV2ViIFNlcnZpY2VzIExMQzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
|
||||
qaIcGFFTx/SO1W5G91jHvyQdGP25n1Y91aXCuOOWAUTvSvNGpXrI4AXNrQF+CmIO
|
||||
C4beBASnHCx082jYudWBBl9Wiza0psYc9flrczSzVLMmN8w/c78F/95NfiQdnUQP
|
||||
pvgqcMeJo82cgHkLR7XoFWgMrZJqrcUK0gnsQcb6kakCAwEAAaOB1DCB0TALBgNV
|
||||
HQ8EBAMCB4AwHQYDVR0OBBYEFNWV53gWJz72F5B1ZVY4O/dfFYBPMIGOBgNVHSME
|
||||
gYYwgYOAFNWV53gWJz72F5B1ZVY4O/dfFYBPoWCkXjBcMQswCQYDVQQGEwJVUzEZ
|
||||
MBcGA1UECBMQV2FzaGluZ3RvbiBTdGF0ZTEQMA4GA1UEBxMHU2VhdHRsZTEgMB4G
|
||||
A1UEChMXQW1hem9uIFdlYiBTZXJ2aWNlcyBMTEOCCQCHvR56vNju1DASBgNVHRMB
|
||||
Af8ECDAGAQH/AgEAMA0GCSqGSIb3DQEBCwUAA4GBACrKjWj460GUPZCGm3/z0dIz
|
||||
M2BPuH769wcOsqfFZcMKEysSFK91tVtUb1soFwH4/Lb/T0PqNrvtEwD1Nva5k0h2
|
||||
xZhNNRmDuhOhW1K9wCcnHGRBwY5t4lYL6hNV6hcrqYwGMjTjcAjBG2yMgznSNFle
|
||||
Rwi/S3BFXISixNx9cILu
|
||||
-----END CERTIFICATE-----`,
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package awsidentity_test
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/awsidentity"
|
||||
)
|
||||
|
||||
const (
|
||||
signature = `M7rX9w1s5zK1V7hK0dsE4hTDXHHaaDuKQ9iIz/W8ZNaA2lJ/usz5YuX+ORt3luJwswl/+B7cYOkJ
|
||||
bXRMx/pEQ6vT+niLGZDC9ZZ1h9Ox4h4e4m4IisQSCUrVIzyLj+MB27/Wyy0NhXcpoZVjNEmioxF2
|
||||
HNpOR4aCwUxxOm81y98=`
|
||||
document = `{
|
||||
"accountId" : "628783029487",
|
||||
"architecture" : "x86_64",
|
||||
"availabilityZone" : "us-east-1b",
|
||||
"billingProducts" : null,
|
||||
"devpayProductCodes" : null,
|
||||
"marketplaceProductCodes" : null,
|
||||
"imageId" : "ami-0c02fb55956c7d316",
|
||||
"instanceId" : "i-076e9b91f7c420782",
|
||||
"instanceType" : "t2.micro",
|
||||
"kernelId" : null,
|
||||
"pendingTime" : "2022-03-25T20:07:16Z",
|
||||
"privateIp" : "172.31.84.238",
|
||||
"ramdiskId" : null,
|
||||
"region" : "us-east-1",
|
||||
"version" : "2017-09-30"
|
||||
}`
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("FailEmpty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := awsidentity.Validate("", "", nil)
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("FailBad", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := awsidentity.Validate(signature, "{}", nil)
|
||||
require.ErrorIs(t, err, rsa.ErrVerification)
|
||||
})
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
identity, err := awsidentity.Validate(signature, document, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, awsidentity.Other, identity.Region)
|
||||
})
|
||||
}
|
|
@ -10,6 +10,7 @@ import (
|
|||
"google.golang.org/api/idtoken"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/coderd/awsidentity"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
|
@ -24,6 +25,7 @@ type Options struct {
|
|||
Database database.Store
|
||||
Pubsub database.Pubsub
|
||||
|
||||
AWSCertificates awsidentity.Certificates
|
||||
GoogleTokenValidator *idtoken.Validator
|
||||
}
|
||||
|
||||
|
@ -135,6 +137,7 @@ func New(options *Options) (http.Handler, func()) {
|
|||
})
|
||||
r.Route("/workspaceresources", func(r chi.Router) {
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Post("/aws-instance-identity", api.postWorkspaceAuthAWSInstanceIdentity)
|
||||
r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity)
|
||||
})
|
||||
r.Route("/agent", func(r chi.Router) {
|
||||
|
|
|
@ -3,11 +3,15 @@ package coderdtest
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
|
@ -31,6 +35,7 @@ import (
|
|||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/awsidentity"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/databasefake"
|
||||
"github.com/coder/coder/coderd/database/postgres"
|
||||
|
@ -43,7 +48,8 @@ import (
|
|||
)
|
||||
|
||||
type Options struct {
|
||||
GoogleTokenValidator *idtoken.Validator
|
||||
AWSInstanceIdentity awsidentity.Certificates
|
||||
GoogleInstanceIdentity *idtoken.Validator
|
||||
}
|
||||
|
||||
// New constructs an in-memory coderd instance and returns
|
||||
|
@ -52,11 +58,11 @@ func New(t *testing.T, options *Options) *codersdk.Client {
|
|||
if options == nil {
|
||||
options = &Options{}
|
||||
}
|
||||
if options.GoogleTokenValidator == nil {
|
||||
if options.GoogleInstanceIdentity == nil {
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancelFunc)
|
||||
var err error
|
||||
options.GoogleTokenValidator, err = idtoken.NewValidator(ctx, option.WithoutAuthentication())
|
||||
options.GoogleInstanceIdentity, err = idtoken.NewValidator(ctx, option.WithoutAuthentication())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -101,7 +107,8 @@ func New(t *testing.T, options *Options) *codersdk.Client {
|
|||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
|
||||
GoogleTokenValidator: options.GoogleTokenValidator,
|
||||
AWSCertificates: options.AWSInstanceIdentity,
|
||||
GoogleTokenValidator: options.GoogleInstanceIdentity,
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
srv.Close()
|
||||
|
@ -334,6 +341,66 @@ func NewGoogleInstanceIdentity(t *testing.T, instanceID string, expired bool) (*
|
|||
})
|
||||
}
|
||||
|
||||
// NewAWSInstanceIdentity returns a metadata client and ID token validator for faking
|
||||
// instance authentication for AWS.
|
||||
func NewAWSInstanceIdentity(t *testing.T, instanceID string) (awsidentity.Certificates, *http.Client) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
document := []byte(`{"instanceId":"` + instanceID + `"}`)
|
||||
hashedDocument := sha256.Sum256(document)
|
||||
|
||||
signatureRaw, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashedDocument[:])
|
||||
require.NoError(t, err)
|
||||
signature := make([]byte, base64.StdEncoding.EncodedLen(len(signatureRaw)))
|
||||
base64.StdEncoding.Encode(signature, signatureRaw)
|
||||
|
||||
certificate, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{
|
||||
SerialNumber: big.NewInt(2022),
|
||||
}, &x509.Certificate{}, &privateKey.PublicKey, privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
certificatePEM := bytes.Buffer{}
|
||||
err = pem.Encode(&certificatePEM, &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certificate,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return awsidentity.Certificates{
|
||||
awsidentity.Other: certificatePEM.String(),
|
||||
}, &http.Client{
|
||||
Transport: roundTripper(func(r *http.Request) (*http.Response, error) {
|
||||
// Only handle metadata server requests.
|
||||
if r.URL.Host != "169.254.169.254" {
|
||||
return http.DefaultTransport.RoundTrip(r)
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/latest/api/token":
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("faketoken"))),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
case "/latest/dynamic/instance-identity/signature":
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(signature)),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
case "/latest/dynamic/instance-identity/document":
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(document)),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
default:
|
||||
panic("unhandled route: " + r.URL.Path)
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func randomUsername() string {
|
||||
return strings.ReplaceAll(namesgenerator.GetRandomName(0), "_", "-")
|
||||
}
|
||||
|
|
|
@ -24,5 +24,6 @@ func TestNew(t *testing.T) {
|
|||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
|
||||
_, _ = coderdtest.NewGoogleInstanceIdentity(t, "example", false)
|
||||
_, _ = coderdtest.NewAWSInstanceIdentity(t, "an-instance")
|
||||
closer.Close()
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/coder/coder/coderd/awsidentity"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/codersdk"
|
||||
|
@ -16,6 +17,24 @@ import (
|
|||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// AWS supports instance identity verification:
|
||||
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
|
||||
// Using this, we can exchange a signed instance payload for an agent token.
|
||||
func (api *api) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
|
||||
var req codersdk.AWSInstanceIdentityToken
|
||||
if !httpapi.Read(rw, r, &req) {
|
||||
return
|
||||
}
|
||||
identity, err := awsidentity.Validate(req.Signature, req.Document, api.AWSCertificates)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("validate: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
api.handleAuthInstanceID(rw, r, identity.InstanceID)
|
||||
}
|
||||
|
||||
// Google Compute Engine supports instance identity verification:
|
||||
// https://cloud.google.com/compute/docs/instances/verifying-instance-identity
|
||||
// Using this, we can exchange a signed instance payload for an agent token.
|
||||
|
@ -47,10 +66,14 @@ func (api *api) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter,
|
|||
})
|
||||
return
|
||||
}
|
||||
agent, err := api.Database.GetWorkspaceAgentByInstanceID(r.Context(), claims.Google.ComputeEngine.InstanceID)
|
||||
api.handleAuthInstanceID(rw, r, claims.Google.ComputeEngine.InstanceID)
|
||||
}
|
||||
|
||||
func (api *api) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, instanceID string) {
|
||||
agent, err := api.Database.GetWorkspaceAgentByInstanceID(r.Context(), instanceID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("instance with id %q not found", claims.Google.ComputeEngine.InstanceID),
|
||||
Message: fmt.Sprintf("instance with id %q not found", instanceID),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -107,7 +130,7 @@ func (api *api) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter,
|
|||
}
|
||||
if latestHistory.ID.String() != resourceHistory.ID.String() {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: fmt.Sprintf("resource found for id %q, but isn't registered on the latest history", claims.Google.ComputeEngine.InstanceID),
|
||||
Message: fmt.Sprintf("resource found for id %q, but isn't registered on the latest history", instanceID),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
|
@ -13,6 +13,46 @@ import (
|
|||
"github.com/coder/coder/provisionersdk/proto"
|
||||
)
|
||||
|
||||
func TestPostWorkspaceAuthAWSInstanceIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
instanceID := "instanceidentifier"
|
||||
certificates, metadataClient := coderdtest.NewAWSInstanceIdentity(t, instanceID)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
AWSInstanceIdentity: certificates,
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
Provision: []*proto.Provision_Response{{
|
||||
Type: &proto.Provision_Response_Complete{
|
||||
Complete: &proto.Provision_Complete{
|
||||
Resources: []*proto.Resource{{
|
||||
Name: "somename",
|
||||
Type: "someinstance",
|
||||
Agent: &proto.Agent{
|
||||
Auth: &proto.Agent_InstanceId{
|
||||
InstanceId: instanceID,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
})
|
||||
project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitProjectVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
client.HTTPClient = metadataClient
|
||||
_, err := client.AuthWorkspaceAWSInstanceIdentity(context.Background())
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Expired", func(t *testing.T) {
|
||||
|
@ -20,7 +60,7 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
|
|||
instanceID := "instanceidentifier"
|
||||
validator, metadata := coderdtest.NewGoogleInstanceIdentity(t, instanceID, true)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
GoogleTokenValidator: validator,
|
||||
GoogleInstanceIdentity: validator,
|
||||
})
|
||||
_, err := client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", metadata)
|
||||
var apiErr *codersdk.Error
|
||||
|
@ -33,7 +73,7 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
|
|||
instanceID := "instanceidentifier"
|
||||
validator, metadata := coderdtest.NewGoogleInstanceIdentity(t, instanceID, false)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
GoogleTokenValidator: validator,
|
||||
GoogleInstanceIdentity: validator,
|
||||
})
|
||||
_, err := client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", metadata)
|
||||
var apiErr *codersdk.Error
|
||||
|
@ -46,27 +86,12 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
|
|||
instanceID := "instanceidentifier"
|
||||
validator, metadata := coderdtest.NewGoogleInstanceIdentity(t, instanceID, false)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
GoogleTokenValidator: validator,
|
||||
GoogleInstanceIdentity: validator,
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionDryRun: []*proto.Provision_Response{{
|
||||
Type: &proto.Provision_Response_Complete{
|
||||
Complete: &proto.Provision_Complete{
|
||||
Resources: []*proto.Resource{{
|
||||
Name: "somename",
|
||||
Type: "someinstance",
|
||||
Agent: &proto.Agent{
|
||||
Auth: &proto.Agent_InstanceId{
|
||||
InstanceId: "",
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Provision: []*proto.Provision_Response{{
|
||||
Type: &proto.Provision_Response_Complete{
|
||||
Complete: &proto.Provision_Complete{
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
|
@ -14,6 +15,11 @@ type GoogleInstanceIdentityToken struct {
|
|||
JSONWebToken string `json:"json_web_token" validate:"required"`
|
||||
}
|
||||
|
||||
type AWSInstanceIdentityToken struct {
|
||||
Signature string `json:"signature" validate:"required"`
|
||||
Document string `json:"document" validate:"required"`
|
||||
}
|
||||
|
||||
// WorkspaceAgentAuthenticateResponse is returned when an instance ID
|
||||
// has been exchanged for a session token.
|
||||
type WorkspaceAgentAuthenticateResponse struct {
|
||||
|
@ -50,3 +56,68 @@ func (c *Client) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, servic
|
|||
var resp WorkspaceAgentAuthenticateResponse
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// AuthWorkspaceAWSInstanceIdentity uses the Amazon Metadata API to
|
||||
// fetch a signed payload, and exchange it for a session token for a workspace agent.
|
||||
//
|
||||
// The requesting instance must be registered as a resource in the latest history for a workspace.
|
||||
func (c *Client) AuthWorkspaceAWSInstanceIdentity(ctx context.Context) (WorkspaceAgentAuthenticateResponse, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, "http://169.254.169.254/latest/api/token", nil)
|
||||
if err != nil {
|
||||
return WorkspaceAgentAuthenticateResponse{}, nil
|
||||
}
|
||||
req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "21600")
|
||||
res, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return WorkspaceAgentAuthenticateResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
token, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("read token: %w", err)
|
||||
}
|
||||
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.169.254/latest/dynamic/instance-identity/signature", nil)
|
||||
if err != nil {
|
||||
return WorkspaceAgentAuthenticateResponse{}, nil
|
||||
}
|
||||
req.Header.Set("X-aws-ec2-metadata-token", string(token))
|
||||
res, err = c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return WorkspaceAgentAuthenticateResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
signature, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("read token: %w", err)
|
||||
}
|
||||
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.169.254/latest/dynamic/instance-identity/document", nil)
|
||||
if err != nil {
|
||||
return WorkspaceAgentAuthenticateResponse{}, nil
|
||||
}
|
||||
req.Header.Set("X-aws-ec2-metadata-token", string(token))
|
||||
res, err = c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return WorkspaceAgentAuthenticateResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
document, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("read token: %w", err)
|
||||
}
|
||||
|
||||
res, err = c.request(ctx, http.MethodPost, "/api/v2/workspaceresources/auth/aws-instance-identity", AWSInstanceIdentityToken{
|
||||
Signature: string(signature),
|
||||
Document: string(document),
|
||||
})
|
||||
if err != nil {
|
||||
return WorkspaceAgentAuthenticateResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return WorkspaceAgentAuthenticateResponse{}, readBodyAsError(res)
|
||||
}
|
||||
var resp WorkspaceAgentAuthenticateResponse
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
|
|
@ -176,7 +176,11 @@ func TestProvisionerd(t *testing.T) {
|
|||
}, nil
|
||||
},
|
||||
updateJob: func(ctx context.Context, update *proto.UpdateJobRequest) (*proto.UpdateJobResponse, error) {
|
||||
close(completeChan)
|
||||
select {
|
||||
case <-completeChan:
|
||||
default:
|
||||
close(completeChan)
|
||||
}
|
||||
return &proto.UpdateJobResponse{}, nil
|
||||
},
|
||||
failJob: func(ctx context.Context, job *proto.FailedJob) (*proto.Empty, error) {
|
||||
|
|
Loading…
Reference in New Issue