Refresh GitHub Actions JWT in the background
GitHub Actions JWTs are only valid for 5 minutes after being issued. FlakeHub uses these JWTs for authentication, which means that after those 5 minutes have passed and the token is expired, FlakeHub (and by extension FlakeHub Cache) will no longer allow requests using this token. However, GitHub gives us a way to repeatedly request new tokens, so we utilize that and refresh the token every 2 minutes (less than half of the lifetime of the token).
This commit is contained in:
parent
a59a765f73
commit
0434d467d3
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -232,7 +232,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attic"
|
name = "attic"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/DeterminateSystems/attic?branch=fixups-for-magic-nix-cache#aa0a6b9b59bc54070e0e97bfb84053f81fbef205"
|
source = "git+https://github.com/DeterminateSystems/attic?branch=fixups-for-magic-nix-cache#536a0614711754ccfd3df050025b5aff612635ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"base64",
|
"base64",
|
||||||
|
@ -262,7 +262,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attic-client"
|
name = "attic-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/DeterminateSystems/attic?branch=fixups-for-magic-nix-cache#aa0a6b9b59bc54070e0e97bfb84053f81fbef205"
|
source = "git+https://github.com/DeterminateSystems/attic?branch=fixups-for-magic-nix-cache#536a0614711754ccfd3df050025b5aff612635ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-channel",
|
"async-channel",
|
||||||
|
@ -293,7 +293,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attic-server"
|
name = "attic-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/DeterminateSystems/attic?branch=fixups-for-magic-nix-cache#aa0a6b9b59bc54070e0e97bfb84053f81fbef205"
|
source = "git+https://github.com/DeterminateSystems/attic?branch=fixups-for-magic-nix-cache#536a0614711754ccfd3df050025b5aff612635ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
|
@ -344,7 +344,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attic-token"
|
name = "attic-token"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/DeterminateSystems/attic?branch=fixups-for-magic-nix-cache#aa0a6b9b59bc54070e0e97bfb84053f81fbef205"
|
source = "git+https://github.com/DeterminateSystems/attic?branch=fixups-for-magic-nix-cache#536a0614711754ccfd3df050025b5aff612635ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"attic",
|
"attic",
|
||||||
"base64",
|
"base64",
|
||||||
|
|
|
@ -7,12 +7,14 @@ use attic_client::{
|
||||||
config::ServerConfig,
|
config::ServerConfig,
|
||||||
push::{PushConfig, Pusher},
|
push::{PushConfig, Pusher},
|
||||||
};
|
};
|
||||||
|
use reqwest::header::HeaderValue;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const USER_AGENT: &str = "magic-nix-cache";
|
const USER_AGENT: &str = "magic-nix-cache";
|
||||||
|
@ -62,7 +64,7 @@ pub async fn init_cache(
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let flakehub_password = flakehub_netrc_entry.password.as_ref().ok_or_else(|| {
|
let flakehub_password = flakehub_netrc_entry.password.ok_or_else(|| {
|
||||||
Error::Config(format!(
|
Error::Config(format!(
|
||||||
"netrc file does not contain a password for '{}'",
|
"netrc file does not contain a password for '{}'",
|
||||||
flakehub_api_server
|
flakehub_api_server
|
||||||
|
@ -91,6 +93,32 @@ pub async fn init_cache(
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let server_config = ServerConfig {
|
||||||
|
endpoint: flakehub_cache_server.to_string(),
|
||||||
|
token: Some(attic_client::config::ServerTokenConfig::Raw {
|
||||||
|
token: flakehub_password.clone(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let api_inner = ApiClient::from_server_config(server_config)?;
|
||||||
|
let api = Arc::new(RwLock::new(api_inner));
|
||||||
|
|
||||||
|
// NOTE(cole-h): This is a workaround -- at the time of writing, GitHub Actions JWTs are only
|
||||||
|
// valid for 5 minutes after being issued. FlakeHub uses these JWTs for authentication, which
|
||||||
|
// means that after those 5 minutes have passed and the token is expired, FlakeHub (and by
|
||||||
|
// extension FlakeHub Cache) will no longer allow requests using this token. However, GitHub
|
||||||
|
// gives us a way to repeatedly request new tokens, so we utilize that and refresh the token
|
||||||
|
// every 2 minutes (less than half of the lifetime of the token).
|
||||||
|
let netrc_path_clone = flakehub_api_server_netrc.to_path_buf();
|
||||||
|
let initial_github_jwt_clone = flakehub_password.clone();
|
||||||
|
let flakehub_cache_server_clone = flakehub_cache_server.to_string();
|
||||||
|
let api_clone = api.clone();
|
||||||
|
tokio::task::spawn(refresh_github_actions_jwt_worker(
|
||||||
|
netrc_path_clone,
|
||||||
|
initial_github_jwt_clone,
|
||||||
|
flakehub_cache_server_clone,
|
||||||
|
api_clone,
|
||||||
|
));
|
||||||
|
|
||||||
// Get the cache UUID for this project.
|
// Get the cache UUID for this project.
|
||||||
let cache_name = {
|
let cache_name = {
|
||||||
let url = flakehub_api_server
|
let url = flakehub_api_server
|
||||||
|
@ -100,7 +128,7 @@ pub async fn init_cache(
|
||||||
let response = reqwest::Client::new()
|
let response = reqwest::Client::new()
|
||||||
.get(url.to_owned())
|
.get(url.to_owned())
|
||||||
.header("User-Agent", USER_AGENT)
|
.header("User-Agent", USER_AGENT)
|
||||||
.basic_auth(flakehub_login, Some(flakehub_password))
|
.basic_auth(flakehub_login, Some(&flakehub_password))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -129,16 +157,7 @@ pub async fn init_cache(
|
||||||
|
|
||||||
let cache = unsafe { CacheName::new_unchecked(cache_name) };
|
let cache = unsafe { CacheName::new_unchecked(cache_name) };
|
||||||
|
|
||||||
let api = ApiClient::from_server_config(ServerConfig {
|
let cache_config = api.read().await.get_cache_config(&cache).await?;
|
||||||
endpoint: flakehub_cache_server.to_string(),
|
|
||||||
token: flakehub_netrc_entry
|
|
||||||
.password
|
|
||||||
.map(|token| attic_client::config::ServerTokenConfig::Raw { token })
|
|
||||||
.as_ref()
|
|
||||||
.cloned(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let cache_config = api.get_cache_config(&cache).await?;
|
|
||||||
|
|
||||||
let push_config = PushConfig {
|
let push_config = PushConfig {
|
||||||
num_workers: 5, // FIXME: use number of CPUs?
|
num_workers: 5, // FIXME: use number of CPUs?
|
||||||
|
@ -160,10 +179,12 @@ pub async fn init_cache(
|
||||||
ignore_upstream_cache_filter: false,
|
ignore_upstream_cache_filter: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(State {
|
let state = State {
|
||||||
substituter: flakehub_cache_server.to_owned(),
|
substituter: flakehub_cache_server.to_owned(),
|
||||||
push_session,
|
push_session,
|
||||||
})
|
};
|
||||||
|
|
||||||
|
Ok(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn enqueue_paths(state: &State, store_paths: Vec<StorePath>) -> Result<()> {
|
pub async fn enqueue_paths(state: &State, store_paths: Vec<StorePath>) -> Result<()> {
|
||||||
|
@ -171,3 +192,110 @@ pub async fn enqueue_paths(state: &State, store_paths: Vec<StorePath>) -> Result
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Refresh the GitHub Actions JWT every 2 minutes (slightly less than half of the default validity
|
||||||
|
/// period) to ensure pushing / pulling doesn't stop working.
|
||||||
|
async fn refresh_github_actions_jwt_worker(
|
||||||
|
netrc_path: std::path::PathBuf,
|
||||||
|
mut github_jwt: String,
|
||||||
|
flakehub_cache_server_clone: String,
|
||||||
|
api: Arc<RwLock<ApiClient>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// TODO(cole-h): this should probably be half of the token's lifetime ((exp - iat) / 2), but
|
||||||
|
// getting this is nontrivial so I'm not going to do it until GitHub changes the lifetime and
|
||||||
|
// breaks this.
|
||||||
|
let next_refresh = std::time::Duration::from_secs(2 * 60);
|
||||||
|
|
||||||
|
// NOTE(cole-h): https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers#requesting-the-jwt-using-environment-variables
|
||||||
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
reqwest::header::ACCEPT,
|
||||||
|
HeaderValue::from_static("application/json;api-version=2.0"),
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
reqwest::header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static("application/json"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let github_client = reqwest::Client::builder()
|
||||||
|
.user_agent(USER_AGENT)
|
||||||
|
.default_headers(headers)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match rewrite_github_actions_token(&github_client, &netrc_path, &github_jwt).await {
|
||||||
|
Ok(new_github_jwt) => {
|
||||||
|
github_jwt = new_github_jwt;
|
||||||
|
|
||||||
|
let server_config = ServerConfig {
|
||||||
|
endpoint: flakehub_cache_server_clone.clone(),
|
||||||
|
token: Some(attic_client::config::ServerTokenConfig::Raw {
|
||||||
|
token: github_jwt.clone(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let new_api = ApiClient::from_server_config(server_config)?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut api_client = api.write().await;
|
||||||
|
*api_client = new_api;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"Stored new token in netrc and API client, sleeping for {next_refresh:?}"
|
||||||
|
);
|
||||||
|
tokio::time::sleep(next_refresh).await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
?e,
|
||||||
|
"Failed to get a new JWT from GitHub, trying again in 10 seconds"
|
||||||
|
);
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rewrite_github_actions_token(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
netrc_path: &Path,
|
||||||
|
old_github_jwt: &str,
|
||||||
|
) -> Result<String> {
|
||||||
|
// NOTE(cole-h): https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers#requesting-the-jwt-using-environment-variables
|
||||||
|
let runtime_token = std::env::var("ACTIONS_ID_TOKEN_REQUEST_TOKEN").map_err(|e| {
|
||||||
|
Error::Internal(format!(
|
||||||
|
"ACTIONS_ID_TOKEN_REQUEST_TOKEN was invalid unicode: {e}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let runtime_url = std::env::var("ACTIONS_ID_TOKEN_REQUEST_URL").map_err(|e| {
|
||||||
|
Error::Internal(format!(
|
||||||
|
"ACTIONS_ID_TOKEN_REQUEST_URL was invalid unicode: {e}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct TokenResponse {
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: TokenResponse = client
|
||||||
|
.request(
|
||||||
|
reqwest::Method::GET,
|
||||||
|
format!("{runtime_url}&audience=api.flakehub.com"),
|
||||||
|
)
|
||||||
|
.bearer_auth(runtime_token)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let new_github_jwt_string = res.value;
|
||||||
|
|
||||||
|
let netrc_contents = tokio::fs::read_to_string(netrc_path).await?;
|
||||||
|
let new_netrc_contents = netrc_contents.replace(old_github_jwt, &new_github_jwt_string);
|
||||||
|
let netrc_path_new = tempfile::NamedTempFile::new()?;
|
||||||
|
tokio::fs::write(&netrc_path_new, new_netrc_contents).await?;
|
||||||
|
tokio::fs::rename(&netrc_path_new, netrc_path).await?;
|
||||||
|
|
||||||
|
Ok(new_github_jwt_string)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue