mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #4227 from Infisical/ENG-3345
feat(machine-identity): Add AWS attributes for ABAC
This commit is contained in:
9
backend/src/@types/fastify.d.ts
vendored
9
backend/src/@types/fastify.d.ts
vendored
@@ -126,6 +126,15 @@ declare module "@fastify/request-context" {
|
||||
namespace: string;
|
||||
name: string;
|
||||
};
|
||||
aws?: {
|
||||
accountId: string;
|
||||
arn: string;
|
||||
userId: string;
|
||||
partition: string;
|
||||
service: string;
|
||||
resourceType: string;
|
||||
resourceName: string;
|
||||
};
|
||||
};
|
||||
identityPermissionMetadata?: Record<string, unknown>; // filled by permission service
|
||||
assumedPrivilegeDetails?: { requesterId: string; actorId: string; actorType: ActorType; projectId: string };
|
||||
|
||||
@@ -162,6 +162,12 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
kubernetes: token?.identityAuth?.kubernetes
|
||||
});
|
||||
}
|
||||
if (token?.identityAuth?.aws) {
|
||||
requestContext.set("identityAuthInfo", {
|
||||
identityId: identity.identityId,
|
||||
aws: token?.identityAuth?.aws
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AuthMode.SERVICE_TOKEN: {
|
||||
|
||||
@@ -15,5 +15,16 @@ export type TIdentityAccessTokenJwtPayload = {
|
||||
namespace: string;
|
||||
name: string;
|
||||
};
|
||||
aws?: {
|
||||
accountId: string;
|
||||
arn: string;
|
||||
userId: string;
|
||||
|
||||
// Derived from ARN
|
||||
partition: string; // "aws", "aws-gov", "aws-cn"
|
||||
service: string; // "iam", "sts"
|
||||
resourceType: string; // "user" or "role"
|
||||
resourceName: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,67 +1,91 @@
|
||||
interface PrincipalArnEntity {
|
||||
Partition: string;
|
||||
Service: "iam" | "sts";
|
||||
AccountNumber: string;
|
||||
Type: "user" | "role" | "instance-profile";
|
||||
Path: string;
|
||||
FriendlyName: string;
|
||||
SessionInfo: string; // Only populated for assumed-role
|
||||
}
|
||||
|
||||
export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => {
|
||||
// split the ARN into parts using ":" as the delimiter
|
||||
const fullParts = arn.split(":");
|
||||
if (fullParts.length !== 6) {
|
||||
throw new Error(`Unrecognized ARN: "${arn}" contains ${fullParts.length} colon-separated parts, expected 6`);
|
||||
}
|
||||
const [prefix, partition, service, , accountNumber, resource] = fullParts;
|
||||
if (prefix !== "arn") {
|
||||
throw new Error(`Unrecognized ARN: "${arn}" does not begin with "arn:"`);
|
||||
}
|
||||
|
||||
// validate the service is either 'iam' or 'sts'
|
||||
if (service !== "iam" && service !== "sts") {
|
||||
throw new Error(`Unrecognized service: "${service}" in ARN "${arn}", expected "iam" or "sts"`);
|
||||
}
|
||||
|
||||
// parse the last part of the ARN which describes the resource
|
||||
const parts = resource.split("/");
|
||||
if (parts.length < 2) {
|
||||
throw new Error(
|
||||
`Unrecognized ARN: "${resource}" in ARN "${arn}" contains fewer than 2 slash-separated parts (expected type/name)`
|
||||
);
|
||||
}
|
||||
|
||||
const [rawType, ...rest] = parts;
|
||||
|
||||
let finalType: PrincipalArnEntity["Type"];
|
||||
let friendlyName: string = parts[parts.length - 1];
|
||||
let path: string = "";
|
||||
let sessionInfo: string = "";
|
||||
|
||||
// handle different types of resources
|
||||
switch (rawType) {
|
||||
case "assumed-role": {
|
||||
if (rest.length < 2) {
|
||||
throw new Error(
|
||||
`Unrecognized ARN: "${resource}" for assumed-role in ARN "${arn}" contains fewer than 3 slash-separated parts (type/roleName/sessionId)`
|
||||
);
|
||||
}
|
||||
// assumed roles use a special format where the friendly name is the role name
|
||||
const [roleName, sessionId] = rest;
|
||||
finalType = "role"; // treat assumed role case as role
|
||||
friendlyName = roleName;
|
||||
sessionInfo = sessionId;
|
||||
break;
|
||||
}
|
||||
case "user":
|
||||
case "role":
|
||||
case "instance-profile":
|
||||
finalType = rawType;
|
||||
path = rest.slice(0, -1).join("/");
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unrecognized principal type: "${rawType}" in ARN "${arn}". Expected "user", "role", "instance-profile", or "assumed-role".`
|
||||
);
|
||||
}
|
||||
|
||||
const entity: PrincipalArnEntity = {
|
||||
Partition: partition,
|
||||
Service: service,
|
||||
AccountNumber: accountNumber,
|
||||
Type: finalType,
|
||||
Path: path,
|
||||
FriendlyName: friendlyName,
|
||||
SessionInfo: sessionInfo
|
||||
};
|
||||
|
||||
return entity;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts the identity ARN from the GetCallerIdentity response to one of the following formats:
|
||||
* - arn:aws:iam::123456789012:user/MyUserName
|
||||
* - arn:aws:iam::123456789012:role/MyRoleName
|
||||
*/
|
||||
export const extractPrincipalArn = (arn: string) => {
|
||||
// split the ARN into parts using ":" as the delimiter
|
||||
const fullParts = arn.split(":");
|
||||
if (fullParts.length !== 6) {
|
||||
throw new Error(`Unrecognized ARN: contains ${fullParts.length} colon-separated parts, expected 6`);
|
||||
}
|
||||
const [prefix, partition, service, , accountNumber, resource] = fullParts;
|
||||
if (prefix !== "arn") {
|
||||
throw new Error('Unrecognized ARN: does not begin with "arn:"');
|
||||
}
|
||||
|
||||
// structure to hold the parsed data
|
||||
const entity = {
|
||||
Partition: partition,
|
||||
Service: service,
|
||||
AccountNumber: accountNumber,
|
||||
Type: "",
|
||||
Path: "",
|
||||
FriendlyName: "",
|
||||
SessionInfo: ""
|
||||
};
|
||||
|
||||
// validate the service is either 'iam' or 'sts'
|
||||
if (entity.Service !== "iam" && entity.Service !== "sts") {
|
||||
throw new Error(`Unrecognized service: ${entity.Service}, not one of iam or sts`);
|
||||
}
|
||||
|
||||
// parse the last part of the ARN which describes the resource
|
||||
const parts = resource.split("/");
|
||||
if (parts.length < 2) {
|
||||
throw new Error(`Unrecognized ARN: "${resource}" contains fewer than 2 slash-separated parts`);
|
||||
}
|
||||
|
||||
const [type, ...rest] = parts;
|
||||
entity.Type = type;
|
||||
entity.FriendlyName = parts[parts.length - 1];
|
||||
|
||||
// handle different types of resources
|
||||
switch (entity.Type) {
|
||||
case "assumed-role": {
|
||||
if (rest.length < 2) {
|
||||
throw new Error(`Unrecognized ARN: "${resource}" contains fewer than 3 slash-separated parts`);
|
||||
}
|
||||
// assumed roles use a special format where the friendly name is the role name
|
||||
const [roleName, sessionId] = rest;
|
||||
entity.Type = "role"; // treat assumed role case as role
|
||||
entity.FriendlyName = roleName;
|
||||
entity.SessionInfo = sessionId;
|
||||
break;
|
||||
}
|
||||
case "user":
|
||||
case "role":
|
||||
case "instance-profile":
|
||||
// standard cases: just join back the path if there's any
|
||||
entity.Path = rest.slice(0, -1).join("/");
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unrecognized principal type: "${entity.Type}"`);
|
||||
}
|
||||
const entity = extractPrincipalArnEntity(arn);
|
||||
|
||||
return `arn:aws:iam::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`;
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
|
||||
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
|
||||
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
|
||||
import { TIdentityAwsAuthDALFactory } from "./identity-aws-auth-dal";
|
||||
import { extractPrincipalArn } from "./identity-aws-auth-fns";
|
||||
import { extractPrincipalArn, extractPrincipalArnEntity } from "./identity-aws-auth-fns";
|
||||
import {
|
||||
TAttachAwsAuthDTO,
|
||||
TAwsGetCallerIdentityHeaders,
|
||||
@@ -107,7 +107,7 @@ export const identityAwsAuthServiceFactory = ({
|
||||
const {
|
||||
data: {
|
||||
GetCallerIdentityResponse: {
|
||||
GetCallerIdentityResult: { Account, Arn }
|
||||
GetCallerIdentityResult: { Account, Arn, UserId }
|
||||
}
|
||||
}
|
||||
}: { data: TGetCallerIdentityResponse } = await axios({
|
||||
@@ -168,11 +168,25 @@ export const identityAwsAuthServiceFactory = ({
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
const splitArn = extractPrincipalArnEntity(Arn);
|
||||
const accessToken = crypto.jwt().sign(
|
||||
{
|
||||
identityId: identityAwsAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
|
||||
identityAuth: {
|
||||
aws: {
|
||||
accountId: Account,
|
||||
arn: Arn,
|
||||
userId: UserId,
|
||||
|
||||
// Derived from ARN
|
||||
partition: splitArn.Partition,
|
||||
service: splitArn.Service,
|
||||
resourceType: splitArn.Type,
|
||||
resourceName: splitArn.FriendlyName
|
||||
}
|
||||
}
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||
|
||||
@@ -30,10 +30,10 @@ For methods like OIDC, these come as claims in the token and can be made availab
|
||||
|
||||
<Tabs>
|
||||
<Tab title="OIDC Login Attributes">
|
||||
1. Navigate to the Identity Authentication settings and select the OIDC Auth Method.
|
||||
2. In the **Advanced section**, locate the Claim Mapping configuration.
|
||||
3. Map the OIDC claims to permission attributes by specifying:
|
||||
- **Attribute Name:** The identifier to be used in your policies (e.g., department).
|
||||
1. Navigate to the Identity Authentication settings and select the OIDC Auth Method.
|
||||
2. In the **Advanced section**, locate the Claim Mapping configuration.
|
||||
3. Map the OIDC claims to permission attributes by specifying:
|
||||
- **Attribute Name:** The identifier to be used in your policies (e.g., department).
|
||||
- **Claim Path:** The dot notation path to the claim in the OIDC token (e.g., user.department).
|
||||
|
||||
For example, if your OIDC provider returns:
|
||||
@@ -64,7 +64,7 @@ For methods like OIDC, these come as claims in the token and can be made availab
|
||||
|
||||
</Tab>
|
||||
<Tab title="Kubernetes Login Attributes">
|
||||
For identities authenticated using Kubernetes, the service account's namespace and name are available in their policy and can be accessed as follows:
|
||||
For identities authenticated using Kubernetes, the service account's namespace and name are available in their policy and can be accessed as follows:
|
||||
|
||||
```
|
||||
{{ identity.auth.kubernetes.namespace }}
|
||||
@@ -72,9 +72,25 @@ For methods like OIDC, these come as claims in the token and can be made availab
|
||||
```
|
||||
|
||||
<img src="/images/platform/access-controls/abac-policy-k8s-format.png" />
|
||||
</Tab>
|
||||
<Tab title="AWS Attributes">
|
||||
For identities authenticated using AWS Auth, several attributes can be accessed. On top of the 3 base attributes, there's 4 derived from the ARN. The example below includes comments showing how each derived attribute looks like based on this ARN: `arn:aws:iam::123456789012:user/example-user`
|
||||
|
||||
```
|
||||
{{ identity.auth.aws.accountId }}
|
||||
{{ identity.auth.aws.arn }}
|
||||
{{ identity.auth.aws.userId }}
|
||||
|
||||
// Derived from ARN
|
||||
{{ identity.auth.aws.partition }} // aws
|
||||
{{ identity.auth.aws.service }} // iam
|
||||
{{ identity.auth.aws.resourceType }} // user
|
||||
{{ identity.auth.aws.resourceName }} // example-user
|
||||
```
|
||||
|
||||
<img src="/images/platform/access-controls/abac-policy-aws-format.png" />
|
||||
</Tab>
|
||||
<Tab title="Other Authentication Method Attributes">
|
||||
At the moment we only support OIDC claims. Payloads on other authentication methods are not yet accessible.
|
||||
At the moment we only support OIDC claims, Kubernetes attributes, and AWS attributes. Payloads on other authentication methods are not yet accessible.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
BIN
docs/images/platform/access-controls/abac-policy-aws-format.png
Normal file
BIN
docs/images/platform/access-controls/abac-policy-aws-format.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 819 KiB |
Reference in New Issue
Block a user