diff --git a/services/identity/Cargo.lock b/services/identity/Cargo.lock
--- a/services/identity/Cargo.lock
+++ b/services/identity/Cargo.lock
@@ -20,6 +20,54 @@
  "libc",
 ]
 
+[[package]]
+name = "anstream"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "anyhow"
 version = "1.0.70"
@@ -99,17 +147,6 @@
  "syn 2.0.28",
 ]
 
-[[package]]
-name = "atty"
-version = "0.2.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
-dependencies = [
- "hermit-abi 0.1.19",
- "libc",
- "winapi",
-]
-
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -637,42 +674,43 @@
 
 [[package]]
 name = "clap"
-version = "3.2.23"
+version = "4.4.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
+checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272"
 dependencies = [
- "atty",
- "bitflags",
+ "clap_builder",
  "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1"
+dependencies = [
+ "anstream",
+ "anstyle",
  "clap_lex",
- "indexmap",
- "once_cell",
  "strsim",
- "termcolor",
- "textwrap",
 ]
 
 [[package]]
 name = "clap_derive"
-version = "3.2.18"
+version = "4.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
+checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
 dependencies = [
  "heck",
- "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 2.0.28",
 ]
 
 [[package]]
 name = "clap_lex"
-version = "0.2.4"
+version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
-dependencies = [
- "os_str_bytes",
-]
+checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
 
 [[package]]
 name = "codespan-reporting"
@@ -684,6 +722,12 @@
  "unicode-width",
 ]
 
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
 [[package]]
 name = "comm-opaque2"
 version = "0.2.0"
@@ -1302,15 +1346,6 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 
-[[package]]
-name = "hermit-abi"
-version = "0.1.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
-dependencies = [
- "libc",
-]
-
 [[package]]
 name = "hermit-abi"
 version = "0.2.6"
@@ -1824,12 +1859,6 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
 
-[[package]]
-name = "os_str_bytes"
-version = "6.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
-
 [[package]]
 name = "outref"
 version = "0.5.1"
@@ -1962,30 +1991,6 @@
  "syn 1.0.109",
 ]
 
-[[package]]
-name = "proc-macro-error"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
-dependencies = [
- "proc-macro-error-attr",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "version_check",
-]
-
-[[package]]
-name = "proc-macro-error-attr"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
-dependencies = [
- "proc-macro2",
- "quote",
- "version_check",
-]
-
 [[package]]
 name = "proc-macro2"
 version = "1.0.66"
@@ -2630,12 +2635,6 @@
  "winapi-util",
 ]
 
-[[package]]
-name = "textwrap"
-version = "0.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
-
 [[package]]
 name = "thiserror"
 version = "1.0.40"
@@ -3023,6 +3022,12 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
 
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
 [[package]]
 name = "uuid"
 version = "1.3.1"
diff --git a/services/identity/Cargo.toml b/services/identity/Cargo.toml
--- a/services/identity/Cargo.toml
+++ b/services/identity/Cargo.toml
@@ -9,7 +9,7 @@
 prost = "0.11"
 tokio = { version = "1.24", features = ["macros", "rt-multi-thread"] }
 ed25519-dalek = "1"
-clap = { version = "3.1.12", features = ["derive"] }
+clap = { version = "4.4", features = ["derive", "env"] }
 derive_more = "0.99"
 aws-config = "0.54.0"
 aws-sdk-dynamodb = "0.24.0"
diff --git a/services/identity/Dockerfile b/services/identity/Dockerfile
--- a/services/identity/Dockerfile
+++ b/services/identity/Dockerfile
@@ -1,4 +1,4 @@
-FROM rust:1.67 as builder
+FROM rust:1.70-bullseye as builder
 
 RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
   build-essential cmake git libgtest-dev libssl-dev zlib1g-dev \
diff --git a/services/identity/src/config.rs b/services/identity/src/config.rs
--- a/services/identity/src/config.rs
+++ b/services/identity/src/config.rs
@@ -1,4 +1,5 @@
 use base64::{engine::general_purpose, DecodeError, Engine as _};
+use clap::{Parser, Subcommand};
 use once_cell::sync::Lazy;
 use std::{collections::HashSet, env, fmt, fs, io, path};
 use tracing::{error, info};
@@ -9,15 +10,56 @@
   TUNNELBROKER_GRPC_ENDPOINT,
 };
 
-pub static CONFIG: Lazy<Config> =
-  Lazy::new(|| Config::load().expect("failed to load config"));
+/// Raw CLI arguments, should be only used internally to create ServerConfig
+static CLI: Lazy<Cli> = Lazy::new(Cli::parse);
 
-pub(super) fn load_config() {
-  Lazy::force(&CONFIG);
+pub static CONFIG: Lazy<ServerConfig> = Lazy::new(|| {
+  ServerConfig::from_cli(&CLI).expect("Failed to load server config")
+});
+
+pub(super) fn parse_cli_command() -> &'static Command {
+  &Lazy::force(&CLI).command
+}
+
+pub(super) fn load_server_config() -> &'static ServerConfig {
+  Lazy::force(&CONFIG)
+}
+
+#[derive(Parser)]
+#[clap(author, version, about, long_about = None)]
+#[clap(propagate_version = true)]
+struct Cli {
+  #[clap(subcommand)]
+  command: Command,
+
+  /// AWS Localstack service URL
+  #[arg(long, global = true)]
+  #[arg(env = LOCALSTACK_ENDPOINT)]
+  localstack_endpoint: Option<String>,
+
+  /// Tunnelbroker gRPC endpoint
+  #[arg(long, global = true)]
+  #[arg(env = TUNNELBROKER_GRPC_ENDPOINT)]
+  #[arg(default_value = DEFAULT_TUNNELBROKER_ENDPOINT)]
+  tunnelbroker_endpoint: String,
+}
+
+#[derive(Subcommand)]
+pub enum Command {
+  /// Runs the server
+  Server,
+  /// Generates and persists a keypair to use for PAKE registration and login
+  Keygen {
+    #[arg(short, long)]
+    #[arg(default_value = SECRETS_DIRECTORY)]
+    dir: String,
+  },
+  /// Populates the `identity-users` table in DynamoDB from MySQL
+  PopulateDB,
 }
 
 #[derive(Clone)]
-pub struct Config {
+pub struct ServerConfig {
   pub localstack_endpoint: Option<String>,
   // Opaque 2.0 server secrets
   pub server_setup: comm_opaque2::ServerSetup<comm_opaque2::Cipher>,
@@ -27,44 +69,31 @@
   pub tunnelbroker_endpoint: String,
 }
 
-impl Config {
-  fn load() -> Result<Self, Error> {
-    let localstack_endpoint = env::var(LOCALSTACK_ENDPOINT).ok();
-    let tunnelbroker_endpoint = match env::var(TUNNELBROKER_GRPC_ENDPOINT) {
-      Ok(val) => {
-        info!("Using Tunnelbroker endpoint from env var: {}", val);
-        val
-      }
-      Err(std::env::VarError::NotPresent) => {
-        let val = DEFAULT_TUNNELBROKER_ENDPOINT;
-        info!("Falling back to default Tunnelbroker endpoint: {}", val);
-        val.to_string()
-      }
-      Err(e) => {
-        error!(
-          "Failed to read environment variable {}: {:?}",
-          TUNNELBROKER_GRPC_ENDPOINT, e
-        );
-        return Err(Error::Env(e));
-      }
-    };
+impl ServerConfig {
+  fn from_cli(cli: &Cli) -> Result<Self, Error> {
+    if !matches!(cli.command, Command::Server) {
+      panic!("ServerConfig is only available for the `server` command");
+    }
+
+    info!("Tunnelbroker endpoint: {}", &cli.tunnelbroker_endpoint);
+    if let Some(endpoint) = &cli.localstack_endpoint {
+      info!("Using Localstack endpoint: {}", endpoint);
+    }
 
     let mut path_buf = path::PathBuf::new();
     path_buf.push(SECRETS_DIRECTORY);
     path_buf.push(SECRETS_SETUP_FILE);
 
     let server_setup = get_server_setup(path_buf.as_path())?;
-
     let reserved_usernames = get_reserved_usernames_set()?;
-
     let keyserver_public_key = env::var(KEYSERVER_PUBLIC_KEY).ok();
 
     Ok(Self {
-      localstack_endpoint,
+      localstack_endpoint: cli.localstack_endpoint.clone(),
+      tunnelbroker_endpoint: cli.tunnelbroker_endpoint.clone(),
       server_setup,
       reserved_usernames,
       keyserver_public_key,
-      tunnelbroker_endpoint,
     })
   }
 
@@ -73,9 +102,9 @@
   }
 }
 
-impl fmt::Debug for Config {
+impl fmt::Debug for ServerConfig {
   fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-    f.debug_struct("Config")
+    f.debug_struct("ServerConfig")
       .field("server_keypair", &"** redacted **")
       .field("keyserver_auth_token", &"** redacted **")
       .field("localstack_endpoint", &self.localstack_endpoint)
diff --git a/services/identity/src/main.rs b/services/identity/src/main.rs
--- a/services/identity/src/main.rs
+++ b/services/identity/src/main.rs
@@ -1,6 +1,6 @@
 use std::time::Duration;
 
-use clap::{Parser, Subcommand};
+use config::Command;
 use database::DatabaseClient;
 use moka::future::Cache;
 use tonic::transport::Server;
@@ -23,8 +23,7 @@
 mod token;
 mod tunnelbroker;
 
-use config::load_config;
-use constants::{IDENTITY_SERVICE_SOCKET_ADDR, SECRETS_DIRECTORY};
+use constants::IDENTITY_SERVICE_SOCKET_ADDR;
 use cors::cors_layer;
 use keygen::generate_and_persist_keypair;
 use tracing::{self, info, Level};
@@ -34,28 +33,6 @@
 use grpc_services::authenticated::auth_proto::identity_client_service_server::IdentityClientServiceServer as AuthServer;
 use grpc_services::authenticated::AuthenticatedService;
 
-#[derive(Parser)]
-#[clap(author, version, about, long_about = None)]
-#[clap(propagate_version = true)]
-struct Cli {
-  #[clap(subcommand)]
-  command: Commands,
-}
-
-#[derive(Subcommand)]
-enum Commands {
-  /// Runs the server
-  Server,
-  /// Generates and persists a keypair to use for PAKE registration and login
-  Keygen {
-    #[clap(short, long)]
-    #[clap(default_value_t = String::from(SECRETS_DIRECTORY))]
-    dir: String,
-  },
-  /// Populates the `identity-users` table in DynamoDB from MySQL
-  PopulateDB,
-}
-
 #[tokio::main]
 async fn main() -> Result<(), Box<dyn std::error::Error>> {
   let filter = EnvFilter::builder()
@@ -66,13 +43,12 @@
   let subscriber = tracing_subscriber::fmt().with_env_filter(filter).finish();
 
   tracing::subscriber::set_global_default(subscriber)?;
-  let cli = Cli::parse();
-  match &cli.command {
-    Commands::Keygen { dir } => {
+  match config::parse_cli_command() {
+    Command::Keygen { dir } => {
       generate_and_persist_keypair(dir)?;
     }
-    Commands::Server => {
-      load_config();
+    Command::Server => {
+      config::load_server_config();
       let addr = IDENTITY_SERVICE_SOCKET_ADDR.parse()?;
       let aws_config = aws_config::from_env().region("us-east-2").load().await;
       let database_client = DatabaseClient::new(&aws_config);
@@ -103,7 +79,7 @@
         .serve(addr)
         .await?;
     }
-    Commands::PopulateDB => unimplemented!(),
+    Command::PopulateDB => unimplemented!(),
   }
 
   Ok(())