Compare commits

...

17 Commits

Author SHA1 Message Date
Tito Sacchi bb5f6fe2f3 Fix broken link to unipv website 2024-03-29 23:34:34 +01:00
Tito Sacchi 805977c4c5 New blogpost on ZFS dataset unlocking w/ YubiKeys 2024-03-29 23:33:19 +01:00
Tito Sacchi ce2e5ffb14 Do not remove .well-known on deploy 2024-02-18 18:52:59 +01:00
Tito Sacchi ca9df83ec9 Update info about me 2024-02-18 18:49:37 +01:00
Tito Sacchi 1ffd39d441 Automazione build con Dockerfile 2024-02-18 13:01:34 +01:00
Tito Sacchi e2d4f66ee9 Sistemazione setup Haskell + Docker 2023-12-28 21:50:52 +01:00
Tito Sacchi 17b685b93c TeamItaly and Olimat :] 2023-07-06 17:25:01 +02:00
Tito Sacchi 34b36e46a1 Oyes non so scrivere in LaTeX 2023-07-06 12:42:32 +02:00
Tito Sacchi aeadeae27c Sono in TeamItaly \o/
e apparentemente la gente continua a prendermi in giro per la
mansione di penetration tester
2023-07-06 10:05:27 +02:00
Tito Sacchi 6033d6a1ed Apparently ho finito il liceo 2023-07-06 10:04:01 +02:00
Tito Sacchi 7b2aa77a6f Update SCSS stylesheet 2023-02-15 18:59:14 +01:00
Tito Sacchi c39834de77 ho passato l'anno o/
(e imparato ad usare Proxmox)
2022-07-07 10:39:16 +02:00
Tito Sacchi 26fe13d440 New blogpost \o/ on CD with GitHub Webhooks 2022-06-20 16:24:56 +02:00
Tito Sacchi 298989eab0 Olicyber 2022 o/ (primo posto assoluto!) 2022-06-07 16:07:35 +02:00
Tito Sacchi 01e3584343 Olimate 2021/2022 (gara a squadre) :] 2022-05-12 09:15:51 +02:00
Tito Sacchi 60c53ae784 New skills o/
(and beautify site.hs)
2022-03-20 11:25:13 +01:00
Tito Sacchi bbd6a50726 I managed to dual boot pfSense and Arch \o/ 2022-01-11 21:48:38 +01:00
15 changed files with 1148 additions and 121 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
_site
_cache
Dockerfile
.dockerignore

31
Dockerfile Normal file
View File

@ -0,0 +1,31 @@
FROM haskell:9.6-slim AS haskell-builder
WORKDIR /opt/site
RUN cabal update
COPY ./Site.cabal /opt/site/Site.cabal
RUN cabal build --only-dependencies -j
COPY ./site.hs /opt/site/site.hs
RUN cabal install
RUN mv $(readlink -f /root/.local/bin/site) /opt/site/site
FROM debian:bookworm AS site-env
WORKDIR /opt/site
RUN apt update && apt install -y texlive texlive-luatex texlive-latex-extra texlive-lang-italian latexmk curl git
RUN mkdir -p /usr/share/fonts/opentype/alegreya-sans && \
curl -fsSL -o - https://github.com/huertatipografica/Alegreya-Sans/archive/refs/tags/v2.008.tar.gz | \
tar --strip-components 3 -C /usr/share/fonts/opentype/alegreya-sans -xzf - Alegreya-Sans-2.008/fonts/otf/
COPY --from=haskell-builder /opt/site/site /opt/site/site
RUN curl -fsSL https://github.com/sass/dart-sass/releases/download/1.71.0/dart-sass-1.71.0-linux-x64.tar.gz | tar xz -C /opt
ENV PATH="${PATH}:/opt/dart-sass"
ENV LANG=C.utf8
ENV LANGUAGE=C.utf8
ENV LC_ALL=C.utf8
ENTRYPOINT ["/opt/site/site"]
FROM site-env AS site-builder
WORKDIR /opt/site
COPY . /opt/site
RUN ["/opt/site/site", "build"]
FROM scratch AS site
COPY --from=site-builder /opt/site/_site/ /

2
Setup.hs Normal file
View File

@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain

16
Site.cabal Normal file
View File

@ -0,0 +1,16 @@
name: Site
version: 1.0
synopsis: Site builder with Hakyll
author: Tito Sacchi
build-type: Simple
executable site
build-depends: hakyll >= 4.16 && < 5
, base >= 4 && < 5
, mtl >= 2 && < 3
, filepath >= 1.4 && < 2
, process >= 1.6 && < 2
, pandoc >= 3.1 && < 4
main-is: site.hs
hs-source-dirs: .
default-language: Haskell2010

View File

@ -1,12 +1,17 @@
% vim: et:ts=2:sts=2:sw=2
\documentclass[parskip=half,oneside]{scrartcl}
\documentclass[parskip=half,oneside,usegeometry]{scrartcl}
\usepackage[italian]{babel}
% \PassOptionsToPackage{hyphens}{url}
\usepackage[pdfusetitle,colorlinks=true,urlcolor=blue]{hyperref}
\usepackage[dvipsnames]{xcolor}
% \usepackage[showframe,pass]{geometry}
\usepackage{fontspec}
\usepackage{tabularx}
\usepackage{textcomp}
\usepackage{makecell}
\usepackage{eso-pic}
\usepackage{geometry}
% \usepackage{showframe}
\AddToShipoutPictureFG{
\AtPageUpperLeft{%
\raisebox{-\height}{%
@ -104,46 +109,80 @@
\end{tabularx}
\section{Presentazione}
Frequento il liceo scientifico e nutro un forte interesse verso l'informatica --
specialmente la sicurezza e la programmazione funzionale -- e la matematica.
Cerco opportunit\`a di svolgere stage presso aziende del settore nell'ambito del
progetto di alternanza scuola-lavoro (PCTO) previsto nel triennio della
formazione superiore.
Sono interessato principalmente alla matematica e all'informatica.
Mi piace particolarmente ciò che si trova agli estremi: la sicurezza
informatica (low level) e la programmazione funzionale (high level e astratta a
tal punto da diventare matematica).
\section{Istruzione e formazione}
\begin{tabularx}{\textwidth}{p{0.2\textwidth}|l}
\textsf{09/2015 -- 06/2018} &
\hfill\textsf{09/2023 -- Oggi} &
\makecell[Xt]{\hphantom{}%
{\usekomafont{subsection}Licenza di scuola media (secondaria di I grado)}\\
\textit{Scuola superiore di I grado ``S. Boezio''}\\
\\
Indirizzo: Via C. Simonetta, 19, 27100 Pavia (Italia)\\
\small{\url{https://icacerbi.edu.it/scuole-secondarie-i-grado/secondaria-boezio}}\\
Voto finale: 10/10 con lode\\
\vspace{0.5\baselineskip}
{\usekomafont{subsection}Corso di Laurea triennale in Matematica}\\
\textit{Università di Pavia}\\
Allievo della \href{https://www.iusspavia.it}{Scuola Universitaria Superiore IUSS}\\
Allievo del \href{https://www.ghislieri.it/collegio}{Collegio Ghislieri}\hfill
\vspace{5pt}\break
{\url{https://matematica.unipv.it}}\\
Via A. Ferrata, 5, 27100~Pavia~(Italia)\\
\vspace{0.8\baselineskip}
} \\
\textsf{09/2018 -- Attuale} &
\hfill\textsf{09/2018 -- 06/2023} &
\makecell[Xt]{\hphantom{}%
{\usekomafont{subsection}Liceo scientifico (ammissione alla classe IV)}\\
\textit{Liceo scientifico ``T. Taramelli''}\\
\\
Indirizzo: Via L. Mascheroni, 53, 27100 Pavia (Italia)\\
\small{\url{https://www.istaramellifoscolo.edu.it}}\\
Media attuale delle valutazioni: 9,5
{\usekomafont{subsection}Diploma di maturità scientifica}\\
\textit{Liceo scientifico ``T. Taramelli''}\hfill
\vspace{5pt}\break
Valutazione finale: 100/100 con lode\\
{\url{https://www.istaramellifoscolo.edu.it}}\\
Via L. Mascheroni, 53, 27100~Pavia~(Italia)\\
\vspace{0.8\baselineskip}
} \\
\hfill\textsf{07/2022} &
\makecell[Xt]{\hphantom{}%
{\usekomafont{subsection}115° Corso di Orientamento}\\
\textit{Scuola Normale Superiore} (selezione e organizzazione)\hfill
\vspace{5pt}\break
{\url{https://www.sns.it/it/orientamento-universitario}}\\
Presso Accademia Nazionale dei Lincei, Via della Lungara, 10,
00165~Roma~(Italia)\\
}
% \textsf{09/2015 -- 06/2018} &
% \makecell[Xt]{\hphantom{}%
% {\usekomafont{subsection}Licenza di scuola media (secondaria di I grado)}\\
% \textit{Scuola superiore di I grado ``S. Boezio''}\\
% \\
% Indirizzo: Via C. Simonetta, 19, 27100 Pavia (Italia)\\
% \small{\url{https://icacerbi.edu.it/scuole-secondarie-i-grado/secondaria-boezio}}\\
% Voto finale: 10/10 con lode
% } \\
\end{tabularx}
\section{Esperienza lavorativa}
\begin{tabularx}{\textwidth}{p{0.2\textwidth}|l}
\hfill\textsf{09/2022 -- Attuale} &
\makecell[Xt]{\hphantom{}%
{\usekomafont{subsection}Pentester / Red team operator}\\
\texttt{[undisclosed]}\\
} \\
\end{tabularx}
\section{Competenze}
\subsubsection{Framework e linguaggi di programmazione}
Haskell; C/C++; Rust; Python; SQL; Assembly x86; Coq; Shell UNIX; JavaScript/React
\subsection{Framework e linguaggi di programmazione}
Haskell; C/C++; Rust; Clojure; Python; SQL; Assembly x86; Coq; Shell UNIX; JavaScript/React
\subsubsection{Sysadmin}
Docker; systemd; server OpenVPN, WireGuard, NGINX, PostgreSQL; networking e firewall setup su Linux
\subsection{Sysadmin e infrastruttura}
Docker, Podman; systemd; RHEL administration; Ansible/AWX; K8s; server OpenVPN,
WireGuard, NGINX, PostgreSQL; networking e firewall setup su Linux (iptables,
nftables, tc); pfSense/OPNsense; Grafana, Telegraf, InfluxDB; Proxmox VE,
VMware ESXi; ZFS
\subsubsection{DevOps}
Git; CI/CD; Docker
\subsection{DevOps}
Git; CI/CD; Docker, Podman
\subsubsection{Software}
\subsection{Software}
Wolfram Mathematica; MATLAB; Adobe Photoshop; QGIS/PostGIS; \LaTeX
\section{Pubblicazioni}
@ -153,6 +192,7 @@ Wolfram Mathematica; MATLAB; Adobe Photoshop; QGIS/PostGIS; \LaTeX
Progetto in compartecipazione con il prof. Giuseppe Camerini volto a documentare
la distribuzione e la biologia delle specie di lampiridi sul territorio
italiano, analogamente a quanto svolto in altri Paesi da associazioni analoghe.
Mi occupo della gestione tecnica e della creazione del sito web; ho partecipato
alla stesura dei testi divulgativi; alcune delle fotografie (artistiche e
documentative) pubblicate sul sito sono miei scatti.
@ -177,6 +217,15 @@ supportate dalla GNU Scientific Library. Ho interfacciato il software con
Wolfram Mathematica per ease-of-use da parte degli altri autori.
\section{Progetti}
\subsection{TeamItaly (2022)}
Nel 2022 sono stato parte di \href{https://teamitaly.eu}{TeamItaly}, la
squadra nazionale italiana di ethical hacking e Capture The Flag. Con TeamItaly
ho partecipato ad \href{https://www.ecsc2022.eu}{ECSC 2022}, la competizione
europea di sicurezza informatica supportata da ENISA, svoltasi a Vienna
(14-15/09/2022).
Siamo arrivati quarti nella classifica finale tra CTF Jeopardy e A/D.
\subsection{CyberChallenge.IT 2021}
Ho partecipato al progetto \href{https://cyberchallenge.it}{CyberChallenge.IT}
proposto dal CINI nell'edizione del 2021, rivolto a studenti universitari e
@ -194,21 +243,61 @@ deployment su container e macchine virtuali degli strumenti e della configurazio
rete).
La nostra squadra si \`e classificata quarta.
CyberChallenge mi ha lasciato un acceso interesse verso le competizioni CTF,
a cui partecipo spesso con la squadra del Politecnico di Milano (Tower of
Hanoi). Mi piace particolarmente la sicurezza dei binari e la crittografia (per
il suo legame con l'algebra astratta).
% CyberChallenge mi ha lasciato un acceso interesse verso le competizioni CTF,
% a cui partecipo spesso con la squadra del Politecnico di Milano
% (\href{https://toh.necst.it}{Tower of Hanoi}).
% Mi piace particolarmente la sicurezza dei binari e la crittografia (per
% il suo legame con l'algebra astratta).
\subsection{Olimpiadi di Cybersecurity (prima edizione, 2021)}
In quanto studente delle scuole superiori partecipante a CyberChallenge.IT, sono
stato invitato a prendere parte il 12/06/2021 alla competizione nazionale della prima edizione
delle Olimpiadi di Cybersecurity organizzate dal CINI, classificandomi al
secondo posto (\url{https://olicyber.it/nazionale}). Questa prima edizione ha
coinvolto 182 istituti superiori federati e 1150 studenti.
La competizione ha avuto una forte risonanza mediatica con diffusione sulla
stampa nazionale%
\footnote{\url{https://www.wired.it/attualita/scuola/2021/06/15/cybersecurity-olimpiadi-italia-vincitori/}}%
\footnote{\url{https://www.repubblica.it/tecnologia/2021/06/15/news/olicyber_ecco_il_medagliere_delle_prime_olimpiadi_italiane_di_cybersicurezza-306190140/}}.
\subsection{Olimpiadi di Cybersecurity (2021, 2022)}
% Ho preso parte alla competizione nazionale in entrambe le edizioni delle
% Olimpiadi di Cybersecurity organizzate dal CINI, classificandomi al secondo
% posto nel 2021 e al primo posto assoluto nel 2022
% (\url{https://olicyber.it/nazionale}). Entrambe le edizioni hanno coinvolto in
% tutto più di 1000 iscritti, da oltre 300 scuole federate nel 2022. La finali
% nazionali hanno avuto una forte risonanza mediatica con diffusione sulla stampa
% nazionale%
Ho preso parte ad entrambe le edizioni delle \href{https://olicyber.it}{Olimpiadi di Cybersecurity}
organizzate dal CINI, un progetto rivolto agli studenti delle scuole superiori
che vengono selezionati per partecipare ad una finale nazionale individuale in
stile CTF Jeopardy.
Ho ottenuto la medaglia d'oro in entrambe le edizioni, classificandomi al
secondo posto nel 2021 ed al primo posto assoluto nella competizione svoltasi
al campus ONU ITCILO di Torino nel 2022 (\url{https://olicyber.it/nazionale}).
Le finali nazionali hanno avuto una forte risonanza mediatica con diffusione sulla stampa nazionale e locale
(%
2022:
\href{https://www.cybersecitalia.it/tito-sacchi-17-anni-1-posto-olimpiadi-italiane-cyber-sono-un-hacker-buono-sogno-lacn/19519/}{CyberSecurity~Italia},
\href{https://www.repubblica.it/tecnologia/2022/05/30/news/sicurezza_informatica_quando_il_gioco_e_una_cosa_seria-351812332/}{La~Repubblica},
\href{https://www.repubblica.it/tecnologia/2022/05/30/news/sicurezza_informatica_quando_il_gioco_e_una_cosa_seria-351812332/}{Wired},
\href{https://laprovinciapavese.gelocal.it/pavia/cronaca/2022/06/02/news/il-campione-italiano-della-cybersecurity-e-tito-sacchi-17enne-studente-del-taramelli-di-pavia-1.41484632}{La Provincia Pavese};
2021:
\href{https://www.repubblica.it/tecnologia/2021/06/15/news/olicyber_ecco_il_medagliere_delle_prime_olimpiadi_italiane_di_cybersicurezza-306190140/}{La~Repubblica},
\href{https://www.wired.it/attualita/scuola/2021/06/15/cybersecurity-olimpiadi-italia-vincitori/}{Wired}%
).
%
Entrambe le edizioni hanno coinvolto in
tutto più di 1000 iscritti, da oltre 300 diverse scuole federate nel 2022.
\subsection{Olimpiadi della Matematica (2023)}
Ho ottenuto una medaglia di bronzo alla finale nazionale delle Olimpiadi di
Matematica, svoltasi a Cesenatico il 05/05/2023, dopo aver passato con successo
la gara provinciale.
Con la squadra del mio istituto ho sempre partecipato alle gare a squadre e
negli ultimi tre anni del liceo siamo sempre arrivati alla finale,
classificandoci al 36° (2021), 18° (2022) e 24° posto (2023).
% Faccio parte della squadra delle Olimpiadi della Matematica del mio Liceo sin
% dal primo anno. Nei quattro anni in cui ho partecipato ci siamo qualificati
% sempre alle semifinali nazionali e nelle edizioni 2020/2021 e 2021/2022 abbiamo
% preso parte alla finale a squadre, classificandoci rispettivamente 36esimi e
% 18esimi.
% YAAS abbiamo spaccato a Cese :]
% -- 12/05/2022
\section{Hobby e interessi}
\subsection{Matematica}
@ -222,19 +311,14 @@ all'informatica poiché mostrano interessanti parallelismi con linguaggi
puramente funzionali di collocazione sia accademica sia applicativa come Haskell
e con proof assistant come Coq e Agda.
Anche riguardo questi interessi più astratti mi piace imparare applicando: ho
sviluppato una parziale formalizzazione nel proof assistant Coq di alcune
strutture condivise tra teoria delle categorie e fisica introdotte inizialmente
da B. Coecke e S. Abramsky dell'Università di Oxford
(\href{https://doi.org/10.1109/LICS.2004.1319636}{\textsf{DOI:10.1109/LICS.2004.1319636}})
e poi ampiamente estese da altri ricercatori negli anni
successivi. I miei file sono reperibili al repository
\url{https://github.com/jabberabbe/CatQM}.
Faccio parte della squadra delle Olimpiadi della Matematica del mio Liceo sin dal
primo anno. Nei tre anni in cui ho partecipato ci siamo qualificati sempre alle
semifinali nazionali e nell'edizione 2020/2021 abbiamo preso parte alla finale a
squadre (36esimo posto).
% Anche riguardo questi interessi più astratti mi piace imparare applicando: ho
% sviluppato una parziale formalizzazione nel proof assistant Coq di alcune
% strutture condivise tra teoria delle categorie e fisica introdotte inizialmente
% da B. Coecke e S. Abramsky dell'Università di Oxford
% (\href{https://doi.org/10.1109/LICS.2004.1319636}{\textsf{DOI:10.1109/LICS.2004.1319636}})
% e poi ampiamente estese da altri ricercatori negli anni
% successivi. I miei file sono reperibili al repository
% \url{https://github.com/jabberabbe/CatQM}.
\subsection{Informatica}
Parallelamente alla matematica, da tempo mi dedico all'informatica da più punti
@ -245,20 +329,7 @@ collaborare allo sviluppo del compilatore Haskell
(\href{https://gitlab.haskell.org/ghc/ghc}{GHC}), sfruttando l'accogliente
community di contributor e mentor che circonda il progetto. Mi è sempre piaciuto
esplorare il funzionamento interno di ogni linguaggio, sistema operativo e
software che uso e per questo un'altra mia prospettiva, ora che sono stato
introdotto alla cybersecurity, è quella di partecipare a programmi di bug
bounty.
\subsection{Musica}
Suono il pianoforte da oltre dieci anni e di recente ho iniziato a studiare la
chitarra elettrica, per ampliare il mio background di musicista classico.
Tra il repertorio classico ho una particolare preferenza per il periodo
romantico.
\subsection{Sport}
Ho praticato nuoto agonistico per quattro stagioni (2015-2019) e sono salito sul
podio di svariate competizioni regionali con la mia squadra; ora pratico canoa a
livello amatoriale presso il Centro Universitario Sportivo di Pavia.
software che uso e mi sto dedicando al red teaming.
\subsection{Fotografia}
Negli ultimi anni ho sviluppato un particolare interesse per la fotografia, sia

View File

@ -5,33 +5,20 @@ showtitle: true
---
I'm Tito, alias "tauroh". I'm a high school student living in Pavia, Italy. I
was born in 2004. I have many more interests than I'm able to pursue and I still
have to decide what to do after high school. Unless I undergo a sudden change in
personality and hobbies, it will be something science-related. I'm quite a geek;
I'm into maths and pretty much everything that has to do with computers. I used
to play the piano regularly until last year but I don't have enough free time to
study music seriously right now.
I'm Tito, alias "tauroh". I'm studying for a bachelor's degree in Maths at the
University of Pavia, in Italy. I'm into maths and pretty much everything that
has to do with computers, except front-end development.
## Maths
I have been fascinated by maths since I was a child, and my parents taught me
where the elegance that some people (like them, and like me) see in mathematics
really resides. Abstraction and formal logic are in my opinion the most advanced
capabilities of the human mind -- the latest that evolution gave us from a
biological perspective. When you get to really appreciate maths, you discover a
brand new world. Unfortunately, that's something that school is not able to
teach -- that's why many people still think that maths is made of formulas,
exercises and grades.
When you get to really appreciate maths, you discover a brand new world.
Unfortunately, that's something that school is not able to teach -- that's why
many people still think that maths is made of formulas, exercises and grades.
As you might have understood, I like pure mathematics, and specifically the most
As you might have understood, I like pure mathematics, and specifically its most
abstract and foundational aspects, often related to logic and philosophy:
[category theory][ct], [abstract algebra][abstract-algebra], [type
theory][type-theory], [model structures][simplicial-sets]... Problem is, you're
supposed to study a lot of undergraduate mathematics before proceeding with this
areas. I haven't done that (yet); my mathematical background is fragile and
therefore my knowledge is quite fragmented. I would like to study maths with a
more consistent approach. Hopefully, that's what I will do after high school.
theory][type-theory], [model structures][simplicial-sets]...
I spent some time studying [Categorical Quantum Mechanics][cqm], a mathematical
setting for quantum physics in dagger-compact categories (such as the one of
@ -43,22 +30,17 @@ two things together! I wrote a partial formalization of these categorical
structures in [Coq][coq]. The sources can be found [on my GitHub][titos-catqm]
(it uses @jwiegley's [category theory library][coq-ct]).
I have taken part in the Italian [Mathematical Olympiad][olimate] with my school
since my first year here. However, training and competitions are fun only with a
team -- I perform much better in the team olympiad, and I don't really put the
required effort and training in the individual competitions.
While attending high school, I took part in the Italian [Mathematical Olympiad][olimate].
This exciting experience led me to study maths.
The [nLab][nLab] is a nice play to get lost in during cold winter nights with a
cup of tea.
## Computer science
Mathematics is hard: it's something I still haven't been able to get around. CS
and programming are easier and that's why I spend a considerable amount of hours
a day playing with my computer. Before examining my interests, I have to stress
that I really happen to hate frontend development and weakly typed programming
languages. This website doesn't depend on 50KB-sized DOM-diffing JavaScript
frameworks and it never will.
I have to stress that I really happen to hate frontend development and weakly
typed programming languages. This website doesn't depend on 50KB-sized
DOM-diffing JavaScript frameworks and it never will.
My approach to computer science is dual. Just like in mathematics, I like
abstract and theoretical areas of CS: functional programming,
@ -71,27 +53,24 @@ operating systems internals.
[Haskell][hs] is an awesome language and [GHC][ghc] is an astonishingly
well-engineered piece of software. Functional programming in general has some
inherently interesting properties related to pure mathematics and
interesting properties related to pure mathematics and
logics[^curry-howard-lambek]. Apart from theoretical and academic topics, I'm
interested in the implementation of call-by-need referentially transparent
functional languages and I'm currently studying the Haskell RTS and the STG
machine (mostly during boring school classes). The GHC codebase is quite hard to
read for a newcomer, and I'm still looking for a mentor! Sometimes I hang out on
`#haskell-it` on `libera.chat`, the IRC channel of [Haskell-ITA][haskell-ita].
functional languages and I've spent some months studying the Haskell RTS and the STG
machine (mostly during boring high school classes).
#### Hacking and cybersecurity
My low-level geek soul sometimes needs to take a break from lambda-calculi and
theoretical CS and gets involved into hacking competitions called [CTFs][ctf]. I
am part of [Tower of Hanoi][toh], the CTF and hacking team from Politecnico di
I like playing hacking competitions called [CTFs][ctf]. I
am part of [Tower of Hanoi][toh], the CTF team from Politecnico di
Milano (although I'm not a student there). Staying up all night looking at
disassemblies and memory dumps has some kind of inherently mystical meaning, and
it's also good fun.
People from ToH were the first to introduce me to offensive cybersecurity during
the [CyberChallenge.IT 2021][cyberchallenge] project. I also take part in the
the [CyberChallenge.IT 2021][cyberchallenge] project. I also took part in the
Italian [Cybersecurity Olympiad][olicyber], which targets high-school students.
I got the second place at the finals in 2021.
[I won the finals in 2022][olicyber-classifica22].
Apart from my technical interests, hacking history is an interesting topic on
its own, and the underground scene that started to fade away a few years before

View File

@ -24,7 +24,7 @@ body {
.container {
padding: 30px 60px 30px 60px;
max-width: 850px;
max-width: 900px;
@media (max-width: 768px) {
padding: 20px 10px 20px 10px;

View File

@ -1,11 +1,11 @@
<h1>~tito</h1>
<p>
Hi! I'm Tito Sacchi, alias "tauroh", an Italian high school student trying to
have fun while deciding which of my interests I'm willing to spend more time
on in the forecoming years. I like maths, computers, classical piano,
swimming, electric guitars, hiking, nature photography and a lot of other
things that I'd be glad to share with someone else.
Hi! I'm Tito Sacchi, alias "tauroh". I'm currently studying maths at the
<a href="https://web.unipv.it">University of Pavia</a>, and I'm staying at
<a href="https://www.ghislieri.it/collegio/">Collegio Ghislieri</a>.
I like computers, classical piano, swimming, Pallas cats and a lot of
other things.
<a href="about_me.html">Read more about me.</a>
</p>
<p>
@ -14,8 +14,8 @@
find some (hopefully) accurate contact information on the
<a href="contacts.html">contact page</a>.
A copy of my CV is available <a href="Tito_Sacchi_CV.pdf">here</a> (in
Italian). Some sensitive information is stripped; send me an email if you want
the complete file.
Italian, out of date). Some sensitive information is stripped; send me an
email if you want the complete file.
</p>
<hr>

View File

@ -0,0 +1,126 @@
---
title: Dual booting pfSense and Linux
tags: sysadmin, linux, pfsense, uefi
summary: Yeah, I'm trying out pfSense. I use Arch btw
---
I bought a low-cost computer on AliExpress recently to experiment with some
network security and administration tools and techniques. I wanted to give a try
to the [pfSense firewall distribution](https://www.pfsense.org) but at the same
time I'm familiar with the Linux networking stack and I honestly don't know
where to start to perform ARP spoofing on a BSD-like platform. I wanted my usual
Arch Linux box to try out nftables, bettercap and tc on the field :)
Dual booting is the best option IMHO. AliExpress standard shipping isn't..
well.. fast so I started experimenting in VirtualBox. Google wasn't very helpful
in telling me how to dual boot pfSense and Linux. It's not officially supported
by Netgate and I had some trouble setting up the bootloader. Anyone with some
experience and knowledge on UEFI systems could have solved the problem in 5
minutes but it took me an entire day. Thought sharing my solution could help
someone.
## Formatting and installing pfSense
I wanted a UEFI-only setup and apparently that's not the most common setup for
pfSense boxes. But installing a bootloader and dual-booting on UEFI is way
easier than on BIOS/MBR so I wanted to go that way. I started by formatting the
disk on Linux; you have to create a FAT32 EFI System Partition (ESP) to store
the bootloader. You can also create now the partitions you need to install Linux
or whatever you want; leave at least 2 GB of free space for pfSense. If you
install your distro now, don't install the bootloader because it will probably
be overwritten by the pfSense installer. To my knowledge the pfSense bootloader
takes less than 2 MB on the filesystem so you can keep the standard ESP size
recommended by your Linux distro; 200 MB is usually a safe choice.
Then boot the machine from the pfSense ISO (you will have to disable UEFI to do
so). Create a partition and set `/` as its mountpoint; you can choose between
UFS and ZFS as the filesystem. UFS is the default choice, while ZFS has more
advanced features and some people say that it is more reliable in case of power
failure[^1]. When you create the partition the installer will ask you whether
you want to create a boot partition; it's basically asking you to create a 512
KB boot sector for MBR (I think). We don't need that as it's completely ignored
on UEFI systems. Answer "No" and finish the installation -- the installer
apparently doesn't install the bootloader on EFI automatically, we'll do that in
a moment.
![The amazing ncurses UI of the pfSense installer. My art history teacher would
say it deserves special praise for the colors and the advanced
shading.](/resources/pfsense-linux-dual-boot/pfsense-installer-1.png)
## Installing the pfSense bootloader
When the installation is completed it will ask you whether you want to open a
shell to perform final modifications; choose "Yes". Now mount your ESP on some
temporary mountpoint, e.g. `/mnt`. Assuming `/dev/ada0p1` is your ESP (it was
shown in the partition editor):
```
$ mount -v -t msdosfs /dev/ada0p1 /mnt
```
Now you have to manually copy the EFI binary on the ESP. It took me some time to
figure out where the binary was stored on the root filesystem. I ended up
looking at the [`bsdinstall` sources][bsdinstall-source]. I'm installing it the
`/efi/pfsense` subdirectory (I like keeping PE/EFI binaries in a different
directory than bootloader configuration files and Linux `vmlinuz`/`initrd`s).
```
$ mkdir -p /mnt/efi/pfsense
$ cp /boot/loader.efi /mnt/efi/pfsense
```
## Installing the Linux bootloader
Now we'll have to set up an EFI bootloader that will allow us to choose between
Linux and pfSense at boot time. I'm using [systemd-boot][sd-boot-archwiki]
(formerly gummiboot), a lightweight and clean EFI-only bootloader. The same can
be achieved with GRUB2 by chainloading an EFI binary; an example can be found
[on the Gentoo wiki][grub-chainloading-efi], just replace the path to the
Microsoft Boot Manager with `/efi/pfsense/loader.efi`.
Boot again on Linux, preferably with UEFI, so that `efibootmgr` will be able to
save the newly installed bootloader. `systemd-boot` usually comes packaged with
systemd -- at least on Arch. In case you haven't installed your distro, you can
do that now. Then mount your ESP (`/dev/sda1` here) and install the bootloader
with `bootctl`. On some distros `/boot` may not be an empty directory on your
root filesystem; `/efi` is a popular choice as an ESP mountpoint in that case.
```
$ mount /dev/sda1 /boot
$ bootctl --esp-path=/boot install
```
Now setup bootloader entries in `/boot/loader/entries/` for Linux and then setup
the entry to chainload the pfSense bootloader:
```
# /boot/loader/entries/pfsense.conf
title pfSense
efi /efi/pfsense/loader.efi
```
Edit your `loader.conf` accordingly (if you don't set a timeout it won't show
you the boot menu):
```
# /boot/loader/loader.conf
timeout 3
console-mode keep
default pfsense.conf # or arch.conf/whatever entry you added for your distro
```
Reboot and you're done! With systemd-boot you can also choose which bootloader
entry you want to fire up on the next boot with `systemctl reboot
--boot-loader-entry=pfsense.conf`. This could be useful for a headless-only
setup, as my portable 150$ AliExpress mini PC will probably be.
[^1]: Considering that this mini PC won't have a fan, ZFS could be a wise choice
in case the temperature rises above critical levels and the CPU does an
emergency stop :)
[bsdinstall-source]: <https://github.com/freebsd/freebsd-src/blob/4042b356a00e12d6888a4e15e2373453804c5121/usr.sbin/bsdinstall/scripts/bootconfig#L67>
[sd-boot-archwiki]: <https://wiki.archlinux.org/title/systemd-boot>
[grub-chainloading-efi]: <https://wiki.gentoo.org/wiki/GRUB2/Chainloading#Dual-booting_Windows_on_UEFI_with_GPT>
[comment]: # vim: ts=2:sts=2:sw=2:et:nojoinspaces:tw=80

View File

@ -0,0 +1,441 @@
---
title: dumb-cd-webhooks
displaytitle: A dumb CD solution with GitHub Webhooks and a shell script
summary: 'Or: how to write a webserver in Bash'
tags: devops, ci, cd, git
---
I've been working on the backend of a school project (it will become public
soon) for the last few weeks. It's a Python 3.10 +
[FastAPI](https://fastapi.tiangolo.com) +
[psycopg3](https://www.psycopg.org/psycopg3/) API backed by a PostgreSQL DB. Our
server is a plain Ubuntu 20.04 box on Hetzner and our deployment is as simple as
a Python venv and a systemd service (with socket activation!). No Docker, no
Kubernetes, no supervisord. We follow the [KISS
philosophy](https://wiki.archlinux.org/title/Arch_terminology#KISS). To be
perfectly honest, I wanted to install Arch on our server, but we agreed that
Ubuntu is a bit more reliable.
We're using GitHub Actions as a CI solution and it works well; it checks our
code, builds a wheel and stores it as a build artifact. And something that I
find *really* boring and time-consuming is manually downloading the wheel on
our server and updating the Python venv. Wait, isn't this problem commonly
solved with ✨ *CD* ✨?
Ok, there are tons of complete CD solutions for containerized and advanced
workloads, using Docker, Kubernetes, AWS... But I'm a dumb idiot with some
decent scripting knowledge and no will to learn any of these technologies. How
can I start a script whenever CI builds a new wheel on master? Enter [*GitHub
Webhooks*][gh-webhooks]!
Basically GH will send a HTTP POST request to a chosen endpoint whenever certain
events happen. In particular, we are interested in the `workflow_run` event.
If you create a webhook on GitHub it will ask you for a secret to be used to
secure requests to your endpoint. Just choose a random string (`openssl rand
-hex 8`) and write it down -- it will be used in our script to check that
requests are actually signed by GitHub.
## Who needs an HTTP library to process requests?
I decided to take a very simple approach: 1 successful CI build = 1 script run.
This means 1 webhook request = 1 script run. The simplest way that I came up
with to do this is with a *systemd socket-activated Bash script*. Everytime
systemd will receive a connection on a socket it will start a custom process
that will handle the connection: this is the way most
[`inetd`](https://en.wikipedia.org/wiki/Inetd)-style daemons work.
**UNIX history recap:** (feel free to skip!) Traditional UNIX network daemons
(i.e., network services) would `accept()` connections on managed sockets and
then `fork()` a different process to handle each of these. With socket
activation (as implemented by inetd, xinetd or systemd) a single daemon (or
directly the init system!) listens on the appropriate ports for *all services*
on the machine and does the job of `accept`ing connections. Each connection will
be handled by a different process, launched as a child of the socket manager.
This minimizes load if the service is not always busy, because there won't be
any processes stuck waiting on sockets. Everytime a connection is closed the
corresponding process exits and the system remains in a clean state.
The socket manager is completely protocol-agnostic: it is up to the
service-specific processes to implement the appropriate protocols. In our case,
systemd will start our Bash script and pass the socket as file descriptors. This
means that our Bash script will have to *talk HTTP*![^overengineering]
## How do we do this?
### systemd and socket activation
Let's start by configuring systemd. The [systemd.socket man
page](https://man7.org/linux/man-pages/man5/systemd.socket.5.html). We have to
create a socket unit and a corresponding service unit template. I'll use
`cd-handler` as the unit name. I will setup systemd to listen on the UNIX domain
socket `/run/myapp-cd.sock` that you can point your reverse proxy (e.g. NGINX)
to. TCP port 8027 is mostly for debugging purposes - but if you don't need HTTPS
you can use systemd's socket directly as the webhook endpoint.[^socat]
**Socket unit:**
```
# /etc/systemd/system/cd-handler.socket
[Unit]
Description=Socket for CD webhooks
[Socket]
ListenStream=/run/myapp-cd.sock
ListenStream=0.0.0.0:8027
Accept=yes
[Install]
WantedBy=sockets.target
```
**Service template unit:**
```
# /etc/systemd/system/cd-handler@.socket
[Unit]
Description=GitHub webhook handler for CD
Requires=cd-handler.socket
[Service]
Type=simple
ExecStart=/var/lib/cd/handle-webhook.sh # path to our Bash webhook handler
StandardInput=socket
StandardOutput=socket
StandardError=journal
TimeoutStopSec=5
[Install]
WantedBy=multi-user.target
```
`Accept=yes` will make systemd start a process for each connection. Create a
Bash script in `/var/lib/cd/handle-webhook.sh`; for now we will only answer
`204 No Content` to every possible request. Remember to make the script
executable (`chmod +x`). We will communicate with the socket using standard
streams (stdin/stdout; stderr will be sent to the system logs).
```bash
#!/bin/bash
# /var/lib/cd/handle-webhook.sh
printf "HTTP/1.1 204 No Content\r\n"
printf "Server: TauroServer CI/CD\r\n"
printf "Connection: close\r\n"
printf "\r\n"
```
`systemctl daemon-reload && systemctl enable --now cd-handler.socket` and you
are ready to go. Test our dumb HTTP server with `curl -vv http://127.0.0.1:8027`
or if you're using the awesome
[HTTPie](https://httpie.io/docs/cli/main-features) `http -v :8027`. If you're
successfully receiving a 204, we have just ~~ignored~~ processed a HTTP request
with Bash ^^
### Parsing the HTTP request
The anatomy of HTTP requests and responses is standardised in [RFC
2616][rfc2616], in sections 5 and 6 respectively.
UNIX systems come with powerful text-processing utilities. Bash itself has
[parameter expansion][bash-expansion] features that will be useful in processing
the HTTP request and we will use [jq][jq] to extract
the fields we're interested in from the JSON payload.
We will build our script step by step. I'll define a function to log data (in my
case, it will use `systemd-cat` to write directly to the journal; you can
substitute its body to adapt it to your needs) and another function to send a
response. `send_response` takes two parameters: the first one is the status code
followed by its description (e.g. `204 No Content`) and the second one is the
response body (optionally empty). We're using `wc` to count characters in the
body (we're subtracting 1 for the extra `\n` that Bash sends to `wc`).
```bash
#!/bin/bash
set -euo pipefail # Good practice!
function log {
systemd-cat -t 'GitHub webhook handler' $@
}
function send_response {
printf "Sending response: %s\n" "$1" | log -p info
printf "HTTP/1.1 %s\r\n" "$1"
printf "Server: TauroServer CI/CD\r\n"
printf "Connection: close\r\n"
printf "Content-Type: application/octet-stream\r\n"
printf "Content-Length: $(($(wc -c <<<"$2") - 1))\r\n"
printf "\r\n"
printf '%s' "$2"
}
```
If you add `send_response "200 OK" "Hello World!"` as a last line, you should be
able to get a 200 response with cURL or HTTPie! You can also test webhooks from
the GitHub web UI and see if an OK response is received as expected. We are not
sending the `Date` response headers that *should* be set according to RFC 2616.
![socat and HTTPie talking nicely to each
other.](/resources/dumb-cd-webhooks/socat-httpie-success.jpg)
**Parsing the request line.** As easy as `read method path version`. There will
probably be a pending `\r` on `version` but we don't care this much (we will
assume HTTP/1.1 everywhere ^^).
**Parsing headers.** Headers follow immediately the request line. Expert bash
users would probably use an associative array to store request headers; we will
just use a `case` statement to extract headers we're interested in. We split
each line on `:` by setting the special variable `IFS`, input field separator,
that defaults to a space. `tr` removes pending `\r\n` and brace substitution is
used to remove the space that follows `:` in each header line.
```bash
content_length=0
delivery_id=none
ghsig=none
event_type=none
while IFS=: read -r key val; do
[[ "$key" == "$(printf '\r\n')" ]] && break;
val=$(tr -d '\r\n' <<<"$val")
case "$key" in
"Content-Length")
content_length="${val# }"
;;
"X-GitHub-Delivery")
delivery_id="${val# }"
;;
"X-GitHub-Event")
event_type="${val# }"
;;
"X-Hub-Signature-256")
# This will be trimmed later when comparing to OpenSSL's HMAC
ghsig=$val
;;
*)
;;
esac
printf 'Header: %s: %s\n' "$key" "$val" | log -p debug
done
```
**Reading body and checking HMAC signature.** GitHub sends a hex-encoded
HMAC-SHA-256 signature of the JSON body as the [`X-Hub-Signature-256`
header][gh-securing-webhooks], signed with the secret chosen while creating the
webhook. Without this layer of security, anyone could send a POST and trigger CD
scripts, maybe making us download malicious builds. In a shell script the
easiest way to calculate an HMAC is with the `openssl` command-line tool. We are
using `dd` to read an exact amount of bytes from stdin and the body is passed to
`openssl` with a pipe to avoid sending trailing newlines (using direct
redirection, i.e., `<<<` did not work for me). Brace expansion is used to split
strings. Place your WebHook secret in the `GITHUB_WEBHOOK_SECRET` variable and
set `ENFORCE_HMAC` to something different from 0 (I thought disabling signature
checking could be useful for debugging purposes). You can now play with
cURL/HTTPie/*[insert your favourite HTTP client here]* to see if you receive 401
Unauthorized responses as expected.
```bash
printf "Trying to read request content... %s bytes\n" "$content_length" | log -p info
content=$(dd bs=1 count="$content_length" 2> >(log -p debug) )
mysig=$(printf '%s' "$content" | openssl dgst -sha256 -hmac $GITHUB_WEBHOOK_SECRET)
if [[ "${mysig#* }" == "${ghsig#*=}" ]]; then
log -p notice <<<"HMAC signatures match, proceeding further."
else
log -p warning <<<"HMAC signatures do not match! Request is not authenticated!"
if [[ $ENFORCE_HMAC != 0 ]]; then
send_response "401 Unauthorized" "Provide signature as HTTP header."
log -p err <<<"Exiting now because HMAC signature enforcing is required."
exit 1
fi
fi
```
**Sending the HTTP response.** We will send an appropriate response to response
to GitHub with the function defined earlier.
```bash
if [[ "$event_type" == "none" ]]; then
send_response "400 Bad Request" "Please provide event type as HTTP header."
log -p err <<<"X-GitHub-Event header was not provided."
exit 1
fi
if [[ "$delivery_id" == "none" ]]; then
send_response "400 Bad Request" "Please provide delivery ID as HTTP header."
log -p err <<<"X-GitHub-Delivery header was not provided."
exit 1
fi
printf "GitHub Delivery ID: %s\n" "$delivery_id" | log -p info
printf "GitHub Event type: %s\n" "$event_type" | log -p info
case "$event_type" in
"workflow_run")
send_response "200 OK" "Acknowledged workflow run!"
;;
*)
send_response "204 No Content" ""
exit 0
;;
esac
```
The "HTTP server" part of the script is complete! You can also test this by
asking GitHub to resend past webhooks from the web UI.
### Parsing the JSON body and downloading artifacts
**JSON body.** The JSON schema of GitHub webhooks payloads can be found on the
[official GH docs][gh-webhook-payloads]. We will use [jq][jq] to parse JSON and
with its `-r` flag we will print the fields we're interested on standard output,
each on a separate line. Its stream can be passed to the `read` builtin with
`IFS` set to `\n`. The `|| true` disjunction at the end of the command makes the
script continue with the execution even if jq doesn't find some of the fields we
asked it to extract (e.g., in event that signal the start of a workflow,
`artifacts_url` is not present).
I want to run CD workflows only on the main branch (`main`, `master`, ...), so I
added a check against the variable `MAIN_BRANCH` that you can configure at the
top of the script. GitHub sends `workflow_run` events even when CI workflows
start, but we're only interested in running a custom action when a workflow
succeeds.
```bash
IFS=$'\n' read -r -d '' action branch workflow_status \
name conclusion url artifacts \
commit message author < <(
jq -r '.action,
.workflow_run.head_branch,
.workflow_run.status,
.workflow_run.name,
.workflow_run.conclusion,
.workflow_run.html_url,
.workflow_run.artifacts_url,
.workflow_run.head_commit.id,
.workflow_run.head_commit.message,
.workflow_run.head_commit.author.name' <<<"$content") || true
printf 'Workflow run "%s" %s! See %s\n' "$name" "$workflow_status" "$url" | log -p notice
printf 'Head commit SHA: %s\n' "$commit" | log -p info
printf 'Head commit message: %s\n' "$message" | log -p info
printf 'Commit author: %s\n' "$author" | log -p info
if [[ "$action" != "completed" ]] \
|| [[ "$conclusion" != "success" ]] \
|| [[ "$branch" != "$MAIN_BRANCH" ]];
then exit 0
fi
log -p notice <<<"Proceeding with continuous delivery!"
```
**Build artifacts.** Before running the custom CD script that depends from your
specific deployments scenario, we will download all artifacts build on GitHub
Actions during CI. For example, in our Dart/Flutter webapp this could include
the built website, with Dart already compiled to JavaScript. In the case of our
Python backend the artifact is a Python wheel. This webhook script handler is
completely language-agnostic though, meaning that you can use it with whatever
language or build system you want.
We will download and extract all artifacts in a temporary directory and then
pass its path as an argument to the CD script, with a bunch of other useful
information such as the branch name and the commit SHA. The function
`download_artifacts` downloads and extracts the ZIP files stored on GitHub using
the [Artifacts API][gh-artifacts-api]. It iterates on the JSON array and
extracts appropriate fields using jq's array access syntax. GitHub returns a 302
temporary redirect when it receives a GET on the `archive_download_url` advised
in the artifact body, so we use cURL's `-L` to make it follow redirects.
```bash
function download_artifacts {
# $1: URL
# $2: directory to download artifacts to
pushd "$2" &>/dev/null
artifacts_payload=$(curl --no-progress-meter -u "$GITHUB_API_USER:$GITHUB_API_TOKEN" "$1" 2> >(log -p debug))
artifacts_amount=$(jq -r '.total_count' <<<"$artifacts_payload")
for i in $(seq 1 "$artifacts_amount"); do
printf 'Downloading artifact %s/%s...\n' "$i" "$artifacts_amount" | log -p info
name=$(jq -r ".artifacts[$((i - 1))].name" <<<"$artifacts_payload")
url=$(jq -r ".artifacts[$((i - 1))].archive_download_url" <<<"$artifacts_payload")
printf 'Artifact name: "%s" (downloading from %s)\n' "$name" "$url" | log -p info
tmpfile=$(mktemp)
printf 'Downloading ZIP to %s\n' "$tmpfile" | log -p debug
curl --no-progress-meter -L -u "$GITHUB_API_USER:$GITHUB_API_TOKEN" --output "$tmpfile" "$url" 2> >(log -p debug)
mkdir "$name"
printf 'Unzipping into %s...\n' "$2/$name" | log -p debug
unzip "$tmpfile" -d "$2/$name" | log -p debug
rm "$tmpfile"
done
popd &>/dev/null
}
artifacts_dir=$(mktemp -d)
printf 'Downloading artifacts to %s...\n' "$artifacts_dir" | log -p info
download_artifacts "$artifacts" "$artifacts_dir"
```
**Final step: running your CD script!** If you aliased `log` to `systemd-cat` as I
did above, the `-t` flag will select a different identifier, to make the output
of the custom script stand out from the garbage of our webhook handler in the
system journal. Again, configure `$CD_SCRIPT` appropriately; it will be run from
the directory specified above in the systemd unit file and it will receive the
path to the directory containing the downloaded artifacts as an argument.
**Note**: it will run with root privileges unless specified otherwise in the
service unit file!
```bash
printf 'Running CD script!\n' | log -p notice
$CD_SCRIPT "$artifacts_dir" "$branch" "$commit" 2>&1 | log -t "CD script" -p info
```
For example, our Python backend CD script looks something like this:
```bash
cd "$1"
systemctl stop my-awesome-backend.service
source /var/lib/my-backend/virtualenv/bin/activate
pip3 install --no-deps --force **/*.whl
systemctl start my-awesome-backend.service
```
**Bonus points for cleanup** :) Remove the tmpdir created earlier to store
artifacts:
```bash
printf 'Deleting artifacts directory...\n' | log -p info
rm -r "$artifacts_dir"
```
## Conclusion
You can find the complete script
[here](/resources/dumb-cd-webhooks/handle-webhook.sh). It's 158LoC, not that
much, and it's very flexible. There's room for improvement; e.g., selecting
different scripts on different branches. Let me know if you extend this script
or use a similar approach!
[gh-webhooks]: https://docs.github.com/en/developers/webhooks-and-events/webhooks/about-webhooks
[rfc2616]: https://datatracker.ietf.org/doc/html/rfc2616
[bash-expansion]: https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
[jq]: https://stedolan.github.io/jq/
[gh-securing-webhooks]: https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks#validating-payloads-from-github
[gh-webhook-payloads]: https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads
[gh-artifacts-api]: https://docs.github.com/en/rest/actions/artifacts
[^overengineering]: At this point, you might be wondering whether it is worth
it. It probably isn't, and the simplest solution could be a 100LoC Python +
FastAPI script to handle webhooks. But I *really* wanted to do this with basic
UNIX tools.
[^socat]: You can also use `socat` to do what systemd is doing for us if you
want to test your script on your local machine: `socat
TCP-LISTEN:8027,reuseaddr,fork SYSTEM:/path/to/handle-webhook.sh`.
[comment]: # vim: ts=2:sts=2:sw=2:et:nojoinspaces:tw=80

View File

@ -0,0 +1,197 @@
---
title: Unlocking ZFS datasets on boot with a YubiKey
summary: 'Custom initramfs scripts to unlock ZFS datasets with a YubiKey.'
tags: sysadmin, linux, zfs
---
My home server runs Rocky Linux 9 on ZFS. Its main root dataset is encrypted, and until a few weeks
ago I had to manually enter the dataset password on the boot console (fortunately the iDRAC allows
me to do that remotely). With a modern server, I would use the TPM2 chip to provide the decryption
key; however, my home server is a PowerEdge R330 and only has the obsolete TPM1.2 chip. I had a
spare YubiKey 5 I bought when Cloudflare offered them at $10 each, so I decided to put that YubiKey
into the internal USB port and use it to unlock datasets without user input. You could argue that
it's as useful as having no encryption, because the YubiKey has no way to detect whether the
boot was from a trusted source or not. Still, I decided to encrypt my root ZFS pool mostly to be
capable of sending encrypted raw sends for backup purposes.
I'm sharing the custom dracut module I built to serve this purpose. It simply loads a symmetrically
encrypted GPG file stored in the initramfs and decrypts it with a passphrase generated by the
YubiKey HMAC feature. The decrypted contents of the GPG file provide the decryption key to ZFS.
## How it works
* During boot, dracut calls our custom hook before mounting ZFS datasets and checks whether
the rootfs dataset has the `zfs_yubikey:keylocation` and `zfs_yubikey:slot` custom attributes set.
The former specifies where the encrypted GPG file containing the ZFS key resides in the initramfs;
the latter is optional and suggests a particular YubiKey HMAC slot to use (defaults to 1).
* If the rootfs requires YubiKey decryption, a HMAC challenge will be generated from the SHA256 sum
of the string `YUBIKEY_ZFS_V1;<machine_id>;<pool_guid>;<dataset_objsetid>`.
* The `ykchalresp` binary sends the aforementioned 256-bit challenge to the YubiKey on the specified
slot and waits for a response.
* The HMAC response is used to decrypt the GPG file and the resulting plaintext is sent to `zfs
load-key -L prompt <dataset>`.
## Setup
The code for the dracut module is provided below. To setup your dataset for automatic unlock,
first setup your YubiKey for HMAC challenge-response on one of the available slots.
Then open a shell prompt and source `zfs-yubikey-lib.sh`; run
`get_response <dataset> [<yubikey_slot>]` to ask the YubiKey the generate the HMAC response and
use it to encrypt the ZFS key with GnuPG. Save the resulting encrypted file in `/etc/zfs/yubikey/`
and set the `zfs_yubikey:keylocation` property to the path of the file you just saved.
Regenerate the initramfs and you're done.
Shell examples:
```sh
source zfs-yubikey-lib.sh
# Set DATASET to your ZFS dataset
DATASET=pool/various/elements/to/dataset
ykinfo -H || echo 'YubiKey not found!'
# Write your ZFS key to the stdin of the following command
gpg --symmetric --pinentry-mode loopback --passphrase-fd 3 --armor \
--output "/etc/zfs/yubikey/${DATASET##*/}.gpg" 3< <(get_response "${DATASET}")
zfs set zfs_yubikey:keylocation="/etc/zfs/yubikey/${DATASET##*/}.gpg" "${DATASET}"
dracut --regenerate-all --force
```
## Code
A dracut module is composed of a `module-setup.sh` (executed on initramfs generation) and an
arbitrary number of hooks and files installed by the module. The directory structure of our module
is the following:
```
zfs-yubikey
├── module-setup.sh (executable)
├── zfs-yubikey-lib.sh (executable)
└── zfs-yubikey-load-key.sh (executable)
```
This directory should be copied to `/usr/lib/dracut/modules.d/91zfs-yubikey` and dracut should be
configured to include this module (see `man 5 dracut.conf`). Code follows.
<br/>
* `module-setup.sh`
```sh
#!/usr/bin/bash
check() {
require_binaries sha256sum gpg ykchalresp ykinfo || return 1
return 0
}
depends() {
echo zfs
return 0
}
install() {
inst_multiple gpg gpg-agent gpg-connect-agent ykchalresp ykinfo sha256sum ||
{ dfatal "Failed to install essential binaries"; exit 1; }
inst_hook pre-mount 85 "${moddir}/zfs-yubikey-load-key.sh"
inst_script "${moddir}/zfs-yubikey-lib.sh" "/lib/dracut-zfs-yubikey-lib.sh"
inst_multiple -o -H /etc/zfs/yubikey/*
}
```
<br/>
* `zfs-yubikey-lib.sh`
```sh
#!/usr/bin/sh
command -v ykchalresp &>/dev/null || return 127
command -v ykinfo &>/dev/null || return 127
command -v zpool &>/dev/null || return 127
command -v zfs &>/dev/null || return 127
command -v gpg &>/dev/null || return 127
generate_challenge () {
local dataset="${1}"
local pool="${dataset%%/*}"
local machine_id=''
if [ -n "$ZFS_YUBI_USE_MACHINE_ID" ]; then
machine_id="$(< /etc/machine-id)"
fi
local pool_guid="$(zpool get -Ho value guid "$pool")"
local dataset_objsetid="$(zfs get -Ho value objsetid "$dataset")"
local key="$(printf 'YUBIKEY_ZFS_V1;%s;%s;%s' "$machine_id" "$pool_guid" "$dataset_objsetid")"
sha256sum < <(printf %s "$key") | cut -f1 -d' '
}
get_response () {
if [ -z "$1" ]; then return 1; fi
local dataset="${1}"
local slot="${2:-1}"
if [ "$slot" != 1 -a "$slot" != 2 ]; then
echo "Invalid slot number!" >&2; return 1
fi
local challenge="$(generate_challenge "$dataset")"
ykchalresp -"$slot" -x "$challenge"
}
```
<br/>
* `zfs-yubikey-load-key.sh`
```sh
#!/usr/bin/sh
. /lib/dracut-zfs-lib.sh
. /lib/dracut-zfs-yubikey-lib.sh
# decode_root_args || return 0
decode_root_args
# There is a race between the zpool import and the pre-mount hooks, so we wait for a pool to be imported
while ! systemctl is-active --quiet zfs-import.target; do
systemctl is-failed --quiet zfs-import-cache.service zfs-import-scan.service && return 1
sleep 0.1s
done
BOOTFS="$root"
if [ "$BOOTFS" = "zfs:AUTO" ]; then
BOOTFS="$(zpool get -Ho value bootfs | grep -m1 -vFx -)"
fi
[ "$(zpool get -Ho value feature@encryption "${BOOTFS%%/*}")" = 'active' ] || return 0
_load_key_yubi_cb() {
ENCRYPTIONROOT="$(zfs get -Ho value encryptionroot "${1}")"
[ "${ENCRYPTIONROOT}" = "-" ] && return 0
[ "$(zfs get -Ho value keystatus "${ENCRYPTIONROOT}")" = "unavailable" ] || return 0
local yubi_keylocation="$(zfs get -Ho value zfs_yubikey:keylocation "${ENCRYPTIONROOT}")"
[ "${yubi_keylocation}" = "-" ] && return 0
[ -r "${yubi_keylocation}" ] || return 0
local yubi_slot="$(zfs get -Ho value zfs_yubikey:slot "${ENCRYPTIONROOT}")"
[ "${yubi_slot}" = "-" ] && yubi_slot=1
udevadm settle
info "ZFS-YubiKey: Checking for YubiKey..."
ykinfo -v &>/dev/null && break
gpg --passphrase-file <(get_response "${ENCRYPTIONROOT}" "${yubi_slot}") --pinentry-mode loopback \
--decrypt "${yubi_keylocation}" | zfs load-key -L prompt "${ENCRYPTIONROOT}"
}
_load_key_yubi_cb "$BOOTFS"
for_relevant_root_children "$BOOTFS" _load_key_yubi_cb
```

View File

@ -0,0 +1,158 @@
#!/bin/bash
set -euo pipefail
GITHUB_WEBHOOK_SECRET=REDACTED
GITHUB_API_TOKEN=REDACTED
GITHUB_API_USER=REDACTED
MAIN_BRANCH=master
CD_SCRIPT="/var/lib/path-to-your-backend/continuous-delivery.sh"
ENFORCE_HMAC=1
function log {
systemd-cat -t 'GitHub webhook handler' $@
}
function send_response {
printf "Sending response: %s\n" "$1" | log -p info
printf "HTTP/1.1 %s\r\n" "$1"
printf "Server: TauroServer CI/CD\r\n"
printf "Connection: close\r\n"
printf "Content-Type: application/octet-stream\r\n"
printf "Content-Length: $(($(wc -c <<<"$2") - 1))\r\n"
printf "\r\n"
printf '%s' "$2"
}
content_length=0
delivery_id=none
ghsig=none
event_type=none
read method path version
printf "Request method: $method\n" | log -p debug
printf "Request path: $path\n" | log -p debug
printf "HTTP version: $version\n" | log -p debug
while IFS=: read -r key val; do
[[ "$key" == "$(printf '\r\n')" ]] && break;
val=$(tr -d '\r\n' <<<"$val")
case "$key" in
"Content-Length")
content_length="${val# }"
;;
"X-GitHub-Delivery")
delivery_id="${val# }"
;;
"X-GitHub-Event")
event_type="${val# }"
;;
"X-Hub-Signature-256")
ghsig=$val
;;
*)
;;
esac
printf 'Header: %s: %s\n' "$key" "$val" | log -p debug
done
printf "Trying to read request content... %s bytes\n" "$content_length" | log -p info
content=$(dd bs=1 count="$content_length" 2> >(log -p debug) )
# xxd <<<"$content" >&2
mysig=$(printf '%s' "$content" | openssl dgst -sha256 -hmac $GITHUB_WEBHOOK_SECRET)
if [[ "${mysig#* }" == "${ghsig#*=}" ]]; then
log -p notice <<<"HMAC signatures match, proceeding further."
else
log -p warning <<<"HMAC signatures do not match! Request is not authenticated!"
if [[ $ENFORCE_HMAC != 0 ]]; then
send_response "401 Unauthorized" "Provide signature as HTTP header."
log -p err <<<"Exiting now because HMAC signature enforcing is required."
exit 1
fi
fi
if [[ "$event_type" == "none" ]]; then
send_response "400 Bad Request" "Please provide event type as HTTP header."
log -p err <<<"X-GitHub-Event header was not provided."
exit 1
fi
if [[ "$delivery_id" == "none" ]]; then
send_response "400 Bad Request" "Please provide delivery ID as HTTP header."
log -p err <<<"X-GitHub-Delivery header was not provided."
exit 1
fi
printf "GitHub Delivery ID: %s\n" "$delivery_id" | log -p info
printf "GitHub Event type: %s\n" "$event_type" | log -p info
case "$event_type" in
"ping")
send_response "204 No Content" ""
exit 0
;;
"workflow_run")
send_response "200 OK" "Acknowledged workflow run!"
;;
*)
send_response "204 No Content" ""
exit 0
;;
esac
IFS=$'\n' read -r -d '' action branch workflow_status name conclusion url artifacts \
commit message author < <(
jq -r '.action,
.workflow_run.head_branch,
.workflow_run.status,
.workflow_run.name,
.workflow_run.conclusion,
.workflow_run.html_url,
.workflow_run.artifacts_url,
.workflow_run.head_commit.id,
.workflow_run.head_commit.message,
.workflow_run.head_commit.author.name' <<<"$content") || true
printf 'Workflow run "%s" %s! See %s\n' "$name" "$workflow_status" "$url" | log -p notice
printf 'Head commit SHA: %s\n' "$commit" | log -p info
printf 'Head commit message: %s\n' "$message" | log -p info
printf 'Commit author: %s\n' "$author" | log -p info
if [[ "$action" != "completed" ]] || \
[[ "$conclusion" != "success" ]] || \
[[ "$branch" != "$MAIN_BRANCH" ]]; then exit 0; fi
log -p notice <<<"Proceeding with continuous delivery!"
function download_artifacts {
# $1: URL
# $2: directory to download artifacts to
pushd "$2" &>/dev/null
artifacts_payload=$(curl --no-progress-meter -u "$GITHUB_API_USER:$GITHUB_API_TOKEN" "$1" 2> >(log -p debug))
artifacts_amount=$(jq -r '.total_count' <<<"$artifacts_payload")
for i in $(seq 1 "$artifacts_amount"); do
printf 'Downloading artifact %s/%s...\n' "$i" "$artifacts_amount" | log -p info
name=$(jq -r ".artifacts[$((i - 1))].name" <<<"$artifacts_payload")
url=$(jq -r ".artifacts[$((i - 1))].archive_download_url" <<<"$artifacts_payload")
printf 'Artifact name: "%s" (downloading from %s)\n' "$name" "$url" | log -p info
tmpfile=$(mktemp)
printf 'Downloading ZIP to %s\n' "$tmpfile" | log -p debug
curl --no-progress-meter -L -u "$GITHUB_API_USER:$GITHUB_API_TOKEN" --output "$tmpfile" "$url" 2> >(log -p debug)
mkdir "$name"
printf 'Unzipping into %s...\n' "$2/$name" | log -p debug
unzip "$tmpfile" -d "$2/$name" | log -p debug
rm "$tmpfile"
done
popd &>/dev/null
}
artifacts_dir=$(mktemp -d)
printf 'Downloading artifacts to %s...\n' "$artifacts_dir" | log -p info
download_artifacts "$artifacts" "$artifacts_dir"
printf 'Running CD script!\n' | log -p notice
$CD_SCRIPT "$artifacts_dir" "$branch" "$commit" 2>&1 | log -t "CD script" -p info
printf 'Deleting artifacts directory...\n' | log -p info
rm -r "$artifacts_dir"

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -1,10 +1,11 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
import Control.Monad.State
import Control.Monad
import Hakyll
import System.Process
import System.FilePath
import System.Process
import Text.Pandoc.Extensions
import System.FilePath
import Text.Pandoc.Options
main :: IO ()
@ -92,7 +93,7 @@ config :: Configuration
config = defaultConfiguration {
deployCommand = "rsync -avP --delete \
\ --exclude blog --exclude cgi-bin --exclude .DS_Store \
\ _site/ tito@tilde.team:~/public_html"
\ --exclude .well-known _site/ tito@tilde.team:~/public_html"
}
postCtx :: Tags -> Context String