Moved config file out of the publicly served directory. Fixes #8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ Unit tests are `#[cfg(test)] mod tests` at the bottom of the file they test (`#[
|
|||||||
## Running locally
|
## Running locally
|
||||||
|
|
||||||
- `config.toml` (gitignored) is read from the working directory, or from `-c <path>`. Copy `config_sample.toml`. The bind address comes from `[network]` there, not from `LEPTOS_SITE_ADDR`.
|
- `config.toml` (gitignored) is read from the working directory, or from `-c <path>`. Copy `config_sample.toml`. The bind address comes from `[network]` there, not from `LEPTOS_SITE_ADDR`.
|
||||||
|
- The config file holds secrets and must stay outside the site root (`target/site`), which is served as static files; the server refuses to start otherwise (`is_publicly_served` in `src/config.rs`). In Docker it is mounted to `/app/config/config.toml`.
|
||||||
- Migrations in `migrations/` are embedded by `sqlx::migrate!()` and run on startup.
|
- Migrations in `migrations/` are embedded by `sqlx::migrate!()` and run on startup.
|
||||||
- First start seeds an `admin` / `admin` user, a placeholder company, empty mail templates and an appearance row (`create_admin`, `check_company`, `check_messages`, `check_appearance` in `src/main.rs`).
|
- First start seeds an `admin` / `admin` user, a placeholder company, empty mail templates and an appearance row (`create_admin`, `check_company`, `check_messages`, `check_appearance` in `src/main.rs`).
|
||||||
- With `transport = "File"` e-mails are written as `.eml` into `Maildir/` instead of being sent.
|
- With `transport = "File"` e-mails are written as `.eml` into `Maildir/` instead of being sent.
|
||||||
|
|||||||
+6
-2
@@ -46,6 +46,10 @@ COPY --from=builder /app/target/site /app/target/site
|
|||||||
# Set any required env variables and
|
# Set any required env variables and
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# -- NB: update binary name from "leptos_start" to match your app name in Cargo.toml --
|
# The config file holds passwords and the session key. Mount it to /app/config, never under
|
||||||
|
# /app/target/site, which is served as static files.
|
||||||
|
RUN mkdir -p /app/config
|
||||||
|
VOLUME /app/config
|
||||||
|
|
||||||
# Run the server
|
# Run the server
|
||||||
CMD ["/app/rezervator", "-c /app/target/site/data/config.toml"]
|
CMD ["/app/rezervator", "-c", "/app/config/config.toml"]
|
||||||
@@ -21,6 +21,22 @@ The `wasm-bindgen` command line tool must be exactly the same version as the `wa
|
|||||||
|
|
||||||
`cargo leptos watch`
|
`cargo leptos watch`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The server reads `config.toml` from the working directory, or from the path given with `-c`. Use `config_sample.toml` as a template.
|
||||||
|
|
||||||
|
The file contains the database and SMTP passwords and the session key. Keep it outside the site directory (`target/site`): everything in there is served as static files. The server refuses to start when the config file is inside it.
|
||||||
|
|
||||||
|
## Running in Docker
|
||||||
|
|
||||||
|
Mount the config file to `/app/config/config.toml`:
|
||||||
|
|
||||||
|
`docker run -p 3000:3000 -v /path/to/config-dir:/app/config rezervator`
|
||||||
|
|
||||||
|
Set `bind_ip = '0.0.0.0'` in the config so the server is reachable from outside the container.
|
||||||
|
|
||||||
|
Older versions of the image expected the config in `/app/target/site/data/config.toml`, where it could be downloaded from `/data/config.toml`. If you ran such a container, move the file and change the database password, the SMTP password and the session key.
|
||||||
|
|
||||||
## Running tests
|
## Running tests
|
||||||
|
|
||||||
`cargo test --features ssr`
|
`cargo test --features ssr`
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ use std::fs::read_to_string;
|
|||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
|
use std::path::Path;
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -149,6 +151,16 @@ impl Configuration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The config file holds passwords and the session key, so it must never lie in the directory
|
||||||
|
/// that is served as static files. Returns true when `config_path` is inside `site_root`.
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub fn is_publicly_served(config_path: &Path, site_root: &Path) -> bool {
|
||||||
|
match (config_path.canonicalize(), site_root.canonicalize()) {
|
||||||
|
(Ok(config), Ok(root)) => config.starts_with(root),
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub fn load_config(path: &str) -> Configuration {
|
pub fn load_config(path: &str) -> Configuration {
|
||||||
toml::from_str(read_to_string(path).expect("Can not open config file").as_str()).expect("Can not parse config file")
|
toml::from_str(read_to_string(path).expect("Can not open config file").as_str()).expect("Can not parse config file")
|
||||||
@@ -177,6 +189,41 @@ mod tests {
|
|||||||
assert_eq!(config.mailing().server(), &None);
|
assert_eq!(config.mailing().server(), &None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates `<temp>/<name>/site/data` and returns the `<temp>/<name>` directory
|
||||||
|
fn site_dir(name: &str) -> std::path::PathBuf {
|
||||||
|
let base = std::env::temp_dir().join(format!("rezervator-test-{}-{}", name, std::process::id()));
|
||||||
|
std::fs::create_dir_all(base.join("site/data")).unwrap();
|
||||||
|
base
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_inside_site_root_is_publicly_served() {
|
||||||
|
let base = site_dir("inside");
|
||||||
|
let config = base.join("site/data/config.toml");
|
||||||
|
std::fs::write(&config, "").unwrap();
|
||||||
|
|
||||||
|
assert!(is_publicly_served(&config, &base.join("site")));
|
||||||
|
// Relative segments must not hide it
|
||||||
|
assert!(is_publicly_served(&base.join("site/data/../data/config.toml"), &base.join("site")));
|
||||||
|
|
||||||
|
std::fs::remove_dir_all(base).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_outside_site_root_is_not_publicly_served() {
|
||||||
|
let base = site_dir("outside");
|
||||||
|
let config = base.join("config.toml");
|
||||||
|
std::fs::write(&config, "").unwrap();
|
||||||
|
|
||||||
|
assert!(!is_publicly_served(&config, &base.join("site")));
|
||||||
|
// A sibling directory with the same prefix is not inside the site root
|
||||||
|
std::fs::create_dir_all(base.join("site-config")).unwrap();
|
||||||
|
std::fs::write(base.join("site-config/config.toml"), "").unwrap();
|
||||||
|
assert!(!is_publicly_served(&base.join("site-config/config.toml"), &base.join("site")));
|
||||||
|
|
||||||
|
std::fs::remove_dir_all(base).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sample_session_key_is_long_enough() {
|
fn sample_session_key_is_long_enough() {
|
||||||
// actix_web::cookie::Key::from panics on keys shorter than 64 bytes
|
// actix_web::cookie::Key::from panics on keys shorter than 64 bytes
|
||||||
|
|||||||
+12
-1
@@ -27,7 +27,8 @@ async fn main() -> std::io::Result<()> {
|
|||||||
use sqlx::migrate;
|
use sqlx::migrate;
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use rezervator::backend::auth_middleware::Authentication;
|
use rezervator::backend::auth_middleware::Authentication;
|
||||||
use rezervator::config::load_config;
|
use rezervator::config::{is_publicly_served, load_config};
|
||||||
|
use std::path::Path;
|
||||||
use env_logger::Env;
|
use env_logger::Env;
|
||||||
use std::env::args;
|
use std::env::args;
|
||||||
use getopts::Options;
|
use getopts::Options;
|
||||||
@@ -54,6 +55,16 @@ async fn main() -> std::io::Result<()> {
|
|||||||
info!("Starting server");
|
info!("Starting server");
|
||||||
|
|
||||||
let conf = get_configuration(None).unwrap();
|
let conf = get_configuration(None).unwrap();
|
||||||
|
|
||||||
|
// The site root is served as static files, the config with passwords must not be there
|
||||||
|
if is_publicly_served(Path::new(cfg_path.trim()), Path::new(&*conf.leptos_options.site_root)) {
|
||||||
|
let msg = format!(
|
||||||
|
"Config file {} is inside the publicly served directory {}. Move it elsewhere and change the passwords and the session key in it.",
|
||||||
|
cfg_path.trim(), conf.leptos_options.site_root);
|
||||||
|
error!("{}", msg);
|
||||||
|
return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, msg));
|
||||||
|
}
|
||||||
|
|
||||||
let key = Key::from(srv_conf.session().key());
|
let key = Key::from(srv_conf.session().key());
|
||||||
|
|
||||||
let pool = PgPoolOptions::new()
|
let pool = PgPoolOptions::new()
|
||||||
|
|||||||
Reference in New Issue
Block a user