push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.SystemdJournal2Gelf;
in
{
options = {
services.SystemdJournal2Gelf = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable SystemdJournal2Gelf.
'';
};
graylogServer = lib.mkOption {
type = lib.types.str;
example = "graylog2.example.com:11201";
description = ''
Host and port of your graylog2 input. This should be a GELF
UDP input.
'';
};
extraOptions = lib.mkOption {
type = lib.types.separatedString " ";
default = "";
description = ''
Any extra flags to pass to SystemdJournal2Gelf. Note that
these are basically `journalctl` flags.
'';
};
package = lib.mkPackageOption pkgs "systemd-journal2gelf" { };
};
};
config = lib.mkIf cfg.enable {
systemd.services.SystemdJournal2Gelf = {
description = "SystemdJournal2Gelf";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/SystemdJournal2Gelf ${cfg.graylogServer} --follow ${cfg.extraOptions}";
Restart = "on-failure";
RestartSec = "30";
};
};
};
}

View File

@@ -0,0 +1,278 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.awstats;
package = pkgs.awstats;
configOpts =
{ name, config, ... }:
{
options = {
type = lib.mkOption {
type = lib.types.enum [
"mail"
"web"
];
default = "web";
example = "mail";
description = ''
The type of log being collected.
'';
};
domain = lib.mkOption {
type = lib.types.str;
default = name;
description = "The domain name to collect stats for.";
example = "example.com";
};
logFile = lib.mkOption {
type = lib.types.str;
example = "/var/log/nginx/access.log";
description = ''
The log file to be scanned.
For mail, set this to
```
journalctl $OLD_CURSOR -u postfix.service | ''${pkgs.perl}/bin/perl ''${pkgs.awstats.out}/share/awstats/tools/maillogconvert.pl standard |
```
'';
};
logFormat = lib.mkOption {
type = lib.types.str;
default = "1";
description = ''
The log format being used.
For mail, set this to
```
%time2 %email %email_r %host %host_r %method %url %code %bytesd
```
'';
};
hostAliases = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "www.example.org" ];
description = ''
List of aliases the site has.
'';
};
extraConfig = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = lib.literalExpression ''
{
"ValidHTTPCodes" = "404";
}
'';
description = "Extra configuration to be appended to awstats.\${name}.conf.";
};
webService = {
enable = lib.mkEnableOption "awstats web service";
hostname = lib.mkOption {
type = lib.types.str;
default = config.domain;
description = "The hostname the web service appears under.";
};
urlPrefix = lib.mkOption {
type = lib.types.str;
default = "/awstats";
description = "The URL prefix under which the awstats pages appear.";
};
};
};
};
webServices = lib.filterAttrs (name: value: value.webService.enable) cfg.configs;
in
{
imports = [
(lib.mkRemovedOptionModule [
"services"
"awstats"
"service"
"enable"
] "Please enable per domain with `services.awstats.configs.<name>.webService.enable`")
(lib.mkRemovedOptionModule [
"services"
"awstats"
"service"
"urlPrefix"
] "Please set per domain with `services.awstats.configs.<name>.webService.urlPrefix`")
(lib.mkRenamedOptionModule [ "services" "awstats" "vardir" ] [ "services" "awstats" "dataDir" ])
];
options.services.awstats = {
enable = lib.mkEnableOption "awstats, a real-time logfile analyzer";
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/awstats";
description = "The directory where awstats data will be stored.";
};
configs = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule configOpts);
default = { };
example = lib.literalExpression ''
{
"mysite" = {
domain = "example.com";
logFile = "/var/log/nginx/access.log";
};
}
'';
description = "Attribute set of domains to collect stats for.";
};
updateAt = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "hourly";
description = ''
Specification of the time at which awstats will get updated.
(in the format described by {manpage}`systemd.time(7)`)
'';
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ package.bin ];
environment.etc = lib.mapAttrs' (
name: opts:
lib.nameValuePair "awstats/awstats.${name}.conf" {
source = pkgs.runCommand "awstats.${name}.conf" { preferLocalBuild = true; } (
''
sed \
''
# set up mail stats
+ lib.optionalString (opts.type == "mail") ''
-e 's|^\(LogType\)=.*$|\1=M|' \
-e 's|^\(LevelForBrowsersDetection\)=.*$|\1=0|' \
-e 's|^\(LevelForOSDetection\)=.*$|\1=0|' \
-e 's|^\(LevelForRefererAnalyze\)=.*$|\1=0|' \
-e 's|^\(LevelForRobotsDetection\)=.*$|\1=0|' \
-e 's|^\(LevelForSearchEnginesDetection\)=.*$|\1=0|' \
-e 's|^\(LevelForFileTypesDetection\)=.*$|\1=0|' \
-e 's|^\(LevelForWormsDetection\)=.*$|\1=0|' \
-e 's|^\(ShowMenu\)=.*$|\1=1|' \
-e 's|^\(ShowSummary\)=.*$|\1=HB|' \
-e 's|^\(ShowMonthStats\)=.*$|\1=HB|' \
-e 's|^\(ShowDaysOfMonthStats\)=.*$|\1=HB|' \
-e 's|^\(ShowDaysOfWeekStats\)=.*$|\1=HB|' \
-e 's|^\(ShowHoursStats\)=.*$|\1=HB|' \
-e 's|^\(ShowDomainsStats\)=.*$|\1=0|' \
-e 's|^\(ShowHostsStats\)=.*$|\1=HB|' \
-e 's|^\(ShowAuthenticatedUsers\)=.*$|\1=0|' \
-e 's|^\(ShowRobotsStats\)=.*$|\1=0|' \
-e 's|^\(ShowEMailSenders\)=.*$|\1=HBML|' \
-e 's|^\(ShowEMailReceivers\)=.*$|\1=HBML|' \
-e 's|^\(ShowSessionsStats\)=.*$|\1=0|' \
-e 's|^\(ShowPagesStats\)=.*$|\1=0|' \
-e 's|^\(ShowFileTypesStats\)=.*$|\1=0|' \
-e 's|^\(ShowFileSizesStats\)=.*$|\1=0|' \
-e 's|^\(ShowBrowsersStats\)=.*$|\1=0|' \
-e 's|^\(ShowOSStats\)=.*$|\1=0|' \
-e 's|^\(ShowOriginStats\)=.*$|\1=0|' \
-e 's|^\(ShowKeyphrasesStats\)=.*$|\1=0|' \
-e 's|^\(ShowKeywordsStats\)=.*$|\1=0|' \
-e 's|^\(ShowMiscStats\)=.*$|\1=0|' \
-e 's|^\(ShowHTTPErrorsStats\)=.*$|\1=0|' \
-e 's|^\(ShowSMTPErrorsStats\)=.*$|\1=1|' \
''
+
# common options
''
-e 's|^\(DirData\)=.*$|\1="${cfg.dataDir}/${name}"|' \
-e 's|^\(DirIcons\)=.*$|\1="icons"|' \
-e 's|^\(CreateDirDataIfNotExists\)=.*$|\1=1|' \
-e 's|^\(SiteDomain\)=.*$|\1="${name}"|' \
-e 's|^\(LogFile\)=.*$|\1="${opts.logFile}"|' \
-e 's|^\(LogFormat\)=.*$|\1="${opts.logFormat}"|' \
''
+
# extra config
lib.concatStringsSep "\n" (
lib.mapAttrsToList (n: v: ''
-e 's|^\(${n}\)=.*$|\1="${v}"|' \
'') opts.extraConfig
)
+ ''
< '${package.out}/wwwroot/cgi-bin/awstats.model.conf' > "$out"
''
);
}
) cfg.configs;
# create data directory with the correct permissions
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 755 root root - -"
]
++ lib.mapAttrsToList (name: opts: "d '${cfg.dataDir}/${name}' 755 root root - -") cfg.configs
++ [ "Z '${cfg.dataDir}' 755 root root - -" ];
# nginx options
services.nginx.virtualHosts = lib.mapAttrs' (name: opts: {
name = opts.webService.hostname;
value = {
locations = {
"${opts.webService.urlPrefix}/css/" = {
alias = "${package.out}/wwwroot/css/";
};
"${opts.webService.urlPrefix}/icons/" = {
alias = "${package.out}/wwwroot/icon/";
};
"${opts.webService.urlPrefix}/" = {
alias = "${cfg.dataDir}/${name}/";
extraConfig = ''
autoindex on;
'';
};
};
};
}) webServices;
# update awstats
systemd.services = lib.mkIf (cfg.updateAt != null) (
lib.mapAttrs' (
name: opts:
lib.nameValuePair "awstats-${name}-update" {
description = "update awstats for ${name}";
script =
lib.optionalString (opts.type == "mail") ''
if [[ -f "${cfg.dataDir}/${name}-cursor" ]]; then
CURSOR="$(cat "${cfg.dataDir}/${name}-cursor" | tr -d '\n')"
if [[ -n "$CURSOR" ]]; then
echo "Using cursor: $CURSOR"
export OLD_CURSOR="--cursor $CURSOR"
fi
fi
NEW_CURSOR="$(journalctl $OLD_CURSOR -u postfix.service --show-cursor | tail -n 1 | tr -d '\n' | sed -e 's#^-- cursor: \(.*\)#\1#')"
echo "New cursor: $NEW_CURSOR"
${package.bin}/bin/awstats -update -config=${name}
if [ -n "$NEW_CURSOR" ]; then
echo -n "$NEW_CURSOR" > ${cfg.dataDir}/${name}-cursor
fi
''
+ ''
${package.out}/share/awstats/tools/awstats_buildstaticpages.pl \
-config=${name} -update -dir=${cfg.dataDir}/${name} \
-awstatsprog=${package.bin}/bin/awstats
'';
startAt = cfg.updateAt;
}
) cfg.configs
);
};
}

View File

@@ -0,0 +1,261 @@
{
config,
lib,
utils,
pkgs,
...
}:
let
inherit (lib)
attrValues
literalExpression
mkEnableOption
mkPackageOption
mkIf
mkOption
types
;
cfg = config.services.filebeat;
json = pkgs.formats.json { };
in
{
options = {
services.filebeat = {
enable = mkEnableOption "filebeat";
package = mkPackageOption pkgs "filebeat" {
example = "filebeat7";
};
inputs = mkOption {
description = ''
Inputs specify how Filebeat locates and processes input data.
This is like `services.filebeat.settings.filebeat.inputs`,
but structured as an attribute set. This has the benefit
that multiple NixOS modules can contribute settings to a
single filebeat input.
An input type can be specified multiple times by choosing a
different `<name>` for each, but setting
[](#opt-services.filebeat.inputs._name_.type)
to the same value.
See <https://www.elastic.co/guide/en/beats/filebeat/current/configuration-filebeat-options.html>.
'';
default = { };
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
freeformType = json.type;
options = {
type = mkOption {
type = types.str;
default = name;
description = ''
The input type.
Look for the value after `type:` on
the individual input pages linked from
<https://www.elastic.co/guide/en/beats/filebeat/current/configuration-filebeat-options.html>.
'';
};
};
}
)
);
example = literalExpression ''
{
journald.id = "everything"; # Only for filebeat7
log = {
enabled = true;
paths = [
"/var/log/*.log"
];
};
};
'';
};
modules = mkOption {
description = ''
Filebeat modules provide a quick way to get started
processing common log formats. They contain default
configurations, Elasticsearch ingest pipeline definitions,
and Kibana dashboards to help you implement and deploy a log
monitoring solution.
This is like `services.filebeat.settings.filebeat.modules`,
but structured as an attribute set. This has the benefit
that multiple NixOS modules can contribute settings to a
single filebeat module.
A module can be specified multiple times by choosing a
different `<name>` for each, but setting
[](#opt-services.filebeat.modules._name_.module)
to the same value.
See <https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-modules.html>.
'';
default = { };
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
freeformType = json.type;
options = {
module = mkOption {
type = types.str;
default = name;
description = ''
The name of the module.
Look for the value after `module:` on
the individual input pages linked from
<https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-modules.html>.
'';
};
};
}
)
);
example = literalExpression ''
{
nginx = {
access = {
enabled = true;
var.paths = [ "/path/to/log/nginx/access.log*" ];
};
error = {
enabled = true;
var.paths = [ "/path/to/log/nginx/error.log*" ];
};
};
};
'';
};
settings = mkOption {
type = types.submodule {
freeformType = json.type;
options = {
output.elasticsearch.hosts = mkOption {
type = with types; listOf str;
default = [ "127.0.0.1:9200" ];
example = [ "myEShost:9200" ];
description = ''
The list of Elasticsearch nodes to connect to.
The events are distributed to these nodes in round
robin order. If one node becomes unreachable, the
event is automatically sent to another node. Each
Elasticsearch node can be defined as a URL or
IP:PORT. For example:
`http://192.15.3.2`,
`https://es.found.io:9230` or
`192.24.3.2:9300`. If no port is
specified, `9200` is used.
'';
};
filebeat = {
inputs = mkOption {
type = types.listOf json.type;
default = [ ];
internal = true;
description = ''
Inputs specify how Filebeat locates and processes
input data. Use [](#opt-services.filebeat.inputs) instead.
See <https://www.elastic.co/guide/en/beats/filebeat/current/configuration-filebeat-options.html>.
'';
};
modules = mkOption {
type = types.listOf json.type;
default = [ ];
internal = true;
description = ''
Filebeat modules provide a quick way to get started
processing common log formats. They contain default
configurations, Elasticsearch ingest pipeline
definitions, and Kibana dashboards to help you
implement and deploy a log monitoring solution.
Use [](#opt-services.filebeat.modules) instead.
See <https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-modules.html>.
'';
};
};
};
};
default = { };
example = literalExpression ''
{
settings = {
output.elasticsearch = {
hosts = [ "myEShost:9200" ];
username = "filebeat_internal";
password = { _secret = "/var/keys/elasticsearch_password"; };
};
logging.level = "info";
};
};
'';
description = ''
Configuration for filebeat. See
<https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-reference-yml.html>
for supported values.
Options containing secret data should be set to an attribute
set containing the attribute `_secret` - a
string pointing to a file containing the value the option
should be set to. See the example to get a better picture of
this: in the resulting
{file}`filebeat.yml` file, the
`output.elasticsearch.password`
key will be set to the contents of the
{file}`/var/keys/elasticsearch_password` file.
'';
};
};
};
config = mkIf cfg.enable {
services.filebeat.settings.filebeat.inputs = attrValues cfg.inputs;
services.filebeat.settings.filebeat.modules = attrValues cfg.modules;
systemd.services.filebeat = {
description = "Filebeat log shipper";
wantedBy = [ "multi-user.target" ];
wants = [ "elasticsearch.service" ];
after = [ "elasticsearch.service" ];
serviceConfig = {
ExecStartPre = pkgs.writeShellScript "filebeat-exec-pre" ''
set -euo pipefail
umask u=rwx,g=,o=
${utils.genJqSecretsReplacementSnippet cfg.settings "/var/lib/filebeat/filebeat.yml"}
'';
ExecStart = ''
${cfg.package}/bin/filebeat -e \
-c "/var/lib/filebeat/filebeat.yml" \
--path.data "/var/lib/filebeat"
'';
Restart = "always";
StateDirectory = "filebeat";
};
};
};
}

View File

@@ -0,0 +1,51 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.fluentd;
pluginArgs = lib.concatStringsSep " " (map (x: "-p ${x}") cfg.plugins);
in
{
###### interface
options = {
services.fluentd = {
enable = lib.mkEnableOption "fluentd, a data/log collector";
config = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Fluentd config.";
};
package = lib.mkPackageOption pkgs "fluentd" { };
plugins = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
description = ''
A list of plugin paths to pass into fluentd. It will make plugins defined in ruby files
there available in your config.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
systemd.services.fluentd = {
description = "Fluentd Daemon";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/fluentd -c ${pkgs.writeText "fluentd.conf" cfg.config} ${pluginArgs}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
};
};
};
}

View File

@@ -0,0 +1,193 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.graylog;
confFile = pkgs.writeText "graylog.conf" ''
is_master = ${lib.boolToString cfg.isMaster}
node_id_file = ${cfg.nodeIdFile}
password_secret = ${cfg.passwordSecret}
root_username = ${cfg.rootUsername}
root_password_sha2 = ${cfg.rootPasswordSha2}
elasticsearch_hosts = ${lib.concatStringsSep "," cfg.elasticsearchHosts}
message_journal_dir = ${cfg.messageJournalDir}
mongodb_uri = ${cfg.mongodbUri}
plugin_dir = /var/lib/graylog/plugins
data_dir = ${cfg.dataDir}
${cfg.extraConfig}
'';
glPlugins = pkgs.buildEnv {
name = "graylog-plugins";
paths = cfg.plugins;
};
in
{
###### interface
options = {
services.graylog = {
enable = lib.mkEnableOption "Graylog, a log management solution";
package = lib.mkPackageOption pkgs "graylog" {
example = "graylog-6_0";
};
user = lib.mkOption {
type = lib.types.str;
default = "graylog";
description = "User account under which graylog runs";
};
isMaster = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether this is the master instance of your Graylog cluster";
};
nodeIdFile = lib.mkOption {
type = lib.types.str;
default = "/var/lib/graylog/server/node-id";
description = "Path of the file containing the graylog node-id";
};
passwordSecret = lib.mkOption {
type = lib.types.str;
description = ''
You MUST set a secret to secure/pepper the stored user passwords here. Use at least 64 characters.
Generate one by using for example: pwgen -N 1 -s 96
'';
};
rootUsername = lib.mkOption {
type = lib.types.str;
default = "admin";
description = "Name of the default administrator user";
};
rootPasswordSha2 = lib.mkOption {
type = lib.types.str;
example = "e3c652f0ba0b4801205814f8b6bc49672c4c74e25b497770bb89b22cdeb4e952";
description = ''
You MUST specify a hash password for the root user (which you only need to initially set up the
system and in case you lose connectivity to your authentication backend)
This password cannot be changed using the API or via the web interface. If you need to change it,
modify it here.
Create one by using for example: echo -n yourpassword | shasum -a 256
and use the resulting hash value as string for the option
'';
};
elasticsearchHosts = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = lib.literalExpression ''[ "http://node1:9200" "http://user:password@node2:19200" ]'';
description = "List of valid URIs of the http ports of your elastic nodes. If one or more of your elasticsearch hosts require authentication, include the credentials in each node URI that requires authentication";
};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/graylog/data";
description = "Directory used to store Graylog server state.";
};
messageJournalDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/graylog/data/journal";
description = "The directory which will be used to store the message journal. The directory must be exclusively used by Graylog and must not contain any other files than the ones created by Graylog itself";
};
mongodbUri = lib.mkOption {
type = lib.types.str;
default = "mongodb://localhost/graylog";
description = "MongoDB connection string. See http://docs.mongodb.org/manual/reference/connection-string/ for details";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Any other configuration options you might want to add";
};
plugins = lib.mkOption {
description = "Extra graylog plugins";
default = [ ];
type = lib.types.listOf lib.types.package;
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
# Note: when changing the default, make it conditional on
# system.stateVersion to maintain compatibility with existing
# systems!
services.graylog.package =
let
mkThrow = ver: throw "graylog-${ver} was removed, please upgrade your graylog version.";
base =
if lib.versionAtLeast config.system.stateVersion "25.05" then
pkgs.graylog-6_0
else if lib.versionAtLeast config.system.stateVersion "23.05" then
mkThrow "5_1"
else
mkThrow "3_3";
in
lib.mkDefault base;
users.users = lib.mkIf (cfg.user == "graylog") {
graylog = {
isSystemUser = true;
group = "graylog";
description = "Graylog server daemon user";
};
};
users.groups = lib.mkIf (cfg.user == "graylog") { graylog = { }; };
systemd.tmpfiles.rules = [
"d '${cfg.messageJournalDir}' - ${cfg.user} - - -"
];
systemd.services.graylog = {
description = "Graylog Server";
wantedBy = [ "multi-user.target" ];
environment = {
GRAYLOG_CONF = "${confFile}";
};
path = [
pkgs.which
pkgs.procps
];
preStart = ''
rm -rf /var/lib/graylog/plugins || true
mkdir -p /var/lib/graylog/plugins -m 755
mkdir -p "$(dirname ${cfg.nodeIdFile})"
chown -R ${cfg.user} "$(dirname ${cfg.nodeIdFile})"
for declarativeplugin in `ls ${glPlugins}/bin/`; do
ln -sf ${glPlugins}/bin/$declarativeplugin /var/lib/graylog/plugins/$declarativeplugin
done
for includedplugin in `ls ${cfg.package}/plugin/`; do
ln -s ${cfg.package}/plugin/$includedplugin /var/lib/graylog/plugins/$includedplugin || true
done
'';
serviceConfig = {
User = "${cfg.user}";
StateDirectory = "graylog";
ExecStart = "${cfg.package}/bin/graylogctl run";
};
};
};
}

View File

@@ -0,0 +1,80 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.heartbeat;
heartbeatYml = pkgs.writeText "heartbeat.yml" ''
name: ${cfg.name}
tags: ${builtins.toJSON cfg.tags}
${cfg.extraConfig}
'';
in
{
options = {
services.heartbeat = {
enable = lib.mkEnableOption "heartbeat, uptime monitoring";
package = lib.mkPackageOption pkgs "heartbeat" {
example = "heartbeat7";
};
name = lib.mkOption {
type = lib.types.str;
default = "heartbeat";
description = "Name of the beat";
};
tags = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Tags to place on the shipped log messages";
};
stateDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/heartbeat";
description = "The state directory. heartbeat's own logs and other data are stored here.";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = ''
heartbeat.monitors:
- type: http
urls: ["http://localhost:9200"]
schedule: '@every 10s'
'';
description = "Any other configuration options you want to add";
};
};
};
config = lib.mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.stateDir}' - nobody nogroup - -"
];
systemd.services.heartbeat = {
description = "heartbeat log shipper";
wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -p "${cfg.stateDir}"/{data,logs}
'';
serviceConfig = {
User = "nobody";
AmbientCapabilities = "cap_net_raw";
ExecStart = "${cfg.package}/bin/heartbeat -c \"${heartbeatYml}\" -path.data \"${cfg.stateDir}/data\" -path.logs \"${cfg.stateDir}/logs\"";
};
};
};
}

View File

@@ -0,0 +1,89 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.journalbeat;
journalbeatYml = pkgs.writeText "journalbeat.yml" ''
name: ${cfg.name}
tags: ${builtins.toJSON cfg.tags}
${cfg.extraConfig}
'';
in
{
options = {
services.journalbeat = {
enable = lib.mkEnableOption "journalbeat";
package = lib.mkPackageOption pkgs "journalbeat" { };
name = lib.mkOption {
type = lib.types.str;
default = "journalbeat";
description = "Name of the beat";
};
tags = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Tags to place on the shipped log messages";
};
stateDir = lib.mkOption {
type = lib.types.str;
default = "journalbeat";
description = ''
Directory below `/var/lib/` to store journalbeat's
own logs and other data. This directory will be created automatically
using systemd's StateDirectory mechanism.
'';
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Any other configuration options you want to add";
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !lib.hasPrefix "/" cfg.stateDir;
message =
"The option services.journalbeat.stateDir shouldn't be an absolute directory."
+ " It should be a directory relative to /var/lib/.";
}
];
systemd.services.journalbeat = {
description = "Journalbeat log shipper";
wantedBy = [ "multi-user.target" ];
wants = [ "elasticsearch.service" ];
after = [ "elasticsearch.service" ];
preStart = ''
mkdir -p ${cfg.stateDir}/data
mkdir -p ${cfg.stateDir}/logs
'';
serviceConfig = {
StateDirectory = cfg.stateDir;
ExecStart = ''
${cfg.package}/bin/journalbeat \
-c ${journalbeatYml} \
-path.data /var/lib/${cfg.stateDir}/data \
-path.logs /var/lib/${cfg.stateDir}/logs'';
Restart = "always";
};
};
};
}

View File

@@ -0,0 +1,121 @@
# This module implements a systemd service for running journaldriver,
# a log forwarding agent that sends logs from journald to Stackdriver
# Logging.
#
# It can be enabled without extra configuration when running on GCP.
# On machines hosted elsewhere, the other configuration options need
# to be set.
#
# For further information please consult the documentation in the
# upstream repository at: https://github.com/tazjin/journaldriver/
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.journaldriver;
in
{
options.services.journaldriver = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable journaldriver to forward journald logs to
Stackdriver Logging.
'';
};
logLevel = mkOption {
type = types.str;
default = "info";
description = ''
Log level at which journaldriver logs its own output.
'';
};
logName = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Configures the name of the target log in Stackdriver Logging.
This option can be set to, for example, the hostname of a
machine to improve the user experience in the logging
overview.
'';
};
googleCloudProject = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Configures the name of the Google Cloud project to which to
forward journald logs.
This option is required on non-GCP machines, but should not be
set on GCP instances.
'';
};
logStream = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Configures the name of the Stackdriver Logging log stream into
which to write journald entries.
This option is required on non-GCP machines, but should not be
set on GCP instances.
'';
};
applicationCredentials = mkOption {
type = with types; nullOr path;
default = null;
description = ''
Path to the service account private key (in JSON-format) used
to forward log entries to Stackdriver Logging on non-GCP
instances.
This option is required on non-GCP machines, but should not be
set on GCP instances.
'';
};
};
config = mkIf cfg.enable {
systemd.services.journaldriver = {
description = "Stackdriver Logging journal forwarder";
script = "${pkgs.journaldriver}/bin/journaldriver";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Restart = "always";
DynamicUser = true;
# This directive lets systemd automatically configure
# permissions on /var/lib/journaldriver, the directory in
# which journaldriver persists its cursor state.
StateDirectory = "journaldriver";
# This group is required for accessing journald.
SupplementaryGroups = "systemd-journal";
};
environment = {
RUST_LOG = cfg.logLevel;
LOG_NAME = cfg.logName;
LOG_STREAM = cfg.logStream;
GOOGLE_CLOUD_PROJECT = cfg.googleCloudProject;
GOOGLE_APPLICATION_CREDENTIALS = cfg.applicationCredentials;
};
};
};
}

View File

@@ -0,0 +1,281 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.journalwatch;
user = "journalwatch";
# for journal access
group = "systemd-journal";
dataDir = "/var/lib/${user}";
journalwatchConfig = pkgs.writeText "config" (
''
# (File Generated by NixOS journalwatch module.)
[DEFAULT]
mail_binary = ${cfg.mailBinary}
priority = ${toString cfg.priority}
mail_from = ${cfg.mailFrom}
''
+ lib.optionalString (cfg.mailTo != null) ''
mail_to = ${cfg.mailTo}
''
+ cfg.extraConfig
);
journalwatchPatterns = pkgs.writeText "patterns" ''
# (File Generated by NixOS journalwatch module.)
${mkPatterns cfg.filterBlocks}
'';
# empty line at the end needed to to separate the blocks
mkPatterns =
filterBlocks:
lib.concatStringsSep "\n" (
map (block: ''
${block.match}
${block.filters}
'') filterBlocks
);
# can't use joinSymlinks directly, because when we point $XDG_CONFIG_HOME
# to the /nix/store path, we still need the subdirectory "journalwatch" inside that
# to match journalwatch's expectations
journalwatchConfigDir =
pkgs.runCommand "journalwatch-config"
{
preferLocalBuild = true;
allowSubstitutes = false;
}
''
mkdir -p $out/journalwatch
ln -sf ${journalwatchConfig} $out/journalwatch/config
ln -sf ${journalwatchPatterns} $out/journalwatch/patterns
'';
in
{
options = {
services.journalwatch = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
If enabled, periodically check the journal with journalwatch and report the results by mail.
'';
};
package = lib.mkPackageOption pkgs "journalwatch" { };
priority = lib.mkOption {
type = lib.types.ints.between 0 7;
default = 6;
description = ''
Lowest priority of message to be considered.
A value between 7 ("debug"), and 0 ("emerg"). Defaults to 6 ("info").
If you don't care about anything with "info" priority, you can reduce
this to e.g. 5 ("notice") to considerably reduce the amount of
messages without needing many {option}`filterBlocks`.
'';
};
# HACK: this is a workaround for journalwatch's usage of socket.getfqdn() which always returns localhost if
# there's an alias for the localhost on a separate line in /etc/hosts, or take for ages if it's not present and
# then return something right-ish in the direction of /etc/hostname. Just bypass it completely.
mailFrom = lib.mkOption {
type = lib.types.str;
default = "journalwatch@${config.networking.hostName}";
defaultText = lib.literalExpression ''"journalwatch@''${config.networking.hostName}"'';
description = ''
Mail address to send journalwatch reports from.
'';
};
mailTo = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Mail address to send journalwatch reports to.
'';
};
mailBinary = lib.mkOption {
type = lib.types.path;
default = "/run/wrappers/bin/sendmail";
description = ''
Sendmail-compatible binary to be used to send the messages.
'';
};
extraConfig = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Extra lines to be added verbatim to the journalwatch/config configuration file.
You can add any commandline argument to the config, without the '--'.
See `journalwatch --help` for all arguments and their description.
'';
};
filterBlocks = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
match = lib.mkOption {
type = lib.types.str;
example = "SYSLOG_IDENTIFIER = systemd";
description = ''
Syntax: `field = value`
Specifies the log entry `field` this block should apply to.
If the `field` of a message matches this `value`,
this patternBlock's {option}`filters` are applied.
If `value` starts and ends with a slash, it is interpreted as
an extended python regular expression, if not, it's an exact match.
The journal fields are explained in {manpage}`systemd.journal-fields(7)`.
'';
};
filters = lib.mkOption {
type = lib.types.str;
example = ''
(Stopped|Stopping|Starting|Started) .*
(Reached target|Stopped target) .*
'';
description = ''
The filters to apply on all messages which satisfy {option}`match`.
Any of those messages that match any specified filter will be removed from journalwatch's output.
Each filter is an extended Python regular expression.
You can specify multiple filters and separate them by newlines.
Lines starting with '#' are comments. Inline-comments are not permitted.
'';
};
};
}
);
example = [
# examples taken from upstream
{
match = "_SYSTEMD_UNIT = systemd-logind.service";
filters = ''
New session [a-z]?\d+ of user \w+\.
Removed session [a-z]?\d+\.
'';
}
{
match = "SYSLOG_IDENTIFIER = /(CROND|crond)/";
filters = ''
pam_unix\(crond:session\): session (opened|closed) for user \w+
\(\w+\) CMD .*
'';
}
];
# another example from upstream.
# very useful on priority = 6, and required as journalwatch throws an error when no pattern is defined at all.
default = [
{
match = "SYSLOG_IDENTIFIER = systemd";
filters = ''
(Stopped|Stopping|Starting|Started) .*
(Created slice|Removed slice) user-\d*\.slice\.
Received SIGRTMIN\+24 from PID .*
(Reached target|Stopped target) .*
Startup finished in \d*ms\.
'';
}
];
description = ''
filterBlocks can be defined to blacklist journal messages which are not errors.
Each block matches on a log entry field, and the filters in that block then are matched
against all messages with a matching log entry field.
All messages whose PRIORITY is at least 6 (INFO) are processed by journalwatch.
If you don't specify any filterBlocks, PRIORITY is reduced to 5 (NOTICE) by default.
All regular expressions are extended Python regular expressions, for details
see: http://doc.pyschools.com/html/regex.html
'';
};
interval = lib.mkOption {
type = lib.types.str;
default = "hourly";
description = ''
How often to run journalwatch.
The format is described in {manpage}`systemd.time(7)`.
'';
};
accuracy = lib.mkOption {
type = lib.types.str;
default = "10min";
description = ''
The time window around the interval in which the journalwatch run will be scheduled.
The format is described in {manpage}`systemd.time(7)`.
'';
};
};
};
config = lib.mkIf cfg.enable {
users.users.${user} = {
isSystemUser = true;
home = dataDir;
group = group;
};
systemd.tmpfiles.rules = [
# present since NixOS 19.09: remove old stateful symlink join directory,
# which has been replaced with the journalwatchConfigDir store path
"R ${dataDir}/config"
];
systemd.services.journalwatch = {
environment = {
# journalwatch stores the last processed timpestamp here
# the share subdirectory is historic now that config home lives in /nix/store,
# but moving this in a backwards-compatible way is much more work than what's justified
# for cleaning that up.
XDG_DATA_HOME = "${dataDir}/share";
XDG_CONFIG_HOME = journalwatchConfigDir;
};
serviceConfig = {
User = user;
Group = group;
Type = "oneshot";
# requires a relative directory name to create beneath /var/lib
StateDirectory = user;
StateDirectoryMode = "0750";
ExecStart = "${lib.getExe cfg.package} mail";
# lowest CPU and IO priority, but both still in best-effort class to prevent starvation
Nice = 19;
IOSchedulingPriority = 7;
};
};
systemd.timers.journalwatch = {
description = "Periodic journalwatch run";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.interval;
AccuracySec = cfg.accuracy;
Persistent = true;
};
};
};
meta = {
maintainers = with lib.maintainers; [ florianjacob ];
};
}

View File

@@ -0,0 +1,9 @@
{ lib, ... }:
{
imports = [
(lib.mkRemovedOptionModule [ "security" "klogd" "enable" ] ''
Logging of kernel messages is now handled by systemd.
'')
];
}

View File

@@ -0,0 +1,281 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.logcheck;
defaultRules = pkgs.runCommand "logcheck-default-rules" { preferLocalBuild = true; } ''
cp -prd ${pkgs.logcheck}/etc/logcheck $out
chmod u+w $out
rm -r $out/logcheck.*
'';
rulesDir = pkgs.symlinkJoin {
name = "logcheck-rules-dir";
paths = ([ defaultRules ] ++ cfg.extraRulesDirs);
};
configFile = pkgs.writeText "logcheck.conf" cfg.config;
logFiles = pkgs.writeText "logcheck.logfiles" cfg.files;
flags = "-r ${rulesDir} -c ${configFile} -L ${logFiles} -${levelFlag} -m ${cfg.mailTo}";
levelFlag = lib.getAttrFromPath [ cfg.level ] {
paranoid = "p";
server = "s";
workstation = "w";
};
cronJob = ''
@reboot logcheck env PATH=/run/wrappers/bin:$PATH nice -n10 ${pkgs.logcheck}/sbin/logcheck -R ${flags}
2 ${cfg.timeOfDay} * * * logcheck env PATH=/run/wrappers/bin:$PATH nice -n10 ${pkgs.logcheck}/sbin/logcheck ${flags}
'';
writeIgnoreRule =
name:
{ level, regex, ... }:
pkgs.writeTextFile {
inherit name;
destination = "/ignore.d.${level}/${name}";
text = ''
^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ ${regex}
'';
};
writeIgnoreCronRule =
name:
{
level,
user,
regex,
cmdline,
...
}:
let
escapeRegex = lib.escape (lib.stringToCharacters "\\[]{}()^$?*+|.");
cmdline_ = builtins.unsafeDiscardStringContext cmdline;
re =
if regex != "" then
regex
else if cmdline_ == "" then
".*"
else
escapeRegex cmdline_;
in
writeIgnoreRule "cron-${name}" {
inherit level;
regex = ''
(/usr/bin/)?cron\[[0-9]+\]: \(${user}\) CMD \(${re}\)$
'';
};
levelOption = lib.mkOption {
default = "server";
type = lib.types.enum [
"workstation"
"server"
"paranoid"
];
description = ''
Set the logcheck level.
'';
};
ignoreOptions = {
options = {
level = levelOption;
regex = lib.mkOption {
default = "";
type = lib.types.str;
description = ''
Regex specifying which log lines to ignore.
'';
};
};
};
ignoreCronOptions = {
options = {
user = lib.mkOption {
default = "root";
type = lib.types.str;
description = ''
User that runs the cronjob.
'';
};
cmdline = lib.mkOption {
default = "";
type = lib.types.str;
description = ''
Command line for the cron job. Will be turned into a regex for the logcheck ignore rule.
'';
};
timeArgs = lib.mkOption {
default = null;
type = lib.types.nullOr (lib.types.str);
example = "02 06 * * *";
description = ''
"min hr dom mon dow" crontab time args, to auto-create a cronjob too.
Leave at null to not do this and just add a logcheck ignore rule.
'';
};
};
};
in
{
options = {
services.logcheck = {
enable = lib.mkEnableOption "logcheck cron job, to mail anomalies in the system logfiles to the administrator";
user = lib.mkOption {
default = "logcheck";
type = lib.types.str;
description = ''
Username for the logcheck user.
'';
};
timeOfDay = lib.mkOption {
default = "*";
example = "6";
type = lib.types.str;
description = ''
Time of day to run logcheck. A logcheck will be scheduled at xx:02 each day.
Leave default (*) to run every hour. Of course when nothing special was logged,
logcheck will be silent.
'';
};
mailTo = lib.mkOption {
default = "root";
example = "you@domain.com";
type = lib.types.str;
description = ''
Email address to send reports to.
'';
};
level = lib.mkOption {
default = "server";
type = lib.types.str;
description = ''
Set the logcheck level. Either "workstation", "server", or "paranoid".
'';
};
config = lib.mkOption {
default = "FQDN=1";
type = lib.types.lines;
description = ''
Config options that you would like in logcheck.conf.
'';
};
files = lib.mkOption {
default = [ "/var/log/messages" ];
type = lib.types.listOf lib.types.path;
example = [
"/var/log/messages"
"/var/log/mail"
];
description = ''
Which log files to check.
'';
};
extraRulesDirs = lib.mkOption {
default = [ ];
example = [ "/etc/logcheck" ];
type = lib.types.listOf lib.types.path;
description = ''
Directories with extra rules.
'';
};
ignore = lib.mkOption {
default = { };
description = ''
This option defines extra ignore rules.
'';
type = with lib.types; attrsOf (submodule ignoreOptions);
};
ignoreCron = lib.mkOption {
default = { };
description = ''
This option defines extra ignore rules for cronjobs.
'';
type = with lib.types; attrsOf (submodule ignoreCronOptions);
};
extraGroups = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.str;
example = [
"postdrop"
"mongodb"
];
description = ''
Extra groups for the logcheck user, for example to be able to use sendmail,
or to access certain log files.
'';
};
};
};
config = lib.mkIf cfg.enable {
services.logcheck.extraRulesDirs =
lib.mapAttrsToList writeIgnoreRule cfg.ignore
++ lib.mapAttrsToList writeIgnoreCronRule cfg.ignoreCron;
users.users = lib.optionalAttrs (cfg.user == "logcheck") {
logcheck = {
group = "logcheck";
isSystemUser = true;
shell = "/bin/sh";
description = "Logcheck user account";
extraGroups = cfg.extraGroups;
};
};
users.groups = lib.optionalAttrs (cfg.user == "logcheck") {
logcheck = { };
};
systemd.tmpfiles.settings.logcheck = {
"/var/lib/logcheck".d = {
mode = "700";
inherit (cfg) user;
};
"/var/lock/logcheck".d = {
mode = "700";
inherit (cfg) user;
};
};
services.cron.systemCronJobs =
let
withTime = name: { timeArgs, ... }: timeArgs != null;
mkCron =
name:
{
user,
cmdline,
timeArgs,
...
}:
''
${timeArgs} ${user} ${cmdline}
'';
in
lib.mapAttrsToList mkCron (lib.filterAttrs withTime cfg.ignoreCron) ++ [ cronJob ];
};
}

View File

@@ -0,0 +1,377 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.services.logrotate;
generateLine =
n: v:
if
builtins.elem n [
"files"
"priority"
"enable"
"global"
]
|| v == null
then
null
else if builtins.elem n [ "frequency" ] then
"${v}\n"
else if
builtins.elem n [
"firstaction"
"lastaction"
"prerotate"
"postrotate"
"preremove"
]
then
"${n}\n ${v}\n endscript\n"
else if lib.isInt v then
"${n} ${toString v}\n"
else if v == true then
"${n}\n"
else if v == false then
"no${n}\n"
else
"${n} ${v}\n";
generateSection =
indent: settings:
lib.concatStringsSep (lib.fixedWidthString indent " " "") (
lib.filter (x: x != null) (lib.mapAttrsToList generateLine settings)
);
# generateSection includes a final newline hence weird closing brace
mkConf =
settings:
if settings.global or false then
generateSection 0 settings
else
''
${lib.concatMapStringsSep "\n" (files: ''"${files}"'') (lib.toList settings.files)} {
${generateSection 2 settings}}
'';
settings = lib.sortProperties (
lib.attrValues (
lib.filterAttrs (_: settings: settings.enable) (
lib.foldAttrs lib.recursiveUpdate { } [
{
header = {
enable = true;
missingok = true;
notifempty = true;
frequency = "weekly";
rotate = 4;
};
}
cfg.settings
{
header = {
global = true;
priority = 100;
};
}
]
)
)
);
configFile = pkgs.writeTextFile {
name = "logrotate.conf";
text = lib.concatStringsSep "\n" (map mkConf settings);
checkPhase = lib.optionalString cfg.checkConfig ''
# logrotate --debug also checks that users specified in config
# file exist, but we only have sandboxed users here so brown these
# out. according to man page that means su, create and createolddir.
# files required to exist also won't be present, so missingok is forced.
user=$(${pkgs.buildPackages.coreutils}/bin/id -un)
group=$(${pkgs.buildPackages.coreutils}/bin/id -gn)
sed -e "s/\bsu\s.*/su $user $group/" \
-e "s/\b\(create\s\+[0-9]*\s*\|createolddir\s\+[0-9]*\s\+\).*/\1$user $group/" \
-e "1imissingok" -e "s/\bnomissingok\b//" \
$out > logrotate.conf
# Since this makes for very verbose builds only show real error.
# There is no way to control log level, but logrotate hardcodes
# 'error:' at common log level, so we can use grep, taking care
# to keep error codes
set -o pipefail
if ! ${pkgs.buildPackages.logrotate}/sbin/logrotate -s logrotate.status \
--debug logrotate.conf 2>&1 \
| ( ! grep "error:" ) > logrotate-error; then
echo "Logrotate configuration check failed."
echo "The failing configuration (after adjustments to pass tests in sandbox) was:"
printf "%s\n" "-------"
cat logrotate.conf
printf "%s\n" "-------"
echo "The error reported by logrotate was as follow:"
printf "%s\n" "-------"
cat logrotate-error
printf "%s\n" "-------"
echo "You can disable this check with services.logrotate.checkConfig = false,"
echo "but if you think it should work please report this failure along with"
echo "the config file being tested!"
false
fi
'';
};
mailOption = lib.optionalString (lib.foldr (n: a: a || (n.mail or false) != false) false (
lib.attrValues cfg.settings
)) "--mail=${pkgs.mailutils}/bin/mail";
in
{
imports = [
(lib.mkRemovedOptionModule [
"services"
"logrotate"
"config"
] "Modify services.logrotate.settings.header instead")
(lib.mkRemovedOptionModule [
"services"
"logrotate"
"extraConfig"
] "Modify services.logrotate.settings.header instead")
(lib.mkRemovedOptionModule [
"services"
"logrotate"
"paths"
] "Add attributes to services.logrotate.settings instead")
];
options = {
services.logrotate = {
enable = lib.mkEnableOption "the logrotate systemd service" // {
default = lib.foldr (n: a: a || n.enable) false (lib.attrValues cfg.settings);
defaultText = lib.literalExpression "cfg.settings != {}";
};
allowNetworking = lib.mkEnableOption "network access for logrotate";
settings = lib.mkOption {
default = { };
description = ''
logrotate freeform settings: each attribute here will define its own section,
ordered by {option}`services.logrotate.settings.<name>.priority`,
which can either define files to rotate with their settings
or settings common to all further files settings.
All attribute names not explicitly defined as sub-options here are passed through
as logrotate config directives,
refer to <https://linux.die.net/man/8/logrotate> for details.
'';
example = lib.literalExpression ''
{
# global options
header = {
dateext = true;
};
# example custom files
"/var/log/mylog.log" = {
frequency = "daily";
rotate = 3;
};
"multiple paths" = {
files = [
"/var/log/first*.log"
"/var/log/second.log"
];
};
# specify custom order of sections
"/var/log/myservice/*.log" = {
# ensure lower priority
priority = 110;
postrotate = '''
systemctl reload myservice
''';
};
};
'';
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
freeformType =
with lib.types;
attrsOf (
nullOr (oneOf [
int
bool
str
])
);
options = {
enable = lib.mkEnableOption "setting individual kill switch" // {
default = true;
};
global = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether this setting is a global option or not: set to have these
settings apply to all files settings with a higher priority.
'';
};
files = lib.mkOption {
type = with lib.types; either str (listOf str);
default = name;
defaultText = ''
The attrset name if not specified
'';
description = ''
Single or list of files for which rules are defined.
The files are quoted with double-quotes in logrotate configuration,
so globs and spaces are supported.
Note this setting is ignored if globals is true.
'';
};
frequency = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
How often to rotate the logs. Defaults to previously set global setting,
which itself defaults to weekly.
'';
};
priority = lib.mkOption {
type = lib.types.int;
default = 1000;
description = ''
Order of this logrotate block in relation to the others. The semantics are
the same as with `lib.mkOrder`. Smaller values are inserted first.
'';
};
};
}
)
);
};
configFile = lib.mkOption {
type = lib.types.path;
default = configFile;
defaultText = ''
A configuration file automatically generated by NixOS.
'';
description = ''
Override the configuration file used by logrotate. By default,
NixOS generates one automatically from [](#opt-services.logrotate.settings).
'';
example = lib.literalExpression ''
pkgs.writeText "logrotate.conf" '''
missingok
"/var/log/*.log" {
rotate 4
weekly
}
''';
'';
};
checkConfig = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether the config should be checked at build time.
Some options are not checkable at build time because of the build sandbox:
for example, the test does not know about existing files and system users are
not known.
These limitations mean we must adjust the file for tests (missingok is forced
and users are replaced by dummy users), so tests are complemented by a
logrotate-checkconf service that is enabled by default.
This extra check can be disabled by disabling it at the systemd level with the
{option}`systemd.services.logrotate-checkconf.enable` option.
Conversely there are still things that might make this check fail incorrectly
(e.g. a file path where we don't have access to intermediate directories):
in this case you can disable the failing check with this option.
'';
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Additional command line arguments to pass on logrotate invocation";
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.logrotate = {
description = "Logrotate Service";
documentation = [
"man:logrotate(8)"
"man:logrotate(5)"
];
startAt = "hourly";
serviceConfig = {
Type = "oneshot";
ExecStart = "${lib.getExe pkgs.logrotate} ${utils.escapeSystemdExecArgs cfg.extraArgs} ${mailOption} ${cfg.configFile}";
# performance
Nice = 19;
IOSchedulingClass = "best-effort";
IOSchedulingPriority = 7;
# hardening
CapabilityBoundingSet = [
"CAP_CHOWN"
"CAP_DAC_OVERRIDE"
"CAP_FOWNER"
"CAP_KILL"
"CAP_SETUID"
"CAP_SETGID"
];
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "full";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = false; # can create sgid directories
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged @resources"
"@chown @setuid"
];
UMask = "0027";
}
// lib.optionalAttrs (!cfg.allowNetworking) {
PrivateNetwork = true; # e.g. mail delivery
RestrictAddressFamilies = [ "AF_UNIX" ];
};
};
systemd.services.logrotate-checkconf = {
description = "Logrotate configuration check";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.logrotate}/sbin/logrotate ${utils.escapeSystemdExecArgs cfg.extraArgs} --debug ${cfg.configFile}";
};
};
};
}

View File

@@ -0,0 +1,208 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.logstash;
ops = lib.optionalString;
verbosityFlag = "--log.level " + cfg.logLevel;
logstashConf = pkgs.writeText "logstash.conf" ''
input {
${cfg.inputConfig}
}
filter {
${cfg.filterConfig}
}
output {
${cfg.outputConfig}
}
'';
logstashSettingsYml = pkgs.writeText "logstash.yml" cfg.extraSettings;
logstashJvmOptionsFile = pkgs.writeText "jvm.options" cfg.extraJvmOptions;
logstashSettingsDir =
pkgs.runCommand "logstash-settings"
{
inherit logstashJvmOptionsFile;
inherit logstashSettingsYml;
preferLocalBuild = true;
}
''
mkdir -p $out
ln -s $logstashSettingsYml $out/logstash.yml
ln -s $logstashJvmOptionsFile $out/jvm.options
'';
in
{
imports = [
(lib.mkRenamedOptionModule
[ "services" "logstash" "address" ]
[ "services" "logstash" "listenAddress" ]
)
(lib.mkRemovedOptionModule [
"services"
"logstash"
"enableWeb"
] "The web interface was removed from logstash")
];
###### interface
options = {
services.logstash = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable logstash.";
};
package = lib.mkPackageOption pkgs "logstash" { };
plugins = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
example = lib.literalExpression "[ pkgs.logstash-contrib ]";
description = "The paths to find other logstash plugins in.";
};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/logstash";
description = ''
A path to directory writable by logstash that it uses to store data.
Plugins will also have access to this path.
'';
};
logLevel = lib.mkOption {
type = lib.types.enum [
"debug"
"info"
"warn"
"error"
"fatal"
];
default = "warn";
description = "Logging verbosity level.";
};
filterWorkers = lib.mkOption {
type = lib.types.int;
default = 1;
description = "The quantity of filter workers to run.";
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address on which to start webserver.";
};
port = lib.mkOption {
type = lib.types.str;
default = "9292";
description = "Port on which to start webserver.";
};
inputConfig = lib.mkOption {
type = lib.types.lines;
default = "generator { }";
description = "Logstash input configuration.";
example = lib.literalExpression ''
'''
# Read from journal
pipe {
command => "''${config.systemd.package}/bin/journalctl -f -o json"
type => "syslog" codec => json {}
}
'''
'';
};
filterConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "logstash filter configuration.";
example = ''
if [type] == "syslog" {
# Keep only relevant systemd fields
# https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html
prune {
whitelist_names => [
"type", "@timestamp", "@version",
"MESSAGE", "PRIORITY", "SYSLOG_FACILITY"
]
}
}
'';
};
outputConfig = lib.mkOption {
type = lib.types.lines;
default = "stdout { codec => rubydebug }";
description = "Logstash output configuration.";
example = ''
redis { host => ["localhost"] data_type => "list" key => "logstash" codec => json }
elasticsearch { }
'';
};
extraSettings = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra Logstash settings in YAML format.";
example = ''
pipeline:
batch:
size: 125
delay: 5
'';
};
extraJvmOptions = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra JVM options, one per line (jvm.options format).";
example = ''
-Xms2g
-Xmx2g
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
systemd.services.logstash = {
description = "Logstash Daemon";
wantedBy = [ "multi-user.target" ];
path = [ pkgs.bash ];
serviceConfig = {
ExecStartPre = ''${pkgs.coreutils}/bin/mkdir -p "${cfg.dataDir}" ; ${pkgs.coreutils}/bin/chmod 700 "${cfg.dataDir}"'';
ExecStart = lib.concatStringsSep " " (
lib.filter (s: lib.stringLength s != 0) [
"${cfg.package}/bin/logstash"
"-w ${toString cfg.filterWorkers}"
(lib.concatMapStringsSep " " (x: "--path.plugins ${x}") cfg.plugins)
"${verbosityFlag}"
"-f ${logstashConf}"
"--path.settings ${logstashSettingsDir}"
"--path.data ${cfg.dataDir}"
]
);
};
};
};
}

View File

@@ -0,0 +1,122 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.promtail;
format = pkgs.formats.json { };
prettyJSON =
conf:
with lib;
pipe conf [
(flip removeAttrs [ "_module" ])
(format.generate "promtail-config.json")
];
allowSystemdJournal =
cfg.configuration ? scrape_configs && lib.any (v: v ? journal) cfg.configuration.scrape_configs;
allowPositionsFile = !lib.hasPrefix "/var/cache/promtail" positionsFile;
positionsFile = cfg.configuration.positions.filename;
configFile = if cfg.configFile != null then cfg.configFile else prettyJSON cfg.configuration;
in
{
options.services.promtail = with types; {
enable = mkEnableOption "the Promtail ingresser";
configuration = mkOption {
type = format.type;
description = ''
Specify the configuration for Promtail in Nix.
This option will be ignored if `services.promtail.configFile` is defined.
'';
};
configFile = mkOption {
type = nullOr path;
default = null;
description = ''
Config file path for Promtail.
If this option is defined, the value of `services.promtail.configuration` will be ignored.
'';
};
extraFlags = mkOption {
type = listOf str;
default = [ ];
example = [ "--server.http-listen-port=3101" ];
description = ''
Specify a list of additional command line flags,
which get escaped and are then passed to Loki.
'';
};
};
config = mkIf cfg.enable {
services.promtail.configuration.positions.filename = mkDefault "/var/cache/promtail/positions.yaml";
systemd.services.promtail = {
description = "Promtail log ingress";
wantedBy = [ "multi-user.target" ];
stopIfChanged = false;
preStart = ''
${lib.getExe pkgs.promtail} -config.file=${configFile} -check-syntax
'';
serviceConfig = {
Restart = "on-failure";
TimeoutStopSec = 10;
ExecStart = "${pkgs.promtail}/bin/promtail -config.file=${configFile} ${escapeShellArgs cfg.extraFlags}";
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
CacheDirectory = "promtail";
ReadWritePaths = lib.optional allowPositionsFile (builtins.dirOf positionsFile);
User = "promtail";
Group = "promtail";
CapabilityBoundingSet = "";
NoNewPrivileges = true;
ProtectKernelModules = true;
SystemCallArchitectures = "native";
ProtectKernelLogs = true;
ProtectClock = true;
LockPersonality = true;
ProtectHostname = true;
RestrictRealtime = true;
MemoryDenyWriteExecute = true;
PrivateUsers = true;
SupplementaryGroups = lib.optional allowSystemdJournal "systemd-journal";
}
// (optionalAttrs (!pkgs.stdenv.hostPlatform.isAarch64) {
# FIXME: figure out why this breaks on aarch64
SystemCallFilter = "@system-service";
});
};
users.groups.promtail = { };
users.users.promtail = {
description = "Promtail service user";
isSystemUser = true;
group = "promtail";
};
};
}

View File

@@ -0,0 +1,106 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.rsyslogd;
syslogConf = pkgs.writeText "syslog.conf" ''
$ModLoad imuxsock
$SystemLogSocketName /run/systemd/journal/syslog
$WorkDirectory /var/spool/rsyslog
${cfg.defaultConfig}
${cfg.extraConfig}
'';
defaultConf = ''
# "local1" is used for dhcpd messages.
local1.* -/var/log/dhcpd
mail.* -/var/log/mail
*.=warning;*.=err -/var/log/warn
*.crit /var/log/warn
*.*;mail.none;local1.none -/var/log/messages
'';
in
{
###### interface
options = {
services.rsyslogd = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable syslogd. Note that systemd also logs
syslog messages, so you normally don't need to run syslogd.
'';
};
defaultConfig = lib.mkOption {
type = lib.types.lines;
default = defaultConf;
description = ''
The default {file}`syslog.conf` file configures a
fairly standard setup of log files, which can be extended by
means of {var}`extraConfig`.
'';
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
example = "news.* -/var/log/news";
description = ''
Additional text appended to {file}`syslog.conf`,
i.e. the contents of {var}`defaultConfig`.
'';
};
extraParams = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "-m 0" ];
description = ''
Additional parameters passed to {command}`rsyslogd`.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.rsyslog ];
systemd.services.syslog = {
description = "Syslog Daemon";
requires = [ "syslog.socket" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.rsyslog}/sbin/rsyslogd ${toString cfg.extraParams} -f ${syslogConf} -n";
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p /var/spool/rsyslog";
# Prevent syslogd output looping back through journald.
StandardOutput = "null";
};
};
};
}

View File

@@ -0,0 +1,96 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.syslog-ng;
syslogngConfig = pkgs.writeText "syslog-ng.conf" ''
${cfg.configHeader}
${cfg.extraConfig}
'';
ctrlSocket = "/run/syslog-ng/syslog-ng.ctl";
pidFile = "/run/syslog-ng/syslog-ng.pid";
persistFile = "/var/syslog-ng/syslog-ng.persist";
syslogngOptions = [
"--foreground"
"--module-path=${
lib.concatStringsSep ":" ([ "${cfg.package}/lib/syslog-ng" ] ++ cfg.extraModulePaths)
}"
"--cfgfile=${syslogngConfig}"
"--control=${ctrlSocket}"
"--persist-file=${persistFile}"
"--pidfile=${pidFile}"
];
in
{
imports = [
(lib.mkRemovedOptionModule [ "services" "syslog-ng" "serviceName" ] "")
(lib.mkRemovedOptionModule [ "services" "syslog-ng" "listenToJournal" ] "")
];
options = {
services.syslog-ng = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable the syslog-ng daemon.
'';
};
package = lib.mkPackageOption pkgs "syslogng" { };
extraModulePaths = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
A list of paths that should be included in syslog-ng's
`--module-path` option. They should usually
end in `/lib/syslog-ng`
'';
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Configuration added to the end of `syslog-ng.conf`.
'';
};
configHeader = lib.mkOption {
type = lib.types.lines;
default = ''
@version: 4.4
@include "scl.conf"
'';
description = ''
The very first lines of the configuration file. Should usually contain
the syslog-ng version header.
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.syslog-ng = {
description = "syslog-ng daemon";
preStart = "mkdir -p /{var,run}/syslog-ng";
wantedBy = [ "multi-user.target" ];
after = [ "multi-user.target" ]; # makes sure hostname etc is set
serviceConfig = {
Type = "notify";
PIDFile = pidFile;
StandardOutput = "null";
Restart = "on-failure";
ExecStart = "${cfg.package}/sbin/syslog-ng ${lib.concatStringsSep " " syslogngOptions}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
};
};
};
}

View File

@@ -0,0 +1,132 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.syslogd;
syslogConf = pkgs.writeText "syslog.conf" ''
${lib.optionalString (cfg.tty != "") "kern.warning;*.err;authpriv.none /dev/${cfg.tty}"}
${cfg.defaultConfig}
${cfg.extraConfig}
'';
defaultConf = ''
# Send emergency messages to all users.
*.emerg *
# "local1" is used for dhcpd messages.
local1.* -/var/log/dhcpd
mail.* -/var/log/mail
*.=warning;*.=err -/var/log/warn
*.crit /var/log/warn
*.*;mail.none;local1.none -/var/log/messages
'';
in
{
###### interface
options = {
services.syslogd = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable syslogd. Note that systemd also logs
syslog messages, so you normally don't need to run syslogd.
'';
};
tty = lib.mkOption {
type = lib.types.str;
default = "tty10";
description = ''
The tty device on which syslogd will print important log
messages. Leave this option blank to disable tty logging.
'';
};
defaultConfig = lib.mkOption {
type = lib.types.lines;
default = defaultConf;
description = ''
The default {file}`syslog.conf` file configures a
fairly standard setup of log files, which can be extended by
means of {var}`extraConfig`.
'';
};
enableNetworkInput = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Accept logging through UDP. Option -r of {manpage}`syslogd(8)`.
'';
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
example = "news.* -/var/log/news";
description = ''
Additional text appended to {file}`syslog.conf`,
i.e. the contents of {var}`defaultConfig`.
'';
};
extraParams = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "-m 0" ];
description = ''
Additional parameters passed to {command}`syslogd`.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !config.services.rsyslogd.enable;
message = "rsyslogd conflicts with syslogd";
}
];
environment.systemPackages = [ pkgs.sysklogd ];
services.syslogd.extraParams = lib.optional cfg.enableNetworkInput "-r";
# FIXME: restarting syslog seems to break journal logging.
systemd.services.syslog = {
description = "Syslog Daemon";
requires = [ "syslog.socket" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.sysklogd}/sbin/syslogd ${toString cfg.extraParams} -f ${syslogConf} -n";
# Prevent syslogd output looping back through journald.
StandardOutput = "null";
};
};
};
}

View File

@@ -0,0 +1,68 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.ulogd;
settingsFormat = pkgs.formats.ini { listsAsDuplicateKeys = true; };
settingsFile = settingsFormat.generate "ulogd.conf" cfg.settings;
in
{
options = {
services.ulogd = {
enable = lib.mkEnableOption "ulogd, a userspace logging daemon for netfilter/iptables related logging";
settings = lib.mkOption {
example = {
global.stack = [
"log1:NFLOG,base1:BASE,ifi1:IFINDEX,ip2str1:IP2STR,print1:PRINTPKT,emu1:LOGEMU"
"log1:NFLOG,base1:BASE,pcap1:PCAP"
];
log1.group = 2;
pcap1 = {
sync = 1;
file = "/var/log/ulogd.pcap";
};
emu1 = {
sync = 1;
file = "/var/log/ulogd_pkts.log";
};
};
type = settingsFormat.type;
default = { };
description = "Configuration for ulogd. See {file}`/share/doc/ulogd/` in `pkgs.ulogd.doc`.";
};
logLevel = lib.mkOption {
type = lib.types.enum [
1
3
5
7
8
];
default = 5;
description = "Log level (1 = debug, 3 = info, 5 = notice, 7 = error, 8 = fatal)";
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.ulogd = {
description = "Ulogd Daemon";
wantedBy = [ "multi-user.target" ];
wants = [ "network-pre.target" ];
before = [ "network-pre.target" ];
serviceConfig = {
ExecStart = "${pkgs.ulogd}/bin/ulogd -c ${settingsFile} --verbose --loglevel ${toString cfg.logLevel}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
};
};
};
}

View File

@@ -0,0 +1,92 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.vector;
in
{
options.services.vector = {
enable = lib.mkEnableOption "Vector, a high-performance observability data pipeline";
package = lib.mkPackageOption pkgs "vector" { };
journaldAccess = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable Vector to access journald.
'';
};
gracefulShutdownLimitSecs = lib.mkOption {
type = lib.types.ints.positive;
default = 60;
description = ''
Set the duration in seconds to wait for graceful shutdown after SIGINT or SIGTERM are received.
After the duration has passed, Vector will force shutdown.
'';
};
validateConfig = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Enable the checking of the vector config during build time. This should be disabled when interpolating environment variables.
'';
};
settings = lib.mkOption {
type = (pkgs.formats.json { }).type;
default = { };
description = ''
Specify the configuration for Vector in Nix.
'';
};
};
config = lib.mkIf cfg.enable {
# for cli usage
environment.systemPackages = [ cfg.package ];
systemd.services.vector = {
description = "Vector event and log aggregator";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
requires = [ "network-online.target" ];
serviceConfig =
let
format = pkgs.formats.toml { };
conf = format.generate "vector.toml" cfg.settings;
validatedConfig =
file:
pkgs.runCommand "validate-vector-conf"
{
nativeBuildInputs = [ cfg.package ];
}
''
vector validate --no-environment "${file}"
ln -s "${file}" "$out"
'';
in
{
ExecStart = "${lib.getExe cfg.package} --config ${
if cfg.validateConfig then (validatedConfig conf) else conf
} --graceful-shutdown-limit-secs ${builtins.toString cfg.gracefulShutdownLimitSecs}";
DynamicUser = true;
Restart = "always";
StateDirectory = "vector";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
# This group is required for accessing journald.
SupplementaryGroups = lib.mkIf cfg.journaldAccess "systemd-journal";
};
unitConfig = {
StartLimitIntervalSec = 10;
StartLimitBurst = 5;
};
};
};
}