Skip to content

Commit b97ce3a

Browse files
authored
Fix SSM parameter reads (#79)
* More verbose SSM logging * Add more logging context * Get param inside handler * Bump param sidecar log level back to info * Read directly instead of via layer
1 parent 23d9490 commit b97ce3a

File tree

5 files changed

+64
-69
lines changed

5 files changed

+64
-69
lines changed

Cargo.lock

Lines changed: 26 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ path = "src/bin/add_subscriber.rs"
2222
anyhow = "1.0"
2323
email_address = "0.2"
2424
askama = "0.15"
25-
aws-config = { version = "1.1.7", features = ["rustls"] }
26-
aws-sdk-dynamodb = "1.16.0"
27-
aws-sdk-sesv2 = "1.14.0"
25+
aws-config = { version = "1.8.12", features = ["rustls"] }
26+
aws-sdk-dynamodb = "1.103.0"
27+
aws-sdk-sesv2 = "1.111.0"
28+
aws-sdk-ssm = "1.102.0"
2829
chrono = { version = "0.4", features = ["serde"] }
2930
futures = "0.3"
3031
lambda_http = "1.0.2"

infrastructure/lambda.tf

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ resource "aws_lambda_function" "hndigest" {
2727
environment {
2828
variables = merge(
2929
{
30-
RUST_LOG = "info"
31-
DYNAMODB_TABLE = each.value.table_name
32-
EMAIL_FROM = each.value.from_email
33-
EMAIL_REPLY_TO = each.value.reply_to_email
34-
RUN_HOUR_UTC = tostring(var.run_hour_utc)
35-
BASE_URL = "https://${each.value.domain}"
30+
AWS_LAMBDA_LOG_FORMAT = "json"
31+
RUST_LOG = "info"
32+
DYNAMODB_TABLE = each.value.table_name
33+
EMAIL_FROM = each.value.from_email
34+
EMAIL_REPLY_TO = each.value.reply_to_email
35+
RUN_HOUR_UTC = tostring(var.run_hour_utc)
36+
BASE_URL = "https://${each.value.domain}"
3637
},
3738
each.value.subject_prefix != "" ? { SUBJECT_PREFIX = each.value.subject_prefix } : {}
3839
)
@@ -64,17 +65,15 @@ resource "aws_lambda_function" "hndigest_api" {
6465
memory_size = var.lambda_memory_size
6566
timeout = var.lambda_timeout
6667

67-
layers = [var.params_secrets_extension_arn]
68-
6968
environment {
7069
variables = {
71-
RUST_LOG = "info"
72-
DYNAMODB_TABLE = each.value.table_name
73-
BASE_URL = "https://${each.value.domain}"
74-
EMAIL_FROM = each.value.from_email
75-
EMAIL_REPLY_TO = each.value.reply_to_email
76-
TURNSTILE_SECRET_KEY_PARAM = aws_ssm_parameter.turnstile_secret_key[each.key].name
77-
PARAMETERS_SECRETS_EXTENSION_LOG_LEVEL = "warn"
70+
AWS_LAMBDA_LOG_FORMAT = "json"
71+
RUST_LOG = "info"
72+
DYNAMODB_TABLE = each.value.table_name
73+
BASE_URL = "https://${each.value.domain}"
74+
EMAIL_FROM = each.value.from_email
75+
EMAIL_REPLY_TO = each.value.reply_to_email
76+
TURNSTILE_SECRET_KEY_PARAM = aws_ssm_parameter.turnstile_secret_key[each.key].name
7877
}
7978
}
8079

infrastructure/variables.tf

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,6 @@ variable "turnstile_site_key" {
7676
default = "0x4AAAAAACTuSJcLuENs4joL"
7777
}
7878

79-
variable "params_secrets_extension_arn" {
80-
description = "Full ARN of the AWS Parameters and Secrets Lambda Extension layer (region- and architecture-specific)"
81-
type = string
82-
# This is for us-west-2, x86. See: https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html#ps-integration-lambda-extensions-add
83-
default = "arn:aws:lambda:us-west-2:345057560386:layer:AWS-Parameters-and-Secrets-Lambda-Extension:24"
84-
}
85-
8679
###
8780
# Hosting config
8881
###

src/bin/api.rs

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use askama::Template;
88
use aws_config::BehaviorVersion;
99
use aws_sdk_sesv2::Client as SesClient;
1010
use aws_sdk_sesv2::types::{Body as SesBody, Content, Destination, EmailContent, Message};
11+
use aws_sdk_ssm::Client as SsmClient;
1112
use email_address::EmailAddress;
1213
use hndigest::storage_adapter::StorageAdapter;
1314
use hndigest::strategies::DigestStrategy;
@@ -68,19 +69,6 @@ struct TurnstileVerifyResponse {
6869
success: bool,
6970
}
7071

71-
/// Response from the AWS Parameters and Secrets Lambda Extension.
72-
#[derive(Debug, Deserialize)]
73-
struct SsmExtensionResponse {
74-
#[serde(rename = "Parameter")]
75-
parameter: SsmParameterValue,
76-
}
77-
78-
#[derive(Debug, Deserialize)]
79-
struct SsmParameterValue {
80-
#[serde(rename = "Value")]
81-
value: String,
82-
}
83-
8472
// ============================================================================
8573
// HTTP Response Helpers
8674
// ============================================================================
@@ -160,18 +148,28 @@ async fn main() -> Result<(), Error> {
160148
.map_err(|_| Error::from("EMAIL_REPLY_TO environment variable must be set"))?;
161149
let base_url = env::var("BASE_URL")
162150
.map_err(|_| Error::from("BASE_URL environment variable must be set"))?;
151+
152+
// I'd love to pass this as an environment variable, but using AWS secrets manager is expensive
153+
// and this is effectively free
154+
let ssm_client = SsmClient::new(&config);
163155
let turnstile_secret_key_param = env::var("TURNSTILE_SECRET_KEY_PARAM")
164156
.map_err(|_| Error::from("TURNSTILE_SECRET_KEY_PARAM environment variable must be set"))?;
157+
let turnstile_secret_key = ssm_client
158+
.get_parameter()
159+
.name(&turnstile_secret_key_param)
160+
.with_decryption(true)
161+
.send()
162+
.await?
163+
.parameter()
164+
.and_then(|p| p.value.clone())
165+
.ok_or_else(|| {
166+
anyhow::format_err!(
167+
"SSM parameter value not found for name {}",
168+
turnstile_secret_key_param
169+
)
170+
})?;
165171

166172
let http_client = reqwest::Client::new();
167-
let turnstile_secret_key = fetch_ssm_parameter(&http_client, &turnstile_secret_key_param)
168-
.await
169-
.map_err(|e| {
170-
Error::from(format!(
171-
"Failed to fetch SSM parameter {}: {}",
172-
turnstile_secret_key_param, e
173-
))
174-
})?;
175173
let storage = Arc::new(StorageAdapter::new(dynamodb_client, dynamodb_table));
176174
let state = Arc::new(AppState {
177175
storage,
@@ -305,27 +303,6 @@ async fn handle_unsubscribe_post(
305303
}
306304
}
307305

308-
/// Fetch a parameter from SSM via the AWS Parameters and Secrets Lambda Extension.
309-
/// The extension runs as a Lambda layer and serves requests on localhost:2773.
310-
async fn fetch_ssm_parameter(
311-
http_client: &reqwest::Client,
312-
parameter_name: &str,
313-
) -> Result<String, Box<dyn std::error::Error>> {
314-
let session_token = env::var("AWS_SESSION_TOKEN")?;
315-
let response: SsmExtensionResponse = http_client
316-
.get(format!(
317-
"http://localhost:2773/systemsmanager/parameters/get?name={}&withDecryption=true",
318-
urlencoding::encode(parameter_name)
319-
))
320-
.header("X-Aws-Parameters-Secrets-Token", &session_token)
321-
.send()
322-
.await?
323-
.error_for_status()?
324-
.json()
325-
.await?;
326-
Ok(response.parameter.value)
327-
}
328-
329306
/// Verify a Cloudflare Turnstile CAPTCHA token.
330307
async fn verify_turnstile_token(
331308
http_client: &reqwest::Client,
@@ -385,6 +362,7 @@ async fn handle_subscribe_post(state: &Arc<AppState>, body: &str) -> Response<Bo
385362
warn!("Missing Turnstile CAPTCHA token");
386363
return json_response(400, r#"{"error": "CAPTCHA verification required"}"#);
387364
}
365+
388366
match verify_turnstile_token(
389367
&state.http_client,
390368
&state.turnstile_secret_key,

0 commit comments

Comments
 (0)