[{"content":"Also available as RSS Feed or Telegram Channel\n","date":null,"permalink":"https://techwolf12.nl/blog/","section":"Blog posts","summary":"","title":"Blog posts"},{"content":"DevOPS is a collection of blog articles related to CI/CD, server management and the likes. It aims to improve the Development lifecycle and ease the burden of managing systems.\nAlso available as RSS Feed.\n","date":null,"permalink":"https://techwolf12.nl/tag/devops/","section":"Tags","summary":"","title":"DevOPS"},{"content":"Also available as RSS Feed.\n","date":null,"permalink":"https://techwolf12.nl/tag/security/","section":"Tags","summary":"","title":"Security"},{"content":"FreeRADIUS is a powerful open-source RADIUS server that provides centralized authentication and authorization for users connecting to a network service. In this post, we\u0026rsquo;ll walk through setting up FreeRADIUS with Google Workspace authentication, per-client VLAN assignment, and SSL authentication. The result: multiple offices authenticate securely against Google, and users land in the right VLAN based on who they are and where they connect from.\nThe setup also handles a Google Workspace environment with multiple domains attached, working correctly with Google\u0026rsquo;s Secure LDAP functionality.\nAs a running example, we assume a Google Workspace environment with two domains, example.com and techwolf12.nl, and two offices, one in amsterdam and one in utrecht.\nInstallation #We assume a Debian 13 server; other distros may require different steps or file paths, which you can adjust accordingly.\nInstall FreeRADIUS and its utilities:\nsudo apt update sudo apt install freeradius freeradius-utils freeradius-ldap Certificates #This setup uses certificates in three places, and it\u0026rsquo;s worth understanding each role before touching the config: the EAP server certificate that Wi-Fi clients validate, the CA bundle the server trusts, and the Google Secure LDAP client certificate used to talk to Google. Certificate problems are by far the most common reason an otherwise-correct FreeRADIUS setup fails, usually with vague supplicant-side errors.\nThe EAP server certificate #During the EAP handshake, the RADIUS server presents a TLS certificate to the connecting device, just like a website would. For clients to accept it, the certificate needs more than just being signed by a trusted CA:\nExtended Key Usage must include TLS Web Server Authentication (serverAuth, OID 1.3.6.1.5.5.7.3.1). Windows, Android, and Apple supplicants all reject certificates without it. This is the classic \u0026ldquo;it\u0026rsquo;s signed by our CA but devices still won\u0026rsquo;t connect\u0026rdquo; gotcha, a CA signature alone is not enough if the EKU is missing. The hostname must be in the Subject Alternative Name, e.g. radius.infra.example.com. Modern supplicants (Android 11+ in particular, which no longer allows \u0026ldquo;don\u0026rsquo;t validate\u0026rdquo;) match the configured domain against the SAN, not the CN. Key Usage should include digitalSignature and keyEncipherment. A reasonable validity period. Apple platforms are strict about long-lived TLS certificates; keep it at a year or two and automate renewal, because when this certificate expires, Wi-Fi breaks for everyone at once. Put the renewal date in your calendar or monitoring. The full chain in certificate_file. If your CA uses an intermediate, concatenate the server certificate and the intermediate into one PEM file. Devices that don\u0026rsquo;t have the intermediate cached will otherwise fail chain validation. If you generate the certificate with OpenSSL yourself, the FreeRADIUS package ships an xpextensions file in /etc/freeradius/3.0/certs/ with the correct extensions ready to use in your signing config.\nWhich CA to use #A certificate from an internal/private CA is actually preferable to a public one here. Wi-Fi supplicants trust the CA you configure for that network profile, if you use a public CA, any certificate that CA has ever issued for anyone could pass validation on devices that skip hostname matching. With a private CA used only for your RADIUS infrastructure, the trust scope is exactly as narrow as it should be. The trade-off is that you must distribute the CA certificate to devices, which in a Google Workspace shop is easy: push it together with the Wi-Fi profile via Google MDM / endpoint management, so devices get the SSID, the CA, and the expected server name in one go.\nThe ca_file in the EAP configuration serves the opposite direction: it lists the CA(s) whose client certificates you accept for EAP-TLS. If you only use TTLS/PEAP with passwords, it\u0026rsquo;s unused, but configuring it correctly from day one means EAP-TLS (more later) is a small upgrade.\nThe Google Secure LDAP client certificate #Google\u0026rsquo;s Secure LDAP service authenticates your server with a client certificate that you download from the Admin console. Two things to know: it\u0026rsquo;s valid for a long time but not forever, so track its expiry alongside your EAP certificate, and it identifies your whole LDAP client, treat the key with the same care as a service-account key. It goes in /etc/freeradius/3.0/certs/ as ldap-client.crt and ldap-client.key, readable only by the freerad user.\nGoogle Workspace setup #Create a new Secure LDAP client # In the Google Admin console, go to Apps → LDAP and click Add Client. Give it a name (e.g. \u0026ldquo;FreeRADIUS\u0026rdquo;) and set the access permissions: verify user credentials and read user information for the organizational units you want to authenticate. If you want group-based overrides (see later section), also allow reading group information. Download the generated client certificate and key, and place them on the server as /etc/freeradius/3.0/certs/ldap-client.crt and /etc/freeradius/3.0/certs/ldap-client.key. Generate access credentials (username/password) for the client, we\u0026rsquo;ll use these as identity and password in the LDAP module. Turn the LDAP client ON for the organizational units that need Wi-Fi access. Note that Secure LDAP requires a Google Workspace edition that includes it (Business Plus, Enterprise, or Cloud Identity Premium).\nConfiguration Files #A working setup consists of several configuration files located in /etc/freeradius/3.0. Below we configure each file in turn; you can replace the entire file with these contents and adjust the values (IPs, secrets, domains, VLAN IDs) for your environment.\n1. dictionary #To make some parts easier, we define custom RADIUS attributes we can use internally:\nATTRIBUTE Ldap-DN 3003 string ATTRIBUTE User-Company 3004 string 2. clients.conf #Define the clients (access points, switches, NASes) that may talk to the RADIUS server. Each client is identified by its source IP address and a shared secret. It\u0026rsquo;s recommended to use a unique secret per location.\nclient localhost { ipaddr = 127.0.0.1 proto = * secret = testing123 require_message_authenticator = no nas_type = other limit { max_connections = 16 lifetime = 0 idle_timeout = 30 } } client amsterdam-office { ipaddr = 1.1.1.1 # Replace with your office IP or range secret = secret-shared-with-accesspoints } client utrecht-office { ipaddr = 1.0.0.1 # Replace with your office IP or range secret = secret-shared-with-accesspoints } This works fine as long as every office has a static IP. If one of your locations sits behind a consumer connection with a changing IP, see the dynamic clients section further down.\n3. proxy.conf #Configure the proxy settings. It\u0026rsquo;s handy to list the different domains that will authenticate to your RADIUS server here as empty realms, so usernames like user@example.com are recognized without being proxied anywhere:\nproxy server { } home_server localhost { type = auth ipaddr = 127.0.0.1 port = 1812 secret = testing123 response_window = 20 zombie_period = 40 revive_interval = 120 status_check = status-server check_interval = 30 check_timeout = 4 num_answers_to_alive = 3 max_outstanding = 65536 coa { irt = 2 mrt = 16 mrc = 5 mrd = 30 } limit { max_connections = 16 max_requests = 0 lifetime = 0 idle_timeout = 0 } } realm LOCAL { } realm example.com { } realm techwolf12.nl { } 4. sites-enabled/default #The default virtual server does the heavy lifting: it maps the user\u0026rsquo;s domain to the right LDAP base DN, authenticates against Google, and assigns a VLAN based on the combination of office (via Client-Shortname) and company:\nserver default { listen { type = auth ipaddr = * port = 0 limit { max_connections = 0 lifetime = 0 idle_timeout = 30 } } listen { ipaddr = * port = 0 type = acct limit { } } listen { type = auth ipv6addr = :: port = 0 limit { max_connections = 16 lifetime = 0 idle_timeout = 30 } } listen { ipv6addr = :: port = 0 type = acct limit { } } authorize { filter_username preprocess chap mschap digest suffix eap { ok = return } files -sql if (User-Name =~ /@example\\.com$/i) { update request { Ldap-DN := \u0026#34;dc=example,dc=com\u0026#34; User-Company := \u0026#34;example\u0026#34; } } elsif (User-Name =~ /@techwolf12\\.nl$/i) { update request { Ldap-DN := \u0026#34;dc=techwolf12,dc=nl\u0026#34; User-Company := \u0026#34;techwolf12\u0026#34; } } # Add more company-specific rules here... # Authenticate the user with the LDAP module -ldap # Pick the VLAN based on the office you are in if (Client-Shortname == \u0026#34;amsterdam\u0026#34;) { if (User-Company == \u0026#34;techwolf12\u0026#34;) { update reply { Tunnel-Type = VLAN, Tunnel-Medium-Type = IEEE-802, Tunnel-Private-Group-Id = 20 } } else { update reply { Tunnel-Type = VLAN, Tunnel-Medium-Type = IEEE-802, Tunnel-Private-Group-Id = 40 } } } if (Client-Shortname == \u0026#34;utrecht\u0026#34;) { if (User-Company == \u0026#34;techwolf12\u0026#34;) { update reply { Tunnel-Type = VLAN, Tunnel-Medium-Type = IEEE-802, Tunnel-Private-Group-Id = 70 } } else { update reply { Tunnel-Type = VLAN, Tunnel-Medium-Type = IEEE-802, Tunnel-Private-Group-Id = 81 } } } # Group-based overrides go here -- see the # \u0026#34;Per-group overrides with LDAP groups\u0026#34; section below. expiration logintime pap if (User-Password) { update control { Auth-Type := ldap } } Autz-Type New-TLS-Connection { ok } } authenticate { Auth-Type PAP { ldap } Auth-Type CHAP { chap } Auth-Type MS-CHAP { mschap } mschap digest Auth-Type LDAP { ldap } eap } preacct { preprocess acct_unique suffix files } accounting { detail unix -sql exec attr_filter.accounting_response } session { } post-auth { if (session-state:User-Name \u0026amp;\u0026amp; reply:User-Name \u0026amp;\u0026amp; request:User-Name \u0026amp;\u0026amp; (reply:User-Name == request:User-Name)) { update reply { \u0026amp;User-Name !* ANY } } update { \u0026amp;reply: += \u0026amp;session-state: } -sql exec remove_reply_message_if_eap Post-Auth-Type REJECT { -sql attr_filter.access_reject eap remove_reply_message_if_eap } Post-Auth-Type Challenge { } Post-Auth-Type Client-Lost { } if (EAP-Key-Name \u0026amp;\u0026amp; \u0026amp;reply:EAP-Session-Id) { update reply { \u0026amp;EAP-Key-Name := \u0026amp;reply:EAP-Session-Id } } } pre-proxy { } post-proxy { eap } } 5. sites-enabled/inner-tunnel #The inner-tunnel virtual server handles the protected inner part of tunneled EAP methods (PEAP). It copies the VLAN attributes back to the outer session so they end up in the final Access-Accept:\nserver inner-tunnel { listen { ipaddr = 127.0.0.1 port = 18120 type = auth } authorize { filter_username chap mschap suffix update control { \u0026amp;Proxy-To-Realm := LOCAL } eap { ok = return } files -sql -ldap expiration logintime pap } authenticate { Auth-Type PAP { pap } Auth-Type CHAP { chap } Auth-Type MS-CHAP { mschap } mschap eap } session { radutmp } post-auth { -sql Post-Auth-Type REJECT { -sql attr_filter.access_reject update outer.session-state { \u0026amp;Module-Failure-Message := \u0026amp;request:Module-Failure-Message } } update { \u0026amp;outer.session-state:Tunnel-Type := Tunnel-Type[*] \u0026amp;outer.session-state:Tunnel-Medium-Type := Tunnel-Medium-Type[*] \u0026amp;outer.session-state:Tunnel-Private-Group-Id := Tunnel-Private-Group-Id[*] \u0026amp;outer.session-state:User-Name := User-Name[*] } } pre-proxy { } post-proxy { eap } } 6. mods-enabled/eap #Configure the EAP module for SSL authentication. EAP-TTLS with GTC is the default here because Google\u0026rsquo;s Secure LDAP only gives us a password-verification interface (no NT hashes), which rules out MSCHAPv2 against LDAP. MSCHAPv2 is also considered to be insecure, so we don\u0026rsquo;t want to use it. This does mean, that on Windows devices, username/password login does not work. The certificate files referenced here are the ones from the Certificates section:\neap { default_eap_type = ttls timer_expire = 60 ignore_unknown_eap_types = no cisco_accounting_username_bug = no max_sessions = ${max_requests} gtc { auth_type = PAP } tls-config tls-common { private_key_file = /etc/freeradius/3.0/certs/radius.infra.example.com.key certificate_file = /etc/freeradius/3.0/certs/radius.infra.example.com.pem ca_file = /etc/freeradius/3.0/certs/authorized-ca.pem cipher_list = \u0026#34;DEFAULT\u0026#34; cipher_server_preference = no tls_min_version = \u0026#34;1.2\u0026#34; tls_max_version = \u0026#34;1.2\u0026#34; ecdh_curve = \u0026#34;\u0026#34; } tls { tls = tls-common } ttls { tls = tls-common default_eap_type = gtc copy_request_to_tunnel = no use_tunneled_reply = yes virtual_server = \u0026#34;default\u0026#34; } peap { tls = tls-common default_eap_type = mschapv2 copy_request_to_tunnel = no use_tunneled_reply = yes virtual_server = \u0026#34;inner-tunnel\u0026#34; } mschapv2 { } } 7. mods-enabled/ldap #Configure the LDAP module for Google authentication. Note the base_dn = \u0026amp;Ldap-DN in the user section, that\u0026rsquo;s where the per-domain DN we set in the authorize section comes into play. The group section is what powers the group-based overrides in the next section; cacheable_name = yes makes group memberships get resolved once per request instead of once per comparison:\nldap { server = \u0026#39;ldaps://ldap.google.com\u0026#39; port = 636 base_dn = \u0026#39;dc=example,dc=com\u0026#39; identity = \u0026#39;x\u0026#39; password = x update { control:Password-With-Header += \u0026#39;password\u0026#39; control: += \u0026#39;radiusControlAttribute\u0026#39; request: += \u0026#39;radiusRequestAttribute\u0026#39; reply: += \u0026#39;radiusReplyAttribute\u0026#39; } user_dn = \u0026#34;LDAP-UserDn\u0026#34; user { base_dn = \u0026amp;Ldap-DN filter = \u0026#34;(uid=%{%{Stripped-User-Name}:-%{User-Name}})\u0026#34; } group { base_dn = \u0026#34;${..base_dn}\u0026#34; # Depending on your network groups, you might want to use \u0026amp;Ldap-DN instead for the user\u0026#39;s authenticated DN, or keep it to the default DN filter = \u0026#39;(objectClass=posixGroup)\u0026#39; membership_attribute = \u0026#39;memberOf\u0026#39; name_attribute = cn cacheable_name = yes } client { base_dn = \u0026#34;${..base_dn}\u0026#34; filter = \u0026#39;(objectClass=radiusClient)\u0026#39; attribute { ipaddr = \u0026#39;radiusClientIdentifier\u0026#39; secret = \u0026#39;radiusClientSecret\u0026#39; } } options { chase_referrals = yes rebind = yes res_timeout = 10 srv_timelimit = 3 net_timeout = 1 idle = 60 probes = 3 interval = 3 ldap_debug = 0x0028 } tls { start_tls = no certificate_file = /etc/freeradius/3.0/certs/ldap-client.crt private_key_file = /etc/freeradius/3.0/certs/ldap-client.key require_cert = \u0026#39;allow\u0026#39; } pool { start = ${thread[pool].start_servers} min = ${thread[pool].min_spare_servers} max = ${thread[pool].max_servers} spare = ${thread[pool].max_spare_servers} uses = 0 retry_delay = 30 lifetime = 0 idle_timeout = 60 } } Per-group overrides with LDAP groups #The domain-and-office logic above covers the general case, but real networks always have exceptions: the infra team that needs the management VLAN from any office, contractors that should be fenced off regardless of their domain, or an emergency \u0026ldquo;kill switch\u0026rdquo; group to lock someone out of Wi-Fi without suspending their whole Google account.\nGoogle Groups are exposed through Secure LDAP, so we can use plain Google Groups for all of this, adding someone to a group in the Admin console changes their network access on the next (re)authentication, with zero RADIUS config changes.\nWith the group section from the LDAP module above in place, the LDAP-Group attribute becomes available in unlang. Comparing against it triggers a membership lookup for the current user. Add the override rules to the authorize section of sites-enabled/default, after the office VLAN logic, the := operator overwrites whatever was set before, so the last match wins, which is exactly what you want for exceptions:\n# --- Group-based overrides (place after the office VLAN logic) --- # Blocked group: kicked off Wi-Fi entirely, regardless of anything else. if (LDAP-Group == \u0026#34;wifi-blocked\u0026#34;) { update reply { Reply-Message := \u0026#34;Access denied by policy\u0026#34; } reject } # Infra team: always the management VLAN, from any office. if (LDAP-Group == \u0026#34;wifi-infra\u0026#34;) { update reply { Tunnel-Type := VLAN, Tunnel-Medium-Type := IEEE-802, Tunnel-Private-Group-Id := 99 } } # Contractors: isolated VLAN and forced reauthentication every 8 hours. if (LDAP-Group == \u0026#34;wifi-contractors\u0026#34;) { update reply { Tunnel-Type := VLAN, Tunnel-Medium-Type := IEEE-802, Tunnel-Private-Group-Id := 150, Session-Timeout := 28800 } } The group names here are the cn of the group as exposed via LDAP, for a Google Group wifi-infra@example.com, that\u0026rsquo;s wifi-infra. If you run multiple domains in one Workspace, keep group names unique across domains, or match on the full DN instead of the name to avoid ambiguity.\nA couple of practical notes. Make sure your Secure LDAP client has permission to read group information for the relevant organizational units, or every comparison silently fails (run freeradius -X and look for the group search to verify). Each group comparison can cost an LDAP round trip to Google, so keep the number of distinct groups you check small, with cacheable_name = yes, all the user\u0026rsquo;s groups are fetched once per request and subsequent comparisons are free. And remember the override only applies at (re)authentication time; combine it with a CoA/Disconnect from your controller if \u0026ldquo;immediately\u0026rdquo; matters for the blocked group.\nDynamic clients: NASes with changing IP addresses #The clients.conf setup above assumes every office has a static IP. But what about an access point behind a consumer connection whose IP changes every so often? RADIUS identifies clients by source IP, so a plain static client block won\u0026rsquo;t work.\nFreeRADIUS 3.x has a feature for exactly this: dynamic clients. You define a network range instead of a fixed IP, and point it at a virtual server that decides, per unknown source IP, whether it\u0026rsquo;s a legitimate client and what its secret is. We\u0026rsquo;ll combine that with dynamic DNS: each roaming NAS keeps a DNS record up to date (most routers can do this out of the box), and FreeRADIUS verifies incoming packets by resolving that hostname, directly at the authoritative nameserver, so resolver caches can never serve us a stale IP.\n1. clients.conf addition #client dynamic_dns_clients { # Narrow this as much as you can (e.g. your ISP\u0026#39;s ranges). ipaddr = 0.0.0.0/0 # Name of the virtual server that resolves/validates the client. dynamic_clients = dynamic_clients # How long a discovered client stays cached. While the entry is alive, # DNS is NOT re-checked for that IP. When the NAS gets a new IP, the new # address is unknown and gets validated immediately; the stale entry # simply ages out. lifetime = 300 } 2. The map file: /etc/freeradius/dyn-clients.map #One line per dynamic NAS, with a per-device secret. Adding a NAS later is just adding a line, no restart needed:\n# hostname shortname secret auth_ns (optional, \u0026#34;-\u0026#34; = auto) nas1.dyn.example.com nas1 S3cret-One ns1.example.com nas2.dyn.example.com nas2 S3cret-Two ns1.example.com ap-loft.example.net ap-loft An0ther-Secret - The shortname column is what shows up as Client-Shortname, so you can use it in the VLAN logic in the default virtual server exactly like the static offices. This file contains secrets, so protect it:\nsudo chown freerad:freerad /etc/freeradius/dyn-clients.map sudo chmod 600 /etc/freeradius/dyn-clients.map 3. The lookup script: /usr/local/bin/radius-dyn-client.sh #The script walks the map file, resolves each hostname straight at its authoritative nameserver with dig +norecurse, and on a match prints the client attributes for FreeRADIUS to pick up:\n#!/bin/sh # radius-dyn-client.sh \u0026lt;source-ip\u0026gt; # # Looks up \u0026lt;source-ip\u0026gt; against a map file of dynamic-DNS hostnames. # Each hostname is resolved *directly at its authoritative nameserver* # (dig +norecurse @ns), so no resolver cache is involved. MAP=\u0026#34;/etc/freeradius/dyn-clients.map\u0026#34; SRC_IP=\u0026#34;$1\u0026#34; [ -n \u0026#34;$SRC_IP\u0026#34; ] || exit 0 [ -r \u0026#34;$MAP\u0026#34; ] || exit 0 while read -r host shortname secret ns _rest; do case \u0026#34;$host\u0026#34; in \u0026#34;\u0026#34;|\\#*) continue ;; esac [ -n \u0026#34;$secret\u0026#34; ] || continue # Column 4 (authoritative NS) is optional. Filling it in saves a # lookup per host per validation. if [ -z \u0026#34;$ns\u0026#34; ] || [ \u0026#34;$ns\u0026#34; = \u0026#34;-\u0026#34; ]; then zone=${host#*.} ns=$(dig +short NS \u0026#34;$zone\u0026#34; | head -n1) fi [ -n \u0026#34;$ns\u0026#34; ] || continue # +norecurse against the authoritative server: the answer comes # straight from the zone data, never from a cache. resolved=$(dig +short +norecurse +time=2 +tries=1 A \u0026#34;$host\u0026#34; @\u0026#34;$ns\u0026#34; | head -n1) if [ -n \u0026#34;$resolved\u0026#34; ] \u0026amp;\u0026amp; [ \u0026#34;$resolved\u0026#34; = \u0026#34;$SRC_IP\u0026#34; ]; then printf \u0026#39;FreeRADIUS-Client-Shortname = \u0026#34;%s\u0026#34;, FreeRADIUS-Client-Secret = \u0026#34;%s\u0026#34;\\n\u0026#39; \\ \u0026#34;$shortname\u0026#34; \u0026#34;$secret\u0026#34; exit 0 fi done \u0026lt; \u0026#34;$MAP\u0026#34; exit 0 Install it with sudo install -m 755 radius-dyn-client.sh /usr/local/bin/.\n4. mods-enabled/dynclient_lookup #An exec module instance runs the script and parses its stdout directly into control attributes, no string matching in unlang needed:\nexec dynclient_lookup { wait = yes program = \u0026#34;/usr/local/bin/radius-dyn-client.sh %{Packet-Src-IP-Address}\u0026#34; input_pairs = request output_pairs = control shell_escape = yes # Worst case is roughly 2s per mapped host that fails to resolve. # Size this to: 2 * number_of_hosts, plus margin. timeout = 15 } 5. sites-enabled/dynamic-clients #Finally, the virtual server that the dynamic_clients directive in clients.conf points at. It runs once per unknown source IP (and again after lifetime expires):\nserver dynamic_clients { authorize { dynclient_lookup # The script only emits a secret when the source IP matched a # hostname\u0026#39;s fresh A record. if (\u0026amp;control:FreeRADIUS-Client-Secret) { update control { \u0026amp;FreeRADIUS-Client-IP-Address := \u0026#34;%{Packet-Src-IP-Address}\u0026#34; } ok } else { reject } } } Enable the module and site, then restart:\ncd /etc/freeradius/3.0 sudo ln -s ../mods-available/dynclient_lookup mods-enabled/ sudo ln -s ../sites-available/dynamic-clients sites-enabled/ sudo systemctl restart freeradius How it behaves, and what to watch out for #The DNS lookup only happens for unknown source IPs. Once validated, the client is cached for lifetime seconds and packets flow with zero DNS overhead. When a NAS gets a new IP, the first packet from the new address triggers an immediate fresh validation, so IP changes are picked up right away; lifetime only bounds how long an old, revoked IP would keep working.\nTwo caveats worth knowing. First, the lookup script runs synchronously in a worker thread, so keep the per-host dig timeouts tight (the +time=2 +tries=1 above) and the map file short, for more than a dozen or so devices, a database keyed by IP would be a better fit than linear probing. Second, this design trusts DNS: if your zone isn\u0026rsquo;t DNSSEC-signed and the path to the authoritative nameserver can be spoofed, an attacker who forges the answer only needs the shared secret. For anything fully internet-facing, RadSec (RADIUS over TLS, where clients authenticate with certificates instead of source IPs) is the more robust long-term answer.\nTesting #Run FreeRADIUS in debug mode to watch everything happen:\nsudo systemctl stop freeradius sudo freeradius -X Then authenticate from a device, or test locally with radtest. Things to check in the debug output: the LDAP bind and user search succeeding, the group search firing when a group comparison runs, the correct Tunnel-Private-Group-Id in the Access-Accept, and, for dynamic clients, the server dynamic_clients section firing on the first packet from an unknown IP, followed by normal processing in the default virtual server.\nNext steps #With this setup you get a single FreeRADIUS instance that authenticates users from multiple Google Workspace domains via Secure LDAP, drops them into the right VLAN based on office and company, supports per-group overrides driven entirely by Google Groups, handles SSL-based EAP methods with properly validated certificates, and even accepts access points on dynamic IP addresses, verified against fresh, authoritative DNS on every IP change.\nA natural next step from here is EAP-TLS with per-device client certificates: the tls-common block and ca_file are already in place, so it\u0026rsquo;s mostly a matter of standing up a small device CA and pushing certificates through your MDM.\n","date":"27 July 2026","permalink":"https://techwolf12.nl/blog/freeradius-with-google-workspace-and-ssl/","section":"Blog posts","summary":"Authenticate Wi-Fi users against Google Workspace with FreeRADIUS: Secure LDAP across multiple domains, per-office and per-group VLAN assignment, EAP over TLS, and access points on dynamic IPs verified through authoritative DNS.","title":"Setting Up FreeRADIUS with Google Workspace Authentication, VLAN Support per Client, and SSL Authentication"},{"content":"Use this to search blog posts based on the tag\n","date":null,"permalink":"https://techwolf12.nl/tag/","section":"Tags","summary":"","title":"Tags"},{"content":"\rOh hello there!\rI\u0026rsquo;m Christiaan also known as Techwolf12, a software writing, network operating hacker! Welcome to my personal website, here I collect and share information in the form of blog articles.\nYou might see me write about some of my interests, like:\n(Mobile) Development\nDevOPS\nLinux server management\nNetworking on AS206477\nHAM Radio PD0TW, DMR: 2040188\nSecurity\n3D Printing\nIf you have any questions or remarks about my site or posts, be sure to send me a message via email, or any other social platform listed above.\n","date":null,"permalink":"https://techwolf12.nl/","section":"Techwolf12 Home","summary":"","title":"Techwolf12 Home"},{"content":"Imagine waking up to an alert that your Google Account password has been changed. You immediately head to Google’s account recovery page to reclaim your digital life, only to be met with a bizarre roadblock. The screen doesn\u0026rsquo;t ask for your recovery phone number or your backup email. Instead, it tells you that you must get permission from your \u0026ldquo;parent\u0026rdquo; to log in.\nYou haven\u0026rsquo;t been standard-hacked. You’ve been trapped in the Family Link Exploit, a severe security loophole that weaponizes Google\u0026rsquo;s child safety features against adult users, and currently leaves victims completely stranded.\nWhen this happened to one of my friends, I originally never heard of this type of exploit and started to look into it.\nHere is how this devastating attack works, why standard recovery fails, and what you can do if it happens to you.\nHow the Attack Works #The exploit relies on a clever and malicious manipulation of Google’s Family Link ecosystem, a suite of tools designed for parents to manage their children\u0026rsquo;s devices and accounts.\nWhen a hacker gains initial access to your password (usually through a data breach, phishing or malware infection stealing cookies), they immediately restructure your account\u0026rsquo;s legal metadata using these steps:\n1. Changing Your Birth Year #The hacker navigates to your personal info and changes your date of birth, making you legally a minor (under 13 in most jurisdictions).\n2. Enforcing \u0026ldquo;Parental Supervision\u0026rdquo; #Because Google’s policy dictates that accounts under 13 must be managed by an adult, the hacker instantly links your compromised account to a \u0026ldquo;Parent\u0026rdquo; Google account that they control.\nOnce your account is designated as a \u0026ldquo;Child\u0026rdquo; account under their family group, the hacker gains absolute administrative authority over your digital identity.\nWhy Standard Recovery is Useless #This is where the attack becomes truly insidious. Google’s automated security systems are built to strictly protect minors from unauthorized changes. By law and policy, a \u0026ldquo;child\u0026rdquo; cannot change their own security settings, delete their account, or remove parental control without the parent\u0026rsquo;s explicit consent.\nBecause of this, the moment you try to use Google’s standard self-service recovery page, the system detects you as a minor and demands the hacker’s parental Gmail password to proceed.\nThe automated system essentially locks you in a room and hands the key directly to the intruder. Your recovery phone number, 2-step verification, and backup emails are completely overridden and rendered useless.\nTotal Surveillance and Control #Once you are trapped in a Family Link group, the hacker doesn\u0026rsquo;t just read your emails. They have god-mode access to your physical and digital life. Through the Family Link dashboard, the attacker can:\nTrack your physical location in real-time via Google Maps. Lock your physical Android devices remotely at any time. See your screen time and intercept app downloads. Read incoming emails and intercept password reset links for your bank accounts, social media, etc. Prevention #Two-factor authentication won\u0026rsquo;t protect you here cookie-stealing bypasses it entirely, and Google doesn\u0026rsquo;t require 2FA to change your age or enroll your account in Family Link.\nIf you have Advanced Protection enabled on your Google account, you cannot be added to a Family Link group. If you have no need for parental controls, enabling Advanced Protection is a simple and effective way to protect yourself from this exploit.\nIs There a Way Around It? #As of right now, there is no automated bypass for this exploit once the parental lock is fully engaged. If you click \u0026ldquo;Stop Supervision,\u0026rdquo; Google requires the hacker\u0026rsquo;s login credentials.\nHowever, victims have found exactly two non-standard \u0026ldquo;backdoors\u0026rdquo; to force Google to intervene manually:\n1. The YouTube Escalation Trick #While standard Google/Gmail support is entirely automated by bots, Google maintains a dedicated, human security team for YouTube creators. If your hacked Google account has ever been used to comment on a video or watch YouTube, you technically have a YouTube profile.\nVictims have had the highest success rate by taking to X (formerly Twitter) and publicly tagging @TeamYouTube, stating that their account was hijacked via the Family Link minor exploit. The YouTube team can manually bypass the automated system and send a secure, human-reviewed hijacking form to pull the account out of the family group and help recover your account.\n2. The Google One Support Paid Route #Google provides zero live phone or chat support for free accounts. However, if a victim creates a temporary, secondary Google account and pays a couple of dollars for a Google One storage subscription, they unlock access to live chat with \u0026ldquo;Google Experts.\u0026rdquo; While these experts cannot fix it directly, they can manually escalate the ticket to the specialized Account Safety team.\nGoogle Needs a Fix #The Family Link exploit is a terrifying reminder of how safety features can be inverted into tools of absolute control. Because Google’s automated systems are designed to trust the \u0026ldquo;Parent\u0026rdquo; account implicitly, they are currently blind to the fact that adult accounts are being forcibly converted into children against their will.\nUntil Google implements a manual verification system to prove adulthood during a security breach, the best defense is absolute prevention. Ensure your Google account has an un-phishable form of 2-Step Verification (like an Authenticator App or a physical security key) that is asked when changing age to stop hackers from getting through the front door in the first place.\n","date":"25 May 2026","permalink":"https://techwolf12.nl/blog/google-family-link-exploit/","section":"Blog posts","summary":"Imagine waking up to an alert that your Google Account password has been changed. You haven\u0026rsquo;t been standard-hacked. You’ve been trapped in the Family Link Exploit, a severe security loophole that weaponizes Google\u0026rsquo;s child safety features against adult users, and currently leaves victims completely stranded.","title":"Google Family Link exploit that locks out victims permanently"},{"content":"Welcome to this practical guide filled with MySQL tips and tricks! Whether you\u0026rsquo;re a seasoned database administrator or just getting started with MySQL, these handy queries and techniques will help you manage your databases more efficiently.\nSize Per Database #Understanding the size of your databases can provide valuable insights into resource utilization and help you optimize performance. Use the following query to retrieve the size of each database on your MySQL server:\nSELECT table_schema, COUNT(*) TABLES, CONCAT(ROUND(SUM(table_rows) / 1000000, 2), \u0026#39;M\u0026#39;) rows, CONCAT(ROUND(SUM(data_length) / (1024 * 1024 * 1024), 2), \u0026#39;G\u0026#39;) DATA, CONCAT(ROUND(SUM(index_length) / (1024 * 1024 * 1024), 2), \u0026#39;G\u0026#39;) idx, CONCAT(ROUND(SUM(data_length + index_length) / (1024 * 1024 * 1024), 2), \u0026#39;G\u0026#39;) total_size, ROUND(SUM(index_length) / SUM(data_length), 2) idxfrac FROM information_schema.TABLES GROUP BY table_schema; This query provides a comprehensive overview of each database, including the number of tables, total rows, data size, index size, total size, and the ratio of index size to data size.\nSize Per Table #All Databases #To examine the size of tables across all databases, use the following query:\nSELECT table_schema AS `Database`, table_name AS `Table`, ROUND(((data_length + index_length) / 1024 / 1024), 2) `Size in MB` FROM information_schema.TABLES ORDER BY (data_length + index_length) DESC; This query retrieves the size of each table in megabytes and sorts the results in descending order by total size.\nSingle Database #If you\u0026rsquo;re interested in the size of tables within a specific database, modify the query to filter by the desired database name:\nSELECT table_schema AS `Database`, table_name AS `Table`, ROUND(((data_length + index_length) / 1024 / 1024), 2) `Size in MB` FROM information_schema.TABLES WHERE table_schema = \u0026#34;your_database_name_here\u0026#34; ORDER BY (data_length + index_length) DESC; Replace \u0026quot;your_database_name_here\u0026quot; with the name of the database you want to inspect. This query provides insights into the size of tables within a single database, facilitating targeted optimization efforts.\nOptimized Pagination Using Variables #SET @row_number = 0; SELECT * FROM ( SELECT *, (@row_number:=@row_number + 1) AS num FROM table_name ORDER BY column1 ) AS ranked WHERE num BETWEEN 51 AND 100; This query demonstrates an optimized way to handle pagination in MySQL, particularly useful for very large datasets. By assigning row numbers to each row and filtering based on these numbers, you can efficiently retrieve a specific page of results without the performance hit of using LIMIT with a high offset.\nRecursive CTE for Hierarchical Data #WITH RECURSIVE CategoryPath AS ( SELECT categoryId, name, 1 AS depth FROM categories WHERE parentCategoryId IS NULL UNION ALL SELECT c.categoryId, CONCAT(cp.name, \u0026#39; \u0026gt; \u0026#39;, c.name), depth+1 FROM CategoryPath AS cp JOIN categories AS c ON cp.categoryId = c.parentCategoryId ) SELECT * FROM CategoryPath; This query uses a recursive Common Table Expression (CTE) to handle hierarchical data, which can be useful for categories. It starts with the root categories (where parentCategoryId IS NULL) and recursively joins to child categories, building a path and computing the depth of each category in the hierarchy.\nThese MySQL tips and tricks offer practical solutions for monitoring database size and optimizing performance. Incorporate these queries into your workflow to streamline database management and ensure optimal resource utilization. Happy querying!\n","date":"1 March 2024","permalink":"https://techwolf12.nl/blog/mysql-tips-and-tricks/","section":"Blog posts","summary":"Welcome to this practical guide filled with MySQL tips and tricks! Whether you\u0026rsquo;re a seasoned database administrator or just getting started with MySQL, these handy queries and techniques will help you manage your databases more efficiently.","title":"MySQL Tips and Tricks"},{"content":"We covered SSH Authentication and Linux user management using OpenLDAP in an article, now wouldn\u0026rsquo;t it be great if you could also manage sudo rules from a central place like an LDAP server? Luckily, sudo has support for this! This article will show you how to set this up within OpenLDAP and the sudo configuration on your Linux machine.\nThere is an assumption the LDAP client is already setup on this machine, if you haven\u0026rsquo;t done that yet, see the previous blog post linked above!\nLDAP changes #Import the sudo schema into your OpenLDAP configuration on the cn=schema,cn=config\ndn: cn=sudo,cn=schema,cn=config\robjectClass: olcConfig\robjectClass: olcSchemaConfig\robjectClass: top\rcn: sudo\rolcAttributeTypes: {0}( 1.3.6.1.4.1.15953.9.1.1 NAME \u0026#39;sudoUser\u0026#39; DESC \u0026#39;User(s\r) who may run sudo\u0026#39; EQUALITY caseExactIA5Match SUBSTR caseExactIA5Substring\rsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )\rolcAttributeTypes: {1}( 1.3.6.1.4.1.15953.9.1.2 NAME \u0026#39;sudoHost\u0026#39; DESC \u0026#39;Host(s\r) who may run sudo\u0026#39; EQUALITY caseExactIA5Match SUBSTR caseExactIA5Substring\rsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )\rolcAttributeTypes: {2}( 1.3.6.1.4.1.15953.9.1.3 NAME \u0026#39;sudoCommand\u0026#39; DESC \u0026#39;Com\rmand(s) to be executed by sudo\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4\r.1.1466.115.121.1.26 )\rolcAttributeTypes: {3}( 1.3.6.1.4.1.15953.9.1.4 NAME \u0026#39;sudoRunAs\u0026#39; DESC \u0026#39;User(\rs) impersonated by sudo\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466\r.115.121.1.26 )\rolcAttributeTypes: {4}( 1.3.6.1.4.1.15953.9.1.5 NAME \u0026#39;sudoOption\u0026#39; DESC \u0026#39;Opti\rons(s) followed by sudo\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466\r.115.121.1.26 )\rolcAttributeTypes: {5}( 1.3.6.1.4.1.15953.9.1.6 NAME \u0026#39;sudoRunAsUser\u0026#39; DESC \u0026#39;U\rser(s) impersonated by sudo\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.\r1466.115.121.1.26 )\rolcAttributeTypes: {6}( 1.3.6.1.4.1.15953.9.1.7 NAME \u0026#39;sudoRunAsGroup\u0026#39; DESC \u0026#39;\rGroup(s) impersonated by sudo\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.\r1.1466.115.121.1.26 )\rolcAttributeTypes: {7}( 1.3.6.1.4.1.15953.9.1.8 NAME \u0026#39;sudoNotBefore\u0026#39; DESC \u0026#39;S\rtart of time interval for which the entry is valid\u0026#39; EQUALITY generalizedTim\reMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.12\r1.1.24 )\rolcAttributeTypes: {8} ( 1.3.6.1.4.1.15953.9.1.9 NAME \u0026#39;sudoNotAfter\u0026#39; DESC \u0026#39;E\rnd of time interval for which the entry is valid\u0026#39; EQUALITY generalizedTimeM\ratch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.\r1.24 )\rolcAttributeTypes: {9} ( 1.3.6.1.4.1.15953.9.1.10 NAME \u0026#39;sudoOrder\u0026#39; DESC \u0026#39;an integer to order the sudoRole entries\u0026#39; EQUALITY integerMatch ORDERING integ\rerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )\rolcObjectClasses: {0}( 1.3.6.1.4.1.15953.9.2.1 NAME \u0026#39;sudoRole\u0026#39; SUP top STRUC\rTURAL DESC \u0026#39;Sudoer Entries\u0026#39; MUST ( cn ) MAY ( sudoUser $ sudoHost $ sudoCom\rmand $ sudoRunAs $ sudoRunAsUser $ sudoRunAsGroup $ sudoOption $ sudoNotBef\rore $ sudoNotAfter $ sudoOrder $ description ) ) Now create a new Organizational unit (OU), for example:\ndn: ou=sudoers,dc=example,dc=com\rou: sudoers\robjectClass: top\robjectClass: organizationalUnit Then in your /etc/ldap/ldap.conf \u0026amp; /etc/libnss-ldap.conf files, add the following line:\nsudoers_base ou=sudoers,dc=example,dc=com Finally in your /etc/nsswitch.conf edit sudoers line to the following:\nsudoers : files ldap Creating defaults #You can setup defaults for the sudoers file like this:\ndn: cn=defaults,ou=sudoers,dc=example,dc=com\robjectClass: top\robjectClass: sudoRole\rcn: defaults\rdescription: Default sudoOption\u0026#39;s for all servers\rsudoOption: env_keep+=SSH_AUTH_SOCK sudoOption: insults Creating rights #For user rights, you can now create the following object:\ndn: cn=techwolf12,ou=sudoers,dc=example,dc=com\robjectClass: top\robjectClass: sudoRole\rcn: techwolf12\rsudoUser: techwolf12\rsudoHost: ALL\rsudoRunAs: ALL\rsudoCommand: ALL For group rights, you can do the same. Make sure the group exists in LDAP or locally, and be sure to prefix it with a %:\ndn: cn=%admin,ou=sudoers,dc=example,dc=com\robjectClass: top\robjectClass: sudoRole\rcn: %admin\rsudoUser: %admin\rsudoHost: ALL\rsudoRunAs: ALL\rsudoCommand: ALL You can also specify hostnames, specific commands or users this user/group is allowed to impersonate or negate the ALL by using an exclamation mark before the command. Another important thing to note, since LDAP objects have no order, if you want specific rules to take precedence you have to use the sudoOrder attribute, a higher number is more important:\ndn: cn=byte,ou=sudoers,dc=example,dc=com\robjectClass: top\robjectClass: sudoRole\rcn: byte\rsudoUser: byte\rsudoHost: lycan\rsudoRunAs: techwolf12\rsudoCommand: ALL\rsudoCommand: !/bin/bash\rsudoOrder: 1 ","date":"28 May 2023","permalink":"https://techwolf12.nl/blog/openldap-sudo-configuration-and-rights/","section":"Blog posts","summary":"We covered SSH Authentication and Linux user management using OpenLDAP in an article, now wouldn\u0026rsquo;t it be great if you could also manage sudo rules from a central place like an LDAP server? Luckily, sudo has support for this! This article will show you how to set this up within OpenLDAP and the sudo configuration on your Linux machine.","title":"OpenLDAP for sudo configuration and rights"},{"content":"Reverse SSH tunneling is a powerful tool that can be used to securely forward ports to or from remote servers to your local machine. It is especially useful when you don’t have direct access to a remote server, such as in a cloud environment. By creating a secure tunnel between the remote server and your local machine, you can access services on the remote server as if they were running on your own machine.\nThere are two ways you can use SSH tunneling, in this post I will describe both and how they can be useful. For this post, I assume you have access to a remote host running an SSH server with no firewall, as well as an SSH client on your local machine. I also assume there are no conflicts with ports already in use on the system, and you have the proper privileges to open ports on the system.\nLocal port forwarding #Personally, I use local port forwarding the most to access ports on a headless server, for example to access a database on my local machine.\nssh -L 3306:localhost:3306 user@server After this command, you can now access port 3306 as if MySQL was running on your local machine and use any tools like you normally would!\nRemote port forwarding #Remote port forwarding is useful for bypassing firewalls or forwarding traffic from a remote server to your local machine to for example show a work in progress web project.\nssh -R 0.0.0.0:8080:localhost:80 user@server This command will forward traffic from any IP (listening address 0.0.0.0) at port 8080 to your local machine on port 80. So someone could access server:8080 and see what is on your laptop on port 80.\nUsing this in a systemd service #You can use a systemd service to auto start a remote port forwarding session, this can be useful to forward port 22 of your laptop to a server you own so you always have ssh access remotely.\n[Unit]\rDescription=Port forwarding to server\rAfter=network.target\r[Service]\rExecStart=/usr/bin/ssh -NT -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -R 2222:localhost:22 user@server\rRestartSec=5\rRestart=always\r[Install]\rWantedBy=multi-user.target Because we didn\u0026rsquo;t specify a listening address, it defaults to localhost access only. If you now login to the server over ssh and then ssh to localhost:2222 you should be able to access your laptop, neat!\nBonus: Using SSH as SOCKS proxy #You can also use SSH as a proxy. This can be useful to access multiple control panel websites hosted on the server that normally are only available on localhost.\nFor this, execute the following command:\nssh -C -D 1080 user@server Now you can setup a SOCKS proxy in your browser to localhost on port 1080 to forward traffic. Now you should be able to access the different sites hosted locally or bypass IP restrictions.\n","date":"27 May 2023","permalink":"https://techwolf12.nl/blog/reverse-ssh-tunnel-port-forwarding/","section":"Blog posts","summary":"Reverse SSH tunneling is a powerful tool that can be used to securely forward ports to or from remote servers to your local machine. It is especially useful when you don’t have direct access to a remote server, such as in a cloud environment. By creating a secure tunnel between the remote server and your local machine, you can access services on the remote server as if they were running on your own machine.","title":"Reverse SSH tunnel for port forwarding"},{"content":"A GPG Keysigning party is an event where people verify each other\u0026rsquo;s identity and sign their GPG keys. Doing so increases the effectiveness of the \u0026ldquo;Web of trust\u0026rdquo; and the total trust each key has.\nThis is a revisited blog post from my old website, I saw a lot of 404\u0026rsquo;s leading there still.\nPlease note, all fingerprints/emails in this post should be replaced by your own!\nIn preparation for the event #You should list your public key and print this out multiple times on a piece of paper, you will distribute this to the other attendees to verify your fingerprint.\nYou can do this with\ngpg --list-key contact@techwolf12.nl This will give an output similar to:\npub rsa4096 2015-01-29 [C] [expires: 2024-11-21] 34B35DD172E366BF6867AB069FB800372F2546D8 uid [ultimate] Christiaan de Die le Clercq \u0026lt;contact@techwolf12.nl\u0026gt; sub rsa2048 2015-01-29 [S] [expires: 2024-11-21] sub rsa2048 2015-01-29 [E] [expires: 2024-11-21] sub rsa2048 2015-01-29 [A] [expires: 2024-11-21] Be sure to pack your ID and a pen as well! People need to validate this is indeed you! You can also refer people to a key signing policy like this one: https://techwolf12.nl/gpg/policy\nThe goal of printing this is to avoid someone editing the content and generating a fake key under your name.\nYou are now ready to go!\nDuring the event #You will collect the paper key of people, and make sure that this person is the person\u0026rsquo;s name on the key, for example by validating their ID. Don\u0026rsquo;t worry about the email validation, for now, that is for later.\nIt is recommended to mark the validated pieces of paper with a pen and write something like \u0026ldquo;valid\u0026rdquo; on it for later.\nAfter this, bring all the pieces of paper home with you.\nSigning keys after the event #You now have the printed public key information from the other participants.\nNow you have to find the key fingerprint on each print and get the public keys from the keyservers like so:\ngpg --recv-keys 34B35DD172E366BF6867AB069FB800372F2546D8 You can now sign the key with\ngpg --sign-key 34B35DD172E366BF6867AB069FB800372F2546D8 A few things to keep in mind:\nIf a key has multiple user IDs, GPG will ask if you want to sign all of them. Unless they seem suspicious to you, It is usually alright to sign all of the user IDs. Compare all of the information displayed by GPG with the information on the paper, only sign the key if it matches exactly. GPG will ask for the passphrase for your secret key, enter it and GPG will sign the other person’s key with yours. Repeat this process with the other keys you have collected. Once this is done, you can send these signatures by email.\nInstead of sending the signed keys back to the keyserver, send each key to its owner via email. This will ensure that the owner of the key also is in control of the email address listed in the key\nFor every key, you can now export the public key like so:\ngpg --armor --output 01234567.signed-by.34B35DD172E366BF6867AB069FB800372F2546D8.asc --export 01234567 You can use your preferred email program to compose messages to the email address from each key’s user ID and attach the corresponding signature file.\nIf possible, have your email program encrypt these messages with the corresponding keys.\nImporting your signed key #Of course, part of the fun is receiving your signed key back! To import this, use the following command:\ngpg --import 34B35DD172E366BF6867AB069FB800372F2546D8.signed-by.01234567.asc You can list the signatures with:\ngpg --list-sigs 34B35DD172E366BF6867AB069FB800372F2546D8 Finally, you should upload your key to a keyserver so other people can find it. You can do that with:\ngpg --send-keys 34B35DD172E366BF6867AB069FB800372F2546D8 That’s it, your key is now signed and you have enlarged your web of trust.\n","date":"22 November 2022","permalink":"https://techwolf12.nl/blog/hosting-successful-gpg-keysigning-party/","section":"Blog posts","summary":"A GPG Keysigning party is an event where people verify each other\u0026rsquo;s identity and sign their GPG keys. Doing so increases the effectiveness of the Web of trust and the total trust each key has.","title":"Hosting a successful GPG Keysigning Party"},{"content":"I was thinking about migrating some cloud services into Terraform, but we seemed to have too many domains. So I wrote a small python script that takes a domain, then using doctl (the DigitalOcean command line) it extracts all current records. After that it outputs the file into tf and also gives a import command to import the current state in Terraform state. This assumes you already installed and configured both doctl and Terraform with the Digitalocean cloud provider.\nThe usage is simple:\n$ python3 doctl-domain-to-terraform.py techwolf12.nl This would then give an output similar to:\ntf file: resource \u0026#34;digitalocean_domain\u0026#34; \u0026#34;techwolf12_nl\u0026#34; { name = \u0026#34;techwolf12.nl\u0026#34; } resource \u0026#34;digitalocean_record\u0026#34; \u0026#34;techwolf12_nl_NS_\u0026#34; { domain = digitalocean_domain.techwolf12_nl.id type = \u0026#34;NS\u0026#34; name = \u0026#34;@\u0026#34; value = \u0026#34;ns1.digitalocean.com.\u0026#34; } resource \u0026#34;digitalocean_record\u0026#34; \u0026#34;techwolf12_nl_NS__\u0026#34; { domain = digitalocean_domain.techwolf12_nl.id type = \u0026#34;NS\u0026#34; name = \u0026#34;@\u0026#34; value = \u0026#34;ns2.digitalocean.com.\u0026#34; } Import commands: terraform import digitalocean_domain.techwolf12_nl techwolf12.nl terraform import digitalocean_record.techwolf12_nl_NS_ techwolf12.nl,1234567 terraform import digitalocean_record.techwolf12_nl_NS__ techwolf12.nl,7654321 You can import this as TF file and then use the import commands to import the current state.\nThe code is available here, save it as a file and run it as instructed above:\n\u0026#34;\u0026#34;\u0026#34;Doctl Domain to Terraform Made by Techwolf12, more info: https://techwolf12.nl/blog/using-python-migrate-digitalocean-domains-terraform-managed \u0026#34;\u0026#34;\u0026#34; import subprocess import sys import json def get_unique_resource(resource_string): if resource_string in list_of_record_resource_strings: return get_unique_resource(f\u0026#39;{resource_string}_\u0026#39;) else: list_of_record_resource_strings.append(resource_string) return resource_string if len(sys.argv) \u0026lt; 2: print(\u0026#34;Need domain as argument\u0026#34;) quit(1) domain = sys.argv[1] output = subprocess.run([\u0026#39;doctl\u0026#39;, \u0026#39;compute\u0026#39;, \u0026#39;domain\u0026#39;, \u0026#39;records\u0026#39;, \u0026#39;list\u0026#39;, \u0026#39;-o\u0026#39;, \u0026#39;json\u0026#39;, domain.encode(\u0026#39;utf-8\u0026#39;)], stdout=subprocess.PIPE).stdout.decode(\u0026#39;utf-8\u0026#39;) records = json.loads(output) domain_resource = str(domain).replace(\u0026#39;.\u0026#39;, \u0026#39;_\u0026#39;) import_commands = \u0026#34;\u0026#34; list_of_record_resource_strings = list() print(\u0026#34;tf file:\\n\\n\u0026#34;) print(f\u0026#39;\u0026#39;\u0026#39; resource \u0026#34;digitalocean_domain\u0026#34; \u0026#34;{domain_resource:s}\u0026#34; {{ name = \u0026#34;{domain:s}\u0026#34; }} \u0026#39;\u0026#39;\u0026#39;) import_commands += f\u0026#39;terraform import digitalocean_domain.{domain_resource:s} {domain:s}\\n\u0026#39; supported_records = [\u0026#39;A\u0026#39;, \u0026#39;AAAA\u0026#39;, \u0026#39;CAA\u0026#39;, \u0026#39;CNAME\u0026#39;, \u0026#39;MX\u0026#39;, \u0026#39;NS\u0026#39;, \u0026#39;TXT\u0026#39;, \u0026#39;SRV\u0026#39;] for record in records: if record.get(\u0026#39;type\u0026#39;) in supported_records: record_name = record.get(\u0026#34;name\u0026#34;).replace(\u0026#34;.\u0026#34;, \u0026#34;_\u0026#34;) if record.get(\u0026#34;name\u0026#34;) != \u0026#34;@\u0026#34; else \u0026#34;\u0026#34; resource = get_unique_resource(f\u0026#39;{domain_resource:s}_{record.get(\u0026#34;type\u0026#34;)}_{record_name}\u0026#39;) record_value = record.get(\u0026#34;data\u0026#34;).replace(\u0026#34;\\\u0026#34;\u0026#34;,\u0026#34;\\\\\\\u0026#34;\u0026#34;) if record.get(\u0026#39;type\u0026#39;) in [\u0026#39;CNAME\u0026#39;, \u0026#39;MX\u0026#39;, \u0026#39;NS\u0026#39;]: record_value += \u0026#34;.\u0026#34; print(f\u0026#39;resource \u0026#34;digitalocean_record\u0026#34; \u0026#34;{resource}\u0026#34; {{\u0026#39;) print(f\u0026#39; domain = digitalocean_domain.{domain_resource:s}.id\u0026#39;) print(f\u0026#39; type = \u0026#34;{record.get(\u0026#34;type\u0026#34;)}\u0026#34;\u0026#39;) if record.get(\u0026#39;type\u0026#39;) == \u0026#39;MX\u0026#39;: print(f\u0026#39; priority = \u0026#34;{record.get(\u0026#34;priority\u0026#34;)}\u0026#34;\u0026#39;) print(f\u0026#39; name = \u0026#34;{record.get(\u0026#34;name\u0026#34;)}\u0026#34;\u0026#39;) print(f\u0026#39; value = \u0026#34;{record_value}\u0026#34;\u0026#39;) print(\u0026#34;}\\n\u0026#34;) import_commands += f\u0026#39;terraform import digitalocean_record.{resource} {domain},{record.get(\u0026#34;id\u0026#34;)}\\n\u0026#39; print(\u0026#34;\\n\\nImport commands:\\n\\n\u0026#34;) print(import_commands) Onto next time! :)\n","date":"15 March 2022","permalink":"https://techwolf12.nl/blog/using-python-migrate-digitalocean-domains-terraform-managed/","section":"Blog posts","summary":"I was thinking about migrating some cloud services into Terraform, but we seemed to have too many domains. So I wrote a small python script that takes a domain, then using doctl (the DigitalOcean command line) it extracts all current records. After that it outputs the file into tf and also gives a import command to import the current state in Terraform state.","title":"Using Python to migrate DigitalOcean domains to Terraform managed"},{"content":"Watermeterkit is an awesome idea to count water usage on most Dutch watermeters (possibly in other countries too). With these steps, it would be easy to add into Home Assistant to show usage in litres or m³.\nSteps to install # Make sure the Watermeterkit is installed and connected to your Home Assistant install. Create a counter in the helpers screen, where the initial value is the current value of your meter. This will be in litres, so fill in the red numbers too. Add the blueprint (No need to copy this, you can press the add button instead) and make an automation from it. Setting the total consumption counter from the Watermeterkit entities as well as the helper counter you just created. blueprint: name: Watermeterkit Meter Value description: Update a counter when the watermeterkit.nl sensor gets updated domain: automation input: watermeterkit: name: Watermeterkit Total Consumption counter description: Watermeterkit Total cosumption counter (provided by ESPHome) selector: entity: {} counter: name: Helper Counter description: The helper counter you made default: [] selector: entity: source_url: https://gist.github.com/Techwolf12/e7f3bf88d33c803b7b024e7e229a1c81 mode: single trigger: platform: state entity_id: !input \u0026#39;watermeterkit\u0026#39; condition: - condition: not conditions: - condition: or conditions: - condition: state entity_id: !input \u0026#39;watermeterkit\u0026#39; state: \u0026#39;0.000\u0026#39; - condition: state entity_id: !input \u0026#39;watermeterkit\u0026#39; state: unavailable action: - service: counter.increment target: entity_id: !input \u0026#39;counter\u0026#39; Congrats! The counter should now show the total usage in litres!\nExtra options # Optionally, Create a sensor to show the meter value in m³ (Update the sensor ID and counter ID where needed): sensor: - platform: template sensors: water_meter_total_m3: friendly_name: \u0026#34;Water meter consumption\u0026#34; unique_id: \u0026#34;sensor.water_meter_total_m3\u0026#34; value_template: \u0026gt; {{ states(\u0026#39;counter.watermeter_consumption_counter\u0026#39;) | float * 0.001 }} unit_of_measurement: m³ Optionally, add a utility_meter on this sensor to show usage over different periods (Update the sensor ID where needed): utility_meter: water_daily: source: sensor.water_meter_total_m3 cycle: daily water_monthly: source: sensor.water_meter_total_m3 cycle: monthly water_yearly: source: sensor.water_meter_total_m3 cycle: yearly ","date":"12 October 2021","permalink":"https://techwolf12.nl/blog/counting-water-usage-watermeterkit/","section":"Blog posts","summary":"Watermeterkit is an awesome idea to count water usage on most Dutch watermeters. With these steps, it would be easy to add into Home Assistant to show usage in liters or m³.","title":"Counting water usage with the Watermeterkit"},{"content":"Also available as RSS Feed.\n","date":null,"permalink":"https://techwolf12.nl/tag/domotica/","section":"Tags","summary":"","title":"Domotica"},{"content":"Also available as RSS Feed.\n","date":null,"permalink":"https://techwolf12.nl/tag/live-streaming/","section":"Tags","summary":"","title":"Live Streaming"},{"content":"Using a GoPro as a stream source over Wi-Fi? What about streaming from SLOBS to two Twitch channels at once? Having a dedicated recording machine? Streaming to multiple platforms? Maybe even host a LAN party with a commentary stream? You can use RTMP and a custom ingest for streaming. This way you are able to accomplish what you want. In this article, I will guide the setup and show examples for the different use cases. It can be a bit technical for most people, but I will be happy to help if you have questions.\nSetting up an NGINX RTMP server #Let\u0026rsquo;s dive in on the technical part first, you might not have this if you got a device/image from me, if so you can skip this chapter. You can use a Raspberry Pi as a test or other generic GNU/Linux server. RTMP should run fine on a Raspberry Pi since it is not too heavy on system resources. But if multi-streaming I can highly recommend a dedicated machine with a bit more powerful CPU.\nInstallation (Debian based systems) #You can install NGINX with RTMP support as root on Debian. This is tested on Debian 10. Run the following to install dependencies:\napt install -y build-essential libpcre3 libpcre3-dev libssl-dev unzip zlib1g-dev You now need to download NGINX, and the NGINX RTMP module:\ncd /usr/local/src\rwget https://nginx.org/download/nginx-1.18.0.tar.gz\rtar -xvzf nginx-1.18.0.tar.gz\rrm -f nginx-1.18.0.tar.gz\rmv nginx-1.18.0 nginx\rcd nginx\rwget https://github.com/sergey-dryabzhinsky/nginx-rtmp-module/archive/dev.zip\runzip dev.zip Great, everything should be downloaded and in place. Now we can build \u0026amp; compile the project as such by executing these commands:\n./configure --with-http_ssl_module --add-module=./nginx-rtmp-module-dev\rmake\rmake install Congrats! It should have installed to /usr/local/nginx, you can now try to run /usr/local/nginx/sbin/nginx and see if it works.\nNow create a Systemd service file in /lib/systemd/system/nginx.service with the following contents:\n[Unit]\rDescription=The NGINX HTTP and reverse proxy server with RTMP\rAfter=syslog.target network-online.target remote-fs.target nss-lookup.target\rWants=network-online.target\r[Service]\rType=forking\rPIDFile=/usr/local/nginx/logs/nginx.pid\rExecStartPre=/usr/local/nginx/sbin/nginx -t\rExecStart=/usr/local/nginx/sbin/nginx\rExecReload=/usr/local/nginx/sbin/nginx -s reload\rExecStop=/bin/kill -s QUIT $MAINPID\rPrivateTmp=true\r[Install]\rWantedBy=multi-user.target Example configuration #This configuration contains examples for the /usr/local/nginx/conf/nginx.conf configuration file. In order to stream from another place than your local network, you need to forward port 1935 and set up security settings (like in the allow publish part of the multichannelstream application) in order to prevent unauthorized people from streaming.\nworker_processes 1;\rerror_log logs/rtmp_error.log debug;\rpid logs/nginx.pid;\rrtmp {\rserver {\rlisten 1935;\rchunk_size 4096;\r# Use a GoPro livestream in other applications\rapplication gopro {\rlive on;\rrecord off;\r}\r# Use a single OBS output for multiple streams\rapplication multichannelstream {\rlive on;\rrecord off;\rmeta copy;\rpush rtmp://live-ams.twitch.tv/app/\u0026lt;YOUR TWITCH STREAM KEY\u0026gt;;\rpush rtmp://live-ams.twitch.tv/app/\u0026lt;YOUR 2nd TWITCH CHANNEL STREAM KEY\u0026gt;;\r# Use this to limit who is allowed to stream to this application, this is important to prevent others from being able to stream to your channels. This has to be an IP or range you plan to stream from.\rallow publish 10.10.0.0/24;\rdeny publish all;\rallow play all;\r}\r# Use a single OBS output for multiple platforms (Facebook, Twitch, Youtube)\rapplication multiplatformstream {\rlive on;\rrecord off;\rmeta copy;\rpush rtmp://live-ams.twitch.tv/app/\u0026lt;YOUR TWITCH STREAM KEY\u0026gt;;\rpush rtmps://live-api-s.facebook.com:443/rtmp/\u0026lt;YOUR FACEBOOK STREAM KEY\u0026gt;;\r# Youtube allows a backup server in case their primary ingress server fails. This is why there are two listed here.\rpush rtmp://a.rtmp.youtube.com/live2/\u0026lt;YOUR YOUTUBE STREAM KEY\u0026gt;;\rpush rtmp://b.rtmp.youtube.com/live2/\u0026lt;YOUR YOUTUBE STREAM KEY\u0026gt;?backup=1;\r}\r# Twitch stream catcher, for commentary streams or recording. You will need multiple of these in order to capture multiple broadcasts with a unique name for each\rapplication \u0026lt;TWITCH CHANNEL\u0026gt; {\rlive on;\rrecord off;\rmeta copy;\rpush rtmp://live-ams.twitch.tv/app/\u0026lt;CHANNEL TWITCH STREAM KEY\u0026gt;;\r}\r}\r}\revents {}\rhttp {\rserver {\rlisten 8080;\rlocation /stat {\rrtmp_stat all;\rrtmp_stat_stylesheet stat.xsl;\r}\rlocation /stat.xsl {\rroot /usr/local/src/nginx/nginx-rtmp-module-dev/stat.xsl;\r}\r}\r} This configuration has examples for the multiple use cases listed below. Please check and alter as needed.\nGoPro setup #Using a GoPro camera in OBS? You can with the magic of RTMP! Playing around with this was how I initially got into RTMP streams and figuring out the other things you can do with it. GoPro\u0026rsquo;s have the option to directly stream to some platforms, but you wouldn\u0026rsquo;t have OBS/Overlays then, to fix this you can stream to a custom RTMP server and catch that stream in OBS. The only downside to this is that it\u0026rsquo;s not as real time as a webcam might be since it is over the network, but not everything requires this. Especially if you use the audio from the RTMP stream!\nStreamlabs OBS setup #First, you got to add a new media source to import your newly generated RTMP feed, set the buffering as low as possible, as well as the reconnect delay. Your source should look something like this:\nThis should now activate automatically when you start streaming to your RTMP server from your Gopro\nUsing RTMP to stream on multiple Twitch channels #You could be playing a game together, on a single console for example but still want to stream to each of your channels. You can configure RTMP to re-stream to two outputs at the same time. This way you can stream from one Streamlabs OBS to two separate Twitch channels for example.\nUsing the example configuration provided above, we need the multichannelstream application example.\nIn order to do\nthis safely, the second streamer needs to share a stream key with you. It isn\u0026rsquo;t recommended to share a primary stream key, the better way would be for the streamer to add you to their \u0026ldquo;People who can stream on your channel\u0026rdquo; on your channel settings page: https://www.twitch.tv/settings/channel. Twitch will then add a secondary streaming key to your account and email this to the person you added. If they remove the person from the list only that stream key will be invalidated.\nSet your OBS to stream to a custom server, and input the following URL, fill in anything you want for stream key since the authentication is based on IP. Once you start streaming it should automatically forward to multiple channels at once!\nrtmp://\u0026lt;your ip\u0026gt;/multichannelstream In case you want notifications from the other streamer(s) make sure to add the alert box URL (for example if you use Streamlabs OBS) from the other streamer(s) as well. There currently isn\u0026rsquo;t a method to filter between the different streamers if you use multi streamer like this. So both channels will show the notifications, maybe something like different parts of the screen for the different streamers work for this?\nUsing RTMP to stream on multiple platforms #Another way to use this, if you want to stream to multiple platforms at the same time, like for example Youtube, Twitch \u0026amp; Facebook Live. For this, we use the multiplatformstream application from the example configuration above.\nIt seems that on Facebook, you need to manually press the \u0026ldquo;Go Live\u0026rdquo; button for the stream to start, even after supplying a RTMP stream.\nAnother thing that has to be noted, if you are a Twitch affiliate or partner you are not allowed to stream to other platforms at the same time since you give Twitch a 24 hour exclusivity on the content you create (Twitch Affiliate Agreement 2.2. Live Content Exclusivity).\nUsing RTMP to embed streams (for example, to give commentary as a gamemaster) #Let say you are the game master for a multiplayer game at a LAN party, everyone is streaming to their own Twitch channels and you want to embed the other streamers while giving commentary on it. Using multiple of the Twitch streamer application, you can embed the streams of other players in your own stream easily. You could already do this using a Browser source but this would be open to more issues like the network connectivity.\nAll streamers need to stream to their application on the RTMP server and provide a stream key. The server acts as a proxy for the stream which others can capture from. You can then use the OBS configuration from the GoPro setup to embed the streams in OBS. Keep in mind your hardware \u0026amp; network needs to be able to handle it.\n","date":"11 March 2021","permalink":"https://techwolf12.nl/blog/using-rtmp-streamlabs-obs-gopro-co-streaming-multiple-channels/","section":"Blog posts","summary":"Using a GoPro as a stream source over Wi-Fi? What about streaming from SLOBS to two Twitch channels at once? Having a dedicated recording machine? Streaming to multiple platforms? Maybe even host a LAN party with a commentary stream? You can use RTMP and a custom ingest for streaming. This way you are able to accomplish what you want. In this article, I will guide the setup and show examples for the different use cases.","title":"Using RTMP for Streamlabs OBS (GoPro, Co-Streaming to multiple Channels)"},{"content":"The Anytone D878UV is quite a nice DMR (Digital Mobile Radio) capable radio packed with features like APRS (Reporting only), Bluetooth and GPS. It\u0026rsquo;s no surprise that this is my favourite DMR radio and I can recommend it if you are getting started with DMR for the first time. This post will help you get DMR up and running on your Anytone D878UV.\nDMR Basic setup #The first step you need to do is to program your DMR ID into your codeplug. Are you new to DMR and don\u0026rsquo;t have an ID yet? You can request one here: https://register.ham-digital.org/\nAfter you got your DMR ID you can use the programming software to set up DMR for your radio.\nSetup your DMR ID #In the sidebar, go to the \u0026ldquo;Digital -\u0026gt; Radio ID List\u0026rdquo; and add your DMR id with your callsign as a name in the 1 slot. Then go to \u0026ldquo;Digital -\u0026gt; Talk Alias\u0026rdquo; and set the \u0026ldquo;Send talk alias\u0026rdquo; option to On. This will transmit your callsign as metadata in the DMR call.\nFrom what I have been told it is still good practice to ID like you would on a analog frequency. For radios without Alias/Contact support, an other ham using your radio or outdated user lists. Keep this in mind :)\nImporting the DMR user list #Download the latest user dump from https://ham-digital.org/status/users.csv and open the programming software. By pressing the menu \u0026ldquo;Tool -\u0026gt; Import\u0026rdquo; you can select this file as the \u0026ldquo;Digital Contact List\u0026rdquo;. This will show the call/country on the display whenever a DMR user is received.\nTalk group setup #DMR works with \u0026ldquo;talk groups\u0026rdquo;, an id number that represents a room of people or the DMR id of a person to make a private call directly to another DMR user. This is the contact list you can call people from the radio. You also get the option to provide a manual id for a call, but this is easier to use.\nChannel setup #One more thing! You need to setup the actual radio channels now. For this, you need to know DMR repeaters or own a hotspot. I\u0026rsquo;ll use a hotspot channel as an example, setting up a hotspot (often MMDVM hotspots, or an Openspot) is not included in this article.\nBe sure to set the radio ID to your callsign, as configured on the ID page, the talk slot, color code and frequency. The contact is the default contact if you press the PTT button, you can change that on the radio by going to the talk group list, selecting a talk group and pressing \u0026ldquo;Select contact\u0026rdquo;. You can also go to the talk group list and press the PTT button on a selected item to only transmit to that ID for one shot.\nYou can call 9990 (Private call! Not group call, a common mistake) to test if DMR is setup correctly. It should transmit a recording of what you said back to you.\nCongratulations, it should now work! Now for the advanced items.\nAPRS (Analog Package Reporting System) Setup #Most HAM\u0026rsquo;s already know APRS, you can also use DMR for APRS reporting. For this, you need to setup the APRS part in your codeplug to include digital APRS, for digital APRS you need to know your DMR master node ID and GPS transmit location. Since I am from the Netherlands this is 204999, the first three digits of the master server followed by 999, but consult the Brandmeister wiki to be sure and replace the value you have in the APRS TG.\nAfter this, APRS should work in the menu of your radio.\nDMR SMS #DMR allows SMS to be send, we can use the Brandmeister network for this as well!\nFor this to work correctly, you need to make sure your Anytone radio uses M-SMS, the Motorola standard. After this, you need to make sure that you have the correct settings on the Brandmeister Network. On the Selfcare page, make sure your brand is set to \u0026ldquo;Chinese radio\u0026rdquo; and edit your APRS call as you like with a SSID.\nThen you need to setup SMS standard for the Anytone. If you don\u0026rsquo;t SMS might not be received or garbled data. For this you have to send three separate text messages to DMR ID 262995 containing: CONFIG ON CONFIG SHORT CONFIG DELAY 10\nAfter this, you can send CONFIG to get the active configuration as SMS back to your radio.\nSome users have reported that having \u0026ldquo;Digi APRS RX\u0026rdquo; on in the channel configuration can give issues with receiving SMS on the Anytone, if you want to use SMS leave this off.\nDMR Information service #Some DMR SMS numbers are handy to know. For example, DMR ID 262993 which is an information service. You can send the following commands:\nCommand Does what? help Sends the available commands. echo Sends a response back to test SMS functionality wx Weather information at Repeater location. wx help Weather command help. wx , Weather information in city. wx gps Weather information from your last transmitted GPS location. metar Airport style METAR information by ICAO code. gps help GPS command help. gps Shows the GPS position from the radio, also shows distance and direction to repeater. gps set Set GPS position as \u0026ldquo;home\u0026rdquo;. gps home Shows distance and direction in relation to the stored \u0026ldquo;home\u0026rdquo; position. gps Shows distance and direction in relation to DMR user by callsign. DAPNET/POCSAG Pager #You can also send a message to DAPNET using this, a HAM Radio system for pagers. To do this you can send a message to DMR ID 262994 containing (no quotes): \u0026ldquo;[CALLSIGN] [Your message]\u0026rdquo;.\nDMR SMS Queueing #There is a way to store SMS messages for if you aren\u0026rsquo;t on the radio at the moment and send by callsign instead of DMR ID. Something I can recommend is\nto enable direct message receiving, if you are online you will still get the SMS directly after your radio sends a APRS packet. Do this by sending a message to DMR ID 262995 containing (no quotes): \u0026ldquo;DIRECT ON\u0026rdquo;\nYou now have the following commands:\nCommand Does what? DIRECT ON Enables direct sending of SMS to your radio when reporting in with APRS. [CALLSIGN] [Message] Sends a message to a callsign. INBOX Sends a response back with messages in queue. GET 1 Sends the first message back from the queue. GET Sends the all messages from the queue. DELETE 1 Deletes the first message back from the queue. DELETE Deletes the all messages from the queue. DMR SMS to Mobile phone SMS gateway #Finally, wouldn\u0026rsquo;t it be cool if you could send an actual text message to a mobile phone number from your HAM radio? That\u0026rsquo;s certainly what the folks behind SMSGTE must have thought. This system works with APRS messages and acts as a gateway between a mobile phone and ham radio messaging, luckily there is a way to send APRS messages via DMR as well. To start, you add your own mobile number (this will be a public message! Please keep that in mind). You need to know your APRS DMR gateway ID, this will be the same ID from the APRS part of this article, so in my case 204999.\nCommand Does what? SMSGTE #mynumber add [Phone number] Adds your phone number to the SMSGTE platform, linked to your callsign with ssid (APRS). SMSGTE @me [Message] Sends a text message to your added number. (you need to do this to get the access number) SMSGTE @[call]-[ssid] [Message] Sends a text message to the phone number of the HAM. That\u0026rsquo;s it for my setup guide, hope it was helpful to you and feel free to contact me with questions you may have.\n73 de PD0TW\nSources used:\nhttps://www.reddit.com/r/amateurradio/comments/ap86gj/getting_dmr_sms_to_work_reliably_on_the_anytone/\nhttps://www.pe2kmv.nl/wp/en/dmr-en/sms-functions-via-dmr/\nhttps://brara.org/BLOG/2020/05/16/dmr-brandmeister-messaging/\nhttps://smsgte.org/dmr/\nhttps://www.reddit.com/r/DMR/comments/k3gq7i/basic_dmr_setup_on_anytone_d878uv_with_aprssms/ge49lo9/\n","date":"13 November 2020","permalink":"https://techwolf12.nl/blog/dmr-setup-anytone-d878uv-aprssms/","section":"Blog posts","summary":"The Anytone D878UV is quite a nice DMR (Digital Mobile Radio) capable radio packed with features like APRS (Reporting only), Bluetooth and GPS. It\u0026rsquo;s no surprise that this is my favourite DMR radio and I can recommend it if you are getting started with DMR for the first time. This post will help you get DMR up and running on your Anytone D878UV.","title":"DMR Setup on Anytone D878UV with APRS/SMS"},{"content":"Ham radio related blog posts \u0026amp; projects are gathered here!\nAlso available as RSS Feed.\n","date":null,"permalink":"https://techwolf12.nl/tag/ham-radio/","section":"Tags","summary":"","title":"HAM Radio"},{"content":"BGP stands for Border Gateway Protocol, more commonly known as the system that keeps the internet (and by definition, routing) working correctly. Sometimes misconfigurations (Like accidentally announcing a wrong prefix) can break the internet. In this blog post, I will explain BGP Hijacking and how to prevent it. Primarily for people without network experience.\nBGP Hijacking #BGP Hijacking is when an internet provider announces a subnet of IP addresses when that provider doesn\u0026rsquo;t have the authorization to do so. This happened recently when ProtonMail\u0026rsquo;s IP addresses got announced by Telstra.\nIt could happen by accident (misconfigurations) or by someone with malicious intent. By announcing someone\u0026rsquo;s subnet you get traffic to your network instead of the network it should normally go to. If you would do this with malicious intent you can use this to pretend to be the service and steal the users data. This has happened before when a BGP Hijack on Amazon\u0026rsquo;s DNS services got the cryptocurrency wallets of MyEtherWallet users stolen.\nLuckily, there is a way to prevent this\u0026hellip;\nPreventing BGP Hijacks - RPKI Filtering #RPKI stands for Resource Public Key Infrastructure. This system uses public keys to authenticate if you are allowed to announce the subnet and with filtering your internet provider can reject the range if it is invalid. This way the subnet won\u0026rsquo;t get propagated. Unfortunately, not all Internet providers have implemented this yet.\nIf you work at an ISP and have no idea how to implement RPKI, be sure to take a look at this site! It could help you to start implementing this in your organisation. https://rpki.readthedocs.io/en/latest/about/faq.html\nIf all Internet Providers had implemented RPKI Filtering, there wouldn\u0026rsquo;t have been a routing issue because of the misconfiguration. The one\u0026rsquo;s that did were unaffected by the invalid announcement. As for example can be seen here, the providers that implemented proper filtering were unaffected.\nIf you want to check if your own internet provider implements RPKI yet, go to Cloudflare\u0026rsquo;s https://isbgpsafeyet.com/ website and press the \u0026ldquo;Test your ISP\u0026rdquo;. For example, my provider Vodafone / Ziggo (AS33915) doesn\u0026rsquo;t support RPKI filtering yet. Be sure to let your provider know how important this is and ask them when they plan to implement this.\nAs always, feedback on these posts are welcome via the contact form or other ways to contact me in the sidebar.\n","date":"30 September 2020","permalink":"https://techwolf12.nl/blog/bgp-hijacking-what-it-and-how-prevent-it/","section":"Blog posts","summary":"BGP stands for Border Gateway Protocol, more commonly known as the system that keeps the internet (and by definition, routing) working correctly. Sometimes misconfigurations (Like accidentally announcing a wrong prefix) can break the internet. In this blog post, I will explain BGP Hijacking and how to prevent it. Primarily for people without network experience.","title":"BGP Hijacking - What is it and how to prevent it?"},{"content":"Also available as RSS Feed.\n","date":null,"permalink":"https://techwolf12.nl/tag/networking/","section":"Tags","summary":"","title":"Networking"},{"content":"This document will get you step by step through the generation of a GPG smartcard key, with the correct subkeys for use on a smartcard like the OpenPGP smartcard or a Yubikey. This will also allow you to use your GPG Authentication subkey for SSH support.\nGenerating a key #Generating the masterkey #With a smartcard key, you split your key into a master key (certify key) and individual subkeys (signing, encrypting, authentication). Often this is done on a airgapped machine. First off, you generate the key using expert mode:\ngpg --full-generate-key --expert Choose option \u0026ldquo;RSA (set your own capabilities)\u0026rdquo;, which is currently number 8 and toggle the sign (S) and encrypt (E) by selecting the option and confirming with enter. The only option on this key should be Certify! Then press Q followed by enter to finish.\nFor keysize, choose 4096 and confirm with enter. For the expire date, choose 10y and confirm with enter. and yes.\nFill in your full name and email, leave the comment blank, confirm your choice and enter in your passphrase.\nYou should now have a masterkey, it should show the fingerprint on the screen. (example: 34B35DD172E366BF6867AB069FB800372F2546D8)\nCreating your subkeys #Now we are going to add the capability to sign or decrypt messages and use the key for ssh authentication.\ngpg --expert --edit-key \u0026lt;YOUR FINGERPRINT\u0026gt; In this interface, you are going to generate some subkeys:\nSigning key #addkey Choose option \u0026ldquo;RSA (set your own capabilities)\u0026rdquo;, which is currently number 8. Toggle E (encryption) so the \u0026ldquo;Current allowed actions\u0026rdquo; only lists Sign and confirm with Q. Choose the keysize 4096. Choose the key expire date 3y. Confirm twice, then enter your passphrase.\nEncryption key #addkey Choose option \u0026ldquo;RSA (set your own capabilities)\u0026rdquo;, which is currently number 8. Toggle S (Signing) so the \u0026ldquo;Current allowed actions\u0026rdquo; only lists Encrypt and confirm with Q. Choose the keysize 4096. Choose the key expire date 3y. Confirm twice, then enter your passphrase.\nAuthentication key #addkey Choose option \u0026ldquo;RSA (set your own capabilities)\u0026rdquo;, which is currently number 8. Toggle S (Signing), E (Encryption) and A (Authentication) so the \u0026ldquo;Current allowed actions\u0026rdquo; only lists Authenticate and confirm with Q. Choose the keysize 4096. Choose the key expire date 3y. Confirm twice, then enter your passphrase.\nTrusting the keys and finishing up #Type in the command:\ntrust Choose 5, confirm. Type in:\nsave And check if you have multiple subkeys like such:\n$ gpg --list-key 015E51CA677A864C61F13D13149B9E17245535D8\rpub rsa4096 2020-03-26 [C] [expires: 2030-03-24]\r015E51CA677A864C61F13D13149B9E17245535D8\ruid [ultimate] Test testtt \u0026lt;testtt@techwolf12.nl\u0026gt;\rsub rsa4096 2020-03-26 [S] [expires: 2023-03-26]\rsub rsa4096 2020-03-26 [E] [expires: 2023-03-26]\rsub rsa4096 2020-03-26 [A] [expires: 2023-03-26] It should show 3 subkeys with a letter S, E and A, as well as the master key with the letter C!\nBackups #One of the most important things. Backup your key now since you have everything in one place!\ngpg --output revoke.asc --gen-revoke \u0026lt;YOUR FINGERPRINT\u0026gt;\rgpg --armor --output privkey.sec --export-secret-key \u0026lt;YOUR FINGERPRINT\u0026gt;\rgpg --armor --output subkeys.sec --export-secret-subkeys \u0026lt;YOUR FINGERPRINT\u0026gt;\rgpg --armor --output pubkey.asc --export \u0026lt;YOUR FINGERPRINT\u0026gt; Save these files somewhere safe. You need access to them if you lose your token or need to edit the expiration date. Share the pubkey.asc file with whoever you want to send you encrypted messages.\nInitialising the smartcard #Insert your smartcard, and execute the following command:\ngpg --card-edit Do the following, between parenthesis are comments:\nadmin\rpasswd\r(change admin pin, currently option 3, default is 12345678)\r3\r(change this to a random, new 8 numeric code)\r(set the reset code, option 4, use your admin pin)\r4\r(change this to a random, new 8 numeric code)\r1 (change this to a random, new 6 numeric code. Default is 123456 This is the one you will use during signing or encryption)\rq\rname\r(enter your (nick)name)\rurl\r(enter the https location of your public key file)\rquit Pushing the subkeys to the smartcard #Execute the following:\ngpg --expert --edit-key \u0026lt;YOUR FINGERPRINT\u0026gt; Then here, do the following:\ntoggle\rkey 1\rkeytocard Now take a look at the selected key after \u0026ldquo;key 1\u0026rdquo;, it should be the one with \u0026ldquo;usage: S\u0026rdquo; Select the storage slot \u0026ldquo;Signature key\u0026rdquo; (1) Enter your passphrase for your gpg masterkey. Enter the ADMIN PINCODE you changed above.\nThen run:\nkey 1\rkey 2\rkeytocard This should be your encryption key with \u0026ldquo;usage: E\u0026rdquo; Select the storage slot \u0026ldquo;Encryption key\u0026rdquo; (2) Enter your passphrase for your gpg masterkey. Enter the ADMIN PINCODE you changed above.\nThen run:\nkey 2\rkey 3\rkeytocard This should be your authentication key with \u0026ldquo;usage: A\u0026rdquo; Select the storage slot \u0026ldquo;Authentication key\u0026rdquo; (3) Enter your passphrase for your gpg masterkey. Enter the ADMIN PINCODE you changed above.\nFinish with:\nsave Testing the key #Test your key with the following, it should prompt for your pincode instead of passphrase and show a signed message:\necho \u0026#34;test\u0026#34; | gpg -a -u \u0026lt;your fingerprint\u0026gt; --sign Using on another computer #This is very easy to do, insert your token and run. This will import your public key and connect it to the smartcard. This only works if your public key is already uploaded to the location of the URL field in your card.\ngpg --card-edit\rfetch\rquit Enabling SSH support #Add enable-ssh-support and write-env-file to ~/.gnupg/gpg-agent.conf\nRestart GPG agent or reboot.\nCheck echo $SSH_AUTH_SOCK - it should be pointing to gpg-agent\u0026rsquo;s socket instead of ssh-agent.\nCheck your authentication key fingerprint\u0026rsquo;s last 16 characters by running:\n$ gpg --fingerprint --fingerprint 015E51CA677A864C61F13D13149B9E17245535D8\rpub rsa4096 2020-03-26 [C] [expires: 2030-03-24]\r015E 51CA 677A 864C 61F1 3D13 149B 9E17 2455 35D8\ruid [ultimate] Test testtt \u0026lt;testtt@techwolf12.nl\u0026gt;\rsub rsa4096 2020-03-26 [S] [expires: 2023-03-26]\r0612 76E3 8F5B 2F4D 095F 95A0 286E 51F1 6D91 53EE\rsub rsa4096 2020-03-26 [E] [expires: 2023-03-26]\rD7C6 3D9E 78BE 0932 F844 42DA 60D9 8F16 D50B B177\rsub rsa4096 2020-03-26 [A] [expires: 2023-03-26]\rCD1F B5B8 78F2 A1DE B036 6D90 3BF1 9BEC 700E 4870 Look at the subkey here with \u0026ldquo;[A]\u0026rdquo;, copy the 4 groups at the end, in this case: 3BF1 9BEC 700E 4870\nRemove the spaces: 3BF19BEC700E4870 and run (keep the exclamation mark in mind!):\ngpg -o gpg_ssh.pub --export-ssh-key 3BF19BEC700E4870! You now have your ssh key in gpg_ssh.pub which you can use on servers like you normally would with SSH keys.\n","date":"13 September 2020","permalink":"https://techwolf12.nl/blog/generating-gpg-key-smartcard-and-ssh-authentication/","section":"Blog posts","summary":"This document will get you step by step through the generation of a GPG smartcard key, with the correct subkeys for use on a smartcard like OpenPGP smartcard or the Yubikey. This will also allow you to use your GPG Authentication subkey for SSH support.","title":"Generating a GPG key with smartcard and SSH Authentication"},{"content":"The following script takes a Bind9 zonefile, gets all AAAA records from it and generates PTR records based on them.\nWhat you need to do:\nEdit the Zone header in the script. Run the script with ./generate_v6_ptr.sh /path/to/zonefile.zone This will output the zones on STDOUT. If you want to save this to a zonefile, you can use this example: ./generate_v6_ptr.sh /path/to/zonefile.zone \u0026gt; /etc/bind/ip6.arpa.zone\n#!/bin/bash read -r -d \u0026#39;\u0026#39; ZONEHEADER \u0026lt;\u0026lt;- EOM $TTL 1h ; Default TTL @ IN SOA \u0026lt;NAMESERVER 1\u0026gt;. \u0026lt;ABUSE EMAIL\u0026gt;. ( 2019071201 ; serial 1h ; slave refresh interval 15m ; slave retry interval 1w ; slave copy expire time 1h ; NXDOMAIN cache time ) ; ; domain name servers ; @ IN NS \u0026lt;NAMESERVER 1\u0026gt;. @ IN NS \u0026lt;NAMESERVER 2\u0026gt;. ; IPv6 PTR entries EOM # Script: function reverseIp6 { echo \u0026#34;$1\u0026#34; | awk -F: \u0026#39;BEGIN {OFS=\u0026#34;\u0026#34;; }{addCount = 9 - NF; for(i=1; i\u0026lt;=NF;i++){if(length($i) == 0){ for(j=1;j\u0026lt;=addCount;j++){$i = ($i \u0026#34;0000\u0026#34;);} } else { $i = substr((\u0026#34;0000\u0026#34; $i), length($i)+5-4);}}; print}\u0026#39; | rev | sed -e \u0026#34;s/./\u0026amp;./g\u0026#34; } if [ -z \u0026#34;$1\u0026#34; ] then echo \u0026#34;Usage: $0 \u0026lt;zonefile\u0026gt;\u0026#34; exit 1 fi RECORD=(`cat $1 | grep AAAA | awk -v\u0026#39;OFS=,\u0026#39; \u0026#39;$2 == \u0026#34;IN\u0026#34; {print $4}\u0026#39;`) HOST=(`cat $1 | grep AAAA | awk -v\u0026#39;OFS=,\u0026#39; \u0026#39;$2 == \u0026#34;IN\u0026#34; {print $1}\u0026#39;`) echo \u0026#34;$ZONEHEADER\u0026#34; for (( i=0; i\u0026lt;${#RECORD[@]}; i++ )); do echo \u0026#34;$(reverseIp6 ${RECORD[i]})ip6.arpa. IN PTR ${HOST[i]}\u0026#34;; done ","date":"2 August 2020","permalink":"https://techwolf12.nl/blog/generating-ipv6-ptr-records-bind9-zonefile-using-bash/","section":"Blog posts","summary":"The following script takes a Bind9 zonefile, gets all AAAA records from it and generated PTR records based on them.","title":"Generating IPv6 PTR records for bind9 using Bash"},{"content":"When you run an LDAP server you want to use it to authenticate as much as possible using this system, either to comply with security policies or make it easier for users to login using one authentication method. If you use the Apache2 webserver you can setup HTTP Basic authentication with LDAP. In this tutorial I will show how I accomplished this.\nApache2 LDAP usage #First you need to enable the LDAP module:\na2enmod ldap \u0026amp;\u0026amp; service apache2 reload To limit on any valid user you can set the config as such:\n\u0026lt;Location \u0026#34;/\u0026#34;\u0026gt;\rLDAPVerifyServerCert On\rLDAPTrustedMode STARTTLS\rAuthLDAPURL ldap://ldap.example.com/dc=example,dc=com\rAuthLDAPBindDN \u0026#34;uid=binduser,dc=example,dc=com\u0026#34;\rAuthLDAPBindPassword \u0026#34;BindPW\u0026#34;\rAuthName \u0026#34;Protected\u0026#34;\rAuthType Basic\rAuthBasicProvider ldap\rRequire valid-user\r\u0026lt;/Location\u0026gt; RBAC - Role based access control #Often you have groups within LDAP with the memberOf variable enabled. If you want to limit on a group of people you can use the following syntax:\n\u0026lt;Location \u0026#34;/\u0026#34;\u0026gt;\rLDAPVerifyServerCert On\rLDAPTrustedMode STARTTLS\rAuthLDAPURL ldap://ldap.example.com/dc=example,dc=com\rAuthLDAPBindDN \u0026#34;uid=binduser,dc=example,dc=com\u0026#34;\rAuthLDAPBindPassword \u0026#34;BindPW\u0026#34;\rAuthName \u0026#34;Protected\u0026#34;\rAuthType Basic\rAuthBasicProvider ldap\rRequire ldap-attribute memberOf=cn=admins,dc=example,dc=com\r\u0026lt;/Location\u0026gt; IP and group limit #You can limit on both coming from an IP address and being in an LDAP group. Just replace the Require rule with something like:\n\u0026lt;RequireAll\u0026gt;\r\u0026lt;RequireAny\u0026gt;\rRequire ip 1.1.1.1\rRequire ip 2.2.2.2\r\u0026lt;/RequireAny\u0026gt;\r\u0026lt;RequireAny\u0026gt;\rRequire ldap-attribute memberOf=cn=admins,dc=example,dc=com\rRequire ldap-attribute memberOf=cn=operations,dc=example,dc=com\r\u0026lt;/RequireAny\u0026gt;\r\u0026lt;/RequireAll\u0026gt; Multiple LDAP servers #If you have different servers for users, such as a server per location. You can add multiple LDAP servers like such:\n\u0026lt;Location \u0026#34;/\u0026#34;\u0026gt;\rLDAPVerifyServerCert On\rLDAPTrustedMode STARTTLS\r\u0026lt;AuthnProviderAlias ldap ldap1\u0026gt;\rAuthLDAPURL ldap://ldap.example.com/dc=example,dc=com\rAuthLDAPBindDN \u0026#34;uid=binduser,dc=example,dc=com\u0026#34;\rAuthLDAPBindPassword \u0026#34;BindPW\u0026#34;\r\u0026lt;/AuthnProviderAlias\u0026gt;\r\u0026lt;AuthnProviderAlias ldap ldap2\u0026gt;\rAuthLDAPURL ldap://ldap.example2.com/dc=example2,dc=com\rAuthLDAPBindDN \u0026#34;uid=binduser,dc=example2,dc=com\u0026#34;\rAuthLDAPBindPassword \u0026#34;BindPW\u0026#34;\r\u0026lt;/AuthnProviderAlias\u0026gt;\rAuthName \u0026#34;Protected\u0026#34;\rAuthType Basic\rAuthBasicProvider ldap1 ldap2\rRequire valid-user\r\u0026lt;/Location\u0026gt; That\u0026rsquo;s all, if there are any questions, feel free to contact me!\n","date":"21 June 2020","permalink":"https://techwolf12.nl/blog/http-basic-authentication-ldap-and-apache2/","section":"Blog posts","summary":"When you run an LDAP server you want to use it to authenticate as much as possible using this system, either to comply with security policies or make it easier for users to login using one authentication method. If you use the Apache2 webserver you can setup HTTP Basic authentication with LDAP. In this tutorial I will show how I accomplished this.","title":"HTTP Basic Authentication with LDAP and Apache2"},{"content":"Thanks to the new VPC functionality in DigitalOcean can be used to provide Kubernetes with a static external IPv4. This can be handy in cases where you need to deal with IP whitelists, for example, if you use your Kubernetes cluster as a CI building tool. However, this requires some config setup and a privileged pod running on each node to automatically update the routes. This article will help guide you through the setup.\nThis will work for other types of Kubernetes instances as long as you have an internal range and one VM in it which you can setup as router. Disclaimer: I am not a Kubernetes expert and I only confirmed it to work in our environment. If you want to use it, please test it yourself first.\nSetting up a VM for routing #To begin, you need a separate small VM for routing with an external static IPv4. DigitalOcean will already have created a default VPC per region you have servers/Kubernetes in. We will use this Private IP range for routing internally.\nCreate a basic Debian server within the DigitalOcean control panel. The external IP of this VM will be the external IP of your Kubernetes pods soon. Run the following commands to start:\napt update apt install iptables iptables-persistent sysctl -w net.ipv4.ip_forward=1 echo \u0026#34;net.ipv4.ip_forward=1\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf Now you need to find which interface your public IP network is connected to and your private IP range with subnet. Use the following to gain this info:\nip address After this, use this info replace the vpc_network_prefix (eg. 10.100.0.0/20) and public_interface_name (eg. ens3).\niptables -t nat -A POSTROUTING -s \u0026lt;vpc_network_prefix\u0026gt; -o \u0026lt;public_interface_name\u0026gt; -j MASQUERADE iptables-save \u0026gt;/etc/iptables.rules.v4 Note down the VPC internal IP address (eg. 10.100.10.5), this server is complete.\nKubernetes #\rDisclaimer! This should no longer be used. When a Node\u0026rsquo;s default route is changed, it is then unable to contact the Control plane and it\u0026rsquo;s associated components. Instead, take a look at DigitalOcean\u0026rsquo;s own static route operator and only set routes for IP ranges you want to access like this.\rFor Kubernetes, I created a container that can change the route on the node hosts with an environment variable. Deploy it like this using kubectl create -f techwolf12.yaml, if you save the contents of this file to techwolf12.yaml. Don\u0026rsquo;t forget to change the INETROUTE env value in this config before applying it to your cluster!\n--- apiVersion: v1 kind: ServiceAccount metadata: name: kube-static-route namespace: default automountServiceAccountToken: true --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: kube-static-route rules: - apiGroups: [\u0026#34;\u0026#34;] resources: - \u0026#34;nodes\u0026#34; - \u0026#34;nodes/metrics\u0026#34; - \u0026#34;nodes/stats\u0026#34; - \u0026#34;nodes/proxy\u0026#34; - \u0026#34;pods\u0026#34; - \u0026#34;secrets\u0026#34; - \u0026#34;services\u0026#34; verbs: [\u0026#34;get\u0026#34;, \u0026#34;list\u0026#34;] - nonResourceURLs: [\u0026#34;/metrics\u0026#34;] verbs: [\u0026#34;get\u0026#34;] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: kube-static-route roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: kube-static-route subjects: - kind: ServiceAccount name: kube-static-route namespace: default --- apiVersion: apps/v1 kind: DaemonSet metadata: name: kube-static-route namespace: default labels: app: kube-static-route spec: selector: matchLabels: name: kube-static-route updateStrategy: type: RollingUpdate # Only supported in Kubernetes version 1.6 or later. template: metadata: labels: name: kube-static-route annotations: # Needed for Kubernetes versions prior to 1.6.0, where tolerations were set via annotations. scheduler.alpha.kubernetes.io/tolerations: | [{\u0026#34;operator\u0026#34;: \u0026#34;Exists\u0026#34;, \u0026#34;effect\u0026#34;: \u0026#34;NoSchedule\u0026#34;},{\u0026#34;operator\u0026#34;: \u0026#34;Exists\u0026#34;, \u0026#34;effect\u0026#34;: \u0026#34;NoExecute\u0026#34;}] spec: serviceAccountName: kube-static-route hostNetwork: true dnsPolicy: ClusterFirstWithHostNet containers: - name: kube-static-route image: ghcr.io/techwolf12/kube-static-route:master securityContext: privileged: true resources: limits: memory: 150Mi requests: cpu: 100m memory: 150Mi env: - name: \u0026#34;INETROUTE\u0026#34; value: \u0026#34;\u0026lt;CHANGE THIS VALUE TO YOUR VPC INTERNAL ROUTE IP\u0026gt;\u0026#34; tolerations: - operator: \u0026#34;Exists\u0026#34; effect: \u0026#34;NoSchedule\u0026#34; - operator: \u0026#34;Exists\u0026#34; effect: \u0026#34;NoExecute\u0026#34; The routing should now automatically update on all nodes, including the ones being added or upgraded. Pods should use this routing by default as well. This works because the container techwolf12/kubernetes-static-route will change the route on the host node and make sure it stays on the correct route.\n","date":"3 May 2020","permalink":"https://techwolf12.nl/blog/digitalocean-kubernetes-static-ipv4/","section":"Blog posts","summary":"Thanks to the new VPC functionality in DigitalOcean can be used to provide Kubernetes with a static external IPv4. This can be handy in cases where you need to deal with IP whitelists, for example, if you use your Kubernetes cluster as a CI building tool. However, this requires some config setup and a privileged pod running on each node to automatically update the routes. This article will help guide you through the setup.","title":"DigitalOcean Kubernetes with Static IPv4"},{"content":"So you got an OpenLDAP server running? Great! Now you want to connect it to as many systems as possible to ease the burden of managing users and authorization. However, you also want to allow SSH key authorisation managed via a central place. Can LDAP be used for this? This article will help you get started to set this up in your organisation.\nLDAP Changes #LDAP allows you to extend data you can enter using a schema. For convenience, you can use the schema listed below.\ndn: cn=techwolf12,cn=schema,cn=config\robjectClass: olcSchemaConfig\rcn: techwolf12\rolcAttributeTypes: {0}( 1.3.6.1.4.1.52389.2.1 NAME \u0026#39;gpgFingerprint\u0026#39; DESC \u0026#39;MA\rNDATORY: GnuPG Public key\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.14\r66.115.121.1.26 )\rolcAttributeTypes: {1}( 1.3.6.1.4.1.52389.2.2 NAME \u0026#39;sshPublicKey\u0026#39; DESC \u0026#39;MAN\rDATORY: OpenSSH Public key\u0026#39; EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.14\r66.115.121.1.40 )\rolcAttributeTypes: {2}( 1.3.6.1.4.1.52389.2.5 NAME \u0026#39;twoFactorPublic\u0026#39; DESC \u0026#39;M\rANDATORY: 2FA Public key\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.146\r6.115.121.1.26 )\rolcAttributeTypes: {3}( 1.3.6.1.4.1.52389.2.6 NAME \u0026#39;phoneExtNumber\u0026#39; DESC \u0026#39;In\rternal PBX extension\u0026#39; SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )\rolcAttributeTypes: {4}( 1.3.6.1.4.1.52389.2.7 NAME \u0026#39;sshHostAllow\u0026#39; DESC \u0026#39;Opti\ronal: Allow access to specific ssh hosts\u0026#39; EQUALITY caseExactIA5Match SYNTAX\r1.3.6.1.4.1.1466.115.121.1.26 )\rolcAttributeTypes: {5}( 1.3.6.1.4.1.52389.2.8 NAME \u0026#39;sshHostDeny\u0026#39; DESC \u0026#39;Optio\rnal: Denies access to specific ssh hosts\u0026#39; EQUALITY caseExactIA5Match SYNTAX\r1.3.6.1.4.1.1466.115.121.1.26 )\rolcAttributeTypes: {6}( 1.3.6.1.4.1.52389.2.9 NAME \u0026#39;userPinCode\u0026#39; DESC \u0026#39;Optio\rnal: user pincode for quick identification\u0026#39; EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )\rolcObjectClasses: {0}( 1.3.6.1.4.1.52389.1.1 NAME \u0026#39;sshUser\u0026#39; SUP top AU\rXILIARY DESC \u0026#39;MANDATORY: Cryptography objectclass\u0026#39; MAY ( sshPublicKey $ gpg\rFingerprint $ twoFactorPublic $ sshHostAllow $ sshHostDeny $ phoneExtNumber $ userPinCode ) ) Creating a group #To properly use ssh key authentication you need to assign all ssh users to a group so they can be filtered out. If you don\u0026rsquo;t do this, key authentication for users without LDAP will no longer work.\ndn: cn=sshldapuser,dc=example,dc=com\robjectClass: posixGroup\robjectClass: top\rcn: sshldapuser\rgidNumber: 10000\rdescription: SSH users from LDAP\rmemberUid: techwolf12 Setting up a user #Create a user like this. Please keep in mind that the sshPublicKey value is in Hex format.\ndn: uid=techwolf12,ou=example-ou,dc=example,dc=com\robjectClass: inetOrgPerson\robjectClass: sshUser\robjectClass: organizationalPerson\robjectClass: person\robjectClass: posixAccount\robjectClass: shadowAccount\robjectClass: top\rcn: Christiaan de Die le Clercq\rgidNumber: 10000\rhomeDirectory: /home/techwolf12\rsn: de Die le Clercq\ruid: techwolf12\ruidNumber: 10000\rgivenName: Christiaan\rgpgFingerprint: 34B35DD172E366BF6867AB069FB800372F2546D8\rloginShell: /bin/bash\rmail: email@example.com\rsambaNTPassword: 3BE81268C8E07E150DDF53D6AEE4E0B6\rsshPublicKey:: c3NoLXJzYSBBQUFBQjNOemFDMXljMkVBQUFBREFRQUJBQUFCQVFET0VnQzJ2b\rHlBMzZ1MVNVUkZVS3hYaEdwZHA2c3ViUU10QU01MmV4bldhR0dkdURLZDNwOU9hb0x4cnB4eUtX\rbzdidGhBWHNRL045N0dETTY0bEIwZStTdkpLdCtrVGllcUZiQ3A3STB2V25zSnZJRWl3WmRjY0d\rzOU42bnRqU0xUbUl4MFdkaG4vMWF3eENId1FxTHpjYS9yM2w1cnVDRkwzR1FKU3JoeDNNMFdBT0\rQ2Yk5OdFQxT0ZDci9tcDhjVGJiYVdTbEhpajJHbjBPak1RWUpmWEpMNWs3QWJwaWJyMUdCN1V3M\r0prNXUwazUvdGRNc2twUVZZRFlkaEhRalBJZ3IrREw5ek4wUkpkaWNNLzRRQnl0aGtFYjhRNWs0\rQ0U1Vk9PM2VDWXVwYjdrYStLNkJ5dldLemVIL2REdDMvSDRIYVBTOW1JZytXamVKT3NuUTkgUHJ\rpbWFyeQ==\ruserPassword:: e1NTSEF9eWdsY1FYci9zR25Wem84WmtydFNjeHhDWjdEZjBMVHM4RjErWlE9P\rQ== Server changes #On the server which you want to have LDAP users login, you need to make a few changes to allow LDAP to be used. In this case, we assume a Debian 9 based server to be used.\nThe first step is to install some dependencies and packages like so:\napt install libnss-ldap libpam-ldap nscd ldap-utils For Debian 11, use this:\napt install libnss-ldapd libpam-ldap nscd ldap-utils After this is done, you need to create a couple of configuration files and scripts to connect to LDAP.\nCreate the file /etc/ldap/ldap.conf with the following content:\nuri ldap://ldap.example.com/\rbase dc=example,dc=com\rbinddn uid=binduser,dc=example,dc=com\rbindpw BINDPASSWORD\rssl start_tls\rbind_timelimit 2\rnetwork_timeout 2\rtimeout 2\rbind_policy soft\rtls_cacertdir /etc/ssl/certs\rTLS_REQCERT allow Create the file with /etc/libnss-ldap.conf with the following content, change the values to yours:\nbase dc=example,dc=com\ruri ldap://ldap.example.com\rldap_version 3\rbinddn uid=binduser,dc=example,dc=com\rbindpw BINDPASSWORD\rssl start_tls\rtls_checkpeer no\rtls_cacertdir /etc/ssl/certs Create a bash file (somewhere like /opt/ldap-sshkey.sh):\n#!/bin/bash /usr/bin/ldapsearch -H \u0026#39;ldap://ldap.example.com\u0026#39; -LLL -b \u0026#34;ou=example-ou,dc=example,dc=com\u0026#34; -ZZ -D \u0026#34;uid=binduser,dc=example,dc=com\u0026#34; -w \u0026#39;BINDPASSWORD\u0026#39; -o ldif-wrap=no uid=\\$1 | /bin/grep sshPublicKey: | cut -d\u0026#34; \u0026#34; -f 2-4 Add the following to /etc/ssh/sshd_config to allow users to login with ssh-keys:\nMatch group sshldapuser\rAuthorizedKeysCommand /opt/ldap-sshkey.sh %u\rAuthorizedKeysCommandUser nobody Finally, execute the following commands for the final setup, activation of LDAP in the system and auto creation of the users home directory on the first login:\nrm /etc/pam_ldap.conf \u0026amp;\u0026amp; ln -s /etc/ldap/ldap.conf /etc/pam_ldap.conf chmod 755 /opt/ldap-sshkey.sh sed -i.bak \u0026#39;s#compat#compat ldap#g\u0026#39; /etc/nsswitch.conf service nscd restart echo \u0026#34;session\trequired\tpam_mkhomedir.so\u0026#34; \u0026gt;\u0026gt; /etc/pam.d/common-session service ssh restart You should now have your ssh users from LDAP and they should be able to login via SSH keys stored in LDAP. If there are any questions, feel free to contact me!\n","date":"1 May 2020","permalink":"https://techwolf12.nl/blog/ssh-authentication-ldap/","section":"Blog posts","summary":"So you got an OpenLDAP server running? Great! Now you want to connect it to as many systems as possible to ease the burden of managing users and authorization. However, you also want to allow SSH key authorisation managed via a central place. Can LDAP be used for this? This article will help you get started to set this up in your organisation.","title":"SSH Authentication via LDAP"},{"content":"","date":"1 January 0001","permalink":"https://techwolf12.nl/app/privacy-policy/","section":"Apps","summary":"","title":""},{"content":"","date":null,"permalink":"https://techwolf12.nl/app/","section":"Apps","summary":"","title":"Apps"},{"content":"See my GPG Key-signing policy.\nFingerprint: 34B35DD172E366BF6867AB069FB800372F2546D8\nFor my public key you can use the text below. Alternatively you can grab it from a keyserver or download this file with signatures included.\n-----BEGIN PGP PUBLIC KEY BLOCK-----\rmQINBFTJ/V0BEACvnMMp+br35d5M0iCadlMLLZpmFh0G3CfFNC6g/CD1dmRBCdyA\rm0nljgvOoqAvCvgp6f4NSYIH/2Qt8We1xeGE2V9RzAtrDjWnhbTaHvyEE8pJ49WS\rvgPPi1sbD+I1RdUOiTbKZ19sbMwzCnXY/argLos1R2KJw2TrnN5D32NUiXmPT5mK\raoL1w/Ak++JKNiR1W14yJsFX1LYKGXcO5tUes2Dt88UVX1tASjNnkpAWtmHoqMkp\rCVNnjdG3rnoCuYIS5hjlZCYaFPsUuBbrh+Td5oOiYYTXPj3wpDlbDazydTu4wpti\rh2cuo2M2E32jXqtlbDUmLEJYzgl4ureJ38FvtTqzuT8BblEM8CFmui64bFr29FkY\rvpS20PUHi0f9HTmH6M+k1YD1Swrd/AESzRrZxNElwtde7SyffPodfVNGMNOAZlBd\rfY0MeVT7VdBja7Xar51Bc7Jbi2NCaezb9nL0tF025511ALXO1zzldps3r65SRrOI\rNpqIcUVMizPfNw3XelyujmGEs5E8x0mhquXKtKtPCN+65ETzjOL1PMODFTrnX4mm\r3W20QugiC66OYG1srDJAyv8cNZaHHKjtbTu6nyDp4zeqrBfx6Qu/1ZeIkiaPcBYv\r9fPHAqfcHLWaKQ97zTY129IHYQP8UNoScenQ8CJ9YV54pc7cApnCHzzlCQARAQAB\rtDNDaHJpc3RpYWFuIGRlIERpZSBsZSBDbGVyY3EgPGNvbnRhY3RAdGVjaHdvbGYx\rMi5ubD6JAlgEEwEIAEICGwEGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAhkBFiEE\rNLNd0XLjZr9oZ6sGn7gANy8lRtgFAmN9PswFCRJ1qG8ACgkQn7gANy8lRtiayxAA\riH2y3DvyfBRXq6EN4/1iRfh3Njrn/uP9a/2SymXJ+20ESc0ZGeNd8XlaHO2TYJIB\rqfGLmEFh6t5I6O4DBpTKn416HRlZm4XYYe5HR9+5c4fTt1n3FxXxJf9ucxXiHS07\rO+ST9yrduzFQlpWhL95Y4PiXyb8pThhtJ9TLa6MuUqBX9AEJucPjdt3TkjGAWpLz\rPwCvWZwptHpz258Y/UKTKhjEKjACdaO9xy/APWUgrYkEgrFDjjaTPMEel0Hng0S0\rJd6s62G68HeIk/2sK9LJy4+WNXOm/yMunPTyyQI2X5+iCODN48pjbn3UampHdm9t\rNLDcGOkd2cPeX7K+qYGFmkDMdnhMX4Nm8IyT+A0FL3YGVUJwQ6x7qG6vXfQ/NcKX\rncNkPzltqZ4+JTUg38ZfCFAjqqvqjD/YS2aU/NkOEubP9My2PY2CKmgptKITsI/p\rAvml9mHNKkf8t1XBKT0gvxJ7KDuAvuFwaF1/9IAA2st7ACQEfgJ/Uk6hwpC3xoVg\rZzBkPQzAu9E75WdCKZMX1cRcptgRo3tJevRMOqcjV/+EqZC3Y4yS3qKOha1BEdY3\rtedBX/V/CYZuH2KyObGSmLyZMAIB0mgGIwdefHoslznRHGh5ytY6IveGCfBGicay\r2fRN9jaWT9qV97sp6qAnwrvSP54WrjOM/QwhydkonF20NENocmlzdGlhYW4gZGUg\rRGllIGxlIENsZXJjcSA8Y2hyaXN0aWFhbkBkcm9pZGRldi5ubD6JAigEMAEKABIF\rAlmbMOQLHSBPbGQgZW1haWwACgkQn7gANy8lRtj8HBAAlTYskZ8iybHDvcmtKets\rFXibIfrIjdf5cQGZubloBOtJDdKR2dy6JvjHjGGoHHmFd7SAWOTpT1aaF0Jic45M\rc2If9x+0tXP4Ruf/dLUrgZnq85ViHyGR1nvzF2Pa9SRhfMqkeP790xt+1KTbhJ0O\rD4BGVojzv0TBJyuIGjoljUCfJMuT4PJKOPje1LIqqJM7vY8ZOoB27xWvblgH+15P\rH23gLatC8FRdIi76MIv2kkh95Ac0X0RDxUQirErbsKJ1yjsOyNoOEYPcSxBKp8Lq\rvV836xpF6uB7vLb+Bb9mhArDQ5WE9R13Ph5OEirzK4aAHg8fkWKshMBHkhm7XQIi\rSfyB575keT3R69R5xtpIZprFCZY40z80CIcqq7c4sd8S4ACIDOxaKz8h3LbU0SWz\r0X+QVYQd9qFDn1SZZSVur8ro2XIv8fCMSKoJlbusbKs6hQRABwYtGrI+p6FW8SDB\rAuB9twTPt/a/9IEtdrMelqE6LEefouQHGQPs1zE1A5NxBPuXqGWrtKbaWsMVOu3W\rdChyjzO9SwewrhIr6jweGsfKZIgAF59sgsC6xFzj2XflbzYeTP/WkeMLzp3F+VL/\rQquZAFhxMQ8hli8xM2LMI5Yujb7uIwWIdWGhaG1uju26VLzxaoHcRmoXniJLu/VE\rvUDwdsY2CBYra9gm5pd8bIa0OENocmlzdGlhYW4gZGUgRGllIGxlIENsZXJjcSA8\rbWVkaWFjb2xsZWdlQHRlY2h3b2xmMTIubmw+iQIoBDABCgASBQJZmzEYCx0gT2xk\rIGVtYWlsAAoJEJ+4ADcvJUbY9qoP/iJx7uMxuoAGUai0ff37WWDWjnhzDYZ1L2GO\rv4BR8pJ01bNfQzO022OQlPXN9cJcVgYy4O7p0QiCDvV8hRaGTkkKccdoGGyeMpRp\rKddmN0B0gJMvfinlypSGgCAMawujjNhTP3Qcx468mO7wQ+bFv4elNg/MteJg4aBw\reFnJ/NU372BiL5aMg3VNx3UK6YgvnLQRlI3BzrTjoiqE7VSHNPJJypRaHUwYa/Uy\rLmbqTSUQaBUYw3bCFaZNfXe+8b31lIKOJsUhmvhLRpWblHewwKxC2/J6pQ3Su0lr\r9hYP9av6eWcbDQG7nKWLjI8pTmSOBVbHrY4ckqZtJKJtvUunOBXHJhNrueN6xtGA\rS7PDyoID34qWanLnmIFxGgmk94woNMEoq8zfpygYQmoNuN7uvSRIeppVNIAqSvqF\r+mDTqI3iagsSV9ZmSwFel7R4pq48Yqi/QGBtv0ljw0Ltf+aJ7YXU5Rm4p3dQCpz+\rKSTY+tWNpPTh7xQJv5CG0VX+x1fP69dw607z2B0/BUi87v2IsnB8po7KUYAfFWiN\r0rSF0SdqtmDvmjq6ao1Ndon6dUUcLXTfW2N0LTnP6IsUfW7mLGE89ckNjjkeg/Pc\rDphlkI87+Ct6D01U5GMXzuTq/VZh1SNhXVA2FLYBwOC8G1dCW48Yts+t4yh3V99J\r+0gOWMfEtDlDaHJpc3RpYWFuIGRlIERpZSBsZSBDbGVyY3EgPHRlY2h3b2xmMTJA\rcGlyYXRlbnBhcnRpai5ubD6JAigEMAEKABIFAlmbMQgLHSBPbGQgZW1haWwACgkQ\rn7gANy8lRtgecg/+ILiNT4mDaAXVkYmZN57mr+50LySQ87xX65/QkXGv8n9Ni/1k\ruv2nUc/ubvaqnDHiRpKm77hSoSCM3EzFrrYSKDuT7D4iv+wBLg6Rx4wMQ7xbvB7m\rbJKfjSwAra6tNBSUBo0f0JcxAIQB3Tj28FmT2T37ZcXCKIee6VI8BFrOnf+0gTMT\rIVKMU/FjYorXWsJTzE24a1zYN5pp0zuJ2X+zVJdyNxdiIaPzq0/53HVJpbTmGdnh\r6O45YPUd04W4Ufatg4og8Sm9C4Rr1rpip+3n/VpbXJ1fsEUdpMMn5LZ5uV543CXw\r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\r=2rzd\r-----END PGP PUBLIC KEY BLOCK----- ","date":null,"permalink":"https://techwolf12.nl/gpg/","section":"GPG","summary":"","title":"GPG"},{"content":"Key Signing Policy for Christiaan de Die le Clercq. https://techwolf12.nl/gpg/policy Version 2020/01/01\npub rsa4096 2015-01-29 [C] [expires: 2024-11-21]\r34B35DD172E366BF6867AB069FB800372F2546D8\ruid [ultimate] Christiaan de Die le Clercq \u0026lt;contact@techwolf12.nl\u0026gt;\rsub rsa2048 2015-01-29 [S] [expires: 2024-11-21]\rsub rsa2048 2015-01-29 [E] [expires: 2024-11-21]\rsub rsa2048 2015-01-29 [A] [expires: 2024-11-21] This policy is used for signatures made by my GnuPG key 0x2F2546D8 - starting from 2017/01/01. (Most signatures before this date were also made under the following conditions. No key was ever signed without checking the identity of the person and the fingerprint.)\nBefore I sign a key, I\nverify the identity of the person owning the to-be-signed key by looking at their identity card, equivalent official proof of identity or in some special cases by knowing the person very good for a long time. receive the key fingerprint from the key owner. This can be on a piece of paper or the fingerprint could get confirmed by the owner during a Key Signing Party. A signature is always on a user ID. By signing a user ID, I confirmed for myself,\nthat the person, who gave me the fingerprint of that key, had the claimed name - at the moment of identity check. I do sign keys of persons from foreign countries as long as there is no indication of fraud (detected by me).\nSignatures by my GnuPG key(s) do not have any legal relevance.\nDescription of my use of trust levels:\nsig3 - I have verified the identity and verified, that the e-mail address of the signed uid belongs/belonged to the person, who has/had control over the key. This is done by a challenge-response system or by sending the signed key to the corresponding user id (both via encrypted mail).\nsig2 - I have verified the identity - but not the e-mail address (for example because the key does not support encryption to it).\nsig1 - unused at the moment.\nThe Certify key is kept on an offline, secure storage.\n","date":null,"permalink":"https://techwolf12.nl/gpg/policy/","section":"GPG","summary":"","title":"GPG Policy"},{"content":"Hi!\nI\u0026rsquo;m PD0TW, a HAM operator from the Netherlands mostly active on the VHF/UHF or DMR (ID: 2040188). My DMR ID is also configured for DAPNET, so you can page me if you like. Maidenhead/grid locator: JO22kq\nMy logbook is available on QRZ.com\nI sometimes blog about HAM Radio\nQSL #I\u0026rsquo;m a member of VERON, which grants me the rights to use the Dutch QSL Bureau. If you have a QSL card for me, feel free to send it to me through your QSL Bureau. If you are not a member of a QSL Bureau, you can send one to:\nDutch QSL Bureau Box 330 6800 AH Arnhem The Netherlands I\u0026rsquo;ll do my best to send one back!\nEquipment / Hardware #APRS \u0026amp; iGate #I run a APRS iGate at home with a RTL-SDR dongle on a small Linux machine running pymultimonaprs. This iGate is running on SSID -10, you can check it on APRS Direct. I will also have a X1C5 Plus mobile APRS gateway, this will be configured on SSID -2.\nVHF/UHF #My daily carry is an Anytone D878UV VHF(7W)/UHF(6W) DMR-capable radio, also used this for APRS (SSID -7) and analog.\nIn my car I have a Anytone D578UV and I also have a Zastone Z218 (25W) Analog VHF/UHF transceiver in the thrunk.\nHF #A Xiegu G90 is currently my HF rig. I\u0026rsquo;m not that active on here yet because of antenna placement challenges. But hopefully in the future!\n73 de PD0TW\n","date":null,"permalink":"https://techwolf12.nl/ham-radio/","section":"Techwolf12 Home","summary":"","title":"HAM Radio"},{"content":"Below you can see the technologies I use:\n","date":null,"permalink":"https://techwolf12.nl/tech-stack/","section":"Techwolf12 Home","summary":"","title":"Tech stack"}]