WIP: feat/automated-account-deletion #174

Draft
b12f wants to merge 23 commits from feat/automated-account-deletion into main
10 changed files with 374 additions and 358 deletions
Showing only changes of commit 74f03555e5 - Show all commits

View file

@ -79,14 +79,25 @@
master = import inputs.master { inherit system; };
};
packages = let
nixos-lib = import (inputs.nixpkgs + "/nixos/lib") { };
testDir = builtins.attrNames (builtins.readDir ./tests);
testFiles = builtins.filter (n: builtins.match "^.*.nix$" n != null) testDir;
in builtins.listToAttrs (map (x: {
name = "test-${lib.strings.removeSuffix ".nix" x}";
value = nixos-lib.runTest (import (./tests + "/${x}") { inherit self; inherit pkgs; inherit lib; inherit config; });
}) testFiles);
packages =
let
nixos-lib = import (inputs.nixpkgs + "/nixos/lib") { };
testDir = builtins.attrNames (builtins.readDir ./tests);
testFiles = builtins.filter (n: builtins.match "^.*.nix$" n != null) testDir;
in
builtins.listToAttrs (
map (x: {
name = "test-${lib.strings.removeSuffix ".nix" x}";
value = nixos-lib.runTest (
import (./tests + "/${x}") {
inherit self;
inherit pkgs;
inherit lib;
inherit config;
}
);
}) testFiles
);
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [

View file

@ -7,247 +7,257 @@
}:
{
options.pub-solar-os.backups = {
stores = with lib; mkOption {
description = ''
Periodic backups to create with Restic.
'';
type = types.attrsOf (types.submodule ({ name, ... }: {
options = {
passwordFile = mkOption {
type = types.str;
description = ''
Read the repository password from a file.
'';
example = "/etc/nixos/restic-password";
};
stores =
with lib;
mkOption {
description = ''
Periodic backups to create with Restic.
'';
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
options = {
passwordFile = mkOption {
type = types.str;
description = ''
Read the repository password from a file.
'';
example = "/etc/nixos/restic-password";
};
repository = mkOption {
type = with types; nullOr str;
default = null;
description = ''
repository to backup to.
'';
example = "sftp:backup@192.168.1.100:/backups/${name}";
};
};
}));
repository = mkOption {
type = with types; nullOr str;
default = null;
description = ''
repository to backup to.
'';
example = "sftp:backup@192.168.1.100:/backups/${name}";
};
};
}
)
);
default = { };
example = {
remotebackup = {
repository = "sftp:backup@host:/backups/home";
passwordFile = "/etc/nixos/secrets/restic-password";
default = { };
example = {
remotebackup = {
repository = "sftp:backup@host:/backups/home";
passwordFile = "/etc/nixos/secrets/restic-password";
};
};
};
};
backups = with lib; mkOption {
description = ''
Periodic backups to create with Restic.
'';
type = types.attrsOf (types.submodule ({ name, ... }: {
options = {
paths = mkOption {
# This is nullable for legacy reasons only. We should consider making it a pure listOf
# after some time has passed since this comment was added.
type = types.nullOr (types.listOf types.str);
default = [ ];
description = ''
Which paths to backup, in addition to ones specified via
`dynamicFilesFrom`. If null or an empty array and
`dynamicFilesFrom` is also null, no backup command will be run.
This can be used to create a prune-only job.
'';
example = [
"/var/lib/postgresql"
"/home/user/backup"
];
backups =
with lib;
mkOption {
description = ''
Periodic backups to create with Restic.
'';
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
options = {
paths = mkOption {
# This is nullable for legacy reasons only. We should consider making it a pure listOf
# after some time has passed since this comment was added.
type = types.nullOr (types.listOf types.str);
default = [ ];
description = ''
Which paths to backup, in addition to ones specified via
`dynamicFilesFrom`. If null or an empty array and
`dynamicFilesFrom` is also null, no backup command will be run.
This can be used to create a prune-only job.
'';
example = [
"/var/lib/postgresql"
"/home/user/backup"
];
};
exclude = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Patterns to exclude when backing up. See
https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for
details on syntax.
'';
example = [
"/var/cache"
"/home/*/.cache"
".git"
];
};
timerConfig = mkOption {
type = types.nullOr (types.attrsOf unitOption);
default = {
OnCalendar = "daily";
Persistent = true;
};
description = ''
When to run the backup. See {manpage}`systemd.timer(5)` for
details. If null no timer is created and the backup will only
run when explicitly started.
'';
example = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
Persistent = true;
};
};
user = mkOption {
type = types.str;
default = "root";
description = ''
As which user the backup should run.
'';
example = "postgresql";
};
extraBackupArgs = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Extra arguments passed to restic backup.
'';
example = [ "--exclude-file=/etc/nixos/restic-ignore" ];
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Extra extended options to be passed to the restic --option flag.
'';
example = [ "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'" ];
};
initialize = mkOption {
type = types.bool;
default = false;
description = ''
Create the repository if it doesn't exist.
'';
};
pruneOpts = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
A list of options (--keep-\* et al.) for 'restic forget
--prune', to automatically prune old snapshots. The
'forget' command is run *after* the 'backup' command, so
keep that in mind when constructing the --keep-\* options.
'';
example = [
"--keep-daily 7"
"--keep-weekly 5"
"--keep-monthly 12"
"--keep-yearly 75"
];
};
runCheck = mkOption {
type = types.bool;
default = (builtins.length config.services.restic.backups.${name}.checkOpts > 0);
defaultText = literalExpression ''builtins.length config.services.backups.${name}.checkOpts > 0'';
description = "Whether to run the `check` command with the provided `checkOpts` options.";
example = true;
};
checkOpts = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
A list of options for 'restic check'.
'';
example = [ "--with-cache" ];
};
dynamicFilesFrom = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that produces a list of files to back up. The
results of this command are given to the '--files-from'
option. The result is merged with paths specified via `paths`.
'';
example = "find /home/matt/git -type d -name .git";
};
backupPrepareCommand = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that must run before starting the backup process.
'';
};
backupCleanupCommand = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that must run after finishing the backup process.
'';
};
package = mkPackageOption pkgs "restic" { };
createWrapper = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to generate and add a script to the system path, that has the same environment variables set
as the systemd service. This can be used to e.g. mount snapshots or perform other opterations, without
having to manually specify most options.
'';
};
};
}
)
);
default = { };
example = {
localbackup = {
paths = [ "/home" ];
exclude = [ "/home/*/.cache" ];
initialize = true;
};
exclude = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Patterns to exclude when backing up. See
https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for
details on syntax.
'';
example = [
"/var/cache"
"/home/*/.cache"
".git"
remotebackup = {
paths = [ "/home" ];
extraOptions = [
"sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
];
};
timerConfig = mkOption {
type = types.nullOr (types.attrsOf unitOption);
default = {
OnCalendar = "daily";
Persistent = true;
};
description = ''
When to run the backup. See {manpage}`systemd.timer(5)` for
details. If null no timer is created and the backup will only
run when explicitly started.
'';
example = {
timerConfig = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
Persistent = true;
};
};
user = mkOption {
type = types.str;
default = "root";
description = ''
As which user the backup should run.
'';
example = "postgresql";
};
extraBackupArgs = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Extra arguments passed to restic backup.
'';
example = [
"--exclude-file=/etc/nixos/restic-ignore"
];
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Extra extended options to be passed to the restic --option flag.
'';
example = [
"sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'"
];
};
initialize = mkOption {
type = types.bool;
default = false;
description = ''
Create the repository if it doesn't exist.
'';
};
pruneOpts = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
A list of options (--keep-\* et al.) for 'restic forget
--prune', to automatically prune old snapshots. The
'forget' command is run *after* the 'backup' command, so
keep that in mind when constructing the --keep-\* options.
'';
example = [
"--keep-daily 7"
"--keep-weekly 5"
"--keep-monthly 12"
"--keep-yearly 75"
];
};
runCheck = mkOption {
type = types.bool;
default = (builtins.length config.services.restic.backups.${name}.checkOpts > 0);
defaultText = literalExpression ''builtins.length config.services.backups.${name}.checkOpts > 0'';
description = "Whether to run the `check` command with the provided `checkOpts` options.";
example = true;
};
checkOpts = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
A list of options for 'restic check'.
'';
example = [
"--with-cache"
];
};
dynamicFilesFrom = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that produces a list of files to back up. The
results of this command are given to the '--files-from'
option. The result is merged with paths specified via `paths`.
'';
example = "find /home/matt/git -type d -name .git";
};
backupPrepareCommand = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that must run before starting the backup process.
'';
};
backupCleanupCommand = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that must run after finishing the backup process.
'';
};
package = mkPackageOption pkgs "restic" { };
createWrapper = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to generate and add a script to the system path, that has the same environment variables set
as the systemd service. This can be used to e.g. mount snapshots or perform other opterations, without
having to manually specify most options.
'';
};
};
}));
default = { };
example = {
localbackup = {
paths = [ "/home" ];
exclude = [ "/home/*/.cache" ];
initialize = true;
};
remotebackup = {
paths = [ "/home" ];
extraOptions = [
"sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
];
timerConfig = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
};
};
};
};
};
config = {
services.restic.backups = let
stores = config.pub-solar-os.backups.stores;
backups = config.pub-solar-os.backups.backups;
services.restic.backups =
let
stores = config.pub-solar-os.backups.stores;
backups = config.pub-solar-os.backups.backups;
storeNames = builtins.attrNames stores;
backupNames = builtins.attrNames backups;
storeNames = builtins.attrNames stores;
backupNames = builtins.attrNames backups;
createBackups = backupName: map
(storeName: {
name = "${backupName}-${storeName}";
value = stores."${storeName}" // backups."${backupName}";
})
storeNames;
createBackups =
backupName:
map (storeName: {
name = "${backupName}-${storeName}";
value = stores."${storeName}" // backups."${backupName}";
}) storeNames;
in builtins.listToAttrs (lib.lists.flatten (map createBackups backupNames));
in
builtins.listToAttrs (lib.lists.flatten (map createBackups backupNames));
};
}

View file

@ -6,9 +6,7 @@
...
}:
{
nixpkgs.config = lib.mkDefault {
allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ ];
};
nixpkgs.config = lib.mkDefault { allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ ]; };
nix = {
# Use default version alias for nix package

View file

@ -42,9 +42,7 @@
};
};
nixpkgs.config = lib.mkDefault {
permittedInsecurePackages = [ "keycloak-23.0.6" ];
};
nixpkgs.config = lib.mkDefault { permittedInsecurePackages = [ "keycloak-23.0.6" ]; };
# keycloak
services.keycloak = {

View file

@ -4,8 +4,10 @@
lib,
config,
...
}: let
in {
}:
let
in
{
name = "keycloak";
hostPkgs = pkgs;
@ -42,9 +44,7 @@ in {
./support/global.nix
];
systemd.tmpfiles.rules = [
"f /tmp/dbf 1777 root root 10d password"
];
systemd.tmpfiles.rules = [ "f /tmp/dbf 1777 root root 10d password" ];
virtualisation.memorySize = 4096;
@ -65,27 +65,30 @@ in {
enableOCR = true;
testScript = {nodes, ...}: let
user = nodes.client.users.users.${nodes.client.pub-solar-os.authentication.username};
#uid = toString user.uid;
bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u ${user.name})/bus";
gdbus = "${bus} gdbus";
su = command: "su - ${user.name} -c '${command}'";
gseval = "call --session -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval";
wmClass = su "${gdbus} ${gseval} global.display.focus_window.wm_class";
in ''
start_all()
testScript =
{ nodes, ... }:
let
user = nodes.client.users.users.${nodes.client.pub-solar-os.authentication.username};
#uid = toString user.uid;
bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u ${user.name})/bus";
gdbus = "${bus} gdbus";
su = command: "su - ${user.name} -c '${command}'";
gseval = "call --session -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval";
wmClass = su "${gdbus} ${gseval} global.display.focus_window.wm_class";
in
''
start_all()
nachtigall.wait_for_unit("system.slice")
nachtigall.succeed("ping 127.0.0.1 -c 2")
nachtigall.wait_for_unit("nginx.service")
nachtigall.wait_for_unit("keycloak.service")
nachtigall.wait_until_succeeds("curl http://127.0.0.1:8080/")
nachtigall.wait_until_succeeds("curl https://auth.test.pub.solar/")
nachtigall.wait_for_unit("system.slice")
nachtigall.succeed("ping 127.0.0.1 -c 2")
nachtigall.wait_for_unit("nginx.service")
nachtigall.wait_for_unit("keycloak.service")
nachtigall.wait_until_succeeds("curl http://127.0.0.1:8080/")
nachtigall.wait_until_succeeds("curl https://auth.test.pub.solar/")
client.wait_for_unit("system.slice")
client.sleep(30)
# client.wait_until_succeeds("${wmClass} | grep -q 'firefox'")
client.screenshot("screen")
'';
client.wait_for_unit("system.slice")
client.sleep(30)
# client.wait_until_succeeds("${wmClass} | grep -q 'firefox'")
client.screenshot("screen")
'';
}

View file

@ -3,14 +3,11 @@
lib,
config,
...
}: {
imports = [
./global.nix
];
}:
{
imports = [ ./global.nix ];
systemd.tmpfiles.rules = [
"f /tmp/step-ca-intermediate-pw 1777 root root 10d password"
];
systemd.tmpfiles.rules = [ "f /tmp/step-ca-intermediate-pw 1777 root root 10d password" ];
networking.interfaces.eth0.ipv4.addresses = [
{
@ -19,30 +16,32 @@
}
];
services.step-ca = let
certificates = pkgs.stdenv.mkDerivation {
name = "certificates";
src = ./step;
installPhase = ''
mkdir -p $out;
cp -r certs $out/
cp -r secrets $out/
'';
};
in {
enable = true;
openFirewall = true;
intermediatePasswordFile = "/tmp/step-ca-intermediate-pw";
port = 443;
address = "0.0.0.0";
settings = (builtins.fromJSON (builtins.readFile ./step/config/ca.json)) // {
root = "${certificates}/certs/root_ca.crt";
crt = "${certificates}/certs/intermediate_ca.crt";
key = "${certificates}/secrets/intermediate_ca_key";
db = {
type = "badgerv2";
dataSource = "/var/lib/step-ca/db";
services.step-ca =
let
certificates = pkgs.stdenv.mkDerivation {
name = "certificates";
src = ./step;
installPhase = ''
mkdir -p $out;
cp -r certs $out/
cp -r secrets $out/
'';
};
in
{
enable = true;
openFirewall = true;
intermediatePasswordFile = "/tmp/step-ca-intermediate-pw";
port = 443;
address = "0.0.0.0";
settings = (builtins.fromJSON (builtins.readFile ./step/config/ca.json)) // {
root = "${certificates}/certs/root_ca.crt";
crt = "${certificates}/certs/intermediate_ca.crt";
key = "${certificates}/secrets/intermediate_ca_key";
db = {
type = "badgerv2";
dataSource = "/var/lib/step-ca/db";
};
};
};
};
}

View file

@ -5,9 +5,7 @@
...
}:
{
imports = [
./global.nix
];
imports = [ ./global.nix ];
services.xserver.enable = true;
services.xserver.displayManager.gdm.enable = true;

View file

@ -3,14 +3,13 @@
lib,
config,
...
}: {
}:
{
pub-solar-os.networking.domain = "test.pub.solar";
security.acme.defaults.server = "https://ca.${config.pub-solar-os.networking.domain}/acme/acme/directory";
security.pki.certificates = [
(builtins.readFile ./step/certs/root_ca.crt)
];
security.pki.certificates = [ (builtins.readFile ./step/certs/root_ca.crt) ];
services.openssh = {
enable = true;
@ -24,16 +23,18 @@
security.pam.services.sshd.allowNullPassword = true;
virtualisation.forwardPorts = let
address = (builtins.elemAt config.networking.interfaces.eth0.ipv4.addresses 0).address;
lastAddressPart = builtins.elemAt (lib.strings.splitString "." address) 3;
in [
{
from = "host";
host.port = 2000 + (lib.strings.toInt lastAddressPart);
guest.port = 22;
}
];
virtualisation.forwardPorts =
let
address = (builtins.elemAt config.networking.interfaces.eth0.ipv4.addresses 0).address;
lastAddressPart = builtins.elemAt (lib.strings.splitString "." address) 3;
in
[
{
from = "host";
host.port = 2000 + (lib.strings.toInt lastAddressPart);
guest.port = 22;
}
];
networking.interfaces.eth0.useDHCP = false;

View file

@ -1,46 +1,44 @@
{
"federatedRoots": null,
"address": ":443",
"insecureAddress": "",
"dnsNames": [
"ca.test.pub.solar"
],
"logger": {
"format": "text"
},
"db": {
"type": "badgerv2",
"badgerFileLoadingMode": ""
},
"authority": {
"provisioners": [
"federatedRoots": null,
"address": ":443",
"insecureAddress": "",
"dnsNames": ["ca.test.pub.solar"],
"logger": {
"format": "text"
},
"db": {
"type": "badgerv2",
"badgerFileLoadingMode": ""
},
"authority": {
"provisioners": [
{
"name": "acme",
"type": "ACME"
"name": "acme",
"type": "ACME"
},
{
"type": "JWK",
"name": "test.pub.solar",
"key": {
"use": "sig",
"kty": "EC",
"kid": "lM-BJXRwwQcdgxLqAS4Za23A2YatZpwXx-PP5NIt8JM",
"crv": "P-256",
"alg": "ES256",
"x": "ouB2mP04Kt8rDa10C8ZzYyzA36rrz-k0c4_ud1hVjyg",
"y": "RbXKcudQRPEFqjG_5AxuqCQXn7pyRToQCwC4MrwLVUQ"
},
"encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjYwMDAwMCwicDJzIjoiNWR5T2puR2Y5aFFNRlc1U25fRWhzUSJ9.a3xtSBuMmzZCMsdfHAXMgFpe9bq8A6bGGOoW9F2Gw7AhxL4bG-AlgA.IA68rSJSGTAKnaVS.XDQc4da-8D9Ykfw-8S4uphsauq5gsEm4qp7zKQUIvcjUlnPAtiHP3xiiBie29ncdg8rKmyzprEEOpTNvXtQl7LsPsHXyKV3SqsTnJecvim9YXGDneAHyWe-XF6hyCZAfSoFbFMgLDKR6d44hMht3ueazL_TPlkFUBLrJbsW782MfdfF3nzcaDf_JDuhKsKHDmKqZyNXDzwf6rINe8adrf5gqaLM2_sGhk7i3XyXygn8HHVw1Dj_w2gPOVm4MS7CO_NgikPqAtGuXDhpWZfXte-FlnMO6d9xQF67b0cwB8kmColPSp1zRiCKPAk9vof8Nn-gGE_aw8zxPi0CJkoY.xbuqSSspgLc_Uw17uiRF7Q"
}
]
},
"tls": {
"cipherSuites": [
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
],
"minVersion": 1.2,
"maxVersion": 1.3,
"renegotiation": false
}
{
"type": "JWK",
"name": "test.pub.solar",
"key": {
"use": "sig",
"kty": "EC",
"kid": "lM-BJXRwwQcdgxLqAS4Za23A2YatZpwXx-PP5NIt8JM",
"crv": "P-256",
"alg": "ES256",
"x": "ouB2mP04Kt8rDa10C8ZzYyzA36rrz-k0c4_ud1hVjyg",
"y": "RbXKcudQRPEFqjG_5AxuqCQXn7pyRToQCwC4MrwLVUQ"
},
"encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjYwMDAwMCwicDJzIjoiNWR5T2puR2Y5aFFNRlc1U25fRWhzUSJ9.a3xtSBuMmzZCMsdfHAXMgFpe9bq8A6bGGOoW9F2Gw7AhxL4bG-AlgA.IA68rSJSGTAKnaVS.XDQc4da-8D9Ykfw-8S4uphsauq5gsEm4qp7zKQUIvcjUlnPAtiHP3xiiBie29ncdg8rKmyzprEEOpTNvXtQl7LsPsHXyKV3SqsTnJecvim9YXGDneAHyWe-XF6hyCZAfSoFbFMgLDKR6d44hMht3ueazL_TPlkFUBLrJbsW782MfdfF3nzcaDf_JDuhKsKHDmKqZyNXDzwf6rINe8adrf5gqaLM2_sGhk7i3XyXygn8HHVw1Dj_w2gPOVm4MS7CO_NgikPqAtGuXDhpWZfXte-FlnMO6d9xQF67b0cwB8kmColPSp1zRiCKPAk9vof8Nn-gGE_aw8zxPi0CJkoY.xbuqSSspgLc_Uw17uiRF7Q"
}
]
},
"tls": {
"cipherSuites": [
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
],
"minVersion": 1.2,
"maxVersion": 1.3,
"renegotiation": false
}
}

View file

@ -1,6 +1,6 @@
{
"ca-url": "https://ca.test.pub.solar",
"ca-config": "/home/b12f/.step/config/ca.json",
"fingerprint": "4d6a1a918355380acbd0256a2203d0a0da8436bb788e8f19326589045c3cd842",
"root": "/home/b12f/.step/certs/root_ca.crt"
}
"ca-url": "https://ca.test.pub.solar",
"ca-config": "/home/b12f/.step/config/ca.json",
"fingerprint": "4d6a1a918355380acbd0256a2203d0a0da8436bb788e8f19326589045c3cd842",
"root": "/home/b12f/.step/certs/root_ca.crt"
}