Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Octoprint: hardening and RFC 42 #335827

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions nixos/doc/manual/release-notes/rl-2505.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@
- `zf` was updated to 0.10.2, which includes breaking changes from the [0.10.0 release](https://github.com/natecraddock/zf/releases/tag/0.10.0).
`zf` no longer does Unicode normalization of the input and no longer supports terminal escape sequences in the `ZF_PROMPT` environment variable.

- The `octoprint` service has gained an `enableRaspberryPi` option, which will
be disabled for state versions following 25.05. Users running on Raspberry Pi
should enable the option to restore full functionality.

<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->

## Other Notable Changes {#sec-release-25.05-notable-changes}
Expand Down
114 changes: 90 additions & 24 deletions nixos/modules/services/misc/octoprint.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@
}:
let

cfg = config.services.octoprint;
inherit (lib)
literalExpression
mkDefault
mkEnableOption
mkOption
mkRenamedOptionModule
optional
types
versionOlder
;

baseConfig = {
plugins.curalegacy.cura_engine = "${pkgs.curaengine_stable}/bin/CuraEngine";
server.port = cfg.port;
webcam.ffmpeg = "${pkgs.ffmpeg.bin}/bin/ffmpeg";
} // lib.optionalAttrs (cfg.host != null) { server.host = cfg.host; };
cfg = config.services.octoprint;

fullConfig = lib.recursiveUpdate cfg.extraConfig baseConfig;
formatType = pkgs.formats.json { };

cfgUpdate = pkgs.writeText "octoprint-config.yaml" (builtins.toJSON fullConfig);
configFile = formatType.generate "octoprint-config.yaml" cfg.settings;

pluginsEnv = package.python.withPackages (ps: [ ps.octoprint ] ++ (cfg.plugins ps));

Expand Down Expand Up @@ -72,25 +77,53 @@ in
description = "State directory of the daemon.";
};

plugins = lib.mkOption {
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = plugins: [ ];
defaultText = lib.literalExpression "plugins: []";
example = lib.literalExpression "plugins: with plugins; [ themeify stlviewer ]";
plugins = mkOption {
type = types.functionTo (types.listOf types.package);
default = _plugins: [ ];
defaultText = literalExpression "plugins: []";
example = literalExpression "plugins: with plugins; [ themeify stlviewer ]";
description = "Additional plugins to be used. Available plugins are passed through the plugins input.";
};

extraConfig = lib.mkOption {
type = lib.types.attrs;
settings = mkOption {
default = { };
description = "Extra options which are added to OctoPrint's YAML configuration file.";
description = ''
The octoprint settings, for definitions see the upstream [documentation](https://docs.octoprint.org).
Will override any existing settings.
'';
type = types.submodule {
freeformType = formatType.type;
config = {
plugins.curalegacy.cura_engine = mkDefault "${pkgs.curaengine_stable}/bin/CuraEngine";
server.host = cfg.host;
server.port = cfg.port;
webcam.ffmpeg = mkDefault "${pkgs.ffmpeg.bin}/bin/ffmpeg";
};
};
};
enableRaspberryPi = mkEnableOption "RaspberryPi specific hardware access rules" // {
default = versionOlder config.system.stateVersion "25.05";
};

};

};

##### implementation
imports = [
(mkRenamedOptionModule
[
"services"
"octoprint"
"extraConfig"
]
[
"services"
"octoprint"
"settings"
]
)
];

config = lib.mkIf cfg.enable {

Expand All @@ -105,12 +138,13 @@ in
octoprint.gid = config.ids.gids.octoprint;
};

systemd.tmpfiles.rules = [
"d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
# this will allow octoprint access to raspberry specific hardware to check for throttling
# read-only will not work: "VCHI initialization failed" error
"a /dev/vchiq - - - - u:octoprint:rw"
];
systemd.tmpfiles.rules =
[ "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -" ]
++ optional cfg.enableRaspberryPi
# this will allow octoprint access to raspberry specific hardware to check for throttling
# read-only will not work: "VCHI initialization failed" error
# FIXME: this should probably be a udev rule
"a /dev/vchiq - - - - u:octoprint:rw";

systemd.services.octoprint = {
description = "OctoPrint, web interface for 3D printers";
Expand All @@ -120,10 +154,10 @@ in

preStart = ''
if [ -e "${cfg.stateDir}/config.yaml" ]; then
${pkgs.yaml-merge}/bin/yaml-merge "${cfg.stateDir}/config.yaml" "${cfgUpdate}" > "${cfg.stateDir}/config.yaml.tmp"
${pkgs.yaml-merge}/bin/yaml-merge "${cfg.stateDir}/config.yaml" "${configFile}" > "${cfg.stateDir}/config.yaml.tmp"
mv "${cfg.stateDir}/config.yaml.tmp" "${cfg.stateDir}/config.yaml"
else
cp "${cfgUpdate}" "${cfg.stateDir}/config.yaml"
cp "${configFile}" "${cfg.stateDir}/config.yaml"
chmod 600 "${cfg.stateDir}/config.yaml"
fi
'';
Expand All @@ -135,9 +169,41 @@ in
SupplementaryGroups = [
"dialout"
];

# Hardening
CapabilityBoundingSet = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProcSubset = "pid";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"@pkey"
];
ReadWritePaths = [ cfg.stateDir ];
UMask = "0077";

};
};

networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
};
meta.maintainers = with lib.maintainers; [ patrickdag ];
}
43 changes: 25 additions & 18 deletions nixos/tests/octoprint.nix
Original file line number Diff line number Diff line change
Expand Up @@ -35,33 +35,40 @@ import ./make-test-python.nix (
};

testScript = ''
import json
import json

@polling_condition
def octoprint_running():
machine.succeed("pgrep octoprint")
@polling_condition
def octoprint_running():
machine.succeed("pgrep octoprint")

with subtest("Wait for octoprint service to start"):
machine.wait_for_unit("octoprint.service")
machine.wait_until_succeeds("pgrep octoprint")
with subtest("Wait for octoprint service to start"):
machine.wait_for_unit("octoprint.service")
machine.wait_until_succeeds("pgrep octoprint")

with subtest("Wait for final boot"):
# this appears whe octoprint is almost finished starting
machine.wait_for_file("/var/lib/octoprint/uploads")
with subtest("Wait for final boot"):
# this appears whe octoprint is almost finished starting
machine.wait_for_file("/var/lib/octoprint/uploads")

# octoprint takes some time to start. This makes sure we'll retry just in case it takes longer
# retry-all-errors in necessary, since octoprint will report a 404 error when not yet ready
curl_cmd = "curl --retry-all-errors --connect-timeout 5 --max-time 10 --retry 5 --retry-delay 0 \
--retry-max-time 40 -X GET --header 'X-API-Key: ${apikey}' "
# octoprint takes some time to start. This makes sure we'll retry just in case it takes longer
# retry-all-errors in necessary, since octoprint will report a 404 error when not yet ready
curl_cmd = "curl --retry-all-errors --connect-timeout 5 --max-time 10 --retry 5 --retry-delay 0 \
--retry-max-time 40 -X GET --header 'X-API-Key: ${apikey}' "

# used to fail early, in case octoprint first starts and then crashes
with octoprint_running: # type: ignore[union-attr]
with subtest("Check for web interface"):
machine.wait_until_succeeds("curl -s localhost:5000")
machine.wait_until_succeeds("curl -s -4 localhost:5000")
machine.wait_until_succeeds("curl -s -6 localhost:5000")

with subtest("Check API"):
version = json.loads(machine.succeed(curl_cmd + "localhost:5000/api/version"))
server = json.loads(machine.succeed(curl_cmd + "localhost:5000/api/server"))
with subtest("Check API IPv4"):
version = json.loads(machine.succeed(curl_cmd + "-4 localhost:5000/api/version"))
server = json.loads(machine.succeed(curl_cmd + "-4 localhost:5000/api/server"))
assert version["server"] == str("${pkgs.octoprint.version}")
assert server["safemode"] == None

with subtest("Check API IPv6"):
version = json.loads(machine.succeed(curl_cmd + "-6 localhost:5000/api/version"))
server = json.loads(machine.succeed(curl_cmd + "-6 localhost:5000/api/server"))
assert version["server"] == str("${pkgs.octoprint.version}")
assert server["safemode"] == None
'';
Expand Down
22 changes: 0 additions & 22 deletions pkgs/by-name/oc/octoprint/ffmpeg-path.patch

This file was deleted.

8 changes: 1 addition & 7 deletions pkgs/by-name/oc/octoprint/package.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
pkgs,
stdenv,
callPackage,
lib,
Expand Down Expand Up @@ -197,12 +196,6 @@ let
src = ./pip-path.patch;
pip = "${self.pip}/bin/pip";
})

# hardcore path to ffmpeg and hide related settings
(substituteAll {
src = ./ffmpeg-path.patch;
ffmpeg = "${pkgs.ffmpeg}/bin/ffmpeg";
})
];

postPatch =
Expand Down Expand Up @@ -266,6 +259,7 @@ let
gebner
WhittlesJr
gador
patrickdag
];
};
};
Expand Down
Loading