Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d897cff7bf | ||
|
|
b686ed892d | ||
|
|
5c03dc8c09 | ||
|
|
3c2a24b248 | ||
|
|
986962bb64 | ||
|
|
a93ee6a433 | ||
|
|
ca4f596a14 | ||
|
|
279f4d6b95 | ||
|
|
243fe1c9d9 | ||
|
|
bae189861f | ||
|
|
981274b20a | ||
|
|
fe1325202b | ||
|
|
432b5e3e24 | ||
|
|
99ec17acfa | ||
|
|
9ca8f90e91 | ||
|
|
d3a5e69162 | ||
|
|
d6130394fa | ||
|
|
7aceb388e7 | ||
|
|
e1fc502286 | ||
|
|
e68994680a | ||
|
|
55a3906f4c | ||
|
|
f8d7fae7a8 | ||
|
|
3def390764 | ||
|
|
a24064c720 | ||
|
|
71d7ac81ae | ||
|
|
4271b880c3 | ||
|
|
884ca49340 | ||
|
|
21b27c5aed | ||
|
|
df819a6f98 |
@@ -0,0 +1,47 @@
|
||||
# Security at Omarchy
|
||||
|
||||
## Report a vulnerability
|
||||
|
||||
If you believe you’ve found a security vulnerability in Omarchy, please tell the [Omarchy Security Team](https://omarchy.org/teams/#security) privately so we have an opportunity to investigate and fix it before it is made public.
|
||||
|
||||
[security@omarchy.org](mailto:security@omarchy.org?subject=Security%20report)
|
||||
|
||||
Please don’t report potential vulnerabilities publicly in GitHub Issues, Discord, or social media before they’ve been resolved.
|
||||
|
||||
## What is a vulnerability?
|
||||
|
||||
We consider a bug a security vulnerability when it can be exploited to cross a meaningful security boundary: an untrusted or lower-privileged party gains access, permissions, or control they didn’t already have.
|
||||
|
||||
Code that could be more robust but does not cross a security boundary is an improvement rather than a security vulnerability. We may still merge a proposed fix and credit the reporter in our release notes.
|
||||
|
||||
Eligibility for our [security credits](https://omarchy.org/security/credits/) page depends on whether a report identifies a confirmed security vulnerability, not on its severity.
|
||||
|
||||
## What to include
|
||||
|
||||
Give us enough information to understand and reproduce the issue:
|
||||
|
||||
- The affected component and Omarchy version.
|
||||
- An explanation of what an attacker can do before and after exploitation.
|
||||
- Steps to reproduce the issue and any proof of concept.
|
||||
- Your preferred contact details for follow-up.
|
||||
|
||||
## Responsible disclosure
|
||||
|
||||
Please act in good faith while investigating and reporting vulnerabilities:
|
||||
|
||||
- Only test systems and accounts you own or have explicit permission to test.
|
||||
- Avoid privacy violations, disruption, data destruction, and service degradation.
|
||||
- Don’t exploit a vulnerability beyond what is needed to demonstrate it.
|
||||
- Give us a reasonable opportunity to investigate and address the issue before publishing details.
|
||||
|
||||
We’ll review your report and keep you informed as we’re able while we work toward a resolution.
|
||||
|
||||
## Credits
|
||||
|
||||
Researchers who privately report a confirmed security vulnerability and give us the chance to ship a fix are thanked on the [security credits](https://omarchy.org/security/credits/) page. Accepted improvements that don’t cross a security boundary may still be credited in our release notes.
|
||||
|
||||
Credits link to each reporter’s X profile and show their avatar. For duplicate reports, only the first reporter is eligible for credit.
|
||||
|
||||
## Regular bugs and support
|
||||
|
||||
For anything that isn’t a security vulnerability, please use the [Omarchy issue tracker](https://github.com/omacom/omarchy/issues).
|
||||
@@ -72,6 +72,7 @@ GROUP_DESCRIPTIONS[plymouth]="Plymouth boot theme management"
|
||||
GROUP_DESCRIPTIONS[power]="Power supply detection"
|
||||
GROUP_DESCRIPTIONS[powerprofiles]="Power profile management"
|
||||
GROUP_DESCRIPTIONS[refresh]="Reset config to defaults"
|
||||
GROUP_DESCRIPTIONS[region]="Region-specific sources and input method"
|
||||
GROUP_DESCRIPTIONS[reinstall]="Reinstall and reset workflows"
|
||||
GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders"
|
||||
GROUP_DESCRIPTIONS[remove]="Removal workflows"
|
||||
|
||||
@@ -677,11 +677,15 @@ user_groups() {
|
||||
if [[ -f $PROVISIONING_DIR/groups ]]; then
|
||||
while IFS= read -r group; do
|
||||
[[ -n $group ]] || continue
|
||||
# Never grant docker at first boot, even if an older install recorded it
|
||||
# (or a factory snapshot predating the opt-in default carries it): the
|
||||
# docker group is root-equivalent. It is opt-in via
|
||||
# omarchy-setup-security-sudoless-docker.
|
||||
# Never replay old privileged group defaults. Docker is always opt-in.
|
||||
# Input is only retained when the factory image has one of the features
|
||||
# whose installer deliberately grants access to raw input devices.
|
||||
[[ $group == "docker" ]] && continue
|
||||
if [[ $group == "input" ]] &&
|
||||
! pacman -Qq xpadneo-dkms &>/dev/null &&
|
||||
! pacman -Qq ydotool &>/dev/null; then
|
||||
continue
|
||||
fi
|
||||
getent group "$group" >/dev/null || continue
|
||||
[[ ",$groups," == *",$group,"* ]] || groups+=",$group"
|
||||
done <"$PROVISIONING_DIR/groups"
|
||||
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Show or set the region the machine is set up for
|
||||
# omarchy:args=[world|china]
|
||||
# omarchy:examples=omarchy region | omarchy region china
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
region_file="$HOME/.config/omarchy/region"
|
||||
|
||||
if (($# == 0)); then
|
||||
region="World"
|
||||
if [[ -f $region_file ]] && read -r stored <"$region_file"; then
|
||||
region=$stored
|
||||
fi
|
||||
echo "$region"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if (($# > 1)); then
|
||||
echo "Usage: omarchy-region <world|china>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
world | World) region="World" ;;
|
||||
china | China) region="China" ;;
|
||||
*)
|
||||
echo "Usage: omarchy-region <world|china>"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
menu_overlay="$HOME/.config/omarchy/extensions/omarchy-menu.jsonc"
|
||||
menu_sample="$OMARCHY_PATH/config/omarchy/extensions/omarchy-menu.jsonc"
|
||||
overlay_marker="// Omarchy region managed"
|
||||
|
||||
if [[ $region == "China" ]]; then
|
||||
# The stock machine carries the commented sample extension; a menu extension
|
||||
# someone wrote by hand refuses the switch instead of being overwritten.
|
||||
if [[ -s $menu_overlay ]] && ! grep -qFx "$overlay_marker" "$menu_overlay" &&
|
||||
! cmp -s "$menu_overlay" "$menu_sample"; then
|
||||
echo "menu: $menu_overlay has content Omarchy does not manage" >&2
|
||||
echo "Resolve it by hand, then rerun: omarchy region china" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${menu_overlay%/*}"
|
||||
cp "$OMARCHY_PATH/default/omarchy/omarchy-menu.zh-cn.jsonc" "$menu_overlay"
|
||||
echo "menu -> Chinese labels"
|
||||
|
||||
# localectl writes /etc/locale.conf; the session picks it up at next login.
|
||||
if ! grep -qsFx 'LANG=zh_CN.UTF-8' /etc/locale.conf; then
|
||||
sudo sed -i 's/^#\(zh_CN\.UTF-8 UTF-8\)/\1/' /etc/locale.gen
|
||||
sudo locale-gen
|
||||
sudo localectl set-locale LANG=zh_CN.UTF-8
|
||||
fi
|
||||
echo "language -> zh_CN.UTF-8 (from the next login)"
|
||||
else
|
||||
if [[ -f $menu_overlay ]] && grep -qFx "$overlay_marker" "$menu_overlay"; then
|
||||
cp "$menu_sample" "$menu_overlay"
|
||||
echo "menu -> stock labels"
|
||||
fi
|
||||
|
||||
if grep -qsFx 'LANG=zh_CN.UTF-8' /etc/locale.conf; then
|
||||
sudo localectl set-locale LANG=en_US.UTF-8
|
||||
echo "language -> en_US.UTF-8 (from the next login)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Rime Ice's TUI drives its own sudo and fcitx5 restart; the schema file is
|
||||
# what a finished install leaves behind. A fresh install has no repo databases
|
||||
# until its first sync, and both pkg-add and the installer's pacman need them.
|
||||
if [[ $region == "China" && ! -f $HOME/.local/share/fcitx5/rime/rime_ice.schema.yaml ]]; then
|
||||
sudo pacman -Sy
|
||||
omarchy-pkg-add rime-ice-installer
|
||||
rime-ice-installer
|
||||
fi
|
||||
|
||||
# Rime first makes Chinese the active default; keyboard-us stays one
|
||||
# Ctrl+Space away, pinned explicitly rather than left to fcitx5's default.
|
||||
if [[ $region == "China" ]]; then
|
||||
fcitx_dir="$HOME/.config/fcitx5"
|
||||
mkdir -p "$fcitx_dir"
|
||||
cat >"$fcitx_dir/profile" <<EOF
|
||||
[Groups/0]
|
||||
Name=Default
|
||||
Default Layout=us
|
||||
DefaultIM=rime
|
||||
|
||||
[Groups/0/Items/0]
|
||||
Name=rime
|
||||
Layout=
|
||||
|
||||
[Groups/0/Items/1]
|
||||
Name=keyboard-us
|
||||
Layout=
|
||||
|
||||
[GroupOrder]
|
||||
0=Default
|
||||
EOF
|
||||
|
||||
touch "$fcitx_dir/config"
|
||||
awk '
|
||||
/^\[Hotkey\/TriggerKeys\]$/ { skip = 1; next }
|
||||
/^\[/ { skip = 0 }
|
||||
!skip { print }
|
||||
' "$fcitx_dir/config" >"$fcitx_dir/config.tmp"
|
||||
printf '[Hotkey/TriggerKeys]\n0=Control+space\n' >>"$fcitx_dir/config.tmp"
|
||||
mv "$fcitx_dir/config.tmp" "$fcitx_dir/config"
|
||||
echo "input method -> Rime Ice by default, Ctrl+Space toggles"
|
||||
|
||||
if systemctl --user is-active omarchy-fcitx5.service >/dev/null 2>&1; then
|
||||
systemctl --user restart omarchy-fcitx5.service
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "${region_file%/*}"
|
||||
printf '%s\n' "$region" >"$region_file"
|
||||
echo "Region set to $region"
|
||||
@@ -143,6 +143,50 @@ authorize_pasted_key() {
|
||||
authorize_key "$key" || exit 1
|
||||
}
|
||||
|
||||
# Only called after a key is authorized. Disabling password authentication
|
||||
# before then could lock the owner out of the machine.
|
||||
disable_password_auth() {
|
||||
local config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf
|
||||
local effective_config
|
||||
|
||||
if [[ ! -s $AUTHORIZED_KEYS ]]; then
|
||||
echo -e "\e[31mCannot disable SSH password authentication without an authorized key.\e[0m" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Disabling SSH password authentication, now that a key is authorized..."
|
||||
sudo install -Dm644 /dev/stdin "$config" <<'CONF'
|
||||
# Written by omarchy-setup-security-sshd once an SSH key was authorized.
|
||||
# Delete this file and reload sshd to allow password logins again.
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
CONF
|
||||
|
||||
# Validate before reloading: a config sshd rejects would otherwise take the
|
||||
# service down on its next restart, potentially stranding a remote owner.
|
||||
if ! sudo sshd -t; then
|
||||
echo -e "\e[31msshd rejected the hardening config; removing it and leaving passwords on.\e[0m" >&2
|
||||
sudo rm -f "$config"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Syntax alone is insufficient because sshd uses the first value it reads for
|
||||
# these settings. An earlier administrator rule could leave passwords enabled.
|
||||
# Match keywords case-insensitively: OpenSSH 9.x dumps them lowercase, 10.x
|
||||
# in CamelCase.
|
||||
if ! effective_config=$(sudo sshd -T) ||
|
||||
! grep -qixF "passwordauthentication no" <<<"$effective_config" ||
|
||||
! grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config"; then
|
||||
echo -e "\e[31msshd did not apply the password-authentication restrictions; removing the ineffective config.\e[0m" >&2
|
||||
sudo rm -f "$config"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Reload rather than restart so an administrator already connected keeps
|
||||
# their session.
|
||||
sudo systemctl reload sshd.service
|
||||
}
|
||||
|
||||
echo -e "\e[32mSetting up SSH server access with key-based authentication.\n\e[0m"
|
||||
|
||||
setup_sshd
|
||||
@@ -161,5 +205,8 @@ else
|
||||
esac
|
||||
fi
|
||||
|
||||
disable_password_auth
|
||||
|
||||
echo -e "\e[32m\nPerfect! The SSH server is running and your key is authorized.\e[0m"
|
||||
echo "Password logins are off; this machine now accepts authorized keys only."
|
||||
echo "You can now connect with: ssh $USER@$(hostname)"
|
||||
|
||||
@@ -5,3 +5,9 @@
|
||||
# omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on
|
||||
|
||||
omarchy-toggle bar-off "${1:-toggle}"
|
||||
|
||||
# The shell's watch on the toggles directory can miss flag changes that land in
|
||||
# quick succession, stranding the bar off screen until the shell restarts.
|
||||
# Nudge the bar to re-read the flag; quiet best-effort so the toggle still
|
||||
# works when the shell is not up.
|
||||
omarchy-shell -q omarchy.bar syncHidden
|
||||
|
||||
@@ -14,6 +14,8 @@ fi
|
||||
|
||||
if grep -q "https://pkgs.omarchy.org/stable/" /etc/pacman.conf; then
|
||||
pkgs="stable"
|
||||
elif grep -q "https://pkgs.omarchy.org/rc/" /etc/pacman.conf; then
|
||||
pkgs="rc"
|
||||
elif grep -q "https://pkgs.omarchy.org/edge/" /etc/pacman.conf; then
|
||||
pkgs="edge"
|
||||
else
|
||||
|
||||
@@ -1,313 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||
<fontconfig>
|
||||
<!-- CJK: give language-tagged text the Noto CJK variant that matches its
|
||||
language. noto-fonts-cjk ships all five, Han glyph shapes differ between
|
||||
them, and the generic assigns below would otherwise hand tagged CJK text
|
||||
to families with no Han coverage, leaving the variant to a charset-scan
|
||||
lottery. Chinese arrives under many tags (W3C recommends the zh-Hans and
|
||||
zh-Hant script forms), so the tags are first folded onto one tag per
|
||||
variant. Order is load-bearing: a bare zh and a bare zh-hant satisfy
|
||||
the contains test of every one of their extensions, so both are pinned
|
||||
by exact matches before the extension folds run. Cantonese under its
|
||||
own primary tag folds to Hong Kong forms, except explicitly
|
||||
Simplified Cantonese, which folds to SC. -->
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="eq">
|
||||
<string>zh</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-cn</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="eq">
|
||||
<string>zh-hant</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-tw</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hant-hk</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-hk</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hant-mo</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-hk</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hant</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-tw</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hans</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-cn</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-sg</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-cn</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-mo</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-hk</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="eq">
|
||||
<string>yue</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-hk</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>yue-hans</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-cn</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>yue-cn</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-cn</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>yue</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-hk</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
|
||||
<!-- Each variant is then prepended for its tag, ahead of the assigns below
|
||||
because these test the generic names those assigns replace. Unlike the
|
||||
Arabic rule further down, these carry a family test, so the prepend
|
||||
lands just ahead of the matched generic and a concrete family the app
|
||||
asked for stays in front of the variant. -->
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-cn</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>sans-serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans CJK SC</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-cn</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Serif CJK SC</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-cn</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>monospace</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans Mono CJK SC</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hk</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>sans-serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans CJK HK</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hk</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Serif CJK HK</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hk</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>monospace</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans Mono CJK HK</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-tw</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>sans-serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans CJK TC</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-tw</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Serif CJK TC</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-tw</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>monospace</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans Mono CJK TC</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>ja</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>sans-serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans CJK JP</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>ja</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Serif CJK JP</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>ja</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>monospace</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans Mono CJK JP</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>ko</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>sans-serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans CJK KR</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>ko</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Serif CJK KR</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>ko</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>monospace</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans Mono CJK KR</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="family" qual="any">
|
||||
<string>sans-serif</string>
|
||||
|
||||
@@ -167,6 +167,9 @@
|
||||
"setup.default.editor.helix": {"icon":"","label":"Helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"},
|
||||
"setup.default.editor.vim": {"icon":"","label":"Vim","checked":"[[ \"$(omarchy-default-editor)\" == \"vim\" ]]","action":"omarchy-default-editor vim"},
|
||||
"setup.default.editor.emacs": {"icon":"","label":"Emacs","checked":"[[ \"$(omarchy-default-editor)\" == \"emacs\" ]]","action":"omarchy-default-editor emacs"},
|
||||
"setup.region": {"icon":"","label":"Region"},
|
||||
"setup.region.world": {"icon":"","label":"World","checked":"[[ \"$(omarchy-region)\" == \"World\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region world'"},
|
||||
"setup.region.china": {"icon":"","label":"China","checked":"[[ \"$(omarchy-region)\" == \"China\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region china'"},
|
||||
"setup.plugin": {"icon":"","label":"Plugins","aliases":["plugin","plugins"]},
|
||||
"setup.plugin.enable": {"icon":"","label":"Enable Plugin","action":"omarchy-menu-plugin enable"},
|
||||
"setup.plugin.disable": {"icon":"","label":"Disable Plugin","action":"omarchy-menu-plugin disable"},
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
// Omarchy region managed
|
||||
// Chinese labels for the shipped menu. `omarchy region china` installs this
|
||||
// as the user menu extension; ids keep their actions and icons.
|
||||
{
|
||||
"apps": {"label":"应用"},
|
||||
"learn": {"label":"学习"},
|
||||
"trigger": {"label":"触发"},
|
||||
"style": {"label":"风格"},
|
||||
"setup": {"label":"设置"},
|
||||
"install": {"label":"安装"},
|
||||
"remove": {"label":"移除"},
|
||||
"update": {"label":"更新"},
|
||||
"about": {"label":"关于"},
|
||||
"system": {"label":"系统"},
|
||||
"system.screensaver": {"label":"屏保"},
|
||||
"system.lock": {"label":"锁屏"},
|
||||
"system.suspend": {"label":"睡眠"},
|
||||
"system.hibernate": {"label":"休眠"},
|
||||
"system.logout": {"label":"注销"},
|
||||
"system.reboot": {"label":"重启"},
|
||||
"system.shutdown": {"label":"关机"},
|
||||
"learn.keybindings": {"label":"快捷键"},
|
||||
"learn.omarchy": {"label":"Omarchy 手册"},
|
||||
"learn.community": {"label":"社区"},
|
||||
"trigger.emoji": {"label":"表情"},
|
||||
"trigger.reminder": {"label":"提醒"},
|
||||
"trigger.capture": {"label":"截取"},
|
||||
"trigger.capture.screenshot": {"label":"截图"},
|
||||
"trigger.capture.screenrecord.stop": {"label":"停止录屏"},
|
||||
"trigger.capture.screenrecord": {"label":"录屏"},
|
||||
"trigger.capture.text": {"label":"OCR 取字"},
|
||||
"trigger.capture.qr": {"label":"二维码"},
|
||||
"trigger.capture.color": {"label":"取色"},
|
||||
"trigger.capture.screenrecord.no-audio": {"label":"无音频"},
|
||||
"trigger.capture.screenrecord.desktop-audio": {"label":"含桌面音频"},
|
||||
"trigger.capture.screenrecord.microphone": {"label":"含桌面与麦克风音频"},
|
||||
"trigger.capture.screenrecord.webcam": {"label":"含桌面、麦克风与摄像头"},
|
||||
"trigger.transcode": {"label":"转码"},
|
||||
"trigger.share": {"label":"分享"},
|
||||
"trigger.toggle": {"label":"开关"},
|
||||
"trigger.hardware": {"label":"硬件"},
|
||||
"trigger.tests": {"label":"测速"},
|
||||
"trigger.hardware.laptop-display": {"label":"笔记本屏幕"},
|
||||
"trigger.hardware.mirror-display": {"label":"镜像显示"},
|
||||
"trigger.hardware.hybrid-gpu": {"label":"混合显卡"},
|
||||
"trigger.hardware.touchpad": {"label":"触控板"},
|
||||
"trigger.hardware.touchpad-haptics": {"label":"触控板振动"},
|
||||
"trigger.hardware.touchpad-haptics.low": {"label":"低"},
|
||||
"trigger.hardware.touchpad-haptics.mid": {"label":"中"},
|
||||
"trigger.hardware.touchpad-haptics.high": {"label":"高"},
|
||||
"trigger.hardware.touchscreen": {"label":"触摸屏"},
|
||||
"trigger.reminder.set": {"label":"设一个提醒"},
|
||||
"trigger.reminder.show": {"label":"查看全部"},
|
||||
"trigger.reminder.clear": {"label":"清除全部"},
|
||||
"trigger.share.clipboard": {"label":"剪贴板"},
|
||||
"trigger.share.file": {"label":"文件"},
|
||||
"trigger.share.folder": {"label":"文件夹"},
|
||||
"trigger.share.receive": {"label":"接收"},
|
||||
"trigger.toggle.idle-lock": {"label":"保持唤醒"},
|
||||
"trigger.toggle.notifications": {"label":"通知"},
|
||||
"trigger.toggle.crash-capture": {"label":"崩溃捕获"},
|
||||
"trigger.toggle.screensaver": {"label":"屏保"},
|
||||
"trigger.toggle.nightlight": {"label":"夜间模式"},
|
||||
"trigger.toggle.top-bar": {"label":"菜单栏"},
|
||||
"trigger.toggle.battery-percentage": {"label":"电量百分比"},
|
||||
"trigger.toggle.workspace-layout": {"label":"工作区布局"},
|
||||
"trigger.toggle.window-gaps": {"label":"窗口间距"},
|
||||
"trigger.toggle.one-window-ratio": {"label":"单窗口比例"},
|
||||
"trigger.tests.network-speedtest": {"label":"网络测速"},
|
||||
"trigger.tests.disk-speedtest": {"label":"磁盘测速"},
|
||||
"style.theme": {"label":"主题"},
|
||||
"style.background": {"label":"壁纸"},
|
||||
"style.unlock": {"label":"解锁画面"},
|
||||
"style.font": {"label":"字体"},
|
||||
"style.bar": {"label":"菜单栏"},
|
||||
"style.bar.position": {"label":"位置"},
|
||||
"style.bar.transparency": {"label":"透明"},
|
||||
"style.screensaver": {"label":"屏保"},
|
||||
"style.about": {"label":"关于"},
|
||||
"style.bar.position.top": {"label":"顶部"},
|
||||
"style.bar.position.bottom": {"label":"底部"},
|
||||
"style.bar.position.left": {"label":"左侧"},
|
||||
"style.bar.position.right": {"label":"右侧"},
|
||||
"style.about.text": {"label":"编辑文字"},
|
||||
"style.about.image": {"label":"取自图片"},
|
||||
"style.about.default": {"label":"恢复默认"},
|
||||
"style.screensaver.text": {"label":"编辑文字"},
|
||||
"style.screensaver.image": {"label":"取自图片"},
|
||||
"style.screensaver.default": {"label":"恢复默认"},
|
||||
"setup.monitors": {"label":"显示器"},
|
||||
"setup.keybindings": {"label":"快捷键"},
|
||||
"setup.input": {"label":"输入设备"},
|
||||
"setup.network": {"label":"网络"},
|
||||
"setup.network.dns.custom": {"label":"自定义"},
|
||||
"setup.network.qr": {"label":"二维码"},
|
||||
"setup.default": {"label":"默认程序"},
|
||||
"setup.default.agent": {"title":"默认 Agent"},
|
||||
"setup.default.browser": {"label":"浏览器","title":"默认浏览器"},
|
||||
"setup.default.terminal": {"label":"终端","title":"默认终端"},
|
||||
"setup.default.editor": {"label":"编辑器","title":"默认编辑器"},
|
||||
"setup.region": {"label":"区域"},
|
||||
"setup.region.world": {"label":"世界"},
|
||||
"setup.region.china": {"label":"中国"},
|
||||
"setup.plugin": {"label":"插件"},
|
||||
"setup.plugin.enable": {"label":"启用插件"},
|
||||
"setup.plugin.disable": {"label":"停用插件"},
|
||||
"setup.plugin.add": {"label":"添加插件"},
|
||||
"setup.plugin.clone": {"label":"克隆插件"},
|
||||
"setup.plugin.remove": {"label":"移除插件"},
|
||||
"setup.security": {"label":"安全"},
|
||||
"setup.config": {"label":"配置"},
|
||||
"setup.security.fingerprint": {"label":"指纹"},
|
||||
"setup.security.passwordless-sudo": {"label":"免密 sudo"},
|
||||
"setup.security.sudoless-docker": {"label":"免 sudo Docker"},
|
||||
"setup.direct-boot": {"label":"直接启动"},
|
||||
"setup.reset": {"label":"恢复出厂设置"},
|
||||
"install.package": {"label":"软件包"},
|
||||
"install.service": {"label":"服务"},
|
||||
"install.development": {"label":"开发环境"},
|
||||
"install.editor": {"label":"编辑器"},
|
||||
"install.style": {"label":"风格"},
|
||||
"install.style.theme": {"label":"主题"},
|
||||
"install.style.background": {"label":"壁纸"},
|
||||
"install.style.font": {"label":"字体"},
|
||||
"install.gaming": {"label":"游戏"},
|
||||
"install.browser": {"label":"浏览器"},
|
||||
"install.webapp": {"label":"网页应用"},
|
||||
"install.terminal": {"label":"终端"},
|
||||
"install.preinstalls": {"label":"预装应用"},
|
||||
"install.service.chromium-account": {"label":"Chromium 账号"},
|
||||
"install.ai.dictation": {"label":"语音听写"},
|
||||
"install.gaming.xbox-cloud": {"label":"Xbox 云游戏"},
|
||||
"install.gaming.xbox-controllers": {"label":"Xbox 手柄"},
|
||||
"install.gaming.retro-launcher": {"label":"RetroArch 启动器"},
|
||||
"install.development.docker-dbs": {"label":"Docker 数据库"},
|
||||
"remove.package": {"label":"软件包"},
|
||||
"remove.ai": {"title":"移除"},
|
||||
"remove.service": {"label":"服务","title":"移除"},
|
||||
"remove.development": {"label":"开发环境","title":"移除"},
|
||||
"remove.theme": {"label":"主题"},
|
||||
"remove.gaming": {"label":"游戏","title":"移除"},
|
||||
"remove.browser": {"label":"浏览器","title":"移除"},
|
||||
"remove.webapp": {"label":"网页应用"},
|
||||
"remove.preinstalls": {"label":"预装应用"},
|
||||
"remove.security": {"label":"安全","title":"移除"},
|
||||
"remove.security.fingerprint": {"label":"指纹"},
|
||||
"remove.security.sudoless-docker": {"label":"免 sudo Docker"},
|
||||
"remove.ai.dictation": {"label":"语音听写"},
|
||||
"remove.gaming.xbox-cloud": {"label":"Xbox 云游戏"},
|
||||
"remove.gaming.xbox-controllers": {"label":"Xbox 手柄 ()"},
|
||||
"remove.development.javascript": {"title":"移除"},
|
||||
"remove.development.php": {"title":"移除"},
|
||||
"remove.development.elixir": {"title":"移除"},
|
||||
"update.channel": {"label":"通道"},
|
||||
"update.config": {"label":"配置","title":"重置为默认"},
|
||||
"update.themes": {"label":"附加主题"},
|
||||
"update.process": {"label":"进程","title":"重启"},
|
||||
"update.hardware": {"label":"硬件","title":"重启"},
|
||||
"update.firmware": {"label":"固件"},
|
||||
"update.password": {"label":"密码"},
|
||||
"update.timezone": {"label":"时区"},
|
||||
"update.time": {"label":"时间"},
|
||||
"update.hardware.audio": {"label":"音频"},
|
||||
"update.hardware.bluetooth": {"label":"蓝牙"},
|
||||
"update.hardware.trackpad": {"label":"触控板"},
|
||||
"update.password.drive": {"label":"磁盘加密"},
|
||||
"update.password.user": {"label":"用户"},
|
||||
}
|
||||
@@ -26,4 +26,4 @@ Include = /etc/pacman.d/mirrorlist
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
Server = https://pkgs.omarchy.org/edge/$arch
|
||||
Server = https://pkgs.omarchy.org/rc/$arch
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
%wheel ALL=(ALL) NOPASSWD: /usr/bin/asdcontrol
|
||||
@@ -4,7 +4,6 @@ run_logged "$OMARCHY_INSTALL/hardware/dell-xps-touchpad-haptics.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/surface.sh"
|
||||
|
||||
run_logged "$OMARCHY_INSTALL/hardware/network.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/input-group.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/set-wireless-regdom.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/fix-fkeys.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/fix-synaptic-touchpad.sh"
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# Give this user privileged input access for dictation tools + xbox controllers to work.
|
||||
# Recorded for provisioning first-boot user creation and factory reset, granted directly
|
||||
# when the install user already exists (deferred-provisioning installs create the user at
|
||||
# first boot instead).
|
||||
provisioning_dir="${OMARCHY_PROVISIONING_DIR:-/var/lib/omarchy/provisioning}"
|
||||
mkdir -p "$provisioning_dir"
|
||||
grep -qxF input "$provisioning_dir/groups" 2>/dev/null || echo input >>"$provisioning_dir/groups"
|
||||
|
||||
if [[ -n ${OMARCHY_INSTALL_USER:-} ]] && getent passwd "$OMARCHY_INSTALL_USER" >/dev/null; then
|
||||
usermod -aG input "$OMARCHY_INSTALL_USER"
|
||||
fi
|
||||
@@ -61,6 +61,8 @@ On Dell XPS laptops with a haptic touchpad, you can also set the click strength
|
||||
|
||||
Omarchy runs the [fcitx5](https://fcitx-im.org/) input method framework as part of every session — it's what powers the CapsLock compose sequences. That means the plumbing for non-Latin input is already in place: install an input engine like `fcitx5-mozc` (Japanese) or `fcitx5-chinese-addons` (Chinese) with `omarchy pkg add`, plus `fcitx5-configtool` to add the engine to your input methods and set the key that switches between them.
|
||||
|
||||
For Chinese there's a shortcut: flip _Setup > Region_ to China and Omarchy installs and configures the [Rime Ice](https://github.com/iDvel/rime-ice) input method for you, using [Andy Stewart's installer](https://github.com/manateelazycat/rime-ice-installer), and switches the system language to Chinese from the next login. Rime comes up active by default, `Ctrl+Space` toggles back to plain English input, and the Omarchy menu itself puts on Chinese labels.
|
||||
|
||||
### Use ALT as SUPER
|
||||
|
||||
On some keyboards, it's not convenient to use the primary meta key (Windows/cmd key) as SUPER. You can change this to be ALT instead using this change:
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
echo "Drop the default input group grant, which allowed unprivileged keylogging"
|
||||
|
||||
# Membership of `input` gives raw read/write access to /dev/input/event*: any
|
||||
# process running as the user can capture keystrokes and synthesize input. The
|
||||
# blanket grant is unnecessary: the Xbox-controller and ydotool installers add
|
||||
# the group themselves when those features are deliberately installed.
|
||||
#
|
||||
# Preserve membership where one of those opt-in features is present; removing
|
||||
# it there would break the feature the user chose to install.
|
||||
if id -nG "$USER" | grep -qw input; then
|
||||
if pacman -Qq xpadneo-dkms &>/dev/null || pacman -Qq ydotool &>/dev/null; then
|
||||
echo "Keeping $USER in the input group: controller or ydotool support is installed."
|
||||
else
|
||||
sudo gpasswd -d "$USER" input >/dev/null
|
||||
echo "Removed $USER from the input group. Log out and back in to apply."
|
||||
omarchy-state set reboot-required
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,12 @@
|
||||
echo "Point rc-channel installs at the rc package repository"
|
||||
|
||||
# pacman-rc.conf shipped with [omarchy] pointing at the edge repository, a
|
||||
# leftover from when release candidates published there. Candidates now publish
|
||||
# to the dedicated rc channel, so a machine on the rc mirror was taking its
|
||||
# omarchy packages from edge. Repoint only a conf that still carries the
|
||||
# shipped pairing: an administrator who chose another combination keeps it.
|
||||
if grep -q "https://rc-mirror.omarchy.org/" /etc/pacman.d/mirrorlist &&
|
||||
grep -q "^Server = https://pkgs.omarchy.org/edge/" /etc/pacman.conf; then
|
||||
sudo sed -i "s|^Server = https://pkgs.omarchy.org/edge/|Server = https://pkgs.omarchy.org/rc/|" /etc/pacman.conf
|
||||
echo "Switched the [omarchy] repository to the rc channel to match this machine's rc mirror."
|
||||
fi
|
||||
@@ -0,0 +1,133 @@
|
||||
echo "Disable SSH password authentication, or sshd itself when no key is authorized"
|
||||
|
||||
config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf
|
||||
authorized_keys="$HOME/.ssh/authorized_keys"
|
||||
|
||||
as_root() {
|
||||
if (( EUID == 0 )); then
|
||||
"$@"
|
||||
else
|
||||
sudo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Passwords staying enabled is the state the machine has been living with, so a
|
||||
# condition this migration cannot repair completes with a notice instead of
|
||||
# failing and holding up every migration queued behind it. Only missing
|
||||
# privileges stay pending below, because rerunning from a terminal fixes that.
|
||||
skip() {
|
||||
echo "$1 SSH password authentication remains enabled; run omarchy-setup-security-sshd to harden manually."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# The fixed setup command writes this file itself. Its presence is also the
|
||||
# machine-wide completion state, so migrations run by another account no-op.
|
||||
if [[ -e $config || -L $config ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Earlier versions enabled sshd before importing the key, but did not leave a
|
||||
# marker saying that Omarchy configured it. Limit the repair to a daemon that is
|
||||
# enabled or currently exposed and a user who already has a usable authorized
|
||||
# key. A machine that never set SSH up exits without prompting for privileges.
|
||||
if ! systemctl is-enabled --quiet sshd.service 2>/dev/null &&
|
||||
! systemctl is-active --quiet sshd.service 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# sshd reads authorized_keys one entry per line, while ssh-keygen -lf
|
||||
# fingerprints whole files in formats sshd does not accept there — a private
|
||||
# key copied in by mistake passes the file-level check even though sshd finds
|
||||
# no usable entry in it. Ask sshd's question instead: does any single line
|
||||
# parse as a public key?
|
||||
has_usable_key() {
|
||||
local line
|
||||
while IFS= read -r line || [[ -n $line ]]; do
|
||||
if [[ $line =~ ^[[:space:]]*(#|$) ]]; then
|
||||
continue
|
||||
fi
|
||||
if ssh-keygen -lf /dev/stdin <<<"$line" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
done <"$authorized_keys"
|
||||
return 1
|
||||
}
|
||||
|
||||
# A file that exists but cannot be read leaves the key question unanswered; do
|
||||
# not treat it as proof the machine is password-only. [[ -f ]] and the read
|
||||
# both follow symlinks on purpose: a dotfiles-managed authorized_keys link with
|
||||
# a working key must not count as keyless.
|
||||
if [[ -f $authorized_keys && ! -r $authorized_keys ]]; then
|
||||
skip "Could not read $authorized_keys to check for a usable key."
|
||||
fi
|
||||
|
||||
# The old setup command enabled sshd before importing a key, so an aborted run
|
||||
# left a password-only server exposed. Without a usable key there is nothing to
|
||||
# harden: close the hole Omarchy opened by disabling the server. Omarchy is a
|
||||
# desktop distro, so the console remains; re-enabling password SSH afterwards
|
||||
# is an intentional, informed choice the warning explains how to make.
|
||||
if [[ ! -f $authorized_keys ]] || ! has_usable_key; then
|
||||
if ! as_root systemctl disable --now sshd.service; then
|
||||
echo "Administrator privileges are required to close the password-only SSH server. Run omarchy-migrate again from a terminal." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "No usable SSH key is authorized, so sshd only accepted password logins. The SSH server has been disabled: run omarchy-setup-security-sshd to set it up with key-based authentication, or re-enable sshd to accept password logins anyway."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Under StrictModes, sshd's default, a group- or world-writable home directory,
|
||||
# ~/.ssh, or authorized_keys makes sshd ignore the key that just validated, and
|
||||
# passwords would then be the only way in. Tighten the two paths the setup
|
||||
# command owns, exactly as it does; the home directory is not ours to change.
|
||||
home_mode=$(stat -c '%a' "$HOME" 2>/dev/null) || skip "Could not inspect the permissions on $HOME."
|
||||
if (( 8#$home_mode & 8#022 )); then
|
||||
skip "$HOME is group- or world-writable, so sshd would ignore the authorized key."
|
||||
fi
|
||||
if ! chmod 700 "$HOME/.ssh" || ! chmod 600 "$authorized_keys"; then
|
||||
skip "Could not tighten the permissions on $authorized_keys."
|
||||
fi
|
||||
|
||||
echo "Disabling SSH password authentication on the existing key-based SSH setup..."
|
||||
if ! as_root install -Dm644 /dev/stdin "$config" <<'CONF'
|
||||
# Written by Omarchy once an SSH key was already authorized.
|
||||
# Delete this file and reload sshd to allow password logins again.
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
CONF
|
||||
then
|
||||
echo "Administrator privileges are required to harden the existing SSH setup. Run omarchy-migrate again from a terminal." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The drop-in itself is always valid, so a rejection means the configuration
|
||||
# was already broken before it arrived — the administrator's to repair.
|
||||
if ! as_root sshd -t; then
|
||||
as_root rm -f -- "$config" || true
|
||||
skip "sshd rejected its configuration."
|
||||
fi
|
||||
|
||||
effective_config=$(as_root sshd -T) || {
|
||||
as_root rm -f -- "$config" || true
|
||||
skip "Could not inspect sshd's effective configuration."
|
||||
}
|
||||
|
||||
# Syntax alone is insufficient because sshd uses the first value it reads. An
|
||||
# sshd_config predating the packaged sshd_config.d Include never reads the
|
||||
# drop-in at all, and an earlier administrator rule overrides it. Either way
|
||||
# the file is ineffective: remove it rather than claiming the machine is
|
||||
# protected.
|
||||
if ! grep -qixF "passwordauthentication no" <<<"$effective_config" ||
|
||||
! grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config"; then
|
||||
as_root rm -f -- "$config" || true
|
||||
skip "sshd does not apply the hardening drop-in, so an earlier rule or a config without the sshd_config.d include wins."
|
||||
fi
|
||||
|
||||
# An enabled but deliberately stopped daemon picks the file up on its next
|
||||
# start. Reload only a daemon that is currently serving connections so existing
|
||||
# sessions survive while new ones get the hardened policy.
|
||||
if systemctl is-active --quiet sshd.service 2>/dev/null; then
|
||||
if ! as_root systemctl reload sshd.service; then
|
||||
echo "The hardening config is installed and valid, but sshd did not reload; it takes effect when sshd next restarts." >&2
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
@@ -948,6 +948,21 @@ Item {
|
||||
onFileChanged: barHiddenProbe.running = true
|
||||
}
|
||||
|
||||
// The directory watch can permanently stop delivering events after flag
|
||||
// changes land in quick succession, stranding the bar off screen until the
|
||||
// shell restarts. `omarchy-toggle-bar` nudges this after flipping the flag
|
||||
// so the probe re-reads it even when the watch has gone quiet.
|
||||
IpcHandler {
|
||||
target: "omarchy.bar"
|
||||
|
||||
// Start rather than restart: a probe already in flight was launched by the
|
||||
// directory watch after the flag flipped, so its answer is current, and
|
||||
// killing it here can swallow the result entirely.
|
||||
function syncHidden(): void {
|
||||
barHiddenProbe.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
|
||||
@@ -58,7 +58,9 @@ function parseMenuJsonc(raw) {
|
||||
for (var id in source) {
|
||||
var entry = source[id]
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue
|
||||
out.push(normalizeItem(id, entry))
|
||||
var item = normalizeItem(id, entry)
|
||||
item.declared = Object.keys(entry)
|
||||
out.push(item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -74,11 +76,25 @@ function mergeMenuSources(defaultItems, userItems) {
|
||||
var entry = src[i]
|
||||
if (!entry || !entry.id) continue
|
||||
if (!nextItems[entry.id]) nextOrder.push(entry.id)
|
||||
var prior = nextItems[entry.id] || {}
|
||||
var prior = nextItems[entry.id]
|
||||
var merged = {}
|
||||
for (var k in prior) merged[k] = prior[k]
|
||||
for (var k2 in entry) merged[k2] = entry[k2]
|
||||
// Overriding an existing id only replaces the fields the file declared,
|
||||
// so a label-only entry retitles a row without clearing its action.
|
||||
if (prior && entry.declared) {
|
||||
for (var k in prior) merged[k] = prior[k]
|
||||
for (var d = 0; d < entry.declared.length; d++) {
|
||||
var key = entry.declared[d]
|
||||
if (key in entry) merged[key] = entry[key]
|
||||
}
|
||||
if (entry.declared.indexOf("action") >= 0 || entry.declared.indexOf("target") >= 0)
|
||||
merged.kind = merged.action ? "action" : (merged.target ? "link" : "menu")
|
||||
} else {
|
||||
prior = prior || {}
|
||||
for (var k1 in prior) merged[k1] = prior[k1]
|
||||
for (var k2 in entry) merged[k2] = entry[k2]
|
||||
}
|
||||
merged.id = entry.id
|
||||
delete merged.declared
|
||||
nextItems[entry.id] = merged
|
||||
}
|
||||
}
|
||||
@@ -398,7 +414,8 @@ var GUARD_READERS = [
|
||||
"omarchy-default-browser",
|
||||
"omarchy-default-editor",
|
||||
"omarchy-default-terminal",
|
||||
"omarchy-dns"
|
||||
"omarchy-dns",
|
||||
"omarchy-region"
|
||||
]
|
||||
|
||||
// Package and command presence account for most of what the guards ask, and
|
||||
|
||||
+7
-1
@@ -19,7 +19,13 @@ mkdir -p "$OMARCHY_ACCEPTANCE_DIR"
|
||||
# the session environment is inherited.
|
||||
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||
export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=$XDG_RUNTIME_DIR/bus}"
|
||||
export OMARCHY_PATH="${OMARCHY_PATH:-$ROOT}"
|
||||
# The suite verifies the installed product the session is running, so default
|
||||
# OMARCHY_PATH to the installed tree — never this checkout, which may hold
|
||||
# only test/ (omarchy-iso-test's --sync-omarchy). qs matches shell instances
|
||||
# by config path, so a suite pointed at any other tree reads the session
|
||||
# shell as "not running". Callers testing a different tree pass it explicitly.
|
||||
export OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
|
||||
|
||||
export PATH="$OMARCHY_PATH/bin:$PATH"
|
||||
|
||||
if [[ -z ${DISPLAY:-} ]]; then
|
||||
|
||||
@@ -36,7 +36,9 @@ screen_contains() {
|
||||
local text="$1"
|
||||
local snapshot="/tmp/omarchy-acceptance-ocr-$$.png"
|
||||
|
||||
if ! timeout 10 grim "$snapshot" 2>/dev/null; then
|
||||
# Capture at 2x scale: tesseract routinely drops small caption text at
|
||||
# native resolution (the weather panel's detail labels, for one).
|
||||
if ! timeout 10 grim -s 2 "$snapshot" 2>/dev/null; then
|
||||
rm -f "$snapshot"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -75,7 +75,7 @@ wtype -k Return
|
||||
wait_until "style submenu is visible" 15 screen_contains "Theme"
|
||||
screenshot "success-menu-03-style-submenu"
|
||||
|
||||
wtype -k Down -k Down -k Down -k Return
|
||||
wtype -k Down -k Down -k Down -k Down -k Return
|
||||
sleep 1
|
||||
screenshot "success-menu-04-menu-bar-submenu"
|
||||
|
||||
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Verifies the security posture of an installed system: the unprivileged
|
||||
# session-to-root paths closed for 4.0.2 (blanket input-group grant, shipped
|
||||
# asdcontrol sudoers authorization) and the SSH hardening flow.
|
||||
#
|
||||
# The sshd section reconfigures the machine (enables sshd, opens the firewall,
|
||||
# disables password logins), so it demands explicit opt-in: it only runs when
|
||||
# OMARCHY_ACCEPTANCE_SUDO_PASSWORD is set, which omarchy-iso-test does for its
|
||||
# throwaway VMs. A cached sudo timestamp alone never triggers it, so running
|
||||
# the suite on a machine you care about cannot reconfigure sshd by accident.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
# Membership of `input` gives raw access to /dev/input/event*: any process
|
||||
# running as the user could log keystrokes. Only the opt-in controller and
|
||||
# ydotool features may grant it.
|
||||
verify_input_group() {
|
||||
if id -nG | grep -qw input; then
|
||||
if pacman -Q xpadneo-dkms &>/dev/null || pacman -Q ydotool &>/dev/null; then
|
||||
pass "input group membership is backed by an opt-in feature"
|
||||
else
|
||||
fail "user is not in the input group" "no controller or ydotool support installed to justify it"
|
||||
fi
|
||||
else
|
||||
pass "user is not in the input group"
|
||||
fi
|
||||
}
|
||||
|
||||
sudo_available() {
|
||||
if sudo -n true 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n ${OMARCHY_ACCEPTANCE_SUDO_PASSWORD:-} ]]; then
|
||||
printf '%s\n' "$OMARCHY_ACCEPTANCE_SUDO_PASSWORD" | sudo -S -v 2>/dev/null
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
verify_asdcontrol_sudoers() {
|
||||
# Omarchy used to ship a passwordless sudoers grant for asdcontrol; that
|
||||
# authorization now belongs to the package alone.
|
||||
if sudo -n test -e /etc/sudoers.d/omarchy-asdcontrol; then
|
||||
fail "no omarchy asdcontrol sudoers grant is shipped" "/etc/sudoers.d/omarchy-asdcontrol exists"
|
||||
fi
|
||||
pass "no omarchy asdcontrol sudoers grant is shipped"
|
||||
}
|
||||
|
||||
verify_sshd_hardening() {
|
||||
local key_file=/tmp/omarchy-acceptance-sshd-key
|
||||
local effective_config
|
||||
|
||||
rm -f "$key_file" "$key_file.pub"
|
||||
ssh-keygen -t ed25519 -N "" -q -C "omarchy-acceptance" -f "$key_file"
|
||||
|
||||
# sudo keys its cached credential on the calling terminal and, absent one, on
|
||||
# the caller's parent process alone, so a timestamp validated in this shell
|
||||
# never reaches the setup command's own sudo calls when the suite runs
|
||||
# without a terminal (omarchy-iso-test drives it over ssh with no pty). Give
|
||||
# the exercise a pseudo-terminal and validate the password on it first, so
|
||||
# every sudo underneath shares that terminal's credential.
|
||||
if ! OMARCHY_ACCEPTANCE_SUDO_PASSWORD="$OMARCHY_ACCEPTANCE_SUDO_PASSWORD" \
|
||||
OMARCHY_ACCEPTANCE_SSHD_KEY="$(cat "$key_file.pub")" SHELL=/bin/bash \
|
||||
script -qec 'printf "%s\n" "$OMARCHY_ACCEPTANCE_SUDO_PASSWORD" | sudo -S -v 2>/dev/null &&
|
||||
omarchy-setup-security-sshd --key="$OMARCHY_ACCEPTANCE_SSHD_KEY"' /dev/null \
|
||||
</dev/null >"$ARTIFACTS/setup-security-sshd.log" 2>&1; then
|
||||
fail "omarchy-setup-security-sshd completes unattended" "$(tail -5 "$ARTIFACTS/setup-security-sshd.log")"
|
||||
fi
|
||||
pass "omarchy-setup-security-sshd completes unattended"
|
||||
|
||||
systemctl is-active sshd.service >/dev/null || fail "sshd is running after setup"
|
||||
pass "sshd is running after setup"
|
||||
|
||||
grep -qxF "$(cat "$key_file.pub")" "$HOME/.ssh/authorized_keys" || fail "the key is authorized"
|
||||
pass "the key is authorized"
|
||||
|
||||
# The command verifies its own hardening before keeping it, but assert the
|
||||
# effective config independently: sshd honors the first value it reads, and
|
||||
# regressions here reopen password logins. Keywords match case-insensitively
|
||||
# because OpenSSH 9.x dumps them lowercase and 10.x in CamelCase.
|
||||
effective_config=$(sudo -n sshd -T) || fail "sshd reports its effective config"
|
||||
grep -qixF "passwordauthentication no" <<<"$effective_config" || fail "password authentication is off"
|
||||
pass "password authentication is off"
|
||||
grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config" || fail "keyboard-interactive authentication is off"
|
||||
pass "keyboard-interactive authentication is off"
|
||||
|
||||
if omarchy-cmd-present ufw; then
|
||||
sudo -n ufw status | grep -qE '^22/tcp\s+LIMIT' || fail "the SSH port is rate limited in the firewall"
|
||||
pass "the SSH port is rate limited in the firewall"
|
||||
fi
|
||||
|
||||
# Leave the machine as found where cheap: the throwaway key stays useless
|
||||
# once removed, while the hardening itself is the state under test.
|
||||
sed -i "\#$(cat "$key_file.pub" | cut -d' ' -f2)#d" "$HOME/.ssh/authorized_keys"
|
||||
rm -f "$key_file" "$key_file.pub"
|
||||
}
|
||||
|
||||
verify_input_group
|
||||
|
||||
if sudo_available; then
|
||||
verify_asdcontrol_sudoers
|
||||
else
|
||||
pass "asdcontrol sudoers check skipped: sudo needs a password"
|
||||
fi
|
||||
|
||||
if [[ -n ${OMARCHY_ACCEPTANCE_SUDO_PASSWORD:-} ]] && sudo_available; then
|
||||
verify_sshd_hardening
|
||||
else
|
||||
pass "sshd hardening exercise skipped: set OMARCHY_ACCEPTANCE_SUDO_PASSWORD to run it"
|
||||
fi
|
||||
@@ -8,12 +8,17 @@ status=0
|
||||
|
||||
verify_core_packages() {
|
||||
local package
|
||||
local manifest="$OMARCHY_PATH/install/omarchy-base.packages"
|
||||
local -a missing=()
|
||||
|
||||
# Without this, a missing manifest reads as an empty package list and the
|
||||
# audit passes having checked nothing.
|
||||
[[ -f $manifest ]] || fail "all Omarchy core packages are installed" "package manifest not found: $manifest"
|
||||
|
||||
while IFS= read -r package; do
|
||||
[[ -z $package || $package == \#* ]] && continue
|
||||
pacman -Q "$package" >/dev/null 2>&1 || missing+=("$package")
|
||||
done <"$OMARCHY_PATH/install/omarchy-base.packages"
|
||||
done <"$manifest"
|
||||
|
||||
(( ${#missing[@]} == 0 )) || fail "all Omarchy core packages are installed" "missing packages: ${missing[*]}"
|
||||
pass "all Omarchy core packages are installed (${#missing[@]} missing)"
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
migration="$ROOT/migrations/1787865477.sh"
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
|
||||
stub_bin="$test_dir/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
cat >"$stub_bin/id" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "${STUB_GROUPS:-wheel}"
|
||||
STUB
|
||||
cat >"$stub_bin/pacman" <<'STUB'
|
||||
#!/bin/bash
|
||||
[[ $1 == "-Qq" ]] || exit 2
|
||||
[[ " ${STUB_PACKAGES:-} " == *" $2 "* ]]
|
||||
STUB
|
||||
cat >"$stub_bin/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
exec "$@"
|
||||
STUB
|
||||
cat >"$stub_bin/gpasswd" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"${GPASSWD_CALLS:?}"
|
||||
STUB
|
||||
cat >"$stub_bin/omarchy-state" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"${STATE_CALLS:?}"
|
||||
STUB
|
||||
chmod +x "$stub_bin"/*
|
||||
|
||||
gpasswd_calls="$test_dir/gpasswd-calls"
|
||||
state_calls="$test_dir/state-calls"
|
||||
|
||||
run_migration() {
|
||||
rm -f "$gpasswd_calls" "$state_calls"
|
||||
USER=tester STUB_GROUPS="$1" STUB_PACKAGES="${2:-}" \
|
||||
GPASSWD_CALLS="$gpasswd_calls" STATE_CALLS="$state_calls" \
|
||||
PATH="$stub_bin:$PATH" bash -euo pipefail "$migration"
|
||||
}
|
||||
|
||||
run_migration "wheel input" >/dev/null
|
||||
grep -qxF -- "-d tester input" "$gpasswd_calls" || fail "migration removes default input membership"
|
||||
grep -qxF "set reboot-required" "$state_calls" || fail "migration flags the session change for reboot"
|
||||
pass "migration removes the blanket input grant"
|
||||
|
||||
run_migration "wheel" >/dev/null
|
||||
[[ ! -e $gpasswd_calls ]] || fail "migration does not remove an already-absent group"
|
||||
[[ ! -e $state_calls ]] || fail "migration does not flag a reboot when nothing changed"
|
||||
pass "migration is idempotent after input membership is gone"
|
||||
|
||||
run_migration "wheel input" xpadneo-dkms >/dev/null
|
||||
[[ ! -e $gpasswd_calls ]] || fail "migration preserves input for controller support"
|
||||
[[ ! -e $state_calls ]] || fail "preserved controller support does not flag a reboot"
|
||||
|
||||
run_migration "wheel input" ydotool >/dev/null
|
||||
[[ ! -e $gpasswd_calls ]] || fail "migration preserves input for ydotool"
|
||||
[[ ! -e $state_calls ]] || fail "preserved ydotool support does not flag a reboot"
|
||||
pass "migration preserves deliberate input-group opt-ins"
|
||||
@@ -44,9 +44,10 @@ assertDeepEqual(
|
||||
aliases: ['theme'],
|
||||
when: '',
|
||||
checked: '',
|
||||
disabled: ''
|
||||
disabled: '',
|
||||
declared: ['label', 'aliases', 'description', 'action']
|
||||
},
|
||||
'menu normalizes parsed items'
|
||||
'menu normalizes parsed items and records what the file declared'
|
||||
)
|
||||
|
||||
const user = [
|
||||
@@ -58,6 +59,16 @@ assertEqual(merged.items['style.theme'].label, 'Theme picker', 'menu user entrie
|
||||
assertEqual(merged.items['style.theme'].order, 2, 'menu preserves original order on override')
|
||||
assert(merged.items.root, 'menu injects root when merging sources')
|
||||
|
||||
const retitled = menu.mergeMenuSources(
|
||||
menu.parseMenuJsonc('{"apps": {"icon":"A","label":"Apps","provider":"apps"}, "apps.go": {"label":"Go","action":"run-go"}}'),
|
||||
menu.parseMenuJsonc('{"apps": {"label":"应用"}, "apps.go": {"label":"出发"}}')
|
||||
)
|
||||
assertEqual(retitled.items['apps'].provider, 'apps', 'menu keeps the provider when a user entry only retitles')
|
||||
assertEqual(retitled.items['apps'].icon, 'A', 'menu keeps the icon when a user entry only retitles')
|
||||
assertEqual(retitled.items['apps.go'].action, 'run-go', 'menu keeps the action when a user entry only retitles')
|
||||
assertEqual(retitled.items['apps.go'].kind, 'action', 'menu keeps the kind when a user entry only retitles')
|
||||
assertEqual(retitled.items['apps.go'].label, '出发', 'menu applies the user retitle')
|
||||
|
||||
assertEqual(menu.slugify('Power Saver!'), 'power-saver', 'menu slugifies provider rows')
|
||||
assertEqual(menu.pathFor(merged.items, 'style.theme'), 'Style › Theme picker', 'menu builds item paths')
|
||||
assertEqual(menu.parentPathFor(merged.items, 'style.theme'), 'Style', 'menu builds parent paths')
|
||||
@@ -261,6 +272,22 @@ assert(
|
||||
'menu always exposes every supported browser, terminal, and editor under Defaults'
|
||||
)
|
||||
assert(!defaultById['install.ai.crush'], 'menu removes Crush from Install > AI')
|
||||
const regionEntries = defaultItems.filter(item => item.parent === 'setup.region')
|
||||
assert(
|
||||
regionEntries.map(item => item.label).join('\0') === 'World\0China'
|
||||
&& regionEntries.every(item => item.checked.includes(`== \"${item.label}\"`) && !item.when)
|
||||
&& defaultById['setup.region.world'].action.includes('omarchy-region world')
|
||||
&& defaultById['setup.region.china'].action.includes('omarchy-region china'),
|
||||
'menu offers World and China under Setup > Region'
|
||||
)
|
||||
const zhOverlay = fs.readFileSync(path.join(root, 'default/omarchy/omarchy-menu.zh-cn.jsonc'), 'utf8')
|
||||
const zhItems = menu.parseMenuJsonc(zhOverlay)
|
||||
assert(zhItems.length > 100, 'Chinese menu overlay parses')
|
||||
assert(zhItems.every(item => defaultById[item.id]), 'Chinese menu overlay only retitles shipped entries')
|
||||
assert(
|
||||
!/"(action|target|icon|when|checked|disabled|provider|aliases)"/.test(zhOverlay),
|
||||
'Chinese menu overlay declares labels and titles only'
|
||||
)
|
||||
// Software you already have keeps its place in Install, dimmed rather than
|
||||
// dropped, so the list reads as a catalog of what Omarchy can install.
|
||||
// Chromium Account is the sole Install row with anything left to hide for, so
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The install scripts that grant group memberships must record them in the provisioning
|
||||
# groups file (for first-boot user creation and factory reset) and only call
|
||||
# usermod when the install user actually exists.
|
||||
#
|
||||
# Docker is deliberately excluded: the docker group is root-equivalent, so it is
|
||||
# no longer granted at install time (opt in with omarchy-setup-security-sudoless-docker).
|
||||
# Privileged groups are never granted by the default install. Docker remains an
|
||||
# explicit opt-in, and raw input-device access is granted only by the optional
|
||||
# controller and ydotool installers.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -16,13 +13,7 @@ trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
export OMARCHY_PROVISIONING_DIR="$TMPDIR/provisioning"
|
||||
|
||||
# Stub getent/usermod: the fake system knows only the user "existing".
|
||||
mkdir -p "$TMPDIR/bin"
|
||||
cat >"$TMPDIR/bin/getent" <<'STUB'
|
||||
#!/bin/bash
|
||||
[[ $1 == passwd && $2 == existing ]] && { echo "existing:x:1000:1000::/home/existing:/bin/bash"; exit 0; }
|
||||
exit 2
|
||||
STUB
|
||||
cat >"$TMPDIR/bin/usermod" <<STUB
|
||||
#!/bin/bash
|
||||
echo "\$@" >>"$TMPDIR/usermod.calls"
|
||||
@@ -44,48 +35,31 @@ cat >"$TMPDIR/bin/sudo" <<STUB
|
||||
echo "\$@" >>"$TMPDIR/sudo.calls"
|
||||
exec "\$@"
|
||||
STUB
|
||||
chmod +x "$TMPDIR/bin"/{getent,usermod,groupadd,install,find,sudo}
|
||||
chmod +x "$TMPDIR/bin"/{usermod,groupadd,install,find,sudo}
|
||||
export PATH="$TMPDIR/bin:$PATH"
|
||||
export OMARCHY_PATH="$ROOT"
|
||||
|
||||
# No install user (deferred-provisioning install): groups recorded, usermod not called.
|
||||
# A deferred-provisioning install records neither privileged group.
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/docker.sh"
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/hardware/input-group.sh"
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/browser-policy.sh"
|
||||
|
||||
[[ -f $OMARCHY_PROVISIONING_DIR/groups ]] || fail "groups file written without an install user"
|
||||
grep -qxF input "$OMARCHY_PROVISIONING_DIR/groups" || fail "input group recorded"
|
||||
! grep -qxF omarchy-browser-policy "$OMARCHY_PROVISIONING_DIR/groups" ||
|
||||
fail "browser-policy group must not be recorded"
|
||||
[[ ! -f $OMARCHY_PROVISIONING_DIR/groups ]] ||
|
||||
! grep -Eq '^(docker|input)$' "$OMARCHY_PROVISIONING_DIR/groups" ||
|
||||
fail "default install must not record docker or input groups"
|
||||
[[ ! -f $TMPDIR/usermod.calls ]] || fail "usermod not called without an install user"
|
||||
[[ ! -f $TMPDIR/groupadd.calls ]] || ! grep -F omarchy-browser-policy "$TMPDIR/groupadd.calls" >/dev/null ||
|
||||
fail "browser-policy group is not created"
|
||||
grep -F -- '-d -m 0755 -o root -g root /etc/chromium/policies/managed' "$TMPDIR/install.calls" >/dev/null ||
|
||||
fail "browser-policy directory is created root-owned"
|
||||
pass "deferred provisioning records groups without calling usermod"
|
||||
pass "deferred provisioning records no privileged groups"
|
||||
|
||||
# The docker group is root-equivalent and must never be granted automatically.
|
||||
! grep -qxF docker "$OMARCHY_PROVISIONING_DIR/groups" || fail "docker group must not be recorded"
|
||||
pass "docker group is not recorded at install"
|
||||
|
||||
# Missing user (defensive): no usermod either.
|
||||
OMARCHY_INSTALL_USER=ghost bash -eE "$ROOT/install/hardware/input-group.sh"
|
||||
OMARCHY_INSTALL_USER=ghost bash -eE "$ROOT/install/config/browser-policy.sh"
|
||||
[[ ! -f $TMPDIR/usermod.calls ]] || fail "usermod not called for a missing user"
|
||||
pass "missing install user defers group grants"
|
||||
|
||||
# Re-running never duplicates entries.
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/hardware/input-group.sh"
|
||||
[[ $(grep -cxF input "$OMARCHY_PROVISIONING_DIR/groups") == 1 ]] || fail "input group recorded once"
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/browser-policy.sh"
|
||||
pass "group recording is idempotent"
|
||||
|
||||
# Existing user: usermod applies the recorded groups, and docker is never among them.
|
||||
# The same remains true when an install user already exists.
|
||||
OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/config/docker.sh"
|
||||
OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/hardware/input-group.sh"
|
||||
OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/config/browser-policy.sh"
|
||||
grep -qx -- "-aG input existing" "$TMPDIR/usermod.calls" || fail "usermod grants input to the install user"
|
||||
! grep -q -- "omarchy-browser-policy" "$TMPDIR/usermod.calls" ||
|
||||
fail "usermod must not grant browser-policy to the install user"
|
||||
! grep -q -- "docker" "$TMPDIR/usermod.calls" || fail "usermod must not grant docker to the install user"
|
||||
pass "existing install user gets input but never docker or browser-policy"
|
||||
[[ ! -f $TMPDIR/usermod.calls ]] || fail "default install must not grant privileged groups"
|
||||
pass "existing install user gets neither docker nor input access"
|
||||
|
||||
! grep -q 'hardware/input-group.sh' "$ROOT/install/hardware/all.sh" ||
|
||||
fail "hardware setup must not call the removed input-group grant"
|
||||
[[ ! -e $ROOT/install/hardware/input-group.sh ]] || fail "blanket input-group grant is removed"
|
||||
pass "hardware setup has no blanket input-group grant"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
mock_bin="$test_tmp/bin"
|
||||
test_home="$test_tmp/home"
|
||||
pkg_log="$test_tmp/pkg-add"
|
||||
installer_log="$test_tmp/installer"
|
||||
sudo_log="$test_tmp/sudo"
|
||||
systemctl_log="$test_tmp/systemctl"
|
||||
mkdir -p "$mock_bin" "$test_home"
|
||||
|
||||
cat >"$mock_bin/omarchy-pkg-add" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"$OMARCHY_TEST_PKG_LOG"
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/sudo" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"$OMARCHY_TEST_SUDO_LOG"
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/systemctl" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"$OMARCHY_TEST_SYSTEMCTL_LOG"
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/rime-ice-installer" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'ran\n' >>"$OMARCHY_TEST_INSTALLER_LOG"
|
||||
mkdir -p "$HOME/.local/share/fcitx5/rime"
|
||||
touch "$HOME/.local/share/fcitx5/rime/rime_ice.schema.yaml"
|
||||
SH
|
||||
|
||||
chmod +x "$mock_bin"/*
|
||||
|
||||
export HOME="$test_home"
|
||||
export PATH="$mock_bin:$ROOT/bin:$PATH"
|
||||
export OMARCHY_TEST_PKG_LOG="$pkg_log"
|
||||
export OMARCHY_TEST_INSTALLER_LOG="$installer_log"
|
||||
export OMARCHY_TEST_SUDO_LOG="$sudo_log"
|
||||
export OMARCHY_TEST_SYSTEMCTL_LOG="$systemctl_log"
|
||||
export OMARCHY_PATH="$ROOT"
|
||||
|
||||
[[ $(omarchy-region) == "World" ]] || fail "region defaults to World before any choice"
|
||||
pass "region defaults to World before any choice"
|
||||
|
||||
mkdir -p "$test_home/.config/omarchy/extensions"
|
||||
cp "$ROOT/config/omarchy/extensions/omarchy-menu.jsonc" "$test_home/.config/omarchy/extensions/omarchy-menu.jsonc"
|
||||
|
||||
omarchy-region china >/dev/null
|
||||
cmp -s "$ROOT/default/omarchy/omarchy-menu.zh-cn.jsonc" "$test_home/.config/omarchy/extensions/omarchy-menu.jsonc" ||
|
||||
fail "china installs the Chinese menu labels over the stock sample"
|
||||
grep -q "locale-gen" "$sudo_log" || fail "china generates the Chinese locale"
|
||||
grep -Fx "localectl set-locale LANG=zh_CN.UTF-8" "$sudo_log" >/dev/null || fail "china sets the system language"
|
||||
grep -Fx "pacman -Sy" "$sudo_log" >/dev/null || fail "china syncs the repo databases before installing"
|
||||
grep -Fx "rime-ice-installer" "$pkg_log" >/dev/null || fail "china installs the Rime Ice installer package"
|
||||
(( $(wc -l <"$installer_log") == 1 )) || fail "china runs the Rime Ice installer"
|
||||
grep -A1 '^\[Groups/0/Items/0\]$' "$test_home/.config/fcitx5/profile" | grep -qFx "Name=rime" ||
|
||||
fail "china makes Rime the first input method"
|
||||
grep -qFx "DefaultIM=rime" "$test_home/.config/fcitx5/profile" || fail "china makes Rime the group default"
|
||||
grep -qFx "0=Control+space" "$test_home/.config/fcitx5/config" || fail "china pins the Ctrl+Space toggle"
|
||||
grep -Fx -- "--user restart omarchy-fcitx5.service" "$systemctl_log" >/dev/null ||
|
||||
fail "china restarts fcitx5 with the new profile"
|
||||
[[ $(omarchy-region) == "China" ]] || fail "china is stored as the region"
|
||||
pass "china sets up the language, menu and input method"
|
||||
|
||||
omarchy-region china >/dev/null
|
||||
(( $(wc -l <"$installer_log") == 1 )) || fail "reapplying china skips the installed input method"
|
||||
(( $(grep -cFx "pacman -Sy" "$sudo_log") == 1 )) || fail "reapplying china skips the database sync too"
|
||||
pass "reapplying china is idempotent and leaves the input method alone"
|
||||
|
||||
omarchy-region world >/dev/null
|
||||
cmp -s "$ROOT/config/omarchy/extensions/omarchy-menu.jsonc" "$test_home/.config/omarchy/extensions/omarchy-menu.jsonc" ||
|
||||
fail "world restores the sample menu extension"
|
||||
[[ $(omarchy-region) == "World" ]] || fail "world is stored as the region"
|
||||
[[ -f $test_home/.local/share/fcitx5/rime/rime_ice.schema.yaml ]] || fail "world leaves the input method installed"
|
||||
grep -qFx "DefaultIM=rime" "$test_home/.config/fcitx5/profile" || fail "world leaves the input method configured"
|
||||
pass "world restores the menu and keeps the input method"
|
||||
|
||||
printf '{"apps": {"label":"Mine"}}\n' >"$test_home/.config/omarchy/extensions/omarchy-menu.jsonc"
|
||||
if omarchy-region china >/dev/null 2>"$test_tmp/refusal"; then
|
||||
fail "china refuses a menu extension someone wrote by hand"
|
||||
fi
|
||||
grep -q "menu:" "$test_tmp/refusal" || fail "china names the menu extension it refuses to touch"
|
||||
[[ $(omarchy-region) == "World" ]] || fail "a refused china run keeps the region"
|
||||
pass "china refuses a hand-written menu extension"
|
||||
grep -Fx '{"apps": {"label":"Mine"}}' "$test_home/.config/omarchy/extensions/omarchy-menu.jsonc" >/dev/null ||
|
||||
fail "a refused china run leaves the extension untouched"
|
||||
|
||||
omarchy-region world >/dev/null
|
||||
grep -Fx '{"apps": {"label":"Mine"}}' "$test_home/.config/omarchy/extensions/omarchy-menu.jsonc" >/dev/null ||
|
||||
fail "world leaves a menu extension someone wrote by hand"
|
||||
pass "world only restores the extension Omarchy wrote"
|
||||
|
||||
if omarchy-region china unexpected >/dev/null 2>&1; then
|
||||
fail "region rejects extra arguments"
|
||||
fi
|
||||
[[ $(omarchy-region) == "World" ]] || fail "extra arguments change nothing"
|
||||
pass "region rejects extra arguments"
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
|
||||
stub_bin="$test_dir/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
cat >"$stub_bin/omarchy-pkg-add" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'pkg %s\n' "$*" >>"${CALL_LOG:?}"
|
||||
STUB
|
||||
cat >"$stub_bin/omarchy-cmd-missing" <<'STUB'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
STUB
|
||||
cat >"$stub_bin/systemctl" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'systemctl %s\n' "$*" >>"${CALL_LOG:?}"
|
||||
STUB
|
||||
cat >"$stub_bin/sshd" <<'STUB'
|
||||
#!/bin/bash
|
||||
case $1 in
|
||||
-t)
|
||||
[[ ${SSHD_SYNTAX_VALID:-1} == 1 ]]
|
||||
;;
|
||||
-T)
|
||||
# OpenSSH 10.x dumps keywords in CamelCase; 9.x dumped them lowercase.
|
||||
if [[ ${SSHD_DUMP_LOWERCASE:-0} == 1 ]]; then
|
||||
printf 'passwordauthentication %s\n' "${SSHD_PASSWORD_AUTH:-no}"
|
||||
printf 'kbdinteractiveauthentication %s\n' "${SSHD_KBD_AUTH:-no}"
|
||||
else
|
||||
printf 'PasswordAuthentication %s\n' "${SSHD_PASSWORD_AUTH:-no}"
|
||||
printf 'KbdInteractiveAuthentication %s\n' "${SSHD_KBD_AUTH:-no}"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
STUB
|
||||
cat >"$stub_bin/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
case $1 in
|
||||
install)
|
||||
destination="${TEST_ROOT:?}${4:?}"
|
||||
/usr/bin/mkdir -p "${destination%/*}"
|
||||
/usr/bin/install -Dm644 /dev/stdin "$destination"
|
||||
;;
|
||||
rm)
|
||||
/usr/bin/rm -f "${TEST_ROOT:?}${3:?}"
|
||||
;;
|
||||
*)
|
||||
exec "$@"
|
||||
;;
|
||||
esac
|
||||
STUB
|
||||
chmod +x "$stub_bin"/*
|
||||
|
||||
ssh-keygen -q -t ed25519 -N "" -f "$test_dir/key"
|
||||
public_key=$(<"$test_dir/key.pub")
|
||||
|
||||
run_setup() {
|
||||
local scenario="$1"
|
||||
local home="$test_dir/$scenario/home"
|
||||
local root="$test_dir/$scenario/root"
|
||||
|
||||
mkdir -p "$home" "$root"
|
||||
: >"$test_dir/$scenario.calls"
|
||||
|
||||
HOME="$home" TEST_ROOT="$root" CALL_LOG="$test_dir/$scenario.calls" \
|
||||
SSHD_SYNTAX_VALID="${SSHD_SYNTAX_VALID:-1}" \
|
||||
SSHD_PASSWORD_AUTH="${SSHD_PASSWORD_AUTH:-no}" \
|
||||
SSHD_KBD_AUTH="${SSHD_KBD_AUTH:-no}" \
|
||||
PATH="$stub_bin:$PATH" \
|
||||
bash "$ROOT/bin/omarchy-setup-security-sshd" --key="$public_key"
|
||||
}
|
||||
|
||||
output=$(run_setup success)
|
||||
config="$test_dir/success/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf"
|
||||
grep -qxF "PasswordAuthentication no" "$config" || fail "SSH setup disables password authentication"
|
||||
grep -qxF "KbdInteractiveAuthentication no" "$config" || fail "SSH setup disables keyboard-interactive authentication"
|
||||
grep -qxF "systemctl reload sshd.service" "$test_dir/success.calls" || fail "SSH setup reloads the validated config"
|
||||
grep -q "Password logins are off" <<<"$output" || fail "SSH setup reports hardening after it succeeds"
|
||||
pass "SSH setup authorizes a key and disables password logins"
|
||||
|
||||
output=$(SSHD_DUMP_LOWERCASE=1 run_setup success-legacy)
|
||||
config="$test_dir/success-legacy/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf"
|
||||
[[ -e $config ]] || fail "SSH setup accepts the lowercase sshd -T dump of OpenSSH 9.x"
|
||||
grep -q "Password logins are off" <<<"$output" || fail "SSH setup reports hardening on OpenSSH 9.x"
|
||||
pass "SSH setup verifies settings across sshd -T keyword casings"
|
||||
|
||||
if SSHD_PASSWORD_AUTH=yes run_setup ineffective >"$test_dir/ineffective.output" 2>&1; then
|
||||
fail "SSH setup must fail when password authentication remains effective"
|
||||
fi
|
||||
[[ ! -e $test_dir/ineffective/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH setup removes an ineffective hardening config"
|
||||
! grep -qF "systemctl reload sshd.service" "$test_dir/ineffective.calls" ||
|
||||
fail "SSH setup must not reload ineffective hardening"
|
||||
! grep -q "Password logins are off" "$test_dir/ineffective.output" ||
|
||||
fail "SSH setup must not claim ineffective hardening succeeded"
|
||||
pass "SSH setup verifies the effective daemon settings"
|
||||
|
||||
if SSHD_SYNTAX_VALID=0 run_setup invalid >"$test_dir/invalid.output" 2>&1; then
|
||||
fail "SSH setup must fail when sshd rejects its config"
|
||||
fi
|
||||
[[ ! -e $test_dir/invalid/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH setup removes a rejected hardening config"
|
||||
! grep -qF "systemctl reload sshd.service" "$test_dir/invalid.calls" ||
|
||||
fail "SSH setup must not reload a rejected config"
|
||||
! grep -q "Password logins are off" "$test_dir/invalid.output" ||
|
||||
fail "SSH setup must not claim rejected hardening succeeded"
|
||||
pass "SSH setup fails safely when sshd rejects the config"
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
|
||||
migration="$ROOT/migrations/1788124236.sh"
|
||||
stub_bin="$test_dir/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
cat >"$stub_bin/systemctl" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'systemctl %s\n' "$*" >>"${CALL_LOG:?}"
|
||||
case "$1 $2" in
|
||||
"is-enabled --quiet") [[ ${SSHD_ENABLED:-0} == 1 ]] ;;
|
||||
"is-active --quiet") [[ ${SSHD_ACTIVE:-0} == 1 ]] ;;
|
||||
"reload sshd.service") [[ ${SSHD_RELOAD_VALID:-1} == 1 ]] ;;
|
||||
"disable --now") ;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
STUB
|
||||
|
||||
cat >"$stub_bin/sshd" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'sshd %s\n' "$*" >>"${CALL_LOG:?}"
|
||||
case $1 in
|
||||
-t) [[ ${SSHD_SYNTAX_VALID:-1} == 1 ]] ;;
|
||||
-T)
|
||||
printf 'PasswordAuthentication %s\n' "${SSHD_PASSWORD_AUTH:-no}"
|
||||
printf 'KbdInteractiveAuthentication %s\n' "${SSHD_KBD_AUTH:-no}"
|
||||
;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
STUB
|
||||
|
||||
cat >"$stub_bin/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'sudo %s\n' "$*" >>"${CALL_LOG:?}"
|
||||
if [[ ${SUDO_ALLOWED:-1} != 1 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exec "$@"
|
||||
STUB
|
||||
|
||||
chmod +x "$stub_bin"/*
|
||||
|
||||
ssh-keygen -q -t ed25519 -N "" -f "$test_dir/key"
|
||||
public_key=$(<"$test_dir/key.pub")
|
||||
|
||||
run_migration() {
|
||||
local scenario=$1
|
||||
local home="$test_dir/$scenario/home"
|
||||
local root="$test_dir/$scenario/root"
|
||||
local config="$root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf"
|
||||
|
||||
mkdir -p "$home/.ssh" "${config%/*}"
|
||||
chmod "${HOME_MODE:-755}" "$home"
|
||||
: >"$test_dir/$scenario.calls"
|
||||
case "${AUTHORIZED_KEY_STATE:-valid}" in
|
||||
valid) printf '%s\n' "$public_key" >"$home/.ssh/authorized_keys" ;;
|
||||
invalid) printf 'not a public key\n' >"$home/.ssh/authorized_keys" ;;
|
||||
private) cat "$test_dir/key" >"$home/.ssh/authorized_keys" ;;
|
||||
symlink)
|
||||
printf '%s\n' "$public_key" >"$home/.ssh/imported_key"
|
||||
ln -s imported_key "$home/.ssh/authorized_keys"
|
||||
;;
|
||||
unreadable)
|
||||
printf '%s\n' "$public_key" >"$home/.ssh/authorized_keys"
|
||||
chmod 000 "$home/.ssh/authorized_keys"
|
||||
;;
|
||||
esac
|
||||
if [[ ${LOOSE_SSH_PERMS:-0} == 1 ]]; then
|
||||
chmod 755 "$home/.ssh"
|
||||
chmod 644 "$home/.ssh/authorized_keys"
|
||||
fi
|
||||
if [[ ${ALREADY_HARDENED:-0} == 1 ]]; then
|
||||
printf 'PasswordAuthentication no\n' >"$config"
|
||||
fi
|
||||
|
||||
# Keep the privileged production destination fixed in the shipped migration.
|
||||
# For this isolated test only, rewrite that one assignment in the input fed to
|
||||
# bash so no scenario can touch the host's /etc.
|
||||
sed "s|^config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf$|config=$config|" "$migration" |
|
||||
HOME="$home" CALL_LOG="$test_dir/$scenario.calls" PATH="$stub_bin:$PATH" \
|
||||
SSHD_ENABLED="${SSHD_ENABLED:-0}" SSHD_ACTIVE="${SSHD_ACTIVE:-0}" \
|
||||
SSHD_SYNTAX_VALID="${SSHD_SYNTAX_VALID:-1}" \
|
||||
SSHD_PASSWORD_AUTH="${SSHD_PASSWORD_AUTH:-no}" \
|
||||
SSHD_KBD_AUTH="${SSHD_KBD_AUTH:-no}" \
|
||||
SSHD_RELOAD_VALID="${SSHD_RELOAD_VALID:-1}" \
|
||||
SUDO_ALLOWED="${SUDO_ALLOWED:-1}" \
|
||||
bash -euo pipefail
|
||||
}
|
||||
|
||||
sshd_disabled() {
|
||||
grep -qxF "sudo systemctl disable --now sshd.service" "$test_dir/$1.calls"
|
||||
}
|
||||
|
||||
SSHD_ENABLED=0 SSHD_ACTIVE=0 run_migration disabled
|
||||
[[ ! -e $test_dir/disabled/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration leaves a disabled daemon alone"
|
||||
! grep -q '^sudo ' "$test_dir/disabled.calls" || fail "disabled SSH does not prompt for privileges"
|
||||
pass "SSH migration no-ops when sshd is not enabled or active"
|
||||
|
||||
ALREADY_HARDENED=1 SSHD_ENABLED=1 SSHD_ACTIVE=1 run_migration hardened >/dev/null
|
||||
[[ ! -s $test_dir/hardened.calls ]] || fail "an already-hardened machine must not touch sshd or prompt"
|
||||
grep -qxF "PasswordAuthentication no" "$test_dir/hardened/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf" ||
|
||||
fail "the existing hardening config is left alone"
|
||||
pass "SSH migration no-ops when the hardening config already exists"
|
||||
|
||||
# Without a usable key, sshd only accepts password logins — the hole the old
|
||||
# setup command could leave open. The migration closes it by disabling sshd.
|
||||
AUTHORIZED_KEY_STATE=missing SSHD_ENABLED=1 run_migration no-key >/dev/null
|
||||
[[ ! -e $test_dir/no-key/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration must not write the hardening config without an authorized key"
|
||||
sshd_disabled no-key || fail "SSH migration disables a password-only sshd"
|
||||
pass "SSH migration disables sshd when no key is authorized"
|
||||
|
||||
AUTHORIZED_KEY_STATE=invalid SSHD_ENABLED=1 run_migration invalid-key >/dev/null
|
||||
sshd_disabled invalid-key || fail "a malformed authorized_keys leaves sshd password-only"
|
||||
pass "SSH migration disables sshd when authorized_keys holds no valid key"
|
||||
|
||||
# ssh-keygen -lf accepts a whole private-key file, so only a per-line check
|
||||
# catches the classic `cp id_ed25519 authorized_keys` slip that sshd cannot use.
|
||||
AUTHORIZED_KEY_STATE=private SSHD_ENABLED=1 run_migration private-key >/dev/null
|
||||
[[ ! -e $test_dir/private-key/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration must not treat a private key as an authorized key"
|
||||
sshd_disabled private-key || fail "a private-key authorized_keys leaves sshd password-only"
|
||||
pass "SSH migration disables sshd when authorized_keys holds a private key"
|
||||
|
||||
# A dotfiles-managed symlink with a working key is a key-based setup, not a
|
||||
# keyless one; it must be hardened, never disabled.
|
||||
AUTHORIZED_KEY_STATE=symlink SSHD_ENABLED=1 SSHD_ACTIVE=1 run_migration symlink-key >/dev/null
|
||||
grep -qxF "PasswordAuthentication no" "$test_dir/symlink-key/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf" ||
|
||||
fail "SSH migration hardens a symlinked authorized_keys with a valid key"
|
||||
! sshd_disabled symlink-key || fail "SSH migration must not disable sshd when the symlinked key is usable"
|
||||
pass "SSH migration follows an authorized_keys symlink to its key"
|
||||
|
||||
# An unreadable file answers neither "keyless" nor "key-based": touch nothing.
|
||||
if (( EUID != 0 )); then
|
||||
AUTHORIZED_KEY_STATE=unreadable SSHD_ENABLED=1 run_migration unreadable >/dev/null
|
||||
[[ ! -e $test_dir/unreadable/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration must not harden against an unverifiable authorized_keys"
|
||||
! grep -q '^sudo ' "$test_dir/unreadable.calls" || fail "an unreadable authorized_keys does not prompt or disable"
|
||||
pass "SSH migration leaves an unreadable authorized_keys alone"
|
||||
fi
|
||||
|
||||
# StrictModes makes sshd ignore authorized_keys under a group-writable home,
|
||||
# so the key that validated would be unusable and passwords the only way in.
|
||||
HOME_MODE=775 SSHD_ENABLED=1 run_migration loose-home >/dev/null
|
||||
[[ ! -e $test_dir/loose-home/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration must not disable passwords when sshd would ignore the key"
|
||||
! grep -q '^sudo ' "$test_dir/loose-home.calls" || fail "a group-writable home does not prompt for privileges"
|
||||
pass "SSH migration leaves a group-writable home directory alone"
|
||||
|
||||
LOOSE_SSH_PERMS=1 SSHD_ENABLED=1 SSHD_ACTIVE=1 run_migration active >/dev/null
|
||||
config="$test_dir/active/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf"
|
||||
grep -qxF "PasswordAuthentication no" "$config" || fail "SSH migration disables password authentication"
|
||||
grep -qxF "KbdInteractiveAuthentication no" "$config" || fail "SSH migration disables keyboard-interactive authentication"
|
||||
[[ $(stat -c '%a' "$test_dir/active/home/.ssh") == "700" ]] ||
|
||||
fail "SSH migration tightens ~/.ssh so StrictModes accepts the key"
|
||||
[[ $(stat -c '%a' "$test_dir/active/home/.ssh/authorized_keys") == "600" ]] ||
|
||||
fail "SSH migration tightens authorized_keys so StrictModes accepts the key"
|
||||
grep -qxF "sudo sshd -t" "$test_dir/active.calls" || fail "SSH migration validates sshd syntax"
|
||||
grep -qxF "sudo sshd -T" "$test_dir/active.calls" || fail "SSH migration validates effective sshd settings"
|
||||
grep -qxF "sudo systemctl reload sshd.service" "$test_dir/active.calls" || fail "SSH migration reloads an active daemon"
|
||||
pass "SSH migration hardens and reloads an existing key-based SSH setup"
|
||||
|
||||
SSHD_ENABLED=1 SSHD_ACTIVE=0 run_migration stopped >/dev/null
|
||||
[[ -e $test_dir/stopped/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration hardens an enabled but stopped daemon"
|
||||
! grep -qF 'reload sshd.service' "$test_dir/stopped.calls" || fail "SSH migration must not start or reload a stopped daemon"
|
||||
pass "SSH migration hardens an enabled daemon without starting it"
|
||||
|
||||
# Conditions the migration cannot repair complete with a notice — leaving the
|
||||
# machine as it was — so they never block the migrations queued behind this one.
|
||||
SSHD_ENABLED=1 SSHD_ACTIVE=1 SSHD_PASSWORD_AUTH=yes run_migration ineffective >"$test_dir/ineffective.output" 2>&1 ||
|
||||
fail "an ineffective drop-in must complete without blocking later migrations"
|
||||
[[ ! -e $test_dir/ineffective/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration removes an ineffective config"
|
||||
! grep -qF 'reload sshd.service' "$test_dir/ineffective.calls" || fail "SSH migration must not reload ineffective hardening"
|
||||
pass "SSH migration backs off when another rule keeps password authentication enabled"
|
||||
|
||||
SSHD_ENABLED=1 SSHD_ACTIVE=1 SSHD_SYNTAX_VALID=0 run_migration invalid-config >"$test_dir/invalid-config.output" 2>&1 ||
|
||||
fail "a rejected config must complete without blocking later migrations"
|
||||
[[ ! -e $test_dir/invalid-config/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration removes a rejected config"
|
||||
! grep -qF 'reload sshd.service' "$test_dir/invalid-config.calls" || fail "SSH migration must not reload rejected hardening"
|
||||
pass "SSH migration backs off when sshd rejects the config"
|
||||
|
||||
# The installed config is valid, so a failed reload only delays it until the
|
||||
# next sshd restart; keep it staged rather than failing or removing it.
|
||||
SSHD_ENABLED=1 SSHD_ACTIVE=1 SSHD_RELOAD_VALID=0 run_migration reload-fail >"$test_dir/reload-fail.output" 2>&1 ||
|
||||
fail "a failed reload must complete without blocking later migrations"
|
||||
[[ -e $test_dir/reload-fail/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "a failed reload keeps the valid hardening config staged"
|
||||
pass "SSH migration keeps the hardening staged when sshd cannot reload"
|
||||
|
||||
# Privileges are the one genuinely retryable failure: stay pending so the
|
||||
# login notifier prompts for a terminal run.
|
||||
if SUDO_ALLOWED=0 SSHD_ENABLED=1 SSHD_ACTIVE=1 run_migration no-sudo >"$test_dir/no-sudo.output" 2>&1; then
|
||||
fail "SSH migration must stay pending when privileges are unavailable"
|
||||
fi
|
||||
[[ ! -e $test_dir/no-sudo/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "no hardening config is left behind without privileges"
|
||||
pass "SSH migration stays pending until privileges are granted"
|
||||
|
||||
if SUDO_ALLOWED=0 AUTHORIZED_KEY_STATE=missing SSHD_ENABLED=1 run_migration no-sudo-keyless >"$test_dir/no-sudo-keyless.output" 2>&1; then
|
||||
fail "SSH migration must stay pending when it cannot disable a password-only sshd"
|
||||
fi
|
||||
pass "SSH migration stays pending when disabling sshd needs privileges"
|
||||
@@ -1,10 +1,7 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The docker group is root-equivalent, so no automatic path may grant it. These
|
||||
# tests guard the paths that are not exercised by a fresh-install run: first-boot
|
||||
# provisioning replaying a recorded (or factory-snapshot) group list, and the
|
||||
# Quattro upgrade. Opting in stays a deliberate, warned step
|
||||
# (omarchy-setup-security-sudoless-docker).
|
||||
# Docker is root-equivalent, so no automatic path may grant it. Raw input access
|
||||
# is likewise excluded unless a feature that explicitly needs it is installed.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -13,11 +10,15 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
# First-boot provisioning must never grant docker even when it is recorded (an
|
||||
# older install, or a factory snapshot predating the opt-in default).
|
||||
# First-boot provisioning must not replay old privileged defaults.
|
||||
mkdir -p "$TMPDIR/bin"
|
||||
printf '#!/bin/bash\nexit 0\n' >"$TMPDIR/bin/getent" # every group "exists"
|
||||
chmod +x "$TMPDIR/bin/getent"
|
||||
cat >"$TMPDIR/bin/pacman" <<'STUB'
|
||||
#!/bin/bash
|
||||
[[ $1 == "-Qq" ]] || exit 2
|
||||
[[ " ${STUB_PACKAGES:-} " == *" $2 "* ]]
|
||||
STUB
|
||||
chmod +x "$TMPDIR/bin/getent" "$TMPDIR/bin/pacman"
|
||||
export PATH="$TMPDIR/bin:$PATH"
|
||||
|
||||
PROVISIONING_DIR="$TMPDIR/prov"
|
||||
@@ -29,9 +30,15 @@ eval "$(sed -n '/^user_groups() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner")"
|
||||
groups=$(user_groups)
|
||||
|
||||
[[ ",$groups," == *",wheel,"* ]] || fail "user_groups always includes wheel"
|
||||
[[ ",$groups," == *",input,"* ]] || fail "user_groups includes recorded non-docker groups"
|
||||
[[ ",$groups," != *",input,"* ]] || fail "user_groups must not replay the blanket input grant"
|
||||
[[ ",$groups," == *",docker,"* ]] && fail "user_groups must never grant the docker group"
|
||||
pass "first-boot user_groups includes recorded groups but never docker"
|
||||
pass "first-boot user_groups replays neither privileged default"
|
||||
|
||||
groups=$(STUB_PACKAGES=xpadneo-dkms user_groups)
|
||||
[[ ",$groups," == *",input,"* ]] || fail "user_groups keeps input for installed controller support"
|
||||
groups=$(STUB_PACKAGES=ydotool user_groups)
|
||||
[[ ",$groups," == *",input,"* ]] || fail "user_groups keeps input for installed ydotool support"
|
||||
pass "first-boot user_groups keeps deliberate input-group opt-ins"
|
||||
|
||||
# The Quattro upgrade must not re-add the user to docker.
|
||||
if rg -q 'usermod -aG docker' "$ROOT/bin/omarchy-upgrade-to-quattro"; then
|
||||
|
||||
@@ -71,8 +71,15 @@ done
|
||||
pass "a URL naming a transport git does not implement never reaches git"
|
||||
|
||||
# The checker is a separate command, so its absence has to refuse the URL rather
|
||||
# than wave it through to git.
|
||||
if install_theme "https://github.com/example/omarchy-cool-theme.git" "$mock_bin:$PATH"; then
|
||||
# than wave it through to git. Installed machines carry the packaged checker in
|
||||
# /usr/bin, so absence is simulated by shadowing it with a stub that reports
|
||||
# command-not-found instead of thinning the PATH.
|
||||
missing_checker_bin="$test_tmp/missing-checker-bin"
|
||||
mkdir -p "$missing_checker_bin"
|
||||
printf '#!/bin/bash\nexit 127\n' >"$missing_checker_bin/omarchy-git-url-check"
|
||||
chmod +x "$missing_checker_bin/omarchy-git-url-check"
|
||||
|
||||
if install_theme "https://github.com/example/omarchy-cool-theme.git" "$missing_checker_bin:$mock_bin:$ROOT/bin:$PATH"; then
|
||||
fail "omarchy-theme-install refuses a URL it cannot check"
|
||||
fi
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ fi
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
# The checkout may live under /home, which the tmpfs below hides, so take a
|
||||
# mount-safe copy of the helper before the mounts land.
|
||||
cp "$ROOT/bin/omarchy-windows-vm" "$test_tmp/omarchy-windows-vm"
|
||||
|
||||
# Hide host state before creating the production paths used by the root helper.
|
||||
mount -t tmpfs -o mode=0755,size=8m run-test /run
|
||||
mkdir -p /run/lock
|
||||
@@ -27,7 +31,7 @@ mount -t tmpfs -o uid=0,gid=0,mode=0710,size=1g home-alice /home/alice
|
||||
export HOME=/home/alice
|
||||
unset OMARCHY_WINDOWS_DIR
|
||||
set -- help
|
||||
source "$ROOT/bin/omarchy-windows-vm" >/dev/null 2>&1
|
||||
source "$test_tmp/omarchy-windows-vm" >/dev/null 2>&1
|
||||
|
||||
# The namespace maps the host filesystem's uid 0 to nobody. Only / remains on
|
||||
# that filesystem; all paths the helper mutates are isolated tmpfs mounts.
|
||||
|
||||
Reference in New Issue
Block a user