├── .github
└── workflows
│ └── build.yaml
├── .gitignore
├── LICENSE
├── README.md
├── docs
└── architecture.md
├── flake.lock
├── flake.nix
├── flakeModule.nix
├── lib
├── add-sops-cfg.nix
├── gen-initial.nix
├── gen-knownhosts-file.nix
├── gen-new-host.nix
├── install-on-beacon.nix
├── sops-add-main-key.nix
├── sops-create-main-key.nix
└── ssh.nix
├── modules
├── beacon.nix
├── configuration.nix
└── disks.nix
├── template
├── .gitignore
├── .sops.yaml
├── README.md
├── flake.nix
├── myskarabox
│ ├── configuration.nix
│ ├── host_key
│ ├── hostid
│ ├── ip
│ ├── known_hosts
│ ├── secrets.yaml
│ ├── ssh_boot_port
│ └── ssh_port
├── secrets.yaml
└── sops.key
└── tests
├── default.nix
└── lib.nix
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: "build"
2 | on:
3 | pull_request:
4 | push:
5 | branches: [ "main" ]
6 |
7 | jobs:
8 | path-filter:
9 | runs-on: ubuntu-latest
10 | outputs:
11 | changed: ${{ steps.filter.outputs.any_changed }}
12 |
13 | steps:
14 | - name: Checkout repository
15 | uses: actions/checkout@v4
16 |
17 | - uses: tj-actions/changed-files@v46
18 | id: filter
19 | with:
20 | files: |
21 | modules/**
22 | tests/**
23 | flake.lock
24 | flake.nix
25 | .github/workflows/build.yaml
26 | separator: "\n"
27 |
28 | - env:
29 | ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_changed_files }}
30 | run: |
31 | echo $ALL_CHANGED_FILES
32 |
33 | tests-matrix:
34 | needs: [ "path-filter" ]
35 | if: needs.path-filter.outputs.changed == 'true'
36 | runs-on: ubuntu-latest
37 | steps:
38 | - name: Checkout repository
39 | uses: actions/checkout@v4
40 | - name: Install Nix
41 | uses: DeterminateSystems/nix-installer-action@main
42 | with:
43 | github-token: ${{ secrets.GITHUB_TOKEN }}
44 | extra-conf: "system-features = nixos-test benchmark big-parallel kvm"
45 | - name: Setup Caching
46 | uses: cachix/cachix-action@v16
47 | with:
48 | name: selfhostblocks
49 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
50 | - name: Generate Matrix
51 | id: generate-matrix
52 | run: |
53 | nix flake show --allow-import-from-derivation --json \
54 | | jq -c '.["checks"]["x86_64-linux"] | keys' > .output
55 |
56 | cat .output
57 |
58 | echo dynamic_list="$(cat .output)" >> "$GITHUB_OUTPUT"
59 | outputs:
60 | check: ${{ steps.generate-matrix.outputs.dynamic_list }}
61 |
62 | tests:
63 | runs-on: ubuntu-latest
64 | needs: [ "tests-matrix" ]
65 | strategy:
66 | matrix:
67 | check: ${{ fromJson(needs.tests-matrix.outputs.check) }}
68 | steps:
69 | - name: Checkout repository
70 | uses: actions/checkout@v4
71 | - name: Install Nix
72 | uses: DeterminateSystems/nix-installer-action@main
73 | with:
74 | github-token: ${{ secrets.GITHUB_TOKEN }}
75 | extra-conf: "system-features = nixos-test benchmark big-parallel kvm"
76 | - name: Setup Caching
77 | uses: cachix/cachix-action@v16
78 | with:
79 | name: selfhostblocks
80 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
81 | - name: Run test
82 | if: ${{ matrix.check != 'lib' }}
83 | run: |
84 | (while true; do free -m; df -h; sleep 10; done)&
85 | nix run .#checks.x86_64-linux.${{ matrix.check }}
86 | - name: Run test
87 | if: ${{ matrix.check == 'lib' }}
88 | run: |
89 | nix build .#checks.x86_64-linux.${{ matrix.check }}
90 |
91 | result:
92 | runs-on: ubuntu-latest
93 | needs: [ tests ]
94 | if: '!cancelled()'
95 | steps:
96 | - run: |
97 | result="${{ needs.tests.result }}"
98 | if ! [[ $result == "success" || $result == "skipped" ]]; then
99 | exit 1
100 | fi
101 | exit 0
102 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | result
2 | *.qcow2
3 | *.log
4 |
5 | .skarabox-tmp
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SkaraboxOS
2 |
3 | [](https://github.com/ibizaman/skarabox/actions/workflows/build.yaml)
4 |
5 | SkaraboxOS aims to be the fastest way to install NixOS on a server
6 | with all batteries included.
7 |
8 |
9 | - [Usage](#usage)
10 | - [Provided operations:](#provided-operations)
11 | - [Why?](#why)
12 | - [Hardware Requirements](#hardware-requirements)
13 | - [Architecture](#architecture)
14 | - [Roadmap](#roadmap)
15 | - [Contribute](#contribute)
16 | - [Links](#links)
17 |
18 |
19 | ## Usage
20 |
21 | 1. Initialize repo
22 |
23 | a. Either from scratch
24 |
25 | ```bash
26 | mkdir myskarabox
27 | cd myskarabox
28 | nix run github:ibizaman/skarabox#init
29 | ```
30 |
31 | b. Or in existing repo
32 |
33 | Merge [./template/flake.nix](./template/flake.nix) with yours, then:
34 |
35 | ```bash
36 | # Create Sops main key `sops.key` if needed
37 | nix run .#sops-create-main-key
38 |
39 | # Add Sops main key to Sops config `.sops.yaml`
40 | nix run .#sops-add-main-key
41 |
42 | # Create config for host `myskarabox` in folder `./myskarabox`
43 | nix run .#gen-new-host myskarabox
44 | ```
45 |
46 | 2. Start beacon
47 |
48 | a. Either test on VM
49 |
50 | ```bash
51 | nix run .#myskarabox-beacon-vm &
52 |
53 | echo 127.0.0.1 > myskarabox/ip
54 | echo x86_64-linux > myskarabox/system
55 | echo 2222 > myskarabox/ssh_port
56 | echo 2223 > myskarabox/ssh_boot_port
57 | nix run .#myskarabox-gen-knownhosts-file
58 | ```
59 |
60 | This VM has 4 hard drives:
61 | - `/dev/nvme0`
62 | - `/dev/nvme1`
63 | - `/dev/sda`
64 | - `/dev/sdb`
65 |
66 | b. Or install on an on-premise host
67 |
68 | ```bash
69 | nix build .#myskarabox-beacon
70 | nix run .#beacon-usbimager
71 | ```
72 |
73 | Use usbimager to burn `./result/iso/beacon.iso`
74 | on a USB key, then boot on that USB key.
75 | Get IP from host and use it in next snippet:
76 |
77 | ```bash
78 | echo 192.168.1.XX > myskarabox/ip
79 | echo x86_64-linux > myskarabox/system
80 | nix run .#myskarabox-gen-knownhosts-file
81 | ```
82 |
83 | c. Or install on Cloud Instance
84 |
85 | For Hetzner, start in recovery mode and retrieve the IP.
86 |
87 | ```bash
88 | echo > myskarabox/ip
89 | echo x86_64-linux > myskarabox/system
90 | nix run .#myskarabox-gen-knownhosts-file
91 | ```
92 |
93 | 3. Install on target host
94 |
95 | ```bash
96 | nix run .#myskarabox-get-facter > ./myskarabox/facter.json
97 | nix run .#myskarabox-install-on-beacon .#myskarabox
98 | ```
99 |
100 | Target host will reboot and ask the passphrase to decrypt
101 | the root partition. See next section for how to give it.
102 |
103 | ## Provided operations:
104 |
105 | ```
106 | # Decrypt root partition:
107 | nix run .#myskarabox-unlock
108 |
109 | # SSH in:
110 | nix run .#myskarabox-ssh
111 |
112 | # Deploy changes if any:
113 | nix run .#deloy-rs
114 |
115 | # Edit Sops file:
116 | nix run .#sops ./myskarabox/secrets.yaml
117 |
118 | # Reboot:
119 | nix run .#myskarabox-ssh sudo reboot
120 | ```
121 |
122 | The flake [template](./template) combines turn-key style:
123 |
124 | - Creating a bootable ISO, installable on an USB key.
125 | - Alternatively, creating a VM based on the bootable ISO
126 | to test the installation procedure (like shown in the snippet above).
127 | - Managing host keys, known hosts and ssh keys
128 | to provide a secure and seamless SSH experience.
129 | - [nixos-anywhere][] to install NixOS headlessly.
130 | - [disko][] to format the drives using native ZFS encryption
131 | - Remote root pool decryption through ssh.
132 | - Disk mirroring: 1 or 2 disks in raid1 using ZFS mirroring for the OS,
133 | boot partition is then mirrored using grub mirrored devices
134 | and 0 or 2 disks in raid1 using ZFS mirroring for the data disks.
135 | - [nixos-facter][] to handle hardware configuration.
136 | - [flake-parts][] to make the resulting `flake.nix` small.
137 | - Handle having multiple hosts managed by one flake
138 | and programmatically add more with generated secrets with one command.
139 | - [sops-nix][] to handle secrets: the user's password and the root and data ZFS pool passphrases.
140 | - Programmatically populate Sops secrets file.
141 | - Fully pinned inputs.
142 | - [deploy-rs][] to deploy updates.
143 | - Backed by [tests][] for all disk variants
144 | and [CI][] to make sure the installation procedure does work!
145 | Why don't you run them yourself: `nix run github:ibizaman/skarabox#checks.x86_64-linux.oneOStwoData -- -g`.
146 | - Supporting `x86_64-linux` and `aarch64-linux` platform.
147 |
148 | I used this successfully on my own on-premise x86 server
149 | and on Hetzner dedicated ARM and x86 hosts.
150 |
151 | [nixos-anywhere]: https://github.com/nix-community/nixos-anywhere
152 | [disko]: https://github.com/nix-community/disko
153 | [nixos-facter]: https://github.com/nix-community/nixos-facter
154 | [flake-parts]: https://flake.parts/
155 | [sops-nix]: https://github.com/Mic92/sops-nix
156 | [deploy-rs]: https://github.com/serokell/deploy-rs
157 | [tests]: ./tests/default.nix
158 | [CI]: ./.github/workflows/build.yaml
159 |
160 | This repository does not invent any of those wonderful tools.
161 | It merely provides an opinionated way to make them all fit together.
162 | By being more opinionated, it gets you set up faster.
163 |
164 | Services can then be installed by using NixOS options directly
165 | or through [Self Host Blocks](https://github.com/ibizaman/selfhostblocks).
166 | The latter, similarly to SkaraboxOS, provides an opinionated way to configure services in a seamless way.
167 |
168 | ## Why?
169 |
170 | Because the landscape of installing NixOS could be better
171 | and this repository is an attempt at that.
172 |
173 | By the way, the name SkaraboxOS comes from the scarab (the animal),
174 | box (for the server) and OS (for Operating System).
175 | Scarab is spelled with a _k_ because it's kool.
176 | A scarab is a _very_ [strong][] animal representing well what this repository's intention.
177 |
178 | [strong]: https://en.wikipedia.org/wiki/Dung_beetle#Ecology_and_behavior
179 |
180 | ## Hardware Requirements
181 |
182 | SkaraboxOS is currently tailored for NAS users, not necessarily homelab users.
183 | It expects a particular hardware layout:
184 |
185 | - 1 or 2 SSD or NVMe drive for the OS.
186 | If 2, they will be formatted in Raid 1 (mirror) so each hard drive should have the same size.
187 | - 0 or 2 Hard drives that will store data.
188 | Capacity depends on the amount of data that will be stored.
189 | If 2, they will too be formatted in Raid 1.
190 |
191 | > [!WARNING]
192 | > The disks will be formatted and completely wiped out of data.
193 |
194 | ## Architecture
195 |
196 | The [Architecture][] document covers how all pieces fit together.
197 |
198 | [Architecture]: ./docs/architecture.md
199 |
200 | ## Roadmap
201 |
202 | All ideas are noted in [issues][]
203 | and prioritized issues can be found in the [milestones][].
204 |
205 | [issues]: https://github.com/ibizaman/skarabox/issues
206 | [milestones]: https://github.com/ibizaman/skarabox/milestones
207 |
208 | ## Contribute
209 |
210 | Contributions are very welcomed!
211 |
212 | To push to the cache, run for example:
213 |
214 | ```
215 | nix build --no-link --print-out-paths .#packages.x86_64-linux.beacon-vm \
216 | | nix run nixpkgs#cachix push selfhostblocks
217 | ```
218 |
219 | ## Links
220 |
221 | - https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/profiles/installation-device.nix
222 | - https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/installer/cd-dvd/installation-cd-base.nix
223 | - https://github.com/nix-community/nixos-anywhere/blob/main/docs/howtos/no-os.md#installing-on-a-machine-with-no-operating-system
224 |
--------------------------------------------------------------------------------
/docs/architecture.md:
--------------------------------------------------------------------------------
1 | # Architecture
2 |
3 | So you want to know more about how all pieces fit together in Skarabox?
4 | That's great. You're at the right place.
5 |
6 |
7 | - [Hardware](#hardware)
8 | - [ZFS root pool encryption](#zfs-root-pool-encryption)
9 | - [ZFS data pool encryption](#zfs-data-pool-encryption)
10 | - [Remote decryption of root pool on boot](#remote-decryption-of-root-pool-on-boot)
11 | - [SSH Access](#ssh-access)
12 | - [Erase your darlings](#erase-your-darlings)
13 | - [Host Key](#host-key)
14 | - [SOPS](#sops)
15 | - [hostid](#hostid)
16 | - [ZFS settings](#zfs-settings)
17 | - [Principles](#principles)
18 |
19 |
20 | ## Hardware
21 |
22 | In essence, we let [nixos-facter][] figure out what's needed.
23 |
24 | Would it fail to detect the hardware,
25 | we include an escape hatch by adding the two following options
26 | to the template's `configuration.nix` file,
27 | although we give them their default values:
28 |
29 | ```nix
30 | boot.initrd.availableKernelModules = [];
31 | hardware.enableAllHardware = false;
32 | ```
33 |
34 | For ZFS, we set the following option which sets up
35 | all the machinery for ZFS to work in initrd and afterwards.
36 | This all happens in [tasks/filesystems/zfs.nix][zfs.nix].
37 |
38 | ```nix
39 | boot.supportedFilesystems = [ "zfs" ];
40 | ```
41 |
42 | [nixos-facter]: https://github.com/nix-community/nixos-facter
43 | [zfs.nix]: https://github.com/NixOS/nixpkgs/blob/nixos-24.11/nixos/modules/tasks/filesystems/zfs.nix
44 |
45 | ## ZFS root pool encryption
46 |
47 | We want to encrypt the root pool with a passphrase
48 | that is _not_ stored on the host.
49 | We will need to enter it on every boot.
50 |
51 | The configuration lives in [./modules/disks.nix](../modules/disks.nix),
52 | under `disko.devices.zpool` and uses [disko][].
53 |
54 | [disko]: https://github.com/nix-community/disko
55 |
56 | For the root pool, the relevant encryption settings are:
57 |
58 | ```nix
59 | boot.supportedFilesystems = [ "zfs" ];
60 | boot.zfs.forceImportRoot = false;
61 |
62 | disko.devices.zpool.${cfg.rootPool} = {
63 | rootFsOptions = {
64 | encryption = "on";
65 | keyformat = "passphrase";
66 | keylocation = "file:///tmp/root_passphrase";
67 | };
68 | postCreateHook = ''
69 | zfs set keylocation="prompt" $pname
70 | '';
71 | };
72 | ```
73 |
74 | This means we will encrypt the zpool
75 | with the key located at `/tmp/root_passphrase`.
76 | After the encryption is done,
77 | we will switch the location of the key
78 | to `prompt` which means ZFS will prompt us
79 | to enter the key. That's indeed what we want:
80 | the key should not live on the server,
81 | otherwise what's the point?
82 |
83 | We also set `boot.forceImportRoot` to false
84 | because that's what's [recommended][forceImportRoot]
85 | but also because it won't work since we
86 | need to give the passphrase to decrypt it
87 | in the first place.
88 |
89 | We add zfs to the `boot.supportedFilesystems`
90 | option otherwise the kernel will not have the
91 | appropriate modules.
92 |
93 | [forceImportRoot]: https://search.nixos.org/options?channel=24.11&show=boot.zfs.forceImportRoot&from=0&size=50&sort=relevance&type=packages&query=forceimportroot
94 |
95 | Then, we actually need to copy over the passphrase
96 | during the installation process by adding the following
97 | argument to the `nixos-anywhere` command :
98 |
99 | ```bash
100 | --disk-encryption-keys /tmp/root_passphrase
101 | ```
102 |
103 | Now, on every boot, a prompt will appear asking us for the passphrase.
104 | We will see in a [later section](#remote-decryption-of-root-pool-on-boot)
105 | how to decrypt the root pool remotely.
106 |
107 | ## ZFS data pool encryption
108 |
109 | For the data pool, the idea is the same as for the [root pool](#zfs-root-pool-encryption).
110 | The difference is that we will store the passphrase
111 | inside the root pool partition, allowing us to unlock
112 | the data pool automatically after decrypting the root pool.
113 |
114 | The relevant encryption settings are:
115 |
116 | ```nix
117 | disko.devices.zpool.${cfg.dataPool} = {
118 | rootFsOptions = {
119 | encryption = "on";
120 | keyformat = "passphrase";
121 | keylocation = "file:///tmp/data_passphrase";
122 | };
123 | postCreateHook = ''
124 | zfs set keylocation="file:///persist/data_passphrase" $pname;
125 | '';
126 | }
127 |
128 | disko.devices.zpool.${cfg.rootPool}.datasets = {
129 | "safe/persist" = {
130 | type = "zfs_fs";
131 | mountpoint = "/persist";
132 | options.mountpoint = "legacy";
133 | postMountHook = ''
134 | cp /tmp/data_passphrase /mnt/persist/data_passphrase
135 | '';
136 | };
137 | };
138 |
139 | boot.zfs.extraPools = [ cfg.dataPool ];
140 | ```
141 |
142 | Similarly to the root pool, we will encrypt
143 | the zpool using the key located at `/tmp/data_passphrase`.
144 | We then switch the location of the key
145 | to `/persist/data_passphrase` which is a dataset
146 | on the root zpool which does not get rolled back
147 | upon reboot (see [Erase your darlings](#erase-your-darlings)).
148 | We copy the key as part of the `postMountHook` commands.
149 |
150 | This all means the data zpool gets decrypted automatically
151 | when the root zpool is,
152 | even though it uses a different key.
153 |
154 | The `extraPools` option is needed to automatically
155 | import the data pool.
156 |
157 | We then copy over the passphrase during the installation
158 | process by adding the following argument to the
159 | `nixos-anywhere` command:
160 |
161 | ```bash
162 | --disk-encryption-keys /tmp/data_passphrase
163 | ```
164 |
165 | ## Remote decryption of root pool on boot
166 |
167 | With the [config above](#zfs-root-pool-encryption),
168 | a prompt will appear during initrd
169 | which will prompt us to enter the root passphrase.
170 | This is all good if you have a keyboard and screen
171 | attached to the host but won't work if not.
172 |
173 | So here, we want to run an ssh server in initrd
174 | which allows us to unlock the root pool
175 | and continue the boot process.
176 |
177 | The relevant config is in [./modules/disks.nix](../modules/disks.nix):
178 |
179 | ```nix
180 | boot.initrd.network = {
181 | enable = true;
182 |
183 | udhcpc.enable = lib.mkDefault true;
184 |
185 | ssh = {
186 | enable = true;
187 | port = lib.mkDefault cfg.bootSSHPort;
188 | authorizedKeyFiles = [
189 | .//ssh.pub
190 | ];
191 | };
192 |
193 | postCommands = ''
194 | zpool import -a
195 | echo "zfs load-key ${cfg.rootPool}; killall zfs; exit" >> /root/.profile
196 | '';
197 | ```
198 |
199 | We enable `boot.initrd.network` and the `.ssh` options.
200 | We set the port to 2222 by default.
201 | We add an ssh public key so we can connect as the root user.
202 |
203 | This ssh public key is generated as part of the [initialization](../lib/gen-initial.nix)
204 | process in `.//ssh.pub` and the private key in `.//ssh`.
205 | We also add that file to `.gitignore` to ensure
206 | we don't store the private file in the repo.
207 |
208 | The commands in `postCommands` are executed when the sshd
209 | daemon has started. The command added in `/root/.profile` will
210 | be executed when we log in through SSH.
211 | This results in ZFS prompting us to enter the
212 | root zpool's passphrase as soon as we're logged in.
213 |
214 | The `udhcpc.enable` option enables DHCP.
215 | Allowing a static IP here is planned.
216 |
217 | If by any change the kernel does not try to connect to the network
218 | and fails to ask for an IP and no error message is shown,
219 | this probably means that the driver for the hardware has failed
220 | loading or that nixos-facter has failed to detect the hardware.
221 | See [Hardware](#hardware) for how to fix this.
222 |
223 | ## SSH Access
224 |
225 | Here, we enable SSH access to the host after it has booted.
226 | We want a password-less connection
227 | and also to pre-validate the host key of the host.
228 | This means we won't let the host generate its own host key,
229 | we will generate it ourselves and add it to a known hosts
230 | file upon installation.
231 |
232 | This last step is often neglected for convenience reasons
233 | but it is important to make sure we connect to the correct
234 | host from the start. [This section](#host-key) goes into
235 | details on how it's done.
236 |
237 | For non-initrd ssh access, we add the ssh public key
238 | to the `authorizedKeys` file of the user:
239 |
240 | ```nix
241 | users.users.${config.skarabox.username} = {
242 | openssh.authorizedKeys.keyFiles = [
243 | config.skarabox.sshAuthorizedKeyFile
244 | ];
245 | };
246 | ```
247 |
248 | For the initrd ssh access, to decrypt the root partition,
249 | the configuration is similar although here the user is `root`:
250 |
251 | ```nix
252 | boot.initrd.network = {
253 | ssh.authorizedKeyFiles = [
254 | config.skarabox.sshAuthorizedKeyFile
255 | ];
256 | };
257 | ```
258 |
259 | For the firmware, we use nixos-facter to figure it out.
260 |
261 | ## Erase your darlings
262 |
263 | The idea here is to explicitly list which directories one wants
264 | to save. The rest will be lost on reboots.
265 | I learned about it from Graham Christensen
266 | and recommend [their blog post][eyd] to understand the motivation.
267 |
268 | [eyd]: https://grahamc.com/blog/erase-your-darlings/
269 |
270 | We implement this by creating a root dataset mounted at `/`
271 | which will get rolled back on every boot:
272 |
273 | ```nix
274 | disko.devices.zpool.${cfg.rootPool}.datasets."local/root" = {
275 | type = "zfs_fs";
276 | mountpoint = "/";
277 | options.mountpoint = "legacy";
278 | postCreateHook = ''
279 | zfs list -t snapshot -H -o name \
280 | | grep -E '^${cfg.rootPool}/local/root@blank$' \
281 | || zfs snapshot ${cfg.rootPool}/local/root@blank
282 | '';
283 | };
284 | ```
285 |
286 | The `postCreateHook` creates a new zfs snapshot during the installation
287 | process. The `grep` part is to make sure we only create one such
288 | snapshot, in case we run the installation process multiple times.
289 | This snapshot is thus empty.
290 |
291 | Now, we revert back to the snapshot upon every boot with:
292 |
293 | ```nix
294 | boot.initrd.postResumeCommands = lib.mkAfter ''
295 | zfs rollback -r ${cfg.rootPool}/local/root@blank
296 | '';
297 | ```
298 |
299 | To save a directory, we must create a dataset and mount it:
300 |
301 | ```nix
302 | disko.devices.zpool.${cfg.rootPool}.datasets."local/nix" = {
303 | type = "zfs_fs";
304 | mountpoint = "/nix";
305 | options.mountpoint = "legacy";
306 | };
307 | ```
308 |
309 | ## Host Key
310 |
311 | By default, upon starting, the sshd systemd service
312 | will generate some host keys under `/etc/ssh` if that
313 | folder is empty.
314 |
315 | When connecting through ssh for the first time,
316 | the ssh client will prompt about verifying the host
317 | key of the server.
318 |
319 | Providing the host key ourselves allows us to skip
320 | this test since we know the host key in advance
321 | and can generate the relevant `known_hosts` file.
322 |
323 | The config for this is simply to copy the `host_key`
324 | in some temporary location by (ab)using the
325 | `disk-encryption-keys` flag for `nixos-anywhere`:
326 |
327 | ```bash
328 | --disk-encryption-keys /tmp/host_key
329 | ```
330 |
331 | Then, we copy the host_key in a _not encrypted_ location.
332 | This is necessary otherwise we can't use it in the initrd phase.
333 |
334 | ```nix
335 | disko.devices.disk."root" = {
336 | type = "disk";
337 | content = {
338 | type = "gpt";
339 | partitions = {
340 | ESP = {
341 | type = "EF00";
342 | content = {
343 | type = "filesystem";
344 | format = "vfat";
345 | mountpoint = "/boot";
346 | postMountHook = ''
347 | cp /tmp/host_key /mnt/boot/host_key
348 | '';
349 | };
350 | };
351 | };
352 | };
353 | };
354 | ```
355 |
356 | The only relevant configuration is the `postMountHook` but
357 | I included the rest here to give some context.
358 |
359 | Then, we use that key from this new location in the initrd ssh daemon:
360 |
361 | ```nix
362 | boot.initrd.network.ssh.hostKeys = lib.mkForce [ "/boot/host_key" ];
363 | ```
364 |
365 | We override the whole list with `mkForce` to avoid the default
366 | behavior of a list option which is to merge.
367 | Here, we don't want any of the default automatic generation.
368 |
369 | For the non-initrd ssh daemon,
370 | we force an empty list so the nix module does not generate any ssh key
371 | and we instead tell the location of our host key:
372 |
373 |
374 | ```nix
375 | services.openssh = {
376 | hostKeys = lib.mkForce [];
377 | extraConfig = ''
378 | HostKey /boot/host_key
379 | '';
380 | };
381 | ```
382 |
383 | ## SOPS
384 |
385 | To store the secrets, we use [sops-nix][] which stores the secrets
386 | encrypted in the repository, here in a `.//secrets.yaml` file.
387 | It's creation and update is governed by a unique `./.sops.yaml` file.
388 |
389 | The process to create this SOPS file is quite involved
390 | but is fully automatic, so that's nice.
391 |
392 | Note that we use one separate secrets file per host to avoid sharing
393 | secrets across hosts and avoid leaking secrets this way.
394 | It is possible to have shared secrets if needed but
395 | not supported out of the box.
396 |
397 | [sops-nix]: https://github.com/Mic92/sops-nix
398 |
399 | We must allow us, the user, to decrypt this `.//secrets.yaml` file
400 | as well as allow the target host to decrypt it.
401 | This means we need to encrypt the file with two keys.
402 |
403 | The user's SOPS private key is generated in [gen-initial.nix][] with:
404 | ```bash
405 | age-keygen -o sops.key
406 | ```
407 |
408 | [gen-initial.nix]: ../lib/gen-initial.nix
409 |
410 | and get the associated SOPS public key with:
411 |
412 | ```bash
413 | age-keygen -y sops.key
414 | ```
415 |
416 | By the way, we add that file to `.gitignore` to ensure
417 | we don't store the private file in the repo.
418 |
419 | The hosts' SOPS public key is derived from the host' public ssh key
420 | we generated [earlier](#host-key) in `.//host_key.pub` with:
421 |
422 | ```bash
423 | cat host_key.pub | ssh-to-age
424 | ```
425 |
426 | We then use those two SOPS public keys to create the configuration
427 | file `.sops.yaml`:
428 |
429 | ```yaml
430 |
431 | keys:
432 | - &me age1sz...
433 | - &server age1ys...
434 | creation_rules:
435 | - path_regex: secrets\.yaml$
436 | key_groups:
437 | - age:
438 | - *me
439 | - *server
440 | ```
441 |
442 | This Sops config file is managed programmatically with some
443 | home brew scripts.
444 |
445 | And finally we encrypt the `secrets.yaml` file with:
446 |
447 | ```bash
448 | SOPS_AGE_KEY_FILE=sops.key sops encrypt -i secrets.yaml
449 | ```
450 |
451 | Note the `.//secrets.yaml` cannot be empty to be encrypted,
452 | that's a limitation of SOPS itself.
453 |
454 | We only add secrets to the `.//secrets.yaml` file
455 | after it has been encrypted, as an added precaution.
456 | This is done by using the `set` [subcommand][set] of the `sops` command.
457 |
458 | Similarly, we can decrypt one value with the `decrypt --extract` [option][extract].
459 |
460 | [set]: https://github.com/getsops/sops?tab=readme-ov-file#set-a-sub-part-in-a-document-tree
461 | [extract]: https://github.com/getsops/sops?tab=readme-ov-file#45extract-a-sub-part-of-a-document-tree
462 |
463 | ## hostid
464 |
465 | The `hostid` must be unique and not change during the lifetime of the server.
466 | It is only used by ZFS which refuses to import the pools if the `hostid` changes.
467 |
468 | It is generated with:
469 |
470 | ```bash
471 | uuidgen | head -c 8
472 | ```
473 |
474 | And its configuration is trivial:
475 |
476 | ```nix
477 | networking.hostId = .//hostid;
478 | ```
479 |
480 | ## ZFS settings
481 |
482 | I wrote a [blog post][] about these.
483 | I'm not an expert on ZFS,
484 | I mostly did some extensive research.
485 |
486 | [blog post]: https://blog.tiserbox.com/posts/2024-02-09-zfs-on-nix-os.html
487 |
488 | ## Principles
489 |
490 | I'm trying to follow these principles as I implement features.
491 | I find they tend to lead to a polished experience
492 | and a maintainable code base.
493 |
494 | - Less manual steps possible.
495 |
496 | Generate secrets automatically, create values with good defaults.
497 |
498 | - All commands should be locked in the template's flake.
499 |
500 | For example, instead of instructing the user to run a command with:
501 |
502 | ```bash
503 | nix run nixpkgs#openssh
504 | ```
505 |
506 | we add the package to the flake:
507 |
508 | ```nix
509 | {
510 | inputs.nixpkgs = ...;
511 |
512 | outputs = { nixpkgs, ... }: {
513 | packages.x86_64-linux = {
514 | inherit (nixpkgs) openssh;
515 | };
516 | };
517 | }
518 | ```
519 |
520 | then instruct the user to use that version of openssh:
521 |
522 | ```bash
523 | nix run .#openssh
524 | ```
525 |
526 | This makes sure that the versions of all commands
527 | match what we expect and avoids one class of problem.
528 |
529 | - The template's flake.nix file should be as empty as possible
530 | and instead provide a small layer on top of Skarabox' flake.
531 | This way, updates are easier to handle by the user since
532 | they don't need to update their flake.nix file.
533 |
534 | Similarly, the template's flake.nix should provide
535 | sensible defaults on top of Skarabox' flake.
536 | For example, if Skarabox' flake provides a function
537 | to generate a file:
538 |
539 | ```nix
540 | mkFile = pkgs.writeShellScriptBin "mkFile" ''
541 | mkdir -p $1
542 | touch $1/$2
543 | '';
544 | ```
545 |
546 | The template's flake fills out the required arguments
547 | using the secrets in the template:
548 |
549 |
550 | ```nix
551 | mkFile = pkgs.writeShellScriptBin "mkFile" ''
552 | ${inputs'.skarabox.packages.mkFile}/bin/mkFile \
553 | ${builtins.readFile ./dir} \
554 | ${builtins.readFile ./file} \
555 | '';
556 | ```
557 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "deploy-rs": {
4 | "inputs": {
5 | "flake-compat": "flake-compat",
6 | "nixpkgs": "nixpkgs",
7 | "utils": "utils"
8 | },
9 | "locked": {
10 | "lastModified": 1727447169,
11 | "narHash": "sha256-3KyjMPUKHkiWhwR91J1YchF6zb6gvckCAY1jOE+ne0U=",
12 | "owner": "serokell",
13 | "repo": "deploy-rs",
14 | "rev": "aa07eb05537d4cd025e2310397a6adcedfe72c76",
15 | "type": "github"
16 | },
17 | "original": {
18 | "owner": "serokell",
19 | "repo": "deploy-rs",
20 | "type": "github"
21 | }
22 | },
23 | "disko": {
24 | "inputs": {
25 | "nixpkgs": [
26 | "nixos-anywhere",
27 | "nixpkgs"
28 | ]
29 | },
30 | "locked": {
31 | "lastModified": 1741786315,
32 | "narHash": "sha256-VT65AE2syHVj6v/DGB496bqBnu1PXrrzwlw07/Zpllc=",
33 | "owner": "nix-community",
34 | "repo": "disko",
35 | "rev": "0d8c6ad4a43906d14abd5c60e0ffe7b587b213de",
36 | "type": "github"
37 | },
38 | "original": {
39 | "owner": "nix-community",
40 | "ref": "master",
41 | "repo": "disko",
42 | "type": "github"
43 | }
44 | },
45 | "flake-compat": {
46 | "flake": false,
47 | "locked": {
48 | "lastModified": 1696426674,
49 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
50 | "owner": "edolstra",
51 | "repo": "flake-compat",
52 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
53 | "type": "github"
54 | },
55 | "original": {
56 | "owner": "edolstra",
57 | "repo": "flake-compat",
58 | "type": "github"
59 | }
60 | },
61 | "flake-parts": {
62 | "inputs": {
63 | "nixpkgs-lib": "nixpkgs-lib"
64 | },
65 | "locked": {
66 | "lastModified": 1741352980,
67 | "narHash": "sha256-+u2UunDA4Cl5Fci3m7S643HzKmIDAe+fiXrLqYsR2fs=",
68 | "owner": "hercules-ci",
69 | "repo": "flake-parts",
70 | "rev": "f4330d22f1c5d2ba72d3d22df5597d123fdb60a9",
71 | "type": "github"
72 | },
73 | "original": {
74 | "owner": "hercules-ci",
75 | "repo": "flake-parts",
76 | "type": "github"
77 | }
78 | },
79 | "flake-parts_2": {
80 | "inputs": {
81 | "nixpkgs-lib": [
82 | "nixos-anywhere",
83 | "nixpkgs"
84 | ]
85 | },
86 | "locked": {
87 | "lastModified": 1741352980,
88 | "narHash": "sha256-+u2UunDA4Cl5Fci3m7S643HzKmIDAe+fiXrLqYsR2fs=",
89 | "owner": "hercules-ci",
90 | "repo": "flake-parts",
91 | "rev": "f4330d22f1c5d2ba72d3d22df5597d123fdb60a9",
92 | "type": "github"
93 | },
94 | "original": {
95 | "owner": "hercules-ci",
96 | "repo": "flake-parts",
97 | "type": "github"
98 | }
99 | },
100 | "nix-flake-tests": {
101 | "locked": {
102 | "lastModified": 1677844186,
103 | "narHash": "sha256-ErJZ/Gs1rxh561CJeWP5bohA2IcTq1rDneu1WT6CVII=",
104 | "owner": "antifuchs",
105 | "repo": "nix-flake-tests",
106 | "rev": "bbd9216bd0f6495bb961a8eb8392b7ef55c67afb",
107 | "type": "github"
108 | },
109 | "original": {
110 | "owner": "antifuchs",
111 | "repo": "nix-flake-tests",
112 | "type": "github"
113 | }
114 | },
115 | "nixlib": {
116 | "locked": {
117 | "lastModified": 1736643958,
118 | "narHash": "sha256-tmpqTSWVRJVhpvfSN9KXBvKEXplrwKnSZNAoNPf/S/s=",
119 | "owner": "nix-community",
120 | "repo": "nixpkgs.lib",
121 | "rev": "1418bc28a52126761c02dd3d89b2d8ca0f521181",
122 | "type": "github"
123 | },
124 | "original": {
125 | "owner": "nix-community",
126 | "repo": "nixpkgs.lib",
127 | "type": "github"
128 | }
129 | },
130 | "nixos-anywhere": {
131 | "inputs": {
132 | "disko": "disko",
133 | "flake-parts": "flake-parts_2",
134 | "nixos-images": "nixos-images",
135 | "nixos-stable": "nixos-stable",
136 | "nixpkgs": [
137 | "nixpkgs"
138 | ],
139 | "treefmt-nix": "treefmt-nix"
140 | },
141 | "locked": {
142 | "lastModified": 1742701397,
143 | "narHash": "sha256-xkr0Bl6LShu0S0ubG+mS0uokPzmZLvm1pRzDFesgugg=",
144 | "owner": "nix-community",
145 | "repo": "nixos-anywhere",
146 | "rev": "d48c8a01968afc8870b5afcba43b7739c943f7f8",
147 | "type": "github"
148 | },
149 | "original": {
150 | "owner": "nix-community",
151 | "repo": "nixos-anywhere",
152 | "type": "github"
153 | }
154 | },
155 | "nixos-facter-modules": {
156 | "locked": {
157 | "lastModified": 1743671943,
158 | "narHash": "sha256-7sYig0+RcrR3sOL5M+2spbpFUHyEP7cnUvCaqFOBjyU=",
159 | "owner": "numtide",
160 | "repo": "nixos-facter-modules",
161 | "rev": "58ad9691670d293a15221d4a78818e0088d2e086",
162 | "type": "github"
163 | },
164 | "original": {
165 | "owner": "numtide",
166 | "repo": "nixos-facter-modules",
167 | "type": "github"
168 | }
169 | },
170 | "nixos-generators": {
171 | "inputs": {
172 | "nixlib": "nixlib",
173 | "nixpkgs": [
174 | "nixpkgs"
175 | ]
176 | },
177 | "locked": {
178 | "lastModified": 1742568034,
179 | "narHash": "sha256-QaMEhcnscfF2MqB7flZr+sLJMMYZPnvqO4NYf9B4G38=",
180 | "owner": "nix-community",
181 | "repo": "nixos-generators",
182 | "rev": "42ee229088490e3777ed7d1162cb9e9d8c3dbb11",
183 | "type": "github"
184 | },
185 | "original": {
186 | "owner": "nix-community",
187 | "repo": "nixos-generators",
188 | "type": "github"
189 | }
190 | },
191 | "nixos-images": {
192 | "inputs": {
193 | "nixos-stable": [
194 | "nixos-anywhere",
195 | "nixos-stable"
196 | ],
197 | "nixos-unstable": [
198 | "nixos-anywhere",
199 | "nixpkgs"
200 | ]
201 | },
202 | "locked": {
203 | "lastModified": 1742432671,
204 | "narHash": "sha256-6M0lxz78i79n0UUm6GP/r7zMFXWr0V7gZhpnmtLSlJQ=",
205 | "owner": "nix-community",
206 | "repo": "nixos-images",
207 | "rev": "55f23642b75d501387691a22a7e86fbc22d06372",
208 | "type": "github"
209 | },
210 | "original": {
211 | "owner": "nix-community",
212 | "repo": "nixos-images",
213 | "type": "github"
214 | }
215 | },
216 | "nixos-stable": {
217 | "locked": {
218 | "lastModified": 1742512142,
219 | "narHash": "sha256-8XfURTDxOm6+33swQJu/hx6xw1Tznl8vJJN5HwVqckg=",
220 | "owner": "NixOS",
221 | "repo": "nixpkgs",
222 | "rev": "7105ae3957700a9646cc4b766f5815b23ed0c682",
223 | "type": "github"
224 | },
225 | "original": {
226 | "owner": "NixOS",
227 | "ref": "nixos-24.11",
228 | "repo": "nixpkgs",
229 | "type": "github"
230 | }
231 | },
232 | "nixpkgs": {
233 | "locked": {
234 | "lastModified": 1702272962,
235 | "narHash": "sha256-D+zHwkwPc6oYQ4G3A1HuadopqRwUY/JkMwHz1YF7j4Q=",
236 | "owner": "NixOS",
237 | "repo": "nixpkgs",
238 | "rev": "e97b3e4186bcadf0ef1b6be22b8558eab1cdeb5d",
239 | "type": "github"
240 | },
241 | "original": {
242 | "owner": "NixOS",
243 | "ref": "nixpkgs-unstable",
244 | "repo": "nixpkgs",
245 | "type": "github"
246 | }
247 | },
248 | "nixpkgs-lib": {
249 | "locked": {
250 | "lastModified": 1740877520,
251 | "narHash": "sha256-oiwv/ZK/2FhGxrCkQkB83i7GnWXPPLzoqFHpDD3uYpk=",
252 | "owner": "nix-community",
253 | "repo": "nixpkgs.lib",
254 | "rev": "147dee35aab2193b174e4c0868bd80ead5ce755c",
255 | "type": "github"
256 | },
257 | "original": {
258 | "owner": "nix-community",
259 | "repo": "nixpkgs.lib",
260 | "type": "github"
261 | }
262 | },
263 | "nixpkgs_2": {
264 | "locked": {
265 | "lastModified": 1742669843,
266 | "narHash": "sha256-G5n+FOXLXcRx+3hCJ6Rt6ZQyF1zqQ0DL0sWAMn2Nk0w=",
267 | "owner": "nixos",
268 | "repo": "nixpkgs",
269 | "rev": "1e5b653dff12029333a6546c11e108ede13052eb",
270 | "type": "github"
271 | },
272 | "original": {
273 | "owner": "nixos",
274 | "ref": "nixos-unstable",
275 | "repo": "nixpkgs",
276 | "type": "github"
277 | }
278 | },
279 | "root": {
280 | "inputs": {
281 | "deploy-rs": "deploy-rs",
282 | "flake-parts": "flake-parts",
283 | "nix-flake-tests": "nix-flake-tests",
284 | "nixos-anywhere": "nixos-anywhere",
285 | "nixos-facter-modules": "nixos-facter-modules",
286 | "nixos-generators": "nixos-generators",
287 | "nixpkgs": "nixpkgs_2"
288 | }
289 | },
290 | "systems": {
291 | "locked": {
292 | "lastModified": 1681028828,
293 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
294 | "owner": "nix-systems",
295 | "repo": "default",
296 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
297 | "type": "github"
298 | },
299 | "original": {
300 | "owner": "nix-systems",
301 | "repo": "default",
302 | "type": "github"
303 | }
304 | },
305 | "treefmt-nix": {
306 | "inputs": {
307 | "nixpkgs": [
308 | "nixos-anywhere",
309 | "nixpkgs"
310 | ]
311 | },
312 | "locked": {
313 | "lastModified": 1742370146,
314 | "narHash": "sha256-XRE8hL4vKIQyVMDXykFh4ceo3KSpuJF3ts8GKwh5bIU=",
315 | "owner": "numtide",
316 | "repo": "treefmt-nix",
317 | "rev": "adc195eef5da3606891cedf80c0d9ce2d3190808",
318 | "type": "github"
319 | },
320 | "original": {
321 | "owner": "numtide",
322 | "repo": "treefmt-nix",
323 | "type": "github"
324 | }
325 | },
326 | "utils": {
327 | "inputs": {
328 | "systems": "systems"
329 | },
330 | "locked": {
331 | "lastModified": 1701680307,
332 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
333 | "owner": "numtide",
334 | "repo": "flake-utils",
335 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
336 | "type": "github"
337 | },
338 | "original": {
339 | "owner": "numtide",
340 | "repo": "flake-utils",
341 | "type": "github"
342 | }
343 | }
344 | },
345 | "root": "root",
346 | "version": 7
347 | }
348 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Skarabox's flake to install NixOS";
3 |
4 | inputs = {
5 | nixpkgs = {
6 | url = "github:nixos/nixpkgs/nixos-unstable";
7 | };
8 |
9 | nixos-generators = {
10 | url = "github:nix-community/nixos-generators";
11 | inputs.nixpkgs.follows = "nixpkgs";
12 | };
13 |
14 | nixos-anywhere = {
15 | url = "github:nix-community/nixos-anywhere";
16 | inputs.nixpkgs.follows = "nixpkgs";
17 | };
18 |
19 | nixos-facter-modules = {
20 | url = "github:numtide/nixos-facter-modules";
21 | };
22 |
23 | flake-parts = {
24 | url = "github:hercules-ci/flake-parts";
25 | };
26 |
27 | deploy-rs = {
28 | url = "github:serokell/deploy-rs";
29 | };
30 |
31 | nix-flake-tests = {
32 | url = "github:antifuchs/nix-flake-tests";
33 | };
34 | };
35 |
36 | outputs = inputs@{
37 | self,
38 | flake-parts,
39 | nixpkgs,
40 | nixos-anywhere,
41 | nixos-facter-modules,
42 | deploy-rs,
43 | nix-flake-tests,
44 | ...
45 | }: flake-parts.lib.mkFlake { inherit inputs; } {
46 | systems = [
47 | "x86_64-linux"
48 | "aarch64-linux"
49 | ];
50 |
51 | perSystem = { self', inputs', pkgs, system, ... }: {
52 | packages = rec {
53 | # Usage:
54 | # init [-h] [-y] [-s] [-v] [-p PATH]
55 | #
56 | # print help:
57 | # init -h
58 | init = import ./lib/gen-initial.nix {
59 | inherit pkgs gen-new-host sops-create-main-key sops-add-main-key;
60 | };
61 |
62 | add-sops-cfg = import ./lib/add-sops-cfg.nix {
63 | inherit pkgs;
64 | };
65 |
66 | sops-create-main-key = import ./lib/sops-create-main-key.nix {
67 | inherit pkgs;
68 | };
69 |
70 | sops-add-main-key = import ./lib/sops-add-main-key.nix {
71 | inherit pkgs add-sops-cfg;
72 | };
73 |
74 | gen-new-host = import ./lib/gen-new-host.nix {
75 | inherit add-sops-cfg pkgs;
76 | };
77 | };
78 |
79 | checks = import ./tests {
80 | inherit pkgs system nix-flake-tests;
81 | };
82 |
83 | # Used to experiment with ruamel library.
84 | devShells.pythonShell = pkgs.mkShell {
85 | packages = [
86 | (pkgs.python3.withPackages (python-pkgs: [
87 | python-pkgs.ruamel-yaml
88 | ]))
89 | ];
90 | };
91 | };
92 |
93 | flake = {
94 | skaraboxInputs = inputs;
95 |
96 | flakeModules.default = ./flakeModule.nix;
97 |
98 | templates = {
99 | skarabox = {
100 | path = ./template;
101 | description = "Skarabox template";
102 | };
103 |
104 | default = self.templates.skarabox;
105 | };
106 |
107 | nixosModules.skarabox = {
108 | imports = [
109 | nixos-anywhere.inputs.disko.nixosModules.disko
110 | nixos-facter-modules.nixosModules.facter
111 | ./modules/disks.nix
112 | ./modules/configuration.nix
113 | ];
114 | };
115 |
116 | nix-ci = {
117 | cachix = {
118 | name = "selfhostblocks";
119 | public-key = "selfhostblocks.cachix.org-1:H5h6Uj188DObUJDbEbSAwc377uvcjSFOfpxyCFP7cVs=";
120 | };
121 | };
122 | };
123 | };
124 | }
125 |
--------------------------------------------------------------------------------
/flakeModule.nix:
--------------------------------------------------------------------------------
1 | {
2 | config,
3 | lib,
4 | inputs,
5 | ...
6 | }:
7 | let
8 | topLevelConfig = config;
9 | cfg = config.skarabox;
10 |
11 | inherit (lib) concatMapAttrs mkOption types toInt;
12 |
13 | readAndTrim = f: lib.strings.trim (builtins.readFile f);
14 | readAsStr = v: if lib.isPath v then readAndTrim v else v;
15 | readAsInt = v: let
16 | vStr = readAsStr v;
17 | in
18 | if lib.isString vStr then toInt vStr else vStr;
19 |
20 | beacon-module = { config, lib, modulesPath, ... }: {
21 | imports = [
22 | ./modules/beacon.nix
23 | (modulesPath + "/profiles/minimal.nix")
24 | ];
25 | };
26 | in
27 | {
28 | options.skarabox = {
29 | sopsKeyName = mkOption {
30 | # Using string here so the sops key does not end up in the nix store.
31 | type = types.str;
32 | default = "sops.key";
33 | };
34 |
35 | hosts = mkOption {
36 | type = types.attrsOf (types.submodule ({ name, ... }: {
37 | options = {
38 | hostKeyName = mkOption {
39 | type = types.str;
40 | default = "host_key";
41 | };
42 | hostKeyPub = mkOption {
43 | type = types.path;
44 | };
45 |
46 | ip = mkOption {
47 | type = with types; oneOf [ str path ];
48 | default = "127.0.0.1";
49 | apply = readAsStr;
50 | };
51 | sshPrivateKeyName = mkOption {
52 | # Using string here so the sops key does not end up in the nix store.
53 | type = types.str;
54 | default = "ssh";
55 | };
56 | secretsFileName = mkOption {
57 | type = types.str;
58 | default = "secrets.yaml";
59 | };
60 | secretsRootPassphrasePath = mkOption {
61 | type = types.str;
62 | default = "['${name}']['disks']['rootPassphrase']";
63 | };
64 | secretsDataPassphrasePath = mkOption {
65 | type = types.str;
66 | default = "['${name}']['disks']['dataPassphrase']";
67 | };
68 | sshPublicKey = mkOption {
69 | type = types.path;
70 | };
71 | knownHostsName = mkOption {
72 | type = types.str;
73 | default = "known_hosts";
74 | };
75 | knownHosts = mkOption {
76 | type = types.path;
77 | };
78 | sshPort = mkOption {
79 | type = with types; oneOf [ int str path ];
80 | default = 22;
81 | apply = readAsInt;
82 | };
83 | sshBootPort = mkOption {
84 | type = with types; oneOf [ int str path ];
85 | default = 2222;
86 | apply = readAsInt;
87 | };
88 | system = mkOption {
89 | type = with types; oneOf [ str path ];
90 | apply = readAsStr;
91 | };
92 |
93 | modules = mkOption {
94 | type = types.listOf types.anything;
95 | default = [];
96 | };
97 | };
98 | }));
99 | };
100 | };
101 |
102 | config = {
103 | perSystem = { self', inputs', config, pkgs, system, ... }: let
104 | sops = pkgs.writeShellApplication {
105 | name = "sops";
106 |
107 | runtimeInputs = [
108 | pkgs.sops
109 | ];
110 |
111 | text = ''
112 | SOPS_AGE_KEY_FILE=${cfg.sopsKeyName} sops "$@"
113 | '';
114 | };
115 |
116 | mkHostPackages = name: cfg': let
117 | # nix run .#boot-ssh [ ...]
118 | # nix run .#boot-ssh
119 | # nix run .#boot-ssh echo hello
120 | boot-ssh = pkgs.writeShellApplication {
121 | name = "boot-ssh";
122 |
123 | runtimeInputs = [
124 | (import ./lib/ssh.nix {
125 | inherit pkgs;
126 | })
127 | ];
128 |
129 | text = ''
130 | ssh \
131 | "${cfg'.ip}" \
132 | "${toString cfg'.sshBootPort}" \
133 | root \
134 | -o UserKnownHostsFile=${cfg'.knownHosts} \
135 | -o ConnectTimeout=10 \
136 | -i ${name}/${cfg'.sshPrivateKeyName} \
137 | "$*"
138 | '';
139 | };
140 |
141 | # Create an ISO file with the beacon.
142 | #
143 | # This ISO file will need to be burned on a USB stick.
144 | # This can be done for example with usbimager that's
145 | # included in the template.
146 | beacon = inputs.nixos-generators.nixosGenerate {
147 | inherit system;
148 | format = "install-iso";
149 |
150 | modules = [
151 | beacon-module
152 | {
153 | skarabox.sshPublicKey = cfg'.sshPublicKey;
154 | }
155 | ];
156 | };
157 |
158 | # Create and Start a VM that boots the ISO file with the beacon.
159 | #
160 | # Useful for testing a full installation.
161 | # This VM comes with 3 disks, one under /dev/nvme0n1
162 | # and the two other under /dev/sda and /dev/sdb. This
163 | # setup imitates a real server with one SSD disk for
164 | # the OS and two HDDs in mirror for the data.
165 | #
166 | # nix run .#beacon-vm [ []]
167 | #
168 | # host-port: Host part of the port forwarding for the SSH server
169 | # when the VM is booted.
170 | # (default: 2222)
171 | # host-boot-port: Host port of the port forwarding for the SSH server
172 | # used to decrypt the root partition upon booting
173 | # or rebooting after the installation process is done.
174 | # (default: 2223)
175 | #
176 | beacon-vm = let
177 | iso = inputs.nixos-generators.nixosGenerate {
178 | inherit system;
179 | format = "install-iso";
180 |
181 | modules = [
182 | beacon-module
183 | {
184 | skarabox.sshPublicKey = cfg'.sshPublicKey;
185 | }
186 | ({ lib, modulesPath, ... }: {
187 | imports = [
188 | # This profile adds virtio drivers needed in the guest
189 | # to be able to share the /nix/store folder.
190 | (modulesPath + "/profiles/qemu-guest.nix")
191 | ];
192 |
193 | config.services.openssh.ports = lib.mkForce [ 2222 ];
194 |
195 | # Since this is the VM and we will mount the hosts' nix store,
196 | # we do not need to create a squashfs file.
197 | config.isoImage.storeContents = lib.mkForce [];
198 |
199 | # Share the host's nix store instead of the one created for the ISO.
200 | # config.lib.isoFileSystems is defined in nixos/modules/installer/cd-dvd/iso-image.nix
201 | config.lib.isoFileSystems = {
202 | "/nix/.ro-store" = lib.mkForce {
203 | device = "nix-store";
204 | fsType = "9p";
205 | neededForBoot = true;
206 | options = [
207 | "trans=virtio"
208 | "version=9p2000.L"
209 | "msize=16384"
210 | "x-systemd.requires=modprobe@9pnet_virtio.service"
211 | "cache=loose"
212 | ];
213 | };
214 | };
215 | })
216 | ];
217 | };
218 | nixos-qemu = pkgs.callPackage "${pkgs.path}/nixos/lib/qemu-common.nix" {};
219 | qemu = nixos-qemu.qemuBinary pkgs.qemu;
220 | # About bootindex. On first boot, the nvme* drives cannot boot
221 | # so we will instead boot on the cdrom. After a successful installation,
222 | # we will be able to boot on the nvme* drives instead.
223 | in (pkgs.writeShellScriptBin "beacon-vm" ''
224 | diskRoot1=.skarabox-tmp/diskRoot1.qcow2
225 | diskRoot2=.skarabox-tmp/diskRoot2.qcow2
226 | diskData1=.skarabox-tmp/diskData1.qcow2
227 | diskData2=.skarabox-tmp/diskData2.qcow2
228 |
229 | mkdir -p .skarabox-tmp
230 | for d in $diskRoot1 $diskRoot2 $diskData1 $diskData2; do
231 | [ ! -f $d ] && ${pkgs.qemu}/bin/qemu-img create -f qcow2 $d 20G
232 | done
233 |
234 | set -x
235 |
236 | guestport=2222
237 | hostport=${toString cfg'.sshPort}
238 | guestbootport=2223
239 | hostbootport=${toString cfg'.sshBootPort}
240 |
241 | ${qemu} \
242 | -m 2048M \
243 | -device virtio-rng-pci \
244 | -net nic -net user,hostfwd=tcp::''${hostport}-:''${guestport},hostfwd=tcp::''${hostbootport}-:''${guestbootport} \
245 | --virtfs local,path=/nix/store,security_model=none,mount_tag=nix-store \
246 | --drive if=pflash,format=raw,unit=0,readonly=on,file=${pkgs.OVMF.firmware} \
247 | --drive media=cdrom,format=raw,readonly=on,file=${iso}/iso/beacon.iso \
248 | --drive format=qcow2,file=$diskRoot1,if=none,id=diskRoot1 \
249 | --device nvme,drive=diskRoot1,serial=nvme0,bootindex=1 \
250 | --drive format=qcow2,file=$diskRoot2,if=none,id=diskRoot2 \
251 | --device nvme,drive=diskRoot2,serial=nvme1,bootindex=2 \
252 | --drive id=diskData1,format=qcow2,if=none,file=$diskData1 \
253 | --device ide-hd,drive=diskData1,serial=sda \
254 | --drive id=diskData2,format=qcow2,if=none,file=$diskData2 \
255 | --device ide-hd,drive=diskData2,serial=sdb \
256 | $@
257 | '');
258 |
259 | # Generate knownhosts file.
260 | #
261 | # gen-knownhosts-file [...]
262 | #
263 | # One line will be generated per port given.
264 | gen-knownhosts-file = pkgs.writeShellApplication {
265 | name = "gen-knownhosts-file";
266 |
267 | runtimeInputs = [
268 | (import ./lib/gen-knownhosts-file.nix {
269 | inherit pkgs;
270 | })
271 | ];
272 |
273 | text = ''
274 | ip=${cfg'.ip}
275 | ssh_port=${toString cfg'.sshPort}
276 | ssh_boot_port=${toString cfg'.sshBootPort}
277 | host_key_pub=${cfg'.hostKeyPub}
278 |
279 | gen-knownhosts-file \
280 | $host_key_pub "$ip" $ssh_port $ssh_boot_port \
281 | > ${name}/${cfg'.knownHostsName}
282 | '';
283 | };
284 |
285 | # Install a nixosConfigurations instance () on a server.
286 | #
287 | # This command is intended to be run against a server which
288 | # was booted on the beacon. Although, the server could be booted
289 | # on any OS supported by nixos-anywhere. The latter was not tested.
290 | # nix run .#install-on-beacon FLAKE [ ...]
291 | # nix run .#install-on-beacon
292 | # nix run .#install-on-beacon .#skarabox
293 | # nix run .#install-on-beacon .#skarabox -v
294 | install-on-beacon = pkgs.writeShellApplication {
295 | name = "install-on-beacon";
296 | runtimeInputs = [
297 | (import ./lib/install-on-beacon.nix {
298 | inherit pkgs;
299 | inherit (inputs.nixos-anywhere.packages.${system}) nixos-anywhere;
300 | })
301 | ];
302 | text = ''
303 | ip=${toString cfg'.ip}
304 | ssh_port=${toString cfg'.sshPort}
305 | flake="$1"
306 | shift
307 |
308 | install-on-beacon \
309 | -i $ip \
310 | -p $ssh_port \
311 | -f "$flake" \
312 | -k ${name}/${cfg'.hostKeyName} \
313 | -s ${cfg.sopsKeyName} \
314 | -e ${name}/${cfg'.secretsFileName} \
315 | -r "${cfg'.secretsRootPassphrasePath}" \
316 | -d "${cfg'.secretsDataPassphrasePath}" \
317 | -a "--ssh-option ConnectTimeout=10 -i ${name}/${cfg'.sshPrivateKeyName} $*"
318 | '';
319 | };
320 |
321 | # nix run .#ssh [ ...]
322 | # nix run .#ssh
323 | # nix run .#ssh echo hello
324 | #
325 | # Note: the private SSH key is not read into the nix store on purpose.
326 | ssh = pkgs.writeShellApplication {
327 | name = "ssh";
328 |
329 | runtimeInputs = [
330 | (import ./lib/ssh.nix {
331 | inherit pkgs;
332 | })
333 | ];
334 |
335 | text = ''
336 | ssh \
337 | "${cfg'.ip}" \
338 | "${toString cfg'.sshPort}" \
339 | ${topLevelConfig.flake.nixosConfigurations.${name}.config.skarabox.username} \
340 | -o UserKnownHostsFile=${cfg'.knownHosts} \
341 | -o ConnectTimeout=10 \
342 | -i ${name}/${cfg'.sshPrivateKeyName} \
343 | "$@"
344 | '';
345 | };
346 |
347 | get-facter = pkgs.writeShellApplication {
348 | name = "get-facter";
349 |
350 | runtimeInputs = [
351 | ssh
352 | ];
353 |
354 | text = ''
355 | ssh -o StrictHostKeyChecking=no sudo nixos-facter
356 | '';
357 | };
358 |
359 | unlock = pkgs.writeShellApplication {
360 | name = "unlock";
361 |
362 | runtimeInputs = [
363 | sops
364 | boot-ssh
365 | ];
366 |
367 | text = ''
368 | root_passphrase="$(sops decrypt --extract "${cfg'.secretsRootPassphrasePath}" "${name}/${cfg'.secretsFileName}")"
369 | printf '%s' "$root_passphrase" | boot-ssh "$@"
370 | '';
371 | };
372 | in {
373 | "${name}-boot-ssh" = boot-ssh;
374 | "${name}-sops" = sops;
375 | "${name}-beacon" = beacon;
376 | "${name}-beacon-vm" = beacon-vm;
377 | "${name}-gen-knownhosts-file" = gen-knownhosts-file;
378 | "${name}-install-on-beacon" = install-on-beacon;
379 | "${name}-ssh" = ssh;
380 | "${name}-get-facter" = get-facter;
381 | "${name}-unlock" = unlock;
382 | };
383 | in {
384 | packages = let
385 | beacon-usbimager = pkgs.usbimager;
386 |
387 | add-sops-cfg = import ./lib/add-sops-cfg.nix {
388 | inherit pkgs;
389 | };
390 |
391 | sops-create-main-key = import ./lib/sops-create-main-key.nix {
392 | inherit pkgs;
393 | };
394 |
395 | sops-add-main-key = import ./lib/sops-add-main-key.nix {
396 | inherit pkgs add-sops-cfg;
397 | };
398 |
399 | gen-new-host = import ./lib/gen-new-host.nix {
400 | inherit add-sops-cfg pkgs;
401 | };
402 | in {
403 | inherit beacon-usbimager gen-new-host;
404 | inherit add-sops-cfg sops sops-add-main-key sops-create-main-key;
405 | inherit (pkgs) age;
406 | } // (concatMapAttrs mkHostPackages cfg.hosts);
407 |
408 | apps = {
409 | deploy-rs = inputs'.deploy-rs.apps.deploy-rs;
410 | };
411 | };
412 |
413 | flake = { pkgs, ... }: let
414 | mkFlake = name: cfg': {
415 | nixosConfigurations.${name} = inputs.nixpkgs.lib.nixosSystem {
416 | inherit (cfg') system;
417 | modules = cfg'.modules ++ [
418 | inputs.skarabox.nixosModules.skarabox
419 | {
420 | skarabox.system = cfg'.system;
421 | }
422 | ];
423 | };
424 |
425 | packages.${cfg'.system} = let
426 | nixosConfigurationConfig = topLevelConfig.flake.nixosConfigurations.${name}.config;
427 | in {
428 | ${name} = nixosConfigurationConfig.system.build.toplevel;
429 | "${name}-debug-facter-nvd" = nixosConfigurationConfig.facter.debug.nvd;
430 | "${name}-debug-facter-nix-diff" = nixosConfigurationConfig.facter.debug.nix-diff;
431 | };
432 |
433 | # Debug eval errors with `nix eval --json .#deploy --show-trace`
434 | deploy.nodes = let
435 | pkgs' = import inputs.nixpkgs {
436 | inherit (cfg') system;
437 | };
438 | # Use deploy-rs from nixpkgs to take advantage of the binary cache.
439 | # https://github.com/serokell/deploy-rs?tab=readme-ov-file#overall-usage
440 | deployPkgs = import inputs.nixpkgs {
441 | inherit (cfg') system;
442 | overlays = [
443 | inputs.deploy-rs.overlay
444 | (self: super: {
445 | deploy-rs = {
446 | inherit (pkgs') deploy-rs;
447 | lib = super.deploy-rs.lib;
448 | };
449 | })
450 | ];
451 | };
452 |
453 | mkNode = name: cfg': {
454 | ${name} = {
455 | hostname = cfg'.ip;
456 | sshUser = topLevelConfig.flake.nixosConfigurations.${name}.config.skarabox.username;
457 | # What out, adding --ssh-opts on the command line will override these args.
458 | # For example, running `nix run .#deploy-rs -- -s --ssh-opts -v` will result in only the -v flag.
459 | sshOpts = [
460 | "-o" "IdentitiesOnly=yes"
461 | "-o" "UserKnownHostsFile=${cfg'.knownHosts}"
462 | "-o" "ConnectTimeout=10"
463 | "-i" "${name}/${cfg'.sshPrivateKeyName}"
464 | "-p" (toString cfg'.sshPort)
465 | ];
466 | profiles = {
467 | system = {
468 | user = "root";
469 | path = deployPkgs.deploy-rs.lib.activate.nixos topLevelConfig.flake.nixosConfigurations.${name};
470 | };
471 | };
472 | };
473 | };
474 | in
475 | concatMapAttrs mkNode cfg.hosts;
476 | };
477 |
478 | common = {
479 | nixosModules.beacon = beacon-module;
480 |
481 | # From https://github.com/serokell/deploy-rs?tab=readme-ov-file#overall-usage
482 | checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks topLevelConfig.flake.deploy) inputs.deploy-rs.lib;
483 | };
484 | in
485 | common // (concatMapAttrs mkFlake cfg.hosts);
486 | };
487 | }
488 |
--------------------------------------------------------------------------------
/lib/add-sops-cfg.nix:
--------------------------------------------------------------------------------
1 | {
2 | pkgs
3 | }:
4 | pkgs.writers.writePython3Bin "add-sops-cfg"
5 | {
6 | libraries = [
7 | pkgs.python312Packages.ruamel-yaml
8 | ];
9 | }
10 | ''
11 | import argparse
12 | from ruamel.yaml import YAML
13 | from ruamel.yaml.scalarstring import PlainScalarString
14 |
15 |
16 | class Yaml:
17 | def __init__(self, path):
18 | self.__y = YAML()
19 | self.__y.preserve_quotes = True
20 |
21 | self.path = path
22 |
23 | def read(self):
24 | content = {}
25 | try:
26 | with open(self.path, 'r') as f:
27 | content = self.__y.load(f)
28 | except FileNotFoundError:
29 | pass
30 | return content
31 |
32 | def write(self, content):
33 | with open(self.path, 'w') as f:
34 | self.__y.dump(content, f)
35 |
36 |
37 | def find_anchor_for_alias(yaml_data, alias):
38 | for i, item in enumerate(yaml_data):
39 | if item.anchor.value == alias:
40 | return i
41 | return None
42 |
43 |
44 | def find_creation_rule(yaml_data, path_regex):
45 | for item in yaml_data:
46 | if 'path_regex' in item \
47 | and item['path_regex'] == path_regex:
48 | return item
49 | return None
50 |
51 |
52 | def unique(lst):
53 | return sorted(list(set(lst)))
54 |
55 |
56 | def add_alias(args):
57 | yaml = Yaml(args.sops_cfg)
58 |
59 | content = yaml.read()
60 |
61 | anchor_to_add = PlainScalarString(args.sops_key, anchor=args.alias)
62 | if 'keys' not in content:
63 | content["keys"] = [anchor_to_add]
64 | else:
65 | anchor_index = find_anchor_for_alias(content['keys'], args.alias)
66 | if anchor_index is None:
67 | content["keys"] += [anchor_to_add]
68 | else:
69 | content["keys"][anchor_index] = anchor_to_add
70 | for rule in content.get('creation_rules', []):
71 | for key_group in rule.get('key_groups', []):
72 | for i, key in enumerate(key_group.get('age', [])):
73 | if key.anchor.value == args.alias:
74 | key_group['age'][i] = anchor_to_add
75 |
76 | content["keys"] = sorted(content["keys"])
77 |
78 | yaml.write(content)
79 |
80 |
81 | def add_parser_path_regex(args):
82 | yaml = Yaml(args.sops_cfg)
83 |
84 | content = yaml.read()
85 |
86 | anchor_index = find_anchor_for_alias(content['keys'], args.alias)
87 | if anchor_index is None:
88 | raise Exception('Cannot add alias to not existing anchor')
89 |
90 | if 'creation_rules' not in content:
91 | content["creation_rules"] = [{
92 | "path_regex": args.path_regex,
93 | "key_groups": [
94 | {
95 | "age": [
96 | content["keys"][anchor_index]
97 | ]
98 | }
99 | ]
100 | }]
101 | else:
102 | existing_creation_rule \
103 | = find_creation_rule(content['creation_rules'], args.path_regex)
104 | if existing_creation_rule is None:
105 | content["creation_rules"] += [{
106 | "path_regex": args.path_regex,
107 | "key_groups": [
108 | {
109 | "age": [
110 | content["keys"][anchor_index]
111 | ]
112 | }
113 | ]
114 | }]
115 | else:
116 | existing_creation_rule["key_groups"][0]["age"] \
117 | += [content["keys"][anchor_index]]
118 | existing_creation_rule["key_groups"][0]["age"] \
119 | = unique(existing_creation_rule["key_groups"][0]["age"])
120 |
121 | yaml.write(content)
122 |
123 |
124 | def main():
125 | parser = argparse.ArgumentParser(
126 | prog='Add keys to Sops config')
127 | parser.add_argument('-o', '--sops-cfg')
128 |
129 | cmdparsers = parser.add_subparsers()
130 |
131 | parser_path_regex = cmdparsers.add_parser('path-regex')
132 | parser_path_regex.add_argument('alias')
133 | parser_path_regex.add_argument('path_regex', metavar='path-regex')
134 | parser_path_regex.set_defaults(func=add_parser_path_regex)
135 |
136 | alias = cmdparsers.add_parser('alias')
137 | alias.add_argument('alias')
138 | alias.add_argument('sops_key', metavar='sops-key')
139 | alias.set_defaults(func=add_alias)
140 |
141 | args = parser.parse_args()
142 | args.func(args)
143 |
144 |
145 | if __name__ == "__main__":
146 | main()
147 | ''
148 |
--------------------------------------------------------------------------------
/lib/gen-initial.nix:
--------------------------------------------------------------------------------
1 | {
2 | pkgs,
3 | sops-create-main-key,
4 | sops-add-main-key,
5 | gen-new-host,
6 | }:
7 | pkgs.writeShellApplication {
8 | name = "gen-initial";
9 |
10 | runtimeInputs = [
11 | sops-create-main-key
12 | sops-add-main-key
13 | gen-new-host
14 | pkgs.age
15 | pkgs.mkpasswd
16 | pkgs.nix
17 | pkgs.openssh
18 | pkgs.openssl
19 | pkgs.sops
20 | pkgs.util-linux
21 | ];
22 |
23 | text = let
24 | nix = "nix --extra-experimental-features nix-command -L";
25 | in ''
26 | set -e
27 | set -o pipefail
28 |
29 | yes=0
30 | path=
31 | verbose=
32 |
33 | usage () {
34 | cat < [...]
7 | #
8 | # One line will be generated per port given.
9 | pkgs.writeShellScriptBin "gen-knownhosts-file" ''
10 | pub=$(cat $1 | ${pkgs.coreutils}/bin/cut -d' ' -f-2)
11 | shift
12 | ip=$1
13 | shift
14 |
15 | for port in "$@"; do
16 | echo "[$ip]:$port $pub"
17 | done
18 | ''
19 |
--------------------------------------------------------------------------------
/lib/gen-new-host.nix:
--------------------------------------------------------------------------------
1 | {
2 | pkgs,
3 | add-sops-cfg,
4 | }:
5 | pkgs.writeShellApplication {
6 | name = "gen-new-host";
7 |
8 | runtimeInputs = [
9 | add-sops-cfg
10 | pkgs.ssh-to-age
11 | pkgs.mkpasswd
12 | pkgs.openssh
13 | pkgs.openssl
14 | pkgs.sops
15 | pkgs.util-linux
16 | ];
17 |
18 | text = ''
19 | set -e
20 | set -o pipefail
21 |
22 | yes=0
23 | mkpasswdargs=
24 | verbose=
25 |
26 | usage () {
27 | cat < "$hostid"
123 |
124 | sops_cfg="./.sops.yaml"
125 | secrets="$hostname/secrets.yaml"
126 | e "Adding host key in $sops_cfg..."
127 | host_age_key="$(ssh-to-age -i "$host_key_pub")"
128 | add-sops-cfg -o "$sops_cfg" alias "$hostname" "$host_age_key"
129 | add-sops-cfg -o "$sops_cfg" path-regex main "$secrets"
130 | add-sops-cfg -o "$sops_cfg" path-regex "$hostname" "$secrets"
131 |
132 | sops_key="./sops.key"
133 | export SOPS_AGE_KEY_FILE=$sops_key
134 | e "Generating sops secrets file $secrets..."
135 | echo "tmp_secret: a" > "$secrets"
136 | sops encrypt -i "$secrets"
137 |
138 | e "Generating initial password for user in $secrets under $hostname/user/hashedPassword"
139 | sops set "$secrets" \
140 | "['$hostname']['user']['hashedPassword']" \
141 | "\"$(mkpasswd $mkpasswdargs)\""
142 |
143 | e "Generating root pool passphrase in $secrets under $hostname/disks/rootPassphrase"
144 | sops set "$secrets" \
145 | "['$hostname']['disks']['rootPassphrase']" \
146 | "\"$(openssl rand -hex 64)\""
147 |
148 | e "Generating data pool passphrase in $secrets under $hostname/disks/dataPassphrase"
149 | sops set "$secrets" \
150 | "['$hostname']['disks']['dataPassphrase']" \
151 | "\"$(openssl rand -hex 64)\""
152 |
153 | sops unset "$secrets" \
154 | "['tmp_secret']"
155 |
156 | e "You will need to fill out the ./$hostname/ip and ./$hostname/system file and generate ./$hostname/known_hosts."
157 | e "Optionally, adjust the ./$hostname/ssh_port and ./$hostname/ssh_boot_port if you want to."
158 | e "Follow the ./README.md for more information and to continue the installation."
159 | '';
160 |
161 | }
162 |
--------------------------------------------------------------------------------
/lib/install-on-beacon.nix:
--------------------------------------------------------------------------------
1 | { pkgs, nixos-anywhere }:
2 | pkgs.writeShellApplication {
3 | name = "install-on-beacon";
4 |
5 | runtimeInputs = [
6 | nixos-anywhere
7 | pkgs.bash
8 | pkgs.sops
9 | ];
10 |
11 | text = ''
12 | usage () {
13 | cat < [ [ [ ...]]]
7 | # 192.168.1.10
8 | # 192.168.1.10 22
9 | # 192.168.1.10 22 nixos
10 | # 192.168.1.10 22 nixos echo hello
11 | pkgs.writeShellScriptBin "ssh" ''
12 | ip=$1
13 | shift
14 | port=$1
15 | shift
16 | user=$1
17 | shift
18 |
19 | ${pkgs.openssh}/bin/ssh \
20 | -p ''${port:-22} \
21 | ''${user:-skarabox}@''$ip \
22 | -o IdentitiesOnly=yes \
23 | $@
24 | ''
25 |
--------------------------------------------------------------------------------
/modules/beacon.nix:
--------------------------------------------------------------------------------
1 | { config, lib, pkgs, ... }: let
2 | inherit (lib) mkForce;
3 |
4 | cfg = config.skarabox;
5 | in {
6 | options.skarabox = {
7 | sshPublicKey = lib.mkOption {
8 | type = lib.types.path;
9 | description = "Public key to connect to the beacon.";
10 | };
11 | };
12 |
13 | config = {
14 | # Also allow root to connect for nixos-anywhere.
15 | users.users.root = {
16 | openssh.authorizedKeys.keyFiles = [ cfg.sshPublicKey ];
17 | };
18 | # Override user set in profiles/installation-device.nix
19 | users.users.skarabox = {
20 | isNormalUser = true;
21 | extraGroups = [ "wheel" "networkmanager" "video" ];
22 | # Allow the graphical user to login without password
23 | initialHashedPassword = "";
24 | # Set shared ssh key
25 | openssh.authorizedKeys.keyFiles = [ cfg.sshPublicKey ];
26 | };
27 | # Automatically log in at the virtual consoles.
28 | services.getty.autologinUser = lib.mkForce "skarabox";
29 | nix.settings.trusted-users = [ "skarabox" ];
30 |
31 | image.fileName = mkForce "beacon.iso";
32 | image.baseName = mkForce "beacon";
33 |
34 | networking.firewall.allowedTCPPorts = [ 22 ];
35 |
36 | boot.loader.systemd-boot.enable = true;
37 |
38 | services.hostapd = {
39 | enable = true;
40 | radios.skarabox = {
41 | band = "2g";
42 | networks.skarabox = {
43 | ssid = "Skarabox";
44 | authentication = {
45 | mode = "wpa2-sha256";
46 | wpaPassword = "skarabox";
47 | };
48 | };
49 | };
50 | };
51 |
52 | services.getty.helpLine = mkForce ''
53 |
54 | / \\
55 | |/ _.-=-._ \\| SKARABOX
56 | \\'_/`-. .-'\\_'/
57 | '-\\ _ V _ /-'
58 | .' 'v' '. Hello, you just booted on the Skarabox beacon.
59 | .'| | |'. Congratulations!
60 | v'| | |'v
61 | | | | Nothing is installed yet on this server. To abort, just
62 | .\\ | /. close this server and remove the USB stick.
63 | (_.'._^_.'._)
64 | \\\\ // To complete the installation of Skarabox on this server, you
65 | \\'- -'/ must follow the steps below to run the Skarabox installer.
66 |
67 |
68 | WARNING - WARNING - WARNING - WARNING - WARNING - WARNING - WARNING - WARNING
69 | * *
70 | * Running the Skarabox installer WILL ERASE EVERYTHING on this server. *
71 | * Make sure the only drives connected and powered on are the disks to *
72 | * install the Operating System on. This drive should be a SSD or NVMe *
73 | * drive for optimal performance and 2 hard drives for data. *
74 | * *
75 | * THESE DRIVES WILL BE ERASED. *
76 | * *
77 | WARNING - WARNING - WARNING - WARNING - WARNING - WARNING - WARNING - WARNING
78 |
79 |
80 | * Step 1. Enable network access to this server.
81 |
82 | For a wired network connection, just plug in an ethernet cable from your router
83 | to this server. The connection will be made automatically.
84 |
85 | If you need a wireless connection, configure a network by typing the command
86 | "wpa_cli" without the enclosing double quotes.
87 |
88 | * Step 2. Identify the disk layout.
89 |
90 | To know what disk existing in the system, type the command "fdisk -l" without
91 | the double quotes. This will show lines like so:
92 |
93 | Disk /dev/nvme0n1 This is an NVMe drive
94 | Disk /dev/sda This is an SSD or HDD drive
95 | Disk /dev/sdb This is an SSD or HDD drive
96 |
97 | With the above setup, in the flake.nix template, set the following options:
98 |
99 | skarabox.disks.rootDisk = "/dev/nvme0n1"
100 | skarabox.disks.dataDisk1 = "/dev/sda"
101 | skarabox.disks.dataDisk2 = "/dev/sdb"
102 |
103 | * Step 3. Run the installer.
104 |
105 | When running the installer, you will need to enter the password "skarabox123" as
106 | well as the IP address of this server. To know the IP address, first follow the
107 | step 1 above then type the command "ip -brief a" verbatim, without the enclosing
108 | double quotes.
109 |
110 | Try all IP addresses one by one until one works. An IP address looks like so:
111 |
112 | 192.168.1.15
113 | 10.0.2.15
114 |
115 | * Step 4.
116 |
117 | No step 4. The server will reboot automatically in the new system as soon as the
118 | installer ran successfully. Enjoy your NixOS system powered by Skarabox!
119 | '';
120 |
121 | environment.systemPackages = let
122 | skarabox-help = pkgs.writeText "skarabox-help" config.services.getty.helpLine;
123 | in [
124 | (pkgs.writeShellScriptBin "skarabox" ''
125 | cat ${skarabox-help}
126 | '')
127 | pkgs.nixos-facter
128 | ];
129 | };
130 | }
131 |
--------------------------------------------------------------------------------
/modules/configuration.nix:
--------------------------------------------------------------------------------
1 | { config, lib, pkgs, ... }:
2 | let
3 | cfg = config.skarabox;
4 |
5 | inherit (lib) mkOption toInt types;
6 |
7 | readAndTrim = f: lib.strings.trim (builtins.readFile f);
8 | readAsStr = v: if lib.isPath v then readAndTrim v else v;
9 | readAsInt = v: let
10 | vStr = readAsStr v;
11 | in
12 | if lib.isString vStr then toInt vStr else vStr;
13 |
14 | in
15 | {
16 | options.skarabox = {
17 | hostname = mkOption {
18 | type = types.str;
19 | default = "skarabox";
20 | description = "Hostname to give to the server.";
21 | };
22 |
23 | username = mkOption {
24 | type = types.str;
25 | default = "skarabox";
26 | description = "Name given to the admin user on the server.";
27 | };
28 |
29 | hashedPasswordFile = mkOption {
30 | type = types.str;
31 | description = "Contains password for the admin user.";
32 | };
33 |
34 | facter-config = lib.mkOption {
35 | type = lib.types.path;
36 | description = ''
37 | nixos-facter config file.
38 | '';
39 | };
40 |
41 | hostId = mkOption {
42 | type = with types; oneOf [ str path ];
43 | description = ''
44 | 8 characters unique identifier for this server. Generate with `uuidgen | head -c 8`.
45 | '';
46 | apply = readAsStr;
47 | };
48 |
49 | sshPort = mkOption {
50 | type = with types; oneOf [ int str path ];
51 | default = [ 22 ];
52 | description = ''
53 | Port the SSH daemon listens to.
54 | '';
55 | apply = readAsInt;
56 | };
57 |
58 | sshAuthorizedKeyFile = mkOption {
59 | type = types.path;
60 | description = ''
61 | Public SSH key used to connect on boot to decrypt the root pool.
62 | '';
63 | example = "./ssh.pub";
64 | };
65 |
66 | setupLanWithDHCP = mkOption {
67 | type = types.bool;
68 | description = ''
69 | Sets up a default IPV4 network on lan.
70 |
71 | This should suit most needs but if not,
72 | disable this and set it manually.
73 | The [wiki][] is very useful.
74 |
75 | [wiki]: https://wiki.nixos.org/wiki/Systemd/networkd
76 | '';
77 | default = true;
78 | };
79 |
80 | system = mkOption {
81 | type = types.str;
82 | };
83 | };
84 |
85 | config = {
86 | nixpkgs.hostPlatform = cfg.system;
87 |
88 | facter.reportPath = lib.mkIf (builtins.pathExists cfg.facter-config) cfg.facter-config;
89 |
90 | networking.hostName = cfg.hostname;
91 | networking.hostId = cfg.hostId;
92 |
93 | # https://wiki.nixos.org/wiki/Systemd/networkd
94 | systemd.network = lib.mkIf cfg.setupLanWithDHCP {
95 | enable = true;
96 | networks."10-lan" = {
97 | matchConfig.Name = "en*";
98 | networkConfig.DHCP = "ipv4";
99 | };
100 | };
101 |
102 | powerManagement.cpuFreqGovernor = "performance";
103 |
104 | nix.settings.trusted-users = [ cfg.username ];
105 | nix.settings.experimental-features = [ "nix-command" "flakes" ];
106 | nix.settings.auto-optimise-store = true;
107 | nix.gc = {
108 | automatic = true;
109 | dates = "weekly";
110 | options = "--delete-older-than 30d";
111 | };
112 |
113 | # See https://www.freedesktop.org/software/systemd/man/journald.conf.html#SystemMaxUse=
114 | services.journald.extraConfig = ''
115 | SystemMaxUse=2G
116 | SystemKeepFree=4G
117 | SystemMaxFileSize=100M
118 | MaxFileSec=day
119 | '';
120 |
121 | # hashedPasswordFile only works if users are not mutable.
122 | users.mutableUsers = false;
123 | users.users.${cfg.username} = {
124 | isNormalUser = true;
125 | extraGroups = [ "wheel" ];
126 | inherit (cfg) hashedPasswordFile;
127 | openssh.authorizedKeys.keyFiles = [ cfg.sshAuthorizedKeyFile ];
128 | };
129 |
130 | security.sudo.extraRules = [
131 | { users = [ cfg.username ];
132 | commands = [
133 | { command = "ALL";
134 | options = [ "NOPASSWD" ];
135 | }
136 | ];
137 | }
138 | ];
139 |
140 | environment.systemPackages = [
141 | pkgs.vim
142 | pkgs.curl
143 | pkgs.nixos-facter
144 | ];
145 |
146 | services.openssh = {
147 | enable = true;
148 | settings = {
149 | PermitRootLogin = "no";
150 | PasswordAuthentication = false;
151 | };
152 | ports = [ cfg.sshPort ];
153 | hostKeys = lib.mkForce [];
154 | extraConfig = ''
155 | HostKey /boot/host_key
156 | '';
157 | };
158 |
159 | system.stateVersion = "23.11";
160 | };
161 | }
162 |
--------------------------------------------------------------------------------
/modules/disks.nix:
--------------------------------------------------------------------------------
1 | { config, options, lib, ... }:
2 | let
3 | cfg = config.skarabox.disks;
4 | opt = options.skarabox.disks;
5 |
6 | inherit (lib) mkIf mkOption optionals optionalString toInt types;
7 |
8 | readAndTrim = f: lib.strings.trim (builtins.readFile f);
9 | readAsStr = v: if lib.isPath v then readAndTrim v else v;
10 | readAsInt = v: let
11 | vStr = readAsStr v;
12 | in
13 | if lib.isString vStr then toInt vStr else vStr;
14 | in
15 | {
16 | options.skarabox.disks = {
17 | rootPool = mkOption {
18 | type = with types; submodule {
19 | options = {
20 | name = mkOption {
21 | type = types.str;
22 | description = "Name of the root pool";
23 | default = "root";
24 | };
25 |
26 | disk1 = mkOption {
27 | type = types.str;
28 | description = "SSD disk on which to install. Required";
29 | example = "/dev/nvme0n1";
30 | };
31 |
32 | disk2 = mkOption {
33 | type = types.nullOr types.str;
34 | description = "Mirror SSD disk on which to install. Optional. Boot partition will be mirrored too.";
35 | example = "/dev/nvme0n2";
36 | default = null;
37 | };
38 |
39 | reservation = mkOption {
40 | type = types.str;
41 | description = ''
42 | Disk size to reserve for ZFS internals. Should be between 10% and 15% of available size as recorded by zpool.
43 |
44 | To get available size on zpool:
45 |
46 | zfs get -Hpo value available ${opt.rootPool.name}
47 |
48 | Then to set manually, if needed:
49 |
50 | sudo zfs set reservation=100G ${opt.rootPool.name}
51 | '';
52 | example = "100G";
53 | };
54 | };
55 | };
56 | };
57 |
58 | dataPool = mkOption {
59 | type = with types; submodule {
60 | options = {
61 | enable = lib.mkEnableOption "the data pool on other hard drives." // {
62 | default = true;
63 | };
64 |
65 | name = mkOption {
66 | type = types.str;
67 | description = "Name of the data pool";
68 | default = "zdata";
69 | };
70 |
71 | disk1 = mkOption {
72 | type = types.str;
73 | description = "First disk on which to install the data pool.";
74 | example = "/dev/sda";
75 | };
76 |
77 | disk2 = mkOption {
78 | type = types.str;
79 | description = "Second disk on which to install the data pool.";
80 | example = "/dev/sdb";
81 | };
82 |
83 | reservation = mkOption {
84 | type = types.str;
85 | description = ''
86 | Disk size to reserve for ZFS internals. Should be between 5% and 10% of available size as recorded by zpool.
87 |
88 | To get available size on zpool:
89 |
90 | zfs get -Hpo value available ${opt.dataPool.name}
91 |
92 | Then to set manually, if needed:
93 |
94 | sudo zfs set reservation=100G ${opt.dataPool.name}
95 | '';
96 | example = "1T";
97 | };
98 | };
99 | };
100 | };
101 |
102 | initialBackupDataset = mkOption {
103 | type = types.bool;
104 | description = "Create the backup dataset.";
105 | default = true;
106 | };
107 |
108 | bootSSHPort = mkOption {
109 | type = with types; oneOf [ int str path ];
110 | description = "Port the SSH daemon used to decrypt the root partition listens to.";
111 | default = 2222;
112 | apply = readAsInt;
113 | };
114 | };
115 |
116 | config = {
117 | disko.devices = {
118 | disk = let
119 | hasRaid = cfg.rootPool.disk2 != null;
120 |
121 | mkRoot = { disk, id ? "" }: {
122 | type = "disk";
123 | device = disk;
124 | content = {
125 | type = "gpt";
126 | partitions = {
127 | ESP = {
128 | size = "500M";
129 | type = "EF00";
130 | content = {
131 | type = "filesystem";
132 | format = "vfat";
133 | mountpoint = "/boot${id}";
134 | # Otherwise you get https://discourse.nixos.org/t/security-warning-when-installing-nixos-23-11/37636/2
135 | mountOptions = [ "umask=0077" ];
136 | # Copy the host_key needed for initrd in a location accessible on boot.
137 | # It's prefixed by /mnt because we're installing and everything is mounted under /mnt.
138 | # We're using the same host key because, well, it's the same host!
139 | postMountHook = ''
140 | cp /tmp/host_key /mnt/boot${id}/host_key
141 | '';
142 | };
143 | };
144 | zfs = {
145 | size = "100%";
146 | content = {
147 | type = "zfs";
148 | pool = cfg.rootPool.name;
149 | };
150 | };
151 | };
152 | };
153 | };
154 |
155 | mkDataDisk = dataDisk: {
156 | type = "disk";
157 | device = dataDisk;
158 | content = {
159 | type = "gpt";
160 | partitions = {
161 | zfs = {
162 | size = "100%";
163 | content = {
164 | type = "zfs";
165 | pool = cfg.dataPool.name;
166 | };
167 | };
168 | };
169 | };
170 | };
171 | in {
172 | root = mkRoot { disk = cfg.rootPool.disk1; };
173 | # Second root must have id=-backup.
174 | root1 = mkIf hasRaid (mkRoot { disk = cfg.rootPool.disk2; id = "-backup"; });
175 | data1 = mkIf cfg.dataPool.enable (mkDataDisk cfg.dataPool.disk1);
176 | data2 = mkIf cfg.dataPool.enable (mkDataDisk cfg.dataPool.disk2);
177 | };
178 | zpool = {
179 | ${cfg.rootPool.name} = {
180 | type = "zpool";
181 | mode = if cfg.rootPool.disk2 != null then "mirror" else "";
182 | options = {
183 | ashift = "12";
184 | autotrim = "on";
185 | };
186 | rootFsOptions = {
187 | encryption = "on";
188 | keyformat = "passphrase";
189 | keylocation = "file:///tmp/root_passphrase";
190 | compression = "lz4";
191 | canmount = "off";
192 | xattr = "sa";
193 | atime = "off";
194 | acltype = "posixacl";
195 | recordsize = "1M";
196 | "com.sun:auto-snapshot" = "false";
197 | };
198 | # Need to use another variable name otherwise I get SC2030 and SC2031 errors.
199 | preCreateHook = ''
200 | pname=$name
201 | '';
202 | # Needed to get back a prompt on next boot.
203 | # See https://github.com/nix-community/nixos-anywhere/issues/161#issuecomment-1642158475
204 | postCreateHook = ''
205 | zfs set keylocation="prompt" $pname
206 | '';
207 |
208 | # Follows https://grahamc.com/blog/erase-your-darlings/
209 | datasets = {
210 | # TODO: compute percentage automatically in postCreateHook
211 | "reserved" = {
212 | options = {
213 | canmount = "off";
214 | mountpoint = "none";
215 | # TODO: compute this value using percentage
216 | reservation = cfg.rootPool.reservation;
217 | };
218 | type = "zfs_fs";
219 | };
220 |
221 | "local/root" = {
222 | type = "zfs_fs";
223 | mountpoint = "/";
224 | options.mountpoint = "legacy";
225 | postCreateHook = "zfs list -t snapshot -H -o name | grep -E '^${cfg.rootPool.name}/local/root@blank$' || zfs snapshot ${cfg.rootPool.name}/local/root@blank";
226 | };
227 |
228 | "local/nix" = {
229 | type = "zfs_fs";
230 | mountpoint = "/nix";
231 | options.mountpoint = "legacy";
232 | };
233 |
234 | "safe/home" = {
235 | type = "zfs_fs";
236 | mountpoint = "/home";
237 | options.mountpoint = "legacy";
238 | };
239 |
240 | "safe/persist" = {
241 | type = "zfs_fs";
242 | mountpoint = "/persist";
243 | # It's prefixed by /mnt because we're installing and everything is mounted under /mnt.
244 | options.mountpoint = "legacy";
245 | postMountHook = optionalString cfg.dataPool.enable ''
246 | cp /tmp/data_passphrase /mnt/persist/data_passphrase
247 | '';
248 | };
249 | };
250 | };
251 |
252 | ${cfg.dataPool.name} = mkIf cfg.dataPool.enable {
253 | type = "zpool";
254 | mode = "mirror";
255 | options = {
256 | ashift = "12";
257 | autotrim = "on";
258 | };
259 | rootFsOptions = {
260 | encryption = "on";
261 | keyformat = "passphrase";
262 | keylocation = "file:///tmp/data_passphrase";
263 | compression = "lz4";
264 | canmount = "off";
265 | xattr = "sa";
266 | atime = "off";
267 | acltype = "posixacl";
268 | recordsize = "1M";
269 | "com.sun:auto-snapshot" = "false";
270 | mountpoint = "none";
271 | };
272 | # Need to use another variable name otherwise I get SC2030 and SC2031 errors.
273 | preCreateHook = ''
274 | pname=$name
275 | '';
276 | postCreateHook = ''
277 | zfs set keylocation="file:///persist/data_passphrase" $pname;
278 | '';
279 | datasets = {
280 | # TODO: create reserved dataset automatically in postCreateHook
281 | "reserved" = {
282 | options = {
283 | canmount = "off";
284 | mountpoint = "none";
285 | # TODO: compute this value using percentage
286 | reservation = cfg.dataPool.reservation;
287 | };
288 | type = "zfs_fs";
289 | };
290 | } // lib.optionalAttrs cfg.initialBackupDataset {
291 | "backup" = {
292 | type = "zfs_fs";
293 | mountpoint = "/srv/backup";
294 | options.mountpoint = "legacy";
295 | };
296 | # TODO: create datasets automatically upon service installation (e.g. Nextcloud, etc.)
297 | #"nextcloud" = {
298 | # type = "zfs_fs";
299 | # mountpoint = "/srv/nextcloud";
300 | #};
301 | };
302 | };
303 | };
304 | };
305 | fileSystems."/srv/backup" = mkIf (cfg.dataPool.enable && cfg.initialBackupDataset) {
306 | options = [ "nofail" ];
307 | };
308 |
309 | boot.supportedFilesystems = [ "zfs" ];
310 | boot.zfs.forceImportRoot = false;
311 | # To import the zpool automatically
312 | boot.zfs.extraPools = optionals cfg.dataPool.enable [ cfg.dataPool.name ];
313 |
314 | # This is needed to make the /boot*/host_key available early
315 | # enough to be able to decrypt the sops file on boot,
316 | # when the /etc/shadow file is first generated.
317 | # We assume mkRoot will always be called with at least id=1.
318 | fileSystems = {
319 | "/boot".neededForBoot = true;
320 | "/boot-backup" = mkIf (cfg.rootPool.disk2 != null) { neededForBoot = true; };
321 | };
322 | # Setup Grub to support UEFI.
323 | # nodev is for UEFI.
324 | boot.loader.grub = {
325 | enable = true;
326 | efiSupport = true;
327 | efiInstallAsRemovable = true;
328 |
329 | mirroredBoots = lib.mkForce ([
330 | {
331 | path = "/boot";
332 | devices = [ "nodev" ];
333 | }
334 | ] ++ (optionals (cfg.rootPool.disk2 != null) [
335 | {
336 | path = "/boot-backup";
337 | devices = [ "nodev" ];
338 | }
339 | ]));
340 | };
341 |
342 | # Follows https://grahamc.com/blog/erase-your-darlings/
343 | # https://github.com/NixOS/nixpkgs/pull/346247/files
344 | boot.initrd.postResumeCommands = lib.mkAfter ''
345 | zfs rollback -r ${cfg.rootPool.name}/local/root@blank
346 | '';
347 |
348 | # Enables DHCP in stage-1 even if networking.useDHCP is false.
349 | boot.initrd.network.udhcpc.enable = lib.mkDefault true;
350 | # From https://wiki.nixos.org/wiki/ZFS#Remote_unlock
351 | boot.initrd.network = {
352 | # This will use udhcp to get an ip address. Nixos-facter should have found the correct drivers
353 | # to load but in case not, they need to be added to `boot.initrd.availableKernelModules`.
354 | # Static ip addresses might be configured using the ip argument in kernel command line:
355 | # https://www.kernel.org/doc/Documentation/filesystems/nfs/nfsroot.txt
356 | enable = true;
357 | ssh = {
358 | enable = true;
359 | # To prevent ssh clients from freaking out because a different host key is used,
360 | # a different port for ssh is used.
361 | port = lib.mkDefault cfg.bootSSHPort;
362 | hostKeys = lib.mkForce ([ "/boot/host_key" ] ++ (optionals (cfg.rootPool.disk2 != null) [ "/boot-backup/host_key" ]));
363 | # Public ssh key used for login.
364 | # This should contain just one line and removing the trailing
365 | # newline could be fixed with a removeSuffix call but treating
366 | # it as a file containing multiple lines makes this forward compatible.
367 | authorizedKeyFiles = [
368 | config.skarabox.sshAuthorizedKeyFile
369 | ];
370 | };
371 |
372 | postCommands = ''
373 | zpool import -a
374 | echo "zfs load-key ${cfg.rootPool.name}; killall zfs; exit" >> /root/.profile
375 | '';
376 | };
377 |
378 | services.zfs.autoScrub.enable = true;
379 | };
380 | }
381 |
--------------------------------------------------------------------------------
/template/.gitignore:
--------------------------------------------------------------------------------
1 | .skarabox-tmp
2 | ssh_skarabox
3 | sops.key
--------------------------------------------------------------------------------
/template/.sops.yaml:
--------------------------------------------------------------------------------
1 | keys:
2 | # To obtain the age key for &me, run:
3 | # nix shell .#age --command age-keygen -y sops.key
4 | - &me
5 | # To obtain the age key for &server, run:
6 | # ssh -o IdentitiesOnly=yes -i ssh_skarabox cat /etc/ssh/ssh_host_ed25519_key.pub | nix run .#ssh-to-age
7 | - &server
8 | creation_rules:
9 | - path_regex: secrets\.yaml$
10 | key_groups:
11 | - age:
12 | - *me
13 | - *server
14 |
--------------------------------------------------------------------------------
/template/README.md:
--------------------------------------------------------------------------------
1 | # Skarabox
2 |
3 | This repository originates from https://github.com/ibizaman/skarabox.
4 |
5 | Help can be asked by [opening an issue][] in the repository
6 | or by [joining the Matrix channel][].
7 |
8 | [opening an issue]: https://github.com/ibizaman/skarabox/issues/new
9 | [joining the Matrix channel]: https://matrix.to/#/#selfhostblocks:matrix.org
10 |
11 | ## Bootstrapping
12 |
13 | Create a directory and download the template.
14 |
15 | ```bash
16 | $ mkdir myskarabox
17 | $ cd myskarabox
18 | $ nix run github:ibizaman/skarabox#init
19 | ```
20 |
21 | This last command will also generate the needed secrets
22 | and ask for the password you want for the admin user
23 | for a host named `myskarabox` whose files are located
24 | under the [myskarabox](./myskarabox) folder.
25 |
26 | All the files at the root of this new repository
27 | are common to all hosts.
28 |
29 | It will finally ask you to fill out two files: [./ip](./ip) and [./system](./system)
30 | and afterwards generate [./known_hosts](./known_hosts) with:
31 |
32 | ```bash
33 | nix run .#myskarabox-gen-knownhosts-file
34 | ```
35 |
36 | ## Add in Existing Repo
37 |
38 | Add inputs:
39 |
40 | ```nix
41 | inputs = {
42 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
43 | skarabox.url = "github:ibizaman/skarabox";
44 |
45 | nixos-generators.url = "github:nix-community/nixos-generators";
46 | nixos-generators.inputs.nixpkgs.follows = "nixpkgs";
47 |
48 | nixos-anywhere.url = "github:nix-community/nixos-anywhere";
49 | nixos-anywhere.inputs.nixpkgs.follows = "nixpkgs";
50 |
51 | nixos-facter-modules.url = "github:numtide/nixos-facter-modules";
52 | flake-parts.url = "github:hercules-ci/flake-parts";
53 | deploy-rs.url = "github:serokell/deploy-rs";
54 | sops-nix.url = "github:Mic92/sops-nix";
55 | };
56 | ```
57 |
58 | Transform the outputs in a flake-parts module like outlined [in the official tutorial][tutorial].
59 |
60 | [tutorial]: https://flake.parts/getting-started.html#existing-flake
61 |
62 | In short:
63 | 1. Add `mkFlake` around the outputs attrset:
64 |
65 | ```nix
66 | outputs = inputs@{ self, skarabox, sops-nix, nixpkgs, flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } (let
67 | in {
68 | });
69 | ```
70 |
71 | 2. Add the `systems` you want to handle:
72 |
73 | ```nix
74 | systems = [
75 | "x86_64-linux"
76 | "aarch64-linux"
77 | ];
78 | ```
79 |
80 | 3. Import Skarabox' flake module:
81 |
82 | ```nix
83 | imports = [
84 | skarabox.flakeModules.default
85 | ];
86 | ```
87 |
88 | 4. Add NixOS module importing your module.
89 |
90 | ```nix
91 | flake = {
92 | nixosModules = {
93 | myskarabox = {
94 | imports = [
95 | ./myskarabox/configuration.nix
96 | ];
97 | };
98 | };
99 | };
100 | ```
101 |
102 | 5. Add a Skarabox managed host, here called `myskarabox`
103 | that uses the above NixOS module:
104 |
105 | ```nix
106 | skarabox.hosts = {
107 | myskarabox = {
108 | system = "x86_64-linux";
109 | hostKeyPub = ./myskarabox/host_key.pub;
110 | ip = "192.168.1.XX";
111 | sshPublicKey = ./myskarabox/ssh.pub;
112 | knownHosts = ./myskarabox/known_hosts;
113 | sshPort = 22;
114 | sshBootPort = 2222;
115 |
116 | modules = [
117 | sops-nix.nixosModules.default
118 | self.nixosModules.myskarabox
119 | ];
120 | };
121 | };
122 | ```
123 |
124 | 6. Create Sops main key `sops.key` if needed:
125 |
126 | `nix run .#sops-create-main-key`.
127 |
128 | 7. Add Sops main key to Sops config `.sops.yaml`:
129 |
130 | `nix run .#sops-add-main-key`.
131 |
132 | 8. Create config for host `myskarabox` in folder `./myskarabox`:
133 |
134 | `nix run .#gen-new-host myskarabox`.
135 |
136 | Tweak `./myskarabox/configuration.nix`.
137 |
138 | ## Installation
139 |
140 | The installation procedure can be followed on a [VM][],
141 | to test the installation process, or on a [real server][].
142 |
143 | > [!CAUTION]
144 | > Following the installation procedure on a real server
145 | > WILL ERASE THE CONTENT of any disk on that server.
146 | > Take the time to remove any disk you care about.
147 |
148 | [VM]: #a1-test-on-a-vm
149 | [real server]: #a2-install-on-a-real-server
150 |
151 | ### A.1. Test on a VM
152 |
153 | Assuming the [./configuration.nix](./myskarabox/configuration.nix) file is left untouched,
154 | you can now test the installation process on a VM.
155 | This VM has 3 hard drives, one for the OS
156 | and two in raid for the data.
157 |
158 | To do that, first we tweak the ports
159 | to more sensible defaults for a VM:
160 |
161 | ```bash
162 | $ echo 2222 > ./myskarabox/ssh_port
163 | $ echo 2223 > ./myskarabox/ssh_boot_port
164 | ```
165 |
166 | Then, start the VM:
167 |
168 | ```bash
169 | $ nix run .#myskarabox-beacon-vm &
170 | ```
171 |
172 | Now, skip to [step B](#b-run-the-installation-process).
173 |
174 | ### A.2. Install on a Real Server
175 |
176 | _This guide assumes you know how to boot your server on a USB stick._
177 |
178 | 1. Create the .iso file.
179 |
180 | ```bash
181 | $ nix build .#myskarabox-beacon
182 | ```
183 |
184 | 2. Copy the .iso file to a USB key. This WILL ERASE THE CONTENT of the USB key.
185 |
186 | ```bash
187 | $ nix run .#beacon-usbimager
188 | ```
189 |
190 | - Select `./result/iso/beacon.iso` file in row 1 (`...`).
191 | - Select USB key in row 3.
192 | - Click write (arrow down) in row 2.
193 |
194 | 3. Plug the USB stick in the server. Choose to boot on it.
195 |
196 | You will be logged in automatically with user `nixos`.
197 |
198 | 4. Note down the IP address and disk layout of the server.
199 | For that, follow the steps that appeared when booting on the USB stick.
200 | To reprint the steps, run the command `skarabox-help`.
201 |
202 | 5. Open the [./myskarabox/configuration.nix](./configuration.nix) file and tweak values to match your hardware.
203 |
204 | ### B. Run the Installation
205 |
206 | Create a `./myskarabox/facter.json` file containing
207 | the hardware specification of the host (or the VM) with:
208 |
209 | ```bash
210 | $ nix run .#myskarabox-get-facter > ./myskarabox/facter.json
211 | ```
212 |
213 | Add the `./myskarabox/facter.json` to git (run `git add ./myskarabox/facter.json`).
214 |
215 | Optionally, if you want to see exactly what `nixos-facter` did find
216 | and will configure, run one or both of:
217 |
218 | ```bash
219 | $ nix run .#myskarabox-debug-facter-nix-diff
220 | $ nix run .#myskarabox-debug-facter-nvd
221 | ```
222 |
223 | Now, run the installation process on the host:
224 |
225 | ```bash
226 | $ nix run .#install-on-beacon .#skarabox
227 | ```
228 |
229 | The server will reboot into NixOS on its own.
230 | Upon booting, the root partition will need to be decrypted
231 | as outlined in the next section.
232 |
233 | ## Normal Operations
234 |
235 | All commands are prefixed by the hostname, allowing to handle multiple hosts.
236 |
237 | 1. Decrypt root pool after boot
238 |
239 | ```bash
240 | $ nix run .#myskarabox-unlock
241 | ```
242 |
243 | The connection will then disconnect automatically with:
244 |
245 | ```
246 | Connection to closed.
247 | ```
248 |
249 | This is normal behavior.
250 |
251 | 2. SSH in
252 |
253 | ```bash
254 | $ nix run .#myskarabox-ssh
255 | ```
256 |
257 | 3. Reboot
258 |
259 | ```bash
260 | $ nix run .#myskarabox-ssh sudo reboot
261 | ```
262 |
263 | You will then be required to decrypt the hard drives upon reboot as explained above.
264 |
265 | 4. Deploy an Update
266 |
267 | Modify the [./configuration.nix](./configuration.nix) file then run:
268 |
269 | ```bash
270 | $ nix run .#deploy-rs
271 | ```
272 |
273 | 5. Update dependencies
274 |
275 | ```bash
276 | $ nix flake update
277 | $ nix run .#deploy-rs
278 | ```
279 |
280 | 6. Edit secrets
281 |
282 | ```bash
283 | $ nix run .#sops ./myskarabox/secrets.yaml
284 | ```
285 |
286 | 7. Add other hosts
287 |
288 | ```bash
289 | $ nix run .#gen-new-host otherhost.
290 | ```
291 |
292 | and copy needed config in `./flake.nix`.
293 |
294 | ## Post Installation Checklist
295 |
296 | These items act as a checklist that you should go through to make sure your installation is robust.
297 | How to proceed with each item is highly dependent on which hardware you have so it is hard for Skarabox to give a detailed explanation here.
298 |
299 | ### Domain Name
300 |
301 | Get your external IP Address by connecting to your home network and going to [https://api.ipify.org/](https://api.ipify.org/).
302 |
303 | - Buy a cheap domain name.
304 | I recommend [https://porkbun.com/](https://porkbun.com/) because I use it and know it works but others work too.
305 | - Configure the domain's DNS entries to have:
306 | - A record: Your domain name to your external IP Address.
307 | - A record: `*` (yes, a literal "asterisk") to your external IP Address.
308 |
309 | To check if this setup works, you will first need to go through the step below too.
310 |
311 | ### Router Configuration
312 |
313 | These items should happen on your router.
314 | Usually, connecting to it is done by entering one of the following IP addresses in your browser: `192.168.1.1` or `192.168.1.254`.
315 |
316 | - Reduce the DHCP pool to the bounds .100 to .200, inclusive.
317 | This way, you are left with some space to statically allocate some IPs.
318 | - Statically assign the IP address of the server.
319 | - Enable port redirection for ports to the server IP:
320 | - 80 to 80.
321 | - 443 to 443.
322 | - A random port to 22 to be able to ssh into your server from abroad.
323 | - A random port to 2222 to be able to start the server from abroad.
324 |
325 | To check if this setup works,
326 | you can connect to another network (like using the tethered connection from your phone or connecting to another WiFi network)
327 | and then ssh into your server like above,
328 | but instead of using the IP address, use the domain name in `./ip`.
329 |
330 | ### Add Services
331 |
332 | I do recommend using the sibling project [Self Host Blocks](https://github.com/ibizaman/selfhostblocks) to setup services like Vaultwarden, Nextcloud and others.
333 |
--------------------------------------------------------------------------------
/template/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Flake For Skarabox.";
3 |
4 | inputs = {
5 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
6 | skarabox.url = "github:ibizaman/skarabox";
7 |
8 | nixos-generators.url = "github:nix-community/nixos-generators";
9 | nixos-generators.inputs.nixpkgs.follows = "nixpkgs";
10 |
11 | nixos-anywhere.url = "github:nix-community/nixos-anywhere";
12 | nixos-anywhere.inputs.nixpkgs.follows = "nixpkgs";
13 |
14 | nixos-facter-modules.url = "github:numtide/nixos-facter-modules";
15 | flake-parts.url = "github:hercules-ci/flake-parts";
16 | deploy-rs.url = "github:serokell/deploy-rs";
17 | sops-nix.url = "github:Mic92/sops-nix";
18 | };
19 |
20 | outputs = inputs@{ self, skarabox, sops-nix, nixpkgs, flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } {
21 | systems = [
22 | "x86_64-linux"
23 | "aarch64-linux"
24 | ];
25 |
26 | imports = [
27 | skarabox.flakeModules.default
28 | ];
29 |
30 | skarabox.hosts = {
31 | myskarabox = {
32 | system = ./myskarabox/system;
33 | hostKeyPub = ./myskarabox/host_key.pub;
34 | ip = ./myskarabox/ip;
35 | sshPublicKey = ./myskarabox/ssh.pub;
36 | knownHosts = ./myskarabox/known_hosts;
37 | sshPort = ./myskarabox/ssh_port;
38 | sshBootPort = ./myskarabox/ssh_boot_port;
39 |
40 | modules = [
41 | sops-nix.nixosModules.default
42 | self.nixosModules.myskarabox
43 | ];
44 | };
45 | };
46 |
47 | flake = {
48 | nixosModules = {
49 | myskarabox = {
50 | imports = [
51 | ./myskarabox/configuration.nix
52 | ];
53 | };
54 | };
55 | };
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/template/myskarabox/configuration.nix:
--------------------------------------------------------------------------------
1 | # This is a NixOS Module.
2 | #
3 | # More info at:
4 | # - https://wiki.nixos.org/wiki/NixOS_modules
5 | # - https://nixos.org/manual/nixos/stable/#sec-writing-modules
6 | { lib, config, ... }:
7 | let
8 | inherit (lib) mkMerge;
9 | in
10 | {
11 | imports = [
12 | ];
13 |
14 | options = {
15 | };
16 |
17 | config = mkMerge [
18 | # Skarabox config. Update the values to match your hardware.
19 | {
20 | skarabox.hostname = "myskarabox";
21 | skarabox.username = "skarabox";
22 | skarabox.hashedPasswordFile = config.sops.secrets."myskarabox/user/hashedPassword".path;
23 | skarabox.facter-config = ./facter.json;
24 | skarabox.disks.rootPool = {
25 | disk1 = "/dev/nvme0n1"; # Update with result of running `fdisk -l` on the USB stick.
26 | disk2 = null; # Set a value only if you have a second disk for the root partition.
27 | reservation = "500M"; # Set to 10% of size SSD.
28 | };
29 | skarabox.disks.dataPool = {
30 | enable = true; # Disable if only an SSD for root is present.
31 | disk1 = "/dev/sda"; # Update with result of running `fdisk -l` on the USB stick.
32 | disk2 = "/dev/sdb"; # Update with result of running `fdisk -l` on the USB stick.
33 | reservation = "10G"; # Set to 5% of size Hard Drives.
34 | };
35 | # For security by obscurity, we choose another ssh port here than the default 22.
36 | skarabox.disks.bootSSHPort = ./ssh_boot_port;
37 | skarabox.sshPort = ./ssh_port;
38 | skarabox.sshAuthorizedKeyFile = ./ssh.pub;
39 | skarabox.hostId = ./hostid;
40 | skarabox.setupLanWithDHCP = true; # Set to false to disable the catch-all network configuration from skarabox and instead set your own
41 |
42 | # Hardware drivers are figured out using nixos-facter.
43 | # If it still fails to find the correct driver,
44 | # run the following command on the host:
45 | # nix shell nixpkgs#pciutils --command lspci -v | grep -iA8 'network\|ethernet'
46 | # then uncomment the following line
47 | # and replace the driver with the one obtained above.
48 | boot.initrd.availableKernelModules = [
49 | # "r8169" # this is an example
50 | ];
51 | # The following catch-all option is worth enabling too
52 | # if some drivers are missing.
53 | hardware.enableAllHardware = false;
54 |
55 | sops.defaultSopsFile = ./secrets.yaml;
56 | sops.age = {
57 | sshKeyPaths = [ "/boot/host_key" ];
58 | };
59 |
60 | sops.secrets."myskarabox/user/hashedPassword" = {
61 | # Keep this option true or the user will not be able to log in.
62 | # https://github.com/Mic92/sops-nix?tab=readme-ov-file#setting-a-users-password
63 | neededForUsers = true;
64 | };
65 | }
66 | # Your config
67 | {
68 | }
69 | ];
70 | }
71 |
--------------------------------------------------------------------------------
/template/myskarabox/host_key:
--------------------------------------------------------------------------------
1 |
2 |
3 | Use this command:
4 |
5 | rm host_key && nix shell .#openssh -- command ssh-keygen -t ed25519 -N "" -f host_key && chmod 600 host_key
--------------------------------------------------------------------------------
/template/myskarabox/hostid:
--------------------------------------------------------------------------------
1 |
2 |
3 | Use this command:
4 |
5 | nix shell .#util-linux --command uuidgen | head -c 8 > hostid
6 |
--------------------------------------------------------------------------------
/template/myskarabox/ip:
--------------------------------------------------------------------------------
1 | # Replace me with the IP or hostname of the server where the beacon is running.
--------------------------------------------------------------------------------
/template/myskarabox/known_hosts:
--------------------------------------------------------------------------------
1 |
2 |
3 | First, generate ./host_key and fill out ./ip.
4 | Also, tweak ./ssh_port and ./ssh_boot_port if the default values do not suit you.
5 | Then, use this command:
6 |
7 | nix run .#gen-knownhosts-file
8 |
--------------------------------------------------------------------------------
/template/myskarabox/secrets.yaml:
--------------------------------------------------------------------------------
1 | # I'm empty and in plain text right now
2 | # but I will contain soon be encrypted with all the secrets!
3 | skarabox:
4 | user:
5 | hashedPassword:
6 | disks:
7 | rootPassphrase:
8 | dataPassphrase:
9 |
--------------------------------------------------------------------------------
/template/myskarabox/ssh_boot_port:
--------------------------------------------------------------------------------
1 | 2222
--------------------------------------------------------------------------------
/template/myskarabox/ssh_port:
--------------------------------------------------------------------------------
1 | 22
--------------------------------------------------------------------------------
/template/secrets.yaml:
--------------------------------------------------------------------------------
1 | # I'm empty and in plain text right now
2 | # but I will contain soon be encrypted with all the secrets!
3 | skarabox:
4 | user:
5 | hashedPassword:
6 | disks:
7 | rootPassphrase:
8 | dataPassphrase:
9 |
--------------------------------------------------------------------------------
/template/sops.key:
--------------------------------------------------------------------------------
1 |
2 |
3 | Use this command:
4 |
5 | rm sops.key && nix shell .#age --command age-keygen -o sops.key
6 |
--------------------------------------------------------------------------------
/tests/default.nix:
--------------------------------------------------------------------------------
1 | { pkgs, system, nix-flake-tests }:
2 | let
3 | nix = "${pkgs.nix}/bin/nix --extra-experimental-features nix-command -L";
4 |
5 | toBashBool = v: if v then "true" else "false";
6 |
7 | templateTest = {
8 | name,
9 | rootDisk2,
10 | dataPool,
11 | }: pkgs.writeShellScriptBin name ''
12 | set -e
13 |
14 | e () {
15 | echo -e "\e[1;31mSKARABOX-TEMPLATE:\e[0m \e[1;0m$@\e[0m"
16 | }
17 |
18 | graphic=-nographic
19 | tmpdir=
20 | rootDisk2=${toBashBool rootDisk2}
21 | dataPool=${toBashBool dataPool}
22 |
23 | while getopts "gp:" o; do
24 | case "''${o}" in
25 | g)
26 | graphic=
27 | ;;
28 | p)
29 | tmpdir=''${OPTARG}
30 | ;;
31 | *)
32 | exit 1
33 | ;;
34 | esac
35 | done
36 | shift $((OPTIND-1))
37 |
38 | if [ -z "$tmpdir" ]; then
39 | tmpdir="$(mktemp -d)"
40 | e "Created temp dir at $tmpdir, will be cleaned up on exit or abort"
41 |
42 | # Kills all children bash processes,
43 | # like the one that will run in the background hereunder.
44 | # https://stackoverflow.com/a/2173421/1013628
45 | trap "rm -rf $tmpdir/* $tmpdir/.* $tmpdir; trap - SIGTERM && kill -- -$$ || :" SIGINT SIGTERM EXIT
46 | else
47 | e "Using provided temp dir $tmpdir, nothing will be cleaned up"
48 | fi
49 | cd $tmpdir
50 |
51 | e "Initialising template"
52 | echo skarabox1234 | ${nix} run ${../.}#init -- -v -y -s -p ${../.}
53 | echo 2223 > ./myskarabox/ssh_boot_port
54 | echo 2222 > ./myskarabox/ssh_port
55 | echo 127.0.0.1 > ./myskarabox/ip
56 | echo ${system} > ./myskarabox/system
57 | ${nix} run .#myskarabox-gen-knownhosts-file
58 | # Using a git repo here allows to only copy in the nix store non temporary files.
59 | # In particular, we avoid copying the disk*.qcow2 files.
60 | git init
61 | echo ".skarabox-tmp" > .gitignore
62 | git add .
63 | git config user.name "skarabox"
64 | git config user.email "skarabox@skarabox.com"
65 | git commit -m 'init repository'
66 | e "Initialisation done"
67 |
68 | if [ "$dataPool" = false ]; then
69 | sed -i 's-enable = true-enable = false-' ./myskarabox/configuration.nix
70 | fi
71 | if [ "$rootDisk2" = true ]; then
72 | sed -i 's-disk2 = null-disk2 = "/dev/nvme1n1"-' ./myskarabox/configuration.nix
73 | fi
74 |
75 | nix flake show
76 |
77 | e "Starting beacon VM."
78 |
79 | ${nix} run .#myskarabox-beacon-vm -- $graphic &
80 |
81 | sleep 10
82 |
83 | e "Starting ssh loop to figure out when beacon started."
84 | e "You might see some flickering on the command line."
85 | # We can't yet be strict on the host key check since the beacon
86 | # initially has a random one.
87 | while ! ${nix} run .#myskarabox-ssh -- -F none -o CheckHostIP=no -o StrictHostKeyChecking=no echo "connected"; do
88 | sleep 5
89 | done
90 | e "Beacon VM has started."
91 |
92 | e "Generating hardware config."
93 | ${nix} run .#myskarabox-get-facter > ./myskarabox/facter.json
94 | git add ./myskarabox/facter.json
95 | git commit -m 'generate hardware config'
96 | e "Generation succeeded."
97 |
98 | e "Starting installation on beacon VM."
99 | ${nix} run .#myskarabox-install-on-beacon -- .#myskarabox --no-substitute-on-destination
100 | e "Installation succeeded."
101 |
102 | e "Starting ssh loop to figure out when VM is ready to receive root passphrase."
103 | e "You might see some flickering on the command line."
104 | while ! ${nix} run .#myskarabox-boot-ssh -- -F none echo "connected"; do
105 | sleep 5
106 | done
107 | e "Beacon VM is ready to accept root passphrase."
108 |
109 | e "Decrypting root dataset."
110 | ${nix} run .#myskarabox-unlock -- -F none
111 | e "Decryption done."
112 |
113 | e "Starting ssh loop to figure out when VM has booted."
114 | e "You might see some flickering on the command line."
115 | while ! ${nix} run .#myskarabox-ssh -- -F none echo "connected"; do
116 | sleep 5
117 | done
118 | e "Beacon VM has started."
119 |
120 | e "Checking password for skarabox user has been set."
121 | hashedpwd="$(${nix} run .#sops -- decrypt --extract '["myskarabox"]["user"]["hashedPassword"]' ./myskarabox/secrets.yaml)"
122 | ${nix} run .#myskarabox-ssh -- -F none sudo cat /etc/shadow | ${pkgs.gnugrep}/bin/grep "$hashedpwd"
123 | e "Password has been set."
124 |
125 | e "Rebooting to confirm we can connect after a reboot."
126 | # We sleep first and run the whole script in the background
127 | # to avoid a race condition where the VM reboots too fast
128 | # and kills the ssh connection, resulting in the test failing.
129 | # So this is all so we can disconnect first.
130 | ${nix} run .#myskarabox-ssh -- -F none "(sleep 2 && sudo reboot)&"
131 | e "Rebooting in progress."
132 |
133 | e "Starting ssh loop to figure out when VM is ready to receive root passphrase."
134 | e "You might see some flickering on the command line."
135 | while ! ${nix} run .#myskarabox-boot-ssh -- -F none echo "connected"; do
136 | sleep 5
137 | done
138 | e "Beacon VM is ready to accept root passphrase."
139 |
140 | e "Decrypting root dataset."
141 | ${nix} run .#myskarabox-unlock -- -F none
142 | e "Decryption done."
143 |
144 | e "Starting ssh loop to figure out when VM has booted."
145 | e "You might see some flickering on the command line."
146 | while ! ${nix} run .#myskarabox-ssh -- -F none echo "connected"; do
147 | sleep 5
148 | done
149 | e "Beacon VM has started."
150 |
151 | e "Checking password for skarabox user is still set."
152 | ${nix} run .#myskarabox-ssh -- -F none sudo cat /etc/shadow | ${pkgs.gnugrep}/bin/grep "$hashedpwd"
153 | e "Password has been set."
154 |
155 | e "Deploying."
156 | ${nix} run .#deploy-rs
157 | e "Deploying done."
158 |
159 | e "Checking password for skarabox user is still set."
160 | ${nix} run .#myskarabox-ssh -- -F none sudo cat /etc/shadow | ${pkgs.gnugrep}/bin/grep "$hashedpwd"
161 | e "Password has been set."
162 |
163 | e "Connecting and shutting down"
164 | ${nix} run .#myskarabox-ssh -- -F none sudo shutdown
165 | e "Shutdown complete."
166 | '';
167 | in
168 | {
169 | lib = nix-flake-tests.lib.check {
170 | inherit pkgs;
171 | tests = pkgs.callPackage ./lib.nix {};
172 | };
173 |
174 | oneOSnoData = templateTest {
175 | name = "oneOSnoData";
176 | rootDisk2 = false;
177 | dataPool = false;
178 | };
179 |
180 | oneOStwoData = templateTest {
181 | name = "oneOStwoData";
182 | rootDisk2 = false;
183 | dataPool = true;
184 | };
185 |
186 | twoOSnoData = templateTest {
187 | name = "twoOSnoData";
188 | rootDisk2 = true;
189 | dataPool = false;
190 | };
191 |
192 | twoOStwoData = templateTest {
193 | name = "twoOStwoData";
194 | rootDisk2 = true;
195 | dataPool = true;
196 | };
197 | }
198 |
--------------------------------------------------------------------------------
/tests/lib.nix:
--------------------------------------------------------------------------------
1 | {
2 | pkgs,
3 | ...
4 | }:
5 | let
6 | add-sops-cfg = pkgs.callPackage ../lib/add-sops-cfg.nix {};
7 |
8 | exec = {
9 | name,
10 | cmd,
11 | init ? ""
12 | }: builtins.readFile ((pkgs.callPackage ({ runCommand }: runCommand name {
13 | nativeBuildInputs = [
14 | add-sops-cfg
15 | ];
16 | } (let
17 | initFile = pkgs.writeText "init-sops" init;
18 | in ''
19 | mkdir $out
20 | ${if init != "" then "cat ${initFile} > $out/.sops.yaml" else ""}
21 | add-sops-cfg -o $out/.sops.yaml ${cmd}
22 | '')) {}) + "/.sops.yaml");
23 | in
24 | {
25 | testAddSopsCfg_new_alias = {
26 | expected = ''
27 | keys:
28 | - &a ASOPSKEY
29 | '';
30 |
31 | expr = exec {
32 | name = "testAddSopsCfg_new_alias";
33 | cmd = "alias a ASOPSKEY";
34 | };
35 | };
36 |
37 | testAddSopsCfg_new_path_regex = {
38 | expected = ''
39 | keys:
40 | - &a ASOPSKEY
41 | creation_rules:
42 | - path_regex: a/b.yaml$
43 | key_groups:
44 | - age:
45 | - *a
46 | '';
47 |
48 | expr = exec {
49 | name = "testAddSopsCfg_new_path_regex";
50 | init = ''
51 | keys:
52 | - &a ASOPSKEY
53 | '';
54 | cmd = "path-regex a a/b.yaml$";
55 | };
56 | };
57 |
58 | testAddSopsCfg_update_alias = {
59 | expected = ''
60 | keys:
61 | - &a ASOPSKEY
62 | - &b BSOPSKEY
63 | creation_rules:
64 | - path_regex: a/b.yaml$
65 | key_groups:
66 | - age:
67 | - *a
68 | '';
69 |
70 | expr = exec {
71 | name = "testAddSopsCfg_update_alias";
72 | init = ''
73 | keys:
74 | - &a ASOPSKEY
75 | creation_rules:
76 | - path_regex: a/b.yaml$
77 | key_groups:
78 | - age:
79 | - *a
80 | '';
81 | cmd = "alias b BSOPSKEY";
82 | };
83 | };
84 |
85 | testAddSopsCfg_update_path_regex = {
86 | expected = ''
87 | keys:
88 | - &a ASOPSKEY
89 | - &b BSOPSKEY
90 | creation_rules:
91 | - path_regex: a/b.yaml$
92 | key_groups:
93 | - age:
94 | - *a
95 | - *b
96 | '';
97 |
98 | expr = exec {
99 | name = "testAddSopsCfg_update_path_regex";
100 | init = ''
101 | keys:
102 | - &a ASOPSKEY
103 | - &b BSOPSKEY
104 | creation_rules:
105 | - path_regex: a/b.yaml$
106 | key_groups:
107 | - age:
108 | - *a
109 | '';
110 | cmd = "path-regex b a/b.yaml$";
111 | };
112 | };
113 |
114 | testAddSopsCfg_append = {
115 | expected = ''
116 | keys:
117 | - &a ASOPSKEY
118 | creation_rules:
119 | - path_regex: a/b.yaml$
120 | key_groups:
121 | - age:
122 | - *a
123 | - path_regex: b/b.yaml$
124 | key_groups:
125 | - age:
126 | - *a
127 | '';
128 |
129 | expr = exec {
130 | name = "testAddSopsCfg_append";
131 | init = ''
132 | keys:
133 | - &a ASOPSKEY
134 | creation_rules:
135 | - path_regex: a/b.yaml$
136 | key_groups:
137 | - age:
138 | - *a
139 | '';
140 | cmd = "path-regex a b/b.yaml$";
141 | };
142 | };
143 |
144 | testAddSopsCfg_replace = {
145 | expected = ''
146 | keys:
147 | - &a OTHERSOPSKEY
148 | '';
149 |
150 | expr = exec {
151 | name = "testAddSopsCfg_replace";
152 | init = ''
153 | keys:
154 | - &a ASOPSKEY
155 | '';
156 | cmd = "alias a OTHERSOPSKEY";
157 | };
158 | };
159 |
160 | testAddSopsCfg_replace_with_reference = {
161 | expected = ''
162 | keys:
163 | - &b BSOPSKEY
164 | - &a OTHERSOPSKEY
165 | creation_rules:
166 | - path_regex: a/b.yaml$
167 | key_groups:
168 | - age:
169 | - *a
170 | - path_regex: b/b.yaml$
171 | key_groups:
172 | - age:
173 | - *b
174 | '';
175 |
176 | expr = exec {
177 | name = "testAddSopsCfg_replace";
178 | init = ''
179 | keys:
180 | - &a ASOPSKEY
181 | - &b BSOPSKEY
182 | creation_rules:
183 | - path_regex: a/b.yaml$
184 | key_groups:
185 | - age:
186 | - *a
187 | - path_regex: b/b.yaml$
188 | key_groups:
189 | - age:
190 | - *b
191 | '';
192 | cmd = "alias a OTHERSOPSKEY";
193 | };
194 | };
195 | }
196 |
--------------------------------------------------------------------------------