Bootstrap (main.rs)

The entry point orchestrates everything: sandbox, config, pool, migrations, state, registration, then the HTTP server.

Cargo.toml #

[dependencies]
tokio    = { version = "1",   features = ["full"] }
axum     = { version = "0.7", features = ["macros"] }
sqlx     = { version = "0.8", features = ["runtime-tokio","postgres","uuid","chrono","json","migrate"] }
serde    = { version = "1",   features = ["derive"] }
serde_json = "1"
reqwest  = { version = "0.12", features = ["json"] }
config   = "0.14"
validator = { version = "0.18", features = ["derive"] }
tracing  = "0.1"
thiserror = "1"
anyhow   = "1"
uuid     = { version = "1",   features = ["v4","serde"] }
chrono   = { version = "0.4", features = ["serde"] }

# Crate partagée, tirée d'un git-tag du core
kubuno-seccomp = { git = "https://github.com/kubuno/core", tag = "seccomp-v0.1.0", package = "kubuno-seccomp" }

main.rs #

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt().init();

    // 1. Bac à sable : interdit l'exécution de processus dans ce module
    kubuno_seccomp::lock_down_process_execution("memo");

    // 2. Configuration (KC__… + overrides KUBUNO_* injectés par le core)
    let settings = Settings::load()?;

    // 3. Pool PostgreSQL
    let pool = PgPoolOptions::new()
        .max_connections(settings.database.max_connections)
        .connect_with(settings.database.connect_options()?)
        .await?;

    // 4. Schéma dédié + migrations (search_path = "memo,public")
    if settings.database.run_migrations {
        sqlx::query("CREATE SCHEMA IF NOT EXISTS memo").execute(&pool).await?;
        let opts = settings.database.connect_options()?
            .options([("search_path", "memo,public")]);
        let mpool = PgPoolOptions::new().max_connections(1).connect_with(opts).await?;
        sqlx::migrate!("./migrations").run(&mpool).await?;
    }

    // 5. État partagé
    let state = AppState { db: pool, settings: Arc::new(settings.clone()) };

    // 6. S'enregistrer auprès du core + heartbeat (cf. article dédié)
    let http = reqwest::Client::new();
    register_with_core(&http, &settings).await;
    spawn_heartbeat(http.clone(), settings.clone());

    // 7. Démarrer le serveur
    let addr = format!("{}:{}", settings.server.host, settings.server.port);
    let listener = tokio::net::TcpListener::bind(&addr).await?;
    axum::serve(listener, router::build(state)).await?;
    Ok(())
}
Security

lock_down_process_execution applies the seccomp filter right at startup: no execve is possible afterwards. Never remove it.

The same skeleton, in other languages #

Cargo, SQLx and the seccomp sandbox are Rust-specific, but a module is just an HTTP service: any runtime will do (declare runtime = "binary" in the manifest). The minimal server:

// Rust — Axum
use axum::{routing::get, Json, Router};
use serde_json::json;

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/ping", get(|| async { Json(json!({ "ok": true })) }));
    let l = tokio::net::TcpListener::bind("127.0.0.1:3190").await.unwrap();
    axum::serve(l, app).await.unwrap();
}
<?php
// PHP — lancé avec : php -S 127.0.0.1:3190 router.php
header('Content-Type: application/json');
if (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) === '/ping') {
    echo json_encode(['ok' => true]);
    exit;
}
http_response_code(404);
# Python — Flask
from flask import Flask, jsonify

app = Flask(__name__)

@app.get("/ping")
def ping():
    return jsonify(ok=True)

app.run(host="127.0.0.1", port=3190)
// Go — net/http
package main

import (
    "encoding/json"
    "net/http"
)

func main() {
    http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(map[string]bool{"ok": true})
    })
    http.ListenAndServe("127.0.0.1:3190", nil)
}
# Perl — Mojolicious::Lite
use Mojolicious::Lite -signatures;

get '/ping' => sub ($c) {
    $c->render(json => { ok => \1 });
};

app->start('daemon', '-l', 'http://127.0.0.1:3190');