Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5695e46
feat(env): add environment variables for Docker registry configuration
Leay15 Feb 1, 2026
844c468
feat(error): add Serde JSON error variant to error handling
Leay15 Feb 1, 2026
dae3566
feat(auth): implement login and logout functionality for Docker regis…
Leay15 Feb 1, 2026
db2169e
feat(docker): add modules for Docker configuration and registry authe…
Leay15 Feb 1, 2026
55cfe31
feat(auth): enhance login and logout commands with registry and crede…
Leay15 Feb 1, 2026
999f340
feat(docker): update registry initialization to support anonymous aut…
Leay15 Feb 1, 2026
87e5c7d
feat(auth): extend login and logout commands with registry options an…
Leay15 Feb 1, 2026
3220bac
feat(docker): integrate registry authentication into OciClient config…
Leay15 Feb 1, 2026
aa1b785
feat(docker): update CLI documentation for private image pulling and …
Leay15 Feb 1, 2026
e56a596
feat(docker): implement Docker config reader for registry authentication
Leay15 Feb 1, 2026
7532ab8
feat(docker): add registry authentication persistence helpers for sto…
Leay15 Feb 1, 2026
a8a6d3a
feat(docker): add integration tests for registry authentication with …
Leay15 Feb 1, 2026
a3698d7
feat(docker): add usage examples for Docker config and registry auth …
Leay15 Feb 1, 2026
c179ffd
feat(docker): update logging for saved registry credentials to indica…
Leay15 Feb 1, 2026
80972fc
feat(docker): remove TODO for supporting credsStore and credHelpers i…
Leay15 Feb 1, 2026
aadfbc4
feat(docker): clean up code formatting and improve readability in con…
Leay15 Feb 1, 2026
6aed59b
feat(docker): add note about credential storage and validation for ms…
Leay15 Feb 1, 2026
7f2d36f
feat(docker): reorder imports for improved organization in registry a…
Leay15 Feb 1, 2026
c706f7d
feat(docker): reorganize imports for better clarity in image and regi…
Leay15 Feb 1, 2026
7fb695e
feat(docker): add base64, serde, and serde_json dependencies to Cargo…
Leay15 Feb 1, 2026
ad82072
feat(auth): implement registry authentication resolution and normaliz…
Leay15 Feb 9, 2026
5294dfa
feat(auth): add compatibility re-exports for registry authentication …
Leay15 Feb 9, 2026
e755eff
feat(auth): normalize registry host in resolve_registry_host function
Leay15 Feb 9, 2026
ea79fba
feat(auth): integrate registry authentication resolution in image han…
Leay15 Feb 9, 2026
2364a5b
feat(image): add image module to the project
Leay15 Feb 9, 2026
f86fa1b
feat(auth): expose registry authentication functions in the module
Leay15 Feb 9, 2026
7a5e91c
feat(cli): clarify credential handling and environment variable prece…
Leay15 Feb 9, 2026
ae43443
fix(auth): clean up import formatting in auth and mod files
Leay15 Feb 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions docs/references/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,15 @@ msb pull [--image] [--image-group] <name> [options]
**Examples:**

```bash
# Pull a private image using env credentials (token)
export MSB_REGISTRY_TOKEN=token123
msb pull registry.example.com/org/app:1.0

# Pull a private image using env credentials (username/password)
export MSB_REGISTRY_USERNAME=user
export MSB_REGISTRY_PASSWORD=pass
msb pull registry.example.com/org/app:1.0

# Pull an image
msb pull --image python:3.11

Expand Down Expand Up @@ -686,6 +695,67 @@ msb push myapp:latest

===

==- `msb login`
Set registry credentials (persisted in microsandbox home).

```bash
msb login [registry] [--username <user>] [--password-stdin] [--token <token>]
```

| Option | Description |
| ----------------- | ---------------------------------- |
| `--username` | Registry username |
| `--password-stdin`| Read password from stdin |
| `--token` | Registry access token (bearer) |

**Examples:**

```bash
# Provide a token
msb login ghcr.io --token token123

# Provide username and password via stdin
echo "pass" | msb login docker.io --username user --password-stdin

# Use env fallback if CLI is invalid
export MSB_REGISTRY_TOKEN=token123
msb login ghcr.io --username user --password-stdin
```

!!!note
`msb login` stores credentials locally but does not validate them against the registry.
When pulling images, environment variables take priority over stored credentials.
!!!

!!!warning Security
Credentials are stored in `~/.microsandbox/registry_auth.json`. Restrict file permissions and avoid sharing it.
!!!

===

==- `msb logout`
Remove stored registry credentials.

```bash
msb logout [registry] [--all]
```

| Option | Description |
| --------- | ---------------------------------------- |
| `--all` | Remove all stored registry credentials |

**Examples:**

```bash
# Remove credentials for a registry
msb logout ghcr.io

# Remove all stored credentials
msb logout --all
```

===

---

### Maintenance
Expand Down
163 changes: 156 additions & 7 deletions microsandbox-cli/bin/msb/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ use microsandbox_core::{
config::{self, Component, ComponentType, SandboxConfig},
home, menv, orchestra, sandbox, toolchain,
},
oci::Reference,
oci::{Reference, normalize_registry_host},
};
use microsandbox_server::MicrosandboxServerResult;
use microsandbox_utils::{NAMESPACES_SUBDIR, env};
use microsandbox_utils::{
NAMESPACES_SUBDIR, StoredRegistryCredentials, clear_registry_credentials, env,
remove_registry_credentials, store_registry_credentials,
};
use std::{collections::HashMap, path::PathBuf};
use tokio::io::{self, AsyncReadExt};
use typed_path::Utf8UnixPathBuf;

//--------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -671,11 +675,60 @@ pub async fn server_status_subcommand(
Ok(())
}

pub async fn login_subcommand() -> MicrosandboxCliResult<()> {
println!(
"{} login functionality is not yet implemented",
"error:".error()
);
pub async fn login_subcommand(
registry: Option<String>,
username: Option<String>,
password_stdin: bool,
token: Option<String>,
) -> MicrosandboxCliResult<()> {
let registry = resolve_registry_host(registry);
let creds = resolve_login_credentials(username, password_stdin, token).await?;

match creds {
LoginCredentials::Basic { username, password } => {
store_registry_credentials(
&registry,
StoredRegistryCredentials::Basic { username, password },
)
.map_err(|err| MicrosandboxCliError::ConfigError(err.to_string()))?;
println!(
"info: credentials saved for registry {} (not validated)",
registry
);
}
LoginCredentials::Token { token } => {
store_registry_credentials(&registry, StoredRegistryCredentials::Token { token })
.map_err(|err| MicrosandboxCliError::ConfigError(err.to_string()))?;
println!(
"info: token saved for registry {} (not validated)",
registry
);
}
}

Ok(())
}

pub async fn logout_subcommand(registry: Option<String>, all: bool) -> MicrosandboxCliResult<()> {
if all {
clear_registry_credentials()
.map_err(|err| MicrosandboxCliError::ConfigError(err.to_string()))?;
println!("info: cleared all stored registry credentials");
return Ok(());
}

let registry = resolve_registry_host(registry);
let removed = remove_registry_credentials(&registry)
.map_err(|err| MicrosandboxCliError::ConfigError(err.to_string()))?;
if removed {
println!("info: removed stored credentials for registry {}", registry);
} else {
println!(
"info: no stored credentials found for registry {}",
registry
);
}

Ok(())
}

Expand Down Expand Up @@ -741,6 +794,102 @@ fn parse_name_and_script(name_and_script: &str) -> (&str, Option<&str>) {
(name, script)
}

//--------------------------------------------------------------------------------------------------
// Functions: Login Helpers
//--------------------------------------------------------------------------------------------------

enum LoginCredentials {
Basic { username: String, password: String },
Token { token: String },
}

fn resolve_registry_host(registry: Option<String>) -> String {
let host = registry
.or_else(env::get_registry_host)
.unwrap_or_else(env::get_oci_registry);

normalize_registry_host(&host)
}

async fn resolve_login_credentials(
username: Option<String>,
password_stdin: bool,
token: Option<String>,
) -> MicrosandboxCliResult<LoginCredentials> {
let cli_password = if password_stdin {
Some(read_password_from_stdin().await?)
} else {
None
};

let cli_provided = token.is_some() || username.is_some() || password_stdin;
if cli_provided {
let cli_result = if token.is_some() && (username.is_some() || cli_password.is_some()) {
Err(MicrosandboxCliError::InvalidArgument(
"token cannot be combined with username/password".to_string(),
))
} else if let Some(token) = token {
Ok(LoginCredentials::Token { token })
} else {
match (username, cli_password) {
(Some(username), Some(password)) => {
Ok(LoginCredentials::Basic { username, password })
}
(None, None) => Err(MicrosandboxCliError::InvalidArgument(
"no credentials provided; use flags or environment variables".to_string(),
)),
_ => Err(MicrosandboxCliError::InvalidArgument(
"both username and password are required".to_string(),
)),
}
};

if let Ok(creds) = cli_result {
return Ok(creds);
}

// CLI was provided but invalid; attempt env fallback.
tracing::debug!("login: CLI credentials invalid, falling back to environment variables");
}

let env_token = env::get_registry_token();
let env_username = env::get_registry_username();
let env_password = env::get_registry_password();

if env_token.is_some() && (env_username.is_some() || env_password.is_some()) {
return Err(MicrosandboxCliError::InvalidArgument(
"token cannot be combined with username/password".to_string(),
));
}

if let Some(token) = env_token {
return Ok(LoginCredentials::Token { token });
}

match (env_username, env_password) {
(Some(username), Some(password)) => Ok(LoginCredentials::Basic { username, password }),
(None, None) => Err(MicrosandboxCliError::InvalidArgument(
"no credentials provided; use flags or environment variables".to_string(),
)),
_ => Err(MicrosandboxCliError::InvalidArgument(
"both username and password are required".to_string(),
)),
}
}

async fn read_password_from_stdin() -> MicrosandboxCliResult<String> {
let mut input = String::new();
let mut stdin = io::stdin();
stdin.read_to_string(&mut input).await?;
let password = input.trim_end_matches(&['\n', '\r'][..]).to_string();
if password.is_empty() {
return Err(MicrosandboxCliError::InvalidArgument(
"password provided via stdin is empty".to_string(),
));
}
Ok(password)
}

/// Parse a file path into project path and config file name.
///
/// If the file path is a directory, it is treated as the project path.
Expand Down
12 changes: 10 additions & 2 deletions microsandbox-cli/bin/msb/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,16 @@ async fn main() -> MicrosandboxCliResult<()> {
handlers::server_ssh_subcommand(namespace, sandbox, name).await?;
}
},
Some(MicrosandboxSubcommand::Login) => {
handlers::login_subcommand().await?;
Some(MicrosandboxSubcommand::Login {
registry,
username,
password_stdin,
token,
}) => {
handlers::login_subcommand(registry, username, password_stdin, token).await?;
}
Some(MicrosandboxSubcommand::Logout { registry, all }) => {
handlers::logout_subcommand(registry, all).await?;
}
Some(MicrosandboxSubcommand::Push { image, name }) => {
handlers::push_subcommand(image, name).await?;
Expand Down
28 changes: 27 additions & 1 deletion microsandbox-cli/lib/args/msb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,33 @@ pub enum MicrosandboxSubcommand {

/// Login to a registry
#[command(name = "login")]
Login,
Login {
/// Registry host (defaults to OCI_REGISTRY_DOMAIN or docker.io)
registry: Option<String>,

/// Registry username
#[arg(short, long)]
username: Option<String>,

/// Read password from stdin
#[arg(long)]
password_stdin: bool,

/// Registry token
#[arg(long)]
token: Option<String>,
},

/// Logout from a registry
#[command(name = "logout")]
Logout {
/// Registry host (defaults to OCI_REGISTRY_DOMAIN or docker.io)
registry: Option<String>,

/// Remove all stored registry credentials
#[arg(long)]
all: bool,
},

/// Push image to a registry
#[command(name = "push")]
Expand Down
3 changes: 3 additions & 0 deletions microsandbox-core/lib/management/image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//! Compatibility re-exports for registry auth resolution.

pub use crate::oci::resolve_registry_auth;
1 change: 1 addition & 0 deletions microsandbox-core/lib/management/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
pub mod config;
pub mod db;
pub mod home;
pub mod image;
pub mod menv;
pub mod orchestra;
pub mod rootfs;
Expand Down
Loading