Skip to content

Commit

Permalink
Add automatic access key rotation
Browse files Browse the repository at this point in the history
  • Loading branch information
sam701 committed Jan 26, 2020
1 parent 938cb5d commit 76ef70d
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 33 deletions.
14 changes: 14 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ clap = "2.33"
rusoto_core = "0.42"
rusoto_credential = "0.42"
rusoto_sts = "0.42"
rusoto_iam = "0.42"
toml = "0.5"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_urlencoded = "*"
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ role_arn = "arn:aws:iam::123456589014:role/K8sAdminRole"
parent_profile = "prod"
```

### Optional automatic access key rotation
```toml
# Uncomment the following line to enable automatic credentials rotation of the main profile every N days.
# rotate_credentials_days = 7
```

### Yubikey integration
The MFA is read from your Yubikey so you do not need to type it.\
![prompt](./doc/yubikey.png)
Expand Down
39 changes: 11 additions & 28 deletions src/assume/assumer.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
use rusoto_sts::{StsClient, Sts, AssumeRoleRequest, GetSessionTokenRequest, NewAwsCredsForStsCreds};
use rusoto_core::{Region, HttpClient};
use rusoto_credential::{AwsCredentials, StaticProvider};
use crate::config::{Config, AssumeSubject};
use crate::credentials::{ProfileName, CredentialsFile};
use std::error::Error;
use chrono::{Utc, Duration};
use hyper_proxy::{Proxy, Intercept, ProxyConnector};
use hyper_tls::HttpsConnector;
use hyper::Uri;
use hyper::client::HttpConnector;
use crate::util;

use chrono::{Duration, Utc};
use rusoto_core::{HttpClient, Region};
use rusoto_credential::{AwsCredentials, StaticProvider};
use rusoto_sts::{AssumeRoleRequest, GetSessionTokenRequest, NewAwsCredsForStsCreds, Sts, StsClient};

use crate::config::{AssumeSubject, Config};
use crate::credentials::{CredentialsFile, ProfileName};

pub struct RoleAssumer<'a> {
region: Region,
Expand Down Expand Up @@ -66,7 +63,7 @@ impl<'a> RoleAssumer<'a> {
let parent_cred = self.profile_credentials(&parent)?;
let sub = self.config.assume_subject(profile)?
.ok_or(format!("cannot get assume subject for profile {}", profile))?;
let parent_client = create_client(parent_cred, self.region.clone())?;
let parent_client = create_sts_client(parent_cred, self.region.clone())?;
let new_cred = assume_subject(&parent_client, sub)?;
let out_cred = (&new_cred).into();
self.store.put_credentials(profile.clone(), new_cred);
Expand Down Expand Up @@ -100,9 +97,9 @@ fn assume_subject(client: &StsClient, subject: AssumeSubject) -> Result<AwsCrede
}


fn create_client(credentials: Cred, region: Region) -> Result<StsClient, String> {
fn create_sts_client(credentials: Cred, region: Region) -> Result<StsClient, String> {
Ok(StsClient::new_with(
HttpClient::from_connector(get_https_connector()?),
HttpClient::from_connector(super::get_https_connector()?),
StaticProvider::new(
credentials.key,
credentials.secret,
Expand All @@ -112,17 +109,3 @@ fn create_client(credentials: Cred, region: Region) -> Result<StsClient, String>
region,
))
}

fn get_https_connector() -> Result<ProxyConnector<HttpsConnector<HttpConnector>>, String> {
let connector = HttpsConnector::new(2)
.expect("connector with 2 threads");
Ok(match util::get_https_proxy() {
Some(proxy_url) => {
let url = proxy_url.parse::<Uri>()
.map_err(|e| format!("cannot parse proxy URL({}): {}", &proxy_url, e))?;
let proxy = Proxy::new(Intercept::All, url);
ProxyConnector::from_proxy(connector, proxy).expect("proxy created")
}
None => ProxyConnector::new(connector).expect("transparent proxy created")
})
}
72 changes: 72 additions & 0 deletions src/assume/main_credentials.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use chrono::{Duration, TimeZone, Utc};
use rusoto_core::{HttpClient, Region};
use rusoto_credential::{AwsCredentials, StaticProvider};
use rusoto_iam::{CreateAccessKeyRequest, Iam, IamClient, DeleteAccessKeyRequest};

use crate::config::Config;
use crate::credentials::CredentialsFile;
use crate::state::State;
use ansi_term::{Style, Color};

pub fn rotate_if_needed(config: &Config, cred_file: &mut CredentialsFile, state: &mut State) -> Result<(), String> {
if let Some(days) = config.rotate_credentials_days {
let now = Utc::now();
let last_rotation = state.last_credentials_rotation_time.unwrap_or(Utc.timestamp(0, 0));
if now - last_rotation >= Duration::days(days) {
rotate_credentials(cred_file, config)?;
state.last_credentials_rotation_time = Some(now);
state.save()?;
}
}
Ok(())
}

fn rotate_credentials(cred_file: &mut CredentialsFile, config: &Config) -> Result<(), String> {
let cred = cred_file.get_credentials(&config.main_profile)
.ok_or(format!("cannot get credentials for main profile '{}'", config.main_profile.as_ref()))?;
let client = create_iam_client(cred)?;

let access_key = cred.aws_access_key_id().to_owned();
drop(cred);

eprintln!("{}: access key is more than {} days old.",
Style::new().fg(Color::Yellow).bold().paint("Rotating Access Key"),
config.rotate_credentials_days.unwrap());
eprint!(" Creating new access key... ");
let ak_resp = client.create_access_key(CreateAccessKeyRequest {
user_name: None,
}).sync().map_err(|e| format!("cannot create new IAM access key: {}", e))?;
let ok_style = Style::new().fg(Color::Green).bold();
eprintln!("{}", ok_style.paint("ok"));

cred_file.put_credentials(config.main_profile.clone(), AwsCredentials::new(
ak_resp.access_key.access_key_id,
ak_resp.access_key.secret_access_key,
None,
None,
));
cred_file.write()?;

eprint!(" Deleting old access key... ");
client.delete_access_key(DeleteAccessKeyRequest{
access_key_id: access_key.clone(),
user_name: None,
}).sync().map_err(|e| format!("cannot delete old access key({}): {}",
&access_key, e))?;
eprintln!("{}", ok_style.paint("ok"));

Ok(())
}

fn create_iam_client(credentials: &AwsCredentials) -> Result<IamClient, String> {
Ok(IamClient::new_with(
HttpClient::from_connector(super::get_https_connector()?),
StaticProvider::new(
credentials.aws_access_key_id().to_owned(),
credentials.aws_secret_access_key().to_owned(),
credentials.token().clone(),
None,
),
Region::UsEast1,
))
}
28 changes: 25 additions & 3 deletions src/assume/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ use std::path::Path;

use ansi_term::{Color, Style};
use chrono::{Duration, Utc};
use hyper::client::HttpConnector;
use hyper::Uri;
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
use hyper_tls::HttpsConnector;

use crate::assume::assumer::RoleAssumer;
use crate::config::Config;
Expand All @@ -11,6 +15,7 @@ use crate::state;
use crate::util;

mod assumer;
mod main_credentials;

pub fn run(profile: &str, config: &Config) {
let error = util::styled_error_word();
Expand All @@ -29,9 +34,12 @@ pub fn run(profile: &str, config: &Config) {
}

fn run_raw(profile: &str, config: &Config) -> Result<(), String> {
let cred_file = CredentialsFile::read_default()?;
let mut cred_file = CredentialsFile::read_default()?;

let mut state = state::State::read();

main_credentials::rotate_if_needed(config, &mut cred_file, &mut state)?;

let mut assumer = RoleAssumer::new(
config.region.clone(),
cred_file,
Expand Down Expand Up @@ -66,13 +74,27 @@ fn print_profile(profile_name: &str, config: &Config) {
"fish" => {
print!("set -xg AWS_PROFILE {}; ", profile_name);
println!("set -l __awscredx_modify_prompt {}", config.modify_shell_prompt);
},
}
_ => {
print!("export AWS_PROFILE={}; ", profile_name);
println!("__awscredx_modify_prompt={}", config.modify_shell_prompt);
},
}
}
}
None => println!("export AWS_PROFILE={}", profile_name)
}
}

fn get_https_connector() -> Result<ProxyConnector<HttpsConnector<HttpConnector>>, String> {
let connector = HttpsConnector::new(2)
.expect("connector with 2 threads");
Ok(match util::get_https_proxy() {
Some(proxy_url) => {
let url = proxy_url.parse::<Uri>()
.map_err(|e| format!("cannot parse proxy URL({}): {}", &proxy_url, e))?;
let proxy = Proxy::new(Intercept::All, url);
ProxyConnector::from_proxy(connector, proxy).expect("proxy created")
}
None => ProxyConnector::new(connector).expect("transparent proxy created")
})
}
3 changes: 3 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub struct Config {
pub modify_shell_prompt: bool,
pub region: Region,
session_name: String,
pub rotate_credentials_days: Option<i64>,
}

#[derive(Deserialize, Debug)]
Expand Down Expand Up @@ -77,6 +78,7 @@ impl Config {
modify_shell_prompt: Option<bool>,
region: Option<String>,
session_name: Option<String>,
rotate_credentials_days: Option<i64>,
}

let rc: RawConfig = toml::from_str(&content)
Expand Down Expand Up @@ -104,6 +106,7 @@ impl Config {
modify_shell_prompt: rc.modify_shell_prompt.unwrap_or(true),
region,
session_name: rc.session_name.unwrap_or("awscredx".to_owned()),
rotate_credentials_days: rc.rotate_credentials_days,
};
Ok(Some(config))
}
Expand Down
3 changes: 3 additions & 0 deletions src/init/config-template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ region = "eu-central-1"
# Session name used for role assumption.
session_name = "awscredx"

# Uncomment the following line to enable automatic credentials rotation of the main profile every N days.
# rotate_credentials_days = 7

[profiles]

# You can specify profiles by either providing the role ARNs
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ extern crate linked_hash_map;
extern crate reqwest;
extern crate rusoto_core;
extern crate rusoto_credential;
extern crate rusoto_iam;
extern crate rusoto_sts;
extern crate serde;
extern crate serde_json;
Expand Down
8 changes: 6 additions & 2 deletions src/state.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use serde::{Serialize, Deserialize};
use std::fs;
use std::path::PathBuf;

use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};

use crate::util;
use chrono::{Utc, DateTime, TimeZone};

#[derive(Deserialize, Serialize)]
pub struct State {
pub last_version_check_time: DateTime<Utc>,
pub last_credentials_rotation_time: Option<DateTime<Utc>>,
}

impl State {
Expand All @@ -17,6 +20,7 @@ impl State {
}
_ => Self {
last_version_check_time: Utc.timestamp(0, 0),
last_credentials_rotation_time: None,
},
}
}
Expand Down

0 comments on commit 76ef70d

Please sign in to comment.