Giving My App Secrets to the AWS SecretsManager

Where I pull most of my secrets out of the .env file and store at AWS instead

Part of a Series: Designing a Full-Featured WebApp with Rust
Part 1: Piecing Together a Rust Web Application
Part 2: My Next Step in Rust Web Application Dev
Part 3: It’s Not a Web Application Without a Database
Part 4: Better Logging for the Web Application
Part 5: Rust Web App Session Management with AWS
Part 6: OAuth Requests, APIs, Diesel, and Sessions
Part 7: Scraping off the Dust: Redeploy of my Rust web app
Part 8: Giving My App Secrets to the AWS SecretManager

Mother telling a child a secret. Is AWS my child in this image??
Psst… want an API key?

I’m still working around my PinPointShooting app without actually making any app progress. You can go back to the start if you like or just jump in here. So far, I’ve had my app ids and secrets hidden inside a .env file, but rather than keep them so close to the code, lets move them into my Amazon Web Services (AWS) account. It will cost me an extra $2.40/month to store these 6 secrets (so far) at Amazon ($0.40/month/secret it claims), but it gets me more practice using the rusoto_core crate and its subsidiaries. So, here is how I went about giving all of my app secrets to the AWS SecretsManager.

Cargo.toml Changes

First, in Cargo.toml, I already have rusoto_core and rusoto_dynamodb for storing my session data. So I only need to add rusoto_secretsmanager:

[dependencies]
slog = "2.5.0"
slog-bunyan = "2.1.0"
base64 = "0.10.1"
rand = "0.7.0"
rand_core = "0.5.0"
rust-crypto = "0.2.36"
config = "0.9.3"
serde_json = "1.0.40"
once_cell = "0.2.2"
dotenv = "0.14.1"
rocket-slog = "0.4.0"
sha2 = "0.8.0"
rusoto_core = "0.40.0"
rusoto_dynamodb = "0.40.0"
rusoto_secretsmanager = "0.40.0"
time = "0.1.42"
google-signin = "0.3.0"

Store Those Secrets in AWS

Of course, I had to log into AWS and create these 6 new secrets. In case I need more for other applications in the future, I prefixed each of these with “PPS_”. So, I created:

Secret name
PPS_google_api_client_id
PPS_google_api_client_secret
PPS_google_maps_api_key
PPS_facebook_app_id
PPS_facebook_app_secret
PPS_database_url
List of secrets I created in my AWS account

For each, I choose the “Other type of secrets (e.g. API key)” as the type. I stored the pair as “key” and the value I wanted to protect as a secret. This becomes the very simple struct:

#[derive(Debug, Deserialize)]
struct AWSSecret {
    key: String,
}



So, instead of pulling these into my global CONFIG from the .env file, I want to pull them from AWS instead. This requires changes to the settings.rs file. First I add the requirements at the top:

use rusoto_core::Region;
use rusoto_secretsmanager::{GetSecretValueRequest, SecretsManager, SecretsManagerClient};

plus I define the AWSSecret struct I mentioned above. Now, right after I pull in all the config settings, just before I try to turn that into a Lazy::New object to initialize the CONFIG, I need to add yet another place to pull settings from. Lets loop through each of the 6 and pull them down from AWS:

Pull Into Config

 // now pull secrets from AWS
 // note, AWS secrets include PPS_ prefix for this application
 for secret in vec![
     "database_urls",
     "google_api_client_id",
     "google_api_client_secret",
     "google_maps_api_key",
     "facebook_app_id",
     "facebook_app_secret",
 ] {
     config
         .set(secret, get_secret(format!("PPS_{}", secret)).key)
         .unwrap();
 }

So, for each Config setting stored on AWS, try to get that secret, prepending a “PPS_” in front of the secret name when checking AWS. Now I just write that get_secret() function:

get_secrets stored at AWS

fn get_secret(secret: String) -> AWSSecret {
    let secrets_manager = SecretsManagerClient::new(Region::UsEast1);
    match secrets_manager
        .get_secret_value(GetSecretValueRequest {
            secret_id: secret.clone(),
            ..Default::default()
        })
        .sync()
    {
        Ok(resp) => serde_json::from_str(&resp.secret_string.unwrap()).unwrap(),
        Err(err) => panic!("Could not retrieve secret {} from AWS: {:?}", secret, err),
    }
}

I’ll probably look back on this code in 6 months or a year and cringe, but this works anyway – and dies if it fails to pull a secret, which is what I want. I guess get_secret could return Some(AWSSecret) or None… but None means something in the app will break – I might as well just panic!

Of course, my AWS credentials are still stored in the .env file. Who protects the secret the protects the secrets!? Anyway, I did update the .env.sample file to reflect the new setup:

# configure for your user and rename to .env
ROCKET_ENV=development
AWS_ACCESS_KEY_ID=your_key_here
AWS_SECRET_ACCESS_KEY=your_key_here

# store as AWS secrets: key/value
#PPS_database_url   (for example "postgres://user:password@localhost/pinpoint")
#PPS_google_api_client_id
#PPS_google_api_client_secret
#PPS_google_maps_api_key
#PPS_facebook_app_id
#PPS_facebook_app_secret

As always, see PinPointShooting.com to see just how little it does so far (if it happens to be running), or more exciting: go see the git repo itself.

Scraping off the Dust: Redeploy of my Rust web app

I do a redeploy of my Rust web app with the Ubuntu 18.04 image on AWS.

Part of a Series: Designing a Full-Featured WebApp with Rust
Part 1: Piecing Together a Rust Web Application
Part 2: My Next Step in Rust Web Application Dev
Part 3: It’s Not a Web Application Without a Database
Part 4: Better Logging for the Web Application
Part 5: Rust Web App Session Management with AWS
Part 6: OAuth Requests, APIs, Diesel, and Sessions
Part 7: Scraping off the Dust: Redeploy of my Rust web app
Part 8: Giving My App Secrets to the AWS SecretManager

Life hasBusy as a bee

Life has been busy – no apologies or excuses, but, ya know, it’s 2020. Yet, I’m trying to slowly make my way back into playing with Rust. I decided to move my EC2 instance from AWS-Linux to the Ubuntu image; for one, I got tired of fighting with LetsEncode to get it to renew my SSL cert every 3 months. Also, I wanted to see how a redeploy of my Rust web app would go and if it still worked (why wouldn’t it?). So, lets see how tough it is to get my environment back to the same place. I took some notes (in case I needed to restart, sigh), so let’s go through it.

Go back to Part 1 to see what this fake web app is about and how I got here – I need to reread it myself! So, first, this is what I ended up needing to add to what I got from the default Ubuntu image:

sudo apt install build-essential checkinstall zlib1g-dev pig-config libssl-dev libpq-dev postgresql postgresql-contrib -y

Lots of that was needed in order to get OpenSSL installed, I was following along hints here. Continuing those instructions, I did:

cd /usr/local/src/
sudo wget https://www.openssl.org/source/openssl-1.1.1g.tar.gz
sudo tar -xf openssl-1.1.1g.tar.gz

cd openssl-1.1.1g
sudo ./config --prefix=/usr/local/ssl --openssldir=/usr/local/ssl shared zlib
sudo make
sudo make test
sudo make install
sudo echo "/usr/local/ssl/lib" > /etc/ld.so.conf.d/openssl-1.1.1g.conf
sudo ldconfig -v
sudo mv /usr/bin/c_rehash /usr/bin/c_rehash.backup
sudo mv /usr/bin/openssl /usr/bin/openssl.backup
sudo nano /etc/environment # to add "/usr/local/ssl/bin" to the PATH

Next, instead of solely storing my code on a potentially tenuous EC2 server, I wanted to keep it backed up on my Google Drive (or whatever you like, this solution works with MANY network storage). I used rclone for my Raspberry Pi photo frame so I was familiar with that already. This is weird though, I don’t really need this for projects I store in GitHub… gotta think about it… maybe I just need a /gdrive synced dir for “things”.

curl https://rclone.org/install.sh | sudo bash
rclone config # to add google drive and authorize it

mkdir ~/projects
mkdir ~/projects/rust



Ok, the most fun step!!

curl https://sh.rustup.rs -sSf | sh
cd ~/projects/rust
git clone git@github.com:jculverhouse/pinpoint_shooting.git

I need nginx for my app

sudo apt install nginx
sudo service nginx start

And now the much more reliable LetsEncrypt using Ubuntu 18.04

# follow instructions at https://certbot.eff.org/lets-encrypt
# setup root cronjob to renew once/week

For my Rocket-powered Rust app, I followed some reminders here to connect it to nginx. Simple enough, really. What’s mostly relevant:

...
server_name pinpointshooting.com; # managed by Certbot
location / {
    proxy_pass http://127.0.0.1:3000;
}
...

What? Nginx still has TLS 1 and 1.1 turned on by default? Followed this and removed those, tested the config, and restarted nginx. All of that I checked with SSLLabs via https://www.ssllabs.com/ssltest/analyze.html :

sudo nano /etc/nginx/nginx.conf # to remove TLS1 TLS1.1 from any line
sudo nano /etc/letsencrypt/options-ssl-nginx.conf # to remove TLS1 TLS1.1 from any line
sudo nginx -t
sudo service nginx reload

I’ll need Postgres for my PinpointShooting app as well, found some steps to follow here, plus I needed to setup for my own app and run the initial migrations to get it up-to-date. That involved another change so I could login with the password from a non-user-account.

cargo install diesel_cli --no-default-features --features posters 
psql -d postgres -U postgres
  create user pinpoint password 'yeahrightgetyourown'; # save this in file .env in app dir
  create database pinpoint;
  grant all privileges on database pinpoint to pinpoint;

sudo nano /etc/postgresql/10/main/pg_hba.conf # to edit "local all all" line to be md5 instead of peer

sudo service postgresql restart
psql -d postgres -U pinpoint # to test password above; just exit

cd ~/projects/rust/pinpointshooting
diesel migration run

Finally:

rustup default nightly # because Rocket, sigh...
cargo update
cargo build --release
target/release/pps &

And, we’re back online! Turns out, a redeploy of my Rust web app was about as easy as I could expect! If the app happens to be running, check it out here (though, there isn’t much to see or anything to do): pinpointshooting.com. Also, browse the repo and feel free to send me comments on how to be better about using idiomatic Rust!