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

revert(iam): oidc provider retrieves leaf certificate instead of root certificate #22805

Merged
merged 1 commit into from
Nov 7, 2022
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
8 changes: 8 additions & 0 deletions packages/@aws-cdk/aws-eks/lib/oidc-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,18 @@ export class OpenIdConnectProvider extends iam.OpenIdConnectProvider {
* @param props Initialization properties
*/
public constructor(scope: Construct, id: string, props: OpenIdConnectProviderProps) {
/**
* For some reason EKS isn't validating the root certificate but a intermediate certificate
* which is one level up in the tree. Because of the a constant thumbprint value has to be
* stated with this OpenID Connect provider. The certificate thumbprint is the same for all the regions.
*/
const thumbprints = ['9e99a48a9960b14926bb7f3b02e22da2b0ab7280'];

const clientIds = ['sts.amazonaws.com'];

super(scope, id, {
url: props.url,
thumbprints,
clientIds,
});
}
Expand Down
3 changes: 3 additions & 0 deletions packages/@aws-cdk/aws-eks/test/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2156,6 +2156,9 @@ describe('cluster', () => {
ClientIDList: [
'sts.amazonaws.com',
],
ThumbprintList: [
'9e99a48a9960b14926bb7f3b02e22da2b0ab7280',
],
Url: {
'Fn::GetAtt': [
'Cluster9EE0221C',
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export declare function arrayDiff(oldValues: string[], newValues: string[]): {
adds: string[];
deletes: string[];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export function arrayDiff(oldValues: string[], newValues: string[]) {
const deletes = new Set(oldValues);
const adds = new Set<string>();

for (const v of new Set(newValues)) {
if (deletes.has(v)) {
deletes.delete(v);
} else {
adds.add(v);
}
}

return {
adds: Array.from(adds),
deletes: Array.from(deletes),
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as aws from 'aws-sdk';
declare function defaultLogger(fmt: string, ...args: any[]): void;
/**
* Downloads the CA thumbprint from the issuer URL
*/
declare function downloadThumbprint(issuerUrl: string): Promise<string>;
export declare const external: {
downloadThumbprint: typeof downloadThumbprint;
log: typeof defaultLogger;
createOpenIDConnectProvider: (req: aws.IAM.CreateOpenIDConnectProviderRequest) => Promise<import("aws-sdk/lib/request").PromiseResult<aws.IAM.CreateOpenIDConnectProviderResponse, aws.AWSError>>;
deleteOpenIDConnectProvider: (req: aws.IAM.DeleteOpenIDConnectProviderRequest) => Promise<{
$response: aws.Response<{}, aws.AWSError>;
}>;
updateOpenIDConnectProviderThumbprint: (req: aws.IAM.UpdateOpenIDConnectProviderThumbprintRequest) => Promise<{
$response: aws.Response<{}, aws.AWSError>;
}>;
addClientIDToOpenIDConnectProvider: (req: aws.IAM.AddClientIDToOpenIDConnectProviderRequest) => Promise<{
$response: aws.Response<{}, aws.AWSError>;
}>;
removeClientIDFromOpenIDConnectProvider: (req: aws.IAM.RemoveClientIDFromOpenIDConnectProviderRequest) => Promise<{
$response: aws.Response<{}, aws.AWSError>;
}>;
};
export {};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* istanbul ignore file */

import * as tls from 'tls';
import * as url from 'url';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as aws from 'aws-sdk';

let client: aws.IAM;

function iam() {
if (!client) { client = new aws.IAM(); }
return client;
}

function defaultLogger(fmt: string, ...args: any[]) {
// eslint-disable-next-line no-console
console.log(fmt, ...args);
}

/**
* Downloads the CA thumbprint from the issuer URL
*/
async function downloadThumbprint(issuerUrl: string) {
external.log(`downloading certificate authority thumbprint for ${issuerUrl}`);
return new Promise<string>((ok, ko) => {
const purl = url.parse(issuerUrl);
const port = purl.port ? parseInt(purl.port, 10) : 443;
if (!purl.host) {
return ko(new Error(`unable to determine host from issuer url ${issuerUrl}`));
}
const socket = tls.connect(port, purl.host, { rejectUnauthorized: false, servername: purl.host });
socket.once('error', ko);
socket.once('secureConnect', () => {
const cert = socket.getPeerCertificate();
socket.end();
const thumbprint = cert.fingerprint.split(':').join('');
external.log(`certificate authority thumbprint for ${issuerUrl} is ${thumbprint}`);
ok(thumbprint);
});
});
}

// allows unit test to replace with mocks
/* eslint-disable max-len */
export const external = {
downloadThumbprint,
log: defaultLogger,
createOpenIDConnectProvider: (req: aws.IAM.CreateOpenIDConnectProviderRequest) => iam().createOpenIDConnectProvider(req).promise(),
deleteOpenIDConnectProvider: (req: aws.IAM.DeleteOpenIDConnectProviderRequest) => iam().deleteOpenIDConnectProvider(req).promise(),
updateOpenIDConnectProviderThumbprint: (req: aws.IAM.UpdateOpenIDConnectProviderThumbprintRequest) => iam().updateOpenIDConnectProviderThumbprint(req).promise(),
addClientIDToOpenIDConnectProvider: (req: aws.IAM.AddClientIDToOpenIDConnectProviderRequest) => iam().addClientIDToOpenIDConnectProvider(req).promise(),
removeClientIDFromOpenIDConnectProvider: (req: aws.IAM.RemoveClientIDFromOpenIDConnectProviderRequest) => iam().removeClientIDFromOpenIDConnectProvider(req).promise(),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export declare function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise<void | {
PhysicalResourceId: string | undefined;
}>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { arrayDiff } from './diff';
import { external } from './external';

export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) {
if (event.RequestType === 'Create') { return onCreate(event); }
if (event.RequestType === 'Update') { return onUpdate(event); }
if (event.RequestType === 'Delete') { return onDelete(event); }
throw new Error('invalid request type');
}

async function onCreate(event: AWSLambda.CloudFormationCustomResourceCreateEvent) {
const issuerUrl = event.ResourceProperties.Url;
const thumbprints: string[] = (event.ResourceProperties.ThumbprintList ?? []).sort(); // keep sorted for UPDATE
const clients: string[] = (event.ResourceProperties.ClientIDList ?? []).sort();

if (thumbprints.length === 0) {
thumbprints.push(await external.downloadThumbprint(issuerUrl));
}

const resp = await external.createOpenIDConnectProvider({
Url: issuerUrl,
ClientIDList: clients,
ThumbprintList: thumbprints,
});

return {
PhysicalResourceId: resp.OpenIDConnectProviderArn,
};
}

async function onUpdate(event: AWSLambda.CloudFormationCustomResourceUpdateEvent) {
const issuerUrl = event.ResourceProperties.Url;
const thumbprints: string[] = (event.ResourceProperties.ThumbprintList ?? []).sort(); // keep sorted for UPDATE
const clients: string[] = (event.ResourceProperties.ClientIDList ?? []).sort();

// determine which update we are talking about.
const oldIssuerUrl = event.OldResourceProperties.Url;

// if this is a URL update, then we basically create a new resource and cfn will delete the old one
// since the physical resource ID will change.
if (oldIssuerUrl !== issuerUrl) {
return onCreate({ ...event, RequestType: 'Create' });
}

const providerArn = event.PhysicalResourceId;

// if thumbprints changed, we can update in-place, but bear in mind that if the new thumbprint list
// is empty, we will grab it from the server like we do in CREATE
const oldThumbprints = (event.OldResourceProperties.ThumbprintList || []).sort();
if (JSON.stringify(oldThumbprints) !== JSON.stringify(thumbprints)) {
const thumbprintList = thumbprints.length > 0 ? thumbprints : [await external.downloadThumbprint(issuerUrl)];
external.log('updating thumbprint list from', oldThumbprints, 'to', thumbprints);
await external.updateOpenIDConnectProviderThumbprint({
OpenIDConnectProviderArn: providerArn,
ThumbprintList: thumbprintList,
});

// don't return, we might have more updates...
}

// if client ID list has changed, determine "diff" because the API is add/remove
const oldClients: string[] = (event.OldResourceProperties.ClientIDList || []).sort();
const diff = arrayDiff(oldClients, clients);
external.log(`client ID diff: ${JSON.stringify(diff)}`);

for (const addClient of diff.adds) {
external.log(`adding client id "${addClient}" to provider ${providerArn}`);
await external.addClientIDToOpenIDConnectProvider({
OpenIDConnectProviderArn: providerArn,
ClientID: addClient,
});
}

for (const deleteClient of diff.deletes) {
external.log(`removing client id "${deleteClient}" from provider ${providerArn}`);
await external.removeClientIDFromOpenIDConnectProvider({
OpenIDConnectProviderArn: providerArn,
ClientID: deleteClient,
});
}

return;
}

async function onDelete(deleteEvent: AWSLambda.CloudFormationCustomResourceDeleteEvent) {
await external.deleteOpenIDConnectProvider({
OpenIDConnectProviderArn: deleteEvent.PhysicalResourceId,
});
}
Loading