Skip to content

Commit c8ee371

Browse files
committed
NixOS flake for prod
1 parent 0be0810 commit c8ee371

File tree

11 files changed

+387
-170
lines changed

11 files changed

+387
-170
lines changed

NixOS/configuration.nix

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
{ lib, pkgs, modulesPath, system, cs2kz-api, ... }:
2+
3+
let
4+
sshKeys = [
5+
''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB4SBKTQ7WJcihtw3QocLXi+xEc/6HklXigYoltI8iNH alphakeks@dawn''
6+
''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPe34iB4eZ5KnO8nKXHtH4V0QZNb7Ro/YxZw7xuCEJ7C max@framework''
7+
];
8+
in
9+
10+
{
11+
environment = {
12+
systemPackages = with pkgs; [ coreutils vim ];
13+
defaultPackages = with pkgs; [ tmux curl git btop fd fzf neovim jq ripgrep ];
14+
variables.EDITOR = "nvim";
15+
};
16+
networking = {
17+
hostName = "cs2kz-api";
18+
firewall = {
19+
interfaces = {
20+
"enp0s6" = {
21+
allowedTCPPorts = [ 22 80 443 ];
22+
};
23+
};
24+
};
25+
};
26+
nixpkgs.hostPlatform = system;
27+
programs.zsh.enable = true;
28+
security.acme = {
29+
acceptTerms = true;
30+
defaults.email = "cs2kz@dawn.sh";
31+
};
32+
services = {
33+
openssh = {
34+
enable = true;
35+
settings.PasswordAuthentication = false;
36+
};
37+
mysql = {
38+
enable = true;
39+
package = pkgs.mariadb;
40+
ensureDatabases = [ "cs2kz" ];
41+
ensureUsers = [{
42+
name = "schnose";
43+
ensurePermissions = {
44+
"cs2kz.*" = "ALL PRIVILEGES"; # TODO: more granular permissions
45+
};
46+
}];
47+
initialDatabases = [{
48+
name = "cs2kz";
49+
schema = ../crates/cs2kz/migrations/0001_initial.up.sql;
50+
}];
51+
};
52+
mysqlBackup = {
53+
enable = true;
54+
calendar = "02:30:00";
55+
databases = [ "cs2kz" ];
56+
};
57+
nginx = {
58+
enable = true;
59+
recommendedTlsSettings = true;
60+
recommendedProxySettings = true;
61+
virtualHosts."api.cs2kz.org" = {
62+
forceSSL = true;
63+
enableACME = true;
64+
locations."/" = {
65+
proxyPass = "http://[::1]:42069";
66+
proxyWebsockets = true;
67+
extraConfig = ''
68+
if ($cloudflare_ip != 1) {
69+
return 403;
70+
}
71+
72+
# required when the server wants to use HTTP Authentication
73+
proxy_pass_header Authorization;
74+
'';
75+
};
76+
};
77+
commonHttpConfig =
78+
let
79+
realIpsFromList = lib.strings.concatMapStringsSep "\n" (x: "set_real_ip_from ${x};");
80+
allowFromList = lib.strings.concatMapStringsSep "\n" (x: "${x} 1;");
81+
fileToList = x: lib.strings.splitString "\n" (builtins.readFile x);
82+
cfipv4 = fileToList (pkgs.fetchurl {
83+
url = "https://www.cloudflare.com/ips-v4";
84+
sha256 = "0ywy9sg7spafi3gm9q5wb59lbiq0swvf0q3iazl0maq1pj1nsb7h";
85+
});
86+
cfipv6 = fileToList (pkgs.fetchurl {
87+
url = "https://www.cloudflare.com/ips-v6";
88+
sha256 = "1ad09hijignj6zlqvdjxv7rjj8567z357zfavv201b9vx3ikk7cy";
89+
});
90+
in
91+
''
92+
geo $realip_remote_addr $cloudflare_ip {
93+
default 0;
94+
${allowFromList cfipv4}
95+
${allowFromList cfipv6}
96+
}
97+
98+
# Proxy CF-ConnectingIP header
99+
${realIpsFromList cfipv4}
100+
${realIpsFromList cfipv6}
101+
real_ip_header CF-Connecting-IP;
102+
'';
103+
};
104+
};
105+
system.stateVersion = "24.05";
106+
systemd.user.services.cs2kz-api = {
107+
enable = true;
108+
wantedBy = [ "multi-user.target" ];
109+
unitConfig.ConditionUser = "schnose";
110+
environment = {
111+
RUST_LOG = "cs2kz=trace,warn";
112+
KZ_API_ENVIRONMENT = "production";
113+
};
114+
script = ''
115+
${cs2kz-api}/bin/cs2kz-api \
116+
--config "/etc/cs2kz-api.toml" \
117+
--depot-downloader-path "${pkgs.depotdownloader}/bin/DepotDownloader"
118+
'';
119+
};
120+
time.timeZone = "Europe/Berlin";
121+
users = {
122+
defaultUserShell = pkgs.zsh;
123+
users.root.openssh.authorizedKeys.keys = sshKeys;
124+
users.schnose = {
125+
isNormalUser = true;
126+
linger = true;
127+
useDefaultShell = true;
128+
extraGroups = [ "wheel" ];
129+
openssh.authorizedKeys.keys = sshKeys;
130+
};
131+
};
132+
}

NixOS/hardware-configuration.nix

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{ modulesPath, ... }:
2+
3+
{
4+
imports = [ (modulesPath + "/profiles/qemu-guest.nix") ];
5+
boot = {
6+
loader.grub = {
7+
efiSupport = true;
8+
efiInstallAsRemovable = true;
9+
device = "nodev";
10+
};
11+
initrd = {
12+
availableKernelModules = [
13+
"ata_piix"
14+
"uhci_hcd"
15+
"xen_blkfront"
16+
];
17+
kernelModules = [ "nvme" ];
18+
};
19+
};
20+
fileSystems = {
21+
"/" = {
22+
device = "/dev/sda1";
23+
fsType = "ext4";
24+
};
25+
"/boot" = {
26+
device = "/dev/disk/by-uuid/1B9E-BDB1";
27+
fsType = "vfat";
28+
};
29+
};
30+
}

crates/cs2kz-api/src/bin/server/main.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ fn main() -> anyhow::Result<()> {
2424

2525
cli_args.apply_to_config(&mut config);
2626

27-
if config.tracing.enable {
28-
init_tracing(&config.tracing).context("failed to initialize tracing")?;
29-
}
27+
let _guard = if config.tracing.enable {
28+
init_tracing(&config.tracing).context("failed to initialize tracing")?
29+
} else {
30+
None
31+
};
3032

3133
cs2kz_api::run(config).context("failed to run API")
3234
}

crates/cs2kz-api/src/lib.rs

+1-4
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,7 @@ pub fn run(config: Config) -> Result<(), Error> {
199199
},
200200
},
201201

202-
result = tokio::signal::ctrl_c() => match result {
203-
Ok(()) => warn!("shutting down"),
204-
Err(error) => error!(%error, "failed to listen for ctrl-c"),
205-
},
202+
() = runtime::signal::shutdown() => {},
206203
}
207204

208205
let _ = shutdown_tx.send(());
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1-
use std::net::SocketAddr;
2-
3-
use axum::extract::{ConnectInfo, Request};
1+
use axum::extract::Request;
42
use axum::middleware::Next;
53
use axum::response::Response;
64

75
use crate::response::ErrorResponse;
6+
use crate::runtime::{self, Environment};
87

98
/// Middleware to check if a given request is coming from localhost.
109
///
1110
/// This is used for private endpoints like `/metrics` and `/taskdump`.
1211
#[tracing::instrument(skip_all, err(Debug, level = "debug"))]
1312
pub async fn client_is_localhost(request: Request, next: Next) -> Result<Response, ErrorResponse> {
14-
if request
15-
.extensions()
16-
.get::<ConnectInfo<SocketAddr>>()
17-
.is_some_and(|addr| addr.ip().is_loopback())
18-
{
19-
Ok(next.run(request).await)
20-
} else {
21-
Err(ErrorResponse::not_found())
13+
match runtime::environment() {
14+
Environment::Local => Ok(next.run(request).await),
15+
Environment::Staging | Environment::Production => {
16+
// If there's an X-Real-Ip header, we went through nginx, which means the request came
17+
// from the outside.
18+
if request.headers().contains_key("X-Real-Ip") {
19+
Err(ErrorResponse::not_found())
20+
} else {
21+
Ok(next.run(request).await)
22+
}
23+
},
2224
}
2325
}

crates/cs2kz-api/src/middleware/trace.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::fmt;
2-
use std::net::SocketAddr;
2+
use std::net::{IpAddr, SocketAddr};
33
use std::time::Duration;
44

55
use axum::body::HttpBody;

crates/cs2kz-api/src/runtime.rs renamed to crates/cs2kz-api/src/runtime/environment.rs

+2-25
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1+
use std::env;
12
use std::sync::LazyLock;
2-
use std::{env, io};
3-
4-
use tokio::runtime::Builder;
5-
pub use tokio::runtime::Runtime;
6-
7-
use crate::config::RuntimeConfig;
83

94
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105
pub enum Environment {
@@ -27,7 +22,7 @@ impl Environment {
2722
}
2823
}
2924

30-
pub fn environment() -> Environment {
25+
pub fn current() -> Environment {
3126
static ENV: LazyLock<Environment> =
3227
LazyLock::new(|| match env::var("KZ_API_ENVIRONMENT").map(|env| env.to_lowercase()) {
3328
Ok(env) => match env.as_str() {
@@ -48,21 +43,3 @@ pub fn environment() -> Environment {
4843

4944
*ENV
5045
}
51-
52-
/// Builds a [Tokio runtime] according to the given `config`.
53-
///
54-
/// [Tokio runtime]: Config
55-
pub fn build(config: &RuntimeConfig) -> io::Result<Runtime> {
56-
let mut builder = Builder::new_multi_thread();
57-
builder.enable_all();
58-
59-
if let Some(n) = config.worker_threads {
60-
builder.worker_threads(n.get());
61-
}
62-
63-
if let Some(n) = config.max_blocking_threads {
64-
builder.max_blocking_threads(n.get());
65-
}
66-
67-
builder.build()
68-
}

crates/cs2kz-api/src/runtime/mod.rs

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use std::io;
2+
3+
use tokio::runtime::Builder;
4+
pub use tokio::runtime::Runtime;
5+
6+
use crate::config::RuntimeConfig;
7+
8+
mod environment;
9+
pub use environment::{Environment, current as environment};
10+
11+
pub mod signal;
12+
13+
/// Builds a [Tokio runtime] according to the given `config`.
14+
///
15+
/// [Tokio runtime]: Config
16+
pub fn build(config: &RuntimeConfig) -> io::Result<Runtime> {
17+
let mut builder = Builder::new_multi_thread();
18+
builder.enable_all();
19+
20+
if let Some(n) = config.worker_threads {
21+
builder.worker_threads(n.get());
22+
}
23+
24+
if let Some(n) = config.max_blocking_threads {
25+
builder.max_blocking_threads(n.get());
26+
}
27+
28+
builder.build()
29+
}
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use tokio::signal;
2+
3+
pub async fn shutdown() {
4+
select! {
5+
() = sigint() => {},
6+
() = sigterm() => {},
7+
}
8+
}
9+
10+
async fn sigint() {
11+
match signal::ctrl_c().await {
12+
Ok(()) => warn!("shutting down"),
13+
Err(error) => error!(%error, "failed to listen for ctrl-c"),
14+
}
15+
}
16+
17+
#[cfg(unix)]
18+
async fn sigterm() {
19+
use tokio::signal::unix;
20+
21+
match unix::signal(unix::SignalKind::terminate()) {
22+
Ok(mut signal) => match signal.recv().await {
23+
Some(()) => warn!("shutting down"),
24+
None => error!("could not listen for more SIGTERM events"),
25+
},
26+
Err(error) => error!(%error, "failed to listen for SIGTERM"),
27+
}
28+
}
29+
30+
#[cfg(not(unix))]
31+
async fn sigterm() {
32+
std::future::pending().await
33+
}

0 commit comments

Comments
 (0)