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;
|
namespace: string;
|
||||||
name: 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
|
identityPermissionMetadata?: Record<string, unknown>; // filled by permission service
|
||||||
assumedPrivilegeDetails?: { requesterId: string; actorId: string; actorType: ActorType; projectId: string };
|
assumedPrivilegeDetails?: { requesterId: string; actorId: string; actorType: ActorType; projectId: string };
|
||||||
|
|||||||
@@ -162,6 +162,12 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
|||||||
kubernetes: token?.identityAuth?.kubernetes
|
kubernetes: token?.identityAuth?.kubernetes
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (token?.identityAuth?.aws) {
|
||||||
|
requestContext.set("identityAuthInfo", {
|
||||||
|
identityId: identity.identityId,
|
||||||
|
aws: token?.identityAuth?.aws
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AuthMode.SERVICE_TOKEN: {
|
case AuthMode.SERVICE_TOKEN: {
|
||||||
|
|||||||
@@ -15,5 +15,16 @@ export type TIdentityAccessTokenJwtPayload = {
|
|||||||
namespace: string;
|
namespace: string;
|
||||||
name: 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:
|
* Extracts the identity ARN from the GetCallerIdentity response to one of the following formats:
|
||||||
* - arn:aws:iam::123456789012:user/MyUserName
|
* - arn:aws:iam::123456789012:user/MyUserName
|
||||||
* - arn:aws:iam::123456789012:role/MyRoleName
|
* - arn:aws:iam::123456789012:role/MyRoleName
|
||||||
*/
|
*/
|
||||||
export const extractPrincipalArn = (arn: string) => {
|
export const extractPrincipalArn = (arn: string) => {
|
||||||
// split the ARN into parts using ":" as the delimiter
|
const entity = extractPrincipalArnEntity(arn);
|
||||||
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}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `arn:aws:iam::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`;
|
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 { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
|
||||||
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
|
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
|
||||||
import { TIdentityAwsAuthDALFactory } from "./identity-aws-auth-dal";
|
import { TIdentityAwsAuthDALFactory } from "./identity-aws-auth-dal";
|
||||||
import { extractPrincipalArn } from "./identity-aws-auth-fns";
|
import { extractPrincipalArn, extractPrincipalArnEntity } from "./identity-aws-auth-fns";
|
||||||
import {
|
import {
|
||||||
TAttachAwsAuthDTO,
|
TAttachAwsAuthDTO,
|
||||||
TAwsGetCallerIdentityHeaders,
|
TAwsGetCallerIdentityHeaders,
|
||||||
@@ -107,7 +107,7 @@ export const identityAwsAuthServiceFactory = ({
|
|||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
GetCallerIdentityResponse: {
|
GetCallerIdentityResponse: {
|
||||||
GetCallerIdentityResult: { Account, Arn }
|
GetCallerIdentityResult: { Account, Arn, UserId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}: { data: TGetCallerIdentityResponse } = await axios({
|
}: { data: TGetCallerIdentityResponse } = await axios({
|
||||||
@@ -168,11 +168,25 @@ export const identityAwsAuthServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
const splitArn = extractPrincipalArnEntity(Arn);
|
||||||
const accessToken = crypto.jwt().sign(
|
const accessToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
identityId: identityAwsAuth.identityId,
|
identityId: identityAwsAuth.identityId,
|
||||||
identityAccessTokenId: identityAccessToken.id,
|
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,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
// 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>
|
<Tabs>
|
||||||
<Tab title="OIDC Login Attributes">
|
<Tab title="OIDC Login Attributes">
|
||||||
1. Navigate to the Identity Authentication settings and select the OIDC Auth Method.
|
1. Navigate to the Identity Authentication settings and select the OIDC Auth Method.
|
||||||
2. In the **Advanced section**, locate the Claim Mapping configuration.
|
2. In the **Advanced section**, locate the Claim Mapping configuration.
|
||||||
3. Map the OIDC claims to permission attributes by specifying:
|
3. Map the OIDC claims to permission attributes by specifying:
|
||||||
- **Attribute Name:** The identifier to be used in your policies (e.g., department).
|
- **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).
|
- **Claim Path:** The dot notation path to the claim in the OIDC token (e.g., user.department).
|
||||||
|
|
||||||
For example, if your OIDC provider returns:
|
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>
|
||||||
<Tab title="Kubernetes Login Attributes">
|
<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 }}
|
{{ 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" />
|
<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>
|
||||||
<Tab title="Other Authentication Method Attributes">
|
<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>
|
</Tab>
|
||||||
</Tabs>
|
</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