Compare commits

...

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

50 changed files with 1878 additions and 2325 deletions

6
.gitattributes vendored
View File

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

View File

@ -1,32 +0,0 @@
# 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`

735
assets/searx-settings.yml Normal file
View File

@ -0,0 +1,735 @@
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,28 +7,20 @@
./packages.nix
./jobs.nix
./matrix.nix
./email.nix
./magnetico.nix
./nameserver.nix
./custom
./secrets
./fish.nix
./neovim.nix
];
### State
# Stateful things to do before updating:
# 1. Postgres migration (https://www.postgresql.org/docs/current/upgrading.html)
# 2. Matrix Synapse migration (https://matrix-org.github.io/synapse/latest/upgrade.html)
system.stateVersion = "23.05";
nixpkgs.source = builtins.fetchTarball
{ url = "https://github.com/NixOS/nixpkgs/archive/3f0a8ac25fb6.tar.gz";
sha256 = "10i7fllqjzq171afzhdf2d9r1pk9irvmq5n55h92rc47vlaabvr4";
};
# 1. Postgres migration
# 2. Matrix Synapse migration
system.stateVersion = "20.03";
boot.kernelPackages = pkgs.linuxPackages_latest;
boot.tmp.useTmpfs = true;
boot.tmpOnTmpfs = true;
boot.kernel.sysctl = {
# avoid OOM hangs
"vm.admin_reserve_kbytes" = 262144;
@ -38,41 +30,34 @@
i18n.defaultLocale = "en_US.UTF-8";
systemd.enableEmergencyMode = false;
systemd.oomd.enable = false;
networking = {
hostName = "maxwell";
firewall.allowedTCPPorts = [
53 # dns
443 80 # reverse proxy
993 # imaps server
25 465 # smtp(s) server
8080 # hubot
5349 # turn server
5350 # turn server
3551 # apcups
5001 # iperf server
18080 # monero p2p
22000 # syncthing transfer
20000 # syncthing transfert
64738 # mumble server
];
firewall.allowedUDPPorts = [
500 # ipsec
53 # dns
53 # powerdns
1194 # dnscrypt
21027 # syncthing discovery
64738 # mumble server
];
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"
'';
firewall.allowedUDPPortRanges = [
{ from=49152; to=49999; } # turn relay
];
usePredictableInterfaceNames = false;
nameservers = [ "127.0.0.1" ];
# ensure hostname works without DNS
hosts = with config.var;
{ ${ipv4LanAddress} = [ hostname ];
${ipv6Address} = [ hostname ];
};
hosts."127.0.0.1" = [ config.var.hostname ];
};
# Only declarative users and no password logins
@ -80,7 +65,7 @@
users.users ={
# Only needed for local (read emergency) shell access
root.hashedPasswordFile = config.secrets.passwords.root;
root.passwordFile = config.secrets.passwords.root;
# Admin
rnhmjoj = {
@ -98,8 +83,9 @@
openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.fazo];
};
# User
# Runs two chatbots
meme = {
extraGroups = [ "ubino" "miguelbridge" ];
isNormalUser = true;
shell = pkgs.fish;
openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.meme ];
@ -117,26 +103,19 @@
builder = {
description = "Remote Nix builds user";
isNormalUser = true;
openssh.authorizedKeys.keyFiles = with config.secrets.publicKeys; [
rnhmjoj-builder
giu-builder
];
openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.rnhmjoj-builder ];
};
# Use "git" instead of the default name to make
# SSH operation handier, example:
# git clone git@maxwell:user/repo
git = {
group = "git";
description = "Git server user";
home = "/var/lib/gitea";
isSystemUser = true;
useDefaultShell = true;
};
};
users.groups.git = { };
# Generate Diffie-Hellman parameters
# for TLS applications, like nginx.
security.dhparams = {
@ -148,57 +127,64 @@
enable = true;
# Users don't have a password
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.pam.loginLimits = [
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 = [
{ domain = "@users";
type = "hard";
item = "nproc";
value = "400";
}
# Disable core dumping
{ domain = "*";
type = "soft";
item = "core";
value = "0";
}
];
### ACME certificates
security.acme = {
defaults.email = "rnhmjoj@inventati.org";
security.acme = with config.var; {
email = "rnhmjoj@inventati.org";
acceptTerms = true;
certs."maxwell.eurofusion.eu" = {
group = "maxwell-eurofusion-eu";
certs."${hostname}" = {
group = "maxwell-ydns-eu";
};
certs."eurofusion.eu" = {
group = "eurofusion-eu";
certs."riot.${hostname}" = {
group = "riot-maxwell-ydns-eu";
};
};
# Allow read access to ACME certificate
# to specific (service) users.
users.groups."maxwell-eurofusion-eu".members = [ "murmur" "nginx" ];
users.groups."eurofusion-eu".members = [ "nginx" ];
users.groups."maxwell-ydns-eu".members = [ "murmur" "turnserver" ];
users.groups."riot-maxwell-ydns-eu".members = [ "nginx" ];
# sensible logging
services.journald = {
storage = "volatile";
extraConfig = ''
RuntimeMaxUse=2G
'';
};
services.openssh = {
enable = true;
settings.PermitRootLogin = "no";
settings.PasswordAuthentication = false;
settings.KbdInteractiveAuthentication = false;
permitRootLogin = "no";
passwordAuthentication = false;
challengeResponseAuthentication = false;
};
# Traceroute easter egg
@ -215,19 +201,21 @@
### Mumble server
services.murmur = {
enable = true;
password = "allwellthatmaxwell";
registerHostname = config.var.hostname;
registerName = "Maxwell Mumble";
registerPassword = "$REG_PASSWORD";
password = "$JOIN_PASSWORD";
registerPassword = config.secrets.murmur.password;
users = 10;
environmentFile = config.secrets.environments.murmur;
sslCert = "/var/lib/acme/${config.var.hostname}/fullchain.pem";
sslKey = "/var/lib/acme/${config.var.hostname}/key.pem";
extraConfig = with config.var; ''
sslCert=/var/lib/acme/${hostname}/fullchain.pem
sslKey=/var/lib/acme/${hostname}/key.pem
'';
};
### Syncthing node
services.syncthing = {
enable = true;
openDefaultPorts = true;
};
### Monero node with local RPC
@ -260,88 +248,26 @@
### Git server
services.gitea = with config.var; {
enable = true;
domain = hostname;
appName = "Maxwell git server";
rootUrl = "https://${hostname}/git/";
user = "git";
database.user = "git";
log.level = "Error";
cookieSecure = true;
disableRegistration = false;
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;
# file upload size (MB)
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
services.searx = {
enable = true;
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"
];
configFile = ./assets/searx-settings.yml;
};
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
services.nginx =
@ -355,13 +281,21 @@
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
'';
in
rec {
{
enable = true;
enableReload = true;
recommendedTlsSettings = true;
commonHttpConfig = ''
# 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;
recommendedProxySettings = true;
appendHttpConfig = disableLog;
# Large enough to allow file uploads.
clientMaxBodySize = "1000M";
@ -369,51 +303,38 @@
sslDhparam = "${config.security.dhparams.path}/nginx.pem";
# Maxwell
virtualHosts."${hostname}" = {
virtualHosts."${hostname}" =
{
enableACME = true;
forceSSL = true;
default = true;
extraConfig = enableSTS;
extraConfig = disableLog + enableSTS;
# Returns IP address
locations."/ip".extraConfig = ''
default_type text/plain;
return 200 $remote_addr;
'';
locations."/ip".extraConfig = "return 200 $remote_addr;";
# Asjon code coverage reports
locations."/asjon/report/" = {
index = "index.html";
alias = "/run/nginx/static/asjon/";
alias = "/var/lib/asjon/tree/report/";
};
# Searx instance
locations."/srx/".extraConfig =
''
include ${pkgs.nginx}/conf/uwsgi_params;
uwsgi_pass unix:/run/searx/uwsgi.sock;
locations."/srx/" = {
proxyPass = "http://localhost:8083/";
extraConfig = ''
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Script-Name /srx/;
proxy_buffering off;
'';
locations."/srx/static/".alias = "${config.services.searx.package}/share/static/";
};
# Git server
locations."/git/" .proxyPass = "http://localhost:3000/";
# Syncthing
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
@ -431,82 +352,87 @@
# The Cactalogue
virtualHosts."cacta.bit" = {
root = "/run/nginx/static/cactalogue";
locations."/".alias = "/home/giu/cactalogue/";
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
services.ubino.enable = true;
services.miguelbridge.enable = true;
services.asjon.enable = true;
# Needed for the Asjon memory module
services.redis.servers."asjon" =
{ enable = true;
user = "asjon";
services.redis.enable = true;
### 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 "
'';
};
};
# Emergency SSH access via tor
services.tor =
{ enable = true;
client.enable = false;
relay.onionServices.emergency-access.map = [ 22 ];
};
nix.settings = {
nix = {
useSandbox = true;
# Can connect to the Nix daemon
# and upload/run code as root!
trusted-users = [ "builder" "rnhmjoj" ];
trustedUsers = [ "builder" "rnhmjoj" ];
# Use at most half the cores
cores = 8;
max-jobs = 16;
buildCores = 8;
extraOptions = ''
# Always keep at least 256MiB free
min-free = 268435456;
min-free = 268435456
'';
};
environment.sessionVariables = {
PATH = [ "$HOME/bin" ];
XDG_CONFIG_HOME = "$HOME/etc";
XDG_DATA_HOME = "$HOME/var/lib";
XDG_CACHE_HOME = "$HOME/var/cache";
SYSTEMD_COLORS = "16";
environment.variables = {
PATH = "$HOME/.local/bin/:$PATH";
XDG_CONFIG_HOME = "$HOME/.config";
XDG_DATA_HOME = "$HOME/.local/share";
XDG_CACHE_HOME = "$HOME/.cache";
NIX_PROFILE = "$XDG_CONFIG_HOME/nix/profile";
};
# 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,12 +7,11 @@
[ # Misc. system services
./modules/breve.nix
./modules/asjon.nix
./modules/ubino.nix
./modules/miguelbridge.nix
# Safely handle secrets
./modules/secrets-store.nix
# Pin Nixpkgs
./modules/nixpkgs.nix
];
}

View File

@ -27,33 +27,21 @@ 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 {
users.users.${cfg.user} = {
group = cfg.group;
users.extraUsers."${cfg.user}" = {
home = cfg.dataDir;
isSystemUser = true;
createHome = true;
description = "asjon user";
shell = "${pkgs.bash}/bin/bash";
};
users.groups.${cfg.group} = { };
systemd.services.asjon = {
description = "asjon: our chat bot";
after = [ "nginx.service" "matrix-synapse.service" "asjon-init.service" ];
partOf = [ "nginx.service" "matrix-synapse.service" "asjon-init.service" ];
requires = [ "nginx.service" "matrix-synapse.service" "asjon-init.service" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [
@ -73,12 +61,11 @@ in {
# Scripts
AUTO_KILL_ON_UPDATE = "1";
AUTO_INFORM_ON_START = "!XQJXsOXfTevAiEbDTA:eurofusion.eu";
ADMIN_ROOM = "!XQJXsOXfTevAiEbDTA:eurofusion.eu";
AUTO_INFORM_ON_START = "!kvLvoCovzInhiablSq:maxwell.ydns.eu";
ADMIN_ROOM = "!kvLvoCovzInhiablSq:maxwell.ydns.eu";
REV_REMOTE_HOST = "proxy@rnhmjoj.ydns.eu";
REV_REMOTE_PORT = "22";
REV_KEY = "~/.ssh/proxy";
REDIS_URL = "redis:///run/redis-asjon/redis.sock";
};
serviceConfig = {
@ -87,7 +74,7 @@ in {
Restart = "always";
WorkingDirectory = "${cfg.dataDir}/tree";
# API keys and passwords definitions
EnvironmentFile = config.secrets.environments.asjon;
EnvironmentFile = config.secrets.asjon.environment;
};
};
@ -109,6 +96,11 @@ in {
git clone https://github.com/rnhmjoj/asjon.git ${cfg.dataDir}/tree
cd ${cfg.dataDir}/tree
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,15 +44,6 @@ 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 {
type = types.str;
default = config.networking.hostName;
@ -107,14 +98,11 @@ in {
config = mkIf cfg.enable {
users.users.${cfg.user} = {
users.extraUsers."${cfg.user}" = {
isSystemUser = true;
group = cfg.group;
description = "Breve daemon user";
};
users.groups.${cfg.group} = {};
networking.firewall = mkIf cfg.openPorts {
allowedTCPPorts = [ cfg.port ]
++ optional (cfg.port == 443) 80;
@ -128,7 +116,6 @@ in {
environment.XDG_CONFIG_HOME = "${dataDir}/conf";
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart = "${pkgs.haskellPackages.breve}/bin/breve";
Restart = "on-failure";
StateDirectory = "breve";

View File

@ -0,0 +1,52 @@
{ 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

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

52
custom/modules/ubino.nix Normal file
View File

@ -0,0 +1,52 @@
{ 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,35 +1,20 @@
{ lib
, writers
, curl
, jq
{ writeScriptBin, fish, curl
, homeserver
, roomId
, authToken
}:
writers.writeDashBin "notify" ''
export PATH="$PATH:${lib.makeBinPath [ curl jq ]}"
writeScriptBin "notify" ''
#!${fish}/bin/fish
if test $(id -u) != 0; then
set token (cat ${authToken})
if test (id -u) != 0
echo 'you must be root to send a notice'
exit 1
fi
end
token=$(cat ${authToken})
url="${homeserver}/rooms/${roomId}/send/m.room.message?access_token=$token"
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"
set url '${homeserver}/rooms/${roomId}/send/m.room.message?access_token='$token
set msg '{"msgtype":"m.text", "body": "'$argv[1]'"}'
${curl}/bin/curl -s -XPOST -d $msg $url
''

View File

@ -1,96 +0,0 @@
{ 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
View File

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

152
jobs.nix
View File

@ -4,92 +4,105 @@ with lib;
{
systemd.services."notify-failed@" = {
description = "notify that %i has failed";
scriptArgs = "%i";
path = [ pkgs.maxwell-notify ];
systemd.services.ydns = {
description = "update ydns address record";
after = [ "network-online.target" ];
startAt = "*:0/30";
serviceConfig.Type = "oneshot";
serviceConfig.environmentFile = config.secrets.ydns.environment;
path = with pkgs; [ curl cacert gawk iproute ];
environment = {
YDNS_HOST = config.var.hostname;
CURL_CA_BUNDLE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
};
script = ''
unit=$1
notify "$unit: failed. last log lines:"
journalctl -u "$unit" -o cat -n 15 | notify
update() {
ret=$(curl -$1 --basic --silent \
-u "$YDNS_USER:$YDNS_PASSWD" \
"https://ydns.io/api/v1/update/?host=$YDNS_HOST&ip=$2" || exit 0)
case "$ret" in
ok)
echo "updated successfully: $YDNS_HOST ($2)"
;;
badauth)
echo "updated failed: $YDNS_HOST (authentication failed)"
;;
*)
echo "update failed: $YDNS_HOST ($ret)"
;;
esac
}
update 4 "$(curl -s -4 https://ydns.io/api/v1/ip)"
update 6 "$(ip addr show mngtmpaddr | awk '/inet6/{print $2; exit}' | cut -d/ -f1)"
'';
};
systemd.services.backup =
let
saved = pkgs.writeText "backup-saved" ''
/etc/lvm
/var/lib
/home
'';
excluded = pkgs.writeText "backup-excluded" ''
/var/lib/systemd
/var/lib/udisks2
/var/lib/postgresql
/var/lib/matrix-synapse/media_store/url_cache
/var/lib/matrix-synapse/media_store/url_cache_thumbnails
'';
systemd.services.backup = {
description = "run system backup";
after = [ "network-online.target" ];
startAt = "weekly";
in {
description = "system backup";
startAt = "*-*-* 03:00"; # every day at 3:00
onFailure = [ "notify-failed@backup.service" ];
serviceConfig.Type = "oneshot";
serviceConfig = {
Type = "oneshot";
PrivateTmp = true;
PrivateMounts = true;
LimitNOFILE = 65536;
};
path = with pkgs; [ bup git nfs-utils ];
environment.BUP_DIR = "/mnt/backup";
path = with pkgs; [ bup git util-linux sudo gzip postgresql ];
script = ''
# mount repository
mount -m -L backup "$BUP_DIR"
${pkgs.fish}/bin/fish << 'EOF'
# init backup, if empty
! test -e $BUP_DIR/bupindex && bup init
set locations \
/etc/lvm \
/etc/nixos \
/var/lib \
/home
# build indices and save
while read -r dir; do
{
name=$(basename "$dir")
echo indexing $name...
bup index "$dir" --exclude-from="${excluded}"
echo done
set excluded \
/var/lib/alsa \
/var/lib/systemd \
/var/lib/udisks2 \
/var/lib/udev \
/var/lib/postgresql
echo saving $name...
bup save -n "$name" "$dir" || true
echo done
} || true
done < "${saved}"
# 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
dir=/tmp/postgresql
mkdir -p "$dir"
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
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
umount /mnt/backup
EOF
'';
};
@ -107,7 +120,6 @@ with lib;
description = "update namecoin names";
after = [ "namecoind.service" ];
startAt = "hourly";
onFailure = [ "notify-failed@namecoin-update.service" ];
path = [ pkgs.namecoind ];
serviceConfig.Type = "oneshot";

View File

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

View File

@ -1,14 +1,31 @@
{ config, lib, pkgs, ... }:
with config.var;
let
domain = "eurofusion.eu";
### Element (Riot) configuration
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
{
### Reverse proxy locations
# Setup for well-known on the bare domain
services.nginx.virtualHosts.${domain} =
services.nginx.virtualHosts."${config.var.hostname}" =
let
client =
{ "m.homeserver" = { "base_url" = "https://${config.var.hostname}"; };
@ -17,9 +34,6 @@ in
server = { "m.server" = "${config.var.hostname}:443"; };
in
{
enableACME = true;
forceSSL = true;
# Needed for matrix federation
locations."/.well-known/matrix/server".extraConfig = ''
add_header Content-Type application/json;
@ -33,24 +47,40 @@ in
add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON client}';
'';
# Forward matrix API calls to synapse
locations."/_matrix".proxyPass = "http://localhost:8448";
};
# Forward matrix/admin API calls to synapse
services.nginx.virtualHosts.${config.var.hostname} =
{ locations."/_matrix".proxyPass = "http://localhost:8448";
locations."/_synapse".proxyPass = "http://localhost:8448";
### Element/Riot static location
services.nginx.virtualHosts."riot.${config.var.hostname}" =
{ enableACME = true;
forceSSL = true;
locations."/" =
{ index = "index.html";
alias = (pkgs.element-web.override { inherit conf; }) + "/";
};
};
### Homeserver
services.matrix-synapse.enable = true;
services.matrix-synapse.settings = {
server_name = domain;
public_baseurl = "https://${config.var.hostname}/";
services.matrix-synapse = {
enable = true;
server_name = 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
listeners = [
{ bind_addresses = [ "localhost" ];
{ bind_address = "localhost";
port = 8448;
type = "http";
tls = false;
@ -64,13 +94,13 @@ in
# Connect to Postrges
database_type = "psycopg2";
database_args =
{ user = "matrix-synapse";
database_args = {
user = "matrix-synapse";
database = "matrix-synapse";
};
# Make logging less verbose
log_config = pkgs.writeText "synapse-log.yml" ''
logConfig = ''
version: 1
formatters:
journal_fmt:
@ -95,68 +125,65 @@ in
expire_access_token = true;
event_cache_size = "2K";
max_upload_size = "1000M";
dynamic_thumbnails = true;
};
# Secrets
services.matrix-synapse.extraConfigFiles =
[
# Password reset via email
# Note: can't be put here, see NixOS/nixpkgs#158605
config.secrets.matrix.email.conf
turn_user_lifetime = "1d";
# 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
config.secrets.matrix.registration
];
registration_shared_secret = config.secrets.matrix.registration;
};
### Database
services.postgresql.enable = true;
# Create databases on the first run
# Create database on the first run
services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" ''
CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
TEMPLATE template0
LC_COLLATE = "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
# allow synapse to read the shared secrets
users.users.matrix-synapse.extraGroups = [ "mautrix-whatsapp" ];
# Allow olm for mautrix-whatsapp
nixpkgs.config.permittedInsecurePackages = [ "olm-3.2.16" ];
services.mautrix-whatsapp =
{
# Handles users behind a NAT,
# needed for reliable VoIP.
services.coturn = {
enable = true;
serviceDependencies = [ "postgresql.service" ];
settings.appservice =
{ database.type = "postgres";
database.uri = "postgresql:///mautrix-whatsapp?host=/run/postgresql";
};
settings.bridge =
{ encryption =
{ allow = true;
default = true;
require = true;
};
permissions =
{ "eurofusion.eu" = "user";
"@rnhmjoj:eurofusion.eu" = "admin";
};
relay.enabled = false;
mute_bridging = true;
};
# Only allow users vouched for
# by the Matrix server.
lt-cred-mech = true;
use-auth-secret = true;
static-auth-secret = config.secrets.matrix.turn;
# Use maxwell certificate for TLS
realm = config.var.hostname;
cert = "/var/lib/acme/${config.var.hostname}/fullchain.pem";
pkey = "/var/lib/acme/${config.var.hostname}/key.pem";
# Port range for TURN relaying
min-port = 49152;
max-port = 49999;
# Enable TLS
secure-stun = true;
no-tcp-relay = false;
extraConfig = ''
external-ip=${config.var.ipAddress}
cipher-list=HIGH
no-loopback-peers
no-multicast-peers
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
allowed-peer-ip=192.168.1.5
user-quota=12
total-quota=1200
verbose=true
'';
};
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,63 +1,44 @@
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
/RRwMmPObImKt129XslxTT7g5j0=
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
Rm3AOulV1eQ8pzOfXrkmDYIwLE49yNGH+w==

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

10
secrets/misc/asjon.env Normal file
View File

@ -0,0 +1,10 @@
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=

2
secrets/misc/ydns.env Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
#!/usr/bin/env nix-shell
#! nix-shell -i bash --pure
#! nix-shell -p bash openssl git unixtools.column perl
#! nix-shell -p bash openssl git unixtools.column
set -euo pipefail
#
@ -18,15 +18,37 @@ set -euo pipefail
##### CONSTANTS
# the release version of this script
readonly VERSION='2.2.0'
readonly VERSION='2.0.0'
# the default cipher to utilize
readonly DEFAULT_CIPHER='aes-256-ctr'
# arguments of the openssl enc command
readonly ENCRYPT_OPTIONS='-pbkdf2 -iter 200000 -pass env:ENC_PASS'
# the openssl options to encrypt/decrypt the files
# shellcheck disable=SC2016
readonly ENCRYPT_OPTIONS='-$cipher -pbkdf2 -iter 200000'
##### FUNCTIONS
# regular expression used to test user input
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
realpath() {
@ -56,38 +78,12 @@ realpath() {
fi
}
# establish repository metadata and directory handling
# shellcheck disable=SC2155
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 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 || printf 'false')
# the current git repository's .git directory
readonly RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || printf '')
RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
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 '')
readonly CORE_ATTRIBUTES=$(git config --get --local --path core.attributesFile)
if [[ $CORE_ATTRIBUTES ]]; then
readonly GIT_ATTRIBUTES=$CORE_ATTRIBUTES
elif [[ $IS_BARE == 'true' ]] || [[ $IS_VCSH == 'true' ]]; then
@ -95,7 +91,8 @@ gather_repo_metadata() {
else
readonly GIT_ATTRIBUTES="${REPO}/.gitattributes"
fi
}
##### FUNCTIONS
# print a message to stderr
warn() {
@ -117,209 +114,26 @@ die() {
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
run_safety_checks() {
# 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"?'
# exit if transcrypt is not in the required state
if [[ $ignore_config_status ]]; then
: # no-op, no need to check $CONFIGURED status
elif [[ $requires_existing_config ]] && [[ ! $CONFIGURED ]]; then
if [[ $requires_existing_config ]] && [[ ! $CONFIGURED ]]; then
die 1 'the current repository is not configured'
elif [[ ! $requires_existing_config ]] && [[ $CONFIGURED ]]; then
die 1 'the current repository is already configured; see --display'
fi
# check for dependencies
for cmd in {column,grep,mktemp,"${openssl_path}",sed,tee}; do
command -v "$cmd" >/dev/null || die 'required command "%s" was not found' "$cmd"
for cmd in {column,grep,mktemp,openssl,sed,tee}; do
command -v $cmd >/dev/null || die 'required command "%s" was not found' "$cmd"
done
# ensure the repository is clean (if it has a HEAD revision) so we can force
# checkout files without the destruction of uncommitted changes
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
if ! git diff-index --quiet HEAD --; then
die 1 'the repo is dirty; commit or stash your changes before running transcrypt'
@ -330,19 +144,23 @@ run_safety_checks() {
# unset the cipher variable if it is not supported by openssl
validate_cipher() {
local list_cipher_commands
list_cipher_commands="${openssl_path} enc -ciphers"
list_cipher_commands='openssl enc -ciphers'
remove_dash() {
sed 's#\(^\| \)-#\1#g'
}
local supported
supported=$($list_cipher_commands | tr -s ' ' '\n' | grep -Fx -- "-$cipher") || true
supported=$($list_cipher_commands | remove_dash | tr -s ' ' '\n' | grep --line-regexp "$cipher") || true
if [[ ! $supported ]]; then
if [[ $interactive ]]; then
printf '"%s" is not a valid cipher; choose one of the following:\n\n' "$cipher"
$list_cipher_commands | column -c 80
$list_cipher_commands | remove_dash | column -c 80
printf '\n'
cipher=''
else
# shellcheck disable=SC2016
die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$list_cipher_commands"
die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$($list_cipher_commands | remove_dash)"
fi
fi
}
@ -382,7 +200,7 @@ get_password() {
if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then
local password_length=30
local random_base64
random_base64=$(${openssl_path} rand -base64 $password_length)
random_base64=$(openssl rand -base64 $password_length)
password=$random_base64
else
printf 'Password: '
@ -458,73 +276,100 @@ stage_rekeyed_files() {
# save helper scripts under the repository's git directory
save_helper_scripts() {
mkdir -p "${CRYPT_DIR}"
mkdir -p "${GIT_DIR}/crypt"
local current_transcrypt
current_transcrypt=$(realpath "$0" 2>/dev/null)
echo '#!/usr/bin/env bash' > "${CRYPT_DIR}/transcrypt"
tail -n +4 "$current_transcrypt" >> "${CRYPT_DIR}/transcrypt"
openssl_command="openssl enc $ENCRYPT_OPTIONS -pass env:ENC_PASS"
# make scripts executable
for script in {transcrypt,}; do
chmod 0755 "${CRYPT_DIR}/${script}"
done
}
# 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.
# 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"
cat <<-'EOF' >"${GIT_DIR}/crypt/clean"
#!/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
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
# 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"
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
for script in {clean,smudge,textconv}; do
chmod 0755 "${GIT_DIR}/crypt/${script}"
sed "s/@openssl_command@/$openssl_command/" -i "${GIT_DIR}/crypt/${script}"
done
}
# write the configuration to the repository's git config
save_configuration() {
save_helper_scripts
save_helper_hooks
# write the encryption info
git config transcrypt.version "$VERSION"
git config transcrypt.cipher "$cipher"
git config transcrypt.password "$password"
git config transcrypt.openssl-path "$openssl_path"
# write the filter settings. Sorry for the horrific quote escaping below...
# write the filter settings
if [[ -d $(git rev-parse --git-common-dir) ]]; then
# this allows us to support multiple working trees via git-worktree
# ...but the --git-common-dir flag was only added in November 2014
# shellcheck disable=SC2016
git config filter.crypt.clean '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt clean %f'
git config filter.crypt.clean '"$(git rev-parse --git-common-dir)"/crypt/clean %f'
# 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.smudge '"$(git rev-parse --git-common-dir)"/crypt/smudge'
# 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 diff.crypt.textconv '"$(git rev-parse --git-common-dir)"/crypt/textconv'
else
# 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 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 diff.crypt.cachetextconv 'true'
git config diff.crypt.binary 'true'
git config merge.renormalize 'true'
git config merge.crypt.name 'Merge transcrypt secret files'
# add a git alias for listing encrypted files
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 }'"
git config alias.ls-crypt "!git ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; /crypt$/{ print \$1 }'"
}
# display the current configuration settings
@ -551,7 +396,6 @@ clean_gitconfig() {
git config --remove-section transcrypt 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 merge.crypt 2>/dev/null || true
git config --unset merge.renormalize
# remove the merge section if it's now empty
@ -562,20 +406,6 @@ clean_gitconfig() {
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;
# 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
@ -589,7 +419,7 @@ force_checkout() {
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
IFS=$'\n'
for file in $encrypted_files; do
rm -f "$file"
rm "$file"
git checkout --force HEAD -- "$file" >/dev/null
done
unset IFS
@ -603,8 +433,7 @@ flush_credentials() {
if [[ $interactive ]]; then
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, and your\n'
printf 'repo will be garbage collected to remove any cached plaintext of secret files.\n\n'
printf 'All previously decrypted files will revert to their encrypted form.\n\n'
printf 'Proceed with credential flush? [y/N] '
read -r answer
printf '\n'
@ -617,8 +446,6 @@ flush_credentials() {
if [[ $answer =~ $YES_REGEX ]]; then
clean_gitconfig
remove_cached_plaintext
# re-encrypt any files that had been previously decrypted
force_checkout
@ -634,8 +461,7 @@ uninstall_transcrypt() {
if [[ $interactive ]]; then
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, but your\n'
printf 'repo will be garbage collected to remove any cached plaintext of secret files.\n\n'
printf 'All previously encrypted files will remain decrypted in this working copy.\n\n'
printf 'Proceed with uninstall? [y/N] '
read -r answer
printf '\n'
@ -648,30 +474,11 @@ uninstall_transcrypt() {
if [[ $answer =~ $YES_REGEX ]]; then
clean_gitconfig
if [[ ! $upgrade ]]; then
remove_cached_plaintext
fi
# remove helper scripts
# Keep obsolete clean,smudge,textconv,merge refs here to remove them on upgrade
for script in {transcrypt,clean,smudge,textconv,merge}; do
[[ ! -f "${CRYPT_DIR}/${script}" ]] || rm "${CRYPT_DIR}/${script}"
for script in {clean,smudge,textconv}; do
[[ ! -f "${GIT_DIR}/crypt/${script}" ]] || rm "${GIT_DIR}/crypt/${script}"
done
[[ ! -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"
[[ ! -d "${GIT_DIR}/crypt" ]] || rmdir "${GIT_DIR}/crypt"
# touch all encrypted files to prevent stale stat info
local encrypted_files
@ -696,85 +503,23 @@ uninstall_transcrypt() {
case $OSTYPE in
darwin*)
/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*)
sed -i '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
sed -i '/filter=crypt diff=crypt merge=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
;;
esac
if [[ ! $upgrade ]]; then
printf 'The transcrypt configuration has been completely removed from the repository.\n'
fi
else
die 1 'uninstallation has been aborted'
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_files() {
if [[ $IS_BARE == 'false' ]]; then
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'
git ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'
fi
}
@ -783,8 +528,8 @@ show_raw_file() {
if [[ -f $show_file ]]; then
# ensure the file is currently being tracked
local escaped_file=${show_file//\//\\\/}
if git -c core.quotePath=false ls-files --others -- "$show_file" | awk "/${escaped_file}/{ exit 1 }"; then
file_paths=$(git -c core.quotePath=false ls-tree --name-only --full-name HEAD "$show_file")
if git ls-files --others -- "$show_file" | awk "/${escaped_file}/{ exit 1 }"; then
file_paths=$(git ls-tree --name-only --full-name HEAD "$show_file")
else
die 1 'the file "%s" is not currently being tracked by git' "$show_file"
fi
@ -817,10 +562,10 @@ export_gpg() {
current_cipher=$(git config --get --local transcrypt.cipher)
local current_password
current_password=$(git config --get --local transcrypt.password)
mkdir -p "${CRYPT_DIR}"
mkdir -p "${GIT_DIR}/crypt"
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 >"${CRYPT_DIR}/${gpg_recipient}.asc"
printf 'password=%s\ncipher=%s\n' "$current_password" "$current_cipher" | $gpg_encrypt_cmd >"${GIT_DIR}/crypt/${gpg_recipient}.asc"
printf "The transcrypt configuration has been encrypted and exported to:\n%s/crypt/%s.asc\n" "$GIT_DIR" "$gpg_recipient"
}
@ -830,10 +575,10 @@ import_gpg() {
command -v gpg >/dev/null || die 'required command "gpg" was not found'
local path
if [[ -f "${CRYPT_DIR}/${gpg_import_file}" ]]; then
path="${CRYPT_DIR}/${gpg_import_file}"
elif [[ -f "${CRYPT_DIR}/${gpg_import_file}.asc" ]]; then
path="${CRYPT_DIR}/${gpg_import_file}.asc"
if [[ -f "${GIT_DIR}/crypt/${gpg_import_file}" ]]; then
path="${GIT_DIR}/crypt/${gpg_import_file}"
elif [[ -f "${GIT_DIR}/crypt/${gpg_import_file}.asc" ]]; then
path="${GIT_DIR}/crypt/${gpg_import_file}.asc"
elif [[ ! -f $gpg_import_file ]]; then
die 1 'the file "%s" does not exist' "$gpg_import_file"
else
@ -891,9 +636,6 @@ help() {
the password to derive the key from;
defaults to 30 random base64 characters
--set-openssl-path=PATH_TO_OPENSSL
use OpenSSL at this path; defaults to 'openssl' in \$PATH
-y, --yes
assume yes and accept defaults for non-specified options
@ -915,10 +657,6 @@ help() {
remove all transcrypt configuration from the repository and
leave files in the current working copy decrypted
--upgrade
apply the latest transcrypt scripts in the repository without
changing your configuration settings
-l, --list
list all of the transparently encrypted files in the repository,
relative to the top-level directory
@ -951,12 +689,12 @@ help() {
$ transcrypt
Once a repository has been configured with transcrypt, you can trans-
parently encrypt files by applying the "crypt" filter, diff and merge
to a pattern in the top-level .gitattributes config. If that pattern
matches a file in your repository, the file will be transparently
encrypted once you stage and commit it:
parently encrypt files by applying the "crypt" filter and diff to a
pattern in the top-level .gitattributes config. If that pattern matches
a file in your repository, the file will be transparently encrypted
once you stage and commit it:
$ echo 'sensitive_file filter=crypt diff=crypt merge=crypt' >> .gitattributes
$ echo 'sensitive_file filter=crypt diff=crypt' >> .gitattributes
$ git add .gitattributes sensitive_file
$ git commit -m 'Add encrypted version of a sensitive file'
@ -984,52 +722,23 @@ help() {
# reset all variables that might be set
cipher=''
display_config=''
flush_creds=''
gpg_import_file=''
gpg_recipient=''
interactive='true'
list=''
password=''
interactive='true'
display_config=''
rekey=''
show_file=''
flush_creds=''
uninstall=''
upgrade=''
openssl_path='openssl'
show_file=''
gpg_recipient=''
gpg_import_file=''
# used to bypass certain safety checks
requires_existing_config=''
requires_clean_repo='true'
ignore_config_status='' # Set for operations where config can exist or not
# parse command line options
while [[ "${1:-}" != '' ]]; do
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)
cipher=$2
shift
@ -1044,11 +753,6 @@ while [[ "${1:-}" != '' ]]; do
--password=*)
password=${1#*=}
;;
--set-openssl-path=*)
openssl_path=${1#*=}
# Immediately apply config setting
git config transcrypt.openssl-path "$openssl_path"
;;
-y | --yes)
interactive=''
;;
@ -1073,15 +777,9 @@ while [[ "${1:-}" != '' ]]; do
requires_existing_config='true'
requires_clean_repo=''
;;
--upgrade)
upgrade='true'
requires_existing_config='true'
requires_clean_repo=''
;;
-l | --list)
list='true'
requires_clean_repo=''
ignore_config_status='true'
list_files
exit 0
;;
-s | --show-raw)
show_file=$2
@ -1133,25 +831,14 @@ while [[ "${1:-}" != '' ]]; do
shift
done
gather_repo_metadata
# always run our 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
# specified in, we must run these here rather than in the case statement above
if [[ $list ]]; then
list_files
exit 0
elif [[ $uninstall ]]; then
if [[ $uninstall ]]; then
uninstall_transcrypt
exit 0
elif [[ $upgrade ]]; then
upgrade_transcrypt
exit 0
elif [[ $display_config ]] && [[ $flush_creds ]]; then
display_configuration
printf '\n'
@ -1193,7 +880,7 @@ fi
# ensure the git attributes file exists
if [[ ! -f $GIT_ATTRIBUTES ]]; then
mkdir -p "${GIT_ATTRIBUTES%/*}"
printf '#pattern filter=crypt diff=crypt merge=crypt\n' >"$GIT_ATTRIBUTES"
printf '#pattern filter=crypt diff=crypt\n' >"$GIT_ATTRIBUTES"
fi
printf 'The repository has been successfully configured by transcrypt.\n'

View File

@ -1,4 +1,8 @@
{ pkgs, lib, ... }:
{ lib, ... }:
let
secrets = toString ./secrets;
in
{
imports = [
@ -6,25 +10,18 @@
./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
# activation scripts at runtime.
virtualisation.sharedDirectories.secrets =
{ source = toString ./secrets;
target = toString ./secrets;
virtualisation.qemu.options = [
"-virtfs local,path=${secrets},security_model=none,mount_tag=secrets"
];
fileSystems = lib.mkVMOverride {
"${secrets}" =
{ device = "secrets";
fsType = "9p";
options = [ "trans=virtio" "version=9p2000.L" ];
neededForBoot = true;
};
};
# These don't work in a virtual machine
systemd.services.smartd.enable = lib.mkForce false;
systemd.services.apcupsd.enable = lib.mkForce false;
# Automatically resize the console
environment.systemPackages = [ pkgs.xterm ];
environment.shellInit = "resize > /dev/null";
}

View File

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