Skip to content

Commit

Permalink
Add GitHub Auth config
Browse files Browse the repository at this point in the history
Signed-off-by: Sayali Gaikawad <gaiksaya@amazon.com>
  • Loading branch information
gaiksaya committed Nov 25, 2024
1 parent a7836fa commit 93cab0a
Show file tree
Hide file tree
Showing 13 changed files with 908 additions and 249 deletions.
130 changes: 73 additions & 57 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions bin/ci-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ const ciStack = new CIStack(app, `OpenSearch-CI-${defaultEnv}`, {

const ciCdnStack = new CiCdnStack(app, `OpenSearch-CI-Cdn-${defaultEnv}`, {});
ciCdnStack.addDependency(ciStack);
ciStack.addDependency(ciConfigStack);
13 changes: 7 additions & 6 deletions lib/ci-config-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class CIConfigStack extends Stack {

static readonly REDIRECT_URL_SECRET_EXPORT_VALUE: string = 'redirectUrlSecret';

static readonly OIDC_CONFIGURATION_VALUE_SECRET_EXPORT_VALUE: string = 'OIDCConfigValueSecret';
static readonly AUTH_CONFIGURATION_VALUE_SECRET_EXPORT_VALUE: string = 'authConfigValueSecret';

static readonly CASC_RELOAD_TOKEN_SECRET_EXPORT_VALUE: string = 'casc';

Expand All @@ -43,9 +43,10 @@ export class CIConfigStack extends Stack {
const redirectUrlSecret = new Secret(this, 'redirectUrl', {
description: 'Redirect url for Jenkins',
});
const OIDCConfigValuesSecret = new Secret(this, 'OIDCConfigValues', {
description: 'OIDC params in JSON format',
const authConfigValuesSecret = new Secret(this, 'authConfigValues', {
description: 'Auth credentials in JSON format',
});

const CascReloadTokenValuesSecret = new Secret(this, 'CascReloadTokenValue', {
description: 'Reload token (password) required for configuration as code plugin',
});
Expand Down Expand Up @@ -75,9 +76,9 @@ export class CIConfigStack extends Stack {
exportName: CIConfigStack.REDIRECT_URL_SECRET_EXPORT_VALUE,
});

new CfnOutput(this, 'OIDCConfigValuesSecret', {
value: OIDCConfigValuesSecret.secretArn,
exportName: CIConfigStack.OIDC_CONFIGURATION_VALUE_SECRET_EXPORT_VALUE,
new CfnOutput(this, 'authConfigValuesSecret', {
value: authConfigValuesSecret.secretArn,
exportName: CIConfigStack.AUTH_CONFIGURATION_VALUE_SECRET_EXPORT_VALUE,
});

new CfnOutput(this, 'cascSecretValue', {
Expand Down
25 changes: 13 additions & 12 deletions lib/ci-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ import { JenkinsWAF } from './security/waf';
export interface CIStackProps extends StackProps {
/** Should the Jenkins use https */
readonly useSsl?: boolean;
/** Should an OIDC provider be installed on Jenkins. */
readonly runWithOidc?: boolean;
/** Type of login mechanism to adopt */
readonly authType?:string;
/** Restrict jenkins access to */
readonly restrictServerAccessTo?: IPeer;
/** Additional verification during deployment and resource startup. */
Expand All @@ -54,7 +54,7 @@ export interface CIStackProps extends StackProps {
readonly useProdAgents?: boolean;
}

function getServerAccess(serverAccessType: string, restrictServerAccessTo: string) : IPeer {
function getServerAccess(serverAccessType: string, restrictServerAccessTo: string): IPeer {
if (typeof restrictServerAccessTo === 'undefined') {
throw new Error('restrictServerAccessTo should be specified');
}
Expand Down Expand Up @@ -100,18 +100,19 @@ export class CIStack extends Stack {

const useSsl = useSslParameter === 'true';

const runWithOidcParameter = `${props?.runWithOidc ?? this.node.tryGetContext('runWithOidc')}`;
if (runWithOidcParameter !== 'true' && runWithOidcParameter !== 'false') {
throw new Error('runWithOidc parameter is required to be set as - true or false');
let authType = `${props?.authType ?? this.node.tryGetContext('authType')}`;
if (authType.toString() === 'undefined') {
authType = 'default';
}
if (authType !== 'default' && authType !== 'github' && authType !== 'oidc') {
throw new Error('authType parameter is required to be set as - default, github or oidc');
}

let useProdAgents = `${props?.useProdAgents ?? this.node.tryGetContext('useProdAgents')}`;
if (useProdAgents.toString() === 'undefined') {
useProdAgents = 'false';
}

const runWithOidc = runWithOidcParameter === 'true';

const serverAccessType = this.node.tryGetContext('serverAccessType');
const restrictServerAccessTo = this.node.tryGetContext('restrictServerAccessTo');
const serverAcess = props?.restrictServerAccessTo ?? getServerAccess(serverAccessType, restrictServerAccessTo);
Expand All @@ -124,7 +125,7 @@ export class CIStack extends Stack {
// Setting CfnParameters to record the value in cloudFormation
new CfnParameter(this, 'runWithOidc', {
description: 'If the jenkins instance should use OIDC + federate',
default: runWithOidc,
default: authType,
});

// Setting CfnParameters to record the value in cloudFormation
Expand All @@ -139,7 +140,7 @@ export class CIStack extends Stack {
const importedCertSecretBucketValue = Fn.importValue(`${CIConfigStack.PRIVATE_KEY_SECRET_EXPORT_VALUE}`);
const importedArnSecretBucketValue = Fn.importValue(`${CIConfigStack.CERTIFICATE_ARN_SECRET_EXPORT_VALUE}`);
const importedRedirectUrlSecretBucketValue = Fn.importValue(`${CIConfigStack.REDIRECT_URL_SECRET_EXPORT_VALUE}`);
const importedOidcConfigValuesSecretBucketValue = Fn.importValue(`${CIConfigStack.OIDC_CONFIGURATION_VALUE_SECRET_EXPORT_VALUE}`);
const importedAuthConfigValuesSecretBucketValue = Fn.importValue(`${CIConfigStack.AUTH_CONFIGURATION_VALUE_SECRET_EXPORT_VALUE}`);
const certificateArn = Secret.fromSecretCompleteArn(this, 'certificateArn', importedArnSecretBucketValue.toString());
const importedReloadPasswordSecretsArn = Fn.importValue(`${CIConfigStack.CASC_RELOAD_TOKEN_SECRET_EXPORT_VALUE}`);
const listenerCertificate = ListenerCertificate.fromArn(certificateArn.secretValue.toString());
Expand Down Expand Up @@ -186,9 +187,9 @@ export class CIStack extends Stack {
sslCertChainArn: importedContentsChainBucketValue.toString(),
sslCertPrivateKeyContentsArn: importedCertSecretBucketValue.toString(),
redirectUrlArn: importedRedirectUrlSecretBucketValue.toString(),
oidcCredArn: importedOidcConfigValuesSecretBucketValue.toString(),
authCredsSecretsArn: importedAuthConfigValuesSecretBucketValue.toString(),
useSsl,
runWithOidc,
authType,
failOnCloudInitError: props?.ignoreResourcesFailures,
adminUsers: props?.adminUsers,
agentNodeSecurityGroup: this.securityGroups.agentNodeSG.securityGroupId,
Expand Down
120 changes: 120 additions & 0 deletions lib/compute/auth-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

export class AuthConfig {
private static readonly adminRolePermissions: string[] = [
'Overall/Administer',
'Overall/Read',
'Job/Move',
'Job/Build',
'Job/Read',
'Job/Delete',
'Job/Create',
'Job/Discover',
'Job/Cancel',
'Job/Configure',
'Job Config History/DeleteEntry',
'Job/Workspace',
'Credentials/Delete',
'Credentials/ManageDomains',
'Credentials/Update',
'Credentials/View',
'Credentials/Create',
'Manage ownership/Nodes',
'Manage ownership/Jobs',
'Agent/Configure',
'Agent/Create',
'Agent/Build',
'Agent/Provision',
'Agent/Connect',
'Agent/Delete',
'Agent/Disconnect',
'Run/Replay',
'Run/Delete',
'Run/Update',
'View/Delete',
'View/Read',
'View/Create',
'View/Configure',
'SCM/Tag',
];

private static readonly readOnlyRolePermissions: string[] = [
'Overall/Read',
'Job/Read',
'View/Read',
];

public static addOidcConfigToJenkinsYaml(yamlObject: any, authType: string, admins?: string[]): any {
const jenkinsYaml: any = yamlObject;
let adminUsers: string[] = ['admin'];
const readOnlyUsers: string[] = ['anonymous'];

if (admins) {
adminUsers = adminUsers.concat(admins);
}

const oidcConfig: { [x: string]: any; } = {
oic: {
clientId: 'clientId',
clientSecret: 'clientSecret',
authorizationServerUrl: 'http://localhost',
wellKnownOpenIDConfigurationUrl: 'wellKnownOpenIDConfigurationUrl',
tokenServerUrl: 'tokenServerUrl',
userInfoServerUrl: 'userInfoServerUrl',
disableSslVerification: false,
userNameField: 'sub',
escapeHatchEnabled: false,
logoutFromOpenidProvider: true,
postLogoutRedirectUrl: '',
scopes: 'openid',
escapeHatchSecret: 'random',
},
};

const githubAuthConfig: { [x: string]: any; } = {
github: {
githubWebUri: 'https://github.com',
githubApiUri: 'https://api.github.com',
clientID: 'clientID',
clientSecret: 'clientSecret',
oauthScopes: 'read:org,user:email',
},
};

const rolesAndPermissions: { [x: string]: any; } = {
roleBased: {
roles: {
global: [{
entries: adminUsers.map((user) => ({ user })),
name: 'admin',
pattern: '.*',
permissions: AuthConfig.adminRolePermissions
,
},
{
entries: readOnlyUsers.map((user) => ({ user })),
name: 'read',
pattern: '.*',
permissions: AuthConfig.readOnlyRolePermissions,
},
],
},
},
};

jenkinsYaml.jenkins.authorizationStrategy = rolesAndPermissions;

if (authType === 'github') {
jenkinsYaml.jenkins.securityRealm = githubAuthConfig;
} else {
jenkinsYaml.jenkins.securityRealm = oidcConfig;
}
return jenkinsYaml;
}
}
38 changes: 21 additions & 17 deletions lib/compute/jenkins-main-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { join } from 'path';
import { CloudwatchAgent } from '../constructs/cloudwatch-agent';
import { AgentNodeConfig, AgentNodeNetworkProps, AgentNodeProps } from './agent-node-config';
import { EnvConfig } from './env-config';
import { OidcConfig } from './oidc-config';
import { AuthConfig } from './auth-config';
import { ViewsConfig } from './views';

interface HttpConfigProps {
Expand All @@ -47,9 +47,9 @@ interface HttpConfigProps {
readonly useSsl: boolean;
}

interface OidcFederateProps {
readonly oidcCredArn: string;
readonly runWithOidc: boolean;
interface LoginAuthProps {
readonly authCredsSecretsArn: string;
readonly authType: string;
readonly adminUsers?: string[];
}

Expand All @@ -58,7 +58,7 @@ interface DataRetentionProps {
readonly efsSG?: SecurityGroup;
}

export interface JenkinsMainNodeProps extends HttpConfigProps, OidcFederateProps, AgentNodeNetworkProps, DataRetentionProps {
export interface JenkinsMainNodeProps extends HttpConfigProps, LoginAuthProps, AgentNodeNetworkProps, DataRetentionProps {
readonly vpc: Vpc;
readonly sg: SecurityGroup;
readonly envVarsFilePath: string;
Expand All @@ -78,8 +78,6 @@ export class JenkinsMainNode {

static readonly PRIVATE_KEY_PATH: String = '/etc/ssl/private/test-jenkins.opensearch.org.key';

static readonly JENKINS_DEFAULT_ID_PASS_PATH: String = '/var/lib/jenkins/secrets/myIdPassDefault';

private readonly EFS_ID: string;

private static ACCOUNT: string;
Expand Down Expand Up @@ -231,8 +229,14 @@ export class JenkinsMainNode {
}

public static configElements(stackName: string, stackRegion: string, httpConfigProps: HttpConfigProps,
oidcFederateProps: OidcFederateProps, dataRetentionProps: DataRetentionProps, jenkinsyaml: string,
loginAuthProps: LoginAuthProps, dataRetentionProps: DataRetentionProps, jenkinsyaml: string,
reloadPasswordSecretsArn: string, efsId?: string): InitElement[] {
let realm = '';
if (loginAuthProps.authType === 'github') {
realm = 'github';
} else if (loginAuthProps.authType === 'oidc') {
realm = 'oic';
}
return [
InitPackage.yum('wget'),
InitPackage.yum('cronie'),
Expand Down Expand Up @@ -272,8 +276,8 @@ export class JenkinsMainNode {

// Change hop limit for IMDSv2 from 1 to 2
InitCommand.shellCommand('TOKEN=`curl -f -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"` &&'
+ ' instance_id=`curl -f -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id` && echo $ami_id &&'
+ ` aws ec2 --region ${stackRegion} modify-instance-metadata-options --instance-id $instance_id --http-put-response-hop-limit 2`),
+ ' instance_id=`curl -f -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id` && echo $ami_id &&'
+ ` aws ec2 --region ${stackRegion} modify-instance-metadata-options --instance-id $instance_id --http-put-response-hop-limit 2`),

// Jenkins CVE https://www.jenkins.io/security/advisory/2024-01-24/ mitigation
InitCommand.shellCommand('mkdir -p /var/lib/jenkins/init.groovy.d'),
Expand Down Expand Up @@ -427,12 +431,12 @@ export class JenkinsMainNode {
InitFile.fromFileInline('/initial_jenkins.yaml', jenkinsyaml),

// Make any changes to initial jenkins.yaml
InitCommand.shellCommand(oidcFederateProps.runWithOidc
InitCommand.shellCommand(loginAuthProps.authType !== 'default'
// eslint-disable-next-line max-len
? `var=\`aws --region ${stackRegion} secretsmanager get-secret-value --secret-id ${oidcFederateProps.oidcCredArn} --query SecretString --output text\` && `
? `var=\`aws --region ${stackRegion} secretsmanager get-secret-value --secret-id ${loginAuthProps.authCredsSecretsArn} --query SecretString --output text\` && `
+ ' varkeys=`echo $var | yq \'keys\' | cut -d "-" -f2 | cut -d " " -f2` &&'
// eslint-disable-next-line max-len
+ ' for i in $varkeys; do newvalue=`echo $var | yq .$i` && myenv=$newvalue i=$i yq -i \'.jenkins.securityRealm.oic.[env(i)]=env(myenv)\' /initial_jenkins.yaml ; done'
+ ` for i in $varkeys; do newvalue=\`echo $var | yq .$i\` && myenv=$newvalue i=$i yq -i '.jenkins.securityRealm.${realm}.[env(i)]=env(myenv)' /initial_jenkins.yaml ; done`
: 'echo No changes made to initial_jenkins.yaml with respect to OIDC'),

InitCommand.shellCommand('while [[ "$(curl -s -o /dev/null -w \'\'%{http_code}\'\' localhost:8080/api/json?pretty)" != "200" ]]; do sleep 5; done'),
Expand All @@ -444,11 +448,11 @@ export class JenkinsMainNode {
];
}

public static addConfigtoJenkinsYaml(stack: Stack, jenkinsMainNodeProps: JenkinsMainNodeProps, oidcProps: OidcFederateProps, agentNodeObject: AgentNodeConfig,
props: AgentNodeNetworkProps, agentNode: AgentNodeProps[], macAgent: string): string {
public static addConfigtoJenkinsYaml(stack: Stack, jenkinsMainNodeProps: JenkinsMainNodeProps, loginAuthProps: LoginAuthProps,
agentNodeObject: AgentNodeConfig, props: AgentNodeNetworkProps, agentNode: AgentNodeProps[], macAgent: string): string {
let updatedConfig = agentNodeObject.addAgentConfigToJenkinsYaml(stack, agentNode, props, macAgent);
if (oidcProps.runWithOidc) {
updatedConfig = OidcConfig.addOidcConfigToJenkinsYaml(updatedConfig, oidcProps.adminUsers);
if (loginAuthProps.authType !== 'default') {
updatedConfig = AuthConfig.addOidcConfigToJenkinsYaml(updatedConfig, loginAuthProps.authType, loginAuthProps.adminUsers);
}
if (jenkinsMainNodeProps.envVarsFilePath !== '' && jenkinsMainNodeProps.envVarsFilePath != null) {
updatedConfig = EnvConfig.addEnvConfigToJenkinsYaml(updatedConfig, jenkinsMainNodeProps.envVarsFilePath);
Expand Down
Loading

0 comments on commit 93cab0a

Please sign in to comment.