These configs use nix flakes which are not stable yet. To enable them we need to
overwrite the nix package by adding the following to the current
/etc/nixos/configuration.nix
.
nix.package = pkgs.nixUnstable;
nix.extraOptions = ''
experimental-features = nix-command flakes
'';
After nixos-rebuild switch
we should have nix
version >= 2.4.
When bootstrapping the installation for a new host we can get nix by installing
nixUnstable
with nix-env
.
nix-env -iA nixpkgs.nixUnstable
To enable flakes we then update /etc/nix/nix.conf
.
echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf
The flake.nix
file in this repository contains the flakes for the individual
hosts. This file and all other *.nix
files are generated by tangling
README.org
(the file you’re currently reading). The individual *.nix
files
should not be modified directly since those changes will be overwritten the next
time org-babel-tangle
is called.
{
description = "NixOS System Configurations";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
mailserver = {
url = "gitlab:simple-nixos-mailserver/nixos-mailserver";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, mailserver, nixpkgs }: {
nixosConfigurations = {
<<flake/hosts>>
};
};
}
New files or folders generated by tangling this file must be added to the git
repository or nixos-rebuild
won’t pick them up.
Some hardware info displayed by the Hetzner rescue system after login.
CPU1: AMD Ryzen 7 3700X 8-Core Processor (Cores 16) Memory: 64251 MB Disk /dev/sda: 10000 GB (=> 9314 GiB) Disk /dev/sdb: 10000 GB (=> 9314 GiB) Total capacity 18 TiB with 2 Disks
The basic hardware configuration was created by nixos-generate-config
.
{ config, lib, pkgs, modulesPath, ... }:
{
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
<<beholder/hardware/boot>>
<<beholder/hardware/filesystems>>
swapDevices = [ ];
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand";
}
Since we are using encrypted partitions and have no way to unlock them without a
remote connection, we need to make sure the network interface is available
within the initrd
. In this case we depend on the r8169
module to be
available during boot. Also tell the initrd
whick device needs encryption.
We also need to set MAILADDR
for mdadm
or the mdmon
service will crash.
boot = {
kernelModules = [ "kvm-amd" ];
initrd = {
availableKernelModules = [ "ahci" "sd_mod" "r8169" ];
luks.devices."data".device = "/dev/disk/by-uuid/db20b995-5cab-45ae-b1b9-0ba20a73eda6";
};
swraid.mdadmConf = "MAILADDR root";
};
fileSystems."/" =
{ device = "/dev/disk/by-uuid/4328df85-8112-495a-ba18-2b23d6fa54e2";
fsType = "btrfs";
options = [ "subvol=root" ];
};
fileSystems."/home" =
{ device = "/dev/disk/by-uuid/4328df85-8112-495a-ba18-2b23d6fa54e2";
fsType = "btrfs";
options = [ "subvol=home" ];
};
fileSystems."/nix" =
{ device = "/dev/disk/by-uuid/4328df85-8112-495a-ba18-2b23d6fa54e2";
fsType = "btrfs";
options = [ "subvol=nix" ];
};
fileSystems."/var" =
{ device = "/dev/disk/by-uuid/4328df85-8112-495a-ba18-2b23d6fa54e2";
fsType = "btrfs";
options = [ "subvol=var" ];
};
fileSystems."/swap" =
{ device = "/dev/disk/by-uuid/4328df85-8112-495a-ba18-2b23d6fa54e2";
fsType = "btrfs";
options = [ "subvol=swap" ];
};
fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/22528ebc-c073-447d-a2cb-74854cc11a19";
fsType = "ext4";
};
fileSystems."/boot-fallback" =
{ device = "/dev/disk/by-uuid/1da184b8-3e1c-4492-9bc1-fd04a2d0e343";
fsType = "ext4";
};
Add this host to the flakes nixosConfigurations
.
beholder = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
mailserver.nixosModules.mailserver
./hosts/beholder/configuration.nix
];
};
{ config, lib, pkgs, mailserver, ... }:
{
imports = [
./hardware-configuration.nix
../../modules/common.nix
../../modules/nginx.nix
../../modules/misskey.nix
../../users/wose.nix
../../users/linda.nix
];
<<beholder/cfg/kernel>>
<<beholder/cfg/bootloader>>
<<beholder/cfg/initrd>>
<<beholder/cfg/network>>
<<beholder/cfg/i18n>>
<<beholder/cfg/packages>>
<<beholder/cfg/services>>
system.stateVersion = "21.05";
}
I had some trouble during the initial installation regarding the network
connectivity after Grub
finished it’s thing. There was no ssh connection
possible to unlock the encrypted partition. I fist suspected the predictable
network interface naming to do something … well unpredictable. So I disabled
it. The resuce system I used to install and troubleshoot the system was Debian
based and had this feature also turned off.
But this changed nothing. I then mistrusted the NixOS configuration to configure
the initrd
network properly (IP address, netmask, etc) and added the
corresponding kernel parameters manually.
This also didn’t fix the problem. As it turned out the network interface driver
was missing in the initrd
. However, I kept the manually set kernel parameters
for now.
boot.kernelParams = [
"net.ifnames=0"
"ip=136.243.47.110::136.243.47.65:255.255.255.192:beholder:eth0:none"
];
We install Grub
on both disks, so we can still boot if one fails. We also need
to mirror the boot partition, which holds the kernel, initrd etc. We do this by
using the NixOS option mirroredBoots
and save us the trouble with configuring
another RAID array.
boot.loader.grub = {
enable = true;
efiSupport = false;
devices = [ "/dev/sda" ];
mirroredBoots = [
{
path = "/boot-fallback";
devices = [ "/dev/sdb" ];
}
];
};
To be able to unlock the encrypted partition we need to enable networking and a ssh server within the initrd. And also set some public keys which should be allowed to login. The host key was created during the inital bootstrap and should be distributed to the clients to make sure they are talking to the right server when sending the luks passphrase.
boot.initrd.network = {
enable = true;
ssh = {
enable = true;
port = 22;
authorizedKeys = config.users.users.wose.openssh.authorizedKeys.keys;
hostKeys = [ "/etc/secrets/initrd/ssh_host_ed25519_key" ];
};
};
The hardest choice during every installation of a new system: the hostname. I’m using the D&D 5e Monster Manual as name source.
networking = {
hostName = "beholder";
domain = "zuendmasse.de";
};
We only have one NIC. Hetzner doesn’t use DHCP so we disable it and use the provided static configuration.
networking = {
usePredictableInterfaceNames = false;
useDHCP = false;
interfaces.eth0 = {
ipv4.addresses = [
{
address = "136.243.47.110";
prefixLength = 26;
}
];
ipv6.addresses = [
{
address = "2a01:4f8:212:f45::1";
prefixLength = 64;
}
];
};
defaultGateway = "136.243.47.65";
defaultGateway6 = {
address = "fe80::1";
interface = "eth0";
};
};
Those are name servers from Hetzner… except the last one.
networking.nameservers = [
"213.133.98.98"
"213.133.99.99"
"213.133.100.100"
"2a01:4f8:0:a0a1::add:1010"
"2a01:4f8:0:a102::add:9999"
"2a01:4f8:0:a111::add:9898"
"8.8.8.8"
];
Set the time zone and default locale.
time.timeZone = "Europe/Berlin";
i18n.defaultLocale = "en_US.UTF-8";
We create the users on this system by importing the corresponding
users/username.nix
files.
environment.systemPackages = with pkgs; [
borgbackup
emacs-nox
fd
pinentry-emacs
ripgrep
];
services.postgresql = {
enable = true;
ensureDatabases = [
<<beholder/db>>
];
ensureUsers = [
<<beholder/db/users>>
];
};
We disable password authentication and root login. This will also open port 22 in the firewall.
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "no";
PasswordAuthentication = false;
};
};
services.nginx.virtualHosts."erlija.de" = {
serverAliases = [ "www.erlija.de" "erlija.de" ];
enableACME = true;
forceSSL = true;
root = "/var/www/erlija.de";
};
services.nginx.virtualHosts."wittch.de" = {
serverAliases = [ "www.wittch.de" "wittch.de" ];
enableACME = true;
forceSSL = true;
root = "/var/www/wittch.de";
};
The rewrite
rules are here so old links to the blog posts don’t break. I
wasn’t able to construct a regular expression which would match paths with an
optional trailing slash. Something like ^/blog/.*/(.*)/?$
didn’t work, the
match was greedy and had the slash as part of the match.
services.nginx.virtualHosts."zuendmasse.de" = {
serverAliases = [ "zuendmasse.de" "www.zuendmasse.de" ];
enableACME = true;
forceSSL = true;
locations."/" = {
root = "/var/www/zuendmasse.de";
extraConfig = ''
rewrite ^/blog/2018/02/23/lets-write-an-embedded-hal-driver.*$ /lets-write-an-embedded-hal-driver.html permanent;
rewrite ^/blog/2018/01/21/gdb-\+-svd.*$ /gdb-svd.html permanent;
rewrite ^/blog/2018/01/19/pdf-multi-view.*$ /multi-view-pdf.html permanent;
rewrite ^/blog/2017/11/03/datenspuren.*$ /datenspuren.html permanent;
rewrite ^/blog/2017/08/26/embedded-rust.*$ /embedded-rust.html permanent;
rewrite ^/blog/2017/08/22/reset.*$ /reset.html permanent;
rewrite ^/blog/$ / permanent;
rewrite ^/blog$ / permanent;
rewrite ^/assets/.*/(.+)$ /images/$1 permanent;
rewrite ^/about.*$ /pages/about.html permanent;
'';
};
listen = [
{ addr = "[::]"; port = 80; ssl = false; }
{ addr = "0.0.0.0"; port = 80; ssl = false; }
{ addr = "[::]"; port = 443; ssl = true; }
{ addr = "0.0.0.0"; port = 443; ssl = true; }
{ addr = "[::]"; port = 8448; ssl = true; }
{ addr = "0.0.0.0"; port = 8448; ssl = true; }
];
locations."/_matrix" = {
proxyPass = "http://localhost:8008";
};
locations."= /.well-known/matrix/server".extraConfig =
let
server = { "m.server" = "zuendmasse.de:8448"; };
in ''
add_header Content-Type application/json;
return 200 '${builtins.toJSON server}';
'';
locations."= /.well-known/matrix/client".extraConfig =
let
client = {
"m.homeserver" = { "base_url" = "https://zuendmasse.de:8448"; };
"m.identity_server" = { "base_url" = "https://vector.im"; };
};
in ''
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON client}';
'';
};
Local user need to generate the corresponding hashed password files with. This file must be present or the service will not start.
nix shell nixpkgs#apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 > ~/.hashed_passwd_domain
mailserver = {
enable = true;
localDnsResolver = false;
enableImap = true;
enablePop3 = false;
enableImapSsl = true;
enablePop3Ssl = false;
fqdn = "beholder.zuendmasse.de";
domains = [ "braunglasmafia.de" "erlija.de" "wittch.de" ];
loginAccounts = {
"[email protected]" = {
hashedPasswordFile = "/home/wose/.hashed_passwd_erlija.de";
aliases = [
"[email protected]"
"[email protected]"
"[email protected]"
"[email protected]"
"[email protected]"
"[email protected]"
];
};
"[email protected]" = {
hashedPasswordFile = "/home/wose/.hashed_passwd_wittch.de";
aliases = [
"[email protected]"
"[email protected]"
"[email protected]"
];
};
"[email protected]" = {
hashedPasswordFile = "/home/linda/.hashed_passwd_erlija.de";
aliases = [
"[email protected]"
];
};
};
certificateScheme = "acme-nginx";
};
This synapse
server was migrated from another machine. Since all users are
already in the datebase we don’t need to enable any type of registration. The
sqlite3
database and the media files (uploaded data etc.) are stored in
/var/lib/matrix-synapse/
.
networking.firewall.allowedTCPPorts = [ 8448 1965 6697 ];
networking.firewall.allowedUDPPorts = [ 2456 2457 2458 ];
services.matrix-synapse = {
enable = true;
extraConfigFiles = [
/etc/secrets/matrix-registration-config
];
# registration_shared_secret = builtins.readFile /etc/secrets/matrix-registration;
settings = {
listeners = [
{
port = 8008;
bind_addresses = [ "::1" ];
type = "http";
tls = false;
x_forwarded = true;
resources = [
{
names = [ "client" "federation" ];
compress = false;
}
];
}
];
database.name = "sqlite3";
server_name = "zuendmasse.de";
allow_guest_access = false;
enable_registration = false;
max_upload_size = "512M";
};
};
services.gitea = {
enable = true;
package = pkgs.forgejo;
appName = "forgejo";
settings = {
service.DISABLE_REGISTRATION = true;
server = {
HTTP_PORT = 3200;
HTTP_ADDR = "127.0.0.1";
DOMAIN = "git.zuendmasse.de";
ROOT_URL = "https://git.zuendmasse.de";
LANDING_PAGE = "/explore/repos";
};
};
};
services.nginx.virtualHosts."git.zuendmasse.de" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://localhost:3200";
};
};
services.fail2ban.enable = true;
We use postgresql
as database for this nextcloud
instance. Don’t forget to
create the adminpassFile
and the dbpassFile
on a fresh bootstrap. Those
files need to be readable by the nextcloud
user.
services.nextcloud = {
enable = true;
package = pkgs.nextcloud27;
hostName = "cloud.zuendmasse.de";
https = true;
enableBrokenCiphersForSSE = false;
caching = {
apcu = true;
memcached = true;
redis = false;
};
config = {
defaultPhoneRegion = "DE";
adminuser = "admin";
adminpassFile = "/etc/secrets/nextcloud-pass";
dbtype = "pgsql";
dbname = "nextcloud";
dbuser = "nextcloud";
dbpassFile = "/etc/secrets/psql-pass";
dbhost = "/run/postgresql";
dbtableprefix = "oc_";
};
autoUpdateApps = {
enable = true;
startAt = "04:00:00";
};
maxUploadSize = "2048M";
};
systemd.services."nextcloud-setup" = {
requires = ["postgresql.service"];
after = ["postgresql.service"];
};
services.nginx.virtualHosts."cloud.zuendmasse.de" = {
enableACME = true;
forceSSL = true;
extraConfig = ''
add_header Strict-Transport-Security "max-age=31536000" always;
client_body_buffer_size 512k;
'';
};
Let’s make sure the nextcloud database exists.
"nextcloud"
And add the corresponding db user with the required rights.
{ name = "nextcloud";
ensurePermissions."DATABASE nextcloud" = "ALL PRIVILEGES";
}
To enable preview generation for larger images we need to increase the maximum
preview memory within the nextcloud config file. There is currently no
method to add extra options to the config, but we can simply create another
file within the nextcloud config directory. The file name must end in
.config.php
and will then be automatically loaded by nextcloud.
# TODO find out how to write this to /var/lib/nextcloud/config/preview.config.php
<?php
$CONFIG = [
'preview_max_memory' => 512,
];
NixOS has service options to configure the Gemini server molly-brown
. This
server doesn’t support virtual hosts but since we only have one site anyway we
use it for now. Another option, which supports virtual hosts but lags dynamic
content (CGI, SCGI), would be agate
.
services.molly-brown = {
hostName = "zuendmasse.de";
enable = true;
certPath = "/var/lib/acme/zuendmasse.de/cert.pem";
keyPath = "/var/lib/acme/zuendmasse.de/key.pem";
docBase = "/var/gemini/zuendmasse.de";
};
We will use the certificate generated for zuendmasse.de
. This means
molly-brown
must be able to read it.
systemd.services.molly-brown.serviceConfig.SupplementaryGroups = [ config.security.acme.certs."zuendmasse.de".group ];
And make sure we can connect to the server.
#networking.firewall.allowedTCPPorts = [ 1965 ];
services.misskey = {
enable = true;
settings = {
url = "https://social.zuendmasse.de/";
port = 11231;
id = "aid";
db = {
host = "/run/postgresql";
port = config.services.postgresql.port;
user = "misskey";
db = "misskey";
};
redis = {
host = "localhost";
port = config.services.redis.servers.misskey.port;
};
};
};
services.redis.servers.misskey = {
enable = true;
bind = "127.0.0.1";
port = 16434;
};
services.nginx.virtualHosts."social.zuendmasse.de" = {
enableACME = true;
forceSSL = true;
locations = {
"/" = {
proxyPass = "http://127.0.0.1:${toString config.services.misskey.settings.port}/";
proxyWebsockets = true;
};
};
};
Make sure the db exists.
"misskey"
And add the corresponding db user with the required rights.
{ name = "misskey";
ensurePermissions."DATABASE misskey" = "ALL PRIVILEGES";
}
Some packages which are not in nixpkgs
upstream yet.
{ pkgs ? import <nixpkgs> { } }:
{
gotosocial = pkgs.callPackage ./gotosocial { };
misskey = pkgs.callPackage ./misskey { };
}
{ buildGoModule, fetchFromGitHub, lib, pkgs }:
buildGoModule rec {
pname = "gotosocial";
version = "0.2.1";
src = fetchFromGitHub {
owner = "superseriousbusiness";
repo = "gotosocial";
rev = "v${version}";
sha256 = "ldU2wzahCjy2rThcBIuzHCSWtUK1jxDEB+QKBv8Aqrw=";
};
vendorSha256 = null;
Currently tests are failing because of some hardcoded path. #412
doCheck = false;
tags = [ "netgo" "osusergo" "static_build" ];
ldflags = ["-s" "-w" "-extldflags '-static'" "-X 'main.Version=${version}'" ];
meta = with lib; {
description = "Golang fediverse server";
homepage = "https://docs.gotosocial.org";
license = licenses.agpl3Only;
maintainer = with maintainers; [ wose ];
platforms = platforms.linux;
};
}
This package definition is part of a pending pull reuqest to add Misskey to nixpkgs. #161855
{ lib
, stdenv
, fetchFromGitHub
, fetchYarnDeps
, fixup_yarn_lock
, yarn
, nodejs
, python3
, pkg-config
, glib
, vips
}:
let
version = "12.108.1";
src = fetchFromGitHub {
owner = "misskey-dev";
repo = "misskey";
rev = version;
sha256 = "sha256-NTspyTNy3cqc43+YLeCKRR46D7BvtIWoNCmwgqykHgs=";
};
deps = fetchYarnDeps {
yarnLock = "${src}/yarn.lock";
sha256 = "sha256-1NEeuBVp5e7RtFzYeT4nTGxGs2oeTxqiz20pEZXmcbo=";
};
backendDeps = fetchYarnDeps {
yarnLock = "${src}/packages/backend/yarn.lock";
sha256 = "sha256-G01hkYthBCZnsvPNaTIXSgTN9/1inJXhh34umxfxUsc=";
};
clientDeps = fetchYarnDeps {
yarnLock = "${src}/packages/client/yarn.lock";
sha256 = "sha256-LwGjqHN59KditL3igVP1/TZ7cZSbrZopOl9A0c1nlW8=";
};
in stdenv.mkDerivation {
pname = "misskey";
inherit version src;
nativeBuildInputs = [ fixup_yarn_lock yarn nodejs python3 pkg-config ];
buildInputs = [ glib vips ];
buildPhase = ''
export HOME=$PWD
export NODE_ENV=production
# Build node modules
fixup_yarn_lock yarn.lock
fixup_yarn_lock packages/backend/yarn.lock
fixup_yarn_lock packages/client/yarn.lock
yarn config --offline set yarn-offline-mirror ${deps}
yarn install --offline --frozen-lockfile --ignore-engines --ignore-scripts --no-progress
(
cd packages/backend
yarn config --offline set yarn-offline-mirror ${backendDeps}
yarn install --offline --frozen-lockfile --ignore-engines --ignore-scripts --no-progress
)
(
cd packages/client
yarn config --offline set yarn-offline-mirror ${clientDeps}
yarn install --offline --frozen-lockfile --ignore-engines --ignore-scripts --no-progress
)
patchShebangs node_modules
patchShebangs packages/backend/node_modules
patchShebangs packages/client/node_modules
(
cd packages/backend/node_modules/re2
npm_config_nodedir=${nodejs} npm run rebuild
)
(
cd packages/backend/node_modules/sharp
npm_config_nodedir=${nodejs} ../.bin/node-gyp rebuild
)
yarn build
'';
installPhase = ''
mkdir -p $out/packages/client
ln -s /var/lib/misskey $out/files
ln -s /run/misskey $out/.config
cp -r locales node_modules built $out
cp -r packages/backend $out/packages/backend
cp -r packages/client/assets $out/packages/client/assets
'';
meta = with lib; {
description = "Interplanetary microblogging platform. 🚀";
homepage = "https://misskey-hub.net/";
license = licenses.agpl3;
maintainers = with maintainers; [ yuka kloenk ];
platforms = platforms.unix;
};
}
- https://github.com/Shou/nixos-configuration/blob/master/systems/nitori/valheim-server.nix
- https://github.com/lukebfox/nix-configs/blob/main/modules/nixos/services/valheim/default.nix
- https://github.com/johnviolano/nixos-system/blob/main/valheim-service.nix
- https://github.com/lukebfox/nix-configs/blob/main/modules/nixos/services/valheim/default.nix
We’ll need/want somethings to be available on all hosts. We can export it into modules and import it in each host.
{ pkgs, ... }:
{
<<common/systempackages>>
<<common/nix>>
<<common/nixpkgs>>
<<common/userconfig>>
}
To be able to acutally do something with a freshly installed system we need some tools.
environment.systemPackages = with pkgs; [
git
gnupg
gnutls
htop
pass
pinentry
vim
vimPlugins.vim-nix
wget
zsh
];
Since nix flakes are not stable yet, we need to make sure we get nix
in
version >=2.4
.
nix = {
package = pkgs.nixUnstable;
extraOptions = ''
experimental-features = nix-command flakes
'';
};
We need to create an overlay to inject our own packages which are currently not
part of upstream nixpgs
.
nixpkgs = {
overlays = [
(self: super:
(import ../pkgs/default.nix { pkgs = super; })
)
];
};
Let’s make sure user can manage their passwords outside of this configuration.
users.mutableUsers = true;
The goal is to be able to simply enable the gotosocial
service and maybe set
some config values and be done with it. Something like:
gotosocial = {
enable = true;
bind-address = "0.0.0.0";
port = 8080;
db-type = "sqlite";
};
{ config, lib, pkgs, ... }:
with lib;
let
<<gotosocial/let>>
in
{
<<gotosocial/options>>
config = mkIf cfg.enable {
<<gotosocial/config>>
};
}
cfg = config.local.services.gotosocial;
name = "gotosocial";
stateDir = "/var/lib/${name}";
settingsFormat = pkgs.formats.yaml { };
#toYAML = name: data: pkgs.writeText name (generators.toYAML {} data);
#configFile = toYAML "gotosocial.yaml" cfg.settings;
#configFile = settingsFormat.generate "gotosocial.yaml" cfg.settings;
#configFile = "/home/wose/gotosocial/config.yaml";
#configFile = pkgs.writeText "gotosocial.yaml" (
# generators.toYAML {} { inherit cfg.settings; }
#);
We translate the corresponding config values into nix options. The resulting
config is a yaml
file.
options.local.services.gotosocial = {
enable = mkEnableOption "Golang fediverse server";
configFile = mkOption {
type = types.path;
description = "Path to gotosocial config.yaml.";
};
settings = mkOption {
# type = types.submodule {
type = settingsFormat.type;
# freeformType = settingsFormat.type;
options = {
#settings = {
host = mkOption {
type = types.str;
default = "localhost";
description = ''
Hostname that this server will be reachable at. Defaults to localhost for local testing,
but you should *definitely* change this when running for real, or your server won't work at all.
DO NOT change this after your server has already run once, or you will break things!
'';
};
account-domain = mkOption {
type = types.str;
default = "";
description = ''
Domain to use when federating profiles. This is useful when you want your server to be at
eg., "gts.example.org", but you want the domain on accounts to be "example.org" because it looks better
or is just shorter/easier to remember.
To make this setting work properly, you need to redirect requests at "example.org/.well-known/webfinger"
to "gts.example.org/.well-known/webfinger" so that GtS can handle them properly.
You should also redirect requests at "example.org/.well-known/nodeinfo" in the same way.
An empty string (ie., not set) means that the same value as 'host' will be used.
DO NOT change this after your server has already run once, or you will break things!
'';
};
protocol = mkOption {
type = types.enum [ "http" "https" ];
default = "https";
description = ''
Protocol to use for the server. Only change to http for local testing!
This should be the protocol part of the URI that your server is actually reachable on. So even if you're
running GoToSocial behind a reverse proxy that handles SSL certificates for you, instead of using built-in
letsencrypt, it should still be https.
'';
};
bind-address = mkOption {
type = types.str;
default = "0.0.0.0";
description = ''
Address to bind the GoToSocial server to.
This can be an IPv4 address or an IPv6 address (surrounded in square brackets), or a hostname.
Default value will bind to all interfaces.
You probably won't need to change this unless you're setting GoToSocial up in some fancy way or
you have specific networking requirements.
'';
};
port = mkOption {
type = types.port;
default = 8080;
description = ''
Listen port for the GoToSocial webserver + API. If you're running behind a reverse proxy and/or in a docker,
container, just set this to whatever you like (or leave the default), and make sure it's forwarded properly.
If you are running with built-in letsencrypt enabled, and running GoToSocial directly on a host machine, you will
probably want to set this to 443 (standard https port), unless you have other services already using that port.
This *MUST NOT* be the same as the letsencrypt port specified below, unless letsencrypt is turned off.
'';
};
trusted-proxies = mkOption {
type = types.listOf types.str;
default = [ "127.0.0.1/32" ];
description = ''
CIDRs or IP addresses of proxies that should be trusted when determining real client IP from behind a reverse proxy.
If you're running inside a Docker container behind Traefik or Nginx, for example, add the subnet of your docker network,
or the gateway of the docker network, and/or the address of the reverse proxy (if it's not running on the host network).
'';
};
db-type = mkOption {
type = types.enum [ "postgres" "sqlite" ];
default = "postgres";
description = "Database type.";
};
db-address = mkOption {
type = types.str;
default = "";
description = ''
For Postgres, this should be the address or socket at which the database can be reached.
For Sqlite, this should be the path to your sqlite database file. Eg., /opt/gotosocial/sqlite.db.
If the file doesn't exist at the specified path, it will be created.
If just a filename is provided (no directory) then the database will be created in the same directory
as the GoToSocial binary.
If address is set to :memory: then an in-memory database will be used (no file).
WARNING: :memory: should NOT BE USED except for testing purposes.
'';
};
db-port = mkOption {
type = types.port;
default = 5432;
description = "Port for database connection.";
};
db-user = mkOption {
type = types.str;
default = "";
description = "Username for the database connection.";
};
db-password = mkOption {
type = types.str;
default = "";
description = "Password to use for the database connection.";
};
db-database = mkOption {
type = types.str;
default = "gotosocial";
description = "Name of the database to use within the provided database type.";
};
db-tls-mode = mkOption {
type = types.enum [ "disable" "enable" "required" ];
default = "disable";
description = ''
Disable, enable, or require SSL/TLS connection to the database.
If "disable" then no TLS connection will be attempted.
If "enable" then TLS will be tried, but the database certificate won't be checked (for self-signed certs).
If "require" then TLS will be required to make a connection, and a valid certificate must be presented.
'';
};
db-tls-ca-cert = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a CA certificate on the host machine for db certificate validation.
If this is left empty, just the host certificates will be used.
If filled in, the certificate will be loaded and added to host certificates.
'';
};
web-template-base-dir = mkOption {
type = types.path;
default = "./web/template/";
description = "Directory from which gotosocial will attempt to load html templates (.tmpl files).";
};
web-asset-base-dir = mkOption {
type = types.path;
default = "./web/assets/";
description = "Directory from which gotosocial will attempt to serve static web assets (images, scripts).";
};
accounts-registration-open = mkOption {
type = types.bool;
default = false;
description = "Do we want people to be able to just submit sign up requests, or do we want invite only?";
};
accounts-approval-required = mkOption {
type = types.bool;
default = true;
description = "Do sign up requests require approval from an admin/moderator before an account can sign in/use the server?";
};
accounts-reason-required = mkOption {
type = types.bool;
default = false;
description = "Are sign up requests required to submit a reason for the request (eg., an explanation of why they want to join the instance)?";
};
media-image-max-size = mkOption {
type = types.ints.u32;
default = 2097152;
description = "Maximum allowed image upload size in bytes.";
};
media-video-max-size = mkOption {
type = types.ints/u32;
default = 10485760;
description = "Maximum allowed video upload size in bytes.";
};
media-description-min-chars = mkOption {
type = types.ints.u32;
default = 0;
description = "Minimum amount of characters required as an image or video description.";
};
media-description-max-chars = mkOption {
type = types.ints.u32;
default = 500;
description = "Maximum amount of characters permitted in an image or video description.";
};
media-remote-cache-days = mkOption {
type = types.ints.u16;
default = 30;
description = ''
Number of days to cache media from remote instances before they are removed from the cache.
A job will run every day at midnight to clean up any remote media older than the given amount of days.
When remote media is removed from the cache, it is deleted from storage but the database entries for the media
are kept so that it can be fetched again if requested by a user.
If this is set to 0, then media from remote instances will be cached indefinitely.
'';
};
storage-backend = mkOption {
type = types.str;
default = "local";
description = "Type of storage backend to use.";
};
storage-local-base-path = mkOption {
type = types.path;
default = "/gotosocial/storage";
description = ''
Directory to use as a base path for storing files.
Make sure whatever user/group gotosocial is running as has permission to access
this directory, and create new subdirectories and files within it.
'';
};
statuses-max-chars = mkOption {
type = types.ints.u16;
default = 5000;
description = ''
Maximum amount of characters permitted for a new status.
Note that going way higher than the default might break federation.
'';
};
statuses-cw-max-chars = mkOption {
type = types.ints.u16;
default = 100;
description = ''
Maximum amount of characters allowed in the CW/subject header of a status.
Note that going way higher than the default might break federation.
'';
};
statuses-poll-max-options = mkOption {
type = types.ints.u8;
default = 6;
description = ''
Maximum amount of options to permit when creating a new poll.
Note that going way higher than the default might break federation.
'';
};
statuses-poll-option-max-chars = mkOption {
type = types.ints.u8;
default = 50;
description = ''
Maximum amount of characters to permit per poll option when creating a new poll.
Note that going way higher than the default might break federation.
'';
};
statuses-media-max-files = mkOption {
type = types.ints.u8;
default = 6;
description = ''
Maximum amount of media files that can be attached to a new status.
Note that going way higher than the default might break federation.
'';
};
letsencrypt-enabled = mkOption {
type = types.bool;
default = false;
description = ''
Whether or not letsencrypt should be enabled for the server.
If false, the rest of the settings here will be ignored.
If you serve GoToSocial behind a reverse proxy like nginx or traefik, leave this turned off.
If you don't, then turn it on so that you can use https.
'';
};
letsencrypt-port = mkOption {
type = types.port;
default = 80;
description = ''
Port to listen for letsencrypt certificate challenges on.
If letsencrypt is enabled, this port must be reachable or you won't be able to obtain certs.
If letsencrypt is disabled, this port will not be used.
This *must not* be the same as the webserver/API port specified.
'';
};
letsencrypt-cert-dir = mkOption {
type = types.path;
default = "/gotosocial/storage/certs";
description = ''
Directory in which to store LetsEncrypt certificates.
It is a good move to make this a sub-path within your storage directory, as it makes
backup easier, but you might wish to move them elsewhere if they're also accessed by other services.
In any case, make sure GoToSocial has permissions to write to / read from this directory.
'';
};
letsencrypt-email-address = mkOption {
type = types.str;
default = "";
description = ''
Email address to use when registering LetsEncrypt certs.
Most likely, this will be the email address of the instance administrator.
LetsEncrypt will send notifications about expiring certificates etc to this address.
'';
};
oidc-enabled = mkOption {
type = types.bool;
default = false;
description = ''
Enable authentication with external OIDC provider. If set to true, then
the other OIDC options must be set as well. If this is set to false, then the standard
internal oauth flow will be used, where users sign in to GtS with username/password.
'';
};
oidc-idp-name = mkOption {
type = types.str;
default = "";
description = ''
Name of the oidc idp (identity provider). This will be shown to users when
they log in.
'';
};
oidc-skip-verification = mkOption {
type = types.bool;
default = false;
description = ''
Skip the normal verification flow of tokens returned from the OIDC provider, ie.,
don't check the expiry or signature. This should only be used in debugging or testing,
never ever in a production environment as it's extremely unsafe!
'';
};
oidc-issuer = mkOption {
type = types.str;
default = "";
description = ''
The OIDC issuer URI. This is where GtS will redirect users to for login.
Typically this will look like a standard web URL.
'';
};
oidc-client-id = mkOption {
type = types.str;
default = "";
description = "The ID for this client as registered with the OIDC provider.";
};
oidc-client-secret = mkOption {
type = types.str;
default = "";
description = "The secret for this client as registered with the OIDC provider.";
};
oidc-scopes = mkOption {
type = types.listOf types.str;
default = [ "openid" "email" "profile" "groups" ];
description = ''
Scopes to request from the OIDC provider. The returned values will be used to
populate users created in GtS as a result of the authentication flow. 'openid' and 'email' are required.
'profile' is used to extract a username for the newly created user.
'groups' is optional and can be used to determine if a user is an admin (if they're in the group 'admin' or 'admins').
'';
};
smtp-host = mkOption {
type = types.str;
default = "";
description = ''
The hostname of the smtp server you want to use.
If this is not set, smtp will not be used to send emails, and you can ignore the other settings.
'';
};
smtp-port = mkOption {
type = types.port;
default = 0;
description = "Port to use to connect to the smtp server.";
};
smtp-username = mkOption {
type = types.str;
default = "";
description = "Username to use when authenticating with the smtp server.";
};
smtp-password = mkOption {
type = types.str;
default = "";
description = "Password to use when authenticating with the smtp server.";
};
smtp-from = mkOption {
type = types.str;
default = "";
description = "'From' address for sent emails.";
};
syslog-enable = mkOption {
type = types.bool;
default = false;
description = "Enable the syslog logging hook. Logs will be mirrored to the configured destination.";
};
syslog-protocol = mkOption {
type = types.str;
default = "udp";
description = "Protocol to use when directing logs to syslog. Leave empty to connect to local syslog.";
};
syslog-address = mkOption {
type = types.str;
default = "localhost:514";
description = "Address:port to send syslog logs to. Leave empty to connect to local syslog.";
};
};
};
};
#};
The user the service runs as.
local.services.gotosocial.configFile = mkDefault (settingsFormat.generate "gotosocial.yaml" cfg.settings);
users = {
users.gotosocial = {
isSystemUser = true;
group = name;
home = stateDir;
};
groups.gotosocial = { };
};
systemd.services.gotosocial = {
description = "GoToSocial fediverse server";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Restart = "on-failure";
User = name;
StateDirectory = name;
ExecStart = "${pkgs.gotosocial}/bin/gotosocial --config-path ${cfg.configFile} server start";
WorkingDirectory = stateDir;
};
};
This service definition is part of a pending pull request to add Misskey to nixpkgs. #161855
{ config, lib, pkgs, ... }:
let
cfg = config.services.misskey;
settingsFormat = pkgs.formats.yaml {};
configFile = settingsFormat.generate "misskey-config.yml" cfg.settings;
in {
options = {
services.misskey = with lib; {
enable = mkEnableOption "misskey";
settings = mkOption {
type = settingsFormat.type;
default = {};
description = ''
Configuration for Misskey, see
<link xlink:href="https://github.com/misskey-dev/misskey/blob/develop/.config/example.yml"/>
for supported settings.
'';
};
};
};
config = lib.mkIf cfg.enable {
documentation.enable = false;
systemd.services.misskey = {
after = [ "network-online.target" "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
preStart = ''
${pkgs.envsubst}/bin/envsubst -i "${configFile}" > /run/misskey/default.yml
cd ${pkgs.misskey}/packages/backend
./node_modules/.bin/typeorm migration:run
'';
serviceConfig = {
StateDirectory = "misskey";
StateDirectoryMode = "700";
RuntimeDirectory = "misskey";
RuntimeDirectoryMode = "700";
ExecStart = "${pkgs.nodejs}/bin/node --experimental-json-modules ${pkgs.misskey}/packages/backend/built/index.js";
TimeoutSec = 240;
# implies RemoveIPC=, PrivateTmp=, NoNewPrivileges=, RestrictSUIDSGID=,
# ProtectSystem=strict, ProtectHome=read-only
DynamicUser = true;
LockPersonality = true;
PrivateDevices = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectProc = "invisible";
ProtectKernelModules = true;
ProtectKernelTunables = true;
RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = "@system-service";
UMask = "0077";
};
environment.NODE_ENV = "production";
};
};
meta.maintainers = with lib.maintainers; [ yuka ];
meta.doc = ./misskey.xml;
}
{ pkgs, lib, ... }:
{
<<nginx/basic>>
}
networking.firewall.allowedTCPPorts = [ 80 443 ];
Enable the Let’s Encrypt CA to be able to autogenerate certificates for virtual hosts.
security.acme.defaults.email = lib.mkDefault "[email protected]";
security.acme.acceptTerms = true;
With this we can use services.nginx.virtualHosts.<name>.enableACME
to enable
certificate generation for the corresponding virtual host.
We enable the nginx service and activate some recommended settings.
services.nginx = {
enable = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
sslCiphers = "TLSv1.2+HIGH+ECDHE@STRENGTH";
};
This user thinks he knows what he does. We set his initial password to be able
to gain root
after the first login throgh ssh after intial bootstrap of a
machine.
{ config, ... }:
{
users.users.wose = {
initialHashedPassword = "$6$KQo7A0p2cYsB3$Kpw2XUByh1gPiUA/JQC63w7WlVOrsWX5HqRudSqxjzBJY.R/hiyYUW24HAZSP54iFxUCrqRYDhghM3PBpB8XN1";
isNormalUser = true;
extraGroups = [ "wheel" ];
openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDqhXu3cahNdm5EjMbJXgCIMTOqVXD/SO5sRu9ep/EBOAQGQiUYg7Eqx7Ka7gCj2xi+X797DejfgOxFMm1jk4L4YQ5oKc+aehLZM/JLM4dNIqU7tGt2pKv7oGJMVi/u3HX6VPcPqdPIvayBN6Odw4aqtaQY6Pr5R9SJFCftWaeLFXGVOL0FauzMrwhxVgYId5RXtEyVJv9npe0wYEFemnNO4kktsxHxKHC8e+EzggGNRQyCyWOr8arXvjqM/HI5ZnqpWP8/R0hXOHw1WbziHBYNJNT4uwkwXBf2496mCxJCNerJQh/qF/fUUU/C1ix24EHDyx+FkA0aheylAjNAgSopGNfvkQ3Hqz8lnRiyu9WDS6W7liras/oJCmcsh/BmCWO+BQQ6P6q9Y2oC3KM+Rj8WWykjfCchTQeuz1GGx9sQOS8Js2DROlWDGKg4NoY0Clp8brcTmRR+8TiZQak7p3zg6CgFPzZfLPk7BA3gHLO5te753TK8jsKZegdzH6gA0hM="
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDgV2nv344CXTk+SX89W2QMPHLJy8VJu8V40uJPQQfaa8m/6Ept51HGfNwTF2E8C6n5jo3Bno1nyJNkuSSop6eYgmnFrkvoay3YQWTH4ZGWUl+vrFIRWlNdlMRASeRuBnMlm2Sp5dm0k7Clqzr2NN0Zq04WdHOi4JO+wTqV73+qdrnB1Sug3kYTyZKM4+Mr+fuhnecJTHw3fQfTYE7Pm9bMMCZjq+orrxyMBIFsDA4ssH6g5c/juKPCm3LC/el5MYjhzG0RhGTzKU1cf4jc3qttJF65FsfhFb3qpwlyrlZP5JJ67N3VyYroQgyoLf9N7UwsfDg73mvn7owGjoOh3PewrjMQwdRtiOaK9oR7+iggFaNNdt/xBHdeeCrjNJPqf3OElQrcXJldlYsAazE9aLCRc7CWHDk39tRAYWGobAkRXuMaLosLt+OtzVvqOYFHlNycvBoGX+TMRERiP9Xqb+hqa9z8qOlIbuwHJ7PptlL556tFfrR1lRE7apufFgS3yvE= wose@elric"
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDiFoKZl/CnDvod0zeBnZjreEVyluQ8Lwcno7wwh73UwXG9qDRBinZqo3iWRsHJM6lqiDQW1IOndbW3uOkyWJsWjb/FI8EMPQpWt9ghmIwqAkEJ+uvJiB/Zhh/vYG642rCzuYQ4aFLlWBRpQWMPacxpvLrH/CIwhH5GoGxphOhew5UtBqGq5kaHucRWB0U9ytpHSRUyHAx6wQEIC1LSPHjDZcw/02RU1gWGFnOcO6wtw0E7AZ3cN4w4nJsVEHUXnbA9M2ODhJjz+YNxw7VsfTbOtVBd/9qndO0QAeVis3/9+1Y2KBG9wde/PbNlk0qsWPfaVX3h+0YyllKNjaxWlgDb+4xweOgdM+4cALS78EO1THOr7KVjJIct3EXcuywn4PjTDAa84Bcz+kNMONjVFabn7nRHnzoEIV5TtlAYonktB22DRmIpjByS0oQmKB3mKCGNAIfiDnmUATkpq34KdCIv4yNkmThr/CBWm7G0ijlMNaAcV6Ts6fg+6T2hHT6Fxo92L4H9WMk4pd2f0CJ41dmYigL8EOfearrWfjLgkulgSvuTYBd5avGxB/OhnGBJkW+vFaCvCwKe1nzo5r4ei40K1e3yZqVpQd9Qq7FNg9qMSNydh049lnNSb0fOkahinps0198gOtNiBC6cTxqRLv0yM2Q1QiwC5Habu4cKBSXglw=="
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC+FNrrRnVNqeBjuEZt5F0WhHBI/OtYgnUrP8NHYsZoY/9dLret+BryhhQBARZFj0eoKGjT2X50FyJq6/7g4BCYmYtXbe/HxkDXa6IEoeSc9uWC8HXaNKibuCIPWxLgDOvuu6YuFVELO7O5xr2JwrY4AoLhVeeGIPUcTPnOoukKQ0tWSu4BmfYdzF9bnfY+qoyDP0funIOg3r3fWKYQEiWcXfZ6yn10yoTLyqojVKIVO2w5SQRK5AHyI5PtOOGOJf8CR3lQZEH9kbLDA5RHOzvXa0G8p6/OtdaUymPBnfB4V9KovhEGej0L3S9lShZq2j/zrkqY1JlFrxzqzmpJNoF90faUwOeggM0VN0pRAMkkSebOYej5pCNRZaQSONBHgBGl4JN7PlDaE7NRbnNlVDGOlk2nxn/T9vlwx/7ww0Rz4YHgmvDLuSNyKvtzI2Mq+tvIg0tcV9Euo85UpaZfAF9WhT0tnX09rotnzf4avE6L08+uZDdInGRC+d3kRbpxcsXLhl/8/lXtaquLijtLmhGQ6UurPioQSgR91dy9hDjGAeahmEcb2OlwNCp5sJXsuw3f1AonrmUi7zvdcP/dZ3lHSn/AqTxUw4JDDDUmR5Vhs3QEWwD5oODPB2YVp0T9i0meWZ5ptrgcvr7/R00WY/1ELJ2Vg4LUMZ4IJuZ327GJKw=="
];
};
}
{ config, ... }:
{
users.users.linda = {
isNormalUser = true;
openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDRXYbTbEfpsbRDPUS1qfimY22k1KsUBTgUJyK2GKsKYnv06bAH+WOqFV7jc7Ps564VkE1n6Bo+rltKC3qjVJlzxSp23iBNt3mY/R1rSF2ENgkynKHtql5Bxh4CB5t0ZSeyBCb+awc7HCbOeIVmYNWZQalen4d2BXcwmTx05U7f5rfoNq9lkSMAdfHZYunsej0/EC1FVE0lyC1x7Ojj73u/SAaKUQbZD4fhMIIUVY7/ca6wAigeI8DRtsqTCZWCL07H2XOWv9E5aKPA9HJHd3GO3z8ewhNc+U/V16IYpt7saRLWH2DpNktZ5Y9goFangdiCVMTodcftPqi/tgCVJuPf linda@weatherwax"
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCpfvxkWvyREXMeDrmmVSnX7vliaRjMAFlo1n8zRl47ypR9K8wP5UE+jvOcHjtXfBHtZI3HTUSqjLPOVcJn0w5DILDsb8YwNR6+bFtRCcC4JOhkcZfjQsdqufANOmoyuXgsGNKIErQypz/AMmmzIdoCvaZccPUBpy4VvUJJo/H7DTo5Nq6y2sG3/LkKTFQf/aft3YRkcMr/i8Fe0PtDSj1ASQZpXuCN/V1YYgrQRixKjPFScalJVA/is5nlMeTka1Qx4G/MsYxY2ml2rN1BqH93pk6+k6NFOHwddHhWU2omFzDTY6XoFbXH7iWrx/bcWfge9tv5UEbi1Rq5CnxQw4AP linda@knudsen"
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvFujULPQwKibZsjeNGTDQoTWthjlJOEMLbmqLyoRNR/Mza0aD4jpYLWN/qr5BsqZ9zfTTUouTNpQWY4WKw+jhxcisVKyJk8yW/RKMcP94mUR2e4syAeZ0jzd8xyuWEgli/j3J4FZhbNMDlkszFsWzziqWZKo8u6lHR5BX7+DptmDwxhhCSKsuIncb3RN8xqqouJUajnWIYUhabC+5omLikj/KCSNEZ7OIIVaDSOdKClcIK86y0EEuSwhG1pDjpjopqdUYcW10XcmX5y44U4Ddy3D1PGgt993F98YG3My+dxkHDIPfPEYS3W6cktoSEnDtqZzvBmiQ96tJcMLQjao/ l.jaeckel@mainsim"
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDdgW3VnuRtROksH8Vp5AIeifrEvFcLPxM5ud5loQhjE21L8DG3d6wkALOM8qziZ3AF5wr8bueYPU3fYxMuOzggFRIJsJzznkOvZ4wH8XMkLDn2yloKkIWZxfX1E3KFw/NeOe1hTvlqvBF5xlibD6U2qHWAEAArUO/c3R847iD1BgaeDYlpzseNJLx8HddzEj7ZEcCeA5S3nr3Cd2tbTcEQ65M25PjfdKJ7d73zZT6Zk6gwLCWwl3j1DARFNQ+eU6R1CYfaSrZMC58jA9jowDYOuCmCpnVTlUZ9/5SHh7hKSHKqJVzAcxFY8KlaMEN5FUVMUTB0DKDkqIM/bfdgkml5XLflDEsNzVCrD3QwE6hbW8ZHWPaf+8LxHytcQtBY2CCvj6VOwnYpHvY+Kp13jlD5YfDrHF28mCMUFEG3lM3pWEBBp0K3VNJIOl/DIOSOCoSiX6ewv9kGHx5LTdCskwPea6QyIWh7QLvKXQRWwYQMBbE/2Kio08SYrPHSHVjRUqX/ihgs+vQu403XEtsFHniryzjce+v3wUvpY6T8WVMyxDmBAZU6IdxSQc+of1r9uEhvQB6lJZrgikyg4XWwzNAVPPK1Kud3uoWcmXxBKVYpzbn/KRwTlUe2TaYWfh3FeHRTVEEnT3cAV/PlC0rLP+RaGRhaHFF6vx6w7wb7OdGbrw== u0_a169@localhost"
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCbUsyXPt8ydXGVh9a4aJPQrt7Ms0xDFAqCJP0qKhzmlOU6COtD9KKFBwJQW82JEWqxKxNNdeO7nMIj0jhWDgtwaKP9mYPd26s2o4F5324qf3YQHrXbsA5b7cjZadQScn4dc1bsLTOn9QGRniPCJhXzdvGnKrfQo83BK/MY0viR+BnxQzF4L+sgvntki8kP28gT9tkuUs7zR1vZsQhGM3WgmjomdPwRBxiV5UF2FgHR68ydqBlpP45JAuaHGrpvW7bhANk4JpctyR08VYyMkoDzSyX5FBoHhL4Dc7aWyqNRhbDLItVYWTB7fifZWBGy+t51VP0xACec+kD613ql+5yQpAC0Zx5mdlZxOMQZ764DwIGsDncfi8tzlgC4wbH4xKB3igLugZ4v8nxwMnIUT2aoq4FMy5WdIQJwO60AptCC0sLBu3qSZ/6oGRQTSyqb6rstRdpFSpbYlHjrLArIDsCdkGPNY71obIiXOb7LC6I3oGugBJ4mkjwfotJ34hln883CoSdPxZhASw+5WHxI67E74862rBSdQlbYsYJWFXsnquaq5nFV2U3zuk6WZvIO0Pc36AP0kCXwgUczFHAMoGmGz6eZQFNafCy8WRClHbt3K9+0w1wI7VVH7WjK2xvzDZcZvVPhJC+aiumFj3Z8dUV7Bdwb2O7Q05n6baeE9opdRQ== linda@prantl"
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC0Kgo3GnaPefgUyZ5b95yrh4kEQ4PMC76Bq4kwW4zsmgHNKe7/RwKCCVWMdAdSJ3dGRfU784fsuqzKVZKdE6DxUOs2J+SCwYKPwpOgrIHY48cRdsfsr64eKCRNVbmkm+JbLYGGsylHQWcOuAp4go/XRrxoOErCb+rd2jKSWB3XK/a6c1UXs48gTEt5U1Ja3q6yYIP3YOhnj15nAfgFJmRvkEN1ASA3RZ1U5maxs4ocZ8ZSZC2z4Xnp2/vSRdjxs3832Xc3bsKs/a1rk15JvrpXvPvJhe3obJtYFEGwXrdnxTln+vS9NsI6aOzFK1YpFOR+qxZvT3EzqVpsZ93qzsYa1Lg6n+tEOUwLiXq72IVrddZJIy6vqmrWAYqDKF1uX8rdVXEkPVBJO6u06GMcRMcKd7ZEHmzuFWD/c9+eh/jULDuLHvLovu2D1VdEG36/l2oJIjL+WPB215YmE1LpsN0DTHCnyQe6FW4TWSBBG61MCDrcyN4/g277fDTNg375cv0="
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM+PYKZu/+pDhWICRU/J+xek/Gy83cVzt1Ymzg01KJiI linda@worklaptop"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC/7fvOJLqQH6jYGnuufs42kyFxwkiof/ld2qllNRuIh"
];
};
}