1.2: Sandboxing #7
@@ -1,4 +1,4 @@
|
||||
FROM debian:trixie
|
||||
FROM rust:1.97-trixie
|
||||
|
qpismont marked this conversation as resolved
|
||||
|
||||
ARG USERNAME=dev
|
||||
ARG USER_UID=1000
|
||||
@@ -18,11 +18,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN groupadd --gid ${USER_GID:-1000} $USERNAME \
|
||||
&& useradd --uid ${USER_UID:-1000} --gid ${USER_GID:-1000} -m $USERNAME
|
||||
&& useradd --uid ${USER_UID:-1000} --gid ${USER_GID:-1000} -m $USERNAME \
|
||||
&& rustup component add clippy
|
||||
|
||||
|
||||
|
||||
USER $USERNAME
|
||||
WORKDIR /home/$USERNAME
|
||||
|
||||
ENV PATH="/home/${USERNAME}/.cargo/bin:${PATH}"
|
||||
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||
|
||||
@@ -19,5 +19,6 @@
|
||||
},
|
||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/herald,type=bind",
|
||||
"workspaceFolder": "/workspaces/herald",
|
||||
"runArgs": ["--userns=keep-id", "--security-opt", "label=disable"],
|
||||
|
qpismont marked this conversation as resolved
Herald
commented
L'option L'option `--security-opt label=disable` désactive la sécurité SELinux, ce qui réduit l'isolation du conteneur. Pour un environnement de développement, cela peut être acceptable, mais il faut être conscient des implications de sécurité. Envisagez d'utiliser `--security-opt label=type:container_runtime_t` ou d'autres options moins permissives si possible.
Herald
commented
Indentation par tabulation incohérente avec le reste du fichier ; Indentation par tabulation incohérente avec le reste du fichier ; `--security-opt label=disable` désactive l'étiquetage SELinux et `--userns=keep-id` est spécifique à Podman. À nettoyer/justifier, d'autant que ce devcontainer sert aussi de contexte d'exécution à la sandbox.
|
||||
"appPort": [3000]
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
target/
|
||||
|
Herald
commented
La suppression de La suppression de `.dockerignore` fait que le contexte envoyé lors de `buildah bud` contient désormais notamment `.git`, les répertoires de build et le fichier `.env` local. Même si le Containerfile ne le copie pas explicitement, ce contexte est transmis au moteur de build et peut contenir des secrets ou devenir inutilement volumineux. Il faut conserver un `.dockerignore` excluant au minimum `.git`, `.env*`, `target` et les fichiers locaux.
|
||||
.env
|
||||
.devcontainer/
|
||||
docs/
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"rust-analyzer.check.command": "clippy"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
|
qpismont marked this conversation as resolved
Herald
commented
Ajouter la configuration Zed tout en supprimant Ajouter la configuration Zed tout en supprimant `.vscode/settings.json` crée une incohérence pour les contributeurs : il vaut mieux conserver les deux (ou documenter l'éditeur recommandé) et s'assurer qu'aucun chemin absolu local ni préférence personnelle n'est committé ici.
|
||||
"lsp": {
|
||||
"rust-analyzer": {
|
||||
"initialization_options": {
|
||||
"check": {
|
||||
"command": "clippy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -490,6 +490,18 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "devcontainer-rs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
@@ -787,12 +799,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "herald"
|
||||
version = "1.1.0"
|
||||
name = "herald-server"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"bytes",
|
||||
"devcontainer-rs",
|
||||
"dotenvy",
|
||||
"futures-util",
|
||||
"hex",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
[package]
|
||||
name = "herald"
|
||||
version = "1.1.0"
|
||||
edition = "2024"
|
||||
[workspace]
|
||||
|
qpismont marked this conversation as resolved
Herald
commented
Passage en workspace multi-crates : vérifier que les dépendances communes sont déclarées dans Passage en workspace multi-crates : vérifier que les dépendances communes sont déclarées dans `[workspace.dependencies]` et réutilisées avec `workspace = true` dans les crates, pour éviter les divergences de versions. Ajouter aussi les métadonnées de package communes (edition, license, version) via `[workspace.package]`.
|
||||
members = [
|
||||
"crates/herald-server",
|
||||
"crates/devcontainer-rs",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[profile.release]
|
||||
debug = 1
|
||||
|
||||
[dependencies]
|
||||
[workspace.dependencies]
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
tokio = { version = "1.53", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
@@ -30,3 +29,6 @@ hex = "0.4"
|
||||
bytes = "1.1"
|
||||
metrics = "0.24"
|
||||
metrics-exporter-prometheus = { version = "0.18", default-features = false, features = ["http-listener"] }
|
||||
|
||||
[profile.release]
|
||||
debug = 1
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
FROM rust:1.96 as builder
|
||||
FROM rust:1.97-trixie as builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN cargo build --release
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/ crates/
|
||||
|
||||
RUN cargo build --release --package herald-server
|
||||
|
||||
|
||||
FROM debian:trixie-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/target/release/herald .
|
||||
CMD [ "./herald" ]
|
||||
COPY --from=builder /app/target/release/herald-server .
|
||||
CMD [ "./herald-server" ]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "devcontainer-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
@@ -0,0 +1,175 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DevContainerBuildSchema {
|
||||
#[serde(default)]
|
||||
pub dockerfile: Option<String>,
|
||||
#[serde(default)]
|
||||
pub args: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DevContainerSchema {
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
pub build: DevContainerBuildSchema,
|
||||
|
||||
#[serde(rename = "workspaceFolder", default)]
|
||||
pub workspace_folder: Option<String>,
|
||||
|
||||
#[serde(rename = "containerEnv", default)]
|
||||
pub container_env: HashMap<String, String>,
|
||||
|
||||
#[serde(rename = "postCreateCommand", default)]
|
||||
pub post_create_command: Option<String>,
|
||||
|
||||
#[serde(rename = "postStartCommand", default)]
|
||||
pub post_start_command: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DevContainer {
|
||||
/// Absolute or relative path to the Dockerfile/Containerfile to build.
|
||||
pub container_file_path: PathBuf,
|
||||
pub name: Option<String>,
|
||||
pub build_args: HashMap<String, String>,
|
||||
pub container_env: HashMap<String, String>,
|
||||
pub workspace_folder: Option<String>,
|
||||
pub post_create_command: Option<String>,
|
||||
pub post_start_command: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ParseError {
|
||||
#[error("failed to read devcontainer file `{path}`: {source}")]
|
||||
Read {
|
||||
path: PathBuf,
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("invalid devcontainer JSON in `{path}`: {source}")]
|
||||
Json {
|
||||
path: PathBuf,
|
||||
source: serde_json::Error,
|
||||
},
|
||||
|
||||
#[error("container file `{0}` does not exist or is not a regular file")]
|
||||
ContainerFileNotFound(PathBuf),
|
||||
|
||||
#[error("the devcontainer file path has no parent directory: `{0}`")]
|
||||
InvalidDevContainerPath(PathBuf),
|
||||
}
|
||||
|
||||
impl TryFrom<(DevContainerSchema, PathBuf)> for DevContainer {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from((schema, devcontainer_path): (DevContainerSchema, PathBuf)) -> Result<Self, Self::Error> {
|
||||
let base_dir = devcontainer_path
|
||||
.parent()
|
||||
.ok_or_else(|| ParseError::InvalidDevContainerPath(devcontainer_path.clone()))?;
|
||||
|
||||
let container_file_path = match schema.build.dockerfile.as_deref() {
|
||||
Some(file) => base_dir.join(file),
|
||||
None => first_existing_container_file(base_dir),
|
||||
};
|
||||
|
||||
if !container_file_path.is_file() {
|
||||
return Err(ParseError::ContainerFileNotFound(container_file_path));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
container_file_path,
|
||||
name: schema.name,
|
||||
build_args: schema.build.args,
|
||||
container_env: schema.container_env,
|
||||
workspace_folder: schema.workspace_folder,
|
||||
post_create_command: schema.post_create_command,
|
||||
post_start_command: schema.post_start_command,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn first_existing_container_file(base_dir: &Path) -> PathBuf {
|
||||
["Dockerfile", "Containerfile"]
|
||||
.iter()
|
||||
.map(|filename| base_dir.join(filename))
|
||||
.find(|path| path.is_file())
|
||||
.unwrap_or_else(|| base_dir.join("Dockerfile"))
|
||||
}
|
||||
|
||||
pub async fn parse(path: impl AsRef<Path>) -> Result<DevContainer, ParseError> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
let contents = tokio::fs::read_to_string(&path)
|
||||
.await
|
||||
.map_err(|source| ParseError::Read {
|
||||
path: path.clone(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
let schema = serde_json::from_str::<DevContainerSchema>(&contents).map_err(|source| {
|
||||
ParseError::Json {
|
||||
path: path.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
|
||||
DevContainer::try_from((schema, path))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn resolves_configured_containerfile_relative_to_devcontainer_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let devcontainer_path = dir.path().join("devcontainer.json");
|
||||
let containerfile_path = dir.path().join("Containerfile");
|
||||
fs::write(&containerfile_path, "FROM alpine\n").unwrap();
|
||||
|
||||
let schema = DevContainerSchema {
|
||||
name: Some("test".into()),
|
||||
build: DevContainerBuildSchema {
|
||||
dockerfile: Some("Containerfile".into()),
|
||||
args: HashMap::new(),
|
||||
},
|
||||
workspace_folder: None,
|
||||
container_env: HashMap::new(),
|
||||
post_create_command: None,
|
||||
post_start_command: None,
|
||||
};
|
||||
|
||||
let config = DevContainer::try_from((schema, devcontainer_path)).unwrap();
|
||||
assert_eq!(config.container_file_path, containerfile_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falls_back_to_dockerfile_before_containerfile() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let devcontainer_path = dir.path().join("devcontainer.json");
|
||||
let dockerfile_path = dir.path().join("Dockerfile");
|
||||
fs::write(&dockerfile_path, "FROM alpine\n").unwrap();
|
||||
fs::write(dir.path().join("Containerfile"), "FROM busybox\n").unwrap();
|
||||
|
||||
let schema = DevContainerSchema {
|
||||
name: None,
|
||||
build: DevContainerBuildSchema {
|
||||
dockerfile: None,
|
||||
args: HashMap::new(),
|
||||
},
|
||||
workspace_folder: None,
|
||||
container_env: HashMap::new(),
|
||||
post_create_command: None,
|
||||
post_start_command: None,
|
||||
};
|
||||
|
||||
let config = DevContainer::try_from((schema, devcontainer_path)).unwrap();
|
||||
assert_eq!(config.container_file_path, dockerfile_path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "herald-server"
|
||||
version = "1.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
sentry = { workspace = true }
|
||||
sentry-anyhow = { workspace = true }
|
||||
openrouter-rs = { workspace = true }
|
||||
dotenvy = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
tower-http = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
ring = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
metrics = { workspace = true }
|
||||
|
qpismont marked this conversation as resolved
Herald
commented
`tempfile = "3"` est déclaré en dur alors que toutes les autres dépendances passent par `[workspace.dependencies]` : le placer dans le workspace pour garder une version unique et cohérente (idem pour le dev-dependency de `devcontainer-rs`).
|
||||
metrics-exporter-prometheus = { workspace = true }
|
||||
devcontainer-rs = { path = "../devcontainer-rs" }
|
||||
Épingler l'image de base par digest (ou au moins une version majeure mineure) pour la reproductibilité, et éviter d'exécuter le conteneur de développement en root si possible. Vérifier que les outils ajoutés dans cette image ne sont pas supposés présents en production (le devcontainer ne doit pas être une dépendance implicite du runtime).