diff --git a/config/consul.d/client.json b/config/consul.d/client.json new file mode 100644 index 0000000..2111840 --- /dev/null +++ b/config/consul.d/client.json @@ -0,0 +1,41 @@ +{ + "datacenter": "samfelag", + "data_dir": "/opt/consul", + + "tls": { + "defaults": { + "verify_incoming": false, + "verify_outgoing": true, + "ca_file": "/etc/consul.d/certs/consul-agent-ca.pem" + }, + "internal_rpc": { + "verify_server_hostname": true + } + }, + "auto_encrypt": { + "tls": true + }, + + "bind_addr": "{{ GetInterfaceIP \"tailscale0\" }}", + "advertise_addr": "{{ GetInterfaceIP \"tailscale0\" }}", + "client_addr": "0.0.0.0", + "retry_join": ["100.80.195.56", "100.107.148.47"], + + "ports": { + "grpc_tls": 8502 + }, + + "acl": { + "enabled": true, + "default_policy": "allow", + "enable_token_persistence": true + }, + + "connect": { + "enabled": true + }, + + "performance": { + "raft_multiplier": 1 + } +} diff --git a/config/doom/config.el b/config/doom/config.el index e273786..13aa164 100644 --- a/config/doom/config.el +++ b/config/doom/config.el @@ -1,9 +1,5 @@ ;;; $DOOMDIR/config.el -*- lexical-binding: t; -*- -;; Place your private configuration here! Remember, you do not need to run 'doom -;; sync' after modifying this file! - - ;; Some functionality uses this to identify you, e.g. GPG configuration, email ;; clients, file templates and snippets. (setq user-full-name "Marc Sastre Rienitz" @@ -74,6 +70,7 @@ (use-package parinfer :defer t) + ;; ----------------------------------------------------------------------------- ;; LSP ;; ----------------------------------------------------------------------------- @@ -86,6 +83,7 @@ (lsp-mode . lsp-enable-which-key-integration)) :commands (lsp lsp-deferred)) + ;; ----------------------------------------------------------------------------- ;; Python ;; ----------------------------------------------------------------------------- @@ -116,42 +114,17 @@ ;; ----------------------------------------------------------------------------- ;; Org ;; ----------------------------------------------------------------------------- +(setq org-directory "~/org") +(setq org-agenda-files (directory-files-recursively "~/org/agenda" "\\.org$")) -(setq org-directory "~/org/") -; (use-package org-roam -; :ensure t -; :init -; (setq org-roam-v2-ack t) -; :custom -; (org-roam-directory "~/org-roam") -; (org-roam-capture-templates -; '(("d" "default" plain "%?" -; :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n") -; :unnarrowed t) -; ("m" "màquina" plain (file "~/org-roam/templates/maquina.org") -; :target (file "%<%Y%m%d%H%M%S>-${slug}.org") -; :unnarrowed t) -; ("s" "software" plain (file "~/org-roam/templates/software.org") -; :target (file "%<%Y%m%d%H%M%S>-${slug}.org") -; :unnarrowed t))) -; :config -; (org-roam-setup)) -; -; (use-package! websocket -; :after org-roam) -; -; (use-package! org-roam-ui -; :after org-roam ;; or :after org -; ;; normally we'd recommend hooking orui after org-roam, but since org-roam does not have -; ;; a hookable mode anymore, you're advised to pick something yourself -; ;; if you don't care about startup time, use -; ;; :hook (after-init . org-roam-ui-mode) -; :config -; (setq org-roam-ui-sync-theme t -; org-roam-ui-follow t -; org-roam-ui-update-on-save t -; org-roam-ui-open-on-start nil)) +;; ----------------------------------------------------------------------------- +;; Agenix +;; ----------------------------------------------------------------------------- +(load! "modules/agenix.el") +(setq agenix-age-program "age") +(setq agenix-agenix-program "agenix") + ;; ----------------------------------------------------------------------------- ;; Appearance - Prettify @@ -159,8 +132,8 @@ (load! "modules/prettify-utils.el") (pretty-hook python-mode - ;; ("def" "󰊕") - ;; ("class" "𝙘") + ("def" " 󰊕 ") + ("class" "𝙘") ("None" "∅") ("lambda" "λ") ("not in" "∉") diff --git a/config/doom/modules/agenix.el b/config/doom/modules/agenix.el new file mode 100644 index 0000000..0b72c53 --- /dev/null +++ b/config/doom/modules/agenix.el @@ -0,0 +1,231 @@ +;;; agenix.el --- Decrypt and encrypt agenix secrets -*- lexical-binding: t -*- + +;; Copyright (C) 2022-2023 Tomasz Maciosowski (t4ccer) + +;; Author: Tomasz Maciosowski +;; Maintainer: Tomasz Maciosowski +;; Package-Requires: ((emacs "27.1")) +;; URL: https://github.com/t4ccer/agenix.el +;; Version: 1.0 +;; +;; Modified version by Marc Sastre that allows subdirectories in the secrets folder. + +;; This file is NOT part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; Fully transparent editing of agenix secrets. Open a file, edit it, save it and it will be +;; encrypted automatically. + +;;; Code: + +(defcustom agenix-age-program "age" + "The age program." + :group 'agenix + :type 'string) + +(defcustom agenix-key-files '("~/.ssh/id_ed25519" "~/.ssh/id_rsa") + "List of age key files." + :group 'agenix + :type '(repeat string)) + +(defcustom agenix-pre-mode-hook nil + "Hook to run before entering `agenix-mode'. +Can be used to set up age binary path." + :group 'agenix + :type 'hook) + +(defvar-local agenix--encrypted-fp nil) + +(defvar-local agenix--keys nil) + +(defvar-local agenix--undo-list nil) + +(defvar-local agenix--point nil) + +(define-derived-mode agenix-mode text-mode "agenix" + "Major mode for agenix files. +Don't use directly, use `agenix-mode-if-with-secrets-nix' to ensure that +secrets.nix exists." + (read-only-mode 1) + + (run-hooks 'agenix-pre-mode-hook) + + (agenix-decrypt-buffer) + (goto-char (point-min)) + (setq buffer-undo-list nil) + + (setq require-final-newline nil) + (setq buffer-auto-save-file-name nil) + (setq write-contents-functions '(agenix-save-decrypted)) + + ;; Reverting loads encrypted file back to the buffer, so we need to decrypt it + (add-hook 'after-revert-hook + (lambda () (when (eq major-mode 'agenix-mode) (agenix-decrypt-buffer))))) + + +(defun agenix--file-search-upward (directory file) + "Search DIRECTORY for FILE and return its full path if found, or NIL if not. + +If FILE is not found in DIRECTORY, the parent of DIRECTORY will be searched." + (let ((parent-dir (file-truename (concat (file-name-directory directory) "../"))) + (current-path (if (not (string= (substring directory (- (length directory) 1)) "/")) + (concat directory "/" file) + (concat directory file)))) + (if (file-exists-p current-path) + current-path + (when (and (not (string= (file-truename directory) parent-dir)) + (< (length parent-dir) (length (file-truename directory)))) + (agenix--file-search-upward parent-dir file))))) + +(defun agenix--secrets-nix-path () + "Search for secrets.nix file in the current directory or any parent directory. +Return it if found" + (agenix--file-search-upward "./" "secrets.nix")) + +(defun agenix--relative-buffer-path () + "Return the filename of the current buffer relative to the secrets.nix path." + (file-relative-name + (buffer-file-name) + (file-name-directory (agenix--secrets-nix-path)))) + +(defun agenix--buffer-string* (buffer) + "Like `buffer-string' but read from BUFFER parameter." + (with-current-buffer buffer + (buffer-substring-no-properties (point-min) (point-max)))) + +(defun agenix--with-temp-buffer (func) + "Like `with-temp-buffer' but doesn't actually switch the buffer. +FUNC takes a temporary buffer that will be disposed after the call." + (let* ((age-buf (generate-new-buffer "*age-buf*")) + (res (funcall func age-buf))) + (kill-buffer age-buf) + res)) + +(defun agenix--process-exit-code-and-output (program &rest args) + "Run PROGRAM with ARGS and return the exit code and output in a list." + (agenix--with-temp-buffer + (lambda (buf) (list (apply #'call-process program nil buf nil args) + (agenix--buffer-string* buf))))) + +;;;###autoload +(defun agenix-decrypt-buffer (&optional encrypted-buffer) + "Decrypt ENCRYPTED-BUFFER in place. +If ENCRYPTED-BUFFER is unset or nil, decrypt the current buffer." + (interactive + (when current-prefix-arg + (list (read-buffer "Encrypted buffer: " (current-buffer) t)))) + + (with-current-buffer (or encrypted-buffer (current-buffer)) + (let* ((nix-res (apply #'agenix--process-exit-code-and-output "nix-instantiate" + (list "--strict" "--json" "--eval" "--expr" + (format + "(import %s).\"%s\".publicKeys" + (agenix--secrets-nix-path) + (agenix--relative-buffer-path))))) + (nix-exit-code (car nix-res)) + (nix-output (car (cdr nix-res)))) + + (if (/= nix-exit-code 0) + (warn "Nix evaluation error. +Probably file %s is not declared as a secret in 'secrets.nix' file. +Error: %s" (buffer-file-name) nix-output) + (let* ((keys (json-parse-string nix-output :array-type 'list)) + (age-flags (list "--decrypt"))) + + ;; Add all user's keys to the age command + (dolist (key-path agenix-key-files) + (when (file-exists-p (expand-file-name key-path)) + (setq age-flags + (nconc age-flags (list "--identity" (expand-file-name key-path)))))) + + ;; Add file-path to decrypt to the age command + (setq age-flags (nconc age-flags (list (buffer-file-name)))) + (setq agenix--encrypted-fp (buffer-file-name)) + (setq agenix--keys keys) + + ;; Check if file already exists + (if (not (file-exists-p (buffer-file-name))) + (progn + (message "Not decrypting. File %s does not exist and will be created when you \ +will save this buffer." (buffer-file-name)) + (read-only-mode -1)) + (let* + ((age-res + (apply #'agenix--process-exit-code-and-output agenix-age-program age-flags)) + (age-exit-code (car age-res)) + (age-output (car (cdr age-res)))) + + (if (= 0 age-exit-code) + (progn + ;; Replace buffer with decrypted content + (read-only-mode -1) + (erase-buffer) + (insert age-output) + + ;; Mark buffer as not modified + (set-buffer-modified-p nil) + (setq buffer-undo-list agenix--undo-list)) + (error age-output)))) + (when agenix--point + (goto-char agenix--point))))))) + +;;;###autoload +(defun agenix-save-decrypted (&optional unencrypted-buffer) + "Encrypt UNENCRYPTED-BUFFER back to the original .age file. +If UNENCRYPTED-BUFFER is unset or nil, use the current buffer." + (interactive + (when current-prefix-arg + (list (read-buffer "Unencrypted buffer: " (current-buffer) t)))) + (with-current-buffer (or unencrypted-buffer (current-buffer)) + (let* ((age-flags (list "--encrypt"))) + (progn + (dolist (k agenix--keys) + (setq age-flags (nconc age-flags (list "--recipient" k)))) + (setq age-flags (nconc age-flags (list "-o" agenix--encrypted-fp))) + (let* ((decrypted-text (buffer-string)) + (age-res + (agenix--with-temp-buffer + (lambda (buf) + (list + (apply #'call-process-region + decrypted-text nil + agenix-age-program + nil + buf + t + age-flags) + (agenix--buffer-string* buf)))))) + (when (/= 0 (car age-res)) + (error (car (cdr age-res)))) + (setq agenix--point (point)) + (setq agenix--undo-list buffer-undo-list) + (revert-buffer :ignore-auto :noconfirm :preserve-modes) + (set-buffer-modified-p nil) + t))))) + +;;;###autoload +(defun agenix-mode-if-with-secrets-nix () + "Enable `agenix-mode' if the current buffer is in a directory with secrets.nix." + (interactive) + (when (agenix--secrets-nix-path) + (agenix-mode))) + +;;;###autoload +(add-to-list 'auto-mode-alist '("\\.age\\'" . agenix-mode-if-with-secrets-nix)) + +(provide 'agenix) +;;; agenix.el ends here diff --git a/config/doom/packages.el b/config/doom/packages.el index 0e61198..b105e37 100644 --- a/config/doom/packages.el +++ b/config/doom/packages.el @@ -73,3 +73,10 @@ (package! doom-nano-modeline :recipe (:host github :repo "ronisbr/doom-nano-modeline")) + +;; Agenix +;; (package! agenix +;; :recipe (:host github +;; :repo "t4ccer/agenix.el" +;; :branch "main" +;; :files ("*.el"))) diff --git a/docs/hosts.org b/docs/hosts.org index 2ba1e88..8943937 100644 --- a/docs/hosts.org +++ b/docs/hosts.org @@ -17,6 +17,10 @@ gpg -d id_.gpg > id_ #+end_src ** Add the public key to secrets.nix In the [[file:../secrets/secrets.nix][agenix secrets file]] add the public key, and give access to the necessary secrets. +Remember to rekey the secrets afterwards: +#+begin_src bash +agenix --rekey +#+end_src ** SSH public key authentication Setting up authentication from localhost (client) to remotehost (server). On localhost run: #+BEGIN_SRC bash diff --git a/flake.lock b/flake.lock index a93b985..0b098d9 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,28 @@ { "nodes": { + "agenix": { + "inputs": { + "darwin": [], + "home-manager": "home-manager", + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1703433843, + "narHash": "sha256-nmtA4KqFboWxxoOAA6Y1okHbZh+HsXaMPFkYHsoDRDw=", + "owner": "ryantm", + "repo": "agenix", + "rev": "417caa847f9383e111d1397039c9d4337d024bf0", + "type": "github" + }, + "original": { + "owner": "ryantm", + "repo": "agenix", + "type": "github" + } + }, "base16-schemes": { "flake": false, "locked": { @@ -50,27 +73,28 @@ "type": "github" } }, - "grub2-themes": { + "home-manager": { "inputs": { "nixpkgs": [ + "agenix", "nixpkgs" ] }, "locked": { - "lastModified": 1668213765, - "narHash": "sha256-mTx1jAy6AOY4moWRGvCHYnUNX4qBL/ba47ZcIkhczJM=", - "owner": "vinceliuice", - "repo": "grub2-themes", - "rev": "c106dfb9b5b18ad092ff0f952f2b734933e8283e", + "lastModified": 1703113217, + "narHash": "sha256-7ulcXOk63TIT2lVDSExj7XzFx09LpdSAPtvgtM7yQPE=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "3bfaacf46133c037bb356193bd2f1765d9dc82c1", "type": "github" }, "original": { - "owner": "vinceliuice", - "repo": "grub2-themes", + "owner": "nix-community", + "repo": "home-manager", "type": "github" } }, - "home-manager": { + "home-manager_2": { "inputs": { "nixpkgs": [ "nixpkgs" @@ -172,13 +196,28 @@ }, "root": { "inputs": { + "agenix": "agenix", "emacs-overlay": "emacs-overlay", - "grub2-themes": "grub2-themes", - "home-manager": "home-manager", + "home-manager": "home-manager_2", "nix-colors": "nix-colors", "nixpkgs": "nixpkgs_2", "nur": "nur" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index a916c3e..8ff0df1 100644 --- a/flake.nix +++ b/flake.nix @@ -6,10 +6,16 @@ # - Nixpkgs ---------------------------------- nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11"; - # - Home-Manager ----------------------------- + # - Home Manager ----------------------------- home-manager.url = "github:nix-community/home-manager/release-23.11"; home-manager.inputs.nixpkgs.follows = "nixpkgs"; + # - Agenix ----------------------------------- + agenix.url = "github:ryantm/agenix"; + agenix.inputs.nixpkgs.follows = "nixpkgs"; + # optionally choose not to download darwin deps (saves some resources on Linux) + agenix.inputs.darwin.follows = ""; + # - NUR -------------------------------------- nur.url = "github:nix-community/NUR"; @@ -17,11 +23,7 @@ emacs-overlay.url = "github:nix-community/emacs-overlay"; # - Themeing --------------------------------- - grub2-themes.url = "github:vinceliuice/grub2-themes"; - grub2-themes.inputs.nixpkgs.follows = "nixpkgs"; - nix-colors.url = "github:misterio77/nix-colors"; - }; outputs = inputs @ { self, nixpkgs, home-manager, ... }: @@ -48,7 +50,14 @@ [ inputs.home-manager.nixosModules.home-manager inputs.nix-colors.homeManagerModule - inputs.grub2-themes.nixosModule + # Agenix + inputs.agenix.nixosModules.default + { + environment.systemPackages = [ + inputs.agenix.packages.${system}.default + pkgs.age + ]; + } ] # All my personal modules ++ (lib.my.mapModulesRec' (toString ./modules) import); diff --git a/hosts/reykjavik/default.nix b/hosts/reykjavik/default.nix index f59d70e..31718c2 100644 --- a/hosts/reykjavik/default.nix +++ b/hosts/reykjavik/default.nix @@ -39,6 +39,10 @@ in kind = "dark"; }; + age.identityPaths = [ + "/home/marc/.ssh/id_ed25519" + ]; + # - Modules ------------------------------------ samfelag.modules = { @@ -57,6 +61,12 @@ in system.pass.enable = true; system.sshfs.enable = true; + # - Server ---------------------------------- + server.consul = { + enable = true; + agent-token = config.age.secrets."consul.d/agent-token-reykjavik.json".path; + }; + # - Desktop ---------------------------------- desktop = { inherit wallpaper; diff --git a/modules/secrets.nix b/modules/secrets.nix new file mode 100644 index 0000000..774fcdc --- /dev/null +++ b/modules/secrets.nix @@ -0,0 +1,26 @@ +{ config, pkgs, lib, ... }: +{ + config = { + age.secrets = { + # Consul ------------------------------- + "consul.d/gossip.json" = { + file = ../secrets/consul.d/gossip.json.age; + owner = "consul"; + group = "consul"; + mode = "644"; + }; + "consul.d/consul-agent-ca.pem" = { + file = ../secrets/consul.d/consul-agent-ca.pem.age; + owner = "consul"; + group = "consul"; + mode = "644"; + }; + "consul.d/agent-token-reykjavik.json" = { + file = ../secrets/consul.d/agent-token-reykjavik.json.age; + owner = "consul"; + group = "consul"; + mode = "644"; + }; + }; + }; +} diff --git a/modules/server/consul.nix b/modules/server/consul.nix new file mode 100644 index 0000000..1df37b9 --- /dev/null +++ b/modules/server/consul.nix @@ -0,0 +1,47 @@ +{ config, lib, pkgs, self, ... }: + +let + cfg = config.samfelag.modules.server.consul; +in +{ + options.samfelag.modules.server.consul = { + enable = lib.mkEnableOption "consul"; + + agent-token = lib.mkOption { + type = lib.types.str; + description = "Agent token config file (should be secret)"; + }; + + }; + config = lib.mkIf cfg.enable { + services.consul = { + enable = true; + webUi = true; + }; + + environment.etc = { + agent-ca = { + # Consul agent CA + target = "consul.d/certs/consul-agent-ca.pem"; + source = config.age.secrets."consul.d/consul-agent-ca.pem".path; + }; + gossip = { + # Gossip encryption key + target = "consul.d/gossip.json"; + source = config.age.secrets."consul.d/gossip.json".path; + }; + client = { + # Client config + target = "consul.d/client.json"; + source = ../../config/consul.d/client.json; + }; + agent-token = { + # Agent token + target = "consul.d/agent-token.json"; + source = cfg.agent-token; + }; + }; + + # networking.firewall.allowedTCPPorts = [ 22 ]; + }; +} diff --git a/secrets/consul.d/agent-token-reykjavik.json.age b/secrets/consul.d/agent-token-reykjavik.json.age new file mode 100644 index 0000000..1bdf203 --- /dev/null +++ b/secrets/consul.d/agent-token-reykjavik.json.age @@ -0,0 +1,5 @@ +age-encryption.org/v1 +-> ssh-ed25519 GWuf0Q WZXSz/V10nOYp7jSK2WapD0IIoV5odZnAQX4bfVG6Hk +K6ESj+ABGvRh6U6/e0NT/5rObkGfRBAQU8ujOEemM3g +--- PHbxtNcEtz+cVZPIbYdu3jjsoVBf2fbFIqB0gSZ2DDk +}i4jRH_y=2'TJ~1zL0 (N eb8Mn?IUZҖ>Si'x+d$*4ۜcĬq=c`И?6ۓ,6 4ͯ1򆠘ĤnKx9! t_v"j?d6 ssh-ed25519 GWuf0Q PZ9afqz3THF8vuV1bBzKU2QQ5j0cA7TriznFu1/eF1Q +sk8JAVRjCyhjjkebWtqJaxoacxiYSdir7w9Ep9ch0/4 +--- lBViOk0i5qkicV2kqyGSI/fiEjtyrGqKAoUIzz3V9lQ +8"V !yYK}{/LD;?g9p;u||"OӋrk#"W,p$7x砾Z~V;Խ߉mg \ No newline at end of file diff --git a/secrets/secrets.nix b/secrets/secrets.nix new file mode 100644 index 0000000..593f581 --- /dev/null +++ b/secrets/secrets.nix @@ -0,0 +1,11 @@ +let + id-reykjavik = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFwwpKfxNmUyBoPZqz1jYc6arCdHPvJrEsBN49m/P3By"; + id-hvannadal = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICy1ocZywBvFHpIj+FvaC7QspRWuLXjy6fwakq9t+0Ev"; +in +{ + # -- Consul ------------------------------- + "consul.d/gossip.json.age".publicKeys = [id-reykjavik]; + "consul.d/consul-agent-ca.pem.age".publicKeys = [id-reykjavik]; + # Agent tokens + "consul.d/agent-token-reykjavik.json.age".publicKeys = [id-reykjavik]; +} diff --git a/secrets/ssh-keys/id_hvannadal.gpg b/secrets/ssh-keys/id_hvannadal.gpg new file mode 100644 index 0000000..74a3bdf Binary files /dev/null and b/secrets/ssh-keys/id_hvannadal.gpg differ diff --git a/secrets/ssh-keys/id_hvannadal.pub b/secrets/ssh-keys/id_hvannadal.pub new file mode 100644 index 0000000..985f7e4 --- /dev/null +++ b/secrets/ssh-keys/id_hvannadal.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICy1ocZywBvFHpIj+FvaC7QspRWuLXjy6fwakq9t+0Ev marc@reykjavik diff --git a/secrets/ssh-keys/id_reykjavik.gpg b/secrets/ssh-keys/id_reykjavik.gpg new file mode 100644 index 0000000..555edb4 Binary files /dev/null and b/secrets/ssh-keys/id_reykjavik.gpg differ diff --git a/secrets/ssh-keys/id_reykjavik.pub b/secrets/ssh-keys/id_reykjavik.pub new file mode 100644 index 0000000..9e53b43 --- /dev/null +++ b/secrets/ssh-keys/id_reykjavik.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFwwpKfxNmUyBoPZqz1jYc6arCdHPvJrEsBN49m/P3By marc@sastre.cat