···508508This blog post is already quite lengthy, so I will split it into separate parts.
509509There probably will be 3 of them:
510510511511-- <a href="#top">Part 1 - Basics, security, and FD passing (this one)</a>
512512-- Part 2 - Socket activation
511511+- [Part 1 - Basics, security, and FD passing (this one)](./#top)
512512+- [Part 2 - Socket activation](@/post/who-watches-watchmen-ii.md)
513513- Part 3 - Logging
+414
content/post/who-watches-watchmen-ii.md
···11++++
22+title = "Who Watches Watchmen? - Part 2"
33+date = 2023-07-04
44+55+description = """
66+Continuation of travel into making systemd to work for us, not against us. This
77+time we will talk about socket activation and how to make our application run
88+only when we need it to run.
99+"""
1010+1111+[taxonomies]
1212+tags = [
1313+ "elixir",
1414+ "programming",
1515+ "systemd",
1616+ "deployment"
1717+]
1818+1919+[[extra.thanks]]
2020+name = "Nicodemus"
2121+why = "Helping me with my poor English"
2222++++
2323+2424+This is continuation of [Part I][part-i] where I described the basics of the
2525+supervising BEAM applications with systemd and how to create basic, secure
2626+service for your Elixir application with it. In this article I will assume that
2727+you have read [the previous one][part-i].
2828+2929+______________________________________________________________________
3030+3131+We already have our super simple service description. Just to refresh your
3232+memory, it is the `hello.service` file once again:
3333+3434+```ini
3535+[Unit]
3636+Description=Hello World service
3737+Requires=network.target
3838+3939+[Service]
4040+Type=notify
4141+Environment=PORT=80
4242+ExecStart=/opt/hello/bin/hello start
4343+WatchdogSec=1min
4444+4545+# We need to add capability to be able to bind on port 80
4646+CapabilityBoundingSet=CAP_NET_BIND_SERVICE
4747+4848+# Hardening
4949+DynamicUser=true
5050+PrivateDevices=true
5151+Environment=ERL_CRASH_DUMP_SECONDS=0
5252+```
5353+5454+However there is one small problem. It allows our service to listen on **any**
5555+restricted port, not just `80` that we want to listen on. This can be
5656+troublesome as an attacker that gains RCE on our server can then capture any
5757+traffic on any port that we do not want to open (for example exposing port 22
5858+using the [`ssh`] module).
5959+6060+It would be nice if we could somehow inject sockets for only the ports we want
6161+to listen to into our application.
6262+6363+## Socket passing
6464+6565+Thanks to the [`systemd.socket`][systemd.socket] feature we can achieve that
6666+with a little work on our side.
6767+6868+First we need to create new unit named `hello.socket` next to our
6969+`hello.service`:
7070+7171+```ini
7272+[Unit]
7373+Description=Listening socket
7474+Requires=sockets.target
7575+7676+[Socket]
7777+ListenStream=80
7878+BindIPv6Only=both
7979+ReusePort=true
8080+NoDelay=true
8181+```
8282+8383+It will create a socket connected to TCP 80 (because we used `ListenStream=`,
8484+and TCP is the stream protocol). By default it will bind that socket to a
8585+service named the same as our socket, so now we need to edit our `hello.service`
8686+a little bit:
8787+8888+```ini
8989+[Unit]
9090+Description=Hello World service
9191+Requires=network.target
9292+9393+[Service]
9494+Type=notify
9595+Environment=PORT=80
9696+ExecStart=/opt/hello/bin/hello start
9797+WatchdogSec=1min
9898+9999+# See, we no longer need to insecurely allow binding to any port
100100+# CapabilityBoundingSet=CAP_NET_BIND_SERVICE
101101+102102+# Hardening
103103+DynamicUser=true
104104+PrivateDevices=true
105105+Environment=ERL_CRASH_DUMP_SECONDS=0
106106+```
107107+108108+And we need to modify our `Hello.Application.cowboy_opts/0` to handle the socket
109109+which is passed to us a file descriptor:
110110+111111+```elixir
112112+# hello/application.ex
113113+defmodule Hello.Application do
114114+ use Application
115115+116116+ def start(_type, _opts) do
117117+ fds = :systemd.listen_fds()
118118+119119+ children = [
120120+ {Plug.Cowboy, [scheme: :http, plug: Hello.Router] ++ cowboy_opts(fds)},
121121+ {Plug.Cowboy.Drainer, refs: :all}
122122+ ]
123123+124124+ Supervisor.start_link(children, strategy: :one_for_one)
125125+ end
126126+127127+ # If there are no sockets passed to the application, then start listening on
128128+ # the port specified by the `PORT` environment variable
129129+ defp cowboy_opts([]) do
130130+ [port: String.to_integer(System.get_env("PORT", "5000"))]
131131+ end
132132+133133+ # If there are any socket passed, then use first one
134134+ defp cowboy_opts([socket | _]) do
135135+ fd =
136136+ case socket do
137137+ # Sockets can be named, which will be passed as the second element in
138138+ # a tuple
139139+ {fd, _name} -> fd
140140+ # Or unnamed, and then it will be just the file descriptor
141141+ fd -> fd
142142+ end
143143+144144+ [
145145+ net: :inet6, # (1)
146146+ port: 0, # (2)
147147+ fd: fd # (3)
148148+ ]
149149+ end
150150+end
151151+```
152152+153153+1. Systemd sockets are IPv6 enabled (we explicitly said that we want to listen
154154+ on both). That means, that we need to mark our connection as an INET6
155155+ connection. This will not affect IPv4 (INET) connections.
156156+1. We are required to pass `:port` key, but its value will be ignored, so we
157157+ just pass `0`.
158158+1. We pass the file descriptor that will be then passed to the Cowboy listener.
159159+160160+Now when we will start our service:
161161+162162+```txt
163163+# systemctl start hello.service
164164+```
165165+166166+It will be available at `https://localhost/` while still running as an
167167+unprivileged user.
168168+169169+### Multiple ports
170170+171171+The question may arise - how to allow our service to listen on more than one
172172+port, for example you want to have your website available as HTTPS alongside
173173+"regular" HTTP. This means that our application needs to listen on two
174174+restricted ports:
175175+176176+- 80 - for HTTP
177177+- 443 - for HTTPS
178178+179179+Now we need to slightly modify a little our socket service and add another one.
180180+First rename our `hello.socket` to `hello-http.socket` and add a line
181181+`Service=hello.service` and `FileDescriptorName=http` to `[Socket]` section, so
182182+we end with:
183183+184184+```ini
185185+[Unit]
186186+Description=HTTP Socket
187187+Requires=sockets.target
188188+189189+[Socket]
190190+# We declare the name of the file descriptor here to simplify extraction in
191191+# the application afterwards. By default it will be the socket name (so
192192+# `hello-http` in our case), but `http` is much cleaner.
193193+FileDescriptorName=http
194194+ListenStream=80
195195+Service=hello.service
196196+BindIPv6Only=both
197197+ReusePort=true
198198+NoDelay=true
199199+```
200200+201201+Next we create a similar file, but for HTTPS named `hello-https.socket`
202202+203203+```ini
204204+[Unit]
205205+Description=HTTPS Socket
206206+Requires=sockets.target
207207+208208+[Socket]
209209+FileDescriptorName=https
210210+ListenStream=443
211211+Service=hello.service
212212+BindIPv6Only=both
213213+ReusePort=true
214214+NoDelay=true
215215+```
216216+217217+And we add the dependency on both of our sockets to the `hello.service`:
218218+219219+```ini
220220+[Unit]
221221+Description=Hello World service
222222+After=hello-http.socket hello-https.socket
223223+BindTo=hello-http.socket hello-https.socket
224224+225225+[Service]
226226+ExecStart=/opt/hello/bin/hello start
227227+228228+# Hardening
229229+DynamicUser=true
230230+PrivateDevices=true
231231+Environment=ERL_CRASH_DUMB_SECONDS=0
232232+```
233233+234234+Now we need to somehow differentiate between our sockets in the
235235+`Hello.Application`, so we will be able to pass the proper FD to each of the
236236+listeners. The `:systemd.listen_fds/0` will return a list of file descriptors,
237237+and if they are named, the format will be a 2-tuple where the first element is
238238+the file descriptor and the second is the name as a string:
239239+240240+```elixir
241241+# hello/application.ex
242242+defmodule Hello.Application do
243243+ use Application
244244+245245+ def start(_type, _opts) do
246246+ fds = :systemd.listen_fds()
247247+248248+ router = Hello.Router
249249+250250+ children = [
251251+ {Plug.Cowboy, [
252252+ scheme: :http,
253253+ plug: router
254254+ ] ++ cowboy_opts(fds, "http")},
255255+ {Plug.Cowboy, [
256256+ scheme: :https,
257257+ plug: router,
258258+ keyfile: "path/to/keyfile.pem",
259259+ certfile: "path/to/certfile.pem",
260260+ dhfile: "path/to/dhfile.pem"
261261+ ] ++ cowboy_opts(fds, "https")},
262262+ {Plug.Cowboy.Drainer, refs: :all}
263263+ ]
264264+265265+ Supervisor.start_link(children, strategy: :one_for_one)
266266+ end
267267+268268+ defp cowboy_opts(fds, protocol) do
269269+ case List.keyfind(fds, protocol, 1) do
270270+ # If there is socket passed for given protocol, then use that one
271271+ {fd, ^protocol} ->
272272+ [
273273+ net: :inet6,
274274+ port: 0,
275275+ fd: fd
276276+ ]
277277+278278+ # If there are no sockets passed to the application that match
279279+ # the protocol, then start listening on the port specified by
280280+ # `PORT_{protocol}` environment variable
281281+ _ ->
282282+ [
283283+ port: String.to_integer(System.get_env("PORT_#{protocol}", "5000"))
284284+ ]
285285+ end
286286+end
287287+```
288288+289289+Now our application will listen on both - HTTP and HTTPS, despite running as
290290+unprivileged user.
291291+292292+## Socket activation
293293+294294+Now, that we can inject sockets to our application with ease we can achieve even
295295+more fascinating feature - socket activation.
296296+297297+Some of you may used `inetd` in the past, that allows you to dynamically start
298298+processes on network requests. It is quite an interesting tool that detects
299299+traffic on certain ports, then spawns a new process to handle it, passing data
300300+to and from that process via `STDIN` and `STDOUT`. There was a quirk though, it
301301+required the spawned process to shutdown after it handled the request and it was
302302+starting a new instance for each request. That works poorly with VMs like BEAM
303303+that have substantial startup time and are expected to be long-running systems.
304304+BEAM is capable of handling network requests on it's own.
305305+306306+Fortunately for us, the way that we have implemented our systemd service is all
307307+that we need to have our application dynamically activated. To observe that we
308308+just need to shutdown everything:
309309+310310+```txt
311311+# systemctl stop hello-http.socket hello-https.socket hello.service
312312+```
313313+314314+And now relaunch **only the sockets**:
315315+316316+```txt
317317+# systemctl start hello-http.socket hello-https.socket
318318+```
319319+320320+We can check, that our service is not running:
321321+322322+```txt
323323+$ systemctl status hello.service
324324+● hello.service - Hello World service
325325+ Loaded: loaded (/usr/local/lib/systemd/system/hello.service; static; vendor preset: enabled)
326326+ Active: inactive (dead)
327327+TriggeredBy: ● hello-http.socket ● hello-https.socket
328328+```
329329+330330+We can see the `TriggeredBy` section that tells us, that this service will be
331331+started by one of the sockets listed there. Let see what will happen when we
332332+will try to request anything from our application:
333333+334334+```txt
335335+$ curl http://localhost/
336336+Hello World!
337337+```
338338+339339+You can see that we got a response from our application. This mean that our
340340+application must have started, and indeed when we check:
341341+342342+```txt
343343+$ systemctl status hello.service
344344+● hello.service - Hello
345345+ Loaded: loaded (/usr/local/lib/systemd/system/hello.service; static; vendor preset: enabled)
346346+ Active: active (running) since Thu 2022-02-03 13:20:27 CET; 4s ago
347347+TriggeredBy: ● hello-http.socket ● hello-https.socket
348348+ Main PID: 1106 (beam.smp)
349349+ Tasks: 19 (limit: 1136)
350350+ Memory: 116.7M
351351+ CGroup: /system.slice/hello.service
352352+ ├─1106 /opt/hello/erts-12.2/bin/beam.smp -- -root /opt/hello -progname erl -- -home /run/hello -- -noshell -s elixir start_cli -mode embedded -setcookie CR63SVI6L5JAMJSDL3H4XPNMOPHEWSV2FPHCHCAN65CY6ASHMXBA==== -sname hello -c>
353353+ └─1138 erl_child_setup 1024
354354+```
355355+356356+It seems to be running, and if we stop it, then we will get information that it
357357+still can be activated by our sockets:
358358+359359+```txt
360360+# systemctl stop hello.service
361361+Warning: Stopping hello.service, but it can still be activated by:
362362+ hello-http.socket hello-https.socket
363363+```
364364+365365+That means, that systemd is still listening on the sockets that we defined, even
366366+when our application is down, and will start our application again as soon as
367367+there are any incoming requests.
368368+369369+Let test that out again:
370370+371371+```txt
372372+$ curl http://localhost/
373373+Hello World!
374374+$ systemctl status hello.service
375375+● hello.service - Hello
376376+ Loaded: loaded (/usr/local/lib/systemd/system/hello.service; static; vendor preset: enabled)
377377+ Active: active (running) since Thu 2022-02-03 13:22:27 CET; 4s ago
378378+TriggeredBy: ● hello-http.socket ● hello-https.socket
379379+ Main PID: 3452 (beam.smp)
380380+ Tasks: 19 (limit: 1136)
381381+ Memory: 116.7M
382382+ CGroup: /system.slice/hello.service
383383+ ├─3452 /opt/hello/erts-12.2/bin/beam.smp -- -root /opt/hello -progname erl -- -home /run/hello -- -noshell -s elixir start_cli -mode embedded -setcookie CR63SVI6L5JAMJSDL3H4XPNMOPHEWSV2FPHCHCAN65CY6ASHMXBA==== -sname hello -c>
384384+ └─3453 erl_child_setup 1024
385385+```
386386+387387+Our application got launched again, automatically, just by the fact that
388388+there was incoming TCP connection.
389389+390390+Does it work for HTTPS connection as well?
391391+392392+```txt
393393+# systemctl stop hello.service
394394+$ curl -k https://localhost/
395395+Hello World!
396396+```
397397+398398+It seems so. Independently of which port we try to reach our application on, it
399399+will be automatically launched for us and the connection will be properly
400400+handled. Do note that systemd will not shut down our process after serving the
401401+request. It will continue to run from that point forward.
402402+403403+## Summary
404404+405405+I know that it took quite while since the last post (ca. 1.5 years), but I hope
406406+that I will be able to write the final part much sooner than this.
407407+408408+- [Part 1 - Basics, security, and FD passing][part-i]
409409+- [Part 2 - Socket activation (this one)](./#top)
410410+- Part 3 - Logging
411411+412412+[part-i]: @/post/who-watches-watchmen-i.md
413413+[systemd.socket]: https://www.freedesktop.org/software/systemd/man/systemd.socket.html
414414+[`ssh`]: https://erlang.org/doc/man/ssh.html