Skip to content

Commit

Permalink
feat: support IMDSv2 for ECS metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
yndu13 committed May 10, 2024
1 parent 906a5a5 commit fbff7e8
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 9 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"nyc": "^15.1.0",
"rewire": "^7.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
"typescript": "^3.7.5"
},
"dependencies": {
"@alicloud/tea-typescript": "^1.5.3",
Expand Down
2 changes: 1 addition & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export default class Credential implements ICredential {
this.credential = new StsTokenCredential(config.accessKeyId, config.accessKeySecret, config.securityToken);
break;
case 'ecs_ram_role':
this.credential = new EcsRamRoleCredential(config.roleName);
this.credential = new EcsRamRoleCredential(config.roleName, runtime, config.enableIMDSv2, config.metadataTokenDuration);
break;
case 'ram_role_arn':
this.credential = new RamRoleArnCredential(config, runtime);
Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export default class Config extends $tea.Model {
publicKeyId?: string;
privateKeyFile?: string;
roleName?: string;
enableIMDSv2?: boolean;
metadataTokenDuration?: number;
credentialsURI?: string;
oidcProviderArn: string;
oidcTokenFilePath: string;
Expand All @@ -32,6 +34,8 @@ export default class Config extends $tea.Model {
publicKeyId: 'publicKeyId',
privateKeyFile: 'privateKeyFile',
roleName: 'roleName',
enableIMDSv2: 'enableIMDSv2',
metadataTokenDuration: 'metadataTokenDuration',
credentialsURI: 'credentialsURI',
oidcProviderArn: 'oidcProviderArn',
oidcTokenFilePath: 'oidcTokenFilePath',
Expand All @@ -53,6 +57,8 @@ export default class Config extends $tea.Model {
publicKeyId: 'string',
privateKeyFile: 'string',
roleName: 'string',
enableIMDSv2: 'boolean',
metadataTokenDuration: 'number',
credentialsURI: 'string',
oidcProviderArn: 'string',
oidcTokenFilePath: 'string',
Expand Down
49 changes: 44 additions & 5 deletions src/ecs_ram_role_credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,65 @@ import ICredential from './icredential';
import Config from './config';

const SECURITY_CRED_URL = 'http://100.100.100.200/latest/meta-data/ram/security-credentials/';
const SECURITY_CRED_TOKEN_URL = 'http://100.100.100.200/latest/api/token';

export default class EcsRamRoleCredential extends SessionCredential implements ICredential {
roleName: string;
runtime: {[key: string]: any};
enableIMDSv2: boolean;
metadataTokenDuration?: number;
runtime: { [key: string]: any };
metadataToken?: string;
staleTime?: number

constructor(roleName: string = '', runtime: { [key: string]: any } = {}) {
constructor(roleName: string = '', runtime: { [key: string]: any } = {}, enableIMDSv2: boolean = false, metadataTokenDuration: number = 21600) {
const conf = new Config({
type: 'ecs_ram_role',
});
super(conf);
this.roleName = roleName;
this.enableIMDSv2 = enableIMDSv2;
this.metadataTokenDuration = metadataTokenDuration;
this.runtime = runtime;
this.sessionCredential = null;
this.metadataToken = null;
this.staleTime = 0;
}

async getBody(url: string): Promise<string> {
const response = await httpx.request(url, {});
async getBody(url: string, options: { [key: string]: any } = {}): Promise<string> {
const response = await httpx.request(url, options);
return (await httpx.read(response, 'utf8')) as string;
}

async getMetadataToken(): Promise<string> {
if (this.needToRefresh()) {
let tmpTime = new Date().getTime() + this.metadataTokenDuration * 1000;
const response = await httpx.request(SECURITY_CRED_TOKEN_URL, {
headers: {
'X-aliyun-ecs-metadata-token-ttl-seconds': `${this.metadataTokenDuration}`
}
});
if (response.statusCode !== 200) {
throw new Error(`Failed to get token from ECS Metadata Service. HttpCode=${response.statusCode}`);
}
this.staleTime = tmpTime;
return (await httpx.read(response, 'utf8')) as string;
}
return this.metadataToken;
}

async updateCredential(): Promise<void> {
let options = {};
if (this.enableIMDSv2) {
this.metadataToken = await this.getMetadataToken();
options = {
headers: {
'X-aliyun-ecs-metadata-token': this.metadataToken
}
}
}
const roleName = await this.getRoleName();
const url = SECURITY_CRED_URL + roleName;
const body = await this.getBody(url);
const body = await this.getBody(url, options);
const json = JSON.parse(body);
this.sessionCredential = {
AccessKeyId: json.AccessKeyId,
Expand All @@ -44,4 +79,8 @@ export default class EcsRamRoleCredential extends SessionCredential implements I

return await this.getBody(SECURITY_CRED_URL);
}

needToRefresh() {
return new Date().getTime() >= this.staleTime;
}
}
3 changes: 2 additions & 1 deletion src/provider/instance_ram_role_credentials_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import EcsRamRoleCredential from '../ecs_ram_role_credential';
export default {
getCredential(): ICredential {
const roleName = process.env.ALIBABA_CLOUD_ECS_METADATA;
const enableIMDSv2 = process.env.ALIBABA_CLOUD_ECS_IMDSV2_ENABLE;
if (roleName && roleName.length) {
return new EcsRamRoleCredential(roleName);
return new EcsRamRoleCredential(roleName, {}, enableIMDSv2 && enableIMDSv2.toLowerCase() === 'true');
}

return null;
Expand Down
2 changes: 1 addition & 1 deletion test/credentials.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('OidcRoleArnCredential with correct config', function () {
try {
await cred.updateCredential();
} catch (error) {
expect(error.code).to.be('AuthenticationFail.OIDCToken.Invalid');
expect(error.code).to.be('AuthenticationFail.NoPermission');
}
});
});
43 changes: 43 additions & 0 deletions test/ecs_ram_role_credential.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import EcsRamRoleCredential from '../src/ecs_ram_role_credential';
import mm from 'mm';
import * as utils from '../src/util/utils';
const REQUEST_URL = 'http://100.100.100.200/latest/meta-data/ram/security-credentials/';
const SECURITY_CRED_TOKEN_URL = 'http://100.100.100.200/latest/api/token';
import 'mocha';
import httpx from 'httpx';

Expand All @@ -13,6 +14,10 @@ const mock = () => {
return {body: 'tem_role_name'};
}

if (url === SECURITY_CRED_TOKEN_URL) {
return {body: 'token', statusCode: 200};
}

let result = {
RequestId: '76C9056D-0E40-4ED9-A82E-D69B30E733C8',
AccessKeySecret: 'AccessKeySecret',
Expand Down Expand Up @@ -146,3 +151,41 @@ describe('EcsRamRoleCredential with no role_name', function () {
expect(credentialModel.accessKeySecret).to.be('temAccessKeySecret');
});
});

describe('EcsRamRoleCredential enable IMDSv2', function () {
const cred = new EcsRamRoleCredential('roleName', {}, true, 1000);

mock();

it('should success', async function () {
let credential = await cred.getCredential();
let id = credential.accessKeyId;
expect(id).to.be('AccessKeyId');
let secret = credential.accessKeySecret;
expect(secret).to.be('AccessKeySecret');
let token = credential.securityToken;
expect(token).to.be('SecurityToken');
let type = credential.type;
expect(type).to.be('ecs_ram_role');
});

it('should refresh credentials with sessionCredential expired', async function () {
cred.sessionCredential.Expiration = utils.timestamp(cred.sessionCredential.Expiration, -1000 * 3600);
let needRefresh = cred.needUpdateCredential();
expect(needRefresh).to.be(true);
let credential = await cred.getCredential();
let token = credential.securityToken;
expect(token).to.be('SecurityToken');
});

it('should refresh credentials with no sessionCredential', async function () {
cred.sessionCredential = null;
let needRefresh = cred.needUpdateCredential();
expect(needRefresh).to.be(true);
let credential = await cred.getCredential();
let secret = credential.accessKeySecret;
expect(secret).to.be('AccessKeySecret');
let id = credential.accessKeyId;
expect(id).to.be('AccessKeyId');
});
});
30 changes: 30 additions & 0 deletions test/instance_ram_role_credentials_provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,36 @@ describe('instanceRamRoleCredentialsProvider with env ALIBABA_CLOUD_ECS_METADATA
instanceRamRoleCredentialsProvider.getCredential();
});
});

describe('when ALIBABA_CLOUD_ECS_IMDSV2_ENABLE is true', function () {
before(function () {
mm(process.env, 'ALIBABA_CLOUD_ECS_METADATA', 'roleName');
mm(process.env, 'ALIBABA_CLOUD_ECS_IMDSV2_ENABLE', 'true');
});

after(function () {
mm.restore();
});

it('should success', async function () {
instanceRamRoleCredentialsProvider.getCredential();
});
});

describe('when ALIBABA_CLOUD_ECS_IMDSV2_ENABLE is false', function () {
before(function () {
mm(process.env, 'ALIBABA_CLOUD_ECS_METADATA', 'roleName');
mm(process.env, 'ALIBABA_CLOUD_ECS_IMDSV2_ENABLE', 'false');
});

after(function () {
mm.restore();
});

it('should success', async function () {
instanceRamRoleCredentialsProvider.getCredential();
});
});
});

describe('instanceRamRoleCredentialsProvider with no env ALIBABA_CLOUD_ECS_METADATA', function () {
Expand Down

0 comments on commit fbff7e8

Please sign in to comment.