Compare commits

..

No commits in common. "vm-test" and "master" have entirely different histories.

50 changed files with 2324 additions and 1877 deletions

6
.gitattributes vendored
View File

@ -1,3 +1,3 @@
#pattern filter=crypt diff=crypt #pattern filter=crypt diff=crypt merge=crypt
secrets/*/** filter=crypt diff=crypt secrets/*/** filter=crypt diff=crypt merge=crypt
secrets/default.nix filter=crypt diff=crypt secrets/default.nix filter=crypt diff=crypt merge=crypt

32
README.md Normal file
View File

@ -0,0 +1,32 @@
# Maxwell configuration
The NixOS configuration of Maxwell
## Switching configuration
1. Mount remotely the secrets directory
`$ rsshfs secrets maxwell:$PWD/secrets -o allow_other &`
2. Run nixos-rebuild
`$ nixos-rebuild test -I nixos-config=configuration.nix
--target-host maxwell --use-remote-sudo`
3. Unmount the secrets directory
`kill %%`
## Testing changes
1. Build a VM
`$ nixos-rebuild build-vm -I nixos-config=testing.nix`
2. Change the open files limit
`$ ulimit -n 90000`
3. Run the VM without internet (to avoid a mess)
`$ unshare -nc result/bin/run-maxwell-vm`

View File

@ -1,735 +0,0 @@
general:
debug : False # Debug mode, only for development
instance_name : "searxwell" # displayed name
search:
safe_search : 0 # Filter results. 0: None, 1: Moderate, 2: Strict
autocomplete : "" # Existing autocomplete backends: "dbpedia", "duckduckgo", "google", "startpage", "wikipedia" - leave blank to turn it off by default
language : "en-US"
ban_time_on_fail : 5 # ban time in seconds after engine errors
max_ban_time_on_fail : 120 # max ban time in seconds after engine errors
server:
port : 8083
bind_address : "127.0.0.1" # address to listen on
secret_key : "QzsHA00oA7H68Z2OXYYk3MaC6BjxkTiS"
base_url : "https://maxwell.ydns.eu/srx/"
image_proxy : False # Proxying image results through searx
http_protocol_version : "1.0" # 1.0 and 1.1 are supported
ui:
static_path : "" # Custom static path - leave it blank if you didn't change
templates_path : "" # Custom templates path - leave it blank if you didn't change
default_theme : oscar # ui theme
default_locale : "" # Default interface locale - leave blank to detect from browser information or use codes from the 'locales' config section
theme_args :
oscar_style : logicodev # default style of oscar
outgoing: # communication with search engines
request_timeout : 2.0 # seconds
useragent_suffix : "" # suffix of searx_useragent, could contain informations like an email address to the administrator
pool_connections : 100 # Number of different hosts
pool_maxsize : 10 # Number of simultaneous requests by host
engines:
- name : arch linux wiki
engine : archlinux
shortcut : al
- name : archive is
engine : xpath
search_url : https://archive.is/{query}
url_xpath : (//div[@class="TEXT-BLOCK"]/a)/@href
title_xpath : (//div[@class="TEXT-BLOCK"]/a)
content_xpath : //div[@class="TEXT-BLOCK"]/ul/li
categories : general
timeout : 7.0
disabled : True
shortcut : ai
- name : arxiv
engine : arxiv
shortcut : arx
categories : science
timeout : 4.0
- name : asksteem
engine : asksteem
shortcut : as
- name : base
engine : base
shortcut : bs
- name : wikipedia
engine : wikipedia
shortcut : wp
base_url : 'https://{language}.wikipedia.org/'
- name : bing
engine : bing
shortcut : bi
- name : bing images
engine : bing_images
shortcut : bii
- name : bing news
engine : bing_news
shortcut : bin
- name : bing videos
engine : bing_videos
shortcut : biv
- name : bitbucket
engine : xpath
paging : True
search_url : https://bitbucket.org/repo/all/{pageno}?name={query}
url_xpath : //article[@class="repo-summary"]//a[@class="repo-link"]/@href
title_xpath : //article[@class="repo-summary"]//a[@class="repo-link"]
content_xpath : //article[@class="repo-summary"]/p
categories : it
timeout : 4.0
disabled : True
shortcut : bb
- name : ccc-tv
engine : xpath
paging : False
search_url : https://media.ccc.de/search/?q={query}
url_xpath : //div[@class="caption"]/h3/a/@href
title_xpath : //div[@class="caption"]/h3/a/text()
content_xpath : //div[@class="caption"]/h4/@title
categories : videos
disabled : True
shortcut : c3tv
- name : crossref
engine : json_engine
paging : True
search_url : http://search.crossref.org/dois?q={query}&page={pageno}
url_query : doi
title_query : title
content_query : fullCitation
categories : science
shortcut : cr
- name : currency
engine : currency_convert
categories : general
shortcut : cc
- name : deezer
engine : deezer
shortcut : dz
- name : deviantart
engine : deviantart
shortcut : da
timeout: 3.0
- name : ddg definitions
engine : duckduckgo_definitions
shortcut : ddd
weight : 2
disabled : True
- name : digbt
engine : digbt
shortcut : dbt
timeout : 6.0
disabled : True
- name : digg
engine : digg
shortcut : dg
- name : erowid
engine : xpath
paging : True
first_page_num : 0
page_size : 30
search_url : https://www.erowid.org/search.php?q={query}&s={pageno}
url_xpath : //dl[@class="results-list"]/dt[@class="result-title"]/a/@href
title_xpath : //dl[@class="results-list"]/dt[@class="result-title"]/a/text()
content_xpath : //dl[@class="results-list"]/dd[@class="result-details"]
categories : general
shortcut : ew
disabled : True
- name : wikidata
engine : wikidata
shortcut : wd
timeout : 3.0
weight : 2
- name : duckduckgo
engine : duckduckgo
shortcut : ddg
disabled : True
- name : duckduckgo images
engine : duckduckgo_images
shortcut : ddi
timeout: 3.0
disabled : True
- name : etymonline
engine : xpath
paging : True
search_url : http://etymonline.com/?search={query}&p={pageno}
url_xpath : //a[contains(@class, "word--")]/@href
title_xpath : //p[contains(@class, "word__name--")]/text()
content_xpath : //section[contains(@class, "word__defination")]/object
first_page_num : 0
shortcut : et
disabled : True
- name : faroo
engine : faroo
shortcut : fa
disabled : True
- name : 1x
engine : www1x
shortcut : 1x
disabled : True
- name : fdroid
engine : fdroid
shortcut : fd
disabled : True
- name : flickr
categories : images
shortcut : fl
# You can use the engine using the official stable API, but you need an API key
# See : https://www.flickr.com/services/apps/create/
# engine : flickr
# api_key: 'apikey' # required!
# Or you can use the html non-stable engine, activated by default
engine : flickr_noapi
- name : free software directory
engine : mediawiki
shortcut : fsd
categories : it
base_url : https://directory.fsf.org/
number_of_results : 5
# what part of a page matches the query string: title, text, nearmatch
# title - query matches title, text - query matches the text of page, nearmatch - nearmatch in title
search_type : title
timeout : 5.0
disabled : True
- name : frinkiac
engine : frinkiac
shortcut : frk
disabled : True
- name : genius
engine : genius
shortcut : gen
- name : gigablast
engine : gigablast
shortcut : gb
timeout : 3.0
disabled: True
- name : gitlab
engine : json_engine
paging : True
search_url : https://gitlab.com/api/v4/projects?search={query}&page={pageno}
url_query : web_url
title_query : name_with_namespace
content_query : description
page_size : 20
categories : it
shortcut : gl
timeout : 10.0
disabled : True
- name : github
engine : github
shortcut : gh
- name : google
engine : google
shortcut : go
- name : google images
engine : google_images
shortcut : goi
- name : google news
engine : google_news
shortcut : gon
- name : google videos
engine : google_videos
shortcut : gov
- name : google scholar
engine : xpath
paging : True
search_url : https://scholar.google.com/scholar?start={pageno}&q={query}&hl=en&as_sdt=0,5&as_vis=1
results_xpath : //div[contains(@class, "gs_r")]/div[@class="gs_ri"]
url_xpath : .//h3/a/@href
title_xpath : .//h3/a
content_xpath : .//div[@class="gs_rs"]
suggestion_xpath : //div[@id="gs_qsuggest"]/ul/li
page_size : 10
first_page_num : 0
categories : science
shortcut : gos
- name : google play apps
engine : xpath
search_url : https://play.google.com/store/search?q={query}&c=apps
url_xpath : //a[@class="title"]/@href
title_xpath : //a[@class="title"]
content_xpath : //a[@class="subtitle"]
categories : files
shortcut : gpa
disabled : True
- name : google play movies
engine : xpath
search_url : https://play.google.com/store/search?q={query}&c=movies
url_xpath : //a[@class="title"]/@href
title_xpath : //a[@class="title"]/@title
content_xpath : //a[contains(@class, "subtitle")]
categories : videos
shortcut : gpm
disabled : True
- name : google play music
engine : xpath
search_url : https://play.google.com/store/search?q={query}&c=music
url_xpath : //a[@class="title"]/@href
title_xpath : //a[@class="title"]
content_xpath : //a[@class="subtitle"]
categories : music
shortcut : gps
disabled : True
- name : geektimes
engine : xpath
paging : True
search_url : https://geektimes.ru/search/page{pageno}/?q={query}
url_xpath : //article[contains(@class, "post")]//a[@class="post__title_link"]/@href
title_xpath : //article[contains(@class, "post")]//a[@class="post__title_link"]
content_xpath : //article[contains(@class, "post")]//div[contains(@class, "post__text")]
categories : it
timeout : 4.0
disabled : True
shortcut : gt
- name : habrahabr
engine : xpath
paging : True
search_url : https://habrahabr.ru/search/page{pageno}/?q={query}
url_xpath : //article[contains(@class, "post")]//a[@class="post__title_link"]/@href
title_xpath : //article[contains(@class, "post")]//a[@class="post__title_link"]
content_xpath : //article[contains(@class, "post")]//div[contains(@class, "post__text")]
categories : it
timeout : 4.0
disabled : True
shortcut : habr
- name : hoogle
engine : json_engine
paging : True
search_url : https://www.haskell.org/hoogle/?mode=json&hoogle={query}&start={pageno}
results_query : results
url_query : location
title_query : self
content_query : docs
page_size : 20
categories : it
shortcut : ho
- name : ina
engine : ina
shortcut : in
timeout : 6.0
disabled : True
- name: kickass
engine : kickass
shortcut : kc
timeout : 4.0
disabled : True
- name : library genesis
engine : xpath
search_url : https://libgen.is/search.php?req={query}
url_xpath : //a[contains(@href,"bookfi.net")]/@href
title_xpath : //a[contains(@href,"book/")]/text()[1]
content_xpath : //td/a[1][contains(@href,"=author")]/text()
categories : general
timeout : 7.0
disabled : True
shortcut : lg
- name : lobste.rs
engine : xpath
search_url : https://lobste.rs/search?utf8=%E2%9C%93&q={query}&what=stories&order=relevance
results_xpath : //li[contains(@class, "story")]
url_xpath : .//span[@class="link"]/a/@href
title_xpath : .//span[@class="link"]/a
content_xpath : .//a[@class="domain"]
categories : it
shortcut : lo
- name : microsoft academic
engine : microsoft_academic
categories : science
shortcut : ma
- name : mixcloud
engine : mixcloud
shortcut : mc
- name : nyaa
engine : nyaa
shortcut : nt
disabled : True
- name : openairedatasets
engine : json_engine
paging : True
search_url : https://api.openaire.eu/search/datasets?format=json&page={pageno}&size=10&title={query}
results_query : response/results/result
url_query : metadata/oaf:entity/oaf:result/children/instance/webresource/url/$
title_query : metadata/oaf:entity/oaf:result/title/$
content_query : metadata/oaf:entity/oaf:result/description/$
categories : science
shortcut : oad
timeout: 5.0
- name : openairepublications
engine : json_engine
paging : True
search_url : https://api.openaire.eu/search/publications?format=json&page={pageno}&size=10&title={query}
results_query : response/results/result
url_query : metadata/oaf:entity/oaf:result/children/instance/webresource/url/$
title_query : metadata/oaf:entity/oaf:result/title/$
content_query : metadata/oaf:entity/oaf:result/description/$
categories : science
shortcut : oap
timeout: 5.0
- name : openstreetmap
engine : openstreetmap
shortcut : osm
- name : openrepos
engine : xpath
paging : True
search_url : https://openrepos.net/search/node/{query}?page={pageno}
url_xpath : //li[@class="search-result"]//h3[@class="title"]/a/@href
title_xpath : //li[@class="search-result"]//h3[@class="title"]/a
content_xpath : //li[@class="search-result"]//div[@class="search-snippet-info"]//p[@class="search-snippet"]
categories : files
timeout : 4.0
disabled : True
shortcut : or
- name : pdbe
engine : pdbe
shortcut : pdb
# Hide obsolete PDB entries.
# Default is not to hide obsolete structures
# hide_obsolete : False
- name : photon
engine : photon
shortcut : ph
- name : piratebay
engine : piratebay
shortcut : tpb
url: https://pirateproxy.red/
timeout : 3.0
- name : pubmed
engine : pubmed
shortcut : pub
categories: science
timeout : 3.0
- name : qwant
engine : qwant
shortcut : qw
categories : general
disabled : True
- name : qwant images
engine : qwant
shortcut : qwi
categories : images
- name : qwant news
engine : qwant
shortcut : qwn
categories : news
- name : qwant social
engine : qwant
shortcut : qws
categories : social media
- name : reddit
engine : reddit
shortcut : re
page_size : 25
timeout : 10.0
disabled : True
- name : scanr structures
shortcut: scs
engine : scanr_structures
disabled : True
- name : soundcloud
engine : soundcloud
shortcut : sc
- name : stackoverflow
engine : stackoverflow
shortcut : st
- name : searchcode doc
engine : searchcode_doc
shortcut : scd
- name : searchcode code
engine : searchcode_code
shortcut : scc
disabled : True
- name : framalibre
engine : framalibre
shortcut : frl
disabled : True
# - name : searx
# engine : searx_engine
# shortcut : se
# instance_urls :
# - http://127.0.0.1:8888/
# - ...
# disabled : True
- name : semantic scholar
engine : xpath
paging : True
search_url : https://www.semanticscholar.org/search?q={query}&sort=relevance&page={pageno}&ae=false
results_xpath : //article
url_xpath : .//div[@class="search-result-title"]/a/@href
title_xpath : .//div[@class="search-result-title"]/a
content_xpath : .//div[@class="search-result-abstract"]
shortcut : se
categories : science
- name : spotify
engine : spotify
shortcut : stf
- name : subtitleseeker
engine : subtitleseeker
shortcut : ss
# The language is an option. You can put any language written in english
# Examples : English, French, German, Hungarian, Chinese...
# language : English
- name : startpage
engine : startpage
shortcut : sp
timeout : 6.0
disabled : True
- name : ixquick
engine : startpage
base_url : 'https://www.ixquick.eu/'
search_url : 'https://www.ixquick.eu/do/search'
shortcut : iq
timeout : 6.0
disabled : True
- name : swisscows
engine : swisscows
shortcut : sw
disabled : True
- name : tokyotoshokan
engine : tokyotoshokan
shortcut : tt
timeout : 6.0
disabled : True
- name : torrentz
engine : torrentz
shortcut : tor
url: https://torrentz2.eu/
timeout : 3.0
- name : twitter
engine : twitter
shortcut : tw
# maybe in a fun category
# - name : uncyclopedia
# engine : mediawiki
# shortcut : unc
# base_url : https://uncyclopedia.wikia.com/
# number_of_results : 5
# tmp suspended - too slow, too many errors
# - name : urbandictionary
# engine : xpath
# search_url : http://www.urbandictionary.com/define.php?term={query}
# url_xpath : //*[@class="word"]/@href
# title_xpath : //*[@class="def-header"]
# content_xpath : //*[@class="meaning"]
# shortcut : ud
- name : yahoo
engine : yahoo
shortcut : yh
disabled : True
- name : yandex
engine : yandex
shortcut : yn
disabled : True
- name : yahoo news
engine : yahoo_news
shortcut : yhn
- name : youtube
shortcut : yt
api_key: 'AIzaSyDvEpB_xVEk3Xt0IIU8sXbyEGIdjf33CEM'
engine : youtube_api
- name : dailymotion
engine : dailymotion
shortcut : dm
- name : vimeo
engine : vimeo
shortcut : vm
- name : wolframalpha
shortcut : wa
api_key: 'AUQ8EY-H452ETQ7RL'
engine : wolframalpha_api
timeout: 6.0
categories : science
- name : seedpeer
engine : seedpeer
shortcut: speu
categories: files, music, videos
disabled: True
- name : dictzone
engine : dictzone
shortcut : dc
- name : mymemory translated
engine : translated
shortcut : tl
timeout : 5.0
disabled : True
# You can use without an API key, but you are limited to 1000 words/day
# See : http://mymemory.translated.net/doc/usagelimits.php
# api_key : ''
- name : voat
engine: xpath
shortcut: vo
categories: social media
search_url : https://searchvoat.co/?t={query}
url_xpath : //div[@class="entry"]/p/a[@class="title"]/@href
title_xpath : //div[@class="entry"]/p/a[@class="title"]
content_xpath : //div[@class="entry"]/p/span[@class="domain"]
timeout : 10.0
disabled : True
- name : 1337x
engine : 1337x
shortcut : 1337x
disabled : True
- name : seznam
shortcut: szn
engine: xpath
paging : True
search_url : https://search.seznam.cz/?q={query}&count=10&from={pageno}
results_xpath: //div[@class="Page-content"]//div[@class="Result "]
url_xpath : ./h3/a/@href
title_xpath : ./h3
content_xpath : .//p[@class="Result-description"]
first_page_num : 0
page_size : 10
disabled : True
# - name : yacy
# engine : yacy
# shortcut : ya
# base_url : 'http://localhost:8090'
# number_of_results : 5
# timeout : 3.0
# Doku engine lets you access to any Doku wiki instance:
# A public one or a privete/corporate one.
# - name : ubuntuwiki
# engine : doku
# shortcut : uw
# base_url : 'http://doc.ubuntu-fr.org'
locales:
en : English
ar : العَرَبِيَّة (Arabic)
bg : Български (Bulgarian)
cs : Čeština (Czech)
da : Dansk (Danish)
de : Deutsch (German)
el_GR : Ελληνικά (Greek_Greece)
eo : Esperanto (Esperanto)
es : Español (Spanish)
fi : Suomi (Finnish)
fil : Wikang Filipino (Filipino)
fr : Français (French)
he : עברית (Hebrew)
hr : Hrvatski (Croatian)
hu : Magyar (Hungarian)
it : Italiano (Italian)
ja : 日本語 (Japanese)
nl : Nederlands (Dutch)
pl : Polski (Polish)
pt : Português (Portuguese)
pt_BR : Português (Portuguese_Brazil)
ro : Română (Romanian)
ru : Русский (Russian)
sk : Slovenčina (Slovak)
sl : Slovenski (Slovene)
sr : српски (Serbian)
sv : Svenska (Swedish)
tr : Türkçe (Turkish)
uk : українська мова (Ukrainian)
zh : 中文 (Chinese)
zh_TW : 國語 (Taiwanese Mandarin)
doi_resolvers :
oadoi.org : 'https://oadoi.org/'
doi.org : 'https://doi.org/'
doai.io : 'https://doai.io/'
sci-hub.tw : 'https://sci-hub.tw/'
default_doi_resolver : 'sci-hub.tw'

View File

@ -7,20 +7,28 @@
./packages.nix ./packages.nix
./jobs.nix ./jobs.nix
./matrix.nix ./matrix.nix
./email.nix
./magnetico.nix ./magnetico.nix
./nameserver.nix ./nameserver.nix
./custom ./custom
./secrets ./secrets
./fish.nix
./neovim.nix
]; ];
### State ### State
# Stateful things to do before updating: # Stateful things to do before updating:
# 1. Postgres migration # 1. Postgres migration (https://www.postgresql.org/docs/current/upgrading.html)
# 2. Matrix Synapse migration # 2. Matrix Synapse migration (https://matrix-org.github.io/synapse/latest/upgrade.html)
system.stateVersion = "20.03"; system.stateVersion = "23.05";
nixpkgs.source = builtins.fetchTarball
{ url = "https://github.com/NixOS/nixpkgs/archive/3f0a8ac25fb6.tar.gz";
sha256 = "10i7fllqjzq171afzhdf2d9r1pk9irvmq5n55h92rc47vlaabvr4";
};
boot.kernelPackages = pkgs.linuxPackages_latest; boot.kernelPackages = pkgs.linuxPackages_latest;
boot.tmpOnTmpfs = true; boot.tmp.useTmpfs = true;
boot.kernel.sysctl = { boot.kernel.sysctl = {
# avoid OOM hangs # avoid OOM hangs
"vm.admin_reserve_kbytes" = 262144; "vm.admin_reserve_kbytes" = 262144;
@ -30,34 +38,41 @@
i18n.defaultLocale = "en_US.UTF-8"; i18n.defaultLocale = "en_US.UTF-8";
systemd.enableEmergencyMode = false; systemd.enableEmergencyMode = false;
systemd.oomd.enable = false;
networking = { networking = {
hostName = "maxwell"; hostName = "maxwell";
firewall.allowedTCPPorts = [ firewall.allowedTCPPorts = [
53 # dns
443 80 # reverse proxy 443 80 # reverse proxy
8080 # hubot 993 # imaps server
5349 # turn server 25 465 # smtp(s) server
5350 # turn server
3551 # apcups
5001 # iperf server
18080 # monero p2p 18080 # monero p2p
20000 # syncthing transfert 22000 # syncthing transfer
64738 # mumble server 64738 # mumble server
]; ];
firewall.allowedUDPPorts = [ firewall.allowedUDPPorts = [
53 # powerdns 500 # ipsec
1194 # dnscrypt 53 # dns
21027 # syncthing discovery 21027 # syncthing discovery
64738 # mumble server 64738 # mumble server
]; ];
firewall.allowedUDPPortRanges = [
{ from=49152; to=49999; } # turn relay nftables.enable = true;
]; firewall.extraInputRules = ''
meta l4proto esp counter accept comment "allow IPsec"
ip saddr 192.168.1.0/24 tcp dport apcupsd accept comment "allow UPS from LAN"
'';
usePredictableInterfaceNames = false; usePredictableInterfaceNames = false;
nameservers = [ "127.0.0.1" ]; nameservers = [ "127.0.0.1" ];
hosts."127.0.0.1" = [ config.var.hostname ];
# ensure hostname works without DNS
hosts = with config.var;
{ ${ipv4LanAddress} = [ hostname ];
${ipv6Address} = [ hostname ];
};
}; };
# Only declarative users and no password logins # Only declarative users and no password logins
@ -65,7 +80,7 @@
users.users ={ users.users ={
# Only needed for local (read emergency) shell access # Only needed for local (read emergency) shell access
root.passwordFile = config.secrets.passwords.root; root.hashedPasswordFile = config.secrets.passwords.root;
# Admin # Admin
rnhmjoj = { rnhmjoj = {
@ -80,12 +95,11 @@
fazo = { fazo = {
extraGroups = [ "wheel" ]; extraGroups = [ "wheel" ];
isNormalUser = true; isNormalUser = true;
openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.fazo]; openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.fazo ];
}; };
# Runs two chatbots # User
meme = { meme = {
extraGroups = [ "ubino" "miguelbridge" ];
isNormalUser = true; isNormalUser = true;
shell = pkgs.fish; shell = pkgs.fish;
openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.meme ]; openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.meme ];
@ -103,19 +117,26 @@
builder = { builder = {
description = "Remote Nix builds user"; description = "Remote Nix builds user";
isNormalUser = true; isNormalUser = true;
openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.rnhmjoj-builder ]; openssh.authorizedKeys.keyFiles = with config.secrets.publicKeys; [
rnhmjoj-builder
giu-builder
];
}; };
# Use "git" instead of the default name to make # Use "git" instead of the default name to make
# SSH operation handier, example: # SSH operation handier, example:
# git clone git@maxwell:user/repo # git clone git@maxwell:user/repo
git = { git = {
group = "git";
description = "Git server user"; description = "Git server user";
home = "/var/lib/gitea"; home = "/var/lib/gitea";
isSystemUser = true;
useDefaultShell = true; useDefaultShell = true;
}; };
}; };
users.groups.git = { };
# Generate Diffie-Hellman parameters # Generate Diffie-Hellman parameters
# for TLS applications, like nginx. # for TLS applications, like nginx.
security.dhparams = { security.dhparams = {
@ -127,64 +148,57 @@
enable = true; enable = true;
# Users don't have a password # Users don't have a password
wheelNeedsPassword = false; wheelNeedsPassword = false;
extraConfig =
let
path = "/run/current-system/sw/bin";
journal = name: "${path}/journalctl -* ${name}";
services = lib.concatMapStringsSep "," (name: "${journal name}");
in ''
# Allow meme to see his logs.
Cmnd_Alias MEME_UNITS = ${services ["ubino" "miguelbridge"]}
meme ALL=(root) NOPASSWD: MEME_UNITS
'';
}; };
security.polkit.extraConfig = ''
// Allow meme to manage his services.
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
subject.user == "meme" &&
(action.lookup("unit") == "ubino.service" ||
action.lookup("unit") == "miguelbridge.service")) {
return polkit.Result.YES;
}
});
'';
# Limit user process to stop fork bombs
security.pam.loginLimits = [ security.pam.loginLimits = [
# Limit user process to stop fork bombs
{ domain = "@users"; { domain = "@users";
type = "hard"; type = "hard";
item = "nproc"; item = "nproc";
value = "400"; value = "400";
} }
# Disable core dumping
{ domain = "*";
type = "soft";
item = "core";
value = "0";
}
]; ];
### ACME certificates ### ACME certificates
security.acme = with config.var; { security.acme = {
email = "rnhmjoj@inventati.org"; defaults.email = "rnhmjoj@inventati.org";
acceptTerms = true; acceptTerms = true;
certs."${hostname}" = { certs."maxwell.eurofusion.eu" = {
group = "maxwell-ydns-eu"; group = "maxwell-eurofusion-eu";
}; };
certs."riot.${hostname}" = { certs."eurofusion.eu" = {
group = "riot-maxwell-ydns-eu"; group = "eurofusion-eu";
}; };
}; };
# Allow read access to ACME certificate # Allow read access to ACME certificate
# to specific (service) users. # to specific (service) users.
users.groups."maxwell-ydns-eu".members = [ "murmur" "turnserver" ]; users.groups."maxwell-eurofusion-eu".members = [ "murmur" "nginx" ];
users.groups."riot-maxwell-ydns-eu".members = [ "nginx" ]; users.groups."eurofusion-eu".members = [ "nginx" ];
# sensible logging
services.journald = {
storage = "volatile";
extraConfig = ''
RuntimeMaxUse=2G
'';
};
services.openssh = { services.openssh = {
enable = true; enable = true;
permitRootLogin = "no"; settings.PermitRootLogin = "no";
passwordAuthentication = false; settings.PasswordAuthentication = false;
challengeResponseAuthentication = false; settings.KbdInteractiveAuthentication = false;
}; };
# Traceroute easter egg # Traceroute easter egg
@ -201,21 +215,19 @@
### Mumble server ### Mumble server
services.murmur = { services.murmur = {
enable = true; enable = true;
password = "allwellthatmaxwell";
registerHostname = config.var.hostname; registerHostname = config.var.hostname;
registerName = "Maxwell Mumble"; registerName = "Maxwell Mumble";
registerPassword = config.secrets.murmur.password; registerPassword = "$REG_PASSWORD";
password = "$JOIN_PASSWORD";
users = 10; users = 10;
extraConfig = with config.var; '' environmentFile = config.secrets.environments.murmur;
sslCert=/var/lib/acme/${hostname}/fullchain.pem sslCert = "/var/lib/acme/${config.var.hostname}/fullchain.pem";
sslKey=/var/lib/acme/${hostname}/key.pem sslKey = "/var/lib/acme/${config.var.hostname}/key.pem";
'';
}; };
### Syncthing node ### Syncthing node
services.syncthing = { services.syncthing = {
enable = true; enable = true;
openDefaultPorts = true;
}; };
### Monero node with local RPC ### Monero node with local RPC
@ -248,26 +260,88 @@
### Git server ### Git server
services.gitea = with config.var; { services.gitea = with config.var; {
enable = true; enable = true;
domain = hostname;
appName = "Maxwell git server"; appName = "Maxwell git server";
rootUrl = "https://${hostname}/git/";
user = "git"; user = "git";
database.user = "git"; database.user = "git";
log.level = "Error";
cookieSecure = true;
disableRegistration = false;
settings = { settings = {
server.ROOT_URL = "https://${hostname}/git/";
server.domain = hostname;
session.COOKIE_SECURE = true;
log.LEVEL = "Error";
service.DISABLE_REGISTRATION = false;
# increase cookie expiration time
security.LOGIN_REMEMBER_DAYS = 365; security.LOGIN_REMEMBER_DAYS = 365;
# file upload size (MB)
attachment.MAX_SIZE = 10; attachment.MAX_SIZE = 10;
# new users can only create PR/issues
service.DEFAULT_ALLOW_CREATE_ORGANIZATION = false;
repository.MAX_CREATION_LIMIT = 0;
# somewhat limit spam
service.EMAIL_DOMAIN_BLOCKLIST = "gmail.com";
# allow the notify webhook to use matrix
webhook.ALLOWED_HOST_LIST = "maxwell.eurofusion.eu";
}; };
}; };
### Searx instance ### Searx instance
services.searx = { services.searx = {
enable = true; enable = true;
configFile = ./assets/searx-settings.yml; environmentFile = config.secrets.environments.searx;
package = pkgs.searxng;
# Use nginx+uWSGI
runInUwsgi = true;
uwsgiConfig = {
disable-logging = true;
# serve using the uwsgi protocol
socket = "/run/searx/uwsgi.sock";
chmod-socket = "660";
# use /searx as url "mountpoint"
mount = "/srx=searx.webapp:application";
module = "";
manage-script-name = true;
# caching
cache2 = lib.concatStringsSep ","
[ "name=searxcache"
"items=2000"
"blocks=2000"
"blocksize=4096"
"bitmap=1"
];
};
settings =
{ general.instance_name = "searxwell";
server.base_url = "https://${config.var.hostname}/";
server.secret_key = "@SEARX_SECRET@";
# Replace DOI links with Sci-Hub
default_doi_resolver = "sci-hub.st";
## Use authenticated APIs for some services
engines = [
{ name = "wolframalpha";
api_key = "@WOLFRAM_API_KEY@";
}
{ name = "youtube";
api_key = "@YOUTUBE_API_KEY@";
}
];
};
}; };
# Allow nginx access to the uwsgi socket
users.groups."searx".members = [ "nginx" ];
### Reverse Proxy ### Reverse Proxy
services.nginx = services.nginx =
@ -281,21 +355,13 @@
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
''; '';
in in
{ rec {
enable = true; enable = true;
enableReload = true; enableReload = true;
commonHttpConfig = '' recommendedTlsSettings = true;
# recommendedTlsSettings = true;
# android doesn't like this one:
# ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:42m;
ssl_session_timeout 23m;
ssl_prefer_server_ciphers on;
ssl_stapling on;
ssl_stapling_verify on;
'';
recommendedGzipSettings = true; recommendedGzipSettings = true;
recommendedProxySettings = true; recommendedProxySettings = true;
appendHttpConfig = disableLog;
# Large enough to allow file uploads. # Large enough to allow file uploads.
clientMaxBodySize = "1000M"; clientMaxBodySize = "1000M";
@ -303,38 +369,51 @@
sslDhparam = "${config.security.dhparams.path}/nginx.pem"; sslDhparam = "${config.security.dhparams.path}/nginx.pem";
# Maxwell # Maxwell
virtualHosts."${hostname}" = virtualHosts."${hostname}" = {
{ enableACME = true;
enableACME = true; forceSSL = true;
forceSSL = true; default = true;
default = true; extraConfig = enableSTS;
extraConfig = disableLog + enableSTS;
# Returns IP address # Returns IP address
locations."/ip".extraConfig = "return 200 $remote_addr;"; locations."/ip".extraConfig = ''
default_type text/plain;
return 200 $remote_addr;
'';
# Asjon code coverage reports # Asjon code coverage reports
locations."/asjon/report/" = { locations."/asjon/report/" = {
index = "index.html"; index = "index.html";
alias = "/var/lib/asjon/tree/report/"; alias = "/run/nginx/static/asjon/";
}; };
# Searx instance # Searx instance
locations."/srx/" = { locations."/srx/".extraConfig =
proxyPass = "http://localhost:8083/"; ''
extraConfig = '' include ${pkgs.nginx}/conf/uwsgi_params;
proxy_set_header X-Scheme $scheme; uwsgi_pass unix:/run/searx/uwsgi.sock;
proxy_set_header X-Script-Name /srx/;
proxy_buffering off;
''; '';
}; locations."/srx/static/".alias = "${config.services.searx.package}/share/static/";
# Git server # Git server
locations."/git/" .proxyPass = "http://localhost:3000/"; locations."/git/".proxyPass = "http://localhost:3000/";
# Syncthing # Syncthing
locations."/sync/".proxyPass = "http://localhost:8384/"; locations."/sync/".proxyPass = "http://localhost:8384/";
# User static files
locations."/~rnhmjoj/" = {
alias = "/run/nginx/static/rnhmjoj/";
extraConfig = ''
charset UTF-8;
# directories with listing
location ~ /~rnhmjoj/[^/]+.index/ { autoindex on; }
'';
};
locations."/~giu/" = {
alias = "/run/nginx/static/giu/";
extraConfig = "charset UTF-8;";
};
}; };
# Breve URL shortner # Breve URL shortner
@ -352,87 +431,82 @@
# The Cactalogue # The Cactalogue
virtualHosts."cacta.bit" = { virtualHosts."cacta.bit" = {
locations."/".alias = "/home/giu/cactalogue/"; root = "/run/nginx/static/cactalogue";
extraConfig = disableLog; extraConfig = disableLog;
}; };
virtualHosts."cacta.eurofusion.eu" = virtualHosts."cacta.bit";
}; };
# Bind mount directories for Nginx
# This avoids giving nginx traversal permission
systemd.mounts =
let bindNginx = from: to:
{ what = from;
where = "/run/nginx/static/" + to;
type = "none";
options = "bind";
wantedBy = [ "nginx.service" ];
};
in [ (bindNginx "/home/rnhmjoj/www" "rnhmjoj")
(bindNginx "/home/giu/www" "giu")
(bindNginx "/home/giu/cactalogue" "cactalogue")
(bindNginx "/var/lib/asjon/tree/report" "asjon")
];
### IPsec mesh
environment.etc."ipsec.d/mesh.secrets".source = config.secrets.passwords.mesh;
services.libreswan.enable = true;
services.libreswan.connections.mesh =
''
leftid=@wes
left=2a01:e11:1001:53ea::1
rightid=@maxwell
right=${config.var.ipv6Address}
authby=secret
type=transport
auto=ondemand
failureshunt=drop
negotiationshunt=hold
'';
### Misc. services ### Misc. services
services.ubino.enable = true;
services.miguelbridge.enable = true;
services.asjon.enable = true; services.asjon.enable = true;
# Needed for the Asjon memory module # Needed for the Asjon memory module
services.redis.enable = true; services.redis.servers."asjon" =
{ enable = true;
user = "asjon";
### Program configuration
programs = {
fish.enable = true;
mosh.enable = true;
tmux = {
enable = true;
newSession = true;
baseIndex = 1;
escapeTime = 0;
historyLimit = 4096;
keyMode = "vi";
terminal = "screen-256color";
customPaneNavigationAndResize = true;
extraConfig = ''
set -g mouse on
# bindings
bind | split-window -h
bind - split-window -v
bind : command-prompt
bind -n C-k clear-history
# colors
set -g pane-border-style fg=brightblack
set -g pane-active-border fg=green
set -g message-style fg=white,bg=black
set -g status-style fg=brightblue,bg=black
setw -g mode-style fg=black,bg=cyan
# status line
set -g status on
set -g status-justify left
set -g status-left ""
set -g status-right-length 60
set -g status-right '#[fg=yellow]#(cut -d\ -f 1-3 /proc/loadavg) | #[fg=brightgreen]%a %H:%M'
setw -g window-status-format "#[fg=black#,bg=brightblack] #I #[fg=blue#,bg=black] #W "
setw -g window-status-current-format "#[fg=white#,bg=cyan] #I #[fg=black#,bg=brightblack] #W "
'';
}; };
};
nix = { # Emergency SSH access via tor
useSandbox = true; services.tor =
{ enable = true;
client.enable = false;
relay.onionServices.emergency-access.map = [ 22 ];
};
nix.settings = {
# Can connect to the Nix daemon # Can connect to the Nix daemon
# and upload/run code as root! # and upload/run code as root!
trustedUsers = [ "builder" "rnhmjoj" ]; trusted-users = [ "builder" "rnhmjoj" ];
# Use at most half the cores # Use at most half the cores
buildCores = 8; cores = 8;
extraOptions = '' max-jobs = 16;
# Always keep at least 256MiB free # Always keep at least 256MiB free
min-free = 268435456 min-free = 268435456;
'';
}; };
environment.variables = { environment.sessionVariables = {
PATH = "$HOME/.local/bin/:$PATH"; PATH = [ "$HOME/bin" ];
XDG_CONFIG_HOME = "$HOME/.config"; XDG_CONFIG_HOME = "$HOME/etc";
XDG_DATA_HOME = "$HOME/.local/share"; XDG_DATA_HOME = "$HOME/var/lib";
XDG_CACHE_HOME = "$HOME/.cache"; XDG_CACHE_HOME = "$HOME/var/cache";
NIX_PROFILE = "$XDG_CONFIG_HOME/nix/profile"; SYSTEMD_COLORS = "16";
};
# Needed to make the mosh server survive a
# user logout: systemd kills everything by default
environment.shellAliases = {
mosh-server = "systemd-run --user --scope mosh-server";
}; };
} }

View File

@ -7,11 +7,12 @@
[ # Misc. system services [ # Misc. system services
./modules/breve.nix ./modules/breve.nix
./modules/asjon.nix ./modules/asjon.nix
./modules/ubino.nix
./modules/miguelbridge.nix
# Safely handle secrets # Safely handle secrets
./modules/secrets-store.nix ./modules/secrets-store.nix
# Pin Nixpkgs
./modules/nixpkgs.nix
]; ];
} }

View File

@ -27,21 +27,33 @@ in {
''; '';
}; };
group = mkOption {
type = types.str;
default = "asjon";
description = ''
Asjon will be run under this group (user will be created if it doesn't exist.
This can be your user name).
'';
};
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
users.extraUsers."${cfg.user}" = { users.users.${cfg.user} = {
group = cfg.group;
home = cfg.dataDir; home = cfg.dataDir;
isSystemUser = true;
createHome = true; createHome = true;
description = "asjon user"; description = "asjon user";
shell = "${pkgs.bash}/bin/bash"; shell = "${pkgs.bash}/bin/bash";
}; };
users.groups.${cfg.group} = { };
systemd.services.asjon = { systemd.services.asjon = {
description = "asjon: our chat bot"; description = "asjon: our chat bot";
after = [ "nginx.service" "matrix-synapse.service" "asjon-init.service" ]; after = [ "nginx.service" "matrix-synapse.service" "asjon-init.service" ];
requires = [ "nginx.service" "matrix-synapse.service" "asjon-init.service" ]; partOf = [ "nginx.service" "matrix-synapse.service" "asjon-init.service" ];
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
path = with pkgs; [ path = with pkgs; [
@ -61,11 +73,12 @@ in {
# Scripts # Scripts
AUTO_KILL_ON_UPDATE = "1"; AUTO_KILL_ON_UPDATE = "1";
AUTO_INFORM_ON_START = "!kvLvoCovzInhiablSq:maxwell.ydns.eu"; AUTO_INFORM_ON_START = "!XQJXsOXfTevAiEbDTA:eurofusion.eu";
ADMIN_ROOM = "!kvLvoCovzInhiablSq:maxwell.ydns.eu"; ADMIN_ROOM = "!XQJXsOXfTevAiEbDTA:eurofusion.eu";
REV_REMOTE_HOST = "proxy@rnhmjoj.ydns.eu"; REV_REMOTE_HOST = "proxy@rnhmjoj.ydns.eu";
REV_REMOTE_PORT = "22"; REV_REMOTE_PORT = "22";
REV_KEY = "~/.ssh/proxy"; REV_KEY = "~/.ssh/proxy";
REDIS_URL = "redis:///run/redis-asjon/redis.sock";
}; };
serviceConfig = { serviceConfig = {
@ -74,7 +87,7 @@ in {
Restart = "always"; Restart = "always";
WorkingDirectory = "${cfg.dataDir}/tree"; WorkingDirectory = "${cfg.dataDir}/tree";
# API keys and passwords definitions # API keys and passwords definitions
EnvironmentFile = config.secrets.asjon.environment; EnvironmentFile = config.secrets.environments.asjon;
}; };
}; };
@ -96,11 +109,6 @@ in {
git clone https://github.com/rnhmjoj/asjon.git ${cfg.dataDir}/tree git clone https://github.com/rnhmjoj/asjon.git ${cfg.dataDir}/tree
cd ${cfg.dataDir}/tree cd ${cfg.dataDir}/tree
yarn install yarn install
# give read/traverse permission to nginx
setfacl -m g:nginx:x ${cfg.dataDir}
setfacl -m g:nginx:x ${cfg.dataDir}/tree
setfacl -Rdm g:nginx:rx ${cfg.dataDir}/tree/report
''; '';
}; };

View File

@ -44,6 +44,15 @@ in {
''; '';
}; };
group = mkOption {
type = types.str;
default = "breve";
description = ''
Breve will run under this group (user will be created if it doesn't exist.
This can be your user name).
'';
};
hostname = mkOption { hostname = mkOption {
type = types.str; type = types.str;
default = config.networking.hostName; default = config.networking.hostName;
@ -98,11 +107,14 @@ in {
config = mkIf cfg.enable { config = mkIf cfg.enable {
users.extraUsers."${cfg.user}" = { users.users.${cfg.user} = {
isSystemUser = true; isSystemUser = true;
group = cfg.group;
description = "Breve daemon user"; description = "Breve daemon user";
}; };
users.groups.${cfg.group} = {};
networking.firewall = mkIf cfg.openPorts { networking.firewall = mkIf cfg.openPorts {
allowedTCPPorts = [ cfg.port ] allowedTCPPorts = [ cfg.port ]
++ optional (cfg.port == 443) 80; ++ optional (cfg.port == 443) 80;
@ -116,6 +128,7 @@ in {
environment.XDG_CONFIG_HOME = "${dataDir}/conf"; environment.XDG_CONFIG_HOME = "${dataDir}/conf";
serviceConfig = { serviceConfig = {
User = cfg.user; User = cfg.user;
Group = cfg.group;
ExecStart = "${pkgs.haskellPackages.breve}/bin/breve"; ExecStart = "${pkgs.haskellPackages.breve}/bin/breve";
Restart = "on-failure"; Restart = "on-failure";
StateDirectory = "breve"; StateDirectory = "breve";

View File

@ -1,52 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.miguelbridge;
in {
options.services.miguelbridge = {
enable = mkEnableOption "miguelbridge: Bridge Telegram - Matrix.";
user = mkOption {
type = types.str;
default = "miguelbridge";
description = ''
miguelbridge will be run under this user (user will be created if it doesn't exist.
This can be your user name).
'';
};
};
config = mkIf cfg.enable {
users.groups.miguelbridge = {};
users.extraUsers."${cfg.user}" = {
isSystemUser = true;
group = "miguelbridge";
description = "miguelbridge user";
};
systemd.services.miguelbridge = {
description = "miguelbridge: Bridge Telegram - Matrix";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Group = "miguelbridge";
ExecStart = "${pkgs.openjdk}/bin/java -jar MiguelBridge.jar";
Restart = "always";
StateDirectory = "miguelbridge";
WorkingDirectory = "%S/miguelbridge";
};
};
};
}

View File

@ -0,0 +1,39 @@
{ config, lib, ... }:
let
nixpkgs = config.nixpkgs.source;
conf = "${toString ../..}/configuration.nix";
rebuild = self: super:
{ nixos-rebuild = super.nixos-rebuild.overrideAttrs (old:
{ postInstall = old.postInstall +
''
sed -i "$target" \
-e '/^export PATH/ a \
export NIX_PATH="nixpkgs=${nixpkgs}:nixos-config=${conf}"' \
-e 's/remoteSudo=/remoteSudo=1/' \
-e 's/-A system/-A system --no-out-link/'
'';
});
};
in
{
options.nixpkgs.source = lib.mkOption
{ type = lib.types.path;
description = "Nixpkgs sources";
};
config =
{ nixpkgs.overlays = [ rebuild ];
nix.nixPath =
[ "nixpkgs=/run/current-system/nixpkgs"
"nixos-config=${conf}"
];
system.extraSystemBuilderCmds = "ln -s ${nixpkgs} $out/nixpkgs";
};
}

View File

@ -5,6 +5,8 @@ with lib;
let let
cfg = config.security.runtimeSecrets; cfg = config.security.runtimeSecrets;
secretsStore = "/var/secrets";
# A recursive attrset of submodule # A recursive attrset of submodule
storeType = types.attrsOf (types.submodule storeType = types.attrsOf (types.submodule
{ freeformType = storeType; { freeformType = storeType;
@ -49,23 +51,24 @@ let
let index = name: value: let index = name: value:
if isAttrs value && cond value if isAttrs value && cond value
then recurse (path ++ [name]) value then recurse (path ++ [name]) value
else singleton { path = path ++ [name]; value = value; }; else singleton { loc = path ++ [name]; value = value; };
in concatLists (mapAttrsToList index set); in concatLists (mapAttrsToList index set);
in recurse [] set; in recurse [] set;
isFile = v: isAttrs v && v.path != ""; isFile = v: isAttrs v && v.path != "";
# Secret files flattened to an index. This is needed # Secret files flattened to an index. This is needed
# to iterate over the set. # to iterate over the set. It contains: {name, path, value}
secretFiles = secretFiles =
filter (pair: isFile pair.value) (map (x: x // { name = concatStringsSep "-" x.loc; })
(attrsToIndex (v: !isFile v) cfg); (filter (pair: isFile pair.value)
(attrsToIndex (v: !isFile v) cfg)));
# Secrets with paths rewritten to the store location # Secrets with paths rewritten to the store location
storedSecrets = mapAttrsRecursiveCond (v: !isFile v) storedSecrets = mapAttrsRecursiveCond (v: !isFile v)
(names: secret: (names: secret:
if isFile secret if isFile secret
then "/run/secrets/${concatStringsSep "-" names}" then "${secretsStore}/${concatStringsSep "-" names}"
else secret) cfg; else secret) cfg;
in { in {
@ -76,7 +79,7 @@ in {
Definitions of runtime secrets. This is a freeform attributes Definitions of runtime secrets. This is a freeform attributes
set: it can contain arbitrarily nested sets of secrets. set: it can contain arbitrarily nested sets of secrets.
Secrets are paths to be copied into the secrets store Secrets are paths to be copied into the secrets store
(/run/secrets) with proper permission and owenership. (${secretsStore}) with proper permission and ownership.
''; '';
}; };
@ -109,33 +112,28 @@ in {
deps = [ ]; deps = [ ];
text = text =
'' ''
echo setting up secrets store... secret=${(head secretFiles).value.path}
rm -rf /run/secrets if test -f "$secret"; then
'' + concatMapStrings (pair: echo copying secrets...
let rm -rf ${secretsStore}
name = "${concatStringsSep "-" pair.path}"; ${concatMapStrings (f: ''
secret = pair.value; install -m ${f.value.mode} -D ${f.value.path} ${secretsStore}/${f.name}
in '') secretFiles}
'' fi
# Install secret ${name} '';
install -m ${secret.mode} -D ${secret.path} /run/secrets/${name}
'') secretFiles;
}; };
# Set secrets ownership, later because the # Set secrets ownership, later because the
# `user` activation script hasn't run yet. # `user` activation script hasn't run yet.
config.system.activationScripts.secrets-own = { config.system.activationScripts.secrets-own = {
deps = [ "secrets-copy" "users" ]; deps = [ "users" "groups" ];
text = concatMapStrings (pair: text =
let ''
name = "${concatStringsSep "-" pair.path}"; echo setting secrets ownership...
secret = pair.value; ${concatMapStrings (f: ''
in chown ${f.value.user}:${f.value.group} ${secretsStore}/${f.name}
'' '') secretFiles}
echo setting secrets store ownership... '';
# Set ownership of ${name}
chown ${secret.user}:${secret.group} /run/secrets/${name}
'') secretFiles;
}; };
} }

View File

@ -1,52 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.ubino;
in {
options.services.ubino = {
enable = mkEnableOption "Ubino: assistente virtuale di Ube, sottoforma di bot di Telegram.";
user = mkOption {
type = types.str;
default = "ubino";
description = ''
Ubino will be run under this user (user will be created if it doesn't exist.
This can be your user name).
'';
};
};
config = mkIf cfg.enable {
users.groups.ubino = {};
users.extraUsers."${cfg.user}" = {
isSystemUser = true;
group = "ubino";
description = "Ubino user";
};
systemd.services.ubino = {
description = "Ubino: assistente virtuale di Ube, sottoforma di bot di Telegram.";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Group = "ubino";
ExecStart = "${pkgs.openjdk}/bin/java -jar UbinoBot.jar";
Restart = "always";
StateDirectory = "ubino";
WorkingDirectory = "%S/ubino";
};
};
};
}

View File

@ -1,20 +1,35 @@
{ writeScriptBin, fish, curl { lib
, writers
, curl
, jq
, homeserver , homeserver
, roomId , roomId
, authToken , authToken
}: }:
writeScriptBin "notify" '' writers.writeDashBin "notify" ''
#!${fish}/bin/fish export PATH="$PATH:${lib.makeBinPath [ curl jq ]}"
set token (cat ${authToken}) if test $(id -u) != 0; then
if test (id -u) != 0
echo 'you must be root to send a notice' echo 'you must be root to send a notice'
exit 1 exit 1
end fi
set url '${homeserver}/rooms/${roomId}/send/m.room.message?access_token='$token token=$(cat ${authToken})
set msg '{"msgtype":"m.text", "body": "'$argv[1]'"}' url="${homeserver}/rooms/${roomId}/send/m.room.message?access_token=$token"
${curl}/bin/curl -s -XPOST -d $msg $url
if test $# -eq 1; then
# send first arg as text
msg=$(printf "%s" "$1" | jq -Rsc '{ "msgtype": "m.text", "body": . }')
else
# send stdin formatted as code
msg=$(jq -Rsc '{
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"body": "",
"formatted_body": ("<pre><code>" + . + "</code></pre>")
}')
fi
curl -s "$url" -d "$msg"
'' ''

96
email.nix Normal file
View File

@ -0,0 +1,96 @@
{ config, pkgs, ... }:
{
imports = [
(builtins.fetchTarball {
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-24.05/nixos-mailserver-nixos-24.11.tar.gz";
sha256 = "08zdidja5kdqgskynxsmcd8skh1b7cfl9ijjy9pak4b5h3aw2iqv";
})
];
mailserver = {
enable = true;
fqdn = "mail.eurofusion.eu";
domains = [ "eurofusion.eu" ];
messageSizeLimit = 78643200; # ~50MiB of base64 binary
loginAccounts = config.secrets.emailAccounts;
extraVirtualAliases = config.secrets.emailAliases;
# store state under /var
mailDirectory = "/var/lib/mail";
dkimKeyDirectory = "/var/lib/dkim";
mailboxes = {
# default IMAP folders
Sent = { specialUse = "Sent"; auto = "subscribe"; };
Drafts = { specialUse = "Drafts"; auto = "subscribe"; };
Spam = { specialUse = "Junk"; auto = "subscribe"; };
Trash = { specialUse = "Trash"; auto = "no"; };
};
# Use Let's Encrypt certificate
certificateScheme = "acme-nginx";
# There is one already (pdns-recursor)
localDnsResolver = false;
# Enable IMAPS (993), SMTPS (465)
enableImapSsl = true;
enableImap = false;
enableSubmissionSsl = true;
enableSubmission = false;
};
services.dovecot2.extraConfig = ''
# Improve hashing speed
auth_cache_verify_password_with_worker = yes
'';
services.postfix.extraConfig = ''
# Prefer IPv6
smtp_address_preference = ipv6
# Prevent binding on temporary addresses
smtp_bind_address6 = ${config.var.ipv6Address}
'';
# Keep the key stable across renewals (for DANE)
security.acme.certs.${config.mailserver.fqdn}.extraLegoRenewFlags = [ "--reuse-key" ];
# Utilities
environment.systemPackages = [
# computes the DANE records
(pkgs.writers.writeDashBin "mailserver-dane" ''
set -e
export PATH=${with pkgs; lib.makeBinPath [ coreutils openssl gawk ]}:$PATH
pubkey_hash() {
openssl x509 -noout -pubkey | \
openssl pkey -pubin -outform DER | \
sha256sum | cut -f1 -d' '
}
fqdn=${config.mailserver.fqdn}
cert="/var/lib/acme/$fqdn/cert.pem"
self=$(awk '{print $0} /END CERT/{exit}' "$cert" | pubkey_hash)
ca=$(awk '{if(keep) print $0} /END CERT/{keep=1}' "$cert" | pubkey_hash)
# main: DANE-EE(3) SPKI(1) SHA2-256(1)
printf '_25._tcp.%s. IN TLSA 3 1 1 %s\n' "$fqdn" "$self"
# fallback: DANE-TA(2) SPKI(1) SHA2-256(1)
printf '_25._tcp.%s. IN TLSA 2 1 1 %s\n' "$fqdn" "$ca"
'')
# computes the DKIM record
(pkgs.writers.writeDashBin "mailserver-dkim" ''
set -e
export PATH=${with pkgs; lib.makeBinPath [ coreutils gawk ]}:$PATH
domain=${builtins.elemAt config.mailserver.domains 0}
raw=$(cat ${config.mailserver.dkimKeyDirectory}/*.txt | tr -d '\n\t' | awk -F'"' '{print $2$4}')
printf 'mail._domainkey.%s IN TXT %s' "$domain" "$raw"
'')
];
}

211
fish.nix Normal file
View File

@ -0,0 +1,211 @@
{ ... }:
{
programs.fish.enable = true;
programs.fish.shellAbbrs =
{ e = "nvim";
l = "ls -lh";
ip = "ip -c";
iftop = "iftop -m 70M";
};
programs.fish.shellAliases =
{ namecoin-cli = "namecoin-cli -conf=$XDG_CONFIG_HOME/namecoin"; };
programs.fish.loginShellInit =
''
# Start abduco in ssh
if set -q SSH_CLIENT
# start abduco on ssh
if not set -q ABDUCO_SESSION
exec abduco -A ssh fish
else if test $SHLVL -eq 1
tput rmcup
end
end
'';
programs.fish.interactiveShellInit =
''
## Fish settings
# mixed emacs/vi
fish_hybrid_key_bindings
# kj to normal mode
bind -M insert kj "
if commandline -P;
commandline -f cancel;
else;
set fish_bind_mode default;
commandline -f backward-char repaint-mode;
end"
# fix unquoted URLs
set -U fish_features ampersand-nobg-in-token qmark-noglob
# change default cursor
set fish_cursor_insert underscore
## Color scheme
# syntax highlighting
set fish_color_command green
set fish_color_param normal
set fish_color_comment brcyan
set fish_color_operator purple
set fish_color_escape bryellow
set fish_color_redirection blue
set fish_color_selection --background=black
# completion/history
set fish_pager_color_prefix yellow
set fish_pager_color_description brblue
set fish_pager_color_progress brblack --background=black
set fish_color_search_match --background=black
# man pages colors
# bold, blink stop
set -x LESS_TERMCAP_md (set_color -o bryellow)
set -x LESS_TERMCAP_mb (set_color -u magenta)
set -x LESS_TERMCAP_me (set_color normal)
# standout start/stop
set -x LESS_TERMCAP_so (set_color brblue -b black)
set -x LESS_TERMCAP_se (set_color normal -b normal)
# underline start/stop
set -x LESS_TERMCAP_us (set_color -u brmagenta)
set -x LESS_TERMCAP_ue (set_color normal)
# used default LS_COLORS
eval (dircolors | sed 's/\(\w\+\)=/set \1 /')
## Aliases
# start process and detach
function start
nohup $argv > /dev/null 2>&1 &; disown
end
# start process without network access
function nnet
unshare -nc fish -ic "$argv"
end
# interactively rename files
function vimv
set tmp (mktemp --tmpdir -d vimv.XXX)
if set -q argv[1]
# directory listing
find $argv[1] -maxdepth 1 | sort | cat -n | tee $tmp/before > $tmp/after
else
# read from stdin
cat -n - | tee $tmp/before > $tmp/after
end
$EDITOR $tmp/after
# only print differing lines
awk '
NR==FNR { line=$0; sub($1, "", line); sub(/\s+/, "", line); lines[$1]=line; next }
NR!=FNR { line=$0; sub($1, "", line); sub(/\s+/, "", line);
if (!($1 in lines)) { printf("rm -vr \"%s\"\n", line); next }
if (lines[$1] != line) printf("mv -vin \"%s\" \"%s\"\n", line, lines[$1])
}
' $tmp/after $tmp/before | sh
rm -r $tmp
end
'';
programs.fish.promptInit =
''
# Outputs colored text
function color
set_color $argv[1]
for i in $argv[2..-1]
echo -n $i
end
set_color normal
end
# Git branch info
function git_branch
if not test -f .git/HEAD
return
end
set branch (git rev-parse --abbrev-ref HEAD 2> /dev/null)
if test $status -ne 0
set branch (cut -f 3 -d '/' .git/HEAD)
else if test $branch = HEAD
set branch (head -c 10 .git/HEAD)
end
timeout 0.1 git diff-files --quiet 2>/dev/null
if test $status -eq 1
set branch (color yellow $branch'*')
else
set branch (color green $branch)
end
timeout 0.1 git status --porcelain 2>/dev/null 1>| read untracked
if test -n "$untracked"
set dirty (color red '*')
end
echo " <$branch$dirty>"
end
# Left prompt
function fish_prompt
if fish_is_root_user
set prompt (color blue Λ)
set user (color red (whoami))
else
set prompt (color blue λ)
set user (color green (whoami))
end
if test \( "$LINES" -lt 10 \) -o \( "$COLUMNS" -lt 30 \)
echo "$prompt "
return
end
if set -q SSH_CLIENT
set host (color yellow (hostname))
else
set host (color green (hostname))
end
set git (git_branch)
set path (color cyan (prompt_pwd))
switch $fish_bind_mode
case default
set mode (color blue n)
case insert
set mode (color red i)
case visual
set mode (color yellow v)
end
echo "$user@$host $mode$path$git"
echo "$prompt "
end
# Right prompt
function fish_right_prompt
set code $status
if test $code -ne 0
set exitcode (color red $code" ")
end
if string match -q '/nix*' $PATH[1]
set nix " <"(color blue nix)">"
end
echo "$exitcode$nix"
end
function fish_mode_prompt; end
function fish_greeting; end
'';
}

View File

@ -12,7 +12,6 @@
]; ];
boot.loader.grub = { boot.loader.grub = {
enable = true; enable = true;
version = 2;
device = "/dev/sda"; device = "/dev/sda";
}; };
@ -31,7 +30,6 @@
fsType = "ext4"; fsType = "ext4";
}; };
nix.maxJobs = lib.mkDefault 16;
powerManagement.cpuFreqGovernor = "ondemand"; powerManagement.cpuFreqGovernor = "ondemand";
services.apcupsd = { services.apcupsd = {
@ -40,36 +38,33 @@
UPSTYPE usb UPSTYPE usb
UPSCABLE usb UPSCABLE usb
NETSERVER on NETSERVER on
NISPORT 3551
MINUTES 5 MINUTES 5
''; '';
hooks = hooks =
let let
# Send notifications on the Maxwell # Send notifications when something bad happens
# room when something bad happens.
notify = msg: ''${pkgs.maxwell-notify}/bin/notify "UPS: ${msg}"''; notify = msg: ''${pkgs.maxwell-notify}/bin/notify "UPS: ${msg}"'';
in in
{ {
changeme = notify "sostituire le batterie"; changeme = notify "replace batteries";
battdetach = notify "batterie disconnesse"; battdetach = notify "batteries disconnected";
battattach = notify "batterie riconnesse"; battattach = notify "batteries reconnected";
commfailure = notify "connessione persa"; commfailure = notify "connection lost";
commok = notify "connessione ristabilita"; commok = notify "connection enstablished";
loadlimit = notify "livello batterie critico (5%)"; loadlimit = notify "critical battery level (5%)";
runlimit = notify "autonomia batterie critico (5min)"; runlimit = notify "critical battery life (5min)";
doshutdown = notify "inizio sequenza di spegnimento"; doshutdown = notify "shutting down!";
powerout = notify "rete elettrica disconnessa"; powerout = notify "main power is out";
mainsback = notify "rete elettrica riconnessa"; mainsback = notify "main power is back";
onbattery = notify "attivate batterie"; onbattery = notify "batteries connected";
offbattery = notify "disattivate batterie"; offbattery = notify "batteries disconnected";
emergency = notify "malfunzionamento batterie, possibile spegnimento!"; emergency = notify "battery malfunction, possible shutdown!";
}; };
}; };
services.smartd = services.smartd =
let let
# Send a notification on the Maxwell # Send a notification when a disk is starting to fail
# when a disk is starting to fail.
failHook = with pkgs; writeScript "disk-fail-hook" '' failHook = with pkgs; writeScript "disk-fail-hook" ''
#!/bin/sh #!/bin/sh
${pkgs.maxwell-notify}/bin/notify \ ${pkgs.maxwell-notify}/bin/notify \

204
jobs.nix
View File

@ -4,126 +4,114 @@ with lib;
{ {
systemd.services.ydns = { systemd.services."notify-failed@" = {
description = "update ydns address record"; description = "notify that %i has failed";
after = [ "network-online.target" ]; scriptArgs = "%i";
startAt = "*:0/30"; path = [ pkgs.maxwell-notify ];
script = ''
unit=$1
notify "$unit: failed. last log lines:"
journalctl -u "$unit" -o cat -n 15 | notify
'';
};
serviceConfig.Type = "oneshot"; systemd.services.backup =
serviceConfig.environmentFile = config.secrets.ydns.environment; let
saved = pkgs.writeText "backup-saved" ''
/etc/lvm
/var/lib
/home
'';
path = with pkgs; [ curl cacert gawk iproute ]; excluded = pkgs.writeText "backup-excluded" ''
environment = { /var/lib/systemd
YDNS_HOST = config.var.hostname; /var/lib/udisks2
CURL_CA_BUNDLE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; /var/lib/postgresql
/var/lib/matrix-synapse/media_store/url_cache
/var/lib/matrix-synapse/media_store/url_cache_thumbnails
'';
in {
description = "system backup";
startAt = "*-*-* 03:00"; # every day at 3:00
onFailure = [ "notify-failed@backup.service" ];
serviceConfig = {
Type = "oneshot";
PrivateTmp = true;
PrivateMounts = true;
LimitNOFILE = 65536;
}; };
environment.BUP_DIR = "/mnt/backup";
path = with pkgs; [ bup git util-linux sudo gzip postgresql ];
script = '' script = ''
update() { # mount repository
ret=$(curl -$1 --basic --silent \ mount -m -L backup "$BUP_DIR"
-u "$YDNS_USER:$YDNS_PASSWD" \
"https://ydns.io/api/v1/update/?host=$YDNS_HOST&ip=$2" || exit 0)
case "$ret" in # init backup, if empty
ok) ! test -e $BUP_DIR/bupindex && bup init
echo "updated successfully: $YDNS_HOST ($2)"
;;
badauth) # build indices and save
echo "updated failed: $YDNS_HOST (authentication failed)" while read -r dir; do
;; {
name=$(basename "$dir")
echo indexing $name...
bup index "$dir" --exclude-from="${excluded}"
echo done
*) echo saving $name...
echo "update failed: $YDNS_HOST ($ret)" bup save -n "$name" "$dir" || true
;; echo done
esac } || true
} done < "${saved}"
update 4 "$(curl -s -4 https://ydns.io/api/v1/ip)" # postgresql backup
update 6 "$(ip addr show mngtmpaddr | awk '/inet6/{print $2; exit}' | cut -d/ -f1)" dir=/tmp/postgresql
mkdir -p "$dir"
echo dumping databases...
sudo -u postgres pg_dumpall > "$dir"/db.bak
echo done
echo saving...
bup index "$dir"
bup save -n postgresql "$dir" --strip-path=/tmp
echo done
echo generating par2 files...
bup fsck -j 8 -g
echo done
# prune backups every week
if test $(( $(date +%s) / 86400 % 7 )) -eq 0; then
echo pruning...
bup prune-older --keep-all-for 6m --keep-monthlies-for 2y --unsafe
echo done
fi
''; '';
}; };
systemd.services.backup = { systemd.services.namecoin-update =
description = "run system backup"; let
after = [ "network-online.target" ]; userFile = with config.services.namecoind;
startAt = "weekly"; pkgs.writeText "namecoin.conf" ''
rpcbind=${rpc.address}
rpcport=${toString rpc.port}
rpcuser=${rpc.user}
rpcpassword=${rpc.password}
'';
in {
description = "update namecoin names";
after = [ "namecoind.service" ];
startAt = "hourly";
onFailure = [ "notify-failed@namecoin-update.service" ];
serviceConfig.Type = "oneshot"; path = [ pkgs.namecoind ];
serviceConfig.Type = "oneshot";
path = with pkgs; [ bup git nfs-utils ]; serviceConfig.ExecStart = "${pkgs.haskellPackages.namecoin-update}/bin/namecoin-update ${userFile}";
};
environment.BUP_DIR = "/mnt/backup";
script = ''
${pkgs.fish}/bin/fish << 'EOF'
set locations \
/etc/lvm \
/etc/nixos \
/var/lib \
/home
set excluded \
/var/lib/alsa \
/var/lib/systemd \
/var/lib/udisks2 \
/var/lib/udev \
/var/lib/postgresql
# mount NFS share
mkdir -p $BUP_DIR
mount.nfs -o nolock 192.168.1.3:/maxwell $BUP_DIR
# check if properly mounted
if not mountpoint -q $BUP_DIR
echo mount failed! 1>&2
exit 1
end
# init backup
if not test -e $BUP_DIR/bupindex
bup init
end
# build indices and copy
for i in $locations
eval bup index $i --exclude=(string join " --exclude=" $excluded)
bup save -n (basename $i) $i
end
# postgresql backup
set dir /var/lib/postgresql-backup
mkdir -p $dir
sudo -u postgres pg_dumpall | gzip > $dir/db.bak
bup index $dir
bup save -n postgresql $dir
rm -rf $dir
umount /mnt/backup
EOF
'';
};
systemd.services.namecoin-update =
let
userFile = with config.services.namecoind;
pkgs.writeText "namecoin.conf" ''
rpcbind=${rpc.address}
rpcport=${toString rpc.port}
rpcuser=${rpc.user}
rpcpassword=${rpc.password}
'';
in {
description = "update namecoin names";
after = [ "namecoind.service" ];
startAt = "hourly";
path = [ pkgs.namecoind ];
serviceConfig.Type = "oneshot";
serviceConfig.ExecStart = "${pkgs.haskellPackages.namecoin-update}/bin/namecoin-update ${userFile}";
};
} }

View File

@ -3,7 +3,7 @@
# Setup: # Setup:
# Maxwell runs the web UI (magneticow) but doesn't # Maxwell runs the web UI (magneticow) but doesn't
# run the crawler (magneticod) because it's too # run the crawler (magneticod) because it's too
# network intesive. The latter is run by Wigfrid, # network intensive. The latter is run by Wigfrid,
# which periodically uploads a sqlite database. # which periodically uploads a sqlite database.
# Once received, Maxwell merges it with the local one. # Once received, Maxwell merges it with the local one.

View File

@ -1,31 +1,14 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
with config.var;
let let
### Element (Riot) configuration domain = "eurofusion.eu";
conf = with config.var; {
default_server_config."m.homeserver" =
{ base_url = "https://${hostname}";
server_name = "Maxwell";
};
default_server_config."m.identity_server" =
{ base_url = "https://matrix.org"; };
roomDirectory.servers = [ "matrix.org" hostname ];
brand = "Maxwell matrix";
defaultCountryCode = "IT";
showLabsSettings = true;
# Use a trusted Jitsi instance
jitsi.preferredDomain = "jitsi.openspeed.org";
jitsi.externalApiUrl = "https://jitsi.openspeed.org/libs/external_api.min.js";
};
in in
{ {
### Reverse proxy locations ### Reverse proxy locations
services.nginx.virtualHosts."${config.var.hostname}" =
# Setup for well-known on the bare domain
services.nginx.virtualHosts.${domain} =
let let
client = client =
{ "m.homeserver" = { "base_url" = "https://${config.var.hostname}"; }; { "m.homeserver" = { "base_url" = "https://${config.var.hostname}"; };
@ -34,6 +17,9 @@ in
server = { "m.server" = "${config.var.hostname}:443"; }; server = { "m.server" = "${config.var.hostname}:443"; };
in in
{ {
enableACME = true;
forceSSL = true;
# Needed for matrix federation # Needed for matrix federation
locations."/.well-known/matrix/server".extraConfig = '' locations."/.well-known/matrix/server".extraConfig = ''
add_header Content-Type application/json; add_header Content-Type application/json;
@ -47,40 +33,24 @@ in
add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON client}'; return 200 '${builtins.toJSON client}';
''; '';
# Forward matrix API calls to synapse
locations."/_matrix".proxyPass = "http://localhost:8448";
}; };
# Forward matrix/admin API calls to synapse
### Element/Riot static location services.nginx.virtualHosts.${config.var.hostname} =
services.nginx.virtualHosts."riot.${config.var.hostname}" = { locations."/_matrix".proxyPass = "http://localhost:8448";
{ enableACME = true; locations."/_synapse".proxyPass = "http://localhost:8448";
forceSSL = true; };
locations."/" =
{ index = "index.html";
alias = (pkgs.element-web.override { inherit conf; }) + "/";
};
};
### Homeserver ### Homeserver
services.matrix-synapse = { services.matrix-synapse.enable = true;
enable = true; services.matrix-synapse.settings = {
server_name = config.var.hostname; server_name = domain;
public_baseurl = "https://${config.var.hostname}/";
# Tell users about our TURN server
turn_uris = [
"turn:${config.var.hostname}:5349?transport=udp"
"turn:${config.var.hostname}:5350?transport=udp"
"turn:${config.var.hostname}:5349?transport=tcp"
"turn:${config.var.hostname}:5350?transport=tcp"
];
# Bind on localhost and used a reverse proxy # Bind on localhost and used a reverse proxy
listeners = [ listeners = [
{ bind_address = "localhost"; { bind_addresses = [ "localhost" ];
port = 8448; port = 8448;
type = "http"; type = "http";
tls = false; tls = false;
@ -94,30 +64,30 @@ in
# Connect to Postrges # Connect to Postrges
database_type = "psycopg2"; database_type = "psycopg2";
database_args = { database_args =
user = "matrix-synapse"; { user = "matrix-synapse";
database = "matrix-synapse"; database = "matrix-synapse";
}; };
# Make logging less verbose # Make logging less verbose
logConfig = '' log_config = pkgs.writeText "synapse-log.yml" ''
version: 1 version: 1
formatters: formatters:
journal_fmt: journal_fmt:
format: '%(name)s: [%(request)s] %(message)s' format: '%(name)s: [%(request)s] %(message)s'
filters: filters:
context: context:
(): synapse.util.logcontext.LoggingContextFilter (): synapse.util.logcontext.LoggingContextFilter
request: "" request: ""
handlers: handlers:
journal: journal:
class: systemd.journal.JournalHandler class: systemd.journal.JournalHandler
formatter: journal_fmt formatter: journal_fmt
filters: [context] filters: [context]
SYSLOG_IDENTIFIER: synapse SYSLOG_IDENTIFIER: synapse
root: root:
level: WARN level: WARN
handlers: [journal] handlers: [journal]
disable_existing_loggers: False disable_existing_loggers: False
''; '';
@ -125,65 +95,68 @@ in
expire_access_token = true; expire_access_token = true;
event_cache_size = "2K"; event_cache_size = "2K";
max_upload_size = "1000M"; max_upload_size = "1000M";
turn_user_lifetime = "1d"; dynamic_thumbnails = true;
# Needed to restrict access to the TURN
# server to only our matrix users.
turn_shared_secret = config.secrets.matrix.turn;
# Needed by the register_new_matrix_user script
registration_shared_secret = config.secrets.matrix.registration;
}; };
# Secrets
services.matrix-synapse.extraConfigFiles =
[
# Password reset via email
# Note: can't be put here, see NixOS/nixpkgs#158605
config.secrets.matrix.email.conf
# Needed by the register_new_matrix_user script
config.secrets.matrix.registration
];
### Database ### Database
services.postgresql.enable = true; services.postgresql.enable = true;
# Create database on the first run # Create databases on the first run
services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" '' services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" ''
CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse'; CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse" CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
TEMPLATE template0 TEMPLATE template0
LC_COLLATE = "C" LC_COLLATE = "C"
LC_CTYPE = "C"; LC_CTYPE = "C";
CREATE ROLE "mautrix-whatsapp" WITH LOGIN PASSWORD 'whatsapp';
CREATE DATABASE "mautrix-whatsapp" WITH OWNER "mautrix-whatsapp"
TEMPLATE template0
LC_COLLATE = "C"
LC_CTYPE = "C";
''; '';
### Whatsapp bridge
# Handles users behind a NAT, # allow synapse to read the shared secrets
# needed for reliable VoIP. users.users.matrix-synapse.extraGroups = [ "mautrix-whatsapp" ];
services.coturn = {
enable = true;
# Only allow users vouched for # Allow olm for mautrix-whatsapp
# by the Matrix server. nixpkgs.config.permittedInsecurePackages = [ "olm-3.2.16" ];
lt-cred-mech = true;
use-auth-secret = true;
static-auth-secret = config.secrets.matrix.turn;
# Use maxwell certificate for TLS services.mautrix-whatsapp =
realm = config.var.hostname; {
cert = "/var/lib/acme/${config.var.hostname}/fullchain.pem"; enable = true;
pkey = "/var/lib/acme/${config.var.hostname}/key.pem"; serviceDependencies = [ "postgresql.service" ];
settings.appservice =
# Port range for TURN relaying { database.type = "postgres";
min-port = 49152; database.uri = "postgresql:///mautrix-whatsapp?host=/run/postgresql";
max-port = 49999; };
settings.bridge =
# Enable TLS { encryption =
secure-stun = true; { allow = true;
no-tcp-relay = false; default = true;
require = true;
extraConfig = '' };
external-ip=${config.var.ipAddress} permissions =
cipher-list=HIGH { "eurofusion.eu" = "user";
no-loopback-peers "@rnhmjoj:eurofusion.eu" = "admin";
no-multicast-peers };
denied-peer-ip=10.0.0.0-10.255.255.255 relay.enabled = false;
denied-peer-ip=192.168.0.0-192.168.255.255 mute_bridging = true;
allowed-peer-ip=192.168.1.5 };
user-quota=12 };
total-quota=1200
verbose=true
'';
};
} }

View File

@ -1,46 +1,51 @@
{ config, ... }: { config, lib, ... }:
# Setup: # Setup:
# PDNS recursor on port 53 # pdns-recursor on localhost:54
# DNSCrypt wrapper on port 1194 # dnsdist on port 53 (DNS)
# NCDNS for Namecoin bit. zone resolution # ncdns for Namecoin bit. zone resolution
{ {
# Recursive DNS resolver # Recursive DNS resolver
services.pdns-recursor = { services.pdns-recursor =
enable = true; { enable = true;
# Configures the bit. zone # Configures the bit. zone
resolveNamecoin = true; resolveNamecoin = true;
dns.allowFrom = [ "0.0.0.0/0" ]; dns.port = 54;
}; };
# Wrap the local recursive resolver # Public DNS resolver
# in DNSCrypt on the default OpenVPN port. services.dnsdist =
# This port is chosen because it's usually { enable = true;
# not blocked in corporate networks. extraConfig = ''
services.dnscrypt-wrapper = { -- Listen on IPv6 and IPv4
enable = true; setLocal("[::]:53"); addLocal("0.0.0.0:53")
address = "0.0.0.0";
port = 1194; -- Allow everything
providerKey.public = config.secrets.dnscrypt.pub; setACL({"0.0.0.0/0", "::/0"})
providerKey.secret = config.secrets.dnscrypt.sec;
}; -- Set upstream resolver
newServer({address="[::1]:54", name="pdns"})
'';
};
# Namecoin resolver # Namecoin resolver
services.ncdns = { services.ncdns =
enable = true; { enable = true;
# This is currently broken, see ncdns issue: # This is currently broken, see ncdns issue:
# https://github.com/namecoin/ncdns/issues/127 # https://github.com/namecoin/ncdns/issues/127
dnssec.enable = false; dnssec.enable = false;
}; };
# Namecoin daemon with RPC server # Namecoin daemon with RPC server
services.namecoind = { services.namecoind =
enable = true; { enable = true;
# This are used by the resolver (ncdns) # This are used by the resolver (ncdns)
# to query the blockchain. # to query the blockchain.
rpc.user = config.secrets.namecoin.user; rpc.user = config.secrets.namecoin.user;
rpc.password = config.secrets.namecoin.password; rpc.password = config.secrets.namecoin.password;
}; };
users.users.namecoin.group = "namecoin";
} }

481
neovim.nix Normal file
View File

@ -0,0 +1,481 @@
{ config, lib, pkgs, ... }:
let
frameline = pkgs.callPackage (pkgs.fetchFromGitea
{ domain = "maxwell.eurofusion.eu/git";
owner = "rnhmjoj";
repo = "nvim-frameline";
rev = "v0.1.0";
sha256 = "PrTSSoXbu+qtTsJUv81z+MuTUmB1RHLPEWFQQnu6+J8=";
}) { };
plugins = with pkgs.vimPlugins;
[ # UI
undotree gitsigns-nvim
frameline nvim-fzf
# Syntax
playground
vim-pandoc-syntax
nix-queries fortran-queries
(nvim-treesitter.withPlugins (p: with p;
[ bash fish
c fortran haskell
html css
nix python lua
]))
# Misc
vim-fugitive supertab neomake
auto-pairs plenary-nvim
];
pack = pkgs.linkFarm "neovim-plugins"
(map (pkg:
{ name = "pack/${pkg.name}/start/${pkg.name}";
path = toString pkg;
}) (lib.concatMap (p: [p] ++ p.dependencies or []) plugins));
neovim-wrapped = pkgs.runCommand "${pkgs.neovim-unwrapped.name}"
{ nativeBuildInputs = [ pkgs.makeWrapper ]; }
''
mkdir -p "$out"
makeWrapper '${pkgs.neovim-unwrapped}/bin/nvim' "$out/bin/nvim" \
--add-flags "-u ${conf}"
'';
nix-queries = pkgs.writeTextDir "/queries/nix/injections.scm"
''
;; extends
; writeText highlight
(apply_expression function:
(apply_expression function: (_) @_func
argument: (string_expression (string_fragment) @injection.language))
argument: [
(string_expression (string_fragment) @injection.content)
(indented_string_expression (string_fragment) @injection.content)]
(#match? @_func "(^|\\.)writeText(Dir)?$")
(#gsub! @injection.language ".*%.(.*)" "%1")
(#set! injection.combined))
'';
fortran-queries = pkgs.writeTextDir "/queries/fortran/highlights.scm"
''
;; extends
(end_block_construct_statement) @keyword
(implicit_statement) @keyword
'';
conf = pkgs.writeText "init.lua" ''
local opt = vim.opt
local cmd = vim.api.nvim_command
local keymap = vim.keymap.set
local autocmd = vim.api.nvim_create_autocmd
-- Load plugins
opt.packpath = ${builtins.toJSON pack}
opt.runtimepath:prepend(${builtins.toJSON pack})
--
-- Options
--
local cache = os.getenv('XDG_CACHE_HOME')..'/nvim'
opt.directory = cache..'/tmp'
opt.backupdir = cache..'/tmp'
opt.shadafile = cache..'/shada'
opt.undodir = cache..'/undo'
opt.hidden = true -- Hide buffers
opt.mouse = 'a' -- Enable mouse support
opt.ignorecase = true -- Case insensitive search...
opt.smartcase = true -- ...for lowercase terms
opt.splitkeep = 'screen' -- keep text still when splitting
opt.laststatus = 3 -- global statusline
opt.fsync = true -- Sync writes
opt.swapfile = false -- Disable swap files
opt.writebackup = true -- Backup file before overwriting...
opt.backup = false -- ...but delete it on success
opt.undofile = true -- Store all changes
opt.modeline = false -- Disable for Security
opt.showmatch = true -- Highlight matched parenthesis
opt.clipboard = 'unnamedplus' -- Yank to clipboard
-- Files to ignore
opt.wildignore = {
'*.so', '*.hi', '*.a', '*.la', '*.mod',
'*/__pycache__/*',
'*/dist/*',
'*/result/*',
'*/.git/*'
}
opt.shiftwidth = 2 -- Tabs
opt.tabstop = 2 --
opt.expandtab = true --
opt.number = true -- Line numbering
opt.smartindent = true -- Indentation
opt.showmode = false -- Disable printing of mode changes
opt.ruler = false -- Already in statusline
opt.fillchars = {
eob=' ', -- Hide ~ on empty lines
vert='', -- make vertical split sign better
fold=' ', -- Hide . in fold markers
}
opt.foldmethod = "expr" -- Folding
opt.foldexpr = 'nvim_treesitter#foldexpr()' --
opt.foldlevel = 99 -- open by default
opt.foldopen:remove("search") -- don't open when searching
--
-- OSC 52 clipboard for SSH
--
if os.getenv('SSH_TTY') ~= nil then
vim.g.clipboard = {
name = 'OSC 52',
copy = {
['+'] = require('vim.ui.clipboard.osc52').copy('+'),
['*'] = require('vim.ui.clipboard.osc52').copy('*'),
},
paste = {
['+'] = require('vim.ui.clipboard.osc52').paste('+'),
['*'] = require('vim.ui.clipboard.osc52').paste('*'),
},
}
end
--
-- Terminal mode
--
-- Hide some UI elements
autocmd('TermOpen', {pattern='*', command='setlocal nonumber'})
autocmd('TermEnter', {pattern='*', command='set cmdheight=0 laststatus=0'})
autocmd('TermLeave', {pattern='*', command='set cmdheight=1 laststatus=3'})
autocmd('VimResized', {pattern='*', command='wincmd ='})
-- Exit without confirmation
autocmd('TermClose', {pattern='*', command='call feedkeys("\\<CR>")'})
keymap('t', '<C-b>', '<C-\\><C-n>', {noremap=true}) -- Easier escape
keymap('n', '<C-b>', '<Nop>', {noremap=true}) --
keymap('n', '<C-w>-', ':split +term<CR>', {silent=true}) -- Tmux-like moves
keymap('n', '<C-w>|', ':vsplit +term<CR>', {silent=true}) --
keymap('n', '<C-w>t', ':tabnew +term<CR>', {silent=true}) --
keymap('n', '<C-w>c', ':quit<CR>', {silent=true}) --
keymap('n', '<M-h>', '<C-w>h', {noremap=true}) --
keymap('n', '<M-j>', '<C-w>j', {noremap=true}) --
keymap('n', '<M-k>', '<C-w>k', {noremap=true}) --
keymap('n', '<M-l>', '<C-w>l', {noremap=true}) --
--
-- Keybindings
--
vim.g.mapleader = ','
function listToggle()
for _, win in ipairs(vim.fn.getwininfo()) do
if win.loclist == 1 then return cmd('lclose') end
end
cmd('lopen')
end
fzf = require'fzf'
function searchFiles()
local query = [[\( -name .git -o -name __pycache__ -o -path ./dist -o -path ./build \) -prune -o -type f]]
coroutine.wrap(function()
res = fzf.fzf('find '..query, "", {border='none'})
cmd('edit '..res[1])
end)()
end
function searchCommands()
coroutine.wrap(function()
local history = {}
for i = 1, vim.fn.histnr("cmd") do
history[i] = vim.fn.histget("cmd", i)
end
res = fzf.fzf(history, "", {border='none'})
vim.fn.feedkeys(':'..res[1], 't')
end)()
end
keymap('n', '<C-p>', searchFiles, {silent=true}) -- Fuzzy search files
keymap('n', '<C-e>', searchCommands, {silent=true}) -- Fuzzy search command history
keymap('n', '<Leader>u', ':UndotreeToggle<CR>', {silent=true}) -- Toggle UndoTree
keymap('n', '<leader>l', listToggle, {silent=true}) -- Toggle Neomake errors
keymap('i', 'kj', '<ESC>', {noremap=true}) -- Exit with kj
keymap('n', 'o', 'o<ESC>', {noremap=true}) -- Add empty lines
keymap('n', 'O', 'O<ESC>', {noremap=true}) --
keymap('x', 'p', 'p:let @+=@0<CR>', {noremap=true, silent=true}) -- Keep selection after p
keymap('c', 'w!!', 'w !sudo tee >/dev/null %', {silent=true}) -- Save with sudo
--
-- Colors
--
opt.bg = 'light' -- Use dark colors
function color(group, args)
vim.api.nvim_set_hl(0, group, args)
end
-- Source code
color('Cursor', {ctermfg=14})
color('Keyword', {ctermfg=04})
color('Define', {ctermfg=03})
color('Type', {ctermfg=06})
color('Identifier', {ctermfg=13})
color('Constant', {ctermfg=03})
color('Function', {ctermfg=02})
color('Include', {ctermfg=04})
color('Statement', {ctermfg=11})
color('String', {ctermfg=03})
color('Number', {ctermfg=04})
color('Comment', {ctermfg=07})
color('SpecialComment', {ctermfg=15})
color('Operator', {ctermfg='none'})
color('Conceal', {ctermfg='none', ctermbg='none'})
-- Text
color('Title', {ctermfg=4})
color('Special', {ctermfg=12})
color('Delimiter', {ctermfg=1})
color('PandocReferenceURL', {ctermfg=3})
color('PandocCiteKey', {ctermfg=6})
color('PandocTableDelims', {ctermfg=8})
color('texBeginEndName', {ctermfg=2})
color('Error', {ctermfg='none', ctermbg='none', underline=true})
-- Editor UI
color('NonText', {ctermfg=0})
color('LineNr', {ctermfg=8})
color('Pmenu', {ctermfg=12, ctermbg=0})
color('Folded', {ctermfg=7, ctermbg=0, cterm={}})
color('VertSplit', {ctermfg=8, cterm={}})
color('FoldColumn', {ctermfg='none', cterm={}})
color('Visual', {ctermfg='none', ctermbg=0})
color('Search', {ctermfg='none', ctermbg=0})
color('CurSearch', {ctermfg='none', ctermbg=0})
-- Diff mode
color('DiffAdd', {ctermfg=2, ctermbg=0, underline=true})
color('DiffChange', {ctermfg=3, ctermbg=0, underline=true})
color('DiffText', {ctermfg=1, ctermbg=0, underline=true})
color('DiffDelete', {ctermfg=0, ctermbg=1, underline=true})
-- Spelling
color('SpellBad', {ctermfg=1, ctermbg=0, underline=true})
color('SpellCap', {ctermfg=3, underline=true})
-- Neomake
color('NeomakeWarning', {ctermfg=3, underline=true})
color('ErrorMsg', {ctermfg=1, ctermbg='none'})
color('WarningMsg', {ctermfg=3})
-- Git signs
color('SignColumn', {ctermfg=1, ctermbg='none', cterm={}})
color('GitSignsAdd', {ctermfg=2})
color('GitSignsChange', {ctermfg=3})
color('GitSignsDelete', {ctermfg=1})
-- Statusline
color('StatusLine', {ctermfg=8, ctermbg=0, cterm={}})
color('StatusLineNC', {ctermfg=4, ctermbg=0, cterm={}})
color('User1', {ctermfg=8, ctermbg=0}) -- base
color('User2', {ctermfg=3, ctermbg=0}) -- location
color('StatusLineErr', {ctermfg=0, ctermbg=1})
color('StatusLineWarn', {ctermfg=0, ctermbg=3})
color('TablineTab', {ctermfg=8, ctermbg=0})
color('TablineTabCur', {ctermfg=7, ctermbg=8})
color('ModeNormal', {ctermfg=7, ctermbg=14})
color('ModeInsert', {ctermfg=7, ctermbg=1})
color('ModeCommand', {ctermfg=7, ctermbg=4})
color('ModeVisual', {ctermfg=7, ctermbg=3})
color('ModeVLine', {ctermfg=7, ctermbg=11})
color('ModeVBloc', {ctermfg=7, ctermbg=11})
color('ModeTerm', {ctermfg=7, ctermbg=2})
--
-- Plugin options
--
-- Neomake
vim.call('neomake#configure#automake', 'nwr', 750)
vim.g.neomake_warning_sign = {text='W', texthl='WarningMsg'}
vim.g.neomake_error_sign = {text='E', texthl='ErrorMsg'}
vim.g.neomake_highlight_lines = 1
vim.g.neomake_virtualtext_current_error = 0
vim.g.neomake_fortran_gfortran_args =
{'-fsyntax-only', '-Wall', '-Wextra', '-Jbuild/obj'}
-- Pandoc Markdown
autocmd({'BufNewFile', 'BufFilePre', 'BufRead'},
{pattern='*.md', command='set filetype=markdown.pandoc | TSBufDisable highlight'})
-- Git signs
gitsigns = require'gitsigns'
gitsigns.setup{signs={
add = {text='+'},
change = {text='δ'},
delete = {text='-'},
topdelete = {text=''},
changedelete = {text='~'}}
}
keymap('n', '<leader>gb', function() gitsigns.blame_line{full=true} end, {silent=true})
keymap('n', '<leader>gp', function() gitsigns.preview_hunk() end, {silent=true})
-- Tree-sitter
require'nvim-treesitter.configs'.setup{
highlight={enable=true},
indent={enable=true},
}
-- Non built-in filetypes
autocmd({'BufNewFile', 'BufRead'},
{pattern='*.nix', command='setlocal filetype=nix'})
--
-- Statusline
--
--
local frameline = require 'frameline'
local utils = frameline.utils
function mode(win, buf)
if not win.is_active then return end
local k = vim.api.nvim_get_mode().mode
local modes = {
n={'Normal', 'Normal'},
i={'Insert', 'Insert'},
R={'Replas', 'Insert'},
v={'Visual', 'Visual'},
V={'VLine', 'VLine'},
t={'Termin', 'Term'},
['']={'VBloc', 'VBloc'}
}
return utils.highlight('Mode'..modes[k][2], ' '..modes[k][1]..' ')
end
function branch(_, buf)
local head = vim.fn.FugitiveHead()
if buf.modifiable and head ~= "" then return ' '..head end
end
function filename(_, buf)
local delta = ""
if buf.modified then delta = 'Δ' end
if not buf.modifiable then delta = '' end
local fname = buf.name ~= "" and utils.filename or 'new-file'
return delta..fname
end
function neomake()
local res, msg = vim.call('neomake#statusline#LoclistCounts'), ""
if res.E then
msg = msg..utils.highlight('StatusLineErr', ' '..res.E..'E ')
end
if res.W then
msg = msg..utils.highlight('StatusLineWarn', ' '..res.W..'W ')
end
return msg
end
function readonly(_, buf)
if buf.modifiable and buf.readonly then return '' end
end
function encoding(_, buf)
return buf.fileencoding ~= "" and buf.fileencoding or nil
end
function filetype(_, buf)
return buf.filetype ~= "" and buf.filetype or 'no ft'
end
-- Tabline
frameline.setup_tabline(function()
local segments = {}
local api = vim.api
local color = '%#StatusLine#'
-- Tabs
local current = api.nvim_get_current_tabpage()
local label = ' %d %s '
for i, tab in pairs(api.nvim_list_tabpages()) do
-- tab -> active win -> active buf -> name
local active_buf = api.nvim_win_get_buf(api.nvim_tabpage_get_win(tab))
local name = api.nvim_buf_get_name(active_buf)
name = vim.fn.fnamemodify(name, ':t') -- filename only
local group = tab == current and 'TablineTabCur' or 'TablineTab'
table.insert(segments, utils.highlight(group, label:format(i, name)))
end
table.insert(segments, color)
table.insert(segments, utils.split)
-- Current date
table.insert(segments, vim.fn.strftime("%a %H:%M"))
-- Battery level
local level = io.popen('cat /sys/class/power_supply/BAT*/capacity'):read()
local symbol = ' '..utils.highlight('User2', '')..color
local battery = level and symbol..level..'%% ' or ""
table.insert(segments, battery)
return table.concat(segments)
end)
-- Statusline
frameline.setup_statusline(function()
local segments = {}
-- Left section
table.insert(segments, utils.subsection{items={mode}})
table.insert(segments, utils.subsection{
separator=' ',
items={branch, filename, readonly},
})
table.insert(segments, utils.split)
-- Right section
table.insert(segments, utils.subsection{items={neomake}})
table.insert(segments, utils.subsection{
user=2,
separator=':', stop=' ',
items={utils.line_number, utils.column_number},
})
table.insert(segments, utils.subsection{
user=1,
separator=' ',
items={utils.percent, encoding, filetype}
})
return segments
end)
'';
in
{
# nix build -f '<nixpkgs/nixos>' pkgs.neovim for testing
nixpkgs.overlays = lib.singleton (self: super: {
neovim = neovim-wrapped;
});
}

View File

@ -1,36 +1,38 @@
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
let
unstable = import <nixos-unstable> { };
in
{ {
nixpkgs.overlays = lib.singleton nixpkgs.overlays = lib.singleton
(self: super: (self: super:
{ maxwell-notify = self.callPackage ./custom/packages/maxwell-notify.nix { maxwell-notify = self.callPackage ./custom/packages/maxwell-notify.nix
{ homeserver = "https://${config.var.hostname}/_matrix/client/r0"; { homeserver = "https://${config.var.hostname}/_matrix/client/r0";
roomId = "!FsUSHSNMPMVTFFcvJo:maxwell.ydns.eu"; roomId = "!mKSxsQWEtUvOBTfjDU:eurofusion.eu";
authToken = config.secrets.passwords.matrix; authToken = config.secrets.passwords.matrix;
}; };
monero = unstable.monero; haskellPackages = super.haskellPackages.extend (hself: hsuper:
element-web = unstable.element-web; { breve = super.haskell.lib.overrideCabal hsuper.breve
(old: { broken = false; });
});
}); });
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
# utilities # utilities
iftop curl ranger neovim iftop curl tree neovim
nix-script openssl nix-script openssl
jq ack jq ack sshfs abduco
# backup # backup
bup git nfs-utils bup git
# admin # admin
dnsutils dnsutils
matrix-synapse matrix-synapse
matrix-synapse-tools.synadm
maxwell-notify maxwell-notify
smartmontools smartmontools
# namecoin
namecoind haskellPackages.rosa
]; ];
} }

View File

@ -1,41 +1,41 @@
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 HnLfAynO1w==

View File

@ -1,224 +1,224 @@
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

View File

@ -1,44 +1,63 @@
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
Rm3AOulV1eQ8pzOfXrkmDYIwLE49yNGH+w== 4LxykOuMbvkymmDcZ0XrwSQFOlg6qaLmGmmtHkF/ZNNC/Y0Z1Qk3EvJEwXA+BLXa
kWlBFqtEV39j0lBVf/9JOEayveFbVqzzqpwZ+azyo64XQt6CFH92WzOTiS7/HThS
S+46LDSfIs64VU0wEKDJpbbLP4vLjC3s/9qWFrQ7f97DjSZl+zv3pMldF8pInv8x
UaXNG3C7tQd1pD433MGXDWeygNZ2Fuv139lrjzwlXYkrq9OZg+bdeNm/uSfAQSsW
U39HMaZrgc8cANoFXIabPAtnePdS3ne+qr2JhYCGztYfyFQlaa97FeveYxqXMtt+
go8FgTvg7mBEtv6IAKwty/VFv2C7s5TCdExAfg2KQETjEDmSN8OtxPTx4Xqf7q8K
rVI1vsXDKsgwG4Uh8oEtWH6PaImbkD6Dx1751frsTIqJmP/WJithNiim/KA8+aBC
G0RTdl2oDEyyS64i6g7oscPkRjOFxfT0asdSLJcK1EFaZ3epyYNN2EXSLPeHZiRD
y8cv+dGq/7Oi0Vyw62jH9e+6uQyKHdP4oKJ58Dbm8B7KkY58tfCME7lK96uZtxiM
qPeeJmB/tYUu2O4HQhkmopixhTxnPiJkIx8qfVgKkrl/JqXYJH8R6Ud2bYVW4HYR
Tr2fkfQONaSx4DE85UI80A49KA5X+mZR8XOcJRhOPQRFmIM//goJlmGdcCQ3ddCp
r6/C8gTwbLXvcvJ6cvkyn0Z042sgj8i7m6eXQADgM/eQo4ki1SlmwxJgzC2350/5
dxjUitBLijrNvUeepX4xOSFkr2Wu3u7aQXt+19fokaP+U2wbXKqCNqrumDVBPWbP
mxnO7rX9wCb6+kfBJiQ4Tqbsh/TxMKVOzK4xFB0vgJOACkj6a5dVjOaJshVwrA28
/F35nwIBo3ig4LaT271gImo+XY36TZEe42MtSJ+Oopy1ENsK4Ii8gORi3lQhsozC
RdwN8jgL4ejc+NS6/a4bjLFrt1oMka2xPuIaNGpX/EzuSO+syk2sxk2vg52Qk5Oa
Zobc4MULf3/vT4rpgQuCUgyeHqzDFd3SlpXllpb6MXbP0K/ZNtg+VTJ6aGOZafKv
F49gm+bcC70SEMqIqHV2Fn0Z6DFhnbeFtq/uUUNN0bMelHghnvNNQ6FvPZOOmPdH
YcHX4LzrTtak7Xfu5lWT1h5leWG5iEHTpzj3Lvkdfb1Q38maqGGv7TQEQkHhQIfe
/RRwMmPObImKt129XslxTT7g5j0=

View File

@ -0,0 +1,10 @@
U2FsdGVkX19SrNDcNQsdjfo/tfZAOq9SOyciusnUhmQwo3TTr+19L5zWErgFPF7n
FLIPBXWOHWJp8S8NwGbwGmg22Pp7rvns/5QeMVlwrRZErsdcmW16OOx36s0fO6Yj
xH2wLL8P3YDsEdTRAKwWmjzng/DCFH+3xhy2NbpsdAoiDIQTXt33v0damhj7Mj0I
Yc2uoSzYjGuV/AMmIW56uR9GLY27dFT+AdbSoQThRHiy9EMjPDsyg1e4a1TjOje+
Ls/DO7nGpqIwZx2ZMbo8glTJPR+oYnzepmGNB3ehdLsivJirDxbsIc6Js9uE36er
qB4usUsIrz8H6HYNg/VHVQBNm4BhLDf9ij0NZC1dryuY/zBGgLYHcoVa9Bj7qhOn
A3tWtKc9/MgINf/kRlMdchKZG6sGRc3VcsB9mDYngJeuiShuysmCgajBCahdY4O1
Ctx9VSIFhVlkrlZoE6SoCw/z3MmJsJa/7Aoa2c2hzbIj+m6C0LyEHbCleP+6zCAF
YY5aJdXWbr2mDTOiaFYprtcYC4A7ZEgt3c51H+Lx/r/GLzhU28BYc81SL4+9LUwv
4cDJf/HNTxxp4YyDl52BZD5wS2tzlgwW5ekkXWC3OVpcC0Nlstrd6blHGVM=

View File

@ -0,0 +1,3 @@
U2FsdGVkX1+iMoNxF8ynmoanDXQhWQndNxROETBHF/DNS0Guiy+YKkV6GhpotHjY
0xWuD3VVFP8zwOx3rWfnlvd34+cqrjgbBoyXDh9Q+mlMMGd3HMHJ1UxnXZSJV5Jx
6+NIKbE=

View File

@ -0,0 +1,5 @@
U2FsdGVkX1/iadIyfkanSYsP/deFkKz7qDB6n1fgcrrwJRFipVuyd1R5ph4g9+fs
jI0a9x+VDI5BWiEVnG6jPHH0uYf5OvAEJp+lGAB2Qqs3TyNEQzAUTs80Ag2V6SuF
KPAiahzl0afwe37jWmjktO7nuMqc5aZZdF+SpgFW9rM6UCsOwe7DYXMOWxUZ+7Lu
f1aCBQOgSq1ISkY2if80RrsYqDiOED7GuEWSoxRA7oS3LWVE0Ieic3WdvHm1+4gP
r8hqrKB44MtDSg/GCSlECM0jBy0yMfEqlY45EaogS54LUphox/o3luVaV+A=

View File

@ -1 +0,0 @@
U2FsdGVkX18b5NlteArdllLCmrXQfit7yWS6pgZ4896+2BLosJFP0y/BauxAkNjU

View File

@ -1,2 +0,0 @@
U2FsdGVkX1+O0rTJQvBCLAdswjOexMM5VjlnGw3sVO/i1dYF/T0UrhTBhsqLboLA
jMv9wkzfbf2LiejBVhnOM0BEOU2FYRt5crZDkUAO090=

View File

@ -1,9 +1,9 @@
U2FsdGVkX1+Ay4K0S+xuJiyoMRj00oMkaw5sYVvu9VBR68aypK8eLTng8xoqKwzm U2FsdGVkX190tK2fRtlU6XqGGKq0rzfIAZQz12ysC+Ltrjgwt95tduUxOcHr32x1
uD1YNhLdh515CgHyMI7/LraT2yDYIlF+pNEfftH6U2qU5IfWSoukD59RscfaAft+ sXgMjc+ZjL76jQV5UVxCRSqpLj46lqJoAQX4CujTk1yoRbG0fZFGqgJr8OQIQGI3
/dend9Y6HyG1WdlyPyLVabruHFScx3d+oaLwEcgggnI/M9coWnHyvBXspo6E75um gkwVqtpMyxSE91o62IAmnHFztL+MUTJ+hZjpDo8IkcHDAZIXB7gRgrRUye1Jj0hV
gyvFntN4GmJLf1sMQIn0I7lW9djC8nupjSTstRo5HNLM/LwlhwAYRb/jbJUOkYSK f6eDIaw5P0wzzq5y7fNzbNzM4cK5IjWQtOyhrpcsmjaTzy2S9qzKib3xtAqwMfUC
D/SDHW5p/9OrACQzAKHFB3mg4+9SufD+cju8qIAn9uFcyCJxkri6Mz+SGqdXtSgi 7or6RLdKWrDcUOZPJWp8nI/cbIITGpNA0hsSK+LV4gbSwzcfhyr00OCGHKMVTf/5
fEZE2r1aCUXFa8Nq+qoYbxVue3BFlzxetC7fZrx2zWnmkSgOn7LWDn6q3B3KWeT5 sEBFZmrcdsGxqvWe1D3Hf2CZS3e9iWzBlu4v78jHhLuXc/6+ltPFosZ5AwSJbnMo
om/g+Ph/RE4piKzm9m2jIx+0TlkUHlpOKAf4Xzwdaivmm6HaCNc5pt1Hw0le1fTW m7tTLPMWMmUYmD4E5I2znITrtg7dtPYXItqsTMoHQJFMX0Kst/U6TqFF3Jcxv55s
JE+6BkXFDJz/8ytROujTGlMaMCB/JHgK04diEAnQJmNQnYVG03PxmRHmmqXc1czQ uTS1p0I/kqldY4p8Fmz2XgNCgm0QvQu5UJukQkuAj0PVOdRtpOR47CqqF0basD82
OnIzyUraBCpBsHSAVsN/afC8 LQ6EbLe863TL1hhdo+bP907A

View File

@ -0,0 +1,3 @@
U2FsdGVkX1/H11TSJKN/qICnL3UpcHDu7b7mnYfvFE76gKtmueKaKgT45+XTI+Ye
3IBFJDs2p7goQawjXjbN4IWS+6q+DQFnLXcOwQiJiMJtCipLLDImg/TrK/9yyiC1
M7AmptY=

View File

@ -1,3 +1,3 @@
U2FsdGVkX19/6c3AzyWTN5p17ujhKlbDdk91iQs9z7Q0HiyA4L7BKFT/ZOk4VNqY U2FsdGVkX1/UgigdAI8G1DADfHlOdqKX2TLIy/33gEUA+sADMY2MjCsBnVgirHe6
2Yh7r0b1F1ScFjvKH3aJ7jYGHT0i+w3LSHsufCDATEUejN/Z9JtEIXYaodOCJaYE oFyts9qlwfUmRjiIiB1GYm0GhZ9YkgzZLgRSw7sSqtqiyjt6glZd7OXwt3/pyTs8
YuIaLuBwWdkAPUl1lhs= TY7U3rA=

View File

@ -1,3 +1,3 @@
U2FsdGVkX1/StXpT1GfebxPB+1TyCHLo5fjFZLNkkWXnCS04WnREE2xlV7OXw0Iq U2FsdGVkX1+j3nwfqf4SUkH2HoSAMqkoYJYgRDPRknNOur7PZ5Ctx+eh2RYufCXd
llqZTflZ/z1hSz7NuUO/vrR57RRo6icf3UXnxvJ8HD6Z9q7uxI+WpIj+ME2zij6B 5yrTs/23jetpqjBGztmrPwZcN1AioGDEMKruyI+Cpr1RLE8SKrRjSvTXXK7nJHhI
Jg== Fw==

View File

@ -1,7 +1,2 @@
U2FsdGVkX19J8VE7lArWiwLIURQ8NjPEUwkOAh4m1oR0yrmBCI5u/vhwSQTC+ETb U2FsdGVkX1/284cSqdL3Tn4Yv682x+kNJ4OMUrOgaD05vJpoQ284b8Bji4PYvOYG
S3b80dqDKkR8QKRxIaquJHw/KRvQqKViZbu1OsHQTPQhK//mvs6vZ8G00vfucphc UazEOc260aNPofw=
6XIuiJS0u1zbzP6CKoLlkUyVOxFsmVSmxRx8460vgqK00JHSXf82mCAXcePVfHX9
uV9w34x3QkqSzmptx1orJrWa/Y/+et19ghJ/d6Utll+kg5Ldkd6vYcSA5bYFMe6L
LzAjJDLSvRpGkwP7EH2/9Kin5qDA7OUQmrXyFvmb9viCnYSD4TUaxXHYy4SPYcPY
qgFFeNDry5PAhkqLCTKgQWylCZXNbnA7JHp5fdbQCyRFD2sNxVN9ptuqNJd5x+hf
0PzTFokhgtE=

View File

@ -1,3 +1,3 @@
U2FsdGVkX18yVvW7ZvcS0Xc/LsBJmDBTjmHeODQqsVSq8AlzjHH0Z15cY2ibL0+2 U2FsdGVkX1//NF0E4XObFG2/rvsrFG7AG6OaLwJDSaJlOed/DbGkXE1d7bTa9SX1
/fq+Sb12nfYhXkdFePGNJl+pwTVN2KmQhQtTPUawwa0bmvqC3wPXmHn8O1AndVP9 7g7SeTKm7KdXsb8vxyjWbjRIBw/RQln3IM77bdDBCSCmsLp+HsHndv0QeXSUhSMP
8g== rg==

View File

@ -1,3 +1,3 @@
U2FsdGVkX19MH3jJZJHEhLZLqIGcQCvd7JS2I8vWztP1Htde6A/xfy3zP8U6NUOc U2FsdGVkX18Qd6gBIE7im5jGZlFK2r9QRPtRj/MtweDdMtXPxO/JbN8zxlGIibcx
QPBYfycwXLqUM89gVrKnnj28HQiAzQNf2zzPqG7MOpQKA6zdRF6i9n+CGtvXC36u 5XxR5dtAQE5++pBw1mY8nxrtZIJLAWSS4r3TWDQnNr7XSM3wP8/kxCUL7KvhJ0d+
zQ== Tw==

View File

@ -1,3 +1,3 @@
U2FsdGVkX18X2ltRnCWQnXMSt/FSKiq/ScbhdjFP4wmPHi5njgtam/c1Dg+0T1fj U2FsdGVkX1/OVD62ZQPn1AUSo2ZWvjHbmfv9x52rv//gH+5wO0bApyxtHHsCZe63
JzOYe53LglUBfjDMbIepcIymHXPteizligpJzNE7DwuzsCp2JTkn9KWzKJb45Qa/ DbfdwiQNPCYKZOYvvf6tzqQsxbZwN0kBeyWKmxK35ZZpuVqA1RGgMB1pk7Ue1iBQ
/UtVdTfkS9WH z9Ta00qOXeWw

2
secrets/matrix/email.pwd Normal file
View File

@ -0,0 +1,2 @@
U2FsdGVkX186wcN9NuydqFDXes3OG2eoric99wmndrgVNV0RXxmEYK3MRkJHFYwj
24nAVlJ8yc8jzXbd7tcewDCzXn0Ac1ERKsuxvFw=

View File

@ -0,0 +1,6 @@
U2FsdGVkX19+JQDp/hdBBgL5TR0tiYujBpbUQ3e7ArhQI9xbGKeRiKi4Bk4tw8rV
MrxwU3Fk95sY75vsnU3uvkMSo6KFVbiOLiGTiFwnT3gwwHWKem1yxJCLmxcP+h4G
SGu8lcGpM4ZUy2yAnt7WowyzQiYmO0Vp8xP1RCmH0z2UdcDhqZB9LjKgEnpVSC4I
i5Se0fX9PB5/oWMCc0kPX9XYz0+/hPlgzcbaS6GT8mN0o08rHtMhN2gvV/xlONZ4
V0JXg7SYuTXz8cRtjLIr3sIwCOU+uBqrIHHvtjFclto0/zsFtfa00FomIMFDCv40
UHE2e7HJc4EXQT55QlcIbL4PdtxTI5gp+Id+eSI6vZF+dPWHKYO4Ug==

View File

@ -0,0 +1,2 @@
U2FsdGVkX1/r1JwvxVZfT2snMpDmQdldp0FvWJ+szrSoIpvmW0MRzqu9t5sC/hvC
XZPhTw7lwakhlw+sERpjQBxBN5TFVy1OBenOERtnnvW9D+E=

View File

@ -1,10 +0,0 @@
U2FsdGVkX18ufnreJQQJJ52gMxajdK5bLn8A7Gqb3OvqThlWWb5mo4UV+VqEf/ob
VSydFr03zlSYuAuyvpHcunlTHnJR6RPgEdv0qV2NFBaVAlVjqJDgZHPKNLCp2Zws
LOgrWiaRGTKrBAD/80JlzsFyk5YVSXd9fTqo05bTym8qKv39vFrrmZQu1SOKRKmn
qrIUr9MjG25iLCxR6ajcANgfb3+hgQMo5ypr7AwjMp1PwkU/IWf1atIWFJzf0ZU1
4JOsDB4FvuX0hdi8J8LKRe+t0hsjQxb4FS3sMWrSDKhjvjRP+AEEwdj/3YbX856i
l9h2Yd36BtKOrwgrMQTS0pHvnUwj+o/4KeFrteccwgJP5bBJYVts10vg52FldNTg
qTrnnjVrjVm/by8Of435ttSXNmqn5g10MUKKLIIgNZXJcUY/fW4v07xduDHFMUYA
YJWfOfyR4Jlb2lJjmG0VwgPqhVMLAqFrL8XLGlqv1D/nKchktwp58cOqo95js+BT
Q8yvEzMbbtPM4MIGUhzfMbVhXFMmQRgfSpQFAPHe/33V0Ddsp7nCj0n7P+g0b8Ka
2BBS8ez8+7DCyTIerKCwB2+Hu9vy1bkhr8ugZXbmxvL3+fSHgMJ//KWFClQ=

View File

@ -1,2 +0,0 @@
U2FsdGVkX1+nBNkZvBovUtzVk+hzQxFfQJ2NoORch7iPe33Zf+UIKOqkAWK3hjgb
aYDcTVL3ef1iD4saMpueUpoz36+TtwXAowPzGq0+BVLDyVikU9LM6QBlQQ==

2
secrets/pass/gmarcer.pwd Normal file
View File

@ -0,0 +1,2 @@
U2FsdGVkX19LFU3NQZnhBBFTrDOdHX2yWfKjrFlvjtajfTZQhkpXIxRDiUnUntr4
lZ5dHXrJok1O7bWlVKCZ59zLtA+e5hHBrjxSnYIErWvnUUDO

View File

@ -1,2 +1,2 @@
U2FsdGVkX19YVs+neL4R4JDT1CSsndTtbggYoDxEF2iwRCRDJRtrBBJthnxRrUsr U2FsdGVkX1/pO8dHm5elQ+5CIICbNLeWCuO5rVEPX6Are8Hxj9SYXfM47dWUTm4z
c+A5NSSRRAu0LQ5vjaHlOYiCtmVCdYu7ECrpHQ40KqYgYhXJAw== wLMdCqxgQBKTxxfs7l+P9RCDLG03K8d6Z9Aqw5S9j1oFoxxANQ==

3
secrets/pass/mesh.pwd Normal file
View File

@ -0,0 +1,3 @@
U2FsdGVkX19/kwiv3qT/dPLvt7EyuI9Bq8A8G3EPIHDqAOZNBnVpjoYL5Lohhjn1
rAlHir5QMigMm6nNBF0dIYh4vIKyWtD4g+6btiagcke3MYCtcz3zVdeZdzpVOHvP
tRoTA9ZQNw==

2
secrets/pass/rnhmjoj.pwd Normal file
View File

@ -0,0 +1,2 @@
U2FsdGVkX1+N08asPnuy9GeOFSnq7pgilg4VwkPidE3n6qu934b9crw009t05+kE
kTfFouHdrcIDLX+Gry1kn3WSZNgIT47A8N9tSE2h33aRRqJV

View File

@ -1,3 +1,3 @@
U2FsdGVkX1+1zBjw7Y2NlBeTLcGS8o3Er/ngQMU57HLCN8jSfKBU0/C4o9D4NDjl U2FsdGVkX18ukCHRMwgdh9+FALCgM9f4u6hqx2xC/6OB/XhMSkRHllfF5GUJ6kYk
C7pRu3oOHmz0Pn9ipLaP87ST9RzVncHw/kqNBh8Dg29n3jNoTdSfwTn6xV/mBwQO O6UyamMNnMyrg0Us2RkP4ax95HskaPSt6uy7DsmV53cZ0hpoxQAfN+SxBLOU36TW
a4OsKusYMI/dCriATixomxe1GkC06YfwJg== RhMdt6gT/t/zN+yBwFStJi13oCVQXWDMQA==

View File

@ -1,6 +1,6 @@
#!/usr/bin/env nix-shell #!/usr/bin/env nix-shell
#! nix-shell -i bash --pure #! nix-shell -i bash --pure
#! nix-shell -p bash openssl git unixtools.column #! nix-shell -p bash openssl git unixtools.column perl
set -euo pipefail set -euo pipefail
# #
@ -18,37 +18,15 @@ set -euo pipefail
##### CONSTANTS ##### CONSTANTS
# the release version of this script # the release version of this script
readonly VERSION='2.0.0' readonly VERSION='2.2.0'
# the default cipher to utilize # the default cipher to utilize
readonly DEFAULT_CIPHER='aes-256-ctr' readonly DEFAULT_CIPHER='aes-256-ctr'
# the openssl options to encrypt/decrypt the files # arguments of the openssl enc command
# shellcheck disable=SC2016 readonly ENCRYPT_OPTIONS='-pbkdf2 -iter 200000 -pass env:ENC_PASS'
readonly ENCRYPT_OPTIONS='-$cipher -pbkdf2 -iter 200000'
# regular expression used to test user input ##### FUNCTIONS
readonly YES_REGEX='^[Yy]$'
## Repository Metadata
# whether or not transcrypt is already configured
readonly CONFIGURED=$(git config --get --local transcrypt.version 2>/dev/null)
# the current git repository's top-level directory
readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null)
# whether or not a HEAD revision exists
readonly HEAD_EXISTS=$(git rev-parse --verify --quiet HEAD 2>/dev/null)
# https://github.com/RichiH/vcsh
# whether or not the git repository is running under vcsh
readonly IS_VCSH=$(git config --get --local --bool vcsh.vcsh 2>/dev/null)
# whether or not the git repository is bare
readonly IS_BARE=$(git rev-parse --is-bare-repository 2>/dev/null)
## Git Directory Handling
# print a canonicalized absolute pathname # print a canonicalized absolute pathname
realpath() { realpath() {
@ -78,21 +56,46 @@ realpath() {
fi fi
} }
# the current git repository's .git directory # establish repository metadata and directory handling
RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) # shellcheck disable=SC2155
readonly GIT_DIR=$(realpath "$RELATIVE_GIT_DIR" 2>/dev/null) gather_repo_metadata() {
# whether or not transcrypt is already configured
readonly CONFIGURED=$(git config --get --local transcrypt.version 2>/dev/null)
# the current git repository's gitattributes file # the current git repository's top-level directory
readonly CORE_ATTRIBUTES=$(git config --get --local --path core.attributesFile) readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null)
if [[ $CORE_ATTRIBUTES ]]; then
readonly GIT_ATTRIBUTES=$CORE_ATTRIBUTES
elif [[ $IS_BARE == 'true' ]] || [[ $IS_VCSH == 'true' ]]; then
readonly GIT_ATTRIBUTES="${GIT_DIR}/info/attributes"
else
readonly GIT_ATTRIBUTES="${REPO}/.gitattributes"
fi
##### FUNCTIONS # whether or not a HEAD revision exists
readonly HEAD_EXISTS=$(git rev-parse --verify --quiet HEAD 2>/dev/null)
# https://github.com/RichiH/vcsh
# whether or not the git repository is running under vcsh
readonly IS_VCSH=$(git config --get --local --bool vcsh.vcsh 2>/dev/null)
# whether or not the git repository is bare
readonly IS_BARE=$(git rev-parse --is-bare-repository 2>/dev/null || printf 'false')
# the current git repository's .git directory
readonly RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || printf '')
readonly GIT_DIR=$(realpath "$RELATIVE_GIT_DIR" 2>/dev/null)
# Respect transcrypt.crypt-dir if present. Default to crypt/ in Git dir
readonly CRYPT_DIR=$(git config transcrypt.crypt-dir 2>/dev/null || printf '%s/crypt' "${RELATIVE_GIT_DIR}")
# respect core.hooksPath setting, without trailing slash. Fall back to default hooks dir
readonly GIT_HOOKS=$(git config core.hooksPath | sed 's:/*$::' 2>/dev/null || printf "%s/hooks" "${RELATIVE_GIT_DIR}")
# the current git repository's gitattributes file
local CORE_ATTRIBUTES
CORE_ATTRIBUTES=$(git config --get --local --path core.attributesFile 2>/dev/null || git config --get --path core.attributesFile 2>/dev/null || printf '')
if [[ $CORE_ATTRIBUTES ]]; then
readonly GIT_ATTRIBUTES=$CORE_ATTRIBUTES
elif [[ $IS_BARE == 'true' ]] || [[ $IS_VCSH == 'true' ]]; then
readonly GIT_ATTRIBUTES="${GIT_DIR}/info/attributes"
else
readonly GIT_ATTRIBUTES="${REPO}/.gitattributes"
fi
}
# print a message to stderr # print a message to stderr
warn() { warn() {
@ -114,26 +117,209 @@ die() {
exit "$st" exit "$st"
} }
# The `decryption -> encryption` process on an unchanged file must be
# deterministic for everything to work transparently. To do that, the same
# salt must be used each time we encrypt the same file. An HMAC has been
# proven to be a PRF, so we generate an HMAC-SHA256 for each decrypted file
# (keyed with a combination of the filename and transcrypt password), and
# then use the last 16 bytes of that HMAC for the file's unique salt.
git_clean() {
filename=$1
# ignore empty files
if [[ ! -s $filename ]]; then
return
fi
# cache STDIN to test if it's already encrypted
tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
trap 'rm -f "$tempfile"' EXIT
tee "$tempfile" &>/dev/null
# the first bytes of an encrypted file are always "Salted" in Base64
# The `head + LC_ALL=C tr` command handles binary data in old and new Bash (#116)
firstbytes=$(head -c8 "$tempfile" | LC_ALL=C tr -d '\0')
if [[ $firstbytes == "U2FsdGVk" ]]; then
cat "$tempfile"
else
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
openssl_path=$(git config --get --local transcrypt.openssl-path)
salt=$("${openssl_path}" dgst -hmac "${filename}:${password}" -sha256 "$tempfile" | tr -d '\r\n' | tail -c16)
openssl_major_version=$($openssl_path version | cut -d' ' -f2 | cut -d'.' -f1)
if [ "$openssl_major_version" -ge "3" ]; then
# Encrypt the file to base64, ensuring it includes the prefix 'Salted__' with the salt. #133
(
echo -n "Salted__" && echo -n "$salt" | perl -pe 's/(..)/chr(hex($1))/ge' &&
# Encrypt file to binary ciphertext
ENC_PASS="$password" "$openssl_path" enc -e -$cipher $ENCRYPT_OPTIONS -S "$salt" -in "$tempfile"
) |
openssl base64
else
# Encrypt file to base64 ciphertext
ENC_PASS="$password" "$openssl_path" enc -e -a -$cipher $ENCRYPT_OPTIONS -S "$salt" -in "$tempfile"
fi
fi
}
git_smudge() {
tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
trap 'rm -f "$tempfile"' EXIT
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
openssl_path=$(git config --get --local transcrypt.openssl-path)
tee "$tempfile" | ENC_PASS="$password" "$openssl_path" enc -d -$cipher $ENCRYPT_OPTIONS -a 2>/dev/null || cat "$tempfile"
}
git_textconv() {
filename=$1
# ignore empty files
if [[ ! -s $filename ]]; then
return
fi
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
openssl_path=$(git config --get --local transcrypt.openssl-path)
ENC_PASS="$password" "$openssl_path" enc -d -$cipher $ENCRYPT_OPTIONS -a -in "$filename" 2>/dev/null || cat "$filename"
}
# shellcheck disable=SC2005,SC2002,SC2181
git_merge() {
# Get path to transcrypt in this script's directory
TRANSCRYPT_PATH="$(dirname "$0")/transcrypt"
# Look up name of local branch/ref to which changes are being merged
OURS_LABEL=$(git rev-parse --abbrev-ref HEAD)
# Look up name of the incoming "theirs" branch/ref being merged in.
# TODO There must be a better way of doing this than relying on this reflog
# action environment variable, but I don't know what it is
if [[ "$GIT_REFLOG_ACTION" = "merge "* ]]; then
THEIRS_LABEL=$(echo "$GIT_REFLOG_ACTION" | awk '{print $2}')
fi
if [[ ! "$THEIRS_LABEL" ]]; then
THEIRS_LABEL="theirs"
fi
# Decrypt BASE $1, LOCAL $2, and REMOTE $3 versions of file being merged
echo "$(cat "$1" | "${TRANSCRYPT_PATH}" smudge)" >"$1"
echo "$(cat "$2" | "${TRANSCRYPT_PATH}" smudge)" >"$2"
echo "$(cat "$3" | "${TRANSCRYPT_PATH}" smudge)" >"$3"
# Merge the decrypted files to the temp file named by $2
git merge-file --marker-size="$4" -L "$OURS_LABEL" -L base -L "$THEIRS_LABEL" "$2" "$1" "$3"
# If the merge was not successful (has conflicts) exit with an error code to
# leave the partially-merged file in place for a manual merge.
if [[ "$?" != "0" ]]; then
exit 1
fi
# If the merge was successful (no conflicts) re-encrypt the merged temp file $2
# which git will then update in the index in a following "Auto-merging" step.
# We must explicitly encrypt/clean the file, rather than leave Git to do it,
# because we can otherwise trigger safety check failure errors like:
# error: add_cacheinfo failed to refresh for path 'FILE'; merge aborting.
# To re-encrypt we must first copy the merged file to $5 (the name of the
# working-copy file) so the crypt `clean` script can generate the correct hash
# salt based on the file's real name, instead of the $2 temp file name.
cp "$2" "$5"
# Now we use the `clean` script to encrypt the merged file contents back to the
# temp file $2 where Git expects to find the merge result content.
cat "$5" | "${TRANSCRYPT_PATH}" clean "$5" >"$2"
}
# shellcheck disable=SC2155
git_pre_commit() {
# Transcrypt pre-commit hook: fail if secret file in staging lacks the magic prefix "Salted" in B64
tmp=$(mktemp)
IFS=$'\n'
slow_mode_if_failed() {
for secret_file in $(git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'); do
# Skip symlinks, they contain the linked target file path not plaintext
if [[ -L $secret_file ]]; then
continue
fi
# Get prefix of raw file in Git's index using the :FILENAME revision syntax
local firstbytes=$(git show :"${secret_file}" | head -c8)
# An empty file does not need to be, and is not, encrypted
if [[ $firstbytes == "" ]]; then
: # Do nothing
# The first bytes of an encrypted file must be "Salted" in Base64
elif [[ $firstbytes != "U2FsdGVk" ]]; then
printf 'Transcrypt managed file is not encrypted in the Git index: %s\n' "$secret_file" >&2
printf '\n' >&2
printf 'You probably staged this file using a tool that does not apply' >&2
printf ' .gitattribute filters as required by Transcrypt.\n' >&2
printf '\n' >&2
printf 'Fix this by re-staging the file with a compatible tool or with'
printf ' Git on the command line:\n' >&2
printf '\n' >&2
printf ' git rm --cached -- %s\n' "$secret_file" >&2
printf ' git add %s\n' "$secret_file" >&2
printf '\n' >&2
exit 1
fi
done
}
# validate file to see if it failed or not, We don't care about the filename currently for speed, we only care about pass/fail, slow_mode_if_failed() is for what failed.
validate_file() {
secret_file=${1}
# Skip symlinks, they contain the linked target file path not plaintext
if [[ -L $secret_file ]]; then
return
fi
# Get prefix of raw file in Git's index using the :FILENAME revision syntax
# The first bytes of an encrypted file are always "Salted" in Base64
local firstbytes=$(git show :"${secret_file}" | head -c8)
if [[ $firstbytes != "U2FsdGVk" ]]; then
echo "true" >>"${tmp}"
fi
}
# if bash version is 4.4 or greater than fork to number of threads otherwise run normally
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]] && [[ "${BASH_VERSINFO[1]}" -ge 4 ]]; then
num_procs=$(nproc)
num_jobs="\j"
for secret_file in $(git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'); do
while ((${num_jobs@P} >= num_procs)); do
wait -n
done
validate_file "${secret_file}" &
done
wait
if [[ -s ${tmp} ]]; then
slow_mode_if_failed
rm -f "${tmp}"
exit 1
fi
else
slow_mode_if_failed
fi
rm -f "${tmp}"
unset IFS
}
# verify that all requirements have been met # verify that all requirements have been met
run_safety_checks() { run_safety_checks() {
# validate that we're in a git repository # validate that we're in a git repository
[[ $GIT_DIR ]] || die 'you are not currently in a git repository; did you forget to run "git init"?' [[ $GIT_DIR ]] || die 'you are not currently in a git repository; did you forget to run "git init"?'
# exit if transcrypt is not in the required state # exit if transcrypt is not in the required state
if [[ $requires_existing_config ]] && [[ ! $CONFIGURED ]]; then if [[ $ignore_config_status ]]; then
: # no-op, no need to check $CONFIGURED status
elif [[ $requires_existing_config ]] && [[ ! $CONFIGURED ]]; then
die 1 'the current repository is not configured' die 1 'the current repository is not configured'
elif [[ ! $requires_existing_config ]] && [[ $CONFIGURED ]]; then elif [[ ! $requires_existing_config ]] && [[ $CONFIGURED ]]; then
die 1 'the current repository is already configured; see --display' die 1 'the current repository is already configured; see --display'
fi fi
# check for dependencies # check for dependencies
for cmd in {column,grep,mktemp,openssl,sed,tee}; do for cmd in {column,grep,mktemp,"${openssl_path}",sed,tee}; do
command -v $cmd >/dev/null || die 'required command "%s" was not found' "$cmd" command -v "$cmd" >/dev/null || die 'required command "%s" was not found' "$cmd"
done done
# ensure the repository is clean (if it has a HEAD revision) so we can force # ensure the repository is clean (if it has a HEAD revision) so we can force
# checkout files without the destruction of uncommitted changes # checkout files without the destruction of uncommitted changes
if [[ $requires_clean_repo ]] && [[ $HEAD_EXISTS ]] && [[ $IS_BARE == 'false' ]]; then if [[ $requires_clean_repo ]] && [[ $HEAD_EXISTS ]] && [[ $IS_BARE == 'false' ]]; then
# ensure index is up-to-date before dirty check
git update-index -q --really-refresh
# check if the repo is dirty # check if the repo is dirty
if ! git diff-index --quiet HEAD --; then if ! git diff-index --quiet HEAD --; then
die 1 'the repo is dirty; commit or stash your changes before running transcrypt' die 1 'the repo is dirty; commit or stash your changes before running transcrypt'
@ -143,24 +329,20 @@ run_safety_checks() {
# unset the cipher variable if it is not supported by openssl # unset the cipher variable if it is not supported by openssl
validate_cipher() { validate_cipher() {
local list_cipher_commands local list_cipher_commands
list_cipher_commands='openssl enc -ciphers' list_cipher_commands="${openssl_path} enc -ciphers"
remove_dash() {
sed 's#\(^\| \)-#\1#g'
}
local supported local supported
supported=$($list_cipher_commands | remove_dash | tr -s ' ' '\n' | grep --line-regexp "$cipher") || true supported=$($list_cipher_commands | tr -s ' ' '\n' | grep -Fx -- "-$cipher") || true
if [[ ! $supported ]]; then if [[ ! $supported ]]; then
if [[ $interactive ]]; then if [[ $interactive ]]; then
printf '"%s" is not a valid cipher; choose one of the following:\n\n' "$cipher" printf '"%s" is not a valid cipher; choose one of the following:\n\n' "$cipher"
$list_cipher_commands | remove_dash | column -c 80 $list_cipher_commands | column -c 80
printf '\n' printf '\n'
cipher='' cipher=''
else else
# shellcheck disable=SC2016 # shellcheck disable=SC2016
die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$($list_cipher_commands | remove_dash)" die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$list_cipher_commands"
fi fi
fi fi
} }
@ -200,7 +382,7 @@ get_password() {
if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then
local password_length=30 local password_length=30
local random_base64 local random_base64
random_base64=$(openssl rand -base64 $password_length) random_base64=$(${openssl_path} rand -base64 $password_length)
password=$random_base64 password=$random_base64
else else
printf 'Password: ' printf 'Password: '
@ -276,100 +458,73 @@ stage_rekeyed_files() {
# save helper scripts under the repository's git directory # save helper scripts under the repository's git directory
save_helper_scripts() { save_helper_scripts() {
mkdir -p "${GIT_DIR}/crypt" mkdir -p "${CRYPT_DIR}"
openssl_command="openssl enc $ENCRYPT_OPTIONS -pass env:ENC_PASS" local current_transcrypt
current_transcrypt=$(realpath "$0" 2>/dev/null)
# The `decryption -> encryption` process on an unchanged file must be echo '#!/usr/bin/env bash' > "${CRYPT_DIR}/transcrypt"
# deterministic for everything to work transparently. To do that, the same tail -n +4 "$current_transcrypt" >> "${CRYPT_DIR}/transcrypt"
# salt must be used each time we encrypt the same file. An HMAC has been
# proven to be a PRF, so we generate an HMAC-SHA256 for each decrypted file
# (keyed with a combination of the filename and transcrypt password), and
# then use the last 16 bytes of that HMAC for the file's unique salt.
cat <<-'EOF' >"${GIT_DIR}/crypt/clean"
#!/usr/bin/env bash
filename=$1
# ignore empty files
if [[ -s $filename ]]; then
# cache STDIN to test if it's already encrypted
tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
trap 'rm -f "$tempfile"' EXIT
tee "$tempfile" &>/dev/null
# the first bytes of an encrypted file are always "Salted" in Base64
read -n 8 firstbytes <"$tempfile"
if [[ $firstbytes == "U2FsdGVk" ]]; then
cat "$tempfile"
else
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
salt=$(openssl dgst -hmac "${filename}:${password}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16)
ENC_PASS=$password @openssl_command@ -e -a -S "$salt" -in "$tempfile"
fi
fi
EOF
cat <<-'EOF' >"${GIT_DIR}/crypt/smudge"
#!/usr/bin/env bash
tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
trap 'rm -f "$tempfile"' EXIT
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
tee "$tempfile" | ENC_PASS=$password @openssl_command@ -d -a 2>/dev/null || cat "$tempfile"
EOF
cat <<-'EOF' >"${GIT_DIR}/crypt/textconv"
#!/usr/bin/env bash
filename=$1
# ignore empty files
if [[ -s $filename ]]; then
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
ENC_PASS=$password @openssl_command@ -d -a -in "$filename" 2>/dev/null || cat "$filename"
fi
EOF
# make scripts executable # make scripts executable
for script in {clean,smudge,textconv}; do for script in {transcrypt,}; do
chmod 0755 "${GIT_DIR}/crypt/${script}" chmod 0755 "${CRYPT_DIR}/${script}"
sed "s/@openssl_command@/$openssl_command/" -i "${GIT_DIR}/crypt/${script}"
done done
} }
# save helper hooks under the repository's git directory
save_helper_hooks() {
# Install pre-commit-crypt hook script
[[ ! -d "${GIT_HOOKS}" ]] && mkdir -p "${GIT_HOOKS}"
pre_commit_hook_installed="${GIT_HOOKS}/pre-commit-crypt"
cat <<-'EOF' >"$pre_commit_hook_installed"
#!/usr/bin/env bash
# Transcrypt pre-commit hook: fail if secret file in staging lacks the magic prefix "Salted" in B64
RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || printf '')
CRYPT_DIR=$(git config transcrypt.crypt-dir 2>/dev/null || printf '%s/crypt' "${RELATIVE_GIT_DIR}")
"${CRYPT_DIR}/transcrypt" pre_commit
EOF
# Activate hook by copying it to the pre-commit script name, if safe to do so
pre_commit_hook="${GIT_HOOKS}/pre-commit"
if [[ -f "$pre_commit_hook" ]]; then
printf 'WARNING:\n' >&2
printf 'Cannot install Git pre-commit hook script because file already exists: %s\n' "$pre_commit_hook" >&2
printf 'Please manually install the pre-commit script saved as: %s\n' "$pre_commit_hook_installed" >&2
printf '\n'
else
cp "$pre_commit_hook_installed" "$pre_commit_hook"
chmod 0755 "$pre_commit_hook"
fi
}
# write the configuration to the repository's git config # write the configuration to the repository's git config
save_configuration() { save_configuration() {
save_helper_scripts save_helper_scripts
save_helper_hooks
# write the encryption info # write the encryption info
git config transcrypt.version "$VERSION" git config transcrypt.version "$VERSION"
git config transcrypt.cipher "$cipher" git config transcrypt.cipher "$cipher"
git config transcrypt.password "$password" git config transcrypt.password "$password"
git config transcrypt.openssl-path "$openssl_path"
# write the filter settings # write the filter settings. Sorry for the horrific quote escaping below...
if [[ -d $(git rev-parse --git-common-dir) ]]; then # shellcheck disable=SC2016
# this allows us to support multiple working trees via git-worktree git config filter.crypt.clean '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt clean %f'
# ...but the --git-common-dir flag was only added in November 2014 # shellcheck disable=SC2016
# shellcheck disable=SC2016 git config filter.crypt.smudge '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt smudge'
git config filter.crypt.clean '"$(git rev-parse --git-common-dir)"/crypt/clean %f' # shellcheck disable=SC2016
# shellcheck disable=SC2016 git config diff.crypt.textconv '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt textconv'
git config filter.crypt.smudge '"$(git rev-parse --git-common-dir)"/crypt/smudge' # shellcheck disable=SC2016
# shellcheck disable=SC2016 git config merge.crypt.driver '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt merge %O %A %B %L %P'
git config diff.crypt.textconv '"$(git rev-parse --git-common-dir)"/crypt/textconv'
else
# shellcheck disable=SC2016
git config filter.crypt.clean '"$(git rev-parse --git-dir)"/crypt/clean %f'
# shellcheck disable=SC2016
git config filter.crypt.smudge '"$(git rev-parse --git-dir)"/crypt/smudge'
# shellcheck disable=SC2016
git config diff.crypt.textconv '"$(git rev-parse --git-dir)"/crypt/textconv'
fi
git config filter.crypt.required 'true' git config filter.crypt.required 'true'
git config diff.crypt.cachetextconv 'true' git config diff.crypt.cachetextconv 'true'
git config diff.crypt.binary 'true' git config diff.crypt.binary 'true'
git config merge.renormalize 'true' git config merge.renormalize 'true'
git config merge.crypt.name 'Merge transcrypt secret files'
# add a git alias for listing encrypted files # add a git alias for listing encrypted files
git config alias.ls-crypt "!git ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; /crypt$/{ print \$1 }'" git config alias.ls-crypt "!git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; /crypt$/{ print \$1 }'"
} }
# display the current configuration settings # display the current configuration settings
@ -396,6 +551,7 @@ clean_gitconfig() {
git config --remove-section transcrypt 2>/dev/null || true git config --remove-section transcrypt 2>/dev/null || true
git config --remove-section filter.crypt 2>/dev/null || true git config --remove-section filter.crypt 2>/dev/null || true
git config --remove-section diff.crypt 2>/dev/null || true git config --remove-section diff.crypt 2>/dev/null || true
git config --remove-section merge.crypt 2>/dev/null || true
git config --unset merge.renormalize git config --unset merge.renormalize
# remove the merge section if it's now empty # remove the merge section if it's now empty
@ -406,6 +562,20 @@ clean_gitconfig() {
fi fi
} }
# Remove from the local Git DB any objects containing the cached plaintext of
# secret files, created due to the setting diff.crypt.cachetextconv='true'
remove_cached_plaintext() {
# Delete ref to cached plaintext objects, to leave these objects
# unreferenced and available for removal
git update-ref -d refs/notes/textconv/crypt
# Remove ANY unreferenced objects in Git's object DB (packed or unpacked),
# to ensure that cached plaintext objects are also removed.
# The vital sub-commands equivalents we require this `gc` command to do are:
# `git prune`, `git repack -ad`
git gc --prune=now --quiet
}
# force the checkout of any files with the crypt filter applied to them; # force the checkout of any files with the crypt filter applied to them;
# this will decrypt existing encrypted files if you've just cloned a repository, # this will decrypt existing encrypted files if you've just cloned a repository,
# or it will encrypt locally decrypted files if you've just flushed the credentials # or it will encrypt locally decrypted files if you've just flushed the credentials
@ -419,7 +589,7 @@ force_checkout() {
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO" cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
IFS=$'\n' IFS=$'\n'
for file in $encrypted_files; do for file in $encrypted_files; do
rm "$file" rm -f "$file"
git checkout --force HEAD -- "$file" >/dev/null git checkout --force HEAD -- "$file" >/dev/null
done done
unset IFS unset IFS
@ -433,7 +603,8 @@ flush_credentials() {
if [[ $interactive ]]; then if [[ $interactive ]]; then
printf 'You are about to flush the local credentials; make sure you have saved them elsewhere.\n' printf 'You are about to flush the local credentials; make sure you have saved them elsewhere.\n'
printf 'All previously decrypted files will revert to their encrypted form.\n\n' printf 'All previously decrypted files will revert to their encrypted form, and your\n'
printf 'repo will be garbage collected to remove any cached plaintext of secret files.\n\n'
printf 'Proceed with credential flush? [y/N] ' printf 'Proceed with credential flush? [y/N] '
read -r answer read -r answer
printf '\n' printf '\n'
@ -446,6 +617,8 @@ flush_credentials() {
if [[ $answer =~ $YES_REGEX ]]; then if [[ $answer =~ $YES_REGEX ]]; then
clean_gitconfig clean_gitconfig
remove_cached_plaintext
# re-encrypt any files that had been previously decrypted # re-encrypt any files that had been previously decrypted
force_checkout force_checkout
@ -461,7 +634,8 @@ uninstall_transcrypt() {
if [[ $interactive ]]; then if [[ $interactive ]]; then
printf 'You are about to remove all transcrypt configuration from your repository.\n' printf 'You are about to remove all transcrypt configuration from your repository.\n'
printf 'All previously encrypted files will remain decrypted in this working copy.\n\n' printf 'All previously encrypted files will remain decrypted in this working copy, but your\n'
printf 'repo will be garbage collected to remove any cached plaintext of secret files.\n\n'
printf 'Proceed with uninstall? [y/N] ' printf 'Proceed with uninstall? [y/N] '
read -r answer read -r answer
printf '\n' printf '\n'
@ -474,11 +648,30 @@ uninstall_transcrypt() {
if [[ $answer =~ $YES_REGEX ]]; then if [[ $answer =~ $YES_REGEX ]]; then
clean_gitconfig clean_gitconfig
if [[ ! $upgrade ]]; then
remove_cached_plaintext
fi
# remove helper scripts # remove helper scripts
for script in {clean,smudge,textconv}; do # Keep obsolete clean,smudge,textconv,merge refs here to remove them on upgrade
[[ ! -f "${GIT_DIR}/crypt/${script}" ]] || rm "${GIT_DIR}/crypt/${script}" for script in {transcrypt,clean,smudge,textconv,merge}; do
[[ ! -f "${CRYPT_DIR}/${script}" ]] || rm "${CRYPT_DIR}/${script}"
done done
[[ ! -d "${GIT_DIR}/crypt" ]] || rmdir "${GIT_DIR}/crypt" [[ ! -d "${CRYPT_DIR}" ]] || rmdir "${CRYPT_DIR}"
# rename helper hooks (don't delete, in case user has custom changes)
pre_commit_hook="${GIT_HOOKS}/pre-commit"
pre_commit_hook_installed="${GIT_HOOKS}/pre-commit-crypt"
if [[ -f "$pre_commit_hook" ]]; then
hook_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook")
installed_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook_installed")
if [[ "$hook_md5" = "$installed_md5" ]]; then
rm "$pre_commit_hook"
else
printf 'WARNING: Cannot safely disable Git pre-commit hook %s please check it yourself\n' "$pre_commit_hook"
fi
fi
[[ -f "$pre_commit_hook_installed" ]] && rm "$pre_commit_hook_installed"
# touch all encrypted files to prevent stale stat info # touch all encrypted files to prevent stale stat info
local encrypted_files local encrypted_files
@ -503,23 +696,85 @@ uninstall_transcrypt() {
case $OSTYPE in case $OSTYPE in
darwin*) darwin*)
/usr/bin/sed -i '' '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES" /usr/bin/sed -i '' '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
/usr/bin/sed -i '' '/filter=crypt diff=crypt merge=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
;; ;;
linux*) linux*)
sed -i '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES" sed -i '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
sed -i '/filter=crypt diff=crypt merge=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
;; ;;
esac esac
printf 'The transcrypt configuration has been completely removed from the repository.\n' if [[ ! $upgrade ]]; then
printf 'The transcrypt configuration has been completely removed from the repository.\n'
fi
else else
die 1 'uninstallation has been aborted' die 1 'uninstallation has been aborted'
fi fi
} }
# uninstall and re-install transcrypt to upgrade scripts and update configuration
upgrade_transcrypt() {
CURRENT_VERSION=$(git config --get --local transcrypt.version 2>/dev/null)
if [[ $interactive ]]; then
printf 'You are about to upgrade the transcrypt scripts in your repository.\n'
printf 'Your configuration settings will not be changed.\n\n'
printf ' Current version: %s\n' "$CURRENT_VERSION"
printf 'Upgraded version: %s\n\n' "$VERSION"
printf 'Proceed with upgrade? [y/N] '
read -r answer
printf '\n'
if [[ $answer =~ $YES_REGEX ]]; then
# User confirmed, don't prompt again
interactive=''
else
# User did not confirm, exit
# Exit if user did not confirm
die 1 'upgrade has been aborted'
fi
fi
# Keep current cipher and password
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
# Keep current openssl-path, or set to default if no existing value
openssl_path=$(git config --get --local transcrypt.openssl-path 2>/dev/null || printf '%s' "$openssl_path")
# Keep contents of .gitattributes
ORIG_GITATTRIBUTES=$(cat "$GIT_ATTRIBUTES")
uninstall_transcrypt
save_configuration
# Re-instate contents of .gitattributes
echo "$ORIG_GITATTRIBUTES" >"$GIT_ATTRIBUTES"
# Update .gitattributes for transcrypt'ed files to include "merge=crypt" config
case $OSTYPE in
darwin*)
/usr/bin/sed -i '' 's/=crypt\(.*\)/=crypt diff=crypt merge=crypt/' "$GIT_ATTRIBUTES"
;;
linux*)
sed -i 's/=crypt\(.*\)/=crypt diff=crypt merge=crypt/' "$GIT_ATTRIBUTES"
;;
esac
printf 'Upgrade is complete\n'
LATEST_GITATTRIBUTES=$(cat "$GIT_ATTRIBUTES")
if [[ "$LATEST_GITATTRIBUTES" != "$ORIG_GITATTRIBUTES" ]]; then
printf '\nYour gitattributes file has been updated with the latest recommended values.\n'
printf 'Please review and commit the new values in:\n'
printf '%s\n' "$GIT_ATTRIBUTES"
fi
}
# list all of the currently encrypted files in the repository # list all of the currently encrypted files in the repository
list_files() { list_files() {
if [[ $IS_BARE == 'false' ]]; then if [[ $IS_BARE == 'false' ]]; then
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO" cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
git ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }' git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'
fi fi
} }
@ -528,8 +783,8 @@ show_raw_file() {
if [[ -f $show_file ]]; then if [[ -f $show_file ]]; then
# ensure the file is currently being tracked # ensure the file is currently being tracked
local escaped_file=${show_file//\//\\\/} local escaped_file=${show_file//\//\\\/}
if git ls-files --others -- "$show_file" | awk "/${escaped_file}/{ exit 1 }"; then if git -c core.quotePath=false ls-files --others -- "$show_file" | awk "/${escaped_file}/{ exit 1 }"; then
file_paths=$(git ls-tree --name-only --full-name HEAD "$show_file") file_paths=$(git -c core.quotePath=false ls-tree --name-only --full-name HEAD "$show_file")
else else
die 1 'the file "%s" is not currently being tracked by git' "$show_file" die 1 'the file "%s" is not currently being tracked by git' "$show_file"
fi fi
@ -562,10 +817,10 @@ export_gpg() {
current_cipher=$(git config --get --local transcrypt.cipher) current_cipher=$(git config --get --local transcrypt.cipher)
local current_password local current_password
current_password=$(git config --get --local transcrypt.password) current_password=$(git config --get --local transcrypt.password)
mkdir -p "${GIT_DIR}/crypt" mkdir -p "${CRYPT_DIR}"
local gpg_encrypt_cmd="gpg --batch --recipient $gpg_recipient --trust-model always --yes --armor --quiet --encrypt -" local gpg_encrypt_cmd="gpg --batch --recipient $gpg_recipient --trust-model always --yes --armor --quiet --encrypt -"
printf 'password=%s\ncipher=%s\n' "$current_password" "$current_cipher" | $gpg_encrypt_cmd >"${GIT_DIR}/crypt/${gpg_recipient}.asc" printf 'password=%s\ncipher=%s\n' "$current_password" "$current_cipher" | $gpg_encrypt_cmd >"${CRYPT_DIR}/${gpg_recipient}.asc"
printf "The transcrypt configuration has been encrypted and exported to:\n%s/crypt/%s.asc\n" "$GIT_DIR" "$gpg_recipient" printf "The transcrypt configuration has been encrypted and exported to:\n%s/crypt/%s.asc\n" "$GIT_DIR" "$gpg_recipient"
} }
@ -575,10 +830,10 @@ import_gpg() {
command -v gpg >/dev/null || die 'required command "gpg" was not found' command -v gpg >/dev/null || die 'required command "gpg" was not found'
local path local path
if [[ -f "${GIT_DIR}/crypt/${gpg_import_file}" ]]; then if [[ -f "${CRYPT_DIR}/${gpg_import_file}" ]]; then
path="${GIT_DIR}/crypt/${gpg_import_file}" path="${CRYPT_DIR}/${gpg_import_file}"
elif [[ -f "${GIT_DIR}/crypt/${gpg_import_file}.asc" ]]; then elif [[ -f "${CRYPT_DIR}/${gpg_import_file}.asc" ]]; then
path="${GIT_DIR}/crypt/${gpg_import_file}.asc" path="${CRYPT_DIR}/${gpg_import_file}.asc"
elif [[ ! -f $gpg_import_file ]]; then elif [[ ! -f $gpg_import_file ]]; then
die 1 'the file "%s" does not exist' "$gpg_import_file" die 1 'the file "%s" does not exist' "$gpg_import_file"
else else
@ -636,6 +891,9 @@ help() {
the password to derive the key from; the password to derive the key from;
defaults to 30 random base64 characters defaults to 30 random base64 characters
--set-openssl-path=PATH_TO_OPENSSL
use OpenSSL at this path; defaults to 'openssl' in \$PATH
-y, --yes -y, --yes
assume yes and accept defaults for non-specified options assume yes and accept defaults for non-specified options
@ -657,6 +915,10 @@ help() {
remove all transcrypt configuration from the repository and remove all transcrypt configuration from the repository and
leave files in the current working copy decrypted leave files in the current working copy decrypted
--upgrade
apply the latest transcrypt scripts in the repository without
changing your configuration settings
-l, --list -l, --list
list all of the transparently encrypted files in the repository, list all of the transparently encrypted files in the repository,
relative to the top-level directory relative to the top-level directory
@ -689,12 +951,12 @@ help() {
$ transcrypt $ transcrypt
Once a repository has been configured with transcrypt, you can trans- Once a repository has been configured with transcrypt, you can trans-
parently encrypt files by applying the "crypt" filter and diff to a parently encrypt files by applying the "crypt" filter, diff and merge
pattern in the top-level .gitattributes config. If that pattern matches to a pattern in the top-level .gitattributes config. If that pattern
a file in your repository, the file will be transparently encrypted matches a file in your repository, the file will be transparently
once you stage and commit it: encrypted once you stage and commit it:
$ echo 'sensitive_file filter=crypt diff=crypt' >> .gitattributes $ echo 'sensitive_file filter=crypt diff=crypt merge=crypt' >> .gitattributes
$ git add .gitattributes sensitive_file $ git add .gitattributes sensitive_file
$ git commit -m 'Add encrypted version of a sensitive file' $ git commit -m 'Add encrypted version of a sensitive file'
@ -722,23 +984,52 @@ help() {
# reset all variables that might be set # reset all variables that might be set
cipher='' cipher=''
password=''
interactive='true'
display_config='' display_config=''
rekey=''
flush_creds='' flush_creds=''
uninstall=''
show_file=''
gpg_recipient=''
gpg_import_file='' gpg_import_file=''
gpg_recipient=''
interactive='true'
list=''
password=''
rekey=''
show_file=''
uninstall=''
upgrade=''
openssl_path='openssl'
# used to bypass certain safety checks # used to bypass certain safety checks
requires_existing_config='' requires_existing_config=''
requires_clean_repo='true' requires_clean_repo='true'
ignore_config_status='' # Set for operations where config can exist or not
# parse command line options # parse command line options
while [[ "${1:-}" != '' ]]; do while [[ "${1:-}" != '' ]]; do
case $1 in case $1 in
clean)
shift
git_clean "$@"
exit $?
;;
smudge)
shift
git_smudge "$@"
exit $?
;;
textconv)
shift
git_textconv "$@"
exit $?
;;
merge)
shift
git_merge "$@"
exit $?
;;
pre_commit)
shift
git_pre_commit "$@"
exit $?
;;
-c | --cipher) -c | --cipher)
cipher=$2 cipher=$2
shift shift
@ -753,6 +1044,11 @@ while [[ "${1:-}" != '' ]]; do
--password=*) --password=*)
password=${1#*=} password=${1#*=}
;; ;;
--set-openssl-path=*)
openssl_path=${1#*=}
# Immediately apply config setting
git config transcrypt.openssl-path "$openssl_path"
;;
-y | --yes) -y | --yes)
interactive='' interactive=''
;; ;;
@ -777,9 +1073,15 @@ while [[ "${1:-}" != '' ]]; do
requires_existing_config='true' requires_existing_config='true'
requires_clean_repo='' requires_clean_repo=''
;; ;;
--upgrade)
upgrade='true'
requires_existing_config='true'
requires_clean_repo=''
;;
-l | --list) -l | --list)
list_files list='true'
exit 0 requires_clean_repo=''
ignore_config_status='true'
;; ;;
-s | --show-raw) -s | --show-raw)
show_file=$2 show_file=$2
@ -831,14 +1133,25 @@ while [[ "${1:-}" != '' ]]; do
shift shift
done done
gather_repo_metadata
# always run our safety checks # always run our safety checks
run_safety_checks run_safety_checks
# regular expression used to test user input
readonly YES_REGEX='^[Yy]$'
# in order to keep behavior consistent no matter what order the options were # in order to keep behavior consistent no matter what order the options were
# specified in, we must run these here rather than in the case statement above # specified in, we must run these here rather than in the case statement above
if [[ $uninstall ]]; then if [[ $list ]]; then
list_files
exit 0
elif [[ $uninstall ]]; then
uninstall_transcrypt uninstall_transcrypt
exit 0 exit 0
elif [[ $upgrade ]]; then
upgrade_transcrypt
exit 0
elif [[ $display_config ]] && [[ $flush_creds ]]; then elif [[ $display_config ]] && [[ $flush_creds ]]; then
display_configuration display_configuration
printf '\n' printf '\n'
@ -880,7 +1193,7 @@ fi
# ensure the git attributes file exists # ensure the git attributes file exists
if [[ ! -f $GIT_ATTRIBUTES ]]; then if [[ ! -f $GIT_ATTRIBUTES ]]; then
mkdir -p "${GIT_ATTRIBUTES%/*}" mkdir -p "${GIT_ATTRIBUTES%/*}"
printf '#pattern filter=crypt diff=crypt\n' >"$GIT_ATTRIBUTES" printf '#pattern filter=crypt diff=crypt merge=crypt\n' >"$GIT_ATTRIBUTES"
fi fi
printf 'The repository has been successfully configured by transcrypt.\n' printf 'The repository has been successfully configured by transcrypt.\n'

View File

@ -1,8 +1,4 @@
{ lib, ... }: { pkgs, lib, ... }:
let
secrets = toString ./secrets;
in
{ {
imports = [ imports = [
@ -10,18 +6,25 @@ in
./configuration.nix ./configuration.nix
]; ];
# VM hardware setup
virtualisation.memorySize = 4000; # MB
virtualisation.graphics = false;
virtualisation.cores = 4;
virtualisation.msize = 1 * 1024 * 1024;
# Ensure secrets are accessible by the # Ensure secrets are accessible by the
# activation scripts at runtime. # activation scripts at runtime.
virtualisation.qemu.options = [ virtualisation.sharedDirectories.secrets =
"-virtfs local,path=${secrets},security_model=none,mount_tag=secrets" { source = toString ./secrets;
]; target = toString ./secrets;
fileSystems = lib.mkVMOverride { };
"${secrets}" =
{ device = "secrets"; # These don't work in a virtual machine
fsType = "9p"; systemd.services.smartd.enable = lib.mkForce false;
options = [ "trans=virtio" "version=9p2000.L" ]; systemd.services.apcupsd.enable = lib.mkForce false;
neededForBoot = true;
}; # Automatically resize the console
}; environment.systemPackages = [ pkgs.xterm ];
environment.shellInit = "resize > /dev/null";
} }

View File

@ -9,8 +9,10 @@
type = lib.types.attrs; type = lib.types.attrs;
readOnly = true; readOnly = true;
default = { default = {
hostname = "maxwell.ydns.eu"; hostname = "maxwell.eurofusion.eu";
ipAddress = "2.25.5.112"; ipv4WanAddress = "2.35.5.112";
ipv4LanAddress = "192.168.1.5";
ipv6Address = "2001:470:b576:0:230:48ff:fefa:91e1";
}; };
description = "Global constants."; description = "Global constants.";
}; };