Using SPIFFE and X.509-SVID with Amazon VPC Lattice for authentication and authorization

11 minute read
Content level: Intermediate
0

This article shows a step by step example of how to use SPIFFE-SVID with VPC Lattice for AuthN/Z

Co-authored with Mohak Kohli, Sr. SDE, VPC Lattice

Introduction

As applications grow more distributed, managing how services identify and trust each other becomes more challenging. Amazon VPC Lattice helps with routing and access control across services, but it still needs a way to verify the identity of a client. That’s where SPIFFE comes in. It defines a standard way to issue strong, workload-based identities that do not rely on IPs or shared secrets. In this post, we show how you can use SPIFFE-issued identities with VPC Lattice to control which services can talk to each other. We’ll walk through how to plug SPIRE (SPIFFE Runtime Identity) into your environment, map identities to Lattice policies, and build a setup that’s simple, scalable, and secure.

SPIFFE

SPIFFE (Secure Production Identity Framework for Everyone) is a set of open-source standards for securely identifying software systems in dynamic and heterogeneous environments. And today, many systems have already adopted SPIFFE for their workloads running in Kubernetes platforms or on EC2. The workload can obtain a SPIFFE ID which is a string that uniquely and specifically identifies the workload. Workload can authenticate itself with other systems through SVID - SPIFFE Verifiable Identity Document.

There are 2 forms of SVID:

  • X.509-SVID
  • JWT-SVID

Here is an example of X.509-SVID, workload identity

Certificate:
  Data:
    Version: 3 (0x2)
    Serial Number:
      ...
  Signature Algorithm: ecdsa-with-SHA256
    Issuer: C=US, O=SPIFFE
    Subject: C=us, O=SPIFFE
  X509v3 extensions:
    ...
    X509v3 Subject Alternative Name:
      URI:spiffe://example.org/service/orders    

Check out the SPIFFE documentation for details on the SPIFFE-SVID X509: https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md

Amazon VPC Lattice auth policies

VPC Lattice auth policies are IAM policy documents that you attach to service networks or services to control whether a specified principal has access to a group of services or specific service. You can attach one auth policy to each service network or service that you want to control access to. Auth policies are different from IAM identity-based policies. IAM identity-based policies are attached to IAM users, groups, or roles and define what actions those identities can do on which resources. Auth policies are attached to services and service networks. VPC Lattice auth policies are specified using the same syntax as IAM policies. For more information, see Identity-based policies and resource-based policies in the IAM User Guide.

An auth policy contains the following elements:

  • Principal – The person or application who is allowed access to the actions and resources in the statement. In an auth policy, the principal is the IAM entity who is the recipient of this permission. The principal is authenticated as an IAM entity to make requests to a specific resource, or group of resources as in the case of services in a service network. You must specify a principal in a resource-based policy. Principals can include accounts, users, roles, federated users, or AWS services. For more information, see AWS JSON policy elements: Principal in the IAM User Guide.
  • Effect – The effect when the specified principal requests the specific action. This can be either Allow or Deny. By default, when you enable access control on a service or service network using IAM, principals have no permissions to make requests to the service or service network.
  • Actions – The specific API action for which you are granting or denying permission. VPC Lattice supports actions that use the vpc-lattice-svcs prefix. For more information, see Actions defined by Amazon VPC Lattice Services in the Service Authorization Reference.
  • Resources – The services that are affected by the action.
  • Condition – Conditions are optional. You can use them to control when your policy is in effect. For more information, see Condition keys for Amazon VPC Lattice Services in the Service Authorization Reference.

For more information, see Control access to VPC Lattice services using auth policies in the VPC Lattice User Guide.

Using X.509-SVID and AWS IAM Roles Anywhere

AWS IAM Roles Anywhere allow your workloads to get temporary AWS credentials using X.509 Certificates issued by your own Public Key Infrastructure (PKI).

Here are the high-level steps:

  1. SPIRE issues X.509-SVIDs to your workloads.
  2. These certificates follow the SPIFFE spec and include identities like: spiffe://example.org/service/
  3. Export your SPIFFE CA (the CA key pair used by SPIRE) and import it into AWS ACM as a trusted source.
  4. Create a trust anchor in IAM Roles Anywhere pointing to that ACM CA.
  5. Create IAM Role and policy that allows VPC Lattice services invocation: vpc-lattice:InvokeService.
  6. Create IAM Roles Anywhere profile that maps SPIFFE identity SANs (Subject Alternative Names) to the Role.
  7. Your workloads call AWS STS via IAM Roles Anywhere and present their SPIFFE X.509-SVID. If it matches the trust policy, they get temporary credentials.
  8. Using the temporary credentials, workloads sign their requests using SIGv4 and invoke Services through VPC Lattice

Setup

Let’s assume two VPC Lattice services, “Billing” and “Rates”. These services need to be securely accessed by “Orders’ and ”Reviews“ services. I use multiple accounts and VPCs just to showcase the possibility of using this setup, but they are not required.

Here is the security policy and identities setup:

VPC Lattice setup

  • “Orders” has X.509 Certificate which contains SPIFFE ID URI:spiffe://example.org/service/orders
  • “Reviews” has X.509 Certificate which contains SPIFFE ID URI:spiffe://example.org/service/reviews
  • VPC Lattice Auth policy has the following rules:
    • workload “Orders” is able to access “Rates” and “Billing”
    • workload “Reviews” is only able to access “Rates”, and not “Billing”
    • other workloads not under “spiffe/example.org” cannot access “Billing” or “Rates”

Service-owner steps

Step 1: Create Lambda functions representing “Billing” and “Rates” services

“Billing” lambda function for VPC Lattice service “Billing” target group

For demo purpose, we create a lambda target group for “Billing” service. So, when client invoke “Billing” service using path “/lambda”, the following function is called. Recently, we have enhanced VPC Lattice to pass along caller’s identity to backend, such as the workload X.509-SVID. In the lambda use case, all of them are encoded in the lambda event structure

import json

def lambda_handler(event, context):
    
    response = {
      "statusCode": 200,
      "statusDescription": "200 OK",
      "isBase64Encoded": False,
      "headers": {
        "Content-Type": "text/html; charset=utf-8",
        "Server": "Lambda"
      }
    }
    response['body'] = json.dumps({
        "message": "HelloWorld from Billing Lambda",
        "event": event,
    })
 
    return response

“Rates” Lambda Function for Lattice “Rates” Service

We also created a lambda target group for “Rates” VPC Lattice service. So, when a client invokes “Rates” service using path “/lambda”, the following function is called.

import json

def lambda_handler(event, context):
  
    response = {
      "statusCode": 200,
      "statusDescription": "200 OK",
      "isBase64Encoded": False,
      "headers": {
        "Content-Type": "text/html; charset=utf-8",
        "Server": "Lambda"
      }
    }
    response['body'] = json.dumps({
        "message": "HelloWorld from Rates Lambda",
        "event": event,
    })
    
    return response

Step 2: Create VPC Lattice services “Billing” and “Rates”, with the respective Lambda functions as targets

VPC Lattice service “Billing”

Here is the VPC Lattice service definition for “Billing”

Enter image description here

VPC Lattice service “Rates”

Here is the VPC Lattice service definition for “Rates”

Enter image description here

Step 3: Configure VPC Lattice Auth Policies for “Billing” and “Rates“

For this post we are using VPC Lattice service-level Auth policies. You can also configure these at the VPC Lattice service network level, if you want.

Billing can only be accessed by “Orders”

Billing VPC Lattice Auth Policy

{
    "Statement": {
        "Effect": "Allow",
        "Principal": "*",
        "Resource": "*",
        "Condition": {
            "StringEquals": {
                "aws: PrincipalTag/x509SAN/URI": "spiffe://example.org/service/orders",
                "aws: PrincipalARN": "arn:aws:iam::ACCOUNT-ID:role/spiffe-workload-roleanywhere"
            }
        },
        "Action": "vpc-lattice-svcs:Invoke"
    }
}

Rates can be accessed by Orders and Reviews

Rates VPC Lattice Auth Policy

{
    "Statement": {
        "Effect": "Allow",
        "Principal": "*",
        "Resource": "*",
        "Condition": {
            "StringEquals": {
                "aws: PrincipalTag/x509SAN/URI": [
                        "spiffe://example.org/service/orders",
                        "spiffe://example.org/service/reviews"
                ],
                "aws: PrincipalARN": "arn:aws:iam::ACCOUNT-ID:role/spiffe-workload-roleanywhere"
            }
        },
        "Action": "vpc-lattice-svcs:Invoke"
    }
}

Security-owner steps

Step 4: Create Trust Anchor Using external root CA

Create IAM anywhere Trust Anchor using an external Certificate Authority (CA):

IAM Trust Anchor

Step 5: Create and configure IAM Role for Roles Anywhere

Create an IAM Anywhere Role

Enter image description here

For permissions, we selected VPCLatticeServicesInvokeAccess

Enter image description here

Step 6: Create an IAM Anywhere Profile that maps the X509-SVID SAN (Subject Alternative Name) to the IAM Role

IAM Anywhere Profile

Step 7: Assign X509-SVID certificates to “Orders” and “Reviews”

Certificate for “Orders”

openssl x509 -text -in ./orders.pem 
Certificate:
 Data:
 Version: 3 (0x2)
 Serial Number:
 [Output omitted]
 Signature Algorithm: [Output omitted]
 Issuer: C=US, ST=WA, L=Seattle, O=Exmaple, OU=IAM, CN=Roles Anywhere CA
 Validity
 [Output omitted]
 Subject: C=US, ST=WA, L=Seattle, O=Example, OU=IAM, CN=Roles Anywhere Client
 Subject Public Key Info:
 Public Key Algorithm: id-ecPublicKey
 Public-Key: (384 bit)
 pub: 
[Output omitted]
 ASN1 OID: secp384r1
 NIST CURVE: P-384
 X509v3 extensions:
 X509v3 Subject Key Identifier: 
 [Output omitted]
 X509v3 Authority Key Identifier: 
 keyid:[Output omitted]

 X509v3 Basic Constraints: critical
 CA:FALSE
 X509v3 Key Usage: critical
 Digital Signature
 X509v3 Extended Key Usage: 
 TLS Web Client Authentication
 X509v3 Subject Alternative Name: 
 URI:spiffe/example.org/service/orders
 Signature Algorithm: ecdsa-with-SHA512
 [Output omitted]
-----BEGIN CERTIFICATE-----
[Output omitted]
-----END CERTIFICATE-----

Certificate for “Reviews“

openssl x509 -text -in ./reviews.pem 
Certificate:
 Data:
 Version: 3 (0x2)
 Serial Number:
 [Output omitted]
Signature Algorithm: [Output omitted]
 Issuer: C=US, ST=WA, L=Seattle, O=Example, OU=IAM, CN=Roles Anywhere CA
 Validity
 [Output omitted]
 Subject: C=US, ST=WA, L=Seattle, O=Example, OU=IAM, CN=Roles Anywhere Client
 Subject Public Key Info:
 Public Key Algorithm: id-ecPublicKey
 Public-Key: (384 bit)
 pub: 
 [Output omitted]
 ASN1 OID: secp384r1
 NIST CURVE: P-384
 X509v3 extensions:
 X509v3 Subject Key Identifier: 
 [Output omitted]
 X509v3 Authority Key Identifier: 
 keyid:[Output omitted]

 X509v3 Basic Constraints: critical
 CA:FALSE
 X509v3 Key Usage: critical
 Digital Signature
 X509v3 Extended Key Usage: 
 TLS Web Client Authentication
 X509v3 Subject Alternative Name: 
 URI:spiffe/example.org/service/reviews
 Signature Algorithm: ecdsa-with-SHA512
 [Output omitted]
-----BEGIN CERTIFICATE-----
[Output omitted]
-----END CERTIFICATE-----

Client-side steps

Step 8: Get temporary security credentials from IAM Roles Anywhere

To obtain temporary security credentials from AWS Identity and Access Management Roles Anywhere, use the credential helper tool that IAM Roles Anywhere provides. This tool is compatible with the credential_process feature available across the language SDKs. When used with an AWS SDK, these credentials automatically refresh before they expire, requiring no additional implementation for credential renewal. The helper manages the process of creating a signature with the certificate and calling the endpoint to obtain session credentials; it returns the credentials to the calling process in a standard JSON format. More details here.

../aws_signing_helper credential-process \
--certificate ./orders.pem \
--private-key ./client.key \
--profile-arn <ARN of the IAM ROLES ANYWHERE PROFILE YOU CREATED> \
--role-arn <ARN of the IAM ROLE FOR ROLES ANYWHERE YOU CREATED> \
--trust-anchor-arn <ARN OF TRUST ANCHOR YOU CREATED>
../aws_signing_helper credential-process \
--certificate ./reviews.pem \
--private-key ./client.key \
--profile-arn <ARN of the IAM ROLES ANYWHERE PROFILE YOU CREATED> \
--role-arn <ARN of the IAM ROLE FOR ROLES ANYWHERE YOU CREATED> \
--trust-anchor-arn <ARN OF TRUST ANCHOR YOU CREATED>

Step 9: Test access

Note: I use some basic client code to sign requests using SIGv4. You can find examples here: https://github.com/aws-samples/sigv4-signing-examples

Request from Orders to Billing - AUTHORIZED

./sigv4_client --http-method GET \
--lattice-dns http://billing-default-038ac14ba[Output omitted]6.7d67968.vpc-lattice-svcs.us-west-2.on.aws/lambda

# Response from Billing Lambda 
############## response ####################
{
...
 "method": "GET",
 "path": "/lambda",
 "requestContext": {
 "identity": {
 "principal": "arn:aws:sts::[Output omitted]:assumed-role/spiffe-workload-roleanywhere/[Output omitted]",
 "sessionName": "[Output omitted]",
 "sourceVpcArn": "arn:aws:ec2:us-west-2:[Output omitted]:vpc/vpc-05c7[Output omitted]",
 "type": "AWS_IAM",
 "x509IssuerOu": "IAM",
 "x509SanUri": "spiffe://example.org/service/orders",
 "x509SubjectCn": "Roles Anywhere Client"
 },
 "region": "us-west-2",
 "serviceArn": "arn:aws:vpc-lattice:us-west-2:[Output omitted]:service/svc-038a[Output omitted]",
 "serviceNetworkArn": "arn:aws:vpc-lattice:us-west-2:[Output omitted]:servicenetwork/sn-0df021[Output omitted]",
 "targetGroupArn": "arn:aws:vpc-lattice:us-west-2:[Output omitted]:targetgroup/tg-0b3f71[Output omitted]",
 "timeEpoch": "[Output omitted]"
 },
 "version": "2.0"
 },
 "message": "HelloWorld from Billing Lambda"
}

Request from Orders to Rates - AUTHORIZED

./sigv4_client --http-method GET \
 --lattice-dns http://rates-default-02a0f6dbe800130d5.7d67968.vpc-lattice-svcs.us-west-2.on.aws/lambda
 
# Response from Rates Lambda
{
... 
"method": "GET",
 "path": "/lambda",
 "requestContext": {
 "identity": {
 "principal": "arn:aws:sts::[Output omitted]:assumed-role/spiffe-workload-roleanywhere/[Output omitted]",
 "sessionName": "[Output omitted]",
 "sourceVpcArn": "arn:aws:ec2:us-west-2:[Output omitted]:vpc/vpc-05c7322a3[Output omitted]",
 "type": "AWS_IAM",
 "x509IssuerOu": "IAM",
 "x509SanUri": "spiffe://example.org/service/orders",
 "x509SubjectCn": "Roles Anywhere Client"
 },
 "region": "us-west-2",
 "serviceArn": "arn:aws:vpc-lattice:us-west-2:[Output omitted]:service/svc-02a0f6d[Output omitted]",
 "serviceNetworkArn": "arn:aws:vpc-lattice:us-west-2:[Output omitted]:servicenetwork/sn-0df021[Output omitted]",
 "targetGroupArn": "arn:aws:vpc-lattice:us-west-2:[Output omitted]:targetgroup/tg-06d1d[Output omitted]",
 "timeEpoch": "[Output omitted]"
 },
 "version": "2.0"
 },
 "message": "HelloWorld from Rates Lambda"
}

Request from Reviews to Rates - AUTHORIZED

./sigv4_client --http-method GET \
 --lattice-dns http://rates-default-02a0f6dbe800130d5.7d67968.vpc-lattice-svcs.us-west-2.on.aws/lambda

# response
{
 "event": {
...
 "method": "GET",
 "path": "/lambda",
 "requestContext": {
 "identity": {
 "principal": "arn:aws:sts::[Output omitted]:assumed-role/spiffe-workload-roleanywhere/00d9fa[Output omitted]",
 "sessionName": "[Output omitted]",
 "sourceVpcArn": "arn:aws:ec2:us-west-2:[Output omitted]:vpc/vpc-05c7322a[Output omitted]",
 "type": "AWS_IAM",
 "x509IssuerOu": "IAM",
 "x509SanUri": "spiffe://example.org/service/reviews",
 "x509SubjectCn": "Roles Anywhere Client"
 },
 "region": "us-west-2",
 "serviceArn": "arn:aws:vpc-lattice:us-west-2:[Output omitted]:service/svc-02a0f[Output omitted]",
 "serviceNetworkArn": "arn:aws:vpc-lattice:us-west-2:[Output omitted]:servicenetwork/sn-0df0[Output omitted]",
 "targetGroupArn": "arn:aws:vpc-lattice:us-west-2:[Output omitted]:targetgroup/tg-06d[Output omitted]",
 "timeEpoch": "[Output omitted]"
 },
 "version": "2.0"
 },
 "message": "HelloWorld from Rates Lambda"
}
#

Request from Reviews to Billing - NOT AUTHORIZED

./sigv4_client --http-method GET \
> --lattice-dns http://billing-default-038ac1[Output omitted].7d67968.vpc-lattice-svcs.us-west-2.on.aws/lambda

############## response ####################
AccessDeniedException: User: arn:aws:sts::[Output omitted]:assumed-role/spiffe-workload-roleanywhere/[Output omitted] is not authorized to perform: vpc-lattice-svcs:Invoke on resource: arn:aws:vpc-lattice:us-west-2:[Output omitted]:service/svc-038ac14[Output omitted]/lambda because no service-based policy allows the vpc-lattice-svcs:Invoke action