@@ -29,16 +29,16 @@ const DAEMON_BINARIES: &[(&str, &str)] = &[
2929] ;
3030
3131pub ( crate ) async fn cmd_install ( mut o : InstallOpts ) -> Result < ( ) > {
32- // --expose is not safe yet: the rendered vmm.toml binds the VM-control
33- // plane with neither TLS nor an auth token (the management RPCs are not
34- // behind an auth guard), so exposing it would hand deploy/destroy to anyone
35- // who can reach the IP. Refuse until the TLS+token transport lands; the
36- // supported path is localhost + an SSH tunnel.
32+ // --expose is not safe yet: the rendered vmm.toml now gates the management
33+ // API behind a generated token, but the transport is still plain HTTP, so
34+ // exposing it would send that bearer token in cleartext to anyone on-path.
35+ // Refuse until the TLS transport lands; the supported path is localhost + an
36+ // SSH tunnel.
3737 if let Some ( ip) = & o. expose {
3838 bail ! (
3939 "--expose {ip} is not yet safe: it would bind the VM-control plane on \
40- {ip}:{port} with no TLS and no auth. reach the dashboard over an SSH \
41- tunnel instead: ssh -L {port}:127.0.0.1:{port} <host>",
40+ {ip}:{port} over plain HTTP, leaking the API token in cleartext. reach \
41+ the dashboard over an SSH tunnel instead: ssh -L {port}:127.0.0.1:{port} <host>",
4242 port = o. dashboard_port
4343 ) ;
4444 }
@@ -81,10 +81,24 @@ pub(crate) async fn cmd_install(mut o: InstallOpts) -> Result<()> {
8181 let client_url = format ! ( "http://{bind}:{}" , o. dashboard_port) ;
8282 let kms_port = resolve_kms_port ( & o, & st) ?;
8383
84+ // management-API token: reuse the one a prior install wrote (so re-runs and
85+ // an already-running VMM keep matching credentials), else mint a fresh one
86+ // below once the config dir exists. The existing token authenticates the
87+ // preflight probe against an already-running, auth-enabled VMM.
88+ let token_path = layout. config_dir . join ( "vmm-auth-token" ) ;
89+ let existing_token = read_token_file ( & token_path) ;
90+
8491 // 4. preflight - fail BEFORE any side effect (image download, key provider,
8592 // dirs, units), so a CID/port clash can't half-install the host.
8693 let cid_start = pick_cid_start ( o. cid_start , & host:: occupied_cid_ranges ( ) ) ?;
87- let kms_owned = kms_port_owned ( & st, & client_url, kms_port, o. no_kms ) . await ;
94+ let kms_owned = kms_port_owned (
95+ & st,
96+ & client_url,
97+ existing_token. as_deref ( ) ,
98+ kms_port,
99+ o. no_kms ,
100+ )
101+ . await ;
88102 let port_plan = tcp_port_plan ( & o, & st, platform, & bind, & client_url, kms_port, kms_owned) ;
89103 preflight_ports ( & port_plan) ?;
90104
@@ -117,6 +131,15 @@ pub(crate) async fn cmd_install(mut o: InstallOpts) -> Result<()> {
117131 fs:: create_dir_all ( & dir) . with_context ( || format ! ( "creating {}" , dir. display( ) ) ) ?;
118132 }
119133
134+ // ensure a management-API token exists on disk (0600) before rendering the
135+ // config that references it. The local `dstack` CLI reads this path from the
136+ // install state, so it authenticates automatically.
137+ let vmm_token = match existing_token {
138+ Some ( t) => t,
139+ None => generate_vmm_token ( ) ?,
140+ } ;
141+ write_token_file ( & token_path, & vmm_token) ?;
142+
120143 // 8. resolve the key provider - run our own unless told to use an existing
121144 // one (TDX only; SNP has no SGX local provider).
122145 let ( kp_addr, kp_port, kp_own_project) =
@@ -144,6 +167,8 @@ pub(crate) async fn cmd_install(mut o: InstallOpts) -> Result<()> {
144167 key_provider_port : kp_port as u32 ,
145168 kms_urls : kms_urls. clone ( ) ,
146169 platform,
170+ auth_enabled : true ,
171+ auth_token : vmm_token. clone ( ) ,
147172 ..Default :: default ( )
148173 } ) ;
149174 // the KMS-in-CVM reaches the host auth webhook at 10.0.2.2:<auth_port>.
@@ -191,6 +216,7 @@ pub(crate) async fn cmd_install(mut o: InstallOpts) -> Result<()> {
191216 st. run_dir = layout. run_dir . display ( ) . to_string ( ) ;
192217 st. allowlist_path = allow_path. display ( ) . to_string ( ) ;
193218 st. client_url = client_url. clone ( ) ;
219+ st. client_token_path = token_path. display ( ) . to_string ( ) ;
194220 st. auth_port = o. auth_port ;
195221 st. platform = platform. vmm_str ( ) . to_string ( ) ;
196222 st. image = o. image . clone ( ) ;
@@ -215,7 +241,7 @@ pub(crate) async fn cmd_install(mut o: InstallOpts) -> Result<()> {
215241 st. auth_unit = auth_unit. clone ( ) ;
216242
217243 // 11. VMM systemd unit (idempotent).
218- if vmm_reachable ( & client_url) . await {
244+ if vmm_reachable ( & client_url, Some ( & vmm_token ) ) . await {
219245 println ! ( " [ok] VMM already serving at {client_url}" ) ;
220246 } else {
221247 install_unit (
@@ -225,7 +251,7 @@ pub(crate) async fn cmd_install(mut o: InstallOpts) -> Result<()> {
225251 . context ( "installing the VMM unit" ) ?;
226252 println ! ( " [ok] started {vmm_unit}.service" ) ;
227253 print ! ( " [..] waiting for VMM at {client_url} " ) ;
228- if wait_ready ( & client_url, Duration :: from_secs ( 25 ) ) . await {
254+ if wait_ready ( & client_url, Some ( & vmm_token ) , Duration :: from_secs ( 25 ) ) . await {
229255 println ! ( "=> ready" ) ;
230256 } else {
231257 println ! ( "=> not ready within timeout (journalctl -u {vmm_unit})" ) ;
@@ -241,7 +267,7 @@ pub(crate) async fn cmd_install(mut o: InstallOpts) -> Result<()> {
241267 if o. no_kms {
242268 println ! ( " (--no-kms: skipping KMS deploy)" ) ;
243269 } else {
244- let vmm = Vmm :: connect ( & client_url) ?;
270+ let vmm = Vmm :: connect_with_token ( & client_url, Some ( & vmm_token ) ) ?;
245271 let existing = match & st. kms_vm_id {
246272 Some ( id) if vmm. has_vm ( id) . await => Some ( id. clone ( ) ) ,
247273 _ => None ,
@@ -1007,7 +1033,13 @@ fn tcp_port_plan(
10071033 ports
10081034}
10091035
1010- async fn kms_port_owned ( st : & State , client_url : & str , kms_port : u16 , no_kms : bool ) -> bool {
1036+ async fn kms_port_owned (
1037+ st : & State ,
1038+ client_url : & str ,
1039+ token : Option < & str > ,
1040+ kms_port : u16 ,
1041+ no_kms : bool ,
1042+ ) -> bool {
10111043 if no_kms
10121044 || st. client_url != client_url
10131045 || state_kms_port ( st) != Some ( kms_port)
@@ -1019,7 +1051,7 @@ async fn kms_port_owned(st: &State, client_url: &str, kms_port: u16, no_kms: boo
10191051 let Some ( kms_vm_id) = & st. kms_vm_id else {
10201052 return false ;
10211053 } ;
1022- match Vmm :: connect ( client_url) {
1054+ match Vmm :: connect_with_token ( client_url, token ) {
10231055 Ok ( vmm) => vmm. has_vm ( kms_vm_id) . await ,
10241056 Err ( _) => false ,
10251057 }
@@ -1158,18 +1190,18 @@ fn kms_vm_boot_state(
11581190}
11591191
11601192/// one-shot liveness probe of the VMM.
1161- async fn vmm_reachable ( client_url : & str ) -> bool {
1162- match Vmm :: connect ( client_url) {
1193+ async fn vmm_reachable ( client_url : & str , token : Option < & str > ) -> bool {
1194+ match Vmm :: connect_with_token ( client_url, token ) {
11631195 Ok ( vmm) => vmm. status ( ) . await . is_ok ( ) ,
11641196 Err ( _) => false ,
11651197 }
11661198}
11671199
11681200/// poll the VMM `Status` RPC until it succeeds or the deadline passes.
1169- async fn wait_ready ( client_url : & str , timeout : Duration ) -> bool {
1201+ async fn wait_ready ( client_url : & str , token : Option < & str > , timeout : Duration ) -> bool {
11701202 let deadline = tokio:: time:: Instant :: now ( ) + timeout;
11711203 loop {
1172- if let Ok ( vmm) = Vmm :: connect ( client_url) {
1204+ if let Ok ( vmm) = Vmm :: connect_with_token ( client_url, token ) {
11731205 if vmm. status ( ) . await . is_ok ( ) {
11741206 return true ;
11751207 }
@@ -1181,6 +1213,33 @@ async fn wait_ready(client_url: &str, timeout: Duration) -> bool {
11811213 }
11821214}
11831215
1216+ /// mint a 256-bit management-API token, hex-encoded, from the OS RNG.
1217+ fn generate_vmm_token ( ) -> Result < String > {
1218+ let mut buf = [ 0u8 ; 32 ] ;
1219+ let mut f = fs:: File :: open ( "/dev/urandom" ) . context ( "opening /dev/urandom" ) ?;
1220+ std:: io:: Read :: read_exact ( & mut f, & mut buf) . context ( "reading /dev/urandom" ) ?;
1221+ Ok ( hex:: encode ( buf) )
1222+ }
1223+
1224+ /// read a previously written management-API token, if the file exists and is
1225+ /// non-empty.
1226+ pub ( crate ) fn read_token_file ( path : & Path ) -> Option < String > {
1227+ let token = fs:: read_to_string ( path) . ok ( ) ?;
1228+ let token = token. trim ( ) ;
1229+ ( !token. is_empty ( ) ) . then ( || token. to_string ( ) )
1230+ }
1231+
1232+ /// write the management-API token atomically with owner-only (0600)
1233+ /// permissions — it is a bearer credential.
1234+ fn write_token_file ( path : & Path , token : & str ) -> Result < ( ) > {
1235+ use std:: os:: unix:: fs:: PermissionsExt ;
1236+ dstack_cli_core:: fsutil:: write_atomic ( path, token)
1237+ . with_context ( || format ! ( "writing {}" , path. display( ) ) ) ?;
1238+ fs:: set_permissions ( path, fs:: Permissions :: from_mode ( 0o600 ) )
1239+ . with_context ( || format ! ( "setting 0600 on {}" , path. display( ) ) ) ?;
1240+ Ok ( ( ) )
1241+ }
1242+
11841243#[ cfg( test) ]
11851244mod tests {
11861245 use super :: * ;
0 commit comments