[{"content":"Also available as RSS Feed or Telegram Channel\n","date":null,"permalink":"/blog/","section":"Blog posts","summary":"Also available as RSS Feed or Telegram Channel","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":"/tag/devops/","section":"Tags","summary":"DevOPS is a collection of blog articles related to CI/CD, server management and the likes.","title":"DevOPS"},{"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":"/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":"Use this to search blog posts based on the tag\n","date":null,"permalink":"/tag/","section":"Tags","summary":"Use this to search blog posts based on the tag","title":"Tags"},{"content":" Oh hello there! I\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":"/","section":"Techwolf12 Home","summary":"Oh hello there!","title":"Techwolf12 Home"},{"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 objectClass: olcConfig objectClass: olcSchemaConfig objectClass: top cn: sudo olcAttributeTypes: {0}( 1.3.6.1.4.1.15953.9.1.1 NAME \u0026#39;sudoUser\u0026#39; DESC \u0026#39;User(s ) who may run sudo\u0026#39; EQUALITY caseExactIA5Match SUBSTR caseExactIA5Substring sMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) olcAttributeTypes: {1}( 1.3.6.1.4.1.15953.9.1.2 NAME \u0026#39;sudoHost\u0026#39; DESC \u0026#39;Host(s ) who may run sudo\u0026#39; EQUALITY caseExactIA5Match SUBSTR caseExactIA5Substring sMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) olcAttributeTypes: {2}( 1.3.6.1.4.1.15953.9.1.3 NAME \u0026#39;sudoCommand\u0026#39; DESC \u0026#39;Com mand(s) to be executed by sudo\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4 .1.1466.115.121.1.26 ) olcAttributeTypes: {3}( 1.3.6.1.4.1.15953.9.1.4 NAME \u0026#39;sudoRunAs\u0026#39; DESC \u0026#39;User( s) impersonated by sudo\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466 .115.121.1.26 ) olcAttributeTypes: {4}( 1.3.6.1.4.1.15953.9.1.5 NAME \u0026#39;sudoOption\u0026#39; DESC \u0026#39;Opti ons(s) followed by sudo\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466 .115.121.1.26 ) olcAttributeTypes: {5}( 1.3.6.1.4.1.15953.9.1.6 NAME \u0026#39;sudoRunAsUser\u0026#39; DESC \u0026#39;U ser(s) impersonated by sudo\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1. 1466.115.121.1.26 ) olcAttributeTypes: {6}( 1.3.6.1.4.1.15953.9.1.7 NAME \u0026#39;sudoRunAsGroup\u0026#39; DESC \u0026#39; Group(s) impersonated by sudo\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4. 1.1466.115.121.1.26 ) olcAttributeTypes: {7}( 1.3.6.1.4.1.15953.9.1.8 NAME \u0026#39;sudoNotBefore\u0026#39; DESC \u0026#39;S tart of time interval for which the entry is valid\u0026#39; EQUALITY generalizedTim eMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.12 1.1.24 ) olcAttributeTypes: {8} ( 1.3.6.1.4.1.15953.9.1.9 NAME \u0026#39;sudoNotAfter\u0026#39; DESC \u0026#39;E nd of time interval for which the entry is valid\u0026#39; EQUALITY generalizedTimeM atch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121. 1.24 ) olcAttributeTypes: {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 erOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 ) olcObjectClasses: {0}( 1.3.6.1.4.1.15953.9.2.1 NAME \u0026#39;sudoRole\u0026#39; SUP top STRUC TURAL DESC \u0026#39;Sudoer Entries\u0026#39; MUST ( cn ) MAY ( sudoUser $ sudoHost $ sudoCom mand $ sudoRunAs $ sudoRunAsUser $ sudoRunAsGroup $ sudoOption $ sudoNotBef ore $ sudoNotAfter $ sudoOrder $ description ) ) Now create a new Organizational unit (OU), for example:\ndn: ou=sudoers,dc=example,dc=com ou: sudoers objectClass: top objectClass: 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 objectClass: top objectClass: sudoRole cn: defaults description: Default sudoOption\u0026#39;s for all servers sudoOption: 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 objectClass: top objectClass: sudoRole cn: techwolf12 sudoUser: techwolf12 sudoHost: ALL sudoRunAs: ALL sudoCommand: 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 objectClass: top objectClass: sudoRole cn: %admin sudoUser: %admin sudoHost: ALL sudoRunAs: ALL sudoCommand: 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 objectClass: top objectClass: sudoRole cn: byte sudoUser: byte sudoHost: lycan sudoRunAs: techwolf12 sudoCommand: ALL sudoCommand: !/bin/bash sudoOrder: 1 ","date":"28 May 2023","permalink":"/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":"Also available as RSS Feed.\n","date":null,"permalink":"/tag/security/","section":"Tags","summary":"Also available as RSS Feed.","title":"Security"},{"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] Description=Port forwarding to server After=network.target [Service] ExecStart=/usr/bin/ssh -NT -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -R 2222:localhost:22 user@server RestartSec=5 Restart=always [Install] WantedBy=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":"/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":"/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":"/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":"/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":"/tag/domotica/","section":"Tags","summary":"Also available as RSS Feed.","title":"Domotica"},{"content":"Also available as RSS Feed.\n","date":null,"permalink":"/tag/live-streaming/","section":"Tags","summary":"Also available as RSS Feed.","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 wget https://nginx.org/download/nginx-1.18.0.tar.gz tar -xvzf nginx-1.18.0.tar.gz rm -f nginx-1.18.0.tar.gz mv nginx-1.18.0 nginx cd nginx wget https://github.com/sergey-dryabzhinsky/nginx-rtmp-module/archive/dev.zip unzip 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 make make 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] Description=The NGINX HTTP and reverse proxy server with RTMP After=syslog.target network-online.target remote-fs.target nss-lookup.target Wants=network-online.target [Service] Type=forking PIDFile=/usr/local/nginx/logs/nginx.pid ExecStartPre=/usr/local/nginx/sbin/nginx -t ExecStart=/usr/local/nginx/sbin/nginx ExecReload=/usr/local/nginx/sbin/nginx -s reload ExecStop=/bin/kill -s QUIT $MAINPID PrivateTmp=true [Install] WantedBy=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; error_log logs/rtmp_error.log debug; pid logs/nginx.pid; rtmp { server { listen 1935; chunk_size 4096; # Use a GoPro livestream in other applications application gopro { live on; record off; } # Use a single OBS output for multiple streams application multichannelstream { live on; record off; meta copy; push rtmp://live-ams.twitch.tv/app/\u0026lt;YOUR TWITCH STREAM KEY\u0026gt;; push rtmp://live-ams.twitch.tv/app/\u0026lt;YOUR 2nd TWITCH CHANNEL STREAM KEY\u0026gt;; # 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. allow publish 10.10.0.0/24; deny publish all; allow play all; } # Use a single OBS output for multiple platforms (Facebook, Twitch, Youtube) application multiplatformstream { live on; record off; meta copy; push rtmp://live-ams.twitch.tv/app/\u0026lt;YOUR TWITCH STREAM KEY\u0026gt;; push rtmps://live-api-s.facebook.com:443/rtmp/\u0026lt;YOUR FACEBOOK STREAM KEY\u0026gt;; # Youtube allows a backup server in case their primary ingress server fails. This is why there are two listed here. push rtmp://a.rtmp.youtube.com/live2/\u0026lt;YOUR YOUTUBE STREAM KEY\u0026gt;; push rtmp://b.rtmp.youtube.com/live2/\u0026lt;YOUR YOUTUBE STREAM KEY\u0026gt;?backup=1; } # 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 application \u0026lt;TWITCH CHANNEL\u0026gt; { live on; record off; meta copy; push rtmp://live-ams.twitch.tv/app/\u0026lt;CHANNEL TWITCH STREAM KEY\u0026gt;; } } } events {} http { server { listen 8080; location /stat { rtmp_stat all; rtmp_stat_stylesheet stat.xsl; } location /stat.xsl { root /usr/local/src/nginx/nginx-rtmp-module-dev/stat.xsl; } } } 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":"/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":"/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":"/tag/ham-radio/","section":"Tags","summary":"Ham radio related blog posts \u0026amp; projects are gathered here!","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":"/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":"/tag/networking/","section":"Tags","summary":"Also available as RSS Feed.","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 pub rsa4096 2020-03-26 [C] [expires: 2030-03-24] 015E51CA677A864C61F13D13149B9E17245535D8 uid [ultimate] Test testtt \u0026lt;testtt@techwolf12.nl\u0026gt; sub rsa4096 2020-03-26 [S] [expires: 2023-03-26] sub rsa4096 2020-03-26 [E] [expires: 2023-03-26] sub 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; gpg --armor --output privkey.sec --export-secret-key \u0026lt;YOUR FINGERPRINT\u0026gt; gpg --armor --output subkeys.sec --export-secret-subkeys \u0026lt;YOUR FINGERPRINT\u0026gt; gpg --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 passwd (change admin pin, currently option 3, default is 12345678) 3 (change this to a random, new 8 numeric code) (set the reset code, option 4, use your admin pin) 4 (change this to a random, new 8 numeric code) 1 (change this to a random, new 6 numeric code. Default is 123456 This is the one you will use during signing or encryption) q name (enter your (nick)name) url (enter the https location of your public key file) quit Pushing the subkeys to the smartcard #Execute the following:\ngpg --expert --edit-key \u0026lt;YOUR FINGERPRINT\u0026gt; Then here, do the following:\ntoggle key 1 keytocard 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 key 2 keytocard 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 key 3 keytocard 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 fetch quit 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 pub rsa4096 2020-03-26 [C] [expires: 2030-03-24] 015E 51CA 677A 864C 61F1 3D13 149B 9E17 2455 35D8 uid [ultimate] Test testtt \u0026lt;testtt@techwolf12.nl\u0026gt; sub rsa4096 2020-03-26 [S] [expires: 2023-03-26] 0612 76E3 8F5B 2F4D 095F 95A0 286E 51F1 6D91 53EE sub rsa4096 2020-03-26 [E] [expires: 2023-03-26] D7C6 3D9E 78BE 0932 F844 42DA 60D9 8F16 D50B B177 sub rsa4096 2020-03-26 [A] [expires: 2023-03-26] CD1F 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":"/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":"/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; LDAPVerifyServerCert On LDAPTrustedMode STARTTLS AuthLDAPURL ldap://ldap.example.com/dc=example,dc=com AuthLDAPBindDN \u0026#34;uid=binduser,dc=example,dc=com\u0026#34; AuthLDAPBindPassword \u0026#34;BindPW\u0026#34; AuthName \u0026#34;Protected\u0026#34; AuthType Basic AuthBasicProvider ldap Require valid-user \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; LDAPVerifyServerCert On LDAPTrustedMode STARTTLS AuthLDAPURL ldap://ldap.example.com/dc=example,dc=com AuthLDAPBindDN \u0026#34;uid=binduser,dc=example,dc=com\u0026#34; AuthLDAPBindPassword \u0026#34;BindPW\u0026#34; AuthName \u0026#34;Protected\u0026#34; AuthType Basic AuthBasicProvider ldap Require ldap-attribute memberOf=cn=admins,dc=example,dc=com \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; \u0026lt;RequireAny\u0026gt; Require ip 1.1.1.1 Require ip 2.2.2.2 \u0026lt;/RequireAny\u0026gt; \u0026lt;RequireAny\u0026gt; Require ldap-attribute memberOf=cn=admins,dc=example,dc=com Require ldap-attribute memberOf=cn=operations,dc=example,dc=com \u0026lt;/RequireAny\u0026gt; \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; LDAPVerifyServerCert On LDAPTrustedMode STARTTLS \u0026lt;AuthnProviderAlias ldap ldap1\u0026gt; AuthLDAPURL ldap://ldap.example.com/dc=example,dc=com AuthLDAPBindDN \u0026#34;uid=binduser,dc=example,dc=com\u0026#34; AuthLDAPBindPassword \u0026#34;BindPW\u0026#34; \u0026lt;/AuthnProviderAlias\u0026gt; \u0026lt;AuthnProviderAlias ldap ldap2\u0026gt; AuthLDAPURL ldap://ldap.example2.com/dc=example2,dc=com AuthLDAPBindDN \u0026#34;uid=binduser,dc=example2,dc=com\u0026#34; AuthLDAPBindPassword \u0026#34;BindPW\u0026#34; \u0026lt;/AuthnProviderAlias\u0026gt; AuthName \u0026#34;Protected\u0026#34; AuthType Basic AuthBasicProvider ldap1 ldap2 Require valid-user \u0026lt;/Location\u0026gt; That\u0026rsquo;s all, if there are any questions, feel free to contact me!\n","date":"21 June 2020","permalink":"/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 # Disclaimer! 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. For 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":"/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 objectClass: olcSchemaConfig cn: techwolf12 olcAttributeTypes: {0}( 1.3.6.1.4.1.52389.2.1 NAME \u0026#39;gpgFingerprint\u0026#39; DESC \u0026#39;MA NDATORY: GnuPG Public key\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.14 66.115.121.1.26 ) olcAttributeTypes: {1}( 1.3.6.1.4.1.52389.2.2 NAME \u0026#39;sshPublicKey\u0026#39; DESC \u0026#39;MAN DATORY: OpenSSH Public key\u0026#39; EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.14 66.115.121.1.40 ) olcAttributeTypes: {2}( 1.3.6.1.4.1.52389.2.5 NAME \u0026#39;twoFactorPublic\u0026#39; DESC \u0026#39;M ANDATORY: 2FA Public key\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.146 6.115.121.1.26 ) olcAttributeTypes: {3}( 1.3.6.1.4.1.52389.2.6 NAME \u0026#39;phoneExtNumber\u0026#39; DESC \u0026#39;In ternal PBX extension\u0026#39; SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) olcAttributeTypes: {4}( 1.3.6.1.4.1.52389.2.7 NAME \u0026#39;sshHostAllow\u0026#39; DESC \u0026#39;Opti onal: Allow access to specific ssh hosts\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) olcAttributeTypes: {5}( 1.3.6.1.4.1.52389.2.8 NAME \u0026#39;sshHostDeny\u0026#39; DESC \u0026#39;Optio nal: Denies access to specific ssh hosts\u0026#39; EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) olcAttributeTypes: {6}( 1.3.6.1.4.1.52389.2.9 NAME \u0026#39;userPinCode\u0026#39; DESC \u0026#39;Optio nal: 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 ) olcObjectClasses: {0}( 1.3.6.1.4.1.52389.1.1 NAME \u0026#39;sshUser\u0026#39; SUP top AU XILIARY DESC \u0026#39;MANDATORY: Cryptography objectclass\u0026#39; MAY ( sshPublicKey $ gpg Fingerprint $ 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 objectClass: posixGroup objectClass: top cn: sshldapuser gidNumber: 10000 description: SSH users from LDAP memberUid: 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 objectClass: inetOrgPerson objectClass: sshUser objectClass: organizationalPerson objectClass: person objectClass: posixAccount objectClass: shadowAccount objectClass: top cn: Christiaan de Die le Clercq gidNumber: 10000 homeDirectory: /home/techwolf12 sn: de Die le Clercq uid: techwolf12 uidNumber: 10000 givenName: Christiaan gpgFingerprint: 34B35DD172E366BF6867AB069FB800372F2546D8 loginShell: /bin/bash mail: email@example.com sambaNTPassword: 3BE81268C8E07E150DDF53D6AEE4E0B6 sshPublicKey:: c3NoLXJzYSBBQUFBQjNOemFDMXljMkVBQUFBREFRQUJBQUFCQVFET0VnQzJ2b HlBMzZ1MVNVUkZVS3hYaEdwZHA2c3ViUU10QU01MmV4bldhR0dkdURLZDNwOU9hb0x4cnB4eUtX bzdidGhBWHNRL045N0dETTY0bEIwZStTdkpLdCtrVGllcUZiQ3A3STB2V25zSnZJRWl3WmRjY0d zOU42bnRqU0xUbUl4MFdkaG4vMWF3eENId1FxTHpjYS9yM2w1cnVDRkwzR1FKU3JoeDNNMFdBT0 Q2Yk5OdFQxT0ZDci9tcDhjVGJiYVdTbEhpajJHbjBPak1RWUpmWEpMNWs3QWJwaWJyMUdCN1V3M 0prNXUwazUvdGRNc2twUVZZRFlkaEhRalBJZ3IrREw5ek4wUkpkaWNNLzRRQnl0aGtFYjhRNWs0 Q0U1Vk9PM2VDWXVwYjdrYStLNkJ5dldLemVIL2REdDMvSDRIYVBTOW1JZytXamVKT3NuUTkgUHJ pbWFyeQ== userPassword:: e1NTSEF9eWdsY1FYci9zR25Wem84WmtydFNjeHhDWjdEZjBMVHM4RjErWlE9P Q== 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/ base dc=example,dc=com binddn uid=binduser,dc=example,dc=com bindpw BINDPASSWORD ssl start_tls bind_timelimit 2 network_timeout 2 timeout 2 bind_policy soft tls_cacertdir /etc/ssl/certs TLS_REQCERT allow Create the file with /etc/libnss-ldap.conf with the following content, change the values to yours:\nbase dc=example,dc=com uri ldap://ldap.example.com ldap_version 3 binddn uid=binduser,dc=example,dc=com bindpw BINDPASSWORD ssl start_tls tls_checkpeer no tls_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 AuthorizedKeysCommand /opt/ldap-sshkey.sh %u AuthorizedKeysCommandUser 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":"/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":"/app/privacy-policy/","section":"Apps","summary":"","title":""},{"content":"","date":null,"permalink":"/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----- 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 bWVkaWFjb2xsZWdlQHRlY2h3b2xmMTIubmw+iQIoBDABCgASBQJZmzEYCx0gT2xk IGVtYWlsAAoJEJ+4ADcvJUbY9qoP/iJx7uMxuoAGUai0ff37WWDWjnhzDYZ1L2GO v4BR8pJ01bNfQzO022OQlPXN9cJcVgYy4O7p0QiCDvV8hRaGTkkKccdoGGyeMpRp KddmN0B0gJMvfinlypSGgCAMawujjNhTP3Qcx468mO7wQ+bFv4elNg/MteJg4aBw eFnJ/NU372BiL5aMg3VNx3UK6YgvnLQRlI3BzrTjoiqE7VSHNPJJypRaHUwYa/Uy LmbqTSUQaBUYw3bCFaZNfXe+8b31lIKOJsUhmvhLRpWblHewwKxC2/J6pQ3Su0lr 9hYP9av6eWcbDQG7nKWLjI8pTmSOBVbHrY4ckqZtJKJtvUunOBXHJhNrueN6xtGA S7PDyoID34qWanLnmIFxGgmk94woNMEoq8zfpygYQmoNuN7uvSRIeppVNIAqSvqF +mDTqI3iagsSV9ZmSwFel7R4pq48Yqi/QGBtv0ljw0Ltf+aJ7YXU5Rm4p3dQCpz+ KSTY+tWNpPTh7xQJv5CG0VX+x1fP69dw607z2B0/BUi87v2IsnB8po7KUYAfFWiN 0rSF0SdqtmDvmjq6ao1Ndon6dUUcLXTfW2N0LTnP6IsUfW7mLGE89ckNjjkeg/Pc DphlkI87+Ct6D01U5GMXzuTq/VZh1SNhXVA2FLYBwOC8G1dCW48Yts+t4yh3V99J +0gOWMfEtDlDaHJpc3RpYWFuIGRlIERpZSBsZSBDbGVyY3EgPHRlY2h3b2xmMTJA cGlyYXRlbnBhcnRpai5ubD6JAigEMAEKABIFAlmbMQgLHSBPbGQgZW1haWwACgkQ n7gANy8lRtgecg/+ILiNT4mDaAXVkYmZN57mr+50LySQ87xX65/QkXGv8n9Ni/1k uv2nUc/ubvaqnDHiRpKm77hSoSCM3EzFrrYSKDuT7D4iv+wBLg6Rx4wMQ7xbvB7m bJKfjSwAra6tNBSUBo0f0JcxAIQB3Tj28FmT2T37ZcXCKIee6VI8BFrOnf+0gTMT IVKMU/FjYorXWsJTzE24a1zYN5pp0zuJ2X+zVJdyNxdiIaPzq0/53HVJpbTmGdnh 6O45YPUd04W4Ufatg4og8Sm9C4Rr1rpip+3n/VpbXJ1fsEUdpMMn5LZ5uV543CXw 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 =2rzd -----END PGP PUBLIC KEY BLOCK----- ","date":null,"permalink":"/gpg/","section":"GPG","summary":"See my GPG Key-signing policy.","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] 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] 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":"/gpg/policy/","section":"GPG","summary":"Key Signing Policy for Christiaan de Die le Clercq.","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":"/ham-radio/","section":"Techwolf12 Home","summary":"Hi!","title":"HAM Radio"},{"content":"Below you can see the technologies I use:\n","date":null,"permalink":"/tech-stack/","section":"Techwolf12 Home","summary":"Below you can see the technologies I use:","title":"Tech stack"}]