Skip to content

Commit 783bf57

Browse files
authored
Add webhook secret fallback for key rotation (#223)
Signed-off-by: Sergio Castaño Arteaga <[email protected]>
1 parent 4e5c0a0 commit 783bf57

File tree

6 files changed

+43
-7
lines changed

6 files changed

+43
-7
lines changed

charts/clowarden/Chart.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: v2
22
name: clowarden
33
description: CLOWarden is a tool that manages access to resources across multiple services
44
type: application
5-
version: 0.1.2
5+
version: 0.1.3-0
66
appVersion: 0.1.1
77
kubeVersion: ">= 1.19.0-0"
88
home: https://clowarden.io

charts/clowarden/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ server:
5151
5252
# GitHub application webhook secret
5353
webhookSecret: "your-webhook-secret"
54+
55+
# GitHub application webhook secret fallback (handy for webhook secret rotation)
56+
webhookSecretFallback: "old-webhook-secret"
5457
```
5558
5659
In addition to the GitHub application configuration, you can also add the organizations you'd like to use CLOWarden with at this point:

charts/clowarden/templates/server_secret.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ stringData:
2424
appId: {{ .Values.server.githubApp.appId }}
2525
privateKey: {{ .Values.server.githubApp.privateKey | quote }}
2626
webhookSecret: {{ .Values.server.githubApp.webhookSecret | quote }}
27+
{{- with .Values.server.githubApp.webhookSecretFallback }}
28+
webhookSecretFallback: {{ . | quote }}
29+
{{- end }}
2730
services:
2831
github:
2932
enabled: {{ .Values.services.github.enabled }}

charts/clowarden/values.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ server:
6363
privateKey: null
6464
# GitHub application webhook secret
6565
webhookSecret: null
66+
# GitHub application webhook secret fallback (handy for webhook secret rotation)
67+
webhookSecretFallback: null
6668

6769
# Ingress configuration
6870
ingress:

clowarden-server/src/handlers.rs

+33-5
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use axum::{
1919
Router,
2020
};
2121
use clowarden_core::cfg::Organization;
22-
use config::Config;
22+
use config::{Config, ConfigError};
2323
use hmac::{Hmac, Mac};
2424
use mime::APPLICATION_JSON;
2525
use octorust::types::JobStatus;
@@ -59,6 +59,7 @@ struct RouterState {
5959
db: DynDB,
6060
gh: DynGH,
6161
webhook_secret: String,
62+
webhook_secret_fallback: Option<String>,
6263
jobs_tx: mpsc::UnboundedSender<Job>,
6364
orgs: Vec<Organization>,
6465
}
@@ -71,7 +72,7 @@ pub(crate) fn setup_router(
7172
jobs_tx: mpsc::UnboundedSender<Job>,
7273
) -> Result<Router> {
7374
// Setup some paths
74-
let static_path = cfg.get_string("server.staticPath").unwrap();
75+
let static_path = cfg.get_string("server.staticPath")?;
7576
let root_index_path = Path::new(&static_path).join("index.html");
7677
let audit_path = Path::new(&static_path).join("audit");
7778
let audit_index_path = audit_path.join("index.html");
@@ -107,7 +108,12 @@ pub(crate) fn setup_router(
107108

108109
// Setup main router
109110
let orgs = cfg.get("organizations")?;
110-
let webhook_secret = cfg.get_string("server.githubApp.webhookSecret").unwrap();
111+
let webhook_secret = cfg.get_string("server.githubApp.webhookSecret")?;
112+
let webhook_secret_fallback = match cfg.get_string("server.githubApp.webhookSecretFallback") {
113+
Ok(secret) => Some(secret),
114+
Err(ConfigError::NotFound(_)) => None,
115+
Err(err) => return Err(err.into()),
116+
};
111117
let router = Router::new()
112118
.route("/webhook/github", post(event))
113119
.route("/health-check", get(health_check))
@@ -128,6 +134,7 @@ pub(crate) fn setup_router(
128134
db,
129135
gh,
130136
webhook_secret,
137+
webhook_secret_fallback,
131138
jobs_tx,
132139
orgs,
133140
});
@@ -147,15 +154,19 @@ async fn health_check() -> impl IntoResponse {
147154
async fn event(
148155
State(gh): State<DynGH>,
149156
State(webhook_secret): State<String>,
157+
State(webhook_secret_fallback): State<Option<String>>,
150158
State(jobs_tx): State<mpsc::UnboundedSender<Job>>,
151159
State(orgs): State<Vec<Organization>>,
152160
headers: HeaderMap,
153161
body: Bytes,
154162
) -> impl IntoResponse {
155163
// Verify payload signature
164+
let webhook_secret = webhook_secret.as_bytes();
165+
let webhook_secret_fallback = webhook_secret_fallback.as_ref().map(String::as_bytes);
156166
if verify_signature(
157167
headers.get(GITHUB_SIGNATURE_HEADER),
158-
webhook_secret.as_bytes(),
168+
webhook_secret,
169+
webhook_secret_fallback,
159170
&body[..],
160171
)
161172
.is_err()
@@ -279,14 +290,31 @@ async fn search_changes(State(db): State<DynDB>, RawQuery(query): RawQuery) -> i
279290
}
280291

281292
/// Verify that the signature provided is valid.
282-
fn verify_signature(signature: Option<&HeaderValue>, secret: &[u8], body: &[u8]) -> Result<()> {
293+
fn verify_signature(
294+
signature: Option<&HeaderValue>,
295+
secret: &[u8],
296+
secret_fallback: Option<&[u8]>,
297+
body: &[u8],
298+
) -> Result<()> {
283299
if let Some(signature) = signature
284300
.and_then(|s| s.to_str().ok())
285301
.and_then(|s| s.strip_prefix("sha256="))
286302
.and_then(|s| hex::decode(s).ok())
287303
{
304+
// Try primary secret
288305
let mut mac = Hmac::<Sha256>::new_from_slice(secret)?;
289306
mac.update(body);
307+
let result = mac.verify_slice(&signature[..]);
308+
if result.is_ok() {
309+
return Ok(());
310+
}
311+
if secret_fallback.is_none() {
312+
return result.map_err(Error::new);
313+
}
314+
315+
// Try fallback secret (if available)
316+
let mut mac = Hmac::<Sha256>::new_from_slice(secret_fallback.expect("secret should be set"))?;
317+
mac.update(body);
290318
mac.verify_slice(&signature[..]).map_err(Error::new)
291319
} else {
292320
Err(format_err!("no valid signature found"))

clowarden-server/src/main.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ async fn main() -> Result<()> {
9797
// Setup and launch HTTP server
9898
let router = handlers::setup_router(&cfg, db.clone(), gh.clone(), jobs_tx)
9999
.context("error setting up http server router")?;
100-
let addr: SocketAddr = cfg.get_string("server.addr").unwrap().parse()?;
100+
let addr: SocketAddr = cfg.get_string("server.addr")?.parse()?;
101101
let listener = TcpListener::bind(addr).await?;
102102
info!("server started");
103103
info!(%addr, "listening");

0 commit comments

Comments
 (0)