<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://tech.uvoo.io/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Busk</id>
	<title>UVOO Tech Wiki - User contributions
 [en]</title>
	<link rel="self" type="application/atom+xml" href="https://tech.uvoo.io/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Busk"/>
	<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php/Special:Contributions/Busk"/>
	<updated>2026-09-30T11:15:35Z</updated>
	<subtitle>User contributions
</subtitle>
	<generator>MediaWiki 1.35.2</generator>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Skills_urls&amp;diff=5754</id>
		<title>Skills urls</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Skills_urls&amp;diff=5754"/>
		<updated>2026-09-28T03:25:16Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;https://github.com/github/awesome-copilot/blob/main/agents/oracle-to-postgres-migration-expert.agent.md?plain=1  https://github.com/github/awesome-copilot/blob/main/plugins/or...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;https://github.com/github/awesome-copilot/blob/main/agents/oracle-to-postgres-migration-expert.agent.md?plain=1&lt;br /&gt;
&lt;br /&gt;
https://github.com/github/awesome-copilot/blob/main/plugins/oracle-to-postgres-migration-expert/README.md&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=2fa_ssh&amp;diff=5753</id>
		<title>2fa ssh</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=2fa_ssh&amp;diff=5753"/>
		<updated>2026-09-16T12:41:41Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;# Using 2FA&lt;br /&gt;
&lt;br /&gt;
Require OTP via Google Authenticator App&lt;br /&gt;
&lt;br /&gt;
publickey,keyboard-interactive (and - requires both)&lt;br /&gt;
&lt;br /&gt;
publickey keyboard-interactive (or - requires one or the other)&lt;br /&gt;
&lt;br /&gt;
## Ubuntu 26.04&lt;br /&gt;
&lt;br /&gt;
Prep user&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
adduser myuser --disabled-password&lt;br /&gt;
# usermod -aG sudo myuser&lt;br /&gt;
visudo  # myuser   ALL=(ALL:ALL) NOPASSWD: ALL&lt;br /&gt;
sudo su - myuser&lt;br /&gt;
mkdir .ssh&lt;br /&gt;
nano .ssh/authorized_keys  # paste public ssh key&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
Update SSHD to require both SSH ID and then OTP&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
#!/usr/bin/env bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y libpam-google-authenticator&lt;br /&gt;
&lt;br /&gt;
# Run as the login user, NOT root/sudo — generates ~/.google_authenticator&lt;br /&gt;
google-authenticator&lt;br /&gt;
&lt;br /&gt;
# Insert BEFORE @include common-auth if you want pubkey+OTP only, no password.&lt;br /&gt;
# As a quick check, comment out the password-based auth line so PAM only asks for the OTP:&lt;br /&gt;
sudo sed -i 's/^@include common-auth/#&amp;amp;/' /etc/pam.d/sshd&lt;br /&gt;
sudo sed -i '1 i\auth required pam_google_authenticator.so' /etc/pam.d/sshd&lt;br /&gt;
&lt;br /&gt;
sudo tee /etc/ssh/sshd_config.d/60-mfa.conf &amp;gt; /dev/null &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
KbdInteractiveAuthentication yes&lt;br /&gt;
PasswordAuthentication no&lt;br /&gt;
AuthenticationMethods publickey,keyboard-interactive&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
sudo systemctl restart ssh&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# KeyCloak realm&lt;br /&gt;
&lt;br /&gt;
setup-authd-keycloak.sh&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
#!/usr/bin/env bash&lt;br /&gt;
# setup-authd-keycloak.sh&lt;br /&gt;
#&lt;br /&gt;
# Idempotent Ubuntu 26.04 Server bootstrap for Canonical authd + generic OIDC&lt;br /&gt;
# broker (authd-oidc), intended for Keycloak device authorization.&lt;br /&gt;
#&lt;br /&gt;
# Default SSH policy for OIDC users:&lt;br /&gt;
#   - SSH password authentication: disabled&lt;br /&gt;
#   - SSH public-key authentication: disabled for the OIDC suffix&lt;br /&gt;
#   - PAM keyboard-interactive/authd: required&lt;br /&gt;
#&lt;br /&gt;
# This ensures OIDC users cannot bypass authd/Keycloak with an SSH key.&lt;br /&gt;
#&lt;br /&gt;
# IMPORTANT:&lt;br /&gt;
# Keycloak OTP/WebAuthn policy is configured in Keycloak, not by this script.&lt;br /&gt;
# Configure the Keycloak client/authentication flow to require the desired MFA.&lt;br /&gt;
&lt;br /&gt;
set -Eeuo pipefail&lt;br /&gt;
IFS=$'\n\t'&lt;br /&gt;
&lt;br /&gt;
PROGRAM=&amp;quot;${0##*/}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
ISSUER=&amp;quot;&amp;quot;&lt;br /&gt;
CLIENT_ID=&amp;quot;&amp;quot;&lt;br /&gt;
SSH_SUFFIX=&amp;quot;&amp;quot;&lt;br /&gt;
ALLOWED_USERS=&amp;quot;&amp;quot;&lt;br /&gt;
CLIENT_SECRET=&amp;quot;&amp;quot;&lt;br /&gt;
EXTRA_SCOPES=&amp;quot;offline_access&amp;quot;&lt;br /&gt;
FORCE_PROVIDER_AUTHENTICATION=&amp;quot;true&amp;quot;&lt;br /&gt;
HOME_BASE_DIR=&amp;quot;/home&amp;quot;&lt;br /&gt;
EXTRA_GROUPS=&amp;quot;&amp;quot;&lt;br /&gt;
SSH_POLICY=&amp;quot;authd-only&amp;quot;&lt;br /&gt;
BROKER_CHANNEL=&amp;quot;0.x/stable&amp;quot;&lt;br /&gt;
SKIP_OIDC_CHECK=&amp;quot;false&amp;quot;&lt;br /&gt;
&lt;br /&gt;
BROKER_SNAP=&amp;quot;authd-oidc&amp;quot;&lt;br /&gt;
BROKER_CONF=&amp;quot;&amp;quot;&lt;br /&gt;
BROKER_DATA_DIR=&amp;quot;&amp;quot;&lt;br /&gt;
BROKER_DISCOVERY_SRC=&amp;quot;/snap/authd-oidc/current/conf/authd/oidc.conf&amp;quot;&lt;br /&gt;
BROKER_DISCOVERY_DST=&amp;quot;/etc/authd/brokers.d/oidc.conf&amp;quot;&lt;br /&gt;
SSHD_DROPIN=&amp;quot;/etc/ssh/sshd_config.d/00-authd-oidc.conf&amp;quot;&lt;br /&gt;
&lt;br /&gt;
usage() {&lt;br /&gt;
    cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
Usage:&lt;br /&gt;
  sudo ./${PROGRAM} \\&lt;br /&gt;
    --issuer URL \\&lt;br /&gt;
    --client-id ID \\&lt;br /&gt;
    --ssh-suffix @DOMAIN \\&lt;br /&gt;
    --allowed-users USERS&lt;br /&gt;
&lt;br /&gt;
Required:&lt;br /&gt;
  --issuer URL&lt;br /&gt;
      OIDC issuer / Keycloak realm URL.&lt;br /&gt;
      Example: https://auth.example.com/realms/linux&lt;br /&gt;
&lt;br /&gt;
  --client-id ID&lt;br /&gt;
      Keycloak OIDC client ID.&lt;br /&gt;
      Example: ubuntu-authd&lt;br /&gt;
&lt;br /&gt;
  --ssh-suffix @DOMAIN&lt;br /&gt;
      Username suffix allowed to perform first SSH authentication.&lt;br /&gt;
      Exactly one suffix is supported by this script.&lt;br /&gt;
      Example: @example.com&lt;br /&gt;
&lt;br /&gt;
  --allowed-users USERS&lt;br /&gt;
      authd broker allowed_users value.&lt;br /&gt;
      Examples:&lt;br /&gt;
        ALL&lt;br /&gt;
        OWNER&lt;br /&gt;
        user1@example.com,user2@example.com&lt;br /&gt;
&lt;br /&gt;
Optional:&lt;br /&gt;
  --client-secret SECRET&lt;br /&gt;
      OIDC client secret, only when your Keycloak client requires one.&lt;br /&gt;
      Prefer a public client for device authorization when appropriate.&lt;br /&gt;
      WARNING: a secret supplied on the command line may be visible in shell&lt;br /&gt;
      history or process listings.&lt;br /&gt;
&lt;br /&gt;
  --extra-scopes SCOPES&lt;br /&gt;
      Comma-separated additional OIDC scopes.&lt;br /&gt;
      Default: offline_access&lt;br /&gt;
&lt;br /&gt;
  --force-provider-authentication true|false&lt;br /&gt;
      Require provider/token refresh validation even for authd local-password&lt;br /&gt;
      authentication.&lt;br /&gt;
      Default: true&lt;br /&gt;
&lt;br /&gt;
  --home-base-dir DIR&lt;br /&gt;
      Base directory for authd-created home directories.&lt;br /&gt;
      Default: /home&lt;br /&gt;
&lt;br /&gt;
  --extra-groups GROUPS&lt;br /&gt;
      Optional comma-separated local groups to assign to authd users.&lt;br /&gt;
      Example: sudo&lt;br /&gt;
      Default: unset&lt;br /&gt;
&lt;br /&gt;
  --ssh-policy POLICY&lt;br /&gt;
      authd-only       Require authd/Keycloak for OIDC users. This is the&lt;br /&gt;
                       recommended default for JIT-provisioned SSH users.&lt;br /&gt;
      publickey+authd  Require BOTH SSH public key AND authd/Keycloak.&lt;br /&gt;
                       The account/key must already be resolvable before SSH&lt;br /&gt;
                       reaches authd, so this is normally unsuitable for the&lt;br /&gt;
                       very first JIT login.&lt;br /&gt;
      Default: authd-only&lt;br /&gt;
&lt;br /&gt;
  --broker-channel CHANNEL&lt;br /&gt;
      Snap channel used when authd-oidc is first installed.&lt;br /&gt;
      Existing installations are not automatically switched.&lt;br /&gt;
      Default: 0.x/stable&lt;br /&gt;
&lt;br /&gt;
  --skip-oidc-check&lt;br /&gt;
      Do not validate the issuer discovery document before changing config.&lt;br /&gt;
&lt;br /&gt;
  -h, --help&lt;br /&gt;
      Show this help.&lt;br /&gt;
&lt;br /&gt;
Example:&lt;br /&gt;
  sudo ./${PROGRAM} \\&lt;br /&gt;
    --issuer https://auth.uvoo.io/realms/test \\&lt;br /&gt;
    --client-id test \\&lt;br /&gt;
    --ssh-suffix @uvoo.io \\&lt;br /&gt;
    --allowed-users ALL&lt;br /&gt;
&lt;br /&gt;
More restrictive production example:&lt;br /&gt;
  sudo ./${PROGRAM} \\&lt;br /&gt;
    --issuer https://auth.uvoo.io/realms/test \\&lt;br /&gt;
    --client-id test \\&lt;br /&gt;
    --ssh-suffix @uvoo.io \\&lt;br /&gt;
    --allowed-users 'test1@uvoo.io,admin1@uvoo.io'&lt;br /&gt;
EOF&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
die() {&lt;br /&gt;
    printf 'ERROR: %s\n' &amp;quot;$*&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
    exit 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
log() {&lt;br /&gt;
    printf '==&amp;gt; %s\n' &amp;quot;$*&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
on_error() {&lt;br /&gt;
    local rc=$?&lt;br /&gt;
    printf 'ERROR: command failed at line %s (exit %s)\n' &amp;quot;${BASH_LINENO[0]}&amp;quot; &amp;quot;$rc&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
    exit &amp;quot;$rc&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
trap on_error ERR&lt;br /&gt;
&lt;br /&gt;
require_value() {&lt;br /&gt;
    local opt=&amp;quot;$1&amp;quot;&lt;br /&gt;
    local value=&amp;quot;${2-}&amp;quot;&lt;br /&gt;
    [[ -n &amp;quot;$value&amp;quot; ]] || die &amp;quot;${opt} requires a value&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
while (($#)); do&lt;br /&gt;
    case &amp;quot;$1&amp;quot; in&lt;br /&gt;
        --issuer)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            ISSUER=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --client-id)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            CLIENT_ID=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --ssh-suffix)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            SSH_SUFFIX=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --allowed-users)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            ALLOWED_USERS=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --client-secret)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            CLIENT_SECRET=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --extra-scopes)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            EXTRA_SCOPES=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --force-provider-authentication)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            FORCE_PROVIDER_AUTHENTICATION=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --home-base-dir)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            HOME_BASE_DIR=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --extra-groups)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            EXTRA_GROUPS=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --ssh-policy)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            SSH_POLICY=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --broker-channel)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            BROKER_CHANNEL=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --skip-oidc-check)&lt;br /&gt;
            SKIP_OIDC_CHECK=&amp;quot;true&amp;quot;&lt;br /&gt;
            shift&lt;br /&gt;
            ;;&lt;br /&gt;
        -h|--help)&lt;br /&gt;
            usage&lt;br /&gt;
            exit 0&lt;br /&gt;
            ;;&lt;br /&gt;
        *)&lt;br /&gt;
            die &amp;quot;unknown argument: $1 (use --help)&amp;quot;&lt;br /&gt;
            ;;&lt;br /&gt;
    esac&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
[[ -n &amp;quot;$ISSUER&amp;quot; ]]       || die &amp;quot;--issuer is required&amp;quot;&lt;br /&gt;
[[ -n &amp;quot;$CLIENT_ID&amp;quot; ]]    || die &amp;quot;--client-id is required&amp;quot;&lt;br /&gt;
[[ -n &amp;quot;$SSH_SUFFIX&amp;quot; ]]   || die &amp;quot;--ssh-suffix is required&amp;quot;&lt;br /&gt;
[[ -n &amp;quot;$ALLOWED_USERS&amp;quot; ]] || die &amp;quot;--allowed-users is required&amp;quot;&lt;br /&gt;
&lt;br /&gt;
[[ &amp;quot;$ISSUER&amp;quot; == https://* ]] || die &amp;quot;--issuer must use https://&amp;quot;&lt;br /&gt;
ISSUER=&amp;quot;${ISSUER%/}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
[[ &amp;quot;$SSH_SUFFIX&amp;quot; == @* ]] || die &amp;quot;--ssh-suffix must begin with '@'&amp;quot;&lt;br /&gt;
[[ &amp;quot;$SSH_SUFFIX&amp;quot; != *&amp;quot;,&amp;quot;* ]] || die &amp;quot;--ssh-suffix accepts exactly one suffix&amp;quot;&lt;br /&gt;
[[ &amp;quot;$SSH_SUFFIX&amp;quot; != *[[:space:]]* ]] || die &amp;quot;--ssh-suffix must not contain whitespace&amp;quot;&lt;br /&gt;
&lt;br /&gt;
case &amp;quot;$FORCE_PROVIDER_AUTHENTICATION&amp;quot; in&lt;br /&gt;
    true|false) ;;&lt;br /&gt;
    *) die &amp;quot;--force-provider-authentication must be true or false&amp;quot; ;;&lt;br /&gt;
esac&lt;br /&gt;
&lt;br /&gt;
case &amp;quot;$SSH_POLICY&amp;quot; in&lt;br /&gt;
    authd-only|publickey+authd) ;;&lt;br /&gt;
    *) die &amp;quot;--ssh-policy must be authd-only or publickey+authd&amp;quot; ;;&lt;br /&gt;
esac&lt;br /&gt;
&lt;br /&gt;
for value_name in ISSUER CLIENT_ID SSH_SUFFIX ALLOWED_USERS CLIENT_SECRET EXTRA_SCOPES HOME_BASE_DIR EXTRA_GROUPS; do&lt;br /&gt;
    value=&amp;quot;${!value_name}&amp;quot;&lt;br /&gt;
    [[ &amp;quot;$value&amp;quot; != *$'\n'* &amp;amp;&amp;amp; &amp;quot;$value&amp;quot; != *$'\r'* ]] \&lt;br /&gt;
        || die &amp;quot;${value_name} must not contain newlines&amp;quot;&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
if [[ $EUID -ne 0 ]]; then&lt;br /&gt;
    die &amp;quot;run this script as root, e.g. sudo ./${PROGRAM} ...&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
[[ -r /etc/os-release ]] || die &amp;quot;cannot read /etc/os-release&amp;quot;&lt;br /&gt;
# shellcheck disable=SC1091&lt;br /&gt;
source /etc/os-release&lt;br /&gt;
[[ &amp;quot;${ID:-}&amp;quot; == &amp;quot;ubuntu&amp;quot; ]] || die &amp;quot;this script supports Ubuntu only&amp;quot;&lt;br /&gt;
[[ &amp;quot;${VERSION_ID:-}&amp;quot; == &amp;quot;26.04&amp;quot; ]] \&lt;br /&gt;
    || die &amp;quot;this script is intentionally limited to Ubuntu 26.04; found ${VERSION_ID:-unknown}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
export DEBIAN_FRONTEND=noninteractive&lt;br /&gt;
&lt;br /&gt;
install_packages() {&lt;br /&gt;
    local required=(&lt;br /&gt;
    authd&lt;br /&gt;
    openssh-server&lt;br /&gt;
    snapd&lt;br /&gt;
    curl&lt;br /&gt;
    ca-certificates&lt;br /&gt;
    python3&lt;br /&gt;
    cracklib-runtime&lt;br /&gt;
    wamerican&lt;br /&gt;
)&lt;br /&gt;
    local missing=()&lt;br /&gt;
    local pkg&lt;br /&gt;
&lt;br /&gt;
    for pkg in &amp;quot;${required[@]}&amp;quot;; do&lt;br /&gt;
        if ! dpkg-query -W -f='${Status}' &amp;quot;$pkg&amp;quot; 2&amp;gt;/dev/null | grep -qx 'install ok installed'; then&lt;br /&gt;
            missing+=(&amp;quot;$pkg&amp;quot;)&lt;br /&gt;
        fi&lt;br /&gt;
    done&lt;br /&gt;
&lt;br /&gt;
    if ((${#missing[@]})); then&lt;br /&gt;
        log &amp;quot;Installing required packages: ${missing[*]}&amp;quot;&lt;br /&gt;
        apt-get update&lt;br /&gt;
        apt-get install -y --no-install-recommends &amp;quot;${missing[@]}&amp;quot;&lt;br /&gt;
    else&lt;br /&gt;
        log &amp;quot;Required Debian packages already installed&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    systemctl enable --now snapd.socket &amp;gt;/dev/null&lt;br /&gt;
    # authd may be socket/D-Bus activated depending on package version; start it&lt;br /&gt;
    # without assuming the unit has an [Install] section.&lt;br /&gt;
    systemctl start authd&lt;br /&gt;
    systemctl enable --now ssh &amp;gt;/dev/null&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
install_broker() {&lt;br /&gt;
    if snap list &amp;quot;$BROKER_SNAP&amp;quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
        log &amp;quot;$BROKER_SNAP is already installed; leaving its current snap channel/revision unchanged&amp;quot;&lt;br /&gt;
        snap list &amp;quot;$BROKER_SNAP&amp;quot;&lt;br /&gt;
    else&lt;br /&gt;
        log &amp;quot;Installing $BROKER_SNAP from channel $BROKER_CHANNEL&amp;quot;&lt;br /&gt;
        snap install &amp;quot;$BROKER_SNAP&amp;quot; --channel=&amp;quot;$BROKER_CHANNEL&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    # Resolve the active snap revision explicitly. The service wrapper uses the&lt;br /&gt;
    # revision-specific SNAP_DATA path (for example /var/snap/authd-oidc/89).&lt;br /&gt;
    local revision&lt;br /&gt;
    revision=&amp;quot;$(snap list &amp;quot;$BROKER_SNAP&amp;quot; | awk 'NR==2 {print $3}')&amp;quot;&lt;br /&gt;
    [[ &amp;quot;$revision&amp;quot; =~ ^[0-9]+$ ]] || die &amp;quot;could not determine active $BROKER_SNAP revision&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    BROKER_DATA_DIR=&amp;quot;/var/snap/${BROKER_SNAP}/${revision}&amp;quot;&lt;br /&gt;
    BROKER_CONF=&amp;quot;${BROKER_DATA_DIR}/broker.conf&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    [[ -f &amp;quot;$BROKER_CONF&amp;quot; ]] \&lt;br /&gt;
        || die &amp;quot;broker configuration was not created at $BROKER_CONF&amp;quot;&lt;br /&gt;
    [[ -f &amp;quot;$BROKER_DISCOVERY_SRC&amp;quot; ]] \&lt;br /&gt;
        || die &amp;quot;broker discovery file not found at $BROKER_DISCOVERY_SRC&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    log &amp;quot;Active broker revision: $revision&amp;quot;&lt;br /&gt;
    log &amp;quot;Active broker config: $BROKER_CONF&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
validate_oidc() {&lt;br /&gt;
    [[ &amp;quot;$SKIP_OIDC_CHECK&amp;quot; == &amp;quot;true&amp;quot; ]] &amp;amp;&amp;amp; {&lt;br /&gt;
        log &amp;quot;Skipping OIDC discovery validation&amp;quot;&lt;br /&gt;
        return&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    local discovery=&amp;quot;${ISSUER}/.well-known/openid-configuration&amp;quot;&lt;br /&gt;
    local tmp&lt;br /&gt;
    tmp=&amp;quot;$(mktemp)&amp;quot;&lt;br /&gt;
    trap 'rm -f &amp;quot;$tmp&amp;quot;' RETURN&lt;br /&gt;
&lt;br /&gt;
    log &amp;quot;Validating OIDC discovery document: $discovery&amp;quot;&lt;br /&gt;
    curl --fail --silent --show-error --location \&lt;br /&gt;
        --connect-timeout 5 --max-time 15 \&lt;br /&gt;
        &amp;quot;$discovery&amp;quot; -o &amp;quot;$tmp&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    python3 - &amp;quot;$tmp&amp;quot; &amp;quot;$ISSUER&amp;quot; &amp;lt;&amp;lt;'PY'&lt;br /&gt;
import json&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
path, expected_issuer = sys.argv[1], sys.argv[2]&lt;br /&gt;
with open(path, encoding=&amp;quot;utf-8&amp;quot;) as f:&lt;br /&gt;
    d = json.load(f)&lt;br /&gt;
&lt;br /&gt;
actual_issuer = str(d.get(&amp;quot;issuer&amp;quot;, &amp;quot;&amp;quot;)).rstrip(&amp;quot;/&amp;quot;)&lt;br /&gt;
if actual_issuer != expected_issuer.rstrip(&amp;quot;/&amp;quot;):&lt;br /&gt;
    raise SystemExit(&lt;br /&gt;
        f&amp;quot;OIDC discovery issuer mismatch: expected {expected_issuer!r}, got {actual_issuer!r}&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
device_ep = d.get(&amp;quot;device_authorization_endpoint&amp;quot;)&lt;br /&gt;
if not device_ep:&lt;br /&gt;
    raise SystemExit(&lt;br /&gt;
        &amp;quot;OIDC provider does not advertise device_authorization_endpoint; &amp;quot;&lt;br /&gt;
        &amp;quot;authd Device Authentication will not work.&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
print(f&amp;quot;OIDC issuer OK: {actual_issuer}&amp;quot;)&lt;br /&gt;
print(f&amp;quot;Device authorization endpoint: {device_ep}&amp;quot;)&lt;br /&gt;
PY&lt;br /&gt;
&lt;br /&gt;
    rm -f &amp;quot;$tmp&amp;quot;&lt;br /&gt;
    trap - RETURN&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
install_if_changed() {&lt;br /&gt;
    local source=&amp;quot;$1&amp;quot;&lt;br /&gt;
    local destination=&amp;quot;$2&amp;quot;&lt;br /&gt;
    local mode=&amp;quot;$3&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    if [[ -f &amp;quot;$destination&amp;quot; ]] &amp;amp;&amp;amp; cmp -s &amp;quot;$source&amp;quot; &amp;quot;$destination&amp;quot;; then&lt;br /&gt;
        return 1&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    install -D -m &amp;quot;$mode&amp;quot; &amp;quot;$source&amp;quot; &amp;quot;$destination&amp;quot;&lt;br /&gt;
    return 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
validate_broker_files_no_placeholders() {&lt;br /&gt;
    local bad=0&lt;br /&gt;
    local f&lt;br /&gt;
&lt;br /&gt;
    # authd-oidc 0.4.1 rejects any parsed config value containing both '&amp;lt;' and '&amp;gt;'.&lt;br /&gt;
    # Scan the active main file and all drop-ins before attempting a restart.&lt;br /&gt;
    if grep -nE '&amp;lt;[^&amp;gt;]+&amp;gt;' &amp;quot;$BROKER_CONF&amp;quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
        printf 'ERROR: unresolved template placeholder(s) in %s:\n' &amp;quot;$BROKER_CONF&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
        grep -nE '&amp;lt;[^&amp;gt;]+&amp;gt;' &amp;quot;$BROKER_CONF&amp;quot; &amp;gt;&amp;amp;2 || true&lt;br /&gt;
        bad=1&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    if [[ -d &amp;quot;${BROKER_CONF}.d&amp;quot; ]]; then&lt;br /&gt;
        while IFS= read -r -d '' f; do&lt;br /&gt;
            if grep -nE '&amp;lt;[^&amp;gt;]+&amp;gt;' &amp;quot;$f&amp;quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
                printf 'ERROR: unresolved template placeholder(s) in drop-in %s:\n' &amp;quot;$f&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
                grep -nE '&amp;lt;[^&amp;gt;]+&amp;gt;' &amp;quot;$f&amp;quot; &amp;gt;&amp;amp;2 || true&lt;br /&gt;
                bad=1&lt;br /&gt;
            fi&lt;br /&gt;
        done &amp;lt; &amp;lt;(find &amp;quot;${BROKER_CONF}.d&amp;quot; -maxdepth 1 -type f -print0)&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    (( bad == 0 )) || die &amp;quot;authd-oidc configuration contains unresolved &amp;lt;...&amp;gt; placeholders&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
configure_broker_discovery() {&lt;br /&gt;
    mkdir -p /etc/authd/brokers.d&lt;br /&gt;
&lt;br /&gt;
    if install_if_changed &amp;quot;$BROKER_DISCOVERY_SRC&amp;quot; &amp;quot;$BROKER_DISCOVERY_DST&amp;quot; 0644; then&lt;br /&gt;
        log &amp;quot;Updated authd broker discovery: $BROKER_DISCOVERY_DST&amp;quot;&lt;br /&gt;
        AUTHD_CHANGED=true&lt;br /&gt;
    else&lt;br /&gt;
        log &amp;quot;authd broker discovery already correct&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
configure_broker() {&lt;br /&gt;
    local tmp&lt;br /&gt;
    tmp=&amp;quot;$(mktemp)&amp;quot;&lt;br /&gt;
    trap 'rm -f &amp;quot;$tmp&amp;quot;' RETURN&lt;br /&gt;
&lt;br /&gt;
    {&lt;br /&gt;
        printf '[oidc]\n'&lt;br /&gt;
        printf 'issuer = %s\n' &amp;quot;$ISSUER&amp;quot;&lt;br /&gt;
        printf 'client_id = %s\n' &amp;quot;$CLIENT_ID&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        if [[ -n &amp;quot;$CLIENT_SECRET&amp;quot; ]]; then&lt;br /&gt;
            printf 'client_secret = %s\n' &amp;quot;$CLIENT_SECRET&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
&lt;br /&gt;
        if [[ -n &amp;quot;$EXTRA_SCOPES&amp;quot; ]]; then&lt;br /&gt;
            printf 'extra_scopes = %s\n' &amp;quot;$EXTRA_SCOPES&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
&lt;br /&gt;
        printf 'force_provider_authentication = %s\n' &amp;quot;$FORCE_PROVIDER_AUTHENTICATION&amp;quot;&lt;br /&gt;
        printf '\n'&lt;br /&gt;
        printf '[users]\n'&lt;br /&gt;
        printf 'ssh_allowed_suffixes_first_auth = %s\n' &amp;quot;$SSH_SUFFIX&amp;quot;&lt;br /&gt;
        printf 'allowed_users = %s\n' &amp;quot;$ALLOWED_USERS&amp;quot;&lt;br /&gt;
        printf 'home_base_dir = %s\n' &amp;quot;$HOME_BASE_DIR&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        if [[ -n &amp;quot;$EXTRA_GROUPS&amp;quot; ]]; then&lt;br /&gt;
            printf 'extra_groups = %s\n' &amp;quot;$EXTRA_GROUPS&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
    } &amp;gt;&amp;quot;$tmp&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # Preserve the snap-provided/generated file the first time we take control.&lt;br /&gt;
    if [[ -f &amp;quot;$BROKER_CONF&amp;quot; &amp;amp;&amp;amp; ! -e &amp;quot;${BROKER_CONF}.pre-${PROGRAM}&amp;quot; ]]; then&lt;br /&gt;
        cp -a &amp;quot;$BROKER_CONF&amp;quot; &amp;quot;${BROKER_CONF}.pre-${PROGRAM}&amp;quot;&lt;br /&gt;
        log &amp;quot;Saved original broker config as ${BROKER_CONF}.pre-${PROGRAM}&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    if install_if_changed &amp;quot;$tmp&amp;quot; &amp;quot;$BROKER_CONF&amp;quot; 0600; then&lt;br /&gt;
        log &amp;quot;Updated broker configuration: $BROKER_CONF&amp;quot;&lt;br /&gt;
        BROKER_CHANGED=true&lt;br /&gt;
    else&lt;br /&gt;
        log &amp;quot;Broker configuration already correct&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    rm -f &amp;quot;$tmp&amp;quot;&lt;br /&gt;
    trap - RETURN&lt;br /&gt;
&lt;br /&gt;
    validate_broker_files_no_placeholders&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
configure_sshd() {&lt;br /&gt;
    local tmp&lt;br /&gt;
    tmp=&amp;quot;$(mktemp)&amp;quot;&lt;br /&gt;
    trap 'rm -f &amp;quot;$tmp&amp;quot;' RETURN&lt;br /&gt;
&lt;br /&gt;
    cat &amp;gt;&amp;quot;$tmp&amp;quot; &amp;lt;&amp;lt;EOF&lt;br /&gt;
# Managed by ${PROGRAM}&lt;br /&gt;
#&lt;br /&gt;
# Local/break-glass Unix accounts may still use SSH public keys.&lt;br /&gt;
# OIDC users matching ${SSH_SUFFIX} are forced through authd/PAM.&lt;br /&gt;
&lt;br /&gt;
UsePAM yes&lt;br /&gt;
PasswordAuthentication no&lt;br /&gt;
KbdInteractiveAuthentication no&lt;br /&gt;
PubkeyAuthentication yes&lt;br /&gt;
&lt;br /&gt;
Match User *${SSH_SUFFIX}&lt;br /&gt;
    PasswordAuthentication no&lt;br /&gt;
    KbdInteractiveAuthentication yes&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
    case &amp;quot;$SSH_POLICY&amp;quot; in&lt;br /&gt;
        authd-only)&lt;br /&gt;
            cat &amp;gt;&amp;gt;&amp;quot;$tmp&amp;quot; &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
    PubkeyAuthentication no&lt;br /&gt;
    AuthenticationMethods keyboard-interactive:pam&lt;br /&gt;
EOF&lt;br /&gt;
            ;;&lt;br /&gt;
        publickey+authd)&lt;br /&gt;
            cat &amp;gt;&amp;gt;&amp;quot;$tmp&amp;quot; &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
    PubkeyAuthentication yes&lt;br /&gt;
    AuthenticationMethods publickey,keyboard-interactive:pam&lt;br /&gt;
EOF&lt;br /&gt;
            ;;&lt;br /&gt;
    esac&lt;br /&gt;
&lt;br /&gt;
    if install_if_changed &amp;quot;$tmp&amp;quot; &amp;quot;$SSHD_DROPIN&amp;quot; 0644; then&lt;br /&gt;
        log &amp;quot;Updated SSH configuration: $SSHD_DROPIN&amp;quot;&lt;br /&gt;
        SSH_CHANGED=true&lt;br /&gt;
    else&lt;br /&gt;
        log &amp;quot;SSH configuration already correct&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    rm -f &amp;quot;$tmp&amp;quot;&lt;br /&gt;
    trap - RETURN&lt;br /&gt;
&lt;br /&gt;
    log &amp;quot;Validating sshd configuration&amp;quot;&lt;br /&gt;
    sshd -t&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
restart_if_needed() {&lt;br /&gt;
    if [[ &amp;quot;$AUTHD_CHANGED&amp;quot; == &amp;quot;true&amp;quot; ]]; then&lt;br /&gt;
        log &amp;quot;Restarting authd&amp;quot;&lt;br /&gt;
        systemctl restart authd&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    if [[ &amp;quot;$BROKER_CHANGED&amp;quot; == &amp;quot;true&amp;quot; || &amp;quot;$AUTHD_CHANGED&amp;quot; == &amp;quot;true&amp;quot; ]]; then&lt;br /&gt;
        log &amp;quot;Restarting authd-oidc broker&amp;quot;&lt;br /&gt;
        if ! snap restart authd-oidc &amp;gt;/dev/null; then&lt;br /&gt;
            printf 'ERROR: authd-oidc failed to restart. Recent broker log:\n' &amp;gt;&amp;amp;2&lt;br /&gt;
            journalctl -u snap.authd-oidc.authd-oidc.service -b -n 60 --no-pager &amp;gt;&amp;amp;2 || true&lt;br /&gt;
&lt;br /&gt;
            if [[ -f &amp;quot;${BROKER_CONF}.pre-${PROGRAM}&amp;quot; ]]; then&lt;br /&gt;
                printf 'ERROR: restoring previous broker configuration and retrying service start\n' &amp;gt;&amp;amp;2&lt;br /&gt;
                cp -a &amp;quot;${BROKER_CONF}.pre-${PROGRAM}&amp;quot; &amp;quot;$BROKER_CONF&amp;quot;&lt;br /&gt;
                systemctl reset-failed snap.authd-oidc.authd-oidc.service || true&lt;br /&gt;
                snap restart authd-oidc &amp;gt;/dev/null || true&lt;br /&gt;
            fi&lt;br /&gt;
            die &amp;quot;authd-oidc broker restart failed; previous config was restored when available&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    if [[ &amp;quot;$SSH_CHANGED&amp;quot; == &amp;quot;true&amp;quot; ]]; then&lt;br /&gt;
        log &amp;quot;Restarting SSH&amp;quot;&lt;br /&gt;
        systemctl restart ssh&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
show_effective_config() {&lt;br /&gt;
    local probe_user=&amp;quot;authd-probe${SSH_SUFFIX}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    log &amp;quot;Service status&amp;quot;&lt;br /&gt;
    systemctl is-active --quiet authd &amp;amp;&amp;amp; echo &amp;quot;authd: active&amp;quot;&lt;br /&gt;
    snap services authd-oidc&lt;br /&gt;
    systemctl is-active --quiet ssh &amp;amp;&amp;amp; echo &amp;quot;ssh: active&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    log &amp;quot;Effective SSH authentication settings for ${probe_user}&amp;quot;&lt;br /&gt;
    sshd -T -C &amp;quot;user=${probe_user},host=localhost,addr=127.0.0.1&amp;quot; 2&amp;gt;/dev/null \&lt;br /&gt;
        | grep -E '^(usepam|passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication|authenticationmethods) ' \&lt;br /&gt;
        || true&lt;br /&gt;
&lt;br /&gt;
    log &amp;quot;Installed versions&amp;quot;&lt;br /&gt;
    dpkg-query -W -f='authd: ${Version}\n' authd 2&amp;gt;/dev/null || true&lt;br /&gt;
    snap list authd-oidc 2&amp;gt;/dev/null || true&lt;br /&gt;
&lt;br /&gt;
    cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
&lt;br /&gt;
Setup complete.&lt;br /&gt;
&lt;br /&gt;
OIDC issuer:      ${ISSUER}&lt;br /&gt;
OIDC client ID:   ${CLIENT_ID}&lt;br /&gt;
SSH suffix:       ${SSH_SUFFIX}&lt;br /&gt;
Allowed users:    ${ALLOWED_USERS}&lt;br /&gt;
SSH policy:       ${SSH_POLICY}&lt;br /&gt;
&lt;br /&gt;
Test from a SECOND terminal while keeping your current administrative&lt;br /&gt;
session open:&lt;br /&gt;
&lt;br /&gt;
  ssh 'user${SSH_SUFFIX}'@&amp;lt;server&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For Keycloak MFA:&lt;br /&gt;
  Configure the Keycloak authentication flow/client so Device Authorization&lt;br /&gt;
  requires OTP/WebAuthn. This script configures the Ubuntu side only.&lt;br /&gt;
&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
    if [[ &amp;quot;$SSH_POLICY&amp;quot; == &amp;quot;publickey+authd&amp;quot; ]]; then&lt;br /&gt;
        cat &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
NOTE:&lt;br /&gt;
  publickey+authd requires the SSH public key to succeed before authd runs.&lt;br /&gt;
  For a brand-new JIT user, pre-provision the account/key or use centralized&lt;br /&gt;
  SSH certificates / AuthorizedKeysCommand. Otherwise first login can fail.&lt;br /&gt;
&lt;br /&gt;
EOF&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AUTHD_CHANGED=false&lt;br /&gt;
BROKER_CHANGED=false&lt;br /&gt;
SSH_CHANGED=false&lt;br /&gt;
&lt;br /&gt;
install_packages&lt;br /&gt;
install_broker&lt;br /&gt;
validate_oidc&lt;br /&gt;
configure_broker_discovery&lt;br /&gt;
configure_broker&lt;br /&gt;
configure_sshd&lt;br /&gt;
restart_if_needed&lt;br /&gt;
show_effective_config&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=2fa_ssh&amp;diff=5752</id>
		<title>2fa ssh</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=2fa_ssh&amp;diff=5752"/>
		<updated>2026-09-16T12:40:49Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;# Using 2FA&lt;br /&gt;
&lt;br /&gt;
Require OTP via Google Authenticator App&lt;br /&gt;
&lt;br /&gt;
publickey,keyboard-interactive (and - requires both)&lt;br /&gt;
&lt;br /&gt;
publickey keyboard-interactive (or - requires one or the other)&lt;br /&gt;
&lt;br /&gt;
## Ubuntu 26.04&lt;br /&gt;
&lt;br /&gt;
Prep user&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
adduser myuser --disabled-password&lt;br /&gt;
# usermod -aG sudo myuser&lt;br /&gt;
visudo  # myuser   ALL=(ALL:ALL) NOPASSWD: ALL&lt;br /&gt;
sudo su - myuser&lt;br /&gt;
mkdir .ssh&lt;br /&gt;
nano .ssh/authorized_keys  # paste public ssh key&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
Update SSHD to require both SSH ID and then OTP&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
#!/usr/bin/env bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y libpam-google-authenticator&lt;br /&gt;
&lt;br /&gt;
# Run as the login user, NOT root/sudo — generates ~/.google_authenticator&lt;br /&gt;
google-authenticator&lt;br /&gt;
&lt;br /&gt;
# Insert BEFORE @include common-auth if you want pubkey+OTP only, no password.&lt;br /&gt;
# As a quick check, comment out the password-based auth line so PAM only asks for the OTP:&lt;br /&gt;
sudo sed -i 's/^@include common-auth/#&amp;amp;/' /etc/pam.d/sshd&lt;br /&gt;
sudo sed -i '1 i\auth required pam_google_authenticator.so' /etc/pam.d/sshd&lt;br /&gt;
&lt;br /&gt;
sudo tee /etc/ssh/sshd_config.d/60-mfa.conf &amp;gt; /dev/null &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
KbdInteractiveAuthentication yes&lt;br /&gt;
PasswordAuthentication no&lt;br /&gt;
AuthenticationMethods publickey,keyboard-interactive&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
sudo systemctl restart ssh&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# KeyCloak realm&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
setup-authd-keycloak.sh&lt;br /&gt;
#!/usr/bin/env bash&lt;br /&gt;
# setup-authd-keycloak.sh&lt;br /&gt;
#&lt;br /&gt;
# Idempotent Ubuntu 26.04 Server bootstrap for Canonical authd + generic OIDC&lt;br /&gt;
# broker (authd-oidc), intended for Keycloak device authorization.&lt;br /&gt;
#&lt;br /&gt;
# Default SSH policy for OIDC users:&lt;br /&gt;
#   - SSH password authentication: disabled&lt;br /&gt;
#   - SSH public-key authentication: disabled for the OIDC suffix&lt;br /&gt;
#   - PAM keyboard-interactive/authd: required&lt;br /&gt;
#&lt;br /&gt;
# This ensures OIDC users cannot bypass authd/Keycloak with an SSH key.&lt;br /&gt;
#&lt;br /&gt;
# IMPORTANT:&lt;br /&gt;
# Keycloak OTP/WebAuthn policy is configured in Keycloak, not by this script.&lt;br /&gt;
# Configure the Keycloak client/authentication flow to require the desired MFA.&lt;br /&gt;
&lt;br /&gt;
set -Eeuo pipefail&lt;br /&gt;
IFS=$'\n\t'&lt;br /&gt;
&lt;br /&gt;
PROGRAM=&amp;quot;${0##*/}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
ISSUER=&amp;quot;&amp;quot;&lt;br /&gt;
CLIENT_ID=&amp;quot;&amp;quot;&lt;br /&gt;
SSH_SUFFIX=&amp;quot;&amp;quot;&lt;br /&gt;
ALLOWED_USERS=&amp;quot;&amp;quot;&lt;br /&gt;
CLIENT_SECRET=&amp;quot;&amp;quot;&lt;br /&gt;
EXTRA_SCOPES=&amp;quot;offline_access&amp;quot;&lt;br /&gt;
FORCE_PROVIDER_AUTHENTICATION=&amp;quot;true&amp;quot;&lt;br /&gt;
HOME_BASE_DIR=&amp;quot;/home&amp;quot;&lt;br /&gt;
EXTRA_GROUPS=&amp;quot;&amp;quot;&lt;br /&gt;
SSH_POLICY=&amp;quot;authd-only&amp;quot;&lt;br /&gt;
BROKER_CHANNEL=&amp;quot;0.x/stable&amp;quot;&lt;br /&gt;
SKIP_OIDC_CHECK=&amp;quot;false&amp;quot;&lt;br /&gt;
&lt;br /&gt;
BROKER_SNAP=&amp;quot;authd-oidc&amp;quot;&lt;br /&gt;
BROKER_CONF=&amp;quot;&amp;quot;&lt;br /&gt;
BROKER_DATA_DIR=&amp;quot;&amp;quot;&lt;br /&gt;
BROKER_DISCOVERY_SRC=&amp;quot;/snap/authd-oidc/current/conf/authd/oidc.conf&amp;quot;&lt;br /&gt;
BROKER_DISCOVERY_DST=&amp;quot;/etc/authd/brokers.d/oidc.conf&amp;quot;&lt;br /&gt;
SSHD_DROPIN=&amp;quot;/etc/ssh/sshd_config.d/00-authd-oidc.conf&amp;quot;&lt;br /&gt;
&lt;br /&gt;
usage() {&lt;br /&gt;
    cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
Usage:&lt;br /&gt;
  sudo ./${PROGRAM} \\&lt;br /&gt;
    --issuer URL \\&lt;br /&gt;
    --client-id ID \\&lt;br /&gt;
    --ssh-suffix @DOMAIN \\&lt;br /&gt;
    --allowed-users USERS&lt;br /&gt;
&lt;br /&gt;
Required:&lt;br /&gt;
  --issuer URL&lt;br /&gt;
      OIDC issuer / Keycloak realm URL.&lt;br /&gt;
      Example: https://auth.example.com/realms/linux&lt;br /&gt;
&lt;br /&gt;
  --client-id ID&lt;br /&gt;
      Keycloak OIDC client ID.&lt;br /&gt;
      Example: ubuntu-authd&lt;br /&gt;
&lt;br /&gt;
  --ssh-suffix @DOMAIN&lt;br /&gt;
      Username suffix allowed to perform first SSH authentication.&lt;br /&gt;
      Exactly one suffix is supported by this script.&lt;br /&gt;
      Example: @example.com&lt;br /&gt;
&lt;br /&gt;
  --allowed-users USERS&lt;br /&gt;
      authd broker allowed_users value.&lt;br /&gt;
      Examples:&lt;br /&gt;
        ALL&lt;br /&gt;
        OWNER&lt;br /&gt;
        user1@example.com,user2@example.com&lt;br /&gt;
&lt;br /&gt;
Optional:&lt;br /&gt;
  --client-secret SECRET&lt;br /&gt;
      OIDC client secret, only when your Keycloak client requires one.&lt;br /&gt;
      Prefer a public client for device authorization when appropriate.&lt;br /&gt;
      WARNING: a secret supplied on the command line may be visible in shell&lt;br /&gt;
      history or process listings.&lt;br /&gt;
&lt;br /&gt;
  --extra-scopes SCOPES&lt;br /&gt;
      Comma-separated additional OIDC scopes.&lt;br /&gt;
      Default: offline_access&lt;br /&gt;
&lt;br /&gt;
  --force-provider-authentication true|false&lt;br /&gt;
      Require provider/token refresh validation even for authd local-password&lt;br /&gt;
      authentication.&lt;br /&gt;
      Default: true&lt;br /&gt;
&lt;br /&gt;
  --home-base-dir DIR&lt;br /&gt;
      Base directory for authd-created home directories.&lt;br /&gt;
      Default: /home&lt;br /&gt;
&lt;br /&gt;
  --extra-groups GROUPS&lt;br /&gt;
      Optional comma-separated local groups to assign to authd users.&lt;br /&gt;
      Example: sudo&lt;br /&gt;
      Default: unset&lt;br /&gt;
&lt;br /&gt;
  --ssh-policy POLICY&lt;br /&gt;
      authd-only       Require authd/Keycloak for OIDC users. This is the&lt;br /&gt;
                       recommended default for JIT-provisioned SSH users.&lt;br /&gt;
      publickey+authd  Require BOTH SSH public key AND authd/Keycloak.&lt;br /&gt;
                       The account/key must already be resolvable before SSH&lt;br /&gt;
                       reaches authd, so this is normally unsuitable for the&lt;br /&gt;
                       very first JIT login.&lt;br /&gt;
      Default: authd-only&lt;br /&gt;
&lt;br /&gt;
  --broker-channel CHANNEL&lt;br /&gt;
      Snap channel used when authd-oidc is first installed.&lt;br /&gt;
      Existing installations are not automatically switched.&lt;br /&gt;
      Default: 0.x/stable&lt;br /&gt;
&lt;br /&gt;
  --skip-oidc-check&lt;br /&gt;
      Do not validate the issuer discovery document before changing config.&lt;br /&gt;
&lt;br /&gt;
  -h, --help&lt;br /&gt;
      Show this help.&lt;br /&gt;
&lt;br /&gt;
Example:&lt;br /&gt;
  sudo ./${PROGRAM} \\&lt;br /&gt;
    --issuer https://auth.uvoo.io/realms/test \\&lt;br /&gt;
    --client-id test \\&lt;br /&gt;
    --ssh-suffix @uvoo.io \\&lt;br /&gt;
    --allowed-users ALL&lt;br /&gt;
&lt;br /&gt;
More restrictive production example:&lt;br /&gt;
  sudo ./${PROGRAM} \\&lt;br /&gt;
    --issuer https://auth.uvoo.io/realms/test \\&lt;br /&gt;
    --client-id test \\&lt;br /&gt;
    --ssh-suffix @uvoo.io \\&lt;br /&gt;
    --allowed-users 'test1@uvoo.io,admin1@uvoo.io'&lt;br /&gt;
EOF&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
die() {&lt;br /&gt;
    printf 'ERROR: %s\n' &amp;quot;$*&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
    exit 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
log() {&lt;br /&gt;
    printf '==&amp;gt; %s\n' &amp;quot;$*&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
on_error() {&lt;br /&gt;
    local rc=$?&lt;br /&gt;
    printf 'ERROR: command failed at line %s (exit %s)\n' &amp;quot;${BASH_LINENO[0]}&amp;quot; &amp;quot;$rc&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
    exit &amp;quot;$rc&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
trap on_error ERR&lt;br /&gt;
&lt;br /&gt;
require_value() {&lt;br /&gt;
    local opt=&amp;quot;$1&amp;quot;&lt;br /&gt;
    local value=&amp;quot;${2-}&amp;quot;&lt;br /&gt;
    [[ -n &amp;quot;$value&amp;quot; ]] || die &amp;quot;${opt} requires a value&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
while (($#)); do&lt;br /&gt;
    case &amp;quot;$1&amp;quot; in&lt;br /&gt;
        --issuer)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            ISSUER=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --client-id)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            CLIENT_ID=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --ssh-suffix)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            SSH_SUFFIX=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --allowed-users)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            ALLOWED_USERS=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --client-secret)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            CLIENT_SECRET=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --extra-scopes)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            EXTRA_SCOPES=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --force-provider-authentication)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            FORCE_PROVIDER_AUTHENTICATION=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --home-base-dir)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            HOME_BASE_DIR=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --extra-groups)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            EXTRA_GROUPS=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --ssh-policy)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            SSH_POLICY=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --broker-channel)&lt;br /&gt;
            require_value &amp;quot;$1&amp;quot; &amp;quot;${2-}&amp;quot;&lt;br /&gt;
            BROKER_CHANNEL=&amp;quot;$2&amp;quot;&lt;br /&gt;
            shift 2&lt;br /&gt;
            ;;&lt;br /&gt;
        --skip-oidc-check)&lt;br /&gt;
            SKIP_OIDC_CHECK=&amp;quot;true&amp;quot;&lt;br /&gt;
            shift&lt;br /&gt;
            ;;&lt;br /&gt;
        -h|--help)&lt;br /&gt;
            usage&lt;br /&gt;
            exit 0&lt;br /&gt;
            ;;&lt;br /&gt;
        *)&lt;br /&gt;
            die &amp;quot;unknown argument: $1 (use --help)&amp;quot;&lt;br /&gt;
            ;;&lt;br /&gt;
    esac&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
[[ -n &amp;quot;$ISSUER&amp;quot; ]]       || die &amp;quot;--issuer is required&amp;quot;&lt;br /&gt;
[[ -n &amp;quot;$CLIENT_ID&amp;quot; ]]    || die &amp;quot;--client-id is required&amp;quot;&lt;br /&gt;
[[ -n &amp;quot;$SSH_SUFFIX&amp;quot; ]]   || die &amp;quot;--ssh-suffix is required&amp;quot;&lt;br /&gt;
[[ -n &amp;quot;$ALLOWED_USERS&amp;quot; ]] || die &amp;quot;--allowed-users is required&amp;quot;&lt;br /&gt;
&lt;br /&gt;
[[ &amp;quot;$ISSUER&amp;quot; == https://* ]] || die &amp;quot;--issuer must use https://&amp;quot;&lt;br /&gt;
ISSUER=&amp;quot;${ISSUER%/}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
[[ &amp;quot;$SSH_SUFFIX&amp;quot; == @* ]] || die &amp;quot;--ssh-suffix must begin with '@'&amp;quot;&lt;br /&gt;
[[ &amp;quot;$SSH_SUFFIX&amp;quot; != *&amp;quot;,&amp;quot;* ]] || die &amp;quot;--ssh-suffix accepts exactly one suffix&amp;quot;&lt;br /&gt;
[[ &amp;quot;$SSH_SUFFIX&amp;quot; != *[[:space:]]* ]] || die &amp;quot;--ssh-suffix must not contain whitespace&amp;quot;&lt;br /&gt;
&lt;br /&gt;
case &amp;quot;$FORCE_PROVIDER_AUTHENTICATION&amp;quot; in&lt;br /&gt;
    true|false) ;;&lt;br /&gt;
    *) die &amp;quot;--force-provider-authentication must be true or false&amp;quot; ;;&lt;br /&gt;
esac&lt;br /&gt;
&lt;br /&gt;
case &amp;quot;$SSH_POLICY&amp;quot; in&lt;br /&gt;
    authd-only|publickey+authd) ;;&lt;br /&gt;
    *) die &amp;quot;--ssh-policy must be authd-only or publickey+authd&amp;quot; ;;&lt;br /&gt;
esac&lt;br /&gt;
&lt;br /&gt;
for value_name in ISSUER CLIENT_ID SSH_SUFFIX ALLOWED_USERS CLIENT_SECRET EXTRA_SCOPES HOME_BASE_DIR EXTRA_GROUPS; do&lt;br /&gt;
    value=&amp;quot;${!value_name}&amp;quot;&lt;br /&gt;
    [[ &amp;quot;$value&amp;quot; != *$'\n'* &amp;amp;&amp;amp; &amp;quot;$value&amp;quot; != *$'\r'* ]] \&lt;br /&gt;
        || die &amp;quot;${value_name} must not contain newlines&amp;quot;&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
if [[ $EUID -ne 0 ]]; then&lt;br /&gt;
    die &amp;quot;run this script as root, e.g. sudo ./${PROGRAM} ...&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
[[ -r /etc/os-release ]] || die &amp;quot;cannot read /etc/os-release&amp;quot;&lt;br /&gt;
# shellcheck disable=SC1091&lt;br /&gt;
source /etc/os-release&lt;br /&gt;
[[ &amp;quot;${ID:-}&amp;quot; == &amp;quot;ubuntu&amp;quot; ]] || die &amp;quot;this script supports Ubuntu only&amp;quot;&lt;br /&gt;
[[ &amp;quot;${VERSION_ID:-}&amp;quot; == &amp;quot;26.04&amp;quot; ]] \&lt;br /&gt;
    || die &amp;quot;this script is intentionally limited to Ubuntu 26.04; found ${VERSION_ID:-unknown}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
export DEBIAN_FRONTEND=noninteractive&lt;br /&gt;
&lt;br /&gt;
install_packages() {&lt;br /&gt;
    local required=(&lt;br /&gt;
    authd&lt;br /&gt;
    openssh-server&lt;br /&gt;
    snapd&lt;br /&gt;
    curl&lt;br /&gt;
    ca-certificates&lt;br /&gt;
    python3&lt;br /&gt;
    cracklib-runtime&lt;br /&gt;
    wamerican&lt;br /&gt;
)&lt;br /&gt;
    local missing=()&lt;br /&gt;
    local pkg&lt;br /&gt;
&lt;br /&gt;
    for pkg in &amp;quot;${required[@]}&amp;quot;; do&lt;br /&gt;
        if ! dpkg-query -W -f='${Status}' &amp;quot;$pkg&amp;quot; 2&amp;gt;/dev/null | grep -qx 'install ok installed'; then&lt;br /&gt;
            missing+=(&amp;quot;$pkg&amp;quot;)&lt;br /&gt;
        fi&lt;br /&gt;
    done&lt;br /&gt;
&lt;br /&gt;
    if ((${#missing[@]})); then&lt;br /&gt;
        log &amp;quot;Installing required packages: ${missing[*]}&amp;quot;&lt;br /&gt;
        apt-get update&lt;br /&gt;
        apt-get install -y --no-install-recommends &amp;quot;${missing[@]}&amp;quot;&lt;br /&gt;
    else&lt;br /&gt;
        log &amp;quot;Required Debian packages already installed&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    systemctl enable --now snapd.socket &amp;gt;/dev/null&lt;br /&gt;
    # authd may be socket/D-Bus activated depending on package version; start it&lt;br /&gt;
    # without assuming the unit has an [Install] section.&lt;br /&gt;
    systemctl start authd&lt;br /&gt;
    systemctl enable --now ssh &amp;gt;/dev/null&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
install_broker() {&lt;br /&gt;
    if snap list &amp;quot;$BROKER_SNAP&amp;quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
        log &amp;quot;$BROKER_SNAP is already installed; leaving its current snap channel/revision unchanged&amp;quot;&lt;br /&gt;
        snap list &amp;quot;$BROKER_SNAP&amp;quot;&lt;br /&gt;
    else&lt;br /&gt;
        log &amp;quot;Installing $BROKER_SNAP from channel $BROKER_CHANNEL&amp;quot;&lt;br /&gt;
        snap install &amp;quot;$BROKER_SNAP&amp;quot; --channel=&amp;quot;$BROKER_CHANNEL&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    # Resolve the active snap revision explicitly. The service wrapper uses the&lt;br /&gt;
    # revision-specific SNAP_DATA path (for example /var/snap/authd-oidc/89).&lt;br /&gt;
    local revision&lt;br /&gt;
    revision=&amp;quot;$(snap list &amp;quot;$BROKER_SNAP&amp;quot; | awk 'NR==2 {print $3}')&amp;quot;&lt;br /&gt;
    [[ &amp;quot;$revision&amp;quot; =~ ^[0-9]+$ ]] || die &amp;quot;could not determine active $BROKER_SNAP revision&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    BROKER_DATA_DIR=&amp;quot;/var/snap/${BROKER_SNAP}/${revision}&amp;quot;&lt;br /&gt;
    BROKER_CONF=&amp;quot;${BROKER_DATA_DIR}/broker.conf&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    [[ -f &amp;quot;$BROKER_CONF&amp;quot; ]] \&lt;br /&gt;
        || die &amp;quot;broker configuration was not created at $BROKER_CONF&amp;quot;&lt;br /&gt;
    [[ -f &amp;quot;$BROKER_DISCOVERY_SRC&amp;quot; ]] \&lt;br /&gt;
        || die &amp;quot;broker discovery file not found at $BROKER_DISCOVERY_SRC&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    log &amp;quot;Active broker revision: $revision&amp;quot;&lt;br /&gt;
    log &amp;quot;Active broker config: $BROKER_CONF&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
validate_oidc() {&lt;br /&gt;
    [[ &amp;quot;$SKIP_OIDC_CHECK&amp;quot; == &amp;quot;true&amp;quot; ]] &amp;amp;&amp;amp; {&lt;br /&gt;
        log &amp;quot;Skipping OIDC discovery validation&amp;quot;&lt;br /&gt;
        return&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    local discovery=&amp;quot;${ISSUER}/.well-known/openid-configuration&amp;quot;&lt;br /&gt;
    local tmp&lt;br /&gt;
    tmp=&amp;quot;$(mktemp)&amp;quot;&lt;br /&gt;
    trap 'rm -f &amp;quot;$tmp&amp;quot;' RETURN&lt;br /&gt;
&lt;br /&gt;
    log &amp;quot;Validating OIDC discovery document: $discovery&amp;quot;&lt;br /&gt;
    curl --fail --silent --show-error --location \&lt;br /&gt;
        --connect-timeout 5 --max-time 15 \&lt;br /&gt;
        &amp;quot;$discovery&amp;quot; -o &amp;quot;$tmp&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    python3 - &amp;quot;$tmp&amp;quot; &amp;quot;$ISSUER&amp;quot; &amp;lt;&amp;lt;'PY'&lt;br /&gt;
import json&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
path, expected_issuer = sys.argv[1], sys.argv[2]&lt;br /&gt;
with open(path, encoding=&amp;quot;utf-8&amp;quot;) as f:&lt;br /&gt;
    d = json.load(f)&lt;br /&gt;
&lt;br /&gt;
actual_issuer = str(d.get(&amp;quot;issuer&amp;quot;, &amp;quot;&amp;quot;)).rstrip(&amp;quot;/&amp;quot;)&lt;br /&gt;
if actual_issuer != expected_issuer.rstrip(&amp;quot;/&amp;quot;):&lt;br /&gt;
    raise SystemExit(&lt;br /&gt;
        f&amp;quot;OIDC discovery issuer mismatch: expected {expected_issuer!r}, got {actual_issuer!r}&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
device_ep = d.get(&amp;quot;device_authorization_endpoint&amp;quot;)&lt;br /&gt;
if not device_ep:&lt;br /&gt;
    raise SystemExit(&lt;br /&gt;
        &amp;quot;OIDC provider does not advertise device_authorization_endpoint; &amp;quot;&lt;br /&gt;
        &amp;quot;authd Device Authentication will not work.&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
print(f&amp;quot;OIDC issuer OK: {actual_issuer}&amp;quot;)&lt;br /&gt;
print(f&amp;quot;Device authorization endpoint: {device_ep}&amp;quot;)&lt;br /&gt;
PY&lt;br /&gt;
&lt;br /&gt;
    rm -f &amp;quot;$tmp&amp;quot;&lt;br /&gt;
    trap - RETURN&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
install_if_changed() {&lt;br /&gt;
    local source=&amp;quot;$1&amp;quot;&lt;br /&gt;
    local destination=&amp;quot;$2&amp;quot;&lt;br /&gt;
    local mode=&amp;quot;$3&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    if [[ -f &amp;quot;$destination&amp;quot; ]] &amp;amp;&amp;amp; cmp -s &amp;quot;$source&amp;quot; &amp;quot;$destination&amp;quot;; then&lt;br /&gt;
        return 1&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    install -D -m &amp;quot;$mode&amp;quot; &amp;quot;$source&amp;quot; &amp;quot;$destination&amp;quot;&lt;br /&gt;
    return 0&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
validate_broker_files_no_placeholders() {&lt;br /&gt;
    local bad=0&lt;br /&gt;
    local f&lt;br /&gt;
&lt;br /&gt;
    # authd-oidc 0.4.1 rejects any parsed config value containing both '&amp;lt;' and '&amp;gt;'.&lt;br /&gt;
    # Scan the active main file and all drop-ins before attempting a restart.&lt;br /&gt;
    if grep -nE '&amp;lt;[^&amp;gt;]+&amp;gt;' &amp;quot;$BROKER_CONF&amp;quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
        printf 'ERROR: unresolved template placeholder(s) in %s:\n' &amp;quot;$BROKER_CONF&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
        grep -nE '&amp;lt;[^&amp;gt;]+&amp;gt;' &amp;quot;$BROKER_CONF&amp;quot; &amp;gt;&amp;amp;2 || true&lt;br /&gt;
        bad=1&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    if [[ -d &amp;quot;${BROKER_CONF}.d&amp;quot; ]]; then&lt;br /&gt;
        while IFS= read -r -d '' f; do&lt;br /&gt;
            if grep -nE '&amp;lt;[^&amp;gt;]+&amp;gt;' &amp;quot;$f&amp;quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
                printf 'ERROR: unresolved template placeholder(s) in drop-in %s:\n' &amp;quot;$f&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
                grep -nE '&amp;lt;[^&amp;gt;]+&amp;gt;' &amp;quot;$f&amp;quot; &amp;gt;&amp;amp;2 || true&lt;br /&gt;
                bad=1&lt;br /&gt;
            fi&lt;br /&gt;
        done &amp;lt; &amp;lt;(find &amp;quot;${BROKER_CONF}.d&amp;quot; -maxdepth 1 -type f -print0)&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    (( bad == 0 )) || die &amp;quot;authd-oidc configuration contains unresolved &amp;lt;...&amp;gt; placeholders&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
configure_broker_discovery() {&lt;br /&gt;
    mkdir -p /etc/authd/brokers.d&lt;br /&gt;
&lt;br /&gt;
    if install_if_changed &amp;quot;$BROKER_DISCOVERY_SRC&amp;quot; &amp;quot;$BROKER_DISCOVERY_DST&amp;quot; 0644; then&lt;br /&gt;
        log &amp;quot;Updated authd broker discovery: $BROKER_DISCOVERY_DST&amp;quot;&lt;br /&gt;
        AUTHD_CHANGED=true&lt;br /&gt;
    else&lt;br /&gt;
        log &amp;quot;authd broker discovery already correct&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
configure_broker() {&lt;br /&gt;
    local tmp&lt;br /&gt;
    tmp=&amp;quot;$(mktemp)&amp;quot;&lt;br /&gt;
    trap 'rm -f &amp;quot;$tmp&amp;quot;' RETURN&lt;br /&gt;
&lt;br /&gt;
    {&lt;br /&gt;
        printf '[oidc]\n'&lt;br /&gt;
        printf 'issuer = %s\n' &amp;quot;$ISSUER&amp;quot;&lt;br /&gt;
        printf 'client_id = %s\n' &amp;quot;$CLIENT_ID&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        if [[ -n &amp;quot;$CLIENT_SECRET&amp;quot; ]]; then&lt;br /&gt;
            printf 'client_secret = %s\n' &amp;quot;$CLIENT_SECRET&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
&lt;br /&gt;
        if [[ -n &amp;quot;$EXTRA_SCOPES&amp;quot; ]]; then&lt;br /&gt;
            printf 'extra_scopes = %s\n' &amp;quot;$EXTRA_SCOPES&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
&lt;br /&gt;
        printf 'force_provider_authentication = %s\n' &amp;quot;$FORCE_PROVIDER_AUTHENTICATION&amp;quot;&lt;br /&gt;
        printf '\n'&lt;br /&gt;
        printf '[users]\n'&lt;br /&gt;
        printf 'ssh_allowed_suffixes_first_auth = %s\n' &amp;quot;$SSH_SUFFIX&amp;quot;&lt;br /&gt;
        printf 'allowed_users = %s\n' &amp;quot;$ALLOWED_USERS&amp;quot;&lt;br /&gt;
        printf 'home_base_dir = %s\n' &amp;quot;$HOME_BASE_DIR&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        if [[ -n &amp;quot;$EXTRA_GROUPS&amp;quot; ]]; then&lt;br /&gt;
            printf 'extra_groups = %s\n' &amp;quot;$EXTRA_GROUPS&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
    } &amp;gt;&amp;quot;$tmp&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # Preserve the snap-provided/generated file the first time we take control.&lt;br /&gt;
    if [[ -f &amp;quot;$BROKER_CONF&amp;quot; &amp;amp;&amp;amp; ! -e &amp;quot;${BROKER_CONF}.pre-${PROGRAM}&amp;quot; ]]; then&lt;br /&gt;
        cp -a &amp;quot;$BROKER_CONF&amp;quot; &amp;quot;${BROKER_CONF}.pre-${PROGRAM}&amp;quot;&lt;br /&gt;
        log &amp;quot;Saved original broker config as ${BROKER_CONF}.pre-${PROGRAM}&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    if install_if_changed &amp;quot;$tmp&amp;quot; &amp;quot;$BROKER_CONF&amp;quot; 0600; then&lt;br /&gt;
        log &amp;quot;Updated broker configuration: $BROKER_CONF&amp;quot;&lt;br /&gt;
        BROKER_CHANGED=true&lt;br /&gt;
    else&lt;br /&gt;
        log &amp;quot;Broker configuration already correct&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    rm -f &amp;quot;$tmp&amp;quot;&lt;br /&gt;
    trap - RETURN&lt;br /&gt;
&lt;br /&gt;
    validate_broker_files_no_placeholders&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
configure_sshd() {&lt;br /&gt;
    local tmp&lt;br /&gt;
    tmp=&amp;quot;$(mktemp)&amp;quot;&lt;br /&gt;
    trap 'rm -f &amp;quot;$tmp&amp;quot;' RETURN&lt;br /&gt;
&lt;br /&gt;
    cat &amp;gt;&amp;quot;$tmp&amp;quot; &amp;lt;&amp;lt;EOF&lt;br /&gt;
# Managed by ${PROGRAM}&lt;br /&gt;
#&lt;br /&gt;
# Local/break-glass Unix accounts may still use SSH public keys.&lt;br /&gt;
# OIDC users matching ${SSH_SUFFIX} are forced through authd/PAM.&lt;br /&gt;
&lt;br /&gt;
UsePAM yes&lt;br /&gt;
PasswordAuthentication no&lt;br /&gt;
KbdInteractiveAuthentication no&lt;br /&gt;
PubkeyAuthentication yes&lt;br /&gt;
&lt;br /&gt;
Match User *${SSH_SUFFIX}&lt;br /&gt;
    PasswordAuthentication no&lt;br /&gt;
    KbdInteractiveAuthentication yes&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
    case &amp;quot;$SSH_POLICY&amp;quot; in&lt;br /&gt;
        authd-only)&lt;br /&gt;
            cat &amp;gt;&amp;gt;&amp;quot;$tmp&amp;quot; &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
    PubkeyAuthentication no&lt;br /&gt;
    AuthenticationMethods keyboard-interactive:pam&lt;br /&gt;
EOF&lt;br /&gt;
            ;;&lt;br /&gt;
        publickey+authd)&lt;br /&gt;
            cat &amp;gt;&amp;gt;&amp;quot;$tmp&amp;quot; &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
    PubkeyAuthentication yes&lt;br /&gt;
    AuthenticationMethods publickey,keyboard-interactive:pam&lt;br /&gt;
EOF&lt;br /&gt;
            ;;&lt;br /&gt;
    esac&lt;br /&gt;
&lt;br /&gt;
    if install_if_changed &amp;quot;$tmp&amp;quot; &amp;quot;$SSHD_DROPIN&amp;quot; 0644; then&lt;br /&gt;
        log &amp;quot;Updated SSH configuration: $SSHD_DROPIN&amp;quot;&lt;br /&gt;
        SSH_CHANGED=true&lt;br /&gt;
    else&lt;br /&gt;
        log &amp;quot;SSH configuration already correct&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    rm -f &amp;quot;$tmp&amp;quot;&lt;br /&gt;
    trap - RETURN&lt;br /&gt;
&lt;br /&gt;
    log &amp;quot;Validating sshd configuration&amp;quot;&lt;br /&gt;
    sshd -t&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
restart_if_needed() {&lt;br /&gt;
    if [[ &amp;quot;$AUTHD_CHANGED&amp;quot; == &amp;quot;true&amp;quot; ]]; then&lt;br /&gt;
        log &amp;quot;Restarting authd&amp;quot;&lt;br /&gt;
        systemctl restart authd&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    if [[ &amp;quot;$BROKER_CHANGED&amp;quot; == &amp;quot;true&amp;quot; || &amp;quot;$AUTHD_CHANGED&amp;quot; == &amp;quot;true&amp;quot; ]]; then&lt;br /&gt;
        log &amp;quot;Restarting authd-oidc broker&amp;quot;&lt;br /&gt;
        if ! snap restart authd-oidc &amp;gt;/dev/null; then&lt;br /&gt;
            printf 'ERROR: authd-oidc failed to restart. Recent broker log:\n' &amp;gt;&amp;amp;2&lt;br /&gt;
            journalctl -u snap.authd-oidc.authd-oidc.service -b -n 60 --no-pager &amp;gt;&amp;amp;2 || true&lt;br /&gt;
&lt;br /&gt;
            if [[ -f &amp;quot;${BROKER_CONF}.pre-${PROGRAM}&amp;quot; ]]; then&lt;br /&gt;
                printf 'ERROR: restoring previous broker configuration and retrying service start\n' &amp;gt;&amp;amp;2&lt;br /&gt;
                cp -a &amp;quot;${BROKER_CONF}.pre-${PROGRAM}&amp;quot; &amp;quot;$BROKER_CONF&amp;quot;&lt;br /&gt;
                systemctl reset-failed snap.authd-oidc.authd-oidc.service || true&lt;br /&gt;
                snap restart authd-oidc &amp;gt;/dev/null || true&lt;br /&gt;
            fi&lt;br /&gt;
            die &amp;quot;authd-oidc broker restart failed; previous config was restored when available&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    if [[ &amp;quot;$SSH_CHANGED&amp;quot; == &amp;quot;true&amp;quot; ]]; then&lt;br /&gt;
        log &amp;quot;Restarting SSH&amp;quot;&lt;br /&gt;
        systemctl restart ssh&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
show_effective_config() {&lt;br /&gt;
    local probe_user=&amp;quot;authd-probe${SSH_SUFFIX}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    log &amp;quot;Service status&amp;quot;&lt;br /&gt;
    systemctl is-active --quiet authd &amp;amp;&amp;amp; echo &amp;quot;authd: active&amp;quot;&lt;br /&gt;
    snap services authd-oidc&lt;br /&gt;
    systemctl is-active --quiet ssh &amp;amp;&amp;amp; echo &amp;quot;ssh: active&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    log &amp;quot;Effective SSH authentication settings for ${probe_user}&amp;quot;&lt;br /&gt;
    sshd -T -C &amp;quot;user=${probe_user},host=localhost,addr=127.0.0.1&amp;quot; 2&amp;gt;/dev/null \&lt;br /&gt;
        | grep -E '^(usepam|passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication|authenticationmethods) ' \&lt;br /&gt;
        || true&lt;br /&gt;
&lt;br /&gt;
    log &amp;quot;Installed versions&amp;quot;&lt;br /&gt;
    dpkg-query -W -f='authd: ${Version}\n' authd 2&amp;gt;/dev/null || true&lt;br /&gt;
    snap list authd-oidc 2&amp;gt;/dev/null || true&lt;br /&gt;
&lt;br /&gt;
    cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
&lt;br /&gt;
Setup complete.&lt;br /&gt;
&lt;br /&gt;
OIDC issuer:      ${ISSUER}&lt;br /&gt;
OIDC client ID:   ${CLIENT_ID}&lt;br /&gt;
SSH suffix:       ${SSH_SUFFIX}&lt;br /&gt;
Allowed users:    ${ALLOWED_USERS}&lt;br /&gt;
SSH policy:       ${SSH_POLICY}&lt;br /&gt;
&lt;br /&gt;
Test from a SECOND terminal while keeping your current administrative&lt;br /&gt;
session open:&lt;br /&gt;
&lt;br /&gt;
  ssh 'user${SSH_SUFFIX}'@&amp;lt;server&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For Keycloak MFA:&lt;br /&gt;
  Configure the Keycloak authentication flow/client so Device Authorization&lt;br /&gt;
  requires OTP/WebAuthn. This script configures the Ubuntu side only.&lt;br /&gt;
&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
    if [[ &amp;quot;$SSH_POLICY&amp;quot; == &amp;quot;publickey+authd&amp;quot; ]]; then&lt;br /&gt;
        cat &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
NOTE:&lt;br /&gt;
  publickey+authd requires the SSH public key to succeed before authd runs.&lt;br /&gt;
  For a brand-new JIT user, pre-provision the account/key or use centralized&lt;br /&gt;
  SSH certificates / AuthorizedKeysCommand. Otherwise first login can fail.&lt;br /&gt;
&lt;br /&gt;
EOF&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AUTHD_CHANGED=false&lt;br /&gt;
BROKER_CHANGED=false&lt;br /&gt;
SSH_CHANGED=false&lt;br /&gt;
&lt;br /&gt;
install_packages&lt;br /&gt;
install_broker&lt;br /&gt;
validate_oidc&lt;br /&gt;
configure_broker_discovery&lt;br /&gt;
configure_broker&lt;br /&gt;
configure_sshd&lt;br /&gt;
restart_if_needed&lt;br /&gt;
show_effective_config&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=2fa_ssh&amp;diff=5751</id>
		<title>2fa ssh</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=2fa_ssh&amp;diff=5751"/>
		<updated>2026-09-15T15:16:02Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;# Using 2FA&lt;br /&gt;
&lt;br /&gt;
Require OTP via Google Authenticator App&lt;br /&gt;
&lt;br /&gt;
publickey,keyboard-interactive (and - requires both)&lt;br /&gt;
&lt;br /&gt;
publickey keyboard-interactive (or - requires one or the other)&lt;br /&gt;
&lt;br /&gt;
## Ubuntu 26.04&lt;br /&gt;
&lt;br /&gt;
Prep user&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
adduser myuser --disabled-password&lt;br /&gt;
# usermod -aG sudo myuser&lt;br /&gt;
visudo  # myuser   ALL=(ALL:ALL) NOPASSWD: ALL&lt;br /&gt;
sudo su - myuser&lt;br /&gt;
mkdir .ssh&lt;br /&gt;
nano .ssh/authorized_keys  # paste public ssh key&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
Update SSHD to require both SSH ID and then OTP&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
#!/usr/bin/env bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y libpam-google-authenticator&lt;br /&gt;
&lt;br /&gt;
# Run as the login user, NOT root/sudo — generates ~/.google_authenticator&lt;br /&gt;
google-authenticator&lt;br /&gt;
&lt;br /&gt;
# Insert BEFORE @include common-auth if you want pubkey+OTP only, no password.&lt;br /&gt;
# As a quick check, comment out the password-based auth line so PAM only asks for the OTP:&lt;br /&gt;
sudo sed -i 's/^@include common-auth/#&amp;amp;/' /etc/pam.d/sshd&lt;br /&gt;
sudo sed -i '1 i\auth required pam_google_authenticator.so' /etc/pam.d/sshd&lt;br /&gt;
&lt;br /&gt;
sudo tee /etc/ssh/sshd_config.d/60-mfa.conf &amp;gt; /dev/null &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
KbdInteractiveAuthentication yes&lt;br /&gt;
PasswordAuthentication no&lt;br /&gt;
AuthenticationMethods publickey,keyboard-interactive&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
sudo systemctl restart ssh&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=2fa_ssh&amp;diff=5750</id>
		<title>2fa ssh</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=2fa_ssh&amp;diff=5750"/>
		<updated>2026-09-15T15:15:32Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;# Using 2FA&lt;br /&gt;
&lt;br /&gt;
Require OTP via Google Authenticator App&lt;br /&gt;
&lt;br /&gt;
publickey,keyboard-interactive (and - requires both)&lt;br /&gt;
&lt;br /&gt;
publickey keyboard-interactive (or - requires one or the other)&lt;br /&gt;
&lt;br /&gt;
## Ubuntu 26.04&lt;br /&gt;
&lt;br /&gt;
Prep user&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
adduser myuser --disabled-password&lt;br /&gt;
# usermod -aG sudo myuser&lt;br /&gt;
visudo  # myuser   ALL=(ALL:ALL) NOPASSWD: ALL&lt;br /&gt;
sudo su - myuser&lt;br /&gt;
mkdir .ssh&lt;br /&gt;
nano .ssh/authorized_keys&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
Update SSHD to require both SSH ID and then OTP&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
#!/usr/bin/env bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y libpam-google-authenticator&lt;br /&gt;
&lt;br /&gt;
# Run as the login user, NOT root/sudo — generates ~/.google_authenticator&lt;br /&gt;
google-authenticator&lt;br /&gt;
&lt;br /&gt;
# Insert BEFORE @include common-auth if you want pubkey+OTP only, no password.&lt;br /&gt;
# As a quick check, comment out the password-based auth line so PAM only asks for the OTP:&lt;br /&gt;
sudo sed -i 's/^@include common-auth/#&amp;amp;/' /etc/pam.d/sshd&lt;br /&gt;
sudo sed -i '1 i\auth required pam_google_authenticator.so' /etc/pam.d/sshd&lt;br /&gt;
&lt;br /&gt;
sudo tee /etc/ssh/sshd_config.d/60-mfa.conf &amp;gt; /dev/null &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
KbdInteractiveAuthentication yes&lt;br /&gt;
PasswordAuthentication no&lt;br /&gt;
AuthenticationMethods publickey,keyboard-interactive&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
sudo systemctl restart ssh&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=2fa_ssh&amp;diff=5749</id>
		<title>2fa ssh</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=2fa_ssh&amp;diff=5749"/>
		<updated>2026-09-15T15:14:08Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;# Using 2FA  Require OTP via Google Authenticator App  publickey,keyboard-interactive (and - requires both)  publickey keyboard-interactive (or - requires one or the other)  #...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;# Using 2FA&lt;br /&gt;
&lt;br /&gt;
Require OTP via Google Authenticator App&lt;br /&gt;
&lt;br /&gt;
publickey,keyboard-interactive (and - requires both)&lt;br /&gt;
&lt;br /&gt;
publickey keyboard-interactive (or - requires one or the other)&lt;br /&gt;
&lt;br /&gt;
## Ubuntu 26.04&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
adduser myuser --disabled-password&lt;br /&gt;
# usermod -aG sudo myuser&lt;br /&gt;
visudo  # myuser   ALL=(ALL:ALL) NOPASSWD: ALL&lt;br /&gt;
sudo su - myuser&lt;br /&gt;
mkdir .ssh&lt;br /&gt;
nano .ssh/authorized_keys&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
#!/usr/bin/env bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y libpam-google-authenticator&lt;br /&gt;
&lt;br /&gt;
# Run as the login user, NOT root/sudo — generates ~/.google_authenticator&lt;br /&gt;
google-authenticator&lt;br /&gt;
&lt;br /&gt;
# Insert BEFORE @include common-auth if you want pubkey+OTP only, no password.&lt;br /&gt;
# As a quick check, comment out the password-based auth line so PAM only asks for the OTP:&lt;br /&gt;
sudo sed -i 's/^@include common-auth/#&amp;amp;/' /etc/pam.d/sshd&lt;br /&gt;
sudo sed -i '1 i\auth required pam_google_authenticator.so' /etc/pam.d/sshd&lt;br /&gt;
&lt;br /&gt;
sudo tee /etc/ssh/sshd_config.d/60-mfa.conf &amp;gt; /dev/null &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
KbdInteractiveAuthentication yes&lt;br /&gt;
PasswordAuthentication no&lt;br /&gt;
AuthenticationMethods publickey,keyboard-interactive&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
sudo systemctl restart ssh&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=CKS_exam_prep&amp;diff=5748</id>
		<title>CKS exam prep</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=CKS_exam_prep&amp;diff=5748"/>
		<updated>2026-09-15T10:05:03Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;https://tech-insider.org/kubernetes-security-hardening-2026/  https://github.com/mikonoid/CKS-exam-cheat-sheets/blob/main/system_hardering/os_footprint.md&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;https://tech-insider.org/kubernetes-security-hardening-2026/&lt;br /&gt;
&lt;br /&gt;
https://github.com/mikonoid/CKS-exam-cheat-sheets/blob/main/system_hardering/os_footprint.md&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Windows_11_prep&amp;diff=5747</id>
		<title>Windows 11 prep</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Windows_11_prep&amp;diff=5747"/>
		<updated>2026-09-14T19:00:56Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;## Windows 11 Prep&lt;br /&gt;
&lt;br /&gt;
## Common Basic Apps&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
winget install -e --id Google.GoogleDrive&lt;br /&gt;
winget install SlackTechnologies.Slack --source winget&lt;br /&gt;
winget install -e --id Zoom.Zoom --source winget&lt;br /&gt;
winget install Google.Chrome --source winget&lt;br /&gt;
winget install Bitwarden.Bitwarden --source winget&lt;br /&gt;
winget update&lt;br /&gt;
winget update --all&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## SSH&lt;br /&gt;
```&lt;br /&gt;
ssh-keygen&lt;br /&gt;
cat .\.ssh\id_ed25519.pub&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## AI CLI&lt;br /&gt;
```&lt;br /&gt;
curl -fsSL https://antigravity.google/cli/install.sh | bash&lt;br /&gt;
curl -fsSL https://gh.io/copilot-install | bash&lt;br /&gt;
curl -fsSL https://claude.ai/install.sh | bash&lt;br /&gt;
curl -fsSL https://chatgpt.com/codex/install.sh | sh&lt;br /&gt;
powershell -ExecutionPolicy ByPass -c &amp;quot;irm https://chatgpt.com/codex/install.ps1 | iex&amp;quot;&lt;br /&gt;
```&lt;br /&gt;
run codex in powershell once as admin to setup sandbox as it needs elevated privileges.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
## Useful Scripts&lt;br /&gt;
&lt;br /&gt;
stay-awake.ps1&lt;br /&gt;
```&lt;br /&gt;
$shell = New-Object -ComObject WScript.Shell&lt;br /&gt;
while ($true) {&lt;br /&gt;
    $shell.SendKeys(&amp;quot;{F15}&amp;quot;)&lt;br /&gt;
    Start-Sleep -Seconds 120&lt;br /&gt;
}&lt;br /&gt;
# Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process&lt;br /&gt;
# Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## More winget apps&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
winget install -e --id CrystalDewWorld.CrystalDiskInfo&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## And even more apps if really wanted&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
https://www.resplendence.com/whysoslow&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Windows_11_prep&amp;diff=5746</id>
		<title>Windows 11 prep</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Windows_11_prep&amp;diff=5746"/>
		<updated>2026-09-14T18:58:57Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;## Windows 11 Prep  ## Common Basic Apps  ``` winget install -e --id Google.GoogleDrive winget install SlackTechnologies.Slack --source winget winget install -e --id Zoom.Zoom...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;## Windows 11 Prep&lt;br /&gt;
&lt;br /&gt;
## Common Basic Apps&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
winget install -e --id Google.GoogleDrive&lt;br /&gt;
winget install SlackTechnologies.Slack --source winget&lt;br /&gt;
winget install -e --id Zoom.Zoom --source winget&lt;br /&gt;
winget install Google.Chrome --source winget&lt;br /&gt;
winget install Bitwarden.Bitwarden --source winget&lt;br /&gt;
winget update&lt;br /&gt;
winget update --all&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## SSH&lt;br /&gt;
```&lt;br /&gt;
ssh-keygen&lt;br /&gt;
cat .\.ssh\id_ed25519.pub&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## AI CLI&lt;br /&gt;
```&lt;br /&gt;
curl -fsSL https://antigravity.google/cli/install.sh | bash&lt;br /&gt;
curl -fsSL https://gh.io/copilot-install | bash&lt;br /&gt;
curl -fsSL https://claude.ai/install.sh | bash&lt;br /&gt;
curl -fsSL https://chatgpt.com/codex/install.sh | sh&lt;br /&gt;
powershell -ExecutionPolicy ByPass -c &amp;quot;irm https://chatgpt.com/codex/install.ps1 | iex&amp;quot;&lt;br /&gt;
```&lt;br /&gt;
run codex in powershell once as admin to setup sandbox as it needs elevated privileges.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
## Useful Scripts&lt;br /&gt;
&lt;br /&gt;
stay-awake.ps1&lt;br /&gt;
```&lt;br /&gt;
$shell = New-Object -ComObject WScript.Shell&lt;br /&gt;
while ($true) {&lt;br /&gt;
    $shell.SendKeys(&amp;quot;{F15}&amp;quot;)&lt;br /&gt;
    Start-Sleep -Seconds 120&lt;br /&gt;
}&lt;br /&gt;
# Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process&lt;br /&gt;
# Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## More winget apps&lt;br /&gt;
```&lt;br /&gt;
winget install -e --id CrystalDewWorld.CrystalDiskInfo&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Ai_cli_install&amp;diff=5745</id>
		<title>Ai cli install</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Ai_cli_install&amp;diff=5745"/>
		<updated>2026-09-14T17:26:13Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;``` curl -fsSL https://antigravity.google/cli/install.sh | bash curl -fsSL https://gh.io/copilot-install | bash curl -fsSL https://claude.ai/install.sh | bash curl -fsSL https...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;```&lt;br /&gt;
curl -fsSL https://antigravity.google/cli/install.sh | bash&lt;br /&gt;
curl -fsSL https://gh.io/copilot-install | bash&lt;br /&gt;
curl -fsSL https://claude.ai/install.sh | bash&lt;br /&gt;
curl -fsSL https://chatgpt.com/codex/install.sh | sh&lt;br /&gt;
powershell -ExecutionPolicy ByPass -c &amp;quot;irm https://chatgpt.com/codex/install.ps1 | iex&amp;quot;&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
for windows codex run once in run with administrator privledges terminal to setup sandbox via escalation then you can run in normal terminal.&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Linux_Windows_USB_Drive_format&amp;diff=5744</id>
		<title>Linux Windows USB Drive format</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Linux_Windows_USB_Drive_format&amp;diff=5744"/>
		<updated>2026-09-07T22:04:35Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;```&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install exfatprogs&lt;br /&gt;
sudo umount /dev/sdc1&lt;br /&gt;
sudo wipefs -a /dev/sdc&lt;br /&gt;
sudo parted /dev/sdc mklabel gpt&lt;br /&gt;
sudo parted -a opt /dev/sdc mkpart primary 0% 100%&lt;br /&gt;
sudo partprobe /dev/sdc&lt;br /&gt;
sudo mkfs.exfat /dev/sdc1&lt;br /&gt;
sudo mkfs.exfat -n usb2Texfat /dev/sdc1&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
sudo parted /dev/sda --script mklabel gpt&lt;br /&gt;
sudo parted /dev/sda --script mkpart primary 1MiB 100%&lt;br /&gt;
sudo parted /dev/sda --script set 1 msftdata on&lt;br /&gt;
sudo partprobe /dev/sda&lt;br /&gt;
```&lt;br /&gt;
nvme&lt;br /&gt;
```&lt;br /&gt;
sudo parted /dev/sdc --script mklabel gpt&lt;br /&gt;
sudo parted /dev/sdc --script mkpart primary 65535s 100%&lt;br /&gt;
sudo parted /dev/sdc --script set 1 msftdata on&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
wipe if needed for existing&lt;br /&gt;
```&lt;br /&gt;
sudo umount /dev/sda*&lt;br /&gt;
sudo wipefs -a /dev/sda&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
force in needed&lt;br /&gt;
```&lt;br /&gt;
sudo wipefs -a -f /dev/sda&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Linux_Windows_USB_Drive_format&amp;diff=5743</id>
		<title>Linux Windows USB Drive format</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Linux_Windows_USB_Drive_format&amp;diff=5743"/>
		<updated>2026-09-07T22:02:22Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;```&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install exfatprogs&lt;br /&gt;
sudo umount /dev/sdc1&lt;br /&gt;
sudo wipefs -a /dev/sdc&lt;br /&gt;
sudo parted /dev/sdc mklabel gpt&lt;br /&gt;
sudo parted -a opt /dev/sdc mkpart primary 0% 100%&lt;br /&gt;
sudo partprobe /dev/sdc&lt;br /&gt;
sudo mkfs.exfat /dev/sdc1&lt;br /&gt;
sudo mkfs.exfat -n usb1t /dev/sdc1&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
sudo parted /dev/sda --script mklabel gpt&lt;br /&gt;
sudo parted /dev/sda --script mkpart primary 1MiB 100%&lt;br /&gt;
sudo parted /dev/sda --script set 1 msftdata on&lt;br /&gt;
sudo partprobe /dev/sda&lt;br /&gt;
```&lt;br /&gt;
nvme&lt;br /&gt;
```&lt;br /&gt;
sudo parted /dev/sdc --script mklabel gpt&lt;br /&gt;
sudo parted /dev/sdc --script mkpart primary 65535s 100%&lt;br /&gt;
sudo parted /dev/sdc --script set 1 msftdata on&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
wipe if needed for existing&lt;br /&gt;
```&lt;br /&gt;
sudo umount /dev/sda*&lt;br /&gt;
sudo wipefs -a /dev/sda&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
force in needed&lt;br /&gt;
```&lt;br /&gt;
sudo wipefs -a -f /dev/sda&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Linux_Windows_USB_Drive_format&amp;diff=5742</id>
		<title>Linux Windows USB Drive format</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Linux_Windows_USB_Drive_format&amp;diff=5742"/>
		<updated>2026-09-07T22:00:20Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;```&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install exfatprogs&lt;br /&gt;
sudo umount /dev/sdc1&lt;br /&gt;
sudo wipefs -a /dev/sdc&lt;br /&gt;
sudo parted /dev/sdc mklabel gpt&lt;br /&gt;
sudo parted -a opt /dev/sdc mkpart primary 0% 100%&lt;br /&gt;
sudo partprobe /dev/sdc&lt;br /&gt;
sudo mkfs.exfat /dev/sdc1&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
sudo parted /dev/sda --script mklabel gpt&lt;br /&gt;
sudo parted /dev/sda --script mkpart primary 1MiB 100%&lt;br /&gt;
sudo parted /dev/sda --script set 1 msftdata on&lt;br /&gt;
sudo partprobe /dev/sda&lt;br /&gt;
```&lt;br /&gt;
nvme&lt;br /&gt;
```&lt;br /&gt;
sudo parted /dev/sdc --script mklabel gpt&lt;br /&gt;
sudo parted /dev/sdc --script mkpart primary 65535s 100%&lt;br /&gt;
sudo parted /dev/sdc --script set 1 msftdata on&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
wipe if needed for existing&lt;br /&gt;
```&lt;br /&gt;
sudo umount /dev/sda*&lt;br /&gt;
sudo wipefs -a /dev/sda&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
force in needed&lt;br /&gt;
```&lt;br /&gt;
sudo wipefs -a -f /dev/sda&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Linux_Windows_USB_Drive_format&amp;diff=5741</id>
		<title>Linux Windows USB Drive format</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Linux_Windows_USB_Drive_format&amp;diff=5741"/>
		<updated>2026-09-07T21:45:10Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;```&lt;br /&gt;
sudo parted /dev/sda --script mklabel gpt&lt;br /&gt;
sudo parted /dev/sda --script mkpart primary 1MiB 100%&lt;br /&gt;
sudo parted /dev/sda --script set 1 msftdata on&lt;br /&gt;
sudo partprobe /dev/sda&lt;br /&gt;
```&lt;br /&gt;
nvme&lt;br /&gt;
```&lt;br /&gt;
sudo parted /dev/sdc --script mklabel gpt&lt;br /&gt;
sudo parted /dev/sdc --script mkpart primary 65535s 100%&lt;br /&gt;
sudo parted /dev/sdc --script set 1 msftdata on&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
wipe if needed for existing&lt;br /&gt;
```&lt;br /&gt;
sudo umount /dev/sda*&lt;br /&gt;
sudo wipefs -a /dev/sda&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
force in needed&lt;br /&gt;
```&lt;br /&gt;
sudo wipefs -a -f /dev/sda&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Linux_Windows_USB_Drive_format&amp;diff=5740</id>
		<title>Linux Windows USB Drive format</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Linux_Windows_USB_Drive_format&amp;diff=5740"/>
		<updated>2026-09-05T19:20:38Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;```&lt;br /&gt;
sudo parted /dev/sda --script mklabel gpt&lt;br /&gt;
sudo parted /dev/sda --script mkpart primary 1MiB 100%&lt;br /&gt;
sudo parted /dev/sda --script set 1 msftdata on&lt;br /&gt;
sudo partprobe /dev/sda&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
wipe if needed for existing&lt;br /&gt;
```&lt;br /&gt;
sudo umount /dev/sda*&lt;br /&gt;
sudo wipefs -a /dev/sda&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
force in needed&lt;br /&gt;
```&lt;br /&gt;
sudo wipefs -a -f /dev/sda&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Linux_Windows_USB_Drive_format&amp;diff=5739</id>
		<title>Linux Windows USB Drive format</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Linux_Windows_USB_Drive_format&amp;diff=5739"/>
		<updated>2026-09-05T19:18:23Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;``` sudo parted /dev/sda --script mklabel gpt sudo parted /dev/sda --script mkpart primary 1MiB 100% sudo parted /dev/sda --script set 1 msftdata on sudo partprobe /dev/sda ```&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;```&lt;br /&gt;
sudo parted /dev/sda --script mklabel gpt&lt;br /&gt;
sudo parted /dev/sda --script mkpart primary 1MiB 100%&lt;br /&gt;
sudo parted /dev/sda --script set 1 msftdata on&lt;br /&gt;
sudo partprobe /dev/sda&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Kong_volcano_sdk&amp;diff=5738</id>
		<title>Kong volcano sdk</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Kong_volcano_sdk&amp;diff=5738"/>
		<updated>2026-08-17T16:28:36Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;## What Kong AI Gateway Does Automatically&lt;br /&gt;
&lt;br /&gt;
* Semantic Caching: If a user asks the exact same question tomorrow, Kong serves the cached response instantly without incurring costs or hitting the LLM again.&lt;br /&gt;
* Prompt Guardrails: Kong blocks malicious prompt injections or sensitive company data from leaking out to the LLM providers.&lt;br /&gt;
* Failover: If OpenAI drops offline mid-execution, Kong can automatically route GPT requests to Azure or Anthropic without breaking your Volcano code.&lt;br /&gt;
&lt;br /&gt;
## Connecting Volcano to Kong AI Gateway&lt;br /&gt;
Volcano SDK is designed to work seamlessly with the Kong AI Gateway (via Kong Advanced Gateway Services). By routing your agent's LLM calls through Kong, you get instant access to enterprise-grade security, request auditing, rate-limiting, and semantic caching.&lt;br /&gt;
Here is a conceptual example of how to connect the two and build an MCP-native workflow.&lt;br /&gt;
------------------------------&lt;br /&gt;
## Step 1: Configure the Volcano Client&lt;br /&gt;
Instead of hitting the LLM provider (like OpenAI or Anthropic) directly, you point the Volcano client to your Kong AI Gateway endpoint. Kong will manage your API keys, load-balancing, and security controls behind the scenes.&lt;br /&gt;
&lt;br /&gt;
import { Volcano, ModelProvider } from '@kong/volcano-sdk';&lt;br /&gt;
// Initialize Volcano to route through your Kong AI Gatewayconst volcano = new Volcano({&lt;br /&gt;
  baseUrl: &amp;quot;https://your-kong-gateway-domain.com&amp;quot;, &lt;br /&gt;
  apiKey: process.env.KONG_AI_GATEWAY_TOKEN, // Your Kong access token&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
------------------------------&lt;br /&gt;
## Step 2: Define an MCP Tool&lt;br /&gt;
The Model Context Protocol (MCP) lets you expose external tools and data to your agent. In this example, we define an MCP tool that connects to a internal company database to fetch shipping updates.&lt;br /&gt;
&lt;br /&gt;
import { McpServer } from '@modelcontextprotocol/sdk/server';&lt;br /&gt;
const mcpServer = new McpServer({&lt;br /&gt;
  name: &amp;quot;shipping-tracker&amp;quot;,&lt;br /&gt;
  version: &amp;quot;1.0.0&amp;quot;&lt;br /&gt;
});&lt;br /&gt;
// Register a tool on the MCP server&lt;br /&gt;
mcpServer.tool(&amp;quot;get_shipping_status&amp;quot;, { orderId: z.string() }, async ({ orderId }) =&amp;gt; {&lt;br /&gt;
  // Logic to fetch real-world data&lt;br /&gt;
  const status = await fetchInternalDb(orderId); &lt;br /&gt;
  return { content: [{ type: &amp;quot;text&amp;quot;, text: `Order status: ${status}` }] };&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
------------------------------&lt;br /&gt;
## Step 3: Run the Multi-Model Workflow&lt;br /&gt;
Volcano allows you to chain multiple models together. In this workflow, Claude 3.5 Sonnet acts as the high-reasoning &amp;quot;brain&amp;quot; to understand the customer's request and call the MCP tool. Then, GPT-4o mini handles the cheaper, faster task of drafting a polite email response.&lt;br /&gt;
&lt;br /&gt;
async function handleCustomerInquiry(userPrompt: string) {&lt;br /&gt;
  &lt;br /&gt;
  // 1. High-reasoning model analyzes prompt and triggers the MCP tool&lt;br /&gt;
  const agentRunner = await volcano.agents.create({&lt;br /&gt;
    model: &amp;quot;kong-managed-claude-sonnet&amp;quot;, // Configured routing inside Kong&lt;br /&gt;
    tools: [mcpServer.getTool(&amp;quot;get_shipping_status&amp;quot;)],&lt;br /&gt;
    prompt: userPrompt&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  const toolOutput = await agentRunner.execute();&lt;br /&gt;
&lt;br /&gt;
  // 2. Chained workflow shifts to a faster, cheaper model for drafting&lt;br /&gt;
  const finalResponse = await volcano.chat.completion({&lt;br /&gt;
    model: &amp;quot;kong-managed-gpt4o-mini&amp;quot;, // Routed and cached by Kong&lt;br /&gt;
    messages: [&lt;br /&gt;
      { role: &amp;quot;system&amp;quot;, content: &amp;quot;You are a polite customer support agent.&amp;quot; },&lt;br /&gt;
      { role: &amp;quot;assistant&amp;quot;, content: `Internal tool data: ${toolOutput}` },&lt;br /&gt;
      { role: &amp;quot;user&amp;quot;, content: &amp;quot;Draft an email update to the customer based on this.&amp;quot; }&lt;br /&gt;
    ]&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  console.log(finalResponse.text);&lt;br /&gt;
}&lt;br /&gt;
// Example usage&lt;br /&gt;
handleCustomerInquiry(&amp;quot;Where is my package for order #98765?&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
------------------------------&lt;br /&gt;
## What Kong AI Gateway Does Automatically&lt;br /&gt;
&lt;br /&gt;
* Semantic Caching: If a user asks the exact same question tomorrow, Kong serves the cached response instantly without incurring costs or hitting the LLM again.&lt;br /&gt;
* Prompt Guardrails: Kong blocks malicious prompt injections or sensitive company data from leaking out to the LLM providers.&lt;br /&gt;
* Failover: If OpenAI drops offline mid-execution, Kong can automatically route GPT requests to Azure or Anthropic without breaking your Volcano code.&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Kong_volcano_sdk&amp;diff=5737</id>
		<title>Kong volcano sdk</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Kong_volcano_sdk&amp;diff=5737"/>
		<updated>2026-08-17T16:25:01Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;What Kong AI Gateway Does AutomaticallySemantic Caching: If a user asks the exact same question tomorrow, Kong serves the cached response instantly without incurring costs or...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;What Kong AI Gateway Does AutomaticallySemantic Caching: If a user asks the exact same question tomorrow, Kong serves the cached response instantly without incurring costs or hitting the LLM again.Prompt Guardrails: Kong blocks malicious prompt injections or sensitive company data from leaking out to the LLM providers.Failover: If OpenAI drops offline mid-execution, Kong can automatically route GPT requests to Azure or Anthropic without breaking your Volcano code.Would you like to explore how to set up the semantic caching plugin inside Kong, or do you need help writing a custom MCP server for your specific database?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
## Connecting Volcano to Kong AI Gateway&lt;br /&gt;
Volcano SDK is designed to work seamlessly with the Kong AI Gateway (via Kong Advanced Gateway Services). By routing your agent's LLM calls through Kong, you get instant access to enterprise-grade security, request auditing, rate-limiting, and semantic caching.&lt;br /&gt;
Here is a conceptual example of how to connect the two and build an MCP-native workflow.&lt;br /&gt;
------------------------------&lt;br /&gt;
## Step 1: Configure the Volcano Client&lt;br /&gt;
Instead of hitting the LLM provider (like OpenAI or Anthropic) directly, you point the Volcano client to your Kong AI Gateway endpoint. Kong will manage your API keys, load-balancing, and security controls behind the scenes.&lt;br /&gt;
&lt;br /&gt;
import { Volcano, ModelProvider } from '@kong/volcano-sdk';&lt;br /&gt;
// Initialize Volcano to route through your Kong AI Gatewayconst volcano = new Volcano({&lt;br /&gt;
  baseUrl: &amp;quot;https://your-kong-gateway-domain.com&amp;quot;, &lt;br /&gt;
  apiKey: process.env.KONG_AI_GATEWAY_TOKEN, // Your Kong access token&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
------------------------------&lt;br /&gt;
## Step 2: Define an MCP Tool&lt;br /&gt;
The Model Context Protocol (MCP) lets you expose external tools and data to your agent. In this example, we define an MCP tool that connects to a internal company database to fetch shipping updates.&lt;br /&gt;
&lt;br /&gt;
import { McpServer } from '@modelcontextprotocol/sdk/server';&lt;br /&gt;
const mcpServer = new McpServer({&lt;br /&gt;
  name: &amp;quot;shipping-tracker&amp;quot;,&lt;br /&gt;
  version: &amp;quot;1.0.0&amp;quot;&lt;br /&gt;
});&lt;br /&gt;
// Register a tool on the MCP server&lt;br /&gt;
mcpServer.tool(&amp;quot;get_shipping_status&amp;quot;, { orderId: z.string() }, async ({ orderId }) =&amp;gt; {&lt;br /&gt;
  // Logic to fetch real-world data&lt;br /&gt;
  const status = await fetchInternalDb(orderId); &lt;br /&gt;
  return { content: [{ type: &amp;quot;text&amp;quot;, text: `Order status: ${status}` }] };&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
------------------------------&lt;br /&gt;
## Step 3: Run the Multi-Model Workflow&lt;br /&gt;
Volcano allows you to chain multiple models together. In this workflow, Claude 3.5 Sonnet acts as the high-reasoning &amp;quot;brain&amp;quot; to understand the customer's request and call the MCP tool. Then, GPT-4o mini handles the cheaper, faster task of drafting a polite email response.&lt;br /&gt;
&lt;br /&gt;
async function handleCustomerInquiry(userPrompt: string) {&lt;br /&gt;
  &lt;br /&gt;
  // 1. High-reasoning model analyzes prompt and triggers the MCP tool&lt;br /&gt;
  const agentRunner = await volcano.agents.create({&lt;br /&gt;
    model: &amp;quot;kong-managed-claude-sonnet&amp;quot;, // Configured routing inside Kong&lt;br /&gt;
    tools: [mcpServer.getTool(&amp;quot;get_shipping_status&amp;quot;)],&lt;br /&gt;
    prompt: userPrompt&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  const toolOutput = await agentRunner.execute();&lt;br /&gt;
&lt;br /&gt;
  // 2. Chained workflow shifts to a faster, cheaper model for drafting&lt;br /&gt;
  const finalResponse = await volcano.chat.completion({&lt;br /&gt;
    model: &amp;quot;kong-managed-gpt4o-mini&amp;quot;, // Routed and cached by Kong&lt;br /&gt;
    messages: [&lt;br /&gt;
      { role: &amp;quot;system&amp;quot;, content: &amp;quot;You are a polite customer support agent.&amp;quot; },&lt;br /&gt;
      { role: &amp;quot;assistant&amp;quot;, content: `Internal tool data: ${toolOutput}` },&lt;br /&gt;
      { role: &amp;quot;user&amp;quot;, content: &amp;quot;Draft an email update to the customer based on this.&amp;quot; }&lt;br /&gt;
    ]&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  console.log(finalResponse.text);&lt;br /&gt;
}&lt;br /&gt;
// Example usage&lt;br /&gt;
handleCustomerInquiry(&amp;quot;Where is my package for order #98765?&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
------------------------------&lt;br /&gt;
## What Kong AI Gateway Does Automatically&lt;br /&gt;
&lt;br /&gt;
* Semantic Caching: If a user asks the exact same question tomorrow, Kong serves the cached response instantly without incurring costs or hitting the LLM again.&lt;br /&gt;
* Prompt Guardrails: Kong blocks malicious prompt injections or sensitive company data from leaking out to the LLM providers.&lt;br /&gt;
* Failover: If OpenAI drops offline mid-execution, Kong can automatically route GPT requests to Azure or Anthropic without breaking your Volcano code.&lt;br /&gt;
&lt;br /&gt;
Would you like to explore how to set up the semantic caching plugin inside Kong, or do you need help writing a custom MCP server for your specific database?&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Python_on_windows&amp;diff=5736</id>
		<title>Python on windows</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Python_on_windows&amp;diff=5736"/>
		<updated>2026-08-11T01:05:08Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;``` winget install Python.Python.3.14  pip install pipx pipx ensurepath pipx install ipython ```&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;```&lt;br /&gt;
winget install Python.Python.3.14&lt;br /&gt;
&lt;br /&gt;
pip install pipx&lt;br /&gt;
pipx ensurepath&lt;br /&gt;
pipx install ipython&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Kube-virt_-_k8s_in_k8s&amp;diff=5735</id>
		<title>Kube-virt - k8s in k8s</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Kube-virt_-_k8s_in_k8s&amp;diff=5735"/>
		<updated>2026-08-02T15:11:02Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;One of the most popular and powerful use cases for KubeVirt, often referred to as **Kubernetes-in-Kubernetes** or **Tenant Clusters**.  In this architecture, your underlying b...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;One of the most popular and powerful use cases for KubeVirt, often referred to as **Kubernetes-in-Kubernetes** or **Tenant Clusters**.&lt;br /&gt;
&lt;br /&gt;
In this architecture, your underlying bare-metal Kubernetes cluster acts as the **infrastructure management layer** (the control plane for hardware), and KubeVirt provisions VMs that join together to form separate, isolated **tenant Kubernetes clusters**.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## Why Do This? (Key Benefits)&lt;br /&gt;
&lt;br /&gt;
* **Multi-Tenancy &amp;amp; Hard Isolation:** Containers share the host kernel. If you need strict security boundaries between teams or environments (e.g., Prod vs. Dev), VMs provide hardware-level virtualization isolation.&lt;br /&gt;
* **Infrastructure-as-Code (GitOps):** You can manage the lifecycle of entire target Kubernetes clusters declaratively using standard `kubectl` commands or GitOps pipelines (ArgoCD/Flux).&lt;br /&gt;
* **Cluster API (CAPI) Integration:** Using the **Cluster API Provider KubeVirt (CAPK)**, you can declare a target Kubernetes cluster in YAML, and CAPI + KubeVirt will automatically spin up the VMs, run `cloud-init` to install Kubernetes on them, and join them into a fully functional tenant cluster.&lt;br /&gt;
* **Testing &amp;amp; Sandbox Environments:** It allows developers to spin up ephemeral, full-featured Kubernetes clusters (with custom kernel modules or CNI configurations) without needing real physical hardware.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## How the Architecture Looks&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
┌────────────────────────────────────────────────────────────────────────┐&lt;br /&gt;
│                      BARE-METAL KUBERNETES CLUSTER                     │&lt;br /&gt;
│  (Management Cluster: Physical Servers + Storage + KubeVirt Operator)  │&lt;br /&gt;
│                                                                        │&lt;br /&gt;
│  ┌──────────────────────────────────────────────────────────────────┐  │&lt;br /&gt;
│  │                    TENANT KUBERNETES CLUSTER                     │  │&lt;br /&gt;
│  │                                                                  │  │&lt;br /&gt;
│  │   ┌────────────────────┐              ┌────────────────────┐     │  │&lt;br /&gt;
│  │   │ KubeVirt VM 1      │              │ KubeVirt VM 2      │     │  │&lt;br /&gt;
│  │   │ (Tenant Control)   │              │ (Tenant Worker)    │     │  │&lt;br /&gt;
│  │   │                    │              │                    │     │  │&lt;br /&gt;
│  │   │ ┌────────────────┐ │   Network    │ ┌────────────────┐ │     │  │&lt;br /&gt;
│  │   │ │ Tenant K8s API │ ◄────────────────►│ Tenant Pods    │ │     │  │&lt;br /&gt;
│  │   │ └────────────────┘ │ (Overlay/VLAN) │ └────────────────┘ │     │  │&lt;br /&gt;
│  │   └────────────────────┘              └────────────────────┘     │  │&lt;br /&gt;
│  └──────────────────────────────────────────────────────────────────┘  │&lt;br /&gt;
└────────────────────────────────────────────────────────────────────────┘&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## Typical Workflow&lt;br /&gt;
&lt;br /&gt;
1. **Deploy Bare-Metal K8s:** You set up physical servers with a Kubernetes distribution (e.g., Talos, RKE2, OpenShift, or upstream K8s).&lt;br /&gt;
2. **Install KubeVirt:** Deploy the KubeVirt operator on the bare-metal cluster.&lt;br /&gt;
3. **Use Cluster API (CAPK):** Apply a manifest defining a tenant cluster (e.g., 1 Control Plane VM, 3 Worker VMs).&lt;br /&gt;
4. **Bootstrapping via cloud-init:** KubeVirt boots the VMs, and `cloud-init` runs scripts (like `kubeadm join` or `k3s`) to wire the nodes into the new nested cluster.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## Things to Consider&lt;br /&gt;
&lt;br /&gt;
* **Nested Virtualization / Overhead:** Ensure hardware-assisted virtualization (`/dev/kvm`) is available on your bare-metal nodes so KubeVirt runs near native speed.&lt;br /&gt;
* **Networking:** You’ll need a CNI or plugin like **Multus** or **OVN-Kubernetes** on the bare-metal cluster to assign dedicated IPs or VLANs to the VM nodes so the tenant cluster can route traffic cleanly.&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Openssl&amp;diff=5734</id>
		<title>Openssl</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Openssl&amp;diff=5734"/>
		<updated>2026-08-01T22:06:24Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;openssl s_client -connect docker2:993 -servername docker2 &amp;lt;/dev/null 2&amp;gt;/dev/null | openssl x509 -noout -subject&lt;br /&gt;
&lt;br /&gt;
https://www.digitalocean.com/community/tutorials/openssl-essentials-working-with-ssl-certificates-private-keys-and-csrs&lt;br /&gt;
&lt;br /&gt;
https://www.openssl.org/docs/manmaster/man5/x509v3_config.html&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
openssl s_client -connect ldaps.example.com:636 -showcerts &amp;lt;/dev/null 2&amp;gt;/dev/null | openssl x509 -noout -ext subjectAltName&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
echo -n | openssl s_client -connect google.com:443 2&amp;gt;/dev/null | openssl x509 -noout -text | grep -E 'Subject:|X509v3 Subject Alternative Name:'&lt;br /&gt;
openssl s_client -connect www.uvoo.me:443&lt;br /&gt;
echo | openssl s_client -showcerts -servername www.uvoo.me -connect www.uvoo.me:443 2&amp;gt;/dev/null | openssl x509 -inform pem -noout -text&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
Quicks like get subject alt names&lt;br /&gt;
```&lt;br /&gt;
openssl x509 -text -in my.crt.pem&lt;br /&gt;
openssl x509 -noout -ext subjectAltName&lt;br /&gt;
openssl x509 -noout -dates&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# self signed cert pem&lt;br /&gt;
```&lt;br /&gt;
fqdn=${FQDN:-autogenerated-selfsigned-cert}; openssl req -x509 -nodes -days 3650 -newkey rsa:4096 -keyout selfsigned.key -out selfsigned.crt -subj &amp;quot;/CN=$fqdn&amp;quot; -addext &amp;quot;subjectAltName = DNS:$fqdn&amp;quot;&lt;br /&gt;
&lt;br /&gt;
cat selfsigned.crt selfsigned.key &amp;gt;&amp;gt; aaa.pem&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# Extract Certs from .pfx PKCS#12&lt;br /&gt;
```&lt;br /&gt;
  openssl pkcs12 -passin env:PASSIN -in ${cn}.pfx -nocerts -nodes &amp;gt; ${cn}.key.pem&lt;br /&gt;
  openssl pkcs12 -passin env:PASSIN -in ${cn}.pfx -clcerts -nokeys &amp;gt; ${cn}.crt.pem&lt;br /&gt;
  openssl pkcs12 -passin env:PASSIN -in ${cn}.pfx -cacerts -nokeys -chain &amp;gt; ${cn}.ca.crt.pem&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# validate&lt;br /&gt;
 https://link.medium.com/9Cif8SlO9ub&lt;br /&gt;
&lt;br /&gt;
# Pem&lt;br /&gt;
```&lt;br /&gt;
Donwload pem&lt;br /&gt;
openssl rsa -in ~/myhost.example.com.pem -out ~/myhost.example.com.pem.key&lt;br /&gt;
and replace encrypted private key at bottom in ~/myhost.example.com.pem with ~/myhost.example.com.pem.key&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# pkcs12&lt;br /&gt;
```&lt;br /&gt;
echo -n | openssl s_client -showcerts -connect $HOST:$PORTNUMBER -servername $SERVERNAME \&lt;br /&gt;
    | openssl x509 &amp;gt; /tmp/$SERVERNAME.cert&lt;br /&gt;
&lt;br /&gt;
openssl pkcs12 -in &amp;lt;filename.pfx&amp;gt; -nocerts -nodes | sed -ne '/-BEGIN PRIVATE KEY-/,/-END PRIVATE KEY-/p' &amp;gt; &amp;lt;clientcert.key&amp;gt;&lt;br /&gt;
openssl pkcs12 -in &amp;lt;filename.pfx&amp;gt; -clcerts -nokeys | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' &amp;gt; &amp;lt;clientcert.cer&amp;gt;&lt;br /&gt;
openssl pkcs12 -in &amp;lt;filename.pfx&amp;gt; -cacerts -nokeys -chain | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' &amp;gt; &amp;lt;cacerts.cer&amp;gt;&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
Expiration dates&lt;br /&gt;
```&lt;br /&gt;
host=example.com; port=443; echo &amp;quot;Q&amp;quot; | openssl s_client -servername $host -connect $host:$port | openssl x509 -noout -dates&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
Show Certs&lt;br /&gt;
```&lt;br /&gt;
host=example.com; port=443; echo &amp;quot;Q&amp;quot; | openssl s_client -showcerts -connect $host:$port&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
Get CA cert PEM from URL and use with curl&lt;br /&gt;
```&lt;br /&gt;
host=example.com; port=443; echo quit | openssl s_client -showcerts -servername server -connect $host:$port &amp;gt; cacert.pem&lt;br /&gt;
host=example.com; port=443; curl -vv --cacert cacert.pem https://$host:$port/notifications/health&lt;br /&gt;
```&lt;br /&gt;
```&lt;br /&gt;
openssl pkcs12 -in example.io.pfx -out example.io.pem -nodes&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# PKS&lt;br /&gt;
```&lt;br /&gt;
# kb.dev.example.com.pks&lt;br /&gt;
host=kb.dev.example.com; openssl pkcs12 -in $host.pfx -out $host.key -nocerts -nodes&lt;br /&gt;
host=kb.dev.example.com; openssl pkcs12 -in $host.pfx -out $host.crt -clcerts -nokeys&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
openssl pkcs12 -in &amp;lt;filename.pfx&amp;gt; -nocerts -nodes | sed -ne '/-BEGIN PRIVATE KEY-/,/-END PRIVATE KEY-/p' &amp;gt; &amp;lt;clientcert.key&amp;gt;&lt;br /&gt;
openssl pkcs12 -in &amp;lt;filename.pfx&amp;gt; -clcerts -nokeys | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' &amp;gt; &amp;lt;clientcert.cer&amp;gt;&lt;br /&gt;
openssl pkcs12 -in &amp;lt;filename.pfx&amp;gt; -cacerts -nokeys -chain | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' &amp;gt; &amp;lt;cacerts.cer&amp;gt;&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
cipher scans&lt;br /&gt;
```&lt;br /&gt;
echo quit | openssl s_client -connect www.uvoo.io:443 -msg -debug 2&amp;gt;/dev/null | grep &amp;quot;Cipher    :&amp;quot;&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
Self signed https certs&lt;br /&gt;
```&lt;br /&gt;
View &amp;amp; Download in Firefox&lt;br /&gt;
PEM (cert)PEM (chain)&lt;br /&gt;
export API_HOST=example.com&lt;br /&gt;
echo quit | openssl s_client -showcerts -servername &amp;quot;${API_HOST}&amp;quot; -connect &amp;quot;${API_HOST}&amp;quot;:443 &amp;gt; cacert.pem&lt;br /&gt;
curl --cacert cacert.pem --location --silent https://${API_HOST}&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
https://www.redhat.com/sysadmin/6-openssl-commands&lt;br /&gt;
&lt;br /&gt;
# Extract cert and key&lt;br /&gt;
```&lt;br /&gt;
openssl pkcs12 -in domain.pfx -clcerts -nokeys -out domain.crt&lt;br /&gt;
openssl pkcs12 -in domain.pfx -nocerts -nodes  -out domain.key   &lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# Update your Apache configuration file with:&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&amp;lt;VirtualHost 192.168.0.1:443&amp;gt;&lt;br /&gt;
 ...&lt;br /&gt;
 SSLEngine on&lt;br /&gt;
 SSLCertificateFile /path/to/domain.crt&lt;br /&gt;
 SSLCertificateKeyFile /path/to/domain.key&lt;br /&gt;
 ...&lt;br /&gt;
&amp;lt;/VirtualHost&amp;gt;&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
echo quit | openssl s_client -connect log.example.com:6514&lt;br /&gt;
echo &amp;quot;Q&amp;quot; | openssl s_client -servername google.com -connect google.com:443 | openssl x509 -noout -dates&lt;br /&gt;
host=example.com; port=443; echo &amp;quot;Q&amp;quot; | openssl s_client -servername $host -connect $host:$port | openssl x509 -noout -dates&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
echo quit | openssl s_client -showcerts -servername server -connect google.com:443 &amp;gt; cacert.pem&lt;br /&gt;
true | openssl s_client -connect google.com:443 2&amp;gt;/dev/null | openssl x509&lt;br /&gt;
rm -f cert.pem &amp;amp;&amp;amp; echo -n | openssl s_client -connect google.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' &amp;gt; ./cert.pem&lt;br /&gt;
```&lt;br /&gt;
curl --cacert google.pem https://google.com&lt;br /&gt;
&lt;br /&gt;
Get cipher&lt;br /&gt;
```&lt;br /&gt;
echo quit | openssl s_client -connect lb.examle.com:443 2&amp;gt;/dev/null | grep -i cipher&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
openssl s_client -connect 10.x.x.x:6514&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
PKI&lt;br /&gt;
```&lt;br /&gt;
#!/usr/bin/env bash&lt;br /&gt;
set -eu&lt;br /&gt;
# ref: https://datacenteroverlords.com/2012/03/01/creating-your-own-ssl-certificate-authority/&lt;br /&gt;
&lt;br /&gt;
bits=4096&lt;br /&gt;
# cert_cn=insights.example.com&lt;br /&gt;
ca_dir=ca&lt;br /&gt;
keys_dir=ca/keys&lt;br /&gt;
mkdir -p $ca_dir&lt;br /&gt;
mkdir -p $keys_dir&lt;br /&gt;
# cd $ca_dir&lt;br /&gt;
ca_subject=&amp;quot;-subj \&amp;quot;/C=US/ST=Utah/L=Lehi/O=Example/OU=IT/CN=ca.example.com\&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
create_CA(){&lt;br /&gt;
  echo &amp;quot;Configuring rootca certs for issueing certs to nodes via CN/fqdn.&amp;quot;&lt;br /&gt;
  openssl genrsa -out $ca_dir/ca.key $bits&lt;br /&gt;
  openssl genrsa -des3 -out $ca_dir/ca.key $bits&lt;br /&gt;
  # openssl genrsa -nodes -out $ca_dir/ca.key $bits&lt;br /&gt;
  # openssl req -x509 -new -nodes -key $ca_dir/ca.key -sha256 -days 10240 -out $ca_dir/ca.pem $ca_subject&lt;br /&gt;
  openssl req -x509 -new -nodes -key $ca_dir/ca.key -sha256 -days 10240 -out $ca_dir/ca.crt -subj &amp;quot;/C=US/ST=Utah/L=Lehi/O=Example/OU=IT/CN=ca.example.com&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
create_client(){&lt;br /&gt;
  cert_cn=$1&lt;br /&gt;
  echo &amp;quot;Configuring certs for nodes with CN/fqdn.&amp;quot;&lt;br /&gt;
  openssl genrsa -out $keys_dir/${cert_cn}.key $bits&lt;br /&gt;
  openssl req -new -key $keys_dir/${cert_cn}.key -out $keys_dir/${cert_cn}.csr -subj &amp;quot;/C=US/ST=Utah/L=Lehi/O=Example/OU=IT/CN=$cert_cn&amp;quot;&lt;br /&gt;
  openssl x509 -req -in $keys_dir/${cert_cn}.csr -CA $ca_dir/ca.crt -CAkey $ca_dir/ca.key -CAcreateserial -out $keys_dir/${cert_cn}.crt -days 730 -sha256&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
copy_keys_to_rsyslog(){&lt;br /&gt;
  cert_cn=$1&lt;br /&gt;
  cp $keys_dir/${cert_cn}.key ../files/etc/rsyslog.d/keys/&lt;br /&gt;
  cp $keys_dir/${cert_cn}.crt ../files/etc/rsyslog.d/keys/&lt;br /&gt;
  cp $ca_dir/ca.crt ../files/etc/rsyslog.d/keys/&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
create_CA&lt;br /&gt;
create_client insights.example.com&lt;br /&gt;
copy_keys_to_rsyslog insights.example.com&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Notes&lt;br /&gt;
&lt;br /&gt;
# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# Shadow&lt;br /&gt;
```&lt;br /&gt;
openssl passwd -6 -salt ly7/kMnF  yourpass&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
LDAP&lt;br /&gt;
```&lt;br /&gt;
openssl s_client -showcerts -connect ldap.yourdomain.com:636&lt;br /&gt;
```&lt;br /&gt;
# Refs&lt;br /&gt;
- https://deliciousbrains.com/ssl-certificate-authority-for-local-https-development/#becoming-certificate-authority&lt;br /&gt;
- https://github.com/OpenVPN/easy-rsa/blob/master/README.quickstart.md&lt;br /&gt;
- https://www.feistyduck.com/library/openssl-cookbook/online/ch-testing-with-openssl.html&lt;br /&gt;
&lt;br /&gt;
# More&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
openssl s_client -connect 127.0.0.1:8443 -tls1_3&lt;br /&gt;
&lt;br /&gt;
openssl s_client -showcerts -servername www.example.com -connect www.example.com:443 &amp;lt;/dev/null&lt;br /&gt;
&lt;br /&gt;
openssl s_client -showcerts -connect www.example.com:443 &amp;lt;/dev/nul&lt;br /&gt;
&lt;br /&gt;
$ echo | \&lt;br /&gt;
    openssl s_client -servername www.example.com -connect www.example.com:443 2&amp;gt;/dev/null | \&lt;br /&gt;
    openssl x509 -text&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
openssl verify -verbose -CAfile _.example.com.crt _.example.com.crt&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
get cert&lt;br /&gt;
```&lt;br /&gt;
openssl x509 -in a.crt -text -certopt no_header,no_pubkey,no_subject,no_issuer,no_signame,no_version,no_serial,no_validity,no&lt;br /&gt;
_extensions,no_sigdump,no_aux,no_extensions&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
pkcs12 from key and crt&lt;br /&gt;
```&lt;br /&gt;
openssl pkcs12 -export -out keyStore.p12 -inkey my.key -in my.crt&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Swap_space&amp;diff=5733</id>
		<title>Swap space</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Swap_space&amp;diff=5733"/>
		<updated>2026-08-01T21:03:21Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;# Linux&lt;br /&gt;
&lt;br /&gt;
## Disable swap&lt;br /&gt;
&lt;br /&gt;
# Disable swap immediately&lt;br /&gt;
sudo swapoff -a&lt;br /&gt;
&lt;br /&gt;
# Comment out or remove any swap entries from /etc/fstab to persist on reboot&lt;br /&gt;
sudo sed -i.bak '/\bswap\b/s/^/#/' /etc/fstab&lt;br /&gt;
&lt;br /&gt;
## Clear swap&lt;br /&gt;
```&lt;br /&gt;
free -m &lt;br /&gt;
swapoff -a&lt;br /&gt;
# wait about 30 seconds&lt;br /&gt;
swapon -a&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## Check swappiness&lt;br /&gt;
```&lt;br /&gt;
sysctl -a | grep swap&lt;br /&gt;
vm.swappiness = 10&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Gitlab_runner_LXD&amp;diff=5732</id>
		<title>Gitlab runner LXD</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Gitlab_runner_LXD&amp;diff=5732"/>
		<updated>2026-08-01T20:10:13Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;https://docs.gitlab.com/runner/executors/custom_examples/lxd/  You need to **install the `gitlab-runner` apt package first**.  Installing the package creates the `gitlab-runne...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;https://docs.gitlab.com/runner/executors/custom_examples/lxd/&lt;br /&gt;
&lt;br /&gt;
You need to **install the `gitlab-runner` apt package first**.&lt;br /&gt;
&lt;br /&gt;
Installing the package creates the `gitlab-runner` system user and the `/etc/gitlab-runner/` directory structure where `config.toml` lives.&lt;br /&gt;
&lt;br /&gt;
Here is the step-by-step order of operations to get everything set up correctly:&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## Step 1: Install the GitLab Runner Package&lt;br /&gt;
&lt;br /&gt;
Run these commands on your host machine to add GitLab's official repository and install the runner:&lt;br /&gt;
&lt;br /&gt;
```bash&lt;br /&gt;
# 1. Download and run the official GitLab repository setup script&lt;br /&gt;
curl -L &amp;quot;https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh&amp;quot; | sudo bash&lt;br /&gt;
&lt;br /&gt;
# 2. Install the gitlab-runner package&lt;br /&gt;
sudo apt-get install gitlab-runner&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## Step 2: Create the LXD Executor Scripts&lt;br /&gt;
&lt;br /&gt;
Next, create your wrapper scripts for the custom executor (e.g., in `/opt/gitlab-runner/`).&lt;br /&gt;
&lt;br /&gt;
```bash&lt;br /&gt;
# Create directory for custom executor scripts&lt;br /&gt;
sudo mkdir -p /opt/gitlab-runner&lt;br /&gt;
&lt;br /&gt;
# Create the prepare, run, and cleanup scripts&lt;br /&gt;
sudo touch /opt/gitlab-runner/lxd-prepare.sh&lt;br /&gt;
sudo touch /opt/gitlab-runner/lxd-run.sh&lt;br /&gt;
sudo touch /opt/gitlab-runner/lxd-cleanup.sh&lt;br /&gt;
&lt;br /&gt;
# Make them executable&lt;br /&gt;
sudo chmod +x /opt/gitlab-runner/lxd-*.sh&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
*(Paste the script contents into these files and ensure the LXD TLS certificate generation step you performed earlier is configured under the `gitlab-runner` system user's home directory so it can authenticate against LXD).*&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## Step 3: Register and Configure `config.toml`&lt;br /&gt;
&lt;br /&gt;
Finally, register the runner with your GitLab instance, which will automatically generate or update `/etc/gitlab-runner/config.toml`.&lt;br /&gt;
&lt;br /&gt;
```bash&lt;br /&gt;
# Register the runner (you'll need a registration token from GitLab UI)&lt;br /&gt;
sudo gitlab-runner register \&lt;br /&gt;
  --non-interactive \&lt;br /&gt;
  --url &amp;quot;https://gitlab.com/&amp;quot; \&lt;br /&gt;
  --token &amp;quot;YOUR_RUNNER_AUTHENTICATION_TOKEN&amp;quot; \&lt;br /&gt;
  --executor &amp;quot;custom&amp;quot; \&lt;br /&gt;
  --description &amp;quot;lxd-restricted-runner&amp;quot; \&lt;br /&gt;
  --custom-prepare-exec &amp;quot;/opt/gitlab-runner/lxd-prepare.sh&amp;quot; \&lt;br /&gt;
  --custom-run-exec &amp;quot;/opt/gitlab-runner/lxd-run.sh&amp;quot; \&lt;br /&gt;
  --custom-cleanup-exec &amp;quot;/opt/gitlab-runner/lxd-cleanup.sh&amp;quot;&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
Once registered, you can edit `/etc/gitlab-runner/config.toml` directly if you need to fine-tune concurrency limits or execution parameters.&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Kubernetes_Bare_Metal&amp;diff=5731</id>
		<title>Kubernetes Bare Metal</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Kubernetes_Bare_Metal&amp;diff=5731"/>
		<updated>2026-08-01T19:26:06Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;https://www.digitalocean.com/community/tutorials/bare-metal-kubernetes  You probably want to use cloud-init like this Ubuntu 24.04  `cloud-init` configuration for provisioning...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;https://www.digitalocean.com/community/tutorials/bare-metal-kubernetes&lt;br /&gt;
&lt;br /&gt;
You probably want to use cloud-init like this Ubuntu 24.04&lt;br /&gt;
&lt;br /&gt;
`cloud-init` configuration for provisioning Kubernetes nodes on **Ubuntu 24.04 LTS**.&lt;br /&gt;
&lt;br /&gt;
This script handles:&lt;br /&gt;
&lt;br /&gt;
* Disabling swap (required by `kubelet`)&lt;br /&gt;
* Enabling necessary kernel modules (`overlay`, `br_netfilter`)&lt;br /&gt;
* Setting required `sysctl` networking parameters&lt;br /&gt;
* Installing and configuring `containerd` with the `SystemdCgroup` driver enabled&lt;br /&gt;
* Adding the official `pkgs.k8s.io` repository (configured for Kubernetes **v1.30**)&lt;br /&gt;
* Installing and locking (`apt-mark hold`) `kubelet`, `kubeadm`, and `kubectl`&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## Cloud-Init YAML (`user-data`)&lt;br /&gt;
&lt;br /&gt;
```yaml&lt;br /&gt;
#cloud-config&lt;br /&gt;
package_update: true&lt;br /&gt;
package_upgrade: false&lt;br /&gt;
&lt;br /&gt;
packages:&lt;br /&gt;
  - apt-transport-https&lt;br /&gt;
  - ca-certificates&lt;br /&gt;
  - curl&lt;br /&gt;
  - gpg&lt;br /&gt;
  - containerd&lt;br /&gt;
&lt;br /&gt;
write_files:&lt;br /&gt;
  # Enable required kernel modules for container networking&lt;br /&gt;
  - path: /etc/modules-load.d/k8s.conf&lt;br /&gt;
    permissions: '0644'&lt;br /&gt;
    content: |&lt;br /&gt;
      overlay&lt;br /&gt;
      br_netfilter&lt;br /&gt;
&lt;br /&gt;
  # Set sysctl rules for iptables bridge &amp;amp; IP forwarding&lt;br /&gt;
  - path: /etc/sysctl.d/k8s.conf&lt;br /&gt;
    permissions: '0644'&lt;br /&gt;
    content: |&lt;br /&gt;
      net.bridge.bridge-nf-call-iptables  = 1&lt;br /&gt;
      net.bridge.bridge-nf-call-ip6tables = 1&lt;br /&gt;
      net.ipv4.ip_forward                 = 1&lt;br /&gt;
&lt;br /&gt;
runcmd:&lt;br /&gt;
  # 1. Disable swap immediately and persistently across reboots&lt;br /&gt;
  - swapoff -a&lt;br /&gt;
  - sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab&lt;br /&gt;
&lt;br /&gt;
  # 2. Load kernel modules and apply sysctl settings&lt;br /&gt;
  - modprobe overlay&lt;br /&gt;
  - modprobe br_netfilter&lt;br /&gt;
  - sysctl --system&lt;br /&gt;
&lt;br /&gt;
  # 3. Configure containerd with SystemdCgroup enabled&lt;br /&gt;
  - mkdir -p /etc/containerd&lt;br /&gt;
  - containerd config default | tee /etc/containerd/config.toml &amp;gt; /dev/null&lt;br /&gt;
  - sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml&lt;br /&gt;
  - systemctl restart containerd&lt;br /&gt;
  - systemctl enable containerd&lt;br /&gt;
&lt;br /&gt;
  # 4. Add official Kubernetes v1.30 apt repository&lt;br /&gt;
  - mkdir -p -m 755 /etc/apt/keyrings&lt;br /&gt;
  - curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg&lt;br /&gt;
  - chmod 644 /etc/apt/keyrings/kubernetes-apt-keyring.gpg&lt;br /&gt;
  - echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list&lt;br /&gt;
&lt;br /&gt;
  # 5. Install Kubernetes binaries and prevent auto-upgrades&lt;br /&gt;
  - apt-get update&lt;br /&gt;
  - apt-get install -y kubelet kubeadm kubectl&lt;br /&gt;
  - apt-mark hold kubelet kubeadm kubectl&lt;br /&gt;
  - systemctl enable --now kubelet&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## Next Steps After Boot&lt;br /&gt;
&lt;br /&gt;
Once your machine finishes running `cloud-init`, choose your role:&lt;br /&gt;
&lt;br /&gt;
### For the Control Plane (Master Node)&lt;br /&gt;
&lt;br /&gt;
Initialize the cluster with your control plane IP and preferred pod CIDR (e.g., Flannel/Calico):&lt;br /&gt;
&lt;br /&gt;
```bash&lt;br /&gt;
sudo kubeadm init --pod-network-cidr=10.244.0.0/16&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
### For Worker Nodes&lt;br /&gt;
&lt;br /&gt;
Run the `kubeadm join` command provided at the end of your control plane's `kubeadm init` output.&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=AWS_vs_Azure&amp;diff=5730</id>
		<title>AWS vs Azure</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=AWS_vs_Azure&amp;diff=5730"/>
		<updated>2026-08-01T15:46:53Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Here is how the core AWS services map directly to their Azure counterparts across key infrastructure categories:&lt;br /&gt;
&lt;br /&gt;
### Core Service Mapping&lt;br /&gt;
&lt;br /&gt;
| Category | AWS Service | Azure Equivalent | Description |&lt;br /&gt;
| --- | --- | --- | --- |&lt;br /&gt;
| **Compute** | **EC2** | Virtual Machines (VMs) | On-demand virtual instances/servers. |&lt;br /&gt;
|  | **AWS Lambda** | Azure Functions | Event-driven serverless compute. |&lt;br /&gt;
|  | **Elastic Beanstalk** | Azure App Service | Managed PaaS for web app deployment. |&lt;br /&gt;
| **Containers** | **EKS** | Azure Kubernetes Service (AKS) | Managed Kubernetes engine. |&lt;br /&gt;
|  | **ECS / Fargate** | Azure Container Apps / ACI | Serverless container execution. |&lt;br /&gt;
|  | **ECR** | Azure Container Registry (ACR) | Docker/OCI image repository. |&lt;br /&gt;
| **Storage** | **S3** | Azure Blob Storage | Scalable object storage. |&lt;br /&gt;
|  | **EBS** | Azure Managed Disks | Block storage attached to compute instances. |&lt;br /&gt;
|  | **EFS / FSx** | Azure Files | Fully managed network file shares (NFS/SMB). |&lt;br /&gt;
|  | **S3 Glacier** | Azure Archive Storage | Cold/archival storage for backups. |&lt;br /&gt;
| **Databases** | **RDS** | Azure SQL Database / Flexible Server | Managed relational databases (PostgreSQL, MySQL, SQL Server). |&lt;br /&gt;
|  | **DynamoDB** | Azure Cosmos DB | Fully managed NoSQL key-value/document database. |&lt;br /&gt;
|  | **ElastiCache** | Azure Cache for Redis | In-memory caching. |&lt;br /&gt;
| **Networking** | **VPC** | Virtual Network (VNet) | Isolated private cloud networking. |&lt;br /&gt;
|  | **Route 53** | Azure DNS | Managed DNS routing service. |&lt;br /&gt;
|  | **Direct Connect** | Azure ExpressRoute | Dedicated private link to on-premise hardware. |&lt;br /&gt;
|  | **ALB / NLB** | Application Gateway / Load Balancer | Layer 7 (HTTP/HTTPS) and Layer 4 load balancing. |&lt;br /&gt;
| **Identity &amp;amp; Security** | **AWS IAM** | Microsoft Entra ID (Azure AD) + RBAC | Identity management, auth, and role definitions. |&lt;br /&gt;
|  | **AWS KMS** | Azure Key Vault | Key, secret, and certificate management. |&lt;br /&gt;
|  | **AWS Shield / WAF** | Azure DDoS Protection / Web Application Firewall | DDoS defense and application security. |&lt;br /&gt;
| **Analytics &amp;amp; AI** | **Redshift** | Azure Synapse Analytics | Cloud data warehousing. |&lt;br /&gt;
|  | **Amazon SageMaker** | Azure Machine Learning | Platform for building, training, and deploying ML models. |&lt;br /&gt;
|  | **AWS Glue** | Azure Data Factory | Managed ETL and data integration pipelines. |&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### Key Structural Differences&lt;br /&gt;
&lt;br /&gt;
* **Identity Model:** AWS centers permissions around IAM policies attached to users/roles via JSON documents. Azure heavily relies on Microsoft Entra ID with Role-Based Access Control (RBAC) applied across standard scope hierarchies (Management Groups → Subscriptions → Resource Groups → Resources).&lt;br /&gt;
* **Resource Management:** Azure mandates that every resource resides within a **Resource Group** for unified lifecycle management. AWS tags resources globally or groups them logically, but lacks the strict single-container grouping model native to Azure.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
**for most greenfield projects, standard microservices, and general container workloads, Fargate is the default best choice.**&lt;br /&gt;
&lt;br /&gt;
The reduced configuration, lack of node management (no OS patching, no AMI rotations, no cluster autoscaling setup), and per-task scaling make it the lower-overhead option for developer velocity.&lt;br /&gt;
&lt;br /&gt;
However, saying it’s *only* when resources are &amp;quot;mostly pegged&amp;quot; misses a few critical non-cost boundaries where Fargate simply cannot be used or becomes significantly more expensive.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### Non-Cost Constraints (When Fargate Is Not an Option)&lt;br /&gt;
&lt;br /&gt;
Sometimes you **must** use EC2 regardless of price or utilization:&lt;br /&gt;
&lt;br /&gt;
1. **GPU / Specialized Hardware Workloads:** Machine learning inference/training (NVIDIA GPUs), high-performance media encoding, or AWS Inferentia/Trainium hardware require EC2 instance types. Fargate doesn't support GPU pass-through.&lt;br /&gt;
2. **Custom Daemon / Host-Level Monitoring:** If you rely on daemonsets/agents that must run at the host OS level (e.g., specific kernel-level eBPF tracing, custom security agents, or system-wide log forwarders), Fargate’s serverless boundary prevents host access. Sidecar containers are required on Fargate instead.&lt;br /&gt;
3. **Privileged Mode &amp;amp; Kernel Tweaks:** Tasks requiring Linux `--privileged` access, custom kernel module loading, raw network sockets, or `sysctl` modifications are blocked by Fargate’s isolation boundary.&lt;br /&gt;
4. **Extreme Local Disk I/O:** Fargate task storage tops out at 200 GB of ephemeral storage. If your app requires terabytes of high-speed local NVMe scratch disk (like `i3en` or `z1d` instances), you need EC2.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### Cost Decision Matrix&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
                    Is the workload continuous &amp;amp; predictable?&lt;br /&gt;
                                  /          \&lt;br /&gt;
                                 /            \&lt;br /&gt;
                                YES            NO (Bursty / Batch / Variable)&lt;br /&gt;
                               /                \&lt;br /&gt;
      Are you running high-volume baseline?      --&amp;gt;  Use FARGATE&lt;br /&gt;
                 /          \                         (Zero idle capacity waste)&lt;br /&gt;
                /            \&lt;br /&gt;
              YES             NO&lt;br /&gt;
              /                \&lt;br /&gt;
    Are you using 3-yr Savings  --&amp;gt;  Use FARGATE &lt;br /&gt;
    Plans + tight bin-packing?       (Engineering time &amp;gt; ~20% savings)&lt;br /&gt;
            /&lt;br /&gt;
          YES&lt;br /&gt;
          /&lt;br /&gt;
     Use EC2&lt;br /&gt;
(Save up to 50%+)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### Summary Checklist&lt;br /&gt;
&lt;br /&gt;
| Dimension | Default Choice | Shift to EC2 when... |&lt;br /&gt;
| --- | --- | --- |&lt;br /&gt;
| **Operational Effort** | **Fargate** (Zero node maintenance) | You already have robust node automation (e.g., Karpenter/Cluster Autoscaler) and dedicated DevOps resource. |&lt;br /&gt;
| **Scale Mechanism** | **Fargate** (Fast task provisioning) | Scaling is predictable and you can handle warm node pools. |&lt;br /&gt;
| **Hardware** | **Fargate** (Standard CPU/ARM + RAM) | You require GPUs, host storage, or kernel/privileged access. |&lt;br /&gt;
| **Cost at Scale** | **Fargate** (No waste from idle nodes) | Your monthly compute bill is large enough ($5k–$10k+/mo) that saving 30–50% pays for the engineering time to manage nodes. |&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=AWS_vs_Azure&amp;diff=5729</id>
		<title>AWS vs Azure</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=AWS_vs_Azure&amp;diff=5729"/>
		<updated>2026-08-01T15:46:15Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;**for most greenfield projects, standard microservices, and general container workloads, Fargate is the default best choice.**  The reduced configuration, lack of node managem...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;**for most greenfield projects, standard microservices, and general container workloads, Fargate is the default best choice.**&lt;br /&gt;
&lt;br /&gt;
The reduced configuration, lack of node management (no OS patching, no AMI rotations, no cluster autoscaling setup), and per-task scaling make it the lower-overhead option for developer velocity.&lt;br /&gt;
&lt;br /&gt;
However, saying it’s *only* when resources are &amp;quot;mostly pegged&amp;quot; misses a few critical non-cost boundaries where Fargate simply cannot be used or becomes significantly more expensive.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### Non-Cost Constraints (When Fargate Is Not an Option)&lt;br /&gt;
&lt;br /&gt;
Sometimes you **must** use EC2 regardless of price or utilization:&lt;br /&gt;
&lt;br /&gt;
1. **GPU / Specialized Hardware Workloads:** Machine learning inference/training (NVIDIA GPUs), high-performance media encoding, or AWS Inferentia/Trainium hardware require EC2 instance types. Fargate doesn't support GPU pass-through.&lt;br /&gt;
2. **Custom Daemon / Host-Level Monitoring:** If you rely on daemonsets/agents that must run at the host OS level (e.g., specific kernel-level eBPF tracing, custom security agents, or system-wide log forwarders), Fargate’s serverless boundary prevents host access. Sidecar containers are required on Fargate instead.&lt;br /&gt;
3. **Privileged Mode &amp;amp; Kernel Tweaks:** Tasks requiring Linux `--privileged` access, custom kernel module loading, raw network sockets, or `sysctl` modifications are blocked by Fargate’s isolation boundary.&lt;br /&gt;
4. **Extreme Local Disk I/O:** Fargate task storage tops out at 200 GB of ephemeral storage. If your app requires terabytes of high-speed local NVMe scratch disk (like `i3en` or `z1d` instances), you need EC2.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### Cost Decision Matrix&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
                    Is the workload continuous &amp;amp; predictable?&lt;br /&gt;
                                  /          \&lt;br /&gt;
                                 /            \&lt;br /&gt;
                                YES            NO (Bursty / Batch / Variable)&lt;br /&gt;
                               /                \&lt;br /&gt;
      Are you running high-volume baseline?      --&amp;gt;  Use FARGATE&lt;br /&gt;
                 /          \                         (Zero idle capacity waste)&lt;br /&gt;
                /            \&lt;br /&gt;
              YES             NO&lt;br /&gt;
              /                \&lt;br /&gt;
    Are you using 3-yr Savings  --&amp;gt;  Use FARGATE &lt;br /&gt;
    Plans + tight bin-packing?       (Engineering time &amp;gt; ~20% savings)&lt;br /&gt;
            /&lt;br /&gt;
          YES&lt;br /&gt;
          /&lt;br /&gt;
     Use EC2&lt;br /&gt;
(Save up to 50%+)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### Summary Checklist&lt;br /&gt;
&lt;br /&gt;
| Dimension | Default Choice | Shift to EC2 when... |&lt;br /&gt;
| --- | --- | --- |&lt;br /&gt;
| **Operational Effort** | **Fargate** (Zero node maintenance) | You already have robust node automation (e.g., Karpenter/Cluster Autoscaler) and dedicated DevOps resource. |&lt;br /&gt;
| **Scale Mechanism** | **Fargate** (Fast task provisioning) | Scaling is predictable and you can handle warm node pools. |&lt;br /&gt;
| **Hardware** | **Fargate** (Standard CPU/ARM + RAM) | You require GPUs, host storage, or kernel/privileged access. |&lt;br /&gt;
| **Cost at Scale** | **Fargate** (No waste from idle nodes) | Your monthly compute bill is large enough ($5k–$10k+/mo) that saving 30–50% pays for the engineering time to manage nodes. |&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Saltstack_docker&amp;diff=5728</id>
		<title>Saltstack docker</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Saltstack_docker&amp;diff=5728"/>
		<updated>2026-07-31T07:15:26Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;```&lt;br /&gt;
services:&lt;br /&gt;
  salt-master:&lt;br /&gt;
    image: saltstack/salt:latest&lt;br /&gt;
    container_name: salt-master&lt;br /&gt;
    restart: unless-stopped&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;4505:4505&amp;quot;&lt;br /&gt;
      - &amp;quot;4506:4506&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ./config/master:/etc/salt/master:ro&lt;br /&gt;
      - salt-pki:/etc/salt/pki&lt;br /&gt;
      - ./srv/salt:/srv/salt&lt;br /&gt;
      - ./srv/pillar:/srv/pillar&lt;br /&gt;
      - salt-cache:/var/cache/salt&lt;br /&gt;
      - salt-run:/var/run/salt&lt;br /&gt;
&lt;br /&gt;
  watchtower:&lt;br /&gt;
    image: containrrr/watchtower&lt;br /&gt;
    container_name: watchtower&lt;br /&gt;
    restart: unless-stopped&lt;br /&gt;
    environment:&lt;br /&gt;
      # Run at 2:00 AM every night&lt;br /&gt;
      # Format: Seconds Minutes Hours DayOfMonth Month DayOfWeek&lt;br /&gt;
      - WATCHTOWER_SCHEDULE=0 0 2 * * *&lt;br /&gt;
      # Remove old unpatched image layers from disk after updating&lt;br /&gt;
      - WATCHTOWER_CLEANUP=true&lt;br /&gt;
      # Check for base image updates regardless of local tag&lt;br /&gt;
      - WATCHTOWER_INCLUDE_RESTARTING=true&lt;br /&gt;
    volumes:&lt;br /&gt;
      # CRITICAL: This must point to the rootless user's socket, not /var/run!&lt;br /&gt;
      # Replace '1001' with the output of `id -u`&lt;br /&gt;
      - /run/user/1001/docker.sock:/var/run/docker.sock&lt;br /&gt;
    # Restrict Watchtower to ONLY update the salt-master container&lt;br /&gt;
    command: salt-master&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  salt-pki:&lt;br /&gt;
  salt-cache:&lt;br /&gt;
  salt-run:&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
# ==========================================&lt;br /&gt;
# CONFIGURATION&lt;br /&gt;
# ==========================================&lt;br /&gt;
# Replace with your Dockerized Salt Master's IP or DNS name&lt;br /&gt;
SALT_MASTER=&amp;quot;mysalt.example.com&amp;quot; &lt;br /&gt;
&lt;br /&gt;
# Uses the machine's hostname by default, or you can hardcode a name (e.g., &amp;quot;web-server-01&amp;quot;)&lt;br /&gt;
MINION_ID=$(hostname) &lt;br /&gt;
# ==========================================&lt;br /&gt;
&lt;br /&gt;
if [[ $EUID -ne 0 ]]; then&lt;br /&gt;
   echo &amp;quot;Error: This script must be run as root. Try using: sudo $0&amp;quot;&lt;br /&gt;
   exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Installing prerequisites...&amp;quot;&lt;br /&gt;
apt-get update&lt;br /&gt;
apt-get install -y curl gnupg&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Adding Salt Project repository for Ubuntu 24.04 LTS (Noble)...&amp;quot;&lt;br /&gt;
mkdir -p /etc/apt/keyrings&lt;br /&gt;
curl -fsSL -o /etc/apt/keyrings/salt-archive-keyring-2023.gpg \&lt;br /&gt;
  https://repo.saltproject.io/salt/py3/ubuntu/24.04/amd64/SALT-PROJECT-GPG-PUBKEY-2023.gpg&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;deb [signed-by=/etc/apt/keyrings/salt-archive-keyring-2023.gpg arch=amd64] https://repo.saltproject.io/salt/py3/ubuntu/24.04/amd64/3006 noble main&amp;quot; \&lt;br /&gt;
  | tee /etc/apt/sources.list.d/salt.list &amp;gt; /dev/null&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Installing Salt Minion...&amp;quot;&lt;br /&gt;
apt-get update&lt;br /&gt;
apt-get install -y salt-minion&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Configuring Minion to connect to $SALT_MASTER...&amp;quot;&lt;br /&gt;
mkdir -p /etc/salt/minion.d&lt;br /&gt;
echo &amp;quot;master: $SALT_MASTER&amp;quot; &amp;gt; /etc/salt/minion.d/master.conf&lt;br /&gt;
echo &amp;quot;$MINION_ID&amp;quot; &amp;gt; /etc/salt/minion_id&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Enabling and starting Salt Minion service...&amp;quot;&lt;br /&gt;
systemctl enable --now salt-minion&lt;br /&gt;
systemctl restart salt-minion&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==================================================&amp;quot;&lt;br /&gt;
echo &amp;quot;Success! The Salt Minion ($MINION_ID) is running.&amp;quot;&lt;br /&gt;
echo &amp;quot;It has sent its public key to $SALT_MASTER.&amp;quot;&lt;br /&gt;
echo &amp;quot;You must now log into the Master server and accept this key.&amp;quot;&lt;br /&gt;
echo &amp;quot;==================================================&amp;quot;&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
# List pending keys to verify the minion showed up&lt;br /&gt;
docker compose exec salt-master salt-key -L&lt;br /&gt;
&lt;br /&gt;
# Accept the specific minion key&lt;br /&gt;
docker compose exec salt-master salt-key -a&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Saltstack_docker&amp;diff=5727</id>
		<title>Saltstack docker</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Saltstack_docker&amp;diff=5727"/>
		<updated>2026-07-31T07:14:03Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;``` #!/bin/bash set -e  # ========================================== # CONFIGURATION # ========================================== # Replace with your Dockerized Salt Master's...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;```&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
# ==========================================&lt;br /&gt;
# CONFIGURATION&lt;br /&gt;
# ==========================================&lt;br /&gt;
# Replace with your Dockerized Salt Master's IP or DNS name&lt;br /&gt;
SALT_MASTER=&amp;quot;mysalt.example.com&amp;quot; &lt;br /&gt;
&lt;br /&gt;
# Uses the machine's hostname by default, or you can hardcode a name (e.g., &amp;quot;web-server-01&amp;quot;)&lt;br /&gt;
MINION_ID=$(hostname) &lt;br /&gt;
# ==========================================&lt;br /&gt;
&lt;br /&gt;
if [[ $EUID -ne 0 ]]; then&lt;br /&gt;
   echo &amp;quot;Error: This script must be run as root. Try using: sudo $0&amp;quot;&lt;br /&gt;
   exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Installing prerequisites...&amp;quot;&lt;br /&gt;
apt-get update&lt;br /&gt;
apt-get install -y curl gnupg&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Adding Salt Project repository for Ubuntu 24.04 LTS (Noble)...&amp;quot;&lt;br /&gt;
mkdir -p /etc/apt/keyrings&lt;br /&gt;
curl -fsSL -o /etc/apt/keyrings/salt-archive-keyring-2023.gpg \&lt;br /&gt;
  https://repo.saltproject.io/salt/py3/ubuntu/24.04/amd64/SALT-PROJECT-GPG-PUBKEY-2023.gpg&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;deb [signed-by=/etc/apt/keyrings/salt-archive-keyring-2023.gpg arch=amd64] https://repo.saltproject.io/salt/py3/ubuntu/24.04/amd64/3006 noble main&amp;quot; \&lt;br /&gt;
  | tee /etc/apt/sources.list.d/salt.list &amp;gt; /dev/null&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Installing Salt Minion...&amp;quot;&lt;br /&gt;
apt-get update&lt;br /&gt;
apt-get install -y salt-minion&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Configuring Minion to connect to $SALT_MASTER...&amp;quot;&lt;br /&gt;
mkdir -p /etc/salt/minion.d&lt;br /&gt;
echo &amp;quot;master: $SALT_MASTER&amp;quot; &amp;gt; /etc/salt/minion.d/master.conf&lt;br /&gt;
echo &amp;quot;$MINION_ID&amp;quot; &amp;gt; /etc/salt/minion_id&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Enabling and starting Salt Minion service...&amp;quot;&lt;br /&gt;
systemctl enable --now salt-minion&lt;br /&gt;
systemctl restart salt-minion&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==================================================&amp;quot;&lt;br /&gt;
echo &amp;quot;Success! The Salt Minion ($MINION_ID) is running.&amp;quot;&lt;br /&gt;
echo &amp;quot;It has sent its public key to $SALT_MASTER.&amp;quot;&lt;br /&gt;
echo &amp;quot;You must now log into the Master server and accept this key.&amp;quot;&lt;br /&gt;
echo &amp;quot;==================================================&amp;quot;&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
# List pending keys to verify the minion showed up&lt;br /&gt;
docker compose exec salt-master salt-key -L&lt;br /&gt;
&lt;br /&gt;
# Accept the specific minion key&lt;br /&gt;
docker compose exec salt-master salt-key -a&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Rootless_docker&amp;diff=5726</id>
		<title>Rootless docker</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Rootless_docker&amp;diff=5726"/>
		<updated>2026-07-31T02:30:32Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Works on Ubuntu 24.04&lt;br /&gt;
&lt;br /&gt;
https://docs.docker.com/engine/security/rootless/&lt;br /&gt;
&lt;br /&gt;
install-rootless-docker-on-ubuntu.sh&lt;br /&gt;
```&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
SERVICE_USER=&amp;quot;dockeruser&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# 1. Ensure script is run with sudo/root privileges&lt;br /&gt;
if [[ $EUID -ne 0 ]]; then&lt;br /&gt;
   echo &amp;quot;Error: This script must be run as root. Try using: sudo $0&amp;quot;&lt;br /&gt;
   exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# ==========================================&lt;br /&gt;
# 2. PRE-SCRIPT SECTION&lt;br /&gt;
# ==========================================&lt;br /&gt;
echo &amp;quot;==&amp;gt; Running pre-installation tasks...&amp;quot;&lt;br /&gt;
# Add any commands you want to run before the setup begins right here.&lt;br /&gt;
# For example, you could source an external script if it exists:&lt;br /&gt;
# if [ -f &amp;quot;./pre-install.sh&amp;quot; ]; then&lt;br /&gt;
#     source ./pre-install.sh&lt;br /&gt;
# fi&lt;br /&gt;
#&lt;br /&gt;
# apt-get remove -y docker.io || true  # Example: remove old conflicting packages&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ==========================================&lt;br /&gt;
# 3. DOCKER-CE &amp;amp; DEPENDENCY INSTALLATION&lt;br /&gt;
# ==========================================&lt;br /&gt;
echo &amp;quot;==&amp;gt; Setting up Docker's official apt repository...&amp;quot;&lt;br /&gt;
apt-get update&lt;br /&gt;
apt-get install -y ca-certificates curl&lt;br /&gt;
install -m 0755 -d /etc/apt/keyrings&lt;br /&gt;
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc&lt;br /&gt;
chmod a+r /etc/apt/keyrings/docker.asc&lt;br /&gt;
&lt;br /&gt;
echo \&lt;br /&gt;
  &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \&lt;br /&gt;
  $(. /etc/os-release &amp;amp;&amp;amp; echo &amp;quot;$VERSION_CODENAME&amp;quot;) stable&amp;quot; | \&lt;br /&gt;
  tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Installing docker-ce and rootless dependencies...&amp;quot;&lt;br /&gt;
apt-get update&lt;br /&gt;
apt-get install -y \&lt;br /&gt;
  docker-ce docker-ce-cli containerd.io \&lt;br /&gt;
  docker-buildx-plugin docker-compose-plugin \&lt;br /&gt;
  docker-ce-rootless-extras uidmap dbus-user-session slirp4netns&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ==========================================&lt;br /&gt;
# 4. SYSTEM CONFIGURATION &amp;amp; ROOTLESS SETUP&lt;br /&gt;
# ==========================================&lt;br /&gt;
echo &amp;quot;==&amp;gt; Configuring Ubuntu 24.04 unprivileged user namespaces...&amp;quot;&lt;br /&gt;
sysctl -w kernel.unprivileged_userns_clone=1&lt;br /&gt;
echo &amp;quot;kernel.unprivileged_userns_clone=1&amp;quot; &amp;gt; /etc/sysctl.d/50-rootless-docker.conf&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Stopping and disabling rootful Docker to prevent socket conflicts...&amp;quot;&lt;br /&gt;
systemctl stop docker.service docker.socket || true&lt;br /&gt;
systemctl disable docker.service docker.socket || true&lt;br /&gt;
rm -f /var/run/docker.sock&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Creating dedicated user: $SERVICE_USER...&amp;quot;&lt;br /&gt;
if id &amp;quot;$SERVICE_USER&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
    echo &amp;quot;User $SERVICE_USER already exists, skipping creation.&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    useradd -m -s /bin/bash &amp;quot;$SERVICE_USER&amp;quot;&lt;br /&gt;
    echo &amp;quot;User $SERVICE_USER created successfully.&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Enabling user lingering (ensures containers run without active SSH session)...&amp;quot;&lt;br /&gt;
loginctl enable-linger &amp;quot;$SERVICE_USER&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Wait a moment for systemd to initialize the new user's DBUS environment&lt;br /&gt;
sleep 2&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Installing Rootless Docker as $SERVICE_USER...&amp;quot;&lt;br /&gt;
sudo -i -u &amp;quot;$SERVICE_USER&amp;quot; bash &amp;lt;&amp;lt; 'EOF'&lt;br /&gt;
    # Define systemd runtime variables for the user session&lt;br /&gt;
    export XDG_RUNTIME_DIR=&amp;quot;/run/user/$(id -u)&amp;quot;&lt;br /&gt;
    export DBUS_SESSION_BUS_ADDRESS=&amp;quot;unix:path=${XDG_RUNTIME_DIR}/bus&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # Run the rootless installer&lt;br /&gt;
    dockerd-rootless-setuptool.sh install&lt;br /&gt;
&lt;br /&gt;
    # Configure .bashrc for future logins&lt;br /&gt;
    if ! grep -q &amp;quot;DOCKER_HOST&amp;quot; ~/.bashrc; then&lt;br /&gt;
        echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' &amp;gt;&amp;gt; ~/.bashrc&lt;br /&gt;
        echo 'export PATH=/usr/bin:$PATH' &amp;gt;&amp;gt; ~/.bashrc&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    # Start and enable the systemd service for Docker&lt;br /&gt;
    systemctl --user enable --now docker&lt;br /&gt;
&lt;br /&gt;
    echo &amp;quot;==&amp;gt; Rootless Docker installed successfully for $(whoami)!&amp;quot;&lt;br /&gt;
    docker info | grep -i rootless&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Script complete!&amp;quot;&lt;br /&gt;
echo &amp;quot;To manage your containers, switch to the new user by running:&amp;quot;&lt;br /&gt;
echo &amp;quot;sudo su - $SERVICE_USER&amp;quot;&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Rootless_docker&amp;diff=5725</id>
		<title>Rootless docker</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Rootless_docker&amp;diff=5725"/>
		<updated>2026-07-31T02:01:46Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Works on Ubuntu 24.04&lt;br /&gt;
&lt;br /&gt;
https://docs.docker.com/engine/security/rootless/&lt;br /&gt;
&lt;br /&gt;
install-rootless-docker-on-ubuntu.sh&lt;br /&gt;
```&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
SERVICE_USER=&amp;quot;dockeruser&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# 1. Ensure script is run with sudo/root privileges&lt;br /&gt;
if [[ $EUID -ne 0 ]]; then&lt;br /&gt;
   echo &amp;quot;Error: This script must be run as root. Try using: sudo $0&amp;quot;&lt;br /&gt;
   exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Installing dependencies...&amp;quot;&lt;br /&gt;
apt-get update&lt;br /&gt;
apt-get install -y uidmap dbus-user-session slirp4netns docker-ce-rootless-extras&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Configuring Ubuntu 24.04 unprivileged user namespaces...&amp;quot;&lt;br /&gt;
sysctl -w kernel.unprivileged_userns_clone=1&lt;br /&gt;
echo &amp;quot;kernel.unprivileged_userns_clone=1&amp;quot; &amp;gt; /etc/sysctl.d/50-rootless-docker.conf&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Stopping and disabling rootful Docker to prevent socket conflicts...&amp;quot;&lt;br /&gt;
systemctl stop docker.service docker.socket || true&lt;br /&gt;
systemctl disable docker.service docker.socket || true&lt;br /&gt;
rm -f /var/run/docker.sock&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Creating dedicated user: $SERVICE_USER...&amp;quot;&lt;br /&gt;
if id &amp;quot;$SERVICE_USER&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
    echo &amp;quot;User $SERVICE_USER already exists, skipping creation.&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    useradd -m -s /bin/bash &amp;quot;$SERVICE_USER&amp;quot;&lt;br /&gt;
    echo &amp;quot;User $SERVICE_USER created successfully.&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Enabling user lingering (ensures containers run without active SSH session)...&amp;quot;&lt;br /&gt;
loginctl enable-linger &amp;quot;$SERVICE_USER&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Wait a moment for systemd to initialize the new user's DBUS environment&lt;br /&gt;
sleep 2&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Installing Rootless Docker as $SERVICE_USER...&amp;quot;&lt;br /&gt;
# We use sudo to switch users and explicitly set XDG_RUNTIME_DIR so systemctl --user works&lt;br /&gt;
sudo -i -u &amp;quot;$SERVICE_USER&amp;quot; bash &amp;lt;&amp;lt; 'EOF'&lt;br /&gt;
    # Define systemd runtime variables for the user session&lt;br /&gt;
    export XDG_RUNTIME_DIR=&amp;quot;/run/user/$(id -u)&amp;quot;&lt;br /&gt;
    export DBUS_SESSION_BUS_ADDRESS=&amp;quot;unix:path=${XDG_RUNTIME_DIR}/bus&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # Run the rootless installer&lt;br /&gt;
    dockerd-rootless-setuptool.sh install&lt;br /&gt;
&lt;br /&gt;
    # Configure .bashrc for future logins&lt;br /&gt;
    if ! grep -q &amp;quot;DOCKER_HOST&amp;quot; ~/.bashrc; then&lt;br /&gt;
        echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' &amp;gt;&amp;gt; ~/.bashrc&lt;br /&gt;
        echo 'export PATH=/usr/bin:$PATH' &amp;gt;&amp;gt; ~/.bashrc&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    # Start and enable the systemd service for Docker&lt;br /&gt;
    systemctl --user enable --now docker&lt;br /&gt;
&lt;br /&gt;
    echo &amp;quot;==&amp;gt; Rootless Docker installed successfully for $(whoami)!&amp;quot;&lt;br /&gt;
    docker info | grep -i rootless&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Script complete!&amp;quot;&lt;br /&gt;
echo &amp;quot;To manage your containers, switch to the new user by running:&amp;quot;&lt;br /&gt;
echo &amp;quot;sudo su - $SERVICE_USER&amp;quot;&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Rootless_docker&amp;diff=5724</id>
		<title>Rootless docker</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Rootless_docker&amp;diff=5724"/>
		<updated>2026-07-31T02:00:06Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;Works on Ubuntu 24.04  install-rootless-docker-on-ubuntu.sh ``` #!/bin/bash set -e  SERVICE_USER=&amp;quot;dockeruser&amp;quot;  # 1. Ensure script is run with sudo/root privileges if  $EUID...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Works on Ubuntu 24.04&lt;br /&gt;
&lt;br /&gt;
install-rootless-docker-on-ubuntu.sh&lt;br /&gt;
```&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
SERVICE_USER=&amp;quot;dockeruser&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# 1. Ensure script is run with sudo/root privileges&lt;br /&gt;
if [[ $EUID -ne 0 ]]; then&lt;br /&gt;
   echo &amp;quot;Error: This script must be run as root. Try using: sudo $0&amp;quot;&lt;br /&gt;
   exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Installing dependencies...&amp;quot;&lt;br /&gt;
apt-get update&lt;br /&gt;
apt-get install -y uidmap dbus-user-session slirp4netns docker-ce-rootless-extras&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Configuring Ubuntu 24.04 unprivileged user namespaces...&amp;quot;&lt;br /&gt;
sysctl -w kernel.unprivileged_userns_clone=1&lt;br /&gt;
echo &amp;quot;kernel.unprivileged_userns_clone=1&amp;quot; &amp;gt; /etc/sysctl.d/50-rootless-docker.conf&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Stopping and disabling rootful Docker to prevent socket conflicts...&amp;quot;&lt;br /&gt;
systemctl stop docker.service docker.socket || true&lt;br /&gt;
systemctl disable docker.service docker.socket || true&lt;br /&gt;
rm -f /var/run/docker.sock&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Creating dedicated user: $SERVICE_USER...&amp;quot;&lt;br /&gt;
if id &amp;quot;$SERVICE_USER&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
    echo &amp;quot;User $SERVICE_USER already exists, skipping creation.&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    useradd -m -s /bin/bash &amp;quot;$SERVICE_USER&amp;quot;&lt;br /&gt;
    echo &amp;quot;User $SERVICE_USER created successfully.&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Enabling user lingering (ensures containers run without active SSH session)...&amp;quot;&lt;br /&gt;
loginctl enable-linger &amp;quot;$SERVICE_USER&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Wait a moment for systemd to initialize the new user's DBUS environment&lt;br /&gt;
sleep 2&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Installing Rootless Docker as $SERVICE_USER...&amp;quot;&lt;br /&gt;
# We use sudo to switch users and explicitly set XDG_RUNTIME_DIR so systemctl --user works&lt;br /&gt;
sudo -i -u &amp;quot;$SERVICE_USER&amp;quot; bash &amp;lt;&amp;lt; 'EOF'&lt;br /&gt;
    # Define systemd runtime variables for the user session&lt;br /&gt;
    export XDG_RUNTIME_DIR=&amp;quot;/run/user/$(id -u)&amp;quot;&lt;br /&gt;
    export DBUS_SESSION_BUS_ADDRESS=&amp;quot;unix:path=${XDG_RUNTIME_DIR}/bus&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # Run the rootless installer&lt;br /&gt;
    dockerd-rootless-setuptool.sh install&lt;br /&gt;
&lt;br /&gt;
    # Configure .bashrc for future logins&lt;br /&gt;
    if ! grep -q &amp;quot;DOCKER_HOST&amp;quot; ~/.bashrc; then&lt;br /&gt;
        echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' &amp;gt;&amp;gt; ~/.bashrc&lt;br /&gt;
        echo 'export PATH=/usr/bin:$PATH' &amp;gt;&amp;gt; ~/.bashrc&lt;br /&gt;
    fi&lt;br /&gt;
&lt;br /&gt;
    # Start and enable the systemd service for Docker&lt;br /&gt;
    systemctl --user enable --now docker&lt;br /&gt;
&lt;br /&gt;
    echo &amp;quot;==&amp;gt; Rootless Docker installed successfully for $(whoami)!&amp;quot;&lt;br /&gt;
    docker info | grep -i rootless&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;==&amp;gt; Script complete!&amp;quot;&lt;br /&gt;
echo &amp;quot;To manage your containers, switch to the new user by running:&amp;quot;&lt;br /&gt;
echo &amp;quot;sudo su - $SERVICE_USER&amp;quot;&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Tpm2&amp;diff=5723</id>
		<title>Tpm2</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Tpm2&amp;diff=5723"/>
		<updated>2026-07-31T01:03:51Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Ubuntu 24.04&lt;br /&gt;
ls -l /dev/tpm*&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
# 1. Update your package lists and install the Clevis toolset&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install clevis clevis-tpm2 clevis-luks clevis-initramfs&lt;br /&gt;
&lt;br /&gt;
# 2. Bind your LUKS partition to the TPM (specifying sha256 to prevent errors)&lt;br /&gt;
sudo clevis luks bind -d /dev/nvme0n1p3 tpm2 '{&amp;quot;pcr_bank&amp;quot;:&amp;quot;sha256&amp;quot;,&amp;quot;pcr_ids&amp;quot;:&amp;quot;1,4,7&amp;quot;}'&lt;br /&gt;
&lt;br /&gt;
# 3. Rebuild the boot image so the Clevis decryption hooks load during early boot&lt;br /&gt;
sudo update-initramfs -u -k all&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
reboot&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Tpm2&amp;diff=5722</id>
		<title>Tpm2</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Tpm2&amp;diff=5722"/>
		<updated>2026-07-30T22:22:57Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;Ubuntu 24.04 ls -l /dev/tpm* sudo apt install libtss2-rc0t64 libtss2-esys-3.0.2-0t64 tpm2-tools systemd-cryptenroll --tpm2-device=list lsblk -f | grep -i luk sudo systemd-cryp...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Ubuntu 24.04&lt;br /&gt;
ls -l /dev/tpm*&lt;br /&gt;
sudo apt install libtss2-rc0t64 libtss2-esys-3.0.2-0t64 tpm2-tools&lt;br /&gt;
systemd-cryptenroll --tpm2-device=list&lt;br /&gt;
lsblk -f | grep -i luk&lt;br /&gt;
sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=1+4+7 /dev/nvme0n1p3&lt;br /&gt;
sudo reboot&lt;br /&gt;
sudo update-initramfs -u -k all&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Codex_gemini_install_cli&amp;diff=5721</id>
		<title>Codex gemini install cli</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Codex_gemini_install_cli&amp;diff=5721"/>
		<updated>2026-07-26T02:01:05Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;# Debian/Ubuntu&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
sudo apt-get remove -y nodejs npm&lt;br /&gt;
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -&lt;br /&gt;
sudo apt-get install -y nodejs&lt;br /&gt;
&lt;br /&gt;
mkdir -p ~/.npm-global&lt;br /&gt;
npm config set prefix '~/.npm-global'&lt;br /&gt;
export PATH=~/.npm-global/bin:$PATH&lt;br /&gt;
echo 'export PATH=~/.npm-global/bin:$PATH' &amp;gt;&amp;gt; ~/.bashrc&lt;br /&gt;
&lt;br /&gt;
npm install -g npm@latest&lt;br /&gt;
npm install -g --allow-scripts=@github/keytar,node-pty @openai/codex @google/gemini-cli&lt;br /&gt;
&lt;br /&gt;
codex --version&lt;br /&gt;
gemini --version&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Codex_gemini_install_cli&amp;diff=5720</id>
		<title>Codex gemini install cli</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Codex_gemini_install_cli&amp;diff=5720"/>
		<updated>2026-07-26T01:57:23Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;# Debian/Ubuntu&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
sudo apt-get remove -y nodejs npm&lt;br /&gt;
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -&lt;br /&gt;
sudo apt-get install -y nodejs&lt;br /&gt;
sudo npm install -g npm@latest&lt;br /&gt;
sudo npm install -g npm@latest @openai/codex @google/gemini-cli&lt;br /&gt;
codex --version&lt;br /&gt;
gemini --version&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Kubectl_sniff&amp;diff=5719</id>
		<title>Kubectl sniff</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Kubectl_sniff&amp;diff=5719"/>
		<updated>2026-07-25T21:16:09Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;kubectl debug -i my-container-77c5f975d8-mt7mm -n my-ns  --image=nicolaka/netshoot   --target=my-container   --profile=netadmin   -- tcpdump -i any -nn -A -s 0 'tcp port 8080'...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;kubectl debug -i my-container-77c5f975d8-mt7mm -n my-ns  --image=nicolaka/netshoot   --target=my-container   --profile=netadmin   -- tcpdump -i any -nn -A -s 0 'tcp port 8080' | grep -iE 'X-Forwarded-For|Host:'&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Cka_kubeadm_init_upgrade&amp;diff=5718</id>
		<title>Cka kubeadm init upgrade</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Cka_kubeadm_init_upgrade&amp;diff=5718"/>
		<updated>2026-07-24T17:30:10Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;## Prep Ubuntu 24.04 &amp;amp; 22.04 Node&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -eu&lt;br /&gt;
&lt;br /&gt;
k8s_minor_version=1.31&lt;br /&gt;
&lt;br /&gt;
prep_k8s_node(){&lt;br /&gt;
  sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&lt;br /&gt;
  sudo apt install apt-transport-https curl -y&lt;br /&gt;
&lt;br /&gt;
  # https://kubernetes.io/docs/setup/production-environment/container-runtimes/&lt;br /&gt;
  sudo apt install containerd -y&lt;br /&gt;
  sudo mkdir -p /etc/containerd&lt;br /&gt;
  containerd config default | sudo tee /etc/containerd/config.toml &amp;gt; /dev/null&lt;br /&gt;
  #  Instruct containerd to use the Systemd cgroup hierarchy (typically cgroupv2) for managing container resources.&lt;br /&gt;
  sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml&lt;br /&gt;
  sudo systemctl restart containerd&lt;br /&gt;
  echo &amp;quot;net.ipv4.ip_forward = 1&amp;quot; | /etc/sysctl.d/k8s.conf&lt;br /&gt;
  sudo sysctl --system&lt;br /&gt;
&lt;br /&gt;
  # https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#installing-kubeadm-kubelet-and-kubectl&lt;br /&gt;
  curl -fsSL https://pkgs.k8s.io/core:/stable:/v${k8s_minor_version}/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg&lt;br /&gt;
  echo &amp;quot;deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v${k8s_minor_version}/deb/ /&amp;quot; | sudo tee /etc/apt/sources.list.d/kubernetes.list&lt;br /&gt;
  sudo apt update&lt;br /&gt;
  sudo apt install -y kubelet kubeadm kubectl&lt;br /&gt;
  sudo apt-mark hold kubelet kubeadm kubectl&lt;br /&gt;
&lt;br /&gt;
  swapon -s # returns summary text if swap is enabled&lt;br /&gt;
  sudo swapoff -a&lt;br /&gt;
  sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab&lt;br /&gt;
&lt;br /&gt;
  # overlay is already modprobed by containerd&lt;br /&gt;
  sudo modprobe br_netfilter&lt;br /&gt;
  echo -e &amp;quot;br_netfilter&amp;quot; | sudo tee /etc/modules-load.d/k8s.conf&lt;br /&gt;
  lsmod | grep overlay&lt;br /&gt;
  lsmod | grep br_netfilter&lt;br /&gt;
&lt;br /&gt;
  echo &amp;quot;export KUBECONFIG=/etc/kubernetes/admin.conf&amp;quot; &amp;gt;&amp;gt; ~/.bashrc&lt;br /&gt;
  sudo reboot&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
prep_k8s_node&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## Install kubeadm kubectl kubelet&lt;br /&gt;
&lt;br /&gt;
https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#installing-kubeadm-kubelet-and-kubectl&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
sudo apt-get update&lt;br /&gt;
# apt-transport-https may be a dummy package; if so, you can skip that package&lt;br /&gt;
sudo apt-get install -y apt-transport-https ca-certificates curl gpg&lt;br /&gt;
&lt;br /&gt;
# If the directory `/etc/apt/keyrings` does not exist, it should be created before the curl command, read the note below.&lt;br /&gt;
# sudo mkdir -p -m 755 /etc/apt/keyrings&lt;br /&gt;
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg&lt;br /&gt;
&lt;br /&gt;
# This overwrites any existing configuration in /etc/apt/sources.list.d/kubernetes.list&lt;br /&gt;
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list&lt;br /&gt;
&lt;br /&gt;
sudo apt-get update&lt;br /&gt;
sudo apt-get install -y kubelet kubeadm kubectl&lt;br /&gt;
sudo apt-mark hold kubelet kubeadm kubectl&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## Init Control Plane&lt;br /&gt;
&lt;br /&gt;
https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/#initializing-your-control-plane-node&lt;br /&gt;
&lt;br /&gt;
HA with LB for control see - https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/high-availability/ - --control-plane-endpoint&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
kubeadm init --apiserver-advertise-address &amp;lt;local ip&amp;gt;&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## Cilium&lt;br /&gt;
&lt;br /&gt;
This is only needed on one node in the cluster for admin functions if wanted as helm is what installs cilium agent on k8s nodes&lt;br /&gt;
&lt;br /&gt;
https://kubernetes.io/docs/tasks/administer-cluster/network-policy-provider/cilium-network-policy/&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
https://kubernetes.io/docs/tasks/administer-cluster/network-policy-provider/cilium-network-policy/&lt;br /&gt;
sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin&lt;br /&gt;
rm cilium-linux-amd64.tar.gz&lt;br /&gt;
cilium install &amp;amp;&amp;amp; cilium wait status&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## Join worker node&lt;br /&gt;
```&lt;br /&gt;
kubeadm join&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## Enable metrics-server api for kubectl top node&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
helm search hub metrics-server -o yaml&lt;br /&gt;
helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/&lt;br /&gt;
helm install metrics-server metrics-server/metrics-server --set args[0]='--kubelet-insecure-tls'&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Docker_install_ubuntu&amp;diff=5717</id>
		<title>Docker install ubuntu</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Docker_install_ubuntu&amp;diff=5717"/>
		<updated>2026-07-15T17:05:45Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;``` sudo apt update sudo apt install -y ca-certificates curl gnupg  sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;```&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y ca-certificates curl gnupg&lt;br /&gt;
&lt;br /&gt;
sudo install -m 0755 -d /etc/apt/keyrings&lt;br /&gt;
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc&lt;br /&gt;
sudo chmod a+r /etc/apt/keyrings/docker.asc&lt;br /&gt;
&lt;br /&gt;
sudo tee /etc/apt/sources.list.d/docker.sources &amp;lt;&amp;lt;EOF&lt;br /&gt;
Types: deb&lt;br /&gt;
URIs: https://download.docker.com/linux/ubuntu&lt;br /&gt;
Suites: $(. /etc/os-release &amp;amp;&amp;amp; echo &amp;quot;${UBUNTU_CODENAME:-$VERSION_CODENAME}&amp;quot;)&lt;br /&gt;
Components: stable&lt;br /&gt;
Architectures: $(dpkg --print-architecture)&lt;br /&gt;
Signed-By: /etc/apt/keyrings/docker.asc&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Lxc_launch&amp;diff=5716</id>
		<title>Lxc launch</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Lxc_launch&amp;diff=5716"/>
		<updated>2026-07-15T17:00:03Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;lxc launch images:debian/11 bullseye-vm --vm&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
lxc --target lxdnode1 init ubuntu:22.04 --vm -c limits.cpu=6 -c limits.memory=12GB docker&lt;br /&gt;
lxc config device override docker root size=100GB&lt;br /&gt;
lxc start docker&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
lxc launch images:debian/11 docker-server --vm -c limits.cpu=4 -c limits.memory=12GiB -d root,size=50GiB&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
s. Assumptions: 1) One zfs pool called ‘default’ that lives on the hard drive at /var/lib/lxd/disks/default.img. 2) Running as root. 3) Growing pool ‘default’ from 25GB to 45GB. Stop all running containers. #truncate -s +20G /var/lib/lxd/disks/default.img #zpool set autoexpand=on default #zpool status -vg default 4a. Note th…&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
lxc init images:ubuntu/focal ${VM} -p default -p vm --vm&lt;br /&gt;
lxc config set ${VM} limits.cpu ${CPU}&lt;br /&gt;
lxc config set ${VM} limits.memory ${MEM}&lt;br /&gt;
lxc config device override ${VM} root size=${DISK}&lt;br /&gt;
lxc start ${VM}&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# Centos7&lt;br /&gt;
```&lt;br /&gt;
lxc launch images:centos/7/amd64 centos7 &lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Envoy_from_nginx_for_ingress&amp;diff=5715</id>
		<title>Envoy from nginx for ingress</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Envoy_from_nginx_for_ingress&amp;diff=5715"/>
		<updated>2026-06-22T05:29:40Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;go install github.com/kubernetes-sigs/ingress2gateway@latest&lt;br /&gt;
&lt;br /&gt;
# Translate an existing NGINX ingress into Gateway API manifests&lt;br /&gt;
ingress2gateway print --providers ingress-nginx &amp;gt; new-envoy-routes.yaml&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
ingress-to-envoy-gateway.sh&lt;br /&gt;
```&lt;br /&gt;
#!/usr/bin/env bash&lt;br /&gt;
set -euo pipefail&lt;br /&gt;
&lt;br /&gt;
usage() {&lt;br /&gt;
  cat &amp;lt;&amp;lt;'USAGE'&lt;br /&gt;
Usage:&lt;br /&gt;
  scripts/ingress-to-envoy-gateway.sh -n NAMESPACE INGRESS [options]&lt;br /&gt;
&lt;br /&gt;
Converts one Kubernetes Ingress into Gateway API resources for Envoy Gateway.&lt;br /&gt;
By default the script prints YAML. Use --apply to apply it.&lt;br /&gt;
&lt;br /&gt;
Options:&lt;br /&gt;
  --gateway NAME                 Gateway name. Default: INGRESS-gateway&lt;br /&gt;
  --gateway-namespace NAMESPACE  Gateway namespace. Default: Ingress namespace&lt;br /&gt;
  --use-existing-gateway         Do not emit a Gateway; attach routes to --gateway&lt;br /&gt;
  --gateway-class NAME           GatewayClass name. Default: eg&lt;br /&gt;
  --issuer NAME                  ClusterIssuer name for generated Certificates. Default: letsencrypt-prod&lt;br /&gt;
  --emit-certificate             Emit cert-manager Certificate resources for Ingress TLS secrets.&lt;br /&gt;
                                  With --use-existing-gateway, Certificates are emitted in&lt;br /&gt;
                                  the Gateway namespace and --apply patches the Gateway&lt;br /&gt;
                                  HTTPS listener certificateRefs.&lt;br /&gt;
  --force-tls                    Route to the HTTPS listener even if the Ingress has no TLS block&lt;br /&gt;
  --tls-secret NAME              TLS Secret/Certificate name to use with --force-tls&lt;br /&gt;
  --backend-tls none|system|ca   Handle nginx backend-protocol=HTTPS. Default: none&lt;br /&gt;
  --backend-tls-hostname NAME    SNI/validation hostname for BackendTLSPolicy. Default: first Ingress host&lt;br /&gt;
  --backend-tls-ca-configmap NAME&lt;br /&gt;
                                  ConfigMap with ca.crt when --backend-tls=ca&lt;br /&gt;
  --apply                        Apply generated YAML instead of printing it&lt;br /&gt;
  -h, --help                     Show this help&lt;br /&gt;
&lt;br /&gt;
Examples:&lt;br /&gt;
  # Preview conversion for uapp/ucontrol-ui.&lt;br /&gt;
  scripts/ingress-to-envoy-gateway.sh -n uapp ucontrol-ui --backend-tls system&lt;br /&gt;
&lt;br /&gt;
  # Apply conversion.&lt;br /&gt;
  scripts/ingress-to-envoy-gateway.sh -n uapp ucontrol-ui --backend-tls system --apply&lt;br /&gt;
&lt;br /&gt;
  # Attach uapp/ucontrol-ui to an existing central Gateway.&lt;br /&gt;
  scripts/ingress-to-envoy-gateway.sh -n uapp ucontrol-ui \&lt;br /&gt;
    --use-existing-gateway \&lt;br /&gt;
    --gateway default-gateway \&lt;br /&gt;
    --gateway-namespace example-io \&lt;br /&gt;
    --backend-tls system&lt;br /&gt;
&lt;br /&gt;
Notes:&lt;br /&gt;
  - The original Ingress is not changed or deleted.&lt;br /&gt;
  - DNS must be moved to the new Envoy Gateway IP after verification.&lt;br /&gt;
  - nginx.ingress.kubernetes.io/backend-protocol=HTTPS requires BackendTLSPolicy.&lt;br /&gt;
    Use --backend-tls system for publicly trusted backend certs, or --backend-tls ca&lt;br /&gt;
    with --backend-tls-ca-configmap for private CAs.&lt;br /&gt;
USAGE&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
die() {&lt;br /&gt;
  printf 'error: %s\n' &amp;quot;$*&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
  exit 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
need() {&lt;br /&gt;
  command -v &amp;quot;$1&amp;quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || die &amp;quot;missing required command: $1&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
default_kubectl() {&lt;br /&gt;
  if [[ -x /snap/kubectl/current/kubectl ]]; then&lt;br /&gt;
    printf '%s\n' /snap/kubectl/current/kubectl&lt;br /&gt;
  else&lt;br /&gt;
    printf '%s\n' kubectl&lt;br /&gt;
  fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
yaml_quote() {&lt;br /&gt;
  printf '%s' &amp;quot;$1&amp;quot; | sed &amp;quot;s/'/''/g; s/^/'/; s/$/'/&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
resource_name() {&lt;br /&gt;
  printf '%s' &amp;quot;$1&amp;quot; | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9.-]+/-/g; s/^-+//; s/-+$//' | cut -c1-63&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
resolve_service_port() {&lt;br /&gt;
  local svc=&amp;quot;$1&amp;quot;&lt;br /&gt;
  local port_name=&amp;quot;$2&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  &amp;quot;$kubectl_cmd&amp;quot; get svc -n &amp;quot;$namespace&amp;quot; &amp;quot;$svc&amp;quot; -o json |&lt;br /&gt;
    jq -r --arg name &amp;quot;$port_name&amp;quot; '&lt;br /&gt;
      .spec.ports[] | select(.name == $name) | .port&lt;br /&gt;
    ' | head -n 1&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
gateway_has_cert_ref() {&lt;br /&gt;
  local secret=&amp;quot;$1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  &amp;quot;$kubectl_cmd&amp;quot; get gateway -n &amp;quot;$gateway_namespace&amp;quot; &amp;quot;$gateway&amp;quot; -o json |&lt;br /&gt;
    jq -e --arg secret &amp;quot;$secret&amp;quot; '&lt;br /&gt;
      .spec.listeners[]&lt;br /&gt;
      | select(.name == &amp;quot;https&amp;quot;)&lt;br /&gt;
      | (.tls.certificateRefs // [])&lt;br /&gt;
      | any(.name == $secret)&lt;br /&gt;
    ' &amp;gt;/dev/null&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
patch_gateway_cert_ref() {&lt;br /&gt;
  local secret=&amp;quot;$1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  if gateway_has_cert_ref &amp;quot;$secret&amp;quot;; then&lt;br /&gt;
    printf 'Gateway %s/%s already references TLS secret %s\n' &amp;quot;$gateway_namespace&amp;quot; &amp;quot;$gateway&amp;quot; &amp;quot;$secret&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
    return&lt;br /&gt;
  fi&lt;br /&gt;
&lt;br /&gt;
  &amp;quot;$kubectl_cmd&amp;quot; patch gateway &amp;quot;$gateway&amp;quot; -n &amp;quot;$gateway_namespace&amp;quot; --type json \&lt;br /&gt;
    -p=&amp;quot;[{\&amp;quot;op\&amp;quot;:\&amp;quot;add\&amp;quot;,\&amp;quot;path\&amp;quot;:\&amp;quot;/spec/listeners/1/tls/certificateRefs/-\&amp;quot;,\&amp;quot;value\&amp;quot;:{\&amp;quot;name\&amp;quot;:\&amp;quot;$secret\&amp;quot;}}]&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
namespace=&amp;quot;&amp;quot;&lt;br /&gt;
ingress=&amp;quot;&amp;quot;&lt;br /&gt;
gateway=&amp;quot;&amp;quot;&lt;br /&gt;
gateway_namespace=&amp;quot;&amp;quot;&lt;br /&gt;
create_gateway=&amp;quot;true&amp;quot;&lt;br /&gt;
gateway_class=&amp;quot;eg&amp;quot;&lt;br /&gt;
issuer=&amp;quot;letsencrypt-prod&amp;quot;&lt;br /&gt;
emit_certificate=&amp;quot;false&amp;quot;&lt;br /&gt;
backend_tls=&amp;quot;none&amp;quot;&lt;br /&gt;
backend_tls_hostname=&amp;quot;&amp;quot;&lt;br /&gt;
backend_tls_ca_configmap=&amp;quot;&amp;quot;&lt;br /&gt;
force_tls=&amp;quot;false&amp;quot;&lt;br /&gt;
tls_secret_override=&amp;quot;&amp;quot;&lt;br /&gt;
apply=&amp;quot;false&amp;quot;&lt;br /&gt;
kubectl_cmd=&amp;quot;${KUBECTL:-$(default_kubectl)}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
while [[ $# -gt 0 ]]; do&lt;br /&gt;
  case &amp;quot;$1&amp;quot; in&lt;br /&gt;
    -n|--namespace)&lt;br /&gt;
      namespace=&amp;quot;${2:-}&amp;quot;&lt;br /&gt;
      shift 2&lt;br /&gt;
      ;;&lt;br /&gt;
    --gateway)&lt;br /&gt;
      gateway=&amp;quot;${2:-}&amp;quot;&lt;br /&gt;
      shift 2&lt;br /&gt;
      ;;&lt;br /&gt;
    --gateway-namespace)&lt;br /&gt;
      gateway_namespace=&amp;quot;${2:-}&amp;quot;&lt;br /&gt;
      shift 2&lt;br /&gt;
      ;;&lt;br /&gt;
    --use-existing-gateway)&lt;br /&gt;
      create_gateway=&amp;quot;false&amp;quot;&lt;br /&gt;
      shift&lt;br /&gt;
      ;;&lt;br /&gt;
    --gateway-class)&lt;br /&gt;
      gateway_class=&amp;quot;${2:-}&amp;quot;&lt;br /&gt;
      shift 2&lt;br /&gt;
      ;;&lt;br /&gt;
    --issuer)&lt;br /&gt;
      issuer=&amp;quot;${2:-}&amp;quot;&lt;br /&gt;
      shift 2&lt;br /&gt;
      ;;&lt;br /&gt;
    --emit-certificate)&lt;br /&gt;
      emit_certificate=&amp;quot;true&amp;quot;&lt;br /&gt;
      shift&lt;br /&gt;
      ;;&lt;br /&gt;
    --force-tls)&lt;br /&gt;
      force_tls=&amp;quot;true&amp;quot;&lt;br /&gt;
      shift&lt;br /&gt;
      ;;&lt;br /&gt;
    --tls-secret)&lt;br /&gt;
      tls_secret_override=&amp;quot;${2:-}&amp;quot;&lt;br /&gt;
      shift 2&lt;br /&gt;
      ;;&lt;br /&gt;
    --backend-tls)&lt;br /&gt;
      backend_tls=&amp;quot;${2:-}&amp;quot;&lt;br /&gt;
      shift 2&lt;br /&gt;
      ;;&lt;br /&gt;
    --backend-tls-hostname)&lt;br /&gt;
      backend_tls_hostname=&amp;quot;${2:-}&amp;quot;&lt;br /&gt;
      shift 2&lt;br /&gt;
      ;;&lt;br /&gt;
    --backend-tls-ca-configmap)&lt;br /&gt;
      backend_tls_ca_configmap=&amp;quot;${2:-}&amp;quot;&lt;br /&gt;
      shift 2&lt;br /&gt;
      ;;&lt;br /&gt;
    --apply)&lt;br /&gt;
      apply=&amp;quot;true&amp;quot;&lt;br /&gt;
      shift&lt;br /&gt;
      ;;&lt;br /&gt;
    -h|--help)&lt;br /&gt;
      usage&lt;br /&gt;
      exit 0&lt;br /&gt;
      ;;&lt;br /&gt;
    -*)&lt;br /&gt;
      die &amp;quot;unknown option: $1&amp;quot;&lt;br /&gt;
      ;;&lt;br /&gt;
    *)&lt;br /&gt;
      if [[ -n &amp;quot;$ingress&amp;quot; ]]; then&lt;br /&gt;
        die &amp;quot;unexpected extra argument: $1&amp;quot;&lt;br /&gt;
      fi&lt;br /&gt;
      ingress=&amp;quot;$1&amp;quot;&lt;br /&gt;
      shift&lt;br /&gt;
      ;;&lt;br /&gt;
  esac&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
[[ -n &amp;quot;$namespace&amp;quot; ]] || die &amp;quot;namespace is required&amp;quot;&lt;br /&gt;
[[ -n &amp;quot;$ingress&amp;quot; ]] || die &amp;quot;ingress name is required&amp;quot;&lt;br /&gt;
case &amp;quot;$backend_tls&amp;quot; in&lt;br /&gt;
  none|system|ca) ;;&lt;br /&gt;
  *) die &amp;quot;--backend-tls must be one of: none, system, ca&amp;quot; ;;&lt;br /&gt;
esac&lt;br /&gt;
if [[ &amp;quot;$backend_tls&amp;quot; == &amp;quot;ca&amp;quot; &amp;amp;&amp;amp; -z &amp;quot;$backend_tls_ca_configmap&amp;quot; ]]; then&lt;br /&gt;
  die &amp;quot;--backend-tls-ca-configmap is required when --backend-tls=ca&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [[ &amp;quot;$force_tls&amp;quot; == &amp;quot;true&amp;quot; &amp;amp;&amp;amp; -z &amp;quot;$tls_secret_override&amp;quot; ]]; then&lt;br /&gt;
  die &amp;quot;--tls-secret is required when --force-tls is set&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
need jq&lt;br /&gt;
need sed&lt;br /&gt;
need tr&lt;br /&gt;
need cut&lt;br /&gt;
need head&lt;br /&gt;
if [[ &amp;quot;$kubectl_cmd&amp;quot; == */* ]]; then&lt;br /&gt;
  [[ -x &amp;quot;$kubectl_cmd&amp;quot; ]] || die &amp;quot;kubectl is not executable: $kubectl_cmd&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
  need &amp;quot;$kubectl_cmd&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
gateway=&amp;quot;${gateway:-$(resource_name &amp;quot;${ingress}-gateway&amp;quot;)}&amp;quot;&lt;br /&gt;
gateway_namespace=&amp;quot;${gateway_namespace:-$namespace}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
ingress_json=&amp;quot;$(&amp;quot;$kubectl_cmd&amp;quot; get ingress -n &amp;quot;$namespace&amp;quot; &amp;quot;$ingress&amp;quot; -o json)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
mapfile -t hosts &amp;lt; &amp;lt;(jq -r '.spec.rules[]?.host // empty' &amp;lt;&amp;lt;&amp;lt;&amp;quot;$ingress_json&amp;quot; | awk 'NF' | sort -u)&lt;br /&gt;
[[ &amp;quot;${#hosts[@]}&amp;quot; -gt 0 ]] || die &amp;quot;Ingress has no host rules&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if [[ -z &amp;quot;$backend_tls_hostname&amp;quot; ]]; then&lt;br /&gt;
  backend_tls_hostname=&amp;quot;${hosts[0]}&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
mapfile -t tls_secrets &amp;lt; &amp;lt;(jq -r '.spec.tls[]?.secretName // empty' &amp;lt;&amp;lt;&amp;lt;&amp;quot;$ingress_json&amp;quot; | awk 'NF' | sort -u)&lt;br /&gt;
if [[ -n &amp;quot;$tls_secret_override&amp;quot; ]]; then&lt;br /&gt;
  tls_secrets=(&amp;quot;$tls_secret_override&amp;quot;)&lt;br /&gt;
fi&lt;br /&gt;
has_tls=&amp;quot;false&amp;quot;&lt;br /&gt;
if [[ &amp;quot;${#tls_secrets[@]}&amp;quot; -gt 0 || &amp;quot;$force_tls&amp;quot; == &amp;quot;true&amp;quot; ]]; then&lt;br /&gt;
  has_tls=&amp;quot;true&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [[ &amp;quot;$create_gateway&amp;quot; == &amp;quot;false&amp;quot; &amp;amp;&amp;amp; &amp;quot;$has_tls&amp;quot; == &amp;quot;true&amp;quot; ]]; then&lt;br /&gt;
  emit_certificate=&amp;quot;true&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
ssl_redirect=&amp;quot;$(&lt;br /&gt;
  jq -r '&lt;br /&gt;
    .metadata.annotations[&amp;quot;nginx.ingress.kubernetes.io/ssl-redirect&amp;quot;] //&lt;br /&gt;
    .metadata.annotations[&amp;quot;nginx.ingress.kubernetes.io/force-ssl-redirect&amp;quot;] //&lt;br /&gt;
    &amp;quot;false&amp;quot;&lt;br /&gt;
  ' &amp;lt;&amp;lt;&amp;lt;&amp;quot;$ingress_json&amp;quot;&lt;br /&gt;
)&amp;quot;&lt;br /&gt;
backend_protocol=&amp;quot;$(jq -r '.metadata.annotations[&amp;quot;nginx.ingress.kubernetes.io/backend-protocol&amp;quot;] // &amp;quot;HTTP&amp;quot;' &amp;lt;&amp;lt;&amp;lt;&amp;quot;$ingress_json&amp;quot; | tr '[:lower:]' '[:upper:]')&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if [[ &amp;quot;$backend_protocol&amp;quot; == &amp;quot;HTTPS&amp;quot; &amp;amp;&amp;amp; &amp;quot;$backend_tls&amp;quot; == &amp;quot;none&amp;quot; ]]; then&lt;br /&gt;
  printf 'warning: %s/%s uses nginx backend-protocol=HTTPS; generated route will need BackendTLSPolicy or backend HTTP support.\n' &amp;quot;$namespace&amp;quot; &amp;quot;$ingress&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
tmp=&amp;quot;$(mktemp)&amp;quot;&lt;br /&gt;
trap 'rm -f &amp;quot;$tmp&amp;quot;' EXIT&lt;br /&gt;
&lt;br /&gt;
{&lt;br /&gt;
  if [[ &amp;quot;$create_gateway&amp;quot; == &amp;quot;true&amp;quot; ]]; then&lt;br /&gt;
    cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
apiVersion: gateway.networking.k8s.io/v1&lt;br /&gt;
kind: Gateway&lt;br /&gt;
metadata:&lt;br /&gt;
  name: $gateway&lt;br /&gt;
  namespace: $gateway_namespace&lt;br /&gt;
  labels:&lt;br /&gt;
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway&lt;br /&gt;
    app.kubernetes.io/source-ingress: $ingress&lt;br /&gt;
spec:&lt;br /&gt;
  gatewayClassName: $gateway_class&lt;br /&gt;
  listeners:&lt;br /&gt;
    - name: http&lt;br /&gt;
      port: 80&lt;br /&gt;
      protocol: HTTP&lt;br /&gt;
      allowedRoutes:&lt;br /&gt;
        namespaces:&lt;br /&gt;
          from: Same&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
    if [[ &amp;quot;$has_tls&amp;quot; == &amp;quot;true&amp;quot; ]]; then&lt;br /&gt;
      cat &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
    - name: https&lt;br /&gt;
      port: 443&lt;br /&gt;
      protocol: HTTPS&lt;br /&gt;
      tls:&lt;br /&gt;
        mode: Terminate&lt;br /&gt;
        certificateRefs:&lt;br /&gt;
EOF&lt;br /&gt;
      for secret in &amp;quot;${tls_secrets[@]}&amp;quot;; do&lt;br /&gt;
        printf '          - name: %s\n' &amp;quot;$secret&amp;quot;&lt;br /&gt;
      done&lt;br /&gt;
      cat &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
      allowedRoutes:&lt;br /&gt;
        namespaces:&lt;br /&gt;
          from: Same&lt;br /&gt;
EOF&lt;br /&gt;
    fi&lt;br /&gt;
    printf '%s\n' '---'&lt;br /&gt;
  fi&lt;br /&gt;
&lt;br /&gt;
  cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
apiVersion: gateway.networking.k8s.io/v1&lt;br /&gt;
kind: HTTPRoute&lt;br /&gt;
metadata:&lt;br /&gt;
  name: $ingress&lt;br /&gt;
  namespace: $namespace&lt;br /&gt;
  labels:&lt;br /&gt;
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway&lt;br /&gt;
    app.kubernetes.io/source-ingress: $ingress&lt;br /&gt;
spec:&lt;br /&gt;
  hostnames:&lt;br /&gt;
EOF&lt;br /&gt;
  for host in &amp;quot;${hosts[@]}&amp;quot;; do&lt;br /&gt;
    printf '    - %s\n' &amp;quot;$(yaml_quote &amp;quot;$host&amp;quot;)&amp;quot;&lt;br /&gt;
  done&lt;br /&gt;
  cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
  parentRefs:&lt;br /&gt;
    - name: $gateway&lt;br /&gt;
      namespace: $gateway_namespace&lt;br /&gt;
      sectionName: $([[ &amp;quot;$has_tls&amp;quot; == &amp;quot;true&amp;quot; ]] &amp;amp;&amp;amp; printf 'https' || printf 'http')&lt;br /&gt;
  rules:&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
  route_count=&amp;quot;$(jq '[.spec.rules[]?.http.paths[]?] | length' &amp;lt;&amp;lt;&amp;lt;&amp;quot;$ingress_json&amp;quot;)&amp;quot;&lt;br /&gt;
  [[ &amp;quot;$route_count&amp;quot; != &amp;quot;0&amp;quot; ]] || die &amp;quot;Ingress has no HTTP paths&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  for i in $(seq 0 &amp;quot;$((route_count - 1))&amp;quot;); do&lt;br /&gt;
    item=&amp;quot;$(jq -c &amp;quot;[.spec.rules[]?.http.paths[]?][$i]&amp;quot; &amp;lt;&amp;lt;&amp;lt;&amp;quot;$ingress_json&amp;quot;)&amp;quot;&lt;br /&gt;
    path=&amp;quot;$(jq -r '.path // &amp;quot;/&amp;quot;' &amp;lt;&amp;lt;&amp;lt;&amp;quot;$item&amp;quot;)&amp;quot;&lt;br /&gt;
    path_type=&amp;quot;$(jq -r '.pathType // &amp;quot;Prefix&amp;quot;' &amp;lt;&amp;lt;&amp;lt;&amp;quot;$item&amp;quot;)&amp;quot;&lt;br /&gt;
    svc=&amp;quot;$(jq -r '.backend.service.name // empty' &amp;lt;&amp;lt;&amp;lt;&amp;quot;$item&amp;quot;)&amp;quot;&lt;br /&gt;
    port_number=&amp;quot;$(jq -r '.backend.service.port.number // empty' &amp;lt;&amp;lt;&amp;lt;&amp;quot;$item&amp;quot;)&amp;quot;&lt;br /&gt;
    port_name=&amp;quot;$(jq -r '.backend.service.port.name // empty' &amp;lt;&amp;lt;&amp;lt;&amp;quot;$item&amp;quot;)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    [[ -n &amp;quot;$svc&amp;quot; ]] || die &amp;quot;path $i has no Service backend&amp;quot;&lt;br /&gt;
    if [[ -z &amp;quot;$port_number&amp;quot; &amp;amp;&amp;amp; -n &amp;quot;$port_name&amp;quot; ]]; then&lt;br /&gt;
      port_number=&amp;quot;$(resolve_service_port &amp;quot;$svc&amp;quot; &amp;quot;$port_name&amp;quot;)&amp;quot;&lt;br /&gt;
      [[ -n &amp;quot;$port_number&amp;quot; ]] || die &amp;quot;could not resolve named port $svc/$port_name&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
    [[ -n &amp;quot;$port_number&amp;quot; ]] || die &amp;quot;path $i has no Service port&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    case &amp;quot;$path_type&amp;quot; in&lt;br /&gt;
      Prefix) gateway_path_type=&amp;quot;PathPrefix&amp;quot; ;;&lt;br /&gt;
      Exact) gateway_path_type=&amp;quot;Exact&amp;quot; ;;&lt;br /&gt;
      ImplementationSpecific) gateway_path_type=&amp;quot;PathPrefix&amp;quot; ;;&lt;br /&gt;
      *) gateway_path_type=&amp;quot;PathPrefix&amp;quot; ;;&lt;br /&gt;
    esac&lt;br /&gt;
&lt;br /&gt;
    cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
    - matches:&lt;br /&gt;
        - path:&lt;br /&gt;
            type: $gateway_path_type&lt;br /&gt;
            value: $(yaml_quote &amp;quot;$path&amp;quot;)&lt;br /&gt;
      backendRefs:&lt;br /&gt;
        - name: $svc&lt;br /&gt;
          port: $port_number&lt;br /&gt;
EOF&lt;br /&gt;
  done&lt;br /&gt;
&lt;br /&gt;
  if [[ &amp;quot;$has_tls&amp;quot; == &amp;quot;true&amp;quot; ]] &amp;amp;&amp;amp; { [[ &amp;quot;$ssl_redirect&amp;quot; == &amp;quot;true&amp;quot; ]] || [[ &amp;quot;$force_tls&amp;quot; == &amp;quot;true&amp;quot; ]]; }; then&lt;br /&gt;
    cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
---&lt;br /&gt;
apiVersion: gateway.networking.k8s.io/v1&lt;br /&gt;
kind: HTTPRoute&lt;br /&gt;
metadata:&lt;br /&gt;
  name: $(resource_name &amp;quot;${ingress}-https-redirect&amp;quot;)&lt;br /&gt;
  namespace: $namespace&lt;br /&gt;
  labels:&lt;br /&gt;
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway&lt;br /&gt;
    app.kubernetes.io/source-ingress: $ingress&lt;br /&gt;
spec:&lt;br /&gt;
  hostnames:&lt;br /&gt;
EOF&lt;br /&gt;
    for host in &amp;quot;${hosts[@]}&amp;quot;; do&lt;br /&gt;
      printf '    - %s\n' &amp;quot;$(yaml_quote &amp;quot;$host&amp;quot;)&amp;quot;&lt;br /&gt;
    done&lt;br /&gt;
    cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
  parentRefs:&lt;br /&gt;
    - name: $gateway&lt;br /&gt;
      namespace: $gateway_namespace&lt;br /&gt;
      sectionName: http&lt;br /&gt;
  rules:&lt;br /&gt;
    - filters:&lt;br /&gt;
        - type: RequestRedirect&lt;br /&gt;
          requestRedirect:&lt;br /&gt;
            scheme: https&lt;br /&gt;
            statusCode: 301&lt;br /&gt;
      matches:&lt;br /&gt;
        - path:&lt;br /&gt;
            type: PathPrefix&lt;br /&gt;
            value: /&lt;br /&gt;
EOF&lt;br /&gt;
  fi&lt;br /&gt;
&lt;br /&gt;
  if [[ &amp;quot;$backend_protocol&amp;quot; == &amp;quot;HTTPS&amp;quot; &amp;amp;&amp;amp; &amp;quot;$backend_tls&amp;quot; != &amp;quot;none&amp;quot; ]]; then&lt;br /&gt;
    mapfile -t https_services &amp;lt; &amp;lt;(&lt;br /&gt;
      jq -r '.spec.rules[]?.http.paths[]?.backend.service.name // empty' &amp;lt;&amp;lt;&amp;lt;&amp;quot;$ingress_json&amp;quot; |&lt;br /&gt;
        awk 'NF' | sort -u&lt;br /&gt;
    )&lt;br /&gt;
    for svc in &amp;quot;${https_services[@]}&amp;quot;; do&lt;br /&gt;
      cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
---&lt;br /&gt;
apiVersion: gateway.networking.k8s.io/v1&lt;br /&gt;
kind: BackendTLSPolicy&lt;br /&gt;
metadata:&lt;br /&gt;
  name: $(resource_name &amp;quot;${ingress}-${svc}-backend-tls&amp;quot;)&lt;br /&gt;
  namespace: $namespace&lt;br /&gt;
  labels:&lt;br /&gt;
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway&lt;br /&gt;
    app.kubernetes.io/source-ingress: $ingress&lt;br /&gt;
spec:&lt;br /&gt;
  targetRefs:&lt;br /&gt;
    - group: &amp;quot;&amp;quot;&lt;br /&gt;
      kind: Service&lt;br /&gt;
      name: $svc&lt;br /&gt;
  validation:&lt;br /&gt;
    hostname: $(yaml_quote &amp;quot;$backend_tls_hostname&amp;quot;)&lt;br /&gt;
EOF&lt;br /&gt;
      if [[ &amp;quot;$backend_tls&amp;quot; == &amp;quot;system&amp;quot; ]]; then&lt;br /&gt;
        cat &amp;lt;&amp;lt;'EOF'&lt;br /&gt;
    wellKnownCACertificates: System&lt;br /&gt;
EOF&lt;br /&gt;
      else&lt;br /&gt;
        cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
    caCertificateRefs:&lt;br /&gt;
      - group: &amp;quot;&amp;quot;&lt;br /&gt;
        kind: ConfigMap&lt;br /&gt;
        name: $backend_tls_ca_configmap&lt;br /&gt;
EOF&lt;br /&gt;
      fi&lt;br /&gt;
    done&lt;br /&gt;
  fi&lt;br /&gt;
&lt;br /&gt;
  if [[ &amp;quot;$emit_certificate&amp;quot; == &amp;quot;true&amp;quot; &amp;amp;&amp;amp; &amp;quot;$has_tls&amp;quot; == &amp;quot;true&amp;quot; ]]; then&lt;br /&gt;
    for secret in &amp;quot;${tls_secrets[@]}&amp;quot;; do&lt;br /&gt;
      mapfile -t cert_hosts &amp;lt; &amp;lt;(&lt;br /&gt;
        jq -r --arg secret &amp;quot;$secret&amp;quot; '&lt;br /&gt;
          .spec.tls[]? | select(.secretName == $secret) | .hosts[]?&lt;br /&gt;
        ' &amp;lt;&amp;lt;&amp;lt;&amp;quot;$ingress_json&amp;quot; | awk 'NF' | sort -u&lt;br /&gt;
      )&lt;br /&gt;
      [[ &amp;quot;${#cert_hosts[@]}&amp;quot; -gt 0 ]] || cert_hosts=(&amp;quot;${hosts[@]}&amp;quot;)&lt;br /&gt;
      cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
---&lt;br /&gt;
apiVersion: cert-manager.io/v1&lt;br /&gt;
kind: Certificate&lt;br /&gt;
metadata:&lt;br /&gt;
  name: $secret&lt;br /&gt;
  namespace: $([[ &amp;quot;$create_gateway&amp;quot; == &amp;quot;false&amp;quot; ]] &amp;amp;&amp;amp; printf '%s' &amp;quot;$gateway_namespace&amp;quot; || printf '%s' &amp;quot;$namespace&amp;quot;)&lt;br /&gt;
  labels:&lt;br /&gt;
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway&lt;br /&gt;
    app.kubernetes.io/source-ingress: $ingress&lt;br /&gt;
spec:&lt;br /&gt;
  secretName: $secret&lt;br /&gt;
  dnsNames:&lt;br /&gt;
EOF&lt;br /&gt;
      for host in &amp;quot;${cert_hosts[@]}&amp;quot;; do&lt;br /&gt;
        printf '    - %s\n' &amp;quot;$(yaml_quote &amp;quot;$host&amp;quot;)&amp;quot;&lt;br /&gt;
      done&lt;br /&gt;
      cat &amp;lt;&amp;lt;EOF&lt;br /&gt;
  issuerRef:&lt;br /&gt;
    group: cert-manager.io&lt;br /&gt;
    kind: ClusterIssuer&lt;br /&gt;
    name: $issuer&lt;br /&gt;
EOF&lt;br /&gt;
    done&lt;br /&gt;
  fi&lt;br /&gt;
} &amp;gt;&amp;quot;$tmp&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if [[ &amp;quot;$apply&amp;quot; == &amp;quot;true&amp;quot; ]]; then&lt;br /&gt;
  &amp;quot;$kubectl_cmd&amp;quot; apply -f &amp;quot;$tmp&amp;quot;&lt;br /&gt;
  if [[ &amp;quot;$create_gateway&amp;quot; == &amp;quot;false&amp;quot; &amp;amp;&amp;amp; &amp;quot;$has_tls&amp;quot; == &amp;quot;true&amp;quot; ]]; then&lt;br /&gt;
    for secret in &amp;quot;${tls_secrets[@]}&amp;quot;; do&lt;br /&gt;
      patch_gateway_cert_ref &amp;quot;$secret&amp;quot;&lt;br /&gt;
    done&lt;br /&gt;
  fi&lt;br /&gt;
else&lt;br /&gt;
  cat &amp;quot;$tmp&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Envoy_from_nginx_for_ingress&amp;diff=5714</id>
		<title>Envoy from nginx for ingress</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Envoy_from_nginx_for_ingress&amp;diff=5714"/>
		<updated>2026-06-22T02:18:10Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;go install github.com/kubernetes-sigs/ingress2gateway@latest  # Translate an existing NGINX ingress into Gateway API manifests ingress2gateway print --providers ingress-nginx...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;go install github.com/kubernetes-sigs/ingress2gateway@latest&lt;br /&gt;
&lt;br /&gt;
# Translate an existing NGINX ingress into Gateway API manifests&lt;br /&gt;
ingress2gateway print --providers ingress-nginx &amp;gt; new-envoy-routes.yaml&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=AI_notes&amp;diff=5713</id>
		<title>AI notes</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=AI_notes&amp;diff=5713"/>
		<updated>2026-06-20T10:24:51Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot;https://youtu.be/sOPDGQjFcuM?is=pGCLuNECaFYHC2xa&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;https://youtu.be/sOPDGQjFcuM?is=pGCLuNECaFYHC2xa&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Docker_install&amp;diff=5712</id>
		<title>Docker install</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Docker_install&amp;diff=5712"/>
		<updated>2026-06-19T23:10:20Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;# Install&lt;br /&gt;
&lt;br /&gt;
## Ubuntu Docker-CE Install using zfs for data mount&lt;br /&gt;
```&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
DATASET_NAME=&amp;quot;tank-nvme/docker&amp;quot;&lt;br /&gt;
DOCKER_DIR=&amp;quot;/var/lib/docker&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if ! sudo zfs list &amp;quot;${DATASET_NAME%%/*}&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
    echo &amp;quot;Error: ZFS pool '${DATASET_NAME%%/*}' does not exist.&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
    exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
if sudo zfs list &amp;quot;$DATASET_NAME&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
    echo &amp;quot;ZFS dataset $DATASET_NAME already exists.&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    sudo zfs create -o mountpoint=legacy &amp;quot;$DATASET_NAME&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# Add Docker's official GPG key:&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install ca-certificates curl&lt;br /&gt;
sudo install -m 0755 -d /etc/apt/keyrings&lt;br /&gt;
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc&lt;br /&gt;
sudo chmod a+r /etc/apt/keyrings/docker.asc&lt;br /&gt;
&lt;br /&gt;
# Add the repository to Apt sources:&lt;br /&gt;
sudo tee /etc/apt/sources.list.d/docker.sources &amp;lt;&amp;lt;EOF&lt;br /&gt;
Types: deb&lt;br /&gt;
URIs: https://download.docker.com/linux/ubuntu&lt;br /&gt;
Suites: $(. /etc/os-release &amp;amp;&amp;amp; echo &amp;quot;${UBUNTU_CODENAME:-$VERSION_CODENAME}&amp;quot;)&lt;br /&gt;
Components: stable&lt;br /&gt;
Architectures: $(dpkg --print-architecture)&lt;br /&gt;
Signed-By: /etc/apt/keyrings/docker.asc&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
sudo apt update&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
sudo apt-get update&lt;br /&gt;
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin&lt;br /&gt;
&lt;br /&gt;
sudo systemctl stop docker&lt;br /&gt;
sudo systemctl stop docker.socket&lt;br /&gt;
&lt;br /&gt;
sudo mkdir -p &amp;quot;$DOCKER_DIR&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if ! mountpoint -q &amp;quot;$DOCKER_DIR&amp;quot;; then&lt;br /&gt;
    sudo mount -t zfs &amp;quot;$DATASET_NAME&amp;quot; &amp;quot;$DOCKER_DIR&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
if ! grep -q &amp;quot;$DATASET_NAME&amp;quot; /etc/fstab; then&lt;br /&gt;
    echo &amp;quot;$DATASET_NAME $DOCKER_DIR zfs defaults 0 0&amp;quot; | sudo tee -a /etc/fstab &amp;gt; /dev/null&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
sudo mkdir -p /etc/docker&lt;br /&gt;
sudo tee /etc/docker/daemon.json &amp;gt; /dev/null &amp;lt;&amp;lt;EOF&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;storage-driver&amp;quot;: &amp;quot;zfs&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
sudo systemctl start docker&lt;br /&gt;
sleep 5&lt;br /&gt;
sudo docker run hello-world&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Docker_install&amp;diff=5711</id>
		<title>Docker install</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Docker_install&amp;diff=5711"/>
		<updated>2026-06-19T23:08:21Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;# Install&lt;br /&gt;
&lt;br /&gt;
## Ubuntu Install using zfs for data&lt;br /&gt;
```&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
DATASET_NAME=&amp;quot;tank-nvme/docker&amp;quot;&lt;br /&gt;
DOCKER_DIR=&amp;quot;/var/lib/docker&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if ! sudo zfs list &amp;quot;${DATASET_NAME%%/*}&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
    echo &amp;quot;Error: ZFS pool '${DATASET_NAME%%/*}' does not exist.&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
    exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
if sudo zfs list &amp;quot;$DATASET_NAME&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
    echo &amp;quot;ZFS dataset $DATASET_NAME already exists.&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    sudo zfs create -o mountpoint=legacy &amp;quot;$DATASET_NAME&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# Add Docker's official GPG key:&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install ca-certificates curl&lt;br /&gt;
sudo install -m 0755 -d /etc/apt/keyrings&lt;br /&gt;
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc&lt;br /&gt;
sudo chmod a+r /etc/apt/keyrings/docker.asc&lt;br /&gt;
&lt;br /&gt;
# Add the repository to Apt sources:&lt;br /&gt;
sudo tee /etc/apt/sources.list.d/docker.sources &amp;lt;&amp;lt;EOF&lt;br /&gt;
Types: deb&lt;br /&gt;
URIs: https://download.docker.com/linux/ubuntu&lt;br /&gt;
Suites: $(. /etc/os-release &amp;amp;&amp;amp; echo &amp;quot;${UBUNTU_CODENAME:-$VERSION_CODENAME}&amp;quot;)&lt;br /&gt;
Components: stable&lt;br /&gt;
Architectures: $(dpkg --print-architecture)&lt;br /&gt;
Signed-By: /etc/apt/keyrings/docker.asc&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
sudo apt update&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
sudo apt-get update&lt;br /&gt;
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin&lt;br /&gt;
&lt;br /&gt;
sudo systemctl stop docker&lt;br /&gt;
sudo systemctl stop docker.socket&lt;br /&gt;
&lt;br /&gt;
sudo mkdir -p &amp;quot;$DOCKER_DIR&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if ! mountpoint -q &amp;quot;$DOCKER_DIR&amp;quot;; then&lt;br /&gt;
    sudo mount -t zfs &amp;quot;$DATASET_NAME&amp;quot; &amp;quot;$DOCKER_DIR&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
if ! grep -q &amp;quot;$DATASET_NAME&amp;quot; /etc/fstab; then&lt;br /&gt;
    echo &amp;quot;$DATASET_NAME $DOCKER_DIR zfs defaults 0 0&amp;quot; | sudo tee -a /etc/fstab &amp;gt; /dev/null&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
sudo mkdir -p /etc/docker&lt;br /&gt;
sudo tee /etc/docker/daemon.json &amp;gt; /dev/null &amp;lt;&amp;lt;EOF&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;storage-driver&amp;quot;: &amp;quot;zfs&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
sudo systemctl start docker&lt;br /&gt;
sleep 5&lt;br /&gt;
sudo docker run hello-world&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Docker_install&amp;diff=5710</id>
		<title>Docker install</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Docker_install&amp;diff=5710"/>
		<updated>2026-06-19T23:07:23Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
DATASET_NAME=&amp;quot;tank-nvme/docker&amp;quot;&lt;br /&gt;
DOCKER_DIR=&amp;quot;/var/lib/docker&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if ! sudo zfs list &amp;quot;${DATASET_NAME%%/*}&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
    echo &amp;quot;Error: ZFS pool '${DATASET_NAME%%/*}' does not exist.&amp;quot; &amp;gt;&amp;amp;2&lt;br /&gt;
    exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
if sudo zfs list &amp;quot;$DATASET_NAME&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
    echo &amp;quot;ZFS dataset $DATASET_NAME already exists.&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    sudo zfs create -o mountpoint=legacy &amp;quot;$DATASET_NAME&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# Add Docker's official GPG key:&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install ca-certificates curl&lt;br /&gt;
sudo install -m 0755 -d /etc/apt/keyrings&lt;br /&gt;
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc&lt;br /&gt;
sudo chmod a+r /etc/apt/keyrings/docker.asc&lt;br /&gt;
&lt;br /&gt;
# Add the repository to Apt sources:&lt;br /&gt;
sudo tee /etc/apt/sources.list.d/docker.sources &amp;lt;&amp;lt;EOF&lt;br /&gt;
Types: deb&lt;br /&gt;
URIs: https://download.docker.com/linux/ubuntu&lt;br /&gt;
Suites: $(. /etc/os-release &amp;amp;&amp;amp; echo &amp;quot;${UBUNTU_CODENAME:-$VERSION_CODENAME}&amp;quot;)&lt;br /&gt;
Components: stable&lt;br /&gt;
Architectures: $(dpkg --print-architecture)&lt;br /&gt;
Signed-By: /etc/apt/keyrings/docker.asc&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
sudo apt update&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
sudo apt-get update&lt;br /&gt;
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin&lt;br /&gt;
&lt;br /&gt;
sudo systemctl stop docker&lt;br /&gt;
sudo systemctl stop docker.socket&lt;br /&gt;
&lt;br /&gt;
sudo mkdir -p &amp;quot;$DOCKER_DIR&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if ! mountpoint -q &amp;quot;$DOCKER_DIR&amp;quot;; then&lt;br /&gt;
    sudo mount -t zfs &amp;quot;$DATASET_NAME&amp;quot; &amp;quot;$DOCKER_DIR&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
if ! grep -q &amp;quot;$DATASET_NAME&amp;quot; /etc/fstab; then&lt;br /&gt;
    echo &amp;quot;$DATASET_NAME $DOCKER_DIR zfs defaults 0 0&amp;quot; | sudo tee -a /etc/fstab &amp;gt; /dev/null&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
sudo mkdir -p /etc/docker&lt;br /&gt;
sudo tee /etc/docker/daemon.json &amp;gt; /dev/null &amp;lt;&amp;lt;EOF&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;storage-driver&amp;quot;: &amp;quot;zfs&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
sudo systemctl start docker&lt;br /&gt;
sleep 5&lt;br /&gt;
sudo docker run hello-world&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Zfs_pool&amp;diff=5709</id>
		<title>Zfs pool</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Zfs_pool&amp;diff=5709"/>
		<updated>2026-06-15T21:46:57Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
```&lt;br /&gt;
sudo zpool create -o ashift=12 tank-nvme mirror /dev/disk/by-id/nvme-eui.0025385281b1b872 /dev/disk/by-id/nvme-eui.0025385281b1b878&lt;br /&gt;
&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/nvme-Samsung_SSD_960_EVO_1TB_S3X3NF0K204029J&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/nvme-Samsung_SSD_960_EVO_1TB_S3X3NF0K204035E&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RMS&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RPX&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN0339T&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RFB&lt;br /&gt;
&lt;br /&gt;
sudo zpool create -o ashift=12 tank-hdd mirror /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RMS /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RPX mirror /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN0339T /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RFB&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
sudo zfs create -o atime=off -o compression=lz4 -o xattr=sa -o acltype=posixacl tank-nvme/lxd&lt;br /&gt;
&lt;br /&gt;
## ashift&lt;br /&gt;
&lt;br /&gt;
`ashift` stands for **alignment shift**. It dictates the minimum block size ZFS will use when formatting and writing data to the physical storage devices in your pool.&lt;br /&gt;
&lt;br /&gt;
The number you provide is an exponent of 2:&lt;br /&gt;
&lt;br /&gt;
* `ashift=9` means $2^9=512$ bytes.&lt;br /&gt;
* `ashift=12` means $2^{12}=4096$ bytes (4K).&lt;br /&gt;
* `ashift=13` means $2^{13}=8192$ bytes (8K).&lt;br /&gt;
&lt;br /&gt;
Here is why forcing `ashift=12` is critical for modern storage.&lt;br /&gt;
&lt;br /&gt;
### **The 512-byte Lie (Emulation)**&lt;br /&gt;
&lt;br /&gt;
Historically, hard drives used physical sectors that were exactly 512 bytes in size. However, almost all modern hard drives and SSDs use **Advanced Format**, meaning their physical layout is built on 4096-byte (4K) sectors. This larger size allows for higher storage density and better error correction.&lt;br /&gt;
&lt;br /&gt;
To avoid breaking older operating systems and legacy hardware controllers, many modern 4K drives &amp;quot;lie&amp;quot; to the host system. They use a firmware feature called **512e (512-byte emulation)** to report themselves as having old-school 512-byte sectors, even though their physical architecture is 4K.&lt;br /&gt;
&lt;br /&gt;
### **The Read-Modify-Write Penalty**&lt;br /&gt;
&lt;br /&gt;
If you create a ZFS pool without specifying the `ashift` value, ZFS will often interrogate the drive, hear the 512-byte lie, and set `ashift=9`. This creates a severe misalignment between ZFS's logical blocks and the drive's physical sectors.&lt;br /&gt;
&lt;br /&gt;
If ZFS attempts to write a 512-byte block to a physical 4K sector, the storage drive is forced to execute a **Read-Modify-Write** operation:&lt;br /&gt;
&lt;br /&gt;
1. **Read:** The drive reads the entire 4K physical sector into its internal memory.&lt;br /&gt;
2. **Modify:** The drive inserts the 512 bytes ZFS sent into the 4K block.&lt;br /&gt;
3. **Write:** The drive writes the whole 4K sector back to the disk.&lt;br /&gt;
&lt;br /&gt;
This massive overhead completely destroys write performance (especially random I/O) and causes severe &amp;quot;write amplification,&amp;quot; which prematurely burns through the endurance limits of NVMe and SSD drives.&lt;br /&gt;
&lt;br /&gt;
### **The Solution**&lt;br /&gt;
&lt;br /&gt;
By explicitly appending `-o ashift=12` to your `zpool create` command, you force ZFS to align all of its data payloads to exact 4K boundaries. This ensures ZFS writes map perfectly to the physical hardware beneath it, entirely bypassing the emulation penalty and ensuring optimal throughput and drive lifespan.&lt;br /&gt;
&lt;br /&gt;
&amp;gt; **Note:** The `ashift` value is permanently baked into a top-level virtual device (vdev) at the moment of creation. If you create a pool with the wrong `ashift`, it cannot be changed later; you have to destroy the pool, wipe the drives, and start over.&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Zfs_pool&amp;diff=5708</id>
		<title>Zfs pool</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Zfs_pool&amp;diff=5708"/>
		<updated>2026-06-15T20:56:16Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
```&lt;br /&gt;
sudo zpool create -o ashift=12 tank-nvme mirror /dev/disk/by-id/nvme-eui.0025385281b1b872 /dev/disk/by-id/nvme-eui.0025385281b1b878&lt;br /&gt;
&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/nvme-Samsung_SSD_960_EVO_1TB_S3X3NF0K204029J&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/nvme-Samsung_SSD_960_EVO_1TB_S3X3NF0K204035E&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RMS&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RPX&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN0339T&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RFB&lt;br /&gt;
&lt;br /&gt;
sudo zpool create -o ashift=12 tank-hdd mirror /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RMS /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RPX mirror /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN0339T /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RFB&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## ashift&lt;br /&gt;
&lt;br /&gt;
`ashift` stands for **alignment shift**. It dictates the minimum block size ZFS will use when formatting and writing data to the physical storage devices in your pool.&lt;br /&gt;
&lt;br /&gt;
The number you provide is an exponent of 2:&lt;br /&gt;
&lt;br /&gt;
* `ashift=9` means $2^9=512$ bytes.&lt;br /&gt;
* `ashift=12` means $2^{12}=4096$ bytes (4K).&lt;br /&gt;
* `ashift=13` means $2^{13}=8192$ bytes (8K).&lt;br /&gt;
&lt;br /&gt;
Here is why forcing `ashift=12` is critical for modern storage.&lt;br /&gt;
&lt;br /&gt;
### **The 512-byte Lie (Emulation)**&lt;br /&gt;
&lt;br /&gt;
Historically, hard drives used physical sectors that were exactly 512 bytes in size. However, almost all modern hard drives and SSDs use **Advanced Format**, meaning their physical layout is built on 4096-byte (4K) sectors. This larger size allows for higher storage density and better error correction.&lt;br /&gt;
&lt;br /&gt;
To avoid breaking older operating systems and legacy hardware controllers, many modern 4K drives &amp;quot;lie&amp;quot; to the host system. They use a firmware feature called **512e (512-byte emulation)** to report themselves as having old-school 512-byte sectors, even though their physical architecture is 4K.&lt;br /&gt;
&lt;br /&gt;
### **The Read-Modify-Write Penalty**&lt;br /&gt;
&lt;br /&gt;
If you create a ZFS pool without specifying the `ashift` value, ZFS will often interrogate the drive, hear the 512-byte lie, and set `ashift=9`. This creates a severe misalignment between ZFS's logical blocks and the drive's physical sectors.&lt;br /&gt;
&lt;br /&gt;
If ZFS attempts to write a 512-byte block to a physical 4K sector, the storage drive is forced to execute a **Read-Modify-Write** operation:&lt;br /&gt;
&lt;br /&gt;
1. **Read:** The drive reads the entire 4K physical sector into its internal memory.&lt;br /&gt;
2. **Modify:** The drive inserts the 512 bytes ZFS sent into the 4K block.&lt;br /&gt;
3. **Write:** The drive writes the whole 4K sector back to the disk.&lt;br /&gt;
&lt;br /&gt;
This massive overhead completely destroys write performance (especially random I/O) and causes severe &amp;quot;write amplification,&amp;quot; which prematurely burns through the endurance limits of NVMe and SSD drives.&lt;br /&gt;
&lt;br /&gt;
### **The Solution**&lt;br /&gt;
&lt;br /&gt;
By explicitly appending `-o ashift=12` to your `zpool create` command, you force ZFS to align all of its data payloads to exact 4K boundaries. This ensures ZFS writes map perfectly to the physical hardware beneath it, entirely bypassing the emulation penalty and ensuring optimal throughput and drive lifespan.&lt;br /&gt;
&lt;br /&gt;
&amp;gt; **Note:** The `ashift` value is permanently baked into a top-level virtual device (vdev) at the moment of creation. If you create a pool with the wrong `ashift`, it cannot be changed later; you have to destroy the pool, wipe the drives, and start over.&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Zfs_pool&amp;diff=5707</id>
		<title>Zfs pool</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Zfs_pool&amp;diff=5707"/>
		<updated>2026-06-15T20:52:51Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
```&lt;br /&gt;
sudo zpool create -o ashift=12 tank-nvme mirror /dev/disk/by-id/nvme-eui.0025385281b1b872 /dev/disk/by-id/nvme-eui.0025385281b1b878&lt;br /&gt;
&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/nvme-Samsung_SSD_960_EVO_1TB_S3X3NF0K204029J&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/nvme-Samsung_SSD_960_EVO_1TB_S3X3NF0K204035E&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RMS&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RPX&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN0339T&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RFB&lt;br /&gt;
&lt;br /&gt;
sudo zpool create -o ashift=12 tank-hdd mirror /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RMS /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RPX mirror /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN0339T /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RFB&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## ashift&lt;br /&gt;
&lt;br /&gt;
ashift stands for alignment shift. It dictates the minimum block size ZFS will use when formatting and writing data to the physical storage devices in your pool.The number you provide is an exponent of 2:ashift=9 means $2^9=512$ bytes.ashift=12 means $2^{12}=4096$ bytes (4K).ashift=13 means $2^{13}=8192$ bytes (8K).Here is why forcing ashift=12 is critical for modern storage.The 512-byte Lie (Emulation)Historically, hard drives used physical sectors that were exactly 512 bytes in size. However, almost all modern hard drives and SSDs use Advanced Format, meaning their physical layout is built on 4096-byte (4K) sectors. This larger size allows for higher storage density and better error correction.To avoid breaking older operating systems and legacy hardware controllers, many modern 4K drives &amp;quot;lie&amp;quot; to the host system. They use a firmware feature called 512e (512-byte emulation) to report themselves as having old-school 512-byte sectors, even though their physical architecture is 4K.The Read-Modify-Write PenaltyIf you create a ZFS pool without specifying the ashift value, ZFS will often interrogate the drive, hear the 512-byte lie, and set ashift=9. This creates a severe misalignment between ZFS's logical blocks and the drive's physical sectors.If ZFS attempts to write a 512-byte block to a physical 4K sector, the storage drive is forced to execute a Read-Modify-Write operation:Read: The drive reads the entire 4K physical sector into its internal memory.Modify: The drive inserts the 512 bytes ZFS sent into the 4K block.Write: The drive writes the whole 4K sector back to the disk.This massive overhead completely destroys write performance (especially random I/O) and causes severe &amp;quot;write amplification,&amp;quot; which prematurely burns through the endurance limits of NVMe and SSD drives.The SolutionBy explicitly appending -o ashift=12 to your zpool create command, you force ZFS to align all of its data payloads to exact 4K boundaries. This ensures ZFS writes map perfectly to the physical hardware beneath it, entirely bypassing the emulation penalty and ensuring optimal throughput and drive lifespan.Note: The ashift value is permanently baked into a top-level virtual device (vdev) at the moment of creation. If you create a pool with the wrong ashift, it cannot be changed later; you have to destroy the pool, wipe the drives, and start over.&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Zfs_pool&amp;diff=5706</id>
		<title>Zfs pool</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Zfs_pool&amp;diff=5706"/>
		<updated>2026-06-15T20:47:33Z</updated>

		<summary type="html">&lt;p&gt;Busk: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
```&lt;br /&gt;
sudo zpool create -o ashift=12 tank-nvme mirror /dev/disk/by-id/nvme-eui.0025385281b1b872 /dev/disk/by-id/nvme-eui.0025385281b1b878&lt;br /&gt;
&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/nvme-Samsung_SSD_960_EVO_1TB_S3X3NF0K204029J&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/nvme-Samsung_SSD_960_EVO_1TB_S3X3NF0K204035E&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RMS&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RPX&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN0339T&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RFB&lt;br /&gt;
&lt;br /&gt;
sudo zpool create -o ashift=12 tank-hdd mirror /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RMS /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RPX mirror /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN0339T /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RFB&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Zfs_pool&amp;diff=5705</id>
		<title>Zfs pool</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Zfs_pool&amp;diff=5705"/>
		<updated>2026-06-15T20:47:12Z</updated>

		<summary type="html">&lt;p&gt;Busk: Created page with &amp;quot; ``` sudo zpool create -o ashift=12 tank1 mirror /dev/disk/by-id/nvme-eui.0025385281b1b872 /dev/disk/by-id/nvme-eui.0025385281b1b878  sudo wipefs -a /dev/disk/by-id/nvme-Samsu...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
```&lt;br /&gt;
sudo zpool create -o ashift=12 tank1 mirror /dev/disk/by-id/nvme-eui.0025385281b1b872 /dev/disk/by-id/nvme-eui.0025385281b1b878&lt;br /&gt;
&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/nvme-Samsung_SSD_960_EVO_1TB_S3X3NF0K204029J&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/nvme-Samsung_SSD_960_EVO_1TB_S3X3NF0K204035E&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RMS&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RPX&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN0339T&lt;br /&gt;
sudo wipefs -a /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RFB&lt;br /&gt;
&lt;br /&gt;
sudo zpool create -o ashift=12 mypool mirror /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RMS /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RPX mirror /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN0339T /dev/disk/by-id/ata-ST4000DM004-2CV104_ZFN07RFB&lt;br /&gt;
```&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
</feed>