[GH-ISSUE #286] better systemd service file #208

Closed
opened 2026-02-25 23:33:42 +03:00 by kerem · 9 comments
Owner

Originally created by @beac0n on GitHub (Dec 31, 2020).
Original GitHub issue: https://github.com/go-shiori/shiori/issues/286

I've created my own systemd service file with the following security settings in the [service] section:

CapabilityBoundingSet=CAP_NET_BIND_SERVICE
RestrictNamespaces=true
NoNewPrivileges=true
PrivateTmp=true
ProtectClock=true
ProtectKernelModules=true
ProtectProc=noaccess
ProtectHostname=true
RestrictRealtime=true
SystemCallArchitectures=native

SystemCallFilter=~@clock
SystemCallFilter=~@debug
SystemCallFilter=~@module
SystemCallFilter=~@mount
SystemCallFilter=~@raw-io
SystemCallFilter=~@reboot
SystemCallFilter=~@privileged
SystemCallFilter=~@resources

SystemCallFilter=~@cpu-emulation
SystemCallFilter=~@obsolete

LockPersonality=true

MemoryDenyWriteExecute=true

UMask=0077

PrivateDevices=true
ProtectControlGroups=true
ProtectKernelTunables=true
ProtectSystem=full
RestrictSUIDSGID=true

I'm using the binary directly (so no docker).
I am also not using --portable (not on purpose, just forgot to add it).

I think when using --portable, ProtectHome=true can be added as well.

I did not found a .service file in the code, just a snippet in the wiki.
I created this issue, because I didn't want to add any changes in the wiki without prior discussion.

Originally created by @beac0n on GitHub (Dec 31, 2020). Original GitHub issue: https://github.com/go-shiori/shiori/issues/286 I've created my own systemd service file with the following security settings in the [service] section: ``` CapabilityBoundingSet=CAP_NET_BIND_SERVICE RestrictNamespaces=true NoNewPrivileges=true PrivateTmp=true ProtectClock=true ProtectKernelModules=true ProtectProc=noaccess ProtectHostname=true RestrictRealtime=true SystemCallArchitectures=native SystemCallFilter=~@clock SystemCallFilter=~@debug SystemCallFilter=~@module SystemCallFilter=~@mount SystemCallFilter=~@raw-io SystemCallFilter=~@reboot SystemCallFilter=~@privileged SystemCallFilter=~@resources SystemCallFilter=~@cpu-emulation SystemCallFilter=~@obsolete LockPersonality=true MemoryDenyWriteExecute=true UMask=0077 PrivateDevices=true ProtectControlGroups=true ProtectKernelTunables=true ProtectSystem=full RestrictSUIDSGID=true ``` I'm using the binary directly (so no docker). I am also not using --portable (not on purpose, just forgot to add it). I think when using --portable, `ProtectHome=true` can be added as well. I did not found a .service file in the code, just a snippet in the wiki. I created this issue, because I didn't want to add any changes in the wiki without prior discussion.
kerem closed this issue 2026-02-25 23:33:42 +03:00
Author
Owner

@minijackson commented on GitHub (Jan 2, 2021):

Hello! I maintain the shiori service over at NixOS, and I also wanted to upstream a "hardened" version of the systemd service.

Here is what I've got, without the NixOS specific options:

shiori.service
[Unit]
Description=Shiori simple bookmarks manager

[Service]
Environment="SHIORI_DIR=/var/lib/shiori"

CapabilityBoundingSet=
DeviceAllow=
DynamicUser=true
ExecStart=/usr/bin/shiori serve <options...>
LockPersonality=true
MemoryDenyWriteExecute=true
PrivateDevices=true
PrivateUsers=true
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
RestrictAddressFamilies=AF_INET
RestrictAddressFamilies=AF_INET6
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
StateDirectory=shiori
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
SystemCallFilter=@system-service
SystemCallFilter=~@chown
SystemCallFilter=~@cpu-emulation
SystemCallFilter=~@debug
SystemCallFilter=~@keyring
SystemCallFilter=~@memlock
SystemCallFilter=~@module
SystemCallFilter=~@obsolete
SystemCallFilter=~@privileged
SystemCallFilter=~@raw-io
SystemCallFilter=~@resources
SystemCallFilter=~@setuid

In the nixos module, shiori actually runs under a chroot (using RootDirectory), and bind mounts the necessary directories, but since I can't really assume the directory structure of other distributions, I removed it here.

All in all, it is quite close to what you proposed. Two main differences that I see:

  • you set a better UMask (I should be doing that ^^)
  • I'm using a combination of DynamicUser, StateDirectory, and the $SHIORI_DIR. With this, systemd can set ProtectSystem=strict, and shiori can only write inside /var/lib/shiori, without having to use the --portable option.

I'd be quite happy to have an upstream systemd file with security options to follow, even if just to have multiple people testing that it works without issue, and keeping up to date with the systemd options.

<!-- gh-comment-id:753486091 --> @minijackson commented on GitHub (Jan 2, 2021): Hello! I maintain the [shiori service](https://github.com/NixOS/nixpkgs/blob/aab78f0e8a730e5ca7393e6720975bd4f1b0d904/nixos/modules/services/web-apps/shiori.nix) over at NixOS, and I also wanted to upstream a "hardened" version of the systemd service. Here is what I've got, without the NixOS specific options: <details> <summary>shiori.service</summary> ```ini [Unit] Description=Shiori simple bookmarks manager [Service] Environment="SHIORI_DIR=/var/lib/shiori" CapabilityBoundingSet= DeviceAllow= DynamicUser=true ExecStart=/usr/bin/shiori serve <options...> LockPersonality=true MemoryDenyWriteExecute=true PrivateDevices=true PrivateUsers=true ProtectClock=true ProtectControlGroups=true ProtectHome=true ProtectHostname=true ProtectKernelLogs=true ProtectKernelModules=true ProtectKernelTunables=true RestrictAddressFamilies=AF_INET RestrictAddressFamilies=AF_INET6 RestrictNamespaces=true RestrictRealtime=true RestrictSUIDSGID=true StateDirectory=shiori SystemCallArchitectures=native SystemCallErrorNumber=EPERM SystemCallFilter=@system-service SystemCallFilter=~@chown SystemCallFilter=~@cpu-emulation SystemCallFilter=~@debug SystemCallFilter=~@keyring SystemCallFilter=~@memlock SystemCallFilter=~@module SystemCallFilter=~@obsolete SystemCallFilter=~@privileged SystemCallFilter=~@raw-io SystemCallFilter=~@resources SystemCallFilter=~@setuid ``` </details> In the nixos module, shiori actually runs under a chroot (using `RootDirectory`), and bind mounts the necessary directories, but since I can't really assume the directory structure of other distributions, I removed it here. All in all, it is quite close to what you proposed. Two main differences that I see: - you set a better UMask (I should be doing that ^^) - I'm using a combination of `DynamicUser`, `StateDirectory`, and the `$SHIORI_DIR`. With this, systemd can set `ProtectSystem=strict`, and shiori can only write inside `/var/lib/shiori`, without having to use the `--portable` option. I'd be quite happy to have an upstream systemd file with security options to follow, even if just to have multiple people testing that it works without issue, and keeping up to date with the systemd options.
Author
Owner

@beac0n commented on GitHub (Jan 2, 2021):

Didn't know about the SHIORI_DIR env var. I will try to merge your suggestion with mine and post the updated service file 👍

<!-- gh-comment-id:753507209 --> @beac0n commented on GitHub (Jan 2, 2021): Didn't know about the SHIORI_DIR env var. I will try to merge your suggestion with mine and post the updated service file 👍
Author
Owner

@beac0n commented on GitHub (Jan 3, 2021):

shiori.service

[Unit]
Description=shiori service
Requires=network-online.target
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/shiori serve
Restart=always
User=shiori
Group=shiori

Environment="SHIORI_DIR=/var/lib/shiori"
DynamicUser=true
PrivateUsers=true
ProtectHome=true
ProtectKernelLogs=true
RestrictAddressFamilies=AF_INET AF_INET6
StateDirectory=shiori
SystemCallErrorNumber=EPERM
SystemCallFilter=@system-service
SystemCallFilter=~@chown
SystemCallFilter=~@keyring
SystemCallFilter=~@memlock
SystemCallFilter=~@setuid
DeviceAllow=

CapabilityBoundingSet=CAP_NET_BIND_SERVICE
LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
ProtectControlGroups=true
ProtectKernelTunables=true
ProtectSystem=full
ProtectClock=true
ProtectKernelModules=true
ProtectProc=noaccess
ProtectHostname=true
ProcSubset=pid
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
SystemCallFilter=~@clock
SystemCallFilter=~@debug
SystemCallFilter=~@module
SystemCallFilter=~@mount
SystemCallFilter=~@raw-io
SystemCallFilter=~@reboot
SystemCallFilter=~@privileged
SystemCallFilter=~@resources
SystemCallFilter=~@cpu-emulation
SystemCallFilter=~@obsolete
UMask=0077

[Install]
WantedBy=multi-user.target

The paragraph between Group=shiori and CapabilityBoundingSet=CAP_NET_BIND_SERVICE are the settings, not included in my service file, but included in the service file provided by @minijackson

I tested it on my instance with arch linux, and it works great.
I also tested if adding a new bookmark and creating an archive works 👍

Result of

systemd-analyze security shiori.service

→ Overall exposure level for shiori.service: 1.2 OK 🙂

Which is pretty good :-)

Edit: @minijackson your CapabilityBoundingSet= is empty. May I ask why? Does the service start with no capabilities? Afaik shiori starts a web service on some port. Therefore it needs CAP_NET_BIND_SERVICE ?

Edit 2:
We could also add

IPAddressAllow=localhost
IPAddressDeny=any

if shiori is behind a reverse proxy like nginx.
This does not seem to work. I get the following error when creating a bookmark: failed to save bookmark: title must not be empty (500)

<!-- gh-comment-id:753647414 --> @beac0n commented on GitHub (Jan 3, 2021): <details><summary>shiori.service</summary> <p> ```ini [Unit] Description=shiori service Requires=network-online.target After=network-online.target [Service] Type=simple ExecStart=/usr/bin/shiori serve Restart=always User=shiori Group=shiori Environment="SHIORI_DIR=/var/lib/shiori" DynamicUser=true PrivateUsers=true ProtectHome=true ProtectKernelLogs=true RestrictAddressFamilies=AF_INET AF_INET6 StateDirectory=shiori SystemCallErrorNumber=EPERM SystemCallFilter=@system-service SystemCallFilter=~@chown SystemCallFilter=~@keyring SystemCallFilter=~@memlock SystemCallFilter=~@setuid DeviceAllow= CapabilityBoundingSet=CAP_NET_BIND_SERVICE LockPersonality=true MemoryDenyWriteExecute=true NoNewPrivileges=true PrivateDevices=true PrivateTmp=true ProtectControlGroups=true ProtectKernelTunables=true ProtectSystem=full ProtectClock=true ProtectKernelModules=true ProtectProc=noaccess ProtectHostname=true ProcSubset=pid RestrictNamespaces=true RestrictRealtime=true RestrictSUIDSGID=true SystemCallArchitectures=native SystemCallFilter=~@clock SystemCallFilter=~@debug SystemCallFilter=~@module SystemCallFilter=~@mount SystemCallFilter=~@raw-io SystemCallFilter=~@reboot SystemCallFilter=~@privileged SystemCallFilter=~@resources SystemCallFilter=~@cpu-emulation SystemCallFilter=~@obsolete UMask=0077 [Install] WantedBy=multi-user.target ``` </p></details> The paragraph between `Group=shiori` and `CapabilityBoundingSet=CAP_NET_BIND_SERVICE` are the settings, *not* included in my service file, but included in the service file provided by @minijackson I tested it on my instance with arch linux, and it works great. I also tested if adding a new bookmark and creating an archive works :+1: Result of ```bash systemd-analyze security shiori.service ``` → Overall exposure level for shiori.service: 1.2 OK 🙂 Which is pretty good :-) Edit: @minijackson your `CapabilityBoundingSet=` is empty. May I ask why? Does the service start with no capabilities? Afaik shiori starts a web service on some port. Therefore it needs `CAP_NET_BIND_SERVICE` ? Edit 2: We could also add ```ini IPAddressAllow=localhost IPAddressDeny=any ``` if shiori is behind a reverse proxy like nginx. This does not seem to work. I get the following error when creating a bookmark: `failed to save bookmark: title must not be empty (500)`
Author
Owner

@minijackson commented on GitHub (Jan 10, 2021):

your CapabilityBoundingSet= is empty. May I ask why?

So from man 7 capabilities: "Bind a socket to Internet domain privileged ports (port numbers less than 1024).". Since on my server (and I believe most servers but I could be wrong), it uses the 8080 port, it does not need the capability.

I'm not sure we can add IPAddressDeny=any, doesn't shiori need to access the global network to fetch articles? I could have misunderstood the option, though. Have you tested this?

<!-- gh-comment-id:757532889 --> @minijackson commented on GitHub (Jan 10, 2021): > your `CapabilityBoundingSet=` is empty. May I ask why? So from `man 7 capabilities`: "Bind a socket to Internet domain privileged ports (port numbers less than 1024).". Since on my server (and I believe most servers but I could be wrong), it uses the 8080 port, it does not need the capability. I'm not sure we can add `IPAddressDeny=any`, doesn't shiori need to access the global network to fetch articles? I could have misunderstood the option, though. Have you tested this?
Author
Owner

@beac0n commented on GitHub (Jan 10, 2021):

Have you tested this?

Yes. Doesn't work, as stated in my last post:

This does not seem to work. I get the following error when creating a bookmark: failed to save bookmark: title must not be empty (500)
<!-- gh-comment-id:757533269 --> @beac0n commented on GitHub (Jan 10, 2021): ``` Have you tested this? ``` Yes. Doesn't work, as stated in my last post: ``` This does not seem to work. I get the following error when creating a bookmark: failed to save bookmark: title must not be empty (500) ```
Author
Owner

@minijackson commented on GitHub (Jan 10, 2021):

Oh sorry I missed that part...

<!-- gh-comment-id:757533926 --> @minijackson commented on GitHub (Jan 10, 2021): Oh sorry I missed that part...
Author
Owner

@beac0n commented on GitHub (Jan 11, 2021):

Final version:

shiori.service

[Unit]
Description=shiori service
Requires=network-online.target
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/shiori serve
Restart=always
User=shiori
Group=shiori

Environment="SHIORI_DIR=/var/lib/shiori"
DynamicUser=true
PrivateUsers=true
ProtectHome=true
ProtectKernelLogs=true
RestrictAddressFamilies=AF_INET AF_INET6
StateDirectory=shiori
SystemCallErrorNumber=EPERM
SystemCallFilter=@system-service
SystemCallFilter=~@chown
SystemCallFilter=~@keyring
SystemCallFilter=~@memlock
SystemCallFilter=~@setuid
DeviceAllow=

CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
ProtectControlGroups=true
ProtectKernelTunables=true
ProtectSystem=full
ProtectClock=true
ProtectKernelModules=true
ProtectProc=noaccess
ProtectHostname=true
ProcSubset=pid
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
SystemCallFilter=~@clock
SystemCallFilter=~@debug
SystemCallFilter=~@module
SystemCallFilter=~@mount
SystemCallFilter=~@raw-io
SystemCallFilter=~@reboot
SystemCallFilter=~@privileged
SystemCallFilter=~@resources
SystemCallFilter=~@cpu-emulation
SystemCallFilter=~@obsolete
UMask=0077

[Install]
WantedBy=multi-user.target

What's the best way to solve this issue? Where do I have to put the service file? Just add it to the wiki?

<!-- gh-comment-id:758167463 --> @beac0n commented on GitHub (Jan 11, 2021): Final version: <details><summary>shiori.service</summary> <p> ```ini [Unit] Description=shiori service Requires=network-online.target After=network-online.target [Service] Type=simple ExecStart=/usr/bin/shiori serve Restart=always User=shiori Group=shiori Environment="SHIORI_DIR=/var/lib/shiori" DynamicUser=true PrivateUsers=true ProtectHome=true ProtectKernelLogs=true RestrictAddressFamilies=AF_INET AF_INET6 StateDirectory=shiori SystemCallErrorNumber=EPERM SystemCallFilter=@system-service SystemCallFilter=~@chown SystemCallFilter=~@keyring SystemCallFilter=~@memlock SystemCallFilter=~@setuid DeviceAllow= CapabilityBoundingSet= LockPersonality=true MemoryDenyWriteExecute=true NoNewPrivileges=true PrivateDevices=true PrivateTmp=true ProtectControlGroups=true ProtectKernelTunables=true ProtectSystem=full ProtectClock=true ProtectKernelModules=true ProtectProc=noaccess ProtectHostname=true ProcSubset=pid RestrictNamespaces=true RestrictRealtime=true RestrictSUIDSGID=true SystemCallArchitectures=native SystemCallFilter=~@clock SystemCallFilter=~@debug SystemCallFilter=~@module SystemCallFilter=~@mount SystemCallFilter=~@raw-io SystemCallFilter=~@reboot SystemCallFilter=~@privileged SystemCallFilter=~@resources SystemCallFilter=~@cpu-emulation SystemCallFilter=~@obsolete UMask=0077 [Install] WantedBy=multi-user.target ``` </p></details> What's the best way to solve this issue? Where do I have to put the service file? Just add it to the wiki?
Author
Owner

@axelsimon commented on GitHub (Mar 5, 2021):

You could just edit the wiki to modify the suggested systemd unit file, but really, this is something that should be part of the repo itself. You could try to create a Pull Request and hopefully it will get merged!

<!-- gh-comment-id:791046261 --> @axelsimon commented on GitHub (Mar 5, 2021): You could just [edit the wiki](https://github.com/go-shiori/shiori/wiki/Frequently-Asked-Question/_edit) to modify the suggested systemd unit file, but really, this is something that should be part of the repo itself. You could try to create a Pull Request and hopefully it will get merged!
Author
Owner

@beac0n commented on GitHub (Mar 19, 2021):

I put it in the wiki as @axelsimon suggested.

<!-- gh-comment-id:803070910 --> @beac0n commented on GitHub (Mar 19, 2021): I put it in the wiki as @axelsimon suggested.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/shiori#208
No description provided.