@@ -32,6 +32,7 @@ use ddkrpc::{InfoRequest, InfoResponse};
3232use opts:: NodeOpts ;
3333use std:: str:: FromStr ;
3434use std:: sync:: Arc ;
35+ use tonic:: service:: Interceptor ;
3536use tonic:: transport:: Server ;
3637use tonic:: Request ;
3738use tonic:: Response ;
@@ -40,6 +41,44 @@ use tonic::{async_trait, Code};
4041
4142type Ddk = DlcDevKit < NostrDlc , PostgresStore , KormirOracleClient > ;
4243
44+ /// API key authentication interceptor for gRPC requests.
45+ ///
46+ /// This interceptor checks for an API key in the `x-api-key` metadata header.
47+ /// If an API key is configured, all requests must include a matching key.
48+ /// If no API key is configured, all requests are allowed (for localhost-only deployments).
49+ #[ derive( Clone ) ]
50+ pub struct ApiKeyInterceptor {
51+ api_key : Option < String > ,
52+ }
53+
54+ impl ApiKeyInterceptor {
55+ pub fn new ( api_key : Option < String > ) -> Self {
56+ Self { api_key }
57+ }
58+ }
59+
60+ impl Interceptor for ApiKeyInterceptor {
61+ fn call ( & mut self , req : Request < ( ) > ) -> Result < Request < ( ) > , Status > {
62+ // If no API key is configured, allow all requests
63+ let Some ( expected_key) = & self . api_key else {
64+ return Ok ( req) ;
65+ } ;
66+
67+ // Check for API key in metadata
68+ let metadata = req. metadata ( ) ;
69+ match metadata. get ( "x-api-key" ) {
70+ Some ( key) => {
71+ if key. to_str ( ) . unwrap_or ( "" ) == expected_key {
72+ Ok ( req)
73+ } else {
74+ Err ( Status :: unauthenticated ( "Invalid API key" ) )
75+ }
76+ }
77+ None => Err ( Status :: unauthenticated ( "Missing API key" ) ) ,
78+ }
79+ }
80+ }
81+
4382#[ derive( Clone ) ]
4483pub struct DdkNode {
4584 pub node : Arc < Ddk > ,
@@ -53,6 +92,18 @@ impl DdkNode {
5392 }
5493
5594 pub async fn serve ( opts : NodeOpts ) -> anyhow:: Result < ( ) > {
95+ // Validate security configuration
96+ let is_localhost =
97+ opts. grpc_host . starts_with ( "127.0.0.1" ) || opts. grpc_host . starts_with ( "localhost" ) ;
98+
99+ if !is_localhost && opts. api_key . is_none ( ) {
100+ anyhow:: bail!(
101+ "API key is required when binding to non-localhost address ({}). \
102+ Use --api-key to set an API key or bind to 127.0.0.1 for local-only access.",
103+ opts. grpc_host
104+ ) ;
105+ }
106+
56107 let logger = Arc :: new ( Logger :: console (
57108 "console_logger" . to_string ( ) ,
58109 LogLevel :: Info ,
@@ -102,15 +153,25 @@ impl DdkNode {
102153 ddk. start ( ) ?;
103154 let node = DdkNode :: new ( ddk) ;
104155 let node_stop = node. node . clone ( ) ;
156+
157+ // Create API key interceptor
158+ let interceptor = ApiKeyInterceptor :: new ( opts. api_key . clone ( ) ) ;
159+
105160 let server = Server :: builder ( )
106- . add_service ( DdkRpcServer :: new ( node) )
161+ . add_service ( DdkRpcServer :: with_interceptor ( node, interceptor ) )
107162 . serve_with_shutdown ( opts. grpc_host . parse ( ) ?, async {
108163 tokio:: signal:: ctrl_c ( )
109164 . await
110165 . expect ( "Failed to install Ctrl+C signal handler" ) ;
111166 let _ = node_stop. stop ( ) ;
112167 } ) ;
113168
169+ if opts. api_key . is_some ( ) {
170+ tracing:: info!( "gRPC server starting with API key authentication enabled" ) ;
171+ } else {
172+ tracing:: info!( "gRPC server starting without authentication (localhost only)" ) ;
173+ }
174+
114175 server. await ?;
115176
116177 Ok ( ( ) )
0 commit comments