Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not skip metadata API check by default #3960

Merged
merged 14 commits into from
May 22, 2024
6 changes: 2 additions & 4 deletions provider/cmd/pulumi-resource-aws/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,7 @@
},
"skipMetadataApiCheck": {
"type": "boolean",
"description": "Skip the AWS Metadata API check. Used for AWS API implementations that do not have a metadata api endpoint.\n",
"default": true
"description": "Skip the AWS Metadata API check. Used for AWS API implementations that do not have a metadata api endpoint.\n"
},
"skipRegionValidation": {
"type": "boolean",
Expand Down Expand Up @@ -157853,8 +157852,7 @@
},
"skipMetadataApiCheck": {
"type": "boolean",
"description": "Skip the AWS Metadata API check. Used for AWS API implementations that do not have a metadata api endpoint.\n",
"default": true
"description": "Skip the AWS Metadata API check. Used for AWS API implementations that do not have a metadata api endpoint.\n"
},
"skipRegionValidation": {
"type": "boolean",
Expand Down
1 change: 0 additions & 1 deletion provider/configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ func TestCheckConfigFastWithCustomEndpoints(t *testing.T) {
"s3UsePathStyle": "true",
"secretKey": "*",
"skipCredentialsValidation": "true",
"skipMetadataApiCheck": "true",
"skipRegionValidation": "true",
"skipRequestingAccountId": "true",
"version": "6.5.0"
Expand Down
56 changes: 55 additions & 1 deletion provider/provider_yaml_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
// Copyright 2016-2023, Pulumi Corporation. All rights reserved.
// Copyright 2016-2024, Pulumi Corporation. All rights reserved.

//go:build !go && !nodejs && !python && !dotnet
// +build !go,!nodejs,!python,!dotnet

package provider

import (
"bytes"
"context"
"fmt"
"math/rand"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/aws/aws-sdk-go-v2/config"
Expand Down Expand Up @@ -315,6 +319,56 @@ func TestRegress3674(t *testing.T) {
require.NotContainsf(t, string(state), "MyTestTag", "Expected MyTestTag to be removed")
}

// Ensure that pulumi-aws can authenticate using IMDS API when Pulumi is running in a context where that is made
// available such as an EC2 instance.
func TestIMDSAuth(t *testing.T) {
var localProviderBuild string
actual := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
expected := "linux/amd64"
cwd, err := os.Getwd()
require.NoError(t, err)
if actual == expected {
currentBinary, err := filepath.Abs(filepath.Join(cwd, "..", "bin", "pulumi-resource-aws"))
require.NoError(t, err)
t.Logf("Reusing prebuilt binary from %s to test %q", currentBinary, expected)
localProviderBuild = currentBinary
} else {
t.Logf("Cross-compiling provider-resource-aws under test to %q", expected)
localProviderBuild = filepath.Join(os.TempDir(), "pulumi-resource-aws")
ldFlags := []string{
"-X", "github.com/pulumi/pulumi-aws/provider/v6/pkg/version.Version=6.0.0-alpha.0+dev",
"-X", "github.com/hashicorp/terraform-provider-aws/version.ProviderVersion=6.0.0-alpha.0+dev",
}
args := []string{
"build", "-o", localProviderBuild,
"-ldflags", strings.Join(ldFlags, " "),
}
cmd := exec.Command("go", args...)
cmd.Dir = filepath.Join(cwd, "cmd", "pulumi-resource-aws")
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env,
fmt.Sprintf("GOOS=linux"),
fmt.Sprintf("GOARCH=amd64"),
)
var stderr, stdout bytes.Buffer
cmd.Stderr = &stderr
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
t.Logf("go %s failed\nStdout:\n%s\nStderr:\n%s\n", strings.Join(args, " "),
stdout.String(), stderr.String())
require.NoError(t, err)
}
}
t.Run("IDMSv2", func(t *testing.T) {
ptest := pulumiTest(t, filepath.Join("test-programs", "imds-auth", "imds-v2"), opttest.SkipInstall())
ptest.SetConfig("localProviderBuild", localProviderBuild)
result := ptest.Up()
t.Logf("stdout: %s", result.StdOut)
t.Logf("stderr: %s", result.StdErr)
t.Logf("commandOut: %v", result.Outputs["commandOut"].Value)
})
}

func configureS3() *s3sdk.Client {
loadOpts := []func(*config.LoadOptions) error{}
if p, ok := os.LookupEnv("AWS_PROFILE"); ok {
Expand Down
41 changes: 22 additions & 19 deletions provider/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,22 +522,23 @@ func stringValue(vars resource.PropertyMap, prop resource.PropertyKey, envs []st
}

// boolValue gets a bool value from a property map if present, else false
func boolValue(vars resource.PropertyMap, prop resource.PropertyKey, envs []string) bool {
func boolValue(vars resource.PropertyMap, prop resource.PropertyKey, envs []string) (*bool, error) {
val, ok := vars[prop]
if ok && val.IsBool() {
return val.BoolValue()
result := val.BoolValue()
return &result, nil
}
for _, env := range envs {
val, ok := os.LookupEnv(env)
if ok {
boolValue, err := strconv.ParseBool(val)
if err != nil {
return false
return nil, err
}
return boolValue
return &boolValue, nil
}
}
return false
return nil, nil
}

func arrayValue(vars resource.PropertyMap, prop resource.PropertyKey, envs []string) []string {
Expand Down Expand Up @@ -641,15 +642,16 @@ func validateCredentials(vars resource.PropertyMap, c shim.ResourceConfig) error
config.AssumeRoleWithWebIdentity = &assumeRole
}

// By default `skipMetadataApiCheck` is true for Pulumi to speed operations
// if we want to authenticate against the AWS API Metadata Service then the user
// will specify that skipMetadataApiCheck: false
// therefore, if we have skipMetadataApiCheck false, then we are enabling the imds client
config.EC2MetadataServiceEnableState = imds.ClientDisabled
skipMetadataApiCheck := boolValue(vars, "skipMetadataApiCheck",
// Only set non-default EC2MetadataServiceEnableState if requested by skipMetadataApiCheck.
skipMetadataApiCheck, err := boolValue(vars, "skipMetadataApiCheck",
[]string{"AWS_SKIP_METADATA_API_CHECK"})
if !skipMetadataApiCheck {
config.EC2MetadataServiceEnableState = imds.ClientEnabled
contract.AssertNoErrorf(err, "Failed to parse skipMetadataApiCheck configuration")
if skipMetadataApiCheck != nil {
if !*skipMetadataApiCheck {
config.EC2MetadataServiceEnableState = imds.ClientEnabled
} else {
config.EC2MetadataServiceEnableState = imds.ClientDisabled
}
}

// lastly let's set the sharedCreds and sharedConfig file. If these are not found then let's default to the
Expand Down Expand Up @@ -751,17 +753,21 @@ func validateCredentials(vars resource.PropertyMap, c shim.ResourceConfig) error
// before passing control to the TF provider to ensure we can report actionable errors.
func preConfigureCallback(alreadyRun *atomic.Bool) func(vars resource.PropertyMap, c shim.ResourceConfig) error {
return func(vars resource.PropertyMap, c shim.ResourceConfig) error {
skipCredentialsValidation := boolValue(vars, "skipCredentialsValidation",
var err error
skipCredentialsValidation, err := boolValue(vars, "skipCredentialsValidation",
[]string{"AWS_SKIP_CREDENTIALS_VALIDATION"})

if err != nil {
return err
}

// if we skipCredentialsValidation then we don't need to do anything in
// preConfigureCallback as this is an explicit operation
if skipCredentialsValidation {
if skipCredentialsValidation != nil && *skipCredentialsValidation {
log.Printf("[INFO] pulumi-aws: skip credentials validation")
return nil
}

var err error
if alreadyRun.CompareAndSwap(false, true) {
log.Printf("[INFO] pulumi-aws: starting to validate credentials. " +
"Disable this by AWS_SKIP_CREDENTIALS_VALIDATION or " +
Expand Down Expand Up @@ -866,9 +872,6 @@ func ProviderFromMeta(metaInfo *tfbridge.MetadataInfo) *tfbridge.ProviderInfo {
},
"skip_metadata_api_check": {
Type: "boolean",
Default: &tfbridge.DefaultInfo{
Value: true,
},
},
"access_key": {
Secret: tfbridge.True(),
Expand Down
193 changes: 193 additions & 0 deletions provider/test-programs/imds-auth/imds-v2/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
name: imds-v2
runtime: yaml
description: Test the ability of pulumi-aws to authenticate on an EC2 instance with IMDSv2 enabled

backend:
url: file://./pulumi-state

config:
localProviderBuild:
type: string

pulumi:tags:
value:
pulumi:template: aws-yaml

variables:
ec2ami:
fn::invoke:
function: aws:ec2:getAmi
arguments:
filters:
- name: name
values: ["al2023*x86_64*"]
owners:
- amazon
mostRecent: true
return: id

instanceType: t2.medium
policyArn: "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess" # example policy

resources:

segroup:
type: aws:ec2:SecurityGroup
properties:
ingress:
- protocol: tcp
fromPort: 80
toPort: 80
cidrBlocks: ["0.0.0.0/0"]
- protocol: tcp
fromPort: 22
toPort: 22
cidrBlocks: ["0.0.0.0/0"]
egress:
- fromPort: 0
toPort: 0
protocol: '-1'
cidrBlocks:
- 0.0.0.0/0
ipv6CidrBlocks:
- ::/0

priv-key:
type: tls:PrivateKey
properties:
algorithm: RSA
rsaBits: 2048

key-pair:
type: aws:ec2/keyPair:KeyPair
properties:
publicKey: ${priv-key.publicKeyOpenssh}

my-role:
type: aws:iam/role:Role
properties:
assumeRolePolicy: |
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {"Service": "ec2.amazonaws.com"},
"Effect": "Allow",
"Sid": ""
}
]
}

my-role-policy-attachment:
type: aws:iam/rolePolicyAttachment:RolePolicyAttachment
properties:
role: ${my-role.name}
policyArn: ${policyArn}

my-instance-profile:
type: aws:iam/instanceProfile:InstanceProfile
properties:
role: ${my-role.name}

inst:
type: aws:ec2/instance:Instance
properties:
ami: ${ec2ami}
instanceType: ${instanceType}
iamInstanceProfile: ${my-instance-profile.name}
keyName: ${key-pair.keyName}
# Enable and enforce IMDSv2
metadataOptions:
httpTokens: required
httpEndpoint: enabled
httpPutResponseHopLimit: 1
vpcSecurityGroupIds:
- ${segroup}
userData: |
#!/bin/bash
# Reconfigure SSHD - workaround for pulumi Command issues
cat /etc/ssh/ssh_config >/tmp/sshd_config
echo "AcceptEnv PULUMI_COMMAND_STDOUT" >> /tmp/sshd_config
echo "AcceptEnv PULUMI_COMMAND_STDERR" >> /tmp/sshd_config
sudo cp /tmp/sshd_config /etc/ssh/sshd_config || echo "FAILED to set sshd_config"
rm /tmp/sshd_config

file-copy:
type: command:remote:CopyFile
properties:
connection:
host: ${inst.publicIp}
user: ec2-user # The default user for Amazon Linux AMI
privateKey: ${priv-key.privateKeyOpenssh}
localPath: remote-program/Pulumi.yaml
remotePath: "/tmp/Pulumi.yaml"
options:
ignoreChanges:
- connection

provider-copy:
type: command:remote:CopyFile
properties:
connection:
host: ${inst.publicIp}
user: ec2-user # The default user for Amazon Linux AMI
privateKey: ${priv-key.privateKeyOpenssh}
localPath: ${localProviderBuild}
remotePath: "/tmp/pulumi-resource-aws"
options:
ignoreChanges:
- connection

install-cmd:
type: command:remote:Command
properties:
create: |
echo "========"
curl -fsSL https://get.pulumi.com | sh
export PATH="/home/ec2-user/.pulumi/bin:$PATH"
echo "========"
pulumi version
echo "========"
connection:
host: ${inst.publicIp}
user: ec2-user # The default user for Amazon Linux AMI
privateKey: ${priv-key.privateKeyOpenssh}
options:
ignoreChanges:
- connection
dependsOn:
- ${file-copy}

init-cmd:
type: command:remote:Command
properties:
create: |
cd /tmp
mkdir ./pulumi-state
export PULUMI_CONFIG_PASSPHRASE=123456
export PATH="/tmp:$PATH"
export PATH="/home/ec2-user/.pulumi/bin:$PATH"
chmod a+x /tmp/pulumi-resource-aws
pulumi version # ensure in PATH
pulumi-resource-aws --help # ensure in PATH
pulumi stack init dev
pulumi stack select dev
pulumi config
pulumi preview
# SSH connection details to the remote machine
connection:
host: ${inst.publicIp}
user: ec2-user # The default user for Amazon Linux AMI
privateKey: ${priv-key.privateKeyOpenssh}
options:
ignoreChanges:
- connection
dependsOn:
- ${install-cmd}
- ${provider-copy}

outputs:
instanceId: ${inst.id}
publicIp: ${inst.publicIp}
commandOut: ${init-cmd.stdout}
Loading
Loading