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

feat: support IMDS v2 default for ecs ram role #104

Merged
merged 1 commit into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ import Credential, { Config } from '@alicloud/credentials';
const config: Config = {
type: 'ecs_ram_role', // 凭证类型
roleName: 'roleName', // 账户RoleName,非必填,不填则自动获取,建议设置,可以减少请求
enableIMDSv2: true, // 开启 V2 安全访问,非必填,可以设置环境变量来开启:ALIBABA_CLOUD_ECS_IMDSV2_ENABLE=true
disableIMDSv1: true, // 禁用 V1 兜底,获取安全令牌失败则报错,可以设置环境变量来开启:ALIBABA_CLOUD_IMDSV1_DISABLE=true
}
const cred = new Credential(config);
const {
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ import Credential, { Config } from '@alicloud/credentials';
const config: Config = {
type: 'ecs_ram_role', // credential type
roleName: 'roleName', // `roleName` is optional. It will be retrieved automatically if not set. It is highly recommended to set it up to reduce requests.
enableIMDSv2: true, // `enableIMDSv2` is optional and is recommended to be turned on. It can be replaced by setting environment variable: ALIBABA_CLOUD_ECS_IMDSV2_ENABLE
disableIMDSv1: true, // `disableIMDSv1` is optional and is recommended to be turned on. It can be replaced by setting environment variable: ALIBABA_CLOUD_IMDSV1_DISABLE
}
const cred = new Credential(config);
const {
Expand Down
2 changes: 1 addition & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export default class Credential implements ICredential {
case 'ecs_ram_role':
this.credential = new InnerCredentialsClient('ecs_ram_role', ECSRAMRoleCredentialsProvider.builder()
.withRoleName(config.roleName)
.withEnableIMDSv2(config.enableIMDSv2)
.withDisableIMDSv1(config.disableIMDSv1)
.build());
break;
case 'ram_role_arn': {
Expand Down
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default class Config extends $tea.Model {
privateKeyFile?: string;
roleName?: string;
enableIMDSv2?: boolean;
disableIMDSv1: boolean;
metadataTokenDuration?: number;
credentialsURI?: string;
oidcProviderArn: string;
Expand All @@ -35,6 +36,7 @@ export default class Config extends $tea.Model {
privateKeyFile: 'privateKeyFile',
roleName: 'roleName',
enableIMDSv2: 'enableIMDSv2',
disableIMDSv1: 'disableIMDSv1',
metadataTokenDuration: 'metadataTokenDuration',
credentialsURI: 'credentialsURI',
oidcProviderArn: 'oidcProviderArn',
Expand All @@ -58,6 +60,7 @@ export default class Config extends $tea.Model {
privateKeyFile: 'string',
roleName: 'string',
enableIMDSv2: 'boolean',
disableIMDSv1: 'boolean',
metadataTokenDuration: 'number',
credentialsURI: 'string',
oidcProviderArn: 'string',
Expand Down
3 changes: 2 additions & 1 deletion src/ecs_ram_role_credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export default class EcsRamRoleCredential extends SessionCredential implements I
const response = await httpx.request(SECURITY_CRED_TOKEN_URL, {
headers: {
'X-aliyun-ecs-metadata-token-ttl-seconds': `${this.metadataTokenDuration}`
}
},
method: "PUT"
});
if (response.statusCode !== 200) {
throw new Error(`Failed to get token from ECS Metadata Service. HttpCode=${response.statusCode}`);
Expand Down
43 changes: 24 additions & 19 deletions src/providers/ecs_ram_role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const defaultMetadataTokenDuration = 21600; // 6 hours

export default class ECSRAMRoleCredentialsProvider implements CredentialsProvider {
private readonly roleName: string
private readonly enableIMDSv2: boolean
private readonly disableIMDSv1: boolean
// for sts
private session: Session
private expirationTimestamp: number
Expand All @@ -21,7 +21,7 @@ export default class ECSRAMRoleCredentialsProvider implements CredentialsProvide

constructor(builder: ECSRAMRoleCredentialsProviderBuilder) {
this.roleName = builder.roleName;
this.enableIMDSv2 = builder.enableIMDSv2;
this.disableIMDSv1 = builder.disableIMDSv1;
}

async getCredentials(): Promise<Credentials> {
Expand Down Expand Up @@ -62,14 +62,19 @@ export default class ECSRAMRoleCredentialsProvider implements CredentialsProvide

// ConnectTimeout: 5 * time.Second,
// ReadTimeout: 5 * time.Second,

const response = await this.doRequest(request);

if (response.statusCode !== 200) {
throw new Error(`get metadata token failed with ${response.statusCode}`);
try {
const response = await this.doRequest(request);
if (response.statusCode !== 200) {
throw new Error(`get metadata token failed with ${response.statusCode}`);
}
return response.body.toString('utf8');
} catch (error) {
if (this.disableIMDSv1) {
throw error;
}
return null;
}

return response.body.toString('utf8');
}

private async getRoleName(): Promise<string> {
Expand All @@ -79,8 +84,8 @@ export default class ECSRAMRoleCredentialsProvider implements CredentialsProvide
.withHost('100.100.100.200')
.withPath('/latest/meta-data/ram/security-credentials/');

if (this.enableIMDSv2) {
const metadataToken = await this.getMetadataToken();
const metadataToken = await this.getMetadataToken();
if (metadataToken !== null) {
builder.withHeaders({
'x-aliyun-ecs-metadata-token': metadataToken
});
Expand Down Expand Up @@ -115,8 +120,8 @@ export default class ECSRAMRoleCredentialsProvider implements CredentialsProvide
// ReadTimeout: 5 * time.Second,
// Headers: map[string]string{ },

if (this.enableIMDSv2) {
const metadataToken = await this.getMetadataToken();
const metadataToken = await this.getMetadataToken();
if (metadataToken !== null) {
builder.withHeaders({
'x-aliyun-ecs-metadata-token': metadataToken
});
Expand Down Expand Up @@ -154,19 +159,19 @@ export default class ECSRAMRoleCredentialsProvider implements CredentialsProvide

class ECSRAMRoleCredentialsProviderBuilder {
roleName: string
enableIMDSv2: boolean
disableIMDSv1: boolean

constructor() {
this.enableIMDSv2 = true;
this.disableIMDSv1 = false;
}

withRoleName(roleName: string): ECSRAMRoleCredentialsProviderBuilder {
this.roleName = roleName
return this;
}

withEnableIMDSv2(enableIMDSv2: boolean): ECSRAMRoleCredentialsProviderBuilder {
this.enableIMDSv2 = enableIMDSv2
withDisableIMDSv1(disableIMDSv1: boolean): ECSRAMRoleCredentialsProviderBuilder {
this.disableIMDSv1 = disableIMDSv1
return this;
}

Expand All @@ -176,9 +181,9 @@ class ECSRAMRoleCredentialsProviderBuilder {
this.roleName = process.env.ALIBABA_CLOUD_ECS_METADATA;
}

// 允许通过环境变量强制关闭 V2
if (process.env.ALIBABA_CLOUD_IMDSV2_DISABLED === 'true') {
this.enableIMDSv2 = false;
// 允许通过环境变量强制关闭 V1
if (process.env.ALIBABA_CLOUD_IMDSV1_DISABLE === 'true') {
this.disableIMDSv1 = true;
}

return new ECSRAMRoleCredentialsProvider(this);
Expand Down
2 changes: 1 addition & 1 deletion test/ecs_ram_role_credential.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const mock = () => {
return {body: 'tem_role_name'};
}

if (url === SECURITY_CRED_TOKEN_URL) {
if (url === SECURITY_CRED_TOKEN_URL && opts.method === 'PUT') {
return {body: 'token', statusCode: 200};
}

Expand Down
85 changes: 63 additions & 22 deletions test/providers/ecs_ram_role.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,43 @@ describe('ECSRAMRoleCredentialsProvider', function () {
it('ECSRAMRoleCredentialsProvider', async function () {
let p = ECSRAMRoleCredentialsProvider.builder().build()
assert.ok(!(p as any).roleName)
assert.strictEqual((p as any).enableIMDSv2, true);
assert.strictEqual((p as any).disableIMDSv1, false);

p = ECSRAMRoleCredentialsProvider.builder()
.withRoleName('role')
.withEnableIMDSv2(false)
.withDisableIMDSv1(false)
.build()
assert.strictEqual((p as any).roleName, 'role');
assert.strictEqual((p as any).enableIMDSv2, false);
assert.strictEqual((p as any).disableIMDSv1, false);

p = ECSRAMRoleCredentialsProvider.builder()
.withRoleName('role')
.withDisableIMDSv1(true)
.build()
assert.strictEqual((p as any).roleName, 'role');
assert.strictEqual((p as any).disableIMDSv1, true);

assert.ok((p as any).needUpdateCredential());
});

it('env ALIBABA_CLOUD_IMDSV2_DISABLED should ok', async function () {
process.env.ALIBABA_CLOUD_IMDSV2_DISABLED = 'true';
it('env ALIBABA_CLOUD_IMDSV1_DISABLE should ok', async function () {
process.env.ALIBABA_CLOUD_IMDSV1_DISABLE = 'true';
let p = ECSRAMRoleCredentialsProvider.builder().build()
assert.strictEqual((p as any).enableIMDSv2, false);
p = ECSRAMRoleCredentialsProvider.builder().withEnableIMDSv2(true).build()
assert.strictEqual((p as any).enableIMDSv2, false);
p = ECSRAMRoleCredentialsProvider.builder().withEnableIMDSv2(false).build()
assert.strictEqual((p as any).enableIMDSv2, false);
delete process.env.ALIBABA_CLOUD_IMDSV2_DISABLED;
assert.strictEqual((p as any).disableIMDSv1, true);
p = ECSRAMRoleCredentialsProvider.builder().withDisableIMDSv1(true).build()
assert.strictEqual((p as any).disableIMDSv1, true);
p = ECSRAMRoleCredentialsProvider.builder().withDisableIMDSv1(false).build()
assert.strictEqual((p as any).disableIMDSv1, true);
process.env.ALIBABA_CLOUD_IMDSV1_DISABLE = 'false';
p = ECSRAMRoleCredentialsProvider.builder().withDisableIMDSv1(false).build()
assert.strictEqual((p as any).disableIMDSv1, false);
p = ECSRAMRoleCredentialsProvider.builder().withDisableIMDSv1(true).build()
assert.strictEqual((p as any).disableIMDSv1, true);
delete process.env.ALIBABA_CLOUD_IMDSV1_DISABLE;
});

it('getRoleName should ok', async function () {
let p = ECSRAMRoleCredentialsProvider.builder().withEnableIMDSv2(false).build();
let p = ECSRAMRoleCredentialsProvider.builder().withDisableIMDSv1(false).build();
// case 1: server error
(p as any).doRequest = async function () {
throw new Error('mock server error')
Expand Down Expand Up @@ -69,7 +81,7 @@ describe('ECSRAMRoleCredentialsProvider', function () {
});

it('getRoleName with metadata v2 should ok', async function () {
let p = ECSRAMRoleCredentialsProvider.builder().withEnableIMDSv2(true).build();
let p = ECSRAMRoleCredentialsProvider.builder().withDisableIMDSv1(true).build();

// case 1: get metadata token failed
(p as any).doRequest = async function () {
Expand Down Expand Up @@ -98,7 +110,7 @@ describe('ECSRAMRoleCredentialsProvider', function () {
});

it('getCredentialsInternal should ok', async function () {
let p = ECSRAMRoleCredentialsProvider.builder().withEnableIMDSv2(false).build();
let p = ECSRAMRoleCredentialsProvider.builder().withDisableIMDSv1(false).build();

// case 1: server error
(p as any).doRequest = async function () {
Expand Down Expand Up @@ -204,7 +216,7 @@ describe('ECSRAMRoleCredentialsProvider', function () {
};

try {
await (p as any).getCredentialsInternal();
await (p as any).getCredentialsInternal();
assert.fail('should not run to here');
} catch (ex) {
assert.strictEqual(ex.message, 'get sts token failed');
Expand All @@ -221,7 +233,7 @@ describe('ECSRAMRoleCredentialsProvider', function () {
withStatusCode(200)
.withBody(Buffer.from(`{"AccessKeyId":"saki","AccessKeySecret":"saks","Expiration":"2021-10-20T04:27:09Z","SecurityToken":"token","Code":"Failed"}`)).build();
};

try {
await (p as any).getCredentialsInternal();
assert.fail('should not run to here');
Expand Down Expand Up @@ -260,14 +272,14 @@ describe('ECSRAMRoleCredentialsProvider', function () {
});

it('getCredentials() with metadata V2 should ok', async function () {
let p = ECSRAMRoleCredentialsProvider.builder().withRoleName('rolename').withEnableIMDSv2(true).build();
let p = ECSRAMRoleCredentialsProvider.builder().withRoleName('rolename').withDisableIMDSv1(true).build();
// case 1: get metadata token failed
(p as any).doRequest = async function () {
throw new Error('mock server error')
};

try {
await (p as any).getCredentialsInternal();
await (p as any).getCredentialsInternal();
assert.fail('should not run to here');
} catch (ex) {
assert.strictEqual(ex.message, 'mock server error');
Expand Down Expand Up @@ -359,7 +371,7 @@ describe('ECSRAMRoleCredentialsProvider', function () {
});

it('getMetadataToken() should ok', async function () {
let p = ECSRAMRoleCredentialsProvider.builder().build();
let p = ECSRAMRoleCredentialsProvider.builder().withDisableIMDSv1(true).build();

// case 1: server error
(p as any).doRequest = async function () {
Expand All @@ -380,7 +392,7 @@ describe('ECSRAMRoleCredentialsProvider', function () {
.withBody(Buffer.from('xxx'))
.build();
};

try {
await (p as any).getMetadataToken();
assert.fail('should not run to here');
Expand All @@ -394,7 +406,36 @@ describe('ECSRAMRoleCredentialsProvider', function () {
withStatusCode(200)
.withBody(Buffer.from('tokenxxxxx')).build();
};
const metadataToken = await (p as any).getMetadataToken()
assert.strictEqual('tokenxxxxx', metadataToken)
let metadataToken = await (p as any).getMetadataToken();
assert.strictEqual('tokenxxxxx', metadataToken);

p = ECSRAMRoleCredentialsProvider.builder().build();

// case 1: server error
(p as any).doRequest = async function () {
throw new Error('mock server error')
};
metadataToken = await (p as any).getMetadataToken();
assert.ok(metadataToken === null);

// case 2: 4xx
(p as any).doRequest = async function () {
return Response.builder()
.withStatusCode(400)
.withBody(Buffer.from('xxx'))
.build();
};

metadataToken = await (p as any).getMetadataToken();
assert.ok(metadataToken === null);

// case 3: return token
(p as any).doRequest = async function () {
return Response.builder().
withStatusCode(200)
.withBody(Buffer.from('tokenxxxxx')).build();
};
metadataToken = await (p as any).getMetadataToken();
assert.strictEqual('tokenxxxxx', metadataToken);
});
});
Loading