Files
spinifex/internal/stsauth/stsauth_test.go
T
Josh Siviour 93a398afb8 feat: ECS task IAM credential endpoint (sprint 4g) (#422)
Serve task-role credentials to containers the AWS-faithful way: an HTTP
endpoint at 169.254.170.2:80 inside the ecs-agent (host VM netns) serving
short-lived credentials at /v2/credentials/{credID}. Containers find it via
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, injected per container.

The agent mints credentials by SigV4-signing sts:AssumeRole(taskRoleARN)
against the gateway with its instance-role credentials, mirroring
internal/ecrauth. This reconciles ecs-v1 Q4/Q10 (STS over NATS + scheduler
JWT assertion) with the 4f rewire that moved the agent entirely onto the
gateway: the agent has no NATS connection and already holds instance creds,
so it assumes the task role directly. Credentials are cached per credID and
refreshed near expiry.

Each awsvpc task netns gets a credentials veth into the host netns and a
host-routed /32 to the endpoint, keeping it off the task ENI's VPC route.
The scheduler now persists taskRoleArn on the task definition and carries it
in the assign payload so the endpoint activates end to end.
2026-06-25 22:06:19 +10:00

64 lines
2.1 KiB
Go

package stsauth
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
// assumeRoleXML is a minimal AWS AssumeRole response the aws-sdk STS client
// decodes into Credentials.
const assumeRoleXML = `<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleResult>
<Credentials>
<AccessKeyId>ASIATESTKEY</AccessKeyId>
<SecretAccessKey>testsecret</SecretAccessKey>
<SessionToken>testtoken</SessionToken>
<Expiration>2031-01-02T03:04:05Z</Expiration>
</Credentials>
</AssumeRoleResult>
</AssumeRoleResponse>`
func TestAssumeRole_DecodesCredentials(t *testing.T) {
var gotBody string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
gotBody = string(b)
w.Header().Set("Content-Type", "text/xml")
_, _ = w.Write([]byte(assumeRoleXML))
}))
defer srv.Close()
creds, err := AssumeRole("us-east-1", srv.URL, srv.Client(),
"AKIAINSTANCE", "instancesecret", "", "arn:aws:iam::111122223333:role/task", "ecs-cred-1")
if err != nil {
t.Fatalf("AssumeRole: %v", err)
}
if creds.AccessKeyID != "ASIATESTKEY" || creds.SecretAccessKey != "testsecret" || creds.SessionToken != "testtoken" {
t.Errorf("bad creds: %+v", creds)
}
want, _ := time.Parse(time.RFC3339, "2031-01-02T03:04:05Z")
if !creds.Expiration.Equal(want) {
t.Errorf("Expiration = %v, want %v", creds.Expiration, want)
}
// The request must carry the AssumeRole action and the role ARN we asked for.
if !strings.Contains(gotBody, "Action=AssumeRole") {
t.Errorf("request not an AssumeRole call: %s", gotBody)
}
if !strings.Contains(gotBody, "role%2Ftask") && !strings.Contains(gotBody, "role/task") {
t.Errorf("request missing role ARN: %s", gotBody)
}
}
func TestAssumeRole_RequiresRoleAndSession(t *testing.T) {
if _, err := AssumeRole("us-east-1", "https://gw", http.DefaultClient, "a", "b", "", "", "sess"); err == nil {
t.Error("want error for empty roleARN")
}
if _, err := AssumeRole("us-east-1", "https://gw", http.DefaultClient, "a", "b", "", "arn:role", ""); err == nil {
t.Error("want error for empty sessionName")
}
}