From 9f27e056d56c70786445bb155923587d668b29b0 Mon Sep 17 00:00:00 2001 From: Mewp Date: Mon, 20 Jan 2025 12:24:59 +0000 Subject: [PATCH] Initial commit. --- .gitignore | 25 ++ authentik.nix | 78 ++++ configuration.nix | 191 +++++++++ encryptor/.gitignore | 16 + encryptor/Cargo.toml | 6 + encryptor/common/Cargo.toml | 12 + encryptor/common/build.rs | 12 + encryptor/common/proto/smtp.proto | 10 + encryptor/common/src/lib.rs | 5 + encryptor/common/src/protos/mod.rs | 1 + encryptor/common/src/protos/smtp.rs | 396 ++++++++++++++++++ encryptor/decryptor/Cargo.toml | 10 + encryptor/decryptor/src/main.rs | 23 + encryptor/encryptor/Cargo.toml | 14 + encryptor/encryptor/src/lib.rs | 64 +++ forgejo.nix | 67 +++ ...-good-patch-it-s-3-am-and-i-am-tired.patch | 34 ++ forgejo/signin_inner.tmpl | 72 ++++ hardware-configuration.nix | 28 ++ mailserver.nix | 247 +++++++++++ nextcloud.nix | 128 ++++++ 21 files changed, 1439 insertions(+) create mode 100644 .gitignore create mode 100644 authentik.nix create mode 100644 configuration.nix create mode 100644 encryptor/.gitignore create mode 100644 encryptor/Cargo.toml create mode 100644 encryptor/common/Cargo.toml create mode 100644 encryptor/common/build.rs create mode 100644 encryptor/common/proto/smtp.proto create mode 100644 encryptor/common/src/lib.rs create mode 100644 encryptor/common/src/protos/mod.rs create mode 100644 encryptor/common/src/protos/smtp.rs create mode 100644 encryptor/decryptor/Cargo.toml create mode 100644 encryptor/decryptor/src/main.rs create mode 100644 encryptor/encryptor/Cargo.toml create mode 100644 encryptor/encryptor/src/lib.rs create mode 100644 forgejo.nix create mode 100644 forgejo/0001-bad-bad-not-good-patch-it-s-3-am-and-i-am-tired.patch create mode 100644 forgejo/signin_inner.tmpl create mode 100644 hardware-configuration.nix create mode 100644 mailserver.nix create mode 100644 nextcloud.nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39162a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +### Vim ### +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### Rust ### +**/target + +### Nix $$$ +**/result diff --git a/authentik.nix b/authentik.nix new file mode 100644 index 0000000..c147b29 --- /dev/null +++ b/authentik.nix @@ -0,0 +1,78 @@ +{ config, pkgs, lib, ... }: + +let + + environment = { + AUTHENTIK_POSTGRESQL__HOST = "10.88.0.1"; + AUTHENTIK_REDIS__HOST = "10.88.0.1"; + }; + +in { + networking.firewall.trustedInterfaces = [ "podman0" ]; + services.postgresql = { + enableTCPIP = true; + ensureDatabases = ["authentik"]; + ensureUsers = [ + { + name = "authentik"; + ensureDBOwnership = true; + } + ]; + authentication = '' + host all all 10.88.0.0/16 md5 + ''; + }; + services.redis = { + servers.authentik = { + enable = true; + requirePassFile = "/var/secrets/redis-password"; + port = 6379; + bind = "10.88.0.1"; + }; + }; + virtualisation.podman.enable = true; + virtualisation.oci-containers.backend = "podman"; + virtualisation.oci-containers.containers = { + authentik-server = { + image = "ghcr.io/goauthentik/server:2024.12.2"; + cmd = [ "server" ]; + inherit environment; + environmentFiles = [ "/opt/authentik.env" ]; + ports = [ "10.88.0.1:9000:9000" ]; + volumes = [ + #"${dataDir}/media:/media" + #"${dataDir}/assets:/web/dist/extra:ro" + #"${dataDir}/templates:/templates" + ]; + }; + authentik-worker = { + image = "ghcr.io/goauthentik/server:2024.12.2"; + cmd = [ "worker" ]; + inherit environment; + environmentFiles = [ "/opt/authentik.env" ]; + volumes = [ + #"${dataDir}/media:/media" + #"${dataDir}/assets:/web/dist/extra:ro" + #"${dataDir}/templates:/templates" + ]; + }; + authentik-ldap = { + image = "ghcr.io/goauthentik/ldap:2024.12.2"; + ports = [ "10.88.0.1:389:3389" "10.88.0.1:636:6636" ]; + environment = { + AUTHENTIK_HOST = "https://auth.orga.cebula.camp"; + }; + environmentFiles = [ + "/var/secrets/authentik-ldap" + ]; + }; + }; + services.nginx.virtualHosts."auth.orga.cebula.camp" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://10.88.0.1:9000/"; + proxyWebsockets = true; + }; + }; +} diff --git a/configuration.nix b/configuration.nix new file mode 100644 index 0000000..0977c6e --- /dev/null +++ b/configuration.nix @@ -0,0 +1,191 @@ +# Edit this configuration file to define what should be installed on +# your system. Help is available in the configuration.nix(5) man page +# and in the NixOS manual (accessible by running ‘nixos-help’). + +{ config, pkgs, ... }: + +{ + imports = + [ # Include the results of the hardware scan. + ./hardware-configuration.nix + #./identity.nix + ./mailserver.nix + ./nextcloud.nix + ./authentik.nix + ./forgejo.nix + ]; + + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + networking.hostName = "szalotka"; + networking.domain = "cebula.camp"; + + networking.useDHCP = false; + networking.interfaces.enp1s0 = { + ipv4.addresses = [ + { address = "135.181.235.222"; prefixLength = 29; } + ]; + ipv6.addresses = [ + { address = "2a01:4f9:4a:4319:1337::14"; prefixLength = 80; } + ]; + }; + networking.defaultGateway = "135.181.235.217"; + networking.defaultGateway6 = "2a01:4f9:4a:4319:1337::1"; + networking.nameservers = [ + "8.8.8.8" + ]; + + # Define a user account. Don't forget to set a password with ‘passwd’. + # users.users.jane = { + # isNormalUser = true; + # extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user. + # }; + + users.users.root = { + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG599UildOrAq+LIOQjKqtGMwjgjIxozI1jtQQRKHtCP q3k@mimeomia" + "cert-authority ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPt2EXhvAwjMZ+5j8P0dCMaUdXeUQePeTv8tBdHXNly mewp" + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQb3YQoiYFZLKwvHYKbu1bMqzNeDCAszQhAe1+QI5SLDOotclyY/vFmOReZOsmyMFl71G2d7d+FbYNusUnNNjTxRYQ021tVc+RkMdLJaORRURmQfEFEKbai6QSFTwErXzuoIzyEPK0lbsQuGgqT9WaVnRzHJ2Q/4+qQbxAS34PuR5NqEkmn4G6LMo3OyJ5mwPkCj9lsqz4BcxRaMWFO3mNcwGDfSW+sqgc3E8N6LKrTpZq3ke7xacpQmcG5DU9VO+2QVPdltl9jWbs3gXjmF92YRNOuKPVfAOZBBsp8JOznfx8s9wDgs7RwPmDpjIAJEyoABqW5hlXfqRbTnfnMvuR informatic@InformaticPC" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOccFr7RFddSB5kdvYCIdCyKgD3X79mC90cMImqziTd9 radex@radpro" + ]; + }; + users.groups.ldap-access = {}; + + # users.ldap = { + # enable = true; + # loginPam = true; + # nsswitch = true; + # base = "ou=users,dc=cebula,dc=camp"; + # server = "ldap://10.88.0.1:389/"; + # timeLimit = 1; + # daemon.enable = true; + # bind = { + # distinguishedName = "cn=ldap-access,ou=users,dc=cebula,dc=camp"; + # passwordFile = "/var/secrets/ldap-access"; + # }; + # }; + + services.sssd = { + enable = true; + sshAuthorizedKeysIntegration = true; + config = let + baseDN = "dc=cebula,dc=camp"; + serviceAccount = "ldap-access"; + allowedGroup = "cn=orga-infra,ou=groups,${baseDN}"; + in '' + [nss] + filter_groups = root + filter_users = root + reconnection_retries = 3 + + [sssd] + config_file_version = 2 + reconnection_retries = 3 + domains = LDAP + services = nss, pam, ssh + + [pam] + reconnection_retries = 3 + + [domain/LDAP] + cache_credentials = True + id_provider = ldap + chpass_provider = ldap + auth_provider = ldap + access_provider = ldap + + ldap_uri = ldaps://10.88.0.1/ + ldap_tls_reqcert = allow + + ldap_schema = rfc2307bis + ldap_search_base = ${baseDN} + ldap_user_search_base = ou=users,${baseDN} + ldap_group_search_base = ${baseDN} + ldap_user_ssh_public_key = sshPublicKey + + ldap_user_object_class = user + ldap_user_name = cn + ldap_group_object_class = group + ldap_group_name = cn + + ldap_default_bind_dn = cn=${serviceAccount},ou=users,${baseDN} + ldap_default_authtok = $LDAP_DEFAULT_AUTHTOK + ldap_access_filter = memberOf=${allowedGroup} + ''; + environmentFile = "/var/secrets/sssd"; + }; + security.pam.services.sshd.makeHomeDir = true; + security.sudo.extraRules = [{ + groups = ["orga-infra"]; + commands = [{ + command = "ALL"; + options = [ "NOPASSWD" ]; + }]; + }]; + + environment.variables.EDITOR = "vim"; + environment.systemPackages = with pkgs; [ + wget rxvt-unicode-unwrapped.terminfo htop dstat + git + + ((vim_configurable.override { features = "normal"; }).customize { + name = "vim"; + vimrcConfig.packages.myVimPackage = with pkgs.vimPlugins; { + start = [ vim-nix nerdtree ]; + }; + vimrcConfig.customRC = '' + syntax on + set expandtab + set tabstop=4 + set autoindent + set shiftwidth=4 + set bs=2 + + autocmd FileType nix setlocal shiftwidth=2 tabstop=2 + ''; + }) + ]; + + programs.mtr.enable = true; + + services.journald.extraConfig = '' + SystemMaxUse=2G + ''; + services.openssh = { + enable = true; + ports = [ 2222 ]; + }; + services.nginx = { + enable = true; + recommendedProxySettings = true; + }; + security.acme = { + # TODO(q3k): change to @cebula.camp address. + email = "q3k@q3k.org"; + acceptTerms = true; + }; + # Limit nscd memory usage, as it sometimes just blows up and the OOMkiller + # sucks at picking it up. + systemd.services.nscd.serviceConfig.MemoryMax = "256M"; + + + # Open ports in the firewall. + networking.firewall.allowedTCPPorts = [ + 80 443 # http + 2222 # host ssh + ]; + # networking.firewall.allowedUDPPorts = [ ... ]; + # Or disable the firewall altogether. + # networking.firewall.enable = false; + + # This value determines the NixOS release from which the default + # settings for stateful data, like file locations and database versions + # on your system were taken. It‘s perfectly fine and recommended to leave + # this value at the release version of the first install of this system. + # Before changing this value read the documentation for this option + # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html). + system.stateVersion = "20.09"; # Did you read the comment? + +} + diff --git a/encryptor/.gitignore b/encryptor/.gitignore new file mode 100644 index 0000000..ad43865 --- /dev/null +++ b/encryptor/.gitignore @@ -0,0 +1,16 @@ +/target + + +# Added by cargo +# +# already existing elements were commented out + +#/target + + +# Added by cargo +# +# already existing elements were commented out + +#/target +Cargo.lock diff --git a/encryptor/Cargo.toml b/encryptor/Cargo.toml new file mode 100644 index 0000000..d9e63d2 --- /dev/null +++ b/encryptor/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +members = [ + "encryptor", + "decryptor", + "common", +] diff --git a/encryptor/common/Cargo.toml b/encryptor/common/Cargo.toml new file mode 100644 index 0000000..caab9ce --- /dev/null +++ b/encryptor/common/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "common" +version = "0.1.0" +edition = "2018" + +[dependencies] +protobuf = "2" +secretbox = "0" +base64 = "0" + +[build-dependencies] +protobuf-codegen-pure = "2.3" diff --git a/encryptor/common/build.rs b/encryptor/common/build.rs new file mode 100644 index 0000000..b3c38a9 --- /dev/null +++ b/encryptor/common/build.rs @@ -0,0 +1,12 @@ +extern crate protobuf_codegen_pure; + +fn main() { + protobuf_codegen_pure::run(protobuf_codegen_pure::Args { + out_dir: "src/protos", + input: &["proto/smtp.proto"], + includes: &["proto"], + customize: protobuf_codegen_pure::Customize { + ..Default::default() + }, + }).expect("protoc"); +} diff --git a/encryptor/common/proto/smtp.proto b/encryptor/common/proto/smtp.proto new file mode 100644 index 0000000..8d2f6ba --- /dev/null +++ b/encryptor/common/proto/smtp.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; +package camp.cebula.smtp; + +message SMTPSource { + message V1 { + int64 timestamp = 1; + string sender = 2; + }; + bytes v1_encrypted = 1; +} diff --git a/encryptor/common/src/lib.rs b/encryptor/common/src/lib.rs new file mode 100644 index 0000000..d43e227 --- /dev/null +++ b/encryptor/common/src/lib.rs @@ -0,0 +1,5 @@ +pub mod protos; + +pub const KEY: &'static str = "thiYux7lohfit2queibee9engo1eiVoi"; +pub const B64C: base64::Config = base64::Config::new(base64::CharacterSet::UrlSafe, false); + diff --git a/encryptor/common/src/protos/mod.rs b/encryptor/common/src/protos/mod.rs new file mode 100644 index 0000000..bdcacaa --- /dev/null +++ b/encryptor/common/src/protos/mod.rs @@ -0,0 +1 @@ +pub mod smtp; diff --git a/encryptor/common/src/protos/smtp.rs b/encryptor/common/src/protos/smtp.rs new file mode 100644 index 0000000..4cc1ea0 --- /dev/null +++ b/encryptor/common/src/protos/smtp.rs @@ -0,0 +1,396 @@ +// This file is generated by rust-protobuf 2.25.1. Do not edit +// @generated + +// https://github.com/rust-lang/rust-clippy/issues/702 +#![allow(unknown_lints)] +#![allow(clippy::all)] + +#![allow(unused_attributes)] +#![cfg_attr(rustfmt, rustfmt::skip)] + +#![allow(box_pointers)] +#![allow(dead_code)] +#![allow(missing_docs)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(trivial_casts)] +#![allow(unused_imports)] +#![allow(unused_results)] +//! Generated file from `smtp.proto` + +/// Generated files are compatible only with the same version +/// of protobuf runtime. +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_1; + +#[derive(PartialEq,Clone,Default)] +pub struct SMTPSource { + // message fields + pub v1_encrypted: ::std::vec::Vec, + // special fields + pub unknown_fields: ::protobuf::UnknownFields, + pub cached_size: ::protobuf::CachedSize, +} + +impl<'a> ::std::default::Default for &'a SMTPSource { + fn default() -> &'a SMTPSource { + ::default_instance() + } +} + +impl SMTPSource { + pub fn new() -> SMTPSource { + ::std::default::Default::default() + } + + // bytes v1_encrypted = 1; + + + pub fn get_v1_encrypted(&self) -> &[u8] { + &self.v1_encrypted + } + pub fn clear_v1_encrypted(&mut self) { + self.v1_encrypted.clear(); + } + + // Param is passed by value, moved + pub fn set_v1_encrypted(&mut self, v: ::std::vec::Vec) { + self.v1_encrypted = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_v1_encrypted(&mut self) -> &mut ::std::vec::Vec { + &mut self.v1_encrypted + } + + // Take field + pub fn take_v1_encrypted(&mut self) -> ::std::vec::Vec { + ::std::mem::replace(&mut self.v1_encrypted, ::std::vec::Vec::new()) + } +} + +impl ::protobuf::Message for SMTPSource { + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> { + while !is.eof()? { + let (field_number, wire_type) = is.read_tag_unpack()?; + match field_number { + 1 => { + ::protobuf::rt::read_singular_proto3_bytes_into(wire_type, is, &mut self.v1_encrypted)?; + }, + _ => { + ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u32 { + let mut my_size = 0; + if !self.v1_encrypted.is_empty() { + my_size += ::protobuf::rt::bytes_size(1, &self.v1_encrypted); + } + my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); + self.cached_size.set(my_size); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + if !self.v1_encrypted.is_empty() { + os.write_bytes(1, &self.v1_encrypted)?; + } + os.write_unknown_fields(self.get_unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn get_cached_size(&self) -> u32 { + self.cached_size.get() + } + + fn get_unknown_fields(&self) -> &::protobuf::UnknownFields { + &self.unknown_fields + } + + fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields { + &mut self.unknown_fields + } + + fn as_any(&self) -> &dyn (::std::any::Any) { + self as &dyn (::std::any::Any) + } + fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) { + self as &mut dyn (::std::any::Any) + } + fn into_any(self: ::std::boxed::Box) -> ::std::boxed::Box { + self + } + + fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { + Self::descriptor_static() + } + + fn new() -> SMTPSource { + SMTPSource::new() + } + + fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBytes>( + "v1_encrypted", + |m: &SMTPSource| { &m.v1_encrypted }, + |m: &mut SMTPSource| { &mut m.v1_encrypted }, + )); + ::protobuf::reflect::MessageDescriptor::new_pb_name::( + "SMTPSource", + fields, + file_descriptor_proto() + ) + }) + } + + fn default_instance() -> &'static SMTPSource { + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(SMTPSource::new) + } +} + +impl ::protobuf::Clear for SMTPSource { + fn clear(&mut self) { + self.v1_encrypted.clear(); + self.unknown_fields.clear(); + } +} + +impl ::std::fmt::Debug for SMTPSource { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for SMTPSource { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Message(self) + } +} + +#[derive(PartialEq,Clone,Default)] +pub struct SMTPSource_V1 { + // message fields + pub timestamp: i64, + pub sender: ::std::string::String, + // special fields + pub unknown_fields: ::protobuf::UnknownFields, + pub cached_size: ::protobuf::CachedSize, +} + +impl<'a> ::std::default::Default for &'a SMTPSource_V1 { + fn default() -> &'a SMTPSource_V1 { + ::default_instance() + } +} + +impl SMTPSource_V1 { + pub fn new() -> SMTPSource_V1 { + ::std::default::Default::default() + } + + // int64 timestamp = 1; + + + pub fn get_timestamp(&self) -> i64 { + self.timestamp + } + pub fn clear_timestamp(&mut self) { + self.timestamp = 0; + } + + // Param is passed by value, moved + pub fn set_timestamp(&mut self, v: i64) { + self.timestamp = v; + } + + // string sender = 2; + + + pub fn get_sender(&self) -> &str { + &self.sender + } + pub fn clear_sender(&mut self) { + self.sender.clear(); + } + + // Param is passed by value, moved + pub fn set_sender(&mut self, v: ::std::string::String) { + self.sender = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_sender(&mut self) -> &mut ::std::string::String { + &mut self.sender + } + + // Take field + pub fn take_sender(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.sender, ::std::string::String::new()) + } +} + +impl ::protobuf::Message for SMTPSource_V1 { + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> { + while !is.eof()? { + let (field_number, wire_type) = is.read_tag_unpack()?; + match field_number { + 1 => { + if wire_type != ::protobuf::wire_format::WireTypeVarint { + return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type)); + } + let tmp = is.read_int64()?; + self.timestamp = tmp; + }, + 2 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.sender)?; + }, + _ => { + ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u32 { + let mut my_size = 0; + if self.timestamp != 0 { + my_size += ::protobuf::rt::value_size(1, self.timestamp, ::protobuf::wire_format::WireTypeVarint); + } + if !self.sender.is_empty() { + my_size += ::protobuf::rt::string_size(2, &self.sender); + } + my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); + self.cached_size.set(my_size); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + if self.timestamp != 0 { + os.write_int64(1, self.timestamp)?; + } + if !self.sender.is_empty() { + os.write_string(2, &self.sender)?; + } + os.write_unknown_fields(self.get_unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn get_cached_size(&self) -> u32 { + self.cached_size.get() + } + + fn get_unknown_fields(&self) -> &::protobuf::UnknownFields { + &self.unknown_fields + } + + fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields { + &mut self.unknown_fields + } + + fn as_any(&self) -> &dyn (::std::any::Any) { + self as &dyn (::std::any::Any) + } + fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) { + self as &mut dyn (::std::any::Any) + } + fn into_any(self: ::std::boxed::Box) -> ::std::boxed::Box { + self + } + + fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { + Self::descriptor_static() + } + + fn new() -> SMTPSource_V1 { + SMTPSource_V1::new() + } + + fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeInt64>( + "timestamp", + |m: &SMTPSource_V1| { &m.timestamp }, + |m: &mut SMTPSource_V1| { &mut m.timestamp }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "sender", + |m: &SMTPSource_V1| { &m.sender }, + |m: &mut SMTPSource_V1| { &mut m.sender }, + )); + ::protobuf::reflect::MessageDescriptor::new_pb_name::( + "SMTPSource.V1", + fields, + file_descriptor_proto() + ) + }) + } + + fn default_instance() -> &'static SMTPSource_V1 { + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(SMTPSource_V1::new) + } +} + +impl ::protobuf::Clear for SMTPSource_V1 { + fn clear(&mut self) { + self.timestamp = 0; + self.sender.clear(); + self.unknown_fields.clear(); + } +} + +impl ::std::fmt::Debug for SMTPSource_V1 { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for SMTPSource_V1 { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Message(self) + } +} + +static file_descriptor_proto_data: &'static [u8] = b"\ + \n\nsmtp.proto\x12\x10camp.cebula.smtp\"u\n\nSMTPSource\x12#\n\x0cv1_enc\ + rypted\x18\x01\x20\x01(\x0cR\x0bv1EncryptedB\0\x1a@\n\x02V1\x12\x1e\n\tt\ + imestamp\x18\x01\x20\x01(\x03R\ttimestampB\0\x12\x18\n\x06sender\x18\x02\ + \x20\x01(\tR\x06senderB\0:\0:\0B\0b\x06proto3\ +"; + +static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT; + +fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto { + ::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap() +} + +pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto { + file_descriptor_proto_lazy.get(|| { + parse_descriptor_proto() + }) +} diff --git a/encryptor/decryptor/Cargo.toml b/encryptor/decryptor/Cargo.toml new file mode 100644 index 0000000..2cf5dde --- /dev/null +++ b/encryptor/decryptor/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "decryptor" +version = "0.1.0" +edition = "2018" + +[dependencies] +common = { path = "../common" } +protobuf = "2" +secretbox = "0" +base64 = "0" diff --git a/encryptor/decryptor/src/main.rs b/encryptor/decryptor/src/main.rs new file mode 100644 index 0000000..3dd7bc7 --- /dev/null +++ b/encryptor/decryptor/src/main.rs @@ -0,0 +1,23 @@ +use protobuf::Message; +use std::io::Read; + +use common::{protos,B64C,KEY}; + +fn main() { + let mut input = String::new(); + std::io::stdin().read_to_string(&mut input).unwrap(); + let input: String = input.split("\n").collect::>().join("").chars().filter(|c| !c.is_whitespace()).collect(); + + let s_bytes = base64::decode_config(input, B64C).expect("base64 decode"); + let s = protos::smtp::SMTPSource::parse_from_bytes(&s_bytes).expect("deproto"); + let v1_encrypted = s.get_v1_encrypted(); + if v1_encrypted.len() == 0 { + println!("v1_encrypted not set"); + return; + } + let b = secretbox::SecretBox::new(KEY, secretbox::CipherType::Salsa20).unwrap(); + let v1_bytes = b.easy_unseal(&v1_encrypted).expect("invalid encrypted data"); + + let v1 = protos::smtp::SMTPSource_V1::parse_from_bytes(&v1_bytes).expect("deproto v1"); + println!("{:?}", v1); +} diff --git a/encryptor/encryptor/Cargo.toml b/encryptor/encryptor/Cargo.toml new file mode 100644 index 0000000..d41a6c8 --- /dev/null +++ b/encryptor/encryptor/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "encryptor" +version = "0.1.0" +edition = "2018" + +[lib] +name = "encryptor" +crate-type = ["cdylib"] + +[dependencies] +common = { path = "../common" } +protobuf = "2" +secretbox = "0" +base64 = "0" diff --git a/encryptor/encryptor/src/lib.rs b/encryptor/encryptor/src/lib.rs new file mode 100644 index 0000000..549d2b7 --- /dev/null +++ b/encryptor/encryptor/src/lib.rs @@ -0,0 +1,64 @@ +use std::os::raw; +use std::time::SystemTime; + +use protobuf::Message; + +use common::{protos,B64C,KEY}; + +extern "C" { + fn string_copyn_function( + i: *const raw::c_char, + l: raw::c_int, + ) -> *mut raw::c_char; +} + +fn write_result(yield_: *mut*const raw::c_char, s: String) { + let s: &str = s.as_str(); + let ptr = s.as_ptr() as *const raw::c_char; + unsafe { + let copy = string_copyn_function(ptr, s.len() as i32); + *yield_ = copy; + } +} + +#[no_mangle] +pub extern "C" fn encryptor(yield_: *mut*const raw::c_char, argc: raw::c_int, argv: *const*const raw::c_char) -> raw::c_int { + if argc != 1 { + write_result(yield_, format!("wrong arg count (want one, got {})", argc)); + return 3; + } + + let cstr = unsafe { std::ffi::CStr::from_ptr(*argv) }; + let sender = match cstr.to_str() { + Ok(s) => s, + Err(_) => { + write_result(yield_, "invalid unicode".to_string()); + return 3; + } + }; + + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos() as i64; + + let mut v1 = protos::smtp::SMTPSource_V1::new(); + v1.set_timestamp(now); + v1.set_sender(sender.to_string()); + let v1_bytes = v1.write_to_bytes().unwrap(); + + let b = secretbox::SecretBox::new(KEY, secretbox::CipherType::Salsa20).unwrap(); + let v1_encrypted = b.easy_seal(&v1_bytes); + + let mut s = protos::smtp::SMTPSource::new(); + s.set_v1_encrypted(v1_encrypted); + let s_bytes = s.write_to_bytes().unwrap(); + + let s_base64 = base64::encode_config(s_bytes, B64C); + let wrapped = s_base64.chars() + .collect::>() + .chunks(70) + .map(|c| c.iter().collect::()) + .collect::>() + .join("\n "); + + write_result(yield_, wrapped); + return 0; +} diff --git a/forgejo.nix b/forgejo.nix new file mode 100644 index 0000000..a216ed8 --- /dev/null +++ b/forgejo.nix @@ -0,0 +1,67 @@ +{ config, pkgs, ... }: + +{ + nixpkgs.overlays = [ + (self: super: { + # Always add '@cebula.camp' to LDAP email attribute, so that we can set + # the attribute to 'cn'. We don't have any other way to get an + # @cebula.camp email address. + forgejo-lts = super.forgejo-lts.overrideAttrs (oa: { + patches = oa.patches ++ [ + ./forgejo/0001-bad-bad-not-good-patch-it-s-3-am-and-i-am-tired.patch + ]; + doCheck = false; + }); + }) + ]; + services.forgejo = { + enable = true; + lfs.enable = true; + settings = { + service = { + DISABLE_REGISTRATION = false; + ALLOW_ONLY_EXTERNAL_REGISTRATION = true; + ENABLE_NOTIFY_MAIL = false; + }; + server = { + ROOT_URL = "https://git.orga.cebula.camp"; + HTTP_PORT = 3001; + DOMAIN = "git.orga.cebula.camp"; + START_SSH_SERVER = true; + SSH_PORT = 22; + SSH_LISTEN_PORT = 2223; + BUILTIN_SSH_SERVER_USER = "git"; + }; + oauth2_client = { + REGISTER_EMAIL_CONFIRM = false; + ENABLE_AUTO_REGISTRATION = true; + USERNAME = "nickname"; + ACCOUNT_LINKING = "auto"; + }; + DEFAULT = { + APP_ANME = "CebulaGit"; + }; + }; + }; + + #systemd.services.forgejo-secrets.script = '' + # ${pkgs.forgejo}/bin/gitea admin user create --username bofh --password dupa.8 --email q3k@q3k.org --admin --must-change-password=false + #''; + + services.nginx.virtualHosts."git.orga.cebula.camp" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://localhost:3001"; + }; + }; + + # redirect external port 22 to internal 2223 + networking.firewall.allowedTCPPorts = [ 22 2223 ]; + networking.firewall.extraCommands = '' + iptables -t nat -A PREROUTING -p tcp --dport 22 -j REDIRECT --to-port 2223 + ''; + networking.firewall.extraStopCommands = '' + iptables -t nat -F PREROUTING + ''; +} diff --git a/forgejo/0001-bad-bad-not-good-patch-it-s-3-am-and-i-am-tired.patch b/forgejo/0001-bad-bad-not-good-patch-it-s-3-am-and-i-am-tired.patch new file mode 100644 index 0000000..597011b --- /dev/null +++ b/forgejo/0001-bad-bad-not-good-patch-it-s-3-am-and-i-am-tired.patch @@ -0,0 +1,34 @@ +From 09949e09b153d14bffdd765d48394f046a0848ef Mon Sep 17 00:00:00 2001 +From: Serge Bazanski +Date: Mon, 20 Jan 2025 02:58:07 +0100 +Subject: [PATCH] bad bad not good patch it's 3 am and i am tired + +--- + services/auth/source/ldap/source_search.go | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go +index 2a61386ae1..aceefe53ad 100644 +--- a/services/auth/source/ldap/source_search.go ++++ b/services/auth/source/ldap/source_search.go +@@ -362,7 +362,7 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR + username := sr.Entries[0].GetAttributeValue(source.AttributeUsername) + firstname := sr.Entries[0].GetAttributeValue(source.AttributeName) + surname := sr.Entries[0].GetAttributeValue(source.AttributeSurname) +- mail := sr.Entries[0].GetAttributeValue(source.AttributeMail) ++ mail := sr.Entries[0].GetAttributeValue(source.AttributeMail) + "@cebula.camp" + + if isAttributeSSHPublicKeySet { + sshPublicKey = sr.Entries[0].GetAttributeValues(source.AttributeSSHPublicKey) +@@ -490,7 +490,7 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) { + Username: v.GetAttributeValue(source.AttributeUsername), + Name: v.GetAttributeValue(source.AttributeName), + Surname: v.GetAttributeValue(source.AttributeSurname), +- Mail: v.GetAttributeValue(source.AttributeMail), ++ Mail: v.GetAttributeValue(source.AttributeMail) + "@cebula.camp", + IsAdmin: checkAdmin(l, source, v.DN), + Groups: usersLdapGroups, + } +-- +2.44.2 + diff --git a/forgejo/signin_inner.tmpl b/forgejo/signin_inner.tmpl new file mode 100644 index 0000000..64b2f1a --- /dev/null +++ b/forgejo/signin_inner.tmpl @@ -0,0 +1,72 @@ +{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}} +{{template "base/alert" .}} +{{end}} +

+ {{if .LinkAccountMode}} + {{ctx.Locale.Tr "auth.oauth_signin_title"}} + {{else}} + {{ctx.Locale.Tr "auth.login_userpass"}} + {{end}} +

+
+
+ {{.CsrfTokenHtml}} +
+
+ + +
+ {{if or (not .DisablePassword) .LinkAccountMode}} +
+ + +
+ {{end}} + {{if not .LinkAccountMode}} +
+
+ + +
+
+ {{end}} + + {{template "user/auth/captcha" .}} + +
+ + {{ctx.Locale.Tr "auth.forgot_password"}} +
+ + {{if .ShowRegistrationButton}} + + {{end}} + + {{if .OAuth2Providers}} +
+ {{ctx.Locale.Tr "sign_in_or"}} +
+
+ + {{end}} +
+
diff --git a/hardware-configuration.nix b/hardware-configuration.nix new file mode 100644 index 0000000..ec701ce --- /dev/null +++ b/hardware-configuration.nix @@ -0,0 +1,28 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/profiles/qemu-guest.nix") + ]; + + boot.initrd.availableKernelModules = [ "uhci_hcd" "ehci_pci" "ahci" "virtio_pci" "sr_mod" "virtio_blk" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { device = "/dev/disk/by-uuid/32cb35b8-5b9e-41f7-888e-8b29be3e4c5e"; + fsType = "ext4"; + }; + + fileSystems."/boot" = + { device = "/dev/disk/by-uuid/A74D-903F"; + fsType = "vfat"; + }; + + swapDevices = [ ]; + +} diff --git a/mailserver.nix b/mailserver.nix new file mode 100644 index 0000000..3aaa57f --- /dev/null +++ b/mailserver.nix @@ -0,0 +1,247 @@ +{ config, pkgs, lib, ... }: let + + aliases = lib.fix (self: { + postmaster = [ "q3k" ]; + MAILER-DAEMON = self.postmaster; + + security = self.postmaster; + nobody = self.postmaster; + usenet = self.postmaster; + uucp = self.postmaster; + webmaster = self.postmaster; + www = self.postmaster; + + orga = [ "rheya" "mewp" "martyna" ]; + cfp = [ "martyna" ]; + conduct = [ "mewp" "q3k" ]; + }); + mkAlias = name: members: "${name}: ${lib.concatStringsSep ", " members}"; + aliasFile = pkgs.writeTextFile { + name = "aliases"; + text = lib.concatStringsSep "\n" (lib.mapAttrsToList mkAlias aliases); + }; + acmeDir = config.security.acme.certs."mail.orga.cebula.camp".directory; + + # Little tool to encrypt the SMTP source. + encryptor = pkgs.rustPlatform.buildRustPackage { + name = "encryptor"; + src = ./encryptor; + #cargoSha256 = lib.fakeSha256; + cargoSha256 = "sha256:1ky42jssq3sk2a3lf29qc019bwj73rczcaqdjaick3nmz7yf1679"; + }; + +in { + security.acme.certs."mail.orga.cebula.camp" = { + group = "acme-mail"; + }; + networking.firewall.allowedTCPPorts = [ 25 587 143 993 465 ]; + users.groups.acme-mail = { + members = [ + config.services.nginx.user + config.services.dovecot2.user + config.services.exim.user + ]; + }; + services.nginx.virtualHosts."mail.orga.cebula.camp" = { + forceSSL = true; + enableACME = true; + }; + services.exim = { + enable = true; + package = (pkgs.exim.override { enableLDAP = true; }).overrideAttrs (oa: { + preBuild = '' + sed ' + s:^# \(EXPAND_DLFUNC\)=.*:\1=yes: + s:^# \(EXTRALIBS_EXIM\)=.*:\1=-ldl -rdynamic -export-dynamic: + ' -i Local/Makefile + ''; + }); + config = '' + primary_hostname = szalotka.cebula.camp + domainlist local_domains = cebula.camp + hostlist relay_from_hosts = 127.0.0.1 + qualify_domain = cebula.camp + + acl_smtp_rcpt = acl_check_rcpt + acl_smtp_data = acl_check_data + + tls_advertise_hosts = * + tls_certificate = ${acmeDir}/fullchain.pem + tls_privatekey = ${acmeDir}/key.pem + tls_on_connect_ports = 465 + + daemon_smtp_ports = 25 : 587 : 465 + + never_users = root + host_lookup = * + + rfc1413_hosts = * + rfc1413_query_timeout = 5s + + ignore_bounce_errors_after = 2d + timeout_frozen_after = 7d + + begin acl + + acl_check_rcpt: + accept hosts = : + control = dkim_disable_verify + + deny message = Restricted characters in address + domains = +local_domains + local_parts = ^[.] : ^.*[@%!/|] + + accept local_parts = postmaster + domains = +local_domains + + require verify = sender + + accept hosts = +relay_from_hosts + control = submission + control = dkim_disable_verify + + accept authenticated = * + control = submission + control = dkim_disable_verify + + require message = Relay not permitted + domains = +local_domains + + require verify = recipient + + accept + + acl_check_data: + + accept + + begin routers + + dnslookup: + driver = dnslookup + domains = ! +local_domains + transport = remote_smtp + ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8 + headers_add = X-Cebulacamp-Smtp-Source: ''${dlfunc{${encryptor}/lib/libencryptor.so}{encryptor}{$sender_rcvhost}} + headers_remove = Received + no_more + + aliases: + driver = redirect + allow_fail + allow_defer + data = ''${lookup{$local_part}lsearch{${aliasFile}} {$value@cebula.camp}} + + ldap_user: + driver = redirect + data = ''${lookup ldap { user="cn=ldap-access,ou=users,dc=cebula,dc=camp" pass="''${readfile(/var/secrets/ldap-access)}" \ + ldap://10.88.0.1:389/ou=users,dc=cebula,dc=camp?\ + cn?sub?(cn=''${quote_ldap:$local_part})} {$value@$domain}} + cannot_route_message = Unknown address + more = false + + localuser: + driver = accept + transport = local_delivery + + begin transports + + remote_smtp: + driver = smtp + + local_delivery: + driver = lmtp + socket = /var/run/dovecot2/lmtp + batch_max = 200 + rcpt_include_affixes + + + begin authenticators + + PLAIN: + driver = plaintext + server_set_id = $auth2 + server_prompts = : + server_advertise_condition = ''${if def:tls_cipher } + server_condition = ''${if and{{ \ + !eq{}{$auth2} }{ \ + ldapauth{\ + user="cn=''${quote_ldap_dn:$auth2},ou=users,dc=cebula,dc=camp" \ + pass=''${quote:$auth3} \ + ldap://10.88.0.1/} }} } + + LOGIN: + driver = plaintext + server_set_id = $auth1 + server_prompts = <| Username: | Password: + server_advertise_condition = ''${if def:tls_cipher } + server_condition = ''${if and{{ \ + !eq{}{$auth1} }{ \ + ldapauth{\ + user="cn=''${quote_ldap_dn:$auth1},ou=users,dc=cebula,dc=camp" \ + pass=''${quote:$auth2} \ + ldap://10.88.0.1/} }} } + ''; + }; + + services.dovecot2 = { + enable = true; + mailUser = "vmail"; + mailGroup = "vmail"; + createMailUser = true; + enableLmtp = true; + sslServerCert = "${acmeDir}/fullchain.pem"; + sslServerKey = "${acmeDir}/key.pem"; + enablePAM = false; + + extraConfig = '' + userdb { + driver = ldap + args = /etc/dovecot/dovecot-users-ldap.conf.ext + } + passdb { + driver = ldap + args = /etc/dovecot/dovecot-pass-ldap.conf.ext + } + protocol lmtp { + auth_username_format = %n + } + ''; + }; + + systemd.services.dovecot2.preStart = '' + cat > /etc/dovecot/dovecot-users-ldap.conf.ext < /etc/dovecot/dovecot-pass-ldap.conf.ext <