mirror of
https://github.com/thiloho/archtika.git
synced 2025-11-22 10:51:36 +01:00
Harden systemd services, restrict file permissions further, add username blocklist and prevent more vulnerabilities
This commit is contained in:
@@ -9,6 +9,29 @@ with lib;
|
|||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.services.archtika;
|
cfg = config.services.archtika;
|
||||||
|
baseHardenedSystemdOptions = {
|
||||||
|
CapabilityBoundingSet = "";
|
||||||
|
LockPersonality = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateDevices = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
ProtectHome = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
RemoveIPC = true;
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
SystemCallFilter = ["@system-service" "~@privileged" "~@resources"];
|
||||||
|
|
||||||
|
ReadWritePaths = ["/var/www/archtika-websites"];
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.archtika = {
|
options.services.archtika = {
|
||||||
@@ -105,9 +128,17 @@ in
|
|||||||
group = cfg.group;
|
group = cfg.group;
|
||||||
};
|
};
|
||||||
|
|
||||||
users.groups.${cfg.group} = { };
|
users.groups.${cfg.group} = {
|
||||||
|
members = [
|
||||||
|
"nginx"
|
||||||
|
"postgres"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [ "d /var/www/archtika-websites 0777 ${cfg.user} ${cfg.group} -" ];
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/www 0755 root root -"
|
||||||
|
"d /var/www/archtika-websites 0770 ${cfg.user} ${cfg.group} -"
|
||||||
|
];
|
||||||
|
|
||||||
systemd.services.archtika-api = {
|
systemd.services.archtika-api = {
|
||||||
description = "archtika API service";
|
description = "archtika API service";
|
||||||
@@ -117,11 +148,13 @@ in
|
|||||||
"postgresql.service"
|
"postgresql.service"
|
||||||
];
|
];
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = baseHardenedSystemdOptions // {
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
Group = cfg.group;
|
Group = cfg.group;
|
||||||
Restart = "always";
|
Restart = "always";
|
||||||
WorkingDirectory = "${cfg.package}/rest-api";
|
WorkingDirectory = "${cfg.package}/rest-api";
|
||||||
|
|
||||||
|
RestrictAddressFamilies = ["AF_INET" "AF_INET6" "AF_UNIX"];
|
||||||
};
|
};
|
||||||
|
|
||||||
script = ''
|
script = ''
|
||||||
@@ -142,11 +175,13 @@ in
|
|||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = baseHardenedSystemdOptions // {
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
Group = cfg.group;
|
Group = cfg.group;
|
||||||
Restart = "always";
|
Restart = "always";
|
||||||
WorkingDirectory = "${cfg.package}/web-app";
|
WorkingDirectory = "${cfg.package}/web-app";
|
||||||
|
|
||||||
|
RestrictAddressFamilies = ["AF_INET" "AF_INET6"];
|
||||||
};
|
};
|
||||||
|
|
||||||
script = ''
|
script = ''
|
||||||
|
|||||||
@@ -32,10 +32,15 @@ ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
|
|||||||
CREATE FUNCTION internal.generate_slug (TEXT)
|
CREATE FUNCTION internal.generate_slug (TEXT)
|
||||||
RETURNS TEXT
|
RETURNS TEXT
|
||||||
AS $$
|
AS $$
|
||||||
SELECT
|
BEGIN
|
||||||
REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE(LOWER(TRIM(REGEXP_REPLACE(unaccent ($1), '\s+', '-', 'g'))), '[^\w-]', '', 'g'), '-+', '-', 'g'), '^-+', '', 'g'), '-+$', '', 'g')
|
IF $1 ~ '[/\\.]' THEN
|
||||||
|
RAISE invalid_parameter_value
|
||||||
|
USING message = 'Title cannot contain "/", "\" or "."';
|
||||||
|
END IF;
|
||||||
|
RETURN REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE(LOWER(TRIM(REGEXP_REPLACE(unaccent ($1), '\s+', '-', 'g'))), '[^\w-]', '', 'g'), '-+', '-', 'g'), '^-+', '', 'g'), '-+$', '', 'g');
|
||||||
|
END;
|
||||||
$$
|
$$
|
||||||
LANGUAGE sql
|
LANGUAGE plpgsql
|
||||||
IMMUTABLE;
|
IMMUTABLE;
|
||||||
|
|
||||||
GRANT EXECUTE ON FUNCTION internal.generate_slug TO authenticated_user;
|
GRANT EXECUTE ON FUNCTION internal.generate_slug TO authenticated_user;
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ AS $$
|
|||||||
DECLARE
|
DECLARE
|
||||||
_role NAME;
|
_role NAME;
|
||||||
_user_id UUID;
|
_user_id UUID;
|
||||||
_exp INT := EXTRACT(EPOCH FROM CLOCK_TIMESTAMP())::INT + 86400;
|
_exp INT := EXTRACT(EPOCH FROM CLOCK_TIMESTAMP())::INT + 43200;
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT
|
SELECT
|
||||||
internal.user_role (login.username, login.pass) INTO _role;
|
internal.user_role (login.username, login.pass) INTO _role;
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ LANGUAGE plpgsql
|
|||||||
SECURITY DEFINER;
|
SECURITY DEFINER;
|
||||||
|
|
||||||
CREATE TRIGGER _cleanup_filesystem_website
|
CREATE TRIGGER _cleanup_filesystem_website
|
||||||
BEFORE UPDATE OR DELETE ON internal.website
|
BEFORE UPDATE OF title OR DELETE ON internal.website
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION internal.cleanup_filesystem ();
|
EXECUTE FUNCTION internal.cleanup_filesystem ();
|
||||||
|
|
||||||
CREATE TRIGGER _cleanup_filesystem_article
|
CREATE TRIGGER _cleanup_filesystem_article
|
||||||
BEFORE UPDATE OR DELETE ON internal.article
|
BEFORE UPDATE OF title OR DELETE ON internal.article
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION internal.cleanup_filesystem ();
|
EXECUTE FUNCTION internal.cleanup_filesystem ();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- migrate:up
|
||||||
|
ALTER TABLE internal.user
|
||||||
|
ADD CONSTRAINT username_not_blocked CHECK (LOWER(username) NOT IN ('admin', 'administrator', 'api', 'auth', 'blog', 'cdn', 'docs', 'help', 'login', 'logout', 'profile', 'register', 'settings', 'setup', 'signin', 'signup', 'support', 'test', 'www'));
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
ALTER TABLE internal.user
|
||||||
|
DROP CONSTRAINT username_not_blocked;
|
||||||
|
|
||||||
@@ -26,6 +26,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tabs = ["settings", "articles", "categories", "collaborators", "publish", "logs"];
|
const tabs = ["settings", "articles", "categories", "collaborators", "publish", "logs"];
|
||||||
|
|
||||||
|
let iframeLoaded = $state(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<input type="checkbox" id="toggle-mobile-preview" hidden />
|
<input type="checkbox" id="toggle-mobile-preview" hidden />
|
||||||
@@ -55,7 +57,15 @@
|
|||||||
|
|
||||||
<div class="preview" bind:this={previewElement}>
|
<div class="preview" bind:this={previewElement}>
|
||||||
{#if fullPreview}
|
{#if fullPreview}
|
||||||
<iframe src={previewContent.value} title="Preview"></iframe>
|
{#if !iframeLoaded}
|
||||||
|
<p>Loading preview...</p>
|
||||||
|
{/if}
|
||||||
|
<iframe
|
||||||
|
src={previewContent.value}
|
||||||
|
title="Preview"
|
||||||
|
onload={() => (iframeLoaded = true)}
|
||||||
|
style:display={iframeLoaded ? "block" : "none"}
|
||||||
|
></iframe>
|
||||||
{:else}
|
{:else}
|
||||||
{@html md(
|
{@html md(
|
||||||
previewContent.value || "Write some markdown content to see a live preview here",
|
previewContent.value || "Write some markdown content to see a live preview here",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const actions: Actions = {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
cookies.set("session_token", response.data.token, { path: "/", maxAge: 86400 });
|
cookies.set("session_token", response.data.token, { path: "/", maxAge: 43200 });
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -269,14 +269,15 @@ const generateStaticFiles = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setPermissions = async (dir: string) => {
|
const setPermissions = async (dir: string) => {
|
||||||
await chmod(dir, 0o777);
|
const mode = dev ? 0o777 : process.env.ORIGIN ? 0o770 : 0o777;
|
||||||
|
await chmod(dir, mode);
|
||||||
const entries = await readdir(dir, { withFileTypes: true });
|
const entries = await readdir(dir, { withFileTypes: true });
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const fullPath = join(dir, entry.name);
|
const fullPath = join(dir, entry.name);
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
await setPermissions(fullPath);
|
await setPermissions(fullPath);
|
||||||
} else {
|
} else {
|
||||||
await chmod(fullPath, 0o777);
|
await chmod(fullPath, mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user