From 54de57e886373f0787519feb65df00ab1758f55c Mon Sep 17 00:00:00 2001 From: Martin Moreira de Jesus Date: Thu, 7 Nov 2024 21:16:00 +0100 Subject: [PATCH] feat: Allow aws access key authentication (#36) --- README.md | 39 +++++++++----- .../templates/_helpers.tpl | 7 +++ .../templates/deployment.yaml | 13 ++++- .../iam-eks-user-mapper/templates/rbac.yaml | 5 +- .../iam-eks-user-mapper/templates/secret.yaml | 16 ++++++ charts/iam-eks-user-mapper/values.yaml | 8 +++ src/config.rs | 38 +++++++++++--- src/main.rs | 51 +++++++++++++++---- 8 files changed, 144 insertions(+), 33 deletions(-) create mode 100644 charts/iam-eks-user-mapper/templates/secret.yaml diff --git a/README.md b/README.md index b9d6c68..e09f55f 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,10 @@ At a given interval (default 30s) it executes the following: ```shell ./iam-eks-user-mapper \ --service-account-name \ + # either fill aws-role-arn or aws-access-key-id and aws-secret-access-key --aws-role-arn \ + --aws-access-key-id \ + --aws-secret-access-key \ --aws-default-region \ --enable-group-user-sync \ --iam-k8s-groups \ @@ -33,24 +36,30 @@ At a given interval (default 30s) it executes the following: --verbose ``` -| Parameter | Type | Default | Required | Description | Example | -|----------------------------|-----------|---------|--------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| -| `service-account-name` | `String` | | `true` | Service account name to be used | `my-service-account` | -| `aws-role-arn` | `String` | | `true` | AWS role ARN to be used | `arn:aws:iam::12345678910:role/my-role` | -| `aws_default_region` | `String` | | `true` | AWS default region to be used | `eu-west-3` | -| `refresh_interval_seconds` | `Integer` | `30` | `false` | Refresh interval in seconds between two user synchronization | `120` | -| `enable_group_user_sync` | `Boolean` | `false` | `false` | Activate User Groups sync | `true` | -| `iam_k8s_groups` | `String` | `""` | `false` (`true` if `enable_group_user_sync` == `true`) | IAM groups to be mapped into Kubernetes, syntax is `->,->` | `Admins->system:masters`, `Admins->system:masters,Devops->system:devops` | -| `enable_sso` | `Boolean` | `false` | `false` | Activate SSO support to connect to the cluster | `true` | -| `iam_sso_role_arn` | `String` | `""` | `false` (`true` if `enable_sso` == `true`) | IAM SSO role ARN to be used to connect to the cluster | `"arn:aws:iam::[AWS_ACCOUNT_ID]:role/aws-reserved/sso.amazonaws.com/[AWS_REGION]/AWSReservedSSO_AdministratorAccess_53b82e109c5e2cac"` | -| `karpenter_role_arn` | `String` | `""` | `false` | Enable Karpenter role ARN | `arn:aws:iam::account_id:role/role_id` | -| `verbose` | `Boolean` | `false` | `false` | Activate verbose mode | `Admins->system:masters`, `Admins->system:masters,Devops->system:devops` | +| Parameter | Type | Default | Required | Description | Example | +| -------------------------- | --------- | ------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | +| `service-account-name` | `String` | | `true` | Service account name to be used | `my-service-account` | +| `aws-role-arn` | `String` | | `true` if aws_access_key_id and aws_secret_access_key are not specified | AWS role ARN to be used | `arn:aws:iam::12345678910:role/my-role` | +| `aws_access_key_id` | `String` | | `true` if aws-role-arn is not specified | AWS Access Key ID to be used | `EXAMPLEACCESSKEYID` | +| `aws_secret_access_key` | `String` | | `true` if aws-role-arn is not specified | AWS Secret Access Key to be used | `EXAMPLESECRETACCESSKEY` | +| `aws_default_region` | `String` | | `true` | AWS default region to be used | `eu-west-3` | +| `refresh_interval_seconds` | `Integer` | `30` | `false` | Refresh interval in seconds between two user synchronization | `120` | +| `enable_group_user_sync` | `Boolean` | `false` | `false` | Activate User Groups sync | `true` | +| `iam_k8s_groups` | `String` | `""` | `false` (`true` if `enable_group_user_sync` == `true`) | IAM groups to be mapped into Kubernetes, syntax is `->,->` | `Admins->system:masters`, `Admins->system:masters,Devops->system:devops` | +| `enable_sso` | `Boolean` | `false` | `false` | Activate SSO support to connect to the cluster | `true` | +| `iam_sso_role_arn` | `String` | `""` | `false` (`true` if `enable_sso` == `true`) | IAM SSO role ARN to be used to connect to the cluster | `"arn:aws:iam::[AWS_ACCOUNT_ID]:role/aws-reserved/sso.amazonaws.com/[AWS_REGION]/AWSReservedSSO_AdministratorAccess_53b82e109c5e2cac"` | +| `karpenter_role_arn` | `String` | `""` | `false` | Enable Karpenter role ARN | `arn:aws:iam::account_id:role/role_id` | +| `verbose` | `Boolean` | `false` | `false` | Activate verbose mode | `Admins->system:masters`, `Admins->system:masters,Devops->system:devops` | + +**Note:** Either `aws_role_arn` or `aws_access_key_id` and `aws_secret_access_key` must be provided. Both cannot be provided at the same time. All parameters can be set as environment variables as well: ```shell SERVICE_ACCOUNT_NAME= \ AWS_ROLE_ARN= \ +AWS_ACCESS_KEY_ID= \ +AWS_SECRET_ACCESS_KEY= \ AWS_DEFAULT_REGION= \ ENABLE_GROUP_USER_SYNC= \ IAM_K8S_GROUPS= \ @@ -81,7 +90,13 @@ refreshIntervalSeconds: aws: defaultRegion: + # either fill roleArn or accessKeyId and secretAccessKey or existingSecretName roleArn: + # if you want to use an existing secret, set the name here + # it must contain AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY + existingSecretName: + accessKeyId: + secretAccessKey: # Repository for the image is there # https://github.com/Qovery/iam-eks-user-mapper diff --git a/charts/iam-eks-user-mapper/templates/_helpers.tpl b/charts/iam-eks-user-mapper/templates/_helpers.tpl index a341c5b..ea0f288 100644 --- a/charts/iam-eks-user-mapper/templates/_helpers.tpl +++ b/charts/iam-eks-user-mapper/templates/_helpers.tpl @@ -60,3 +60,10 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +Name of the secret +*/}} +{{- define "iam-eks-user.aws.secretName" -}} +{{- default (include "iam-eks-user.fullname" .) .Values.aws.existingSecretName }} +{{- end }} diff --git a/charts/iam-eks-user-mapper/templates/deployment.yaml b/charts/iam-eks-user-mapper/templates/deployment.yaml index 4956aeb..6e806b6 100644 --- a/charts/iam-eks-user-mapper/templates/deployment.yaml +++ b/charts/iam-eks-user-mapper/templates/deployment.yaml @@ -1,7 +1,6 @@ kind: Deployment apiVersion: apps/v1 metadata: - namespace: kube-system name: {{ include "iam-eks-user.fullname" . }} labels: {{- include "iam-eks-user.labels" . | nindent 4 }} @@ -46,6 +45,18 @@ spec: - name: "KARPENTER_ROLE_ARN" value: "{{ .Values.karpenter.iamKarpenterRoleArn }}" {{ end }} + - name: "AWS_ACCESS_KEY_ID" + valueFrom: + secretKeyRef: + name: {{ include "iam-eks-user.aws.secretName" . }} + key: AWS_ACCESS_KEY_ID + - name: "AWS_SECRET_ACCESS_KEY" + valueFrom: + secretKeyRef: + name: {{ include "iam-eks-user.aws.secretName" . }} + key: AWS_SECRET_ACCESS_KEY + - name: AWS_DEFAULT_REGION + value: "{{ .Values.aws.defaultRegion }}" resources: {{- toYaml .Values.resources | nindent 12 }} command: diff --git a/charts/iam-eks-user-mapper/templates/rbac.yaml b/charts/iam-eks-user-mapper/templates/rbac.yaml index 6849c1e..a722226 100644 --- a/charts/iam-eks-user-mapper/templates/rbac.yaml +++ b/charts/iam-eks-user-mapper/templates/rbac.yaml @@ -3,8 +3,9 @@ apiVersion: v1 metadata: name: {{ .Values.serviceAccount.name }} namespace: kube-system -{{- if .Values.serviceAccount.annotations }} - annotations: {{ toYaml .Values.serviceAccount.annotations | nindent 4 }} +{{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} {{- end }} --- kind: Role diff --git a/charts/iam-eks-user-mapper/templates/secret.yaml b/charts/iam-eks-user-mapper/templates/secret.yaml new file mode 100644 index 0000000..b421b29 --- /dev/null +++ b/charts/iam-eks-user-mapper/templates/secret.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.aws.accessKeyId .Values.aws.secretAccessKey }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "iam-eks-user.aws.secretName" . }} + namespace: kube-system + labels: + {{- include "iam-eks-user.labels" . | nindent 4 }} + {{- with .Values.extraLabels }} + {{ toYaml . | indent 4 }} + {{- end }} +type: Opaque +data: + AWS_ACCESS_KEY_ID: {{ .Values.aws.accessKeyId | b64enc | quote }} + AWS_SECRET_ACCESS_KEY: {{ .Values.aws.secretAccessKey | b64enc | quote }} +{{- end }} diff --git a/charts/iam-eks-user-mapper/values.yaml b/charts/iam-eks-user-mapper/values.yaml index 7c0a99c..c2425ec 100644 --- a/charts/iam-eks-user-mapper/values.yaml +++ b/charts/iam-eks-user-mapper/values.yaml @@ -8,6 +8,14 @@ groupUsersSync: enabled: false iamK8sGroups: "" # "group1,group2" +aws: + # if you want to use an existing secret, set the name here + # it must contain AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY + existingSecretName: "" + accessKeyId: "" + secretAccessKey: "" + defaultRegion: "us-west-1" + sso: enabled: false iamSSORoleArn: "" # "arn:aws:iam::[AWS_ACCOUNT_ID]:role/aws-reserved/sso.amazonaws.com/[AWS_REGION]/AWSReservedSSO_AdministratorAccess_53b82e109c5e2cac" diff --git a/src/config.rs b/src/config.rs index 3afcde3..bfb0f03 100644 --- a/src/config.rs +++ b/src/config.rs @@ -26,15 +26,30 @@ pub enum ConfigurationError { pub struct Credentials { pub region: Region, pub _service_account_name: String, - pub _role_arn: RoleArn, + pub _credentials_mode: CredentialsMode, +} + +#[derive(Clone)] +pub enum CredentialsMode { + RoleBased { + _aws_role_arn: RoleArn, + }, + AccessKeyBased { + _aws_access_key_id: String, + _aws_secret_access_key: String, + }, } impl Credentials { - pub fn new(region: Region, service_account_name: String, role_arn: RoleArn) -> Self { - Self { + pub fn new( + region: Region, + service_account_name: String, + credentials_mode: CredentialsMode, + ) -> Credentials { + Credentials { region, _service_account_name: service_account_name, - _role_arn: role_arn, + _credentials_mode: credentials_mode, } } } @@ -193,7 +208,8 @@ impl Config { mod tests { use crate::aws::iam::IamGroup; use crate::config::{ - Config, ConfigurationError, Credentials, IamK8sGroup, KarpenterRoleConfig, SSORoleConfig, + Config, ConfigurationError, Credentials, CredentialsMode, IamK8sGroup, KarpenterRoleConfig, + SSORoleConfig, }; use crate::kubernetes::{IamArn, KubernetesGroupName}; use std::str::FromStr; @@ -294,7 +310,9 @@ mod tests { Credentials::new( "whatever".to_string(), "whatever".to_string(), - "whatever".to_string(), + CredentialsMode::RoleBased { + _aws_role_arn: "whatever".to_string(), + }, ), Duration::from_secs(60), false, @@ -334,7 +352,9 @@ mod tests { Credentials::new( "whatever".to_string(), "whatever".to_string(), - "whatever".to_string(), + CredentialsMode::RoleBased { + _aws_role_arn: "whatever".to_string(), + }, ), Duration::from_secs(60), false, @@ -357,7 +377,9 @@ mod tests { Credentials::new( "whatever".to_string(), "whatever".to_string(), - "whatever".to_string(), + CredentialsMode::RoleBased { + _aws_role_arn: "whatever".to_string(), + }, ), Duration::from_secs(60), false, diff --git a/src/main.rs b/src/main.rs index bdbdea1..944a59f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,8 @@ use crate::kubernetes::{ IamArn, IamUserName, KubernetesGroupName, KubernetesRole, KubernetesService, KubernetesUser, SyncedBy, }; -use clap::Parser; +use clap::{ArgGroup, Parser}; +use config::CredentialsMode; use std::collections::{HashMap, HashSet}; use std::time::Duration; use tokio::{task, time}; @@ -20,13 +21,24 @@ use tracing_subscriber::{prelude::*, EnvFilter, FmtSubscriber}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] +#[command(group( + ArgGroup::new("aws_credentials") + .args(&["aws_role_arn", "aws_access_key_id"]) + .required(true) +))] struct Args { /// Service account name to be used, e.q: my-service-account #[arg(short = 's', long, env, required = true)] pub service_account_name: String, /// AWS role ARN to be used, e.q: arn:aws:iam::12345678910:role/my-role - #[arg(short = 'R', long, env, required = true)] - pub aws_role_arn: String, + #[arg(short = 'R', long, env, conflicts_with_all = &["aws_access_key_id", "aws_secret_access_key"])] + pub aws_role_arn: Option, + /// AWS access key ID to be used + #[arg(short = 'a', long, env, requires = "aws_secret_access_key")] + pub aws_access_key_id: Option, + /// AWS secret access key to be used + #[arg(short = 'k', long, env, requires = "aws_access_key_id")] + pub aws_secret_access_key: Option, /// AWS default region to be used, e.q: eu-west-3 #[arg(short = 'r', long, env, required = true)] pub aws_default_region: String, @@ -34,7 +46,7 @@ struct Args { #[arg(short = 'i', long, env, default_value_t = 60)] pub refresh_interval_seconds: u64, /// Activate group user sync (requires `iam_k8s_groups` to be set) - #[clap(long, env, required = true, default_value_t = false)] + #[clap(long, env, required = false, default_value_t = false)] pub enable_group_user_sync: bool, /// IAM groups to be mapped into Kubernetes, e.q: Admins->system:masters /// @@ -44,7 +56,7 @@ struct Args { #[clap(short = 'g', long, env, value_parser, num_args = 1.., value_delimiter = ',', required = false)] pub iam_k8s_groups: Vec, /// Activate SSO on the cluster (requires `iam_sso_role_arn` to be set) - #[clap(long, env, required = true, default_value_t = false)] + #[clap(long, env, default_value_t = false, required = false)] pub enable_sso: bool, /// IAM SSO role arn #[clap(long, env, value_delimiter = ',', required = false)] @@ -113,6 +125,8 @@ async fn sync_iam_eks_users_and_roles( underlying_error: e.into(), })?; + info!("Found {} users in IAM groups", iam_users.len()); + Some(HashSet::from_iter(iam_users.iter().map(|u| { KubernetesUser::new( IamUserName::new(&u.user_name.to_string()), @@ -164,12 +178,29 @@ async fn main() -> Result<(), errors::Error> { let args = Args::parse(); + let credentials_mode = if let Some(aws_role_arn) = &args.aws_role_arn { + CredentialsMode::RoleBased { + _aws_role_arn: aws_role_arn.clone(), + } + } else if let (Some(aws_access_key_id), Some(aws_secret_access_key)) = + (&args.aws_access_key_id, &args.aws_secret_access_key) + { + CredentialsMode::AccessKeyBased { + _aws_access_key_id: aws_access_key_id.clone(), + _aws_secret_access_key: aws_secret_access_key.clone(), + } + } else { + panic!("Bad configuration"); + }; + + let credentials = Credentials::new( + args.aws_default_region, + args.service_account_name, + credentials_mode, + ); + let config = config::Config::new( - Credentials::new( - args.aws_default_region, - args.service_account_name, - args.aws_role_arn, - ), + credentials, Duration::from_secs(args.refresh_interval_seconds), args.enable_group_user_sync, args.iam_k8s_groups,