replace haproxy with nginx as the main reverseproxy
This commit is contained in:
parent
c4938abceb
commit
d16ef8b82e
8 changed files with 281 additions and 762 deletions
|
@ -9,8 +9,8 @@
|
||||||
outputs = inputs@{ self, nixpkgs, sops-nix, ... }: {
|
outputs = inputs@{ self, nixpkgs, sops-nix, ... }: {
|
||||||
nixosModules.default = { config, ... }: {
|
nixosModules.default = { config, ... }: {
|
||||||
imports = [
|
imports = [
|
||||||
|
modules/ssl.nix
|
||||||
modules/backup.nix
|
modules/backup.nix
|
||||||
modules/haproxy.nix
|
|
||||||
modules/home-assistant.nix
|
modules/home-assistant.nix
|
||||||
modules/jellyfin.nix
|
modules/jellyfin.nix
|
||||||
modules/monitoring.nix
|
modules/monitoring.nix
|
||||||
|
|
|
@ -1,492 +0,0 @@
|
||||||
{ lib
|
|
||||||
, pkgs
|
|
||||||
}:
|
|
||||||
|
|
||||||
with builtins;
|
|
||||||
with lib;
|
|
||||||
with lib.attrsets;
|
|
||||||
with lib.lists;
|
|
||||||
with lib.strings;
|
|
||||||
let
|
|
||||||
getAttrWithDefault = name: default: attrset:
|
|
||||||
if isAttrs attrset && hasAttr name attrset then
|
|
||||||
getAttr name attrset
|
|
||||||
else
|
|
||||||
default;
|
|
||||||
|
|
||||||
recursiveMerge = attrList:
|
|
||||||
let f = attrPath:
|
|
||||||
zipAttrsWith (n: values:
|
|
||||||
if all isList values then
|
|
||||||
concatLists values
|
|
||||||
else if all isAttrs values then
|
|
||||||
f (attrPath ++ [n]) values
|
|
||||||
else
|
|
||||||
last values
|
|
||||||
);
|
|
||||||
in f [] attrList;
|
|
||||||
|
|
||||||
augmentedContent = fieldName: rules: parent: set:
|
|
||||||
let
|
|
||||||
print = {rule = k: parent: v:
|
|
||||||
assert assertMsg (isString v || isInt v) "cannot print key '${fieldName}.${k}' of type '${typeOf v}', should be string or int instead";
|
|
||||||
"${k} ${toString v}";};
|
|
||||||
|
|
||||||
matchingRule = k: v: findFirst (rule: rule.match k parent v) print rules;
|
|
||||||
|
|
||||||
augment = parent: k: v:
|
|
||||||
let
|
|
||||||
match = matchingRule k v;
|
|
||||||
rule = if hasAttr "rule" match then match.rule else null;
|
|
||||||
rules = if hasAttr "rules" match then match.rules else null;
|
|
||||||
indent = map (x: if hasAttr "indent" match then match.indent + x else x);
|
|
||||||
headerFn = if hasAttr "header" match then match.header else null;
|
|
||||||
header = optional (headerFn != null) (headerFn k);
|
|
||||||
trailer = optional (headerFn != null) "";
|
|
||||||
content = header ++ indent (augmentedContent "${fieldName}.${k}" rules (parent ++ [k]) v) ++ trailer;
|
|
||||||
in
|
|
||||||
if rule != null
|
|
||||||
then rule k parent v
|
|
||||||
else
|
|
||||||
assert assertMsg (isAttrs v) "attempt to apply rules on key '${toString k}' which is a '${typeOf v}' but should be a set:\n${toString v}";
|
|
||||||
if hasAttr "order" match then
|
|
||||||
{
|
|
||||||
inherit (match) order;
|
|
||||||
inherit content;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
content;
|
|
||||||
|
|
||||||
augmented = mapAttrsToList (augment parent) (
|
|
||||||
assert assertMsg (isAttrs set) "attempt to apply rules on field ${fieldName} having type '${typeOf set}':\n${toString set}";
|
|
||||||
set
|
|
||||||
);
|
|
||||||
|
|
||||||
sortAugmented = sort (a: b:
|
|
||||||
(isAttrs a && hasAttr "order" a)
|
|
||||||
&& (isAttrs b && hasAttr "order" b)
|
|
||||||
&& a.order < b.order
|
|
||||||
);
|
|
||||||
|
|
||||||
onlyContent = (x: if isAttrs x && hasAttr "content" x then x.content else x);
|
|
||||||
in
|
|
||||||
flatten (map onlyContent (sortAugmented augmented));
|
|
||||||
|
|
||||||
updateByPath = path: fn: set:
|
|
||||||
if hasAttrByPath path set then
|
|
||||||
recursiveUpdate set (setAttrByPath path (fn (getAttrFromPath path set)))
|
|
||||||
else
|
|
||||||
set;
|
|
||||||
|
|
||||||
schema =
|
|
||||||
let
|
|
||||||
mkRule =
|
|
||||||
{ redirect ? false
|
|
||||||
, scheme ? "https"
|
|
||||||
, code ? null
|
|
||||||
, condition ? null
|
|
||||||
}:
|
|
||||||
concatStringsRecursive " " [
|
|
||||||
(optional redirect "redirect")
|
|
||||||
"scheme" scheme
|
|
||||||
(optional (code != null) "code ${toString code}")
|
|
||||||
(optional (condition != null) "if ${condition}")
|
|
||||||
];
|
|
||||||
|
|
||||||
mkBind =
|
|
||||||
{ addr
|
|
||||||
, ssl ? false
|
|
||||||
, crt ? null
|
|
||||||
}:
|
|
||||||
concatStringsRecursive " " [
|
|
||||||
"bind"
|
|
||||||
addr
|
|
||||||
(optional ssl "ssl")
|
|
||||||
(optional (crt != null) "crt ${crt}")
|
|
||||||
];
|
|
||||||
|
|
||||||
mkServer =
|
|
||||||
{ name
|
|
||||||
, address
|
|
||||||
, balance ? null
|
|
||||||
, check ? null
|
|
||||||
, httpcheck ? null
|
|
||||||
, forwardfor ? true
|
|
||||||
, resolvers ? null
|
|
||||||
}:
|
|
||||||
[
|
|
||||||
"mode http"
|
|
||||||
(optional forwardfor "option forwardfor")
|
|
||||||
(optional (httpcheck != null) "option httpchk ${httpcheck}")
|
|
||||||
(optional (balance != null) "balance ${balance}")
|
|
||||||
(concatStringsRecursive " " [
|
|
||||||
"server"
|
|
||||||
name
|
|
||||||
address
|
|
||||||
(optionals (check != null) (if
|
|
||||||
isBool check
|
|
||||||
then (if check then ["check"] else [])
|
|
||||||
else mapAttrsToList (k: v: "${k} ${v}") check))
|
|
||||||
(optional (resolvers != null) "resolvers ${resolvers}")
|
|
||||||
])
|
|
||||||
];
|
|
||||||
|
|
||||||
# Lua's import system requires the import path to be something like:
|
|
||||||
#
|
|
||||||
# /nix/store/123-name/<package>/<file.lua>
|
|
||||||
#
|
|
||||||
# Then the lua-prepend-path can be:
|
|
||||||
#
|
|
||||||
# /nix/store/123-name/?/<file.lua>
|
|
||||||
#
|
|
||||||
# Then when lua code imports <package>, it will search in the
|
|
||||||
# prepend paths and replace the question mark with the <package>
|
|
||||||
# name to get a match.
|
|
||||||
#
|
|
||||||
# But the config.source is actually without the <package> name:
|
|
||||||
#
|
|
||||||
# /nix/store/123-name/<file.lua>
|
|
||||||
#
|
|
||||||
# This requires us to create a new directory structure and we're
|
|
||||||
# using a linkFarm for this.
|
|
||||||
createPluginLinks = configs:
|
|
||||||
let
|
|
||||||
mkLink = name: config: {
|
|
||||||
inherit name;
|
|
||||||
path = config.source;
|
|
||||||
};
|
|
||||||
|
|
||||||
in
|
|
||||||
pkgs.linkFarm "haproxyplugins" (mapAttrsToList mkLink configs);
|
|
||||||
|
|
||||||
mkPlugin = links: name:
|
|
||||||
{ luapaths ? []
|
|
||||||
, cpaths ? []
|
|
||||||
, load ? null
|
|
||||||
, ...
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
lua-prepend-path =
|
|
||||||
let
|
|
||||||
f = ext: type: path:
|
|
||||||
{
|
|
||||||
inherit type;
|
|
||||||
path =
|
|
||||||
if path == "." then
|
|
||||||
"${links}/${name}/?.${ext}"
|
|
||||||
else
|
|
||||||
"${links}/${name}/${path}/?.${ext}";
|
|
||||||
};
|
|
||||||
in
|
|
||||||
map (f "lua" "path") (toList luapaths)
|
|
||||||
++ map (f "so" "cpath") (toList cpaths);
|
|
||||||
} // optionalAttrs (load != null) {
|
|
||||||
lua-load = ["${links}/${name}/${load}"];
|
|
||||||
};
|
|
||||||
|
|
||||||
# Takes plugins as an attrset of name to {init, load, source},
|
|
||||||
# transforms them to a [attrset] with fields lua-prepend-path
|
|
||||||
# and optionally lua-load then returns a list of lines with all
|
|
||||||
# lua-prepend-path first and all lua-load afterwards.
|
|
||||||
mkPlugins = v:
|
|
||||||
let
|
|
||||||
f = recursiveMerge (mapAttrsToList (mkPlugin (createPluginLinks v)) v);
|
|
||||||
lua-prepend-path = map ({path, type}: "lua-prepend-path ${path} ${type}") (getAttrWithDefault "lua-prepend-path" [] f);
|
|
||||||
lua-load = map (x: "lua-load ${x}") (getAttrWithDefault "lua-load" [] f);
|
|
||||||
in
|
|
||||||
lua-prepend-path ++ lua-load;
|
|
||||||
in [
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "defaults";
|
|
||||||
order = 2;
|
|
||||||
indent = " ";
|
|
||||||
header = k: k;
|
|
||||||
rules = [
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "timeout";
|
|
||||||
rule = k: parent: v: mapAttrsToList (k1: v1: "${k} ${k1} ${v1}") v;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "global";
|
|
||||||
order = 1;
|
|
||||||
indent = " ";
|
|
||||||
header = k: k;
|
|
||||||
rules = [
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "plugins";
|
|
||||||
rule = k: parent: v: mkPlugins v;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "setenv";
|
|
||||||
rule = k: parent: v: mapAttrsToList (k: v: "setenv ${k} ${v}" ) v;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "resolvers";
|
|
||||||
order = 3;
|
|
||||||
rules = [
|
|
||||||
{
|
|
||||||
match = k: parent: v: true;
|
|
||||||
header = k: "resolvers " + k;
|
|
||||||
indent = " ";
|
|
||||||
rules = [
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "nameservers";
|
|
||||||
rule = k: parent: v: mapAttrsToList (k1: v1: "nameserver ${k1} ${v1}") v;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "frontend";
|
|
||||||
order = 4;
|
|
||||||
rules = [
|
|
||||||
{
|
|
||||||
match = k: parent: v: true;
|
|
||||||
header = k: "frontend " + k;
|
|
||||||
indent = " ";
|
|
||||||
rules = [
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "rules";
|
|
||||||
rule = k: parent: v: map mkRule v;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "bind" && isAttrs v;
|
|
||||||
rule = k: parent: v: mkBind v;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "use_backend";
|
|
||||||
rule = k: parent: v:
|
|
||||||
let
|
|
||||||
use = name: value: "use_backend ${name} ${toString value}";
|
|
||||||
in
|
|
||||||
if isList v then
|
|
||||||
map (v: use v.name v.value) v
|
|
||||||
else
|
|
||||||
use v.name v.value;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
match = k: parent: v: true ;
|
|
||||||
rule = k: parent: v:
|
|
||||||
let
|
|
||||||
l = prefix: v:
|
|
||||||
if isAttrs v then
|
|
||||||
mapAttrsToList (k: v: l "${prefix} ${k}" v) v
|
|
||||||
else if isList v then
|
|
||||||
map (l prefix) v
|
|
||||||
else if isBool v then
|
|
||||||
optional v prefix
|
|
||||||
else
|
|
||||||
assert assertMsg (isString v) "value for field ${k} should be a string, bool, attr or list, got: ${typeOf v}";
|
|
||||||
"${prefix} ${v}";
|
|
||||||
in
|
|
||||||
l k v;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "backend";
|
|
||||||
order = 5;
|
|
||||||
rules = [
|
|
||||||
{
|
|
||||||
match = k: parent: v: true;
|
|
||||||
header = k: "backend " + k;
|
|
||||||
indent = " ";
|
|
||||||
rules = [
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "options";
|
|
||||||
rule = k: parent: v: v;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
match = k: parent: v: k == "servers";
|
|
||||||
rule = k: parent: v: map mkServer v;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
concatStringsRecursive = sep: strings:
|
|
||||||
concatStringsSep sep (flatten strings);
|
|
||||||
|
|
||||||
assertHasAttr = name: attrPath: v:
|
|
||||||
assertMsg
|
|
||||||
(hasAttrByPath attrPath v)
|
|
||||||
"no ${last attrPath} defined in config for site ${name}.${concatStringsSep "." (init attrPath)}, found attr names: ${toString (attrNames (getAttrFromPath (init attrPath) v))}";
|
|
||||||
|
|
||||||
# Takes a function producing a [nameValuePair], applies it to
|
|
||||||
# all name-value pair in the given set and merges the resulting
|
|
||||||
# [[nameValuePair]].
|
|
||||||
mapAttrsFlatten = f: set: listToAttrs (concatLists (mapAttrsToList f set));
|
|
||||||
|
|
||||||
mapIfIsAttrs = f: value:
|
|
||||||
if isAttrs value
|
|
||||||
then f value
|
|
||||||
else value;
|
|
||||||
|
|
||||||
flattenAttrs = sep: cond: set:
|
|
||||||
let
|
|
||||||
recurse = mapIfIsAttrs (mapAttrsFlatten (
|
|
||||||
n: v: let
|
|
||||||
result = recurse v;
|
|
||||||
in
|
|
||||||
if isAttrs result && cond n v
|
|
||||||
then mapAttrsToList (n2: v2: nameValuePair "${n}${sep}${n2}" v2) result
|
|
||||||
else [(nameValuePair n result)]
|
|
||||||
));
|
|
||||||
in recurse set;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit updateByPath recursiveMerge;
|
|
||||||
|
|
||||||
default =
|
|
||||||
{ user
|
|
||||||
, group
|
|
||||||
, certPath
|
|
||||||
, plugins ? {}
|
|
||||||
, globalEnvs ? {}
|
|
||||||
, stats ? null
|
|
||||||
, debug ? false
|
|
||||||
, sites ? {}
|
|
||||||
, globals ? {}
|
|
||||||
, defaults ? {}
|
|
||||||
, resolvers ? {}
|
|
||||||
}: {
|
|
||||||
global = {
|
|
||||||
# Silence a warning issued by haproxy. Using 2048
|
|
||||||
# instead of the default 1024 makes the connection stronger.
|
|
||||||
"tune.ssl.default-dh-param" = 2048;
|
|
||||||
|
|
||||||
maxconn = 20000;
|
|
||||||
|
|
||||||
inherit user group;
|
|
||||||
|
|
||||||
log = "/dev/log local0 info";
|
|
||||||
|
|
||||||
inherit plugins;
|
|
||||||
|
|
||||||
setenv = globalEnvs;
|
|
||||||
} // globals;
|
|
||||||
|
|
||||||
defaults = {
|
|
||||||
log = "global";
|
|
||||||
option = "httplog";
|
|
||||||
|
|
||||||
timeout = {
|
|
||||||
connect = "10s";
|
|
||||||
client = "15s";
|
|
||||||
server = "30s";
|
|
||||||
queue = "100s";
|
|
||||||
};
|
|
||||||
} // defaults;
|
|
||||||
|
|
||||||
frontend = {
|
|
||||||
http-to-https = {
|
|
||||||
mode = "http";
|
|
||||||
bind = "*:80";
|
|
||||||
rules = [
|
|
||||||
{
|
|
||||||
redirect = true;
|
|
||||||
scheme = "https";
|
|
||||||
code = 301;
|
|
||||||
condition = "!{ ssl_fc }";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
backend = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
https = (
|
|
||||||
let
|
|
||||||
r = (
|
|
||||||
[{
|
|
||||||
mode = "http";
|
|
||||||
bind = {
|
|
||||||
addr = "*:443";
|
|
||||||
ssl = true;
|
|
||||||
crt = certPath;
|
|
||||||
};
|
|
||||||
|
|
||||||
http-request = {
|
|
||||||
set-header = [
|
|
||||||
"X-Forwarded-Port %[dst_port]"
|
|
||||||
"X-Forwarded-For %[src]"
|
|
||||||
];
|
|
||||||
add-header = [
|
|
||||||
"X-Forwarded-Proto https"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
http-response = {
|
|
||||||
set-header = [
|
|
||||||
''Strict-Transport-Security "max-age=15552000; includeSubDomains; preload;"''
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}]
|
|
||||||
++ (mapAttrsToList (name: config:
|
|
||||||
assert assertHasAttr name ["frontend"] config;
|
|
||||||
(updateByPath ["frontend" "use_backend"] (x: [(nameValuePair name x)]) config).frontend
|
|
||||||
) sites)
|
|
||||||
++ (mapAttrsToList (name: config:
|
|
||||||
if (hasAttr "debugHeaders" config && (getAttr "debugHeaders" config) != null) then {
|
|
||||||
option = "httplog";
|
|
||||||
http-request = {
|
|
||||||
capture = "req.hdrs len 512 if ${config.debugHeaders}";
|
|
||||||
};
|
|
||||||
log-format = ''"%ci:%cp [%tr] %ft [[%hr]] %hs %{+Q}r"'';
|
|
||||||
} else {}
|
|
||||||
) sites)
|
|
||||||
);
|
|
||||||
in
|
|
||||||
recursiveMerge r
|
|
||||||
)
|
|
||||||
// optionalAttrs (debug) {
|
|
||||||
log-format = ''"%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r %sslv %sslc %[ssl_fc_cipherlist_str]"'';
|
|
||||||
};
|
|
||||||
} // optionalAttrs (stats != null)
|
|
||||||
(let
|
|
||||||
stats_ = {
|
|
||||||
enable = true;
|
|
||||||
port = 8404;
|
|
||||||
uri = "/stats";
|
|
||||||
refresh = "10s";
|
|
||||||
prometheusUri = null;
|
|
||||||
hide-version = false;
|
|
||||||
} // stats;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
stats = {
|
|
||||||
bind = "localhost:${toString stats_.port}";
|
|
||||||
mode = "http";
|
|
||||||
stats = {
|
|
||||||
enable = stats_.enable;
|
|
||||||
hide-version = stats_.hide-version;
|
|
||||||
uri = stats_.uri;
|
|
||||||
refresh = stats_.refresh;
|
|
||||||
};
|
|
||||||
} // optionalAttrs (stats_.prometheusUri != null) {
|
|
||||||
http-request = [
|
|
||||||
"use-service prometheus-exporter if { path ${stats_.prometheusUri} }"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
backend =
|
|
||||||
mapAttrs' (name: config:
|
|
||||||
assert assertMsg (hasAttr "backend" config) "no backend defined in config for site ${name}, found attr names: ${toString (attrNames config)}";
|
|
||||||
nameValuePair name config.backend)
|
|
||||||
sites;
|
|
||||||
|
|
||||||
inherit resolvers;
|
|
||||||
};
|
|
||||||
|
|
||||||
render = config:
|
|
||||||
concatStringsSep "\n" (augmentedContent "" schema [] config);
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
{ config, pkgs, lib, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.shb.reverseproxy;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.shb.reverseproxy = {
|
|
||||||
enable = lib.mkEnableOption "selfhostblocks.reverseproxy";
|
|
||||||
|
|
||||||
sopsFile = lib.mkOption {
|
|
||||||
type = lib.types.path;
|
|
||||||
description = "Sops file location";
|
|
||||||
example = "secrets/haproxy.yaml";
|
|
||||||
};
|
|
||||||
|
|
||||||
domain = lib.mkOption {
|
|
||||||
description = lib.mdDoc "Domain to serve sites under.";
|
|
||||||
type = lib.types.str;
|
|
||||||
};
|
|
||||||
|
|
||||||
adminEmail = lib.mkOption {
|
|
||||||
description = lib.mdDoc "Admin email in case certificate retrieval goes wrong.";
|
|
||||||
type = lib.types.str;
|
|
||||||
};
|
|
||||||
|
|
||||||
sites = lib.mkOption {
|
|
||||||
description = lib.mdDoc "Sites to serve through the reverse proxy.";
|
|
||||||
type = lib.types.anything;
|
|
||||||
default = {};
|
|
||||||
example = {
|
|
||||||
homeassistant = {
|
|
||||||
frontend = {
|
|
||||||
acl = {
|
|
||||||
acl_homeassistant = "hdr_beg(host) ha.";
|
|
||||||
};
|
|
||||||
use_backend = "if acl_homeassistant";
|
|
||||||
};
|
|
||||||
backend = {
|
|
||||||
servers = [
|
|
||||||
{
|
|
||||||
name = "homeassistant1";
|
|
||||||
address = "127.0.0.1:8123";
|
|
||||||
forwardfor = false;
|
|
||||||
balance = "roundrobin";
|
|
||||||
check = {
|
|
||||||
inter = "5s";
|
|
||||||
downinter = "15s";
|
|
||||||
fall = "3";
|
|
||||||
rise = "3";
|
|
||||||
};
|
|
||||||
httpcheck = "GET /";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
|
||||||
|
|
||||||
security.acme = {
|
|
||||||
acceptTerms = true;
|
|
||||||
certs."${cfg.domain}" = {
|
|
||||||
extraDomainNames = ["*.${cfg.domain}"];
|
|
||||||
};
|
|
||||||
defaults = {
|
|
||||||
email = cfg.adminEmail;
|
|
||||||
dnsProvider = "linode";
|
|
||||||
dnsResolver = "8.8.8.8";
|
|
||||||
group = config.services.haproxy.user;
|
|
||||||
# For example, to use Linode to prove the dns challenge,
|
|
||||||
# the content of the file should be the following, with
|
|
||||||
# XXX replaced by your Linode API token.
|
|
||||||
# LINODE_HTTP_TIMEOUT=10
|
|
||||||
# LINODE_POLLING_INTERVAL=10
|
|
||||||
# LINODE_PROPAGATION_TIMEOUT=240
|
|
||||||
# LINODE_TOKEN=XXX
|
|
||||||
credentialsFile = "/run/secrets/linode";
|
|
||||||
enableDebugLogs = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
sops.secrets.linode = {
|
|
||||||
inherit (cfg) sopsFile;
|
|
||||||
restartUnits = [ "acme-${cfg.domain}.service" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
services.haproxy.enable = true;
|
|
||||||
|
|
||||||
services.haproxy.config = let
|
|
||||||
configcreator = pkgs.callPackage ./haproxy-configcreator.nix {};
|
|
||||||
in configcreator.render ( configcreator.default {
|
|
||||||
inherit (config.services.haproxy) user group;
|
|
||||||
|
|
||||||
certPath = "/var/lib/acme/${cfg.domain}/full.pem";
|
|
||||||
|
|
||||||
stats = {
|
|
||||||
port = 8404;
|
|
||||||
uri = "/stats";
|
|
||||||
refresh = "10s";
|
|
||||||
prometheusUri = "/metrics";
|
|
||||||
};
|
|
||||||
|
|
||||||
defaults = {
|
|
||||||
default-server = "init-addr last,none";
|
|
||||||
};
|
|
||||||
|
|
||||||
inherit (cfg) sites;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.home-assistant;
|
cfg = config.shb.home-assistant;
|
||||||
|
|
||||||
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.shb.home-assistant = {
|
options.shb.home-assistant = {
|
||||||
|
@ -66,7 +68,9 @@ in
|
||||||
# https://www.home-assistant.io/integrations/default_config/
|
# https://www.home-assistant.io/integrations/default_config/
|
||||||
default_config = {};
|
default_config = {};
|
||||||
http = {
|
http = {
|
||||||
use_x_forwarded_for = "true";
|
use_x_forwarded_for = true;
|
||||||
|
server_host = "127.0.0.1";
|
||||||
|
server_port = 8123;
|
||||||
trusted_proxies = "127.0.0.1";
|
trusted_proxies = "127.0.0.1";
|
||||||
};
|
};
|
||||||
logger.default = "info";
|
logger.default = "info";
|
||||||
|
@ -110,6 +114,20 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."${fqdn}" = {
|
||||||
|
forceSSL = true;
|
||||||
|
http2 = true;
|
||||||
|
sslCertificate = "/var/lib/acme/${cfg.domain}/cert.pem";
|
||||||
|
sslCertificateKey = "/var/lib/acme/${cfg.domain}/key.pem";
|
||||||
|
extraConfig = ''
|
||||||
|
proxy_buffering off;
|
||||||
|
'';
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://${toString config.services.home-assistant.config.http.server_host}:${toString config.services.home-assistant.config.http.server_port}/";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
sops.secrets."home-assistant" = {
|
sops.secrets."home-assistant" = {
|
||||||
inherit (cfg) sopsFile;
|
inherit (cfg) sopsFile;
|
||||||
mode = "0440";
|
mode = "0440";
|
||||||
|
@ -125,32 +143,6 @@ in
|
||||||
"f ${config.services.home-assistant.configDir}/scripts.yaml 0755 hass hass"
|
"f ${config.services.home-assistant.configDir}/scripts.yaml 0755 hass hass"
|
||||||
];
|
];
|
||||||
|
|
||||||
shb.reverseproxy.sites.homeassistant = {
|
|
||||||
frontend = {
|
|
||||||
acl = {
|
|
||||||
acl_homeassistant = "hdr_beg(host) ${cfg.subdomain}.";
|
|
||||||
};
|
|
||||||
use_backend = "if acl_homeassistant";
|
|
||||||
};
|
|
||||||
backend = {
|
|
||||||
servers = [
|
|
||||||
{
|
|
||||||
name = "homeassistant1";
|
|
||||||
address = "127.0.0.1:8123";
|
|
||||||
forwardfor = false;
|
|
||||||
balance = "roundrobin";
|
|
||||||
check = {
|
|
||||||
inter = "5s";
|
|
||||||
downinter = "15s";
|
|
||||||
fall = "3";
|
|
||||||
rise = "3";
|
|
||||||
};
|
|
||||||
httpcheck = "GET /";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.backup.instances.home-assistant = lib.mkIf (cfg.backupCfg != {}) (
|
shb.backup.instances.home-assistant = lib.mkIf (cfg.backupCfg != {}) (
|
||||||
cfg.backupCfg
|
cfg.backupCfg
|
||||||
// {
|
// {
|
||||||
|
|
|
@ -2,10 +2,24 @@
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.jellyfin;
|
cfg = config.shb.jellyfin;
|
||||||
|
|
||||||
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.shb.jellyfin = {
|
options.shb.jellyfin = {
|
||||||
enable = lib.mkEnableOption "shb jellyfin";
|
enable = lib.mkEnableOption "shb jellyfin";
|
||||||
|
|
||||||
|
subdomain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Subdomain under which home-assistant will be served.";
|
||||||
|
example = "jellyfin";
|
||||||
|
};
|
||||||
|
|
||||||
|
domain = lib.mkOption {
|
||||||
|
description = lib.mdDoc "Domain to serve sites under.";
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "domain.com";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
@ -26,36 +40,111 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.reverseproxy.sites.jellyfin = {
|
# Take advice from https://jellyfin.org/docs/general/networking/nginx/ and https://nixos.wiki/wiki/Plex
|
||||||
frontend = {
|
services.nginx.virtualHosts."${fqdn}" = {
|
||||||
acl = {
|
addSSL = true;
|
||||||
acl_jellyfin = "hdr_beg(host) jellyfin.";
|
http2 = true;
|
||||||
acl_jellyfin_network_allowed = "src 127.0.0.1";
|
sslCertificate = "/var/lib/acme/${cfg.domain}/cert.pem";
|
||||||
acl_jellyfin_restricted_page = "path_beg /metrics";
|
sslCertificateKey = "/var/lib/acme/${cfg.domain}/key.pem";
|
||||||
};
|
extraConfig = ''
|
||||||
http-request = {
|
# The default `client_max_body_size` is 1M, this might not be enough for some posters, etc.
|
||||||
deny = "if acl_jellyfin acl_jellyfin_restricted_page !acl_jellyfin_network_allowed";
|
client_max_body_size 20M;
|
||||||
};
|
|
||||||
use_backend = "if acl_jellyfin";
|
# Some players don't reopen a socket and playback stops totally instead of resuming after an extended pause
|
||||||
};
|
send_timeout 100m;
|
||||||
# TODO: enable /metrics and block from outside https://jellyfin.org/docs/general/networking/monitoring/#prometheus-metrics
|
|
||||||
backend = {
|
# use a variable to store the upstream proxy
|
||||||
servers = [
|
# in this example we are using a hostname which is resolved via DNS
|
||||||
{
|
# (if you aren't using DNS remove the resolver line and change the variable to point to an IP address e.g `set $jellyfin 127.0.0.1`)
|
||||||
name = "jellyfin1";
|
set $jellyfin 127.0.0.1;
|
||||||
address = "127.0.0.1:8091";
|
# resolver 127.0.0.1 valid=30;
|
||||||
forwardfor = false;
|
|
||||||
balance = "roundrobin";
|
#include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||||
check = {
|
#ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||||
inter = "5s";
|
#add_header Strict-Transport-Security "max-age=31536000" always;
|
||||||
downinter = "15s";
|
#ssl_trusted_certificate /etc/letsencrypt/live/DOMAIN_NAME/chain.pem;
|
||||||
fall = "3";
|
# Why this is important: https://blog.cloudflare.com/ocsp-stapling-how-cloudflare-just-made-ssl-30/
|
||||||
rise = "3";
|
ssl_stapling on;
|
||||||
};
|
ssl_stapling_verify on;
|
||||||
httpcheck = "GET /health";
|
|
||||||
}
|
# Security / XSS Mitigation Headers
|
||||||
];
|
# NOTE: X-Frame-Options may cause issues with the webOS app
|
||||||
};
|
add_header X-Frame-Options "SAMEORIGIN";
|
||||||
|
add_header X-XSS-Protection "0"; # Do NOT enable. This is obsolete/dangerous
|
||||||
|
add_header X-Content-Type-Options "nosniff";
|
||||||
|
|
||||||
|
# COOP/COEP. Disable if you use external plugins/images/assets
|
||||||
|
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||||
|
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||||
|
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
||||||
|
|
||||||
|
# Permissions policy. May cause issues on some clients
|
||||||
|
add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), battery=(), bluetooth=(), camera=(), clipboard-read=(), display-capture=(), document-domain=(), encrypted-media=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), local-fonts=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=(), serial=(), sync-xhr=(), usb=(), xr-spatial-tracking=()" always;
|
||||||
|
|
||||||
|
# Tell browsers to use per-origin process isolation
|
||||||
|
add_header Origin-Agent-Cluster "?1" always;
|
||||||
|
|
||||||
|
|
||||||
|
# Content Security Policy
|
||||||
|
# See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
|
||||||
|
# Enforces https content and restricts JS/CSS to origin
|
||||||
|
# External Javascript (such as cast_sender.js for Chromecast) must be whitelisted.
|
||||||
|
# NOTE: The default CSP headers may cause issues with the webOS app
|
||||||
|
#add_header Content-Security-Policy "default-src https: data: blob: http://image.tmdb.org; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com/cv/js/sender/v1/cast_sender.js https://www.gstatic.com/eureka/clank/95/cast_sender.js https://www.gstatic.com/eureka/clank/96/cast_sender.js https://www.gstatic.com/eureka/clank/97/cast_sender.js https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'";
|
||||||
|
|
||||||
|
# From Plex: Plex has A LOT of javascript, xml and html. This helps a lot, but if it causes playback issues with devices turn it off.
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_types text/plain text/css text/xml application/xml text/javascript application/x-javascript image/svg+xml;
|
||||||
|
gzip_disable "MSIE [1-6]\.";
|
||||||
|
|
||||||
|
location = / {
|
||||||
|
return 302 http://$host/web/;
|
||||||
|
#return 302 https://$host/web/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# Proxy main Jellyfin traffic
|
||||||
|
proxy_pass http://$jellyfin:8096;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Protocol $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
|
||||||
|
# Disable buffering when the nginx proxy gets very resource heavy upon streaming
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# location block for /web - This is purely for aesthetics so /web/#!/ works instead of having to go to /web/index.html/#!/
|
||||||
|
location = /web/ {
|
||||||
|
# Proxy main Jellyfin traffic
|
||||||
|
proxy_pass http://$jellyfin:8096/web/index.html;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Protocol $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /socket {
|
||||||
|
# Proxy Jellyfin Websockets traffic
|
||||||
|
proxy_pass http://$jellyfin:8096;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Protocol $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
}
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.backup.instances.jellyfin = {
|
shb.backup.instances.jellyfin = {
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.monitoring;
|
cfg = config.shb.monitoring;
|
||||||
|
|
||||||
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.shb.monitoring = {
|
options.shb.monitoring = {
|
||||||
|
@ -12,6 +14,18 @@ in
|
||||||
# description = "Sops file location";
|
# description = "Sops file location";
|
||||||
# example = "secrets/monitoring.yaml";
|
# example = "secrets/monitoring.yaml";
|
||||||
# };
|
# };
|
||||||
|
|
||||||
|
subdomain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Subdomain under which home-assistant will be served.";
|
||||||
|
example = "grafana";
|
||||||
|
};
|
||||||
|
|
||||||
|
domain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "domain under which home-assistant will be served.";
|
||||||
|
example = "mydomain.com";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
@ -34,50 +48,66 @@ in
|
||||||
services.grafana = {
|
services.grafana = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
database = {
|
|
||||||
host = "/run/postgresql";
|
|
||||||
user = "grafana";
|
|
||||||
name = "grafana";
|
|
||||||
type = "postgres";
|
|
||||||
# Uses peer auth for local users, so we don't need a password.
|
|
||||||
# Here's the syntax anyway for future refence:
|
|
||||||
# password = "$__file{/run/secrets/homeassistant/dbpass}";
|
|
||||||
};
|
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
|
database = {
|
||||||
|
host = "/run/postgresql";
|
||||||
|
user = "grafana";
|
||||||
|
name = "grafana";
|
||||||
|
type = "postgres";
|
||||||
|
# Uses peer auth for local users, so we don't need a password.
|
||||||
|
# Here's the syntax anyway for future refence:
|
||||||
|
# password = "$__file{/run/secrets/homeassistant/dbpass}";
|
||||||
|
};
|
||||||
|
|
||||||
server = {
|
server = {
|
||||||
http_addr = "127.0.0.1";
|
http_addr = "127.0.0.1";
|
||||||
http_port = 3000;
|
http_port = 3000;
|
||||||
|
domain = fqdn;
|
||||||
|
root_url = "https://${fqdn}";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.reverseproxy.sites.grafana = {
|
services.prometheus = {
|
||||||
frontend = {
|
enable = true;
|
||||||
acl = {
|
port = 3001;
|
||||||
acl_grafana = "hdr_beg(host) grafana.";
|
};
|
||||||
|
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
# recommendedProxySettings = true;
|
||||||
|
|
||||||
|
virtualHosts.${fqdn} = {
|
||||||
|
addSSL = true;
|
||||||
|
sslCertificate = "/var/lib/acme/${cfg.domain}/cert.pem";
|
||||||
|
sslCertificateKey = "/var/lib/acme/${cfg.domain}/key.pem";
|
||||||
|
locations."/" = {
|
||||||
|
extraConfig = ''
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
'';
|
||||||
|
proxyPass = "http://${toString config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}/";
|
||||||
|
proxyWebsockets = true;
|
||||||
};
|
};
|
||||||
use_backend = "if acl_grafana";
|
|
||||||
};
|
|
||||||
backend = {
|
|
||||||
servers = [
|
|
||||||
{
|
|
||||||
name = "grafana1";
|
|
||||||
address = "127.0.0.1:3000";
|
|
||||||
forwardfor = true;
|
|
||||||
balance = "roundrobin";
|
|
||||||
check = {
|
|
||||||
inter = "5s";
|
|
||||||
downinter = "15s";
|
|
||||||
fall = "3";
|
|
||||||
rise = "3";
|
|
||||||
};
|
|
||||||
httpcheck = "GET /";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
services.prometheus.scrapeConfigs =
|
||||||
|
(lib.lists.optional config.services.nginx.enable {
|
||||||
|
job_name = "nginx";
|
||||||
|
static_configs = [
|
||||||
|
{
|
||||||
|
targets = ["127.0.0.1:9113"];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
services.prometheus.exporters.nginx = lib.mkIf config.services.nginx.enable {
|
||||||
|
enable = true;
|
||||||
|
port = 9113;
|
||||||
|
listenAddress = "127.0.0.1";
|
||||||
|
scrapeUri = "http://localhost:80/nginx_status";
|
||||||
|
};
|
||||||
|
services.nginx.statusPage = lib.mkDefault config.services.nginx.enable;
|
||||||
|
|
||||||
# sops.secrets."grafana" = {
|
# sops.secrets."grafana" = {
|
||||||
# inherit (cfg) sopsFile;
|
# inherit (cfg) sopsFile;
|
||||||
# mode = "0440";
|
# mode = "0440";
|
||||||
|
|
|
@ -2,15 +2,23 @@
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.nextcloud;
|
cfg = config.shb.nextcloud;
|
||||||
|
|
||||||
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.shb.nextcloud = {
|
options.shb.nextcloud = {
|
||||||
enable = lib.mkEnableOption "selfhostblocks.nextcloud-server";
|
enable = lib.mkEnableOption "selfhostblocks.nextcloud-server";
|
||||||
|
|
||||||
fqdn = lib.mkOption {
|
subdomain = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Fully qualified domain under which nextcloud will be served.";
|
description = "Subdomain under which home-assistant will be served.";
|
||||||
example = "nextcloud.domain.com";
|
example = "nextcloud";
|
||||||
|
};
|
||||||
|
|
||||||
|
domain = lib.mkOption {
|
||||||
|
description = lib.mdDoc "Domain to serve sites under.";
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "domain.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
sopsFile = lib.mkOption {
|
sopsFile = lib.mkOption {
|
||||||
|
@ -41,7 +49,7 @@ in
|
||||||
package = pkgs.nextcloud26;
|
package = pkgs.nextcloud26;
|
||||||
|
|
||||||
# Enable php-fpm and nginx which will be behind the shb haproxy instance.
|
# Enable php-fpm and nginx which will be behind the shb haproxy instance.
|
||||||
hostName = cfg.fqdn;
|
hostName = fqdn;
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
dbtype = "pgsql";
|
dbtype = "pgsql";
|
||||||
|
@ -63,8 +71,8 @@ in
|
||||||
webfinger = true;
|
webfinger = true;
|
||||||
|
|
||||||
extraOptions = {
|
extraOptions = {
|
||||||
"overwrite.cli.url" = "https://" + cfg.fqdn;
|
"overwrite.cli.url" = "https://" + fqdn;
|
||||||
"overwritehost" = cfg.fqdn;
|
"overwritehost" = fqdn;
|
||||||
"overwriteprotocol" = "https";
|
"overwriteprotocol" = "https";
|
||||||
"overwritecondaddr" = "^127\\.0\\.0\\.1$";
|
"overwritecondaddr" = "^127\\.0\\.0\\.1$";
|
||||||
};
|
};
|
||||||
|
@ -83,68 +91,11 @@ in
|
||||||
group = "nextcloud";
|
group = "nextcloud";
|
||||||
};
|
};
|
||||||
|
|
||||||
# The following changed the listen address for nginx and puts haproxy in front. See
|
services.nginx.virtualHosts.${fqdn} = {
|
||||||
# https://nixos.wiki/wiki/Nextcloud#Change_default_listening_port
|
# listen = [ { addr = "0.0.0.0"; port = 443; } ];
|
||||||
#
|
sslCertificate = "/var/lib/acme/${cfg.domain}/cert.pem";
|
||||||
# It's a bit of a waste in resources to have nginx behind haproxy but the config for nginx is
|
sslCertificateKey = "/var/lib/acme/${cfg.domain}/key.pem";
|
||||||
# complex enough that I find it better to re-use the one from nixpkgs instead of trying to copy
|
addSSL = true;
|
||||||
# it over to haproxy. At least for now.
|
|
||||||
services.nginx.virtualHosts.${cfg.fqdn}.listen = [ { addr = "127.0.0.1"; port = 8080; } ];
|
|
||||||
shb.reverseproxy.sites.nextcloud = {
|
|
||||||
frontend = {
|
|
||||||
acl = {
|
|
||||||
acl_nextcloud = "hdr_beg(host) n.";
|
|
||||||
# well_known = "path_beg /.well-known";
|
|
||||||
# caldav-endpoint = "path_beg /.well-known/caldav";
|
|
||||||
# carddav-endpoint = "path_beg /.well-known/carddav";
|
|
||||||
# webfinger-endpoint = "path_beg /.well-known/webfinger";
|
|
||||||
# nodeinfo-endpoint = "path_beg /.well-known/nodeinfo";
|
|
||||||
};
|
|
||||||
http-request.set-header = {
|
|
||||||
"X-Forwarded-Host" = "%[req.hdr(host)]";
|
|
||||||
"X-Forwarded-Port" = "%[dst_port]";
|
|
||||||
};
|
|
||||||
# http-request = [
|
|
||||||
# "redirect code 301 location /remote.php/dav if acl_nextcloud caldav-endpoint"
|
|
||||||
# "redirect code 301 location /remote.php/dav if acl_nextcloud carddav-endpoint"
|
|
||||||
# "redirect code 301 location /public.php?service=webfinger if acl_nextcloud webfinger-endpoint"
|
|
||||||
# "redirect code 301 location /public.php?service=nodeinfo if acl_nextcloud nodeinfo-endpoint"
|
|
||||||
# ];
|
|
||||||
# http-response = {
|
|
||||||
# set-header = {
|
|
||||||
# # These headers are from https://github.com/NixOS/nixpkgs/blob/d3bb401dcfc5a46ce51cdfb5762e70cc75d082d2/nixos/modules/services/web-apps/nextcloud.nix#L1167-L1173
|
|
||||||
# X-Content-Type-Options = "nosniff";
|
|
||||||
# X-XSS-Protection = "\"1; mode=block\"";
|
|
||||||
# X-Robots-Tag = "\"noindex, nofollow\"";
|
|
||||||
# X-Download-Options = "noopen";
|
|
||||||
# X-Permitted-Cross-Domain-Policies = "none";
|
|
||||||
# X-Frame-Options = "sameorigin";
|
|
||||||
# Referrer-Policy = "no-referrer";
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
use_backend = "if acl_nextcloud";
|
|
||||||
};
|
|
||||||
backend = {
|
|
||||||
servers = [
|
|
||||||
{
|
|
||||||
name = "nextcloud1";
|
|
||||||
address =
|
|
||||||
let
|
|
||||||
addrs = config.services.nginx.virtualHosts.${cfg.fqdn}.listen;
|
|
||||||
in
|
|
||||||
builtins.map (c: "${c.addr}:${builtins.toString c.port}") addrs;
|
|
||||||
forwardfor = true;
|
|
||||||
balance = "roundrobin";
|
|
||||||
check = {
|
|
||||||
inter = "5s";
|
|
||||||
downinter = "15s";
|
|
||||||
fall = "3";
|
|
||||||
rise = "3";
|
|
||||||
};
|
|
||||||
httpcheck = "GET /";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.phpfpm-nextcloud.serviceConfig = {
|
systemd.services.phpfpm-nextcloud.serviceConfig = {
|
||||||
|
|
61
modules/ssl.nix
Normal file
61
modules/ssl.nix
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.shb.ssl;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.shb.ssl = {
|
||||||
|
enable = lib.mkEnableOption "selfhostblocks.ssl";
|
||||||
|
|
||||||
|
sopsFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "Sops file location";
|
||||||
|
example = "secrets/haproxy.yaml";
|
||||||
|
};
|
||||||
|
|
||||||
|
domain = lib.mkOption {
|
||||||
|
description = lib.mdDoc "Domain to serve sites under.";
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "domain.com";
|
||||||
|
};
|
||||||
|
|
||||||
|
adminEmail = lib.mkOption {
|
||||||
|
description = lib.mdDoc "Admin email in case certificate retrieval goes wrong.";
|
||||||
|
type = lib.types.str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
users.users.${config.services.nginx.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "nginx";
|
||||||
|
extraGroups = [ config.security.acme.defaults.group ];
|
||||||
|
};
|
||||||
|
users.groups.ngins = {};
|
||||||
|
|
||||||
|
security.acme = {
|
||||||
|
acceptTerms = true;
|
||||||
|
certs."${cfg.domain}" = {
|
||||||
|
extraDomainNames = ["*.${cfg.domain}"];
|
||||||
|
};
|
||||||
|
defaults = {
|
||||||
|
email = cfg.adminEmail;
|
||||||
|
dnsProvider = "linode";
|
||||||
|
dnsResolver = "8.8.8.8";
|
||||||
|
# For example, to use Linode to prove the dns challenge,
|
||||||
|
# the content of the file should be the following, with
|
||||||
|
# XXX replaced by your Linode API token.
|
||||||
|
# LINODE_HTTP_TIMEOUT=10
|
||||||
|
# LINODE_POLLING_INTERVAL=10
|
||||||
|
# LINODE_PROPAGATION_TIMEOUT=240
|
||||||
|
# LINODE_TOKEN=XXX
|
||||||
|
credentialsFile = "/run/secrets/linode";
|
||||||
|
enableDebugLogs = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
sops.secrets.linode = {
|
||||||
|
inherit (cfg) sopsFile;
|
||||||
|
restartUnits = [ "acme-${cfg.domain}.service" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue