├── .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 | [![build](https://github.com/ibizaman/skarabox/actions/workflows/build.yaml/badge.svg)](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 | --------------------------------------------------------------------------------