Commit 5aa7e3a77d

5aa7e3a77d87e4ee0defe2502b471945733cb637

parent: e56343efd4

Unsigned

cmc <hello@cleberg.net> · 2025-12-16 01:36 UTC

remove footer padding

Layout: unified · split

content/blog/all_posts.txt added +24644
@@ -0,0 +1,24644 @@
1#+date: <2018-11-28 Wed 00:00:00>
2#+title: How AES Encryption Works
3#+description: Detailed explanation of AES encryption, including key handling, encryption modes, algorithm structure, and implementation considerations for secure data protection.
4#+slug: aes-encryption
5
6* Basic AES
7
8If you're not familiar with encryption techniques, the Advanced Encryption
9Standard ([[https://en.wikipedia.org/wiki/Advanced_Encryption_Standard][AES]]) is a specification for encryption of electronic data. The
10National Institute of Standards and Technology established this specification,
11sub-selected from the Rijndael family of ciphers (128, 192, and 256 bits)
12in 2001. Furthering its popularity and status, the US government chose AES as
13their default encryption method for top-secret data, removing the previous
14standard which had been in place since 1977.
15
16AES has proven to be a safe encryption method, with 7-round and 8-round attacks
17making no material improvements since the release of this encryption standard
18almost two decades ago.
19
20#+begin_quote
21Though many papers have been published on the cryptanalysis of AES, the fastest
22single-key attacks on round-reduced AES variants [20, 33] so far are only
23slightly more powerful than those proposed 10 years ago [23,24].
24
25- [[http://research.microsoft.com/en-us/projects/cryptanalysis/aesbc.pdf][Bogdonav, et al.]]
26#+end_quote
27
28* How Secure is AES?
29
30In theory, AES-256 (AES using a 256-bit key) is non-crackable due to the massive
31number of combinations that can the encryption process can produce. However,
32AES-128 is no longer recommended as a viable implementation to protect important
33data due the 128-bit key's short length.
34
35A semi-short [[http://www.moserware.com/2009/09/stick-figure-guide-to-advanced.html][comic strip]] from Moserware quickly explains AES for the public to
36understand. Basically, AES encrypts the data by obscuring the relationship
37between the data and the encrypted data. Additionally, this method spreads the
38message out. Lastly, the key produced by AES is the secret to decrypting it.
39Someone may know the method of AES, but without the key, they are powerless.
40
41To obscure and spread the data out, AES creates a substitution-permutation
42network. Wikipedia has a wonderful [[https://upload.wikimedia.org/wikipedia/commons/thumb/c/cd/SubstitutionPermutationNetwork2.png/468px-SubstitutionPermutationNetwork2.png][example of an SP network]] available. This
43network sends the data through a set of S boxes (using the unique key) to
44substitute the bits with another block of bits. Then, a P box will permutate, or
45rearrange, the bits. This is performed numerous times, with the last round
46deriving the key. For AES, the key size specifies the number of transformation
47rounds: 10, 12, and 14 rounds for 128-bit, 192-bit, and 256-bit keys,
48respectively.
49
50* The Process
51
521. *KeyExpansion*: Using [[https://en.m.wikipedia.org/wiki/Advanced_Encryption_Standard][Rijndael's key schedule]], the keys are dynamically
53 generated.
542. *AddRoundKey*: The process combines each byte of the data with the generated
55 key(s) using bitwise xor.
563. *SubBytes*: Next, the process substitutes each byte of data.
574. *ShiftRows*: Then, the process shifts the final three rows a specific number
58 of steps, dictated by the cipher.
595. *MixColumns*: Finally, the process mixes and combines the columns into the
60 final data for this round of processing.
61
62This process does not necessarily stop after one full round. Steps 2 through 5
63will repeat for the number of rounds specified by the key. However, the final
64round excludes the MixColumns step. As you can see, this is a fairly complex
65process. One must have a solid understanding of general mathematic principles to
66fully understand how the sequence works (and to even attempt to find a
67weakness).
68
69According to research done by Bogdanov et al., it would take billions of years
70to brute force a 126-bit key with current hardware. Additionally, this brute
71force attack would require storing 2^{88} bits of data! However, people have
72shown different attacks displaying vulnerabilities with the use of this
73technology. Side-channel attacks use inadvertent leaks of data from the hardware
74or software, which can allow attackers to obtain the key or run programs on a
75user's hardware.
76
77Please note that this is not something you should run out and try to implement
78in your =Hello, World!= application after only hours of research. While AES
79(basically all encryption methods) is efficient in what it does, it takes a lot
80of time and patience to understand. If you're looking for something which
81currently implements AES, check out the [[https://www.bouncycastle.org/documentation.html][Legion of the Bouncy Castle]] for Java
82implementations of cryptographic algorithms.
83
84* Why Does Encryption Matter?
85
86There are limitless reasons to enable encryption at-rest or in-transit for
87aspects of your digital life. You can research specific examples, such as
88[[https://arstechnica.com/tech-policy/2018/12/australia-passes-new-law-to-thwart-strong-encryption/][Australia passes new law to thwart strong encryption]]. However, I will simply
89list basic reasons to always enable encryption, where feasible:
90
911. Privacy is a human right and some countries recognize it as a national right
92 (e.g., [[https://www.law.cornell.edu/wex/fourth_amendment][US Fourth Amendment]]).
932. "Why not?" Encryption rarely affects performance or speed, so there's usually
94 not a reason to avoid it in the first place.
953. Your digital identity and activity (texts, emails, phone calls, online
96 accounts, etc.) are valuable and can result in consequences, such as identity
97 theft, if leaked to other parties. Encrypting this data prevents such leaks
98 from ruining lives.
994. Wiping or factory-resetting does not actually wipe all data from the storage
100 device. There are methods to read data from the physical disks/boards inside
101 devices.
1025. Corporations, governments, and other groups or individuals are actively
103 looking for ways to collect personal information about anyone they can. If
104 someone's data is not encrypted, that person may become a target due to the
105 ease of data collection.
106
107* Read More
108
109- [[http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf][Federal Information Processing Standards Publication 197]]
110#+date: <2018-11-28 Wed 00:00:00>
111#+title: Inside the C++ Compiler
112#+description: Systematic presentation of the C++ compilation phases including preprocessing, compilation, assembly, and linking, with technical insights into each stage's function.
113#+slug: cpp-compiler
114
115* A Brief Introduction
116
117[[https://en.wikipedia.org/wiki/C%2B%2B][C++]] is a general-purpose programming language with object-oriented, generic, and
118functional features in addition to facilities for low-level memory manipulation.
119
120A developer must compile source code, such as the example shown in the snippet
121below, before they can execute the compiled program. There are numerous steps
122and intricacies to the compilation process, and this post was a personal
123exercise to learn and remember as much information as I can.
124
125#+begin_src cpp
126#include <iostream>
127
128int main()
129{
130 std::cout << "Hello, world!\n";
131}
132#+end_src
133
134** Compilation Process
135
136*** An Overview
137
138Compiling C++ projects is a frustrating task most days. Seemingly nonexistent
139errors keeping your program from successfully compiling can be annoying
140(especially since you know you wrote it perfectly the first time, right?).
141
142I'm learning more and more about C++ these days and decided to write this
143concept down so that I can cement it even further in my own head. However, C++
144is not the only compiled language. Check out [[https://en.wikipedia.org/wiki/Compiled_language][the Wikipedia entry for compiled
145languages]] for more examples of compiled languages.
146
147I'll start with a wonderful, graphical way to conceptualize the C++ compiler.
148View [[https://web.archive.org/web/20190419035048/http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html][The C++ Compilation Process]] by Kurt MacMahon, a Northern Illinois
149University (NIU) professor, to see the graphic and an explanation. The goal of
150the compilation process is to take the C++ code and produce a shared library,
151dynamic library, or an executable file.
152
153** Compilation Phases
154
155Let's break down the compilation process. There are four major steps to
156compiling C++ code.
157
158*** Step 1
159
160The first step is to expand the source code file to meet all dependencies. The
161C++ preprocessor includes the code from all the header files, such as =#include
162<iostream>=. Now, what does that mean? The previous example includes the
163=iostream= header. This tells the computer that you want to use the =iostream=
164standard library, which contains classes and functions written in the core
165language. This specific header allows you to manipulate input/output streams.
166After all this, you'll end up which a temporary file that contains the expanded
167source code.
168
169In the example of the C++ code above, the =iostream= class would be included in
170the expanded code.
171
172*** Step 2
173
174After the compiler expands the code, the compiler comes into play. The compiler
175takes the C++ code and converts this code into the assembly language, understood
176by the platform. You can see this in action if you head over to the [[https://godbolt.org][GodBolt
177Compiler Explorer]], which shows the compiled converting C++ into assembly
178dynamically.
179
180For example, the =Hello, world!= code snippet above compiles into the following
181assembly code:
182
183#+begin_src asm
184.LC0:
185 .string "Hello, world!\n"
186main:
187 push rbp
188 mov rbp, rsp
189 mov esi, OFFSET FLAT:.LC0
190 mov edi, OFFSET FLAT:_ZSt4cout
191 call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
192 mov eax, 0
193 pop rbp
194 ret
195__static_initialization_and_destruction_0(int, int):
196 push rbp
197 mov rbp, rsp
198 sub rsp, 16
199 mov DWORD PTR [rbp-4], edi
200 mov DWORD PTR [rbp-8], esi
201 cmp DWORD PTR [rbp-4], 1
202 jne .L5
203 cmp DWORD PTR [rbp-8], 65535
204 jne .L5
205 mov edi, OFFSET FLAT:_ZStL8__ioinit
206 call std::ios_base::Init::Init() [complete object constructor]
207 mov edx, OFFSET FLAT:__dso_handle
208 mov esi, OFFSET FLAT:_ZStL8__ioinit
209 mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
210 call __cxa_atexit
211.L5:
212 nop
213 leave
214 ret
215_GLOBAL__sub_I_main:
216 push rbp
217 mov rbp, rsp
218 mov esi, 65535
219 mov edi, 1
220 call __static_initialization_and_destruction_0(int, int)
221 pop rbp
222 ret
223#+end_src
224
225*** Step 3
226
227Third, the compiled assembles the assembly code into the object code for the
228platform. Essentially, this is when the compiler takes the assembly code and
229assembles it into machine code in a binary format. After researching this
230online, I figured out that a lot of compilers will allow you to stop compilation
231at this step. This would be useful for compiling each source code file
232separately. This saves time later if a single file changes, since the developer
233will only need to re-compile single file.
234
235*** Step 4
236
237Finally, the compiler links the object code file generated by the assembler
238together with the object code files for any library functions used to produce a
239shared library, dynamic library, or an executable file. It replaces all
240references to undefined symbols with the correct addresses.
241#+date: <2019-01-07 Mon 00:00:00>
242#+title: Real‑World CSS Techniques: Flexbox, Shadows, and Variables
243#+description: Practical guide on applying CSS rules and constructs such as flexbox, box shadows, and variable usage to achieve standard web styling and layout objectives.
244#+slug: useful-css
245
246* Introduction to CSS
247
248Cascading Style Sheets ([[https://en.wikipedia.org/wiki/CSS][CSS]]), the language used to mark up HTML code and give it
249a visual style, is one of the most effective ways to increase the attractiveness
250of a website. It can also lead to increased user engagement, retention, and
251satisfaction. In fact, there are whole career fields dedicated to the
252improvement of user experiences, known as User Interface (UI) design and User
253Experience (UX) design.
254
255Some web developers are familiar with the common CSS properties, such as element
256sizing, fonts, and colors, but are not as well versed in less-used properties
257and values such as =flexbox=, =clip-path=, and =transform=. This article will
258provide some insight into the less-used and unique CSS properties.
259
260* CSS Variables
261
262The first topic today is CSS variables. Variables are not often used by smaller
263developers. CSS variables allow you to give your website a well-defined
264structure, where you can easily reuse CSS properties throughout the project.
265
266You can use variables to define things, such as color palettes. Then, you can
267use these colors for backgrounds anywhere else in the HTML. You can expand this
268and define variables for =primary-text=, =quoted-text=, etc. You can also use
269variables to define spacing (e.g. =32px= or =2rem=), which you can then apply to
270margins, padding, font sizes, and more.
271
272For example, here are some variables defined at the root of the website, which
273allows for any subsequent CSS rules to use those variables:
274
275#+begin_src css
276:root {
277 --primary-color: black;
278 --secondary-color: white;
279}
280
281body {
282 background-color: var(--primary-color);
283 color: var(--secondary-color);
284}
285#+end_src
286
287* CSS Box Shadows
288
289Box shadows were once my mortal enemy. No matter how hard I tried, I just
290couldn't get them to work how I wanted. Because of this, my favorite discovery
291has been CSSMatic's [[https://www.cssmatic.com/box-shadow][box shadow generator]]. It provides a tool to generate box
292shadows using their simple sliders. Surprisingly, this is the reason I learned
293how box shadows work! You can use the sliders and watch how the CSS code changes
294in the image that the tool displays. Through this, you should understand that
295the basic structure for box shadows is:
296
297#+begin_src css
298box-shadow: inset horizontal vertical blur spread color;
299#+end_src
300
301Now, let's look at some basic examples! You can copy and paste the following
302code into a site like CodePen or your own HTML files. Feel free to play around
303with the code, experiment, and learn.
304
305*Box Shadow #1*
306
307#+begin_src html
308<div class="shadow-examples">
309 <div class="box effect1">
310 <h3>Effect 1</h3>
311 </div>
312</div>
313#+end_src
314
315#+begin_src css
316.box h3 {
317 text-align: center;
318 position: relative;
319 top: 80px;
320}
321.box {
322 width: 70%;
323 height: 200px;
324 background: #fff;
325 margin: 40px auto;
326}
327.effect1 {
328 box-shadow: 0 10px 6px -6px #777;
329}
330#+end_src
331
332*Box Shadow #2*
333
334#+begin_src html
335<div class="shadow-examples">
336 <div class="box effect2">
337 <h3>Effect 2</h3>
338 </div>
339</div>
340#+end_src
341
342#+begin_src css
343.box h3 {
344 text-align: center;
345 position: relative;
346 top: 80px;
347}
348.box {
349 width: 70%;
350 height: 200px;
351 background: #fff;
352 margin: 40px auto;
353}
354.effect2 {
355 box-shadow: 10px 10px 5px -5px rgba(0, 0, 0, 0.75);
356}
357#+end_src
358
359Try these box shadows out on your own and see how changing each shadow value
360works.
361
362* CSS Flex Box
363
364Now, let's move on to the best part of this article: flex box. The flex box is by
365far my favorite new toy. I originally stumbled across this solution after
366looking for more efficient ways of centering content horizontally AND
367vertically. I had used other methods before, but flex box throws those out the
368window. The best part of it all is that flex box is /dead simple/.
369
370Flex Box pertains to the parent div of any element. You want the parent to be the
371flex box and the children's behavior modified by the parent's attributes. It's
372easier to see this in action that explained, so let's see an example.
373
374*Flex Box*
375
376#+begin_src html
377<div class="flex-examples">
378 <div class="sm-box">
379 <h3>1</h3>
380 </div>
381 <div class="sm-box">
382 <h3>2</h3>
383 </div>
384</div>
385#+end_src
386
387#+begin_src css
388.flex-examples {
389 display: flex;
390 flex-wrap: wrap;
391 justify-content: flex-start;
392 align-items: center;
393 padding: 10px;
394 background-color: #f2f2f2;
395}
396.sm-box {
397 display: flex;
398 justify-content: center;
399 align-items: center;
400 width: 20%;
401 height: 100px;
402 background: #fff;
403 margin: 40px 10px;
404}
405#+end_src
406
407You may notice that we no longer need to use the =top= property for the =h3=
408elements in our code. This is because we set the display box to be a flex
409container for the small boxes, AND we made the small boxes flex containers for
410their elements (the h3 tags). You can nest flex boxes to center content that is
411inside centered content.
412
413For the example above, we designated the =justify-content= property to be
414=flex-start= so that the boxes stack from the left side of the screen. You can
415change this property to =center= to make the boxes appear in the center of the
416screen.
417
418For an interactive example, [[https://codepen.io/LandonSchropp/pen/KpzzGo][check out this CodePen]] from [[https://codepen.io/LandonSchropp/][LandonScropp]]. Resize the
419window with dice to see how they collapse and re-align.
420
421* Even More CSS
422
423For more inspiration, you can visit [[https://www.codepen.io][CodePen]], [[https://dribbble.com][Dribbble]], or [[https://uimovement.com][UI Movement]] to browse
424the collections of amazing web designers.
425#+date: <2019-09-09 Mon 00:00:00>
426#+title: Bringing Data Analytics Into Internal Audit
427#+description: Examination of data analytical approaches applied within internal audit functions to improve risk evaluation, process efficiency, and decision support.
428#+slug: audit-analytics
429
430* What Are Data Analytics?
431
432A quick aside before I dive into this post: =data analytics= is a vague term
433that has become popular in recent years. Think of a =data analytic= as the
434output of any data analysis you perform. For example, a pivot table or a pie
435chart could be a data analytic.
436
437[[https://en.wikipedia.org/wiki/Data_analysis][Data analysis]] is a process that utilizes statistics and other mathematical
438methods to discover useful information within datasets. This involves examining,
439cleaning, transforming, and modeling data so that you can use the data to
440support an opinion, create more useful viewpoints, and gain knowledge to
441implement into audit planning or risk assessments.
442
443One of the common mistakes that managers (and anyone new to the process) make is
444assuming that everything involved with this process is "data analytics". In
445fact, data analytics are only a small part of the process.
446
447See *Figure 1* for a more accurate representation of where data analysis sits
448within the full process. This means that data analysis does not include querying
449or extracting data, selecting samples, or performing audit tests. These steps
450can be necessary for an audit (and the same associates may perform these steps),
451but they are not data analytics.
452
453* Current Use of Analytics in Auditing
454
455While data analysis has been an integral part of most businesses and departments
456for the better part of the last century, only recently have internal audit
457functions been adopting this practice. The internal audit function works
458exclusively to provide assurance and consulting services to the business areas
459within the firm (except for internal auditing firms hired by different companies
460to perform their roles).
461
462#+begin_quote
463Internal Auditing helps an organization accomplish its objectives by bringing a
464systematic, disciplined approach to evaluate and improve the effectiveness of
465risk management, control and governance processes.
466
467- The IIA's Definition of Internal Audit
468#+end_quote
469
470Part of the blame for the slow adoption of data analysis can be attributed to
471the fact that internal auditing is strongly based on tradition and following the
472precedents set by previous auditors. However, there can be no progress without
473auditors who are willing to break the mold and test new audit techniques. In
474fact, as of 2018, [[https://www.cpapracticeadvisor.com/accounting-audit/news/12404086/internal-audit-groups-are-lagging-in-data-analytics][only 63% of internal audit departments currently utilize data
475analytics]] in North America. This number should be as close as possible to 100%.
476I have never been part of an audit that would not have benefited from data
477analytics.
478
479So, how do internal audit functions remedy this situation? It's definitely not
480as easy as walking into work on Monday and telling your Chief Audit Executive
481that you're going to start implementing analytics in the next audit. You need a
482plan and a system to make the analysis process as effective as possible.
483
484* The DELTA Model
485
486One of the easiest ways to experiment with data analytics and gain an
487understanding of the processes is to implement them within your own department.
488But how do we do this if we've never worked with analysis before? One of the
489most common places to start is to research some data analysis models currently
490available. For this post, we'll take a look at the DELTA model. You can take a
491look at *Figure 2* for a quick overview of the model.
492
493The DELTA model sets guidelines for areas wanting to implement data analytics so
494that the results can be as comprehensive as possible:
495
496- *Data*: Must be clean, accessible, and (usually) unique.
497- *Enterprise-Wide Focus*: Key data systems and analytical resources must be
498 available for use (by the Internal Audit Function).
499- *Leaders*: Must promote a data analytics approach and show the value of
500 analytical results.
501- *Targets*: Must be defined for key areas and risks that the analytics can be
502 compared against (KPIs).
503- *Analysts*: There must be auditors willing and able to perform data analytics
504 or else the system cannot be sustained.
505
506* Finding the Proper KPIs
507
508Once the Internal Audit Function has decided that they want to start using data
509analytics internally and have ensured they're properly set up to do so, they
510need to figure out what they will be testing against. Key Performance Indicators
511(KPIs) are qualitative or quantitative factors that can be evaluated and
512assessed to determine if the department is performing well, usually compared to
513historical or industry benchmarks. Once KPIs have been agreed upon and set,
514auditors can use data analytics to assess and report on these KPIs. This allows
515the person performing the analytics the freedom to express opinions on the
516results, whereas the results are ambiguous if no KPIs exist.
517
518Note that tracking KPIs in the department can help ensure you have a rigorous
519Quality Assurance and Improvement Program (QAIP) in accordance with some
520applicable standards, such as IPPF Standard 1300.
521
522#+begin_quote
523The chief audit executive must develop and maintain a quality assurance and
524improvement program that covers all aspects of the internal audit activity.
525
526- IPPF Standard 1300
527#+end_quote
528
529Additionally, IPPF Standard 2060 discusses reporting:
530
531#+begin_quote
532The chief audit executive must report periodically to senior management and the
533board on the internal audit activity's purpose, authority, responsibility, and
534performance relative to its plan and on its conformance with the Code of Ethics
535and the Standards. Reporting must also include significant risk and control
536issues, including fraud risks, governance issues, and other matters that require
537the attention of senior management and/or the board.
538
539- IPPF Standard 2060
540#+end_quote
541
542The hardest part of finding KPIs is to determine which KPIs are appropriate for
543your department. Since every department is different and has different goals,
544KPIs will vary drastically between companies. To give you an idea of where to
545look, here are some ideas I came up with when discussing the topic with a few
546colleagues.
547
548- Efficiency/Budgeting:
549 - Audit hours to staff utilization ratio (annual hours divided by total annual
550 work hours).
551 - Audit hours compared to the number of audits completed.
552 - Time between audit steps or to complete the whole audit. E.g., time from
553 fieldwork completion to audit report issuance.
554- Reputation:
555 - The frequency that management has requested the services of the IAF.
556 - Management, audit committee, or external audit satisfaction survey results.
557 - Education, experience, certifications, tenure, and training of the auditors
558 on staff.
559- Quality:
560 - Number and frequency of audit findings. Assign monetary or numerical values,
561 if possible.
562 - Percentage of recommendations issued and implemented.
563- Planning:
564 - Percentage or number of key risks audited per year or per audit.
565 - Proportion of audit universe audited per year.
566
567* Data Analysis Tools
568
569Finally, to be able to analyze and report on the data analysis, auditors need to
570evaluate the tools at their disposal. There are options available, but the most
571common ones can easily get the job done. For example, almost every auditor
572already has access to Microsoft Excel. Excel is more powerful than most people
573give it credit for and can accomplish a lot of basic statistics without much
574work. If you don't know a lot about statistics but still want to see some of the
575more basic results, Excel is a great option.
576
577To perform more in-depth statistical analysis or to explore large datasets that
578Excel cannot handle, auditors will need to explore other options. The big three
579that have had a lot of success in recent years are Python, R, and ACL Analytics
580(ACL). You can use ACL as either a graphical tool (point and click) or as a
581scripting tool, where the auditor must write the scripts manually. Python and
582the R-language are solely scripting languages.
583
584The general trend in the data analytics environment is that if the tool allows
585you to do everything by clicking buttons or dragging elements, you won't be able
586to fully utilize the analytics you need. To create the most robust solutions,
587you should write the scripts manually. Note that as the utility of a tool
588increases, it usually means that the learning curve for that tool will also be
589higher. It will take auditors longer to learn how to utilize Python, R, or ACL
590versus learning how to utilize Excel.
591
592* Visualization
593
594Once an auditor has finally found the right data, KPIs, and tools, they must
595report these results so that readers of their results can take action.
596Performing in-depth data analysis is only useful if the audience can understand
597the results. The best way to create this understanding is to visualize the
598results of the data. Let's take a look at some of the best options to visualize
599and report the results you've found.
600
601Some of the most popular commercial tools for visualization are Microsoft
602PowerBI and Tableau Desktop. However, other tools exist such as JMP, Plotly,
603Qlikview, Alteryx, or D3. Some require commercial licenses while others are
604simply free to use. For corporate data, you may want to make sure that the tool
605does not communicate any of the data outside the company (such as cloud
606storage). I won't be going into depth on any of these tools since visualization
607is a subjective and creative experience, but remember to constantly explore new
608options as you repeat the process.
609
610Lastly, let's take a look at an example of data visualization. This example
611comes from a [[https://talent.works/2018/03/28/the-science-of-the-job-search-part-iii-61-of-entry-level-jobs-require-3-years-of-experience/][blog post written by Kushal Chakrabarti]] in 2018 about the percent
612of entry-level US jobs that require experience. *Figure 3* shows us an
613easy-to-digest picture of the data. We can quickly tell that only about 12.5% of
614entry-level jobs don't require experience.
615
616This is the kind of result that easily describes the data for you. However, make
617sure to include an explanation of what the results mean. Don't let the reader
618assume what the data means, especially if it relates to a complex subject. /Tell
619a story/ about the data and why the results matter.
620
621* Wrap-Up
622
623While this is not an all-encompassing program that you can just adopt into your
624department, it should be enough to get anyone started on the process of
625understanding and implementing data analytics. Always remember to continue
626learning and exploring new options as your processes grow and evolve.
627#+date: <2019-12-03 Tue 00:00:00>
628#+title: How the Ansoff Matrix Guides Business Growth
629#+description: Analytical description of the Ansoff Matrix, detailing market and product development strategies for measured business expansion and competitive positioning.
630#+slug: the-ansoff-matrix
631
632* Overview
633
634As the world of business evolves, managers must approach business planning and
635strategy with a contemporary mindset. According to Dess, McNamara, Eisner, and
636Lee, managers must be willing to adapt to the modern business environment by
637going beyond "'incremental management', whereby they view their job as making a
638series of small, minor changes to improve the efficiency of the firm's
639operations"(2019).
640
641One reason that strategic management is crucial is because most businesses that
642fail in the United States each year fail due to a lack of strategic focus or
643direction(2019). The rate of failure for businesses with poor strategies shows
644that strategic planning and management are crucial to a business's strength and
645longevity, injecting the critical factors of growth and direction into a
646company's business plan.
647
648One of the most significant strategic planning and management frameworks that
649companies can use is the [[https://en.wikipedia.org/wiki/Ansoff_matrix][Ansoff Matrix]]. While this framework has unique purposes
650and use-cases, it can effectively help an organization grow and compete.
651Specifically, the Ansoff matrix is one of the most effective frameworks for
652companies who want to focus on increasing sales revenue or profitability(2019).
653
654This framework uses a two-by-two figure to show the four strategic options for
655companies to use in this framework: market penetration, market development,
656product development, and diversification (see *Figure 1*). The x-axis of the
657matrix focuses on the firm's markets and also determines if the firm is looking
658to enter new markets or innovate in its current markets. The y-axis of the
659matrix focuses on the firm's products and determines if the firm wants to pursue
660strategies around their existing products or explore new products.
661
662* Strategic Options
663
664** Market Penetration
665
666The most straightforward strategy in the Ansoff matrix is to focus on existing
667products in existing markets, also known as market penetration(2019). Companies
668such as Coca-Cola have used market penetration successfully by investing a lot
669of money to get further value out of their current markets. Coca-Cola does this
670by introducing new features such as Christmas-themed bottles, personal names on
671the bottles, and other marketing schemes.
672
673** Market Development
674
675Market development extends existing products into new markets in an attempt to
676increase the number of buyers. One interesting way that Coca-Cola used this
677strategy comes from the stigma that Diet Coke is a woman's drink(2019).
678Coca-Cola introduced Coca-Cola Zero, which contained the same nutritional
679content as Diet Coke, but the company packaged it in a dark black can to appear
680more "manly" (2019).
681
682** Product Development
683
684Product development uses existing markets to introduce new products so that the
685firm can better meet customer needs(2019). The extreme end of diversification is
686home to companies such as Johnson & Johnson, a healthcare company that has
687developed a business portfolio of more than 60,000 different products(2019).
688Johnson & Johnson's dedication to continuous diversification has led them to a
689balance sheet rating of "AAA", industry recognition for diversification, and
690increases in their investor dividends for 57 consecutive years(2019).
691
692** Related Diversification
693
694Diversification, the final strategy of the Ansoff Matrix, is more difficult than
695the others since it involves exploring both new markets and new products.
696Related diversification is a diversification strategy that closely relates to
697the firm's core business. Coca-Cola's best example of related diversification is
698its acquisition of Glaceau and Vitamin Water, which expanded their drinking
699lines of business(2019).
700
701** Unrelated Diversification
702
703Unrelated diversification is a diversification strategy that does not really
704relate to the firm's core business but still diversifies their business
705portfolio. A good example of this would be a coffee company who has decided to
706enter the market for bicycle sales. The main purpose of this strategy is to a
707diverse company that will not go bankrupt if one market goes through difficult
708times. However, this requires a lot of independent skills and heavy investments
709since the company most likely cannot easily transfer knowledge between the
710markets they compete in.
711
712* Requirements for Success
713
714To use the Ansoff Matrix framework, managers need to formulate corporate goals
715and objectives. Without goals and direction, management frameworks do not
716present much practical utility. Further, the Ansoff Matrix requires the managers
717involved to make tactical decisions and create a path for the company to take
718toward their goals. Lastly, both the Ansoff Matrix needs to consider both
719internal and external perspectives throughout the strategy formulation process.
720
721One interesting probability is that companies will be using multiple strategic
722planning and management frameworks at the same time. While this may sound like
723it could crowd the management process, there are numerous reasons to do so. For
724example, the Ansoff Matrix and the Balanced Scorecard are popular, and they
725cover entirely different parts of a company's strategy. Using the results from
726the Balanced Scorecard could inform a company of the potential product and
727market demands, such as from customer or supplier survey results, to help the
728company determine which Ansoff Matrix strategy to pursue. However, a combined
729approach at this level would require mature frameworks and focused managers who
730are able to strategize at a high level.
731
732Lastly, please note that the author of the Ansoff matrix, Igor Ansoff,
733often used the term [[https://en.wikipedia.org/wiki/Analysis_paralysis][paralysis by analysis]] to explain the mistake of companies
734who overuse analysis and spend too much time planning. Companies need to
735understand the utility of a strategic management framework while ensuring that
736the company is poised to execute as efficiently as they have planned.
737#+date: <2019-12-16 Mon 00:00:00>
738#+title: How to Secure Passwords: Best Practices and NIST Guidelines
739#+description: Compilation of guidelines for password generation, storage, protection, and compliance with established standards such as NIST for maintaining digital security.
740#+slug: password-security
741
742* Users
743
744** Why Does It Matter?
745
746Information security, including passwords and identities, has become one of the
747most important digital highlights of the last decade. With [[https://www.usatoday.com/story/money/2018/12/28/data-breaches-2018-billions-hit-growing-number-cyberattacks/2413411002/][billions of people
748affected by data breaches each year]], there's a greater need to introduce strong
749information security systems. If you think you've been part of a breach, or you
750want to check and see, you can use [[https://haveibeenpwned.com/][Have I Been Pwned]] to see if any public
751breaches have exposed your email(s). Remember that there's a possibility that a
752company experienced a breach and did not report it to anyone.
753
754** How Do I Protect Myself?
755
756The first place to start with any personal security check-up is to gather a list
757of all the different websites, apps, or programs that require you to have login
758credentials. Optionally, once you know where you are storing your information,
759you can sort the list from the most-important items such as banks or government
760logins to less important items such as your favorite meme site. You will want to
761ensure that your critical logins are secure before getting to the others.
762
763Once you think you have a good idea of all your different authentication
764methods, I recommend using a password manager such as [[https://bitwarden.com/][Bitwarden]]. Using a
765password manager allows you to automatically save your logins, create randomized
766passwords, and transfer passwords across devices. However, you'll need to
767memorize your "vault password" that allows you to open the password manager.
768It's important to make this something hard to guess since it would allow anyone
769who has it to access every password you've stored in there.
770
771Personally, I recommend using a [[https://en.wikipedia.org/wiki/Passphrase][passphrase]] instead of a [[https://en.wikipedia.org/wiki/Password][password]] for your vault
772password. Instead of using a string of characters (whether random or simple),
773use a phrase and add in symbols and a number. For example, your vault password
774could be =Racing-Alphabet-Gourd-Parrot3=. Swap the symbols out for whichever
775symbol you want, move the number around, and fine-tune the pass phrase until you
776are confident that you can remember it whenever necessary.
777
778Once you've stored your passwords, make sure you continually check up on your
779account and make sure you aren't following bad password practices. Krebs on
780Security has a great [[https://krebsonsecurity.com/password-dos-and-donts/][blog post on password recommendations]]. Any time that a data
781breach happens, make sure you check to see if the breach exposed your email, and
782if you need to reset any account passwords.
783
784* Developers
785
786** What Are the Basic Requirements?
787
788When developing any password-protected application, there are basic rules that
789anyone should follow even if they do not follow any official guidelines such as
790NIST. The foremost practice is to require users to use passwords that are at
791least 8 characters and bad actors cannot easily guess them. This sounds simple,
792but it requires different strategies. First, the application should check the
793potential passwords against a dictionary of insecure passwords such =password=,
794=1234abc=, or =application_name=.
795
796Next, the application should offer guidance on the strength of passwords you
797enter during enrollment. Further, NIST officially recommends *not* implementing
798any composition rules that make passwords hard to remember (e.g. passwords with
799letters, numbers, and special characters) and instead encouraging the use of
800long pass phrases which can include spaces. Note that to be able to keep spaces
801within passwords, you should support all unicode characters, and you should not
802truncate spaces.
803
804** What Does NIST Recommend?
805
806The National Institute of Standards and Technology ([[https://www.nist.gov][NIST]]) in the US Department
807of Commerce regularly publishes information around information security and
808digital identity guidelines. Recently, NIST published [[https://pages.nist.gov/800-63-3/sp800-63b.html][Special Publication
809800-63b]]: Digital Identity Guidelines and Authentication and Lifecycle
810Management.
811
812#+begin_quote
813A Memorized Secret authenticator - commonly referred to as a password or, if
814numeric, a PIN - is a secret value intended to be chosen and memorized by the
815user. Memorized secrets need to be of sufficient complexity and secrecy that it
816would be impractical for an attacker to guess or otherwise discover the correct
817secret value. A memorized secret is something you know.
818
819- NIST Special Publication 800-63B
820#+end_quote
821
822NIST offers a lot of guidance on passwords, but I'm going to highlight just a
823few of the important factors:
824
825- Require passwords to be a minimum of 8 characters (6 characters if randomly
826 generated and generate using an approved random bit generator).
827- Compare potential passwords against a list that contains values known to be
828 commonly-used, expected, or compromised.
829- Offer guidance on password strength, such as a strength meter.
830- Implement a rate-limiting mechanism to limit the number of failed
831 authentication attempts for each user account.
832- Do not require composition rules for passwords and do not require users to
833 change their passwords periodically (unless compromised).
834- Allow pasting of user identification and passwords to facilitate the use of
835 password managers.
836- Allow users to view the password as they type.
837- Use secure forms of communication and storage, including salting and hashing
838 passwords using a one-way key derivation function.
839
840NIST offers further guidance on other devices that require specific security
841policies, querying for passwords, and more. All the information discussed so far
842comes from [[https://pages.nist.gov/800-63-3/sp800-63b.html][NIST SP800-63b]] but NIST offers a lot of information on digital
843identities, enrollment, identity proofing, authentication, lifecycle management,
844federation, and assertions in the total [[https://pages.nist.gov/800-63-3/][NIST SP800-63 Digital Identity
845Guidelines]].
846#+date: <2020-01-25 Sat 00:00:00>
847#+title: Linux Essentials: Top Tools and Easy Setup Tips
848#+description: Inventory and description of key Linux applications including graphical and command-line tools, with installation instructions for various distributions.
849#+slug: linux-software
850
851* Graphical Applications
852
853** Etcher
854
855[[https://www.balena.io/etcher/][Etcher]] is a quick and easy way to burn an optical disc image (ISO) to a compact
856disc (CD) or universal serial bus (USB) device. There are two different ways you
857can install this program. First, you can navigate to the [[https://www.balena.io/etcher/][official website]] and
858download the AppImage file, which can run without installation.
859
860However, AppImage files are not executable by default, so you'll either need to
861right-click to open the properties of the file and click the "Allow executing
862file as program" box in the Permissions tab or use the following command:
863
864#+begin_src sh
865chmod u+x FILE_NAME
866#+end_src
867
868If you don't like AppImage files or just prefer repositories, you can use the
869following commands to add the author's repository and install it through the
870command-line only.
871
872First, you'll have to write the repository to a list file:
873
874#+begin_src sh
875echo "deb https://deb.etcher.iostable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list
876#+end_src
877
878Next, add the application keys to Ubuntu's key ring:
879
880#+begin_src sh
881sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 379CE192D401AB61
882#+end_src
883
884Finally, update the repositories and install the app.
885
886#+begin_src sh
887sudo apt update && sudo apt install balena-etcher-electron
888#+end_src
889
890Using Arch, Manjaro, or another distribution using the Arch User Repository
891(AUR)? Use this command instead:
892
893#+begin_src sh
894sudo pacman -S etcher
895#+end_src
896
897** Atom
898
899[[https://atom.io][Atom]] is the self-proclaimed "hackable text editor for the 21st century". GitHub
900develops this editor, [[https://news.microsoft.com/2018/06/04/microsoft-to-acquire-github-for-7-5-billion/][now owned by Microsoft]], and has some of the best add-ons
901available to customize the layout and abilities of the app.
902
903First, add the Atom repository to your sources.
904
905#+begin_src sh
906sudo add-apt-repository ppa:webupd8team/atom
907#+end_src
908
909Next, update your package listings and install atom.
910
911#+begin_src sh
912sudo apt update && sudo apt install atom
913#+end_src
914
915If you have issues updating your packages with the Atom repository, you'll need
916to use the snap package described below instead of the repository. To remove the
917repository we just added, use this command:
918
919#+begin_src sh
920sudo add-apt-repository -r ppa:webupd8team/atom
921#+end_src
922
923You can also install Atom as a snap package, but you must install it with the
924=--classic= flag. A [[https://language-bash.com/blog/how-to-snap-introducing-classic-confinement][full explanation is available]] if you'd like to read more
925about why you need the classic flag.
926
927#+begin_src sh
928snap install atom --classic
929#+end_src
930
931Using Arch, Manjaro, or another distro using the AUR? Use this command instead:
932
933#+begin_src sh
934sudo pacman -S atom
935#+end_src
936
937** Visual Studio Code
938
939[[https://code.visualstudio.com][Visual Studio Code]] (VS Code) is yet another fantastic choice for programming on
940Linux, especially if you need those extra add-ons to spice up your late-night
941coding sessions. The theme used in the screenshot is [[https://marketplace.visualstudio.com/items?itemName=EliverLara.mars][Mars]] by theme creator
942[[https://github.com/EliverLara][Eliver Lara]], who makes a ton of great themes for VS Code, Atom, and Linux
943desktop environments.
944
945To install VS Code, you'll need to download the =.deb= file from the official
946website. Once you've downloaded the file, either double-click it to install
947through the Software Center or run the following command:
948
949#+begin_src sh
950sudo dpkg -i FILE_NAME.deb
951#+end_src
952
953You can also install VS Code as a snap package, but you must install it with the
954=--classic= flag. A [[https://language-bash.com/blog/how-to-snap-introducing-classic-confinement][full explanation is available]] if you'd like to read more
955about why you need the classic flag.
956
957#+begin_src sh
958snap install code --classic
959#+end_src
960
961Using Arch, Manjaro, or another distro using the AUR? Use these commands
962instead:
963
964#+begin_src sh
965sudo pacman -S yay binutils make gcc pkg-config fakeroot yay -S visual-studio-code-bin
966#+end_src
967
968** GNOME Tweaks
969
970[[https://gitlab.gnome.org/GNOME/gnome-tweaks][Gnome Tweaks]] is the ultimate tool to use if you want to customize your GNOME
971desktop environment. This is how you can switch application themes (GTK), shell
972themes, icons, fonts, and more. To install GNOME Tweaks on Ubuntu, you just need
973to install the official package.
974
975#+begin_src sh
976sudo apt install gnome-tweaks
977#+end_src
978
979If you've installed Manjaro or Arch with Gnome, you should have the tweak tool
980pre-installed. If you're on Fedora, this tool is available as an official
981package:
982
983#+begin_src sh
984sudo dnf install gnome-tweaks
985#+end_src
986
987** Steam
988
989[[https://steampowered.com][Steam]] is one of the most popular gaming libraries for computers and is one of
990the main reasons that people have been able to switch to Linux in recent years,
991thanks to Steam Proton, which makes it easier to play games not officially
992created for Linux platforms.
993
994To install Steam on Ubuntu, you just need to install the official package.
995
996#+begin_src sh
997sudo apt install steam-installer
998#+end_src
999
1000For Arch-based systems, you'll simply need to install the =steam=
1001package. However, this requires that you enable the =multilib= source.
1002To do so, use the following command:
1003
1004#+begin_src sh
1005sudo nano /etc/pacman.conf
1006#+end_src
1007
1008Now, scroll down and uncomment the =multilib= section.
1009
1010#+begin_src config
1011# Before:
1012#[multilib]
1013#Include = /etc/pacman.d/mirrorlist
1014
1015# After:
1016[multilib]
1017Include = /etc/pacman.d/mirrorlist
1018#+end_src
1019
1020Finally, install the program:
1021
1022#+begin_src sh
1023sudo pacman -S steam
1024#+end_src
1025
1026[[./2020-01-26-steam-on-ntfs-drives.html][Problem Launching Steam Games? Click Here.]]
1027
1028* Command-Line Packages
1029
1030** neofetch
1031
1032[[https://github.com/dylanaraps/neofetch][Neofetch]] is a customizable tool used in the command-line to show system
1033information. This is exceptionally useful if you want to see your system's
1034information quickly without the clutter of some resource-heavy GUI apps.
1035
1036This is an official package if you're running Ubuntu 17.04 or later, so simply
1037use the following command:
1038
1039#+begin_src sh
1040sudo apt install neofetch
1041#+end_src
1042
1043If you're running Ubuntu 16.10 or earlier, you'll have to use a series of
1044commands:
1045
1046#+begin_src sh
1047sudo add-apt-repository ppa:dawidd0811/neofetch; sudo apt update; sudo apt
1048install neofetch
1049#+end_src
1050
1051Using Arch, Manjaro, or another distro using the AUR? Use this command instead:
1052
1053#+begin_src sh
1054sudo pacman -S neofetch
1055#+end_src
1056
1057** yt-dlp
1058
1059[[https://github.com/yt-dlp/yt-dlp][yt-dlp]] is a handy command-line tool that allows you to download video or audio
1060files from websites, such as YouTube. There are a ton of different options when
1061running this package, so be sure to run =yt-dlp --help= first to look through
1062everything you can do (or give up and search for the best configuration online).
1063
1064While this shouldn't be a problem for most users, yt-dlp requires Python 2.6,
10652.7, or 3.2+ to work correctly, so install Python if you don't have it already.
1066You can check to see if you have Python installed by running:
1067
1068#+begin_src sh
1069python -V
1070#+end_src
1071
1072To get the youtube-dl package, simply curl the URL and output the results.
1073
1074#+begin_src sh
1075sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o
1076/usr/local/bin/yt-dlp
1077#+end_src
1078
1079Finally, make the file executable so that you can run it from the command-line.
1080
1081#+begin_src sh
1082sudo chmod a+rx /usr/local/bin/yt-dlp
1083#+end_src
1084#+date: <2020-01-26 Sun 00:00:00>
1085#+title: How to Get Steam Running Smoothly on NTFS Drives in Linux
1086#+description: Procedures for mounting NTFS drives using ntfs-3g to enable reliable execution of Steam gaming software on Linux environments.
1087#+slug: steam-on-ntfs
1088
1089* Auto-Mount Steam Drives
1090
1091If you want to see how to install Steam on Linux, see my other post: [[../linux-software/][Linux
1092Software]].
1093
1094Are you having trouble launching games, even though they've installed correctly?
1095This may happen if you're storing your games on an NT File System
1096(NTFS)-formatted drive. This shouldn't be an issue if you're storing your games
1097on the same drive that Steam is on, but some gamers prefer to put Steam on their
1098main drive and game files on another solid state drive (SSD) or hard disk drive
1099(HDD).
1100
1101To fix this problem, you'll need to try two things. First, you'll need to
1102install the =ntfs-3g= package, which allows for better interoperability with
1103Linux.
1104
1105#+begin_src sh
1106sudo apt install ntfs-3g
1107#+end_src
1108
1109Next, you should set up the =/etc/fstab= file to automatically mount your drives
1110on boot. To automatically mount your drives when the computer boots up, you'll
1111have to create the folders you want to mount your drive to first. I store mine
1112in the =/mnt= folder using names that I'll recognize, but you can create your
1113folders wherever you want.
1114
1115#+begin_src sh
1116mkdir /path/to/folder
1117#+end_src
1118
1119For example:
1120
1121#+begin_src sh
1122mkdir /mnt/steam_library
1123#+end_src
1124
1125To automatically mount drives upon system boot, you will need to collect five
1126items. The universally unique identifier (UUID) is the identification number
1127connected to whichever drive you're using to store Steam games.
1128
1129Drives are usually labeled similar to =/dev/nvme0n1p1= or =/dev/sda1=, so you'll
1130need to find the line in the output of the command below that correlates to your
1131drive and copy the UUID over to the =/etc/fstab= file.
1132
1133#+begin_src sh
1134sudo blkid | grep UUID=
1135#+end_src
1136
1137Next, you'll need your user identified (=uid=) and group identifier (=gid=). To
1138find these, run the following command:
1139
1140#+begin_src sh
1141id -u && id -g
1142#+end_src
1143
1144Now that you have collected the necessary information, open the =/etc/fstab=
1145file:
1146
1147#+begin_src sh
1148sudo nano /etc/fstab
1149#+end_src
1150
1151Each drive you want to mount on boot should have its own line in the
1152=/etc/fstab= file that looks similar to this:
1153
1154#+begin_src config
1155UUID=B64E53824E5339F7 /mnt/steam_library ntfs-3g uid=1000,gid=1000 0 0
1156#+end_src
1157
1158#+begin_src sh
1159sudo umount /dev/sdxX
1160#+end_src
1161
1162Now all you need to do is unmount your drive and re-mount it. You can unmount
1163the drive by doing this (be sure to use the correct drive name here):
1164
1165You can re-mount all your drives by executing the following:
1166
1167#+begin_src sh
1168sudo mount -a
1169#+end_src
1170
1171If you don't know what your drive name is, or you're nervous about unmounting
1172and re-mounting, simply reboot your computer, and the system will mount the
1173drive for you automatically.
1174#+date: <2020-02-09 Sunday 00:00:00>
1175#+title: A Practical Guide to Encryption, Keys, and Secure Communication
1176#+description: Systematic overview of encryption methodologies, key categories, algorithmic frameworks, and their applications in data confidentiality and integrity.
1177#+slug: cryptography
1178
1179* Similar Article Available
1180
1181If you haven't already, feel free to read my post on [[../aes-encryption/][AES Encryption]].
1182
1183* What is Cryptography?
1184
1185In layman's terms, cryptography is a process that can change data from a
1186readable format into an unreadable format (and vice-versa) through a series of
1187processes and secrets. More technically, this is the Internet Security
1188Glossary's definition:
1189
1190#+begin_quote
1191[Cryptography is] the mathematical science that deals with transforming data to
1192render its meaning unintelligible (i.e., to hide its semantic content), prevent
1193its undetected alteration, or prevent its unauthorized use. If the
1194transformation is reversible, cryptography also deals with restoring encrypted
1195data to an intelligible form.
1196
1197- [[https://tools.ietf.org/html/rfc2828][Internet Security Glossary (2000)]]
1198#+end_quote
1199
1200Cryptography cannot offer protection against the loss of data; it simply offers
1201encryption methods to protect data at-rest and data in-traffic. At a high-level,
1202encrypted is when plain text data is encrypted to cipher text (a secure form of
1203text that cannot be understood unless decrypted back to plain text). The
1204encryption process is completed through the use of a mathematical function that
1205utilizes one or more values called keys to encrypt or decrypt the data.
1206
1207* Key Elements of Cryptographic Systems
1208
1209To create or evaluate a cryptographic system, you need to know the essential
1210pieces to the system:
1211
1212- *Encryption Algorithm (Primitive):* A mathematical process that encrypts and
1213 decrypts data.
1214- *Encryption Key:* A string of bits used within the encryption algorithm as the
1215 secret that allows successful encryption or decryption of data.
1216- *Key Length (Size):* The maximum number of bits within the encryption key.
1217 It's important to remember that key size is regulated in certain countries.
1218- *Message Digest:* A smaller, fixed-size bit string version of the original
1219 message. This is practically infeasible to reverse, which is why it's commonly
1220 used to verify integrity.
1221
1222* Symmetric Systems (Secret Key Cryptography)
1223
1224Symmetric cryptography utilizes a secret, bidirectional key to perform both
1225encryption and decryption of the data. The most common implementation of
1226symmetric cryptography is the Advanced Encryption Standard, which uses keys that
1227are 128 bits to 256 bits in size. This standard came after the National
1228Institute of Standards and Technology (NIST) decided to retire the Data
1229Encryption Standard (DES) in 2001.
1230
1231Since brute force attacks strongly correlate with key length, the 56-bit key
1232length of DES was considered insecure after it was publicly broken in under 24
1233hours. However, there is a modern implementation of DES called Triple DES where
1234the DES method is applied three times to each data block.
1235
1236The main advantages to symmetric systems are the ease of use, since only one key
1237is required for both encryption and decryption, and the simplicity of the
1238algorithms. This helps with bulk data encryption that may unnecessarily waste
1239time and power using asymmetric systems.
1240
1241However, symmetric systems have disadvantages to keep in mind. Since the key is
1242private, it can be difficult to safely distribute keys to communication
1243partners. Additionally, the key cannot be used to sign messages since it's
1244necessary to keep the key private.
1245
1246* Asymmetric Systems (Public Key Cryptography)
1247
1248Asymmetric cryptography utilizes two keys within the system: a secret key that
1249is privately-held and a public key that can be distributed freely. The
1250interesting aspect of asymmetric cryptography is that either key can be used to
1251encrypt the data, there's no rule that dictates which key must be used for
1252encryption. Once one key is used to encrypt the data, only the other key can be
1253used to decrypt the data. This means that if the private key encrypts the data,
1254only the public key can decrypt the data.
1255
1256An advantage of this system is that if you successfully decrypt data using one
1257of the keys, you can be sure of the sender since only the other key could have
1258encrypted the data.
1259
1260One of the major implementations of an asymmetric system is a digital signature.
1261A digital signature can be generated using the sender's private key, or a
1262one-way hash function and is used to provide assurance for the integrity and
1263authenticity of the message. A couple common message digest algorithms are
1264SHA-256 and SHA-512, which securely compress data and produce a 128-bit message
1265digest.
1266
1267It should be noted that man-in-the-middle attacks are one of the risks with
1268digital signatures and public keys. To combat this, applications often use a
1269public key infrastructure (PKI) to independently authenticate the validity of
1270signatures and keys.
1271
1272Due to the large key size and [[https://crypto.stackexchange.com/a/591][inefficient mathematical functions]] of asymmetric
1273encryption, elliptical curve cryptography (ECC) is often used to increase
1274security while using fewer resources.
1275
1276* Applications of Cryptographic Systems
1277
1278There are numerous implementations of cryptographic systems around the world.
1279Here are six popular examples:
1280
1281*Transport Layer Security (TLS):* One of the most famous cryptographic solutions
1282created is TLS, a session-layered or connection-layered internet protocol that
1283allows for secure communications between browsers and servers. Using handshakes,
1284peer negotiation, and authentication allows TLS to prevent eavesdropping and
1285malicious transformation of data. The major reason for TLS popularity is that a
1286major vulnerability was found in the SSL protocol in 2014. Instead of SSL, TLS
1287can be used with HTTP to form HTTPS and is the preferred method for modern web
1288development due to its increased security.
1289
1290*Secure Hypertext Transfer Protocol (HTTPS):* An application layer protocol that
1291allows for secure transport of data between servers and web clients. One of the
1292unique parts of HTTPS is that it uses a secured port number instead of the
1293default web port address.
1294
1295*Virtual Private Network (VPN):* VPNs are made to securely extend a private
1296network across public networks by utilizing an encrypted layered tunneling
1297protocol paired with an authentication method, such as usernames and passwords.
1298This technology originally allowed remote employees to access their company's
1299data but have evolved into one of the top choices for anyone who wishes to mask
1300their sensitive personal data.
1301
1302*Internet Protocol Security (IPSec):* This protocol suite facilitates
1303communication between two or more hosts or subnets by authenticating and
1304encrypting the data packets. IPSec is used in a lot of VPNs to establish the VPN
1305connection through the transport and tunnel mode encryption methods. IPSec
1306encrypts just the data portion of packets in the transport methods, but it
1307encrypts both the data and headers in the tunnel method (introducing an
1308additional header for authentication).
1309
1310*Secure Shell (SSH):* SSH is another network protocol used to protect network
1311services by authenticating users through a secure channel. This protocol is
1312often used for command-line (shell) functions such as remote shell commands,
1313logins, and file transfers.
1314
1315*Kerberos:* Developed by MIT, Kerberos is a computer-network authentication
1316protocol that works on the basis of tickets to allow nodes communicating over a
1317non-secure network to prove their identity to one another securely. This is most
1318commonly used in business environments when used as the authentication and
1319encryption method for Windows Active Directory (AD).
1320
1321* Cybersecurity Controls
1322
1323If you're someone who needs solutions on how to control risks associated with
1324utilizing a crytograhpic system, start with these basic controls:
1325
1326- *Policies:* A policy on the use of cryptographic controls for protection of
1327 information is implemented and is in accordance with organizational
1328 objectives.
1329- *Key management:* A policy on the use, protection and lifetime of
1330 cryptographic keys is implemented through the entire application lifecycle.
1331- *Key size:* The organization has researched the optimal key size for their
1332 purposes, considering national laws, required processing power, and longevity
1333 of the solution.
1334- *Algorithm selection:* Implemented algorithms are sufficiently appropriate for
1335 the business of the organization, robust, and align with recommended
1336 guidelines.
1337- *Protocol configuration:* Protocols have been reviewed and configured suitable
1338 to the purpose of the business.
1339#+date: <2020-03-25 Wed 00:00:00>
1340#+title: Session Messenger: Is It Secure?
1341#+description: Technical description of the Session messaging protocol, including end-to-end encryption, metadata minimization, multi-device synchronization, and privacy-centric design.
1342#+slug: session-messenger
1343
1344* Privacy Warning
1345
1346The company behind Session (Loki Foundation) is from Australia. If you didn't
1347know, Australia has introduced [[https://parlinfo.aph.gov.au/parlInfo/download/legislation/bills/r6195_aspassed/toc_pdf/18204b01.pdf][legislation]] mandating companies comply with
1348government requests to build back door access into applications. For more
1349information, read my article on [[./2020-01-25-aes-encryption.html][AES Encryption]].
1350
1351* About Session
1352
1353[[https://getsession.org][Session]] is a private, cross-platform messaging app from the [[https://loki.foundation][Loki Foundation]]. As
1354someone who has spent years looking for quality alternatives to major messaging
1355apps, I was excited when I first heard about Session. Reading through [[https://arxiv.org/pdf/2002.04609.pdf][Session's
1356white paper]], you can learn the technologies behind the Session app. Part of the
1357security of Session comes from the Signal protocol, which was forked as the
1358origin of Session.
1359
1360#+begin_quote
1361Session is an end-to-end encrypted messenger that removes sensitive metadata
1362collection, and is designed for people who want privacy and freedom from any
1363forms of surveillance.
1364#+end_quote
1365
1366In general, this app promises security through end-to-end encryption,
1367decentralized onion routing, and private identities. The biggest change that the
1368Loki Foundation has made to the Signal protocol is removing the need for a phone
1369number. Instead, a random identification string is generated for any session you
1370create. This means you can create a new session for each device if you want to,
1371or link new devices with your ID.
1372
1373Since Session's website and white paper describe the details of Session's
1374security, I'm going to focus on using the app in this post.
1375
1376* Features
1377
1378Since most people are looking for an alternative to a popular chat app, I am
1379going to list out the features that Session has so that you are able to
1380determine if the app would suit your needs:
1381
1382- Multiple device linking [via a quick-response (QR) code or identifier (ID)]
1383- App locking via device screen lock, password, or fingerprint
1384- Screenshot blocking
1385- Incognito keyboard
1386- Read receipts and typing indicators
1387- Mobile notification customization
1388- Old message deletion and conversation limit
1389- Backups
1390- Recovery phrase
1391- Account deletion, including ID, messages, sessions, and contacts
1392
1393* Downloads
1394
1395I have tested this app on Ubuntu 19.10, Android 10, macOS Monterey, and iOS 15.
1396All apps have worked well without issue.
1397
1398Below is a brief overview of the Session app on Linux. To get this app, you'll
1399need to go to the [[https://getsession.org/download/][Downloads]] page and click to link to the operating system
1400you're using.
1401
1402For Linux, it will download an AppImage that you'll need to enable with the
1403following command:
1404
1405#+begin_src sh
1406sudo chmod u+x session-messenger-desktop-linux-x86_64-1.0.5.AppImage
1407#+end_src
1408
1409* Creating an Account
1410
1411Once you've installed the app, simply run the app and create your unique Session
1412ID. It will look something like this:
1413=05af1835afdd63c947b47705867501d6373f486aa1ae05b1f2f3fcd24570eba608=.
1414
1415You'll need to set a display name and, optionally, a password. If you set a
1416password, you will need to enter it every time you open the app.
1417
1418* Start Messaging
1419
1420Once you've created your account and set up your profile details, the next step
1421is to start messaging other people. To do so, you'll need to share your Session
1422ID with other people. From this point, it's straightforward and acts like any
1423other messaging app, so I won't dive into much detail here.
1424
1425** macOS
1426
1427One key feature to note is that the desktop application now provides a helpful
1428pop-up box explaining the process that Session uses to hide your IP address:
1429
1430** iOS
1431
1432The mobile app is simple and effective, giving you all the standard mobile
1433messaging options you'd expect.
1434
1435* Potential Issues
1436
1437I've discovered one annoying issue that would prevent from using this app
1438regularly. On a mobile device, there have been issues with receiving messages on
1439time. Even with battery optimization disabled and no network restrictions,
1440Session notifications sometimes do not display until I open the app or the
1441conversation itself and wait. This is actually one of the reasons I stopped
1442using Signal (this seems fixed as of my updates in 2021/2022, so I wouldn't
1443worry about this issue anymore).
1444
1445Looking for another messenger instead of Session? I recommend Signal, Matrix,
1446and internet relay chat (IRC).
1447#+date: <2020-05-03 Sun 00:00:00>
1448#+title: My Homelab Setup: Gear, Network, and Lessons Learned
1449#+description: Comprehensive instructions on assembling and configuring a homelab environment, emphasizing hardware selection, software deployment, and network setup for operational efficiency.
1450#+slug: homelab
1451
1452* What is a Homelab?
1453
1454Starting as a developer, I have stayed away from hardware-based hobbies (other
1455than building a gaming desktop). However, as the quarantine for COVID-19
1456stretches out further and further, I found myself bored and in search of new
1457hobbies. After spending the last few months browsing the [[https://www.reddit.com/r/homelab/][r/homelab]] subreddit, I
1458decided it was time to jump in and try things out for myself.
1459
1460Since I am a beginner and just recently graduated from college, everything I've
1461done so far in my homelab is low-budget.
1462
1463* Hardware
1464
1465** Raspberry Pi 4
1466
1467Luckily, I had actually purchased a [[https://www.raspberrypi.org/products/raspberry-pi-4-model-b/][Raspberry Pi 4]] before the quarantine started
1468so that I could try to keep Plex Media Center running, even while my desktop
1469computer was turned off. I started here, using the Pi to hold Plex and Pi-hole
1470until I grew tired with the slow performance.
1471
1472Here are the specifications for the Pi 4:
1473
1474- Broadcom BCM2711, Quad core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz
1475- 4GB LPDDR4-3200 SDRAM
1476- Gigabit Ethernet
1477- H.265 (4kp60 decode), H264 (1080p60 decode, 1080p30 encode)
1478- 64 GB MicroSD Card
1479
1480** Dell Optiplex 5040
1481
1482Since I wasn't happy with the Pi as my main server, I turned to Craigslist. I
1483know a lot of other homelabbers use Ebay, but I can't seem to ever trust it
1484enough to purchase items on there. So I used Craigslist and found a Dell
1485Optiplex 5040 desktop computer on sale for $90. While this computer might be
1486underpowered, it was one of the few computers under $100 that was available
1487during quarantine.
1488
1489Here are the specifications for the Dell Optiplex 5040:
1490
1491- Intel Core i3 6100
1492- 8GB RAM DDR3
1493- Intel HD Graphics
1494- Gigabit Ethernet
1495- 500GB Hard Drive
1496
1497While this hardware would be awful for a work computer or a gaming rig, it
1498turned out to be wonderful for my server purposes. The only limitation I have
1499found so far is the central processing unit (CPU). The i3-6100 only has enough
1500power for a single 4k video transcode at a time. I haven't tested more than
1501three 1080p streams at a time, but the maximum amount of streams I've ever
1502actually used is two.
1503
1504** WD easystore 10TB & 8TB
1505
1506Application storage and temporary files are stored on the internal hard drive of
1507the server, but all media files (movies, television, games, books, etc) are stored
1508externally on my WD easystore hard drive. Creating auto-boot configurations in
1509the =/etc/fstab= file on my server allows the hard drives to automatically mount
1510whenever I need to restart my server.
1511
1512#+begin_quote
1513Update: In March 2022, I shucked the hard drives out of their external cases,
1514put some Kapton tape on the third power pin to prevent power shutdowns, and
1515stuck them inside my server tower using internal Serial AT Attachment (SATA)
1516cables.
1517#+end_quote
1518
1519** Netgear Unmanaged Switch
1520
1521To manage all the ethernet cords used by my homelab, my desktop, and my living
1522room media center, I purchased an 8-port gigabit ethernet switch for $50 at my
1523local computer store. This is probably much more than I should have spent on an
1524unmanaged switch, but I am comfortable with the choice.
1525
1526** TP-Link Managed Switch
1527
1528Since I use the unmanaged switch to group all living room devices together, I
1529use the managed switch to configure virtual local area networks (VLANs) and
1530secure my network.
1531
1532** Arris TM1602A Modem & Sagecom Fast 5280 Router
1533
1534My default modem and router, provided by my ISP, are fairly standard. The Arris
1535modem supports DOCSIS 3.0, which is something that I definitely wanted as a
1536minimum. The Sagecom router is also standard, no fancy bells or whistles.
1537However, it does support the dynamic host configuration protocol (DHCP) for
1538internet protocol version 4 (IPv4) and internet protocol version 6 (IPv6), which
1539is something iyou can use to route all household traffic through a pi-hole or
1540firewall.
1541
1542** TP-Link EAP
1543
1544In order to gain better control over the network, I use my own wireless access
1545point instead of the one included in the Sagecom router above. Now I can control
1546and organize all of my ethernet connections through the VLANs on the managed
1547switch and wireless connections through the VLANS on the EAP.
1548
1549** Generic Printer
1550
1551The last piece to my homelab is a standard wireless printer. Nothing special
1552here.
1553
1554* Software
1555
1556** Ubuntu Server 20.04
1557
1558While the 20.04 version of Ubuntu was just released, I always like to experiment
1559with new features (and I don't mind breaking my system - it just gives me more
1560experience learning how to fix things). So, I have Ubuntu Server 20.04 installed
1561on the Dell Optiplex server and Ubuntu Server 19.10 installed on the Raspberry
1562Pi. Once I find an acceptable use for the Pi, I will most likely switch the
1563operating system.
1564
1565** Docker
1566
1567I am /very/ new to Docker, but I have had a lot of fun playing with it so far.
1568Docker is used to create containers that can hold all the contents of a system
1569without interfering with other software on the same system. So far, I have
1570successfully installed pi-hole, GitLab, Gogs, and Nextcloud in containers.
1571However, I opted to delete all of those so that I can reconfigure them more
1572professionally at a later time.
1573
1574** Plex Media Server
1575
1576Plex is a media center software that allows you to organize your movies, TV
1577shows, music, photos, and videos automatically. It will even download metadata
1578for you so that you can easily browse these collections.
1579
1580** Pi-hole
1581
1582Pi-hole is an alternative ad-blocker that runs at the DNS level, allowing you to
1583block traffic when it hits your network, so that you can reject any traffic you
1584deem to be bad. Pi-hole uses blacklists and whitelists to decide which traffic
1585block and, luckily, there are a lot of pre-made lists out there on Reddit,
1586GitHub, etc.
1587
1588** Nextcloud
1589
1590While I had trouble with the Docker version of Nextcloud, I was very successful
1591when setting up the snap version. Using this, I was able to map Nextcloud to a
1592subdomain of a domain I own in Namecheap. Additionally, Nextcloud has an
1593integration with Let's Encrypt that allows me to issue certificates
1594automatically to any new domain I authorize.
1595
1596** Webmin
1597
1598To monitor my servers, and the processes running on them, I use the Webmin
1599dashboard. This was fairly painless to set up, and I currently access it
1600straight through the server's IP address. In the future, I will be looking to
1601configure Webmin to use a custom domain just like Nextcloud.
1602#+date: <2020-05-19 Tue 00:00:00>
1603#+title: Tweak Your Ubuntu Look: A Beginner’s Guide to GNOME Customization
1604#+description: Detailed instructions for modifying system appearance and interface elements in Ubuntu 20.04, including setting themes, icon packs, font adjustments, terminal configurations, and related customization parameters.
1605#+slug: customizing-ubuntu
1606
1607* More Information
1608
1609For inspiration on designing your *nix computer, check out the [[https://libredd.it/r/unixporn][r/unixporn]]
1610subreddit!
1611
1612* Customizing Ubuntu
1613
1614New to Linux and want to add a personal touch to your machine? One of the best
1615perks of Linux is that it is *extremely* customizable. You can change the styles
1616of the windows, shell (status bars/docks), icons, fonts, terminals, and more.
1617
1618In this post, I'm going to go through customization on Ubuntu 20.04 (GNOME)
1619since most new users tend to choose Ubuntu-based distros. If you've found a way
1620to install Arch with i3-gaps, I'm assuming you know how to find more advanced
1621tutorials out there on customizations.
1622
1623** Required Tools
1624
1625Ubuntu 20.04 ships with the default desktop environment [[https://www.gnome.org/][Gnome]], which includes
1626the handy =gnome-tweaks= tool to quickly change designs. To install this, just
1627open your terminal and enter the following command:
1628
1629#+begin_src sh
1630sudo apt install gnome-tweaks
1631#+end_src
1632
1633After you've finished installing the tool, simply launch the Tweaks application,
1634and you'll be able to access the various customization options available by
1635default on Ubuntu. You might even like some of the pre-installed options.
1636
1637** GNOME Application Themes
1638
1639To change the themes applied to applications in GNOME, you will need to change
1640the Applications drop-down in the Appearance section of Tweaks. To add more
1641themes, you will have to find your preferred theme online and follow the steps
1642below to have it show up in the Tweaks tool. While you may find themes anywhere,
1643one of the most popular sites for GNOME themes is [[https://www.gnome-look.org/][gnome-look.org]]. This website
1644contains themes for applications, shells, icons, and cursors.
1645
1646Steps to import themes into Tweaks:
1647
16481. Download the theme.
16492. These files are usually compressed (.zip, .tar.gz, .tar.xz), so you will need
1650 to extract the contents. This is easiest when opening the file explorer,
1651 right-clicking the compressed file, and choosing "Extract here."
16523. Move the theme folder to =/usr/share/themes/=. You can do so with the
1653 following command: =sudo mv theme-folder/ /usr/share/themes/=.
1654 - Icons and cursors will be moved to the =/usr/share/icons/= folder.
1655 - Fonts will be moved to the =/usr/share/fonts/= folder Alternatively, you
1656 can move them to the =/usr/share/fonts/opentype/= or
1657 =/usr/share/fonts/opentype/= folders, if you have a specific font type.
16584. Close tweaks if it is open. Re-open Tweaks and your new theme will be
1659 available in the Applications drop-down in the Appearance section of Tweaks.
1660
1661If the theme is not showing up after you've moved it into the themes folder, you
1662may have uncompressed the folder into a sub-folder. You can check this by
1663entering the theme folder and listing the contents:
1664
1665#+begin_src sh
1666cd /usr/share/themes/Mojave-Dark && ls -la
1667#+end_src
1668
1669This is an example of what the contents of your theme folder should look like.
1670If you just see another folder there, you should move that folder up into the
1671=/usr/share/themes/= folder.
1672
1673#+begin_src sh
1674cinnamon COPYING gnome-shell gtk-2.0 gtk-3.0 index.theme metacity-1 plank xfwm4
1675#+end_src
1676
1677** GNOME Shell Themes
1678
1679To change the appearance of the title bar, default dock, app menu, and other
1680parts of the GNOME shell, you'll need to install the [[https://extensions.gnome.org/extension/19/user-themes/][user themes]] extension on
1681[[https://extensions.gnome.org/][Gnome Extensions]]. To be able to install extensions, you will first need to
1682install the browser extension that the website instructs you to. See this
1683screenshot for the blue box with a link to the extension.
1684
1685After the browser extension is installed, you will need to install the native
1686host connector:
1687
1688#+begin_src sh
1689sudo apt install chrome-gnome-shell
1690#+end_src
1691
1692Finally, you can go the [[https://extensions.gnome.org/extension/19/user-themes/][user themes]] extension page and click the installation
1693button. This will enable the Shell option in Tweaks. Now you can move shell
1694themes to the =/usr/share/themes= directory, using the same steps mentioned in
1695the previous section, and enable the new theme in Tweaks.
1696
1697** Icons & Cursors
1698
1699Icons and cursors are installed exactly the same way, so I'm grouping these
1700together in this post. Both of these items will need to follow the same process
1701as installing themes, except you will want to move your font folders to the
1702=/usr/share/icons/= directory instead.
1703
1704** Fonts
1705
1706Fonts are one of the overlooked parts of customization, but a good font can make
1707the whole screen look different. For example, I have installed the [[https://github.com/IBM/plex/releases][IBM Plex]]
1708fonts on my system. This follows the same process as installing themes, except
1709you will want to move your font folders to the =/usr/share/fonts/= directory
1710instead.
1711
1712** Terminal
1713
1714If you spend a lot of time typing commands, you know how important the style and
1715functionality of the terminal is. After spending a lot of time using the default
1716GNOME terminal with [[https://en.wikipedia.org/wiki/Bash_(Unix_shell)][unix shell]], I decided to try some different options. I ended
1717up choosing [[https://terminator-gtk3.readthedocs.io/en/latest/][Terminator]] with [[https://en.wikipedia.org/wiki/Z_shell][zsh]] (Z Shell).
1718
1719Terminator is great if you need to open multiple terminals at one time by simply
1720right-clicking and splitting the screen into as many terminals as you want.
1721While this project hasn't been updated in a while, [[https://github.com/gnome-terminator/terminator/issues/1][it is coming under new
1722development]]. However, this terminal is great and I haven't experienced any
1723errors yet.
1724
1725For the shell choice, I decided to choose zsh after trying it out on a fresh
1726Manjaro installation. Zsh is great if you like to change the themes of your
1727terminal, include icons, or add plugins.
1728
1729The desktop uses the [[https://github.com/zsh-users/zsh-autosuggestions][zsh-autosuggestions]] to suggest past commands as you type.
1730In addition, it suggests corrections if you misspell a command. Lastly, it uses
1731the =af-magic= theme, which adds dashed lines between commands, moving the
1732user@host tag to the right side of the terminal, and changes the colors. There
1733are plenty of plugins and themes to choose from. Just figure out what you like
1734and add it to your =~/.zshrc= file!
1735
1736*** Steps to Replicate My Terminal
1737
1738To install zsh on Ubuntu, enter the following command into a terminal:
1739
1740#+begin_src sh
1741sudo apt install zsh
1742#+end_src
1743
1744Then, enter the next command to activate zsh:
1745
1746#+begin_src sh
1747sudo chsh -s $(which zsh) $(whoami)
1748#+end_src
1749
1750To install Terminator on Ubuntu:
1751
1752#+begin_src sh
1753sudo apt install terminator
1754#+end_src
1755
1756To install Oh My Zsh on Ubuntu:
1757
1758#+begin_src sh
1759sh -c "$(curl -fsSL
1760https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
1761#+end_src
1762
1763To install zsh-autosuggestions via Oh My Zsh:
1764
1765#+begin_src sh
1766git clone https://github.com/zsh-users/zsh-autosuggestions
1767${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
1768#+end_src
1769
1770Then, add the following plugin wording to your =~/.zshrc= file (the default
1771config usually has the =git= plugin activated, so just add any other plugins to
1772the parentheses separated by a space):
1773
1774#+begin_src sh
1775nano ~/.zshrc
1776#+end_src
1777
1778#+begin_src sh
1779plugins=(git zsh-autosuggestions)
1780#+end_src
1781
1782Finally, you need to log out of your computer and log back in so your user shell
1783can refresh.
1784#+date: <2020-07-20 Mon 00:00:00>
1785#+title: A Data-Driven Look at Video Game Sales (1980–2020)
1786#+description: Presentation of analyzed data regarding global video game sales, covering platform distributions, genre classifications, and geographic market performance metrics.
1787#+slug: video-game-sales
1788
1789* Background Information
1790
1791This dataset (obtained from [[https://www.kaggle.com/gregorut/videogamesales/data][Kaggle]]) contains a list of video games with sales
1792greater than 100,000 copies. It was generated by a scrape of vgchartz.com.
1793
1794Fields include:
1795
1796- Rank: Ranking of overall sales
1797- Name: The game name
1798- Platform: Platform of the game release (i.e. PC,PS4, etc.)
1799- Year: Year of the game's release
1800- Genre: Genre of the game
1801- Publisher: Publisher of the game
1802- NA_{Sales}: Sales in North America (in millions)
1803- EU_{Sales}: Sales in Europe (in millions)
1804- JP_{Sales}: Sales in Japan (in millions)
1805- Other_{Sales}: Sales in the rest of the world (in millions)
1806- Global_{Sales}: Total worldwide sales.
1807
1808There are 16,598 records. 2 records were dropped due to incomplete information.
1809
1810* Import the Data
1811
1812#+begin_src python
1813# Import the Python libraries we will be using
1814import pandas as pd
1815import numpy as np
1816import seaborn as sns; sns.set()
1817import matplotlib.pyplot as plt
1818
1819# Load the file using the path to the downloaded file
1820file = r'video_game_sales.csv'
1821df = pd.read_csv(file)
1822df
1823#+end_src
1824
1825* Explore the Data
1826
1827#+begin_src python
1828# With the description function, we can see the basic stats. For example, we can
1829# also see that the 'Year' column has some incomplete values.
1830df.describe()
1831#+end_src
1832
1833#+begin_src python
1834# This function shows the rows and columns of NaN values. For example, df[179,3] = nan
1835np.where(pd.isnull(df))
1836
1837(array([179, ..., 16553], dtype=int64),
1838 array([3, ..., 5], dtype=int64))
1839#+end_src
1840
1841* Visualize the Data
1842
1843#+begin_src python
1844# This function plots the global sales by platform
1845sns.catplot(x='Platform', y='Global_Sales', data=df, jitter=False).set_xticklabels(rotation=90)
1846#+end_src
1847
1848#+begin_src python
1849# This function plots the global sales by genre
1850sns.catplot(x='Genre', y='Global_Sales', data=df, jitter=False).set_xticklabels(rotation=45)
1851#+end_src
1852
1853#+begin_src python
1854# This function plots the global sales by year
1855sns.lmplot(x='Year', y='Global_Sales', data=df).set_xticklabels(rotation=45)
1856#+end_src
1857
1858#+begin_src python
1859# This function plots four different lines to show sales from different regions.
1860# The global sales plot line is commented-out, but can be included for comparison
1861df2 = df.groupby('Year').sum()
1862years = range(1980,2019)
1863
1864a = df2['NA_Sales']
1865b = df2['EU_Sales']
1866c = df2['JP_Sales']
1867d = df2['Other_Sales']
1868# e = df2['Global_Sales']
1869
1870fig, ax = plt.subplots(figsize=(12,12))
1871ax.set_ylabel('Region Sales (in Millions)')
1872ax.set_xlabel('Year')
1873
1874ax.plot(years, a, label='NA_Sales')
1875ax.plot(years, b, label='EU_Sales')
1876ax.plot(years, c, label='JP_Sales')
1877ax.plot(years, d, label='Other_Sales')
1878# ax.plot(years, e, label='Global_Sales')
1879
1880ax.legend()
1881plt.show()
1882#+end_src
1883
1884** Investigate Outliers
1885
1886#+begin_src python
1887# Find the game with the highest sales in North America
1888df.loc[df['NA_Sales'].idxmax()]
1889
1890Rank 1
1891Name Wii Sports
1892Platform Wii
1893Year 2006
1894Genre Sports
1895Publisher Nintendo
1896NA_Sales 41.49
1897EU_Sales 29.02
1898JP_Sales 3.77
1899Other_Sales 8.46
1900Global_Sales 82.74
1901Name: 0, dtype: object
1902
1903# Explore statistics in the year 2006 (highest selling year)
1904df3 = df[(df['Year'] == 2006)]
1905df3.describe()
1906#+end_src
1907
1908#+begin_src python
1909# Plot the results of the previous dataframe (games from 2006) - we can see the year's results were largely carried by Wii Sports
1910sns.catplot(x="Genre", y="Global_Sales", data=df3, jitter=False).set_xticklabels(rotation=45)
1911#+end_src
1912
1913#+begin_src python
1914# We can see 4 outliers in the graph above, so let's get the top 5 games from that dataframe
1915# The results below show that Nintendo had all top 5 games (3 on the Wii and 2 on the DS)
1916df3.sort_values(by=['Global_Sales'], ascending=False).head(5)
1917#+end_src
1918
1919* Discussion
1920
1921The purpose of exploring datasets is to ask questions, answer questions, and
1922discover intelligence that can be used to inform decision-making. So, what have
1923we found in this dataset?
1924
1925Today we simply explored a publicly-available dataset to see what kind of
1926information it contained. During that exploration, we found that video game
1927sales peaked in 2006. That peak was largely due to Nintendo, who sold the top 5
1928games in 2006 and has a number of games in the top-10 list for the years
19291980-2020. Additionally, the top four platforms by global sales (Wii, NES, GB,
1930DS) are owned by Nintendo.
1931
1932We didn't explore everything this dataset has to offer, but we can tell from a
1933brief analysis that Nintendo seems to rule sales in the video gaming world.
1934Further analysis could provide insight into which genres, regions, publishers,
1935or world events are correlated with sales.
1936#+date: <2020-07-26 Sun 00:00:00>
1937#+title: A Location Intelligence Framework: Mapping Urban Business Clusters with Python
1938#+description: Methodology for utilizing data science techniques and Foursquare API data to analyze and select promising business sites within urban environments.
1939#+slug: business-analysis
1940
1941* Background Information
1942
1943This project aims to help investors learn more about a random city in order to
1944determine optimal locations for business investments. The data used in this
1945project was obtained using Foursquare's developer API.
1946
1947Fields include:
1948
1949- Venue Name
1950- Venue Category
1951- Venue Latitude
1952- Venue Longitude
1953
1954There are 232 records found using the center of Lincoln as the area of interest
1955with a radius of 10,000.
1956
1957* Import the Data
1958
1959The first step is the simplest: import the applicable libraries. We will be
1960using the libraries below for this project.
1961
1962#+begin_src python
1963# Import the Python libraries we will be using
1964import pandas as pd
1965import requests
1966import folium
1967import math
1968import json
1969from pandas.io.json import json_normalize
1970from sklearn.cluster import KMeans
1971#+end_src
1972
1973To begin our analysis, we need to import the data for this project. The data we
1974are using in this project comes directly from the Foursquare application
1975programming interface (API). The first step is to get the latitude and longitude
1976of the city being studied (Lincoln, NE) and setting up the folium map.
1977
1978#+begin_src python
1979# Define the latitude and longitude, then map the results
1980latitude = 40.806862
1981longitude = -96.681679
1982map_LNK = folium.Map(location=[latitude, longitude], zoom_start=12)
1983
1984map_LNK
1985#+end_src
1986
1987Now that we have defined our city and created the map, we need to go get the
1988business data. The Foursquare API will limit the results to 100 per API call, so
1989we use our first API call below to determine the total results that Foursquare
1990has found. Since the total results are 232, we perform the API fetching process
1991three times (100 + 100 + 32 = 232).
1992
1993#+begin_src python
1994# Foursquare API credentials
1995CLIENT_ID = 'your-client-id'
1996CLIENT_SECRET = 'your-client-secret'
1997VERSION = '20180604'
1998
1999# Set up the URL to fetch the first 100 results
2000LIMIT = 100
2001radius = 10000
2002url = 'https://api.foursquare.com/v2/venues/explore?&client_id={}&client_secret={}&v={}&ll={},{}&radius={}&limit={}'.format(
2003 CLIENT_ID,
2004 CLIENT_SECRET,
2005 VERSION,
2006 latitude,
2007 longitude,
2008 radius,
2009 LIMIT)
2010
2011# Fetch the first 100 results
2012results = requests.get(url).json()
2013
2014# Determine the total number of results needed to fetch
2015totalResults = results['response']['totalResults']
2016totalResults
2017
2018# Set up the URL to fetch the second 100 results (101-200)
2019LIMIT = 100
2020offset = 100
2021radius = 10000
2022url2 = 'https://api.foursquare.com/v2/venues/explore?&client_id={}&client_secret={}&v={}&ll={},{}&radius={}&limit={}&offset={}'.format(
2023 CLIENT_ID,
2024 CLIENT_SECRET,
2025 VERSION,
2026 latitude,
2027 longitude,
2028 radius,
2029 LIMIT,
2030 offset)
2031
2032# Fetch the second 100 results (101-200)
2033results2 = requests.get(url2).json()
2034
2035# Set up the URL to fetch the final results (201 - 232)
2036LIMIT = 100
2037offset = 200
2038radius = 10000
2039url3 = 'https://api.foursquare.com/v2/venues/explore?&client_id={}&client_secret={}&v={}&ll={},{}&radius={}&limit={}&offset={}'.format(
2040 CLIENT_ID,
2041 CLIENT_SECRET,
2042 VERSION,
2043 latitude,
2044 longitude,
2045 radius,
2046 LIMIT,
2047 offset)
2048
2049# Fetch the final results (201 - 232)
2050results3 = requests.get(url3).json()
2051#+end_src
2052
2053* Clean the Data
2054
2055Now that we have our data in three separate dataframes, we need to combine them
2056into a single dataframe and make sure to reset the index so that we have a
2057unique ID for each business. The =get_category_type= function below will pull
2058the categories and name from each business's entry in the Foursquare data
2059automatically. Once all the data has been labeled and combined, the results are
2060stored in the =nearby_venues= dataframe.
2061
2062#+begin_src python
2063# This function will extract the category of the venue from the API dictionary
2064def get_category_type(row):
2065 try:
2066 categories_list = row['categories']
2067 except:
2068 categories_list = row['venue.categories']
2069
2070 if len(categories_list) == 0:
2071 return None
2072 else:
2073 return categories_list[0]['name']
2074
2075# Get the first 100 venues
2076venues = results['response']['groups'][0]['items']
2077nearby_venues = json_normalize(venues)
2078
2079# filter columns
2080filtered_columns = ['venue.name', 'venue.categories', 'venue.location.lat', 'venue.location.lng']
2081nearby_venues = nearby_venues.loc[:, filtered_columns]
2082
2083# filter the category for each row
2084nearby_venues['venue.categories'] = nearby_venues.apply(get_category_type, axis=1)
2085
2086# clean columns
2087nearby_venues.columns = [col.split(".")[-1] for col in nearby_venues.columns]
2088
2089---
2090
2091# Get the second 100 venues
2092venues2 = results2['response']['groups'][0]['items']
2093nearby_venues2 = json_normalize(venues2) # flatten JSON
2094
2095# filter columns
2096filtered_columns2 = ['venue.name', 'venue.categories', 'venue.location.lat', 'venue.location.lng']
2097nearby_venues2 = nearby_venues2.loc[:, filtered_columns]
2098
2099# filter the category for each row
2100nearby_venues2['venue.categories'] = nearby_venues2.apply(get_category_type, axis=1)
2101
2102# clean columns
2103nearby_venues2.columns = [col.split(".")[-1] for col in nearby_venues.columns]
2104nearby_venues = nearby_venues.append(nearby_venues2)
2105
2106---
2107
2108# Get the rest of the venues
2109venues3 = results3['response']['groups'][0]['items']
2110nearby_venues3 = json_normalize(venues3) # flatten JSON
2111
2112# filter columns
2113filtered_columns3 = ['venue.name', 'venue.categories', 'venue.location.lat', 'venue.location.lng']
2114nearby_venues3 = nearby_venues3.loc[:, filtered_columns]
2115
2116# filter the category for each row
2117nearby_venues3['venue.categories'] = nearby_venues3.apply(get_category_type, axis=1)
2118
2119# clean columns
2120nearby_venues3.columns = [col.split(".")[-1] for col in nearby_venues3.columns]
2121
2122nearby_venues = nearby_venues.append(nearby_venues3)
2123nearby_venues = nearby_venues.reset_index(drop=True)
2124nearby_venues
2125#+end_src
2126
2127* Visualize the Data
2128
2129We now have a complete, clean data set. The next step is to visualize this data
2130onto the map we created earlier. We will be using folium's =CircleMarker()=
2131function to do this.
2132
2133#+begin_src python
2134# add markers to map
2135for lat, lng, name, categories in zip(nearby_venues['lat'], nearby_venues['lng'], nearby_venues['name'], nearby_venues['categories']):
2136 label = '{} ({})'.format(name, categories)
2137 label = folium.Popup(label, parse_html=True)
2138 folium.CircleMarker(
2139 [lat, lng],
2140 radius=5,
2141 popup=label,
2142 color='blue',
2143 fill=True,
2144 fill_color='#3186cc',
2145 fill_opacity=0.7,
2146 ).add_to(map_LNK)
2147
2148map_LNK
2149#+end_src
2150
2151* Clustering: /k-means/
2152
2153To cluster the data, we will be using the /k-means/ algorithm. This algorithm is
2154iterative and will automatically make sure that data points in each cluster are
2155as close as possible to each other, while being as far as possible away from
2156other clusters.
2157
2158However, we first have to figure out how many clusters to use (defined as the
2159variable 'k'). To do so, we will use the next two functions to calculate the sum
2160of squares within clusters and then return the optimal number of clusters.
2161
2162#+begin_src python
2163# This function will return the sum of squares found in the data
2164def calculate_wcss(data):
2165 wcss = []
2166 for n in range(2, 21):
2167 kmeans = KMeans(n_clusters=n)
2168 kmeans.fit(X=data)
2169 wcss.append(kmeans.inertia_)
2170
2171 return wcss
2172
2173# Drop 'str' cols so we can use k-means clustering
2174cluster_df = nearby_venues.drop(columns=['name', 'categories'])
2175
2176# calculating the within clusters sum-of-squares for 19 cluster amounts
2177sum_of_squares = calculate_wcss(cluster_df)
2178
2179# This function will return the optimal number of clusters
2180def optimal_number_of_clusters(wcss):
2181 x1, y1 = 2, wcss[0]
2182 x2, y2 = 20, wcss[len(wcss)-1]
2183
2184 distances = []
2185 for i in range(len(wcss)):
2186 x0 = i+2
2187 y0 = wcss[i]
2188 numerator = abs((y2-y1)*x0 - (x2-x1)*y0 + x2*y1 - y2*x1)
2189 denominator = math.sqrt((y2 - y1)**2 + (x2 - x1)**2)
2190 distances.append(numerator/denominator)
2191
2192 return distances.index(max(distances)) + 2
2193
2194# calculating the optimal number of clusters
2195n = optimal_number_of_clusters(sum_of_squares)
2196#+end_src
2197
2198Now that we have found that our optimal number of clusters is six, we need to
2199perform k-means clustering. When this clustering occurs, each business is
2200assigned a cluster number from 0 to 5 in the dataframe.
2201
2202#+begin_src python
2203# set number of clusters equal to the optimal number
2204kclusters = n
2205
2206# run k-means clustering
2207kmeans = KMeans(n_clusters=kclusters, random_state=0).fit(cluster_df)
2208
2209# add clustering labels to dataframe
2210nearby_venues.insert(0, 'Cluster Labels', kmeans.labels_)
2211#+end_src
2212
2213Success! We now have a dataframe with clean business data, along with a
2214cluster number for each business. Now let's map the data using six
2215different colors.
2216
2217#+begin_src python
2218# create map with clusters
2219map_clusters = folium.Map(location=[latitude, longitude], zoom_start=12)
2220colors = ['#0F9D58', '#DB4437', '#4285F4', '#800080', '#ce12c0', '#171717']
2221
2222# add markers to the map
2223for lat, lng, name, categories, cluster in zip(nearby_venues['lat'], nearby_venues['lng'], nearby_venues['name'], nearby_venues['categories'], nearby_venues['Cluster Labels']):
2224 label = '[{}] {} ({})'.format(cluster, name, categories)
2225 label = folium.Popup(label, parse_html=True)
2226 folium.CircleMarker(
2227 [lat, lng],
2228 radius=5,
2229 popup=label,
2230 color=colors[int(cluster)],
2231 fill=True,
2232 fill_color=colors[int(cluster)],
2233 fill_opacity=0.7).add_to(map_clusters)
2234
2235map_clusters
2236#+end_src
2237
2238* Investigate Clusters
2239
2240Now that we have figured out our clusters, let's do a little more analysis to
2241provide more insight into the clusters. With the information below, we can see
2242which clusters are more popular for businesses and which are less popular. The
2243results below show us that clusters 0 through 3 are popular, while clusters 4
2244and 5 are not very popular at all.
2245
2246#+begin_src python
2247# Show how many venues are in each cluster
2248color_names = ['Dark Green', 'Red', 'Blue', 'Purple', 'Pink', 'Black']
2249for x in range(0,6):
2250 print("Color of Cluster", x, ":", color_names[x])
2251 print("Venues found in Cluster", x, ":", nearby_venues.loc[nearby_venues['Cluster Labels'] == x, nearby_venues.columns[:]].shape[0])
2252 print("---")
2253#+end_src
2254
2255Our last piece of analysis is to summarize the categories of businesses within
2256each cluster. With these results, we can clearly see that restaurants, coffee
2257shops, and grocery stores are the most popular.
2258
2259#+begin_src python
2260# Calculate how many venues there are in each category
2261# Sort from largest to smallest
2262temp_df = nearby_venues.drop(columns=['name', 'lat', 'lng'])
2263
2264cluster0_grouped = temp_df.loc[temp_df['Cluster Labels'] == 0].groupby(['categories']).count().sort_values(by='Cluster Labels', ascending=False)
2265cluster1_grouped = temp_df.loc[temp_df['Cluster Labels'] == 1].groupby(['categories']).count().sort_values(by='Cluster Labels', ascending=False)
2266cluster2_grouped = temp_df.loc[temp_df['Cluster Labels'] == 2].groupby(['categories']).count().sort_values(by='Cluster Labels', ascending=False)
2267cluster3_grouped = temp_df.loc[temp_df['Cluster Labels'] == 3].groupby(['categories']).count().sort_values(by='Cluster Labels', ascending=False)
2268cluster4_grouped = temp_df.loc[temp_df['Cluster Labels'] == 4].groupby(['categories']).count().sort_values(by='Cluster Labels', ascending=False)
2269cluster5_grouped = temp_df.loc[temp_df['Cluster Labels'] == 5].groupby(['categories']).count().sort_values(by='Cluster Labels', ascending=False)
2270
2271# show how many venues there are in each cluster (> 1)
2272with pd.option_context('display.max_rows', None, 'display.max_columns', None):
2273 print("\n\n", "Cluster 0:", "\n", cluster0_grouped.loc[cluster0_grouped['Cluster Labels'] > 1])
2274 print("\n\n", "Cluster 1:", "\n", cluster1_grouped.loc[cluster1_grouped['Cluster Labels'] > 1])
2275 print("\n\n", "Cluster 2:", "\n", cluster2_grouped.loc[cluster2_grouped['Cluster Labels'] > 1])
2276 print("\n\n", "Cluster 3:", "\n", cluster3_grouped.loc[cluster3_grouped['Cluster Labels'] > 1])
2277 print("\n\n", "Cluster 4:", "\n", cluster4_grouped.loc[cluster4_grouped['Cluster Labels'] > 1])
2278 print("\n\n", "Cluster 5:", "\n", cluster5_grouped.loc[cluster5_grouped['Cluster Labels'] > 1])
2279#+end_src
2280
2281* Discussion
2282
2283In this project, we gathered location data for Lincoln, Nebraska, USA and
2284clustered the data using the k-means algorithm in order to identify the unique
2285clusters of businesses in Lincoln. Through these actions, we found that there
2286are six unique business clusters in Lincoln and that two of the clusters are
2287likely unsuitable for investors. The remaining four clusters have a variety of
2288businesses, but are largely dominated by restaurants and grocery stores.
2289
2290Using this project, investors can now make more informed decisions when deciding
2291the location and category of business in which to invest.
2292
2293Further studies may involve other attributes for business locations, such as
2294population density, average wealth across the city, or crime rates. In addition,
2295further studies may include additional location data and businesses by utilizing
2296multiple sources, such as Google Maps and OpenStreetMap.
2297#+date: <2020-08-22 Sat 00:00:00>
2298#+title: Configure GitHub Pages to Use Your Root Domain Instead of WWW
2299#+description: Stepwise instructions for configuring redirection of GitHub Pages sites from the www subdomain to the apex domain to improve domain resolution and access consistency.
2300#+slug: redirect-github-pages
2301
2302* Short answer
2303
2304** Step 1
2305
2306Add a new file called =CNAME= (canonical name) to your GitHub Pages repository
2307containing only one line: your top-level domain name. E.g.: =example.com=
2308
2309** Step 2
2310
2311[Optional] but highly recommended
2312
23132.1: Remove all other top-level records (prefixed with @) of type A from your
2314DNS configuration.
2315
23162.2: Remove a CNAME record for the second-level domain =www=, if it is present.
2317
2318** Step 3
2319
2320Add these 5 entries to the very top of your domain name system (DNS)
2321configuration:
2322
2323#+begin_src txt
2324@ A 185.199.108.153
2325@ A 185.199.109.153
2326@ A 185.199.110.153
2327@ A 185.199.111.153
2328www CNAME your_github_username.github.io.
2329#+end_src
2330
2331Replace =your_github_username= with your actual GitHub username.
2332
2333** Step 4
2334
2335Wait for your DNS changes to propagate. DNS changes aren't effective
2336immediately. They can take up to a full day to propagate.
2337
2338* Long answer
2339
2340This issue has two sides. One is the DNS configuration itself. Another one is
2341the way GitHub Pages will forward hypertext transfer protocol (HTTP) requests.
2342
2343We need to know a few things to understand what GitHub is trying to say in their
2344documentation.
2345
2346** DNS Entry Types
2347
2348There are two types of DNS records which interest us: CNAME and A.
2349
2350=A= is also known as =Apex= or sometimes as =root entry=. It forwards requests
2351to a specified fixed IP address. =CNAME= entry forwards requests to a specified
2352URL (actual valid plain text URL, not an internet protocol (IP) address).
2353
2354** DNS Load balancing
2355
2356GitHub has one central URL address which accepts all DNS requests for GitHub
2357Pages: =http://username.github.io=. That URL is resolved to different IP
2358addresses based on your geographical location. Website hosted on GitHub Pages is
2359a simple collection of =HTML=, =CSS= and =JS= files. GitHub distributes these
2360files to different servers across the globe. So that when your browser sends a
2361request from Europe, it receives data from a server in Europe. The same is valid
2362for the requests from Asia and the USA.
2363
2364** What GitHub is trying to say
2365
2366Since =A= records in DNS must contain IP addresses, and they must be either
2367=185.199.108.153= or =185.199.109.153= or =185.199.110.153= or
2368=185.199.111.153=, there is no way to forward requests to a server located
2369somewhere in Europe or Asia. Your website hosted at GitHub Pages will be
2370downloaded from a central GitHub Pages server. There is a minor risk that if
2371GitHub Pages DNS servers (=x.x.x.153=) are down for some reason, all custom
2372domains which use fixed GitHub Pages IP addresses will not be accessible (their
2373DNS requests will not be resolvable).
2374
2375That is why GitHub strongly suggests to either use a second-level domain for
2376your GitHub Pages (e.g. =blog.example.com=) or use a DNS service provider that
2377supports a record type =ALIAS= that acts as =A= record but forwards request to a
2378URL address (e.g. =username.github.io=) instead of a fixed IP address.
2379
2380** How GitHub Pages treats HTTP requests
2381
2382After a DNS request for =your_github_username.github.io= is resolved into an IP
2383address, e.g. =185.199.108.153= your browser sends an HTTP request to that
2384server with an HTTP header =Host=. Below are =curl= examples that load the same
2385website (these examples might not work if you are behind a proxy server):
2386
2387#+begin_src sh
2388curl --header "Host: your_github_username.github.io" http://185.199.108.153/
2389curl --header "Host: www.example.com" http://185.199.108.153/
2390curl --header "Host: example.com" http://185.199.108.153/
2391#+end_src
2392
2393This way GitHub Pages servers know which user website to serve.
2394
2395#+begin_quote
2396GitHub Pages server will automatically redirect HTTP requests to the top-level
2397domain if your =CNAME= file contains =example.com= but =www.example.com= is
2398requested.
2399
2400The same is valid if your =CNAME= file contains =www.example.com= but the header
2401=Host= in the =HTTP= request contains =example.com=.
2402#+end_quote
2403
2404** Why can't I add a =CNAME= record entry that accepts a top-level request (=@=) to my DNS configuration?
2405
2406Quote from the GitHub Pages documentation:
2407
2408#+begin_quote
2409Warning: Do not create a CNAME record for your custom apex domain! Doing so may
2410cause issues with other services, such as email, on that domain.
2411#+end_quote
2412
2413** References:
2414
24151. [[https://docs.github.com/en/github/working-with-github-pages/configuring-a-custom-domain-for-your-github-pages-site][Setting up a custom domain with GitHub Pages]]
24162. [[https://docs.github.com/en/github/working-with-github-pages/troubleshooting-custom-domains-and-github-pages][My custom domain isn't working]]
24173. [[https://serverfault.com/questions/589370/cannot-access-my-github-pages-website-by-ip-address][Cannot access my GitHub Pages website by IP Address]]
24184. [[https://stackoverflow.com/questions/23375422/how-do-i-set-up-github-pages-to-redirect-dns-requests-from-a-subdomain-e-g-www][How do I set up GitHub Pages to redirect DNS requests from a subdomain (e.g.
2419 www) to the top-level domain (TLD, Apex record)?]]
2420#+date: <2020-08-29 Sat 00:00:00>
2421#+title: PHP User Authentication: Create, Validate, and Manage Sessions
2422#+description: Comprehensive guide to developing a secure user login system in PHP, including account creation, session management, and logout procedures.
2423#+slug: php-auth-flow
2424
2425* Introduction
2426
2427When creating websites that will allow users to create accounts, the developer
2428always needs to consider the proper authentication flow for their app. For
2429example, some developers will utilize an application programming interface (API)
2430for authentication, some will use OAuth, and some may just use their own simple
2431database.
2432
2433For those using pre-built libraries, authentication may simply be a problem of
2434copying and pasting the code from their library's documentation. For example,
2435here's the code I use to authenticate users with the Tumblr OAuth API for my
2436Tumblr client, Vox Populi:
2437
2438#+begin_src php
2439// Start the session
2440session_start();
2441
2442// Use my key/secret pair to create a new client connection
2443$consumer_key = getenv('CONSUMER_KEY');
2444$consumer_secret = getenv('CONSUMER_SECRET');
2445$client = new Tumblr\API\Client($consumer_key, $consumer_secret);
2446$requestHandler = $client->getRequestHandler();
2447$requestHandler->setBaseUrl('https://www.tumblr.com/');
2448
2449// Check the session and cookies to see if the user is authenticated
2450// Otherwise, send user to Tumblr authentication page and set tokens from Tumblr's response
2451
2452// Authenticate client
2453$client = new Tumblr\API\Client(
2454 $consumer_key,
2455 $consumer_secret,
2456 $token,
2457 $token_secret
2458);
2459#+end_src
2460
2461However, developers creating authentication flows from scratch will need to
2462think carefully about when to make sure a web page will check the user's
2463authenticity.
2464
2465In this article, we're going to look at a simple authentication flow using a
2466MySQL database and PHP.
2467
2468* Creating User Accounts
2469
2470The beginning to any type of user authentication is to create a user account.
2471This process can take many formats, but the simplest is to accept user input
2472from a form (e.g., username and password) and send it over to your database. For
2473example, here's a snippet that shows how to get username and password parameters
2474that would come when a user submits a form to your PHP script.
2475
2476*Note*: Ensure that your password column is large enough to hold the hashed
2477value (at least 60 characters or longer).
2478
2479#+begin_src php
2480// Get the values from the URL
2481$username = $_POST['username'];
2482$raw_password = $_POST['password'];
2483
2484// Hash password
2485// password_hash() will create a random salt if one isn't provided, and this is generally the easiest and most secure approach.
2486$password = password_hash($raw_password, PASSWORD_DEFAULT);
2487
2488// Save database details as variables
2489$servername = "localhost";
2490$username = "username";
2491$password = "password";
2492$dbname = "myDB";
2493
2494// Create connection to the database
2495$conn = new mysqli($servername, $username, $password, $dbname);
2496
2497// Check connection
2498if ($conn->connect_error) {
2499 die("Connection failed: " . $conn->connect_error);
2500}
2501
2502$sql = "INSERT INTO users (username, password)
2503VALUES ('$username', '$password')";
2504
2505if ($conn->query($sql) === TRUE) {
2506 echo "New record created successfully";
2507} else {
2508 echo "Error: " . $sql . "<br>" . $conn->error;
2509}
2510
2511$conn->close();
2512#+end_src
2513
2514** Validate Returning Users
2515
2516To be able to verify that a returning user has a valid username and password in
2517your database is as simple as having users fill out a form and comparing their
2518inputs to your database.
2519
2520#+begin_src php
2521// Query the database for username and password
2522// ...
2523
2524if(password_verify($password_input, $hashed_password)) {
2525 // If the input password matched the hashed password in the database
2526 // Do something, log the user in.
2527}
2528
2529// Else, Redirect them back to the login page.
2530...
2531#+end_src
2532
2533* Storing Authentication State
2534
2535Once you've created the user's account, now you're ready to initialize the
2536user's session. *You will need to do this on every page you load while the user
2537is logged in.* To do so, simply enter the following code snippet:
2538
2539#+begin_src php
2540session_start();
2541#+end_src
2542
2543Once you've initialized the session, the next step is to store the session in a
2544cookie so that you can access it later.
2545
2546#+begin_src php
2547setcookie(session_name());
2548#+end_src
2549
2550Now that the session name has been stored, you'll be able to check if there's an
2551active session whenever you load a page.
2552
2553#+begin_src php
2554if(isset(session_name())) {
2555 // The session is active
2556}
2557#+end_src
2558
2559** Removing User Authentication
2560
2561The next logical step is to give your users the option to log out once they are
2562done using your application. This can be tricky in PHP since a few of the
2563standard ways do not always work.
2564
2565#+begin_src php
2566// Initialize the session.
2567// If you are using session_name("something"), don't forget it now!
2568session_start();
2569
2570// Delete authentication cookies
2571unset($_COOKIE[session_name()]);
2572setcookie(session_name(), "", time() - 3600, "/logged-in/");
2573unset($_COOKIE["PHPSESSID"]);
2574setcookie("PHPSESSID", "", time() - 3600, "/logged-in/");
2575
2576// Unset all of the session variables.
2577$_SESSION = array();
2578session_unset();
2579
2580// If it's desired to kill the session, also delete the session cookie.
2581// Note: This will destroy the session, and not just the session data!
2582if (ini_get("session.use_cookies")) {
2583 $params = session_get_cookie_params();
2584 setcookie(session_name(), '', time() - 42000,
2585 $params["path"], $params["domain"],
2586 $params["secure"], $params["httponly"]
2587 );
2588}
2589
2590// Finally, destroy the session.
2591session_destroy();
2592session_write_close();
2593
2594// Go back to sign-in page
2595header('Location: https://example.com/logged-out/');
2596die();
2597#+end_src
2598
2599* Wrapping Up
2600
2601Now you should be ready to begin your authentication programming with PHP. You
2602can create user accounts, create sessions for users across different pages of
2603your site, and then destroy the user data when they're ready to leave.
2604
2605For more information on this subject, I recommend reading the [[https://www.php.net/][PHP Documentation]].
2606Specifically, you may want to look at [[https://www.php.net/manual/en/features.http-auth.php][HTTP Authentication with PHP]], [[https://www.php.net/manual/en/book.session.php][session
2607handling]], and [[https://www.php.net/manual/en/function.hash.php][hash]].
2608#+date: <2020-09-01 Tue 00:00:00>
2609#+title: How to Use IBM Watson Visual Recognition API for Image Analysis
2610#+description: Technical overview and application instructions for using IBM Watson Visual Recognition service, with focus on API configuration, image input processing, and classification output handling.
2611#+slug: visual-recognition
2612
2613* What is IBM Watson?
2614
2615If you've never heard of [[https://www.ibm.com/watson][Watson]], this service is a suite of enterprise-ready
2616artificial intelligence (AI) services, applications, and tooling provided by
2617IBM. Watson contains quite a few useful tools for data scientists and students,
2618including the subject of this post today: visual recognition.
2619
2620If you'd like to view the official documentation for the Visual Recognition
2621application programming interface (API), visit the [[https://cloud.ibm.com/apidocs/visual-recognition/visual-recognition-v3?code=python][API Docs]].
2622
2623* Prerequisites
2624
2625To be able to use Watson Visual Recognition, you'll need the following:
2626
26271. Create a free account on [[https://www.ibm.com/cloud/watson-studio][IBM Watson Studio]].
26282. Add the [[https://www.ibm.com/cloud/watson-visual-recognition][Watson Visual Recognition]] service to your IBM Watson account.
26293. Get your API key and URL (uniform resource locator). To do this, first go to
2630 the [[https://dataplatform.cloud.ibm.com/home2?context=cpdaas][profile dashboard]] for your IBM account and click on the Watson Visual
2631 Recognition service you created. This will be listed in the section titled
2632 *Your services*. Then click the *Credentials* tab and open the
2633 *Auto-generated credentials* dropdown. Copy your API key and URL so that you
2634 can use them in the Python script later.
26354. *[Optional]* While not required, you can also create the Jupyter Notebook for
2636 this project right inside [[https://www.ibm.com/cloud/watson-studio][Watson Studio]]. Watson Studio will save your
2637 notebooks inside an organized project and allow you to use their other
2638 integrated products, such as storage containers, AI models, documentation,
2639 external sharing, etc.
2640
2641* Calling the IBM Watson Visual Recognition API
2642
2643Okay, now let's get started.
2644
2645To begin, we need to install the proper Python package for IBM Watson.
2646
2647#+begin_src sh
2648pip install --upgrade --user "ibm-watson>=4.5.0"
2649#+end_src
2650
2651Next, we need to specify the API key, version, and URL given to us when we
2652created the Watson Visual Recognition service.
2653
2654#+begin_src python
2655apikey = "<your-apikey>"
2656version = "2018-03-19"
2657url = "<your-url>"
2658#+end_src
2659
2660Now, let's import the necessary libraries and authenticate our service.
2661
2662#+begin_src python
2663import json
2664from ibm_watson import VisualRecognitionV3
2665from ibm_cloud_sdk_core.authenticators import IAMAuthenticator
2666
2667authenticator = IAMAuthenticator(apikey)
2668visual_recognition = VisualRecognitionV3(
2669 version=version,
2670 authenticator=authenticator
2671)
2672
2673visual_recognition.set_service_url(url)
2674#+end_src
2675
2676*[Optional]* If you'd like to tell the API not to use any data to improve their
2677products, set the following header.
2678
2679#+begin_src python
2680visual_recognition.set_default_headers({'x-watson-learning-opt-out': "true"})
2681#+end_src
2682
2683Now we have our API all set and ready to go. For this example, I'm going to
2684include a =dict= of photos to load as we test out the API.
2685
2686#+begin_src python
2687data = [
2688 {
2689 "title": "Grizzly Bear",
2690 "url": "https://example.com/photos/image1.jpg"
2691 },
2692 {
2693 "title": "Nature Lake",
2694 "url": "https://example.com/photos/image2.jpg"
2695 },
2696 {
2697 "title": "Welcome Sign",
2698 "url": "https://example.com/photos/image3.jpg"
2699 },
2700 {
2701 "title": "Honey Badger",
2702 "url": "https://example.com/photos/image4.jpg"
2703 },
2704 {
2705 "title": "Grand Canyon Lizard",
2706 "url": "https://example.com/photos/image5.jpg"
2707 },
2708 {
2709 "title": "Castle",
2710 "url": "https://example.com/photos/image6.jpg"
2711 }
2712]
2713#+end_src
2714
2715Now that we've set up our libraries and have the photos ready, let's create a
2716loop to call the API for each image. The code below shows a loop that calls the
2717URL of each image and sends it to the API, requesting results with at least 60%
2718confidence. The results are output to the console with dotted lines separating
2719each section.
2720
2721In the case of an API error, the codes and explanations are output to the
2722console.
2723
2724#+begin_src python
2725from ibm_watson import ApiException
2726
2727for x in range(len(data)):
2728try:
2729 url = data[x]["url"]
2730 images_filename = data[x]["title"]
2731 classes = visual_recognition.classify(
2732 url=url,
2733 images_filename=images_filename,
2734 threshold='0.6',
2735 owners=["IBM"]).get_result()
2736 print("-----------------------------------------------")
2737 print("Image Title: ", data[x]["title"], "\n")
2738 print("Image URL: ", data[x]["url"], "\n")
2739 classification_results = classes["images"][0]["classifiers"][0]["classes"]
2740 for result in classification_results:
2741 print(result["class"], "(", result["score"], ")")
2742 print("-----------------------------------------------")
2743except ApiException as ex:
2744 print("Method failed with status code " + str(ex.code) + ": " + ex.message)
2745#+end_src
2746
2747* The Results
2748
2749Here we can see the full result set of our function above. If you view each of
2750the URLs that we sent to the API, you'll be able to see that it was remarkably
2751accurate. To be fair, these are clear high-resolution, clear photos shot with a
2752professional camera. In reality, you will most likely be processing images that
2753are lower quality and may have a lot of noise in the photo.
2754
2755However, we can clearly see the benefit of being able to call this API instead
2756of attempting to write our own image recognition function. Each of the
2757classifications returned was a fair description of the image.
2758
2759If you wanted to restrict the results to those that are at least 90% confident
2760or greater, you would simply adjust the =threshold= in the
2761=visual_recognition.classify()= function.
2762
2763When your program runs, it should show the output below for each photo you
2764provide.
2765
2766#+begin_src txt
2767----------------------------------------------------------------
2768Image Title: Grizzly Bear
2769Image URL: https://example.com/photos/image1.jpg
2770
2771brown bear ( 0.944 )
2772bear ( 1 )
2773carnivore ( 1 )
2774mammal ( 1 )
2775animal ( 1 )
2776Alaskan brown bear ( 0.759 )
2777greenishness color ( 0.975 )
2778----------------------------------------------------------------
2779#+end_src
2780
2781* Discussion
2782
2783Now, this was a very minimal implementation of the API. We simply supplied some
2784images and looked to see how accurate the results were. However, you could
2785implement this type of API into many machine learning (ML) models.
2786
2787For example, you could be working for a company that scans their warehouses or
2788inventory using drones. Would you want to pay employees to sit there and watch
2789drone footage all day in order to identify or count things in the video?
2790Probably not. Instead, you could use a classification system similar to this one
2791in order to train your machine learning model to correctly identify items that
2792the drones show through video. More specifically, you could have your machine
2793learning model watch a drone fly over a field of sheep in order to count how
2794many sheep are living in that field.
2795
2796There are many ways to implement machine learning functionality, but hopefully
2797this post helped inspire some deeper thought about the tools that can help
2798propel us further into the future of machine learning and AI.
2799#+date: <2020-09-22 Tue 00:00:00>
2800#+title: The Strategic Role of Internal Audit in Risk, Governance, and Compliance
2801#+description: Analysis of internal audit processes, their contribution to risk management, governance frameworks, and compliance monitoring within corporate structures.
2802#+slug: internal-audit
2803
2804* Definitions
2805
2806One of the many reasons that Internal Audit needs such thorough explaining to
2807non-auditors is that Internal Audit can serve many purposes, depending on the
2808organization's size and needs. However, the Institute of Internal Auditors (IIA)
2809defines Internal Auditing as:
2810
2811#+begin_quote
2812Internal auditing is an independent, objective assurance and consulting activity
2813designed to add value and improve an organization's operations. It helps an
2814organization accomplish its objectives by bringing a systematic, disciplined
2815approach to evaluate and improve the effectiveness of risk management, control,
2816and governance processes.
2817#+end_quote
2818
2819However, this definition uses quite a few terms that aren't clear unless the
2820reader already has a solid understanding of the auditing profession. To further
2821explain, the following is a list of definitions that can help supplement
2822understanding of internal auditing.
2823
2824** Independent
2825
2826Independence is the freedom from conditions that threaten the ability of the
2827internal audit activity to carry out internal audit responsibilities in an
2828unbiased manner. To achieve the degree of independence necessary to effectively
2829carry out the responsibilities of the internal audit activity, the chief audit
2830executive has direct and unrestricted access to senior management and the board.
2831This can be achieved through a dual-reporting relationship. Threats to
2832independence must be managed at the individual auditor, engagement, functional,
2833and organizational levels.
2834
2835** Objective
2836
2837Objectivity is an unbiased mental attitude that allows internal auditors to
2838perform engagements in such a manner that they believe in their work product and
2839that no quality compromises are made. Objectivity requires that internal
2840auditors do not subordinate their judgment on audit matters to others. Threats
2841to objectivity must be managed at the individual auditor, engagement,
2842functional, and organizational levels.
2843
2844** Assurance
2845
2846Assurance services involve the internal auditor's objective assessment of
2847evidence to provide opinions or conclusions regarding an entity, operation,
2848function, process, system, or other subject matters. The internal auditor
2849determines the nature and scope of an assurance engagement. Generally, three
2850parties are participants in assurance services: (1) the person or group directly
2851involved with the entity, operation, function, process, system, or other
2852subject - (the process owner), (2) the person or group making the assessment -
2853(the internal auditor), and (3) the person or group using the assessment - (the
2854user).
2855
2856** Consulting
2857
2858Consulting services are advisory in nature and are generally performed at the
2859specific request of an engagement client. The nature and scope of the consulting
2860engagement are subject to agreement with the engagement client. Consulting
2861services generally involve two parties: (1) the person or group offering the
2862advice (the internal auditor), and (2) the person or group seeking and receiving
2863the advice (the engagement client). When performing consulting services, the
2864internal auditor should maintain objectivity and not assume management
2865responsibility.
2866
2867** Governance, Risk Management, & Compliance (GRC)
2868
2869The integrated collection of capabilities that enable an organization to
2870reliably achieve objectives, address uncertainty and act with integrity.
2871
2872* Audit Charter & Standards
2873
2874First, it's important to note that not every organization needs internal
2875auditors. In fact, it's unwise for an organization to hire internal auditors
2876unless they have regulatory requirements for auditing and have the capital to
2877support the department. Internal audit is a cost center that can only affect
2878revenue indirectly.
2879
2880Once an organization determines the need for internal assurance services, they
2881will hire a Chief Audit Executive and create the audit charter. This charter is
2882a document, approved by the company's governing body, that will define internal
2883audit's purpose, authority, responsibility, and position within the
2884organization. Fortunately, the IIA has model charters available to IIA members
2885for those developing or improving their charter.
2886
2887Beyond the charter and organizational documents, internal auditors follow a few
2888different standards in order to perform their job. First is the International
2889Professional Practices Framework (IPPF) by the IIA, which is the model of
2890standards for internal auditing. In addition, ISACA's Information Technology
2891Assurance Framework (ITAF) helps guide auditors in reference to information
2892technology (IT) compliance and assurance. Finally, additional standards such as
2893FASB, GAAP, and industry-specific standards are used when performing internal
2894audit work.
2895
2896* Three Lines of Defense
2897
2898[[https://theiia.org][The IIA]] released the original Three Lines of Defense model in 2013, but have
2899released an updated version in 2020.
2900
2901I won't go into depth about the changes made to the model in this article.
2902Instead, let's take a look at the most current model.
2903
2904The updated model forgets the strict idea of areas performing their own
2905functions or line of defense. Instead of talking about management, risk, and
2906internal audit as 1-2-3, the new model creates a more fluid and cooperative
2907model.
2908
2909Looking at this model from an auditing perspective shows us that auditors will
2910need to align, communicate, and collaborate with management, including business
2911area managers and chief officers, as well as reporting to the governing body.
2912The governing body will instruct internal audit /functionally/ on their goals
2913and track their progress periodically.
2914
2915However, the internal audit department will report /administratively/ to a chief
2916officer in the company for the purposes of collaboration, direction, and
2917assistance with the business. Note that in most situations, the governing body
2918is the audit committee on the company's board of directors.
2919
2920The result of this structure is that internal audit is an independent and
2921objective function that can provide assurance over the topics they audit.
2922
2923* Audit Process
2924
2925A normal audit will generally follow the same process, regardless of the topic.
2926However, certain special projects or abnormal business areas may call for
2927changes to the audit process. The audit process is not set in stone, it's simply
2928a set of best practices so that audits can be performed consistently.
2929
2930While different organizations may tweak the process, it will generally follow
2931this flow:
2932
2933** 1. Risk Assessment
2934
2935The risk assessment part of the process has historically been performed
2936annually, but many organizations have moved to performing this process much more
2937frequently. In fact, some organizations are moving to an agile approach that can
2938take new risks into the risk assessment and re-prioritize risk areas on-the-go.
2939To perform a risk assessment, leaders in internal audit will research industry
2940risks, consult with business leaders around the company, and perform analyses on
2941company data.
2942
2943Once a risk assessment has been documented, the audit department has a
2944prioritized list of risks that can be audited. This is usually in the form of
2945auditable entities, such as business areas or departments.
2946
2947** 2. Planning
2948
2949During the planning phase of an audit, auditors will meet with the business area
2950to discuss the various processes, controls, and risks applicable to the
2951business. This helps the auditors determine the scope limits for the audit, as
2952well as timing and subject-matter experts. Certain documents will be created in
2953this phase that will be used to keep the audit on-track an in-scope as it goes
2954forward.
2955
2956** 3. Testing
2957
2958The testing phase, also known as fieldwork or execution, is where internal
2959auditors will take the information they've discovered and test it against
2960regulations, industry standards, company rules, best practices, as well as
2961validating that any processes are complete and accurate. For example, an audit
2962of HR would most likely examine processes such as employee on-boarding, employee
2963termination, security of personally identifiable information (PII), or the IT
2964systems involved in these processes. Company standards would be examined and
2965compared against how the processes are actually being performed day-to-day, as
2966well as compared against regulations such as the Equal Employment Opportunity
2967(EEO), American with Disabilities Act, and National Labor Relations Act.
2968
2969** 4. Reporting
2970
2971Once all the tests have been completed, the audit will enter the reporting
2972phase. This is when the audit team will conclude on the evidence they've
2973collected, interviews they've held, and any opinions they've formed on the
2974controls in place. A summary of the audit findings, conclusions, and specific
2975recommendations are officially communicated to the client through a draft
2976report. Clients have the opportunity to respond to the report and submit an
2977action plan and time frame. These responses become part of the final report
2978which is distributed to the appropriate level of administration.
2979
2980** 5. Follow-Up
2981
2982After audits have been completed and management has formed action plans and time
2983frames for audit issues, internal audit will follow up once that due date has
2984arrived. In most cases, the follow-up will simply consist of a meeting to
2985discuss how the action plan has been completed and to request documentation to
2986prove it.
2987
2988* Audit Department Structure
2989
2990While an internal audit department is most often thought of as a team of
2991full-time employees, there are actually many different ways in which a
2992department can be structured. As the world becomes more digital and fast-paced,
2993outsourcing has become a more attractive option for some organizations. Internal
2994audit can be fully outsourced or partially outsourced, allowing for flexibility
2995in cases where turnover is high.
2996
2997In addition, departments can implement a rotational model. This allows for
2998interested employees around the organization to rotate into the internal audit
2999department for a period of time, allowing them to obtain knowledge of risks and
3000controls and allowing the internal audit team to obtain more business area
3001knowledge. This program is popular in very large organizations, but
3002organizations tend to rotate lower-level audit staff instead of managers. This
3003helps prevent any significant knowledge loss as auditors rotate out to business
3004areas.
3005
3006* Consulting
3007
3008Consulting is not an easy task at any organization, especially for a department
3009that can have negative perceptions within the organization as the "compliance
3010police." However, once an internal audit department has delivered value to
3011organization, adding consulting to their suite of services is a smart move. In
3012most cases, Internal Audit can insert themselves into a consulting role without
3013affecting the process of project management at the company. This means that
3014internal audit can add objective assurance and opinions to business areas as
3015they develop new processes, instead of coming in periodically to audit an area
3016and file issues that could have been fixed at the beginning.
3017
3018* Data Science & Data Analytics
3019
3020One major piece of the internal audit function in the modern world is data
3021science. While the process is data science, most auditors will refer to anything
3022in this realm as data analytics. Hot topics such as robotic process automation
3023(RPA), machine learning (ML), and data mining have taken over the auditing world
3024in recent years. These technologies have been immensely helpful with increasing
3025the effectiveness and efficiency of auditors.
3026
3027For example, mundane and repetitive tasks can be automated in order for auditors
3028to make more room in their schedules for labor-intensive work. Further, auditors
3029will need to adapt technologies like machine learning in order to extract more
3030value from the data they're using to form conclusions.
3031#+date: <2020-09-25 Fri 00:00:00>
3032#+title: Visualizing Global Happiness: A Choropleth Map with Python and Folium
3033#+description: Instructions for creating an interactive choropleth map visualizing international happiness data based on metrics such as GDP, social support, health indicators, freedom indices, generosity, and corruption levels.
3034#+slug: happiness-map
3035
3036* Background Information
3037
3038The dataset (obtained from [[https://www.kaggle.com/unsdsn/world-happiness][Kaggle]]) used in this article contains a list of
3039countries around the world, their happiness rankings and scores, as well as
3040other national scoring measures.
3041
3042Fields include:
3043
3044- Overall rank
3045- Country or region
3046- GDP per capita
3047- Social support
3048- Healthy life expectancy
3049- Freedom to make life choices
3050- Generosity
3051- Perceptions of corruption
3052
3053There are 156 records. Since there are ~195 countries in the world, we can see
3054that around 40 countries will be missing from this dataset.
3055
3056* Install Packages
3057
3058As always, run the =install= command for all packages needed to perform
3059analysis.
3060
3061#+begin_src python
3062!pip install folium geopandas matplotlib numpy pandas
3063#+end_src
3064
3065* Import the Data
3066
3067We only need a couple packages to create a choropleth map. We will use [[https://python-visualization.github.io/folium/][Folium]],
3068which provides map visualizations in Python. We will also use geopandas and
3069pandas to wrangle our data before we put it on a map.
3070
3071#+begin_src python
3072# Import the necessary Python packages
3073import folium
3074import geopandas as gpd
3075import pandas as pd
3076#+end_src
3077
3078To get anything to show up on a map, we need a file that will specify the
3079boundaries of each country. Luckily, GeoJSON files exist (for free!) on the
3080internet. To get the boundaries of every country in the world, we will use the
3081GeoJSON link shown below.
3082
3083GeoPandas will take this data and load it into a dataframe so that we can easily
3084match it to the data we're trying to analyze. Let's look at the GeoJSON
3085dataframe:
3086
3087#+begin_src python
3088# Load the GeoJSON data with geopandas
3089geo_data = gpd.read_file('https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson')
3090geo_data.head()
3091#+end_src
3092
3093Next, let's load the data from the Kaggle dataset. I've downloaded this file, so
3094update the file path if you have it somewhere else. After loading, let's take a
3095look at this dataframe:
3096
3097#+begin_src python
3098# Load the world happiness data with pandas
3099happy_data = pd.read_csv(r'~/Downloads/world_happiness_data_2019.csv')
3100happy_data.head()
3101#+end_src
3102
3103* Clean the Data
3104
3105Some countries need to be renamed, or they will be lost when you merge the
3106happiness and GeoJSON dataframes. This is something I discovered when the map
3107below showed empty countries. I searched both data frames for the missing
3108countries to see the naming differences. Any countries that do not have records
3109in the =happy_data= data frame will not show up on the map.
3110
3111#+begin_src python
3112# Rename some countries to match our GeoJSON data
3113
3114# Rename USA
3115usa_index = happy_data.index[happy_data['Country or region'] == 'United States']
3116happy_data.at[usa_index, 'Country or region'] = 'United States of America'
3117
3118# Rename Tanzania
3119tanzania_index = happy_data.index[happy_data['Country or region'] == 'Tanzania']
3120happy_data.at[tanzania_index, 'Country or region'] = 'United Republic of Tanzania'
3121
3122# Rename the Congo
3123republic_congo_index = happy_data.index[happy_data['Country or region'] == 'Congo (Brazzaville)']
3124happy_data.at[republic_congo_index, 'Country or region'] = 'Republic of Congo'
3125
3126# Rename the DRC
3127democratic_congo_index = happy_data.index[happy_data['Country or region'] == 'Congo (Kinshasa)']
3128happy_data.at[democratic_congo_index, 'Country or region'] = 'Democratic Republic of the Congo'
3129#+end_src
3130
3131* Merge the Data
3132
3133Now that we have clean data, we need to merge the GeoJSON data with the
3134happiness data. Since we've stored them both in dataframes, we just need to call
3135the =.merge()= function.
3136
3137We will also rename a couple columns, just so that they're a little easier to
3138use when we create the map.
3139
3140#+begin_src python
3141# Merge the two previous dataframes into a single geopandas dataframe
3142merged_df = geo_data.merge(happy_data,left_on='ADMIN', right_on='Country or region')
3143
3144# Rename columns for ease of use
3145merged_df = merged_df.rename(columns = {'ADMIN':'GeoJSON_Country'})
3146merged_df = merged_df.rename(columns = {'Country or region':'Country'})
3147#+end_src
3148
3149* Create the Map
3150
3151The data is finally ready to be added to a map. The code below shows the
3152simplest way to find the center of the map and create a Folium map object. The
3153important part is to remember to reference the merged dataframe for our GeoJSON
3154data and value data. The columns specify which geo data and value data to use.
3155
3156#+begin_src python
3157# Assign centroids to map
3158x_map = merged_df.centroid.x.mean()
3159y_map = merged_df.centroid.y.mean()
3160print(x_map,y_map)
3161
3162# Creating a map object
3163world_map = folium.Map(location=[y_map, x_map], zoom_start=2,tiles=None)
3164folium.TileLayer('CartoDB positron',name='Dark Map',control=False).add_to(world_map)
3165
3166# Creating choropleth map
3167folium.Choropleth(
3168 geo_data=merged_df,
3169 name='Choropleth',
3170 data=merged_df,
3171 columns=['Country','Overall rank'],
3172 key_on='feature.properties.Country',
3173 fill_color='YlOrRd',
3174 fill_opacity=0.6,
3175 line_opacity=0.8,
3176 legend_name='Overall happiness rank',
3177 smooth_factor=0,
3178 highlight=True
3179).add_to(world_map)
3180#+end_src
3181
3182Let's look at the resulting map.
3183
3184* Create a Tooltip on Hover
3185
3186Now that we have a map set up, we could stop. However, I want to add a tooltip
3187so that I can see more information about each country. The =tooltip_data= code
3188below will show a popup on hover with all the data fields shown.
3189
3190#+begin_src python
3191 # Adding labels to map
3192 style_function = lambda x: {'fillColor': '#ffffff',
3193 'color':'#000000',
3194 'fillOpacity': 0.1,
3195 'weight': 0.1}
3196
3197tooltip_data = folium.features.GeoJson(
3198 merged_df,
3199 style_function=style_function,
3200 control=False,
3201 tooltip=folium.features.GeoJsonTooltip(
3202 fields=['Country'
3203 ,'Overall rank'
3204 ,'Score'
3205 ,'GDP per capita'
3206 ,'Social support'
3207 ,'Healthy life expectancy'
3208 ,'Freedom to make life choices'
3209 ,'Generosity'
3210 ,'Perceptions of corruption'
3211 ],
3212 aliases=['Country: '
3213 ,'Happiness rank: '
3214 ,'Happiness score: '
3215 ,'GDP per capita: '
3216 ,'Social support: '
3217 ,'Healthy life expectancy: '
3218 ,'Freedom to make life choices: '
3219 ,'Generosity: '
3220 ,'Perceptions of corruption: '
3221 ],
3222 style=('background-color: white; color: #333333; font-family: arial; font-size: 12px; padding: 10px;')
3223 )
3224)
3225world_map.add_child(tooltip_data)
3226world_map.keep_in_front(tooltip_data)
3227folium.LayerControl().add_to(world_map)
3228
3229# Display the map
3230world_map
3231#+end_src
3232
3233The tooltip will now appear whenever you hover over a country.
3234#+date: <2020-10-12 Mon 00:00:00>
3235#+title: When Good Is Good Enough—And When It Isn't
3236#+description: Examination of decision-making principles relating to achieving sufficient quality levels, including trade-offs between perfection and adequacy in professional and operational contexts.
3237#+slug: mediocrity
3238
3239* Perfect is the Enemy of Good
3240
3241As the saying goes, "the best is the enemy of the good." As we strive for
3242perfection, we often fail to realize the implications of such an undertaking.
3243Attempting to reach perfection is often unrealistic. Even worse, it can get in
3244the way of achieving a good outcome. In certain situations, we try so hard to
3245achieve the ideal solution that we have burned the bridges that would have
3246allowed us to reach a lesser yet still superb solution.
3247
3248Philosophers throughout history have inspected this plight from many viewpoints.
3249Greek mythology speaks of the [[https://en.wikipedia.org/wiki/Golden_mean_(philosophy)][golden mean]], which uses the story of Icarus to
3250illustrate that sometimes "the middle course" is the best solution. In this
3251story, Daedalus, a famous artist of his time, built feathered wings for himself
3252and his son so that they might escape the clutches of King Minos. Daedalus warns
3253his beloved son whom he loved so much to "fly the middle course", between the
3254sea spray and the sun's heat. Icarus did not heed his father; he flew up and up
3255until the sun melted the wax off his wings. For not heeding the middle course,
3256he fell into the sea and drowned.
3257
3258More recently, management scholars have explored the [[https://en.wikipedia.org/wiki/Pareto_principle][Pareto principle]] and found
3259that as we increase the frequency of something, or strive to perform actions to
3260achieve some form of perfection, we run into [[https://en.wikipedia.org/wiki/Diminishing_returns][diminishing returns]].
3261
3262Even further, Harold Demsetz is noted as coining the term [[https://en.wikipedia.org/wiki/Nirvana_fallacy][the Nirvana fallacy]] in
32631969, which shows the fallacy of comparing actual things with unrealistic,
3264idealized alternatives. This is another trap that we may fall into, where we are
3265constantly thinking of the ultimate solutions to problems, when something more
3266realistic needs to be considered.
3267
3268Over and over throughout history, we've found that perfection is often
3269unrealistic and unachievable. However, we push ourselves and our peers to "give
3270100%" or "go the extra mile," while it may be that the better course is to give
3271a valuable level of effort while considering the effects of further effort on
3272the outcome. Working harder does not always help us achieve loftier goals.
3273
3274This has presented itself to me most recently during my time studying at my
3275university. I was anxious and feeling the stresses of my courses, career, and
3276personal life for quite a while, which was greatly affecting how well I was
3277doing at school and my level of effort at work. One day, I happened to be
3278talking to my father when he said something simple that hit home:
3279
3280#+begin_quote
3281All you can do is show up and do your best. Worrying about the outcomes won't
3282affect the outcome itself.
3283#+end_quote
3284
3285The thought was extremely straightforward and uncomplicated, yet it was
3286something that I had lost sight of during my stress-filled years at school. Ever
3287since then, I've found myself pausing and remembering that quote every time I
3288get anxious or stressed. It helps to stop and think "Can I do anything to affect
3289the outcome, or am I simply worrying over something I can't change?"
3290
3291* When Mediocrity Isn't Enough
3292
3293One problem with the philosophies presented in this post is that they are
3294implemented far too often in situations where mediocrity simply isn't adequate.
3295For example, let's take a look at digital user data, specifically
3296personally-identifiable information (PII). As a cybersecurity auditor in the
3297United States, I have found that most companies are concerned more with
3298compliance than any actual safeguards over the privacy or protection of user
3299data. Other than companies who have built their reputation on privacy and
3300security, most companies will use [[https://en.wikipedia.org/wiki/Satisficing][satisficing]] as their primary decision-making
3301strategy around user data.
3302
3303#+begin_quote
3304Satisficing is a decision-making strategy or cognitive heuristic that entails
3305searching through the available alternatives until an acceptability threshold is
3306met.
3307#+end_quote
3308
3309This means that each decision will be met with certain possible solutions until
3310one of the solutions meets their minimum acceptable standards. For companies
3311that deal with user data, the minimum-acceptable standards come from three
3312areas:
3313
33141. Laws and regulations
33152. Competitive pressure
33163. Risk of monetary or reputation loss
3317
3318Working with project management or auditing, the primary concern here is the
3319risk of legal ramifications. Since the primary risk comes from laws and
3320regulations, companies will require that any project that involves user data
3321must follow all the rules of those laws so that the company can protect itself
3322from fines or other penalties.
3323
3324Following this, companies will consider best practices in order to place itself
3325in a competitive position (e.g. Google vs. Apple) and review any recent or
3326ongoing litigation against companies regarding user data. In a perfect company,
3327management would then consider the ethical responsibilities of their
3328organization and discuss their responsibilities over things like
3329personally-identifiable information.
3330
3331However, as we mentioned above, most companies follow the idea of satisficing,
3332which states that they have met the minimum acceptable standards and can now
3333move on to other decisions. Modern business culture in the United States
3334dictates that profits are the golden measure of how well a company or manager is
3335performing, so we often don't think about our responsibilities beyond these
3336basic standards.
3337
3338Not all situations demand excellence, but I believe that applying any philosophy
3339as a broad stroke across one's life can be a mistake. We must be able to think
3340critically about what we are doing as we do it and ask ourselves a few
3341questions. Have I done everything I can in this situation? Is mediocrity an
3342acceptable outcome, or should we strive for perfection, even if we can't attain
3343it?
3344
3345Taking a few moments to think critically throughout our day, as we make
3346decisions, can have a tremendous effect on the outcomes we create.
3347#+date: <2020-12-27 Sun 00:00:00>
3348#+title: Designing for Speed: A 5KB Site That Scores 100s on Lighthouse
3349#+description: Detailed process outlining methods for reducing website page size to approximately 5 kilobytes, enhancing loading speed, search engine indexing efficiency, and user interface responsiveness.
3350#+slug: website-redesign
3351
3352* A Brief History
3353
3354As a form of continuous learning and entertainment, I've been running a handful
3355of websites since 2016 when I took my first programming courses in college. I
3356maintain one main website, the place I consider the official website to
3357represent me. Under this site, I have a handful of subdirectories and
3358subdomains.
3359
3360One of the parts I've enjoyed the most about web development is the aspect of
3361designing an identity for a web page and working to find exciting ways to
3362display the site's content. Inevitably, this means I've changed the designs for
3363my websites more times than I could possibly count. Since I don't really host
3364anything on my main webpage that's vital, it allows me the freedom to change
3365things as inspiration strikes.
3366
3367Historically, I've relied on core utilities for spacing, components, and layouts
3368from [[https://getbootstrap.com][Bootstrap]] and added custom cascading style sheets (CSS) for fonts, accents,
3369colors, and other items. I also tend to create sites with no border radius on
3370items, visible borders, and content that takes up the entire screen (using
3371whitespace inside components instead of whitespace around my components).
3372
3373* The Redesign Process
3374
3375About a week ago, I found myself wishing for a new design yet again. The prior
3376design was largely inspired by IBM's [[https://www.carbondesignsystem.com][Carbon Design System]] and relied on jQuery,
3377Bootstrap, along with some compressed [[https://en.wikipedia.org/wiki/WebP][.webp]] images.
3378
3379To anyone who knows my preferences toward web design - and even in my personal
3380life - it should be no surprise that I immediately started looking for
3381inspiration on minimalism. While there are some decent minimalistic designs on
3382sites like [[https://dribbble.com/search/shots/popular/web-design?q=minimalism][Dribbble]], people seem to mostly discuss [[https://brutalist-web.design][brutalist web design]] when you
3383ask about minimalism. While brutalist web design doesn't have to be minimal, it
3384often is.
3385
3386I suppose, in a way, I did create a brutalist website since my HTML is semantic
3387and accessible, hyperlinks are colored and underlined, and all native browser
3388functions like scrolling and the back button work as expected. However, I didn't
3389think about brutalism while designing these sites.
3390
3391The new design followed a simple design process. I walked through the screens on
3392my blog and asked myself: "Is this element necessary for a user?" This allowed
3393me to first start by removing all javascript, which had the sole purpose of
3394allowing users to open a collapsed navbar on mobile. Replacing the collapsible
3395navbar allowed me to remove both jQuery and Bootstrap's javascript.
3396
3397Next, I removed things like author names (since I'm literally the only person
3398who will ever write on this site), multiple ways to click on a blog post card,
3399blog post descriptions, and the scroll-to-top button. It also helped to move all
3400categories to a single page, rather than have each category on its own page.
3401
3402The final big piece to finish the "[[https://en.wikipedia.org/wiki/Marie_Kondo#KonMari_method][KonMari]]"-like part of my process was to
3403remove Bootstrap CSS in its entirety. However, this meant pulling out a few very
3404useful classes, such as =.img-fluid= and the default font stacks to keep in my
3405custom CSS.
3406
3407After removing all the unnecessary pieces, I was finally able to reorganize my
3408content and add a very small amount of custom CSS to make everything pretty.
3409This took a brief amount of time, effectively just consisting of me converting
3410=<div>= tags into things like =<ul>= lists and choosing accent colors.
3411
3412* The Results
3413
3414** Reflection
3415
3416So, what did all of this reorganizing do to my webpages? Well, first, my
3417websites are now *ridiculously fast*. Since the prior designs were also minimal
3418and didn't have many images, they measured up in Firefox's Network Monitor
3419around 300 KB - 600KB. After making the changes, my main site is at 5 KB
3420transferred (22 KB total), and my blog is at 6.5 KB transferred (13 KB total).
3421*That means the redesigned pages are less than 2% the size of the old designs.*
3422
3423Google Lighthouse ranks the new webpage as 100 in performance, accessibility,
3424and best practices, with search engine optimization (SEO) at 92 since they think
3425tap targets are not sized appropriately for mobile users. First contextual
3426paints of the pages are under 0.8 seconds with 0 ms (millisecond) of blocking
3427time. However, the blog subdomain ranks at 100 for all four categories! First
3428contextual paints of the blog homepage are under 1.0 seconds with 0 ms of
3429blocking time, due to the fact that the CSS for my blog is within a separate CSS
3430file, and the CSS for my main website is simply embedded in the HTML file.
3431
3432Now that everything is complete, I can confidently say I'm happy with the result
3433and proud to look at the fastest set of websites I've created so far.
3434#+date: <2020-12-28 Mon 00:00:00>
3435#+title: Outrun the Future: A Review of Neon Drive's Gameplay and Style
3436#+description: Description of game mechanics, graphical style, and level design in Neon Drive, an arcade racing game inspired by 1980s synthwave aesthetics.
3437#+slug: neon-drive
3438
3439* Game Description
3440
3441[[https://store.steampowered.com/app/433910/Neon_Drive/][Neon Drive]] presents itself as a simple arcade-style game inspired by the arcade
3442race games of the 1980s, yet it has managed to take up hours of my life without
3443much effort. The game description, directly from the Steam page, is intriguing
3444enough to entice anyone who's been looking for a good arcade racing game:
3445
3446#+begin_quote
3447Neon Drive is a slick retro-futuristic arcade game that will make your brain
3448melt. You've been warned. From beautiful cityscapes and ocean roads to exploding
3449enemy spaceships, Neon Drive has it all.
3450#+end_quote
3451
3452* Gameplay
3453
3454The game holds true to the [[https://en.wikipedia.org/wiki/Retrofuturism][retro-futurism]] style, including chrome female robots,
3455pixelated arcade machines, and [[https://teddit.net/r/outrun/][outrun]] aesthetics.
3456
3457Each level of the game is shown as a separate arcade machine. Each arcade
3458machine lets you play on Normal, Hard, Insane, Practice, and Free Run. To beat
3459each arcade, you must reach the end of the level without crashing your car into
3460the various obstacles on the course. Basic levels let you move left or right to
3461avoid blocks in the road. Later levels put you through other tests, such as
3462dodging traffic or blasting asteroids.
3463
3464The game uses synthwave music to keep you on track to make the correct moves by
3465timing the beats of the songs to the correct moves on the screen. It reminds me
3466of the early Guitar Hero games, as well as mobile apps like VOEZ - repetition
3467and staying on-beat is the only way to win.
3468
3469* In-Game Screenshots
3470
3471Taking a look at the main menu, you can see that Neon Drive plays into every
3472stereotype you can think of around retro-futuristic, synthwave arcades (in a
3473good way).
3474
3475Once you get into the first level, we see that the choice of car fits right in
3476with the stereotypical cars of the 80s, like the [[https://en.wikipedia.org/wiki/DMC_DeLorean][DeLorean]] or the [[https://en.wikipedia.org/wiki/Ferrari_F40][Ferrari F40]].
3477Each new level comes with new color schemes and cars, so you should never get
3478tired of the aesthetic.
3479
3480Personally, I love the orange and blue colors used in level 2:
3481
3482If you're the competitive type and getting 100% on all arcade machines isn't
3483enough, there are leaderboards for the regular part of the game, and the
3484endurance game mode.
3485
3486* Other Suggestions
3487
3488Neon Drive sits nicely within the well-founded cult genre of Outrun. Other games
3489that I've enjoyed in this same spectrum are:
3490
3491- [[https://store.steampowered.com/app/233270/Far_Cry_3__Blood_Dragon/][Far Cry 3: Blood Dragon]]
3492- [[https://store.steampowered.com/app/1239690/Retrowave/][Retrowave]]
3493- [[https://store.steampowered.com/app/732810/Slipstream/][Slipstream]]
3494
3495Although these games aren't necessarily in the same genre, they do have aspects
3496that place them close enough to interest gamers that enjoyed Neon Drive:
3497
3498- [[https://store.steampowered.com/app/311800/Black_Ice/][Black Ice]]
3499- [[https://store.steampowered.com/app/746850/Cloudpunk/][Cloudpunk]]
3500- [[https://store.steampowered.com/app/1222680/Need_for_Speed_Heat/][Need for Speed: Heat]]
3501- [[https://store.steampowered.com/app/1019310/VirtuaVerse/][VirtuaVerse]]
3502
3503Of course, if all you really care about is the arcade aspect of these games, you
3504can check out the [[https://store.steampowered.com/app/400020/Atari_Vault/][Atari Vault]] or any of the other classic games sold on Steam by
3505companies like Namco, Atari. For something like Nintendo, you'd have to settle
3506for buying used classic consoles or delve into the world of emulation.
3507#+date: <2020-12-29 Tue 00:00:00>
3508#+title: Your First Grue: Getting Started with Zork, the Iconic Text Adventure
3509#+description: Detailed operational guide for navigating Zork, a text-based interactive fiction game from the 1980s. Includes instructions for exploration, puzzle resolution, and item collection.
3510#+slug: zork
3511
3512* Download (Free)
3513
3514Before we get into the game itself, you should know that you can download Zork
3515for free from Infocom's [[http://infocom-if.org/downloads/downloads.html][download page]]. So feel free to boot it up and take a
3516ride back to the 1980s with this masterpiece.
3517
3518* Game Description
3519
3520Zork is an interactive, text-based computer game originally released in 1980.
3521This series, split into three separate games, introduced a robust and
3522sophisticated text parser to gamers. People were largely used to the simple
3523commands used in the popular game [[https://en.wikipedia.org/wiki/Colossal_Cave_Adventure][Colossal Cave Adventure]], but Zork allowed
3524users to send more complex commands that included prepositions and conjunctions.
3525
3526Zork tracks your score as you explore the map, find tools, and collect trophy
3527items (e.g., a jewel-encrusted egg). When you place your trophy items in the
3528trophy case found in the Living Room area, you gain score points. Collecting the
3529Twenty Treasures of Zork and placing them within the trophy case wins the game.
3530However, you must explore the map, solve puzzles, and avoid being eaten by a
3531grue to collect these treasures.
3532
3533* The Map
3534
3535Since Zork is a vast and complex game, it helps to have a map as you
3536explore and collect your trophies. However, if you want to play the game
3537as it was truly intended, you should try to play it without using the
3538map.
3539
3540#+caption: Zork Map
3541#+attr_html: :alt A map for Zork, showing passageways, restrictions, and all locations.
3542[[https://img.cleberg.net/blog/20201229-zork/zork_map.jpg]]
3543
3544/[[https://www.filfre.net/2012/01/exploring-zork-part-1/][Map Source]]/
3545
3546* In-Game Screenshots
3547
3548After playing the game (for the first time ever) for several weeks around 2014,
3549I was finally able to beat the game with some online help to find the last
3550couple items. As I was writing this post, I installed the game again to grab
3551some screenshots to show off the true glory of this game. As noted in [[https://www.filfre.net/2012/01/exploring-zork-part-1/][Jimmy
3552Maher's Playthrough]], the original Zork games looked quite a bit different due to
3553the older hardware of computers like the Apple II and multiple bug fixes that
3554Infocom pushed out after the game's initial release. My play-through uses the
3555[[https://store.steampowered.com/app/570580/Zork_Anthology/][Zork Anthology]] version, which utilizes DOSBox on Windows.
3556
3557The first screenshot here shows the introductory information, which doesn't
3558include instructions of any kind for the player. If you haven't played text
3559adventures before, try to use simple commands like "go west," "look around," or
3560"hit troll with elvish sword."
3561
3562In this second screenshot, we see the player has entered the house and found the
3563trophy case in the living room. The lantern and sword in this room allow the
3564player to explore dark areas and attack enemies. If you don't use the lantern,
3565you won't be able to see anything in dark areas, and you may be eaten by a grue.
3566
3567Finally, we see that the player has found the first treasure: a jewel-encrusted
3568egg. These treasures can be taken back to the house and placed in the trophy
3569case or carried until you feel like you want to put things away.
3570
3571* Conclusion
3572
3573It's been quite a few years since I first played Zork, but I clearly remember
3574the late nights and bloodshot eyes that helped me find all the treasures. This
3575game is well worth the time and effort, even though the text-based aspect may be
3576off-putting to gamers who didn't have to grow up playing games without graphics.
3577However, I believe that the strategy and skills learned in early video games
3578like Zork can actually help you, even when playing newer games.
3579
3580If you do decide to play Zork, you can download Zork I, II, and III from
3581Infocom's [[http://infocom-if.org/downloads/downloads.html][download page]] for free or search the internet for an online version.
3582#+date: <2021-01-01 Fri 00:00:00>
3583#+title: SEUM: Speedrunners from Hell
3584#+description: Technical overview for utilizing the game SEUM, focusing on speedrunning techniques, gravity manipulation, teleportation functions, and level completion criteria.
3585#+slug: seum
3586
3587* Game Description
3588
3589[[https://store.steampowered.com/app/457210/SEUM_Speedrunners_from_Hell/][SEUM: Speedrunners from Hell]] is an incredibly fast-paced mashup of a puzzle game
3590and a free-running game. Created by [[https://pinestudio.co][Pine Studio]] in early 2016 and first released
3591as a [[https://gamejolt.com/games/seum-speedrunners-from-hell-demo/154868][demo on GameJolt]], this game was successfully green-lit on Steam and has
3592amassed a cult following on multiple consoles.
3593
3594Here's the game description straight from the developers:
3595
3596#+begin_quote
3597Like a bastard child of Quake 3 and Super Meat Boy, SEUM: Speedrunners
3598from Hell is truly hardcore and focuses on speed and fast reaction.
3599#+end_quote
3600
3601* Story
3602
3603SEUM does a great job setting the stage when you open the game for the
3604first time, playing an animated video in the form of comic book images.
3605You see Marty, the protagonist, sitting around drinking his beer as
3606Satan busts down the door, cuts his hand off, and steals all his beer
3607and bacon. As Satan is leaving, Marty whips a vinyl record across the
3608room and cuts off one of Satan's hands. This hand is what allows you to
3609use all the powers in the game.
3610
3611* Gameplay
3612
3613To accomplish each level, you will need to get to the final blue portal
3614under a certain time limit. You can beat a level by getting to the
3615portal before the time meter runs out or "Dominate" a level by beating
3616it within a shorter time limit (noted by a bright red/orange color in
3617the time meter).
3618
3619The portal is usually set behind locked gates, protected by dangerous
3620defenses, or in hard-to-reach places. To reach the portal, you'll need
3621to go through any existing orange portals, light all yellow beacons,
3622avoid things like fireballs and blades, or use any satanic power orbs
3623lying around. These special abilities include:
3624
3625- Gravity
3626- Teleport
3627- Rewind
3628- Spawn platform
3629- Roar (DLC)
3630- Rocket (DLC)
3631- Shadow world (DLC)
3632
3633For the main storyline, there are nine floors to beat. Each floor
3634contains nine regular levels, one boss level, and one bonus level;
3635although you don't technically need to beat all levels to advance to the
3636next floor. # In-Game Screenshots
3637
3638The main menu gives you plenty of options for tuning your system, playing main
3639levels, playing the downloadable content (DLC), or exploring the other game
3640modes.
3641
3642Once you enter a level, you can look around and strategize before
3643starting. Clicking any button will start the menu, and you'll have to
3644restart if you die. One of the great things about SEUM is that it has
3645great keyboard shortcuts. You can quickly smash =R= to restart the level
3646or =M= to return to the level menu.
3647
3648Once you're able to win a level, you'll see the high scores and can
3649watch replays of the best scores.
3650
3651Each regular level also contains a beer in a disguised location that may
3652take some out-of-the-box thinking.
3653#+date: <2021-01-04 Mon 00:00:00>
3654#+title: Navigating the Fediverse: User Manual for Decentralized Social Media
3655#+description: Detailed introduction to the Fediverse network architecture, user registration procedures, platform navigation, and protocols ensuring content decentralization and censorship resistance.
3656#+slug: fediverse
3657
3658* What is the Fediverse?
3659
3660The fediverse is a federated universe of servers commonly used for sharing
3661content, like social media. So, instead of having to rely on a single
3662organization to run the server (e.g. Facebook), the fediverse is a giant
3663collection of servers across the world, owned by many people and organizations.
3664
3665Take a look at this depiction of a federated network. Each server in this photo
3666is owned and run by different administrators/owners. Federated networks are best
3667explained as email servers: you have an email account that exists on a server
3668(e.g. Outlook), your friend has an account on a different server (e.g. GMail),
3669and another friend has an account on a third server (e.g. ProtonMail). All three
3670of you can talk and communicate back and forth without having to be on the same
3671server. However, responsible email admins are there to set rules and control the
3672traffic going in/out of the server.
3673
3674The main objective of this architecture is to decentralize the control within
3675the internet connections. For example, if you run your own Mastodon instance,
3676you and your users can't be censored or impacted in any way by authorities of
3677another Mastodon instance. Some users have praised these features due to recent
3678criticism of popular social media websites that may be over-censoring their
3679users.
3680
3681This strategy is great for making sure control of the social web isn't
3682controlled by a single organization, but it also has some downsides. If I create
3683a Mastodon instance and get a ton of users to sign up, I can shut the server
3684down at any time. That means you're at risk of losing the content you've created
3685unless you back it up, or the server backs it up for you. Also, depending on the
3686software used (e.g. Mastodon, Pixelfed, etc.), censorship may still be an issue
3687if the server admins decide they want to censor their users. Now, censorship
3688isn't always a bad thing and can even benefit the community as a whole, but
3689you'll want to determine which servers align with your idea of proper
3690censorship.
3691
3692However, these are risks that we take when we sign up for any online platform.
3693Whatever your reason is for trying out federated social networks, they are part
3694of the future of the internet. However, the popularity of these services is
3695still yet to be determined, especially with the increased difficulty
3696understanding and signing up for these platforms. Perhaps increased regulation
3697and litigation against current social media sites will push more users into the
3698fediverse.
3699
3700* Federated Alternatives to Popular Sites
3701
3702The list below is a small guide that will show you federated alternatives to
3703current popular websites. There are many more out there, so go and explore: you
3704might just find the perfect home.
3705
3706** Reddit
3707
3708- [[https://lemmy.ml/instances][Lemmy]]
3709
3710** Twitter/Facebook/Tumblr
3711
3712- [[https://joinmastodon.org][Mastodon]]
3713- [[https://diasporafoundation.org][Diaspora]]
3714- [[https://friendi.ca][Friendica]]
3715- [[https://gnusocial.network][GNU Social]]
3716- [[https://pleroma.social][Pleroma]]
3717
3718** Instagram
3719
3720- [[https://pixelfed.org][Pixelfed]]
3721
3722** Slack/Discord
3723
3724- [[https://element.io][Matrix]]
3725
3726** Youtube/Vimeo
3727
3728- [[https://joinpeertube.org][Peertube]]
3729
3730** Spotify/Soundcloud
3731
3732- [[https://funkwhale.audio][Funkwhale]]
3733
3734** Podcasting
3735
3736- [[https://pubcast.pub][Pubcast]]
3737
3738** Medium/Blogger
3739
3740- [[https://writefreely.org][WriteFreely]]
3741
3742* Get Started
3743
3744The best way to get started is to simply sign up and learn as you go. If you're
3745comfortable signing up through a Mastodon, Pleroma, or Friendica server, here is
3746[[https://fediverse.party/en/portal/servers][a list of themed servers]] to choose from. If you're looking for something else,
3747try a web search for a federated alternative to your favorite sites.
3748
3749Find a server that focuses on your passions and start there!
3750#+date: <2021-01-07 Thu 00:00:00>
3751#+title: Hardening Ubuntu Servers with UFW: A Step-by-Step Firewall Guide
3752#+description: Stepwise instructions for installation, configuration, enabling, and management of the Uncomplicated Firewall utility to secure network interfaces on Ubuntu server environments.
3753#+slug: ufw
3754
3755* Uncomplicated Firewall
3756
3757Uncomplicated Firewall (UFW) is a convenient and beginner-friendly
3758way to enforce operating system (OS)-level firewall rules. For those who are
3759hosting servers or any device that is accessible to the world (i.e., by public
3760IP or domain name), it's critical that a firewall is properly implemented and
3761active.
3762
3763UFW is available by default in all Ubuntu installations after 8.04 LTS
3764(long-term support). For other distributions, you can look to install UFW or
3765check if there are alternative firewalls installed already. There are usually
3766alternatives available, such as Fedora's =firewall= and the package available on
3767most distributions: =iptables=. UFW is considered a beginner-friendly front-end
3768to iptables.
3769
3770[[https://gufw.org][Gufw]] is available as a graphical user interface (GUI) application for users who
3771are uncomfortable setting up a firewall through a terminal.
3772
3773* Getting Help
3774
3775If you need help figuring out commands, remember that you can run the
3776=--help= flag to get a list of options.
3777
3778#+begin_src sh
3779sudo ufw --help
3780#+end_src
3781
3782* Set Default State
3783
3784The proper way to run a firewall is to set a strict default state and slowly
3785open up ports that you want to allow. This helps prevent anything malicious from
3786slipping through the cracks. The following command prevents all incoming traffic
3787(other than the rules we specify later), but you can also set this for outgoing
3788connections, if necessary.
3789
3790#+begin_src sh
3791sudo ufw default deny incoming
3792#+end_src
3793
3794You should also allow outgoing traffic if you want to allow the device to
3795communicate back to you or other parties. For example, media servers like Plex
3796need to be able to send out data related to streaming the media.
3797
3798#+begin_src sh
3799sudo ufw default allow outgoing
3800#+end_src
3801
3802* Adding Port Rules
3803
3804Now that we've disabled all incoming traffic by default, we need to open up some
3805ports (or else no traffic would be able to come in). If you need to be able to
3806=ssh= into the machine, you'll need to open up port 22.
3807
3808#+begin_src sh
3809sudo ufw allow 22
3810#+end_src
3811
3812You can also issue more restrictive rules. The following rule will allow =ssh=
3813(secure shell protocol) connections only from machines on the local subnet.
3814
3815#+begin_src sh
3816sudo ufw allow proto tcp from 192.168.0.0/24 to any port 22
3817#+end_src
3818
3819If you need to set a rule that isn't TCP (Transmission Control Protocol) just
3820append your connection type to the end of the rule.
3821
3822#+begin_src sh
3823sudo ufw allow 1900/udp
3824#+end_src
3825
3826* Enable ufw
3827
3828Now that the firewall is configured and ready to go, you can enable the
3829firewall.
3830
3831#+begin_src sh
3832sudo ufw enable
3833#+end_src
3834
3835A restart may be required for the firewall to begin operating.
3836
3837#+begin_src sh
3838sudo reboot now
3839#+end_src
3840
3841* Checking Status
3842
3843Now that the firewall is enabled, let's check and see what the rules look like.
3844
3845#+begin_src sh
3846sudo ufw status numbered
3847#+end_src
3848
3849#+begin_src txt
3850Status: active
3851
3852 To Action From
3853 -- ------ ----
3854[ 1] 22 ALLOW IN Anywhere
3855[ 2] 22 (v6) ALLOW IN Anywhere (v6)
3856#+end_src
3857
3858* Deleting Rules
3859
3860If you need to delete a rule, you need to know the number associated with that
3861rule. Let's delete the first rule in the table above. You'll be asked to confirm
3862the deletion as part of this process.
3863
3864#+begin_src sh
3865sudo ufw delete 1
3866#+end_src
3867
3868* Managing App Rules
3869
3870Luckily, there's a convenient way for installed applications to create files
3871that ufw can easily implement so that you don't have to search and find which
3872ports your application requires. To see if your device has any applications with
3873pre-installed UFW rules, execute the following command:
3874
3875#+begin_src sh
3876sudo ufw app list
3877#+end_src
3878
3879The results should look something like this:
3880
3881#+begin_src txt
3882Available applications:
3883 OpenSSH
3884 Samba
3885 plexmediaserver
3886 plexmediaserver-all
3887 plexmediaserver-dlna
3888#+end_src
3889
3890If you want to get more information on a specific app rule, use the =info=
3891command.
3892
3893#+begin_src sh
3894sudo ufw app info plexmediaserver-dlna
3895#+end_src
3896
3897You'll get a blurb of info back like this:
3898
3899#+begin_src txt
3900Profile: plexmediaserver-dlna
3901Title: Plex Media Server (DLNA)
3902Description: The Plex Media Server (additional DLNA capability only)
3903
3904Ports:
3905 1900/udp
3906 32469/tcp
3907#+end_src
3908
3909You can add or delete app rules the same way that you'd add or delete specific
3910port rules.
3911
3912#+begin_src sh
3913sudo ufw allow plexmediaserver-dlna
3914#+end_src
3915
3916#+begin_src sh
3917sudo ufw delete RULE|NUM
3918#+end_src
3919
3920* Creating App Rules
3921
3922If you'd like to create you own app rule, you'll need to create a file in the
3923=/etc/ufw/applications.d= directory. Within the file you create, you need to
3924make sure the content is properly formatted.
3925
3926For example, here are the contents my =plexmediaserver= file, which creates
3927three distinct app rules for ufw:
3928
3929#+begin_src config
3930[plexmediaserver]
3931title=Plex Media Server (Standard)
3932description=The Plex Media Server
3933ports=32400/tcp|3005/tcp|5353/udp|8324/tcp|32410:32414/udp
3934
3935[plexmediaserver-dlna]
3936title=Plex Media Server (DLNA)
3937description=The Plex Media Server (additional DLNA capability only)
3938ports=1900/udp|32469/tcp
3939
3940[plexmediaserver-all]
3941title=Plex Media Server (Standard + DLNA)
3942description=The Plex Media Server (with additional DLNA capability)
3943ports=32400/tcp|3005/tcp|5353/udp|8324/tcp|32410:32414/udp|1900/udp|32469/tcp
3944#+end_src
3945
3946So, if I wanted to create a custom app rule called "mycustomrule," I'd create a
3947file and add my content like this:
3948
3949#+begin_src sh
3950sudo nano /etc/ufw/applications.d/mycustomrule
3951#+end_src
3952
3953#+begin_src config
3954[mycustomrule]
3955title=My Custom Rule
3956description=This is a temporary ufw app rule.
3957ports=88/tcp|9100/udp
3958#+end_src
3959
3960Then, I would just enable this rule in ufw.
3961
3962#+begin_src sh
3963sudo ufw allow mycustomrule
3964#+end_src
3965#+date: <2021-02-19 Fri 00:00:00>
3966#+title: macOS for Linux Users: Initial Setup and CLI Customization
3967#+description: Comprehensive overview for first-time macOS users transitioning from other OS platforms, including system setup, terminal usage, and user interface customization.
3968#+slug: macos
3969
3970* Diving into macOS
3971
3972After spending nearly 15 years working with Windows and 8 years on Linux, I have
3973experienced macOS for the first time. By chance, my spouse happened to buy a new
3974MacBook and gifted me their 2013 model. Of course, I still consider my Linux
3975desktop to be my daily driver and keep Windows around for gaming needs, but over
3976the past week I've found myself using the MacBook more and more for things that
3977don't require gaming specs or advanced developer tools.
3978
3979* Initial Thoughts
3980
3981Before I move on to the technical aspects of my set-up, I want to take some time
3982and express my thoughts on the overall operating system (OS).
3983
3984As expected, the initial computer setup is a breeze with Mac's guided graphical
3985user interface (GUI) installer.
3986
3987The desktop itself reminds me of GNOME more than anything else I've seen: even
3988Pantheon from [[https://elementary.io/][ElementaryOS]], which people commonly refer to as the closest Linux
3989distro to macOS. The desktop toolbar is great and far surpasses the utility of
3990the GNOME toolbar due to the fact that the extensions and icons /actually work/.
3991I launch macOS and immediately see my shortcuts for Tresorit, Bitwarden, and
3992Mullvad pop up as the computer loads.
3993
3994Even further, the app dock is very useful and will be yet another familiarity
3995for GNOME users. I know many people like panels instead of docks, but I've
3996always found docks to have a more pleasing UI. However, I had to disable the
3997"Show recent applications in Dock" preference; I can't stand items taking up
3998precious screen space if I'm not currently using them. On that same note, it's
3999taking me some time to get use to the fact that I have to manually quit an app
4000or else it will still stay open/active in the dock, even if I've closed out all
4001windows for that app (e.g. Firefox).
4002
4003Overall, I'm having a lot of fun and for users who spend a large majority of
4004their time performing basic tasks like web browsing, writing, watching media,
4005etc., macOS is a fantastic option.
4006
4007The rest of this post explains the technicalities of how I set up my command
4008line interface (CLI) environment to make me feel more at-home, similar to the
4009environments I set up on Fedora, Ubuntu, etc.
4010
4011* Making it Feel Like Home
4012
4013If you're someone who uses Linux primarily, no doubt your first thought when
4014booting macOS will be the same as mine was: "Where is the terminal and how do I
4015set up my favorite utilities?"
4016
4017Luckily, macOS hasn't completely hidden away the development tools from the
4018average user. You can easily find the Terminal app in the Launchpad area, but
4019it's probably not what you're used to. I was surprised (and happy) to see that
4020the default shell is =zsh=, the shell I use on all of my Linux distros. However,
4021the commands are not the same - even the ones you may think are native to the
4022shell. Commands like =dir= do not exist, so other native commands like =ls -la=
4023or =pwd= are more useful here.
4024
4025With only a few minutes of installing and tweaking a few packages, I was able to
4026recreate a terminal environment that I feel very comfortable using. See the
4027image below for a preview of the iTerm2 app with a split view between my macOS
4028desktop shell and an SSH (secure shell protocol) session into my server.
4029
4030* Xcode
4031
4032My first step was to search the web for any hints on how to get =zsh= back up to
4033the state I like, with extensions, themes, etc. My first step was to install the
4034CLI tools for [[https://developer.apple.com/xcode/][Xcode]], Apple's suite of development tools.
4035
4036#+begin_src sh
4037sudo xcode-select -r
4038#+end_src
4039
4040#+begin_src sh
4041sudo xcode-select --install
4042#+end_src
4043
4044* Homebrew
4045
4046Next up is to install [[https://brew.sh][Homebrew]], a nifty package manager for macOS.
4047
4048#+begin_src sh
4049/bin/bash -c "$(curl -fsSL
4050https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
4051#+end_src
4052
4053I ran into a permission error when installing Homebrew:
4054
4055#+begin_src sh
4056Error: Failed to link all completions, docs and manpages: Permission denied @
4057 rb_file_s_symlink - (../../../Homebrew/completions/zsh/_brew,
4058 /usr/local/share/zsh/site-functions/_brew) Failed during:
4059 /usr/local/bin/brew update --force --quiet
4060#+end_src
4061
4062I found that the following permission modification worked like a charm. However,
4063I noted that some users online discussed the fact that this solution may not
4064work if your system has multiple users who use Homebrew.
4065
4066#+begin_src sh
4067sudo chown -R $(whoami) $(brew --prefix)/*
4068#+end_src
4069
4070Next up is to ensure Homebrew is updated and cleaned.
4071
4072#+begin_src sh
4073brew update
4074#+end_src
4075
4076#+begin_src sh
4077brew cleanup
4078#+end_src
4079
4080* iTerm2
4081
4082Now that I've installed the basic utilities for development, I moved onto
4083installing iTerm2, a much better terminal than the default.
4084
4085#+begin_src sh
4086brew install --cask iterm2
4087#+end_src
4088
4089I also used the =Make iTerm2 Default Term= and =Install Shell Integration=
4090options in the iTerm2 application menu to make sure I don't run into any issues
4091later on with different terminals.
4092
4093We will also install =zsh= so we can use it in iTerm2.
4094
4095#+begin_src sh
4096brew install zsh
4097#+end_src
4098
4099* Oh-My-Zsh
4100
4101I've shown the great aspects of [[https://ohmyz.sh][Oh My Zsh]] in other blog posts, so I'll skip over
4102that speech for now. Simply install it and run an update.
4103
4104#+begin_src sh
4105sh -c "$(curl -fsSL
4106https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"
4107#+end_src
4108
4109#+begin_src sh
4110omz update
4111#+end_src
4112
4113Finally, restart the iTerm2 application to ensure all changes go into effect.
4114
4115* Oh-My-Zsh Themes
4116
4117Let's change the theme of the terminal to make it a little more friendly.
4118
4119#+begin_src sh
4120open ~/.zshrc
4121#+end_src
4122
4123The third section of this file should contain a line like the code below. Change
4124that theme to [[https://github.com/ohmyzsh/ohmyzsh/wiki/Themes][any theme you want]], save the file, and exit.
4125
4126#+begin_src sh
4127ZSH_THEME="af-magic"
4128#+end_src
4129
4130After changing the =.zshrc= file, you'll need to close your terminal and re-open
4131it to see the changes. Optionally, just open a new tab if you're using iTerm2,
4132and you'll see the new shell config.
4133
4134* Oh-My-Zsh Plugins
4135
4136Of course, my customization of =zsh= would not be complete without
4137[[https://github.com/zsh-users/zsh-autosuggestions][zsh-autosuggestions]]. This will bring up commands you've run in the past as you
4138type them. For example, if you've run =ssh user@192.168.1.99= before, the
4139terminal will show this command as soon as you start typing it (e.g. =zsh u=),
4140and you can hit the right arrow to autocomplete the command.
4141
4142#+begin_src sh
4143git clone https://github.com/zsh-users/zsh-autosuggestions
4144${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
4145#+end_src
4146
4147#+begin_src sh
4148open ~/.zshrc
4149#+end_src
4150
4151#+begin_src sh
4152# Scroll down the script and edit this line to add zsh-autosuggestions
4153plugins=(git zsh-autosuggestions)
4154#+end_src
4155
4156Remember: After changing the =.zshrc= file, you'll need to close your terminal
4157and re-open it to see the changes. Optionally, just open a new tab if you're
4158using iTerm2, and you'll see the new shell config.
4159#+date: <2021-03-19 Fri 00:00:00>
4160#+title: GitHub and Sourcehut: Scripting Mass Clones and Remote Updates
4161#+description: Script-based methodology for cloning multiple repositories from GitHub and Sourcehut accounts. Includes automation techniques to streamline repository management and backups.
4162#+slug: clone-github-repos
4163
4164* Cloning from GitHub
4165
4166If you're like me and use a lot of different devices [and sometimes decide to
4167just wipe your device and start with a new operating system (OS)], you probably know
4168the pain of cloning all your old code repositories down to your local file
4169system.
4170
4171If you're using GitHub, you can easily clone all of your code back down in just
4172seconds. First, create a bash script. I do so by opening a new file in =nano=,
4173but you can use =gedit=, =vim=, or something else:
4174
4175#+begin_src sh
4176nano clone_github_repos.sh
4177#+end_src
4178
4179Next, paste in the following information. Note that you can replace the
4180word =users= in the first line with =orgs= and type an organization's
4181name instead of a user's name.
4182
4183#+begin_src sh
4184CNTX=users; NAME=YOUR-USERNAME; PAGE=1
4185curl "https://api.github.com/$CNTX/$NAME/repos?page=$PAGE&per_page=100" |
4186 grep -e 'git_url*' |
4187 cut -d " -f 4 |
4188 xargs -L1 git clone
4189#+end_src
4190
4191Finally, save the bash script and make it executable.
4192
4193#+begin_src sh
4194chmod a+x clone_github_repos.sh
4195#+end_src
4196
4197Now you can run the script and should see the cloning process begin.
4198
4199#+begin_src sh
4200./clone_github_repos.sh
4201#+end_src
4202
4203* Cloning from Sourcehut
4204
4205I haven't fully figured out how to directly incorporate Sourcehut's GraphQL
4206application programming interface (API) into a bash script yet, so this one will
4207take two steps.
4208
4209First, log-in to Sourcehut and go to their [[https://git.sr.ht/graphql][GraphQL playground for Git]]. Next,
4210paste the following query into the left box:
4211
4212#+begin_src sh
4213query {
4214 me {
4215 canonicalName
4216 repositories() {
4217 cursor
4218 results {
4219 name
4220 }
4221 }
4222 }
4223}
4224#+end_src
4225
4226The output on the right side will give you an object of all your repositories.
4227Just grab that text and remove all the characters such as quotation marks and
4228curly brackets. You will need a single-line list of space-separated values for
4229the next step.
4230
4231Now let's create the bash script:
4232
4233#+begin_src sh
4234nano clone_sourcehut_repos.sh
4235#+end_src
4236
4237Next, paste the following bash script in with the list of repositories you
4238obtained above and replace =your-username= with your username.
4239
4240Note that this uses the secure shell protocol (SSH)-based Git cloning method
4241(e.g. =git@git...=), so you'll need to ensure you have set up Sourcehut with
4242your SSH key.
4243
4244#+begin_src sh
4245repos=(repo1 repo2 repo3)
4246
4247# List all sub-directories in the current directory
4248for repo in "${repos[@]}"
4249do
4250 # Clone
4251 git clone git@git.sr.ht:~your-username/$repo
4252done
4253#+end_src
4254
4255Finally, save the bash script and make it executable.
4256
4257#+begin_src sh
4258chmod a+x clone_sourcehut_repos.sh
4259#+end_src
4260
4261Now you can run the script and should see the cloning process begin.
4262
4263#+begin_src sh
4264./clone_sourcehut_repos.sh
4265#+end_src
4266
4267* Moving Repositories to a New Host
4268
4269Now that you have all of your code repositories cloned to your local computer,
4270you may want to change the remote host (e.g., moving from GitHub to GitLab). To
4271do this, let's create another bash script:
4272
4273#+begin_src sh
4274nano change_remote_urls.sh
4275#+end_src
4276
4277Past the following information and be sure to change the URL (uniform resource
4278locator) information to whichever host you are moving to. For this example, I am
4279looping through all of my cloned GitHub directories and changing them to
4280Sourcehut (e.g. =<YOUR_NEW_REMOTE_URL>= -> =git@git.sr.ht:~myusername=).
4281
4282#+begin_src sh
4283# List all sub-directories in the current directory
4284for dir in */
4285do
4286 # Remove the trailing "/"
4287 dir=${dir%*/}
4288 # Enter sub-directory
4289 cd $dir
4290 # Change remote Git URL
4291 git remote set-url origin <YOUR_NEW_REMOTE_URL>/"${dir##*/}"
4292 # Push code to new remote
4293 git push
4294 # Go back to main directory
4295 cd ..
4296done
4297#+end_src
4298
4299Finally, save the bash script and make it executable.
4300
4301#+begin_src sh
4302chmod a+x change_remote_urls.sh
4303#+end_src
4304
4305Now you can run the script and should see the cloning process begin.
4306
4307#+begin_src sh
4308./change_remote_urls.sh
4309#+end_src
4310#+date: <2021-03-28 Sun 00:00:00>
4311#+title: Getting Started with Gemini: Deploy Your First Capsule
4312#+description: Incremental procedural guide to initiate and maintain a Gemini capsule server. Covers installation, deployment, and operational best practices for beginners.
4313#+slug: gemini-capsule
4314
4315* What is Gemini?
4316
4317[[https://gemini.circumlunar.space/][Gemini]] is an internet protocol introduced in June 2019 as an alternative to the
4318Hypertext Transfer Protocol (HTTP) or Gopher. In layman's terms, it's an
4319alternative way to browse sites (called capsules) that requires a special
4320browser. Since Gemini is not standardized as an internet standard, normal web
4321browsers won't be able to load a Gemini capsule. Instead, you'll need to use [[https://gemini.circumlunar.space/clients.html][a
4322Gemini-specific browser]].
4323
4324The content found within a Gemini page is called [[https://gemini.circumlunar.space/docs/cheatsheet.gmi][Gemtext]] and is /extremely/
4325basic (on purpose). Gemini only processes the text, no media content like
4326images. However, you're able to style 3 levels of headings, regular text, links
4327(which will display on their own line), quotes, and an unordered list.
4328
4329Here's a complete listing of valid Gemtext:
4330
4331#+begin_src txt
4332# Heading 1
4333## Heading 2
4334### Heading 3
4335
4336Regular text! Lorem ipsum dolor sit amet.
4337
4338=> https://example.com My Website
4339=> gemini://example.com My Gemini Capsule
4340
4341> "If life were predictable it would cease to be life, and be without flavor." - Eleanor Roosevelt
4342
4343My List:
4344,** Item
4345,** Item
4346
4347```Anything between three backticks will be rendered as code.```
4348#+end_src
4349
4350*** Free Option
4351
4352There are probably numerous websites that allow you to create your personal
4353Gemini capsule, but I'm going to focus on the two sites that I have personally
4354tested. The first option below, Midnight Pub, allows you to create/edit any
4355Gemini files you want in your account. This is essentially a graphical option
4356with a built-in text box for editing. The second option below, Sourcehut, allows
4357you to use a Git repository and automatic build process to deploy your personal
4358Gemini capsule every time you push a commit.
4359
4360** Midnight Pub - Beginner Friendly
4361
4362[[https://midnight.pub/][Midnight Pub]] is a small, virtual community meant to reflect the atmosphere of
4363wandering into a small alley pub. The site is built in Gemtext and has a
4364server-side process to convert Gemtext to HTML if someone loads the site in an
4365HTTP(S) browser.
4366
4367To create an account, you'll need to email the owner of the website to obtain a
4368key. You can find their email on the Midnight Pub homepage. Once registered,
4369head to [[https://midnight.pub/account][your account]] and select [[https://midnight.pub/site][manage site]]. This is the screen where you can
4370upload or create any files to be displayed on the internet.
4371
4372For example, I've created both a Hypertext Markup Language (HTML) file and a
4373Gemini file. Remember that Gemini is automatically converted to HTML on the Pub,
4374so you don't need an HTML version. For example, I created an HTML version to add
4375in some extra styling.
4376
4377All you need to do is create a page like =index.gmi= and use your Gemini browser
4378to head over to your-username.midnight.pub to see the result.
4379
4380That's all there is to it! Easy enough, right? Let's check out a more advanced
4381version in the next section.
4382
4383* Paid Option
4384
4385As of 2021, Sourcehut has decided to require users to have a paid account in
4386order to utilize their automated build system. For now, paid accounts can be as
4387low as $2/month.
4388
4389** Sourcehut
4390
4391[[https://sourcehut.org/][Sourcehut]] is a collection of software development tools, but mostly surrounds
4392their hosted Git repository service. Simply put, it's a minimal and more private
4393alternative to services like GitHub.
4394
4395This walkthrough is more advanced and involves things like Git, SSH, the command
4396line. If you don't think you know enough to do this, check out my walkthrough on
4397creating a Gemini capsule for the Midnight Pub instead.
4398
4399The first thing you'll need to do is create an SSH key pair, if you don't
4400already have one on your system. Once created, grab the contents of =id_rsa.pub=
4401and add it to your Sourcehut account settings - this will allow you to push and
4402pull code changes without using a username/password.
4403
4404#+begin_src sh
4405ssh keygen
4406#+end_src
4407
4408Next up, let's create a repository with the proper name so that the Sourcehut
4409build system will know we want them to host a website for us. Use the following
4410format exactly:
4411
4412#+begin_src sh
4413mkdir your-username.srht.site && cd your-username.srht.site
4414#+end_src
4415
4416Now that we've created the repo, let's initialize Git and add the proper remote
4417URL.
4418
4419#+begin_src sh
4420git init
4421#+end_src
4422
4423#+begin_src sh
4424git remote add origin git@git.sr.ht:~your-username/your-username.srht.site
4425#+end_src
4426
4427Now that our repository is set up and configured, we will need to create at
4428least two files:
4429
4430- =index.gmi=
4431- =.build.yml=
4432
4433For your =.build.yml= file, use the following content and be sure to update the
4434=site= line with your username!
4435
4436#+begin_src yaml
4437image: alpine/latest
4438oauth: pages.sr.ht/PAGES:RW
4439environment:
4440 site: your-username.srht.site
4441tasks:
4442 - package: |
4443 cd $site
4444 tar -cvz . > ../site.tar.gz
4445 - upload: |
4446 acurl -f https://pages.sr.ht/publish/$site -Fcontent=@site.tar.gz -Fprotocol=GEMINI
4447#+end_src
4448
4449For the =index.gmi= file, put whatever you want in there and save it. You could
4450even just copy and paste the Gemtext cheatsheet.
4451
4452If you want to serve both HTML and Gemini files from this repository, just add a
4453second command to the =upload= section:
4454
4455#+begin_src yaml
4456- upload: |
4457 acurl -f https://pages.sr.ht/publish/$site -Fcontent=@site.tar.gz -Fprotocol=GEMINI
4458 acurl -f https://pages.sr.ht/publish/$site -Fcontent=@site.tar.gz
4459#+end_src
4460
4461Lastly, commit your changes and push them to the remote repo.
4462
4463#+begin_src sh
4464git add .; git commit -m "initial commit"; git push --set-upstream origin HEAD
4465#+end_src
4466
4467If you've successfully created the files with the proper format, you'll see the
4468terminal print a message that lets you know where the automatic build is taking
4469place. For example, here's what the terminal tells me:
4470
4471#+begin_src sh
4472remote: Build started:
4473remote: https://builds.sr.ht/~user/job/689803 [.build.yml]
4474#+end_src
4475
4476Now that you've properly built your Sourcehut page, you can browse to
4477your-username.srht.site in a Gemini browser and view the final results.
4478#+date: <2021-03-28 Sun 00:00:00>
4479#+title: Exploring Vaporwave and Outrun: Color, Culture, and Sound
4480#+description: Systematic examination of Vaporwave and Outrun aesthetic frameworks, focusing on graphical elements, chromatic schemes, musical associations, and period-specific cultural influences.
4481#+slug: vaporwave-vs-outrun
4482
4483* Overview
4484
4485When it comes to an aesthetic that originated primarily online, there tends to
4486be a lot of confusion around what falls into each specific genre. This post
4487discusses Vaporwave and Outrun, which have become almost synonymous in the minds
4488of those online who aren't hardcore into these genres of media. More
4489specifically, Outrun is largely an unknown term while all aesthetics in these
4490two genres are usually attributed to Vaporwave. For example, take a look at the
4491image search results for Vaporwave: the results include *a lot* of Outrun-themed
4492images. You'll find a similar trend almost everywhere.
4493
4494#+caption: Vaporwave Search Results
4495#+attr_html: :alt Search engine results for vaporwave, showing many outrun-related images.
4496[[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/vaporwave-search-results.webp]]
4497
4498* Vaporwave
4499
4500#+caption: Macintosh Plus Album Cover
4501#+attr_html: :alt Album cover of the Macintosh Plus album.
4502[[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/macintosh-plus.webp]]
4503
4504Okay, so what is Vaporwave? I'm going to just copy-and-paste some general info
4505from the Wikipedia article on [[https://en.wikipedia.org/wiki/Vaporwave][Vaporwave]], so that I'm not repeating everything
4506you can already search for online:
4507
4508#+begin_quote
4509Vaporwave is a microgenre of electronic music, a visual art style, and an
4510Internet meme that emerged in the early 2010s. It is defined partly by its
4511slowed-down, chopped and screwed samples of smooth jazz, elevator, R&B, and
4512lounge music from the 1980s and 1990s. The surrounding subculture is sometimes
4513associated with an ambiguous or satirical take on consumer capitalism and pop
4514culture, and tends to be characterized by a nostalgic or surrealist engagement
4515with the popular entertainment, technology and advertising of previous decades.
4516Visually, it incorporates early Internet imagery, late 1990s web design, glitch
4517art, anime, 3D-rendered objects, and cyberpunk tropes in its cover artwork and
4518music videos.
4519#+end_quote
4520
4521This is an excellent summary, and it helps address my point here: there are
4522specific aspects that make Vaporwave unique:
4523
4524** Time Frame
4525
4526The time frame for references, logos, etc. focuses mostly on the 1990s in
4527Vaporwave. You'll see old school Pepsi logos, Microsoft 95 screens, tropical
4528plants, classic marble sculptures, and many references from Japan's influence in
4529the 90s.
4530
4531** Art
4532
4533The color scheme is generally a soft, light palette that uses pastel colors. The
4534backdrop will often be in a light-pink or teal blue.
4535
4536** Music
4537
4538The musical genre of Vaporwave incorporates soft tunes, lounge music, and sound
4539effects that will make the listener reminisce of the 90s. The sounds of the
4540genre are generally slower-paced and calming. The major breakthrough artist for
4541Vaporwave was [[https://en.wikipedia.org/wiki/Vektroid][Macintosh Plus]], who released the album [[https://archive.org/details/MACINTOSHPLUS-FLORALSHOPPE_complete][Floral Shoppe]] in 2011.
4542Another more recent example is the artist [[https://sadbert.bandcamp.com/][sadbert]], whose latest album
4543incorporates the upbeat tones of the 1999 Dilbert television series.
4544
4545Notice that Vaporwave doesn't include things like racing cars, futuristic
4546technology, chrome, or the deep orange/purple color scheme. Vaporwave is a focus
4547on the idyllic state of the world when technology was becoming common in
4548households, a reality that we have already experienced. Focus on the most
4549aesthetically-pleasing parts of that past is a large part of Vaporwave.
4550
4551* Outrun
4552
4553#+caption: Outrun
4554#+attr_html: :alt The classic outrun picture with digital hills and a grid leading to a far-away sunset.
4555[[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/outrun.jpg]]
4556
4557Now, let's get to Outrun. This one is a little trickier since the genre has
4558largely been lumped under the Vaporwave title for so long. However, it stems
4559from the [[https://en.wikipedia.org/wiki/Synthwave][Synthwave]] music genre and is likely named after the 1986 racer game,
4560[[https://en.wikipedia.org/wiki/Out_Run][Out Run]].
4561
4562** Time Frame
4563
4564Outrun can be thought of as a retro-futuristic aesthetic born from the 1980s.
4565
4566** Art
4567
4568The color scheme uses a very dark color palette with the main colors being deep
4569oranges, blues, and purples. Red edges are common around objects in Outrun art.
4570The background of the Outrun aesthetic is almost always a neon grid like you'd
4571expect to see in Tron or a 1980s arcade machine.
4572
4573Classic sports cars, chrome robots, computer generated graphics and fonts, and
4574the occasional use of rain or palm trees can be found in Outrun art.
4575
4576** Music
4577
4578This aesthetic has a more aggressive and fast-paced style of music, which tends
4579to match the subject of the art in this aesthetic.
4580
4581Outrun enthusiasts love what people in the 1980s thought the future would look
4582like. Take a look at a common video game discussed in Outrun circles, Far Cry 3:
4583Blood Dragon:
4584
4585#+caption: Fry Cry 3: Blood Dragon
4586#+attr_html: :alt The game cover for Far Cary 3: Blood Dragon, showing ninjas, lasers, dinosaurs, and cyborgs.
4587[[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/far-cry.jpg]]
4588
4589Another example that doesn't force the color scheme as hard as some online art
4590does is Kung Fury:
4591
4592#+caption: Kung Fury
4593#+attr_html: :alt The movie cover for Kung Fury, showing the proteagonist in a city at night with a moon.
4594[[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/kung-fury.webp]]
4595
4596#+caption: Kung Fury Hacker Scene
4597#+attr_html: :alt The HACKERMAN scene from Kung Fury, showing a classic 1980's hacker in a computer room.
4598[[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/kung-fury-hacker.webp]]
4599
4600* Conclusion
4601
4602While Vaporwave and Outrun share similarities, they are two distinct aesthetics
4603with many important distinctions. Someone who enjoys one may not necessarily
4604enjoy the other, so it's important to make sure we properly describe the
4605aesthetic we're looking for.
4606#+date: <2021-03-30 Tue 00:00:00>
4607#+title: From Shared Hosting to VPS: Web Server Setup Made Easy
4608#+description: Instructional outline for setting up and maintaining a VPS environment configured for secure and scalable web hosting applications.
4609#+slug: vps-web-server
4610
4611* Shared Hosting vs. VPS (Virtual Private Server)
4612
4613Choosing a place to host a website is one of the most confusing decisions for
4614beginner web developers. Even for experienced web devs, choosing between
4615different forms of web hosting can be a daunting choice.
4616
4617First, let's take a look at [[https://en.wikipedia.org/wiki/Shared_web_hosting_service][shared web hosting]]. Shared web hosting is a product
4618where you are purchasing a small piece of a web server that is being shared
4619between many websites. As a result, the cost of shared hosting is extremely low.
4620You won't have access to the server itself, which means you can't install your
4621own software on the server, such as Docker. Usually, you are simply allowed to
4622connect your domains to the server, set up domain security, and other small
4623utilities.
4624
4625In contrast, a [[https://en.wikipedia.org/wiki/Virtual_private_server][virtual private server]] (VPS) is a virtual machine that replicates
4626the environment of having a dedicated server to yourself. You are able to
4627control the virtual server's host names, base file system, package manager, etc.
4628Another great upside of a VPS is that since it's virtual, the company providing
4629the VPS can dynamically increase the disk size, RAM size, or number of CPUs at
4630any time. However, the virtual server is still physically located on a server
4631that is shared between multiple virtual servers.
4632
4633The choice between shared hosting and VPS mostly depends on your skill level
4634with system administration. If you're comforting working on a server that is
4635mostly left up to you (or you're willing to learn), then a VPS is usually a
4636better option. However, shared hosting is a fantastic option for people who
4637don't want to have to learn how to manage their server.
4638
4639* My Situation
4640
4641I had used shared hosting for approximately 5 years before trying my first VPS.
4642I manage a homelab and have had success running a server and performing typical
4643sysadmin duties, but I was still hesitant to get a VPS. One fear was that I
4644always struggled to properly set up the networking part of a server - DNS and
4645hostname configurations were not my friend.
4646
4647As a little bit of background, I originally used [[https://www.siteground.com][Siteground]] for my initially
4648shared hosting and stayed on that platform for at least a year. However, the
4649user interface (UI) was clunky, and I didn't like how they handled certain
4650technical aspects, so I switched to [[https://www.namecheap.com][Namecheap]]. Namecheap was great because it is
4651the service I primarily use for purchasing domain names, which made it
4652incredibly easy to link them to my hosting service. However, it was still
4653mediocre shared hosting, and Namecheap is notorious for not letting you use
4654[[https://letsencrypt.org][Let's Encrypt]] to obtain free SSL (Secure Socket Layer)/TLS (Transport Layer
4655Security) certificates; Namecheap wants to make you purchase certificates
4656through their store.
4657
4658Finally, I settled down with [[https://www.iwebfusion.net][iWebFusion]] for about the last year of my shared
4659hosting. This service was pretty great, came with free SSL/TLS, and I never had
4660any complaints.
4661
4662However, I finally grew tired of not being able to install software on my own
4663web server. I wanted to be able to try out things like [[https://postmill.xyz][Postmill]] or [[https://matrix.org][Matrix]]. This
4664is possible with a VPS, so I decided to grab a new domain name to try it out.
4665
4666* Getting Started: Buying a VPS
4667
4668The first step to moving over to a VPS is (you guessed it): finding a VPS
4669provider. For my VPSs, I use [[https://1984hosting.com][1984]] and prefer their services much more than any
4670alternative, due to their location (Iceland), their [[https://1984hosting.com/GDPR/][privacy policy]], their
4671respect for GDPR, and the ability to remain anonymous if you pay in Bitcoin or
4672Monero.
4673
4674[[https://njal.la][Njalla]] is another good, privacy-oriented option for VPS services.
4675
4676You'll have to decide what specifications you want on your VPS. For me, I only
4677build and deploy low-resource HTML, PHP, and Python websites. This means I can
4678survive on the smallest VPS: 1 CPU, 1GB of RAM, and 25GB SSD for $5.00 per
4679month.
4680
4681As noted above, the great thing about a VPS is you can request your provider to
4682increase the resources at any time.
4683
4684* Configuring DNS Settings
4685
4686Okay, so now let's get into some actual work that has to be done to get content
4687moved from a shared host to a VPS. At this point, I'm assuming you have a shared
4688host with website content that you can still access, and you've purchased a new
4689VPS and can SSH into that server.
4690
4691The first change is minor, but it should be done immediately in order to get
4692things moving: DNS settings. Go to wherever your DNS settings are handled. If
4693your shared host also managed your DNS settings, you'll need to first move that
4694DNS over to your new VPS provider. For me, I route my DNS through [[https://www.gandi.net][Gandi]].
4695
4696Once you know where your DNS settings are, go ahead and update the =A= records
4697to match the public IP address of your VPS. For example:
4698
4699#+begin_src txt
4700A example.com xxx.xxx.xxx.xxx
4701A subdomain xxx.xxx.xxx.xxx
4702CNAME www example.com.
4703#+end_src
4704
4705If you have any other records that require updates, such as MX or TXT records
4706for a mail server, be sure to update those accordingly. Personally, I don't host
4707my own mail server. I route all mail on my custom domains to [[https://www.migadu.com][Migadu]]. Hosting
4708your own email server can become complex quickly and is not for beginners.
4709
4710DNS changes can take up to 48 hours to propagate, so be sure to give it some
4711time before assuming you've made an error.
4712
4713* Server Updates and Packages
4714
4715Now that the DNS settings have been changed, let's set up our server while we
4716wait for the DNS to propagate. First up is to ssh into your server. If you've
4717signed up with a service like DigitalOcean, you can add your SSH key to your
4718account and to your VPS droplet so that you don't need a password in order to
4719SSH.
4720
4721#+begin_src sh
4722ssh root@xxx.xxx.xxx.xxx
4723#+end_src
4724
4725The VPS that is used in this blog post runs Ubuntu 20.04 with an Apache web
4726server. If you're working on a different operating system (OS) or want a
4727different web server, such as Nginx, you'll have to use different commands to
4728set it up.
4729
4730First, let's update and upgrade our server.
4731
4732*NOTE:* Since we have logged in to the server as =root= for now, we don't need
4733to use the =sudo= modifier before our commands.
4734
4735#+begin_src sh
4736apt update && apt upgrade -y
4737#+end_src
4738
4739* Create A User Account
4740
4741While being able to use =root= can be beneficial at times, you shouldn't use
4742=root= unless you have to.
4743
4744So let's set up a new user in our system. The =-m= option below tells the OS to
4745create a home directory for the new user.
4746
4747#+begin_src sh
4748adduser USERNAME
4749#+end_src
4750
4751Now, create a password for that user.
4752
4753#+begin_src sh
4754passwd USERNAME
4755#+end_src
4756
4757Finally, add the user to the sudoers file, so they can perform privileged
4758commands.
4759
4760#+begin_src sh
4761usermod -a -G sudo USERNAME
4762#+end_src
4763
4764If you are using SSH (secure shell protocol) keys and not passwords, you'll need
4765to copy your SSH key from your local machine to the VPS. If you haven't disabled
4766password-based SSH yet, the easiest way to do this is =ssh-copy-id= from your
4767local computer (not from the VPS):
4768
4769#+begin_src sh
4770ssh-copy-id testuser@xxx.xxx.xxx.xxx
4771#+end_src
4772
4773If you've disabled password-based SSH, you'll need to manually copy your SSH key
4774into the =~/.ssh/authorized_keys= file.
4775
4776* Install Software
4777
4778Our goal here is to host a web server, so the next step is to install the Apache
4779web server and any other packages we need.
4780
4781From this point on, I will be logged in as a user (not =root=) and will need to
4782use the =sudo= modifier for most commands.
4783
4784#+begin_src sh
4785sudo apt update; sudo apt upgrade -y; sudo apt autoremove -y
4786sudo apt install apache2
4787#+end_src
4788
4789If you need other language support, such as PHP, you'll need to install that
4790too.
4791
4792#+begin_src sh
4793sudo apt install libapache2-mod-php php-dom
4794sudo a2enmod php
4795sudo systemctl restart apache2
4796#+end_src
4797
4798* Website Files & Folders
4799
4800Next up is to create the directories for the domain(s) we want to be hosted on
4801this web server.
4802
4803#+begin_src sh
4804cd /var/www
4805sudo mkdir example.com
4806#+end_src
4807
4808We have a folder for =example.com= now, so let's add an =index.html= file and
4809put it within a specific =public_html= folder. You don't need this =public_html=
4810if you don't want it, but it helps with organizing items related to
4811=example.com= that you don't want to publish to the internet.
4812
4813#+begin_src sh
4814cd example.com
4815sudo mkdir public_html && cd public_html
4816sudo nano index.html
4817#+end_src
4818
4819You can put anything you want in this =index.html= file. If you can't think of
4820anything, paste this in there:
4821
4822#+begin_src html
4823<!DOCTYPE html>
4824<html lang="en">
4825 <head>
4826 <meta charset="utf-8" />
4827 <meta name="viewport" content="width=device-width, initial-scale=1" />
4828 <title>Hello, world!</title>
4829 </head>
4830 <body>
4831 <h1>Hello, world!</h1>
4832 </body>
4833</html>
4834#+end_src
4835
4836If you want something to be served at =example.com/page01/file.txt=, you'll have
4837to create the =page01= directory under the =example.com= directory. For example:
4838
4839#+begin_src sh
4840cd /var/www/example.com/public_html
4841sudo mkdir page01
4842sudo nano file.txt
4843#+end_src
4844
4845* Apache Configuration
4846
4847Now, let's set up the files that will tell the server where to find the files
4848for =example.com=. We will copy the default configuration file and create our
4849own.
4850
4851#+begin_src sh
4852cd /etc/apache2/sites-available
4853sudo cp 000-default.conf example.com.conf
4854sudo nano example.com.conf
4855#+end_src
4856
4857This configuration file will have a few default lines, but you'll need to edit
4858it to look similar to this (settings may change based on your personal needs):
4859
4860#+begin_src config
4861<VirtualHost *:80>
4862 ServerAdmin your-email@email-provider.com
4863 ServerName example.com
4864 ServerAlias www.example.com
4865 DocumentRoot /var/www/example.com/public_html
4866 ErrorLog ${APACHE_LOG_DIR}/error.log
4867 CustomLog ${APACHE_LOG_DIR}/access.log combined
4868</VirtualHost>
4869#+end_src
4870
4871Now, enable the configuration for your new site, disable the default
4872configuration, and reload the web server.
4873
4874#+begin_src sh
4875sudo a2ensite example.com.conf
4876sudo a2dissite 000-default.conf
4877sudo systemctl reload apache2
4878#+end_src
4879
4880You can always run a test to make sure no errors or warnings are found in your
4881configuration files.
4882
4883#+begin_src sh
4884sudo apache2ctl configtest
4885#+end_src
4886
4887Now, restart the web server entirely. After this, you should be able to browse
4888to =http://example.com= and see the HTML (hypertext markup language) content you
4889provided earlier. Note that SSL/TLS has not been enabled yet, so you won't be
4890able to use the secure version yet (=https://example.com=).
4891
4892#+begin_src sh
4893sudo systemctl restart apache2
4894#+end_src
4895
4896You can repeat this for as many websites as you need. Just create the domain
4897folders in =/var/www/=, add the configuration file, enable the configuration,
4898and restart =apache2=.
4899
4900* SSL/TLS Certificates: Serve Websites Over HTTPS
4901
4902In order to serve secure content, you'll need to obtain SSL/TLS certificates.
4903Luckily, there's a free tool called [[https://certbot.eff.org][Certbot]] that helps us with the process.
4904
4905The first step is to install =snapd= and =core= for Ubuntu.
4906
4907#+begin_src sh
4908sudo apt install snapd
4909sudo snap install core
4910sudo snap refresh core
4911#+end_src
4912
4913Next, install the =certbot= snap package.
4914
4915#+begin_src sh
4916sudo snap install --classic certbot
4917#+end_src
4918
4919Execute the following command to ensure that the =certbot= command can
4920be run.
4921
4922#+begin_src sh
4923sudo ln -s /snap/bin/certbot /usr/bin/certbot
4924#+end_src
4925
4926Finally, you can run =certbot= one of two ways:
4927
49281. run it and let it alter your Apache configuration files automatically to
4929 enable HTTPS (Hypertext Transport Protocol) redirects.
49302. run it and only allow it to create certificates. You'll need to manually
4931 alter the config files to enable HTTPS redirects.
4932
4933Run certbot and allow automatic config changes:
4934
4935#+begin_src sh
4936sudo certbot --apache
4937#+end_src
4938
4939Run certbot for certificates only and don't allow it to alter config
4940files:
4941
4942#+begin_src sh
4943sudo certbot certonly --apache
4944#+end_src
4945
4946The Certbot packages on your system come with a =cron= job or =systemd= timer
4947that will renew your certificates automatically before they expire. You will not
4948need to run Certbot again unless you change your configuration. You can test
4949automatic renewal for your certificates by running this command:
4950
4951#+begin_src sh
4952sudo certbot renew --dry-run
4953#+end_src
4954
4955Now, test your domains by going to =https://example.com=.
4956
4957* Firewall Security
4958
4959To enable better security on your server, you'll need to enable a basic
4960firewall. For Ubuntu, we'll use [[https://cleberg.net/blog/ufw.html][the uncomplicated firewall]].
4961
4962Now, add the following rules to the firewall allow SSH, Apache, and HTTP(S)
4963connections. If you need to, you can enable different ports for specific
4964applications.
4965
4966#+begin_src sh
4967sudo ufw default deny incoming
4968sudo ufw default allow outgoing
4969sudo ufw allow OpenSSH
4970sudo ufw allow Apache
4971sudo ufw allow proto tcp from any to any port 80,443
4972#+end_src
4973
4974Once you've added all the rules you need, enable the firewall.
4975
4976#+begin_src sh
4977sudo ufw enable
4978#+end_src
4979
4980* Troubleshooting
4981
4982If you run into any issues during your VPS set-up, be sure to walk back through
4983your actions and make sure you didn't miss any steps.
4984
4985Many websites have fantastic guides to setting up various web servers. This is
4986one of the areas [[https://www.digitalocean.com/community/tutorials][where DigitalOcean shines]]. For simpler or more Linux-oriented
4987questions, I suggest using [Linuxize] ([[https://linuxize.com]]).
4988
4989If you're getting certain errors (e.g. =500 Internal Server Error=) and need to
4990debug locally, you can view the =access.log= and =error.log= files in the
4991=/var/log/apache/= directory.
4992#+date: <2021-04-17 Sat 00:00:00>
4993#+title: Agate Gemini Server Installation and Firewall Configuration
4994#+description: Detailed directives for installation and configuration of a Gemini protocol server on Ubuntu, including network security and firewall setup.
4995#+slug: gemini-server
4996
4997* Similar Article Available
4998
4999To read more about Gemini and ways to test out this new protocol without your
5000own server, see my previous post [[../launching-a-gemini-capsule/][Launching a Gemini Capsule]].
5001
5002* Preparation
5003
5004This guide assumes you have access to a server accessible to the world through a
5005public IP (internet protocol) address and that you own a domain name used for
5006this Gemini capsule.
5007
5008* Getting Started with Agate
5009
5010We are going to use [[https://github.com/mbrubeck/agate][Agate]] for this tutorial. This is a basic Gemini server
5011written in Rust. It takes very little time and maintenance to get it running.
5012
5013* Install Dependencies
5014
5015First, you will need to install the Rust package for your system. On Ubuntu, use
5016the following commands (remember to use =sudo= if you are not the root user).
5017The Rust installation will give you options to customize the installation; I
5018used the default installation options.
5019
5020#+begin_src sh
5021sudo apt update && sudo apt upgrade -y
5022curl https://sh.rustup.rs -sSf | sh
5023#+end_src
5024
5025Remember to configure your shell with the new configuration:
5026
5027#+begin_src sh
5028source $HOME/.cargo/env
5029#+end_src
5030
5031Before we install agate, make sure you have the =gcc= package installed:
5032
5033#+begin_src sh
5034sudo apt install gcc
5035#+end_src
5036
5037Next, you'll need to install the agate executable with Rust's Cargo
5038package maintainer:
5039
5040#+begin_src sh
5041cargo install agate
5042#+end_src
5043
5044* Create Symlinks
5045
5046Once Cargo has finished installing all the required packages, create a symbolic
5047link to the executable to your $PATH.
5048
5049#+begin_src sh
5050sudo ln -s $HOME/.cargo/bin/agate /usr/local/bin/agate
5051#+end_src
5052
5053* Using Agate's Built-In Installation Tool
5054
5055If you're running Ubuntu or Debian, use the Debian installation script found in
5056Agate's GitHub repository, under the =tools/debian= folder.
5057
5058#+begin_src sh
5059git clone https://github.com/mbrubeck/agate
5060cd agate/tools/debian
5061sudo ./install.sh
5062#+end_src
5063
5064* Configure the Gemini Service
5065
5066We have a little more to do, but since this script tries to immediately run the
5067service, it will likely fail with an exit code. Let's add our finishing touches.
5068Edit the following file and replace the hostname with your desired URL (uniform
5069resource locator). You can also change the directory where content will be
5070served.
5071
5072#+begin_src sh
5073sudo nano /etc/systemd/system/gemini.service
5074#+end_src
5075
5076#+begin_src sh
5077# Edit these lines to whatever you want - see the next code block for my personal configuration.
5078WorkingDirectory=/srv/gemini
5079ExecStart=agate --hostname $(uname -n) --lang en
5080#+end_src
5081
5082This is my personal config:
5083
5084#+begin_src sh
5085WorkingDirectory=/var/gemini/
5086ExecStart=agate --hostname gemini.example.com --lang en
5087#+end_src
5088
5089Since we've altered the systemd configuration files, we have to reload the
5090daemon. Let's do that, restart our service, and check its status.
5091
5092#+begin_src sh
5093sudo systemctl daemon-reload
5094sudo systemctl restart gemini.service
5095sudo systemctl status gemini.service
5096#+end_src
5097
5098* Fixing Systemd Errors
5099
5100If you're still getting errors, the installation process may not have properly
5101enabled the gemini service. Fix it with the following commands.
5102
5103#+begin_src sh
5104sudo systemctl enable gemini.service
5105sudo systemctl restart gemini.service
5106sudo systemctl status gemini.service
5107#+end_src
5108
5109* Firewall Rules
5110
5111Great! Our server is now functional and running. The first consideration now is
5112that you need to be able to access port 1965 on the server. If you have a
5113firewall enabled, you'll need to open that port up.
5114
5115#+begin_src sh
5116sudo ufw allow 1965
5117sudo ufw reload
5118#+end_src
5119
5120* Creating Content
5121
5122Let's create the Gemini capsule. Note that wherever you set the
5123=WorkingDirectory= variable to earlier, Agate will expect you to put your Gemini
5124capsule contents in a sub-folder called =content=. So, I place my files in
5125=/var/gmi/content=. I'm going to create that folder now and put a file in there.
5126
5127#+begin_src sh
5128sudo mkdir /var/gemini/content
5129sudo nano /var/gemini/content/index.gmi
5130#+end_src
5131
5132You can put whatever you want in the =index.gmi= file, just make sure it's valid
5133Gemtext.
5134
5135* The Results
5136
5137To view the results, you can use a Gemini browser, such as [[https://gmi.skyjake.fi/lagrange/][Lagrange]] or [[https://github.com/makeworld-the-better-one/amfora][amfora]].
5138#+date: <2021-04-23 Fri 00:00:00>
5139#+title: Building a Lightweight PHP Static Commenting System Without JavaScript
5140#+description: Technical walkthrough for constructing a lightweight, privacy-oriented static comment system using PHP, with no client-side scripting or external dependencies.
5141#+slug: php-comment-system
5142
5143* The Terrible-ness of Commenting Systems
5144
5145The current state of affairs regarding interactive comment systems is, well,
5146terrible. It is especially awful if you're a privacy conscious person who does
5147not generally load third-party scripts or frames on the websites you visit.
5148
5149Even further, many comment systems are charging exorbitant fees for something
5150that should be standard.
5151
5152Of course, there are some really terrible options:
5153
5154- Facebook Comments
5155- Discourse
5156
5157There are some options that are better but still use too many scripts, frames,
5158or social integrations on your web page that could impact some users:
5159
5160- Disqus
5161- Isso
5162- Remark42
5163
5164Lastly, I looked into a few unique ways of generating blog comments, such as
5165using Twitter threads or GitHub issues to automatically post issues. However,
5166these both rely on external third-party sites that I don't currently use.
5167
5168* Stay Static with Server-Side Comments
5169
5170The main issue for my personal use-case is that my blog is completely, 100%
5171static. I use PHP on the back-end but website visitors only see HTML (Hypertext
5172Markup Language) and a single CSS (Cascading Style Sheets) file. No external
5173JavaScript and no embedded frames.
5174
5175So, how do we keep a site static and still allow users to interact with blog
5176posts? The key actually pretty simple - I'm already using PHP, so why not rely
5177on the classic HTML =<form>= and a PHP script to save the comments somewhere? As
5178it turns out, this was a perfect solution for me.
5179
5180The second issue for my personal use-case is that I am trying to keep the
5181contents of my website accessible over time, as described by [cite/t:@brandur],
5182in his post entitled [[https://brandur.org/fragments/graceful-degradation-time][Blog with Markdown + Git, and degrade gracefully through
5183time]] .
5184
5185This means I cannot rely on a database for comments, since I do not rely on a
5186database for any other part of my websites.
5187
5188I blog in plain Markdown files, commit all articles to Git, and ensure that
5189future readers will be able to see the source data long after I'm gone, or the
5190website has gone offline. However, I still haven't committed any images served
5191on my blog to Git, as I'm not entirely sold on Git LFS (large file storage)
5192yet - for now, images can be found at [[https://img.cleberg.net][img.cleberg.net]].
5193
5194Saving my comments back to the Git repository ensures that another aspect of my
5195site will degrade gracefully.
5196
5197* Create a Comment Form
5198
5199Okay, let's get started. The first step is to create an HTML form that users can
5200see and utilize to submit comments. This is fairly easy and can be changed
5201depending on your personal preferences.
5202
5203Take a look at the code block below for the form I currently use. Note that
5204=<current-url>= is replaced automatically in PHP with the current post's URL
5205(uniform resource locator), so that my PHP script used later will know which
5206blog post the comment is related to.
5207
5208The form contains the following structure:
5209
52101. =<form>= - This is the form and will determine which PHP script to send the
5211 comment to.
52122. =<section hidden>= - This section is hidden from the user and is used to
5213 ensure that we know which blog post sent the comment.
52143. =<section>= Display Name (Optional) - Used to accept a display name, if
5215 entered.
52164. =<section>= Comment (Required) - Used to accept the user's full comment.
5217 Markdown is allowed.
52185. =<button>= - A button to submit the form.
5219
5220#+begin_src html
5221<form action="/comment.php" method="POST">
5222 <h3>Leave a Comment</h3>
5223 <section hidden>
5224 <label class="form-label" for="postURL">Post URL</label>
5225 <input
5226 class="form-control"
5227 id="postURL"
5228 name="postURL"
5229 type="text"
5230 value="<current-url>"
5231 />
5232 </section>
5233 <section>
5234 <label class="form-label" for="userName">Display Name</label>
5235 <input
5236 class="form-control"
5237 id="userName"
5238 name="userName"
5239 placeholder="John Doe"
5240 type="text"
5241 />
5242 </section>
5243 <section>
5244 <label class="form-label" for="userContent">Your Comment</label>
5245 <textarea
5246 class="form-control"
5247 id="userContent"
5248 name="userContent"
5249 rows="3"
5250 placeholder="# Feel free to use Markdown"
5251 aria-describedby="commentHelp"
5252 required
5253 ></textarea>
5254 <div id="commentHelp" class="form-text">
5255 Comments are saved as Markdown and cannot be edited or deleted.
5256 </div>
5257 </section>
5258 <button type="submit">Submit</button>
5259</form>
5260#+end_src
5261
5262* Handle Comments via POST
5263
5264Now that we have a form and can submit comments, we need to create a PHP script
5265so that the server can fetch the comment data and save it. Make sure your PHP
5266script name matches the name you entered in the =action= field in your form.
5267
5268See the code block below for the contents of my =comment.php= script. We perform
5269the following tasks in this script:
5270
52711. Grab the POST data from the HTML form.
52722. Sanitize the comment data with =htmlentities=.
52733. Set the display name to =Anonymous= if it was left blank.
52744. Create a PHP object that combines all of this data.
52755. Check if our destination file =comments.json= exists.
52766. If so, convert the PHP object to JSON (JavaScript Object Notation) and save
5277 it to the file.
52787. If the =comments.json= file does not exist, the script will exit with an
5279 error. You can alter this to ensure it creates the script, but my source code
5280 includes the file by default, so it will always exist.
52818. Finally, send the user back to the =#comments= section of the blog post they
5282 just read.
5283
5284#+begin_src php
5285// Get the content sent from the comment form
5286$comment = htmlentities($_POST['userContent']);
5287$post_url = $_POST['postURL'];
5288
5289// Set default values if blank
5290if (isset($_POST['userName']) && trim($_POST['userName']) !== "") {
5291 $username = $_POST['userName'];
5292} else {
5293 $username = 'Anonymous';
5294}
5295
5296// Create an empty PHP object
5297$user_object = new stdClass();
5298
5299// Add object content
5300$user_object->timestamp = date('Y-m-d H:i:s');
5301$user_object->username = $username;
5302$user_object->comment = $comment;
5303$user_object->post_url = $post_url;
5304
5305// Append JSON to file
5306$file_name = 'comments.json';
5307if (file_exists($file_name)) {
5308 $source_data = file_get_contents($file_name);
5309 $temp_array = json_decode($source_data);
5310 array_push($temp_array, $user_object);
5311 $json_data = json_encode($temp_array, JSON_PRETTY_PRINT);
5312 file_put_contents($file_name, $json_data);
5313} else {
5314 die('Error: The "comments.json" file does not exist.');
5315}
5316
5317// Send the user back
5318header('Location: ' . $post_url . '#comments');
5319#+end_src
5320
5321If you're using Apache, make sure the =www-data= user on your server has the
5322correct permissions to your website directory or else it will not be able to
5323write to =comments.json=.
5324
5325#+begin_src sh
5326chgrp -R www-data /path/to/website/
5327chmod -R g+w comments.json
5328#+end_src
5329
5330* Display User Comments
5331
5332Now that we can submit comments, and they are saved to the =comments.json= file,
5333let's make sure we can show those comments in each blog post.
5334
5335The code block below shows the function I use to decode my =comments.json= file,
5336check if the comments apply to the current post, and then display them.
5337
5338This piece of code should *really* be inside a function (or at least in an
5339organized PHP workflow). Don't just copy-and-paste and expect it to work. You
5340need to at least supply a =$query= variable depending on the page visited.
5341
5342#+begin_src php
5343$query = 'your-blog-post.html';
5344
5345// Load saved comments
5346$comments_file = 'comments.json';
5347$comments_raw = file_get_contents($comments_file);
5348$comments = json_decode($comments_raw);
5349$comment_section = '<section id="comments" class="comments"><h3>Comments</h3>';
5350foreach ($comments as $comment) {
5351 if ($comment->post_url == "https://example.com/post/" . $query) {
5352 // Assign metadata to variables
5353 $comment_timestamp = $comment->timestamp;
5354 $comment_username = $comment->username;
5355 $comment_content = $comment->comment;
5356
5357 // Parse the comment, in case it contains Markdown
5358 $comment_md = Parsedown::instance()->text($comment_content);
5359 $comment_html = new DOMDocument();
5360 $comment_html->loadHTML($comment_md);
5361 $comment_html_links = $comment_html->getElementsByTagName('a');
5362 foreach ($comment_html_links as $comment_html_link) {
5363 $comment_html_link->setAttribute('rel', 'noreferrer');
5364 $comment_html_link->setAttribute('target', '_blank');
5365 }
5366 $comment_secured_html = $comment_html->saveHTML();
5367
5368 // Apply metadata to comments section
5369 $comment_section .= '<div class="user-comment"><div class="row"><label>Timestamp:</label><p>' . $comment_timestamp . '</p></div><div class="row"><label>Name:</label><p>' . $comment_username . '</p></div><div class="row markdown"><label>Comment:</label><div class="comment-markdown">' . $comment_secured_html . '</div></div></div>';
5370 }
5371}
5372
5373echo $comment_section;
5374#+end_src
5375
5376* Bonus: Create a 'Recent Comments' Page
5377
5378Finally, the last part of my current system is to create a Recent Comments page
5379so that I can easily check-in on my blog and moderate any spam. As an
5380alternative, you could use PHP's =mail()= function to email you for each blog
5381comment.
5382
5383The code to do this is literally the same code as the previous section, I just
5384make sure it is printed when someone visits =https://example.com/comments/=.
5385
5386* Possible Enhancements
5387
5388This comment system is by no means a fully-developed system. I have noted a few
5389possible enhancements here that I may implement in the future:
5390
5391- Create a secure moderator page with user authentication at
5392 =https://blog.example.com/mod/=. This page could have the option to edit or
5393 delete any comment found in =comments.json=.
5394- Create a temporary file, such as =pending_comments.json=, that will store
5395 newly-submitted comments and won't display on blog posts until approved by a
5396 moderator.
5397- Create a =/modlog/= page with a chronological log, showing which moderator
5398 approved which comments and why certain comments were rejected.
5399#+date: <2021-04-28 Wed 00:00:00>
5400#+title: Getting Started with Photography: Gear and Best Practices
5401#+description: Instructional document covering basic techniques in photography, including equipment selection, operational best practices, and methods for capturing high-quality images.
5402#+slug: photography
5403
5404* Why Photography?
5405
5406I've often wondered why photography is as enticing as it is. You can see
5407billions of people around the world taking photographs every single moment of
5408the day. New technology often boasts about their photographic capabilities,
5409especially smartphones. I would even assume that we live in a world where there
5410is never a moment in which a photograph is not being taken somewhere on Earth.
5411
5412As for myself, I would simply say that I enjoy preserving a memory in physical
5413(or digital) form. I've never had the best memory when it comes to recalling
5414details of places and people gone by, so it helps to have a frame of reference
5415lying around.
5416
5417Regardless of the reason, I think most people would agree that you simply cannot
5418have too many hobbies.
5419
5420* Older Cameras
5421
5422I started playing around with the idea of photography when my family purchased a
5423Fujifilm camera for family-specific events. I don't recall the specific model,
5424but I do recall it was a point-and-shoot camera without an interchangeable lens.
5425However, it was of great value to someone, like myself, who couldn't afford any
5426other camera. I took about 10,000 shots with that camera over a 3-5 year span.
5427Most notably, all of my trips to California were documented through this camera.
5428
5429When possible, I would borrow my sister's camera, which is a Sony SLT-A58. This
5430camera was great and allowed for some of my best early shots, especially those
5431taken in Utah's and Nevada's parks.
5432
5433* My Current Kit
5434
5435I've finally come to a point in my life where I have the disposable income to
5436invest in a solid photography kit. I played around with the idea of a lot of
5437different cameras, different types, new vs used, etc. Finally, I settled on the
5438[[https://en.wikipedia.org/wiki/Sony_%CE%B17_III][Sony α7 III]]. This camera is mirror-less and uses a full-frame image sensor at 24
5439megapixels. I don't create large prints, and I am mostly focused on preserving
5440memories in high quality for the next 5-10 years with this camera, so the
5441specifications here are just perfect for me.
5442
5443For lenses, I decided to buy two lenses that could carry me through most
5444situations:
5445
5446- [[https://electronics.sony.com/imaging/lenses/full-frame-e-mount/p/sel2470z][Vario-Tessar T** FE 24-70 mm F4 ZA OSS]]
5447- [[https://www.tamron-usa.com/product/lenses/a047.html][Tamron 70-300mm f4.5-6.3 Di III RXD]]
5448
5449In addition, I grabbed a couple [[https://www.promaster.com/Product/6725][HGX Prime 67mm]] protection filters for the
5450lenses.
5451
5452As I delve further into photography and pick up more skills, I will most likely
5453go back and grab a lens with a higher f-stop value, such as f/1.8. I toyed with
5454the idea of grabbing a 50 mm at =f/1.8=, but decided to keep things in a
5455reasonable price range instead.
5456
5457Finally, I made sure to buy a photography-specific backpack with a rain guard,
5458and the zipper on the back panel, to protect the equipment while wearing the
5459bag. If you've ever had to haul around a DSLR (digital single-lens reflex)
5460camera in a bag that only has a shoulder strap, you'll know the pain it can
5461cause. Putting all my equipment in a backpack was an easy decision.
5462#+date: <2021-05-30 Sun 00:00:00>
5463#+title: Automated Script for Correcting Git Author Details in Multiple Projects
5464#+description: Detailed instructions for systematically updating the Git author name and email information in multiple repositories using command-line tools and scripts.
5465#+slug: changing-git-authors
5466
5467* Changing Git Author/Email Based on Previously Committed Email
5468
5469Here's the dilemma: You've been committing changes to your Git repository with
5470an incorrect name or email (or multiple repositories), and now you want to fix
5471it. Luckily, there's a semi-reliable way to fix that. While I have never
5472experienced issues with this method, some people have warned that it can mess
5473with historical hashes and integrity of commits, so use this method only if
5474you're okay accepting that risk.
5475
5476Okay, let's create the bash script:
5477
5478#+begin_src sh
5479nano change_git_authors.sh
5480#+end_src
5481
5482The following information can be pasted directly into your bash script. The only
5483changes you need to make are to the following variables:
5484
5485- =OLD_EMAIL=
5486- =CORRECT_NAME=
5487- =CORRECT_EMAIL=
5488
5489#+begin_src sh
5490#!/bin/sh
5491
5492# List all sub-directories in the current directory
5493for dir in */
5494do
5495 # Remove the trailing "/"
5496 dir=${dir%*/}
5497 # Enter sub-directory
5498 cd $dir
5499
5500 git filter-branch --env-filter '
5501
5502 OLD_EMAIL="old@example.com"
5503 CORRECT_NAME="your-new-name"
5504 CORRECT_EMAIL="new@example.com"
5505
5506 if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]
5507 then
5508 export GIT_COMMITTER_NAME="$CORRECT_NAME"
5509 export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
5510 fi
5511 if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]
5512 then
5513 export GIT_AUTHOR_NAME="$CORRECT_NAME"
5514 export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"
5515 fi
5516 ' --tag-name-filter cat -- --branches --tags
5517
5518 git push --force --tags origin 'refs/heads/*'
5519
5520 cd ..
5521done
5522#+end_src
5523
5524Finally, save the bash script and make it executable.
5525
5526#+begin_src sh
5527chmod a+x change_git_authors.sh
5528#+end_src
5529
5530Now you can run the script and should see the process begin.
5531
5532#+begin_src sh
5533./change_git_authors.sh
5534#+end_src
5535#+date: <2021-07-15 Thu 00:00:00>
5536#+title: Automated Deletion of All GitLab Repositories Using Python
5537#+description: Stepwise instructions for executing a Python script to delete all repositories associated with a GitLab account. Includes authentication and error handling procedures.
5538#+slug: delete-gitlab-repos
5539
5540* Background
5541
5542Have you ever used GitLab to host your source code, moved to a different host,
5543and wanted to delete everything from your GitLab account? Well, this post covers
5544any scenario where you would want to delete all repositories from your GitLab
5545account.
5546
5547For me, I currently maintain around 30 repositories and don't like to manually
5548delete them whenever I switch host. GitHub has a few different tools online to
5549delete all repositories for you, but I have not found anything similar for
5550GitLab, so I needed an alternative solution.
5551
5552* Use a Python Script
5553
5554** Requirements
5555
5556Before we look at the script, make sure you know your GitLab username. Next,
5557[[https://gitlab.com/-/profile/personal_access_tokens][create an authorization token]] so that the Python script can delete your
5558repositories. Don't lose this token or else you'll need to create a new one.
5559
5560** Create the Script
5561
5562To run a Python script, you must first create it. Open a terminal and enter the
5563following commands in whichever directory you prefer to store the script. You
5564can do the same things in a file manager if you prefer.
5565
5566#+begin_src sh
5567mkdir delete-gitlab
5568#+end_src
5569
5570#+begin_src sh
5571cd delete-gitlab
5572#+end_src
5573
5574#+begin_src sh
5575nano main.py
5576#+end_src
5577
5578Enter the following code into your =main.py= script.
5579
5580#+begin_src python
5581import request
5582import json
5583
5584
5585def get_project_ids():
5586 url = "https://gitlab.com/api/v4/users/{user-id}/projects"
5587
5588 querystring = {"owned": "true", "simple": "true", "per_page": "50"}
5589
5590 payload = ""
5591 headers = {'authorization': 'Bearer {auth-token}'}
5592
5593 response = requests.request("GET", url, data=payload, headers=headers, params=querystring)
5594
5595 projects = json.loads(response.text)
5596 projects_ids = list(map(lambda project: project.get('id'), projects))
5597
5598 return projects_ids
5599
5600
5601def remove_project(project_id):
5602 url_temp = "https://gitlab.com/api/v4/projects/{project}"
5603 headers = {'authorization': 'Bearer {auth-token}'}
5604 querystring = ""
5605 payload = ""
5606
5607 url = url_temp.format(project=project_id)
5608
5609 response = requests.request("DELETE", url, data=payload, headers=headers, params=querystring)
5610 project = json.loads(response.text)
5611 print(project)
5612
5613
5614def main():
5615 projects_ids = get_project_ids()
5616
5617 url_temp = "https://gitlab.com/api/v4/projects/{project}"
5618 headers = {'authorization': 'Bearer {auth-token}'}
5619 querystring = ""
5620 payload = ""
5621
5622 for project_id in projects_ids:
5623 url = url_temp.format(project=project_id)
5624
5625 response = requests.request("GET", url, data=payload, headers=headers, params=querystring)
5626 project = json.loads(response.text)
5627 print(str(project.get('id')) + " " + project.get('name'))
5628 print("Removing...")
5629 remove_project(project_id)
5630
5631
5632if __name__ == "__main__":
5633 main()
5634#+end_src
5635
5636Now that you have the proper information, replace ={user-id}= with your GitLab
5637username and ={auth-token}= with the authorization token you created earlier.
5638
5639Finally, simply run the script and watch the output. You can also use PyCharm
5640Community Edition to edit and run the Python script if you don't want to work in
5641a terminal.
5642
5643#+begin_src sh
5644python3 main.py
5645#+end_src
5646#+date: <2021-08-25 Wed 00:00:00>
5647#+title: Audit Sampling Made Easy: Using Pandas for Random and Stratified Samples
5648#+description: Presentation of methods for implementing audit sampling techniques including simple random, stratified, and systematic sampling using Python's Pandas library for precise audit outcomes.
5649#+slug: audit-sampling
5650
5651* Introduction
5652
5653For anyone who is familiar with internal auditing, external auditing, or
5654consulting, you will understand how tedious audit testing can become when you
5655are required to test large swaths of data. When we cannot establish an automated
5656means of testing an entire population, we generate samples to represent the
5657population of data. This helps ensure we can have a small enough data pool to
5658test and that our results still represent the population.
5659
5660However, sampling data within the world of audit still seems to confuse quite a
5661lot of people. While some audit-focused tools have introduced sampling
5662functionality (e.g. Wdesk), many audit departments and firms cannot use software
5663like this due to certain constraints, such as the team's budget or knowledge.
5664Here is where this article comes in: we're going to use [[https://www.python.org][Python]], a free and
5665open-source programming language, to generate random samples from a dataset in
5666order to suffice numerous audit situations.
5667
5668* Audit Requirements for Sampling
5669
5670Before we get into the details of how to sample with Python, I want to make sure
5671I discuss the different requirements that auditors may have of samples used
5672within their projects.
5673
5674** Randomness
5675
5676First, let's discuss randomness. When testing out new technology to help assist
5677with audit sampling, you need to understand exactly how your samples are being
5678generated. For example, if the underlying function is just picking every 57th
5679element from a list, that's not truly random; it's a systematic form of
5680sampling. Luckily, since Python is open-source, we have access to its codebase.
5681Through this blog post, I will be using the [[https://pandas.pydata.org][pandas]] module in order to generate
5682the random samples. More specifically, I will be using the
5683[[https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.sample.html][pandas.DataFrame.sample]] function provided by Pandas.
5684
5685Now that you know what you're using, you can always check out the code behind
5686=pandas.DataFrame.sample=. This function does a lot of work, but we really only
5687care about the following snippets of code:
5688
5689#+begin_src python
5690# Process random_state argument
5691rs = com.random_state(random_state)
5692
5693...
5694
5695locs = rs.choice(axis_length, size=n, replace=replace, p=weights)
5696result = self.take(locs, axis=axis)
5697if ignore_index:
5698result.index = ibase.default_index(len(result))
5699
5700return result
5701#+end_src
5702
5703The block of code above shows you that if you assign a =random_state= argument
5704when you run the function, that will be used as a seed number in the random
5705generation and will allow you to reproduce a sample, given that nothing else
5706changes. This is critical to the posterity of audit work. After all, how can you
5707say your audit process is adequately documented if the next person can't run the
5708code and get the same sample? The final piece here on randomness is to look at
5709the [[https://docs.python.org/3/library/random.html#random.choice][choice]] function used above. This is the crux of the generation and can also
5710be examined for more detailed analysis on its reliability. As far as auditing
5711goes, we will trust that these functions are mathematically random.
5712
5713** Sample Sizes
5714
5715As mentioned in the intro, sampling is only an effective method of auditing when
5716it truly represents the entire population. While some audit departments or firms
5717pmay consider certain judgmental sample sizes to be adequate, you may need to
5718rely on statistically-significant confidence levels of sample testing at certain
5719
5720points. I will demonstrate both here. For statistically-significant confidence
5721levels, most people will assume a 90% - 99% confidence level. In order to
5722actually calculate the correct sample size, it is best to use statistical tools
5723due to the tedious math work required. For example, for a population of 1000,
5724and a 90% confidence level that no more than 5% of the items are nonconforming,
5725you would sample 45 items.
5726
5727However, in my personal experience, many audit departments and firms do not use
5728statistical sampling. Most people use a predetermined, often proprietary, table
5729that will instruct auditors which sample sizes to choose. This allows for
5730uniform testing and reduces overall workload. See the table below for a common
5731implementation of sample sizes:
5732
5733| Control Frequency | Sample Size (High Risk) | Sample Size (Low Risk) |
5734|-------------------+-------------------------+------------------------|
5735| More Than Daily | 40 | 25 |
5736| Daily | 40 | 25 |
5737| Weekly | 12 | 5 |
5738| Monthly | 5 | 3 |
5739| Quarterly | 2 | 2 |
5740| Semi-Annually | 1 | 1 |
5741| Annually | 1 | 1 |
5742| Ad-hoc | 1 | 1 |
5743
5744*** Sampling with Python & Pandas
5745
5746In this section, I am going to cover a few basic audit situations that require
5747sampling. While some situations may require more effort, the syntax,
5748organization, and intellect used remain largely the same. If you've never used
5749Python before, note that lines starting with a '=#=' symbol are called comments,
5750and they will be skipped by Python. I highly recommend taking a quick tutorial
5751online to understand the basics of Python if any of the code below is confusing
5752to you.
5753
5754** Simple Random Sample
5755
5756First, let's look at a simple, random sample. The code block below will import
5757the =pandas= module, load a data file, sample the data, and export the sample to
5758a file.
5759
5760#+begin_src python
5761# Import the Pandas module
5762import pandas
5763
5764# Specify where to find the input file & where to save the final sample
5765file_input = r'Population Data.xlsx'
5766file_output = r'Sample.xlsx'
5767
5768# Load the data with pandas
5769# Remember to use the sheet_name parameter if your Excel file has multiple sheets
5770df = pandas.read_excel(file_input)
5771
5772# Sample the data for 25 selections
5773# Remember to always use the random_state parameter so the sample can be re-performed
5774sample = df.sample(n=25, random_state=0)
5775
5776# Save the sample to Excel
5777sample.to_excel(file_output)
5778#+end_src
5779
5780** Simple Random Sample: Using Multiple Input Files
5781
5782Now that we've created a simple sample, let's create a sample from multiple
5783files.
5784
5785#+begin_src python
5786# Import the Pandas module
5787import pandas
5788
5789# Specify where to find the input file & where to save the final sample
5790file_input_01 = r'Population Data Q1.xlsx'
5791file_input_02 = r'Population Data Q2.xlsx'
5792file_input_03 = r'Population Data Q3.xlsx'
5793file_output = r'Sample.xlsx'
5794
5795# Load the data with pandas
5796# Remember to use the sheet_name parameter if your Excel file has multiple sheets
5797df_01 = pandas.read_excel(file_input_01)
5798df_02 = pandas.read_excel(file_input_02)
5799df_03 = pandas.read_excel(file_input_03)
5800
5801# Sample the data for 5 selections from each quarter
5802# Remember to always use the random_state parameter so the sample can be re-performed
5803sample_01 = df_01.sample(n=5, random_state=0)
5804sample_02 = df_02.sample(n=5, random_state=0)
5805sample_03 = df_03.sample(n=5, random_state=0)
5806
5807# If required, combine the samples back together
5808sample = pandas.concat([sample_01, sample_02, sample_03], ignore_index=True)
5809
5810# Save the sample to Excel
5811sample.to_excel(file_output)
5812#+end_src
5813
5814** Stratified Random Sample
5815
5816Well, what if you need to sample distinct parts of a single file? For example,
5817let's write some code to separate our data by "Region" and sample those regions
5818independently.
5819
5820#+begin_src python
5821# Import the Pandas module
5822import pandas
5823
5824# Specify where to find the input file & where to save the final sample
5825file_input = r'Sales Data.xlsx'
5826file_output = r'Sample.xlsx'
5827
5828# Load the data with pandas
5829# Remember to use the sheet_name parameter if your Excel file has multiple sheets
5830df = pandas.read_excel(file_input)
5831
5832# Stratify the data by "Region"
5833df_east = df[df['Region'] == 'East']
5834df_west = df[df['Region'] == 'West']
5835
5836# Sample the data for 5 selections from each quarter
5837# Remember to always use the random_state parameter so the sample can be re-performed
5838sample_east = df_east.sample(n=5, random_state=0)
5839sample_west = df_west.sample(n=5, random_state=0)
5840
5841# If required, combine the samples back together
5842sample = pandas.concat([sample_east, sample_west], ignore_index=True)
5843
5844# Save the sample to Excel
5845sample.to_excel(file_output)
5846#+end_src
5847
5848** Stratified Systematic Sample
5849
5850This next example is quite useful if you need audit coverage over a certain time
5851period. This code will generate samples for each month in the data and combine
5852them all together at the end. Obviously, this code can be modified to stratify
5853by something other than months, if needed.
5854
5855#+begin_src python
5856# Import the Pandas module
5857import pandas
5858
5859# Specify where to find the input file & where to save the final sample
5860file_input = r'Sales Data.xlsx'
5861file_output = r'Sample.xlsx'
5862
5863# Load the data with pandas
5864# Remember to use the sheet_name parameter if your Excel file has multiple sheets
5865df = pandas.read_excel(file_input)
5866
5867# Convert the date column to datetime so the function below will work
5868df['Date of Sale'] = pandas.to_datetime(df['Date of Sale'])
5869
5870# Define a function to create a sample for each month
5871def monthly_stratified_sample(df: pandas.DataFrame, date_column: str, num_selections: int) -> pandas.DataFrame:
5872 static_num_selections = num_selections final_sample = pandas.DataFrame()
5873 for month in range(1, 13):
5874 num_selections = static_num_selections
5875 rows_list = []
5876 for index, row in df.iterrows():
5877 df_month = row[date_column].month
5878 if month == df_month:
5879 rows_list.append()
5880 monthly_df = pd.DataFrame(data=rows_list)
5881 if (len(monthly_df)) == 0:
5882 continue
5883 elif not (len(monthly_df) > sample_size):
5884 num_selections = sample_size
5885 elif len(monthly_df) >= sample_size:
5886 num_selections = sample_size
5887 sample = monthly_df.sample(n=num_selections, random_state=0)
5888 final_sample = final_sample.append(sample)
5889 return sample
5890
5891# Sample for 3 selections per month
5892sample_size = 3
5893sample = monthly_stratified_sample(df, 'Date of Sale', sample_size)
5894sample.to_excel(file_output)
5895#+end_src
5896
5897*** Documenting the Results
5898
5899Once you've generated a proper sample, there are a few things left to do in
5900order to properly ensure your process is reproducible.
5901
59021. Document the sample. Make sure the resulting file is readable and includes
5903 the documentation listed in the next bullet.
59042. Include documentation around the data source, extraction techniques, any
5905 modifications made to the data, and be sure to include a copy of the script
5906 itself.
59073. Whenever possible, perform a completeness and accuracy test to ensure your
5908 sample is coming from a complete and accurate population. To ensure
5909 completeness, compare the record count from the data source to the record
5910 count loaded into Python. To ensure accuracy, test a small sample against the
5911 source data (e.g., test 5 sales against the database to see if the details
5912 are accurate).
5913#+date: <2021-10-09 Sat 00:00:00>
5914#+title: Setting Up Apache Rewrite Rules for Extensionless URLs
5915#+description: Technical guide to applying Apache mod_rewrite directives for transforming .html file requests into clean directory-style URL structures to enhance website navigation and indexing.
5916#+slug: apache-redirect
5917
5918* The Problem
5919
5920After recently switching static site generators (SSG), my blog URLs (uniform
5921resource locators) changed with no option to preserve the classic =.html=
5922extension at the end of my blog post URLs.
5923
5924I really disliked using my old SSG ([[https://jekyllrb.com][Jekyll]]) and prefer my new tool ([[https://www.getzola.org][Zola]]) much
5925more, so I was determined to figure out a way to get the proper redirect set up
5926so that people who find my posts online aren't constantly met by 404 errors.
5927
5928* The Solution
5929
5930To solve this problem, I really needed to solve two pieces:
5931
59321. Redirect all blog post URL requests from =/blog/some-post.html= to
5933 =/blog/some-post/=.
59342. Ensure that no other =.html= files are redirected, such as =index.html=.
5935
5936After /a lot/ of tweaking and testing, I believe I have finally found the
5937solution. The solution is shown below.
5938
5939#+begin_src conf
5940RewriteEngine On
5941RewriteCond %{REQUEST_URI} !\index.html$ [NC]
5942RewriteRule ^(.*).html$ https://example.com/$1 [R=301,L]
5943#+end_src
5944
5945This piece of code in the Apache =.conf= or =.htaccess= file will do the
5946following:
5947
59481. Turn on the RewriteEngine so that we can modify URLs.
59492. Ignore any =index.html= files from the rule we are about to specify.
59503. Find any =.html= files within the website directory and redirect it to
5951 exclude the file extension.
59524. The final piece is adding the trailing slash (=/=) at the end of the URL -
5953 you'll notice that I don't have an Apache rule for that since Apache handles
5954 that automatically.
5955#+date: <2021-12-04 Sat 00:00:00>
5956#+title: How I Prepared for and Passed the CISA Exam: A Practical Study Guide
5957#+description: Comprehensive outline of study materials, preparation strategies, and procedural advice for passing the CISA examination on the initial attempt.
5958#+slug: cisa
5959
5960* What is the CISA?
5961
5962For those of you lucky enough not to be knee-deep in the world of information
5963technology (IT) and information systems (IS) auditing, the [[https://www.isaca.org/credentialing/cisa][CISA]] (Certified
5964Information Systems Auditor) certification and exam are part of ISACA's suite of
5965certifications. As I often explain it to people like my family, it basically
5966means you're employed to use your knowledge of information systems, regulations,
5967common threats, risks, etc. in order to assess an organization's current control
5968of their risk. If a risk isn't controlled (and the company doesn't want to
5969accept the risk), an IS auditor will suggest implementing a control to address
5970that risk.
5971
5972Now, the CISA certification itself is, in my opinion, the main certification for
5973this career. While certifications such as the CPA (Certified Public Accountant)
5974or CISSP (Certified Information Systems Security Professional) are beneficial,
5975nothing matches the power of the CISA for an IS auditor when it comes to getting
5976hired, getting a raise/bonus, or earning respect in the field.
5977
5978However, to be honest, I am a skeptic of most certifications. I understand the
5979value they hold in terms of how much you need to commit to studying or learning
5980on the job, as well as the market value for certifications such as the CISA. But
5981I also have known some very +incompetent+ /less than stellar/ auditors who have
5982relevant certifications.
5983
5984The same goes for most industries: if a person is good at studying, they can
5985earn the certification. However, that knowledge means nothing unless you're
5986actually able to use it in real life and perform as expected of a certification
5987holder. The challenge comes when people are hired or connected strictly because
5988of their certifications or resume; you need to see a person work before you can
5989assume them having a CISA means they're better than someone without the CISA.
5990
5991Okay, rant over. Certifications are generally accepted as a measuring stick of
5992commitment and quality of an employee, so I am accepting it too.
5993
5994* Exam Content
5995
5996The CISA is broken down into five sections, each weighted with a percentage of
5997test questions that may appear.
5998
5999Since the exam contains 150 questions, here's how those sections break down:
6000
6001| Exam Section | Percentage of Exam | Questions |
6002|---------------+--------------------+-----------|
6003| 1 | 21% | 32 |
6004| 2 | 17% | 26 |
6005| 3 | 12% | 18 |
6006| 4 | 23% | 34 |
6007| 5 | 27% | 40 |
6008| *Grand Total* | *100%* | *150* |
6009
6010* My Studying Habits
6011
6012This part is a little hard for me to break down into specific detail due to the
6013craziness of the last year. While I officially purchased my studying materials
6014in December 2020 and opened them to "start studying" in January 2021, I really
6015wasn't able to study much due to the demands of my job and personal life.
6016
6017Let me approach this from a few different viewpoints.
6018
6019** Study Materials
6020
6021Let's start by discussing the study materials I purchased. I'll be referring to
6022#1 as the CRM (CISA Review Manual) and #2 as the QAE (Questions, Answers &
6023Explanations).
6024
60251. [[https://store.isaca.org/s/store#/store/browse/detail/a2S4w000004KoCbEAK][CISA Review Manual, 27th Edition | Print]]
60262. [[https://store.isaca.org/s/store#/store/browse/detail/a2S4w000004KoCcEAK][CISA Review Questions, Answers & Explanations Manual, 12th Edition | Print]]
6027
6028The CRM is an excellent source of information and could honestly be used as a
6029reference for most IS auditors as a learning reference during their daily audit
6030responsibilities. However, it is *full* of information and can be overloading if
6031you're not good at filtering out useless information while studying.
6032
6033The QAE is the real star of the show here. This book contains 1000 questions,
6034separated by exam section, and a practice exam. My only complaint about the QAE
6035is that each question is immediately followed with the correct answer and
6036explanations below it, which means I had to use something to constantly cover
6037the answers while I was studying.
6038
6039I didn't use the online database version of the QAE, but I've heard that it's
6040easier to use than the printed book. However, it is more expensive ($299
6041database vs $129 book) which might be important if you're paying for materials
6042yourself.
6043
6044In terms of question difficulty, I felt that the QAE was a good representation
6045of the actual exam. I've seen a lot of people online say it wasn't accurate to
6046the exam or that it was much easier/harder, but I disagree with all of those.
6047The exam was fairly similar to the QAE, just focusing on whichever topics they
6048chose for my version of the exam.
6049
6050If you understand the concepts, skim the CRM (and read in-depth on topics you
6051struggle with), and use the QAE to continue practicing exam-like questions, you
6052should be fine. I didn't use any online courses, videos, etc. - the ISACA
6053materials are more than enough.
6054
6055** Studying Process
6056
6057While I was able to briefly read through sections 1 and 2 in early 2021, I had
6058to stop and take a break from February/March to September. I switched jobs in
6059September, which allowed me a lot more free time to study.
6060
6061In September, I studied sections 3-5, took notes, and did a quick review of the
6062section topics. Once I felt comfortable with my notes, I took a practice exam
6063from the QAE manual and scored 70% (105/150).
6064
6065Here's a breakdown of my initial practice exam:
6066
6067| Exam Section | Incorrect | Correct | Grand Total | Percent |
6068|---------------+-----------+---------+-------------+---------|
6069| 1 | 8 | 25 | 33 | 76% |
6070| 2 | 5 | 20 | 25 | 80% |
6071| 3 | 6 | 12 | 18 | 67% |
6072| 4 | 10 | 23 | 33 | 70% |
6073| 5 | 16 | 25 | 41 | 61% |
6074| *Grand Total* | *45* | *105* | *150* | *70%* |
6075
6076As I expected, my toughest sections were related to project management,
6077development, implementation, and security.
6078
6079This just leaves October and November. For these months, I tried to practice
6080every few days, doing 10 questions for each section, until the exam. This came
6081out to 13 practice sessions, ~140 questions per section, and ~700 questions
6082total.
6083
6084While some practice sessions were worse and some were better, the final results
6085were similar to my practice exam results. As you can see below, my averages were
6086slightly worse than my practice exam. However, I got in over 700 questions of
6087practice and, most importantly, /I read through the explanations every time I
6088answered incorrectly and learned from my mistakes/.
6089
6090| Exam Section | Incorrect | Correct | Grand Total | Percent |
6091|---------------+-----------+---------+-------------+---------|
6092| 1 | 33 | 108 | 141 | 77% |
6093| 2 | 33 | 109 | 142 | 77% |
6094| 3 | 55 | 89 | 144 | 62% |
6095| 4 | 52 | 88 | 140 | 63% |
6096| 5 | 55 | 85 | 140 | 61% |
6097| *Grand Total* | *228* | *479* | *707* | *68%* |
6098
6099* Results
6100
6101Now, how do the practice scores reflect my actual results? After all, it's hard
6102to tell how good a practice regimen is unless you see how it turns out.
6103
6104| Exam Section | Section Name | Score |
6105|--------------+------------------------------------------------------------------+-------|
6106| 1 | Information Systems Auditing Process | 678 |
6107| 2 | Governance and Management of IT | 590 |
6108| 3 | Information Systems Acquisition, Development, and Implementation | 721 |
6109| 4 | Information Systems Operations and Business Resilience | 643 |
6110| 5 | Protection of Information Assets | 511 |
6111
6112Now, in order to pass the CISA, you need at least 450 on a sliding scale of
6113200-800. Personally, I really have no clue what an average CISA score is. After
6114a /very/ brief look online, I can see that the high end is usually in the low
6115700s. In addition, only about 50-60% of people pass the exam.
6116
6117Given this information, I feel great about my scores. 616 may not be phenomenal,
6118and I wish I had done better on sections 2 & 5, but my practicing seems to have
6119worked very well overall.
6120
6121However, the practice results do not conform to the actual results. Section 2
6122was one of my highest practice sections and was my second-lowest score in the
6123exam. Conversely, section 3 was my second-lowest practice section and turned out
6124to be my highest actual score!
6125
6126After reflecting, it is obvious that if you have any background on the CISA
6127topics at all, the most important part of studying is doing practice questions.
6128You really need to understand how to read the questions critically and pick the
6129best answer.
6130
6131* Looking Forward
6132
6133I am extremely happy that I was finally able to pass the CISA. Looking to the
6134future, I'm not sure what's next in terms of professional learning. My current
6135company offers internal learning courses, so I will most likely focus on that if
6136I need to gain more knowledge in certain areas.
6137
6138To be fair, even if you pass the CISA, it's hard to become an expert on any
6139specific topic found within. My career may take me in a different direction, and
6140I might need to focus more on security or networking certifications (or possibly
6141building a better analysis/visualization portfolio if I want to go into data
6142analysis/science).
6143
6144All I know is that I am content at the moment and extremely proud of my
6145accomplishment.
6146#+date: <2022-02-10 Thu 00:00:00>
6147#+title: From Cubicles to Home Offices: My Journey Through Changing Workspaces
6148#+description: Examination of factors influencing the shift from traditional office environments to remote work, including benefits, constraints, and implementation recommendations.
6149#+slug: leaving-the-office
6150
6151* The Working World is Changing
6152
6153There has been a trend for the past few years of companies slowly realizing that
6154the pandemic is not just a temporary state that will go away eventually and let
6155everything return to the way it was before. In terms of business and employment,
6156this means that more and more jobs are being offered as permanently remote
6157roles.
6158
6159I had always dreamt of working from home but thought of it as a fantasy,
6160especially since I did not want to move over into the software development
6161field. However, I have found that almost all roles being sent to me via
6162recruiters are permanently remote (although most are limited to US citizens or
6163even region-locked for companies who only operate in select states).
6164
6165I decided to take a look back at my relatively short career so far and compare
6166the positive and negative effects of the different work environments I've been
6167in.
6168
6169* In-Person Offices
6170
6171** Retail Internship
6172
6173I started my first job as a management intern at a busy retail pharmacy, working
6174my 40-hour weeks on my feet. As these retail stores don't believe in resting or
6175sitting down, you can guarantee that you will spend entire shifts standing,
6176walking, or running around the store. Unfortunately, I worked at a time when our
6177store didn't have enough managers, so I spent the majority of my tenure at the
6178store running and breaking a sweat.
6179
6180Now, things aren't all bad in retail stores like this. It is definitely tiring
6181and inefficient to force employees to work constantly, or pretend to work if
6182there's nothing to do, and not allow anyone to sit down. However, if you are
6183able to operate a retail store with a limited crew and provide enough comfort
6184and support, I believe these jobs could be both comfortable and efficient.
6185
6186** Semi-Private Cubicles
6187
6188After about a year, I was able to find another internship - this time, it was in
6189my field of interest: internal auditing. This was for a life insurance company
6190that was well over 100 years old. The age of the company shows if you work
6191there, as most people in management are well into their 40s-60s with little to
6192no youthful leadership in the company. Likewise, they owned a large headquarters
6193in a nice area of town with plenty of space, parking, etc.
6194
6195One upside is that each person gets their own large L-shaped desk, formed into
6196cubicles that house 4 desks/employees. These "pods" of 4-person cubicles are
6197linked throughout each floor of the headquarters (except the sales people, who
6198had that open-floor concept going on). The walls of the cubicle were tall and
6199provided a lot of privacy and sound-proofing, except when I used the standing
6200desk feature (I'm over 6 feet tall, so probably not an issue for most people).
6201
6202I loved this environment, it allowed me to focus on my work with minimal
6203distractions, but also allowed easy access, so I could spin around in my chair
6204and chat with my friends without leaving my chair. This is the closest I've been
6205to a home office environment (which is my personal favorite, as I'll get to
6206later in this post).
6207
6208** Semi-Open Floor Concept
6209
6210When I shifted to my first full-time internal audit job out of college, I was
6211working at a company that was headquartered on a floor in a downtown high-rise
6212building. The company was only about 20 years old when I worked there and were
6213trying a lot of new things to attract young talent, one of which was a semi-open
6214floor concept for the office. My department worked just around the hallway
6215corner from the executive offices and used that "modern" layout young tech
6216companies started using in the 2000s/2010s.
6217
6218Each desk was brief, and you could look most coworkers in the face without
6219moving from your chair, I hated this so much. Directly to my left was the Chief
6220Audit Executive (our department's leading boss), and his desk was pointed so
6221that his face would stare straight at my desk all day. I spent more time
6222thinking about who was looking at me or checking on me than actually working.
6223
6224The other annoying part of the open concept they used was that the kitchen area
6225and pathways were too close to everyone's desks (since the desks were spread
6226out, to provide space or something), so noise and conversation would be constant
6227throughout the day while you try to work. For someone like me, who needs silence
6228to get work done, that was a non-starter.
6229
6230** Hotel Office Concept
6231
6232I currently work for a company remotely (for now) and travel to the office every
6233once in a while for events and to help coach the staff underneath me. The office
6234I visit uses the hotel desk concept, where you need to check in at a touch
6235screen when you enter the office and "rent" a desk for the day. The same goes
6236for offices and meeting rooms.
6237
6238These desks are flat-top only and do not have any walls at all. In addition,
6239they're stacked with one row of 4 desks facing another row of 4 desks. These
6240pairs of desk rows are repeated through the office.
6241
6242This means that when I go, I need to rent a random desk or try to remember the
6243unique identification (ID) numbers on desks I like. Once I rent it, I have to
6244make sure no one sat down in that desk without renting it. Then, I can sit down
6245and work, but will probably need to adjust the monitors so that I'm not staring
6246in the face of the person across from me all day. Finally, I need to wear
6247headphones as this environment does nothing to provide you with peace or quiet.
6248
6249Luckily, you can rent offices with doors that offer quiet and privacy, which can
6250be very nice if you have a lot of meetings or webinars on a certain day.
6251
6252* Home Office
6253
6254Okay, now let's finally get to the home office concept. I have worked from home
6255for a little over two years at this point, across three different
6256jobs/employers. Over this time, I have experimented with a plethora of different
6257organizational ideas, desks, and room layouts to find what works best for me.
6258
6259These things might not apply to you, and that's fine. Everyone has a different
6260situation, and I really don't think you'll know what works until you try.
6261
6262** Tip #1
6263
6264Let's start with my top rule for a home office:
6265
6266#+begin_quote
6267If you live with others, working in a shared space is not effective.
6268#+end_quote
6269
6270It just does not work. If you have another person sleeping in your bedroom, it
6271is difficult to manage your work schedule with their sleeping/work/school
6272schedule. If they wake up after you need to start work, you might wake them up
6273or have to suffer the agony of staring at bright screens in a dark room.
6274
6275In a similar vein, working from a location such as the living room likely won't
6276work either. Distractions will come far more frequently: televisions, cooking,
6277cleaning, deliveries, etc. If you're like me, you'll end up playing a game
6278instead of actually doing any work.
6279
6280** Tip #2
6281
6282Okay, the second thing I've discovered that works for me:
6283
6284#+begin_quote
6285Use the pomodoro method (or something similar) to balance work tasks
6286with personal tasks.
6287#+end_quote
6288
6289I use a very casual version of the pomodoro method where I will work for 1-2
6290hours (usually set in strict intervals like 1, 1.5, 2 hours) and then will allow
6291myself 30-60 minutes for personal tasks. This schedule works for me, since my
6292work schedule really only comes to 3-6 hours of work per day.
6293
6294In this case, I'll work through my list of tasks for an hour or two and then
6295give myself personal time to get drinks and food, wash dishes, put clothes in
6296the washer, get the mail, etc. If you're in a convenient location, this usually
6297gives time for things like getting groceries (as long as you're not a slow
6298shopper).
6299
6300** Tip #3
6301
6302While I listed this one as number three, I don't think I'd accomplish anything
6303without it:
6304
6305#+begin_quote
6306Document everything: even things you didn't before - such as task lists
6307and notes from casual calls or meetings.
6308#+end_quote
6309
6310I've noticed that staying in an office gave me more constant reminders of
6311outstanding tasks or facts I had learned in a conversation. Translating
6312everything to a digital world has made me lose a bit of that focus (perhaps
6313since I don't have visual reminders?).
6314
6315Keeping a running task list of all things I have to do - even potential tasks! -
6316has helped me keep up without missing anything small. Likewise, keeping notes
6317for ALL meetings and calls, no matter how casual/quick, has improved my
6318retention immensely. Beyond helping my mental recollection, it has saved me
6319numerous times when I need to do a keyword search for some topic that was
6320discussed 6+ months ago.
6321
6322** Tip #4
6323
6324Okay, last one for now.
6325
6326#+begin_quote
6327Keep your work area clean.
6328#+end_quote
6329
6330This one is straightforward, but I know some people struggle with cleanliness or
6331may not believe it makes a difference. Trust me, keeping your desk area clean
6332and organized makes a huge difference, both mentally and emotionally.
6333
6334Just think about it, you walk into your home office and see a clean desk with a
6335laptop, dock, monitors, keyboard, mouse, and a notepad with a pen on top.
6336
6337Now imagine the opposite, there's an office with the same equipment, but there
6338are clothes hanging on the chair, empty drink bottles, candy wrappers and dirty
6339plates. This can take both a mental and emotional toll by bringing constant
6340disarray and stress into your working environment.
6341
6342Just keep things clean each day, and you won't need to do any big cleaning days
6343to recover.
6344
6345* My Preferences
6346
6347I've talked about the different environments I've worked in and expressed some
6348honest thoughts on pros or cons to each, but what do I prefer? Well, if you're
6349reading along, you should be able to tell that I much prefer a home office above
6350all else.
6351
6352Being able to control my own day and allot my time as needed has brought a
6353calmness to my life and has allowed me to maximize each day. I feel far more
6354effective and efficient in a home office than any other office, especially
6355open-office layouts.
6356
6357If I do need to return to an office part-time in the future, I really hope the
6358office will have privacy and quietness in order for me to get my work done.
6359#+date: <2022-02-10 Thu 00:00:00>
6360#+title: Automating Dynamic DNS Record Updates via Njalla API Using Python
6361#+description: Instructional material detailing automation of DNS record updates by interfacing with Njalla's API, suitable for environments with frequently changing IP addresses.
6362#+slug: njalla-dns-api
6363
6364* Njalla's API
6365
6366As noted in my recent post about [[https://cleberg.net/blog/ditching-cloudflare/][switching to Njalla from Cloudflare]], I was
6367searching for a way to replace my [[https://cleberg.net/blog/cloudflare-dns-api.html][bash script]] to update my domain's =A= record
6368with my home's internet protocol (IP) address dynamically.
6369
6370To reiterate what I said in those posts, this is a common necessity for those of
6371us who have non-static IP addresses that can change at any moment due to
6372internet service provider (ISP) policy.
6373
6374In order to keep a home server running smoothly, the server admin needs to have
6375a process to constantly monitor their public IP address and update their
6376domain's DNS records if it changes.
6377
6378This post explains how to use Python to update Njalla's DNS (domain name system)
6379records whenever a machine's public IP address changes.
6380
6381** Creating a Token
6382
6383To use Njalla's API, you will first need to create a token that will be used to
6384authenticate you every time you call the API (application programming
6385interface). Luckily, this is very easy to do if you have an account with Njalla.
6386
6387Simply go the [[https://njal.la/settings/api/][API Settings]] page and click the =Add Token= button. Next, enter a
6388name for the token and click =Add=.
6389
6390Finally, click the =Manage= button next to your newly created token and copy the
6391=API Token= field.
6392
6393** Finding the Correct API Request
6394
6395Once you have a token, you're ready to call the Njalla API for any number of
6396requests. For a full listing of available requests, see the [[https://njal.la/api/][Njalla API
6397Documentation]].
6398
6399For this demo, we are using the =list-records= and =edit-record= requests.
6400
6401The =list-records= request requires the following payload to be sent when
6402calling the API:
6403
6404#+begin_src txt
6405params: {
6406 domain: string
6407}
6408#+end_src
6409
6410The =edit-record= request requires the following payload to be sent when calling
6411the API:
6412
6413#+begin_src txt
6414params: {
6415 domain: string
6416 id: int
6417 content: string
6418}
6419#+end_src
6420
6421* Server Set-Up
6422
6423To create this script, we will be using Python. By default, I use Python 3 on my
6424servers, so please note that I did not test this in Python 2, and I do not know
6425if Python 2 will work for this.
6426
6427** Creating the Script
6428
6429First, find a suitable place to create your script. Personally, I just create a
6430directory called =ddns= in my home directory:
6431
6432#+begin_src sh
6433mkdir ~/ddns
6434#+end_src
6435
6436Next, create a Python script file:
6437
6438#+begin_src sh
6439nano ~/ddns/ddns.py
6440#+end_src
6441
6442The following code snippet is quite long, so I won't go into depth on each part.
6443However, I suggest you read through the entire script before running it; it is
6444quite simple and contains comments to help explain each code block.
6445
6446*Note*: You will need to update the following variables for this to work:
6447
6448- =token=: This is the Njalla API token you created earlier.
6449- =user_domain=: This is the top-level domain you want to modify.
6450- =include_subdomains=: Set this to =True= if you also want to modify subdomains
6451 found under the TLD (top-level domain).
6452- =subdomains=: If =include_subdomains= = =True=, you can include your list of
6453 subdomains to be modified here.
6454
6455#+begin_src python
6456#!/usr/bin/python
6457# -*- coding: utf-8 -*-
6458# Import Python modules
6459
6460from requests import get
6461import requests
6462import json
6463
6464# Set global variables
6465
6466url = 'https://njal.la/api/1/'
6467token = '<your-api-token>'
6468user_domain = 'example.com'
6469include_subdomains = True
6470subdomains = ['one', 'two']
6471
6472
6473# Main API call function
6474
6475def njalla(method, **params):
6476 headers = {'Authorization': 'Njalla ' + token}
6477 response = requests.post(url, json={'method': method,
6478 'params': params}, headers=headers).json()
6479 if 'result' not in response:
6480 raise Exception('API Error', response)
6481 return response['result']
6482
6483
6484# Gather all DNS records for a domain
6485
6486def get_records(domain):
6487 return njalla('list-records', domain=user_domain)
6488
6489
6490# Update a DNS record for a domain
6491
6492def update_record(domain, record_id, record_content):
6493 return njalla('edit-record', domain=domain, id=record_id,
6494 content=record_content)
6495
6496
6497# Get public IP addresses
6498
6499ipv4 = get('https://api.ipify.org').text
6500print('IPv4: {}'.format(ipv4))
6501ipv6 = get('https://api64.ipify.org').text
6502print('IPv6: {}'.format(ipv6))
6503
6504# Call API to get all DNS records
6505
6506data = get_records(user_domain)
6507
6508# Loop through records and check if each one is IPv4 (A) or IPv6 (AAAA)
6509# Update only if DNS is different from server IP
6510
6511for record in data['records']:
6512 if record['name'] == '@' or (include_subdomains and record['name'] \
6513 in subdomains):
6514 if record['type'] == 'A':
6515 if record['content'] == ipv4:
6516 print(record['type'], 'record for', record['name'],
6517 'already matches public IPv4 address. Skipping...'
6518 )
6519 else:
6520 print('IPv4 of', ipv4,
6521 'does not match Njalla's value of',
6522 record['content'], '. Updating...')
6523 update_record(user_domain, record['id'], ipv4)
6524 elif record['type'] == 'AAAA':
6525 if record['content'] == ipv6:
6526 print(record['type'], 'record for', record['name'],
6527 'already matches public IPv6 address. Skipping...'
6528 )
6529 else:
6530 print('IPv6 of', ipv6,
6531 'does not match Njalla's value of',
6532 record['content'], '. Updating...')
6533 update_record(user_domain, record['id'], ipv6)
6534#+end_src
6535
6536** Running the Script
6537
6538Once you've created the script and are ready to test it, run the following
6539command:
6540
6541#+begin_src sh
6542python3 ~/ddns/ddns.py
6543#+end_src
6544
6545** Setting the Script to Run Automatically
6546
6547To make sure the scripts run automatically, add it to the =cron= file so that it
6548will run on a schedule. To do this, open the =cron= file:
6549
6550#+begin_src sh
6551crontab -e
6552#+end_src
6553
6554In the cron file, paste the following at the bottom of the editor in order to
6555check the IP every five minutes:
6556
6557#+begin_src sh
6558*/5 * * * * python3 /home/<your_username>/ddns/ddns.py
6559#+end_src
6560#+date: <2022-02-16 Wed 00:00:00>
6561#+title: Switching to Debian and Nginx: A Modern Web and Gemini Server Migration Walkthrough
6562#+description: Step-by-step protocol for transitioning web server infrastructure to Debian operating system, including installation, configuration, and security hardening of Nginx and Agate services.
6563#+slug: debian-and-nginx
6564
6565* Server Operating System (OS): Debian
6566
6567I've used various Linux distributions throughout the years, but I've never used
6568anything except Ubuntu for my servers. Why? I really have no idea, mostly just
6569comfort around the commands and software availability.
6570
6571However, I have always wanted to try Debian as a server OS after testing it out
6572in a VM a few years ago (side-note: I'd love to try Alpine too, but I always
6573struggle with compatibility). So, I decided to launch a new VPS and use [[https://www.debian.org][Debian]]
657411 as the operating system (OS). Spoiler alert: it feels identical to Ubuntu for my purposes.
6575
6576I did the normal things when first launching the VPS (virtual private server),
6577such as adding a new user, locking down SSH (secure shell protocol), etc. If you
6578want to see that level of detail, read my other post about [[https://cleberg.net/blog/how-to-set-up-a-vps-web-server/][How to Set Up a VPS
6579Web Server]].
6580
6581All of this has been similar, apart from small things such as the location of
6582users' home folders. No complaints at all from me - Debian seems great.
6583
6584* Web Server: Nginx
6585
6586Once I had the baseline server configuration set-up for Debian, I moved on to
6587trying out [[https://nginx.org][Nginx]] as my web server software. This required me to install the
6588=nginx= and =ufw= packages, as well as setting up the initial UFW (Uncomplicated
6589Firewall) config:
6590
6591#+begin_src sh
6592sudo apt install nginx ufw
6593sudo ufw allow 'Nginx Full'
6594sudo ufw allow SSH
6595sudo ufw enable
6596sudo ufw status
6597sudo systemctl status nginx
6598#+end_src
6599
6600Once I had the firewall set, I moved on to creating the directories and files
6601for my website. This is very easy and is basically the same as setting up an
6602Apache server, so no struggles here.
6603
6604#+begin_src sh
6605sudo mkdir -p /var/www/your_domain/html
6606sudo chown -R $USER:$USER /var/www/your_domain/html
6607sudo chmod -R 755 /var/www/your_domain
6608nano /var/www/your_domain/html/index.html
6609#+end_src
6610
6611The next part, creating the Nginx configuration files, is quite a bit different
6612from Apache. First, you need to create the files in the =sites-available= folder
6613and symlink it the =sites-enabled= folder.
6614
6615Creating the configuration file for your domain:
6616
6617#+begin_src sh
6618sudo nano /etc/nginx/sites-available/your_domain
6619#+end_src
6620
6621Default content for an Nginx configuration file:
6622
6623#+begin_src sh
6624server {
6625 listen 80;
6626 listen [::]:80;
6627
6628 root /var/www/your_domain/html;
6629 index index.html index.htm index.nginx-debian.html;
6630
6631 server_name your_domain www.your_domain;
6632
6633 location / {
6634 try_files $uri $uri/ =404;
6635 }
6636}
6637#+end_src
6638
6639Finally, symlink it together:
6640
6641#+begin_src sh
6642sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
6643#+end_src
6644
6645This will make your site available to the public (as long as you have
6646=your_domain= DNS (Domain Name System) records pointed at the server's IP address)!
6647
6648Next, I used [[https://certbot.eff.org/][certbot]] to issue an HTTPS (Hypertext Transfer Protocol Secure)
6649certificate for my domains using the following commands:
6650
6651#+begin_src sh
6652sudo apt install snapd; sudo snap install core; sudo snap refresh core
6653sudo snap install --classic certbot
6654sudo ln -s /snap/bin/certbot /usr/bin/certbot
6655sudo certbot --nginx
6656#+end_src
6657
6658Now that =certbot= ran successfully and updated my Nginx configuration files to
6659include a =443= server block of code, I went back in and edited the
6660configuration file to include security HTTP headers. This part is optional, but
6661is recommended for security purposes; you can even test a website's HTTP header
6662security at [[https://securityheaders.com/][Security Headers]].
6663
6664The configuration below shows a set-up where you only want your website to serve
6665content from its own domain, except for images and scripts, which may come from
6666=nullitics.com=. All other content would be blocked from loading in a browser.
6667
6668#+begin_src sh
6669sudo nano /etc/nginx/sites-available/your_domain
6670#+end_src
6671
6672#+begin_src sh
6673server {
6674 ...
6675 add_header Content-Security-Policy "default-src 'none'; img-src 'self' https://nullitics.com; script-src 'self' https://nullitics.com; style-src 'self'; font-src 'self'";
6676 add_header X-Content-Type-Options "nosniff";
6677 add_header X-XSS-Protection "1; mode=block";
6678 add_header X-Frame-Options "DENY";
6679 add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
6680 add_header Referrer-Policy "no-referrer";
6681 ...
6682}
6683#+end_src
6684
6685#+begin_src sh
6686sudo systemctl restart nginx
6687#+end_src
6688
6689** Nginx vs. Apache
6690
6691As I stated at the beginning, my historical hesitation with trying Nginx was
6692that the differences in configuration formats scared me away from leaving
6693Apache. However, I prefer Nginx to Apache for a few reasons:
6694
66951. Nginx uses only one configuration file (=your_domain=) vs. Apache's two-file
6696 approach for HTTP vs. HTTPS (=your_domain.conf= and
6697 =your_domain-le-ssl.conf=).
66982. Symlinking new configurations files and reloading Nginx are way easier than
6699 Apache's process of having to enable headers with =a2enmod mod_headers=,
6700 enable PHP with =a2enmod php= (plus any other mods you need), and then
6701 enabling sites with =a2ensite=, and THEN reloading Apache.
67023. The contents of the Nginx configuration files seem more organized and logical
6703 with the curly-bracket approach. This is a minor reason, but everything just
6704 felt cleaner while I was installing my sites and that had a big quality of
6705 life impact on the installation for me.
6706
6707They're both great software packages, but Nginx just seems more organized and
6708easier to use these days. I will certainly be exploring the Nginx docs to see
6709what other fun things I can do with all of this.
6710
6711* Gemini Server: Agate
6712
6713Finally, I set up the Agate software on this server again to host my Gemini
6714server content, using Rust as I have before. You can read my other post for more
6715information on installing Agate: [[https://cleberg.net/blog/hosting-a-gemini-server/][Hosting a Gemini Server]].
6716
6717All in all, Debian + Nginx is very slick and I prefer it over my old combination
6718of Ubuntu + Apache (although it's really just Nginx > Apache for me, since
6719Debian seems mostly the same as Ubuntu is so far).
6720#+date: <2022-02-17 Thu 00:00:00>
6721#+title: Protect Your Privacy: Automating Image Metadata Removal Using Exiftool
6722#+description: Technical instructions for utilizing Exiftool to systematically strip metadata from image files to ensure privacy and reduce file size.
6723#+slug: exiftool
6724
6725** Why Strip Metadata?
6726
6727Okay, so you want to strip metadata from your photos. Perhaps you take pictures
6728of very rare birds, and the location metadata is a gold mine for poachers, or
6729perhaps you're just privacy-oriented like me and prefer to strip metadata from
6730publicly-available images.
6731
6732There are various components of image metadata that you may want to delete
6733before releasing a photo to the public. Here's an incomplete list of things I
6734could easily see just by inspecting a photo on my laptop:
6735
6736- Location (Latitude & Longitude)
6737- Dimensions
6738- Device Make & Model
6739- Color Space
6740- Color Profile
6741- Focal Length
6742- Alpha Channel
6743- Red Eye
6744- Metering Mode
6745- F Number
6746
6747Regardless of your reasoning, I'm going to explain how I used the =exiftool=
6748package in Linux to automatically strip metadata from all images in a directory
6749(+ subdirectories).
6750
6751** Installing =exiftool=
6752
6753First things first: we need to install the tool. I'm running Debian 11 on my
6754server (Ubuntu will work the same), so the command is as simple as:
6755
6756#+begin_src sh
6757sudo apt install exiftool
6758#+end_src
6759
6760There are different tools that can accomplish the same thing across
6761distributions, but I really only care to test out this one package.
6762
6763** Recursively Strip Data
6764
6765I actually use this tool extensively to strip any photos uploaded to the website
6766that serves all the images for my blog (=img.cleberg.net=).
6767
6768The following command is incredibly useful and can be modified to include any
6769image extensions that =exiftool= supports:
6770
6771#+begin_src sh
6772exiftool -r -all= -ext jpg -ext png /path/to/directory/
6773#+end_src
6774
6775The output of the command will let you know how many directories were scanned,
6776how many images were updated, and how many images were unchanged.
6777#+date: <2022-02-20 Sun 00:00:00>
6778#+title: How to Configure Nginx for Efficient Static Content Caching
6779#+description: Detailed methods for configuring Nginx to cache static resources such as CSS, JavaScript, and images to improve website loading times and resource efficiency.
6780#+slug: nginx-caching
6781
6782* Update Your Nginx Config to Cache Static Files
6783
6784If you run a website on Nginx that serves static content (i.e., content that is
6785not dynamic and changing with interactions from the user), you would likely
6786benefit from caching that content on the client-side. If you're used to Apache
6787and looking for the Nginx equivalent, this post should help.
6788
6789Luckily, setting up the cache is as easy as identifying the file types you want
6790to cache and determining the expiration length. To include more file types,
6791simply use the bar separator (=|=) and type the new file extension you want to
6792include.
6793
6794#+begin_src config
6795server {
6796 ...
6797
6798 location ~** .(css|js|jpg|jpeg|gif|png|ico)$ {
6799 expires 30d;
6800 }
6801
6802 ...
6803}
6804#+end_src
6805
6806I have seen some people who prefer to set =expires= as =365d= or even =max=, but
6807that is only for stable, infrequently changing websites. As my site often
6808changes (i.e., I'm never content with my website), I need to know that my
6809readers are seeing the new content without waiting too long.
6810
6811So, I went ahead and set the expiration date at =30d=, which is short enough to
6812refresh for readers but long enough that clients/browsers won't be re-requesting
6813the static files too often, hopefully resulting in faster loading times, as
6814images should be the only thing slowing down my site.
6815
6816* Testing Results
6817
6818To test my changes to the Nginx configuration, I used the [[https://addons.mozilla.org/en-US/firefox/addon/http-header-live/][HTTP Header Live]]
6819extension on my Gecko browser and used the sidebar to inspect the headers of a
6820recent image from my blog.
6821
6822In the image below, you can see that the =Cache-Control= header is now present
6823and set to 2592000, which is 30 days represented in seconds (30 days _ 24
6824hours/day _ 60 minutes/hour ** 60 seconds/minute = 2,592,000 seconds).
6825
6826The =Expires= field is now showing 22 March 2022, which is 30 days from the day
6827of this post, 20 February 2022.
6828
6829* Caveats
6830
6831Remember that this caching system is *client-side*, which means that content is
6832only cached for as long as a client allows it. For example, my browser purges
6833all caches, data, etc. upon exit, so this caching policy will only work as long
6834as my browser remains open and running.
6835
6836If you need to test updates to your site, you'll need to clear the cache to see
6837updates for any file extension you configured. This can often be done with the
6838=Shift + F5= or =Ctrl + F5= key combinations in most browsers.
6839#+date: <2022-02-22 Tue 00:00:00>
6840#+title: Twosday: Exploring the Unique Palindromic Date of 2-22-22
6841#+description: Examination of the date Tuesday, February 22, 2022, highlighting the palindromic properties and expected observations on this specific calendar occurrence.
6842#+slug: tuesday
6843
6844* Tuesday, Twosday
6845
6846I'm taking a break from my usual technology-related posts and writing about
6847something a little more enjoyable today.
6848
6849Today is Tuesday, February 22nd, 2022. Today is 02-22-2022. Today is Twosday.
6850
6851Let's look at everything that fell in place today:
6852
68531. Written in the =m-dd-yy= or =dd-m-yy= formats, today is 2-22-22 or 22-2-22,
6854 which is a neat little palindrome in either format. (The last ubiquitous
6855 six-digit palindrome was 1-11-11.)
68562. Today is Tuesday, which is why everyone is using the nickname Twosday to call
6857 out these similarities.
68583. Falling on Tuesday means today is the 2nd day of the week (for most cultures.
6859 For the US, it's the 3rd day of the week since we start on Sunday).
68604. The only culture I could find with a connection to a =2= is that some Slavic
6861 languages derived their version of Tuesday from the Old Church Slavonic word
6862 =въторъ=, meaning "the second."
68635. Written in the classic monospaced, digital font (think of digital clocks from
6864 the 80s/90s), there is nice symmetry to the numbers ([[https://img.cleberg.net/blog/20220222-tuesday/digital_font.webp][view the image here]]!).
68656. This one isn't naturally-occurring, but it seems people around the world are
6866 celebrating the day. For example, a group is putting together [[https://www.eventbrite.com/e/2-22-22-a-collective-wedding-ceremony-at-the-state-capitol-tickets-211434605597][a wedding of
6867 222 couples at the California State Capitol in Sacramento]], concluding at
6868 exactly 2:22 PM. These couples will record their marriage dates as 2-22-22
6869 2:22 PM. Tickets were on sale for $222.22.
6870#+date: <2022-03-02 Wed 00:00:00>
6871#+title: Reliable Note-Taking with Markdown: Methods for Efficient and Portable Notes
6872#+description: Instructions on applying standardized note-taking procedures with Markdown format, including synchronization and cross-platform compatibility considerations.
6873#+slug: reliable-notes
6874
6875* Choosing Durable File Formats
6876
6877#+begin_quote
6878TL;DR (Too Long; Didn't Read): Write in a format that can be easily rendered and
6879read in plain-text mode (e.g., =.txt=, =.md=, etc.).
6880#+end_quote
6881
6882As I've written more and more over the years, I've found that my love of
6883note-taking is always growing. Everything I learn or need to remember can be
6884written down in a note and saved digitally, with no cost to myself. Unlike paper
6885copies that need physical storage space, digital files simply need space on your
6886local disk or cloud storage, which is fairly abundant these days.
6887
6888However, I had a historical struggle with migration of notes between different
6889apps that require different formats and applied proprietary styling. This meant
6890that I had to go through each note during migration and edit the file to look
6891presentable again.
6892
6893For the last year or two, I have written everything exclusively in [[https://en.wikipedia.org/wiki/Markdown][Markdown]]
6894format. Small notes, long-form writings, and even these blog posts are all
6895written in Markdown.
6896
6897Why Markdown? While I do appreciate the simplicity of plain-text files without
6898any formatting, I often need visual cues such as heading and code blocks to keep
6899my thoughts straight. Markdown provides a minimal set of styling indicators for
6900me to style my notes without adding any proprietary, embedded data into the
6901files. If I want a top-level heading, I simply add a hash (=#=) before the line.
6902An added bonus is that even if a system doesn't understand Markdown, it will
6903render it as plain-text and I can read it just as easily.
6904
6905For example, here's how TextEdit on macOS will open and display a Markdown file
6906in plain-text, since it does contain any features to preview Markdown as HTML:
6907
6908#+caption: Plain Text Markdown
6909#+attr_html: :alt A view of the README.md from the cleberg.net repository.
6910[[https://img.cleberg.net/blog/20220302-easy-reliable-note-taking/plain_markdown.webp]]
6911
6912** Saving & Syncing Files
6913
6914In order to read and edit my notes across platforms, I use my personal cloud
6915storage through Tresorit due to its native integration with macOS and iOS file
6916managers. In addition, Tresorit works well on Debian-based Linux distributions,
6917which I used before macOS (and will likely switch back to in a few years).
6918
6919You can use whatever sync software you want - syncing plain-text or markdown
6920files is incredibly easy and fast, since the files are generally tiny.
6921
6922Since the cloud storage syncs files automatically, there is no need for me to
6923sync anything manually or kick-off a sync job to update my files. This means
6924that I can edit on mobile, and it takes about 5-10 seconds to see the changes on
6925desktop.
6926
6927*** Version Control with Git
6928:PROPERTIES:
6929:CUSTOM_ID: version-control-with-git
6930:END:
6931A different approach I've contemplated is storing my notes and attachments is
6932using a hosted Git repository to track changes to the files. However, I don't
6933want to rely on an external service that could potentially see into my data,
6934even if the repository is private.
6935
6936I might just do =git init= locally and then commit my changes each time I write
6937or update a note, but that seems to be a lot of work just for tracking changes -
6938which I don't necessarily care to know.
6939
6940*** Backups!
6941:PROPERTIES:
6942:CUSTOM_ID: backups
6943:END:
6944One small addition to the storage/sync conversation is the idea of backups.
6945Personally, I manually create periodic backups of my entire cloud storage,
6946compress it into an archive, and store it on my home server.
6947
6948To improve my workflow, I am going to be exploring options to automatically
6949compress the mounted cloud directory and send it over to my server on a set
6950schedule.
6951
6952** Writing on Desktop
6953
6954#+begin_quote
6955*Update (06.14.22)*: Since writing this post, I have reverted to simply keeping
6956my =notes= folder open and opening notes individually in TextEdit for a more
6957minimal and relaxing writing experience on the desktop.
6958#+end_quote
6959
6960The bulk of my writing occurs in a desktop environment, with a full keyboard
6961layout and wide screen. I don't illustrate with a smart pen, I rarely use
6962embedded images, and I love being able to see all of my notes/directories in a
6963sidebar.
6964
6965With this simple set of requirements, I chose [[https://obsidian.md][Obsidian]] as my desktop text
6966editor. Obsidian has some in-depth tools like a graph view, command palette,
6967mentions, etc., but I've found that using it as a simple Markdown editor is
6968incredibly easy and straightforward.
6969
6970Here's an example of how my Markdown notes look when opened in plain-text mode:
6971
6972#+caption: Obsidian Markdown Source Mode
6973#+attr_html: :alt Markdown source mode, showing the symbols used by markdown to markup text.
6974[[https://img.cleberg.net/blog/20220302-easy-reliable-note-taking/obsidian_source_mode.webp]]
6975
6976Here's the "live preview" version, where the Markdown is rendered into its HTML
6977format:
6978
6979#+caption: Obsidian Markdown Live Preview
6980#+attr_html: :alt Markdown preview mode, hiding all symbols used in Markdown syntax.
6981[[https://img.cleberg.net/blog/20220302-easy-reliable-note-taking/obsidian_live_preview.webp]]
6982
6983*** Programming on Desktop
6984
6985While I was writing this, I realized I should specify that I don't use the same
6986editor for writing notes and for writing code. For programming purposes, I use
6987[[https://vscodium.com][VSCodium]] as my development IDE.
6988
6989** Writing on Mobile
6990
6991Personally, I write very little on mobile, except when needing to take important
6992notes on-the-go. Any long-form writing, journals, etc. are done at home, where I
6993always have my laptop available.
6994
6995I wanted a simple and foolproof editor for iOS, preferably open-source. After a
6996long journey of testing the few (& terrible) open-source iOS note-taking apps, I
6997finally found a phenomenal one: [[https://github.com/simonbs/runestone][Runestone]]. This app is fantastic for
6998note-taking, has plenty of optional features, and integrates natively with the
6999iOS file manager.
7000
7001This app opens the iOS file manager and allows you to click any file you want,
7002opens it up in an editor, and lets me save and close out of that note.
7003
7004Quite simple but effective.
7005#+date: <2022-03-03 Thu 00:00:00>
7006#+title: From Spreadsheets to Databases: Creating a Custom Financial Tracker with Python
7007#+description: Procedures for creating and maintaining a personal financial database utilizing SQLite, Python programming language, and Jupyter Notebook interface.
7008#+slug: financial-database
7009
7010* Personal Financial Tracking
7011
7012For the last 6-ish years, I've tracked my finances in a spreadsheet. This is
7013common practice in the business world, but any good dev will cringe at the
7014thought of storing long-term data in a spreadsheet. A spreadsheet is not for
7015long-term storage or as a source of data to pull data/reports.
7016
7017As I wanted to expand the functionality of my financial data (e.g., adding more
7018reports), I decided to migrate the data into a database. To run reports, I would
7019query the database and use a language like Python or Javascript to process the
7020data, perform calculations, and visualize the data.
7021
7022* SQLite
7023
7024When choosing the type of database I wanted to use for this project, I was split
7025between three options:
7026
70271. MySQL: The database I have the most experience with and have used for years.
70282. PostgreSQL: A database I'm new to, but want to learn.
70293. SQLite: A database that I've used for a couple projects and have moderate
7030 experience.
7031
7032I ended up choosing SQLite since it can be maintained within a single =.sqlite=
7033file, which allows me more flexibility for storage and backup. I keep this file
7034in my cloud storage and pull it up whenever needed.
7035
7036** Visual Editing
7037
7038Since I didn't want to try and import 1000--1500 records into my new database
7039via the command line, I opted to use [[https://sqlitebrowser.org/][DB Browser for SQLite (DB4S)]] as a GUI
7040(graphical user interface) tool. This application is excellent, and I don't see
7041myself going back to the CLI (command line interface) when working in this
7042database.
7043
7044DB4S allows you to copy a range of cells from a spreadsheet and paste it
7045straight into the SQL table. I used this process for all 36 accounts, 1290
7046account statements, and 126 pay statements. Overall, I'm guessing this took
7047anywhere between 4--8 hours. In comparison, it probably took me 2-3 days to
7048initially create the spreadsheet.
7049
7050** Schema
7051
7052The schema for this database is actually extremely simple and involves only
7053three tables (for now):
7054
70551. Accounts
70562. Statements
70573. Payroll
7058
7059*Accounts*
7060
7061The Accounts table contains summary information about an account, such as a car
7062loan or a credit card. By viewing this table, you can find high-level data, such
7063as interest rate, credit line, or owner.
7064
7065#+begin_src sql
7066CREATE TABLE "Accounts" (
7067 "AccountID" INTEGER NOT NULL UNIQUE,
7068 "AccountType" TEXT,
7069 "AccountName" TEXT,
7070 "InterestRate" NUMERIC,
7071 "CreditLine" NUMERIC,
7072 "State" TEXT,
7073 "Owner" TEXT,
7074 "Co-Owner" TEXT,
7075 PRIMARY KEY("AccountID" AUTOINCREMENT)
7076)
7077#+end_src
7078
7079*Statements*
7080
7081The Statements table uses the same unique identifier as the Accounts table,
7082meaning you can join the tables to find a monthly statement for any of the
7083accounts listed in the Accounts table. Each statement has an account identified
7084(ID), statement date, and total balance.
7085
7086#+begin_src sql
7087CREATE TABLE "Statements" (
7088 "StatementID" INTEGER NOT NULL UNIQUE,
7089 "AccountID" INTEGER,
7090 "StatementDate" INTEGER,
7091 "Balance" NUMERIC,
7092 PRIMARY KEY("StatementID" AUTOINCREMENT),
7093 FOREIGN KEY("AccountID") REFERENCES "Accounts"("AccountID")
7094)
7095#+end_src
7096
7097*Payroll*
7098
7099The Payroll table is a separate entity, unrelated to the Accounts or Statements
7100tables. This table contains all information you would find on a pay statement
7101from an employer. As you change employers or obtain new perks/benefits, just add
7102new columns to adapt to the new data.
7103
7104#+begin_src sql
7105CREATE TABLE "Payroll" (
7106 "PaycheckID" INTEGER NOT NULL UNIQUE,
7107 "PayDate" TEXT,
7108 "Payee" TEXT,
7109 "Employer" TEXT,
7110 "JobTitle" TEXT,
7111 "IncomeRegular" NUMERIC,
7112 "IncomePTO" NUMERIC,
7113 "IncomeHoliday" NUMERIC,
7114 "IncomeBonus" NUMERIC,
7115 "IncomePTOPayout" NUMERIC,
7116 "IncomeReimbursements" NUMERIC,
7117 "FringeHSA" NUMERIC,
7118 "FringeStudentLoan" NUMERIC,
7119 "Fringe401k" NUMERIC,
7120 "PreTaxMedical" NUMERIC,
7121 "PreTaxDental" NUMERIC,
7122 "PreTaxVision" NUMERIC,
7123 "PreTaxLifeInsurance" NUMERIC,
7124 "PreTax401k" NUMERIC,
7125 "PreTaxParking" NUMERIC,
7126 "PreTaxStudentLoan" NUMERIC,
7127 "PreTaxOther" NUMERIC,
7128 "TaxFederal" NUMERIC,
7129 "TaxSocial" NUMERIC,
7130 "TaxMedicare" NUMERIC,
7131 "TaxState" NUMERIC,
7132 PRIMARY KEY("PaycheckID" AUTOINCREMENT)
7133)
7134#+end_src
7135
7136** Python Reporting
7137
7138Once I created the database tables and imported all my data, the only step left
7139was to create a process to report and visualize on various aspects of the data.
7140
7141In order to explore and create the reports I'm interested in, I utilized a
7142two-part process involving Jupyter Notebooks and Python scripts.
7143
7144*** Step 1: Jupyter Notebooks
7145
7146When I need to explore data, try different things, and re-run my code
7147cell-by-cell, I use Jupyter Notebooks. For example, I explored the =Accounts=
7148table until I found the following useful information:
7149
7150#+begin_src python
7151import sqlite3
7152import pandas as pd
7153import matplotlib
7154
7155# Set up database filename and connect
7156db = "finances.sqlite"
7157connection = sqlite3.connect(db)
7158df = pd.read_sql_query("SELECT ** FROM Accounts", connection)
7159
7160# Set global matplotlib variables
7161%matplotlib inline
7162matplotlib.rcParams['text.color'] = 'white'
7163matplotlib.rcParams['axes.labelcolor'] = 'white'
7164matplotlib.rcParams['xtick.color'] = 'white'
7165matplotlib.rcParams['ytick.color'] = 'white'
7166matplotlib.rcParams['legend.labelcolor'] = 'black'
7167
7168# Display graph
7169df.groupby(['AccountType']).sum().plot.pie(title='Credit Line by Account Type', y='CreditLine', figsize=(5,5), autopct='%1.1f%%')
7170#+end_src
7171
7172*** Step 2: Python Scripts
7173
7174Once I explored enough through the notebooks and had a list of reports I wanted,
7175I moved on to create a Python project with the following structure:
7176
7177#+begin_src txt
7178finance/
7179├── notebooks/
7180│ │ ├── account_summary.ipynb
7181│ │ ├── account_details.ipynb
7182│ │ └── payroll.ipynb
7183├── public/
7184│ │ ├── image-01.png
7185│ │ └── image-0X.png
7186├── src/
7187│ └── finance.sqlite
7188├── venv/
7189├── _init.py
7190├── database.py
7191├── process.py
7192├── requirements.txt
7193└── README.md
7194#+end_src
7195
7196This structure allows me to:
7197
71981. Compile all required python packages into =requirements.txt= for easy
7199 installation if I move to a new machine.
72002. Activate a virtual environment in =venv/= so I don't need to maintain a
7201 system-wide Python environment just for this project.
72023. Keep my =notebooks/= folder to continuously explore the data as I see fit.
72034. Maintain a local copy of the database in =src/= for easy access.
72045. Export reports, images, HTML files, etc. to =public/=.
7205
7206Now, onto the differences between the code in a Jupyter Notebook and the actual
7207Python files. To create the report in the Notebook snippet above, I created the
7208following function inside =process.py=:
7209
7210#+begin_src python
7211# Create summary pie chart
7212def summary_data(accounts: pandas.DataFrame) -> None:
7213 accounts_01 = accounts[accounts["Owner"] == "Person01"]
7214 accounts_02 = accounts[accounts["Owner"] == "Person02"]
7215 for x in range(1, 4):
7216 if x == 1:
7217 df = accounts
7218 account_string = "All Accounts"
7219 elif x == 2:
7220 df = accounts_01
7221 account_string = "Person01's Accounts"
7222 elif x == 3:
7223 df = accounts_02
7224 account_string = "Person02's Accounts"
7225 print(f"Generating pie chart summary image for {account_string}...")
7226 summary_chart = (
7227 df.groupby(["AccountType"])
7228 .sum()
7229 .plot.pie(
7230 title=f"Credit Line by Type for {account_string}",
7231 y="CreditLine",
7232 autopct="%1.1f%%",
7233 )
7234 )
7235 summary_chart.figure.savefig(f"public/summary_chart_{x}.png", dpi=1200)
7236#+end_src
7237
7238The result? A high-quality pie chart that is read directly by the
7239=public/index.html= template I use.
7240
7241Other charts generated by this project include:
7242
7243- Charts of account balances over time.
7244- Line chart of effective tax rate (taxes divided by taxable income).
7245- Salary projections and error limits using past income and inflation rates.
7246- Multi-line chart of gross income, taxable income, and net income.
7247
7248The best thing about this project? I can improve it at any given time, shaping
7249it into whatever helps me the most for that time. I imagine that I will be
7250introducing an asset tracking table soon to track the depreciating value of
7251cars, houses, etc. Who knows what's next?
7252#+date: <2022-03-08 Tue 00:00:00>
7253#+title: Migrating Plex Media Server to New Hardware with Nvidia GPU Setup
7254#+description: Stepwise instructions for transferring Plex Media Server to new hardware and enabling Nvidia GPU transcoding to optimize media processing performance.
7255#+slug: plex-migration
7256
7257* Migration Phases
7258
7259I recently decided to migrate my server from an old OptiPlex desktop machine to
7260a custom-built tower with better hardware in every category. In order to do
7261this, I would need to properly migrate a full Plex installation.
7262
7263The second part of this migration is that the new server uses an Nvidia GPU
7264(graphics processing unit) and does not have any integrated graphics, which
7265requires extra work for installation, but provides much better hardware
7266transcoding options for Plex.
7267
7268Therefore, I have broken this migration down into three phases:
7269
72701. Configure the New Server
72712. Migrate Plex Data & Devices
72723. Configure GPU Transcoding
7273
7274* Phase 1: Configure the New Server
7275
7276** Choosing an Operating System (OS)
7277
7278In order to migrate Plex to my new server, I first needed to choose an
7279appropriate OS and install it on the machine. Given that I have encountered
7280numerous issues installing other Linux distributions properly with Nvidia
7281graphics, I chose [[https://ubuntu.com/download/server][Ubuntu Server]].
7282
7283The first step is to create a bootable USB (universal serial bus) with Ubuntu
7284Server. This is easy with [[https://www.balena.io/etcher/][Etcher]], an app that runs on many different platforms.
7285Just download the Ubuntu Server =.iso= image, launch Etcher, and install the
7286=.iso= on the USB.
7287
7288Once the USB is created, insert it into my server, reboot, and click =Esc= (or
7289any of the =F1-12= keys) until the BIOS (Basic Input/Output System) menu
7290appears. Finally, launch the USB boot drive.
7291
7292** Booting with Nvidia
7293
7294In order to install Ubuntu Server with an Nvidia Graphics card (and no
7295integrated graphics on this device for some reason), you'll have to configure
7296the boot menu to allow different graphics drivers to be loaded.
7297
7298When booting from the USB, the machine will launch the initial installation
7299menu. From this menu, type =e= to view the default command options that come
7300with the device - it's a good idea to take a photo of this screen, so you can
7301enter these commands on the next screen (along with adding support for Nvidia).
7302
7303Finally, type =Ctrl + C= to enter the command line. From this command line,
7304enter the commands found on the =e= screen. Remember to add =nomodeset= to the
7305=linux ...= line so that your Nvidia device will display the installation
7306screens properly!
7307
7308Here's an example of the commands I pulled from the =e= screen and entered on
7309the command line.
7310
7311#+begin_src sh
7312setparams 'Install Ubuntu Server'
7313setgfxpayload=keep
7314linux /casper/vmlinuz quiet nomodeset ---
7315initrd /casper/initrd
7316boot
7317#+end_src
7318
7319Once the machine is rebooted, enter the =e= screen again and add =nomodeset= to
7320the =linux ...= line again and press =Ctrl + X= to save the boot options.
7321
7322The machine is now fully installed and can properly display on an external
7323display using the Nvidia GPU.
7324
7325Always remember to update and upgrade on a new installation:
7326
7327#+begin_src sh
7328sudo apt update; sudo apt upgrade -y; sudo apt autoremove -y
7329#+end_src
7330
7331* Phase 2: Migrate Plex Data & Devices
7332
7333This phase uses the great Plex article on migrations ([[https://support.plex.tv/articles/201370363-move-an-install-to-another-system/][Move an Installation to
7334Another System]]) and adds a bit more information to help with commands and
7335context.
7336
7337** Terminology
7338
7339- *Source:* The original server that is being replaced.
7340- *Destination:* The new server.
7341- *Client:* Any application that can be used to modify settings for both
7342 source/destination.
7343
7344** Step 01: [Client] Update Settings
7345
7346Open up a Plex app and /disable/ the =Account= > =Library= > =Empty trash
7347automatically after every scan= preference for the source server.
7348
7349** Step 02: [Destination] Install Plex
7350
7351Open up the [[https://www.plex.tv/media-server-downloads/][Plex Media Server download page]] and copy the link for the
7352appropriate platform.
7353
7354Execute the following commands on the destination server to install Plex:
7355
7356#+begin_src sh
7357wget <url>
7358sudo dpkg -i <filename>
7359sudo systemctl stop plexmediaserver.service
7360#+end_src
7361
7362** Step 03: [Source] Stop Plex & Migrate Data
7363
7364First, stop the Plex service so that no data is created or modified during the
7365migration.
7366
7367#+begin_src sh
7368sudo systemctl stop plexmediaserver.service
7369#+end_src
7370
7371Next, copy the data to the new server. To find where the Plex data directory is
7372located, Plex has another excellent article available: [[https://support.plex.tv/articles/202915258-where-is-the-plex-media-server-data-directory-located/][Where is the Plex Media
7373Server data directory located?]].
7374
7375There are many ways to copy the data to the new server and will largely depend
7376on the size of the folder being copied. Personally, my data folder was ~23GB and
7377I opted to simply use the =scp= command to copy the files over SSH (Secure Shell
7378Protocol).
7379
7380This process was throttled by the old server's slow hard disk and ports and took
7381approximately 90 minutes to complete. In comparison, moving the data from the
7382new server's =home/user/= directory to the =/var/.../Plex Media Server=
7383directory took 2-3 minutes.
7384
7385#+begin_src sh
7386scp -r "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server" your_user@xxx.xxx.xxx.xxx:"'/path/to/destination/'"
7387#+end_src
7388
7389** Step 04: [Destination] Update File Permissions
7390
7391In case you move the data directory to a common area on the new server, it will
7392have to be moved to the proper location before Plex can function properly:
7393
7394#+begin_src sh
7395mv "Plex Media Server" /var/lib/plexmediaserver/Library/Application Support/
7396#+end_src
7397
7398To ensure permissions were retained properly, the server will need to show that
7399all files and folders in the data directory are owned by =plex:plex= (or
7400whichever user is running the Plex application).
7401
7402#+begin_src sh
7403sudo chown -R plex:plex "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server"
7404#+end_src
7405
7406Finally, start the service and check the status.
7407
7408#+begin_src sh
7409sudo systemctl start plexmediaserver.service
7410sudo systemctl status plexmediaserver.service
7411#+end_src
7412
7413** Step 05: [Client] Update Libraries & Metadata
7414
7415The first step - now that the new server is up and running - is to sign out of
7416the client and sign back in. Once this is done, update any library locations, if
7417necessary. This was unnecessary in my case since I simply moved my storage
7418drives from the source server to the destination server.
7419
7420Next, perform the following actions in the client:
7421
74221. On the left sidebar, click =More= > Three-Dot Menu > =Scan Library Files=
74232. /Enable/ the =Account= > =Library= > =Empty trash automatically after every
7424 scan= preference for the source server.
74253. On the left sidebar, click =More= > Three-Dot Menu > =Manage Server= > =Empty
7426 Trash=
74274. On the left sidebar, click =More= > Three-Dot Menu > =Manage Server= > =Clean
7428 Bundles=
74295. On the left sidebar, click =More= > Three-Dot Menu > =Manage Server= >
7430 =Optimize Database=
7431
7432Finally, double-check the Remote Access settings to make sure no changes have
7433caused issues with accessing the server from outside the network.
7434
7435In my case, I use a single port forwarding rule in my router and needed to
7436update the Local LAN (local area network) IP (internet protocol) Address to the
7437new server IP address.
7438
7439* Phase 3: Configure GPU Transcoding
7440
7441The final piece to the migration is enabling hardware transcoding so that Plex
7442can fully utilize the new Nvidia GPU available in the server. The first step is
7443to install Nvidia graphics drivers. This process may take a few minutes, but the
7444commands are pretty simple:
7445
7446#+begin_src sh
7447sudo add-apt-repository ppa:graphics-drivers/ppa
7448sudo apt update
7449sudo apt-get install ubuntu-drivers-common
7450sudo ubuntu-drivers autoinstall
7451#+end_src
7452
7453Finally, reboot so that the changes are loaded:
7454
7455#+begin_src sh
7456sudo reboot now
7457#+end_src
7458
7459To ensure that the Nvidia graphics drivers are working properly, run the
7460following command to view the available GPUs, statistics, and processes:
7461
7462#+begin_src sh
7463sudo nvidia-smi
7464#+end_src
7465
7466Finally, enable hardware transcoding settings in the Plex application to finish
7467the process.
7468#+date: <2022-03-23 Wed 00:00:00>
7469#+title: Dynamic DNS Record Updates via Cloudflare API
7470#+description: Command-line procedures to automate updating of DNS A and AAAA records by interfacing with the Cloudflare API, suitable for environments with variable IP addresses.
7471#+slug: cloudflare-dns-api
7472
7473* DDNS: Dynamic DNS
7474:PROPERTIES:
7475:CUSTOM_ID: ddns-dynamic-dns
7476:END:
7477If you're hosting a service from a location with DDNS (Dynamic Domain Name
7478System), where your internet protocol (IP)address may change at any time, you
7479must have a solution to update the DNS (Domain Name System) so that you can
7480access your service even when the IP of the server changes.
7481
7482The process below uses the [[https://api.cloudflare.com/][Cloudflare API]] (application programming interface) to
7483update DNS =A= and =AAAA= records with the server's current IP. If you use
7484another DNS provider, you will have to find a way to update your DNS (or find a
7485way to get a static IP).
7486
7487First, install =jq= since we will use it in the next script:
7488
7489#+begin_src sh
7490sudo apt install jq
7491#+end_src
7492
7493Next, create a location for your DDNS update scripts and open the first script:
7494
7495#+begin_src sh
7496mkdir ~/ddns
7497nano ~/ddns/update.sh
7498#+end_src
7499
7500The following =update.sh= script will take all of your domains and subdomains
7501and check Cloudflare to see if the current =A= and =AAAA= records match your
7502server's IP address. If not, it will update the records.
7503
7504#+begin_src sh
7505# file: update.sh
7506#!/bin/bash
7507
7508# Update TLDs
7509domains=(example.com example.net)
7510
7511for domain in "${domains[@]}"
7512do
7513 echo -e "\nUpdating $domain..."
7514 zone_name=$domain /home/<your-username>/ddns/ddns.sh
7515done
7516
7517# Update subdomains for example.com
7518domain=example.com
7519subdomains=(photos.example.com)
7520
7521for subdomain in "${subdomains[@]}"
7522do
7523 echo -e "\nUpdating $subdomain..."
7524 zone_name=$domain dns_record=$subdomain /home/<your-username>/ddns/ddns.sh
7525done
7526#+end_src
7527
7528Next, open up the =ddns.sh= script. Paste the following into the script and
7529update the =api_token= and =email= variables.
7530
7531#+begin_src sh
7532nano ~/ddns/ddns.sh
7533#+end_src
7534
7535*Note*: If you want your DNS records to be proxied through Cloudflare, find and
7536update the following snippet: ="proxied":false}"= to say =true= instead of
7537=false=.
7538
7539#+begin_src sh
7540# file: ddns.sh
7541#!/bin/bash
7542# based on https://gist.github.com/Tras2/cba88201b17d765ec065ccbedfb16d9a
7543# initial data; they need to be filled by the user
7544## API token
7545api_token=<YOUR_API_TOKEN>
7546## email address associated with the Cloudflare account
7547email=<YOUR_EMAIL>
7548
7549# get the basic data
7550ipv4=$(curl -s -X GET -4 https://ifconfig.co)
7551ipv6=$(curl -s -X GET -6 https://ifconfig.co)
7552user_id=$(curl -s -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
7553 -H "Authorization: Bearer $api_token" \
7554 -H "Content-Type:application/json" \
7555 | jq -r '{"result"}[] | .id'
7556 )
7557
7558echo "Your IPv4 is: $ipv4"
7559echo "Your IPv6 is: $ipv6"
7560
7561# check if the user API is valid and the email is correct
7562if [ $user_id ]
7563then
7564 zone_id=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$zone_name&status=active" \
7565 -H "Content-Type: application/json" \
7566 -H "X-Auth-Email: $email" \
7567 -H "Authorization: Bearer $api_token" \
7568 | jq -r '{"result"}[] | .[0] | .id'
7569 )
7570 # check if the zone ID is
7571 if [ $zone_id ]
7572 then
7573 # check if there is any IP version 4
7574 if [ $ipv4 ]
7575 then
7576 dns_record_a_id=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records?type=A&name=$dns_record" \
7577 -H "Content-Type: application/json" \
7578 -H "X-Auth-Email: $email" \
7579 -H "Authorization: Bearer $api_token"
7580 )
7581 # if the IPv6 exist
7582 dns_record_a_ip=$(echo $dns_record_a_id | jq -r '{"result"}[] | .[0] | .content')
7583 echo "The set IPv4 on Cloudflare (A Record) is: $dns_record_a_ip"
7584 if [ $dns_record_a_ip != $ipv4 ]
7585 then
7586 # change the A record
7587 curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$(echo $dns_record_a_id | jq -r '{"result"}[] | .[0] | .id')" \
7588 -H "Content-Type: application/json" \
7589 -H "X-Auth-Email: $email" \
7590 -H "Authorization: Bearer $api_token" \
7591 --data "{"type":"A","name":"$dns_record","content":"$ipv4","ttl":1,"proxied":false}" \
7592 | jq -r '.errors'
7593 else
7594 echo "The current IPv4 and DNS record IPv4 are the same."
7595 fi
7596 else
7597 echo "Could not get your IPv4. Check if you have it; e.g. on https://ifconfig.co"
7598 fi
7599
7600 # check if there is any IP version 6
7601 if [ $ipv6 ]
7602 then
7603 dns_record_aaaa_id=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records?type=AAAA&name=$dns_record" \
7604 -H "Content-Type: application/json" \
7605 -H "X-Auth-Email: $email" \
7606 -H "Authorization: Bearer $api_token"
7607 )
7608 # if the IPv6 exist
7609 dns_record_aaaa_ip=$(echo $dns_record_aaaa_id | jq -r '{"result"}[] | .[0] | .content')
7610 echo "The set IPv6 on Cloudflare (AAAA Record) is: $dns_record_aaaa_ip"
7611 if [ $dns_record_aaaa_ip != $ipv6 ]
7612 then
7613 # change the AAAA record
7614 curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$(echo $dns_record_aaaa_id | jq -r '{"result"}[] | .[0] | .id')" \
7615 -H "Content-Type: application/json" \
7616 -H "X-Auth-Email: $email" \
7617 -H "Authorization: Bearer $api_token" \
7618 --data "{"type":"AAAA","name":"$dns_record","content":"$ipv6","ttl":1,"proxied":false}" \
7619 | jq -r '.errors'
7620 else
7621 echo "The current IPv6 and DNS record IPv6 are the same."
7622 fi
7623 else
7624 echo "Could not get your IPv6. Check if you have it; e.g. on https://ifconfig.co"
7625 fi
7626 else
7627 echo "There is a problem with getting the Zone ID. Check if the Zone Name is correct."
7628 fi
7629else
7630 echo "There is a problem with either the email or the password"
7631fi
7632#+end_src
7633
7634Once the script is saved and closed, make the scripts executable:
7635
7636#+begin_src sh
7637chmod +x ~/ddns/ddns.sh
7638chmod +x ~/ddns/update.sh
7639#+end_src
7640
7641You can test the script by running it manually:
7642
7643#+begin_src sh
7644./update.sh
7645#+end_src
7646
7647To make sure the scripts run automatically, add it to the =cron= file so that it
7648will run on a schedule. To do this, open the cron file:
7649
7650#+begin_src sh
7651crontab -e
7652#+end_src
7653
7654In the =cron= file, paste the following at the bottom of the editor:
7655
7656#+begin_src sh
7657,*/5 ** ** ** ** bash /home/<your_username>/ddns/update.sh
7658#+end_src
7659#+date: <2022-03-23 Wed 00:00:00>
7660#+title: Self-Hosting Guide: Nextcloud
7661#+description: Comprehensive guide for implementing Nextcloud as a self-hosted solution on an Ubuntu server, including installation steps, configuration parameters, and basic security measures.
7662#+slug: nextcloud-on-ubuntu
7663
7664* What is Nextcloud?
7665:PROPERTIES:
7666:CUSTOM_ID: what-is-nextcloud
7667:END:
7668[[https://nextcloud.com/][Nextcloud]] is a self-hosted solution for storage, communications, editing,
7669calendar, contacts, and more.
7670
7671This tutorial assumes that you have an Ubuntu server and a domain name
7672configured to point toward the server.
7673
7674* Install Dependencies
7675:PROPERTIES:
7676:CUSTOM_ID: install-dependencies
7677:END:
7678To start, you will need to install the packages that Nextcloud requires:
7679
7680#+begin_src sh
7681sudo apt install apache2 mariadb-server libapache2-mod-php7.4
7682sudo apt install php7.4-gd php7.4-mysql php7.4-curl php7.4-mbstring php7.4-intl
7683sudo apt install php7.4-gmp php7.4-bcmath php-imagick php7.4-xml php7.4-zip
7684#+end_src
7685
7686* Set Up MySQL
7687:PROPERTIES:
7688:CUSTOM_ID: set-up-mysql
7689:END:
7690Next, you will need to log in to MySQL as the =root= user of the machine.
7691
7692#+begin_src sh
7693sudo mysql -uroot -p
7694#+end_src
7695
7696Once you've logged in, you must create a new user so that Nextcloud can manage
7697the database. You will also create a =nextcloud= database and assign privileges:
7698
7699#+begin_src sql
7700CREATE USER 'username'@'localhost' IDENTIFIED BY 'password';
7701CREATE DATABASE IF NOT EXISTS nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
7702GRANT ALL PRIVILEGES ON nextcloud.** TO 'username'@'localhost';
7703FLUSH PRIVILEGES;
7704quit;
7705#+end_src
7706
7707* Download & Install Nextcloud
7708:PROPERTIES:
7709:CUSTOM_ID: download-install-nextcloud
7710:END:
7711To download Nextcloud, go the [[https://nextcloud.com/install/#instructions-server][Nextcloud downloads page]], click on =Archive File=
7712and right-click the big blue button to copy the link.
7713
7714Then, go to your server and enter the following commands to download, unzip, and
7715move the files to your destination directory. This example uses =example.com= as
7716the destination, but you can put it wherever you want to server your files from.
7717
7718#+begin_src sh
7719wget https://download.nextcloud.com/server/releases/nextcloud-23.0.3.zip
7720sudo apt install unzip
7721unzip nextcloud-23.0.3.zip
7722sudo cp -r nextcloud /var/www/example.com
7723#+end_src
7724
7725* Configure the Apache Web Server
7726:PROPERTIES:
7727:CUSTOM_ID: configure-the-apache-web-server
7728:END:
7729Now that the database is set up and Nextcloud is installed, you need to set up
7730the Apache configuration files to tell the server how to handle requests for
7731=example.com/nextcloud=.
7732
7733First, open the following file in the editor:
7734
7735#+begin_src sh
7736sudo nano /etc/apache2/sites-available/nextcloud.conf
7737#+end_src
7738
7739Once the editor is open, paste the following information in. Then, save and
7740close the file.
7741
7742#+begin_src config
7743<VirtualHost *:80>
7744 DocumentRoot /var/www/example.com
7745 ServerName example.com
7746 ServerAlias www.example.com
7747 ErrorLog ${APACHE_LOG_DIR}/error.log
7748 CustomLog ${APACHE_LOG_DIR}/access.log combined
7749
7750 <Directory /var/www/example.com/nextcloud/>
7751 Require all granted
7752 AllowOverride All
7753 Options FollowSymLinks MultiViews
7754 Satisfy Any
7755
7756 <IfModule mod_dav.c>
7757 Dav off
7758 </IfModule>
7759 </Directory>
7760</VirtualHost>
7761#+end_src
7762
7763Once the file is saved, enable it with Apache:
7764
7765#+begin_src sh
7766sudo a2ensite nextcloud.conf
7767#+end_src
7768
7769Next, enable the Apache modules required by Nextcloud:
7770
7771#+begin_src sh
7772sudo a2enmod rewrite headers env dir mime
7773#+end_src
7774
7775Finally, restart Apache. If any errors arise, you must solve those before
7776continuing.
7777
7778#+begin_src sh
7779sudo systemctl restart apache2
7780#+end_src
7781
7782For the app to work, you must have the correct file permissions on your
7783=nextcloud= directory. Set the owner to be =www-data=:
7784
7785#+begin_src sh
7786sudo chown -R www-data:www-data /var/www/example.com/nextcloud/
7787#+end_src
7788
7789* DNS
7790:PROPERTIES:
7791:CUSTOM_ID: dns
7792:END:
7793If you do not have a static internet protocol (IP) address, you will need to
7794update your DNS (Domain Name System) settings (at your DNS provider) whenever
7795your dynamic IP address changes.
7796
7797For an example on how I do that with Cloudflare, see my other post: [[https://cleberg.net/blog/cloudflare-dns-api.html][Dynamic DNS
7798Record Updates via Cloudflare API]].
7799
7800* Certbot
7801:PROPERTIES:
7802:CUSTOM_ID: certbot
7803:END:
7804If you want to serve Nextcloud from HTTPS (Hypertext Transfer Protocol Secure)
7805rather than plain HTTP (Hypertext Transfer Protocol), use the following commands
7806to issue Let's Encrypt SSL (Secure Socket Layer) certificates:
7807
7808#+begin_src sh
7809sudo apt install snapd
7810sudo snap install core
7811sudo snap refresh core
7812sudo snap install --classic certbot
7813sudo ln -s /snap/bin/certbot /usr/bin/certbot
7814sudo certbot --apache
7815#+end_src
7816
7817* Results
7818:PROPERTIES:
7819:CUSTOM_ID: results
7820:END:
7821Voilà! You're all done and should be able to access Nextcloud from your domain
7822or IP address.
7823#+date: <2022-03-24 Thu 00:00:00>
7824#+title: Step-by-Step Guide to Securing Your Home Server with Firewalls, SSH, and VLANs
7825#+description: Detailed instructions to enhance the security posture of home servers exposed to public networks, including firewall setup, secure SSH configuration, fail2ban deployment, and network segmentation.
7826#+slug: server-hardening
7827
7828* Planning Data Flows & Security
7829
7830** My Personal Data Flow
7831
7832#+begin_src
7833 ┌───────┐ ┌─────────────────┐
7834 ┌──► VLAN1 ├───► Private Devices │
7835 │ └───────┘ └─────────────────┘
7836┌──────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ │
7837│ Internet ├───► Router ├───► Firewall ├───► Switch ├──┤
7838└──────────┘ └────────┘ └──────────┘ └────────┘ │
7839 │ ┌───────┐ ┌───────────────┐
7840 └──► VLAN2 ├───► Public Server │
7841 └───────┘ └───────────────┘
7842#+end_src
7843
7844** Thought Process
7845
7846To serve content from your home server and harden your security posture, you
7847have to think about the transport of data from =server= to =client=.
7848
7849Let's start with the actual server itself. Think about the following:
7850
7851- Do I have a firewall enabled? Do I need to update this to allow new ports or
7852 internet protocol (IP) addresses?
7853- Do I have an intrusion prevention system (IPS) or intrusion detection system
7854 (IDS) that may prevent outside traffic?
7855- Do I have any other security software installed?
7856- Are the services hosted inside Docker containers, behind a reverse proxy, or
7857 virtualized? If so, are they configured to allow outside traffic?
7858
7859Once the data leaves the server, where does it go? In my case, it goes to a
7860managed switch. In this case, I asked the following:
7861
7862- What configurations is the switch using?
7863- Am I using VLANs (virtual local area networks)?
7864 - Yes, I am using 802.1Q VLANs.
7865- Are the VLANs configured properly?
7866 - Yes, as shown in the Switch section below, I have a separate VLAN to allow
7867 outside traffic to and from the server alone. No other devices, except for a
7868 service port, and in that VLAN.
7869
7870At this point, the data has been processed through the switch. Where does it go
7871next? In my case, it's pretty simple: it goes to the router/modem device.
7872
7873- Does my internet service provider (ISP) block any ports that I need?
7874 - This is an important step that a lot of people run into when self-hosting at
7875 home. Use an online port-checker tool for your IP or call your ISP if you
7876 think ports are blocked.
7877- Is there a router firewall?
7878 - Yes, I checked that it's configured to allow the ports I need to run my
7879 services publicly. Common web servers and reverse proxies require ports 80
7880 and 443, but other services like media servers or games can require unique
7881 ports, so be sure to check the documentation for your service(s).
7882- Are there any other settings affecting inbound/outbound traffic?
7883 - Schedules or access blocks
7884 - Static Routing
7885 - QoS (Quality of Service)
7886 - Port Forwarding
7887 - DMZ (demilitarized zone) hosting
7888 - Remote Management (this can sometimes mess with services that also require
7889 the use of ports 80 and 443)
7890
7891Once the data leaves my router, it goes to the upstream ISP and can be accessed
7892publicly.
7893
7894*** Server
7895
7896The services I run on my server are installed straight into the operating system
7897(OS), without any use of Docker or virtual machines (VMs), so I don't need any
7898extra application configuration to make them accessible to the outside world.
7899
7900#+BEGIN_QUOTE
7901As of 2022-10-04, the paragraph above is no longer true as I now run a reverse
7902proxy with Nginx and host many services inside Docker. However, it doesn't
7903change anything regarding this post as I still just need to open ports 80 & 443
7904and create the necessary website configuration files.
7905#+END_QUOTE
7906
7907When creating new services - either installed directly on bare metal or within
7908something like Docker - I ensure that I read through the documentation
7909thoroughly to understand a few key things:
7910
7911- What network activities should this app perform (if any)? Using which ports
7912 and protocols?
7913- Does this app require any commands/services to be run as =root=?
7914- Does this app log errors, authentication failures/successes, or anything else
7915 that would be useful for an investigation?
7916
7917For extra security, I use limit all incoming connections to SSH connections
7918through my server firewall [=ufw= (Uncomplicated Firewall)] and disable common
7919SSH (Secure Shell Protocol) settings. After all of that, I use =fail2ban= as a
7920preventative measure against brute-force login attempts.
7921
7922As another piece of security, you can randomize your SSH port to ensure that
7923random scanners or attackers can't easily try to force their way into your
7924network. For example, you can edit the port rules in your server to block all
7925connection requests to port =22= but forward all remote connections from port
7926=12345= to your server's port =22=. Then you just need to SSH to your network
7927via your randomized port.
7928
7929** =ufw=
7930
7931To see how to configure =ufw=, see my other post: [[https://cleberg.net/blog/ufw.html][Secure Your Network with the
7932Uncomplicated Firewall]].
7933
7934The general notion with an on-device firewall is that you want to deny all
7935incoming connections by default and then selectively open certain ports for
7936services or users that you know need access.
7937
7938If you know that you will only be logging into this server from a certain set or
7939list of IPs, you can always set the firewall to only allow connections to port
794022 from those IPs.
7941
7942For a quick start to only allow SSH connections to the server, use this:
7943
7944#+begin_src sh
7945sudo ufw default deny incoming
7946sudo ufw default allow outgoing
7947sudo ufw allow 22
7948sudo ufw enable
7949#+end_src
7950
7951** =ssh=
7952
79531. Using SSH Keys
7954
7955 First, make sure you have an SSH keypair generated on the device(s) that
7956 you'll be using to log in to the server. If you don't have an SSH key, run
7957 this command:
7958
7959 #+begin_src sh
7960 ssh-keygen
7961 #+end_src
7962
7963 Now that we have an SSH key, copy it to the server with the following
7964 command, which will ask for the user's password before accepting the key:
7965
7966 #+begin_src sh
7967 ssh-copy-id my_user@my_server
7968 #+end_src
7969
7970 If you have multiple keys, you'll need to specify which to use. After it's
7971 complete, =ssh= back into the server as that user and make sure it doesn't
7972 ask for a password.
7973
79742. Disable Password & Root Authentication
7975
7976 Now that we can access the server without a password, we will disable
7977 password authentication and disable anyone from using =ssh= to login as
7978 =root=.
7979
7980 To do this, open the =sshd_config= file:
7981
7982 #+begin_src sh
7983 sudo nano /etc/ssh/sshd_config
7984 #+end_src
7985
7986 You'll need to update the parameters to the values below. If one of these
7987 rules is commented-out or doesn't exist, create the rule at the bottom of the
7988 file.
7989
7990 #+begin_src config
7991 PermitRootLogin no
7992 PasswordAuthentication no
7993 PubkeyAuthentication yes
7994 #+end_src
7995
7996 Finally, restart the =ssh= service:
7997
7998 #+begin_src sh
7999 sudo systemctl restart sshd.service
8000 #+end_src
8001
8002 To test that everything's working so far, open ANOTHER terminal and try
8003 logging in as =root= over SSH. It is very important that you keep your
8004 current SSH session open and test with an additional session, or you will
8005 lock yourself out at some point and will need to use a recovery method (e.g.,
8006 hooking monitor up to home server) to get yourself back in.
8007
80083. Enable Multi-Factor Authentication (MFA) for =ssh=
8009
8010 This part is optional, but I highly recommend it. So far, we've ensured that
8011 no one can log into our user on the server without using our secret key, and
8012 we've ensured that no one can log in remotely as =root=. Next, you can enable
8013 MFA authentication for =ssh= connections.
8014
8015 This process involves editing a couple files and installing an MFA package,
8016 so I will not include all the details in this post. To see how to configure
8017 MFA for =ssh=, see my other post: [[https://cleberg.net/blog/ssh-mfa.html][Enabling MFA for SSH]].
8018
8019** =fail2ban=
8020
8021I haven't written a post on how I use =fail2ban=, but it's quite simple. I use
8022the default =sshd= jail, but you can always create new jails for respective
8023applications or ports. For example, if you use Nginx as your web server, you can
8024use the =nginx-http-auth= jail.
8025
8026In order to get it up and running, use the following commands:
8027
8028#+begin_src sh
8029sudo apt install fail2ban
8030sudo fail2ban-client start sshd
8031sudo fail2ban-client status sshd
8032#+end_src
8033
8034This should be used as a last-resort defense and shouldn't be a replacement for
8035the security measures mentioned above.
8036
8037* Switch
8038
8039Between the router and any local devices is my managed switch, which is used to
8040create VLANs. The example below shows how I would isolate the VLANs if I were
8041starting to host a single service at home.
8042
8043** 802.1Q VLAN Configuration
8044
8045In this configuration, port 8 is the public server that needs to be accessed
8046from the outside. Port 23 is my 'dedicated service port' for this server. In
8047order to SSH to this server, I need to plug my laptop into port 23 or else I
8048cannot SSH. Otherwise, I'd need to hook up a monitor and keyboard directly to
8049the server to manage it.
8050
8051| VLAN ID | VLAN Name | Member Ports | Tagged Ports | Untagged Ports |
8052|---------+-----------+--------------+--------------+----------------|
8053| 1 | Default | 1-24 | 1-24 | |
8054| 2 | Server | 1,8,23 | 1,8,23 | |
8055
8056** 802.1Q VLAN PVID Setting
8057
8058Once the VLAN is created, I simply add the =VLAN ID= of =2= as the =PVID= for
8059any related ports (in this case, see that ports =8= and =23= have a PVID of
8060=2=).
8061
8062| Port | PVID |
8063|------+------|
8064| 1 | 1 |
8065| 2 | 1 |
8066| 3 | 1 |
8067| 4 | 1 |
8068| 5 | 1 |
8069| 6 | 1 |
8070| 7 | 1 |
8071| 8 | 2 |
8072| 9 | 1 |
8073| 10 | 1 |
8074| 11 | 1 |
8075| 12 | 1 |
8076| 13 | 1 |
8077| 14 | 1 |
8078| 15 | 1 |
8079| 16 | 1 |
8080| 17 | 1 |
8081| 18 | 1 |
8082| 19 | 1 |
8083| 20 | 1 |
8084| 21 | 1 |
8085| 22 | 1 |
8086| 23 | 2 |
8087| 24 | 1 |
8088
8089* Router
8090
8091On my router, the configuration was as easy as opening the firewall settings and
8092unblocking the ports I needed for my services (e.g., HTTP/S, Plex, SSH, MySQL,
8093etc.).
8094
8095Since I'm relying on an ISP-provided modem/router combo for now (not by choice),
8096I do not use any other advanced settings on my router that would inhibit any
8097valid traffic to these services.
8098
8099The paragraph above regarding the ISP-owned router is no longer accurate as I
8100now use the Ubiquiti Unifi Dream Machine Pro as my router. Within this router, I
8101enabled port forwarding/firewall rules, segregate the network based on the
8102device, and enable traffic restrictions (e.g., silently drop traffic from
8103certain countries and threat categories).
8104
8105If you have the option with your ISP, I recommend using a personal router with
8106software that you are familiar with so that you can explore all the options
8107available to you.
8108
8109* Physical Security
8110
8111One large piece of self-hosting that people generally don't discuss online is
8112physical security. However, physical security is very important for everyone who
8113hosts a server like this. Exactly /how/ important it is depends on the server
8114use/purpose.
8115
8116If you self-host customer applications that hold protected data, then physical
8117security is extremely important and cannot be ignored. If you simply host a blog
8118and some hobby sites, then it's a relatively minor consideration, but one you
8119still need to think about.
8120
8121** Location
8122
8123The first consideration is quite simple: location.
8124
8125- Is the server within a property you own or housed on someone else's property?
8126- Is it nearby (in your house, in your work office, in your neighbor's garage,
8127 in a storage unit, etc.)?
8128- Do you have 24/7 access to the server?
8129- Are there climate considerations, such as humidity, fires, tornadoes,
8130 monsoons?
8131- Do you have emergency equipment nearby in case of emergency?
8132
8133** Hardware Ownership
8134
8135Secondly, consider the hardware itself:
8136
8137- Do you own the server in its entirety?
8138- Are any other users able to access the server, even if your data/space is
8139 segregated?
8140- If you're utilizing a third party, do they have any documentation to show
8141 responsibility? This could be a Service Organization Controls (SOC) 1/2/3
8142 report, International Organization for Standardization (ISO) compliance
8143 report, internal security/safety documentation.
8144
8145** Physical Controls
8146
8147Regardless of who owns the hardware, ensure that there are adequate safeguards
8148in place, if necessary. These usually don't apply to small home servers and are
8149usually covered already if you're utilizing a third party.
8150
8151These can include:
8152
8153- Server bezel locks
8154- Server room locks - physical, digital, or biometric authentication
8155- Security cameras
8156- Raised floors/lowered ceilings with proper guards/gates in-place within the
8157 floors or ceilings
8158- Security personnel
8159- Log sheets and/or guest badges
8160#+date: <2022-03-26 Sat 00:00:00>
8161#+title: Secure Your SSH Access: Deploying Time-Based One-Time Password Authentication
8162#+description: Step-by-step deployment guide for enabling TOTP multi-factor authentication on SSH services using Google Authenticator and Pluggable Authentication Module (PAM) integration.
8163#+slug: ssh-mfa
8164
8165* Why Do I Need Multi-Factor Authentication (MFA) for SSH (Secure Shell Protocol)?
8166
8167If you are a sysadmin of a server anywhere (that includes at home!), you may
8168want an added layer of protection against intruders. This is not a replacement
8169for other security measures, such as:
8170
8171- Disable root SSH
8172- Disable SSH password authentication
8173- Allow only certain users to login via SSH
8174- Allow SSH only from certain internet protocol (IP) addressess
8175
8176However, MFA can be added as an additional security measure to ensure that your
8177server is protected. This is especially important if you need to allow password
8178authentication for SSH.
8179
8180For more guidance on server security measures, see my other post: [[https://cleberg.net/blog/server-hardening.html][Step-by-Step
8181Guide to Securing Your Home Server with Firewalls, SSH, and VLANs]].
8182
8183* Install MFA PAM Module
8184
8185PAM (Pluggable Authentication Module) is an authentication infrastructure used
8186on Linux systems to authenticate a user. In order to use this technology, let's
8187install the =libpam-google-authenticator= package:
8188
8189#+begin_src sh
8190sudo apt-get update
8191#+end_src
8192
8193#+begin_src sh
8194sudo apt-get install libpam-google-authenticator
8195#+end_src
8196
8197* Initialize the PAM Module
8198
8199** Interactive Method
8200
8201Once the package is installed, initialize it and following the interactive
8202prompts to generate your OTP (One-Time Password) or TOTP (Time-based One-Time
8203Password):
8204
8205#+begin_src sh
8206google-authenticator
8207#+end_src
8208
8209If you are not sure how to answer, read the prompts carefully and think about
8210having to how each situation would affect your normal login attempts. If you are
8211still not sure, use my default responses below.
8212
8213#+begin_src txt
8214OUTPUT
8215
8216Do you want authentication tokens to be time-based (y/n) y
8217#+end_src
8218
8219At this point, use an authenticator app somewhere one of your devices to scan
8220the QR (quick-response) code. Any future login attempts after our upcoming
8221configuration changes will require that TOTP.
8222
8223#+begin_src txt
8224OUTPUT
8225
8226Do you want me to update your "/home/user/.google_authenticator" file? (y/n) y
8227#+end_src
8228
8229#+begin_src txt
8230OUTPUT
8231
8232Do you want to disallow multiple uses of the same authentication
8233token? This restricts you to one login about every 30s, but it increases
8234your chances to notice or even prevent man-in-the-middle attacks (y/n) y
8235#+end_src
8236
8237#+begin_src txt
8238OUTPUT
8239
8240By default, a new token is generated every 30 seconds by the mobile app.
8241In order to compensate for possible time-skew between the client and the server,
8242we allow an extra token before and after the current time. This allows for a
8243time skew of up to 30 seconds between authentication server and client. If you
8244experience problems with poor time synchronization, you can increase the window
8245from its default size of 3 permitted codes (one previous code, the current
8246code, the next code) to 17 permitted codes (the 8 previous codes, the current
8247code, and the 8 next codes). This will permit for a time skew of up to 4 minutes
8248between client and server.
8249Do you want to do so? (y/n) n
8250#+end_src
8251
8252#+begin_src txt
8253OUTPUT
8254
8255If the computer that you are logging into isn't hardened against brute-force
8256login attempts, you can enable rate-limiting for the authentication module.
8257By default, this limits attackers to no more than 3 login attempts every 30s.
8258Do you want to enable rate-limiting? (y/n) y
8259#+end_src
8260
8261** Non-Interactive Method
8262
8263If you need to do this quickly, know your responses to the prompts, or
8264are setting this up for numerous users, the non-interactive method can
8265be much faster:
8266
8267#+begin_src sh
8268google-authenticator -t -d -f -r 3 -R 30 -w 3
8269#+end_src
8270
8271The options referenced above are as follows:
8272
8273#+begin_src txt
8274google-authenticator [<options>]
8275 -h, --help Print this message
8276 -c, --counter-based Set up counter-based (HOTP) verification
8277 -t, --time-based Set up time-based (TOTP) verification
8278 -d, --disallow-reuse Disallow reuse of previously used TOTP tokens
8279 -D, --allow-reuse Allow reuse of previously used TOTP tokens
8280 -f, --force Write file without first confirming with user
8281 -l, --label=<label> Override the default label in "otpauth://" URL
8282 -i, --issuer=<issuer> Override the default issuer in "otpauth://" URL
8283 -q, --quiet Quiet mode
8284 -Q, --qr-mode={NONE,ANSI,UTF8} QRCode output mode
8285 -r, --rate-limit=N Limit logins to N per every M seconds
8286 -R, --rate-time=M Limit logins to N per every M seconds
8287 -u, --no-rate-limit Disable rate-limiting
8288 -s, --secret=<file> Specify a non-standard file location
8289 -S, --step-size=S Set interval between token refreshes
8290 -w, --window-size=W Set window of concurrently valid codes
8291 -W, --minimal-window Disable window of concurrently valid codes
8292 -e, --emergency-codes=N Number of emergency codes to generate
8293#+end_src
8294
8295This fully configures the authenticator, saves it to a file, and then outputs
8296the secret key, QR code, and recovery codes. (If you add the flag =-q=, then
8297there won't be any output). If you use this command in an automated fashion,
8298make sure your script captures the secret key and/or recovery codes and makes
8299them available to the user.
8300
8301* PAM Configuration Settings
8302
8303Once you've enabled MFA and have it saved to an MFA app on your phone or other
8304device, open the PAM =sshd= file:
8305
8306#+begin_src sh
8307sudo nano /etc/pam.d/sshd
8308#+end_src
8309
8310You need to do two things in this file. First, add the following lines to the
8311bottom of the file:
8312
8313#+begin_src config
8314auth required pam_google_authenticator.so nullok
8315auth required pam_permit.so
8316#+end_src
8317
8318Second, comment-out the following line near the top of the file.
8319
8320If you leave this line uncommented, every SSH login attempt will ask for the
8321following three authentication factors:
8322
83231. Publickey
83242. Password
83253. T/OTP code
8326
8327#+begin_src config
8328#@include common-auth
8329#+end_src
8330
8331* SSH Configuration Settings
8332
8333Finally, edit the =sshd_config= file again:
8334
8335#+begin_src sh
8336sudo nano /etc/ssh/sshd_config
8337#+end_src
8338
8339You'll need to change =ChallengeResponseAuthentication= to yes and add the
8340=AuthenticationMethods= line to the bottom of the file.
8341
8342#+begin_src config
8343ChallengeResponseAuthentication yes
8344AuthenticationMethods publickey,password publickey,keyboard-interactive
8345#+end_src
8346
8347Finally, restart the =ssh= service:
8348
8349#+begin_src sh
8350sudo systemctl restart sshd.service
8351#+end_src
8352
8353The next time you log in, you should be greeted with a verification code
8354request!
8355#+date: <2022-04-02 Sat 00:00:00>
8356#+title: How to Configure Nginx as a Reverse Proxy on Ubuntu Server
8357#+description: Technical guide for setting up Nginx server to operate as a reverse proxy on Ubuntu systems, including configuration files setup and operational parameters.
8358#+slug: nginx-reverse-proxy
8359
8360* What is a Reverse Proxy?
8361
8362A reverse proxy is a server that is placed between local servers or services and
8363clients/users (e.g., the internet). The reverse proxy intercepts all requests
8364from clients at the network edge and uses its configuration files to determine
8365where each request should be sent.
8366
8367** A Brief Example
8368
8369For example, let's say that I run three servers in my home:
8370
8371- Server01 (=example.com=)
8372- Server02 (=service01.example.com=)
8373- Server03 (=service02.example.com=)
8374
8375I also run a reverse proxy in my home that intercepts all public traffic:
8376
8377- Reverse Proxy
8378
8379Assume that I have a domain name (=example.com=) that allows clients to request
8380websites or services from my home servers.
8381
8382In this case, the reverse proxy will intercept all traffic from =example.com=
8383that enters my network and determine if the client is requesting valid data,
8384based on my configuration.
8385
8386If the user is requesting =example.com= and my configuration files say that
8387Server_{01} holds that data, Nginx will send the user to Server_{01}. If I were
8388to change the configuration so that =example.com= is routed to Server_{02}, that
8389same user would be sent to Server_{02} instead.
8390
8391#+begin_src txt
8392┌──────┐ ┌───────────┐
8393│ User │─┐ ┌──► Server_01 │
8394└──────┘ │ │ └───────────┘
8395 │ ┌──────────┐ ┌───────────────┐ │ ┌───────────┐
8396 ├────► Internet ├───► Reverse Proxy ├─────├──► Server_02 │
8397 │ └──────────┘ └───────────────┘ │ └───────────┘
8398┌──────┐ │ │ ┌───────────┐
8399│ User │─┘ └──► Server_03 │
8400└──────┘ └───────────┘
8401#+end_src
8402
8403* Reverse Proxy Options
8404
8405There are a lot of options when it comes to reverse proxy servers, so I'm just
8406going to list a few of the options I've heard recommended over the last few
8407years:
8408
8409- [[https://nginx.com][Nginx]]
8410- [[https://caddyserver.com][Caddy]]
8411- [[https://traefik.io/][Traefik]]
8412- [[https://www.haproxy.org/][HAProxy]]
8413- [[https://ubuntu.com/server/docs/proxy-servers-squid][Squid]]
8414
8415In this post, we will be using Nginx as our reverse proxy, running on Ubuntu
8416Server 20.04.4 LTS.
8417
8418* Nginx Reverse Proxy Example
8419
8420** Local Applications
8421
8422You may be like me and have a lot of applications running on your local network
8423that you'd like to expose publicly with a domain.
8424
8425In my case, I have services running in multiple Docker containers within a
8426single server and want a way to visit those services from anywhere with a URL.
8427For example, on my local network, [[https://dashy.to][Dashy]] runs through port 4000
8428(=localhost:4000=) and [[https://github.com/louislam/uptime-kuma][Uptime Kuma]] runs through port 3001 (=localhost:3001=).
8429
8430In order to expose these services to the public, I will need to do the
8431following:
8432
84331. Set up DNS (Domain Name System) records for a domain or subdomain (one per
8434 service) to point toward the internet protocol (IP) address of the server.
84352. Open up the server network's HTTP (Hypertext Transfer Protocol) and HTTPS
8436 (Hypertext Transfer Protocol Secure) ports (80 & 443) so that the reverse
8437 proxy can accept traffic and determine where to send it.
84383. Install the reverse proxy software.
84394. Configure the reverse proxy to recognize which service should get traffic
8440 from any of the domains or subdomains.
8441
8442** Step 1: DNS Configuration
8443
8444To start, update your DNS configuration so that you have an =A= record for each
8445domain or subdomain.
8446
8447The =A= records should point toward the public IP address of the server. If you
8448don't know the public IP address, log in to the server and run the following
8449command:
8450
8451#+begin_src sh
8452curl ifconfig.co
8453#+end_src
8454
8455In the DNS example below, =xxx.xxx.xxx.xxx= is the public IP address of
8456the server.
8457
8458#+begin_src conf
8459example.com A xxx.xxx.xxx.xxx
8460uptime.example.com A xxx.xxx.xxx.xxx
8461dashy.example.com A xxx.xxx.xxx.xxx
8462www CNAME example.com
8463#+end_src
8464
8465Finally, ensure the DNS has propagated correctly with [[https://dnschecker.org][DNS Checker]] by entering
8466your domains or subdomains in the search box and ensuring the results are
8467showing the correct IP address.
8468
8469** Step 2: Open Network Ports
8470
8471This step will be different depending on which router you have in your home. If
8472you're not sure, try to visit [[http://192.168.1.1][192.168.1.1]] in your browser. Login credentials are
8473usually written on a sticker somewhere on your modem/router.
8474
8475Once you're able to log in to your router, find the Port Forwarding settings.
8476You will need to forward ports =80= and =443= to whichever machine is running
8477the reverse proxy.
8478
8479In my case, the table below shows the port-forwarding rules I've created. In
8480this table, =xxx.xxx.xxx.xxx= is the local device IP of the reverse proxy
8481server, it will probably be an IP between =192.168.1.1= and =192.168.1.255=.
8482
8483| NAME | FROM PORT | DEST PORT/IP | ENABLED |
8484|-------+-----------+-----------------+---------|
8485| HTTP | 80 | xxx.xxx.xxx.xxx | TRUE |
8486| HTTPS | 443 | xxx.xxx.xxx.xxx | TRUE |
8487
8488Once configured, these rules will direct all web traffic to your reverse proxy.
8489
8490** Step 3: Nginx Installation
8491
8492To install Nginx, simply run the following command:
8493
8494#+begin_src sh
8495sudo apt install nginx
8496#+end_src
8497
8498If you have a firewall enabled, open up ports =80= and =443= on your server so
8499that Nginx can accept web traffic from the router.
8500
8501For example, if you want to use =ufw= for web traffic and SSH, run the following
8502commands:
8503
8504#+begin_src sh
8505sudo ufw allow 'Nginx Full'
8506sudo ufw allow SSH
8507sudo ufw enable
8508#+end_src
8509
8510** Step 4: Nginx Configuration
8511
8512Now that we have domains pointing toward the server, the only step left is to
8513configure the reverse proxy to direct traffic from domains to local services.
8514
8515To start, you'll need to create a configuration file for each domain in
8516=/etc/nginx/sites-available/=. They will look identical except for the
8517=server_name= variable and the =proxy_pass= port.
8518
8519Dashy:
8520
8521#+begin_src sh
8522nano /etc/nginx/sites-available/dashy.example.com
8523#+end_src
8524
8525#+begin_src config
8526server {
8527 listen 80;
8528 server_name dashy.example.com;
8529
8530 location / {
8531 proxy_pass http://localhost:4000;
8532 }
8533}
8534#+end_src
8535
8536Uptime:
8537
8538#+begin_src sh
8539nano /etc/nginx/sites-available/uptime.example.com
8540#+end_src
8541
8542#+begin_src config
8543server {
8544 listen 80;
8545 server_name uptime.example.com;
8546
8547 location / {
8548 proxy_pass http://localhost:3001;
8549 }
8550}
8551#+end_src
8552
8553Once the configuration files are created, you will need to enable them with the
8554=symlink= command:
8555
8556#+begin_src sh
8557sudo ln -s /etc/nginx/sites-available/dashy.example.com /etc/nginx/sites-enabled/
8558#+end_src
8559
8560Voilà! Your local services should now be available through their URLs (uniform
8561resource locators).
8562
8563* HTTPS with Certbot
8564
8565If you've followed along, you'll notice that your services are only available
8566via HTTP (not HTTPS).
8567
8568If you want to enable HTTPS for your new domains, you will need to generate TLS
8569(Transport Layer Security) certificates for them. The easiest way to generate
8570certificates on Nginx is [[https://certbot.eff.org][Certbot]]:
8571
8572#+begin_src sh
8573sudo apt install snapd; sudo snap install core; sudo snap refresh core
8574sudo snap install --classic certbot
8575sudo ln -s /snap/bin/certbot /usr/bin/certbot
8576sudo certbot --nginx
8577#+end_src
8578#+date: <2022-04-09 Sat 00:00:00>
8579#+title: PineTime Smartwatch: An Open-Source Hardware & Software Overview
8580#+description: Technical description of PineTime smartwatch capabilities, including heart rate measurement, step counting, sleep monitoring, and smartphone connectivity functions.
8581#+slug: pinetime
8582
8583* PineTime Product Information
8584
8585** Overview
8586
8587The [[https://www.pine64.org/pinetime/][PineTime]] is an open-source smartwatch, created by [[https://www.pine64.org][PINE64]]. Originally
8588announced in September 2019, this ARM-based watch is a fantastic option for
8589users who want the benefits of a modern smartwatch with the backing of
8590open-source components and software.
8591
8592** Product Specifications
8593
8594I won't dive into too many details that you can find on [[https://www.pine64.org/pinetime/][the product page]], but I
8595wanted to point out the prices for each watch and the primary functions:
8596
85971. Price:
8598
8599 - $26.99 (Sealed)
8600 - $24.99 (Dev Kit)
8601 - $51.98 (One Sealed + One Dev Kit)
8602
86032. Primary Functionality:
8604
8605 - Clock (+ Smartphone Sync)
8606 - Pedometer
8607 - Heart Rate Monitor
8608 - Sleep Monitor
8609 - Calories burned
8610 - Messaging
8611 - Smartphone Notifications
8612 - Media Controls
8613
8614* Unboxing
8615
8616Now, my PineTime was ordered on 2022-02-17, shipped on 2022-02-22, and was
8617delivered on 2022-03-23. With the current delays on shipping times around the
8618world (and the semiconductor shortage), a month for delivery from China seems
8619reasonable to me.
8620
8621The packaging is simple, and the watch comes with instructions, technical
8622information, the watch, and a charger (it does not include a USB wall adapter).
8623
8624The watch itself was able to turn on immediately when I pulled it out of the
8625box, but the battery was depleted and required charging right away.
8626
8627* Software
8628
8629** Watch Operating System (OS): InfiniTime
8630
8631While turning on the watch for the first time, some of the main design choices
8632you can see in the watch OS, [[https://wiki.pine64.org/wiki/InfiniTime][InfiniTime]], are:
8633
8634- A square bezel, not too thin against the sides of the watch.
8635- A simple, rubber band.
8636- Basic font and screen pixel design.
8637- Swipe gestures to access other screens.
8638
8639The OS itself is fantastic in terms of functionality for me. It does exactly
8640what a smartwatch should do - track time, steps, heart rates, and connect to
8641another smart device, without being overly burdensome to the user.
8642
8643My only gripe so far is that it's /really/ difficult to swipe to different
8644screens, such as pulling down the notification tray. I'm not sure if this is an
8645OS or hardware issue, but it makes it quite hard to quickly move around the
8646screens.
8647
8648However, my absolute favorite design choice is that the button the side turns
8649the screen on and off and tilting/waving my wrist doesn't accidentally turn on
8650the screen. With other watches, I absolutely hated not being able to turn off
8651the raise-to-wake or wave features (i.e., blinding myself while wearing a watch
8652at night because I moved my arm).
8653
8654** iOS App: InfiniLink
8655
8656Since I am using iOS as my primary mobile device OS, I am using the [[https://github.com/xan-m/InfiniLink][InfiniLink]]
8657app to connect my watch.
8658
8659This app provides the following for PineTime owners:
8660
8661- Firmware updates
8662- Steps
8663- Charts
8664- Notifications
8665
8666Another big feature of InfiniLink is the ability to track pedometer steps in a
8667collection of beautiful graphs, with the option to change your step goal and add
8668in manual steps.
8669
8670Finally, there are charts to display the battery percentage and heart rates over
8671time. This area also comes with an option to clear data.
8672
8673* Final Thoughts
8674
8675** Pros
8676
8677After wearing my watch for a few weeks, I have mostly positive thoughts about
8678the watch so far. In the past, I have owned smartwatches by FitBit, Fossil,
8679Apple, etc. - *but I prefer the PineTime over all of those watches*.
8680
8681The PineTime strips out all the unnecessary features and performs the functions
8682that it provides effectively and efficiently.
8683
8684The battery life is amazing on this device. By default, the watch seems to last
8685anywhere from a few days to a week before dying.
8686
8687And of course, it's open source and backed by some of the most dedicated
8688enthusiasts and developers I've seen. Watching the Matrix channel, forums, and
8689website have been exciting to see.
8690
8691** Cons
8692
8693If I had to complain about anything, it would simply be the small bugs in some
8694features that can be contributed to the companion apps more than the watch
8695itself.
8696
8697A regular user would want native notification support out-of-the-box, which is
8698the biggest item not working for me at the moment.
8699
8700My only other complaint is that the battery indicator on the watch doesn't seem
8701accurate when it's nearing depletion - it seems that there's a bit of battery
8702life left and then my watch is dead very suddenly after. This could just be me
8703misinterpreting the battery level icons, but it has fooled me a few times into
8704thinking I had more battery left than I actually did.
8705
8706Other than those small items, I really do love this watch and am glad I replaced
8707my Apple Watch with the PineTime.
8708#+date: <2022-06-01 Wed 00:00:00>
8709#+title: Migrating from Cloudflare to Njalla: A Privacy-Focused DNS and Domain Management Transition
8710#+description: Evaluation and procedural explanation for migrating DNS management services from Cloudflare to Njalla, with emphasis on privacy controls and domain registration mechanics.
8711#+slug: ditching-cloudflare
8712
8713* Registrar
8714
8715After spending a year or so using Cloudflare for DNS (Domain Name System) only -
8716no proxying or applications - I spent the last few months using Cloudflare
8717Tunnels and Cloudflare Access to protect my self-hosted websites and
8718applications via their proxy traffic model.
8719
8720However, I have never liked using Cloudflare due to their increasingly large
8721share of control over web traffic, as well as their business model of being a
8722MITM (man-in-the-middle) for all of your traffic.
8723
8724So, as of today, I have switched over to [[https://njal.la][Njalla]] as my registrar and DNS manager.
8725I was able to easily transfer my domains over rapidly, with only one domain
8726taking more than 15-30 minutes to propagate.
8727
8728+I do still have two domains sitting at Cloudflare for the moment while I decide
8729if they're worth the higher rates (one domain is 30€ and the other is 45€).+
8730
8731#+begin_quote
8732*Update (2022.06.03)*: I ended up transferring my final two domains over to
8733Njalla, clearing my Cloudflare account of personal data, and deleting the
8734Cloudflare account entirely. /I actually feel relieved to have moved on to a
8735provider I trust./
8736#+end_quote
8737
8738* DNS
8739
8740As noted above, I'm using Njalla exclusively for DNS configurations on my
8741domains.
8742
8743However, the transfer process was not ideal. As soon as the domains transferred
8744over, I switched the nameservers from Cloudflare to Njalla and lost most of the
8745associated DNS records. So, the majority of the time spent during the migration
8746was simply re-typing all the DNS records back in one-by-one.
8747
8748This would be much simpler if I were able to edit the plain-text format of the
8749DNS configuration. I was able to do that at a past registrar (perhaps it was
8750[[https://gandi.net/][Gandi.net]]?) and it made life a lot easier.
8751
8752** Dynamic DNS Updates
8753
8754I have built an easy Python script to run (or set-up in =cron= to run
8755automatically) that will check my server's IPv4 (Internet Protocol version 4)and
8756IPv6 (Internet Protocol version 6), compare it to Njalla, and update the DNS
8757records if they don't match. You can see the full script and process in my other
8758post: [[https://cleberg.net/blog/njalla-dns-api.html][Automating Dynamic DNS Record Updates via Njalla API]].
8759
8760I haven't used this other method, but I do know that you can create =Dynamic=
8761DNS records with Njalla that [[https://njal.la/docs/ddns/][work for updating dynamic subdomains]].
8762
8763** Njalla's DNS Tool
8764
8765One neat upside to Njalla is that they have a [[https://check.njal.la/dns/][DNS lookup tool]] that provides a
8766lot of great information for those of you (AKA: me) who hate using the =dig=
8767command.
8768
8769This was very useful for monitoring a couple of my transferred domains to see
8770when the changes in nameservers, records, and DNSSEC (Domain Name System
8771Security Extensions) went into effect.
8772
8773* Tunnel
8774
8775Cloudflare Tunnel is a service that acts as a reverse-proxy (hosted on
8776Cloudflare's servers) and allowed me to mask the private internet protocol (IP)
8777address of the server hosting my various websites and apps.
8778
8779However, as I was moving away from Cloudflare, I was not able to find a suitable
8780replacement that was both inexpensive and simple. So, I simply went back to
8781hosting [[https://cleberg.net/blog/set-up-nginx-reverse-proxy/][my own reverse proxy with Nginx]]. With the recent additions of Unifi
8782hardware in my server/network rack, I am much more protected against spam and
8783malicious attacks at the network edge than I was before I switched to
8784Cloudflare.
8785
8786* Access
8787
8788Cloudflare Access, another app I used in combination with Cloudflare Tunnel,
8789provided an authentication screen that required you to enter valid credentials
8790before Cloudflare would forward you to the actual website or app (if the
8791website/app has their own authentication, you'd then have to authenticate a
8792second time).
8793
8794I did not replace this service with anything since I only host a handful of
8795non-sensitive apps that don't require duplicate authentication.
8796#+date: <2022-06-07 Tue 00:00:00>
8797#+title: Self-Hosting Guide: FreshRSS
8798#+description: Stepwise instructions for installing FreshRSS using Docker and configuring Nginx as a reverse proxy to enable secure and synchronized RSS feed access.
8799#+slug: self-hosting-freshrss
8800
8801* Why Use Really Simple Syndication (RSS)?
8802
8803After noticing that I have collected 50+ blogs as bookmarks, I decided to
8804migrate back to using RSS feeds to stay up-to-date with my favorite websites.
8805Using RSS allows me to read all of these posts in a single app (on both mobile &
8806desktop) and allows me to be notified when new posts are available.
8807
8808However, I ran into one issue: syncing subscriptions and read/unread posts
8809across devices. Since I want to be able to easily read on both mobile and
8810desktop, I decided to look for a self-hosted RSS solution.
8811
8812Thus, I found [[https://www.freshrss.org/][FreshRSS]] and was able to successfully install it on my server in
8813about 30 minutes.
8814
8815* Documentation
8816
8817While it's certainly not robust, the [[https://freshrss.github.io/FreshRSS/][FreshRSS documentation]] is helpful for
8818figuring out basic information about the service.
8819
8820However, I wanted to install this service as a Docker container and stumbled
8821across the [[https://github.com/FreshRSS/FreshRSS/tree/edge/Docker][Docker README]] within the GitHub repository.
8822
8823This README was the documentation I actually needed. However, as you'll see
8824below, I still had to manually edit one file (=config.php=) to access the API
8825externally via my RSS apps.
8826
8827* Installation
8828
8829** DNS
8830
8831The first step, as required by any external web service, was assigning a domain
8832name to use. I chose to use a subdomain, like =rss.example.com=.
8833
8834To assign this, I created an =A= record in my DNS settings with the IPv4
8835(Internet Protocol version 4) address of the server and an =AAAA= record with
8836the IPv6 (Internet Protocol version 6) address of the server. Note: assigning an
8837IPv6 (=AAAA=) record is optional, but I like to enable IPV6 for my services.
8838
8839#+begin_src config
8840rss.example.com A xxx.xxx.xxx.xxx
8841rss.example.com AAAA xxxx:xxxx: ... :xxxx
8842#+end_src
8843
8844** Docker
8845
8846I initially tried to set up a =docker-compose.yml= file with a =.env= file
8847because I prefer to have a file I can look back at later to see how I initially
8848started the container, but it simply wouldn't work for me. I'm not sure why, but
8849I assume I wasn't telling =docker-compose= where the =.env= file was.
8850
8851Regardless, I chose to simply run the service with =docker run=. See the
8852following command for my =docker run= configuration:
8853
8854#+begin_src sh
8855sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
8856 -p 8080:80 \
8857 -e TZ=America/Chicago \
8858 -e 'CRON_MIN=1,31' \
8859 -v freshrss_data:/var/www/FreshRSS/data \
8860 -v freshrss_extensions:/var/www/FreshRSS/extensions \
8861 --name freshrss \
8862 freshrss/freshrss
8863#+end_src
8864
8865This started the container successfully and allowed me to visit the FreshRSS
8866instance at =localhost:8080=.
8867
8868** Fresh RSS Set-Up
8869
8870I *HIGHLY* suggest that you set up your user account prior to exposing this
8871service to the public. It's unlikely that someone is trying to access the exact
8872domain or IP/port you're assigning here, but as soon as you expose this service,
8873the first person to open the URL will be able to create the administrative user.
8874
8875In order to set up your FreshRSS service, open the =localhost:8080= URL in your
8876browser (you may need to use a local internet protocol (IP) instead of
8877=localhost= if you're accessing the page from a different machine on the
8878network - e.g., =192.168.1.20:8080=).
8879
8880Once the page loads, set up your default user with a strong username and
8881password. You may also choose to configure other settings prior to exposing this
8882service.
8883
8884** Nginx Reverse-Proxy
8885
8886In order to access this service outside my home, I needed to set up a
8887reverse-proxy to connect =localhost:8080= to =rss.example.com=.
8888
8889First, I created a new Nginx configuration file:
8890
8891#+begin_src sh
8892sudo nano /etc/nginx/sites-available/rss.example.com
8893#+end_src
8894
8895Within the configuration file, I pasted the following code:
8896
8897#+begin_src config
8898upstream freshrss {
8899 server 127.0.0.1:8080;
8900 keepalive 64;
8901}
8902
8903server {
8904 server_name rss.example.com;
8905 listen 80;
8906
8907 location / {
8908 # The final `/` is important.
8909 proxy_pass http://localhost:8080/;
8910 add_header X-Frame-Options SAMEORIGIN;
8911 add_header X-XSS-Protection "1; mode=block";
8912 proxy_redirect off;
8913 proxy_buffering off;
8914 proxy_set_header Host $host;
8915 proxy_set_header X-Real-IP $remote_addr;
8916 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
8917 proxy_set_header X-Forwarded-Proto $scheme;
8918 proxy_set_header X-Forwarded-Port $server_port;
8919 proxy_read_timeout 90;
8920
8921 # Forward the Authorization header for the Google Reader API.
8922 proxy_set_header Authorization $http_authorization;
8923 proxy_pass_header Authorization;
8924 }
8925}
8926#+end_src
8927
8928Finally, restart Nginx and you will be able to access your service via HTTP (Hypertext Transfer Protocol):
8929
8930#+begin_src sh
8931sudo systemctl restart nginx.service
8932#+end_src
8933
8934** HTTPS
8935
8936However, I don't want to access my RSS feeds via HTTP. I want it available only
8937via HTTPS (Hypertext Transfer Protocol Secure). In order to do this, I ran the
8938[[https://certbot.eff.org/][certbot]] program to generate SSL (Secure Socket Layer) certificates for me:
8939
8940#+begin_src sh
8941sudo certbot --nginx
8942#+end_src
8943
8944This process will automatically generate an SSL certificate for you and modify
8945the Nginx configuration file to include a redirect from HTTP to HTTPS.
8946
8947* Post-Installation Fixes
8948
8949At this point, we have a functional FreshRSS website, available from anywhere
8950and secured with HTTPS. However, attempting to connect this service to an RSS
8951app resulted in many errors regarding unavailable URLs and incorrect
8952credentials.
8953
8954** API Set-Up
8955
8956First, you need to open your user profile in FreshRSS (=Settings= > =Profile=)
8957and set an API password in the field at the bottom. This is the password you
8958will need to provide to your RSS apps.
8959
8960Once that is set and saved, click the link below the API password field to open
8961the API (application programming interface) check tool. It should look something
8962like =https://localhost:8080/api/= or =https://rss.example.com/api/=.
8963
8964Within this page, you /should/ see your correct external URL and "PASS" at the
8965bottom of each API type. This would mean everything is set up correctly, and you
8966can now move on and login to any RSS apps that support self-hosted options.
8967
8968In my case, the URL showed an internal URL and I had a warning that the
8969=base_url= variable may be misconfigured. If this is the case, see the next
8970section for a fix.
8971
8972** Base URL Fix
8973
8974In order to fix the =base_url= for the API, I opened up my docker container with
8975the following command:
8976
8977#+begin_src sh
8978sudo docker exec -it freshrss bash
8979#+end_src
8980
8981Within this container, update the packages and install an editor:
8982
8983#+begin_src sh
8984apt-get update
8985apt-get install nano
8986#+end_src
8987
8988Finally, open up =config.php= in the =data= directory:
8989
8990#+begin_src sh
8991nano data/config.php
8992#+end_src
8993
8994Within =config.php=, you will need to update the =base_url= variable and update
8995it to match your external URL (uniform resource locator). In my case, I simply
8996commented-out the incorrect URL with =//= and added the correct one on a new
8997line:
8998
8999#+begin_src php
9000<?php
9001 return array (
9002 ...
9003 // 'base_url' => 'http://localhost:8080',
9004 'base_url' => 'https://rss.example.com',
9005 ...
9006 )
9007>
9008#+end_src
9009
9010You can now exit the file with =Ctrl + x=, press =y= to save the file, and then
9011click =Enter= to keep the same file name.
9012
9013Finally, just exit out of the docker container:
9014
9015#+begin_src sh
9016exit
9017#+end_src
9018
9019Next, just restart the container:
9020
9021#+begin_src sh
9022sudo docker restart freshrss
9023#+end_src
9024
9025Voilà! Your API check should now "PASS" and you should be able to use one of the
9026API URLs in your RSS apps.
9027
9028In my case, I use [[https://netnewswire.com][NetNewsWire]] on my desktop and phone.
9029#+date: <2022-06-16 Thu 00:00:00>
9030#+title: Living the Terminal Lifestyle: Efficient Workflows for Focused Computing
9031#+description: Detailed procedures for reducing digital interruptions and increasing efficiency through terminal-based utilities for web browsing, communication, electronic mail, RSS feeds, and programming tasks.
9032#+slug: terminal-lifestyle
9033
9034* Text-Based Simplicity
9035
9036I've detailed my views on web-based minimalism and related topics in other posts
9037throughout the years; e.g., JavaScript/CSS (Cascading Style Sheets) bloat
9038slowing down websites that are essentially a text document. However, I have
9039never really expanded beyond talking about the web and describing how I focus on
9040minimizing distractions in other digital environments.
9041
9042This post is going to set the baseline for how I /try/ to live my digital life.
9043It does not necessarily get into my physical life, which is often harder to
9044control and contain all the noise in our modern world.
9045
9046While there are new things to do every day in our digital world, I find that
9047keeping a core set of values and interests can ground you and keep you mindful
9048of /why/ you are participating in the digital world. For example, if - at your
9049core - you have no interest in what strangers think about random topics, it
9050would be unwise to start participating in social media. However, I am someone
9051who has been dragged in by effective advertising to participate in communities
9052that I realize I do not care for.
9053
9054I won't dive much further into explaining the philosophy of all this, but I will
9055link a few helpful articles that may pique your interest if you're in search of
9056more meaningful experiences:
9057
9058- [[https://en.wikipedia.org/wiki/Mindfulness][Mindfulness]]
9059- [[https://en.wikipedia.org/wiki/Minimalism][Minimalism]]
9060- [[https://en.wikipedia.org/wiki/Stoicism][Stoicism]]
9061
9062* Living Life in the Terminal
9063
9064My personal approach to reducing digital distractions and increasing my focus on
9065the task at hand is to use a terminal for as much as I possibly can.
9066
9067Most days, this means that I have a few tabs open constantly in my terminal:
9068
90691. A web browser
90702. A chat client
90713. An email client
90724. An RSS (Really Simple Syndication) feed reader
90735. A local shell for navigating my computer's files
90746. A remote shell for managing servers and other machines
9075
9076Beyond this, I rarely open other tabs or graphical applications, unless
9077absolutely necessary. If you look, you may be surprised what can be accomplished
9078in the terminal.
9079
9080For example, I have moved my music and entertainment downloads to the terminal,
9081along with my device virtual private network (VPN) connections. I am exploring
9082options for moving my RSS subscriptions to something like [[https://newsboat.org/][Newsboat]], so that I
9083can read my daily articles without all the fuss.
9084
9085Now that we have some examples out of the way, let's dive into the specifics.
9086
9087** Browsing the Web
9088
9089I'm going to start off with a hard topic for those who prefer to live in the
9090terminal: web browsing. This task is made hard mostly by websites and web apps
9091that require JavaScript to run. The other difficult part is that if you're using
9092a text-based browser, that means images won't load (hopefully that's obvious).
9093
9094I am using [[https://lynx.invisible-island.net][Lynx]], a text-based browser that runs quickly and easily in the
9095terminal. Lynx allows me to browser most websites by simply typing =g= and then
9096typing in the URL I want.
9097
9098If you need a search engine while in Lynx, I recommend [[https://lite.duckduckgo.com/lite/][DuckDuckGo (Lite)]], which
9099allows you to search the web using their text-only interface.
9100
9101Eventually, you will run into websites that don't work (or are just too ugly and
9102messy) in a text-only mode, and you'll be forced to switch over to a GUI browser
9103to look at that site. Personally, I don't mind this as it doesn't happen as
9104often as I thought it would.
9105
9106The only time I need to do this is when I want to browse an image/video-focused
9107webpage or if I need to log in to a site, and it doesn't support a text-only
9108login page. For example, I am able to easily log in to [[https://sr.ht][Sourcehut]] in lynx.
9109
9110** Chatting with Friends
9111
9112After web browsing activities, my main form of terminal communication is Matrix.
9113I use the [[https://docs.mau.fi/gomuks/][gomuks]] client currently.
9114
9115This was incredibly easy to install on macOS (but I will need to see if it'll be
9116just as easy on Linux when my new laptop arrives):
9117
9118#+begin_src sh
9119brew install gomuks
9120#+end_src
9121
9122Once you launch gomuks, it will sync and require your username and password to
9123login. After doing so, the only problem I ran into was verifying my gomuks
9124client so that I could participate in rooms with E2EE (end-to-end encryption).
9125
9126Finally, I was able to verify the session by opening the Element desktop app (I
9127assume you can do this in the browser and mobile app too, but I'm not sure) and
9128manually verifying myself with this process:
9129
91301. Open the Element desktop app
91312. Open a room I was a member of
91323. Open the =Room Info= pane
91334. Open the =People= menu and search for myself
91345. Click on my profile name
91356. Click on the session link under the =Security= section and follow the prompts
9136 to manually verify the session
9137
9138Overall, I like gomuks and am able to enjoy all the features I was using in
9139Element. The only hiccup I have occurred is manually downloading images to view
9140them, which can be annoying.
9141
9142** Email
9143
9144Moving email to the terminal has been the hardest of the tasks for me. Unlike
9145web browsing, where I can simply decide to not look at a website that does not
9146work in the terminal, I cannot simply ignore emails sent to me.
9147
9148Personally, I am experimenting with [[https://neomutt.org/][neomutt]] as a potential email client.
9149
9150However, this requires a *TON* of configuration and tweaking to get right. Even
9151when I was able to set up neomutt, configure my email account, and customize a
9152few personal preferences, a lot of emails still do not display correctly (mostly
9153due to HTML (Hypertext Markup Language) and images).
9154
9155I won't get into the details of configuring =neomutt=; I mostly followed this
9156blog post: [[https://gideonwolfe.com/posts/workflow/neomutt/intro/][Email in the Terminal: Configuring Neomutt]].
9157
9158Finally, I have yet to figure out how to connect my PGP (Pretty Good Privacy)
9159keys to =neomutt=, but that's a problem for another day.
9160
9161** RSS Feed Reader
9162
9163I have just started using [[https://newsboat.org/][Newsboat]] to read articles in my terminal and have
9164found quick success with it.
9165
9166The configuration was super easy for this app; I simply installed the app,
9167created a file for URLs, and imported my OPML (Outline Processor Markup
9168Language) subscriptions that I had exported out of my old feed reader:
9169
9170#+begin_src sh
9171brew install newsboat
9172#+end_src
9173
9174#+begin_src sh
9175touch ~/.newsboat/urls
9176#+end_src
9177
9178#+begin_src sh
9179newsboat -i=my_subscriptions.opml
9180#+end_src
9181
9182** Writing & Programming
9183
9184Unfortunately, the weak link in my terminal-based environment right now is my
9185grasp of the possibilities of editing files within a shell.
9186
9187I am used to the easy extensions found in VSCodium and Kate, so I am slowly
9188learning how to mold the default editing tools to my needs. Currently, this
9189means I am using =nano= with the following configuration:
9190
9191#+begin_src config
9192set breaklonglines
9193set autoindent
9194set linenumbers
9195set tabstospaces
9196set tabsize 2
9197set fill 80
9198#+end_src
9199
9200This configuration allows nano to automatically hard-wrap lines at 80
9201characters, autoindent the wrapped lines (if the previous line was indented),
9202use 2 spaces per tab, and display line numbers within each file I open.
9203
9204I am currently looking to see if =vim= or =emacs= would be more useful for my
9205current needs, but I'm not in any rush, so I don't expect to find an answer
9206anytime soon.
9207
9208With my current life demands, I am not programming at the moment and have not
9209explored the best terminal set-up for programming. However, I have seen many
9210peers find success configuring =vim= and =emacs=, so that's where I will start
9211when I pick my projects back up.
9212#+date: <2022-06-22 Wed 00:00:00>
9213#+title: How to Receive a Daily Dose of Poetry via Automated Email
9214#+description: Instructional guide for scheduling and automating the distribution of classic and contemporary poetry in plaintext format to an electronic mail inbox using Python scripting and SMTP protocol.
9215#+slug: daily-poetry
9216
9217* Source Code
9218
9219I don't want to bury the lede here, so if you'd like to see the full source code
9220I use to email myself plaintext poems daily, visit the repository:
9221[[https://github.com/ccleberg/daily-poem.git][daily-poem.git]].
9222
9223* My Daily Dose of Poetry
9224
9225Most of my programming projects are small, random projects that are made
9226strictly to fix some small problem I have or enhance my quality of life.
9227
9228In this case, I was looking for a simply and easy way to get a daily dose of
9229literature or poetry to read in the mornings.
9230
9231However, I don't want to sign up for a random mailing list on just any
9232website. I also don't want to have to work to find the reading content
9233each morning, as I know I would simply give up and stop reading daily.
9234
9235Thus, I found a way to deliver poetry to myself in plain-text format, on
9236a daily basis, and scheduled to deliver automatically.
9237
9238* Prerequisites
9239
9240This solution uses Python and email, so the following process requires
9241the following to be installed:
9242
92431. A Simple Mail Transfer Protocol (SMTP) server, which can be as easy as
9244 installing =mailutils= if you're on a Debian-based distribution.
92452. Python (& pip!)
92463. The following Python packages: =email=, =smtplib=, =json=, and =requests=
9247
9248* Breaking Down the Logic
9249
9250I want to break down the logic for this program, as it's quite simple and
9251informational.
9252
9253** Required Packages
9254
9255This program starts with a simple import of the required packages, so I wanted
9256to explain why each package is used:
9257
9258#+begin_src python
9259from email.mime.text import MIMEText # Required for translating MIMEText
9260import smtplib # Required to process the SMTP mail delivery
9261import json # Required to parse the poetry API results
9262import requests # Required to send out a request to the API
9263#+end_src
9264
9265** Sending the API Request
9266
9267Next, we need to actually send the application programming interface (API)
9268request. In my case, I'm calling a random poem from the entire API. If you want,
9269you can call specific poems or authors from this API.
9270
9271#+begin_src python
9272json_data = requests.get('https://poetrydb.org/random').json()
9273#+end_src
9274
9275This gives us the following result in JSON (JavaScript Object Notation):
9276
9277#+begin_src json
9278[
9279 {
9280 "title": "Sonnet XXII: With Fools and Children",
9281 "author": "Michael Drayton",
9282 "lines": [
9283 "To Folly",
9284 "",
9285 "With fools and children, good discretion bears;",
9286 "Then, honest people, bear with Love and me,",
9287 "Nor older yet, nor wiser made by years,",
9288 "Amongst the rest of fools and children be;",
9289 "Love, still a baby, plays with gauds and toys,",
9290 "And, like a wanton, sports with every feather,",
9291 "And idiots still are running after boys,",
9292 "Then fools and children fitt'st to go together.",
9293 "He still as young as when he first was born,",
9294 "No wiser I than when as young as he;",
9295 "You that behold us, laugh us not to scorn;",
9296 "Give Nature thanks you are not such as we.",
9297 "Yet fools and children sometimes tell in play",
9298 "Some, wise in show, more fools indeed than they."
9299 ],
9300 "linecount": "15"
9301 }
9302]
9303#+end_src
9304
9305** Parsing the API Results
9306
9307In order to parse this into a readable format, we need to use the =json= package
9308and extract the fields we want. In the example below, I am grabbing every field
9309presented by the API.
9310
9311For the actual poem content, we need to loop over each line in the =lines=
9312variable since each line is a separate string by default.
9313
9314#+begin_quote
9315You /could/ also extract the title or author and make another call out
9316to the API to avoid having to build the plaintext poem with a loop, but
9317it just doesn't make sense to me to send multiple requests when we can
9318create a simple loop on our local machine to work with the data we
9319already have.
9320
9321For
9322[[https://poetrydb.org/title/Sonnet%20XXII:%20With%20Fools%20and%20Children/lines.text][example]],
9323look at the raw data response of this link to see the poem's lines
9324returned in plaintext.
9325#+end_quote
9326
9327#+begin_src python
9328title = json_data[0]['title']
9329author = json_data[0]['author']
9330line_count = json_data[0]['linecount']
9331lines = ''
9332for line in json_data[0]['lines']:
9333 lines = lines + line + "\n"
9334#+end_src
9335
9336** Composing the Email
9337
9338Now that I have all the data I need, I just need to compose it into a message
9339and prepare the message metadata.
9340
9341For my daily email, I want to see the title of the poem first, followed by the
9342author, then a blank line, and finally the full poem. This code snippet combines
9343that data and packages it into a MIMEText container, ready to be emailed.
9344
9345#+begin_src python
9346msg_body = title + "\n" + author + "\n\n" + lines
9347msg = MIMEText(msg_body)
9348#+end_src
9349
9350Before we send the email, we need to prepare the metadata (subject, from, to,
9351etc.):
9352
9353#+begin_src python
9354sender_email = 'example@server.local'
9355recipient_emails = ['user@example.com']
9356msg['Subject'] = 'Your Daily Poem (' + line_count + ' lines)'
9357msg['From'] = sender_email
9358msg['To'] = recipient_email
9359#+end_src
9360
9361** Sending the Email
9362
9363Now that I have everything ready to be emailed, the last step is to simply
9364connect to an SMTP server and send the email out to the recipients. In my case,
9365I installed =mailutils= on Ubuntu and let my SMTP server be =localhost=.
9366
9367#+begin_src python
9368smtp_server = 'localhost'
9369s = smtplib.SMTP(smtp_server)
9370s.sendmail(sender_email, recipient_emails, msg.as_string())
9371s.quit()
9372#+end_src
9373
9374* The Result!
9375
9376Instead of including a screenshot, I've copied the contents of the email that
9377was delivered to my inbox below since I set this process up in plaintext format.
9378
9379#+begin_src txt
9380Date: Wed, 22 Jun 2022 14:37:19 +0000 (UTC)
9381From: REDACTED
9382To: REDACTED
9383Subject: Your Daily Poem (36 lines)
9384MIME-Version: 1.0
9385Content-Transfer-Encoding: 8bit
9386Content-Type: text/plain; charset=utf-8
9387
9388Sonnet XXII: With Fools and Children
9389Michael Drayton
9390
9391With fools and children, good discretion bears;
9392Then, honest people, bear with Love and me,
9393Nor older yet, nor wiser made by years,
9394Amongst the rest of fools and children be;
9395Love, still a baby, plays with gauds and toys,
9396And, like a wanton, sports with every feather,
9397And idiots still are running after boys,
9398Then fools and children fitt'st to go together.
9399He still as young as when he first was born,
9400No wiser I than when as young as he;
9401You that behold us, laugh us not to scorn;
9402Give Nature thanks you are not such as we.
9403Yet fools and children sometimes tell in play
9404Some, wise in show, more fools indeed than they.
9405#+end_src
9406
9407* Scheduling the Daily Email
9408
9409Last, but not least, is scheduling this Python script with =crontab=. To
9410schedule a script to run daily, you can add it to the =crontab= file. To do
9411this, open =crontab= in editing mode:
9412
9413#+begin_src sh
9414crontab -e
9415#+end_src
9416
9417In the file, simply paste the following snippet at the bottom of the file and
9418ensure that the file path is correctly pointing to wherever you saved your
9419Python script:
9420
9421#+begin_src config
94220 8 ** ** ** python3 /home/<your_user>/dailypoem/main.py
9423#+end_src
9424
9425We have now set up the script and scheduled it to run daily at 08:00!
9426#+date: <2022-06-24 Fri 00:00:00>
9427#+title: My Journey Back to Linux: Setting Up Fedora with i3 Window Manager
9428#+description: Examination of the transition from macOS to Linux, including a detailed description of Fedora operating system installation and i3 window manager configuration.
9429#+slug: fedora-i3
9430
9431* Leaving macOS
9432
9433As I noted in a recent post, I have been planning on migrating from macOS back
9434to a Linux-based operating system (OS). I am happy to say that I have finally
9435completed my migration and am now stuck in the wonderful world of Linux again.
9436
9437My decision to leave macOS really came down to just a few important
9438things:
9439
9440- Apple Security (Gatekeeper) restricting me from running any software I want.
9441 Even if you disable Gatekeeper and allow software to bypass the rest of the
9442 device installation security, you still have to repeat that process every time
9443 the allowed software is updated.
9444- macOS sends out nearly constant connections, pings, telemetry, etc. to a
9445 myriad of mysterious Apple services. I'm not even going to dive into how many
9446 macOS apps have constant telemetry on, as well.
9447- Lastly, I just /really/ missed the customization and freedom that comes with
9448 Linux. Being able to switch to entirely new kernel, OS, or desktop within
9449 minutes is a freedom I took for granted when I switched to macOS.
9450
9451Now that I've covered macOS, I'm going to move on to more exciting topics: my
9452personal choice of OS, desktop environment (DE), and various customizations I'm
9453using.
9454
9455* Fedora
9456
9457After trying a ton of distributions (I think I booted and tested around 20-25
9458distributions), I finally landed on [[https://getfedora.org/][Fedora Linux]]. I have quite a bit of experience
9459with Fedora and enjoy the =dnf= package manager. Fedora allows me to keep
9460up-to-date with recent software (I'm looking at you, Debian), but still provides
9461a level of stability you don't find in every distribution.
9462
9463In a very close second place was Arch Linux, as well as its spin-off: Garuda
9464Linux (Garuda w/ sway is /beautiful/). Arch is great for compatibility and the
9465massive community it has, but I have just never had the time to properly sit
9466down and learn the methodology behind their packaging systems.
9467
9468Basically, everything else I tested was unacceptable in at least one way or
9469another. Void (=glibc=) was great, but doesn't support all the software I need.
9470Slackware worked well as a text-based user interface (TUI), but I wasn't skilled
9471enough to get a tiling window manager (WM) working on it.
9472
9473** i3
9474
9475One of the reasons I settled on Fedora is that it comes with an official i3
9476spin. Being able to use a tiling WM, such as i3 or sway, is one of the biggest
9477things I wanted to do as soon as I adopted Linux again.
9478
9479I will probably set up a dotfile repository soon, so that I don't lose any of my
9480configurations, but nothing big has been configured thus far.
9481
9482The two main things I have updated in i3wm are natural scrolling and binding my
9483brightness keys to the =brightnessctl= program.
9484
94851. Natural Scrolling
9486
9487 You can enable natural scrolling by opening the following file:
9488
9489 #+begin_src sh
9490 sudo nano /usr/share/X11/xorg.conf.d/40-libinput.conf
9491 #+end_src
9492
9493 Within the =40-libinput.conf= file, find the following input sections and
9494 enable the natural scrolling option.
9495
9496 This is the =pointer= section:
9497
9498 #+begin_src conf
9499 Section "InputClass"
9500 Identifier "libinput pointer catchall"
9501 MatchIsPointer "on"
9502 MatchDevicePath "/dev/input/event*"
9503 Driver "libinput"
9504 Option "NaturalScrolling" "True"
9505 EndSection
9506 #+end_src
9507
9508 This is the =touchpad= section:
9509
9510 #+begin_src conf
9511 Section "InputClass"
9512 Identifier "libinput touchpad catchall"
9513 MatchIsTouchpad "on"
9514 MatchDevicePath "/dev/input/event*"
9515 Driver "libinput"
9516 Option "NaturalScrolling" "True"
9517 EndSection
9518 #+end_src
9519
95202. Enabling Brightness Keys
9521
9522 Likewise, enabling brightness key functionality is as simple as binding the
9523 keys to the =brightnessctl= program.
9524
9525 To do this, open up your i3 configuration file. Mine is located here:
9526
9527 #+begin_src sh
9528 nano /home/<my-user>/.config/i3/config
9529 #+end_src
9530
9531 #+begin_src conf
9532 # Use brightnessctl to adjust brightness.
9533 bindsym XF86MonBrightnessDown exec --no-startup-id brightnessctl --min-val=2 -q set 3%-
9534 bindsym XF86MonBrightnessUp exec --no-startup-id brightnessctl -q set 3%+
9535 #+end_src
9536
95373. =polybar=
9538
9539 Instead of using the default =i3status= bar, I have opted to use =polybar=
9540 instead (as you can also see in the screenshot above).
9541
9542 My configuration for this menu bar is basically just the default settings
9543 with modified colors and an added battery block to quickly show me the
9544 machine's battery info.
9545
95464. =alacritty=
9547
9548 Not much to say on this part yet, as I haven't configured it much, but I
9549 installed =alacritty= as my default terminal, and I am using =zsh= and the
9550 shell.
9551
9552* Software Choices
9553
9554Again, I'm not going to say much that I haven't said yet in other blog posts, so
9555I'll just do a quick rundown of the apps I installed immediately after I set up
9556the environment.
9557
9558Flatpak Apps:
9559
9560- Cryptomator
9561- pCloud
9562- Signal
9563
9564Fedora Packages:
9565
9566- gomuks
9567- neomutt
9568- neofetch
9569- Firefox
9570 - uBlock Origin
9571 - Bitwarden
9572 - Stylus
9573 - Privacy Redirect
9574
9575Other:
9576
9577- exiftool
9578#+date: <2022-07-01 Fri 00:00:00>
9579#+title: Self-Hosting Guide: Git & cGit
9580#+description: Comprehensive instructions for secure and efficient setup, hosting, and management of personal Git repositories to facilitate collaborative development and remote access.
9581#+slug: git-server
9582
9583* My Approach to Self-Hosting Git
9584
9585I have often tried to self-host my Git repositories, but have always fallen
9586short when I tried to find a suitable web interface to show on the front-end.
9587
9588After a few years, I have finally found a combination of methods that allow me
9589to easily self-host my projects, view them on the web, and access them from
9590anywhere.
9591
9592Before I dive into the details, I want to state a high-level summary of my
9593self-hosted Git approach:
9594
9595- This method uses the =ssh://= (read & write) and =git://= (read-only)
9596 protocols for push and pull access.
9597 - For the =git://= protocol, I create a =git-daemon-export-ok= file in any
9598 repository that I want to be cloneable by anyone.
9599 - The web interface I am using (=cgit=) allows simple HTTP cloning by default.
9600 I do not disable this setting as I want beginners to be able to clone one of
9601 my repositories even if they don't know the proper method.
9602- I am not enabling Smart HTTPS (Hypertext Transfer Protocol Secure) for any
9603 repositories. Updates to repositories must be pushed via SSH (Secure Shell
9604 Protocol).
9605- Beyond the actual repository management, I am using =cgit= for the front-end
9606 web interface.
9607 - If you use the =scan-path=<path>= configuration in the =cgitrc=
9608 configuration file to automatically find repositories, you can't exclude a
9609 repository from =cgit= if it's stored within the path that =cgit= reads. To
9610 host private repositories, you'd need to set up another directory that
9611 =cgit= can't read.
9612
9613* Assumptions
9614
9615For the purposes of this walkthrough, I am assuming you have a URL
9616(=git.example.com=) or internet protocol (IP) address (=207.84.26.991=)
9617addressed to the server that you will be using to host your git repositories.
9618
9619* Adding a Git User
9620
9621In order to use the SSH method associated with Git, we will need to add a user
9622named =git=. If you have used the SSH method for other git hosting sites, you
9623are probably used to the following syntax:
9624
9625#+begin_src sh
9626git clone [user@]server:project.git
9627#+end_src
9628
9629The syntax above is an =scp=-like syntax for using SSH on the =git= user on the
9630server to access your repository.
9631
9632Let's delete any remnants of an old =git= user, if any, and create the new user
9633account:
9634
9635#+begin_src sh
9636sudo deluser --remove-home git
9637sudo adduser git
9638#+end_src
9639
9640** Import Your SSH Keys to the Git User
9641
9642Once the =git= user is created, you will need to copy your public SSH key on
9643your local development machine to the =git= user on the server.
9644
9645If you don't have an SSH key yet, create one with this command:
9646
9647#+begin_src sh
9648ssh-keygen
9649#+end_src
9650
9651Once you create the key pair, the public should be saved to =~/.ssh/id_rsa.pub=.
9652
9653If your server still has password-based authentication available, you can copy
9654it over to your user's home directory like this:
9655
9656#+begin_src sh
9657ssh-copy-id git@server
9658#+end_src
9659
9660Otherwise, copy it over to any user that you can access.
9661
9662#+begin_src sh
9663scp ~/.ssh/id_rsa.pub your_user@your_server:
9664#+end_src
9665
9666Once on the server, you will need to copy the contents into the =git= user's
9667=authorized_keys= file:
9668
9669#+begin_src sh
9670cat id_rsa.pub > /home/git/.ssh/authorized_keys
9671#+end_src
9672
9673** (Optional) Disable Password-Based SSH
9674
9675If you want to lock down your server and ensure that no one can authenticate in
9676via SSH with a password, you will need to edit your SSH configuration.
9677
9678#+begin_src sh
9679sudo nano /etc/ssh/sshd_config
9680#+end_src
9681
9682Within this file, find the following settings and set them to the values I am
9683showing below:
9684
9685#+begin_src conf
9686PermitRootLogin no
9687PasswordAuthentication no
9688AuthenticationMethods publickey
9689#+end_src
9690
9691You may have other Authentication Methods required in your personal set-up, so
9692the key here is just to ensure that =AuthenticationMethods= does not allow
9693passwords.
9694
9695*** Setting up the Base Directory
9696
9697Now that we have set up a =git= user to handle all transport methods, we need to
9698set up the directory that we will be using as our base of all repositories.
9699
9700In my case, I am using =/git= as my source folder. To create this folder and
9701assign it to the user we created, execute the following commands:
9702
9703#+begin_src sh
9704sudo mkdir /git
9705sudo chown -R git:git /git
9706#+end_src
9707
9708*** Creating a Test Repository
9709
9710On your server, switch over to the =git= user in order to start managing git
9711files.
9712
9713#+begin_src sh
9714su git
9715#+end_src
9716
9717Once logged-in as the =git= user, go to your base directory and create a test
9718repository.
9719
9720#+begin_src sh
9721cd /git
9722mkdir test.git && cd test.git
9723git init --bare
9724#+end_src
9725
9726If you want to make this repo viewable/cloneable to the public via the =git://=
9727protocol, you need to create a =git-daemon-export-ok= file inside the
9728repository.
9729
9730#+begin_src sh
9731touch git-daemon-export-ok
9732#+end_src
9733
9734* Change the Login Shell for =git=
9735
9736To make sure that the =git= user is only used for git operations and nothing
9737else, you need to change the user's login shell. To do this, simply use the
9738=chsh= command:
9739
9740#+begin_src sh
9741sudo chsh git
9742#+end_src
9743
9744The interactive prompt will ask which shell you want the =git= user to use. You
9745must use the following value:
9746
9747#+begin_src sh
9748/usr/bin/git-shell
9749#+end_src
9750
9751Once done, no one will be able to SSH to the =git= user or execute commands
9752other than the standard git commands.
9753
9754* Opening the Firewall
9755
9756Don't forget to open up ports on the device firewall and network firewall if you
9757want to access these repositories publicly. If you're using default ports,
9758forward ports =22= (ssh) and =9418= (git) from your router to your server's IP
9759address.
9760
9761If your server also has a firewall, ensure that the firewall allows the same
9762ports that are forwarded from the router. For example, if you use =ufw=:
9763
9764#+begin_src sh
9765sudo ufw allow 22
9766sudo ufw allow 9418
9767#+end_src
9768
9769** Non-Standard SSH Ports
9770
9771If you use a non-standard port for SSH, such as =9876=, you will need to create
9772an SSH configuration file on your local development machine in order to connect
9773to your server's git repositories.
9774
9775To do this, you'll need to define your custom port on your client machine in
9776your =~/.ssh/config= file:
9777
9778#+begin_src sh
9779nano ~/.ssh/config
9780#+end_src
9781
9782#+begin_src conf
9783Host git.example.com
9784 # HostName can be a URL or an IP address
9785 HostName git.example.com
9786 Port 9876
9787 User git
9788#+end_src
9789
9790** Testing SSH
9791
9792There are two main syntaxes you can use to manage git over SSH:
9793
9794- =git clone [user@]server:project.git=
9795- =git clone ssh://[user@]server/project.git=
9796
9797I prefer the first, which is an =scp=-like syntax. To test it, try to clone the
9798test repository you set up on the server:
9799
9800#+begin_src sh
9801git clone git@git.example.com:/git/test.git
9802#+end_src
9803
9804* Enabling Read-Only Access
9805
9806If you want people to be able to clone any repository where you've placed a
9807=git-daemon-export-ok= file, you will need to start the git daemon.
9808
9809To do this on a system with =systemd=, create a service file:
9810
9811#+begin_src sh
9812sudo nano /etc/systemd/system/git-daemon.service
9813#+end_src
9814
9815Inside the =git-daemon.service= file, paste the following:
9816
9817#+begin_src conf
9818[Unit]
9819Description=Start Git Daemon
9820
9821[Service]
9822ExecStart=/usr/bin/git daemon --reuseaddr --base-path=/git/ /git/
9823
9824Restart=always
9825RestartSec=500ms
9826
9827StandardOutput=syslog
9828StandardError=syslog
9829SyslogIdentifier=git-daemon
9830
9831User=git
9832Group=git
9833
9834[Install]
9835WantedBy=multi-user.target
9836#+end_src
9837
9838Once created, enable and start the service:
9839
9840#+begin_src sh
9841sudo systemctl enable git-daemon.service
9842sudo systemctl start git-daemon.service
9843#+end_src
9844
9845To clone read-only via the =git://= protocol, you can use the following syntax:
9846
9847#+begin_src sh
9848git clone git://git.example.com/test.git
9849#+end_src
9850
9851* Migrating Repositories
9852
9853At this point, we have a working git server that works with both SSH and
9854read-only access.
9855
9856For each of the repositories I had hosted a different provider, I executed the
9857following commands in order to place a copy on my server as my new source of
9858truth:
9859
9860Server:
9861
9862#+begin_src sh
9863su git
9864mkdir /git/<REPOSITORY_NAME>.git && cd /git/<REPOSITORY_NAME>.git
9865git init --bare
9866
9867# If you want to make this repo viewable/cloneable to the public
9868touch git-daemon-export-ok
9869#+end_src
9870
9871Client:
9872
9873#+begin_src sh
9874git clone git@<PREVIOUS_HOST>:<REPOSITORY_NAME>
9875git remote set-url origin git@git.EXAMPLE.COM:/git/<REPOSITORY_NAME>.git
9876git push
9877#+end_src
9878
9879* Optional Web View: =cgit=
9880
9881If you want a web viewer for your repositories, you can use various tools, such
9882as =gitweb=, =cgit=, or =klaus=. I chose =cgit= due to its simple interface and
9883fairly easy set-up (compared to others). Not to mention that the [[https://git.kernel.org/][Linux kernel
9884uses =cgit=]].
9885
9886** Docker Compose
9887
9888Instead of using my previous method of using a =docker run= command, I've
9889updated this section to use =docker-compose= instead for an easier installation
9890and simpler management and configuration.
9891
9892In order to use Docker Compose, you will set up a =docker-compose.yml= file to
9893automatically connect resources like the repositories, =cgitrc=, and various
9894files or folders to the =cgit= container you're creating:
9895
9896#+begin_src sh
9897mkdir ~/cgit && cd ~/cgit
9898nano docker-compose.yml
9899#+end_src
9900
9901#+begin_src conf
9902# docker-compose.yml
9903version: '3'
9904
9905services:
9906 cgit:
9907 image: invokr/cgit
9908 volumes:
9909 - /git:/git
9910 - ./cgitrc:/etc/cgitrc
9911 - ./logo.png:/var/www/htdocs/cgit/logo.png
9912 - ./favicon.png:/var/www/htdocs/cgit/favicon.png
9913 - ./filters:/var/www/htdocs/cgit/filters
9914 ports:
9915 - "8763:80"
9916 restart: always
9917#+end_src
9918
9919Then, just start the container:
9920
9921#+begin_src sh
9922sudo docker-compose up -d
9923#+end_src
9924
9925Once it's finished installing, you can access the site at =<SERVER_IP>:8763= or
9926use a reverse-proxy service to forward =cgit= to a URL, such as
9927=git.example.com=. See the next section for more details on reverse proxying a
9928URL to a local port.
9929
9930** Nginx Reverse Proxy
9931
9932I am using Nginx as my reverse proxy so that the =cgit= Docker container can use
9933=git.example.com= as its uniform resource locator (URL). To do so, I simply
9934created the following configuration file:
9935
9936#+begin_src sh
9937sudo nano /etc/nginx/sites-available/git.example.com
9938#+end_src
9939
9940#+begin_src conf
9941server {
9942 listen 80;
9943 server_name git.example.com;
9944
9945 if ($host = git.example.com) {
9946 return 301 https://$host$request_uri;
9947 }
9948
9949 return 404;
9950}
9951
9952server {
9953 server_name git.example.com;
9954 listen 443 ssl http2;
9955
9956 location / {
9957 # The final `/` is important.
9958 proxy_pass http://localhost:8763/;
9959 add_header X-Frame-Options SAMEORIGIN;
9960 add_header X-XSS-Protection "1; mode=block";
9961 proxy_redirect off;
9962 proxy_buffering off;
9963 proxy_set_header Host $host;
9964 proxy_set_header X-Real-IP $remote_addr;
9965 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
9966 proxy_set_header X-Forwarded-Proto $scheme;
9967 proxy_set_header X-Forwarded-Port $server_port;
9968 }
9969
9970 # INCLUDE ANY SSL CERTS HERE
9971 include /etc/letsencrypt/options-ssl-nginx.conf;
9972 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
9973}
9974#+end_src
9975
9976Once created, symlink it and restart the web server.
9977
9978#+begin_src sh
9979sudo ln -s /etc/nginx/sites-available/git.example.com /etc/nginx/sites-enabled/
9980sudo systemctl restart nginx.service
9981#+end_src
9982
9983As we can see below, my site at =git.example.com= is available and running:
9984
9985** Settings Up Git Details
9986
9987Once you have =cgit= running, you can add some small details, such as repository
9988owners and descriptions by editing the following files within each repository.
9989
9990Alternatively, you can use the =cgitrc= file to edit these details if you only
9991care to edit them for the purpose of seeing them on your website.
9992
9993The =description= file within the repository on your server will display the
9994description online.
9995
9996#+begin_src sh
9997cd /git/example.git
9998nano description
9999#+end_src
10000
10001You can add a =[gitweb]= block to the =config= file in order to display the
10002owner of the repository.
10003
10004#+begin_src sh
10005cd /git/example.git
10006nano config
10007#+end_src
10008
10009#+begin_src conf
10010[gitweb]
10011 owner = "YourName"
10012#+end_src
10013
10014Note that you can ignore the configuration within each repository and simply set
10015up this information in the =cgitrc= file, if you want to do it that way.
10016
10017** Editing =cgit=
10018
10019In order to edit certain items within =cgit=, you need to edit the =cgitrc=
10020file.
10021
10022#+begin_src sh
10023nano ~/cgit/cgitrc
10024#+end_src
10025
10026Below is an example configuration for =cgitrc=. You can find all the
10027configuration options within the [[https://git.zx2c4.com/cgit/plain/cgitrc.5.txt][configuration manual]].
10028
10029#+begin_src conf
10030css=/cgit.css
10031logo=/logo.png
10032favicon=/favicon.png
10033robots=noindex, nofollow
10034
10035enable-index-links=1
10036enable-commit-graph=1
10037enable-blame=1
10038enable-log-filecount=1
10039enable-log-linecount=1
10040enable-git-config=1
10041
10042clone-url=git://git.example.com/$CGIT_REPO_URL ssh://git@git.example.com:/git/$CGIT_REPO_URL
10043
10044root-title=My Git Website
10045root-desc=My personal git repositories.
10046
10047# Allow download of tar.gz, tar.bz2 and zip-files
10048snapshots=tar.gz tar.bz2 zip
10049
10050##
10051## List of common mimetypes
10052##
10053mimetype.gif=image/gif
10054mimetype.html=text/html
10055mimetype.jpg=image/jpeg
10056mimetype.jpeg=image/jpeg
10057mimetype.pdf=application/pdf
10058mimetype.png=image/png
10059mimetype.svg=image/svg+xml
10060
10061# Highlight source code
10062# source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.sh
10063source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.py
10064
10065# Format markdown, restructuredtext, manpages, text files, and html files
10066# through the right converters
10067about-filter=/var/www/htdocs/cgit/filters/about-formatting.sh
10068
10069##
10070## Search for these files in the root of the default branch of repositories
10071## for coming up with the about page:
10072##
10073readme=:README.md
10074readme=:readme.md
10075readme=:README.mkd
10076readme=:readme.mkd
10077readme=:README.rst
10078readme=:readme.rst
10079readme=:README.html
10080readme=:readme.html
10081readme=:README.htm
10082readme=:readme.htm
10083readme=:README.txt
10084readme=:readme.txt
10085readme=:README
10086readme=:readme
10087
10088# Repositories
10089
10090# Uncomment the following line to scan a path instead of adding repositories manually
10091# scan-path=/git
10092
10093## Test Section
10094section=git/test-section
10095
10096repo.url=test.git
10097repo.path=/git/test.git
10098repo.readme=:README.md
10099repo.owner=John Doe
10100repo.desc=An example repository!
10101#+end_src
10102
10103** Final Fixes: Syntax Highlighting & README Rendering
10104
10105After completing my initial install and playing around with it for a few days, I
10106noticed two issues:
10107
101081. Syntax highlighting did not work when viewing the source code within a file.
101092. The =about= tab within a repository was not rendered to HTML.
10110
10111The following process fixes these issues. To start, let's go to the =cgit=
10112directory where we were editing our configuration file earlier.
10113
10114#+begin_src sh
10115cd ~/cgit
10116#+end_src
10117
10118In here, create two folders that will hold our syntax files:
10119
10120#+begin_src sh
10121mkdir filters && mkdir filters/html-converters && cd filters
10122#+end_src
10123
10124Next, download the default filters:
10125
10126#+begin_src sh
10127curl https://git.zx2c4.com/cgit/plain/filters/about-formatting.sh > about-formatting.sh
10128chmod 755 about-formatting.sh
10129curl https://git.zx2c4.com/cgit/plain/filters/syntax-highlighting.py > syntax-highlighting.py
10130chmod 755 syntax-highlighting.py
10131#+end_src
10132
10133Finally, download the HTML conversion files you need. The example below
10134downloads the Markdown converter:
10135
10136#+begin_src sh
10137cd html-converters
10138curl https://git.zx2c4.com/cgit/plain/filters/html-converters/md2html > md2html
10139chmod 755 md2html
10140#+end_src
10141
10142If you need other filters or html-converters found within [[https://git.zx2c4.com/cgit/tree/filters][the cgit project
10143files]], repeat the =curl= and =chmod= process above for whichever files you need.
10144
10145However, formatting will not work quite yet since the Docker cgit container
10146we're using doesn't have the formatting package installed. You can install this
10147easily by install Python 3+ and the =pygments= package:
10148
10149#+begin_src sh
10150# Enter the container's command line
10151sudo docker exec -it cgit bash
10152#+end_src
10153
10154#+begin_src sh
10155# Install the necessary packages and then exit
10156yum update -y && \
10157yum upgrade -y && \
10158yum install python3 python3-pip -y && \
10159pip3 install markdown pygments && \
10160exit
10161#+end_src
10162
10163*You will need to enter the cgit docker container and re-run these =yum=
10164commands every time you kill and restart the container!*
10165
10166If not done already, we need to add the following variables to our =cgitrc= file
10167in order for =cgit= to know where our filtering files are:
10168
10169#+begin_src conf
10170# Highlight source code with python pygments-based highlighter
10171source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.py
10172
10173# Format markdown, restructuredtext, manpages, text files, and html files
10174# through the right converters
10175about-filter=/var/www/htdocs/cgit/filters/about-formatting.sh
10176#+end_src
10177
10178Now you should see that syntax highlighting and README rendering to the =about=
10179tab is fixed.
10180
10181** Theming
10182
10183I won't go into much detail in this section, but you can fully theme your
10184installation of =cgit= since you have access to the =cgit.css= file in your web
10185root. This is another file you can add as a volume to the =docker-compose.yml=
10186file if you want to edit this without entering the container's command line.
10187
10188*** Remember to Back Up Your Data!
10189
10190The last thing to note is that running services on your own equipment means that
10191you're assuming a level of risk that exists regarding data loss, catastrophes,
10192etc. In order to reduce the impact of any such occurrence, I suggest backing up
10193your data regularly.
10194
10195Backups can be automated via =cron=, by hooking your base directory up to a
10196cloud provider, or even setting up hooks to push all repository info to git
10197mirrors on other git hosts. Whatever the method, make sure that your data
10198doesn't vanish in the event that your drives or servers fail.
10199#+date: <2022-07-14 Thu 00:00:00>
10200#+title: Getting Started with GPG: Secure Encryption, Signing, and Key Usage
10201#+description: Technical manual covering the architecture, cryptographic algorithms, vulnerability considerations, key management, and typical applications of GNU Privacy Guard for secure data and email encryption.
10202#+slug: gnupg
10203
10204* The History of GPG
10205
10206[[https://gnupg.org/][GNU Privacy Guard]], also known as GnuPG and GPG, is a free ("free" as in both
10207speech and beer) software that fully implements the OpenPGP Message Format
10208documented in [[https://www.rfc-editor.org/rfc/rfc4880][RFC 4880]].
10209
10210I won't go in-depth on the full history of the software in this post, but it is
10211important to understand that GPG is not the same as PGP (Pretty Good Privacy),
10212which is a different implementation of RFC 4880. However, GPG was designed to
10213interoperate with PGP.
10214
10215GPG was originally developed in the late 1990s by [[https://en.wikipedia.org/wiki/Werner_Koch][Werner Koch]] and has
10216historically been funded generously by the German government.
10217
10218Now that we have all the high-level info out of the way, let's dive into the
10219different aspects of GPG and its uses.
10220
10221* Encryption Algorithms
10222
10223GPG supports a wide range of different encryption algorithms, including
10224public-key, cipher, hash, and compression algorithms. The support for these
10225algorithms has grown since the adoption of the Libgcrypt library in the 2.x
10226versions of GPG.
10227
10228As you will be able to see below in an example of a full key generation with the
10229GPG command line tool, GPG recommends the following algorithms to new users:
10230
10231#+begin_src sh
10232Please select what kind of key you want:
10233 (1) RSA and RSA
10234 (2) DSA and Elgamal
10235 (3) DSA (sign only)
10236 (4) RSA (sign only)
10237 (9) ECC (sign and encrypt) *default*
10238 (10) ECC (sign only)
10239#+end_src
10240
10241I am not doing an in-depth explanation here in order to keep the focus on GPG
10242and not encryption algorithms. If you want a deep dive into cryptography or
10243encryption algorithms, please read my other posts:
10244
10245- [[https://cleberg.net/blog/aes-encryption.html][How AES Encryption Works]] (2018)
10246- [[https://cleberg.net/blog/cryptography.html][A Practical Guide to Encryption, Keys, and Secure Communication]] (2020)
10247
10248** Vulnerabilities
10249
10250As of 2022-07-14, there are a few different vulnerabilities associated with GPG
10251or the libraries it uses:
10252
10253- GPG versions 1.0.2--1.2.3 contains a bug where "as soon as one (GPG-generated)
10254 ElGamal signature of an arbitrary message is released, one can recover the
10255 signer's private key in less than a second on a PC." ([[https://www.di.ens.fr/~pnguyen/pub_Ng04.htm][Source]])
10256- GPG versions prior to 1.4.2.1 contain a false positive signature verification
10257 bug. ([[https://lists.gnupg.org/pipermail/gnupg-announce/2006q1/000211.html][Source]])
10258- GPG versions prior to 1.4.2.2 cannot detect injection of unsigned data. (
10259 [[https://lists.gnupg.org/pipermail/gnupg-announce/2006q1/000218.html][Source]])
10260- Libgcrypt, a library used by GPG, contained a bug which enabled full key
10261 recovery for RSA-1024 and some RSA-2048 keys. This was resolved in a GPG
10262 update in 2017. ([[https://lwn.net/Articles/727179/][Source]])
10263- The [[https://en.wikipedia.org/wiki/ROCA_vulnerability][ROCA Vulnerability]] affects RSA keys generated by YubiKey 4 tokens.
10264 ([[https://crocs.fi.muni.cz/_media/public/papers/nemec_roca_ccs17_preprint.pdf][Source]])
10265- The [[https://en.wikipedia.org/wiki/SigSpoof][SigSpoof Attack]] allows an attacker to spoof digital signatures. ([[https://arstechnica.com/information-technology/2018/06/decades-old-pgp-bug-allowed-hackers-to-spoof-just-about-anyones-signature/][Source]])
10266- Libgcrypt 1.9.0 contains a severe flaw related to a heap buffer overflow,
10267 fixed in Libgcrypt 1.9.1 ([[https://web.archive.org/web/20210221012505/https://www.theregister.com/2021/01/29/severe_libgcrypt_bug/][Source]])
10268
10269*** Platforms
10270
10271Originally developed as a command-line program for *nix systems, GPG now has a
10272wealth of front-end applications and libraries available for end-users. However,
10273the most recommended programs remain the same:
10274
10275- [[https://gnupg.org][GnuPG]] for Linux (depending on distro)
10276- [[https://gpg4win.org][Gpg4win]] for Windows
10277- [[https://gpgtools.org][GPGTools]] for macOS
10278
10279* Creating a Key Pair
10280
10281In order to create a GPG key pair, a user would first need to install GPG on
10282their system. If we're assuming that the user is on Fedora Linux, they would
10283execute the following:
10284
10285#+begin_src sh
10286sudo dnf install gpg
10287#+end_src
10288
10289Once installed, a user can create a new key pair with the following command(s):
10290
10291#+begin_src sh
10292gpg --full-generate-key
10293#+end_src
10294
10295GPG will walk the user through an interactive setup that asks for an algorithm
10296preference, expiration date, name, and email to associate with this key.
10297
10298See the following example key set-up for a default key generation using the
10299GnuPG command-line interface:
10300
10301#+begin_src sh
10302gpg (GnuPG) 2.3.6; Copyright (C) 2021 Free Software Foundation, Inc.
10303This is free software: you are free to change and redistribute it.
10304There is NO WARRANTY, to the extent permitted by law.
10305
10306Please select what kind of key you want:
10307 (1) RSA and RSA
10308 (2) DSA and Elgamal
10309 (3) DSA (sign only)
10310 (4) RSA (sign only)
10311 (9) ECC (sign and encrypt) *default*
10312 (10) ECC (sign only)
10313 (14) Existing key from card
10314Your selection? 9
10315Please select which elliptic curve you want:
10316 (1) Curve 25519 *default*
10317 (4) NIST P-384
10318Your selection? 1
10319Please specify how long the key should be valid.
10320 0 = key does not expire
10321 <n> = key expires in n days
10322 <n>w = key expires in n weeks
10323 <n>m = key expires in n months
10324 <n>y = key expires in n years
10325Key is valid for? (0) 0
10326Key does not expire at all
10327Is this correct? (y/N) y
10328
10329GnuPG needs to construct a user ID to identify your key.
10330
10331Real name: John Doe
10332Email address: johndoe@example.com
10333Comment: test key
10334You selected this USER-ID:
10335 "John Doe (test key) <johndoe@example.com>"
10336
10337Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
10338We need to generate a lot of random bytes. It is a good idea to perform
10339some other action (type on the keyboard, move the mouse, utilize the
10340disks) during the prime generation; this gives the random number
10341generator a better chance to gain enough entropy.
10342We need to generate a lot of random bytes. It is a good idea to perform
10343some other action (type on the keyboard, move the mouse, utilize the
10344disks) during the prime generation; this gives the random number
10345generator a better chance to gain enough entropy.
10346gpg: revocation certificate stored as 'example.rev'
10347public and secret key created and signed.
10348
10349pub ed25519 2022-07-14 [SC]
10350 E955B7700FFC11EF51C2BA1FE096AACDD4C32E9C
10351uid John Doe (test key) <johndoe@example.com>
10352sub cv25519 2022-07-14 [E]
10353#+end_src
10354
10355Please note that graphical apps may differ slightly from the GPG command-line
10356interface.
10357
10358* Common Usage
10359
10360As noted in RFC (Request for Comments) 4880, the general functions of OpenPGP
10361are as follows:
10362
10363- digital signatures
10364- encryption
10365- compression
10366- Radix-64 conversion
10367- key management and certificate services
10368
10369From this, you can probably gather that the main use of GPG is for encrypting
10370data and/or signing the data with a key. The purpose of encrypting data with GPG
10371is to ensure that no one except the intended recipient(s) can access the data.
10372
10373Let's explore some specific GPG use-cases.
10374
10375** Email
10376
10377One of the more popular uses of GPG is to sign and/or encrypt emails. With the
10378use of a GPG keypair, you can encrypt a message, its subject, and even the
10379attachments within.
10380
10381The first process, regarding the signing of a message without any encryption, is
10382generally used to provide assurance that an email is truly coming from the
10383sender that the message claims. When I send an email, and it's signed with my
10384public key, the recipient(s) of the message can verify that the message was
10385signed with my personal key.
10386
10387The second process, regarding the actual encryption of the message and its
10388contents, works by using a combination of the sender's keys and the recipient's
10389keys. This process may vary slightly by implementation, but it most commonly
10390uses asymmetric cryptography, also known as public-key cryptography. In this
10391version of encryption, the sender's private key to sign the message and a
10392combination of the sender's keys and the recipient's public key to encrypt the
10393message.
10394
10395If two people each have their own private keys and exchange their public keys,
10396they can send encrypted messages back and forth with GPG. This is also possible
10397with symmetric cryptography, but the process differs since there are no key
10398pairs.
10399
10400Implementation of email encryption varies greatly between email clients, so you
10401will need to reference your email client's documentation to ensure you are
10402setting it up correctly for that specific client.
10403
10404** File Encryption
10405
10406As noted in the section above regarding emails, GPG enables users to be able to
10407send a message to each other if they are both set-up with GPG keys. In this
10408example, I am going to show how a user could send a file called
10409=example_file.txt= to another user via the recipient's email.
10410
10411The sender would find the file they want to send and execute the following
10412command:
10413
10414#+begin_src sh
10415gpg --encrypt --output example_file.txt.gpg --recipient \
10416recipient@example.com example_file.txt
10417#+end_src
10418
10419Once received, the recipient can decrypt the file with the following command:
10420
10421#+begin_src sh
10422gpg --decrypt --output example_file.txt example_file.txt.gpg
10423#+end_src
10424
10425** Ownership Signatures
10426
10427One important aspect of GPG, especially for developers, is the ability to sign
10428data without encrypting it. For example, developers often sign code changes when
10429they commit the changes back to a central repository, in order to display
10430ownership of who made the changes. This allows other users to look at a code
10431change and determine that the change was valid.
10432
10433In order to do this using [[https://git-scm.com][Git]], the developer simply needs to alter the =git
10434commit= command to include the =-S= flag. Here's an example:
10435
10436#+begin_src sh
10437git commit -S -m "my commit message"
10438#+end_src
10439
10440As an expansion of the example above, Git users can configure their environment
10441with a default key to use by adding their GPG signature:
10442
10443#+begin_src sh
10444git config --global user.signingkey XXXXXXXXXXXXXXXX
10445#+end_src
10446
10447If you're not sure what your signature is, you can find it titled =sig= in the
10448output of this command:
10449
10450#+begin_src sh
10451gpg --list-signatures
10452#+end_src
10453
10454** File Integrity
10455
10456When a person generates a signature for data, they are allowing users the
10457ability to verify the signature on that data in the future to ensure the data
10458has not been corrupted. This is most common with software applications hosted on
10459the internet - developers provide signatures so that users can verify a website
10460was not hijacked and download links replaced with dangerous software.
10461
10462In order to verify signed data, a user needs to have:
10463
104641. The signed data
104652. A signature file
104663. The public GPG key of the signer
10467
10468Once the signer's public key is imported on the user's system, and they have the
10469data and signature, they can verify the data with the following commands:
10470
10471#+begin_src sh
10472# If the signature is attached to the data
10473gpg --verify [signature-file]
10474
10475# If the signature is detached as a separate file from the data
10476gpg --verify [signature-file] [original-file]
10477#+end_src
10478
10479*** Finding Public Keys
10480
10481In order to use GPG with others, a user needs to know the other user(s) keys.
10482This is easy to do if the user knows the other user(s) in person, but may be
10483hard if the relationship is strictly digital. Luckily, there are a few options.
10484The first option is to look at a user's web page or social pages if they have
10485them.
10486
10487Otherwise, the best option is to use a keyserver, such as:
10488
10489- [[https://pgp.mit.edu][pgp.mit.edu]]
10490- [[https://keys.openpgp.org][keys.openpgp.org]]
10491#+date: <2022-07-25 Mon 00:00:00>
10492#+title: Curseradio: A Lightweight Command-Line Internet Radio Player for Linux
10493#+description: Stepwise instructions for installing and utilizing Curseradio, a lightweight command-line program for streaming radio stations within Linux environments.
10494#+slug: curseradio
10495
10496* Overview
10497
10498While exploring some interesting Linux applications, I stumbled across
10499[[https://github.com/chronitis/curseradio][curseradio]], a command-line radio player based on Python.
10500
10501This application is fantastic and incredibly easy to install, so I wanted to
10502dedicate a post today to this app. Let's look at the features within the app and
10503then walk through the installation process I took to get =curseradio= working.
10504
10505* Features
10506
10507The radio player itself is quite minimal. As you can see in the screenshot
10508above, it contains a simple plaintext list of all available categories, which
10509can be broken down further and further. In addition, radio shows are available
10510for listening, alongside regular radio stations.
10511
10512For example, the =Sports= > =Pro Basketball= > =Shows= category contains a
10513number of specific shows related to Professional Basketball.
10514
10515Aside from being able to play any of the listed stations/shows, you can make a
10516channel your favorite by pressing =f=. It will now show up at the top of the
10517radio player in the =Favourites= category.
10518
10519** Commands/Shortcuts
10520
10521| Key(s) | Command |
10522|------------+---------------------------------|
10523| ↑, ↓ | navigate |
10524| PgUp, PgDn | navigate quickly |
10525| Home, End | to top/bottom |
10526| Enter | open/close folders, play stream |
10527| k | stop playing stream |
10528| q | quit |
10529| f | toggle favourite |
10530
10531* Installation
10532
10533** Dependencies
10534
10535Before installing =curseradio=, a handful of system and Python packages are
10536required. To get started, install =python3=, =pip3=, and =mpv= on your system.
10537In this example, I'm using Fedora Linux, which uses the =dnf= package manager.
10538You may need to adjust this if you're using a different system.
10539
10540#+begin_src sh
10541sudo dnf install python3 pip3 mpv
10542#+end_src
10543
10544Next, use =pip3= to install =requests=, =xdg=, and =lxml=:
10545
10546#+begin_src sh
10547pip3 install requests xdg lxml
10548#+end_src
10549
10550** Repository Source Installation
10551
10552Once all the dependencies are installed, we can clone the source code and enter
10553that directory:
10554
10555#+begin_src sh
10556git clone https://github.com/chronitis/curseradio && cd curseradio
10557#+end_src
10558
10559Once you're within the =curseradio= directory, you can install the application
10560with the provided =setup.py= script.
10561
10562#+begin_src sh
10563sudo python3 setup.py install
10564#+end_src
10565
10566In my case, I ran into a few errors and needed to create the folders that
10567curseradio wanted to use for its installation. If you don't get any errors, you
10568can skip this and run the app.
10569
10570#+begin_src sh
10571sudo mkdir /usr/local/lib/python3.10/
10572sudo mkdir /usr/local/lib/python3.10/site-packages/
10573#+end_src
10574
10575#+begin_src sh
10576sudo chown -R $USER:$USER /usr/local/lib/python3.10/
10577#+end_src
10578
10579* Run the Application
10580
10581Once fully installed without errors, you can run the application!
10582
10583#+begin_src sh
10584python3 /usr/local/bin/curseradio
10585#+end_src
10586#+date: <2022-07-30 Sat 00:00:00>
10587#+title: Automating Recursive FLAC to Opus Conversion with a Bash Script
10588#+description: Instructions for automating the conversion of audio files from FLAC to Opus format recursively within directory structures, including performance considerations and script usage.
10589#+slug: flac-to-opus
10590
10591* Converting FLAC to OPUS
10592
10593I am currently rebuilding my music library from scratch so that I can
10594effectively archive all the music I own in [[https://wikipedia.org/wiki/FLAC][FLAC]] (Free Lossless Audio Codec)
10595files.
10596
10597However, streaming FLAC files outside the home can be difficult due to the size
10598of the files, especially if you're using a weak connection.
10599
10600So, in order to archive the music in a lossless format and still be able to
10601stream it easily, I opted to create a copy of my FLAC files in the [[https://en.wikipedia.org/wiki/Opus_(audio_format)][Opus audio
10602codec]]. This allows me to archive a quality, lossless version of the music and
10603then point my streaming service to the smaller, stream-ready version.
10604
10605** Dependencies
10606
10607The process I follow utilizes the =opus-tools= package in Ubuntu. Before
10608proceeding, install the package:
10609
10610#+begin_src sh
10611sudo apt install opus-tools
10612#+end_src
10613
10614If you want to use a different conversion method, such as =ffmpeg= or =avconv=,
10615simply install that package instead.
10616
10617** Conversion Process
10618
10619The script I'm using is stored in my home directory, but feel free to create it
10620wherever you want. It does not need to be in the same directory as your music
10621files.
10622
10623#+begin_src sh
10624cd ~ && nano transform.sh
10625#+end_src
10626
10627Once you have your new bash script opened in an editor, go ahead and paste the
10628following logic into the script.
10629
10630You *MUST* edit the following variables in order for it to work:
10631
10632- =source=: The source directory where your FLAC files are stored.
10633- =dest=: The destination directory where you want the resulting Opus files to
10634 be stored.
10635
10636You *MAY* want to edit the following variables to suit your needs:
10637
10638- =filename=: If you are converting to a file format other than Opus, you'll
10639 need to edit this so that your resulting files have the correct filename
10640 extension.
10641- =reldir=: This variable can be edited to strip out more leading directories in
10642 the file path. As you'll see later, I ignore this for now and simply clean it
10643 up afterward.
10644- =opusenc=: This is the actual conversion process. You may want to edit the
10645 bitrate to suit your needs. I set mine at 128 but some prefer 160 or higher.
10646
10647#+begin_src sh
10648#!/bin/bash
10649## - The IFS takes care of spaces in file and dirnames
10650## - your folders may vary
10651## - what you mount to the folders does not matter
10652## - in RELDIR, the f5 most likely MUST be edited,
10653## since its responsible, how many leading directories
10654## will be removed from the directory structure in order
10655## to append that exact path to the outfile
10656## - the commented echos are still in place in order to give
10657## you the variables for testing, before running.
10658
10659IFS=$'\n'
10660
10661## the paths given here contain the directory structure that I want to keep
10662## source=/mnt/music/archives/ARTIST/ALBUM/FLACFILE.flac
10663## local=/mnt/music/library/ARTIST/ALBUM/OPUSFILE.opus
10664
10665source=/mnt/music/archives
10666dest=/mnt/music/library
10667
10668for i in $(find $source -type f -iname '*.flac' );
10669do
10670## SET VARIABLES for PATHS and FILENAMES
10671 fullfile=$i
10672 filename="${i##*/}"
10673 filename="${filename%.*}.opus"
10674 fulldir=$(dirname "${i}")
10675 reldir="$(echo $fulldir | cut -d'/' -f5-)"
10676 reldir=${reldir//flac}
10677 outdir="$dest/$reldir"
10678 outfile="$outdir/$filename"
10679
10680# is that working?
10681# outfile='$local/""$(echo $(dirname "${i}") | cut -d'/' -f5-)"//flac"/"${i##*/}"'
10682# echo 'output file: ' "$outfile"
10683
10684## SHOW ME THE CONTENTS of the VARIABLES
10685# echo 'File found:' "$i"
10686# echo 'Relative dir: ' "$reldir"
10687# echo 'directory will be created: ' "$outdir"
10688# echo 'Filename: ' "$filename"
10689# echo 'FileExt: ' "$extension"
10690# echo 'output file: ' "$outfile"
10691
10692echo "\n\n"
10693
10694## CREATE Output Folders
10695 mkdir -p "$outdir"
10696
10697## RUN
10698# ffmpeg and avconv are alternative options if opusenc isn't adequate
10699opusenc --vbr --bitrate 128 --date "$DATE" \
10700--title "$TITLE" --artist "$ARTIST" --album "$ALBUM" --genre "$GENRE" \
10701--comment "ALBUMARTIST=$ALBUMARTIST" --comment "DISCNUMBER=$DISCNUMBER" \
10702--comment "TRACKNUMBER=$TRACKNUMBER" --comment "TRACKTOTAL=$TRACKTOTAL" \
10703--comment "LYRICS=$LYRICS" "$fullfile" "$outfile"
10704
10705
10706## just for testing
10707# sleep 1
10708done
10709#+end_src
10710
10711Once you're done, simply save the file and exit your editor. Don't forget to
10712enable execution of the script:
10713
10714#+begin_src sh
10715chmod +x transform.sh
10716#+end_src
10717
10718Finally, you may now run the script:
10719
10720#+begin_src sh
10721./transform.sh
10722#+end_src
10723
10724If you used =opusenc=, you'll see the conversions happen within the terminal as
10725it progresses. You will also see variables printed if you uncommented any of the
10726bash script's comments.
10727
10728** Cleanup
10729
10730As I noted above, I didn't customize my =reldir= variable in the script, which
10731caused my output directory to be =/mnt/music/library/archives= instead of
10732=/mnt/music/library=. So, I moved the output up one level and deleted the
10733accidental directory.
10734
10735#+begin_src sh
10736cd /mnt/music/library
10737mv archives/** .
10738rm -rf archives
10739#+end_src
10740
10741** Check the Resulting Size
10742
10743If you want to see what kind of file size savings you've gained, you can always
10744use the =du= command to check:
10745
10746#+begin_src sh
10747cd /mnt/music
10748du -h --max-depth=1 .
10749#+end_src
10750
10751In my case, my small library went from 78GB to 6.3GB!
10752
10753#+begin_src txt
1075478G ./archives
107556.3G ./library
10756#+end_src
10757#+date: <2022-07-31 Sun 00:00:00>
10758#+title: Boosting Bash Productivity: Using Bash-it and ble.sh for Plugins and Autosuggestions
10759#+description: Guide to augmenting Bash shell features by integrating Bash-It framework along with ble.sh for input autosuggestions, improving command-line efficiency on Linux systems.
10760#+slug: bash-it
10761
10762* Bash
10763
10764For those who are not familiar, [[https://en.wikipedia.org/wiki/Bash_(Unix_shell)][Bash]] is a Unix shell that is used as the default
10765login shell for most Linux distributions. This shell and command processor
10766should be familiar if you've used Linux (or older version of macOS) before.
10767
10768However, bash is not the only option. There are numerous other shells that
10769exist. Here are some popular examples:
10770
10771- [[https://en.wikipedia.org/wiki/Z_shell][zsh]]
10772- [[https://en.wikipedia.org/wiki/Fish_(Unix_shell)][fish]]
10773- [[https://github.com/ibara/oksh][oksh]]
10774- [[https://wiki.gentoo.org/wiki/Mksh][mksh]]
10775- [[https://en.wikipedia.org/wiki/Debian_Almquist_shell][dash]]
10776
10777While each shell has its differences, bash is POSIX compliant and the default
10778for many Linux users. Because of this, I am going to explore a program called
10779=bash-it= below that helps bash users increase the utility of their shell
10780without installing a completely new shell.
10781
10782** Installation
10783
10784First, if bash is not already installed on your system, you can [[https://www.gnu.org/software/bash/][download bash
10785from GNU]] or use your package manager to install it.
10786
10787For example, this is how you can install bash on Fedora Linux:
10788
10789#+begin_src sh
10790sudo dnf install bash
10791#+end_src
10792
10793If you are not using bash as your default shell, use the =chsh= command to
10794change your shell:
10795
10796#+begin_src sh
10797chsh
10798#+end_src
10799
10800You should see a prompt like the one below. If the brackets (=[]=) contain
10801=bash= already, you're done, and you can simply continue by hitting the Enter
10802key.
10803
10804If the brackets contain another shell path (e.g. =/usr/bin/zsh=), enter the path
10805to the bash program on your system (it's most likely located at
10806=/usr/bin/bash=).
10807
10808#+begin_src sh
10809Changing shell for <user>.
10810New shell [/usr/bin/bash]:
10811#+end_src
10812
10813You must log out or restart the machine in order for the login shell to be
10814refreshed. You can do it now or wait until you're finished customizing the
10815shell.
10816
10817#+begin_src sh
10818sudo reboot now
10819#+end_src
10820
10821* Bash-it
10822
10823As noted on the [[https://github.com/Bash-it/bash-it][Bash-it]] repository:
10824
10825#+begin_quote
10826Bash-it is a collection of community Bash commands and scripts for Bash
108273.2+. (And a shameless ripoff of oh-my-zsh 😃)
10828#+end_quote
10829
10830Bash-it makes it easy to install plugins, set up aliases for common commands,
10831and easily change the visual theme of your shell.
10832
10833** Installation
10834
10835To install the framework, simply copy the repository files and use the
10836=install.sh= script provided. If you want, you can (and should!) inspect the
10837contents of the installation script before you run it.
10838
10839#+begin_src sh
10840git clone --depth=1 https://github.com/Bash-it/bash-it.git ~/.bash_it
10841~/.bash_it/install.sh
10842#+end_src
10843
10844If you didn't restart your session after making bash the default, and are
10845currently working within another shell, be sure to enter a bash session before
10846using =bash-it=:
10847
10848#+begin_src sh
10849bash
10850#+end_src
10851
10852** Aliases
10853
10854Bash-it contains a number of aliases for common commands to help improve
10855efficiency in the terminal. To list all available options, use the following
10856command:
10857
10858#+begin_src sh
10859bash-it show aliases
10860#+end_src
10861
10862This will provide you a list that looks like the following text block. Within
10863this screen, you will be able to see all available options and which ones are
10864currently enabled.
10865
10866#+begin_src txt
10867Alias Enabled? Description
10868ag [ ] the silver searcher (ag) aliases
10869ansible [ ] ansible abbreviations
10870apt [ ] Apt and dpkg aliases for Ubuntu and Debian distros.
10871atom [ ] Atom.io editor abbreviations
10872bash-it [ ] Aliases for the bash-it command (these aliases are automatically included with the "general" aliases)
10873bolt [ ] puppet bolt aliases
10874bundler [ ] ruby bundler
10875clipboard [ ] xclip shortcuts
10876composer [ ] common composer abbreviations
10877curl [x] Curl aliases for convenience.
10878...
10879#+end_src
10880
10881To enable an alias, do:
10882
10883#+begin_src sh
10884bash-it enable alias <alias name> [alias name]... -or- $ bash-it enable alias all
10885#+end_src
10886
10887To disable an alias, do:
10888
10889#+begin_src sh
10890bash-it disable alias <alias name> [alias name]... -or- $ bash-it disable alias all
10891#+end_src
10892
10893** Plugins
10894
10895Similar to aliases, plugins are available with bash-it. You can find a complete
10896list of plugins in the same way as aliases. Simply execute the following:
10897
10898#+begin_src sh
10899bash-it show plugins
10900#+end_src
10901
10902You will see the following output showing enabled and disabled plugins:
10903
10904#+begin_src txt
10905Plugin Enabled? Description
10906alias-completion [ ]
10907autojump [ ] Autojump configuration, see https://github.com/wting/autojump for more details
10908aws [ ] AWS helper functions
10909base [x] miscellaneous tools
10910basher [ ] initializes basher, the shell package manager
10911battery [x] display info about your battery charge level
10912blesh [ ] load ble.sh, the Bash line editor!
10913boot2docker [ ] Helpers to get Docker setup correctly for boot2docker
10914browser [ ] render commandline output in your browser
10915#+end_src
10916
10917To enable a plugin, do:
10918
10919#+begin_src sh
10920bash-it enable plugin <plugin name> [plugin name]... -or- $ bash-it enable plugin all
10921#+end_src
10922
10923To disable a plugin, do:
10924
10925#+begin_src sh
10926bash-it disable plugin <plugin name> [plugin name]... -or- $ bash-it disable plugin all
10927#+end_src
10928
10929** Themes
10930
10931There are quite a few pre-defined [[https://bash-it.readthedocs.io/en/latest/themes-list/#list-of-themes][themes]] available with bash-it.
10932
10933To list all themes:
10934
10935#+begin_src sh
10936ls ~/.bash_it/themes/
10937#+end_src
10938
10939To use a new theme, you'll need to edit =.bashrc= and alter the =BASH_IT_THEME=
10940variable to your desired theme. For example, I am using the =zork= theme.
10941
10942#+begin_src sh
10943nano ~/.bashrc
10944#+end_src
10945
10946#+begin_src sh
10947export BASH_IT_THEME='zork'
10948#+end_src
10949
10950Once you save your changes, you just need to exit your terminal and create a new
10951one in order to see your changes to the =.bashrc= file. You can also =source=
10952the file to see changes, but I recommend starting a completely new shell
10953instead.
10954
10955*** ble.sh
10956
10957One big feature I was missing in Bash that both =zsh= and =fish= have is an
10958autosuggestion feature. To explain: as you type, an autosuggestion feature in
10959the shell will offer suggestions in a lighter font color beyond the characters
10960already typed. Once you see the command you want, you can click the right arrow
10961and have the shell auto-complete that line for you.
10962
10963Luckily, the [[https://github.com/akinomyoga/ble.sh][Bash Line Editor]] (ble.sh) exists! This program provides a wonderful
10964autosuggestions feature perfectly, among other features that I haven't tested
10965yet.
10966
10967In order to install ble.sh, execute the following:
10968
10969#+begin_src sh
10970git clone --recursive https://github.com/akinomyoga/ble.sh.git
10971make -C ble.sh install PREFIX=~/.local
10972echo 'source ~/.local/share/blesh/ble.sh' >> ~/.bashrc
10973#+end_src
10974
10975Again, exit the terminal and open a new one in order to see the newly-configured
10976shell.
10977
10978* Restart the Session
10979
10980Finally, as mentioned above, you'll need to restart the session to ensure that
10981your user is using bash by default.
10982
10983You will also need to exit and re-open a shell (e.g., terminal or terminal tab)
10984any time you make changes to the =.bashrc= file.
10985
10986#+begin_src sh
10987sudo reboot now
10988#+end_src
10989#+date: <2022-08-31 Wed 00:00:00>
10990#+title: Privacy.com Shifts From Prepaid Debit to Charge Card: What Users Need to Know
10991#+description: Detailed examination of modifications to Privacy.com service terms and their implications on user privacy and payment security protocols.
10992#+slug: privacy-com-changes
10993
10994* Privacy.com Changes Their Terms
10995
10996Recently, Privacy.com reached out to their customers regarding a change in their
10997terms of use. Further, all customers are required to agree to the changes in
10998order to continue using their accounts.
10999
11000[[https://privacy.com/commercial-cardholder-agreement][You can view the new cardholder agreement here]].
11001
11002When you log in, you'll be greeted with a pop-up window asking you to review and
11003agree to the new terms of use. You will also not be able to open any new cards
11004until the terms are agreed to.
11005
11006** Changing from a "Prepaid Debit" Model to a "Charge Card" Model
11007
11008The actual content of the changes is interesting. While the historical model of
11009using Privacy.com was akin to prepaid debit cards, the new model is very similar
11010to a credit card (they use the term "charge card").
11011
11012I have used Privacy.com for 1-2 years, and the process was always that you would
11013create a single-use or merchant-locked card. This card could be used for any
11014dollar limit you set and would immediately draw the funds from whatever funding
11015source you connected, e.g. PayPal account or a bank account.
11016
11017The benefit this service provides with a premium account is masking the merchant
11018names from your funding source. If you have a concern that your bank account
11019uses merchant data from your account, you could direct all charges through
11020Privacy.com and set the merchant as one of their pre-set options, such as
11021"Smiley's Corner Store" or "NSA Gift Shop."
11022
11023The new model still works with a bank account as a funding source, but the model
11024is changed so that you get a "line of credit" set according to a 14-day billing
11025cycle. It seems that Privacy.com will now allow charges to be incurred without
11026being immediately paid.
11027
11028** Daily Payments and Available Credit
11029
11030Instead of paying as charges are incurred, you must make a "Daily Payment" and
11031your "Available Credit" will be locked until you make that payment. There are
11032also "End of Billing Cycle Payments" that are assigned a due date.
11033
11034Further, Privacy.com will decline charges that would cause you to exceed your
11035Available Credit or Credit Limit.
11036
11037One particular interesting section states the following:
11038
11039#+begin_quote
11040YOUR OBLIGATION TO PAY US BACK FOR ALL CARD TRANSACTIONS AND OTHER
11041OBLIGATIONS YOU INCUR IS SECURED BY THE SECURED ACCOUNT. IF YOU DO NOT
11042PAY US BACK FOR ANY AMOUNT YOU OWE US WHEN YOUR PAYMENTS ARE DUE, WE
11043WILL EXERCISE OUR INTEREST AND DEBIT THE SECURED ACCOUNT, AND YOU WILL
11044LOSE THE MONEY IN THE SECURED ACCOUNT. SEE SECTION 8: SECURITY AGREEMENT
11045FOR MORE INFORMATION.
11046#+end_quote
11047
11048** Personal Information
11049
11050Now that Privacy.com is more of a financial institution, they are obligated to
11051comply with the [[https://en.wikipedia.org/wiki/Know_your_customer][know your customer]] guidelines/laws.
11052
11053I did not proceed with the change to my Privacy.com account, but I have heard
11054from some peers that the changes require more personal information to be
11055submitted, such as SSN (Social Security Number). I am not aware of all new
11056personal information required or if the funding source is now required to only
11057be a bank account.
11058
11059** Fees
11060
11061Luckily, the fees section did not change much. The subscription fees for a
11062premium account are still the only fees.
11063
11064* My Thoughts
11065
11066Personally, I wiped my personal information from my account and then permanently
11067deleted it when I heard about these changes. I have no interest in yet another
11068method of credit lending offered by private companies. While I accepted that
11069they would have access to my bank account information for the purposes of paying
11070off my prepaid debit payments, I have no interest in incurring charges that will
11071need to be paid back at a later date. I also have no interest in submitting
11072personal information to Privacy.com.
11073
11074This type of change toward a "buy it now, pay us later" model is concerning, and
11075I will be watching Privacy.com to see if they further their interests in the
11076credit model as time goes on.
11077
11078Could we see them start charging interest, fees, etc.? I'm not sure, but this
11079change does not inspire confidence in their mission as a privacy-focused
11080company.
11081#+date: <2022-09-17 Sat 00:00:00>
11082#+title: Building and Exploring SerenityOS: A Retro-Inspired Unix-Like Desktop
11083#+description: Technical overview of Serenity OS including system architecture, building procedures, and operational instructions for this Unix-like operating system with a 1990s user interface style.
11084#+slug: serenity-os
11085
11086* Overview
11087
11088[[https://serenityos.org][SerenityOS]] is a unique operating system (OS) that I have seen pop up in my news
11089feed a few times over the last few years, but I have never had time to test it
11090out until now.
11091
11092Testing out this system brought back fond memories of yellowed,
11093modem-screeching, 100-pound computers that brought so many fond memories to my
11094youth.
11095
11096Per their website:
11097
11098#+begin_quote
11099A graphical Unix-like operating system for desktop computers!
11100
11101SerenityOS is a love letter to '90s user interfaces with a custom
11102Unix-like core. It flatters with sincerity by stealing beautiful ideas
11103from various other systems.
11104
11105Roughly speaking, the goal is a marriage between the aesthetic of
11106late-1990s productivity software and the power-user accessibility of
11107late-2000s *nix.
11108
11109This is a system by us, for us, based on the things we like.
11110#+end_quote
11111
11112* Building
11113
11114Your first question may be "Where's the iso?" and the answer is... there are
11115none. SerenityOS does not provide pre-built images for testing. You must build
11116the images yourself. This seems intentionally to limit participation to only
11117those who are truly interested enough to learn how to build the OS.
11118
11119** Clone
11120
11121In order to get started, you'll need to clone the source repository:
11122
11123#+begin_src sh
11124git clone https://github.com/SerenityOS/serenity && cd serenity
11125#+end_src
11126
11127** Build
11128
11129Note that I followed the [[https://github.com/SerenityOS/serenity/blob/master/Documentation/BuildInstructions.md][Build Instructions]] in the SerenityOS repository as of
11130commit =660d2b53b1206e868d5470eee80b5e62d7e30da7=. Things may have changed since
11131my installation, and you should double-check the instructions first.
11132
11133Regardless, I want to repeat my steps here to illustrate any errors or differing
11134commands I needed to run in order to build and run SerenityOS.
11135
11136Since I am running Fedora, I needed to install these packages in order to build
11137the OS images:
11138
11139#+begin_src sh
11140sudo dnf install texinfo binutils-devel curl cmake mpfr-devel libmpc-devel gmp-devel e2fsprogs ninja-build patch ccache rsync @"C Development Tools and Libraries" @Virtualization
11141#+end_src
11142
11143Next, make sure you're inside the =serenity= directory created earlier during
11144the git cloning process and process to build the toolchain:
11145
11146#+begin_src sh
11147Meta/serenity.sh rebuild-toolchain
11148#+end_src
11149
11150Once the toolchain is built, you can Build and run the OS!
11151
11152#+begin_src sh
11153Meta/serenity.sh run
11154#+end_src
11155
11156After this process is completed, the image should run automatically and
11157launch.
11158
11159* Issues
11160
11161I played around in SerenityOS for an hour or two in order to see what I could do
11162and had a lot of fun with it. The only issue I ran into was a lack of working
11163internet. I didn't try very hard, but I could tell that the main network link
11164wasn't connecting to my Fedora host properly.
11165
11166* Screenshots
11167
11168The initial launch of the image displays the SerenityOS desktop, with a simple
11169terminal already launched.
11170
11171There is also a "Fire" application (literally just shows fire burning), a
11172browser with the local Serenity Browser page loaded, and a text editor.
11173
11174I also poked around the system utilities and found most tools you'd expect to
11175find within a standard desktop, such as a system monitoring tool.
11176
11177Lastly, I noted that the default desktop contains numerous pre-defined themes to
11178choose from. This is a small piece, but it's actually wonderful to see desktop
11179developers consider theming directly out of the box rather than using an
11180addon-based mentality.
11181
11182I didn't take a screenshot of the other pre-installed games, but I did spend
11183nearly 30 minutes playing Solitaire before remembering that I was supposed to be
11184writing a post about the OS.
11185#+date: <2022-09-21 Wed 00:00:00>
11186#+title: Installing GrapheneOS on Pixel 6 Pro
11187#+description: Step-by-step instructions to perform secure installation of the GrapheneOS operating system on Google Pixel 6 Pro hardware platform.
11188#+slug: graphene-os
11189
11190* Introduction
11191
11192After using iOS for a couple of years, I finally took the plunge and purchased a
11193Pixel 6 Pro in order to test and use [[https://grapheneos.org][GrapheneOS]].
11194
11195The installation process was rather quick once you have the tools and files you
11196need. Overall, it can be done in just a few minutes.
11197
11198* Gathering Tools & Files
11199
11200** Android Tools
11201
11202First, in order to interact with the device, we will need the [[https://developer.android.com/studio/releases/platform-tools.html][Android platform
11203tools]]. Find the Linux download and save the ZIP folder to your preferred
11204location.
11205
11206Once we've downloaded the files, we will need to unzip them, enter the
11207directory, and move the necessary executables to a central location, such as
11208=/usr/bin/=. For this installation, we only need the =fastboot= and =adb=
11209executables.
11210
11211#+begin_src sh
11212cd ~/Downloads
11213#+end_src
11214
11215#+begin_src sh
11216unzip platform-tools_r33.0.3-linux.zip
11217cd platform-tools
11218sudo mv fastboot /usr/bin/
11219sudo mv adb /usr/bin
11220#+end_src
11221
11222** GrapheneOS Files
11223
11224Next, we need the [[https://grapheneos.org/releases][GrapheneOS files]] for our device and model. For example, the
11225Pixel 6 Pro is codenamed =raven= on the release page.
11226
11227Once we have the links, let's download them to our working directory:
11228
11229#+begin_src sh
11230curl -O https://releases.grapheneos.org/factory.pub
11231curl -0 https://releases.grapheneos.org/raven-factory-2022091400.zip
11232curl -0 https://releases.grapheneos.org/raven-factory-2022091400.zip.sig
11233#+end_src
11234
112351. Validate Integrity
11236
11237 In order to validate the integrity of the downloaded files, we will need the
11238 =signify= package and Graphene's =factory.pub= file.
11239
11240 #+begin_src sh
11241 sudo dnf install signify
11242 #+end_src
11243
11244 #+begin_src sh
11245 curl -O https://releases.grapheneos.org/factory.pub
11246 #+end_src
11247
11248 Then we can validate the files and ensure that no data was corrupted or
11249 modified before it was saved to our device.
11250
11251 #+begin_src sh
11252 signify -Cqp factory.pub -x raven-factory-2022091400.zip.sig && echo verified
11253 #+end_src
11254
112552. Unzip Files
11256
11257 Once the files are verified, we can unzip the Graphene image and enter the
11258 directory:
11259
11260 #+begin_src sh
11261 unzip raven-factory-2022091400.zip && cd raven-factory-2022091400
11262 #+end_src
11263
11264* Installation Process
11265
11266** Enable Developer Debugging & OEM Unlock
11267
11268Before we can actually flash anything to the phone, we will need to enable OEM
11269(Original Equipment Manufacturer) Unlocking, as well as either USB (Universal
11270Serial Bus) Debugging or Wireless Debugging, depending on which method we will
11271be using.
11272
11273To start, enable developer mode by going to =Settings= > =About= and tapping
11274=Build Number= seven (7) times. You may need to enter your personal identified
11275number (PIN) to enable this mode.
11276
11277Once developer mode is enabled, go to =Settings= > =System= > =Devloper Options=
11278and enable OEM Unlocking, as well as USB or Wireless Debugging. In my case, I
11279chose USB Debugging and performed all actions via USB cable.
11280
11281Once these options are enabled, plug the phone into the computer and execute the
11282following command:
11283
11284#+begin_src sh
11285adb devices
11286#+end_src
11287
11288If an unauthorized error occurs, make sure the USB mode on the phone is changed
11289from charging to something like "File Transfer" or "PTP" (Picture Transfer
11290Protocol). You can find the USB mode in the notification tray.
11291
11292** Reboot Device
11293
11294Once we have found the device via =adb=, we can either boot into the bootloader
11295interface by holding the volume down button while the phone reboots or by
11296executing the following command:
11297
11298#+begin_src sh
11299adb reboot bootloader
11300#+end_src
11301
11302** Unlock the Bootloader
11303
11304The phone will reboot and load the bootloader screen upon startup. At this
11305point, we are ready to start the actual flashing of GrapheneOS onto the device.
11306
11307*NOTE*: In my situation, I needed to use =sudo= with every =fastboot= command,
11308but not with =adb= commands. I am not sure if this is standard or a Fedora
11309quirk, but I'm documenting my commands verbatim in this post.
11310
11311First, we start by unlocking the bootloader so that we can load other ROMs:
11312
11313#+begin_src sh
11314sudo fastboot flashing unlock
11315#+end_src
11316
11317** Flashing Factory Images
11318
11319Once the phone is unlocked, we can flash it with the =flash-all.sh= script found
11320inside the =raven-factory-2022091400= folder we entered earlier:
11321
11322#+begin_src sh
11323sudo ./flash-all.sh
11324#+end_src
11325
11326This process should take a few minutes and will print informational messages as
11327things progress. Avoid doing anything on the phone while this process is
11328operating.
11329
11330** Lock the Bootloader
11331
11332If everything was successful, the phone should reboot a few times and finally
11333land back on the bootloader screen. At this point, we can re-lock the bootloader
11334to enable full verified boot and protect the device from unwanted flashing or
11335erasure of data.
11336
11337#+begin_src sh
11338sudo fastboot flashing lock
11339#+end_src
11340
11341Once done, the device will be wiped and ready for a fresh set-up!
11342#+date: <2022-10-04 Tue 00:00:00>
11343#+title: How to Mount MTP Mobile Devices on Fedora Linux Using jmtpfs
11344#+description: Instructions for mounting and accessing Media Transfer Protocol (MTP) compatible mobile devices on Fedora Linux using jmtpfs for file transfer and management.
11345#+slug: mtp-linux
11346
11347I recently ran into trouble attempting to mount my GrapheneOS phone to my laptop
11348running Fedora Linux via the [[https://en.wikipedia.org/wiki/Media_transfer_protocol][Media Transfer Protocol]] (MTP) and discovered a
11349simple and effective solution.
11350
11351* Use a Universal Serial Bus (USB) 3.0 Port
11352
11353First, ensure that the device was plugged in to the laptop through a USB 3.0
11354port, if possible. From a brief glance online, it seems that USB 2.0 ports may
11355cause issues with dropped connections over MTP. This is purely anecdotal since I
11356don't have any evidence to link showing that USB 2.0 causes issues, but I can
11357confirm that switching to a USB 3.0 port seemed to cut out most of my issues.
11358
11359* Switch USB Preferences to MTP
11360
11361Secondly, you need to ensure that the phone's USB preferences/mode is changed to
11362MTP or File Transfer once the phone is plugged in. Other modes will not allow
11363you to access the phone's file system.
11364
11365* Install =jmtpfs=
11366
11367Next, I used the =jmtpfs= package to mount my phone to my laptop. There are
11368other packages that exist, but this one worked perfectly for me. On Fedora
11369Linux, you can install it like this:
11370
11371#+begin_src sh
11372sudo dnf install jmtpfs -y
11373#+end_src
11374
11375* Create a Mount Point
11376
11377Once you have the package installed, you just need to create a folder for the
11378device to use as a mount point. In my case, I used =/mnt/pixel=:
11379
11380#+begin_src sh
11381sudo mkdir /mnt/pixel
11382sudo chown -R $USER:$USER /mnt/pixel
11383#+end_src
11384
11385* Mount & Access the Phone's File System
11386
11387Finally, plug-in and mount the device, and you should be able to see all storage
11388(internal and external) inside your new folder!
11389
11390#+begin_src sh
11391jmtpfs /mnt/pixel
11392#+end_src
11393
11394The output should look something like this:
11395
11396#+begin_src sh
11397Device 0 (VID=18d1 and PID=4ee1) is a Google Inc Nexus/Pixel (MTP).
11398Android device detected, assigning default bug flags
11399#+end_src
11400
11401Now you are mounted and can do anything you'd like with the device's files:
11402
11403#+begin_src sh
11404cd /mnt/pixel
11405ls -lha
11406#+end_src
11407
11408From here, you will be able to see any internal or external storage available on
11409the device:
11410
11411#+begin_src sh
11412total 0
11413drwxr-xr-x. 3 user user 0 Jan 1 1970 .
11414drwxr-xr-x. 1 root root 10 Oct 4 13:29 ..
11415drwxr-xr-x. 16 user user 0 Apr 21 4426383 'Internal shared storage'
11416#+end_src
11417#+date: <2022-10-04 Tue 00:00:00>
11418#+title: Self-Hosting Guide: Syncthing
11419#+description: Technical instructions for setting up Syncthing as a peer-to-peer self-hosted file synchronization system that preserves data privacy by eliminating third-party intermediaries.
11420#+slug: syncthing
11421
11422* An Overview of Syncthing
11423
11424If you've been looking around the self-hosted cloud storage space for a while,
11425you've undoubtedly run into someone suggesting [[https://syncthing.net][Syncthing]] as an option. However,
11426it is an unusual alternative for those users out there who are used to having a
11427centralized cloud server that serves as the "controller" of the data and
11428interacts with clients on devices to fetch files.
11429
11430This post is a walkthrough of the Syncthing software, how I set up my personal
11431storage, and some pros and cons of using the software.
11432
11433* Installing Syncthing
11434
11435To install Syncthing, visit the [[https://syncthing.net/downloads/][Downloads]] page or install via your device's
11436package manager.
11437
11438** Server & Desktop
11439
11440You can install Syncthing on servers and desktops via the Downloads page linked
11441above or via the command-line.
11442
11443For Debian-based distros:
11444
11445#+begin_src sh
11446sudo apt install syncthing
11447#+end_src
11448
11449For Fedora-based distros:
11450
11451#+begin_src sh
11452sudo dnf install syncthing
11453#+end_src
11454
11455** Mobile
11456
11457Syncthing for Android is available on [[https://f-droid.org/packages/com.nutomic.syncthingandroid/][F-Droid]] and [[https://play.google.com/store/apps/details?id=com.nutomic.syncthingandroid][Google Play]]. Syncthing does
11458not have an official iOS client, but there is a third-party client called [[https://apps.apple.com/us/app/m%C3%B6bius-sync/id1539203216][Möbius
11459Sync]].
11460
11461* How Does Syncthing Work?
11462
11463To start, I wanted to include the main marketing blurb from their website:
11464
11465#+begin_quote
11466Syncthing is a continuous file synchronization program. It synchronizes
11467files between two or more computers in real time, safely protected from
11468prying eyes. Your data is your data alone, and you deserve to choose
11469where it is stored, whether it is shared with some third party, and how
11470it's transmitted over the internet.
11471#+end_quote
11472
11473Let's break this apart and add in some other details to help explain what
11474exactly Syncthing does in order to sync files between devices.
11475
11476** Local Syncthing Server(s)
11477
11478Syncthing syncs files between multiple devices by creating a local server on
11479each device. These local servers handle a few different things, such as watching
11480files and directories for changes, hosting an administrative GUI website, and
11481authenticating with connected devices.
11482
11483You can also start, stop, and restart the Syncthing server via the command-line
11484or web dashboard. If you're running Syncthing on a device with =systemd=, you
11485can use the following commands:
11486
11487#+begin_src sh
11488sudo systemctl start syncthing@username.service
11489sudo systemctl restart syncthing@username.service
11490sudo systemctl stop syncthing@username.service
11491#+end_src
11492
11493** Syncthing Dashboard
11494
11495This biggest part of Syncthing is the admin GUI website that runs on each device
11496(note that mobile devices will use the Syncthing app rather than the web GUI).
11497The administrative graphical user interface (GUI) is available through the web
11498browser on the local device that is running Syncthing - simply go to
11499=http://localhost:8384= or =http://127.0.0.1:8384=. This web page is the place
11500where you will change settings, add/modify synced files, and add/modify
11501connected devices.
11502
11503Here's an example web GUI dashboard:
11504
11505#+caption: Syncthing Dashboard
11506#+attr_html: :alt Syncthing web page on macOS, showing folders, device info, and remote devices.
11507[[https://img.cleberg.net/blog/20221020-syncthing/syncthing_gui.webp]]
11508
11509** Remote Devices
11510
11511A cloud storage solution wouldn't be very useful if you aren't able to share
11512data among various devices. Syncthing does this by sharing Device IDs to connect
11513servers, and then by manually sharing Folders with devices that have been
11514connected.
11515
11516For instance, if you have a laptop running Syncthing and then install the
11517Syncthing mobile app on a phone, you could scan the laptop's QR (quick-response)
11518code for Device ID and then accept the authentication on the laptop's dashboard.
11519Next, you can use either device to select a folder for sharing and dictating
11520which device should send, receive, or both.
11521
11522When you connect devices, you can set one device as an "Introducer," which can
11523add devices from the introducer to the device list, for mutually shared folders.
11524You can also configure Auto Accept, compression, rate limits, and more settings
11525per device.
11526
11527* My Personal Cloud Storage Set-up
11528
11529Personally, I use a model similar to a traditional cloud storage service. I have
11530a "centralized" server running 24/7 that acts as an Introducer for my Syncthing
11531network. I think of this as my main storage and all other devices as tertiary
11532client devices. I will likely add additional servers as backups as time goes on
11533so that I don't have to rely on my laptop or phone as the only backups.
11534
11535Currently, I have one desktop and one mobile device connected to the network,
11536both running intermittently as they are not powered-on 24/7.
11537
11538The initial set-up of the software was easy enough, but data transfer rates were
11539incredibly slow for me due to the Wi-Fi. Instead, I plugged my laptop into the
11540ethernet network that my server is on and manually copied my folders over to the
11541server with =scp=. Once complete, Syncthing validated that all files were there
11542and not missing, and it did not need to transfer any data through the WAN.
11543
11544As slow as the transfer was going, this probably saved me a few days of waiting
11545for my ~100 gigabytes to sync.
11546
11547* Pros & Cons
11548
11549I've put together a short list of pros and cons for Syncthing. I thought about
11550my experiences with Nextcloud, WebDAV, proprietary services (Google Drive,
11551iCloud, etc.), and privacy-focused cloud solutions (pCloud, Tresorit, etc.).
11552
11553*Pros:*
11554
11555- I've faced no data loss at all through my two-month trial run.
11556- No third-parties store your data on their servers.
11557- You have full control over your data and can take your data and leave at any
11558 time.
11559- It's possible to encrypt client-side easily with software like Cryptomator.
11560- No proprietary clients or mounted volumes, just plain files and folders.
11561
11562*Cons:*
11563
11564- The learning curve is steeper than traditional cloud services and is focused
11565 on a technical audience.
11566- If a device needs to modify files in a Folder, the devices will need to sync
11567 ALL files from the folder, which may be large. To avoid size restraints, split
11568 large folders into smaller folders for syncing.
11569- Syncing can be slow due to the clients/servers initially connecting or
11570 re-connecting after sleeping.
11571- Multiple personal devices are required and require the user to own or rent
11572 them as no third-party servers are involved in the storage of data.
11573
11574Overall, I've had a great experience with Syncthing so far. I've had no data
11575loss, syncing has been quick and easy when changes are made to files, device
11576connections are reliable, and I love the freedom of controlling the clients and
11577servers as I choose.
11578
11579Not to mention that I appreciate that I - or someone else - could pull the
11580Syncthing [[https://github.com/syncthing][source code]] and continue development/support if the Syncthing
11581Foundation decides to stop developing the software or sells the business.
11582#+date: <2022-10-22 Sat 00:00:00>
11583#+title: Alpine Linux Essentials: Installing and Setting Up a Secure Minimal Server
11584#+description: Detailed procedure for deploying Alpine Linux to achieve a secure, lightweight server environment optimized for web hosting and containerization.
11585#+slug: alpine-linux
11586
11587* Alpine Linux
11588
11589[[https://alpinelinux.org][Alpine Linux]] is a very small distribution, built on musl libc and busybox. It
11590uses ash as the default shell, OpenRC as the init system, and apk as the package
11591manager. According to their website, an Alpine container "requires no more than
115928 megabytes (MB) and a minimal installation to disk requires around 130 MB of
11593storage." An actual bare metal machine is recommended to have 100 MB of RAM and
115940-700 MB of storage space.
11595
11596Historically, I've used Ubuntu's minimal installation image as my server OS for
11597the last five years. Ubuntu worked well and helped as my original server
11598contained an nVidia GPU (graphics processing unit) and no onboard graphics, so
11599quite a few distributions won't boot or install without a lot of tinkering.
11600
11601Alpine has given me a huge increase in performance across my Docker apps and
11602Nginx websites. CPU (central processing unit) load for the new server I'm using
11603to test Alpine hovers around 0-5% on average with an Intel(R) Core(TM) i3-6100
11604CPU @ 3.70 Gigahertz (GHz).
11605
11606The only services I haven't moved over to Alpine are Plex Media Server and
11607Syncthing, which may increase CPU load quite a bit depending on how many streams
11608are running.
11609
11610** Installation
11611
11612In terms of installation, Alpine has an incredibly useful [[https://wiki.alpinelinux.org/wiki/Installation][wiki]] that will guide a
11613user throughout the installation and post-installation processes, as well as
11614various other articles and guides.
11615
11616To install Alpine, find an appropriate [[https://alpinelinux.org/downloads/][image to download]] and flash it to a USB
11617(Universal Serial Bus) using software such as Rufus or Etcher. I opted to use
11618the Standard image for my x86_{64} architecture.
11619
11620Once the USB is ready, plug it into the machine and reboot. Note that you may
11621have to use a key such as =Esc= or =F1-12= to access the boot menu. The Alpine
11622Linux terminal will load quickly and for a login.
11623
11624To log in to the installation image, use the =root= account; there is no
11625password. Once logged-in, execute the setup command:
11626
11627#+begin_src sh
11628setup-alpine
11629#+end_src
11630
11631The setup script will ask a series of questions to configure the system. Be sure
11632to answer carefully or else you may have to re-configure the system after boot.
11633
11634- *Keyboard Layout*: Local keyboard language and usage mode, e.g., us and variant
11635 of us-nodeadkeys.
11636- *Hostname*: The name for the computer.
11637- *Network*: For example, automatic internet protocol (IP) address discovery
11638 with the Dynamic Host Configuration Protocol (DHCP) protocol.
11639- *DNS (Domain Name System)*: DNS servers to query. For privacy reasons, it is
11640 NOT recommended to route every local request to servers like Google's
11641 8.8.8.8.
11642- *Timezone
11643- *Proxy*: Proxy server to use for accessing the web. Use =none= for direct
11644 connections to the internet.
11645- *Mirror*: From where to download packages. Choose the organization you trust
11646 giving your usage patterns to.
11647- *SSH (Secure Shell Protocol)*: Remote access server. =Openssh= is part of the
11648 default install image. Use =none= to disable remote login, e.g. on laptops.
11649- *NTP (Network Time Protocol)*: Client used for keeping the system clock in
11650 sync with a time-server. Package =chrony= is part of the default install
11651 image.
11652- *Disk Mode*: Select between diskless (=none=), =data= or =sys=, as described
11653 above.
11654
11655Once the setup script is finished, be sure to reboot the machine and remove the
11656USB device.
11657
11658#+begin_src sh
11659reboot
11660#+end_src
11661
11662** Post-Installation
11663
11664There are many things you can do once your Alpine Linux system is up and
11665running, and it largely depends on what you'll use the machine for. I'm going to
11666walk through my personal post-installation setup for my web server.
11667
116681. Upgrade the System
11669
11670 First, login as =root= in order to update and upgrade the system:
11671
11672 #+begin_src sh
11673 apk -U upgrade
11674 #+end_src
11675
116762. Adding a User
11677
11678 I needed to add a user so that I don't need to log in as root. Note that if
11679 you're used to using the =sudo= command, you will now need to use the =doas=
11680 command on Alpine Linux.
11681
11682 #+begin_src sh
11683 apk add doas
11684 adduser <username>
11685 adduser <username> wheel
11686 #+end_src
11687
11688 You can now log out and log back in using the newly-created user:
11689
11690 #+begin_src sh
11691 exit
11692 #+end_src
11693
116943. Enable Community Packages
11695
11696 In order to install more common packages that aren't found in the =main=
11697 repository, you will need to enable the =community= repository:
11698
11699 #+begin_src sh
11700 doas nano /etc/apk/repositories
11701 #+end_src
11702
11703 Uncomment the community line for whichever version of Alpine you're running:
11704
11705 #+begin_src sh
11706 /media/usb/apks
11707 http://dl-cdn.alpinelinux.org/alpine/v3.16/main
11708 http://dl-cdn.alpinelinux.org/alpine/v3.16/community
11709 #http://dl-cdn.alpinelinux.org/alpine/edge/main
11710 #http://dl-cdn.alpinelinux.org/alpine/edge/community
11711 #http://dl-cdn.alpinelinux.org/alpine/edge/testing
11712 #+end_src
11713
117144. Install Required Packages
11715
11716 Now that the community packages are available, you can install any packages
11717 you need. In my case, I installed the web server packages I need for my
11718 services:
11719
11720 #+begin_src sh
11721 doas apk add nano nginx docker docker-compose ufw
11722 #+end_src
11723
117245. SSH
11725
11726 If you didn't install OpenSSH as part of the installation, you can do so now:
11727
11728 #+begin_src sh
11729 doas apk add openssh
11730 #+end_src
11731
11732 Next, either create a new key or copy your SSH key to the server from your
11733 current machines:
11734
11735 #+begin_src sh
11736 # Create a new key
11737 ssh-keygen
11738 #+end_src
11739
11740 If you need to copy an existing SSH key from a current machine:
11741
11742 #+begin_src sh
11743 # Copy key from existing machines
11744 ssh-copy-id <username>@<ip_address>
11745 #+end_src
11746
117476. Firewall
11748
11749 Lastly, I installed =ufw= above as my firewall. To set up, default to deny
11750 incoming and allow outgoing connections. Then selectively allow other ports
11751 or apps as needed.
11752
11753 #+begin_src sh
11754 doas ufw default deny incoming
11755 doas ufw default allow outgoing
11756 doas ufw allow SSH
11757 doas ufw allow "WWW Full"
11758 doas ufw allow 9418 # Git server port
11759 #+end_src
11760
117617. Change Hostname
11762
11763 If you don't like the hostname set during installation, you just need to edit
11764 two files. First, edit the simple hostname file:
11765
11766 #+begin_src sh
11767 doas nano /etc/hostname
11768 #+end_src
11769
11770 #+begin_src sh
11771 <hostname>
11772 #+end_src
11773
11774 Next, edit the =hosts= file:
11775
11776 #+begin_src sh
11777 doas nano /etc/hosts
11778 #+end_src
11779
11780 #+begin_src sh
11781 127.0.0.1 <hostname>.local <hostname> localhost.local localhost
11782 ::1 <hostname> <hostname>.local
11783 #+end_src
11784
11785* Nginx Web Server
11786
11787To set up my web server, I simply created the =www= user and created the
11788necessary files.
11789
11790#+begin_src sh
11791doas adduser -D -g 'www' www
11792mkdir /www
11793doas mkdir /www
11794doas chown -R www:www /var/lib/nginx/
11795doas chown -R www:www /www
11796#+end_src
11797
11798If you're running a simple webroot, you can alter the main =nginx.conf= file.
11799Otherwise, you can drop configuration files in the following directory. You
11800don't need to enable or symlink the configuration file like you do in other
11801systems.
11802
11803#+begin_src sh
11804doas nano /etc/nginx/http.d/example_website.conf
11805#+end_src
11806
11807Once the configuration is set and pointed at the =/www= directory to serve
11808files, enable the Nginx service:
11809
11810#+begin_src sh
11811# Note that 'default' must be included or Nginx will not start on boot
11812doas rc-update add nginx default
11813#+end_src
11814
11815* Docker Containers
11816
11817Docker works exactly the same as other systems. Either execute a =docker run=
11818command or create a =docker-compose.yml= file and do =docker-compose up -d=.
11819
11820* Git Server
11821
11822I went in-depth on how to self-host a git server in another post: [[https://cleberg.net/blog/git-server.html][Self-Hosting
11823Guide: Git & cGit]].
11824
11825However, there are a few differences with Alpine. First note that in order to
11826change the =git= user's shell, you must do a few things a little different:
11827
11828#+begin_src sh
11829doas apk add libuser
11830doas touch /etc/login.defs
11831doas mkdir /etc/default
11832doas touch /etc/default/useradd
11833doas lchsh git
11834#+end_src
11835
11836* Thoughts on Alpine
11837
11838So far, I love Alpine Linux. I have no complaints about anything at this point,
11839but I'm not completely finished with the migration yet. Once I'm able to upgrade
11840my hardware to a rack-mounted server, I will migrate Plex and Syncthing over to
11841Alpine as well - possibly putting Plex into a container or VM.
11842
11843The performance is stellar, the =apk= package manager is seamless, and system
11844administration tasks are effortless. My only regret is that I didn't install
11845Alpine sooner.
11846#+date: <2022-10-30 Sun 00:00:00>
11847#+title: Managing Display Managers in Void Linux: Disable, Enable, and Configure
11848#+description: Stepwise instructions to manage and replace the display manager on a Void Linux installation, including service control and configuration editing.
11849#+slug: linux-display-manager
11850
11851* Display Manager Services
11852
11853In order to change the [[https://en.wikipedia.org/wiki/Display_manager][display manager]] on Void Linux - or any other Linux
11854distribution - you need to identify the currently enabled display manager.
11855
11856** Disabling the Current Display Manager
11857
11858Void Linux only has one optical disc image (ISO) available for download with a
11859pre-built display manager at the time of this post: the XFCE ISO. If you've
11860installed this version, the pre-assigned display manager is =lxdm=. If you
11861installed another display manager, replace =lxdm= in the following command with
11862the display manager you have installed.
11863
11864To disable =lxdm=, simply remove the service symlink:
11865
11866#+begin_src sh
11867sudo rm /var/service/lxdm
11868#+end_src
11869
11870** Enabling a New Display Manager
11871
11872If you want to enable a new display manager, you can do so after =lxdm= is
11873disabled. Make sure to replace =<new_display_manager>= with your new DM, such as
11874=gdm=, =xdm=, etc.
11875
11876#+begin_src sh
11877sudo ln -s /etc/sv/<new_display_manager> /var/service
11878#+end_src
11879
11880* Set Up =.xinitrc=
11881
11882Depending on your setup, you may need to create a few X files, such as
11883=~/.xinitrc=. For my personal set-up, I created this file to launch the i3wm as
11884my desktop.
11885
11886#+begin_src sh
11887nano ~/.xinitrc
11888#+end_src
11889
11890#+begin_src sh
11891#!/bin/sh
11892
11893exec i3
11894#+end_src
11895
11896If you run a desktop other than i3, simply replace =i3= with the shell command
11897that launches that desktop.
11898
11899* Set Up Your Shell Profile
11900
11901Finally, in order to automatically launch an X session upon login, you will need
11902to edit the =.bash_profile= (bash) or =.zprofile= (zsh) files for your shell:
11903
11904#+begin_src sh
11905nano ~/.zprofile
11906#+end_src
11907
11908Add the following snippet to the end of the shell profile file. This will
11909execute the =startx= command upon login.
11910
11911#+begin_src sh
11912if [ -z "${DISPLAY}" ] && [ "${XDG_VTNR}" -eq 1 ]; then
11913 exec startx
11914fi
11915#+end_src
11916
11917Alternatively, you can ignore this step and simply choose to manually execute
11918=startx= upon login. This can be useful if you have issues with your desktop or
11919like to manually launch different desktops by choice.
11920#+date: <2022-11-07 Mon 00:00:00>
11921#+title: Self-Hosting Guide: Matrix Synapse
11922#+description: Systematic guide for deploying Matrix Synapse on Alpine Linux, covering dependency installation, reverse proxy setup, TLS certificate application, and user account management.
11923#+slug: self-hosting-matrix
11924
11925* Synapse
11926
11927If you're reading this, you likely know that [[https://github.com/matrix-org/synapse/][Synapse]] is a popular [[https://matrix.org/][Matrix]] home
11928server software that allows users to run their own Matrix home server.
11929
11930This post is a short guide describing how I was able to get Synapse working in a
11931minimally-usable state on Alpine Linux.
11932
11933* Installation Process
11934
11935** Dependencies
11936
11937First, since there is no Alpine-specific package for Synapse, we need to ensure
11938that Alpine has the required dependencies for the Python-based installation
11939method.
11940
11941#+begin_src sh
11942doas apk -U update
11943doas apk add python3 py3-virtualenv
11944#+end_src
11945
11946Next, we need to set up a Python virtual environment for Synapse:
11947
11948#+begin_src sh
11949mkdir -p ~/synapse && cd ~/synapse
11950virtualenv -p python3 ~/synapse/env
11951source ~/synapse/env/bin/activate
11952pip install --upgrade pip
11953pip install --upgrade setuptools
11954pip install matrix-synapse
11955#+end_src
11956
11957** Running Synapse
11958
11959Once installed, running Synapse is easy. Simply execute the following command,
11960replacing =example.com= with the domain name that will be used with this home
11961server. This will generate the configuration files needed to run the server.
11962
11963#+begin_src sh
11964python -m synapse.app.homeserver \
11965 --server-name example.com \
11966 --config-path homeserver.yaml \
11967 --generate-config \
11968 --report-stats=no
11969#+end_src
11970
11971Once the configuration is generated, we can start up the Synapse server:
11972
11973#+begin_src sh
11974synctl start
11975#+end_src
11976
11977** Configuring Synapse
11978
11979To make any change to Synapse, we need to edit the =YAML= configuration file:
11980
11981#+begin_src sh
11982nano ~/synapse/homeserver.yaml
11983#+end_src
11984
11985For now, we just need to ensure the =server_name= is accurate. However, there
11986are a lot of other configuration options found in the [[https://matrix-org.github.io/synapse/develop/usage/configuration/config_documentation.html][Configuring Synapse]]
11987documentation that can be enabled/disabled at any point.
11988
11989#+begin_src yaml
11990server_name: "example.com"
11991#+end_src
11992
11993Make sure to restart Synapse when you make changes to the configuration:
11994
11995#+begin_src sh
11996synctl restart
11997#+end_src
11998
11999** Nginx Reverse-Proxy
12000
12001To ensure that Synapse is reachable from the public, we need to connect our
12002domain to the Synapse server. In my case, I use a Nginx reverse-proxy for this
12003purpose.
12004
12005To use Nginx, we need to create a reverse-proxy configuration file:
12006
12007#+begin_src sh
12008doas nano /etc/nginx/http.d/example.com.conf
12009#+end_src
12010
12011If you already have TLS (Transport Layer Security) certificates for this domain
12012(=example.com=), you can simply use the SSL (Secure Socket Layer) configuration
12013and point toward your TLS certificates.
12014
12015#+begin_src conf
12016server {
12017 listen 443 ssl http2;
12018 listen [::]:443 ssl http2;
12019
12020 # For the federation port
12021 listen 8448 ssl http2;
12022 listen [::]:8448 ssl http2;
12023
12024 server_name example.com;
12025
12026 location ~ ^(/_matrix|/_synapse/client) {
12027 # note: do not add a path (even a single /) after the port in `proxy_pass`,
12028 # otherwise nginx will canonicalise the URI and cause signature verification
12029 # errors.
12030 proxy_pass http://localhost:8008;
12031 proxy_set_header X-Forwarded-For $remote_addr;
12032 proxy_set_header X-Forwarded-Proto $scheme;
12033 proxy_set_header Host $host;
12034
12035 # Nginx by default only allows file uploads up to 1M in size
12036 # Increase client_max_body_size to match max_upload_size defined in homeserver.yaml
12037 client_max_body_size 50M;
12038 }
12039
12040 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
12041 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
12042 access_log /var/log/nginx/matrix.access.log;
12043}
12044
12045server {
12046 if ($host = example.com) {
12047 return 301 https://$host$request_uri;
12048 }
12049
12050 server_name example.com;
12051 listen 80;
12052 return 404;
12053}
12054#+end_src
12055
12056If you need to generate TLS certificates (I recommend [[https://certbot.eff.org/][Certbot]]), you'll need a
12057more minimal Nginx conf file before you can use the TLS-enabled example above.
12058Instead, use this configuration file during the Certbot certificate generation
12059process:
12060
12061#+begin_src conf
12062server {
12063 server_name example.com;
12064 location / {
12065 try_files $uri $uri/ =404;
12066 }
12067 listen 80;
12068}
12069#+end_src
12070
12071Once you're done editing the Nginx conf file, restart Nginx:
12072
12073#+begin_src sh
12074doas rc-service nginx restart
12075#+end_src
12076
12077If you still need to generate TLS certificates, run =certbot= now and obtain the
12078certificates. Certbot will ask if you want to use a webroot or spin up a
12079temporary web server. I *highly* recommend using the temporary web server due to
12080the many issues with using a webroot.
12081
12082You will need to stop Nginx in order to user the temporary web server option
12083with Certbot:
12084
12085#+begin_src sh
12086# Stop Nginx so certbot can spin up a temp webserver for cert generation
12087doas rc-service nginx stop
12088doas certbot certonly -v
12089doas rc-service nginx start
12090#+end_src
12091
12092** Open Firewall & Router Ports
12093
12094If you use a firewall on the server, open the =8448= port for discovery and
12095federation, as well as the normal web server ports if you're using a reverse
12096proxy. If you want additional services, such as voice calls, you will need to
12097read the Synapse documentation to see which ports need to be opened for those
12098features.
12099
12100Here's an example of the Universal Firewall (UFW) software:
12101
12102#+begin_src sh
12103# Matrix port
12104doas ufw allow 8448
12105# Standard web server ports
12106doas ufw allow "Nginx Full"
12107#+end_src
12108
12109Remember to forward any Synapse ports, such as =8448=, =80=, and =443=, in your
12110Router from the internet to your server's internet protocol (IP) address.
12111
12112** Adding Matrix Users
12113
12114Finally, if you didn't enable public registration in the =homeserver.yaml= file,
12115you can manually create users via the command-line:
12116
12117#+begin_src sh
12118cd ~/synapse
12119register_new_matrix_user -c homeserver.yaml
12120#+end_src
12121
12122Remember that the format for federated Matrix usernames is
12123=@username:example.com= when logging in to client applications.
12124
12125Once Synapse is running, and you have a username, you are ready to log in to a
12126Matrix client and start sending messages, joining rooms, and utilizing your very
12127own Matrix server.
12128#+date: <2022-11-11 Fri 00:00:00>
12129#+title: Troubleshooting and Fixing Nginx Permission Denied Errors on /var/lib/nginx
12130#+description: Step-by-step remediation for addressing permission denied errors encountered by Nginx in the /var/lib/nginx directory to restore temporary file caching functionality.
12131#+slug: nginx-tmp-errors
12132
12133/This is a brief post so that I personally remember the solution as it
12134has occurred multiple times for me./
12135
12136* The Problem
12137
12138After migrating to a new server operating system (OS), I started receiving quite
12139a few permission errors like the one below. These popped up for various
12140different websites I'm serving via Nginx on this server, but did not prevent the
12141website from loading.
12142
12143I found the errors in the standard log file:
12144
12145#+begin_src sh
12146cat /var/log/nginx/error.log
12147#+end_src
12148
12149#+begin_src sh
121502022/11/11 11:30:34 [crit] 8970#8970: *10 open() "/var/lib/nginx/tmp/proxy/3/00/0000000003" failed (13: Permission denied) while reading upstream, client: 169.150.203.10, server: cyberchef.example.com, request: "GET /assets/main.css HTTP/2.0", upstream: "http://127.0.0.1:8111/assets/main.css", host: "cyberchef.example.com", referrer: "https://cyberchef.example.com/"
12151#+end_src
12152
12153You can see that the error is =13: Permission denied= and it occurs in the
12154=/var/lib/nginx/tmp/= directory. In my case, I had thousands of errors where
12155Nginx was denied permission to read/write files in this directory.
12156
12157So how do I fix it?
12158
12159* The Solution
12160
12161In order to resolve the issue, I had to ensure the =/var/lib/nginx= directory is
12162owned by Nginx. Mine was owned by the =www= user and Nginx was not able to read
12163or write files within that directory. This prevented Nginx from caching
12164temporary files.
12165
12166#+begin_src sh
12167# Alpine Linux
12168doas chown -R nginx:nginx /var/lib/nginx
12169
12170# Other Distros
12171sudo chown -R nginx:nginx /var/lib/nginx
12172#+end_src
12173
12174You /may/ also be able to change the =proxy_temp_path= in your Nginx config, but
12175I did not try this. Here's a suggestion I found online that may work if the
12176above solution does not:
12177
12178#+begin_src sh
12179nano /etc/nginx/http.d/example.com.conf
12180#+end_src
12181
12182#+begin_src conf
12183server {
12184 ...
12185
12186 # Set the proxy_temp_path to your preference, make sure it's owned by the
12187 # `nginx` user
12188 proxy_temp_path /tmp;
12189
12190 ...
12191}
12192#+end_src
12193
12194Finally, restart Nginx and your server should be able to cache temporary files
12195again.
12196
12197#+begin_src sh
12198# Alpine Linux (OpenRC)
12199doas rc-service nginx restart
12200
12201# Other Distros (systemd)
12202sudo systemctl restart nginx
12203#+end_src
12204#+date: <2022-11-27 Sun 00:00:00>
12205#+title: Building a High-Performance Rack-Mounted Server with Consumer PC Components
12206#+description: Detailed instructions on selecting components, assembling, and configuring a rack-mounted server suitable for advanced computing tasks in a professional or laboratory setting.
12207#+slug: server-build
12208
12209* The Dilemma
12210
12211For years, I have been using desktops and a Raspberry Pi as the backbone of my
12212homelab. I have always wanted to move toward a single dedicated server that
12213could handle all of my tasks, but was often put off by the complexity of the
12214choices (and financial roadblocks at some times).
12215
12216However, after purchasing a small server rack this past year, I have been
12217researching to see what kind of rack-mounted server I could buy. I initially
12218bought a Dell R720XD loaded up with everything I could ever want in a server -
12219but I did not care for it. It was far too loud, and the age of the CPU/RAM was
12220something I wanted to improve upon.
12221
12222After returning the R720XD, I decided that I wanted to build my own server with
12223modern, consumer-grade PC components. This time, I am very happy with the
12224results of my server.
12225
12226* Components
12227
12228I'll start by listing all the components I used for this server build:
12229
12230- *Case*: [[https://www.rosewill.com/rosewill-rsv-r4100u-black/p/9SIA072GJ92825][Rosewill RSV-R4100U 4U Server Chassis Rackmount Case]]
12231- *Motherboard*: [[https://nzxt.com/product/n7-b550][NZXT B550]]
12232- *CPU*: AMD Ryzen 7 5700G with Radeon Graphics
12233- *GPU*: N/A - I specifically chose one of the few AMD CPUs that support onboard
12234 graphics.
12235- *RAM*: 64GB RAM (2x32GB) /Max of 128GB RAM on this motherboard/
12236- *Boot Drive*: Western Digital 500GB M.2 NVME SSD
12237- *HDD Bay*:
12238 - 10TB WD White /(shucked, moved from previous server)/
12239 - 8TB WD White /(shucked, moved from previous server)/
12240 - 2 x 8TB WD Red Plus /(Black Friday lined up perfectly with this build, so I
12241 grabbed two of these)/
12242- *PSU*: Corsair RM850 PSU
12243- *Extras*:
12244 - Corsair TM3Q Thermal Paste
12245 - Noctua 120mm fan /(replacement for front case fan)/
12246 - 2 x Noctua 80mm fans /(replacement for rear case fans)/
12247 - CableMatters 6Gbps SATA Cables
12248
12249* Building the Server
12250
12251This took quite a while for me to build (in my opinion of time), totaling around
122523 hours from start to finish. The case has some peculiar construction, so you
12253have to completely remove the ODD & HDD cages to install the motherboard and
12254other components first.
12255
12256Now, I've never built a computer of any kind before, so I was quite nervous.
12257Personally, the only challenging part was getting the CPU cooler to screw into
12258the motherboard without sliding the thermal paste around too much underneath.
12259I'm still not entirely sure if I did a great job of it, but nothing's broken
12260yet.
12261
12262The main components were all fine and simple. However, installing the hard
12263drives is slightly tedious as I need to power off the server and completely
12264unscrew the HDD cage to install or remove any drives. Additionally, the drives
12265are screwed directly into the metal cage with small screws, which are quite a
12266bit different from the HDD trays I'm used to in other machines.
12267
12268Seeing that the cases with hot-swap bays were 3-4x the price, I'm okay dealing
12269with the tedium of removing the cage to install new drives.
12270
12271* Software
12272
12273I'm not going to dive into the software as I have done so in other recent posts.
12274However, I wanted to note that I am using Alpine Linux on this server and
12275hosting most services inside Docker. No virtual machines (VMs) and very few
12276bare-metal services.
12277
12278* The Results
12279
12280How did my build turn out? Well, after migrating my other servers and their
12281services over, I found that my server is blazing fast. The heaviest of my
12282applications, Plex, is handled with ease. Even 4k streaming seems to be
12283effortless.
12284
12285I am very happy with the results and will likely continue to improve on this
12286server as the years go by rather than buying another used server online.
12287
12288** Mistakes I Made
12289
12290This post wouldn't be complete unless I wrote about the mistakes I made while
12291building. The only real mistake I made beyond a "whoops I dropped a screw"
12292related to airflow and fan direction.
12293
12294While installing the two new hard drives that showed up on 2022-11-30 and
12295getting ready to install the case in my rack, I noticed that the hard drive
12296temperatures were quite high.
12297
12298I used the =smartctl= command for each of my drives (=/dev/sda= through
12299=/dev/sdd=):
12300
12301#+begin_src sh
12302doas smartctl -a /dev/sda | grep Temperature_Celsius
12303#+end_src
12304
12305The results were unusual - all four drives were idling at ~44-46 degrees
12306Celsius. The only drive that was cooler was my 10TB drive, which was at 38
12307degrees Celsius. I noted that this 10TB drive was also closest to the case fan.
12308
12309#+begin_src sh
12310ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE
12311# /dev/sda
12312194 Temperature_Celsius 0x0002 147 147 000 Old_age Always - 44 (Min/Max 22/46)
12313# /dev/sdb
12314194 Temperature_Celsius 0x0002 141 141 000 Old_age Always - 46 (Min/Max 21/48)
12315# /dev/sdc
12316194 Temperature_Celsius 0x0002 144 144 000 Old_age Always - 45 (Min/Max 19/61)
12317# /dev/sdd
12318194 Temperature_Celsius 0x0002 171 171 000 Old_age Always - 38 (Min/Max 14/56)
12319#+end_src
12320
12321After looking to see if I could fit more fans into the case, I noticed that the
12322120mm fan used for intake from the front of the case was actually pushing air
12323out of the case by mistake. This fan sits right in front of the hard drive bay.
12324
12325Once I flipped the fan around to act as an intake fan, the temperatures dropped
12326immediately! They are now idling at ~31-33 degrees Celsius. A single fan
12327spinning the wrong way caused my drives to idle 10-15 degrees higher than they
12328should have.
12329
12330#+begin_src sh
12331ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE
12332# /dev/sda
12333194 Temperature_Celsius 0x0002 209 209 000 Old_age Always - 31 (Min/Max 14/56)
12334# /dev/sdb
12335194 Temperature_Celsius 0x0002 196 196 000 Old_age Always - 33 (Min/Max 19/61)
12336# /dev/sdc
12337194 Temperature_Celsius 0x0002 203 203 000 Old_age Always - 32 (Min/Max 21/48)
12338# /dev/sdd
12339194 Temperature_Celsius 0x0002 196 196 000 Old_age Always - 33 (Min/Max 22/46)
12340#+end_src
12341
12342This was a silly error to make, but I'm glad I found it today before I screwed
12343the case into the rack and made things a lot more tedious to fix.
12344#+date: <2022-11-29 Tue 00:00:00>
12345#+title: How to Block Unwanted HTTP Referrers in Nginx Using a Ban List
12346#+description: Technical guide to implementing a referrer ban list in Nginx configurations to prevent access from undesired domains and improve web server security.
12347#+slug: nginx-referrer-ban-list
12348
12349* Creating the Ban List
12350
12351In order to ban list referral domains or websites with Nginx, you need to create
12352a ban list file. The file below will accept regexes for different domains or
12353websites you wish to block.
12354
12355First, create the file in your nginx directory:
12356
12357#+begin_src sh
12358doas nano /etc/nginx/banlist.conf
12359#+end_src
12360
12361Next, paste the following contents in and fill out the regexes with whichever
12362domains you're blocking.
12363
12364#+begin_src conf
12365# /etc/nginx/banlist.conf
12366
12367map $http_referer $bad_referer {
12368 hostnames;
12369
12370 default 0;
12371
12372 # Put regexes for undesired referrers here
12373 "~news.ycombinator.com" 1;
12374}
12375#+end_src
12376
12377* Configuring Nginx
12378
12379In order for the ban list to work, Nginx needs to know it exists and how to
12380handle it. For this, edit the =nginx.conf= file.
12381
12382#+begin_src sh
12383doas nano /etc/nginx/nginx.conf
12384#+end_src
12385
12386Within this file, find the =http= block and add your ban list file location to
12387the end of the block.
12388
12389#+begin_src conf
12390# /etc/nginx/nginx.conf
12391
12392http {
12393 ...
12394
12395 # Include ban list
12396 include /etc/nginx/banlist.conf;
12397}
12398#+end_src
12399
12400* Enabling the Ban List
12401
12402Finally, we need to take action when a bad referral site is found. To do so,
12403edit the configuration file for your website. For example, I have all website
12404configuration files in the =http.d= directory. You may have them in the
12405=sites-available= directory on some distributions.
12406
12407#+begin_src sh
12408doas nano /etc/nginx/http.d/example.com.conf
12409#+end_src
12410
12411Within each website's configuration file, edit the =server= blocks that are
12412listening to ports 80 and 443 and create a check for the =$bad_referrer=
12413variable we created in the ban list file.
12414
12415If a matching site is found, you can return any [[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes][HTTP Status Code]] you want. Code
12416403 (Forbidden) is logical in this case since you are preventing a client
12417connection due to a banned domain.
12418
12419#+begin_src conf
12420server {
12421 ...
12422
12423 # If a referral site is banned, return an error
12424 if ($bad_referer) {
12425 return 403;
12426 }
12427
12428 ...
12429}
12430#+end_src
12431
12432* Restart Nginx
12433
12434Lastly, restart Nginx to enable all changes made.
12435
12436#+begin_src sh
12437doas rc-service nginx restart
12438#+end_src
12439
12440* Testing Results
12441
12442In order to test the results, let's curl the contents of our site. To start,
12443I'll curl the site normally:
12444
12445#+begin_src sh
12446curl https://cleberg.net
12447#+end_src
12448
12449The HTML contents of the page come back successfully:
12450
12451#+begin_src html
12452<!doctype html>...</html>
12453#+end_src
12454
12455Next, let's include a banned referrer:
12456
12457#+begin_src sh
12458curl --referer https://news.ycombinator.com https://cleberg.net
12459#+end_src
12460
12461This time, I'm met with a 403 Forbidden response page. That means we are
12462successful and any clients being referred from a banned domain will be met with
12463this same response code.
12464
12465#+begin_src html
12466<html>
12467 <head>
12468 <title>403 Forbidden</title>
12469 </head>
12470 <body>
12471 <center><h1>403 Forbidden</h1></center>
12472 <hr />
12473 <center>nginx</center>
12474 </body>
12475</html>
12476#+end_src
12477#+date: <2022-12-01 Thu 00:00:00>
12478#+title: Optimizing Nginx with GZIP: A Step-by-Step Guide to Text Compression
12479#+description: Procedures for activating and configuring GZIP compression in Nginx to decrease data transmission size, reduce bandwidth consumption, and accelerate page load times.
12480#+slug: nginx-compression
12481
12482* Text Compression
12483
12484Text compression allows a web server to serve text-based resources faster than
12485uncompressed data. This can speed up things like First Contentful Paint, Tie to
12486Interactive, and Speed Index.
12487
12488* Enable Nginx Compression with gzip
12489
12490In order to enable text compression on Nginx, we need to enable it within the
12491configuration file:
12492
12493#+begin_src sh
12494nano /etc/nginx/nginx.conf
12495#+end_src
12496
12497Within the =http= block, find the section that shows something like the block
12498below. This is the default gzip configuration I found in my =nginx.conf= file on
12499Alpine Linux 3.17. Yours may look slightly different, just make sure that you're
12500not creating any duplicate gzip options.
12501
12502#+begin_src conf
12503# Enable gzipping of responses.
12504#gzip on;
12505
12506# Set the Vary HTTP header as defined in the RFC 2616. Default is 'off'.
12507gzip_vary on;
12508#+end_src
12509
12510Remove the default gzip lines and replace them with the following:
12511
12512#+begin_src conf
12513# Enable gzipping of responses.
12514gzip on;
12515gzip_vary on;
12516gzip_min_length 10240;
12517gzip_proxied expired no-cache no-store private auth;
12518gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
12519gzip_disable "MSIE [1-6]";
12520#+end_src
12521
12522* Explanations of ngx_{httpgzipmodule} Options
12523
12524Each of the lines above enables a different aspect of the gzip response for
12525Nginx. Here are the full explanations:
12526
12527- =gzip=: Enables or disables gzipping of responses.
12528- =gzip_vary=: Enables or disables inserting the "Vary: Accept-Encoding"
12529 response header field if the directives gzip, gzip_{static}, or gunzip are
12530 active.
12531- =gzip_min_length=: Sets the minimum length of a response that will be
12532 gzipped. The length is determined only from the "Content-Length" response
12533 header field.
12534- =gzip_proxied=: Enables or disables gzipping of responses for proxied
12535 requests depending on the request and response. The fact that the request is
12536 proxied is determined by the presence of the "Via" request header field.
12537- =gzip_types=: Enables gzipping of responses for the specified MIME types in
12538 addition to "text/html". The special value "*" matches any MIME type (0.8.29).
12539 Responses with the "text/html" type are always compressed.
12540- =gzip_disable=: Disables gzipping of responses for requests with
12541 "User-Agent" header fields matching any of the specified regular expressions.
12542 - The special mask "msie6" (0.7.12) corresponds to the regular expression
12543 "MSIE [4-6].", but works faster. Starting from version 0.8.11, "MSIE 6.0;
12544 ... SV1" is excluded from this mask.
12545
12546More information on these directives and their options can be found on the
12547[[https://nginx.org/en/docs/http/ngx_http_gzip_module.html][Module ngx_{httpgzipmodule}]] page in Nginx's documentation.
12548#+date: <2022-12-07 Wed 00:00:00>
12549#+title: Nginx Wildcard Redirects: Seamlessly Redirecting Domains and Subdomains with Trailing Paths
12550#+description: Instructional steps for applying regex-based redirects in Nginx to manage subdomain routing and trailing path modifications effectively.
12551#+slug: nginx-wildcard-redirect
12552
12553* Problem
12554
12555I recently migrated domains and replaced the old webpage with a simple info page
12556with instructions to users on how to edit their bookmarks and URLs to get to the
12557page they were seeking.
12558
12559This was not ideal as it left the work up to the user and may have caused
12560friction for users who accessed my RSS (Really Simple Syndication) feed.
12561
12562* Solution
12563
12564Instead, I finally found a solution that allows me to redirect both subdomains
12565AND trailing content. For example, both of these URLs now redirect properly
12566using the logic I'll explain below:
12567
12568#+begin_src txt
12569# Example 1 - Simple base domain redirect with trailing content
12570https://domain1.com/blog/alpine-linux/ -> https://domain2.com/blog/alpine-linux/
12571
12572# Example 2 - Complex redirect with both a subdomain and trailing content
12573https://libreddit.domain1.com/r/history/comments/7z8cbg/new_discovery_mode_turns_video_game_assassins/
12574->
12575https://libreddit.domain2.com/r/history/comments/7z8cbg/new_discovery_mode_turns_video_game_assassins/
12576#+end_src
12577
12578Go ahead, try the URLs (uniform resource locators) if you want to test them.
12579
12580** Nginx Config
12581
12582To make this possible. I needed to configure a proper redirect scheme in my
12583Nginx configuration.
12584
12585#+begin_src sh
12586doas nano /etc/nginx/http.d/domain1.conf
12587#+end_src
12588
12589Within this file, I had one block configured to redirect HTTP requests to HTTPS
12590for the base domain and all subdomains.
12591
12592#+begin_src conf
12593server {
12594 listen [::]:80;
12595 listen 80;
12596 server_name domain1.com *.domain1.com;
12597
12598 if ($host = domain1.com) {
12599 return 301 https://$host$request_uri;
12600 }
12601
12602 if ($host = *.domain1.com) {
12603 return 301 https://$host$request_uri;
12604 }
12605
12606 return 404;
12607}
12608#+end_src
12609
12610For the base domain, I have another =server= block dedicated to redirecting all
12611base domain requests. You can see that the =rewrite= line is instructing Nginx
12612to gather all trailing content and append it to the new =domain2.com= URL.
12613
12614#+begin_src conf
12615server {
12616 listen [::]:443 ssl http2;
12617 listen 443 ssl http2;
12618
12619 server_name domain1.com;
12620
12621 rewrite ^/(.*)$ https://domain2.com/$1 permanent;
12622
12623 ssl_certificate /etc/letsencrypt/live/domain1.com/fullchain.pem;
12624 ssl_certificate_key /etc/letsencrypt/live/domain1.com/privkey.pem;
12625}
12626#+end_src
12627
12628Finally, the tricky part is figuring out how to tell Nginx to redirect while
12629keeping both a subdomain and trailing content intact. I found that the easiest
12630way to do this is to give it a =server= block of its own.
12631
12632Within this block, we need to do some regex on the =server_name= line before we
12633can rewrite anything. This creates a variable called =subdomain=.
12634
12635Once the server gets to the =rewrite= line, it pulls the =subdomain= variable
12636from above and uses it on the new =domain2.com= domain before appending the
12637trailing content (=$request_uri=).
12638
12639#+begin_src conf
12640server {
12641 listen [::]:443 ssl http2;
12642 listen 443 ssl http2;
12643
12644 server_name ~^(?<subdomain>\w+)\.domain1\.com$;
12645
12646 rewrite ^ https://$subdomain.domain2.com$request_uri permanent;
12647
12648 ssl_certificate /etc/letsencrypt/live/domain1.com/fullchain.pem;
12649 ssl_certificate_key /etc/letsencrypt/live/domain1.com/privkey.pem;
12650}
12651#+end_src
12652
12653That's all there is to it. With this, I simply restarted Nginx and watched the
12654redirections work in-action.
12655
12656#+begin_src sh
12657doas rc-service nginx restart
12658#+end_src
12659
12660Looking back on it, I wish I had done this sooner. Who knows how many people
12661went looking for my sites or bookmarks and gave up when they saw the redirect
12662instructions page.
12663
12664Oh well, it's done now. Live and learn.
12665#+date: <2022-12-17 Sat 00:00:00>
12666#+title: Installing and Customizing the suckless Simple Terminal (st) on Fedora Linux
12667#+description: Comprehensive instructions to obtain source code, compile, apply patches, and install the Simple Terminal application on Fedora Linux systems.
12668#+slug: st
12669
12670* st
12671
12672[[https://st.suckless.org][st]] standards for Simple Terminal, a simple terminal implementation for X made by
12673the [[https://suckless.org][suckless]] team.
12674
12675This post walks through the dependencies needed and process to build and install
12676=st= on Fedora Workstation.
12677
12678** Obtain Files
12679
12680To start, obtain the source files for =st= via =git clone=.
12681
12682#+begin_src sh
12683mkdir ~/suckless && cd ~/suckless
12684git clone https://git.suckless.org/st && cd st
12685#+end_src
12686
12687** Dependencies
12688
12689Once you have the files and are in the =st= directory, ensure the following
12690packages are installed.
12691
12692#+begin_src sh
12693sudo dnf update && sudo dnf upgrade
12694sudo dnf install gcc patch libX11-devel libXft-devel
12695#+end_src
12696
12697** Building
12698
12699Before building, ensure that you read the README file.
12700
12701#+begin_src sh
12702cat README
12703#+end_src
12704
12705Once you've read the instructions, open the =config.mk= file and ensure it
12706matches your setup. If you're not sure, leave the default options within the
12707file.
12708
12709Finally, you can build =st= with the following command. Ensure you run as root
12710(e.g., =sudo=) or else you may not end up with a usable application file.
12711
12712#+begin_src sh
12713sudo make clean install
12714#+end_src
12715
12716** Customization (Patches)
12717
12718Note that customizing =st= requires you to modify the source files or to
12719download one of the [[https://st.suckless.org/patches/][available patches]] for suckless.org.
12720
12721If you've already installed =st= and want to customize or install a patch, start
12722by uninstalling the current program.
12723
12724#+begin_src sh
12725cd ~/suckless/st
12726sudo make uninstall
12727#+end_src
12728
12729Next, grab the =<path>.diff= file from the page of the patch you chose. For
12730example, I will be using the [[https://st.suckless.org/patches/defaultfontsize/][defaultfontsize]] patch in the below example.
12731
12732#+begin_src sh
12733wget https://st.suckless.org/patches/defaultfontsize/st-defaultfontsize-20210225-4ef0cbd.diff
12734#+end_src
12735
12736Once the file is downloaded inside the =st= folder, apply the patch and
12737re-install the program. You may need to install the =patch= command if you don't
12738have it installed already (you should have installed it above).
12739
12740#+begin_src sh
12741patch -i st-defaultfontsize-20210225-4ef0cbd.diff
12742sudo make clean install
12743#+end_src
12744
12745Once installed, you can use the default font size patch to launch =st= with any
12746font size you wish:
12747
12748#+begin_src sh
12749st -z 16
12750#+end_src
12751#+date: <2022-12-23 Fri 00:00:00>
12752#+title: How to Set Up Alpine Linux as a Desktop OS with Sway
12753#+description: Stepwise procedures for installing and setting up Alpine Linux as a desktop operating system, including window manager setup and relevant system adjustments.
12754#+slug: alpine-desktop
12755
12756* Isn't Alpine Linux for Servers?
12757
12758This is a question I see a lot when people are presented with an example of
12759Alpine Linux running as a desktop operating system (OS).
12760
12761While Alpine is small, fast, and minimal, that doesn't stop it from functioning
12762at a productive level for desktop users.
12763
12764This post is documentation of how I installed and modified Alpine Linux to
12765become my daily desktop OS.
12766
12767* Installation
12768
12769Note that I cover the installation of Alpine Linux in my other post, so I won't
12770repeat it here: [[https://cleberg.net/blog/alpine-linux.html][Alpine Linux Essentials: Installing and Setting Up a Secure
12771Minimal Server]].
12772
12773Basically, get a bootable USB (Universal Serial Bus) device or whatever you
12774prefer with Alpine on it, boot the optical disc image (ISO), and run the setup
12775script.
12776
12777#+begin_src sh
12778setup-alpine
12779#+end_src
12780
12781Once you have gone through all the options and installer finishes without
12782errors, reboot.
12783
12784#+begin_src sh
12785reboot
12786#+end_src
12787
12788* Initial Setup
12789
12790Once Alpine is installed and the machine has rebooted, login is as root
12791initially or =su= to root once you log in as your user. From here, you should
12792start by updating and upgrading the system in case the ISO was not fully
12793up-to-date.
12794
12795#+begin_src sh
12796# Update and upgrade system
12797apk -U update && apk -U upgrade
12798
12799# Add an editor so we can enable the community repository
12800apk add nano
12801#+end_src
12802
12803You need to uncomment the =community= repository for your version of Alpine
12804Linux.
12805
12806For v3.17, the =repositories= file should look like this:
12807
12808#+begin_src sh
12809nano /etc/apk/repositories
12810#+end_src
12811
12812#+begin_src conf
12813#/media/sda/apks
12814http://mirrors.gigenet.com/alpinelinux/v3.17/main
12815http://mirrors.gigenet.com/alpinelinux/v3.17/community
12816#http://mirrors.gigenet.com/alpinelinux/edge/main
12817#http://mirrors.gigenet.com/alpinelinux/edge/community
12818#http://mirrors.gigenet.com/alpinelinux/edge/testing
12819#+end_src
12820
12821#+begin_src sh
12822# Add the rest of your packages
12823apk add linux-firmware iwd doas git curl wget
12824
12825# Add yourself to the wheel group so you can use the doas command
12826adduser $USER wheel
12827#+end_src
12828
12829* Window Manager (Desktop)
12830
12831The [[https://wiki.alpinelinux.org/wiki/Sway][Sway installation guide]] has everything you need to get Sway working on
12832Alpine.
12833
12834However, I'll include a brief list of the commands I ran and their purpose for
12835posterity here.
12836
12837#+begin_src sh
12838# Add eudev and set it up
12839apk add eudev
12840setup-devd udev
12841
12842# Since I have Radeon graphics, I need the following packages
12843apk add mesa-dri-gallium mesa-va-gallium
12844
12845# Add user to applicable groups
12846adduser $USER input
12847adduser $USER video
12848
12849# Add a font package
12850apk add ttf-dejavu
12851
12852# Add the seatd daemon
12853apk add seatd
12854rc-update add seatd
12855rc-service seatd start
12856
12857# Add user to seat group
12858adduser $USER seat
12859
12860# Add elogind
12861apk add elogind polkit-elogind
12862rc-update add elogind
12863rc-service elogind start
12864
12865# Finally, add sway and dependencies
12866apk add sway sway-doc
12867apk add \ # Install optional dependencies:
12868 xwayland \ # recommended for compatibility reasons
12869 foot \ # default terminal emulator
12870 bemenu \ # wayland menu
12871 swaylock swaylockd \ # lockscreen tool
12872 swaybg \ # wallpaper daemon
12873 swayidle # idle management (DPMS) daemon
12874#+end_src
12875
12876Once you have the packages installed and set-up, you need to export the
12877=XDG_RUNTIME_DIR= upon login. To do this, edit your =.profile= file.
12878
12879If you use another shell, such as =zsh=, you need to edit that shell's profile
12880(e.g., =~/.zprofile=)!
12881
12882#+begin_src sh
12883nano ~/.profile
12884#+end_src
12885
12886Within the file, paste this:
12887
12888#+begin_src sh
12889if test -z "${XDG_RUNTIME_DIR}"; then
12890 export XDG_RUNTIME_DIR=/tmp/$(id -u)-runtime-dir
12891 if ! test -d "${XDG_RUNTIME_DIR}"; then
12892 mkdir "${XDG_RUNTIME_DIR}"
12893 chmod 0700 "${XDG_RUNTIME_DIR}"
12894 fi
12895fi
12896#+end_src
12897
12898Once that's complete, you can launch Sway manually.
12899
12900#+begin_src sh
12901dbus-run-session -- sway
12902#+end_src
12903
12904** Personal Touches
12905
12906I also added the following packages, per my personal preferences and situation.
12907
12908#+begin_src sh
12909doas apk add brightnessctl \ # Brightness controller
12910 zsh \ # Shell
12911 firefox \ # Browser
12912 syncthing \ # File sync service
12913 wireguard-tools \ # Wireguard VPN
12914 gomuks \ # CLI Matrix client
12915 neomutt \ # CLI email client
12916 thunderbird \ # GUI email client
12917 gnupg # GPG key manager
12918#+end_src
12919
12920From here, I use my Syncthing storage to pull all the configuration files I
12921stored from prior desktops, such as my [[https://github.com/ccleberg/dotfiles.git][dotfiles]].
12922
12923* Resolving Issues
12924
12925** WiFi Issues
12926
12927I initially tried to set up my Wi-Fi the standard way with =iwd=, but it didn't
12928work.
12929
12930Here is what I initially tried (I did all of this as =root=):
12931
12932#+begin_src sh
12933apk add iwd
12934rc-service iwd start
12935iwctl station wlan0 connect <SSID> # This will prompt for the password
12936rc-update add iwd boot && rc-update add dbus boot
12937#+end_src
12938
12939Then, I added the Wi-Fi entry to the bottom of the networking interface file:
12940
12941#+begin_src sh
12942nano /etc/network/interfaces
12943#+end_src
12944
12945#+begin_src conf
12946auto wlan0
12947iface wlan0 inet dhcp
12948#+end_src
12949
12950Finally, restart the networking service:
12951
12952#+begin_src sh
12953rc-service networking restart
12954#+end_src
12955
12956My Wi-Fi interface would receive an internet protocol (IP) address from the
12957router, but it could not ping anything in the network. To solve the Wi-Fi
12958issues, I originally upgraded to Alpine's =edge= repositories, which was
12959unnecessary.
12960
12961Really, the solution was to enable the =NameResolvingService=resolvconf= in
12962=/etc/iwd/main.conf=.
12963
12964#+begin_src sh
12965doas nano /etc/iwd/main.conf
12966#+end_src
12967
12968#+begin_src conf
12969[Network]
12970
12971NameResolvingService=resolvconf
12972#+end_src
12973
12974Once I finished this process, my Wi-Fi is working flawlessly.
12975
12976** Sound Issues
12977
12978Same as with the Wi-Fi, I had no sound and could not control the mute/unmute or
12979volume buttons on my laptop.
12980
12981To resolve this, I installed [[https://wiki.alpinelinux.org/wiki/PipeWire][pipewire]].
12982
12983#+begin_src sh
12984# Add your user to the following groups
12985addgroup $USER audio
12986addgroup $USER video
12987
12988# Install pipewire and other useful packages
12989apk add pipewire wireplumber pipewire-pulse pipewire-jack pipewire-alsa
12990#+end_src
12991
12992Finally, I needed to add =/usr/libexec/pipewire-launcher= to my
12993=.config/sway/config= file so that Pipewire would run every time I launched
12994sway.
12995
12996#+begin_src sh
12997nano ~/.config/sway/config
12998#+end_src
12999
13000#+begin_src conf
13001# Run pipewire audio server
13002exec /usr/libexec/pipewire-launcher
13003
13004# Example audio button controls
13005bindsym XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +5%
13006bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -5%
13007bindsym XF86AudioMute exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ toggle
13008bindsym XF86AudioMicMute exec --no-startup-id pactl set-source-mute @DEFAULT_SOURCE@ toggle
13009#+end_src
13010
13011Note that I do not use bluetooth or screen sharing, so I won't cover those
13012options in this post.
13013
13014Other than these issues, I have a working Alpine desktop. No other complaints
13015thus far!
13016#+date: <2023-01-03 Tue 00:00:00>
13017#+title: 2023 Website Updates: Minimalist Style, Accessibility Improvements, and Content Prioritization
13018#+description: Documentation of significant changes applied to site design, layout, and accessibility features to enhance navigability and content clarity.
13019#+slug: recent-website-changes
13020
13021* The State of This Website
13022
13023Over the years, this website has changed URLs, styles, content focus,
13024and much more. However, it seems that I am never done, as I am yet again
13025changing this page to fit my current needs and wants.
13026
13027While this site was already minimal (~12kb), it contained a lot of
13028disorganized content and some poorly chosen color schemes.
13029
13030The recent updates attempt to fix these items while focusing on what I
13031truly care about here: the content within each page.
13032
13033* Recent Changes
13034
13035I've made quite a few changes to the style of this website today, both
13036in the CSS and in the HTML.
13037
13038Here are some of the key takeaways from today's changes:
13039
130401. Reduce the width of the website from =60em= to =40em=.
130412. Remove breadcrumb navigation and replaced with a simple "Return Home"
13042 link when visiting anything other than the homepage.
130433. Remove syntax highlighting from code blocks. CSS now loads from a
13044 single file.
130454. Move blog posts on the homepage to the top, pushing tertiary content
13046 down.
130475. Update font-family from =monospace= to =sans-serif= for readability.
13048
13049* Future Focus
13050
13051** Accessibility
13052
13053My main focus amidst all the changes this past year was accessibility. I
13054tried to improve the accessibility of all pages on this site as well as
13055I know how. For example, I added aria labels to identify different
13056navigation menus in November.
13057
13058I want to continue this in 2023 and learn more about the accessibility
13059features I can utilize to help those who may need them.
13060
13061** Content First
13062
13063Beyond accessibility, I am making a concerted effort to bring the
13064content of this site to the forefront and push the tertiary information
13065(e.g., About Me, Services, etc.) down below the content.
13066
13067Further, I want to review and edit previous blog posts for grammar,
13068accuracy, dead links, and more. Where necessary, I may even add a blurb
13069at the top of old posts that are no longer accurate or helpful.
13070
13071** Style
13072
13073As always, I will be searching for ways to reduce distractions and bring
13074a minimalistic approach to this site. While there are certainly more
13075drastic measures I could take, such as making this a
13076[[https://shinobi.bt.ht][Shinobi Website]] or a [[https://nocss.club][No
13077CSS Website]], I prefer to keep some modern features here.
13078
13079Two ideas have been floating around in my head, but I am not sure how I
13080feel about these approaches yet:
13081
130821. Going all-in with Emacs org-mode and creating a script to
13083 auto-publish all my blog posts from org-mode to plain HTML files and
13084 then letting my CSS style it after the fact.
130852. Focus on publishing for Gemini or Gopher and then utilize a
13086 conversion program to translate the pages to HTML.
13087
13088* Looking Back
13089
13090As I sit here and finalize the changes, as well as this post, I am happy
13091with the current state of this website. It's certainly a long way from
13092my first attempts (parallax background images, anyone?) and it's good to
13093see the progress.
13094
13095I can only hope and work hard to ensure that I make more meaningful
13096progress by this time next year.
13097#+date: <2023-01-05 Thu 00:00:00>
13098#+title: How to Quickly Remove All Likes on Tumblr Desktop
13099#+description: Instructions for executing a JavaScript script to automate the mass unlike operation for Tumblr posts without requiring additional software.
13100#+slug: mass-unlike-tumblr-posts
13101
13102* The Dilemma
13103
13104The dilemma I had was pretty simple: I wanted to unlike all the posts I
13105have liked on Tumblr so that I could follow a new focus on blogs and
13106start fresh. Otherwise, Tumblr will keep recommending content based on
13107your previous likes.
13108
13109* The Solution
13110
13111I searched the web for a while and noted that most solutions referenced
13112Tumblr setting and dashboard pages that no longer exist. Additionally, I
13113did not want to install a third party extension to do this, as some
13114suggested.
13115
13116Luckily, I used Javascript for a while a few years ago and figured it
13117would be easy enough to script a solution, as long as Tumblr had a
13118system for the unlike buttons.
13119
13120** Identifying Unlike Buttons
13121
13122Tumblr's unlike buttons are structured as you can see in the following
13123code block. All unlike buttons have an =aria-label= with a value of
13124=Unlike=.
13125
13126#+begin_src html
13127<button class="TRX6J" aria-label="Unlike">
13128 <span class="EvhBA B1Z5w ztpfZ" tabindex="-1">
13129 <svg
13130 xmlns="http://www.w3.org/2000/svg"
13131 height="21"
13132 width="23"
13133 role="presentation"
13134 >
13135 <use href="#managed-icon__like-filled"></use>
13136 </svg>
13137 </span>
13138</button>
13139#+end_src
13140
13141** Running a Script to Unlike All Likes
13142
13143To run this script, you will need to load the
13144[[https://www.tumblr.com/likes][Likes | Tumblr]] page while logged in to
13145your account.
13146
13147Further, be sure to scroll down to the bottom and force Tumblr to load
13148more posts so that this script unlikes more posts at a time.
13149
13150Once you are logged in and the page is loaded, open the Developer Tools
13151and be sure you're on the "Console" tab. It should look something like
13152this (this is in Firefox, Chromium should be similar):
13153
13154#+caption: Firefox Dev Tools
13155#+attr_html: :alt Firefox Dev Tools' Console tab will allow you to paste this command.
13156[[https:///img.cleberg.net/blog/20230105-mass-unlike-tumblr-posts/dev_console.webp]]
13157
13158All you need to do is paste the following snippet into the dev console.
13159This code will collect all unlike buttons (=elements=) and then click
13160each button to unlike it.
13161
13162Optionally, you can comment-out the line =elements[i].click();= and
13163uncomment the =console.log()= lines to simply print out information
13164without performing any actions. This can be useful to debug issues or
13165confirm that the code below isn't doing anything you don't want it to.
13166
13167#+begin_src javascript
13168const elements = document.querySelectorAll('[aria-label="Unlike"]');
13169// console.log(elements); // 👉 [button]
13170
13171for (let i = 0; i < elements.length; i++) {
13172 // console.log(elements[i]);
13173 elements[i].click();
13174}
13175#+end_src
13176
13177* Results
13178
13179The results were quick for my situation, as it unliked ~200 posts within
131802-3 seconds. I am not sure how this will perform on larger sets of likes
13181(or if Tumblr has a limit to unliking posts).
13182
13183You can see the below screenshot showing that I pasted the snippet into
13184the console, pressed Enter, and then the posts are automatically
13185unliked.
13186
13187#+caption: Script Results
13188#+attr_html: :alt The script will return "undefined" when run in the console.
13189[[https://img.cleberg.net/blog/20230105-mass-unlike-tumblr-posts/script_results.webp]]
13190
13191Thinking about this further, I would bet that this would be fairly
13192simple to package into a browser add-on so that users could install the
13193add-on, go to their Likes page, and click a button to run the script.
13194Food for thought.
13195#+date: <2023-01-08 Sun 00:00:00>
13196#+title: How to Remove Fedora i3's Login Manager and Start i3 Manually
13197#+description: Step-by-step guide for uninstalling the default Fedora i3 login manager and configuring system to launch i3 window manager manually.
13198#+slug: fedora-login-manager
13199
13200* Fedora i3's Login Manager
13201
13202Since I use the i3 spin of Fedora Workstation, I don't like to have a
13203login manager installed by default. As of the current version of Fedora
13204i3, the default login manager is LightDM.
13205
13206If this is no longer the case, you can search for currently-installed
13207packages with the following command and see if you can identify a
13208different login manager.
13209
13210#+begin_src sh
13211sudo dnf list installed
13212#+end_src
13213
13214* Removing the Login Manager
13215
13216In order to remove the login manager, simply uninstall the package.
13217
13218#+begin_src sh
13219sudo dnf remove lightdm
13220#+end_src
13221
13222* Launching i3 Manually
13223
13224In order to launch i3 manually, you need to set up your X session
13225properly. To start, create or edit the =~/.xinitrc= file to include the
13226following at the bottom.
13227
13228#+begin_src config
13229exec i3
13230#+end_src
13231
13232Now, whenever you log in to the TTY, you can launch your desktop with
13233the following command.
13234
13235#+begin_src sh
13236startx
13237#+end_src
13238#+date: <2023-01-21 Sat 00:00:00>
13239#+title: Simplify Flatpak Commands: Making Symlinks for Easier App Launching
13240#+description: Detailed instructions for establishing symbolic links for Flatpak applications to facilitate faster execution from terminal or application launcher environments.
13241#+slug: flatpak-symlinks
13242
13243* Running Flatpak Apps Should Be Faster
13244
13245If you're like me and use Flatpak for those pesky apps that cannot run
13246on your system for one reason or another, you likely get annoyed with
13247opening a terminal and manually running the Flatpak app with the lengthy
13248=flatpak run ...= command.
13249
13250In the past, I manually created aliases in my =.zshrc= file for certain
13251apps. For example, an alias would look like the example below.
13252
13253This would allow me to run the command fast within the terminal, but it
13254wouldn't allow me to run it in an application launcher.
13255
13256#+begin_src sh
13257# ~/.zshrc
13258alias librewolf = "flatpak run io.gitlab.librewolf-community"
13259#+end_src
13260
13261However, I now use a much faster and better method that integrates with
13262the tiling WMs I use and their application launchers - =dmenu= and
13263=bemenu=.
13264
13265* Creating Symlinks for Flatpak Apps
13266
13267Let's use the example of Librewolf below. I can install the application
13268like so:
13269
13270#+begin_src sh
13271flatpak install flathub io.gitlab.librewolf-community
13272#+end_src
13273
13274Once installed, I can create a symlink to link the flatpak app to my new
13275symlink in a location commonly included in your PATH. In this case, I
13276chose =/usr/bin=. You may need to choose a different location if
13277=/usr/bin= isn't in your PATH.
13278
13279#+begin_src sh
13280ln -s /var/lib/flatpak/exports/bin/io.gitlab.librewolf-community /usr/bin/librewolf
13281#+end_src
13282
13283Once complete, you should be able to launch the app using the command
13284name you chose above in the symlink (=librewolf=) from a terminal or
13285from your application launcher!
13286#+date: <2023-01-23 Mon 00:00:00>
13287#+title: Automate Random Mullvad Wireguard VPN Connection on Startup
13288#+description: Stepwise method to configure system startup scripts for automatic connection to a randomly chosen Mullvad Wireguard VPN server to maintain privacy.
13289#+slug: random-wireguard
13290
13291* Mullvad Wireguard
13292
13293If you're using an OS that does not support one of Mullvad's apps,
13294you're likely using the Wireguard configuration files instead.
13295
13296If not, the first step is to visit Mullvad's
13297[[https://mullvad.net/en/account/#/wireguard-config][Wireguard
13298configuration files]] page and download a ZIP of the configuration files
13299you want to use.
13300
13301Personally, I downloaded all configuration files across the world and
13302chose my connections using the script below.
13303
13304Once the files are downloaded, unzip them and move them to your
13305preferred location:
13306
13307#+begin_src sh
13308cd Downloads
13309unzip mullvad_wireguard_linux_all_all.zip
13310mkdir ~/mullvad && mv ~/Downloads/*.conf ~/mullvad/
13311#+end_src
13312
13313*** Creating a Script to Connect to a Random Host
13314
13315Once you have a folder of Wireguard configuration files from Mullvad,
13316you can create a script to randomly connect to any one of the locations.
13317
13318Start by creating a shell script - mine is called =vpn.sh=.
13319
13320#+begin_src sh
13321nano ~/vpn.sh
13322#+end_src
13323
13324Within this script, you can paste the following info. Note that I
13325specify =us-*= in my script, which means that it will only consider
13326US-based VPN locations. You can alter this or simply change it =*= to
13327consider all locations.
13328
13329#+begin_src sh
13330#!/bin/sh
13331
13332ls /home/$USER/mullvad/us-** |sort -R |tail -n 1 |while read file; do
13333 # Replace `doas` with `sudo` if your machine uses `sudo`,
13334 # or remove `doas` if users don't need to su to run wg-quick
13335 doas wg-quick up $file;
13336 printf "\nCreated Mullvad wireguard connection with file: $file";
13337 printf "\n\nPrinting new IP info:\n"
13338 curl https://am.i.mullvad.net/connected
13339done
13340#+end_src
13341
13342Once you've modified the script to your liking, add executable
13343permissions and run the script:
13344
13345#+begin_src sh
13346chmod +x ~/vpn.sh
13347~/vpn.sh
13348#+end_src
13349
13350The output should look like the following:
13351
13352#+begin_src txt
13353doas (user@host) password:
13354
13355# ... The script will process all of the iptables and wg commands here
13356
13357Created Mullvad wireguard connection with file: /home/user/mullvad/us-nyc-wg-210.conf
13358
13359Printing new IP info:
13360You are connected to Mullvad (server country-city-wg-num). Your IP address is 12.345.678.99
13361#+end_src
13362
13363That's all there is to it. You can see your new location and IP via the
13364=printf= and =curl= commands included in the script.
13365
13366You can also go to the [[https://mullvad.net/en/check/][Connection Check
13367​| Mullvad]] page to see if you are fully connected to Mullvad and if any
13368leaks exist.
13369
13370* Disconnecting from the Wireguard Connection
13371
13372If you forget which connection you're using, you can execute the
13373following command to see where Wireguard is currently connected:
13374
13375#+begin_src sh
13376wg show
13377#+end_src
13378
13379This command will show you the Wireguard interfaces and should output a
13380connection like so: =interface: us-lax-wg-104=.
13381
13382Once you have this, just disconnect using that files' full path:
13383
13384#+begin_src sh
13385wg-quick down /home/user/mullvad/us-lax-wg-104.conf
13386#+end_src
13387
13388I have a TODO item on figuring out how to easily export an environment
13389variable that contains the configuration file's full name, so that I can
13390just execute the following:
13391
13392#+begin_src sh
13393# Ideal situation if I can export the $file variable to the environment
13394wg-quick down $file
13395#+end_src
13396
13397If you have an idea on how to do this, email me!
13398#+date: <2023-01-28 Sat 00:00:00>
13399#+title: Self-Hosting Guide: Wger
13400#+description: Comprehensive steps for installing, configuring, and operating the Wger workout management application on a dedicated server platform.
13401#+slug: self-hosting-wger
13402
13403* Wger: The Self-Hosted Workout Manager
13404
13405[[https://wger.de][Wger Workout Manager]] is a fitness tracking tool for
13406those who enjoy self-hosting their data. You can also register an
13407account on their main website if you'd prefer to try without
13408self-hosting.
13409
13410** Features
13411
13412I didn't see a full listing of features anywhere, so I compiled this
13413list of my own after installing wger:
13414
134151. Dashboard
13416
13417 - Dashboard view of Workout Schedule, Nutrition Plan, Weight Graph, &
13418 last 5 Weight Logs
13419
134202. Training
13421
13422 - Workout Log
13423 - Workout Schedule
13424 - Calendar (shows weight logs and Bad/Neutral/Good days)
13425 - Gallery (shows images you upload)
13426 - Workout templates
13427 - Public templates
13428 - Exercises
13429
134303. Nutrition
13431
13432 - Nutrition plans
13433 - BMI calculator
13434 - Daily calories calculator
13435 - Ingredient overview
13436
134374. Body Weight
13438
13439 - Weight overview
13440
13441** Documentation
13442
13443In order to self-host wger, I opted to use the Docker version of the
13444application. You can read the README within the
13445[[https://github.com/wger-project/docker][wger-project/docker]] project
13446on GitHub for information and installation instructions.
13447
13448** Installation
13449
13450To start the installation, I created a folder for wger and started
13451creating the three necessary files:
13452
13453#+begin_src sh
13454mkdir ~/wger && mkdir ~/wger/config
13455touch ~/wger/docker-compose.yml && \
13456touch ~/wger/config/prod.env && \
13457touch ~/wger/config/nginx.conf
13458#+end_src
13459
13460Once you have the folders and files created, you will need to copy the
13461contents of the =docker-compose.yml=, =prod.env=, and =nginx.conf= from
13462the GitHub link above.
13463
13464A few notes to explain the changes I made to the default files:
13465
13466- I updated the =ALLOW_REGISTRAION= variable in =prod.env= to =False=
13467 after I created an account via my LAN connection, *before* I connected
13468 this app to a publicly-available domain.
13469- I uncommented and updated =CSRF_TRUSTED_ORIGINS= to be equal to the
13470 public version of this app: =https://wger.example.com=.
13471- I updated the port within =docker-compose.yml=, within the =nginx=
13472 block. The port I updated this to will be reflected in my nginx
13473 configuration file on the server (NOT the wger nginx.conf file).
13474
13475** Deploy
13476
13477Once all files are created and modified to your needs, simply start the
13478container.
13479
13480#+begin_src sh
13481docker-compose up -d
13482#+end_src
13483
13484You can now visit the website on your LAN by going to
13485=localhost:YOUR_PORT= or by the server's IP, if you're not on the same
13486machine that is running the container.
13487
13488If you wish to connect this app to a public domain name, you'll need to
13489point an =A= DNS record from the domain to your server's public IP.
13490You'll then need to create a configuration file for whichever web server
13491or reverse proxy you're using.
13492
13493Wger's README suggests the following reverse proxy configuration for
13494Nginx:
13495
13496#+begin_src conf
13497upstream wger {
13498 # This port should match the port in the `nginx` block of docker-compose.yml
13499 # If the container is running on this same machine, replace this with
13500 # server 127.0.0.1:8080
13501 server 123.456.789.0:8080;
13502}
13503
13504server {
13505 listen 80;
13506 listen [::]:443 ssl;
13507 listen 443 ssl;
13508
13509 location / {
13510 proxy_pass http://wger;
13511 proxy_set_header Host $http_host;
13512 proxy_set_header X-Real-IP $remote_addr;
13513 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
13514 proxy_set_header X-Forwarded-Proto $scheme;
13515 proxy_redirect off;
13516 }
13517
13518 server_name my.domain.example.com;
13519
13520 ssl_certificate /path/to/https/certificate.crt;
13521 ssl_certificate_key /path/to/https/certificate.key;
13522}
13523#+end_src
13524
13525* Thoughts on Wger
13526
13527I'm still playing around with the app itself, but it seems to be a solid
13528all-around workout manager, weight log, and food log.
13529
13530I like that the weight log graph is fluid and updates quickly. You can
13531also import or export data in CSV format if you'd like to move your data
13532elsewhere.
13533
13534The workout manager is slightly odd, as it requires you to enter sets
13535and reps for each exercise when you enter it into the plan. Then, when
13536you add a log entry for performing a workout, you then add what you
13537actually performed, in terms of reps and weight.
13538
13539I haven't tried the food log yet and I likely will not, at least for a
13540while. I have no need for a food log or calorie tracker at the moment.
13541#+date: <2023-02-02 Thu 00:00:00>
13542#+title: Getting Started with the Hare Language
13543#+description: Overview of the Hare programming language features, installation procedure, and elementary programming examples targeting system-level development.
13544#+slug: exploring-hare
13545
13546* A Quick Note
13547
13548By no means am I a professional developer, so this post will be rather
13549short. I won't be going into depth on the specification or anything that
13550technical.
13551
13552Instead, I will simply be talking about how I (a relatively basic
13553hobbyist programmer) have been playing with Hare and what intrigues me
13554about the language.
13555
13556* Hare
13557
13558The [[https://harelang.org][Hare]] programming language is a
13559straightforward language that should look familiar if you've ever
13560programmed with C, Rust, or other languages that aim to build software
13561at the system-level.
13562
13563The Hare homepage states the following:
13564
13565#+begin_quote
13566Hare is a systems programming language designed to be simple, stable,
13567and robust. Hare uses a static type system, manual memory management,
13568and minimal runtime. It is well-suited to writing operating systems,
13569system tools, compilers, networking software, and other low-level, high
13570performance tasks.
13571#+end_quote
13572
13573I have found this all to be true while playing with it for the first
13574time today. In the next few sections, I'm going to walk through my
13575installation and first program.
13576
13577** Installation
13578
13579I'm currently running Alpine Linux on my Thinkpad, so the installation
13580was quite easy as there is a package for Hare in the =apk= repositories.
13581
13582#+begin_src sh
13583doas apk add hare hare-doc
13584#+end_src
13585
13586However, I was able to install Hare from scratch on Fedora Linux a short while
13587ago, which was also very easy to do. If you need further instructions and Hare
13588doesn't have a package on your system, take a look at the [[https://harelang.org/installation/][Hare Installation]]
13589page.
13590
13591** Creating a Test Project
13592
13593In order to play with the language, I created [[https://github.com/ccleberg/learn/tree/main/hare][learn/hare]] and will be putting any
13594of my Hare-related adventures in here.
13595
13596Luckily, Hare doesn't require any complex set-up tools or build
13597environment. Once you have Hare installed, you simply need to create a
13598file ending with =.ha= and you can run a Hare program.
13599
13600I created a file called =rgb.ha= in order to test out the random number
13601generation and passing parameters between functions.
13602
13603#+begin_src sh
13604nano rgb.ha
13605#+end_src
13606
13607Within this file, I was able to easily import a few of the
13608[[https://harelang.org/tutorials/stdlib/][standard library modules]]:
13609=fmt=, =math::random=, and =datetime=.
13610
13611With these modules, I created two functions:
13612
136131. =main=: This function calls the =generate_rgb= function and then
13614 prints out the returned values.
136152. =generate_rgb=: This function uses the current Unix epoch time to
13616 generate a pseudo-random value and uses this value to create three
13617 more random values between 0 and 255. These three numbers represent a
13618 color in RGB format.
13619
13620#+begin_quote
13621*Note*: Some syntax coloring may look odd, as Zola currently doesn't
13622have a syntax highlighting theme for Hare. Instead, I'm using the C
13623theme, which may not be exactly accurate when coloring the code below.
13624#+end_quote
13625
13626#+begin_src C
13627use datetime;
13628use fmt;
13629use math::random;
13630
13631export fn main() void = {
13632 const rgb = generate_rgb();
13633 fmt::printfln("RGB: ({}, {}, {})", rgb[0], rgb[1], rgb[2])!;
13634};
13635
13636fn generate_rgb() []u64 = {
13637 // Use the current Unix epoch time as the seed value
13638 let datetime = datetime::epochunix(&datetime::now());
13639
13640 // Generate initial pseudo-random value
13641 // You must cast the datetime from int to u64
13642 let x = random::init(datetime: u64);
13643
13644 // Generate RGB values between (0, 255) using pseudo-random init value
13645 let r = random::u64n(&x, 255);
13646 let g = random::u64n(&x, 255);
13647 let b = random::u64n(&x, 255);
13648
13649 // Structure data as array and return
13650 let rgb_array: [3]u64 = [r, g, b];
13651 return rgb_array;
13652};
13653#+end_src
13654
13655** Running a Program
13656
13657Once you have a Hare file written and ready to run, you simply need to
13658run it:
13659
13660#+begin_src sh
13661hare run file.ha
13662#+end_src
13663
13664You can also compile the program into an executable:
13665
13666#+begin_src sh
13667hare build -o example file.ha
13668./example
13669#+end_src
13670
13671** Initial Thoughts
13672
136731. Documentation Improvements Would Help
13674
13675 While I was able to piece everything together eventually, the biggest
13676 downfall right now in Hare's documentation. For such a new project,
13677 the documentation is in a great spot. However, bare specifications
13678 don't help as much as a brief examples section would.
13679
13680 For example, it took me a while to figure out what the =u64n=
13681 function was looking for. I could tell that it took two parameters
13682 and the second was my max value (255), but couldn't figure out what
13683 the first value should be. Eventually, I inspected the =random.ha=
13684 file in the
13685 [[https://git.sr.ht/~sircmpwn/hare/tree/master/item/math/random/random.ha][Hare
13686 source code]] and found the test suite that helped me discover that
13687 it needed an =init()= value in the form of =&var=.
13688
136892. More Basic Modules
13690
13691 This is another point that comes from Hare being new and awaiting
13692 more contributions, but there are some basic functions that I would
13693 personally enjoy seeing in Hare, such as one to convert decimal
13694 (base 10) values to hexadecimal (base 16).
13695
13696 If I'm feeling comfortable with my math, I may work on the list of
13697 functions I want and see if any can make it into the Hare source
13698 code.
13699
137003. Overall Thoughts
13701
13702 Overall, I actually really enjoy Hare. It's not as tedious to get a
13703 project up and running as Rust, but it's also simpler and more
13704 user-friendly than learning C. I am going to continue playing with it
13705 and see if I can make anything of particular value.
13706#+date: <2023-05-22 Mon 00:00:00>
13707#+title: Navigating Burnout in Audit and Consulting: A Season Without Rest
13708#+description: Examination of burnout phenomena accompanied by strategies and practical approaches for workload management and restoration of optimal functioning.
13709#+slug: burnout
13710
13711* RE: Burnout
13712
13713I recently read
13714[[https://drewdevault.com/2023/05/01/2023-05-01-Burnout.html][Drew
13715DeVault's post on burnout]] around the same time I was pulling out of a
13716burnout rut myself earlier this month. Finally, seeing the light at the
13717end of my burnout tunnel made me want to write my first post back on
13718this topic.
13719
13720* Busy Seasons on Busy Seasons
13721
13722My career deals with busy seasons, generally driven by client demand.
13723This last year, I dealt with a harsh busy season from Aug to Oct 2022 to
13724issue a few SOC reports for the period ending 2022-09-30. Immediately
13725following that, I had to pivot and found another busy season from Oct to
13726Jan for financial statement audits ending on 2022-12-31. Then again,
13727supporting other clients from Jan to Mar 2023, followed by my current
13728client workload aiming for SOC reports due on 2023-06-30.
13729
13730The result? A busy season that has lasted from August 2022 through
13731today. I will likely be rushing throughout the next month or two before
13732I have a brief break and need to focus on the 2023-09-30 SOC reports
13733again. While auditing and consulting always involve a busy season, this
13734is the first time I've had one last 9+ months without a break.
13735
13736While it's been tough, I have a handful of breaks pre-planned throughout
13737this next cycle and should be able to moderate the level of commitment
13738required for each client.
13739
13740* Refocusing
13741
13742Outside of work, I finally have time to work on hobbies such as this
13743website, programming, athletics, games, etc.
13744
13745You may have noticed my absence if you're in the same channels, forums,
13746and rooms that I am, but I should finally be active again.
13747
13748I'm hoping to break an item out of my backlog soon and start working on
13749building a new project or hack around with a stale one.
13750#+date: <2023-06-08 Thu 00:00:00>
13751#+title: Real-Time Traffic Insights from Nginx Logs Using GoAccess + GeoIP
13752#+description: Step-by-step instructions to utilize GoAccess tool incorporating MaxMind GeoIP integration for real-time geographic and traffic analysis of Nginx log files.
13753#+slug: goaccess-geoip
13754
13755* Overview
13756
13757[[https://goaccess.io/][GoAccess]] is an open source real-time web log
13758analyzer and interactive viewer that runs in a terminal in *nix systems
13759or through your browser.
13760
13761* Installation
13762
13763To start, you'll need to install GoAccess for your OS. Here's an example
13764for Debian-based distros:
13765
13766#+begin_src sh
13767sudo apt install goaccess
13768#+end_src
13769
13770Next, find any number of the MaxMind GeoIP database files on GitHub or
13771another file hosting website. We're going to use P3TERX's version in
13772this example:
13773
13774#+begin_src sh
13775wget https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-City.mmdb
13776#+end_src
13777
13778Be sure to save this file in an easy to remember location!
13779
13780* Usage
13781
13782In order to utilize the full capabilities of GoAccess and MMDB, start
13783with the command template below and customize as necessary. This will
13784export an HTML view of the GoAccess dashboard, showing all relevant
13785information related to that site's access log. You can also omit the
13786=-o output.html= parameter if you prefer to view the data within the CLI
13787instead of creating an HTML file.
13788
13789With the addition of the GeoIP Database parameter, section
13790=16 - Geo Location= will be added with the various countries that are
13791associated with the collected IP addresses.
13792
13793#+begin_src sh
13794zcat /var/log/nginx/example.access.log.*.gz | goaccess \
13795--geoip-database=/home/user/GeoLite2-City.mmdb \
13796--date-format=%d/%b/%Y \
13797--time-format=%H:%M:%S \
13798--log-format=COMBINED \
13799-o output.html \
13800/var/log/nginx/example.access.log -
13801#+end_src
13802#+date: <2023-06-08 Thu 00:00:00>
13803#+title: Self-Hosting Guide: Baikal
13804#+description: Detailed procedure for deploying the Baikal server software to provide CalDAV and CardDAV services, including security considerations and Docker deployment.
13805#+slug: self-hosting-baikal
13806
13807* What is Baikal?
13808
13809[[https://sabre.io/baikal/][Baikal]] is a lightweight CalDAV + CardDAV
13810server that you can self-host on your own machine. While I have tried (&
13811failed) to get this CalDAV + CardDAV server running before, it was quite
13812easy this time. Not really sure what I did differently this time, but
13813I'm documenting my process here to ensure I don't forget.
13814
13815* Installation
13816
13817First, create a folder on your server and open a =docker-compose.yml=
13818file for editing:
13819
13820#+begin_src sh
13821mkdir baikal && cd baikal
13822nano docker-compose.yml
13823#+end_src
13824
13825Within this file, you'll need to paste the information below. You can
13826customize the =ports= section to use any port on your server to pass
13827through to port 80 in the container. You can also edit the =volumes=
13828section to use docker volumes instead of local folders.
13829
13830#+begin_src conf
13831version: "2"
13832services:
13833 baikal:
13834 image: ckulka/baikal:nginx
13835 restart: always
13836 ports:
13837 - "8567:80"
13838 volumes:
13839 - ./config:/var/www/baikal/config
13840 - ./data:/var/www/baikal/Specific
13841#+end_src
13842
13843Once finished with editing, save and close the file. Then, launch the
13844docker container:
13845
13846#+begin_src sh
13847sudo docker-compose up -d
13848#+end_src
13849
13850* Intial Setup
13851
13852As long as no issues came up when starting the container, you should be
13853able to visit the server's set-up page at =http://<server_ip>:<port>=.
13854The application will ask you to create an administrator account and
13855choose the database type for your storage. Personally, I opted to use
13856SQLite.
13857
13858Make sure the administrator credentials are adequate to protect against
13859common attacks.
13860
13861* Creating Users
13862
13863Once you've set up the application, you will be greeted by the Dashboard
13864page, which will show the version of the app, status of the
13865admin/CalDAV/CardDAV services, and the number of users, calendars,
13866events, address books, and contacts.
13867
13868To create a new user, navigate to the =Users and resources= page. This
13869process is as simple as entering a username, password, and email.
13870
13871Once a user has been created, you can create any number of calendars and
13872address books for user, as well as inspect their information.
13873
13874* Setting Up a Public URL
13875
13876Once your application is working locally, you can open access remotely
13877via a URL by using a reverse-proxy like Nginx.
13878
13879As a prerequisite, you must own a domain name and set up DNS records to
13880point a domain name to the server hosting Baikal.
13881
13882Start by navigating to your web server's configuration directory and
13883create a new file for this application.
13884
13885#+begin_src sh
13886cd /etc/nginx/sites-available/
13887nano dav
13888#+end_src
13889
13890Within this file, paste in the configuration from below and change
13891=dav.example.com= to match the URL you'll be using.
13892
13893#+begin_src conf
13894server {
13895 if ($host ~ ^[^.]+\.example\.com$) {
13896 return 301 https://$host$request_uri;
13897 }
13898
13899 listen [::]:80;
13900 listen 80;
13901 server_name dav.example.com;
13902 return 404;
13903}
13904
13905server {
13906 listen [::]:443 ssl http2;
13907 listen 443 ssl http2;
13908 server_name dav.example.com;
13909 access_log /var/log/nginx/dav.access.log;
13910 error_log /var/log/nginx/dav.error.log;
13911
13912 location / {
13913 proxy_http_version 1.1;
13914 proxy_pass http://localhost:8567;
13915 proxy_set_header Host $host;
13916 }
13917
13918 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
13919 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
13920 include /etc/letsencrypt/options-ssl-nginx.conf;
13921 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
13922}
13923#+end_src
13924
13925For Nginx on Ubuntu, you'll need to symlink the configuration file to
13926the =sites-enabled= directory and then restart Nginx.
13927
13928#+begin_src sh
13929sudo ln -s /etc/nginx/sites-available/dav /etc/nginx/sites-enabled/dav
13930sudo systemctl restart nginx.service
13931#+end_src
13932
13933At this point, the Baikal server should be available over the internet
13934at the URL configured above!
13935
13936* Configuring Clients
13937
13938Lastly, configuring clients and applications is essential to making sure
13939the DAV server is being fully utilized.
13940
13941You can also use the =Users and resources= page to inspect a user's
13942personal link to their calendars and address books by clicking the info
13943(i) button. It will show a URI like
13944=/dav.php/calendars/your-user/default/=.
13945
13946However, I found that the following URL works for most applications:
13947=/dav.php/principals/your-user/=.
13948
13949I used the =principals= URL above for Thunderbird (calendar, tasks, and
13950contacts), as well as iOS (calendar, tasks, and contacts) and everything
13951works flawlessly so far.
13952
13953Syncing is quick between the server and clients, and I haven't seen any
13954disruptions in the service or data integrity.
13955#+date: <2023-06-18 Sun 00:00:00>
13956#+title: Blocking Malicious IPs on Unifi: Manual Firewall Rule Setup
13957#+description: Instructions to identify and block harmful IP addresses and subnets through Unifi firewall settings to improve network security posture.
13958#+slug: unifi-ip-blocklist
13959
13960* Identifying Abusive IPs
13961
13962If you're like me and use Unifi network equipment at the edge of the
13963network you manage, you may know that Unifi is only somewhat decent at
13964identifying and blocking IPs that represent abusive or threat actors.
13965
13966While Unifi has a
13967[[https://help.ui.com/hc/en-us/articles/360006893234-UniFi-Gateway-Threat-Management][threat
13968management]] tool inside their Network application, it can be lacking in
13969functionality and identification. For example, I have my UDM Pro set to
13970identify and block almost all categories of threats available within the
13971Unifi settings. However, I regularly identify abusive actors on my web
13972server via the server logs.
13973
13974In addition, I have identified IP addresses and subnets directly within
13975Unifi's logs that the UDM did not block for whatever reason.
13976
13977This guide is meant to be another step in the process to manually block
13978abusive IP addresses or subnets that you have identified but are not
13979being automatically blocked yet.
13980
13981* Create an IP Group Profile
13982
13983To start, login to the Unifi machine's web GUI and navigate to the
13984Network app > Settings > Profiles.
13985
13986Within this page, choose the =IP Groups= tab and click =Create New=.
13987
13988Each IP Group profile can be used as one of three options:
13989
139901. Port Group
139912. IPv4 Address/Subnet
139923. IPv6 Address/Subnet
13993
13994In this example, I'm creating an IPv4 Address/Subnet group and adding a
13995few different IP addresses and a subnet. Once you've added all IP
13996addresses and subnets, click the =Apply= button that should appear at
13997the bottom.
13998
13999At this point, the IPv4 Address/Subnet has been created but not yet
14000used.
14001
14002* Drop IP Group Profile via the Unifi Firewall
14003
14004To instruct the Unifi machine to block the profile we just created, we
14005need to navigate to the Network app > Settings > Firewall & Security.
14006
14007Within this screen, find the Firewall Rules table and click
14008=Create Entry=. This entry should contain the following settings:
14009
14010- Type: =Internet In=
14011- Description: =<Your Custom Rule>=
14012- Rule Applied: =Before Predefined Rules=
14013- Action: =Drop=
14014- Source Type: =Port/IP Group=
14015- IPv4 Address Group: =<Name of the Group Profile You Created Above>=
14016
14017Customize the remaining configurations to your liking, and then save and
14018enable the firewall rule.
14019
14020Once enabled, the Unifi machine will be able to drop all incoming
14021connections from the defined IP addresses and subnets within the created
14022profile.
14023
14024#+begin_quote
14025As a personal aside to this topic, I'm looking for a convenient way to
14026update the firewall rules or profiles remotely (within the LAN) from the
14027web server to accelerate this process. If you have an idea on how to
14028automatically update Unifi IP groups or firewall rules, let me know!
14029#+end_quote
14030#+date: <2023-06-20 Tue 00:00:00>
14031#+title: Audit Testing Review Checklist
14032#+description: Practical audit testing review template with detailed steps to support comprehensive evaluation of financial audits and SOC reports.
14033#+slug: audit-review-template
14034
14035* Overview
14036
14037This post is a /very/ brief overview on the basic process to review
14038audit test results, focusing on work done as part of a financial
14039statement audit (FSA) or service organization controls (SOC) report.
14040
14041While there are numerous different things to review and look for - all
14042varying wildly depending on the report, client, and tester - this list
14043serves as a solid base foundation for a reviewer.
14044
14045I have used this throughout my career as a starting point to my reviews,
14046and it has worked wonders for creating a consistent and objective
14047template to my reviews. The goal is to keep this base high-level enough
14048to be used on a wide variety of engagements, while still ensuring that
14049all key areas are covered.
14050
14051* Review Template
14052
140531. [ ] Check all documents for spelling and grammar.
140542. [ ] Ensure all acronyms are fully explained upon first use.
140553. [ ] For all people referenced, use their full names and job titles
14056 upon first use.
140574. [ ] All supporting documents must cross-reference to the lead sheet
14058 and vice-versa.
140595. [ ] Verify that the control has been adequately tested:
14060 - [ ] *Test of Design*: Did the tester obtain information regarding
14061 how the control should perform normally and abnormally (e.g.,
14062 emergency scenarios)?
14063 - [ ] *Test of Operating Effectiveness*: Did the tester inquire,
14064 observe, inspect, or re-perform sufficient evidence to support
14065 their conclusion over the control? Inquiry alone is not adequate!
140666. [ ] For any information used in the control, whether by the control
14067 operator or by the tester, did the tester appropriately document the
14068 source (system or person), extraction method, parameters, and
14069 completeness and accuracy (C&A)?
14070 - [ ] For any reports, queries, etc. used in the extraction, did the
14071 tester include a copy and notate C&A considerations?
140727. [ ] Did the tester document the specific criteria that the control is
14073 being tested against?
140748. [ ] Did the tester notate in the supporting documents where each
14075 criterion was satisfied?
140769. [ ] If testing specific policies or procedures, are the documents
14077 adequate?
14078 - [ ] e.g., a test to validate that a review of policy XYZ occurs
14079 periodically should also evaluate the sufficiency of the policy
14080 itself, if meant to cover the risk that such a policy does not
14081 exist and is not reviewed.
1408210. [ ] Does the test cover the appropriate period under review?
14083 - [ ] If the test is meant to cover only a portion of the audit
14084 period, do other controls exist to mitigate the risks that exist
14085 for the remainder of the period?
1408611. [ ] For any computer-aided audit tools (CAATs) or other automation
14087 techniques used in the test, is the use of such tools explained and
14088 appropriately documented?
1408912. [ ] If prior-period documentation exists, are there any missing
14090 pieces of evidence that would further enhance the quality of the
14091 test?
1409213. [ ] Was any information discovered during the walkthrough or inquiry
14093 phase that was not incorporated into the test?
1409414. [ ] Are there new rules or expectations from your company's internal
14095 guidance or your regulatory bodies that would affect the audit
14096 approach for this control?
1409715. [ ] Was an exception, finding, or deficiency identified as a result
14098 of this test?
14099 - [ ] Was the control deficient in design, operation, or both?
14100 - [ ] What was the root cause of the finding?
14101 - [ ] Does the finding indicate other findings or potential fraud?
14102 - [ ] What's the severity and scope of the finding?
14103 - [ ] Do other controls exist as a form of compensation against the
14104 finding's severity, and do they mitigate the risk within the
14105 control objective?
14106 - [ ] Does the finding exist at the end of the period, or was it
14107 resolved within the audit period?
14108#+date: <2023-06-23 Fri 00:00:00>
14109#+title: Using Byobu for Efficient Terminal Multiplexing
14110#+description: Detailed overview of Byobu terminal multiplexer features, configuration settings, session management, and keybindings to enhance command-line efficiency.
14111#+slug: byobu
14112
14113* Byobu
14114
14115[[https://www.byobu.org][byobu]] is a command-line tool that allows you
14116to use numerous screens within a single terminal emulator instance. More
14117specifically, it's a text based window manager, using either =screen= or
14118=tmux=.
14119
14120This post is mostly just a self-reference as I explore byobu, so I may
14121come back later and update this post with more content.
14122
14123** Screenshot
14124
14125Take a look below at my current multi-window set-up in byobu while I
14126write this blog post:
14127
14128#+caption: byobu
14129#+attr_html: :alt The Byobu multiplexer with both nano and eww opened.
14130[[https://img.cleberg.net/blog/20230623-byobu/byobu.webp]]
14131
14132*** Keybindings
14133
14134You can open the help menu with either of the following commands; they
14135will both open the same manpage:
14136
14137#+begin_src sh
14138byobu --help
14139# or
14140man byobu
14141#+end_src
14142
14143While the manpage contains a ton of information about the functionality
14144of byobu (such as status notifications, sessions, and windows), the
14145first location to explore should be the keybindings section.
14146
14147The keybindings are configured as follows:
14148
14149#+begin_src txt
14150byobu keybindings can be user defined in /usr/share/byobu/keybindings/ (or
14151within .screenrc if byobu-export was used). The common key bindings are:
14152
14153F2 - Create a new window
14154F3 - Move to previous window
14155F4 - Move to next window
14156F5 - Reload profile
14157F6 - Detach from this session
14158F7 - Enter copy/scrollback mode
14159F8 - Re-title a window
14160F9 - Configuration Menu
14161F12 - Lock this terminal
14162shift-F2 - Split the screen horizontally
14163ctrl-F2 - Split the screen vertically
14164shift-F3 - Shift the focus to the previous split region
14165shift-F4 - Shift the focus to the next split region
14166shift-F5 - Join all splits
14167ctrl-F6 - Remove this split
14168ctrl-F5 - Reconnect GPG and SSH sockets
14169shift-F6 - Detach, but do not logout
14170alt-pgup - Enter scrollback mode
14171alt-pgdn - Enter scrollback mode
14172Ctrl-a $ - show detailed status
14173Ctrl-a R - Reload profile
14174Ctrl-a ! - Toggle key bindings on and off
14175Ctrl-a k - Kill the current window
14176Ctrl-a ~ - Save the current window's scrollback buffer
14177#+end_src
14178#+date: <2023-06-23 Fri 00:00:00>
14179#+title: Self-Hosting Guide: Convos
14180#+description: Stepwise instructions for installation, Docker Compose configuration, Nginx reverse proxy setup, and IRC nickname registration for Convos web client hosting.
14181#+slug: self-hosting-convos
14182
14183* Convos
14184
14185[[https://convos.chat/][Convos]] is an always-online web client for IRC.
14186It has a few features that made it attractive to me as a self-hosted
14187option:
14188
14189- Extremely simple Docker Compose installation method.
14190- Runs in the background and monitors chats even while you're not logged
14191 in.
14192- Neatly organized sidebar for conversation and client settings.
14193- Ability to connect to different hosts and create profiles for hosts.
14194- By default, registration is closed to the public. You can enable
14195 public registration on the Settings page or generate invitation links
14196 on the Users page.
14197- Customization of the client theme, organization name and URL, admin
14198 email, and video service.
14199
14200* Docker Installation
14201
14202To install Convos, simply create a directory and a =docker-compose.yml=
14203file:
14204
14205#+begin_src sh
14206mkdir ~/convos && cd ~/convos
14207nano docker-compose.yml
14208#+end_src
14209
14210With the =docker-compose.yml= file open, paste the configuration below
14211into the file. You can customize the host port to be something unique,
14212such as =21897:3000=. You can also change the =data= folder to be a
14213docker volume instead, if you prefer.
14214
14215#+begin_src config
14216version: '3'
14217
14218services:
14219 convos:
14220 image: 'convos/convos:stable'
14221 ports:
14222 - '3000:3000'
14223 volumes:
14224 - './data:/data'
14225 environment:
14226 - CONVOS_REVERSE_PROXY=1
14227 restart: always
14228#+end_src
14229
14230Save the =docker-compose.yml= file and bring the container up:
14231
14232#+begin_src sh
14233sudo docker-compose up -d
14234#+end_src
14235
14236At this point, Convos is available at =<server_ip>:3000= but not
14237available to the public.
14238
14239* Nginx Reverse Proxy
14240
14241If you're using Nginx, you can create a configuration file for the
14242convos application. Start by opening the file:
14243
14244#+begin_src sh
14245nano /etc/nginx/sites-available/convos
14246#+end_src
14247
14248Within the nginx configuration file, paste the following content and be
14249sure to update =convos.example.com= to match your domain and
14250=127.0.0.1:3000= to match the port you opened in the
14251=docker-compose.yml= file.
14252
14253#+begin_src config
14254# Host and port where convos is running
14255upstream convos_upstream { server 127.0.0.1:3000; }
14256
14257server {
14258 if ($host ~ ^[^.]+\.example\.com$) {
14259 return 301 https://$host$request_uri;
14260 }
14261
14262 listen [::]:80;
14263 listen 80;
14264 server_name convos.example.com;
14265 return 404;
14266}
14267
14268server {
14269 listen [::]:443 ssl http2;
14270 listen 443 ssl http2;
14271 server_name convos.example.com;
14272 access_log /var/log/nginx/convos.access.log;
14273 error_log /var/log/nginx/convos.error.log;
14274
14275 location / {
14276 proxy_pass http://convos_upstream;
14277 proxy_http_version 1.1;
14278 proxy_set_header Upgrade $http_upgrade;
14279 proxy_set_header Connection "upgrade";
14280 client_max_body_size 0;
14281 proxy_set_header Host $host;
14282 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
14283 proxy_set_header X-Request-Base "$scheme://$host/";
14284 }
14285
14286 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
14287 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
14288 include /etc/letsencrypt/options-ssl-nginx.conf;
14289 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
14290}
14291#+end_src
14292
14293Once the file is saved, link it to the =sites-enabled= directory and
14294restart Nginx.
14295
14296#+begin_src sh
14297sudo ln -s /etc/nginx/sites-available/convos /etc/nginx/sites-enabled/convos
14298sudo systemctl restart nginx.service
14299#+end_src
14300
14301* Screenshots
14302
14303See the screenshots below for an example of the Convos homepage and an
14304excerpt of the chat screen.
14305
14306There are numerous themes to choose from; the theme shown in the images
14307below is Dracula.
14308
14309#+caption: Convos Home
14310#+attr_html: :alt The Convos login page, requiring an email and password.
14311[[https://img.cleberg.net/blog/20230623-convos/convos_home.webp]]
14312
14313#+caption: Convos Chat
14314#+attr_html: :alt An example of the #org-mode channel on Libera.Chat.
14315[[https://img.cleberg.net/blog/20230623-convos/convos_chat.webp]]
14316
14317* Registering a Nickname
14318
14319Hop into the server channel so that you can message NickServ. In the
14320case of Convos, the default sever is libera.chat. Simply click the
14321=libera= conversation at the top of the sidebar to open it. Once the
14322chat is open, you can claim a nickname by typing:
14323
14324#+begin_src txt
14325/nick <nick>
14326#+end_src
14327
14328If the nickname is available, and you'd like to register the nickname to
14329yourself, you'll need to type another command:
14330
14331#+begin_src txt
14332/msg NickServ REGISTER
14333<password> <email>
14334#+end_src
14335
14336On libera.chat, the server will send a confirmation email with a command
14337that you must message in IRC to verify registration of the nickname:
14338
14339#+begin_src txt
14340/msg NickServ VERIFY REGISTER <nick> <verification_code>
14341#+end_src
14342
14343Once entered, the server should confirm registration of the nickname to
14344the supplied email with the password specified.
14345#+date: <2023-06-28 Wed 00:00:00>
14346#+title: Automating Secure Backups with Backblaze B2 and b2 CLI
14347#+description: Methodical guide for configuring and utilizing Backblaze B2 cloud storage services to perform reliable and secure offsite data backups.
14348#+slug: backblaze-b2
14349
14350* Overview
14351
14352Backblaze [[https://www.backblaze.com/b2/cloud-storage.html][B2 Cloud
14353Storage]] is an inexpensive and reliable on-demand cloud storage and
14354backup solution.
14355
14356The service starts at $5/TB/month ($0.005/GB/month) with a download rate
14357of $0.01/GB/month.
14358
14359However, there are free tiers:
14360
14361- The first 10 GB of storage is free.
14362- The first 1 GB of data downloaded each day is free.
14363- Class A transactions are free.
14364- The first 2500 Class B transactions each day are free.
14365- The first 2500 Class C transactions each day are free.
14366
14367You can see which API calls fall into categories A, B, or C here:
14368[[https://www.backblaze.com/b2/b2-transactions-price.html][Pricing
14369Organized by API Calls]].
14370
14371For someone like me, who wants an offsite backup of their server's
14372=/home/= directory and various other server configs that fall under 10
14373GB total, Backblaze is a great solution from a financial perspective.
14374
14375* Create An Account
14376
14377To start with Backblaze, you'll need to
14378[[https://www.backblaze.com/b2/sign-up.html][create a free account]] -
14379no payment method is required to sign up.
14380
14381Once you have an account, you can test out the service with their web
14382GUI, their mobile app, or their CLI tool. I'm going to use the CLI tool
14383below to test a file upload and then sync an entire directory to my
14384Backblaze bucket.
14385
14386* Create a Bucket
14387
14388Before you can start uploading, you need to create a bucket. If you're
14389familiar with other object storage services, this will feel familiar. If
14390not, it's pretty simple to create one.
14391
14392As their webpage says:
14393
14394#+begin_quote
14395A bucket is a container that holds files that are uploaded into B2 Cloud
14396Storage. The bucket name must be globally unique and must have a minimum
14397of 6 characters. A limit of 100 buckets may be created per account. An
14398unlimited number of files may be uploaded into a bucket.
14399#+end_quote
14400
14401Once you click the =Create a Bucket= button on their webpage or mobile
14402app, you need to provide the following:
14403
14404- Bucket Unique Name
14405- Files in Bucket are: =Private= or =Public=
14406- Default Encryption: =Disable= or =Enable=
14407- Object Lock: =Disable= or =Enable=
14408
14409For my bucket, I created a private bucket with encryption enabled and
14410object lock disabled.
14411
14412Once your bucket is created, you can test the upload/download feature on
14413their web GUI or mobile app! At this point, you have a fully functional
14414bucket and account.
14415
14416* Linux CLI Tool
14417
14418** Installation
14419
14420To install the =b2= CLI tool, you'll need to download it from the
14421[[https://www.backblaze.com/docs/cloud-storage-command-line-tools][CLI
14422Tools]] page. I recommend copying the URL from the link that says
14423=Linux= and using wget to download it, as shown below.
14424
14425Once downloaded, make the file executable and move it to a location on
14426your =$PATH=, so that you can execute that command from anywhere on the
14427machine.
14428
14429#+begin_src sh
14430wget <b2_cli_url>
14431chmod +x b2_linux
14432mv b2_linux /usr/bin/b2
14433#+end_src
14434
14435** Log In
14436
14437The first step after installation is to log in. To do this, execute the
14438following command and provide your =<applicationKeyId>= and
14439=<applicationKey>=.
14440
14441If you don't want to provide these values in the command itself, you can
14442simply execute the base command and it will request them in an
14443interactive prompt.
14444
14445#+begin_src sh
14446# if you want to provide the keys directly:
14447b2 authorize-account [<applicationKeyId>] [<applicationKey>]
14448
14449# or, if you don't want your keys in your shell history:
14450b2 authorize-account
14451#+end_src
14452
14453** Upload a Test File
14454
14455In order to test the functionality of the CLI tool, I'll start by
14456uploading a single test file to the bucket I created above. We can do
14457this with the =upload_file= function.
14458
14459The command is issued as follows:
14460
14461#+begin_src sh
14462b2 upload_file <bucket_name> <local_file> <remote_file>
14463#+end_src
14464
14465In my situation, I executed the following command with my username.
14466
14467#+begin_src sh
14468b2 upload_file my_unique_bucket /home/<user>/test.md test.md
14469#+end_src
14470
14471To confirm that the file was uploaded successfully, list the files in
14472your bucket:
14473
14474#+begin_src sh
14475b2 ls <bucket_name>
14476#+end_src
14477
14478#+begin_src txt
14479test.md
14480#+end_src
14481
14482** Sync a Directory
14483
14484If you have numerous files, you can use the =sync= function to perform
14485functionality similar to =rsync=, where you can check what's in your
14486bucket and sync anything that is new or modified.
14487
14488The command is issued as follows:
14489
14490#+begin_src sh
14491b2 sync <source file location> <B2 bucket destination>
14492#+end_src
14493
14494In my case, I can sync my user's entire home directory to my bucket
14495without specifying any of the files directly:
14496
14497#+begin_src sh
14498b2 sync /home/<user>/ "b2://<bucketName>/home/<user>"
14499#+end_src
14500
14501* Caveats
14502
14503** Timing of Updates to the Web GUI
14504
14505When performing actions over a bucket, there is a slight delay in the
14506web GUI when inspecting a bucket or its file. Note that simple actions
14507such as uploading or deleting files may have a delay of a few minutes up
14508to 24 hours. In my experience (<10 GB and ~20,000 files), any actions
14509took only a few minutes to update across clients.
14510
14511** Symlinks
14512
14513Note that symlinks are resolved by b2, so if you have a link from
14514=/home/<user>/nas-storage= that symlinks out to a =/mnt/nas-storage=
14515folder that has 10TB of data, =b2= will resolve that link and start
14516uploading all 10TB of data linked within the folder.
14517
14518If you're not sure if you have any symlinks, a symlink will look like
14519this (note the =->= symbol):
14520
14521#+begin_src sh
14522> ls -lha
14523lrwxrwxrwx 1 root root 20 Jun 28 13:32 nas -> /mnt/nas-storage/
14524#+end_src
14525
14526You can recursively find symlink in a path with the following command:
14527
14528#+begin_src sh
14529ls -lR /path/to/search | grep '^l'
14530#+end_src
14531#+date: <2023-06-30 Fri 00:00:00>
14532#+title: Self-Hosting Guide: Voyager for Lemmy
14533#+description: Instructions for building, running, and reverse proxying Voyager, a Lemmy web client, on a server environment utilizing Docker and Nginx components.
14534#+slug: self-hosting-voyager
14535
14536* Installation Guide
14537
14538[[https://github.com/aeharding/voyager][Voyager]] is a mobile-first
14539Lemmy web client, based on iOS design standards. It follows very closely
14540to Apollo's design.
14541
14542This post is a guide showing how I was able to build and launch my own
14543instance of Voyager via Docker Compose.
14544
14545** Clone the Repository
14546
14547Start by cloning the repository and entering it:
14548
14549#+begin_src sh
14550git clone https://github.com/aeharding/voyager
14551cd voyager
14552#+end_src
14553
14554** Build the Image
14555
14556With this repository, you can build the image yourself without any
14557further configuration. When complete, it'll give you the image ID for
14558you to run.
14559
14560#+begin_src sh
14561sudo docker build .
14562# Successfully built 5f00723cb5be
14563#+end_src
14564
14565With the image ID above, you can run the container and pass the
14566requested port =5314= through or use a custom port, if you wish.
14567
14568You can also set the =CUSTOM_LEMMY_SERVERS= environment variable if you
14569want to add to the default suggested login servers. This must be set
14570with a comma separated list of suggested servers. The first instance in
14571the list will be the default view for logged-out users.
14572
14573I will be using a =docker-compose.yml= file to run this container,
14574instead of a =docker run= command.
14575
14576#+begin_src sh
14577nano docker-compose.yml
14578#+end_src
14579
14580#+begin_src conf
14581version: "2"
14582services:
14583 voyager:
14584 image: 5f00723cb5be
14585 restart: always
14586 ports:
14587 - "<custom_port>:5314"
14588 environment:
14589 - CUSTOM_LEMMY_SERVERS=lemmy.dbzer0.com,lemmy.world,lemmy.ml,beehaw.org
14590#+end_src
14591
14592#+begin_src sh
14593sudo docker-compose up -d
14594#+end_src
14595
14596The web app will now be available at the following address:
14597=<machine_ip>:<custom_port>=. If you are running it on your local
14598device, try =localhost:<custom_port>=.
14599
14600** Reverse Proxy
14601
14602If you want to visit this app via an external URL or domain name, you'll
14603need to set up a reverse proxy. The example below uses Nginx as a
14604reverse proxy.
14605
14606Simply create the configuration file, paste the contents below, save the
14607file, symlink the file, and restart Nginx.
14608
14609#+begin_src sh
14610sudo nano /etc/nginx/sites-available/voyager
14611#+end_src
14612
14613#+begin_src conf
14614server {
14615 if ($host ~ ^[^.]+\.example\.com$) {
14616 return 301 https://$host$request_uri;
14617 }
14618
14619 listen [::]:80;
14620 listen 80;
14621 server_name voyager.example.com;
14622 return 404;
14623}
14624
14625server {
14626 listen [::]:443 ssl http2;
14627 listen 443 ssl http2;
14628 server_name voyager.example.com;
14629 access_log /var/log/nginx/voyager.access.log;
14630 error_log /var/log/nginx/voyager.error.log;
14631
14632 location / {
14633 proxy_http_version 1.1;
14634 proxy_pass http://localhost:5314;
14635 proxy_set_header Host $host;
14636 }
14637
14638 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
14639 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
14640 include /etc/letsencrypt/options-ssl-nginx.conf;
14641 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
14642}
14643#+end_src
14644
14645#+begin_src sh
14646sudo ln sudo ln -s /etc/nginx/sites-available/voyager /etc/nginx/sites-enabled/voyager
14647sudo systemctl restart nginx.service
14648#+end_src
14649
14650The site will now be available at the =server_name= you specified above!
14651
14652You can visit my instance at
14653[[https://voyager.cleberg.net][voyager.cleberg.net]] for an example.
14654#+date: <2023-07-12 Wed 00:00:00>
14655#+title: Enabling Local Network Access with Mullvad WireGuard Configs
14656#+description: Detailed procedure to modify Mullvad Wireguard VPN configuration files and iptables rules to allow Local Area Network access while maintaining VPN connectivity.
14657#+slug: wireguard-lan
14658
14659* Download Configuration Files from Mullvad
14660
14661To begin, you'll need
14662[[https://mullvad.net/account/wireguard-config][Wireguard configuration
14663files from Mullvad]]. You can choose any of the options as you download
14664them. For example, I enabled the kill switch, selected all countries,
14665and selected a few content filters.
14666
14667Once downloaded, unzip the files and move them to the Wireguard folder
14668on your system.
14669
14670#+begin_src sh
14671cd ~/Downloads
14672unzip mullvad_wireguard_linux_all_all.zip
14673doas mv *.conf /etc/wireguard/
14674#+end_src
14675
14676** Configuration File Layout
14677
14678The default configuration files will look something like this:
14679
14680#+begin_src conf
14681[Interface]
14682# Device: <redacted>
14683PrivateKey = <redacted>
14684Address = <redacted>
14685DNS = <redacted>
14686PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT && ip6tables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
14687PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT && ip6tables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
14688
14689[Peer]
14690PublicKey = <redacted>
14691AllowedIPs = <redacted>
14692Endpoint = <redacted>
14693#+end_src
14694
14695#+begin_quote
14696Note: If you didn't select the kill switch option, you won't see the
14697=PostUp= and =PreDown= lines. In this case, you'll need to modify the
14698script below to simply append those lines to the =[Interface]= block.
14699#+end_quote
14700
14701* Editing the Configuration Files
14702
14703Once you have the files, you'll need to edit them and replace the
14704=PostUp= and =PreDown= lines to enable LAN access.
14705
14706I recommend that you do this process as root, since you'll need to be
14707able to access files in =/etc/wireguard=, which are generally owned by
14708root. You can also try using =sudo= or =doas=, but I didn't test that
14709scenario so you may need to adjust, as necessary.
14710
14711#+begin_src sh
14712su
14713#+end_src
14714
14715Create the Python file that we'll be using to update the Wireguard
14716configuration files.
14717
14718#+begin_src sh
14719nano replace.py
14720#+end_src
14721
14722Within the Python file, copy and paste the logic below. This script will
14723open a directory, loop through every configuration file within the
14724directory, and replace the =PostUp= and =PreDown= lines with the new
14725LAN-enabled iptables commands.
14726
14727#+begin_quote
14728Note: If your LAN is on a subnet other than =192.168.1.0/24=, you'll
14729need to update the Python script below appropriately.
14730#+end_quote
14731
14732#+begin_src python
14733import os
14734import fileinput
14735
14736print("--- starting ---")
14737
14738dir = "/etc/wireguard/"
14739
14740for file in os.listdir(dir):
14741 print(os.path.join(dir, file))
14742 for line in fileinput.input(os.path.join(dir, file), inplace=True):
14743 if "PostUp" in line:
14744 print("PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL ! -d 192.168.1.0/24 -j REJECT && ip6tables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT")
14745 elif "PreDown" in line:
14746 print("PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL ! -d 192.168.1.0/24 -j REJECT && ip6tables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT")
14747 else:
14748 print(line, end="")
14749
14750print("--- done ---")
14751#+end_src
14752
14753Once you're done, save and close the file. You can now run the Python
14754script and watch as each file is updated.
14755
14756#+begin_src sh
14757python3 replace.py
14758#+end_src
14759
14760To confirm it worked, you can =cat= one of the configuration files to
14761inspect the new logic and connect to one to test it out.
14762
14763#+begin_src sh
14764cat /etc/wireguard/us-chi-wg-001.conf
14765#+end_src
14766
14767The configuration files should now look like this:
14768
14769#+begin_src conf
14770[Interface]
14771# Device: <redacted>
14772PrivateKey = <redacted>
14773Address = <redacted>
14774DNS = <redacted>
14775PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL ! -d 192.168.1.0/24 -j REJECT && ip6tables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
14776PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL ! -d 192.168.1.0/24 -j REJECT && ip6tables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
14777
14778[Peer]
14779PublicKey = <redacted>
14780AllowedIPs = <redacted>
14781Endpoint = <redacted>
14782#+end_src
14783
14784If you connect to a Wireguard interface, such as =us-chi-wg-001=, you
14785can test your SSH functionality and see that it works even while on the
14786VPN.
14787
14788#+begin_src sh
14789wg-quick up us-chi-wg-001
14790ssh user@lan-host
14791#+end_src
14792
14793To confirm your VPN connection, you can curl Mullvad's connection API:
14794
14795#+begin_src sh
14796curl https://am.i.mullvad.net/connected
14797# You are connected to Mullvad (server us-chi-wg-001). Your IP address is <redacted>
14798#+end_src
14799#+date: <2023-07-19 Wed 00:00:00>
14800#+title: Plex Subtitle Error: Transcoder Fails to Start
14801#+description: Technical procedures to identify and correct Plex transcoder failures at startup to maintain uninterrupted media conversion and playback.
14802#+slug: plex-transcoder-errors
14803
14804* Plex Transcoder Error
14805
14806Occasionally, you may see an error in your Plex client that references a
14807failure with the transcoder conversion process. The specific error
14808wording is:
14809
14810#+begin_src txt
14811Conversion failed. The transcoder failed to start up.
14812#+end_src
14813
14814* Debugging the Cause
14815
14816In order to get a better look at what is causing the error, I'm going to
14817observe the Plex console while the error occurs. To do this, open the
14818Plex web client, go to =Settings= > =Manage= > =Console=. Now, try to
14819play the title again and watch to see which errors occur.
14820
14821In my case, you can see the errors below are related to a subtitle file
14822(=.srt=) causing the transcoder to crash.
14823
14824#+begin_src txt
14825Jul 19, 2023 16:49:34.945 [140184571120440] Error — Couldn't find the file to stream: /movies/Movie Title (2021)/Movie Title (2021).srt
14826Jul 19, 2023 16:49:34.947 [140184532732728] Error — [Req#7611/Transcode/42935159-67C1-4192-9336-DDC6F7BC9330] Error configuring transcoder: TPU: Failed to download sub-stream to temporary file
14827Jul 19, 2023 16:49:35.225 [140184532732728] Warning — [Req#760d/Transcode] Got a request to stop a transcode session without a valid session GUID.
14828Jul 19, 2023 16:49:45.561 [140184532732728] Info — [Req#7648] AutoUpdate: no updates available
14829Jul 19, 2023 16:51:23.402 [140184510081848] Info — Library section 1 (Movies) will be updated because of a change in "/movies/Movie Title (2021)/Movie Title (2021).srt"
14830#+end_src
14831
14832* Solving the Error
14833
14834In my case, I simply removed the subtitle file because it was not
14835critical to keep. You may also avoid this by turning off subtitles if
14836you don't want to delete the file.
14837
14838#+begin_src sh
14839rm -rf "/movies/Movie Title (2021)/Movie Title (2021).srt"
14840#+end_src
14841
14842Once the subtitle is removed from the directory or subtitles are turned
14843off, try to play the title again. At this point, it should play without
14844error. If not, reopen or refresh your Plex client and the Plex server:
14845
14846#+begin_src sh
14847sudo systemctl restart plexmediaserver.service
14848#+end_src
14849
14850* Related Discussion
14851
14852Looking at
14853[[https://forums.plex.tv/t/subtitles-crashing-plex-transcoder-samsung-q80-tv-with-or-without-hardware-transcode/741441/2][a
14854related Plex forum post]], it seems that =.srt= subtitles are the core
14855issue here. However, Plex does not seem to have a resolution that fixes
14856these errors.
14857
14858Unfortunately, I would suggest converting subtitle formats, burning the
14859subtitles into the title, or simply removing subtitles when they cause
14860crashes.
14861#+date: <2023-08-18 Fri 00:00:00>
14862#+title: Agile Auditing Framework: Using Scrum and Kanban in Audit Projects
14863#+description: Detailed examination of Agile auditing methodologies, including Scrum and Kanban frameworks, with practical steps for application in audit processes.
14864#+slug: agile-auditing
14865
14866* What is Agile Auditing?
14867
14868[[https://en.wikipedia.org/wiki/Agile_software_development][Agile]], the
14869collaborative philosophy behind many software development methods, has
14870been picking up steam as a beneficial tool to use in the external and
14871internal auditing world.
14872
14873This blog post will walk through commonly used terms within Agile,
14874Scrum, and Kanban in order to translate these terms and roles into
14875audit-specific terms.
14876
14877Whether your team is in charge of a financial statement audit, an
14878attestation (SOC 1, SOC 2, etc.), or a unique internal audit, the terms
14879used throughout this post should still apply.
14880
14881* Agile
14882
14883To start, I'll take a look at Agile.
14884
14885#+begin_quote
14886The Agile methodology is a project management approach that involves
14887breaking the project into phases and emphasizes continuous collaboration
14888and improvement. Teams follow a cycle of planning, executing, and
14889evaluating.
14890#+end_quote
14891
14892While this approach may seem familiar to what audit teams have
14893historically done, an audit team must make distinct changes in their
14894mentality and how they approach and manage a project.
14895
14896** Agile Values
14897
14898The Agile Manifesto, written in 2001 at a summit in Utah, contain a set
14899of four main values that comprise the Agile approach:
14900
149011. Individuals and interactions over processes and tools.
149022. Working software over comprehensive documentation.
149033. Customer collaboration over contract negotiation.
149044. Responding to change over following a plan.
14905
14906Beyond the four values,
14907[[https://agilemanifesto.org/principles.html][twelve principles]] were
14908also written as part of the summit.
14909
14910In order to relate these values to an audit or attestation engagement,
14911we need to shift the focus from software development to the main goal of
14912an engagement: completing sufficient audit testing to address to
14913relevant risks over the processes and controls at hand.
14914
14915Audit Examples:
14916
14917- Engagement teams must value the team members, client contacts, and
14918 their interactions over the historical processes and tools that have
14919 been used.
14920- Engagement teams must value a final report that contains sufficient
14921 audit documentation over excessive documentation or scope creep.
14922- Engagement teams must collaborate with the audit clients as much as
14923 feasible to ensure that both sides are constantly updated with current
14924 knowledge of the engagement's status and any potential findings,
14925 rather than waiting for pre-set meetings or the end of the engagement
14926 to communicate.
14927- Engagement teams must be able to respond to change in an engagement's
14928 schedule, scope, or environment to ensure that the project is
14929 completed in a timely manner and that all relevant areas are tested.
14930 - In terms of an audit department's portfolio, they must be able to
14931 respond to changes in their company's or client's environment and be
14932 able to dynamically change their audit plan accordingly.
14933
14934* Scrum
14935
14936The above section discusses the high-level details of the Agile
14937philosophy and how an audit team can potentially mold that mindset into
14938the audit world, but how does a team implement these ideas?
14939
14940There are many methods that use an Agile mindset, but I prefer
14941[[https://en.wikipedia.org/wiki/Scrum_(software_development)][Scrum]].
14942Scrum is a framework based on Agile that enables a team to work through
14943a project through a series of roles, ceremonies, artifacts, and values.
14944
14945Let's dive into each of these individually.
14946
14947** Scrum Team
14948
14949A scrum project is only as good as the team running the project.
14950Standard scrum teams are separated into three distinct areas:
14951
149521. *Product Owner (Client Contact)*: The client contact is the audit
14953 equivalent of the product owner in Scrum. They are responsible for
14954 partnering with the engagement or audit team to ensure progress is
14955 being made, priorities are established, and clear guidance is given
14956 when questions or findings arise within each sprint.
149572. *Scrum Master (Engagement Lead)*: The engagement or audit team lead
14958 is responsible for coaching the team and the client contact on the
14959 scrum process, tracking team progress against plan, scheduling
14960 necessary resources, and helping remove obstacles.
149613. *Scrum Developers (Engagement Members)*: The engagement or audit team
14962 is the set of team members responsible for getting the work done.
14963 These team members will work on each task, report progress, resolve
14964 obstacles, and collaborate with other team members and the client
14965 contact to ensure goals are being met.
14966
14967** Scrum Ceremonies
14968
14969Scrum ceremonies are events that are performed on a regular basis.
14970
149711. *Sprint Planning*: The team works together to plan the upcoming
14972 sprint goal and which user stories (tasks) will be added to the
14973 sprint to achieve that goal.
149742. *Sprint*: The time period, typically at least one week and no more
14975 than one month in length, where the team works on the stories and
14976 anything in the backlog.
149773. *Daily Scrum*: A very short meeting held each day, typically 15
14978 minutes, to quickly emphasize alignment on the sprint goal and plan
14979 the next 24 hours. Each team member may share what they did the day
14980 before, what they'll do today, and any obstacles to their work.
149814. *Sprint Review*: At the end of each sprint, the team will gather and
14982 discuss the progress, obstacles, and backlog from the previous
14983 sprint.
149845. *Sprint Retrospective*: More specific than the sprint review, the
14985 retrospective is meant to discuss what worked and what did not work
14986 during the sprint. This may be processes, tools, people, or even
14987 things related to the Scrum ceremonies.
14988
14989One additional ceremony that may be applicable is organizing the
14990backlog. This is typically the responsibility of the engagement leader
14991and is meant to prioritize and clarify what needs to be done to complete
14992items in the backlog.
14993
14994** Artifacts
14995
14996While artifacts are generally not customizable in the audit world (i.e.,
14997each control test must include some kind of working paper with evidence
14998supporting the test results), I wanted to include some quick notes on
14999associating scrum artifact terms with an audit.
15000
150011. *Product Backlog*: This is the overall backlog of unfinished audit
15002 tasks from all prior sprints.
150032. *Sprint Backlog*: This is the backlog of unfinished audit tasks from
15004 one individual sprint.
150053. *Increment*: This is the output of each sprint - generally this is
15006 best thought of as any documentation prepared during the sprint, such
15007 as risk assessments, control working papers, deficiency analysis,
15008 etc.
15009
15010* Kanban
15011
15012Last but not least, Kanban is a methodology that relies on boards to
15013categorize work into distinct, descriptive categories that allow an
15014agile or scrum team to effectively plan the work of a sprint or project.
15015
15016See Atlassian's [[https://www.atlassian.com/agile/kanban][Kanban]] page
15017for more information.
15018#+date: <2023-09-15 Fri 00:00:00>
15019#+title: Self-Hosting Guide: GitWeb
15020#+description: Stepwise instructions for deploying GitWeb on Linux servers using Nginx, configuring fastcgi, and managing git repositories effectively.
15021#+slug: self-hosting-gitweb
15022
15023* Overview
15024
15025[[https://git-scm.com/book/en/v2/Git-on-the-Server-GitWeb][GitWeb]] is a
15026simple web-based visualizer for git repositories. By default, GitWeb
15027will only run with the =lighttpd= or =webrick= web servers.
15028
15029However, this guide will show you how to keep GitWeb running in the
15030background and display information for all repositories in a chosen
15031directory.
15032
15033* Install Dependencies
15034
15035To start, you'll need install the following packages:
15036
15037#+begin_src sh
15038sudo apt install git gitweb fcgiwrap nginx
15039#+end_src
15040
15041* Configure Nginx
15042
15043Once installed, create an Nginx configuration file.
15044
15045#+begin_src sh
15046sudo nano /etc/nginx/sites-available/git.example.com
15047#+end_src
15048
15049#+begin_src conf
15050server {
15051 listen 80;
15052 server_name example.com;
15053
15054 location /index.cgi {
15055 root /usr/share/gitweb/;
15056 include fastcgi_params;
15057 gzip off;
15058 fastcgi_param SCRIPT_NAME $uri;
15059 fastcgi_param GITWEB_CONFIG /etc/gitweb.conf;
15060 fastcgi_pass unix:/var/run/fcgiwrap.socket;
15061 }
15062
15063 location / {
15064 root /usr/share/gitweb/;
15065 index index.cgi;
15066 }
15067}
15068#+end_src
15069
15070To make the configuration active, you need to symlink it and then
15071restart Nginx.
15072
15073#+begin_src sh
15074sudo ln -s /etc/nginx/sites-available/git.example.com /etc/nginx/sites-enabled/git.example.com
15075sudo systemctl restart nginx.service
15076#+end_src
15077
15078The GitWeb application should now be available via the URL you set in
15079the Nginx configuration above.
15080
15081* Customize GitWeb
15082
15083If you need to, you can customize many things about Gitweb by editing
15084the [[https://git-scm.com/docs/gitweb.conf][gitweb.conf]] file.
15085
15086#+begin_src sh
15087sudo nano /etc/gitweb.conf
15088#+end_src
15089#+date: <2023-09-19 Tue 00:00:00>
15090#+title: Auditing User Privileges in Oracle, SQL Server, and MySQL
15091#+description: Collection of SQL queries designed for auditing user access parameters on Oracle, Microsoft SQL Server, and MySQL database systems.
15092#+slug: audit-sql-scripts
15093
15094* Overview
15095
15096When you have to scope a database into your engagement, you may be
15097curious how to best extract the information from the database. While
15098there are numerous different methods to extract this type of
15099information, I'm going to show an example of how to gather all users and
15100privileges from three main database types: Oracle, Microsoft SQL, and
15101MySQL.
15102
15103* Oracle
15104
15105You can use the following SQL script to see all users and their
15106privileges in an Oracle database:
15107
15108#+begin_src sql
15109SELECT
15110 grantee AS "User",
15111 privilege AS "Privilege"
15112FROM
15113 dba_sys_privs
15114WHERE
15115 grantee IN (SELECT DISTINCT grantee FROM dba_sys_privs)
15116UNION ALL
15117SELECT
15118 grantee AS "User",
15119 privilege AS "Privilege"
15120FROM
15121 dba_tab_privs
15122WHERE
15123 grantee IN (SELECT DISTINCT grantee FROM dba_tab_privs);
15124#+end_src
15125
15126This script queries the =dba_sys_privs= and =dba_tab_privs= views to
15127retrieve system and table-level privileges respectively. It then
15128combines the results using =UNION ALL= to show all users and their
15129associated privileges. Please note that this method does not extract
15130information from the =dba_role_privs= table - use the method below for
15131that data.
15132
15133Please note that you might need appropriate privileges (e.g., DBA
15134privileges) to access these views, and you should exercise caution when
15135querying system tables in a production Oracle database.
15136
15137** Alternative Oracle Query
15138
15139You can also extract each table's information separately and perform
15140processing outside the database to explore and determine the information
15141necessary for the audit:
15142
15143#+begin_src sql
15144SELECT ** FROM sys.dba_role_privs;
15145SELECT ** FROM sys.dba_sys_privs;
15146SELECT ** FROM sys.dba_tab_privs;
15147SELECT ** FROM sys.dba_users;
15148#+end_src
15149
15150* Microsoft SQL
15151
15152You can use the following SQL script to see all users and their
15153privileges in a Microsoft SQL Server database
15154([[https://stackoverflow.com/a/30040784][source]]):
15155
15156#+begin_src sql
15157/*
15158Security Audit Report
151591) List all access provisioned to a sql user or windows user/group directly
151602) List all access provisioned to a sql user or windows user/group through a database or application role
151613) List all access provisioned to the public role
15162
15163Columns Returned:
15164UserName : SQL or Windows/Active Directory user account. This could also be an Active Directory group.
15165UserType : Value will be either 'SQL User' or 'Windows User'. This reflects the type of user defined for the
15166 SQL Server user account.
15167DatabaseUserName: Name of the associated user as defined in the database user account. The database user may not be the
15168 same as the server user.
15169Role : The role name. This will be null if the associated permissions to the object are defined at directly
15170 on the user account, otherwise this will be the name of the role that the user is a member of.
15171PermissionType : Type of permissions the user/role has on an object. Examples could include CONNECT, EXECUTE, SELECT
15172 DELETE, INSERT, ALTER, CONTROL, TAKE OWNERSHIP, VIEW DEFINITION, etc.
15173 This value may not be populated for all roles. Some built in roles have implicit permission
15174 definitions.
15175PermissionState : Reflects the state of the permission type, examples could include GRANT, DENY, etc.
15176 This value may not be populated for all roles. Some built in roles have implicit permission
15177 definitions.
15178ObjectType : Type of object the user/role is assigned permissions on. Examples could include USER_TABLE,
15179 SQL_SCALAR_FUNCTION, SQL_INLINE_TABLE_VALUED_FUNCTION, SQL_STORED_PROCEDURE, VIEW, etc.
15180 This value may not be populated for all roles. Some built in roles have implicit permission
15181 definitions.
15182ObjectName : Name of the object that the user/role is assigned permissions on.
15183 This value may not be populated for all roles. Some built in roles have implicit permission
15184 definitions.
15185ColumnName : Name of the column of the object that the user/role is assigned permissions on. This value
15186 is only populated if the object is a table, view or a table value function.
15187,*/
15188
15189--List all access provisioned to a sql user or windows user/group directly
15190SELECT
15191 [UserName] = CASE princ.[type]
15192 WHEN 'S' THEN princ.[name]
15193 WHEN 'U' THEN ulogin.[name] COLLATE Latin1_General_CI_AI
15194 END,
15195 [UserType] = CASE princ.[type]
15196 WHEN 'S' THEN 'SQL User'
15197 WHEN 'U' THEN 'Windows User'
15198 END,
15199 [DatabaseUserName] = princ.[name],
15200 [Role] = null,
15201 [PermissionType] = perm.[permission_name],
15202 [PermissionState] = perm.[state_desc],
15203 [ObjectType] = obj.type_desc,--perm.[class_desc],
15204 [ObjectName] = OBJECT_NAME(perm.major_id),
15205 [ColumnName] = col.[name]
15206FROM
15207 --database user
15208 sys.database_principals princ
15209LEFT JOIN
15210 --Login accounts
15211 sys.login_token ulogin on princ.[sid] = ulogin.[sid]
15212LEFT JOIN
15213 --Permissions
15214 sys.database_permissions perm ON perm.[grantee_principal_id] = princ.[principal_id]
15215LEFT JOIN
15216 --Table columns
15217 sys.columns col ON col.[object_id] = perm.major_id
15218 AND col.[column_id] = perm.[minor_id]
15219LEFT JOIN
15220 sys.objects obj ON perm.[major_id] = obj.[object_id]
15221WHERE
15222 princ.[type] in ('S','U')
15223UNION
15224--List all access provisioned to a sql user or windows user/group through a database or application role
15225SELECT
15226 [UserName] = CASE memberprinc.[type]
15227 WHEN 'S' THEN memberprinc.[name]
15228 WHEN 'U' THEN ulogin.[name] COLLATE Latin1_General_CI_AI
15229 END,
15230 [UserType] = CASE memberprinc.[type]
15231 WHEN 'S' THEN 'SQL User'
15232 WHEN 'U' THEN 'Windows User'
15233 END,
15234 [DatabaseUserName] = memberprinc.[name],
15235 [Role] = roleprinc.[name],
15236 [PermissionType] = perm.[permission_name],
15237 [PermissionState] = perm.[state_desc],
15238 [ObjectType] = obj.type_desc,--perm.[class_desc],
15239 [ObjectName] = OBJECT_NAME(perm.major_id),
15240 [ColumnName] = col.[name]
15241FROM
15242 --Role/member associations
15243 sys.database_role_members members
15244JOIN
15245 --Roles
15246 sys.database_principals roleprinc ON roleprinc.[principal_id] = members.[role_principal_id]
15247JOIN
15248 --Role members (database users)
15249 sys.database_principals memberprinc ON memberprinc.[principal_id] = members.[member_principal_id]
15250LEFT JOIN
15251 --Login accounts
15252 sys.login_token ulogin on memberprinc.[sid] = ulogin.[sid]
15253LEFT JOIN
15254 --Permissions
15255 sys.database_permissions perm ON perm.[grantee_principal_id] = roleprinc.[principal_id]
15256LEFT JOIN
15257 --Table columns
15258 sys.columns col on col.[object_id] = perm.major_id
15259 AND col.[column_id] = perm.[minor_id]
15260LEFT JOIN
15261 sys.objects obj ON perm.[major_id] = obj.[object_id]
15262UNION
15263--List all access provisioned to the public role, which everyone gets by default
15264SELECT
15265 [UserName] = '{All Users}',
15266 [UserType] = '{All Users}',
15267 [DatabaseUserName] = '{All Users}',
15268 [Role] = roleprinc.[name],
15269 [PermissionType] = perm.[permission_name],
15270 [PermissionState] = perm.[state_desc],
15271 [ObjectType] = obj.type_desc,--perm.[class_desc],
15272 [ObjectName] = OBJECT_NAME(perm.major_id),
15273 [ColumnName] = col.[name]
15274FROM
15275 --Roles
15276 sys.database_principals roleprinc
15277LEFT JOIN
15278 --Role permissions
15279 sys.database_permissions perm ON perm.[grantee_principal_id] = roleprinc.[principal_id]
15280LEFT JOIN
15281 --Table columns
15282 sys.columns col on col.[object_id] = perm.major_id
15283 AND col.[column_id] = perm.[minor_id]
15284JOIN
15285 --All objects
15286 sys.objects obj ON obj.[object_id] = perm.[major_id]
15287WHERE
15288 --Only roles
15289 roleprinc.[type] = 'R' AND
15290 --Only public role
15291 roleprinc.[name] = 'public' AND
15292 --Only objects of ours, not the MS objects
15293 obj.is_ms_shipped = 0
15294ORDER BY
15295 princ.[Name],
15296 OBJECT_NAME(perm.major_id),
15297 col.[name],
15298 perm.[permission_name],
15299 perm.[state_desc],
15300 obj.type_desc--perm.[class_desc]
15301#+end_src
15302
15303* MySQL
15304
15305You can use the following SQL script to see all users and their
15306privileges in a MySQL database:
15307
15308#+begin_src sh
15309mysql -u root -p
15310#+end_src
15311
15312Find all users and hosts with access to the database:
15313
15314#+begin_src sql
15315SELECT ** FROM information_schema.user_privileges;
15316#+end_src
15317
15318This script retrieves user information and their associated
15319database-level privileges from the =information_schema.user_privileges=
15320table in MySQL. It lists various privileges such as SELECT, INSERT,
15321UPDATE, DELETE, CREATE, and more for each user and database combination.
15322
15323Please note that you may need appropriate privileges (e.g., =SELECT=
15324privileges on =information_schema.user_privileges=) to access this
15325information in a MySQL database. Additionally, some privileges like
15326GRANT OPTION, EXECUTE, EVENT, and TRIGGER may not be relevant for all
15327users and databases.
15328
15329** Alternative MySQL Query
15330
15331You can also grab individual sets of data from MySQL if you prefer to
15332join them after extraction. I have marked the queries below with
15333=SELECT ...= and excluded most =WHERE= clauses for brevity. You should
15334determine the relevant privileges in-scope and query for those
15335privileges to reduce the length of time to query.
15336
15337#+begin_src sql
15338-- Global Permissions
15339SELECT ... FROM mysql.user;
15340
15341-- Database Permissions
15342SELECT ... FROM mysql.db
15343WHERE db = @db_name;
15344
15345-- Table Permissions
15346SELECT ... FROM mysql.tables
15347WHERE db = @db_name;
15348
15349-- Column Permissions
15350SELECT ... FROM mysql.columns_priv
15351WHERE db = @db_name;
15352
15353-- Password Configuration
15354SHOW GLOBAL VARIABLES LIKE 'validate_password%';
15355SHOW VARIABLES LIKE 'validate_password%';
15356#+end_src
15357#+date: <2023-10-04 Wed 00:00:00>
15358#+title: Digital Minimalism: Reducing Distractions
15359#+description: Practical guide for minimizing digital clutter and optimizing work focus by applying digital minimalism tactics.
15360#+slug: digital-minimalism
15361
15362I've written [[https://cleberg.net/wiki/#digital-garden][a note about minimalism]]
15363before, but I wanted to dedicate some time to reflect on digital
15364minimalism and how I've been able to minimize the impact of digital
15365devices in my life.
15366
15367#+begin_quote
15368These changes crept up on us and happened fast, before we had a chance
15369to step back and ask what we really wanted out of the rapid advances of
15370the past decade. We added new technologies to the periphery of our
15371experience for minor reasons, then woke one morning to discover that
15372they had colonized the core of our daily life. We didn't, in other
15373words, sign up for the digital world in which we're currently
15374entrenched; we seem to have stumbled backward into it.
15375
15376/(Digital Minimalism, 2019)/
15377#+end_quote
15378
15379* The Principles of Digital Minimalism
15380
15381As noted in Cal Newport's book, /Digital Minimalism/, there are three
15382main principles to digital minimalism that I tend to agree with:
15383
153841. Clutter is costly.
15385 - Digital minimalists recognize that cluttering their time and
15386 attention with too many devices, apps, and services creates an
15387 overall negative cost that can swamp the small benefits that each
15388 individual item provides in isolation.
153892. Optimization is important.
15390 - Digital minimalists believe that deciding a particular technology
15391 supports something they value is only the first step. To truly
15392 extract its full potential benefit, it's necessary to think
15393 carefully about how they'll use the technology.
153943. Intentionality is satisfying.
15395 - Digital minimalists derive significant satisfaction from their
15396 general commitment to being more intentional about how they engage
15397 with new technologies. This source of satisfaction is independent
15398 of the specific decisions they make and is one of the biggest
15399 reasons that minimalism tends to be immensely meaningful to its
15400 practitioners.
15401
15402* Taking Action
15403
15404In order to put the logic into practice, I've created a few new habits
15405and continued performing old habits that are working well:
15406
15407** Using Devices With Intention
15408
15409- I already rarely use "social media", mostly limited to forums such as
15410 Hacker News and Tildes, so I've just tweaked my behavior to stop
15411 looking for content in those places when I'm bored.
15412- Use devices with intention. Each time I pick up a digital device,
15413 there should be an intention to use the device to improve my current
15414 situation. No more endless scrolling or searching for something to
15415 interest me.
15416
15417** Prevent Distractions
15418
15419- Disable (most) notifications on all devices. I spent 15-30 minutes
15420 going through the notifications on my phone, watch, and computer to
15421 ensure that only a select few apps have the ability to interrupt me:
15422 Calendar, Messages, Phone, Reminders, & Signal.
15423- Disable badges for any apps except the ones mentioned in the bullet
15424 above.
15425- Set-up focus profiles across devices so that I can enable different
15426 modes, such as Personal when I only want to see notifications from
15427 people I care about or Do Not Disturb, where absolutely nothing can
15428 interrupt me.
15429- Clean up my home screens. This one was quite easy as I already
15430 maintain a minimalist set-up, but I went extreme by limiting my phone
15431 to just eight apps on the home screen and four in the dock. If I need
15432 another app, I'll have to search or use the app library.
15433- Remove the work profile from my phone. This was a tough decision as
15434 having my work profile on my device definitely makes my life easier at
15435 times, but it also has quite a negative effect when I'm "always
15436 online" and can see the notifications and team activity 24/7. I
15437 believe creating a distinct barrier between my work and personal
15438 devices will be beneficial in the end.
15439
15440** Creating Alternative Activities
15441
15442This is the most difficult piece, as most of my hobbies and interests
15443lie in the digital world. However, I'm making a concerted effort to put
15444devices down unless necessary and force myself to perform other
15445activities in the physical world instead.
15446
15447I've started with a few basics that are always readily available to me:
15448
15449- Do a chore, such as organizing or cleaning.
15450- Read a book, study a piece of art, etc.
15451- Exercise or get outdoors.
15452- Participate in a hobby, such as photography, birding, disc golf, etc.
15453- Let yourself be bored and wander into creativity.
15454
15455* Making Progress
15456
15457I'll be taking notes as I continue down this journey and hope to see
15458positive trends. I've always been a minimalist in the physical world and
15459it feels refreshing to filter out the clutter that has come to dominate
15460my digital life over the years.
15461
15462I'm excited to see where this journey leads.
15463#+date: <2023-10-11 Wed 00:00:00>
15464#+title: Self-Hosting Guide: Authelia
15465#+description: Instructional guide for setting up Authelia to provide secure two-factor authentication and access control in self-hosted environments.
15466#+slug: self-hosting-authelia
15467
15468* Overview
15469
15470[[https://www.authelia.com/][Authelia]] is an open-source authentication
15471service that allows you to place a portal between end users on the
15472internet and self-hosted services on your server.
15473
15474You can require one factor (username+password) or two factor
15475authentication for any such user before allowing them to access a
15476specific service on your domain.
15477
15478This guide will walk through a standard installation of Authelia for
15479=example.com=, using =auth.example.com= as Authelia's authentication
15480domain and =teddit.example.com= as the website we want to protect behind
15481the authentication portal.
15482
15483* Prerequisites
15484
15485This guide assumes you have the following already set-up:
15486
15487- A registered domain with DNS pointing to your server.
15488- A subdomain for Authelia (=auth.example.com=) and a subdomain to
15489 protect via Authelia (=app.example.com=).
15490- A working Nginx web server.
15491- Docker and docker-compose installed.
15492
15493* Installation
15494
15495This guide will walk through each installation step one-by-one, starting
15496with the container and finishing by cleaning up external access via an
15497Nginx reverse proxy.
15498
15499** Docker-Compose
15500
15501To start, create a directory for Authelia and create a
15502=docker-compose.yml= file.
15503
15504#+begin_src sh
15505mkdir ~/authelia
15506nano ~/authelia/docker-compose.yml
15507#+end_src
15508
15509Within this file, paste the following content. If you prefer a different
15510local port, modify the port on the left side of the colon on the
15511=9091:9091= line. Be sure to modify the =TZ= variable to your timezone.
15512
15513#+begin_src yml
15514version: "3.3"
15515
15516services:
15517 authelia:
15518 image: authelia/authelia
15519 container_name: authelia
15520 volumes:
15521 - ./config:/config
15522 ports:
15523 - 9091:9091
15524 environment:
15525 - TZ=America/Chicago
15526#+end_src
15527
15528Start the container with docker-compose:
15529
15530#+begin_src sh
15531sudo docker-compose up -d
15532#+end_src
15533
15534After the first start, the container will automatically exit and require
15535you to modify the app's configuration files before continuing. Read on
15536to learn more.
15537
15538** Authelia Configuration
15539
15540To configure Authelia before we restart the container, we need to open
15541the =config= directory and modify the files. Start by editing the
15542=configuration.yml= file, where all of Authelia's settings are stored.
15543
15544My personal preference is to copy the original configuration file to a
15545backup file and edit a fresh copy.
15546
15547#+begin_src sh
15548sudo cp ~/authelia/config/configuration.yml ~/authelia/config/configuration.yml.bk
15549sudo nano ~/authelia/config/configuration.yml
15550#+end_src
15551
15552Within the blank =configuration.yml= file, paste the following
15553information. You will need to make quite a few updates, so be sure to
15554read each line carefully and modify as necessary.
15555
15556The major required changes are:
15557
15558- Any instances of =example.com= should be replaced by your domain.
15559- =jwt_secret= - Use the =pwgen 40 1= command to generate a secret for
15560 yourself.
15561- =access_control= - Set the Authelia domain to bypass here, as well as
15562 any subdomains you want to protect.
15563- =session= > =secret= - Use the =pwgen 40 1= command to generate a
15564 secret for yourself.
15565- =regulation= - Set the variables here to restrict login attempts and
15566 bans.
15567- =storage= > =encryption_key= - Use the =pwgen 40 1= command to
15568 generate a secret for yourself.
15569- =smtp= - If you have access to an SMTP service, set up the information
15570 here to active outgoing emails.
15571
15572#+begin_src yml
15573# yamllint disable rule:comments-indentation
15574---
15575###############################################################################
15576# Authelia Configuration #
15577###############################################################################
15578
15579theme: dark
15580jwt_secret: aiS5iedaiv6eeVaideeLeich5roo6ohvaf3Vee1a # pwgen 40 1
15581
15582default_redirection_url: https://example.com
15583
15584server:
15585 host: 0.0.0.0
15586 port: 9091
15587 path: ""
15588 read_buffer_size: 4096
15589 write_buffer_size: 4096
15590 enable_pprof: false
15591 enable_expvars: false
15592 disable_healthcheck: false
15593 tls:
15594 key: ""
15595 certificate: ""
15596
15597log:
15598 level: debug
15599
15600totp:
15601 issuer: example.com
15602 period: 30
15603 skew: 1
15604
15605authentication_backend:
15606 disable_reset_password: false
15607 refresh_interval: 5m
15608 file:
15609 path: /config/users_database.yml
15610 password:
15611 algorithm: argon2id
15612 iterations: 1
15613 key_length: 32
15614 salt_length: 16
15615 memory: 1024
15616 parallelism: 8
15617
15618access_control:
15619 default_policy: deny
15620 rules:
15621 - domain:
15622 - "auth.example.com"
15623 policy: bypass
15624 - domain: "teddit.example.com"
15625 policy: one_factor
15626
15627session:
15628 name: authelia_session
15629 secret: aiS5iedaiv6eeVaideeLeich5roo6ohvaf3Vee1a # pwgen 40 1
15630 expiration: 3600
15631 inactivity: 300
15632 domain: example.com
15633
15634regulation:
15635 max_retries: 5
15636 find_time: 10m
15637 ban_time: 12h
15638
15639storage:
15640 local:
15641 path: /config/db.sqlite3
15642 encryption_key: aiS5iedaiv6eeVaideeLeich5roo6ohvaf3Vee1a # pwgen 40 1
15643
15644notifier:
15645 disable_startup_check: true
15646 smtp:
15647 username: user@example.com
15648 password: password
15649 host: smtp.example.com
15650 port: 465
15651 sender: user@example.com
15652 identifier: example.com
15653 subject: "[Authelia] {title}"
15654 startup_check_address: user@example.com
15655 disable_require_tls: false
15656 disable_html_emails: true
15657 tls:
15658 skip_verify: false
15659 minimum_version: TLS1.2
15660#+end_src
15661
15662** Authelia Users
15663
15664Next, create the users file for authentication.
15665
15666#+begin_src sh
15667sudo nano ~/authelia/config/users_database.yml
15668#+end_src
15669
15670Within the file, you will need to create an entry for each user that
15671needs access to Authelia. The =my_username= entry will be the username
15672used on the login page.
15673
15674To generate the password, go to [[https://argon2.online][Argon2 Hash
15675Generator]], generate a random salt, and make sure the rest of the
15676settings match the =authentication_backend= section of
15677=configuration.yml= file.
15678
15679#+begin_src yaml
15680users:
15681 my_username:
15682 displayname: "My User"
15683 # Generated at https://argon2.online/ -- match the settings in
15684 # the `authentication_backend` section of configuration.yml
15685 password: ""
15686 email: email@example.com
15687 groups:
15688 - admins
15689 - dev
15690#+end_src
15691
15692Once the app is configured, restart the container from scratch.
15693
15694#+begin_src sh
15695cd ~/authelia
15696sudo docker-compose down && sudo docker-compose up -d
15697#+end_src
15698
15699** Nginx: Authelia Domain
15700
15701Once the container is running and configured, the final step is to
15702configure external access to the server via Nginx reverse proxy.
15703
15704Start by creating the Authelia domain.
15705
15706#+begin_src sh
15707sudo nano /etc/nginx/sites-available/auth
15708#+end_src
15709
15710Within this file, paste the following information and be sure to update
15711=example.com= to your domain. Make sure the =$upstream_authelia=
15712variable matches the location of your Authelia container.
15713
15714#+begin_src conf
15715server {
15716 if ($host ~ ^[^.]+\.example\.com$) {
15717 return 301 https://$host$request_uri;
15718 }
15719
15720 listen [::]:80;
15721 listen 80;
15722 server_name auth.example.com;
15723 return 404;
15724}
15725
15726server {
15727 listen [::]:443 ssl http2;
15728 listen 443 ssl http2;
15729 server_name auth.example.com;
15730 access_log /var/log/nginx/auth.access.log;
15731 error_log /var/log/nginx/auth.error.log;
15732
15733 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
15734 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
15735 include /etc/letsencrypt/options-ssl-nginx.conf;
15736 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
15737
15738 location / {
15739 set $upstream_authelia http://127.0.0.1:9091;
15740 proxy_pass $upstream_authelia;
15741 client_body_buffer_size 128k;
15742
15743 proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
15744
15745 send_timeout 5m;
15746 proxy_read_timeout 360;
15747 proxy_send_timeout 360;
15748 proxy_connect_timeout 360;
15749
15750 proxy_set_header Host $host;
15751 proxy_set_header X-Real-IP $remote_addr;
15752 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
15753 proxy_set_header X-Forwarded-Proto $scheme;
15754 proxy_set_header X-Forwarded-Host $http_host;
15755 proxy_set_header X-Forwarded-Uri $request_uri;
15756 proxy_set_header X-Forwarded-Ssl on;
15757 proxy_redirect http:// $scheme://;
15758 proxy_http_version 1.1;
15759 proxy_set_header Connection "";
15760 proxy_cache_bypass $cookie_session;
15761 proxy_no_cache $cookie_session;
15762 proxy_buffers 64 256k;
15763 }
15764
15765}
15766#+end_src
15767
15768Next, symlink the file and restart Nginx. If there are errors, be sure
15769to resolve those before moving on.
15770
15771#+begin_src sh
15772sudo ln -s /etc/nginx/sites-available/auth /etc/nginx/sites-enabled/auth
15773sudo systemctl restart nginx.service
15774#+end_src
15775
15776** Nginx: Protected Domain(s)
15777
15778Now that Authelia is accessible externally, you need to configure the
15779domain you intend to protect with Authelia. In this example, I'm
15780protecting =teddit.example.com=.
15781
15782Similar to the process above, paste the content and update the relevant
15783variables.
15784
15785#+begin_src sh
15786sudo nano /etc/nginx/sites-available/teddit
15787#+end_src
15788
15789#+begin_src conf
15790server {
15791 if ($host ~ ^[^.]+\.example\.com$) {
15792 return 301 https://$host$request_uri;
15793 }
15794
15795 listen [::]:80;
15796 listen 80;
15797 server_name teddit.example.com;
15798 return 404;
15799}
15800
15801server {
15802 listen [::]:443 ssl http2;
15803 listen 443 ssl http2;
15804 server_name teddit.example.com;
15805 access_log /var/log/nginx/teddit.access.log;
15806 error_log /var/log/nginx/teddit.error.log;
15807
15808 add_header X-Content-Type-Options "nosniff";
15809 add_header X-XSS-Protection "1; mode=block";
15810 add_header X-Frame-Options "DENY";
15811 add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
15812 add_header Referrer-Policy "no-referrer";
15813
15814 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
15815 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
15816 include /etc/letsencrypt/options-ssl-nginx.conf;
15817 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
15818
15819 location /authelia {
15820 internal;
15821 set $upstream_authelia http://127.0.0.1:9091/api/verify;
15822 proxy_pass_request_body off;
15823 proxy_pass $upstream_authelia;
15824 proxy_set_header Content-Length "";
15825
15826 proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
15827 client_body_buffer_size 128k;
15828 proxy_set_header Host $host;
15829 proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
15830 proxy_set_header X-Real-IP $remote_addr;
15831 proxy_set_header X-Forwarded-For $remote_addr;
15832 proxy_set_header X-Forwarded-Proto $scheme;
15833 proxy_set_header X-Forwarded-Host $http_host;
15834 proxy_set_header X-Forwarded-Uri $request_uri;
15835 proxy_set_header X-Forwarded-Ssl on;
15836 proxy_redirect http:// $scheme://;
15837 proxy_http_version 1.1;
15838 proxy_set_header Connection "";
15839 proxy_cache_bypass $cookie_session;
15840 proxy_no_cache $cookie_session;
15841 proxy_buffers 4 32k;
15842
15843 send_timeout 5m;
15844 proxy_read_timeout 240;
15845 proxy_send_timeout 240;
15846 proxy_connect_timeout 240;
15847 }
15848
15849 location / {
15850 set $upstream_teddit http://127.0.0.1:8686;
15851 proxy_pass $upstream_teddit;
15852
15853 auth_request /authelia;
15854 auth_request_set $target_url https://$http_host$request_uri;
15855 auth_request_set $user $upstream_http_remote_user;
15856 auth_request_set $email $upstream_http_remote_email;
15857 auth_request_set $groups $upstream_http_remote_groups;
15858 proxy_set_header Remote-User $user;
15859 proxy_set_header Remote-Email $email;
15860 proxy_set_header Remote-Groups $groups;
15861
15862 error_page 401 =302 https://auth.example.com/?rd=$target_url;
15863
15864 client_body_buffer_size 128k;
15865
15866 proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
15867
15868 send_timeout 5m;
15869 proxy_read_timeout 360;
15870 proxy_send_timeout 360;
15871 proxy_connect_timeout 360;
15872
15873 proxy_set_header Host $host;
15874 proxy_set_header Upgrade $http_upgrade;
15875 proxy_set_header Connection upgrade;
15876 proxy_set_header Accept-Encoding gzip;
15877 proxy_set_header X-Real-IP $remote_addr;
15878 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
15879 proxy_set_header X-Forwarded-Proto $scheme;
15880 proxy_set_header X-Forwarded-Host $http_host;
15881 proxy_set_header X-Forwarded-Uri $request_uri;
15882 proxy_set_header X-Forwarded-Ssl on;
15883 proxy_redirect http:// $scheme://;
15884 proxy_http_version 1.1;
15885 proxy_set_header Connection "";
15886 proxy_cache_bypass $cookie_session;
15887 proxy_no_cache $cookie_session;
15888 proxy_buffers 64 256k;
15889 }
15890}
15891#+end_src
15892
15893Same as before, symlink the file and restart Nginx.
15894
15895#+begin_src sh
15896sudo ln -s /etc/nginx/sites-available/teddit /etc/nginx/sites-enabled/teddit
15897sudo systemctl restart nginx.service
15898#+end_src
15899
15900* Results
15901
15902When visiting the protected domain, you will now be redirected to your
15903authentication domain and presented with the Authelia login portal.
15904
15905#+caption: Authelia Portal
15906#+attr_html: :alt A view of the Authelia login page, requiring a username and password.
15907[[https://img.cleberg.net/blog/20231010-authelia/authelia_portal.webp]]
15908
15909Once you've successfully authenticated, you can visit your
15910authentication domain directly and see that you're currently
15911authenticated to any domain protected by Authelia.
15912
15913#+caption: Authelia Success
15914#+attr_html: :alt A view of the success page when succesfully authenticated.
15915[[https://img.cleberg.net/blog/20231010-authelia/authelia_success.webp]]
15916#+date: <2023-10-15 Sun 00:00:00>
15917#+title: Alpine Linux SSH Hardening Guide
15918#+description: Detailed guide to enhance Alpine Linux SSH server security through configuration adjustments and vulnerability mitigation.
15919#+slug: alpine-ssh-hardening
15920
15921* Overview
15922
15923This guide follows the standard
15924[[https://www.ssh-audit.com/hardening_guides.html][ssh-audit]] hardening
15925guide, tweaked for Alpine Linux.
15926
15927* Hardening Guide
15928
15929These steps must be performed as root. You can try to use =doas= or
15930=sudo=, but there may be issues.
15931
159321. Re-generate the RSA and ED25519 keys
15933
15934#+begin_src sh
15935rm /etc/ssh/ssh_host_*
15936ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N ""
15937ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
15938#+end_src
15939
159402. [@2] Remove small Diffie-Hellman moduli
15941
15942#+begin_src sh
15943awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe
15944mv /etc/ssh/moduli.safe /etc/ssh/moduli
15945#+end_src
15946
159473. [@3] Enable the RSA and ED25519 HostKey directives in the
15948 /etc/ssh/sshd_{config} file
15949
15950#+begin_src sh
15951sed -i 's/^\#HostKey \/etc\/ssh\/ssh_host_\(rsa\|ed25519\)_key$/HostKey \/etc\/ssh\/ssh_host_\1_key/g' /etc/ssh/sshd_config
15952#+end_src
15953
159544. [@4] Restrict supported key exchange, cipher, and MAC algorithms
15955
15956#+begin_src sh
15957echo -e "\n# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com\n# hardening guide.\nKexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr\nMACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\nHostKeyAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256,rsa-sha2-256-cert-v01@openssh.com" > /etc/ssh/sshd_config.d/ssh-audit_hardening.conf
15958#+end_src
15959
159605. [@5] Include the /etc/ssh/sshd_{config}.d directory
15961
15962#+begin_src sh
15963echo -e "Include /etc/ssh/sshd_config.d/*.conf" > /etc/ssh/sshd_config
15964#+end_src
15965
159666. [@6] Restart OpenSSH server
15967
15968#+begin_src sh
15969rc-service sshd restart
15970#+end_src
15971
15972* Testing SSH
15973
15974You can test the results with the =ssh-audit= python script.
15975
15976#+begin_src sh
15977pip3 install ssh-audit
15978ssh-audit localhost
15979#+end_src
15980
15981If everything succeeded, the results will show as all green. If anything
15982is yellow, orange, or red, you may need to tweak additional settings.
15983#+date: <2023-10-17 Tue 00:00:00>
15984#+title: Self-Hosting Guide: AnonymousOverflow
15985#+description: Detailed installation and setup instructions for the AnonymousOverflow application using Docker Compose and Nginx reverse proxy.
15986#+slug: self-hosting-anonymousoverflow
15987
15988* Overview
15989
15990I recently launched an instance of AnonymousOverflow at
15991[[https://ao.cleberg.net][ao.cleberg.net]] and wanted to write a brief
15992post on how easy it is to install with Docker Compose and Nginx.
15993
15994This guide uses Ubuntu server, Docker Compose, and Nginx as a reverse
15995proxy.
15996
15997* Installation
15998
15999** Docker Compose
16000
16001To install AnonymousOverflow, start by creating a directory for the
16002application and create its =docker-compose.yml= file.
16003
16004#+begin_src sh
16005mkdir ~/anonymousoverflow && cd ~/anonymousoverflow
16006nano docker-compose.yml
16007#+end_src
16008
16009Within this file, paste the following information. Be sure to change the
16010=APP_URL=, =JWT_SIGNING_SECRET=, and =ports= to match your needs.
16011
16012#+begin_src yaml
16013version: "3"
16014
16015services:
16016 anonymousoverflow:
16017 container_name: "app"
16018 image: "ghcr.io/httpjamesm/anonymousoverflow:release"
16019 environment:
16020 - APP_URL=https://ao.example.com
16021 - JWT_SIGNING_SECRET=secret #pwgen 40 1
16022 ports:
16023 - "9380:8080"
16024 restart: "always"
16025#+end_src
16026
16027Save and exit the file when complete. You can now launch the container
16028and access it via your local network.
16029
16030#+begin_src sh
16031sudo docker-compose up -d
16032#+end_src
16033
16034** Nginx Reverse Proxy
16035
16036If you want to access this service outside the local network, I
16037recommend using Nginx as a reverse proxy.
16038
16039Let's start by creating a configuration file.
16040
16041#+begin_src sh
16042sudo nano /etc/nginx/sites-available/ao
16043#+end_src
16044
16045Within this file, paste the following content and repace
16046=ao.example.com= with your URL. You may need to update the SSL
16047certificate statements if your certificates are in a different location.
16048
16049#+begin_src conf
16050server {
16051 if ($host ~ ^[^.]+\.cleberg\.net$) {
16052 return 301 https://$host$request_uri;
16053 }
16054
16055 listen [::]:80;
16056 listen 80;
16057 server_name ao.example.com;
16058 return 404;
16059}
16060
16061server {
16062 listen [::]:443 ssl http2;
16063 listen 443 ssl http2;
16064 server_name ao.example.com;
16065 access_log /var/log/nginx/ao.access.log;
16066 error_log /var/log/nginx/ao.error.log;
16067
16068 add_header X-Content-Type-Options "nosniff";
16069 add_header X-XSS-Protection "1; mode=block";
16070 add_header X-Frame-Options "DENY";
16071 add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
16072 add_header Referrer-Policy "no-referrer";
16073
16074 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
16075 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
16076 include /etc/letsencrypt/options-ssl-nginx.conf;
16077 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
16078
16079 location / {
16080 set $upstream_ao http://127.0.0.1:9380;
16081 proxy_pass $upstream_ao;
16082
16083 proxy_set_header Host $host;
16084 proxy_set_header Upgrade $http_upgrade;
16085 proxy_set_header Connection upgrade;
16086 proxy_set_header Accept-Encoding gzip;
16087 proxy_set_header X-Real-IP $remote_addr;
16088 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
16089 proxy_set_header X-Forwarded-Proto $scheme;
16090 proxy_set_header X-Forwarded-Host $http_host;
16091 proxy_set_header X-Forwarded-Uri $request_uri;
16092 proxy_set_header X-Forwarded-Ssl on;
16093 proxy_redirect http:// $scheme://;
16094 proxy_http_version 1.1;
16095 proxy_set_header Connection "";
16096 proxy_cache_bypass $cookie_session;
16097 proxy_no_cache $cookie_session;
16098 proxy_buffers 64 256k;
16099 }
16100}
16101#+end_src
16102
16103Save and exit the file when complete. On Ubuntu, you will need to
16104symlink the configuration file before it will be recognized by Nginx.
16105Once complete, simply restart the web server.
16106
16107#+begin_src sh
16108sudo ln -s /etc/nginx/sites-available/ao /etc/nginx/sites-enabled/ao
16109sudo systemctl restart nginx.service
16110#+end_src
16111
16112The website will now be available publicly. Visit
16113[[https://ao.cleberg.net][my instance]] for an example.
16114#+date: <2023-11-08 Wed 00:00:00>
16115#+title: Signal CLI (scli) Installation on Alpine
16116#+description: Step-by-step instructions for installing the Signal messenger command-line client scli on Alpine Linux with musl libc, including dependencies and configuration.
16117#+slug: scli
16118
16119[[https://github.com/isamert/scli][scli]] is a command-line tool that
16120allows you to connect to your Signal messenger account. This program
16121utilizes a two-pane display that shows you chats on the left and the
16122focused conversation on the right.
16123
16124This guide will show you how to install =scli= and its dependencies on
16125Alpine Linux, which requires some extra work due to musl.
16126
16127If you're using a non-musl system, you can likely following the =scli=
16128README and download the packaged binaries for an easier installation
16129process.
16130
16131* Dependencies
16132
16133In order to use =scli=, you need a few dependencies:
16134
16135- =openjdk17-jre= - Used as a dependency for the =signal-cli= tool.
16136 Version may vary.
16137- =signal-cli= - Used as the backbone of the =scli= tool.
16138- =findutils= - Replaces the standard Busybox version of =xargs=.
16139- =urwid= - A console user interface library for Python.
16140- =urwid-readline= - For GNU emacs-like keybinds on the input line.
16141- =qrencode= - Displays a QR code in the terminal to link the device
16142 using your phone. Not necessary if you're only linking on desktop and
16143 can copy/paste the connection URL.
16144
16145Let's start by installing the packages available via Alpine's
16146repositories. Be sure to install the latest version of =openjdk=. If you
16147run into Java-related issues, uninstall =openjdk= and install an older
16148version.
16149
16150#+begin_src sh
16151doas apk add openjdk17-jre findutils qrencode
16152#+end_src
16153
16154Next, let's install =signal-cli=. Be sure to export the version of
16155=signal-cli= that you want. I use version =0.12.4= below, but that may
16156be outdated by the time you're reading this.
16157
16158#+begin_src sh
16159export VERSION="0.12.4"
16160wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}".tar.gz
16161doas tar xf signal-cli-"${VERSION}".tar.gz -C /opt
16162doas ln -sf /opt/signal-cli-${VERSION}"/bin/signal/cli /usr/local/bin
16163#+end_src
16164
16165Finally, install the =urwid= packages using the Python packaging
16166utility.
16167
16168#+begin_src sh
16169pip3 install urwid urwid-readline
16170#+end_src
16171
16172* Installation
16173
16174Now that we have all of the dependencies we need, we can install =scli=.
16175Start by simply cloning the repository.
16176
16177#+begin_src sh
16178git clone https://github.com/isamert/scli
16179#+end_src
16180
16181When I cloned this repository on 2023-11-08, I found a bug in the logic
16182that required a fix. You must edit the =scli= file and replace the one
16183instance of =RLIMIT_OFILE= with =RLIMIT_NOFILE=.
16184
16185#+begin_src sh
16186cd scli
16187nano scli
16188#+end_src
16189
16190Once complete, you can move this program to anywhere on your =$PATH=. I
16191chose the following directory.
16192
16193#+begin_src sh
16194doas mv scli /usr/local/bin/scli
16195#+end_src
16196
16197* Initial Setup
16198
16199Now that everything is installed, we can login and configure the client.
16200Start by generating a connection link.
16201
16202#+begin_src sh
16203signal-cli link -n "YOUR-DEVICE-NICKNAME" | tee >(xargs -L 1 qrencode -t utf8)
16204#+end_src
16205
16206This will generate a connection link and related QR code for you to use
16207to link the devices together. Once complete, *wait patiently* for the
16208connection process to finish.
16209
16210Once it completes, it will exit and return you to the prompt. From here,
16211you need to perform an initial =receive= command to start things off.
16212The =USERNAME= variable should be your phone number, such as
16213=+15551237890=.
16214
16215#+begin_src sh
16216signal-cli -u USERNAME receive
16217#+end_src
16218
16219Also be sure to test the daemon to ensure it works properly. If no
16220errors occur, it's working. If you run into errors because you're not
16221running a DBUS session, see my notes below.
16222
16223#+begin_src sh
16224signal-cli -u USERNAME daemon
16225#+end_src
16226
16227Once the initial reception is complete, you are ready to use =scli=.
16228
16229This process will differ depending on your desktop environment (DE). If
16230you are running a DE, you likely have a DBUS session running already and
16231can simply launch the program.
16232
16233However, if you're like me and running your computer straight on the TTY
16234without a DE, you'll need to start a DBUS session for this program.
16235
16236#+begin_src sh
16237# If you're not running a DBUS session yet, you need to start one for scli
16238dbus-run-session -- scli
16239
16240# OR - If you're already running a DBUS session, simply run scli
16241scli
16242#+end_src
16243
16244* Configuration
16245
16246Lastly, there are a number of configuration options that you can pass
16247via the command or in the =~/.config/sclirc= file. See the Github README
16248for more information on configuration options.
16249
16250#+begin_src sh
16251nano ~/.config/sclirc
16252#+end_src
16253
16254#+begin_src conf
16255# ~/.config/sclirc
16256
16257wrap-at = 80
16258enable-notifications = true
16259#+end_src
16260
16261That's it! Following this guide, I have a functional =scli= program that
16262successfully sends messages to my contacts and myself!
16263#+date: <2023-12-03 Sun 00:00:00>
16264#+title: Installing and Configuring NextDNS on Unifi Dream Machine Routers
16265#+description: Procedural guide for installation and configuration of NextDNS on Unifi Dream Machine devices for network DNS filtering.
16266#+slug: unifi-nextdns
16267
16268* Overview
16269
16270I recently installed NextDNS on my Unifi Dream Machine router using the
16271[[https://github.com/nextdns/nextdns/wiki/UnifiOS][UnifiOS]] wiki page
16272on NextDNS's GitHub repository.
16273
16274As a result of this, I wanted to write down the process in case the wiki
16275or installer ever gets lost.
16276
16277* Wiki
16278
16279The following is copied from the wiki page linked above, with one
16280difference in the =ssh= command.
16281
16282Install instructions for Unifi Dream Machine (UDM) standard and pro
16283routers.
16284
16285** Install
16286
16287Enable SSH:
16288
16289- Go to your unifi admin interface and select your device (not the
16290 controller settings, but the Dream Machine settings)
16291- Click on "Settings" at the bottom of the page
16292- Go to the "Advanced" section on the left pan
16293- Enable SSH
16294- Set a SSH password
16295
16296Connect to your router using =ssh root@xxx.xxx.xxx.xxx= with the
16297password you configured.
16298
16299Run the following command and follow the instructions:
16300
16301#+begin_src sh
16302sh -c 'sh -c "$(curl -sL https://nextdns.io/install)"'
16303#+end_src
16304
16305Note: Queries from the UDM itself won't be routed to NextDNS nor
16306encrypted due to current system limitation. All traffic from other
16307devices on then network will.
16308
16309** Upgrade
16310
16311To upgrade to the last version, simply re-run the installer above. If a
16312new version is available, the upgrade action will added to the list of
16313possible actions.
16314
16315** Uninstall
16316
16317To uninstall, re-run the installer above and select "Remove" in the
16318menu.
16319
16320** Troubleshooting
16321
16322If the installation fail, please the installer in debug mode and contact
16323us at team@nextdns.io with the transcript of the installation:
16324
16325#+begin_src sh
16326sh -c 'DEBUG=1 sh -c "$(curl -sL https://nextdns.io/install)"'
16327#+end_src
16328
16329*** Content Filtering Conflict
16330
16331NextDNS CLI and the UDM Content Filtering or the Ad Blocking features
16332are incompatible. If you want to use NextDNS CLI, please make sure they
16333are disabled.
16334
16335To disable Content Filtering, go to Settings > Network, then for each
16336network, set the Content Filtering feature to None
16337
16338To disable Ad Blocking, go to Settings > Application Firewall. In the
16339General tab, uncheck the Ad Blocking checkbox.
16340
16341*** APT Error
16342
16343If you get an apt error as follow:
16344
16345#+begin_src sh
16346E: Failed to fetch http://security.debian.org/dists/stretch/updates/main/binary-arm64/Packages 404 Not Found [IP: 151.101.70.132 80]
16347#+end_src
16348
16349You may try to following:
16350
16351#+begin_src sh
16352sed -i -e 's/deb.debian.org/archive.debian.org/g' \
16353 -e 's|security.debian.org|archive.debian.org/|g' \
16354 -e '/stretch-updates/d' /etc/apt/sources.list
16355#+end_src
16356
16357** install.sh
16358
16359Here are the contents of the =install.sh= file used above, as of
163602023-12-03:
16361
16362#+begin_src sh
16363#!/bin/sh
16364
16365main() {
16366 OS=$(detect_os)
16367 GOARCH=$(detect_goarch)
16368 GOOS=$(detect_goos)
16369 NEXTDNS_BIN=$(bin_location)
16370 INSTALL_RELEASE=$(get_release)
16371
16372 export NEXTDNS_INSTALLER=1
16373
16374 log_info "OS: $OS"
16375 log_info "GOARCH: $GOARCH"
16376 log_info "GOOS: $GOOS"
16377 log_info "NEXTDNS_BIN: $NEXTDNS_BIN"
16378 log_info "INSTALL_RELEASE: $INSTALL_RELEASE"
16379
16380 if [ -z "$OS" ] || [ -z "$GOARCH" ] || [ -z "$GOOS" ] || [ -z "$NEXTDNS_BIN" ] || [ -z "$INSTALL_RELEASE" ]; then
16381 log_error "Cannot detect running environment."
16382 exit 1
16383 fi
16384
16385 case "$RUN_COMMAND" in
16386 install|upgrade|uninstall|configure) "$RUN_COMMAND"; exit ;;
16387 esac
16388
16389 while true; do
16390 CURRENT_RELEASE=$(get_current_release)
16391 log_debug "Start install loop with CURRENT_RELEASE=$CURRENT_RELEASE"
16392
16393 if [ "$CURRENT_RELEASE" ]; then
16394 if ! is_version_current; then
16395 log_debug "NextDNS is out of date ($CURRENT_RELEASE != $INSTALL_RELEASE)"
16396 menu \
16397 u "Upgrade NextDNS from $CURRENT_RELEASE to $INSTALL_RELEASE" upgrade \
16398 c "Configure NextDNS" configure \
16399 r "Remove NextDNS" uninstall \
16400 e "Exit" exit
16401 else
16402 log_debug "NextDNS is up to date ($CURRENT_RELEASE)"
16403 menu \
16404 c "Configure NextDNS" configure \
16405 r "Remove NextDNS" uninstall \
16406 e "Exit" exit
16407 fi
16408 else
16409 log_debug "NextDNS is not installed"
16410 menu \
16411 i "Install NextDNS" install \
16412 e "Exit" exit
16413 fi
16414 done
16415}
16416
16417install() {
16418 if [ "$(get_current_release)" ]; then
16419 log_info "Already installed"
16420 return
16421 fi
16422 if type=$(install_type); then
16423 log_info "Installing NextDNS..."
16424 log_debug "Using $type install type"
16425 if "install_$type"; then
16426 if [ ! -x "$NEXTDNS_BIN" ]; then
16427 log_error "Installation failed: binary not installed in $NEXTDNS_BIN"
16428 return 1
16429 fi
16430 configure
16431 post_install
16432 exit 0
16433 fi
16434 else
16435 return $?
16436 fi
16437}
16438
16439upgrade() {
16440 if [ "$(get_current_release)" = "$INSTALL_RELEASE" ]; then
16441 log_info "Already on the latest version"
16442 return
16443 fi
16444 if type=$(install_type); then
16445 log_info "Upgrading NextDNS..."
16446 log_debug "Using $type install type"
16447 "upgrade_$type"
16448 else
16449 return $?
16450 fi
16451}
16452
16453uninstall() {
16454 if type=$(install_type); then
16455 log_info "Uninstalling NextDNS..."
16456 log_debug "Using $type uninstall type"
16457 "uninstall_$type"
16458 else
16459 return $?
16460 fi
16461}
16462
16463precheck() {
16464 if [ -e "/data/unifi" ] && [ -f "/run/dnsfilter/dnsfilter" ]; then
16465 log_warn "UDM Content Filtering and/or Ad Blocking feature is enabled."
16466 log_warn "Please disable it to use NextDNS."
16467 log_warn ""
16468 log_warn " To disable Content Filtering, go to Settings > Network."
16469 log_warn " For each network, set the Content Filtering feature to None."
16470 log_warn ""
16471 log_warn " To disable Ad Blocking, go to Settings > Application Firewall"
16472 log_warn " In the General tab, uncheck the Ad Blocking checkbox."
16473 log_warn ""
16474 while [ -f "/run/dnsfilter/dnsfilter" ]; do
16475 sleep 1
16476 done
16477 log_info "Content Filtering feature successfuly disabled."
16478 fi
16479}
16480
16481configure() {
16482 log_debug "Start configure"
16483 precheck
16484 args=""
16485 add_arg() {
16486 for value in $2; do
16487 log_debug "Add arg -$1=$value"
16488 args="$args -$1=$value"
16489 done
16490 }
16491 add_arg_bool_ask() {
16492 arg=$1
16493 msg=$2
16494 default=$3
16495 if [ -z "$default" ]; then
16496 default=$(get_config_bool "$arg")
16497 fi
16498 # shellcheck disable=SC2046
16499 add_arg "$arg" $(ask_bool "$msg" "$default")
16500 }
16501 # Use profile from now on
16502 add_arg profile "$(get_profile_id)"
16503
16504 doc "Sending your devices name lets you filter analytics and logs by device."
16505 add_arg_bool_ask report-client-info 'Report device name?' true
16506
16507 case $(guess_host_type) in
16508 router)
16509 add_arg setup-router true
16510 ;;
16511 unsure)
16512 doc "Accept DNS request from other network hosts."
16513 if [ "$(get_config_bool setup-router)" = "true" ]; then
16514 router_default=true
16515 fi
16516 if [ "$(ask_bool 'Setup as a router?' $router_default)" = "true" ]; then
16517 add_arg setup-router true
16518 fi
16519 ;;
16520 esac
16521
16522 doc "Make NextDNS CLI cache responses. This improves latency and reduces the amount"
16523 doc "of queries sent to NextDNS."
16524 if [ "$(guess_host_type)" = "router" ]; then
16525 doc "Note that enabling this feature will disable dnsmasq for DNS to avoid double"
16526 doc "caching."
16527 fi
16528 if [ "$(get_config cache-size)" != "0" ]; then
16529 cache_default=true
16530 fi
16531 if [ "$(ask_bool 'Enable caching?' $cache_default)" = "true" ]; then
16532 add_arg cache-size "10MB"
16533
16534 doc "Instant refresh will force low TTL on responses sent to clients so they rely"
16535 doc "on CLI DNS cache. This will allow changes on your NextDNS config to be applied"
16536 doc "on your LAN hosts without having to wait for their cache to expire."
16537 if [ "$(get_config max-ttl)" = "5s" ]; then
16538 instant_refresh_default=true
16539 fi
16540 if [ "$(ask_bool 'Enable instant refresh?' $instant_refresh_default)" = "true" ]; then
16541 add_arg max-ttl "5s"
16542 fi
16543 fi
16544
16545 if [ "$(guess_host_type)" != "router" ]; then
16546 doc "Changes DNS settings of the host automatically when NextDNS is started."
16547 doc "If you say no here, you will have to manually configure DNS to 127.0.0.1."
16548 add_arg_bool_ask auto-activate 'Automatically setup local host DNS?' true
16549 fi
16550 # shellcheck disable=SC2086
16551 asroot "$NEXTDNS_BIN" install $args
16552}
16553
16554post_install() {
16555 println
16556 println "Congratulations! NextDNS is now installed."
16557 println
16558 println "To upgrade/uninstall, run this command again and select the appropriate option."
16559 println
16560 println "You can use the NextDNS command to control the daemon."
16561 println "Here are a few important commands to know:"
16562 println
16563 println "# Start, stop, restart the daemon:"
16564 println "nextdns start"
16565 println "nextdns stop"
16566 println "nextdns restart"
16567 println
16568 println "# Configure the local host to point to NextDNS or not:"
16569 println "nextdns activate"
16570 println "nextdns deactivate"
16571 println
16572 println "# Explore daemon logs:"
16573 println "nextdns log"
16574 println
16575 println "# For more commands, use:"
16576 println "nextdns help"
16577 println
16578}
16579
16580install_bin() {
16581 bin_path=$NEXTDNS_BIN
16582 if [ "$1" ]; then
16583 bin_path=$1
16584 fi
16585 log_debug "Installing $INSTALL_RELEASE binary for $GOOS/$GOARCH to $bin_path"
16586 case "$INSTALL_RELEASE" in
16587 ,*/*)
16588 # Snapshot
16589 branch=${INSTALL_RELEASE%/*}
16590 hash=${INSTALL_RELEASE#*/}
16591 url="https://snapshot.nextdns.io/${branch}/nextdns-${hash}_${GOOS}_${GOARCH}.tar.gz"
16592 ;;
16593 ,*)
16594 url="https://github.com/nextdns/nextdns/releases/download/v${INSTALL_RELEASE}/nextdns_${INSTALL_RELEASE}_${GOOS}_${GOARCH}.tar.gz"
16595 ;;
16596 esac
16597 log_debug "Downloading $url"
16598 asroot mkdir -p "$(dirname "$bin_path")" &&
16599 curl -sL "$url" | asroot sh -c "tar Ozxf - nextdns > "$bin_path"" &&
16600 asroot chmod 755 "$bin_path"
16601}
16602
16603upgrade_bin() {
16604 tmp=$NEXTDNS_BIN.tmp
16605 if install_bin "$tmp"; then
16606 asroot "$NEXTDNS_BIN" uninstall
16607 asroot mv "$tmp" "$NEXTDNS_BIN"
16608 asroot "$NEXTDNS_BIN" install
16609 fi
16610 log_debug "Removing spurious temporary install file"
16611 asroot rm -rf "$tmp"
16612}
16613
16614uninstall_bin() {
16615 asroot "$NEXTDNS_BIN" uninstall
16616 asroot rm -f "$NEXTDNS_BIN"
16617}
16618
16619install_rpm() {
16620 asroot curl -Ls https://repo.nextdns.io/nextdns.repo -o /etc/yum.repos.d/nextdns.repo &&
16621 asroot yum install -y nextdns
16622}
16623
16624upgrade_rpm() {
16625 asroot yum update -y nextdns
16626}
16627
16628uninstall_rpm() {
16629 asroot yum remove -y nextdns
16630}
16631
16632install_zypper() {
16633 if asroot zypper repos | grep -q nextdns >/dev/null; then
16634 echo "Repository nextdns already exists. Skipping adding repository..."
16635 else
16636 asroot zypper ar -f -r https://repo.nextdns.io/nextdns.repo nextdns
16637 fi
16638 asroot zypper refresh && asroot zypper in -y nextdns
16639}
16640
16641upgrade_zypper() {
16642 asroot zypper up nextdns
16643}
16644
16645uninstall_zypper() {
16646 asroot zypper remove -y nextdns
16647 case $(ask_bool 'Do you want to remove the repository from the repositories list?' true) in
16648 true)
16649 asroot zypper removerepo nextdns
16650 ;;
16651 esac
16652}
16653
16654install_deb() {
16655 if [ -f /etc/default/ubnt-dpkg-cache ]; then
16656 # On UnifiOS 2, make sure the package is persisted over upgrades
16657 sed -e '/^DPKG_CACHE_UBNT_PKGS+=" nextdns"/{:a;n;ba;q}' \
16658 -e '$aDPKG_CACHE_UBNT_PKGS+=" nextdns"' \
16659 -i /etc/default/ubnt-dpkg-cache
16660 fi
16661
16662 install_deb_keyring &&
16663 asroot sh -c 'echo "deb [signed-by=/etc/apt/keyrings/nextdns.gpg] https://repo.nextdns.io/deb stable main" > /etc/apt/sources.list.d/nextdns.list' &&
16664 (dpkg --compare-versions $(dpkg-query --showformat='${Version}' --show apt) ge 1.1 ||
16665 asroot ln -s /etc/apt/keyrings/nextdns.gpg /etc/apt/trusted.gpg.d/.) &&
16666 (test "$OS" = "debian" && asroot apt-get -y install apt-transport-https || true) &&
16667 asroot apt-get update &&
16668 asroot apt-get install -y nextdns
16669}
16670
16671install_deb_keyring() {
16672 # Fallback on curl, some debian based distrib don't have wget while debian
16673 # doesn't have curl by default.
16674 asroot mkdir -p /etc/apt/keyrings
16675 ( asroot wget -qO /etc/apt/keyrings/nextdns.gpg https://repo.nextdns.io/nextdns.gpg ||
16676 asroot curl -sfL https://repo.nextdns.io/nextdns.gpg -o /etc/apt/keyrings/nextdns.gpg ) &&
16677 asroot chmod 0644 /etc/apt/keyrings/nextdns.gpg
16678}
16679
16680upgrade_deb() {
16681 install_deb_keyring &&
16682 asroot apt-get update &&
16683 asroot apt-get install -y nextdns
16684}
16685
16686uninstall_deb() {
16687 asroot apt-get remove -y nextdns
16688}
16689
16690install_apk() {
16691 repo=https://repo.nextdns.io/apk
16692 asroot wget -O /etc/apk/keys/nextdns.pub https://repo.nextdns.io/nextdns.pub &&
16693 (grep -v $repo /etc/apk/repositories; echo $repo) | asroot tee /etc/apk/repositories >/dev/null &&
16694 asroot apk update &&
16695 asroot apk add nextdns
16696}
16697
16698upgrade_apk() {
16699 asroot apk update && asroot apk upgrade nextdns
16700}
16701
16702uninstall_apk() {
16703 asroot apk del nextdns
16704}
16705
16706install_arch() {
16707 asroot pacman -Sy yay &&
16708 yay -Sy nextdns
16709}
16710
16711upgrade_arch() {
16712 yay -Suy nextdns
16713}
16714
16715uninstall_arch() {
16716 asroot pacman -R nextdns
16717}
16718
16719install_merlin_path() {
16720 # Add next to Merlin's path
16721 mkdir -p /tmp/opt/sbin
16722 ln -sf "$NEXTDNS_BIN" /tmp/opt/sbin/nextdns
16723}
16724
16725install_merlin() {
16726 if install_bin; then
16727 install_merlin_path
16728 fi
16729}
16730
16731uninstall_merlin() {
16732 uninstall_bin
16733 rm -f /tmp/opt/sbin/nextdns
16734}
16735
16736upgrade_merlin() {
16737 if upgrade_bin; then
16738 install_merlin_path
16739 fi
16740}
16741
16742install_openwrt() {
16743 opkg update &&
16744 opkg install nextdns
16745 rt=$?
16746 if [ $rt -eq 0 ]; then
16747 case $(ask_bool 'Install the GUI?' true) in
16748 true)
16749 opkg install luci-app-nextdns
16750 rt=$?
16751 ;;
16752 esac
16753 fi
16754 return $rt
16755}
16756
16757upgrade_openwrt() {
16758 opkg update &&
16759 opkg upgrade nextdns
16760}
16761
16762uninstall_openwrt() {
16763 opkg remove nextdns
16764}
16765
16766install_ddwrt() {
16767 if [ "$(nvram get enable_jffs2)" = "0" ]; then
16768 log_error "JFFS support not enabled"
16769 log_info "To enabled JFFS:"
16770 log_info " 1. On the router web page click on Administration."
16771 log_info " 2. Scroll down until you see JFFS2 Support section."
16772 log_info " 3. Click Enable JFFS."
16773 log_info " 4. Click Save."
16774 log_info " 5. Wait couple seconds, then click Apply."
16775 log_info " 6. Wait again. Go back to the Enable JFFS section, and enable Clean JFFS."
16776 log_info " 7. Do not click Save. Click Apply instead."
16777 log_info " 8. Wait till you get the web-GUI back, then disable Clean JFFS again."
16778 log_info " 9. Click Save."
16779 log_info "10. Relaunch this installer."
16780 exit 1
16781 fi
16782 mkdir -p /jffs/nextdns &&
16783 openssl_get https://curl.haxx.se/ca/cacert.pem | http_body > /jffs/nextdns/ca.pem &&
16784 install_bin
16785}
16786
16787upgrade_ddwrt() {
16788 upgrade_bin
16789}
16790
16791uninstall_ddwrt() {
16792 uninstall_bin
16793 rm -rf /jffs/nextdns
16794}
16795
16796install_brew() {
16797 silent_exec brew install nextdns/tap/nextdns
16798}
16799
16800upgrade_brew() {
16801 silent_exec brew upgrade nextdns/tap/nextdns
16802 asroot "$NEXTDNS_BIN" install
16803}
16804
16805uninstall_brew() {
16806 silent_exec brew uninstall nextdns/tap/nextdns
16807}
16808
16809install_freebsd() {
16810 # TODO: port install
16811 install_bin
16812}
16813
16814upgrade_freebsd() {
16815 # TODO: port upgrade
16816 upgrade_bin
16817}
16818
16819uninstall_freebsd() {
16820 # TODO: port uninstall
16821 uninstall_bin
16822}
16823
16824install_pfsense() {
16825 # TODO: port install + UI
16826 install_bin
16827}
16828
16829upgrade_pfsense() {
16830 # TODO: port upgrade
16831 upgrade_bin
16832}
16833
16834uninstall_pfsense() {
16835 # TODO: port uninstall
16836 uninstall_bin
16837}
16838
16839install_opnsense() {
16840 # TODO: port install + UI
16841 install_bin
16842}
16843
16844upgrade_opnsense() {
16845 # TODO: port upgrade
16846 upgrade_bin
16847}
16848
16849uninstall_opnsense() {
16850 # TODO: port uninstall
16851 uninstall_bin
16852}
16853
16854ubios_install_source() {
16855 echo "deb [signed-by=/etc/apt/keyrings/nextdns.gpg] https://repo.nextdns.io/deb stable main" > /data/nextdns.list
16856 podman exec unifi-os mv /data/nextdns.list /etc/apt/sources.list.d/nextdns.list
16857 rm -f /tmp/nextdns.list
16858 podman exec unifi-os apt-get install -y gnupg1 curl
16859 podman exec unifi-os mkdir -p /etc/apt/keyrings/
16860 podman exec unifi-os curl -sfL https://repo.nextdns.io/nextdns.gpg -o /etc/apt/keyrings/nextdns.gpg
16861 podman exec unifi-os apt-get update -o Dir::Etc::sourcelist="sources.list.d/nextdns.list" -o Dir::Etc::sourceparts="-" -o APT::Get::List-Cleanup="0"
16862}
16863
16864install_ubios() {
16865 ubios_install_source
16866 podman exec unifi-os apt-get install -y nextdns
16867}
16868
16869upgrade_ubios() {
16870 ubios_install_source
16871 podman exec unifi-os apt-get install --only-upgrade -y nextdns
16872}
16873
16874uninstall_ubios() {
16875 podman exec unifi-os apt-get remove -y nextdns
16876}
16877
16878install_ubios_snapshot() {
16879 branch=${INSTALL_RELEASE%/*}
16880 hash=${INSTALL_RELEASE#*/}
16881 url="https://snapshot.nextdns.io/${branch}/nextdns-${hash}_${GOOS}_${GOARCH}.tar.gz"
16882 podman exec unifi-os sh -c "curl -o- $url | tar Ozxf - nextdns > /usr/bin/nextdns; /usr/bin/nextdns install"
16883}
16884
16885upgrade_ubios_snapshot() {
16886 /data/nextdns uninstall
16887 install_ubios_snapshot
16888}
16889
16890install_type() {
16891 if [ "$FORCE_INSTALL_TYPE" ]; then
16892 echo "$FORCE_INSTALL_TYPE"; return 0
16893 fi
16894 case "$INSTALL_RELEASE" in
16895 ,*/*)
16896 case $OS in
16897 ubios)
16898 echo "ubios_snapshot"; return 0
16899 ;;
16900 ,*)
16901 # Snapshot mode always use binary install
16902 echo "bin"; return 0
16903 ;;
16904 esac
16905 esac
16906 case $OS in
16907 centos|fedora|rhel)
16908 echo "rpm"
16909 ;;
16910 opensuse-tumbleweed|opensuse-leap|opensuse)
16911 echo "zypper"
16912 ;;
16913 debian|ubuntu|elementary|raspbian|linuxmint|pop|neon|sparky|vyos|Deepin)
16914 echo "deb"
16915 ;;
16916 alpine)
16917 echo "apk"
16918 ;;
16919 arch|manjaro|steamos)
16920 #echo "arch" # TODO: fix AUR install
16921 echo "bin"
16922 ;;
16923 openwrt)
16924 # shellcheck disable=SC1091
16925 . /etc/os-release
16926 major=$(echo "$VERSION_ID" | cut -d. -f1)
16927 case $major in
16928 ,*[!0-9]*)
16929 if [ "$VERSION_ID" = "19.07.0-rc1" ]; then
16930 # No opkg support before 19.07.0-rc2
16931 echo "bin"
16932 else
16933 # Likely 'snapshot' build in this case, but still > major version 19
16934 echo "openwrt"
16935 fi
16936 ;;
16937 ,*)
16938 if [ "$major" -lt 19 ]; then
16939 # No opkg support before 19.07.0-rc2
16940 echo "bin"
16941 else
16942 echo "openwrt"
16943 fi
16944 ;;
16945 esac
16946 ;;
16947 asuswrt-merlin)
16948 echo "merlin"
16949 ;;
16950 edgeos|synology|clear-linux-os|solus|openbsd|netbsd|overthebox)
16951 echo "bin"
16952 ;;
16953 ddwrt)
16954 echo "ddwrt"
16955 ;;
16956 darwin)
16957 if [ -x /usr/local/bin/brew ] || [ -x /opt/homebrew/bin/brew ]; then
16958 echo "brew"
16959 else
16960 log_debug "Homebrew not installed, fallback on binary install"
16961 echo "bin"
16962 fi
16963 ;;
16964 freebsd)
16965 echo "freebsd"
16966 ;;
16967 pfsense)
16968 echo "pfsense"
16969 ;;
16970 opnsense)
16971 echo "opnsense"
16972 ;;
16973 ubios)
16974 echo "ubios"
16975 ;;
16976 gentoo)
16977 echo "bin"
16978 ;;
16979 void)
16980 # TODO: pkg for xbps
16981 echo "bin"
16982 ;;
16983 ,*)
16984 log_error "Unsupported installation for $(detect_os)"
16985 return 1
16986 ;;
16987 esac
16988}
16989
16990get_config() {
16991 "$NEXTDNS_BIN" config | grep -E "^$1 " | cut -d' ' -f 2
16992}
16993
16994get_config_bool() {
16995 val=$(get_config "$1")
16996 case $val in
16997 true|false)
16998 echo "$val"
16999 ;;
17000 esac
17001 echo "$2"
17002}
17003
17004get_profile_id() {
17005 log_debug "Get profile ID"
17006 if [ "$CONFIG_ID" ]; then
17007 # backward compat
17008 PROFILE_ID="$CONFIG_ID"
17009 fi
17010 while [ -z "$PROFILE_ID" ]; do
17011 default=
17012 prev_id=$(get_config profile)
17013 if [ -z "$prev_id" ]; then
17014 # backward compat
17015 prev_id=$(get_config config)
17016 fi
17017 if [ "$prev_id" ]; then
17018 log_debug "Previous profile ID: $prev_id"
17019 default=" (default=$prev_id)"
17020 fi
17021 print "NextDNS Profile ID%s: " "$default"
17022 read -r id
17023 if [ -z "$id" ]; then
17024 id=$prev_id
17025 fi
17026 if echo "$id" | grep -qE '^[0-9a-f]{6}$'; then
17027 PROFILE_ID=$id
17028 break
17029 else
17030 log_error "Invalid profile ID."
17031 println
17032 println "ID format is 6 alphanumerical lowercase characters (example: 123abc)."
17033 println "Your ID can be found on the Setup tab of https://my.nextdns.io."
17034 println
17035 fi
17036 done
17037 echo "$PROFILE_ID"
17038}
17039
17040log_debug() {
17041 if [ "$DEBUG" = "1" ]; then
17042 printf "\033[30;1mDEBUG: %s\033[0m\n" "$*" >&2
17043 fi
17044}
17045
17046log_info() {
17047 printf "INFO: %s\n" "$*" >&2
17048}
17049
17050log_warn() {
17051 printf "\033[33mWARN: %s\033[0m\n" "$*" >&2
17052}
17053
17054log_error() {
17055 printf "\033[31mERROR: %s\033[0m\n" "$*" >&2
17056}
17057
17058print() {
17059 format=$1
17060 if [ $# -gt 0 ]; then
17061 shift
17062 fi
17063 # shellcheck disable=SC2059
17064 printf "$format" "$@" >&2
17065}
17066
17067println() {
17068 format=$1
17069 if [ $# -gt 0 ]; then
17070 shift
17071 fi
17072 # shellcheck disable=SC2059
17073 printf "$format\n" "$@" >&2
17074}
17075
17076doc() {
17077 # shellcheck disable=SC2059
17078 printf "\033[30;1m%s\033[0m\n" "$*" >&2
17079}
17080
17081menu() {
17082 while true; do
17083 n=0
17084 default=
17085 for item in "$@"; do
17086 case $((n%3)) in
17087 0)
17088 key=$item
17089 if [ -z "$default" ]; then
17090 default=$key
17091 fi
17092 ;;
17093 1)
17094 echo "$key) $item"
17095 ;;
17096 esac
17097 n=$((n+1))
17098 done
17099 print "Choice (default=%s): " "$default"
17100 read -r choice
17101 if [ -z "$choice" ]; then
17102 choice=$default
17103 fi
17104 n=0
17105 for item in "$@"; do
17106 case $((n%3)) in
17107 0)
17108 key=$item
17109 ;;
17110 2)
17111 if [ "$key" = "$choice" ]; then
17112 if ! "$item"; then
17113 log_error "$item: exit $?"
17114 fi
17115 break 2
17116 fi
17117 ;;
17118 esac
17119 n=$((n+1))
17120 done
17121 echo "Invalid choice"
17122 done
17123}
17124
17125ask_bool() {
17126 msg=$1
17127 default=$2
17128 case $default in
17129 true)
17130 msg="$msg [Y|n]: "
17131 ;;
17132 false)
17133 msg="$msg [y|N]: "
17134 ;;
17135 ,*)
17136 msg="$msg (y/n): "
17137 esac
17138 while true; do
17139 print "%s" "$msg"
17140 read -r answer
17141 if [ -z "$answer" ]; then
17142 answer=$default
17143 fi
17144 case $answer in
17145 y|Y|yes|YES|true)
17146 echo "true"
17147 return 0
17148 ;;
17149 n|N|no|NO|false)
17150 echo "false"
17151 return 0
17152 ;;
17153 ,*)
17154 echo "Invalid input, use yes or no"
17155 ;;
17156 esac
17157 done
17158}
17159
17160detect_endiannes() {
17161 if ! hexdump /dev/null 2>/dev/null; then
17162 # Some firmwares do not contain hexdump, for those, try to detect endianness
17163 # differently.
17164 case $(cat /proc/cpuinfo) in
17165 ,*BCM5300*)
17166 # RT-AC66U does not support Merlin version over 380.70 which
17167 # lacks hexdump command.
17168 echo "le"
17169 ;;
17170 ,*)
17171 log_error "Cannot determine endianness"
17172 return 1
17173 ;;
17174 esac
17175 return 0
17176 fi
17177 case $(hexdump -s 5 -n 1 -e '"%x"' /bin/sh | head -c1) in
17178 1)
17179 echo "le"
17180 ;;
17181 2)
17182 echo ""
17183 ;;
17184 esac
17185}
17186
17187detect_goarch() {
17188 if [ "$FORCE_GOARCH" ]; then
17189 echo "$FORCE_GOARCH"; return 0
17190 fi
17191 case $(uname -m) in
17192 x86_64|amd64)
17193 echo "amd64"
17194 ;;
17195 i386|i686)
17196 echo "386"
17197 ;;
17198 arm)
17199 # FreeBSD does not include arm version
17200 case "$(sysctl -b hw.model 2>/dev/null)" in
17201 ,*A9*)
17202 echo "armv7"
17203 ;;
17204 ,*)
17205 # Unknown version, fallback to the lowest
17206 echo "armv5"
17207 ;;
17208 esac
17209 ;;
17210 armv5*)
17211 echo "armv5"
17212 ;;
17213 armv6*|armv7*)
17214 if grep -q vfp /proc/cpuinfo 2>/dev/null; then
17215 echo "armv$(uname -m | sed -e 's/[[:alpha:]]//g')"
17216 else
17217 # Soft floating point
17218 echo "armv5"
17219 fi
17220 ;;
17221 aarch64)
17222 case "$(uname -o 2>/dev/null)" in
17223 ASUSWRT-Merlin*)
17224 # XXX when using arm64 build on ASUS AC66U and ACG86U, we get Go error:
17225 # "out of memory allocating heap arena metadata".
17226 echo "armv7"
17227 ;;
17228 ,*)
17229 echo "arm64"
17230 ;;
17231 esac
17232 ;;
17233 armv8*|arm64)
17234 echo "arm64"
17235 ;;
17236 mips*)
17237 # TODO: detect hardfloat
17238 echo "$(uname -m)$(detect_endiannes)_softfloat"
17239 ;;
17240 ,*)
17241 log_error "Unsupported GOARCH: $(uname -m)"
17242 return 1
17243 ;;
17244 esac
17245}
17246
17247detect_goos() {
17248 if [ "$FORCE_GOOS" ]; then
17249 echo "$FORCE_GOOS"; return 0
17250 fi
17251 case $(uname -s) in
17252 Linux)
17253 echo "linux"
17254 ;;
17255 Darwin)
17256 echo "darwin"
17257 ;;
17258 FreeBSD)
17259 echo "freebsd"
17260 ;;
17261 NetBSD)
17262 echo "netbsd"
17263 ;;
17264 OpenBSD)
17265 echo "openbsd"
17266 ;;
17267 ,*)
17268 log_error "Unsupported GOOS: $(uname -s)"
17269 return 1
17270 esac
17271}
17272
17273detect_os() {
17274 if [ "$FORCE_OS" ]; then
17275 echo "$FORCE_OS"; return 0
17276 fi
17277 case $(uname -s) in
17278 Linux)
17279 case $(uname -o) in
17280 GNU/Linux|Linux)
17281 if grep -q -e '^EdgeRouter' -e '^UniFiSecurityGateway' /etc/version 2> /dev/null; then
17282 echo "edgeos"; return 0
17283 fi
17284 if uname -u 2>/dev/null | grep -q '^synology'; then
17285 echo "synology"; return 0
17286 fi
17287 # shellcheck disable=SC1091
17288 dist=$(. /etc/os-release; echo "$ID")
17289 case $dist in
17290 ubios)
17291 if [ -z "$(command -v podman)" ]; then
17292 log_error "This version of UnifiOS is not supported. Make sure you run version 1.7.0 or above."
17293 return 1
17294 fi
17295 echo "$dist"; return 0
17296 ;;
17297 debian|ubuntu|elementary|raspbian|centos|fedora|rhel|arch|manjaro|openwrt|clear-linux-os|linuxmint|opensuse-tumbleweed|opensuse-leap|opensuse|solus|pop|neon|overthebox|sparky|vyos|void|alpine|Deepin|gentoo|steamos)
17298 echo "$dist"; return 0
17299 ;;
17300 esac
17301 # shellcheck disable=SC1091
17302 for dist in $(. /etc/os-release; echo "$ID_LIKE"); do
17303 case $dist in
17304 debian|ubuntu|rhel|fedora|openwrt)
17305 log_debug "Using ID_LIKE"
17306 echo "$dist"; return 0
17307 ;;
17308 esac
17309 done
17310 ;;
17311 ASUSWRT-Merlin*)
17312 echo "asuswrt-merlin"; return 0
17313 ;;
17314 DD-WRT)
17315 echo "ddwrt"; return 0
17316 esac
17317 ;;
17318 Darwin)
17319 echo "darwin"; return 0
17320 ;;
17321 FreeBSD)
17322 if [ -f /etc/platform ]; then
17323 case $(cat /etc/platform) in
17324 pfSense)
17325 echo "pfsense"; return 0
17326 ;;
17327 esac
17328 fi
17329 if [ -x /usr/local/sbin/opnsense-version ]; then
17330 case $(/usr/local/sbin/opnsense-version -N) in
17331 OPNsense)
17332 echo "opnsense"; return 0
17333 ;;
17334 esac
17335 fi
17336 echo "freebsd"; return 0
17337 ;;
17338 NetBSD)
17339 echo "netbsd"; return 0
17340 ;;
17341 OpenBSD)
17342 echo "openbsd"; return 0
17343 ;;
17344 ,*)
17345 esac
17346 log_error "Unsupported OS: $(uname -o) $(grep ID "/etc/os-release" 2>/dev/null | xargs)"
17347 return 1
17348}
17349
17350guess_host_type() {
17351 if [ -d /data/unifi ]; then
17352 # Special case when installer is run from inside the ubios podman
17353 echo "router"; return 0
17354 fi
17355
17356 case $OS in
17357 pfsense|opnsense|openwrt|asuswrt-merlin|edgeos|ddwrt|synology|overthebox|ubios)
17358 echo "router"
17359 ;;
17360 darwin|steamos)
17361 echo "workstation"
17362 ;;
17363 ,*)
17364 echo "unsure"
17365 ;;
17366 esac
17367}
17368
17369asroot() {
17370 # Some platform (Merlin) do not have the "id" command and $USER report a non root username with uid 0.
17371 if [ "$(grep '^Uid:' /proc/$$/status 2>/dev/null|cut -f2)" = "0" ] || [ "$USER" = "root" ] || [ "$(id -u 2>/dev/null)" = "0" ]; then
17372 "$@"
17373 elif [ "$(command -v sudo 2>/dev/null)" ]; then
17374 sudo "$@"
17375 else
17376 echo "Root required"
17377 su -m root -c "$*"
17378 fi
17379}
17380
17381silent_exec() {
17382 if [ "$DEBUG" = 1 ]; then
17383 "$@"
17384 else
17385 if ! out=$("$@" 2>&1); then
17386 rt=$?
17387 println "\033[30;1m%s\033[0m" "$out"
17388 return $rt
17389 fi
17390 fi
17391}
17392
17393bin_location() {
17394 case $OS in
17395 centos|fedora|rhel|debian|ubuntu|elementary|raspbian|arch|manjaro|clear-linux-os|linuxmint|opensuse-tumbleweed|opensuse-leap|opensuse|solus|pop|neon|sparky|vyos|void|alpine|Deepin|gentoo)
17396 echo "/usr/bin/nextdns"
17397 ;;
17398 openwrt|overthebox)
17399 echo "/usr/sbin/nextdns"
17400 ;;
17401 synology)
17402 echo "/usr/local/bin/nextdns"
17403 ;;
17404 darwin)
17405 echo "$(brew --prefix 2>/dev/null || echo /usr/local)/bin/nextdns"
17406 ;;
17407 asuswrt-merlin|ddwrt)
17408 echo "/jffs/nextdns/nextdns"
17409 ;;
17410 freebsd|pfsense|opnsense|netbsd|openbsd)
17411 echo "/usr/local/sbin/nextdns"
17412 ;;
17413 edgeos)
17414 echo "/config/nextdns/nextdns"
17415 ;;
17416 ubios)
17417 echo "/data/nextdns"
17418 ;;
17419 steamos)
17420 echo "$HOME/.local/bin/nextdns"
17421 ;;
17422 ,*)
17423 log_error "Unknown bin location for $OS"
17424 ;;
17425 esac
17426}
17427
17428is_version_current() {
17429 case "$INSTALL_RELEASE" in
17430 ,*/*)
17431 # Snapshot
17432 hash=${INSTALL_RELEASE#*/}
17433 test "0.0.0-$hash" = "$CURRENT_RELEASE"
17434 ;;
17435 ,*)
17436 test "$INSTALL_RELEASE" = "$CURRENT_RELEASE"
17437 ;;
17438 esac
17439}
17440
17441get_current_release() {
17442 if [ -x "$NEXTDNS_BIN" ]; then
17443 $NEXTDNS_BIN version|cut -d' ' -f 3
17444 fi
17445}
17446
17447get_release() {
17448 if [ "$NEXTDNS_VERSION" ]; then
17449 echo "$NEXTDNS_VERSION"
17450 else
17451 for cmd in curl wget openssl true; do
17452 # command is the "right" way but may be compiled out of busybox shell
17453 ! command -v $cmd > /dev/null 2>&1 || break
17454 ! which $cmd > /dev/null 2>&1 || break
17455 done
17456 case "$cmd" in
17457 curl) cmd="curl -A curl -s" ;;
17458 wget) cmd="wget -qO- -U curl" ;;
17459 openssl) cmd="openssl_get" ;;
17460 ,*)
17461 log_error "Cannot retrieve latest version"
17462 return
17463 ;;
17464 esac
17465 v=$($cmd "https://api.github.com/repos/nextdns/nextdns/releases/latest" | \
17466 grep '"tag_name":' | esed 's/.*"([^"]+)".*/\1/' | sed -e 's/^v//')
17467 if [ -z "$v" ]; then
17468 log_error "Cannot get latest version: $out"
17469 fi
17470 echo "$v"
17471 fi
17472}
17473
17474esed() {
17475 if (echo | sed -E '' >/dev/null 2>&1); then
17476 sed -E "$@"
17477 else
17478 sed -r "$@"
17479 fi
17480}
17481
17482http_redirect() {
17483 while read -r header; do
17484 case $header in
17485 Location:*)
17486 echo "${header#Location: }"
17487 return
17488 ;;
17489 esac
17490 if [ "$header" = "" ]; then
17491 break
17492 fi
17493 done
17494 cat > /dev/null
17495 return 1
17496}
17497
17498http_body() {
17499 sed -n '/^\r/,$p' | sed 1d
17500}
17501
17502openssl_get() {
17503 host=${1#https://*} # https://dom.com/path -> dom.com/path
17504 path=/${host#*/} # dom.com/path -> /path
17505 host=${host%$path} # dom.com/path -> dom.com
17506 printf "GET %s HTTP/1.0\nHost: %s\nUser-Agent: curl\n\n" "$path" "$host" |
17507 openssl s_client -quiet -connect "$host:443" 2>/dev/null
17508}
17509
17510umask 0022
17511main
17512#+end_src
17513#+date: <2024-01-08 Mon 00:00:00>
17514#+title: Don't Say Hello
17515#+description: Technical recommendations for improving professional communication by eliminating non-essential introductory phrases to enhance message clarity.
17516#+slug: dont-say-hello
17517
17518I recently came back from a winter break and have started working again... only
17519to immediately run into the dilemma of people sending me cliffhanger messages
17520again.
17521
17522* No Hello
17523
17524A year or two ago, I discovered [[https://nohello.net/en/][no hello]] and have thought about it often since
17525then. I've even sent it to a few people since then (who wouldn't take offense to
17526it).
17527
17528I work in a fast-paced environment where efficiency is extremely valuable.
17529Therefore, I have always held a deep displeasure for conversations where people
17530start with "Hello" and then.
17531
17532I searched back through my work messages and found that I received ~50 messages
17533from ~10 people last year from people that contained "hi", "hey", or "hello" and
17534did not contain any indication of the purpose of the conversation. I also
17535noticed that a few of the users were responsible for the large majority of the
17536cliffhangers.
17537
17538There's no real point to this post, just a desparate request for people to
17539please stop doing this.
17540#+date: <2024-01-09 Tue 00:00:00>
17541#+title: A Guide to Customizing macOS with Terminal, Window Managers, and Widgets
17542#+description: Detailed overview of methods and tools for modifying macOS system settings, including terminal usage, window management, and interface customization.
17543#+slug: macos-customization
17544
17545I have been using macOS more than Linux lately, so I wrote this post to describe
17546some simple options to customize macOS beyond the normal built-in settings menu.
17547
17548While not all-encompassing, the options below should be a good start for anyone
17549looking to dive down the rabbit hole.
17550
17551* Basics
17552
17553** Package Management
17554
17555To install a lot of software on macOS, you will need [[https://brew.sh/][Homebrew]]. You can use their
17556installation script to get started. Simply open the =Terminal= application and
17557paste the following snippet:
17558
17559#+begin_src sh
17560/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
17561#+end_src
17562
17563This will allow you to easily install and manage applications and other software
17564easily through the =brew= command.
17565
17566** Terminal
17567
17568If you're serious about customizing your macOS system, I highly recommend
17569installing a terminal emulator that you like and if you're not comfortable on
17570the command line yet, start learning. A lot of customization options require you
17571to edit hidden files, which is easiest in a terminal.
17572
17573There are options like iTerm2, Kitty, Alacritty, Hyper, Warp, or the built-in
17574Terminal app.
17575
17576I use [[https://iterm2.com/][iTerm2]], which can be installed with Homebrew:
17577
17578#+begin_src sh
17579brew install iterm2
17580#+end_src
17581
17582To install color schemes, such as the Dracula scheme, you visit [[https://iterm2colorschemes.com/][iTerm Themes]] and
17583follow their installation instructions to install any of the themes.
17584
17585* Desktop
17586
17587** Window Management
17588
17589[[https://github.com/koekeishiya/yabai][yabai]] is a tiling window manager for macOS. While other window managers exist, I
17590found that most of them struggled to create logical layouts and to allow me to
17591easily move windows around the screen.
17592
17593Some advanced settings for yabai are only available if partially disable System
17594Integrity Protection (SIP). However, I chose not to do this and it hasn't
17595affected my basic usage of yabai at all.
17596
17597Refer to the [[https://github.com/koekeishiya/yabai/wiki/Installing-yabai-(latest-release)][yabai wiki]] for installation instructions. You will need to ensure
17598that yabai is allowed to access the accessibility and screen recording APIs.
17599
17600** Keyboard Shortcuts
17601
17602[[https://github.com/koekeishiya/skhd][skhd]] is a simple hotkey daemon that allows you to define hotkeys in a file for
17603usage on your system.
17604
17605Installation is simple:
17606
17607#+begin_src sh
17608brew install koekeishiya/formulae/skhd
17609skhd --start-service
17610#+end_src
17611
17612After installation, be sure to allow =skhd= access to the accessibility API in
17613the macOS privacy settings.
17614
17615You can configure your hotkeys in the =~/.config/skhd/skhdrc= file:
17616
17617#+begin_src sh
17618nano ~/.config/skhd/skhdrc
17619#+end_src
17620
17621For example, I have hotkeys to open my browser and terminal:
17622
17623#+begin_src conf
17624# Terminal
17625cmd - return : /Applications/iTerm.app/Contents/MacOS/iTerm2
17626
17627# Browser
17628cmd + shift - return : /Applications/LibreWolf.app/Contents/MacOS/librewolf
17629#+end_src
17630
17631** Widgets
17632
17633[[https://github.com/felixhageloh/uebersicht/][uebersicht]] is a handy desktop-based widget tool with a plethora of
17634community-made widgets available in the [[https://tracesof.net/uebersicht-widgets/][widgets gallery]]. You can also write your
17635own widgets with this tool.
17636
17637To install, simply download the latest release from the [[https://tracesof.net/uebersicht/][uebersicht website]] and
17638copy it to the Applications folder.
17639
17640See below for an example of the [[https://tracesof.net/uebersicht-widgets/#Mond][Mond]] widget in action.
17641
17642** Status Bar
17643
17644[[https://github.com/FelixKratz/SketchyBar][SketchyBar]] is a customizable replacement for the macOS status or menu bar.
17645
17646You can browse a discussion where various users shared their [[https://github.com/FelixKratz/SketchyBar/discussions/47?sort=top][configurations]] for
17647inspiration or to copy their dotfiles.
17648
17649** Dock
17650
17651The easiest way to customize the dock is to install [[https://ubarapp.com/][uBar]], which uses a
17652Windows-like menu bar as the default style.
17653
17654However, the built-in macOS dock cannot be disabled and can only be set to
17655"always hidden". This can be annoying as it will pop out any time your mouse
17656cursor passes closely to the dock edge of the screen. Because of this, I simply
17657use the built-in dock instead of customizing it with third-party software.
17658
17659Regardless, see below for the default installation style of uBar.
17660
17661** Application Icons
17662
17663You can also customize the icon of any application in macOS, which will show up
17664in Finder, the Dock, Launchpad, search results, etc. I recommend using
17665[[https://macosicons.com/][macOSicons]] to download icons you want, and then apply them by following this
17666process.
17667
176681. Open the Finder application.
176692. Navigate to the =Applications= folder.
176703. Right-click an application of your choice, and select =Get Info=.
176714. Drag the image you downloaded on top of the application's icon at the top of
17672 information window (you will see a green "plus" symbol when you're hovering
17673 over it).
176745. Release the new icon on top of the old icon and it will update!
17675#+date: <2024-01-13 Sat 00:00:00>
17676#+title: Running Local Large Language Models on macOS and iOS: A Practical Guide
17677#+description: Technical guide to deploying and running local large language models on macOS and iOS devices focusing on open-source implementations and privacy considerations.
17678#+slug: local-llm
17679
17680* Requirements
17681
17682I've recently started playing with large language models (LLMs), mostly in the
17683popular chatbot form, as part of my job and have decided to see if there's a
17684consistent and reliable way to interact with these models on Apple devices
17685without sacrificing privacy or requiring in-depth technical setup.
17686
17687My requirements for this test:
17688
17689- Open source platform
17690- On-device model files
17691- Minimal required configuration
17692- Preferably pre-built, but a simple build process is acceptable
17693
17694I tested a handful of apps and have summarized my favorite (so far) for macOS
17695and iOS below.
17696
17697#+begin_quote
17698TL;DR - Here are the two that met my requirements and I have found the easiest
17699to install and use so far:
17700#+end_quote
17701
17702- macOS: [[https://ollama.ai/][Ollama]]
17703- iOS : [[https://llmfarm.site/][LLM Farm]]
17704
17705* macOS
17706
17707[[https://ollama.ai/][Ollama]] is a simple Go application for macOS and Linux that can run various LLMs
17708locally.
17709
17710For macOS, you can download the pplication on the [[https://ollama.ai/download/mac][Ollama download page]] and
17711install it by unzipping the =Ollama.app= file and moving it to the
17712=Applications= folder.
17713
17714If you prefer the command line, you can run these commands after the
17715download finished:
17716
17717#+begin_src sh
17718cd ~/Downloads && \
17719unzip Ollama-darwin.zip && \
17720mv ~/Downloads/Ollama.app /Applications/
17721#+end_src
17722
17723After running the app, the app will ask you to open a terminal and run the
17724default =llama2= model, which will open an interactive chat session in the
17725terminal. You can startfully using the application at this point.
17726
17727If you don't want to use the default =llama2= model, you can download and run
17728additional models found on the [[https://ollama.ai/library][Models]] page.
17729
17730To see the information for the currently-used model, you can run the
17731=/show info= command in the chat.
17732
17733** Community Integrations
17734
17735I highly recommend browsing the [[https://github.com/jmorganca/ollama#community-integrations][Community Integrations]] section of the project to
17736see how you would prefer to extend Ollama beyond a simple command-line
17737interface. There are options for APIs, browser UIs, advanced terminal
17738configurations, and more.
17739
17740* iOS
17741
17742While there are a handful of decent macOS options, it was quite difficult to
17743find an iOS app that offered an open source platform without an extensive
17744configuration and building process. I found LLM Farm to be decent enough in
17745quality to sit at the top of my list - however, it's definitely not user
17746friendly enough for me to consider using it on a daily basis.
17747
17748[[https://llmfarm.site/][LLM Farm]] is available on TestFlight, so there's no manual build process
17749required. However, you can view the [[https://github.com/guinmoon/LLMFarm][LLMFarm repository]] if you wish.
17750
17751The caveat is that you will have to manually download the model files from the
17752links in the [[https://github.com/guinmoon/LLMFarm/blob/main/models.md][models.md]] file to your iPhone to use the app - there's currently no
17753option in the app to reach out and grab the latest version of any supported
17754model.
17755
17756Once you have a file downloaded, you simply create a new chat and select the
17757downloaded model file and ensure the inference matches the requirement in the
17758=models.md= file.
17759
17760[[https://github.com/AugustDev/enchanted][Enchanted]] is also an iOS for private AI models, but it requires a public-facing
17761Ollama API, which did not meet my "on device requirement." Nonetheless, it's an
17762interesting looking app and I will likely set it up to test soon.
17763#+date: <2024-01-26 Fri 00:00:00>
17764#+title: Building an Interactive Audit Dashboard with Alteryx and Power BI
17765#+description: Instructions for transforming audit data into dynamic dashboards via data preparation with Alteryx and visualization using Microsoft Power BI.
17766#+slug: audit-dashboard
17767
17768Alteryx and Power BI are powerful tools that can help turn your old-school audit
17769trackers into interactive tools that provide useful insights and potential
17770action plans.
17771
17772With these tools, we are going to build a dashboard that can effectively
17773communicate project status.
17774
17775* Requirements
17776
17777This project assumes the following:
17778
17779- You have access to Alteryx Designer and Power BI Desktop.
17780 - If you only have Power BI Desktop, you may need to perform some analysis in
17781 Power BI instead of Alteryx.
17782- Your data is in a format that can be imported into Alteryx and/or Power BI.
17783- You have a basic understanding of data types and visualization.
17784
17785* Alteryx: Data Preparation & Analysis
17786
17787** Import Data
17788
17789With Alteryx, importing data is easy with the use of the =Input Data= tool.
17790Simply drag this tool onto the canvas from the =In/Out= tab in the Ribbon to
17791create it as a node.
17792
17793You can choose the File Format manually or simply connect to your file/database
17794and let Alteryx determine the format for you. For this example, we will be
17795importing an Excel file and changing the =Start Data Import on Line= variable to
17796=2=.
17797
17798** Transform Data
17799
17800Next, let's replace null data and remove whitespace to clean up our data. We can
17801do this with the =Data Cleansing= tool in the =Preparation= tab in the Ribbon.
17802
17803Ensure that the following options are enabled:
17804
17805- Replace Nulls
17806 - Replace with Blanks (String Fields)
17807 - Replace with 0 (Numeric Fields)
17808- Remove Unwanted Characters
17809 - Leading and Trailing Whitespace
17810
17811For our next step, we will transform the date fields from strings to datetime
17812format. Add a =Datetime= tool for each field you want to transform - in the
17813example below, I am using the tool twice for the "Started On" and "Submitted On"
17814fields.
17815
17816Now that the dates are in the correct format, let's perform a calculation based
17817on those fields. Start by adding a =Filter= tool, naming a new Output Column,
17818and pasting the formula below into it (the two fields used in this formula must
17819match the output of the =Datetime= tools above):
17820
17821```txt
17822DateTimeDiff([SubmittedOn_Out],[StartedOn_Out], "days")
17823```
17824
17825** Export Data
17826
17827Finalize the process by exporting the transformed data set to a new file, for
17828use in the following visualization step.
17829
17830* Power BI: Data Visualization
17831
17832** Import Data
17833
17834To start, open the Power BI Desktop application. Upon first use, Power BI will
17835ask if you want to open an existing dashboard or import new data.
17836
17837As we are creating our first dashboard, let's import our data. In my example
17838below, I'm importing data from the "Tracker" sheet of the Excel file I'm using
17839for this project.
17840
17841During this process, I also imported the export from the Alteryx workflow above.
17842Therefore, we have two different files available for use in our dashboard.
17843
17844** Add Visuals
17845
17846To create the dashboard, you will need to follow the list instructions below and
17847format as needed.
17848
17849Instructions to create the visuals above:
17850
17851- =Text Box=: Explain the name and purpose of the dashboard. You can also add
17852 images and logos at the top of the dashboard.
17853- =Donut Chart=: Overall status of the project.
17854 - =Legend=: Status
17855 - =Values=: Count of Status
17856- =Stacked Column Chart=: Task count by assignee.
17857 - =X-axis=: Preparer
17858 - =Y-axis=: Count of Control ID
17859 - =Legend=: Status
17860- =Treemap=: Top N client submitters by average days to submit.
17861 - =Details=: Preparer
17862 - =Values=: Sum of Avg_{DaysToSubmit}
17863- =Line Chart=: Projected vs. actual hours over time.
17864- =Clustered Bar Chart=: Projected vs. actual hours per person.
17865- =Slicer & Table= - Upcoming due dates.
17866 - =Slicer=:
17867 - =Values=: Date Due
17868 - =Table=:
17869 - =Columns=: Count of Control ID, Date Due, Preparer, Status
17870
17871** Format the Dashboard
17872
17873You can choose a theme in the View tab of the Ribbon. You can even browse for
17874custom JSON files that define themes, such as ones found online or custom ones
17875created by your organization.
17876
17877For each visual, you can click the =Format= button in the =Visualizations= side
17878pane and explore the options. You can custom options such as:
17879
17880- Visual
17881 - Legend
17882 - Colors
17883 - Data labels
17884 - Category labels
17885- General
17886 - Properties
17887 - Title
17888 - Effects
17889 - Header icons
17890 - Tooltips
17891 - Alt text
17892
17893You can always look online for inspiration when trying to decide how best to
17894organize and style your dashboard.
17895
17896* Sharing the Results
17897
17898Generally, you have a few different options for sharing your dashboards with
17899others:
17900
179011. Export the dashboard as a PDF in the file menu of Power BI. This will export
17902 all tabs and visuals as they are set when the export button is pressed. You
17903 will lose all interactivity with this option.
179042. Send the full Power BI file to those you wish to share the dashboard. This
17905 will retain all settings and interactivity. However, you will also need to
17906 send the source files if they need to refresh the dashboard and you will need
17907 to re-send the files if you make updates.
179083. Store the dashboard in a synced location, such as a shared drive or Microsoft
17909 Teams. Depending on how a user configures their local Windows paths, the data
17910 source paths may not be compatible for all users with such a setup.
17911#+date: <2024-01-27 Sat 00:00:00>
17912#+title: Preparing and Visualizing Omaha Crime Data (2015-2023) in Tableau
17913#+description: Detailed procedure for data preparation and interactive visualization of crime trends in Omaha, using Tableau software from 2015 through 2023 data.
17914#+slug: tableau-dashboard
17915
17916In this project, I am going to show you how to use Tableau Public for free to
17917create simple dashboards.
17918
17919I will be creating simple visuals from an Omaha crime data set and combining
17920them to create the dashboard below. You can view this dashboard interactively
17921online here: [[https://public.tableau.com/app/profile/c.c7042/viz/OmahaCrimeData2015-2023/OmahaCrimeData2015-2023#1][Omaha Crime Data (2015 - 2023)]].
17922
17923#+caption: Tableau Dashboard
17924#+attr_html: :alt A Tableau dashboard showing a map of Omaha with a dot for each crime, a bar chart with totals per crime category, and a line chart showing a timeline of crime totals by month.
17925[[https://img.cleberg.net/blog/20240127-tableau-dashboard/dashboard.webp]]
17926
17927* Gather the Data
17928
17929You can download incident data from the Omaha Police Department on their
17930[[https://police.cityofomaha.org/crime-information/incident-data-download][Incident Data Download]] page. They currently have files for the years 2015
17931through 2023.
17932
17933Each file will be downloaded as a CSV file, approximately 3 MB - 8 MB.
17934
17935* Clean and Transform the Data
17936
17937I have used Python to combine the files into a single CSV file, as well as
17938adding a custom =datetime= column. You could do this step in any software you
17939prefer, but I prefer Python as its free, easy to use, and has a plethora of
17940support resources online.
17941
17942Start by opening a terminal, navigating to your Downloads directory, and
17943creating a python script.
17944
17945#+begin_src sh
17946cd ~/Downloads
17947nano data_processing.py
17948#+end_src
17949
17950Within the Python script, paste the following:
17951
17952#+begin_src python
17953# Import modules
17954import pandas as pd
17955import glob
17956import os
17957
17958# Import the data
17959path = r"~/Downloads/*.csv"
17960files = glob.glob(path)
17961
17962list = []
17963
17964for file in files:
17965 df_tmp = pd.read_csv(file)
17966 li.append(df_tmp)
17967
17968df = pd.concat(list, axis=0, ignore_index=True)
17969
17970# Create a combined datetime column
17971df["datetime"] = pd.to_datetime(
17972 df["date"] + " " + df["time"],
17973 format="%m/%d/%Y %H:%M:%S"
17974)
17975df.head()
17976
17977# Export the combined data
17978df.to_csv(r"~/Downloads/combined_incidents.csv")
17979#+end_src
17980
17981Once pasted, save and close the file. You can execute the file like so:
17982
17983#+begin_src sh
17984python3 data_processing.py
17985#+end_src
17986
17987After this, you should have a combined data file that contains all incidents
17988between 2015 and 2023. Mine was approximately 55 MB.
17989
17990* Tableau Public
17991
17992[[https://public.tableau.com/][Tableau Public]] is a free-to-use web application that allows you to create
17993visualizations by uploading data sources. Note that there's no way to keep the
17994data and visualizations private, so don't upload anything private.
17995
17996After creating an account, you can click the =Create= > =Web Authoring= link to
17997create your first visualization.
17998
17999** Upload the Data
18000
18001Once you've opened your first project, Tableau will ask you to connect to your
18002data. For this project, click the =Upload from computer= button and select the
18003CSV file previously combined in the step above.
18004
18005Once connected, you can refresh the preview of the data with the =Refresh Data
18006Source= button in the toolbar.
18007
18008If you need to edit any of the data types, column names, etc., you can do so
18009now. Once complete, generate an extract so that you can start creating
18010visualizations.
18011
18012** Create Visualizations
18013
18014To start, create a worksheet in the toolbar at the bottom of the screen.
18015
18016Within this screen, select a column from the =Data= side bar on the left and
18017drag it into the =Columns= or =Rows= area of the canvas.
18018
18019See below for the map visualization. You can recreate this by adding the
18020following fields:
18021
18022- =Columns=: Lon
18023- =Rows=: Lat
18024- =Marks=:
18025 - Description
18026 - Datetime
18027- =Filters=: Datetime
18028
18029You can repeat this process for each visualization you want to create. Explore
18030your options by dragging data fields to different areas and by opening the field
18031options to explore what operations can be performed on different data types
18032(e.g., average, count, etc.).
18033
18034** Create Dashboard
18035
18036To create a dashboard, click the button on the toolbar at the bottom of the
18037screen. Within the dashboard, drag each sheet from the left side bar onto the
18038dashboard canvas.
18039
18040** Formatting
18041
18042You can explore a ton of different formatting options throughout the worksheets
18043and dashboard. Specifically for maps, you can alter the map layers, background,
18044and visible features through the =Map= menu in the top file menu of the editing
18045screen.
18046
18047In the finished dashboard below, I opted for a dark mode with a map that showed
18048county lines and city names.
18049
18050There's a ton of other options available to be used in a dashboard like this,
18051but this project shows a quick preview of what you can do in Tableau Public.
18052
18053#+caption: Tableau Dashboard
18054#+attr_html: :alt A Tableau dashboard showing a map of Omaha with a dot for each crime, a bar chart with totals per crime category, and a line chart showing a timeline of crime totals by month.
18055[[https://img.cleberg.net/blog/20240127-tableau-dashboard/dashboard.webp]]
18056#+date: <2024-02-06 Tue 00:00:00>
18057#+title: Creating and Managing ZFS Storage Pools on Ubuntu Linux
18058#+description: Stepwise guide to setting up ZFS storage pools on Ubuntu, including initial installation, pool creation, expansion, and maintenance.
18059#+slug: zfs
18060
18061This post details the process I used to create ZFS pools, datasets, and
18062snapshots on Ubuntu Server.
18063
18064I found the following pages very helpful while going through this process:
18065
18066- [[https://ubuntu.com/tutorials/setup-zfs-storage-pool][Setup a ZFS storage pool]]
18067- [[https://wiki.ubuntu.com/Kernel/Reference/ZFS][Kernel/Reference/ZFS]]
18068- [[https://blog.victormendonca.com/2020/11/03/zfs-for-dummies/][ZFS for Dummies]]
18069
18070* Installation
18071
18072To start, I installed the ZFS package with the following command:
18073
18074#+begin_src sh
18075sudo apt install zfsutils-linux
18076#+end_src
18077
18078Once installed, you can check the version to see if it installed correctly.
18079
18080#+begin_src sh
18081> zsf --version
18082
18083zfs-2.1.5-1ubuntu6~22.04.2
18084zfs-kmod-2.1.5-1ubuntu6~22.04.1
18085#+end_src
18086
18087* ZFS Configuration
18088
18089Now that ZFS is installed, we can create and configure the pool.
18090
18091You have various options for configuring ZFS pools that all come different pros
18092and cons. I suggest visiting the links at the top of this post or searching
18093online for the best configuration for your use-case.
18094
18095- Striped VDEVs (Raid0)
18096- Mirrored VDEVs (Raid1)
18097- Striped Mirrored VDEVs (Raid10)
18098- RAIDz (Raid5)
18099- RAIDz2 (Raidd6)
18100- RAIDz3
18101- Nested RAIDz (Raid50, Raid60)
18102
18103I will be using Raid10 in this guide. However, the majority of the steps are the
18104same regardless of your chosen pool configuration.
18105
18106** Creating the Pool
18107
18108To start, let's list the disks available to use. You can use =fdisk= command to
18109see all available disks.
18110
18111#+begin_src sh
18112sudo fdisk -l
18113#+end_src
18114
18115Or, if you currently have them mounted, you can use the =df= command to view
18116your disks.
18117
18118#+begin_src sh
18119> sudo df -h
18120
18121Filesystem Size Used Avail Use% Mounted on
18122...
18123/dev/sda1 7.3T 28K 6.9T 1% /mnt/red-01
18124/dev/sdb1 7.3T 144G 6.8T 3% /mnt/red-02
18125/dev/sdc1 7.3T 5.5T 1.9T 75% /mnt/white-02
18126/dev/sdd1 9.1T 8.7T 435G 96% /mnt/white-01
18127/dev/sde1 7.3T 28K 6.9T 1% /mnt/red-03
18128/dev/sdf1 7.3T 28K 6.9T 1% /mnt/red-04
18129#+end_src
18130
18131If you're going to use mounted disks, make sure to umount them before creating
18132the pool.
18133
18134#+begin_src sh
18135sudo umount /dev/sda1
18136sudo umount /dev/sdb1
18137#+end_src
18138
18139Now that I've identified the disks I want to use and have them unmounted, let's
18140create the pool. For this example, I will call it =tank=.
18141
18142#+begin_src sh
18143sudo zpool create -f -m /mnt/pool tank mirror /dev/sda /dev/sdb
18144#+end_src
18145
18146See below for the results of the new ZFS pool named =tank=, with a vdev
18147automatically named =mirror-0=.
18148
18149#+begin_src sh
18150> zfs list
18151
18152NAME USED AVAIL REFER MOUNTPOINT
18153tank 396K 7.14T 96K /tank
18154#+end_src
18155
18156#+begin_src sh
18157> zpool status
18158
18159 pool: tank
18160 state: ONLINE
18161config:
18162
18163 NAME STATE READ WRITE CKSUM
18164 tank ONLINE 0 0 0
18165 mirror-0 ONLINE 0 0 0
18166 sda ONLINE 0 0 0
18167 sdb ONLINE 0 0 0
18168
18169errors: No known data errors
18170#+end_src
18171
18172We can also look at the mounted filesystem to see where the pool is mounted and
18173some quick stats.
18174
18175#+begin_src sh
18176> df -h
18177
18178Filesystem Size Used Avail Use% Mounted on
18179...
18180tank 7.2T 128K 7.2T 1% /tank
18181#+end_src
18182
18183** Expanding the Pool
18184
18185If you want to expand this pool, you will need to add a new VDEV to the pool.
18186Since I am using 2 disks per VDEV, I will need to add a new 2-disk VDEV to the
18187existing =tank= pool.
18188
18189#+begin_src sh
18190sudo zpool add tank mirror /dev/sdX /dev/sdY
18191#+end_src
18192
18193If you're adding disks of different sizes, you'll need to use the =-f= flag.
18194Keep in mind that the max size will be limited to the smallest disk added.
18195
18196#+begin_src sh
18197sudo zpool add -f tank mirror /dev/sdX /dev/sdY
18198#+end_src
18199
18200I added two 8TB hard drives and this process took around 10 seconds to complete.
18201
18202When viewing the pool again, you can see that the pool has now doubled in size.
18203We have 14.3 TB useable space and the same space used for mirroring.
18204
18205#+begin_src sh
18206> zfs list
18207
18208NAME USED AVAIL REFER MOUNTPOINT
18209tank 145G 14.3T 104K /tank
18210tank/cloud 145G 14.3T 145G /tank/cloud
18211tank/media 96K 14.3T 96K /tank/media
18212#+end_src
18213
18214*** Converting Disks
18215
18216Some disks, such as NTFS-formatted drives, will need to be partitioned and
18217formatted prior to being added to the pool.
18218
18219Start by identifying the disks you want to format and add to the pool.
18220
18221#+begin_src sh
18222sudo fdisk -l | grep /dev
18223#+end_src
18224
18225I am going to format my =/dev/sdc= and =/dev/sdd= disks with the =fdisk=
18226command.
18227
18228See below for instructions on how to use =fdisk=. Here's what I did to create
18229basic Linux formatted disks:
18230
18231- =g= : Create GPT partition table
18232- =n= : Create a new partition, hit Enter for all default options
18233- =t= : Change partition type to =20= for =Linux filesystem=
18234- =w= : Write the changes to disk and exit
18235
18236I repeated this process for both disks.
18237
18238#+begin_src sh
18239> sudo fdisk /dev/sdc
18240
18241Welcome to fdisk (util-linux 2.37.2). Changes will remain in memory only, until
18242you decide to write them. Be careful before using the write command.
18243
18244This disk is currently in use - repartitioning is probably a bad idea. It's
18245recommended to umount all file systems, and swapoff all swap partitions on this
18246disk.
18247
18248
18249Command (m for help): m
18250
18251Help:
18252
18253 GPT
18254 M enter protective/hybrid MBR
18255
18256 Generic
18257 d delete a partition
18258 F list free unpartitioned space
18259 l list known partition types
18260 n add a new partition
18261 p print the partition table
18262 t change a partition type
18263 v verify the partition table
18264 i print information about a partition
18265
18266 Misc
18267 m print this menu
18268 x extra functionality (experts only)
18269
18270 Script
18271 I load disk layout from sfdisk script file
18272 O dump disk layout to sfdisk script file
18273
18274 Save & Exit
18275 w write table to disk and exit
18276 q quit without saving changes
18277
18278 Create a new label
18279 g create a new empty GPT partition table
18280 G create a new empty SGI (IRIX) partition table
18281 o create a new empty DOS partition table
18282 s create a new empty Sun partition table
18283#+end_src
18284
18285Once the drives are formatted, we can add these disks to the pool.
18286
18287#+begin_src sh
18288sudo zpool add tank mirror /dev/sdc /dev/sdd
18289#+end_src
18290
18291When we list the pool again, we can see that our size is now updated to
18292approximately 22TB. This represents my hard drives totalling 45.6TB when shown
18293with =fdisk -l=, with a Raid10 configuration using 22TB for mirroring and 22TB
18294of useable space.
18295
18296#+begin_src sh
18297> zfs list
18298
18299NAME USED AVAIL REFER MOUNTPOINT
18300tank 145G 21.7T 104K /tank
18301tank/cloud 145G 21.7T 145G /tank/cloud
18302tank/media 145GT 21.7T 96K /tank/media
18303#+end_src
18304
18305** Creating Datasets
18306
18307According to [[https://docs.oracle.com/cd/E18752_01/html/819-5461/ftyue.html][ZFS Terminology]], a =dataset= can refer to “clones, file systems,
18308snapshots, and volumes.
18309
18310For this guide, I will use the =dataset= term to refer to file systems created
18311under a pool.
18312
18313Within my =tank= pool, I am going to create some datasets to help organize my
18314files. This will give me location to store data rather than simply dumping
18315everything at the =/tank/= location.
18316
18317#+begin_src sh
18318sudo zfs create tank/cloud
18319sudo zfs create tank/media
18320#+end_src
18321
18322Once created, you can see these datasets in the output of your pool
18323list:
18324
18325#+begin_src sh
18326> zfs list
18327NAME USED AVAIL REFER MOUNTPOINT
18328tank 752K 7.14T 104K /tank
18329tank/cloud 96K 7.14T 96K /tank/cloud
18330tank/media 96K 7.14T 96K /tank/media
18331#+end_src
18332
18333** Creating Snapshots
18334
18335Next, let's create our first snapshot. We can do this by calling the =snapshot=
18336command and give it an output name. I will be throwing the current date and time
18337into my example.
18338
18339#+begin_src sh
18340sudo zfs snapshot tank@$(date '+%Y-%m-%d_%H-%M')
18341#+end_src
18342
18343We can list the snapshots in our pool with the following command:
18344
18345#+begin_src sh
18346> zfs list -t snapshot
18347NAME USED AVAIL REFER MOUNTPOINT
18348tank@2024-02-06_19-41 0B - 104K -
18349#+end_src
18350
18351** Destroy Snapshots
18352
18353You can always destroy snapshots that are no longer needed:
18354
18355#+begin_src sh
18356sudo zfs destroy tank@2024-02-06_19-41
18357#+end_src
18358
18359Once deleted, they will no longer appear in the list:
18360
18361#+begin_src sh
18362> zfs list -t snapshot
18363no datasets available
18364#+end_src
18365
18366* My Thoughts on ZFS So Far
18367
18368- I sacrificed 25TB to be able to mirror my data, but I feel more comfortable
18369 with the potential to save my data by quickly replacing a disk if I need to.
18370- The set-up was surprisingly easy and fast.
18371- Disk I/O is fast as well. I was worried that the data transfer speeds would be
18372 slower due to the RAID configuration.
18373- Media streaming and transcoding has seen no noticeable drop in performance.
18374- My only limitation really is the number of HDD bays in my server HDD cage.
18375#+date: <2024-02-13 Tue 00:00:00>
18376#+title: Resolving Ubuntu Boot Issues Caused by Invalid /etc/fstab Mounts
18377#+description: Detailed instructions to identify errors in the /etc/fstab file causing Ubuntu to enter emergency mode and steps to edit and restore normal boot operations.
18378#+slug: ubuntu-emergency-mode
18379
18380* The Problem
18381
18382I recently [[../zfs/][migrated my hard drives to a ZFS pool]] and found myself stuck in
18383Ubuntu's emergency mode after the first reboot I performed after creating the
18384ZFS pool.
18385
18386My server was stuck in the boot process and showed the following error on the
18387screen:
18388
18389#+begin_src txt
18390You are in emergency mode.
18391After logging in, type "journalctl -xb" to view system logs,
18392"systemctl reboot" to reboot, "systemctl default"
18393or ^D to try again to boot into default mode".
18394#+end_src
18395
18396After rebooting the server and watching the logs scroll on a monitor, I noticed
18397the root cause was related to a very long search for certain drives. I kept
18398seeing errors like this:
18399
18400#+begin_src txt
18401[ TIME ] Timed out waiting of device dev-disk-by/[disk-uuid]
18402#+end_src
18403
18404I realized that I had not removed the =/etc/fstab= references that asked Ubuntu
18405to mount two disks on boot, but I had recently changed those disks to be part of
18406my ZFS pool instead. Therefore, Ubuntu was trying to identify and mount a disk
18407that was not available.
18408
18409Now that we have an idea of the issue, let's move to solution.
18410
18411* The Solution
18412
18413In order to fix the issue, I waited until I was allowed to type the root user's
18414password, and then I executed the following command:
18415
18416#+begin_src sh
18417nano /etc/fstab
18418#+end_src
18419
18420Within the =fstab= file, I needed to comment/remove the following lines at the
18421bottom of the file. You can comment-out a line by prepending a =#= symbol at the
18422beginning of the line. You can also delete the line entirely.
18423
18424#+begin_src conf
18425# What it looked like when running into the issue:
18426UUID=B64E53824E5339F7 /mnt/white-01 ntfs-3g uid=1000,gid=1000 0 0
18427UUID=E69867E59867B32B /mnt/white-02 ntfs-3g uid=1000,gid=1000 0 0
18428
18429# What I changed it to, in order to fix the issue:
18430# UUID=B64E53824E5339F7 /mnt/white-01 ntfs-3g uid=1000,gid=1000 0 0
18431# UUID=E69867E59867B32B /mnt/white-02 ntfs-3g uid=1000,gid=1000 0 0
18432#+end_src
18433
18434Once removing the lines above from the =/etc/fstab= file, save and exit the file
18435by hitting the =Ctrl= + =x= key combo.
18436
18437You can now hit =Ctrl= + =D= to continue, or reboot:
18438
18439#+begin_src sh
18440systemctl reboot
18441#+end_src
18442
18443Once rebooted, I was able to watch the machine boot properly and launch to the
18444TTY login screen without errors!
18445#+date: <2024-02-21 Wed 00:00:00>
18446#+title: Self-Hosting Guide: An Otter Wiki
18447#+description: Stepwise directions for installing Otter Wiki, configuring Docker Compose environment, and setting up Nginx reverse proxy for secure access.
18448#+slug: self-hosting-otter-wiki
18449
18450* An Otter Wiki
18451
18452[[https://otterwiki.com/][An Otter Wiki]] is an easy to use wiki software that takes almost no effort to set
18453up and maintain.
18454
18455* Installation
18456
18457To install An Otter Wiki, I'm going to use Docker Compose to create the
18458container and then use Nginx as a reverse proxy to allow external access via a
18459subdomain I own.
18460
18461** Docker Compose
18462
18463Start by creating a directory for the container's files.
18464
18465#+begin_src sh
18466mkdir ~/otterwiki
18467#+end_src
18468
18469Next, create the =docker-compose.yml= file to define the container's parameters.
18470
18471#+begin_src sh
18472nano ~/otterwiki/docker-compose.yml
18473#+end_src
18474
18475Within the file, paste the following content. You can read the project's
18476documentation if you want to further override or customize the container.
18477
18478#+begin_src conf
18479version: '3'
18480services:
18481 otterwiki:
18482 image: redimp/otterwiki:2
18483 restart: unless-stopped
18484 # Internal port must be assigned to port 80
18485 # External port can be customized
18486 ports:
18487 - 8337:80
18488 volumes:
18489 - ./app-data:/app-data
18490#+end_src
18491
18492Once the file is saved and closed, you can launch the container.
18493
18494#+begin_src sh
18495cd ~/otterwiki
18496sudo docker-compose up -d
18497#+end_src
18498
18499The container is now available at =localhost:8337=. Next, we will use Nginx to
18500serve this app externally.
18501
18502** Nginx
18503
18504To access the app externally, let's set up a reverse proxy. I'll start by
18505creating the Nginx configuration file for my wiki.
18506
18507#+begin_src sh
18508sudo nano /etc/nginx/sites-available/wiki
18509#+end_src
18510
18511Within the file, I have saved the following content. This assumes you already
18512have a TLS/SSL cert to use with this subdomain. If not, simply remove the
18513=ssl_*= variables, remove the =80= server block, and change the =443= server
18514block to =80= to serve the app without SSL.
18515
18516#+begin_src conf
18517server {
18518 if ($host ~ ^[^.]+\.example\.com$) {
18519 return 301 https://$host$request_uri;
18520 }
18521
18522 listen [::]:80;
18523 listen 80;
18524 server_name wiki.example.com;
18525 return 404;
18526}
18527
18528server {
18529 listen [::]:443 ssl http2;
18530 listen 443 ssl http2;
18531 server_name wiki.example.com;
18532 access_log /var/log/nginx/wiki.access.log;
18533 error_log /var/log/nginx/wiki.error.log;
18534
18535 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
18536 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
18537 include /etc/letsencrypt/options-ssl-nginx.conf;
18538 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
18539
18540 location / {
18541 set $upstream_wiki http://127.0.0.1:8337;
18542 proxy_pass $upstream_wiki;
18543
18544 proxy_set_header Host $http_host;
18545 proxy_set_header X-Real-IP $remote_addr;
18546 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
18547 proxy_set_header X-Forwarded-Host $http_host;
18548 }
18549}
18550#+end_src
18551
18552Save and close the configuration file. On Nginx, we need to symlink the file to
18553enable it.
18554
18555#+begin_src sh
18556sudo ln -s /etc/nginx/sites-available/wiki /etc/nginx/sites-enabled/wiki
18557#+end_src
18558
18559Once enabled, restart the Nginx server to start serving the app externally.
18560
18561#+begin_src sh
18562sudo systemctl restart nginx.service
18563#+end_src
18564#+date: <2024-03-13 Wed 00:00:00>
18565#+title: Doom Emacs & Org-Mode Setup
18566#+description: Technical manual covering installation, configuration files, and usage procedures for Doom Emacs and Org-Mode targeting note management and task organization.
18567#+slug: doom-emacs
18568
18569** Screenshots
18570
18571These screenshots are showing a project opened with projectile, a treemacs side
18572pane open with the project contents, multiple buffers tiled next to each other,
18573and the help pane open at the bottomm.
18574
18575The themes are =doom-homage-white= and =doom-homage-black=.
18576
18577#+caption: Doom Emacs Light Mode
18578#+attr_html: :alt An example of the doom-homage-white theme on the Doom dashboard.
18579[[https://img.cleberg.net/blog/20240314-doom-emacs/light.webp]]
18580
18581#+caption: Doom Emacs Dark Mode
18582#+attr_html: :alt An example of the doom-homage-black theme on the Doom dashboard.
18583[[https://img.cleberg.net/blog/20240314-doom-emacs/dark.webp]]
18584
18585** Getting Started
18586
18587I have been switching back and forth between [[https://en.wikipedia.org/wiki/Markdown][markdown]] and [[https://en.wikipedia.org/wiki/Org-mode][org-mode]] recently for
18588my personal note taking, wiki, and even this blog. As a result, I have been
18589stumbling further into the world of Emacs and found myself at a point where I
18590now prefer to do most of my basic editing within Emacs.
18591
18592I'll leave the markdown vs. org-mode debate for another post, but I love
18593org-mode's extensibility and interactive nature within Emacs, but it becomes
18594very unwieldy in any other client implementation of org-mode - especially on
18595iOS. On the flip side, markdown is limited in functionality and fractured into
18596different standards, but it's simple and popular enough that there are a
18597plethora of great clients to choose from that will get the job done.
18598
18599For now, I want to focus on how I have been using Emacs and some of the things
18600that would have helped me learn it faster had I known where to start.
18601
18602*** Installation
18603
18604This post focuses on [[https://github.com/doomemacs/doomemacs][Doom Emacs]], which is an Emacs framework that provides an
18605alternative experience to the vanilla [[https://www.gnu.org/software/emacs/][GNU Emacs]].
18606
18607The [[https://github.com/doomemacs/doomemacs/blob/master/docs/getting_started.org][Getting Start Guide]] has an extremely detailed walkthrough of installation
18608for all systems, so please refer to that guide for up-to-date instructions.
18609
18610I chose to install on macOS, using the Homebrew option with the
18611=railwaycat/emacsmacport= version of Emacs.
18612
18613Once the program is installed, you can run the program by typing =emacs= in a
18614terminal. If you installed a version of Emacs that supports both a GUI and TUI,
18615you will have to run =emacs -nw= to get the TUI instead of the default GUI.
18616
18617*** Configuration
18618
18619Once installed, you can configure Doom by editing the files within the
18620=~/.doom.d/= directory. This directory holds four files:
18621
186221. =config.el= - Personal configuration file
186232. =custom.el= - Custom set variables
186243. =init.el= - Doom modules and load order, must run =doom sync= after modifying
186254. =packages.el= - Declare packages to install in this file, then run =doom
18626 sync= to install
18627
18628I only needed a few customizations for my configuration, so I'll list them
18629below.
18630
18631#+begin_src lisp
18632;; ~/.doom.d/config.el
18633(setq doom-theme 'doom-homage-black)
18634(setq display-line-numbers-type t)
18635(setq org-directory "~/Documents/Notes/")
18636
18637;; lengthy org-publish directives at the bottom of the file
18638#+end_src
18639
18640#+begin_src lisp
18641;; ~/.doom.d/init.el
18642(doom! :input
18643 :completion
18644 company ; the ultimate code completion backend
18645 vertico ; the search engine of the future
18646
18647 :ui
18648 doom ; what makes DOOM look the way it does
18649 doom-dashboard ; a nifty splash screen for Emacs
18650 (emoji +unicode) ; 🙂
18651 hl-todo ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW
18652 minimap ; show a map of the code on the side
18653 modeline ; snazzy, Atom-inspired modeline, plus API
18654 ophints ; highlight the region an operation acts on
18655 (popup +defaults) ; tame sudden yet inevitable temporary windows
18656 tabs ; a tab bar for Emacs
18657 treemacs ; a project drawer, like neotree but cooler
18658 (vc-gutter +pretty) ; vcs diff in the fringe
18659 vi-tilde-fringe ; fringe tildes to mark beyond EOB
18660 workspaces ; tab emulation, persistence & separate workspaces
18661
18662 :editor
18663 (evil +everywhere); come to the dark side, we have cookies
18664 file-templates ; auto-snippets for empty files
18665 fold ; (nigh) universal code folding
18666 snippets ; my elves. They type so I don't have to
18667
18668 :emacs
18669 dired ; making dired pretty [functional]
18670 electric ; smarter, keyword-based electric-indent
18671 undo ; persistent, smarter undo for your inevitable mistakes
18672 vc ; version-control and Emacs, sitting in a tree
18673
18674 :term
18675 term ; basic terminal emulator for Emacs
18676
18677 :checkers
18678 syntax ; tasing you for every semicolon you forget
18679
18680 :tools
18681 (eval +overlay) ; run code, run (also, repls)
18682 lookup ; navigate your code and its documentation
18683 magit ; a git porcelain for Emacs
18684
18685 :os
18686 (:if (featurep :system 'macos) macos) ; improve compatibility with macOS
18687
18688 :lang
18689 common-lisp ; if you've seen one lisp, you've seen them all
18690 emacs-lisp ; drown in parentheses
18691 markdown ; writing docs for people to ignore
18692 org ; organize your plain life in plain text
18693 python ; beautiful is better than ugly
18694 sh ; she sells {ba,z,fi}sh shells on the C xor
18695
18696 :app
18697 irc ; how neckbeards socialize
18698 (rss +org) ; emacs as an RSS reader
18699
18700 (default +bindings +smartparens))
18701#+end_src
18702
18703If you're editing these files within Doom directly, remember to run =SPC h r r=
18704to reload the configuration. Also remember to run =doom sync= for any changes to
18705the =init.el= or =packages.el= files.
18706
18707** Basic Functionality
18708
18709I kept a cheat sheet note open at first with all of the basic functions typed
18710out, copied as I went through the tutorial. After a little while, I no longer
18711needed it. I highly recommend writing down the most applicable shortcuts for
18712your preferred functionality and refer back to it until you've memorized it.
18713
18714Memorizing the shortcuts will differ based on the type of Emacs framework being
18715used. Personally, migrating from vanilla Emacs to Doom Emacs simplified
18716everything by a large factor and instantly enabled me to start working on my
18717projects, eliminating most of the hurdles I was running into. The vanilla emacs
18718hotkeys became obnoxious and I actually stopped using Emacs entirely for about a
18719month before trying Doom.
18720
18721For me, the first logical step is to interact with the local filesystem. To do
18722this, I needed to know how to open directories, open files, save files, discard
18723changes, close files, and switch between open files. Here are some example
18724shortcuts I've written down in order to accomplish file-based actions.
18725
18726| Doom Hotkey | Emacs Hotkey | Description |
18727|-----------------+---------------+----------------------------------------|
18728| =SPC := | =C-x= | Run functions |
18729| =SPC f f= | =C-x f= | Open file in buffer |
18730| =SPC f d= | =C-x d= | Open directory with =dired= |
18731| =i= | =C-x C-q= | Edit current buffer (insert mode) |
18732| =q= | =C-x C-q= | Quit out of insert mode |
18733| =SPC f s= | =C-x s= | Save current buffer |
18734| =SPC b k= | =C-x k= | Kill current buffer |
18735| =SPC w h/j/k/l= | =C-x o=[fn:2] | Move left/down/up/right to next buffer |
18736
18737In general, when in Doom, you can press =SPC= and wait a second for the help
18738pane to appear with all available hotkey options. For example, you can press
18739=SPC=, wait for the help pane, and then select a key such as =g= to enter the
18740git help pane and explore further command options.
18741
18742** Editing
18743
18744Next in my process is to dive into editing for any languages I'm currently
18745using. In this post, I will just cover Markdown and Org-Mode but I have also
18746been slowly adoping some Python and general web dev tools as well.
18747
18748*** Markdown
18749
18750#+caption: Markdown Preview
18751#+attr_html: :alt An editing buffer opened to a markdown file.
18752[[https://img.cleberg.net/blog/20240314-doom-emacs/markdown.webp]]
18753
18754Markdown is fairly simple as the syntax is limited, so just make sure the
18755=~/.doom.d/init.el= includes the =markdown= declaration in the =:lang= section.
18756
18757This package includes the following hotkey menus. The insert and toggle menu
18758expands further, allowing you to insert various markdown elements and toggle
18759things like link hiding.
18760
18761| Doom Hotkey | Function |
18762|------------------------------+--------------------------|
18763| =SPC m '= | markdown-edit-code-block |
18764| =SPC m e= | markdown-export |
18765| =SPC m i= | +insert |
18766| =SPC m o= | markdown-open |
18767| =SPC m p= | markdown-preview |
18768| =SPC m t= | +toggle |
18769| =SPC : markdown-table-align= | markdown-table-align |
18770
18771*** Org-Mode
18772
18773#+caption: Org-Mode Preview
18774#+attr_html: :alt An editing buffer opened to an org-mode file.
18775[[https://img.cleberg.net/blog/20240314-doom-emacs/org.webp]]
18776
18777Similar to the markdown section above, ensure that the =~/.doom.d/init.el=
18778includes the =org= declaration in the =:lang= section.
18779
18780There are a few hot keys, but a quick search with =SPC : org= shows that there
18781are 865 possible org-related functions you can run. I won't possibly be able to
18782list them all, so I will simply cover a few of the basic commands I use myself.
18783
18784| Doom Hotkey | Function |
18785|----------------+---------------------------------------|
18786| =SPC m t= | org-todo |
18787| =SPC n t= | org-todo-list |
18788| =SPC o A= | org-agenda |
18789| =SPC X= | org-capture |
18790| =SPC m p p= | org-priority |
18791| =SPC m d s= | org-schedule |
18792| =TAB= | org-cycle |
18793| =SHIFT TAB= | Collapse/open all headings in buffer |
18794| =M-q= | Format/wrap current section |
18795| =M-Left/Right= | Demote/promote current heading |
18796| =M-Down/Up= | Shift current heading section down/up |
18797
187981. Org-Publish
18799
18800 Org includes a [[https://orgmode.org/manual/Publishing.html][publishing management system]] by default that allows you to
18801 export org files to Org, iCalendar, HTML, LaTex, Markdown, ODT, and Plain
18802 Text. Most of these can be exported into another buffer and opened, or simply
18803 to an external file.
18804
18805 While inside an org file, simply run =SPC m e= or =M-x org-export-dispatch=
18806 to open the export menu. This menu will show all options and ask you to
18807 select an option. If you want to export to HTML, simply press =h= and then
18808 =H= (As HTML buffer), =h= (As HTML file), or =o= (As HTML file and open).
18809
188102. Projects
18811
18812 Some publishing options are easier with a defined project in Emacs. To create
18813 a project within Emacs, I use two methods:
18814
18815 1. Add the project via the projectile command =SPC p a=. Does not always work
18816 for me.
18817 2. Add an empty =.projectile= file in the project root.
18818
18819 Once a project has been created, you can create custom publishing actions
18820 within your =~/.doom.d/config.el= file. For example, here's a test project I
18821 created to try and convert this blog to org-mode recently.
18822
18823 #+begin_src lisp
18824 ;; org-publish
18825 (require 'ox-publish)
18826
18827 (defun my/org-sitemap-date-entry-format (entry style project) "Format ENTRY in
18828 org-publish PROJECT Sitemap format ENTRY ENTRY STYLE format that includes
18829 date." (let ((filename (org-publish-find-title entry project))) (if (= (length
18830 filename) 0) (format "*%s*" entry) (format "{{{timestamp(%s)}}}
18831 [[file:%s][%s]]" (format-time-string "%Y-%m-%d" (org-publish-find-date entry
18832 project)) entry filename))))
18833
18834 (setq org-export-global-macros '(("timestamp" . "@@html:<time datetime='[$1]'
18835 class='timestamp'>[$1]</time>@@")))
18836
18837 (setq org-publish-project-alist
18838 `(("blog"
18839 :base-directory "~/Source/cleberg.net/"
18840 :base-extension "org"
18841 :recursive t
18842 :publishing-directory "~/Source/cleberg.net/public/"
18843 :publishing-function org-html-publish-to-html
18844 ;; HTML5
18845 :html-doctype "html5"
18846 :html-html5-fancy t
18847 ;; Disable some Org's HTML defaults
18848 :html-head-include-scripts nil
18849 :html-head-include-default-style nil
18850 :section-numbers nil
18851 :with-title nil
18852 ;; Sitemap
18853 :auto-sitemap t
18854 :sitemap-title: "Sitemap"
18855 :sitemap-sort-files anti-chronologically
18856 ; :sitemap-function my/org-sitemap-date-entry-format
18857 ;; Customize HTML output
18858 :html-divs ((preamble "header" "preamble")
18859 (content "main" "content")
18860 (postamble "footer" "postamble"))
18861 :html-head "<meta name='theme-color' content='#111' media='(prefers-color-scheme: dark)'>
18862 <meta name='theme-color' content='#fff' media='(prefers-color-scheme: light)'>
18863 <link rel='stylesheet' href='/syntax-theme-dark.css' media='(prefers-color-scheme: dark)'>
18864 <link rel='stylesheet' href='/syntax-theme-light.css' media='(prefers-color-scheme: light)'>
18865 <link rel='stylesheet' href='/styles.css' type='text/css'>"
18866 :html-preamble "<nav class='site-nav' aria-label='site-nav' role='navigation'>
18867 <ul>
18868 <li><a href='/'>Home</a></li>
18869 <li><a href='/blog/'>Blog</a></li>
18870 <li><a href='/services/'>Services</a></li>
18871 <li><a href='/wiki/'>Wiki</a></li>
18872 </ul></nav>
18873 <h1>%t</h1>
18874 <time datetime='%d'>%d</time>"
18875 :html-postamble "
18876 <p>Last build: %T</p>
18877 <p>Created with %c</p>"
18878 )
18879
18880 ("static"
18881 :base-directory "~/Source/cleberg.net/static/"
18882 :base-extension "css\\|txt\\|jpg\\|gif\\|png"
18883 :recursive t
18884 :publishing-directory "~/Source/cleberg.net/public/"
18885 :publishing-function org-publish-attachment)
18886
18887 ("cleberg.net" :components ("blog" "static"))))
18888 #+end_src
18889
18890** General Thoughts
18891
18892I have enjoyed Doom Emacs (far more than GNU Emacs) and will likely continue to
18893use it as my main editor for the time being. Org-Mode is certainly the largest
18894factor here, as I far prefer it over Markdown due to its inherent features and
18895detailed markup options. However, working with org-mode on iOS has been a pain
18896and I will have to see if there's an easier way to resolve those issues or if
18897going back to separate Markdown, Reminders, and Calendar apps is easier to work
18898with than an all-in-one org solution.
18899
18900[fn:1] Doom's evil-window functionality is a bit different from GNU Emacs, but
18901 you can always switch to the "other" buffer with =C-x o= or =C-x b= to
18902 get a list of buffers to select.
18903
18904[fn:2] Doom's evil-window functionality is a bit different from GNU Emacs, but
18905 you can always switch to the "other" buffer with =C-x o= or =C-x b= to
18906 get a list of buffers to select.
18907#+date: <2024-03-15 Fri 00:00:00>
18908#+title: Self-Hosting Guide: DDNS Updater
18909#+description: Step-by-step configuration guide for deploying a Dynamic DNS updater using Docker Compose, integrating with Cloudflare API, and securing with Nginx reverse proxy.
18910#+slug: self-hosting-ddns-updater
18911
18912#+caption: DDNS Updater Web View
18913#+attr_html: :alt An example of the DDNS Updater app running and showing that cleberg.net's IP has been updated on Cloudflare.
18914[[https://img.cleberg.net/blog/20240315-ddns-updater/ddns.webp]]
18915
18916[[https://github.com/qdm12/ddns-updater][DDNS Updater]] is a program to keep DNS A and/or AAAA records updated for multiple
18917DNS providers.
18918
18919If you've read any of my other posts, you'll notice that I have been searching
18920for and using a few different DDNS updating solutions for years. You'll also
18921notice that I love any projects that offer a Docker Compose solution.
18922
18923Luckily, DDNS Upater fits both of these preferences.
18924
18925** Installation
18926
18927To get started, always make sure to review the project's [[https://github.com/qdm12/ddns-updater/blob/master/README.md][README]]. I'll be
18928documenting my steps below, but they may have changed by the time you read this.
18929
18930The first step is to set up the directories and files required for the project.
18931
18932#+begin_src sh
18933mkdir ~/ddns-updater
18934mkdir ~/ddns-updater/data
18935touch ~/ddns-updater/data/config.json
18936#+end_src
18937
18938*** Configuration
18939
18940The main configuration you need to update is the =data/config.json= file. There
18941is a large list of supported providers in the README, but I'm going to use
18942Cloudflare in this example.
18943
18944#+begin_src sh
18945nano ~/ddns-updater/data/config.json
18946#+end_src
18947
18948When setting up the configuration for Cloudflare, you'll need the following:
18949
18950- Required Parameters
18951 - ="zone_identifier"= is the Zone ID of your site from the domain overview
18952 page
18953 - ="host"= is your host and can be ="@"=, a subdomain or the wildcard ="*"=.
18954 See [[https://github.com/qdm12/ddns-updater/issues/243#issuecomment-928313949][this issue comment for context]].
18955 - ="ttl"= integer value for record TTL in seconds (specify 1 for automatic)
18956 - One of the following ([[https://developers.cloudflare.com/fundamentals/api/get-started/][how to find API keys]]):
18957 - Email ="email"= and Global API Key ="key"=
18958 - User service key ="user_service_key"=
18959 - API Token ="token"=, configured with DNS edit permissions for your DNS
18960 name's zone
18961- Optional Parameters
18962 - ="proxied"= can be set to =true= to use the proxy services of Cloudflare
18963 - ="ip_version"= can be =ipv4= (A records), or =ipv6= (AAAA records) or =ipv4
18964 or ipv6= (update one of the two, depending on the public ip found). It
18965 defaults to =ipv4 or ipv6=.
18966 - ="ipv6_suffix"= is the IPv6 interface identifier suffix to use. It can be
18967 for example =0:0:0:0:72ad:8fbb:a54e:bedd/64=. If left empty, it defaults to
18968 no suffix and the raw public IPv6 address obtained is used in the record
18969 updating.
18970
18971#+begin_src conf
18972{
18973 "settings": [
18974 {
18975 "provider": "cloudflare",
18976 "zone_identifier": "some id",
18977 "domain": "domain.com",
18978 "host": "@",
18979 "ttl": 1,
18980 "proxied": true,
18981 "token": "yourtoken",
18982 "ip_version": "ipv4",
18983 "ipv6_suffix": ""
18984 }
18985 ]
18986}
18987#+end_src
18988
18989Once you have configured the provider of your choice, correct the file and
18990directory permissions and ownership.
18991
18992#+begin_src sh
18993cd ~/ddns_updater
18994# Owned by user ID of Docker container (1000)
18995chown -R 1000 data
18996# all access (for creating json database file data/updates.json)
18997chmod 700 data
18998# read access only
18999chmod 400 data/config.json
19000#+end_src
19001
19002*** Docker Compose
19003
19004After creating the project structure, let's create the =docker-compose.yml=
19005file.
19006
19007#+begin_src sh
19008nano ~/ddns_-pdater/docker-compose.yml
19009#+end_src
19010
19011#+begin_src config
19012version: "3.7"
19013services:
19014 ddns-updater:
19015 image: qmcgaw/ddns-updater
19016 container_name: ddns-updater
19017 network_mode: bridge
19018 ports:
19019 - 8097:8000/tcp # Change the 8097 value to whichever port you want to use
19020 volumes:
19021 - ./data:/updater/data
19022 environment:
19023 - CONFIG=
19024 - PERIOD=5m
19025 - UPDATE_COOLDOWN_PERIOD=5m
19026 - PUBLICIP_FETCHERS=all
19027 - PUBLICIP_HTTP_PROVIDERS=all
19028 - PUBLICIPV4_HTTP_PROVIDERS=all
19029 - PUBLICIPV6_HTTP_PROVIDERS=all
19030 - PUBLICIP_DNS_PROVIDERS=all
19031 - PUBLICIP_DNS_TIMEOUT=3s
19032 - HTTP_TIMEOUT=10s
19033
19034 # Web UI
19035 - LISTENING_ADDRESS=:8000
19036 - ROOT_URL=/
19037
19038 # Backup
19039 - BACKUP_PERIOD=0 # 0 to disable
19040 - BACKUP_DIRECTORY=/updater/data
19041
19042 # Other
19043 - LOG_LEVEL=info
19044 - LOG_CALLER=hidden
19045 - SHOUTRRR_ADDRESSES=
19046 restart: always
19047#+end_src
19048
19049After configuring your preferences in the =docker-compose.yml=, launch the
19050container.
19051
19052#+begin_src sh
19053cd ~/ddns-updater
19054sudo docker-compose up -d
19055#+end_src
19056
19057If you've launched this on your local machine, you can launch =localhost:8097=
19058in your browser to see the results.
19059
19060*** Nginx Reverse Proxy
19061
19062If you launched this service on a server, other machine, or just want to access
19063it remotely via a domain name, you can use Nginx as a reverse proxy to expose
19064the service publicly.
19065
19066Start by creating the Nginx configuration file.
19067
19068#+begin_src sh
19069sudo nano /etc/nginx/sites-available/ddns
19070#+end_src
19071
19072Here's a basic example that should work properly.
19073
19074#+begin_src conf
19075server {
19076 # If using 443, remember to include your ssl_certificate
19077 # and ssl_certificate_key
19078 listen [::]:80;
19079 listen 80;
19080 server_name ddns.example.com;
19081
19082 location / {
19083 set $upstream_ao http://127.0.0.1:9380;
19084 proxy_pass $upstream_ao;
19085
19086 # May need some additional proxy_* parameters,
19087 # see the full example below if necessary
19088 }
19089}
19090#+end_src
19091
19092Here's a full example that uses my Authelia authentication service to require
19093authentication before someone can access the web page.
19094
19095#+begin_src conf
19096server {
19097 if ($host ~ ^[^.]+\.example\.com$) {
19098 return 301 https://$host$request_uri;
19099 }
19100
19101 listen [::]:80;
19102 listen 80;
19103 server_name ddns.example.com;
19104 return 404;
19105}
19106
19107server {
19108 listen [::]:443 ssl http2;
19109 listen 443 ssl http2;
19110 server_name ddns.example.com;
19111 access_log /var/log/nginx/ddns.access.log;
19112 error_log /var/log/nginx/ddns.error.log;
19113
19114 add_header X-Content-Type-Options "nosniff";
19115 add_header X-XSS-Protection "1; mode=block";
19116 add_header X-Frame-Options "DENY";
19117 add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
19118 add_header Referrer-Policy "no-referrer";
19119
19120 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
19121 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
19122 include /etc/letsencrypt/options-ssl-nginx.conf;
19123 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
19124
19125 location /authelia {
19126 internal;
19127 set $upstream_authelia http://127.0.0.1:9091/api/verify; #change the IP and Port to match the IP and Port of your Authelia container
19128 proxy_pass_request_body off;
19129 proxy_pass $upstream_authelia;
19130 proxy_set_header Content-Length "";
19131
19132 # Timeout if the real server is dead
19133 proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
19134 client_body_buffer_size 128k;
19135 proxy_set_header Host $host;
19136 proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
19137 proxy_set_header X-Real-IP $remote_addr;
19138 proxy_set_header X-Forwarded-For $remote_addr;
19139 proxy_set_header X-Forwarded-Proto $scheme;
19140 proxy_set_header X-Forwarded-Host $http_host;
19141 proxy_set_header X-Forwarded-Uri $request_uri;
19142 proxy_set_header X-Forwarded-Ssl on;
19143 proxy_redirect http:// $scheme://;
19144 proxy_http_version 1.1;
19145 proxy_set_header Connection "";
19146 proxy_cache_bypass $cookie_session;
19147 proxy_no_cache $cookie_session;
19148 proxy_buffers 4 32k;
19149
19150 send_timeout 5m;
19151 proxy_read_timeout 240;
19152 proxy_send_timeout 240;
19153 proxy_connect_timeout 240;
19154 }
19155
19156 location / {
19157 set $upstream_ddns http://127.0.0.1:8097; #change ddns to match your container name: $upstream_some-container-name or $upstream_somecontainername
19158 proxy_pass $upstream_ddns; #change ddns to match your container name: $upstream_some-container-name or $upstream_somecontainername
19159
19160 auth_request /authelia;
19161 auth_request_set $target_url https://$http_host$request_uri;
19162 auth_request_set $user $upstream_http_remote_user;
19163 auth_request_set $email $upstream_http_remote_email;
19164 auth_request_set $groups $upstream_http_remote_groups;
19165 proxy_set_header Remote-User $user;
19166 proxy_set_header Remote-Email $email;
19167 proxy_set_header Remote-Groups $groups;
19168
19169 error_page 401 =302 https://auth.example.com/?rd=$target_url; #change this to match your authentication domain/subdomain
19170
19171 client_body_buffer_size 128k;
19172
19173 proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
19174
19175 send_timeout 5m;
19176 proxy_read_timeout 360;
19177 proxy_send_timeout 360;
19178 proxy_connect_timeout 360;
19179
19180 proxy_set_header Host $host;
19181 proxy_set_header Upgrade $http_upgrade;
19182 proxy_set_header Connection upgrade;
19183 proxy_set_header Accept-Encoding gzip;
19184 proxy_set_header X-Real-IP $remote_addr;
19185 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
19186 proxy_set_header X-Forwarded-Proto $scheme;
19187 proxy_set_header X-Forwarded-Host $http_host;
19188 proxy_set_header X-Forwarded-Uri $request_uri;
19189 proxy_set_header X-Forwarded-Ssl on;
19190 proxy_redirect http:// $scheme://;
19191 proxy_http_version 1.1;
19192 proxy_set_header Connection "";
19193 proxy_cache_bypass $cookie_session;
19194 proxy_no_cache $cookie_session;
19195 proxy_buffers 64 256k;
19196
19197 # set_real_ip_from 192.168.1.0/16; #make sure this matches your network setup
19198 # real_ip_header CF-Connecting-IP;
19199 # real_ip_recursive on;
19200 }
19201}
19202#+end_src
19203
19204When complete, simply link the file and restart the web server.
19205
19206#+begin_src sh
19207sudo ln -s /etc/nginx/sites-available/ddns /etc/nginx/sites-enabled/ddns
19208sudo systemctl restart nginx.service
19209#+end_src
19210
19211Your ddns-updater service will now be available via =ddns.example.com=!
19212#+date: <2024-03-29 Fri 00:00:00>
19213#+title: Blogging with Emacs Org-Mode and Weblorg
19214#+description: Instructional content for preparing and publishing blogs using Emacs Org-Mode, including setup of static site generators and content management workflows.
19215#+slug: org-blog
19216
19217First and foremost, apologies to those who subscribe via RSS as I know that my
19218feed duplicated itself when I moved this blog over to org-mode last night.
19219
19220This post focuses specifically on the configuration and tools I use to blog from
19221Emacs with Org-Mode and does not focus on Emacs or Org-Mode themselves. Refer to
19222the post I wrote about [[https://cleberg.net/blog/doom-emacs-org-mode.html][Doom Emacs & Org-Mode]] for more information about my base
19223Emacs configuration.
19224
19225* Weblorg
19226
19227The first step in blogging with Org-Mode is to choose a method to convert the
19228source files to HTML and publish them. The Worg site maintains a nice list of
19229[[https://orgmode.org/worg/org-blog-wiki.html][Blogs and Wikis with Org]], but the tools are inevitably different and
19230opinionated, so you'll need to find what works for you.
19231
19232I tried using Jekyll, Hugo, ox-hugo, Nikola, Blorg, org-static-blog, and the
19233native org-publish functions before finally settling on Weblorg. For one reason
19234or another, the other solutions were a drastic step down from my previous
19235workflow that used [[https://www.getzola.org/][Zola]] with Markdown content.
19236
19237[[https://github.com/emacs-love/weblorg][Weblorg]] is a static site generator for [[https://orgmode.org/][org-mode]], built for use within [[https://www.gnu.org/software/emacs/][Emacs]].
19238Since it's written in Emacs Lisp, there's no need to install other languages or
19239frameworks to get started. More than that, you can write in any editor you
19240please and simply invoke the Emacs build process with the =--script= parameter
19241instead of requiring you to blog inside Emacs.
19242
19243** Installation
19244
19245The [[https://emacs.love/weblorg/doc/index.html][Getting Started]] page details broad installation requirements. I am using
19246Doom Emacs on macOS, which requires you to add the package to the
19247=~/.doom.d/packages.el= file and configure the =publish.el= file slightly
19248differently.
19249
19250To start, add the =htmlize= and =weblorg= packages to Doom, sync the changes,
19251and reload.
19252
19253#+begin_src sh
19254nano ~/.doom.d/packages.el
19255#+end_src
19256
19257#+begin_src lisp
19258(package! htmlize)
19259(package! weblorg)
19260#+end_src
19261
19262#+begin_src sh
19263doom sync
19264#+end_src
19265
19266Either re-open Emacs or hit =SPC h r r= to reload the changes.
19267
19268** Configuration
19269
19270Now that I've installed weblorg, I need to configure the project. I'll start by
19271navigating to my site's source code and creating a =publish.el= file.
19272
19273#+begin_src sh
19274cd ~/Source/cleberg.net && nano publish.el
19275#+end_src
19276
19277Since I'm using Doom, Emacs will not automatically load the packages I need
19278later in the build process. To compensate, my =publish.el= file needs to
19279explicitly tell Emacs where Doom stores the =htmlize=, =weblorg=, and
19280=templatel= packages.
19281
19282#+begin_src lisp
19283;; explicity load packages since I'm using Doom Emacs
19284(add-to-list 'load-path "~/.emacs.d/.local/straight/repos/emacs-htmlize")
19285(add-to-list 'load-path "~/.emacs.d/.local/straight/repos/weblorg")
19286(add-to-list 'load-path "~/.emacs.d/.local/straight/repos/templatel")
19287(require 'htmlize)
19288(require 'weblorg)
19289
19290;; defaults to http://localhost:8000
19291;; To build with the custom URL below, call:
19292;;;; ENV=prod emacs --script publish.el
19293(if (string= (getenv "ENV") "prod")
19294 (setq weblorg-default-url "https://cleberg.net"))
19295
19296;; site metadata
19297(weblorg-site
19298 :theme nil
19299 :template-vars '(("site_name" . "cleberg.net")
19300 ("site_owner" . "hello@cleberg.net")
19301 ("site_description" . "Just a blip of ones and zeroes.")))
19302
19303;; route for rendering the index page of the website
19304(weblorg-route
19305 :name "index"
19306 :input-pattern "content/index.org"
19307 :template "index.html"
19308 :output ".build/index.html"
19309 :url "/")
19310
19311;; route for rendering each blog post
19312(weblorg-route
19313 :name "blog"
19314 :input-pattern "content/blog/*.org"
19315 :template "post.html"
19316 :output ".build/blog/{{ slug }}.html"
19317 :url "/blog/{{ slug }}.html")
19318
19319;; route for rendering the index page of the blog
19320(weblorg-route
19321 :name "blog-index"
19322 :input-pattern "content/blog/*.org"
19323 :input-aggregate #'weblorg-input-aggregate-all-desc
19324 :template "blog.html"
19325 :output ".build/blog/index.html"
19326 :url "/blog/")
19327
19328;; route for rendering each wiki post
19329(weblorg-route
19330 :name "wiki"
19331 :input-pattern "content/wiki/*.org"
19332 :template "post.html"
19333 :output ".build/wiki/{{ slug }}.html"
19334 :url "/wiki/{{ slug }}.html")
19335
19336;; route for rendering the index page of the wiki
19337(weblorg-route
19338 :name "wiki-index"
19339 :input-pattern "content/wiki/*.org"
19340 :input-aggregate #'weblorg-input-aggregate-all
19341 :template "wiki.html"
19342 :output ".build/wiki/index.html"
19343 :url "/wiki/")
19344
19345;; routes for rendering all other pages
19346(weblorg-route
19347 :name "pages"
19348 :input-pattern "content/*.org"
19349 :template "page.html"
19350 :output ".build/{{ slug }}.html"
19351 :url "/{{ slug }}.html")
19352
19353(weblorg-route
19354 :name "salary"
19355 :input-pattern "content/salary/*.org"
19356 :template "page.html"
19357 :output ".build/salary/{{ slug }}.html"
19358 :url "/salary/{{ slug }}.html")
19359
19360(weblorg-route
19361 :name "services"
19362 :input-pattern "content/services/*.org"
19363 :template "page.html"
19364 :output ".build/services/{{ slug }}.html"
19365 :url "/services/{{ slug }}.html")
19366
19367;; RSS Feed
19368(weblorg-route
19369 :name "rss"
19370 :input-pattern "content/blog/*.org"
19371 :input-aggregate #'weblorg-input-aggregate-all-desc
19372 :template "feed.xml"
19373 :output ".build/feed.xml"
19374 :url "/feed.xml")
19375
19376;; route for static assets that also copies files to .build directory
19377(weblorg-copy-static
19378 :output ".build/{{ file }}"
19379 :url "/{{ file }}")
19380
19381;; fire the engine and export all the files declared in the routes above
19382(weblorg-export)
19383#+end_src
19384
19385* Project
19386
19387** Structure
19388
19389The project structure for weblorg is highly customizable and the main
19390restriction is that the =publish.el= file must point to the correct paths.
19391
19392For my blog, I prefer to keep the blog content out of the top-level directory.
19393This results in the following structure (shortened for brevity):
19394
19395#+begin_src txt
19396.build/
19397content/
19398 blog/
19399 example-blog-post.org
19400 index.org
19401 wiki/
19402 example-wiki-post.org
19403 index.org
19404 index.org
19405 other-example-page.org
19406theme/
19407 static/
19408 styles.css
19409 robots.txt
19410 templates/
19411 base.html
19412 blog.html
19413 index.html
19414 page.html
19415 post.html
19416 wiki.html
19417build.sh
19418publish.el
19419#+end_src
19420
19421This is simply my preferred structure and you can alter it to fit your needs.
19422The key here really is that you can customize at will, as long as the
19423=publish.el= file matches.
19424
19425** Build & Deploy
19426
19427Once you're content with the status of the project, you're ready to build and
19428deploy the blog.
19429
19430My process utilizes a =build.sh= script that combines the steps I take every
19431time.
19432
19433#+begin_src sh
19434touch build.sh && chmod +x build.sh && nano build.sh
19435#+end_src
19436
19437Within this script, I do the following:
19438
194391. Remove any files within the =.build= directory that I use to store published
19440 files.
194412. Set the environment variable to =prod= to ensure the =base_url= matches my
19442 configuration in =publish.el=.
194433. Build the site with Emacs & =publish.el=.
194444. Use =scp= to copy files to my site's public directory on my server.
19445
19446#+begin_src sh
19447rm -rf .build/* && \
19448ENV=prod emacs--script publish.el && \
19449scp -r .build/* ubuntu:/var/www/cleberg.net/
19450#+end_src
19451
19452*** Time to Build
19453
19454My only current complaints are:
19455
194561. Errors messages are not helpful. It takes work to determine what the error is
19457 and where it's coming from. I generally have to sit and watch the build
19458 process to see the file that weblorg pubslishes right before the error
19459 occurred.
194602. The build process re-builds every single file on each run, which takes a long
19461 time for a blog of my size. See below for the last time I measured.
19462
19463#+begin_src sh
19464> time ./build.sh
19465
19466./build.sh 35.46s user 0.59s system 85% cpu 41.965 total
19467#+end_src
19468
19469Overall, I have thoroughly enjoyed using weblog and will continue to use it
19470going forward until I find something better.
19471#+date: <2024-04-06 Sat 00:00:00>
19472#+title: Converting OneNote Exports to Markdown and Org-Mode
19473#+description: Detailed conversion instructions to transform OneNote document files into Markdown or Org-Mode formats using Pandoc command-line utility on Windows systems.
19474#+slug: convert-onenote-to-markdown
19475
19476If you're looking to convert your OneNote content to another format, such as
19477Markdown or Org-Mode, you're in luck. I use a solution that doesn't require
19478other programs, such as Evernote or Notion. Personally, I used this solution on
19479a managed corporate laptop that doesn't allow installation of other programs
19480like these.
19481
19482This solution uses OneNote and Pandoc on Windows 10.
19483
19484* Export OneNote Content to Word
19485
19486To start, export any pages or tabs from OneNote to the Word format (=.docx=):
19487
194881. Open OneNote desktop.
194892. Select =File= and then =Export=.
194903. Select the scope of content to export, such as =Tab= or =Page=.
194914. Name and save the file in an easy to remember location. I recommend your
19492 Downloads or Desktop folder.
19493
19494See below for a visual walkthrough of the export process.
19495
19496* Download Pandoc
19497
19498Start by downloading Pandoc from their [[https://github.com/jgm/pandoc/releases][GitHub releases]] page. I cannot install
19499=.msi= files on my corporate laptop, so I downloaded the
19500=pandoc-3.1.12.3-windows-x86_64.zip= file, which contains a simple =.exe= file
19501that you do not need to install - you will simply run it from the command line
19502below.
19503
19504Once downloaded, unzip the archive and move the =pandoc.exe= file to the same
19505folder where your Word documents were saved above. If you prefer, you can move
19506this file to an easier location, such as =C:\Users\youruser\Downloads=.
19507
19508* Convert Word to Markdown
19509
19510In this example, I will be converting the Word documents to Markdown, but Pandoc
19511supports [[https://github.com/jgm/pandoc?tab=readme-ov-file#the-universal-markup-converter][a ton of different formats for conversion]]. Choose the format you prefer
19512and then modify the following commands as needed.
19513
19514To perform the conversion, open the Command Prompt. If you can't find it, open
19515the start menu and search for it.
19516
19517Within the command prompt, navigate to the directory where you stored the
19518=pandoc.exe= file and the Word documents.
19519
19520#+begin_src ps1
19521cd "C:\Users\yourusername\Downloads"
19522#+end_src
19523
19524You can verify that you're in the correct directory with the =dir=
19525command.
19526
19527#+begin_src ps1
19528dir
19529#+end_src
19530
19531Once you have verified that you have the command prompt open in the correct
19532directory with the =pandoc.exe= and the Word documents, you can run the
19533following loop to convert all Word documents to Markdown.
19534
19535#+begin_src ps1
19536for %f in (*.docx) do (pandoc.exe --extract-media=. --wrap=preserve "%f" -o "%f.md")
19537#+end_src
19538
19539This loop will perform the following actions:
19540
195411. Find all documents matching the pattern =*.docx=, which means all Word
19542 documents ending with that file extension.
195432. Iterate through all files found in step 1.
195443. For each file, perform the pandoc command.
195454. Within the pandoc command, =--extract-media= saves all media found in the
19546 files to the current folder, with pandoc automatically creating a =media=
19547 subfolder to hold all images.
195485. Within the pandoc command, =--wrap=preserve= will attempt to prseerve the
19549 wrapping from the source document.
195506. Within the pandoc command, the final step is to specify the output path with
19551 =-o=. This option adds the =.md= file extension to recognize the output files
19552 as Markdown files.
19553
19554If you want to export to another format, simply specify the =-f==/=--from== and
19555=-t==/=--to== options.
19556
19557For example, you can convert the Word document to org-mode. You can also convert
19558to one format and subsequently convert to other formats as needed.
19559
19560#+begin_src ps1
19561pandoc.exe -f docx -t org file.docx
19562#+end_src
19563#+date: <2024-04-08 Mon 00:00:00>
19564#+title: Local Web Dev Server Setup with Docker & Nginx
19565#+description: Instructions for setting up a local web development environment employing Docker Desktop and Nginx web server to facilitate development and testing workflows.
19566#+slug: docker-local-web-server
19567
19568When developing websites locally, I often use a simple Python web server to
19569observe the changes.
19570
19571#+begin_src sh
19572python3 -m http.server
19573#+end_src
19574
19575However, this approach has its limitations. For example, this approach does not
19576enable logging or access controls. You also need to customize =SimpleHTTPServer=
19577if you have advanced needs from your web server.
19578
19579So, I went to find an alternative that is almost as easy and far more extensible
19580and found Docker Desktop to be a suitable replacement.
19581
19582* Docker Desktop
19583
19584** Installation
19585
19586[[https://www.docker.com/products/docker-desktop/][Docker Desktop]] is a desktop GUI for the phenomenal Docker container software.
19587This allows you to manage containers, images, volumes, environments, and
19588extensions via an easy-to-use GUI.
19589
19590To install, open the link above and click the =Download= button for your
19591platform. I'm going through this process on an M2 Macbook, so I downloaded the
19592Mac - Apple Chip version.
19593
19594Open the installer and follow the installation process until the application
19595finishes the installation process.
19596
19597#+caption: Docker Desktop on macOS
19598#+attr_html: :alt The Containers home page of the Docker Desktop app on macOS.
19599[[https://img.cleberg.net/blog/20240408-docker-local-web-server/docker-desktop.webp]]
19600
19601** Creating an Nginx Container
19602
19603I prefer to use the command line to create containers, so the following commands
19604will be input via the terminal.
19605
19606The following command will create a container, using the =nginx= image:
19607
196081. =-d=: Run this container as a daemon (detach)
196092. =-p=: Allocate a port in the format =<external>:<internal>=
196103. =-i=: Keep STDIN open even if not attached
196114. =-t=: Allocate a pseudo-TTY
196125. =-p=: Allocate a port in the format =<external>:<internal>=
196136. =--rm=: Remove the container once it's done running
19614
19615#+begin_src sh
19616docker run -it --rm -d -p 8000:80 --name web nginx
19617#+end_src
19618
19619You can navigate to [[http://localhost:8000]] to see the resulting page.
19620
19621#+caption: Default Nginx Container
19622#+attr_html: :alt The generic "Welcome to nginx!" page viewable at localhost:8000.
19623[[https://img.cleberg.net/blog/20240408-docker-local-web-server/default-container.webp]]
19624
19625** Customizing the Nginx Container
19626
19627Now that I have a container running the Nginx web server, I need to link some
19628volumes so that I can modify the site configuration and provide the web files to
19629serve.
19630
19631Let's start with the new command, which adds two volumes:
19632
196331. =<your_content>:/usr/share/nginx/html=: This is the directory where you will
19634 provide the web pages for the server to serve.
196352. =<your_config>:/etc/nginx/conf.d/default.conf=: This is the Nginx
19636 configuration file for your site.
19637
19638To see the updates, you can delete the previous container in the GUI or run
19639=docker stop web= to stop the container. Once stopped, you can run the new
19640=docker run= command below.
19641
19642#+begin_src sh
19643docker run -it -d -p 8000:80 --name web -v ~/Source/cleberg.net/.build:/usr/share/nginx/html -v ~/Source/cleberg.net/nginx-config.conf:/etc/nginx/conf.d/default.conf nginx
19644#+end_src
19645
19646Here's an example of my development configuration file.
19647
19648#+begin_src conf
19649# nginx-config.conf
19650server {
19651 server_name cleberg.net www.cleberg.net;
19652
19653 root /usr/share/nginx/html;
19654 index index.html;
19655 autoindex on;
19656
19657 access_log /var/log/nginx/cleberg.net.access.log;
19658 error_log /var/log/nginx/cleberg.net.error.log;
19659
19660 location / {
19661 try_files $uri $uri/ =404;
19662 }
19663
19664 listen [::]:80;
19665 listen 80;
19666}
19667#+end_src
19668
19669#+caption: Deploying My Website via Docker
19670#+attr_html: :alt A view of cleberg.net on the localhost, as the docker container is pointing to the cleberg.net repository directory.
19671[[https://img.cleberg.net/blog/20240408-docker-local-web-server/custom-container.webp]]
19672
19673* Customizing Deployment Actions
19674
19675I am currently blogging with [[https://emacs.love/weblorg/][weblorg]], which uses a custom =publish.el= file to
19676build the static site. Within this file, I have configured my deployment process
19677to check for the =ENV= variable in thesh and if it's set to =prod=, the script
19678will set the base URLs to =https://cleberg.net=. If not, it sets the base URLs
19679to =localhost:8000= (which matches the port used in the container above).
19680
19681Therefore, I have modified my =build.sh= script to build with =localhost= URLs
19682if =ENV= is not set to =prod=. It also prevents the build process from sending
19683the built files to the production web server.
19684
19685#+begin_src sh
19686#!/bin/bash
19687
19688if [ "$ENV" == "prod" ]; then
19689 echo "Environment = Production" && \
19690 rm -rf .build/* && \
19691 emacs --script publish.el && \
19692 scp -r .build/* ubuntu:/var/www/cleberg.net/
19693else
19694 echo "Environment = Development" && \
19695 rm -rf .build/* && \
19696 emacs --script publish.el
19697fi
19698#+end_src
19699
19700You can modify the container in numerous ways and this approach allows you to
19701create complex scenarios for your web development purposes. I highly recommend
19702switching over to a container-based approach for simple, local web development.
19703#+date: <2024-04-18 Thu 00:00:00>
19704#+title: Configuring Mu4e with Doom Emacs on macOS
19705#+description: Stepwise procedure for installing, configuring, and using Mu4e email client within Doom Emacs on macOS for organized email management.
19706#+slug: mu4e
19707
19708This post was heavily inspired by [[https://macowners.club/posts/email-emacs-mu4e-macos/][Email setup in Emacs with Mu4e on macOS]], but
19709with my own tweaks for a single-account configuration and some Doom-specific
19710configurations.
19711
19712* Overview
19713
19714[[https://github.com/emacsmirror/mu4e][Mu4e]] is an Emacs-based email client based on [[https://www.djcbsoftware.nl/code/mu/][mu]], an indexer that stores email in
19715the Maildir format.
19716
19717This blog post covers the installation of Mu4e in Doom Emacs on macOS. This
19718guide should be very similar to GNU Emacs and Linux-based systems, with a few
19719tweaks required in the various configuration files.
19720
19721* Installation
19722
19723** Prerequisites
19724
19725*** Create the Folder Structure
19726
19727Start by creating the base folder structure:
19728
19729#+begin_src sh
19730mkdir ~/.maildir
19731mkdir ~/.maildir/example # use whatever name you want to call your email account
19732mkdir ~/.maildir/certificates # used to store system root certificates
19733#+end_src
19734
19735*** Store Email Account Passwords in macOS Keychain
19736
19737Next, I will be using the macOS Keychain to store my email account passwords
19738using the command below.
19739
19740#+begin_src sh
19741security add-generic-password -s mu4e-example -a you@example.com -w
19742#+end_src
19743
19744This will prompt you to input your password twice to confirm. Keep the
19745=mu4e-example= name in mind, as you will need to reference it later in the IMAP
19746and SMTP configuration files.
19747
19748*** Store Root Certificates
19749
19750In order to use IMAP and SMTP, we need to provide certificates to the local
19751services. We will use the macOS defaults for this.
19752
197531. Open =Keychain Access.app=.
197542. Select =System Roots= in the sidebar.
197553. Select all items with =CMD + a=.
197564. Export selected items with =SHIFT + CMD + a= to the file
19757 =~/.maildir/certificates/root-certificates.pem=.
19758
19759*** Install Dependencies
19760
19761Install =mbsync= (via =isync=) to fetch emails via IMAP, =mu= to index emails,
19762and =msmtp= to send emails via SMTP.
19763
19764#+begin_src sh
19765brew install mu isync msmtp
19766#+end_src
19767
19768** Installing Mu4e
19769
19770Within Doom Emacs, we can install Mu4e by enabling the package.
19771
19772#+begin_src sh
19773nano ~/.doom.d/init.el
19774#+end_src
19775
19776In this file, uncomment the =mu4e= line within the =:email= section. You can
19777also enable the =+org= and =+gmail= options if you prefer.
19778
19779#+begin_src lisp
19780(doom! :input
19781 ...
19782 :email
19783 mu4e
19784 ;;(mu4e +org +gmail)
19785 ...
19786 (default +bindings +smartparens))
19787#+end_src
19788
19789* Configuration
19790
19791As an overall suggestion, I create the following configuration files in the
19792=~/.maildir= directory and using symlinks to their proper locations so that I
19793can backup and restore these files easily.
19794
19795#+begin_src sh
19796touch ~/.maildir/.mbsyncrc && \
19797touch ~/.maildir/.msmtprc && \
19798ln -s /Users/username/.maildir/.mbsyncrc /Users/username/.mbsyncrc && \
19799ln -s /Users/username/.maildir/.msmtprc /Users/username/.msmtprc
19800#+end_src
19801
19802You can also create these files in your home directory and skip the symlinking
19803process above.
19804
19805** IMAP
19806
19807Next, let's configure =mbsync= in the file created above. Paste the following
19808information and customize it to match your mail provider's information.
19809
19810#+begin_src sh
19811nano ~/.maildir/.mbsyncrc
19812#+end_src
19813
19814#+begin_src conf
19815IMAPAccount example
19816Host imap.example.com
19817User dummy@example.com
19818PassCmd "security find-generic-password -s mu4e-example -a dummy@example.com -w"
19819Port 993
19820SSLType IMAPS
19821AuthMechs Login
19822CertificateFile ~/.maildir/certificates/root-certificates.pem
19823
19824IMAPStore example-remote
19825Account example
19826
19827MaildirStore example-local
19828SubFolders Verbatim
19829Path ~/.maildir/example/
19830Inbox ~/.maildir/example/INBOX
19831
19832Channel example
19833Far :example-remote:
19834Near :example-local:
19835Patterns *
19836Create Near
19837Sync All
19838Expunge Both
19839SyncState *
19840#+end_src
19841
19842** SMTP
19843
19844Next, let's configured =msmtprc= in the file created above. Paste the following
19845information and customize it to match your mail provider's information.
19846
19847#+begin_src sh
19848nano ~/.maildir/.msmtprc
19849#+end_src
19850
19851#+begin_src conf
19852# Set default values for all the accounts.
19853defaults
19854logfile ~/.maildir/msmtp.log
19855tls_trust_file ~/.maildir/certificates/root-certificates.pem
19856
19857# ======================================================================
19858
19859account startmail
19860auth on
19861host smtp.startmail.com
19862port 465
19863protocol smtp
19864from hello@cleberg.net
19865user hello@cleberg.net
19866passwordeval security find-generic-password -s startmail -a hello@cleberg.net -w
19867tls on
19868tls_starttls off
19869
19870# ======================================================================
19871
19872account default : startmail
19873#+end_src
19874
19875** Doom Emacs
19876
19877Finally, we need to configure Doom Emacs to use the proper packages and set some
19878variables and functions.
19879
19880#+begin_src sh
19881nano ~/.doom.d/config.el
19882#+end_src
19883
19884#+begin_src lisp
19885;; load packages and programs
19886(use-package mu4e
19887 :load-path "/Users/username/.emacs.d/modules/email/mu4e/")
19888(require 'smtpmail)
19889(setq mu4e-mu-binary (executable-find "mu"))
19890
19891;; set base directory
19892(setq mu4e-maildir "~/.maildir")
19893
19894;; sync imap servers
19895(setq mu4e-get-mail-command (concat (executable-find "mbsync") " -a"))
19896
19897;; how often to sync in seconds
19898(setq mu4e-update-interval 300)
19899
19900;; save attachments to defined directory
19901(setq mu4e-attachment-dir "~/Downloads")
19902
19903;; rename files when moving - needed for mbsync:
19904(setq mu4e-change-filenames-when-moving t)
19905
19906;; list of your email adresses:
19907(setq mu4e-user-mail-address-list '("you@example.com"))
19908
19909;; check your ~/.maildir to see naming of subdirectories
19910(setq mu4e-maildir-shortcuts
19911 '(("/example/INBOX" . ?e)
19912 ("/example/Sent" . ?E)))
19913
19914(setq mu4e-contexts
19915 `(,(make-mu4e-context
19916 :name "example"
19917 :enter-func
19918 (lambda () (mu4e-message "Enter you@example.com context"))
19919 :leave-func
19920 (lambda () (mu4e-message "Leave you@example.com context"))
19921 :match-func
19922 (lambda (msg)
19923 (when msg
19924 (mu4e-message-contact-field-matches msg
19925 :to "you@example.com")))
19926 :vars '((user-mail-address . "you@example.com")
19927 (user-full-name . "Christian Cleberg")
19928 ;; check your ~/.maildir to see how the subdirectories are called
19929 ;; e.g `ls ~/.maildir/example'
19930 (mu4e-drafts-folder . "/example/Drafts")
19931 (mu4e-refile-folder . "/example/Archive")
19932 (mu4e-sent-folder . "/example/Sent")
19933 (mu4e-trash-folder . "/example/Trash")))))
19934
19935(setq mu4e-context-policy 'pick-first) ;; start with the first (default) context;
19936(setq mu4e-compose-context-policy 'ask) ;; ask for context if no context matches;
19937
19938;; gpg encryptiom & decryption:
19939;; this can be left alone
19940(require 'epa-file)
19941(epa-file-enable)
19942(setq epa-pinentry-mode 'loopback)
19943(auth-source-forget-all-cached)
19944
19945;; don't keep message compose buffers around after sending:
19946(setq message-kill-buffer-on-exit t)
19947
19948;; send function:
19949(setq send-mail-function 'sendmail-send-it
19950 message-send-mail-function 'sendmail-send-it)
19951
19952;; send program:
19953(setq sendmail-program (executable-find "msmtp"))
19954
19955;; select the right sender email from the context.
19956(setq message-sendmail-envelope-from 'header)
19957
19958;; mu4e cc & bcc
19959(add-hook 'mu4e-compose-mode-hook
19960 (defun timu/add-cc-and-bcc ()
19961 "My Function to automatically add Cc & Bcc: headers.
19962 This is in the mu4e compose mode."
19963 (save-excursion (message-add-header "Cc:\n"))
19964 (save-excursion (message-add-header "Bcc:\n"))))
19965
19966;; mu4e address completion
19967(add-hook 'mu4e-compose-mode-hook 'company-mode)
19968#+end_src
19969
19970Be sure to sync Doom to update the current configurations.
19971
19972#+begin_src sh
19973doom sync
19974#+end_src
19975
19976If you have Doom open, execute =SPC h r r= to reload the new configurations.
19977
19978* Initial Sync
19979
19980Once you have configured all of the relevant files, you can perform an initial
19981sync. Note that you can perform syncing within Mu4e itself after this.
19982
19983#+begin_src sh
19984mbsync -aV
19985#+end_src
19986
19987Once you sync the data, you can index the emails.
19988
19989#+begin_src sh
19990mu init -m ~/.maildir --my-address you@example.com && \
19991mu index
19992#+end_src
19993
19994The emails will now to be ready to use!
19995
19996* Screenshots
19997
19998You can now launch Doom and open Mu4e with =SPC o m=. You can also explore the
19999Mu4e options with =SPC : mu4e=.
20000
20001The home page shows various options and metadata about the account you've
20002opened.
20003
20004#+caption: Mu4e Home Page
20005#+attr_html: :alt The mu4e homepage within iTerm2, showing the basics, bookmarks, maildirs, and misc sections.
20006[[https://img.cleberg.net/blog/20240418-mu4e/mu4e.webp]]
20007#+date: <2024-05-03 Fri 00:00:00>
20008#+title: OrbStack on macOS: Running a Full Ubuntu Linux Environment
20009#+description: Technical guide outlining the installation and operation of OrbStack to run Ubuntu Linux containers natively on macOS systems with performance considerations.
20010#+slug: ubuntu-on-macos
20011
20012Being a macOS user who previously used Linux for many years, I often find myself
20013searching for alternatives to the Linux-native tools and methods that I had
20014become used to over the years.
20015
20016Luckily, there's an option to simply use Linux directly on your macOS device
20017without having to boot the whole device into Linux or having to SSH into another
20018Linux machine.
20019
20020In this post, I use [[https://orbstack.dev/][OrbStack]] to create an Ubuntu Linux machine to take advantage
20021of the Linux system.
20022
20023** Installing OrbStack
20024
20025OrbStack has a [[https://orbstack.dev/download][download page]] if you prefer to use =*.dmg= files to install
20026applications.
20027
20028Personally, I prefer to install everything through Homebrew. For OrbStack, run
20029the following to install the application via Homebrew:
20030
20031#+begin_src sh
20032brew install orbstack
20033#+end_src
20034
20035** Creating a Linux Machine
20036
20037Once installed, you can launch OrbStack and create a Linux machine in a matter
20038of seconds. The creation dialog defaults to Ubuntu.
20039
20040#+caption: OrbStack's Linux Machine Creation Page
20041#+attr_html: :alt A popup within OrbStack to create a new machine with a custom name, distribution, version, and CPU type.
20042[[https://img.cleberg.net/blog/20240503-ubuntu-on-macos/create.webp]]
20043
20044** Launching the Machine
20045
20046Once created, the dialog will close and you will be presented with your new list
20047of machines. Simply double-click on a machine to run it.
20048
20049When you run the machine for the first time, it may warn you that a program is
20050attempting to run a shell script. You can choose to manually inspect this shell
20051script via the provided path if you wish. When you're comfortable with the
20052script, you can toggle the =Suppress this message permanently= button and click
20053OK to run the machine.
20054
20055#+begin_quote
20056*Quick Tip*!
20057
20058You will always be able to launch the OrbStack application and control the
20059machine via the Machines page, but you can also use the OrbStack icon in the
20060macOS menu bar to quickly control the machine without launching OrbStack itself.
20061#+end_quote
20062
20063** Working in Linux
20064
20065Once successful, OrbStack will launch your default terminal and log you into the
20066Linux machine you created above.
20067
20068In the example below, we can see that the machine is using =Linux ubuntu 6.9.6=
20069and operates similar to a fresh Ubuntu install on bare metal.
20070
20071#+caption: Ubuntu CLI
20072#+attr_html: :alt An example of the Ubuntu CLI, running the "uname -a" command.
20073[[https://img.cleberg.net/blog/20240503-ubuntu-on-macos/cli.webp]]
20074
20075You can also click the machine's folder icon in OrbStack to open the machine's
20076file system within macOS's native Files app. Within Files, you can double-click
20077to open any files and they will open in the macOS defaults for those file types
20078(e.g., TextEdit).
20079
20080#+caption: Ubuntu Files
20081#+attr_html: :alt The Ubuntu machine's files available within the macOS Files app.
20082[[https://img.cleberg.net/blog/20240503-ubuntu-on-macos/files.webp]]
20083
20084Here's an example of opening the =test.md= file in the screenshot above, which
20085defaults to opening in Xcode on my system.
20086
20087#+caption: Ubuntu Files
20088#+attr_html: :alt An example of editing the test.md file in Xcode.
20089[[https://img.cleberg.net/blog/20240503-ubuntu-on-macos/xcode.webp]]
20090
20091*** Test Examples
20092
20093As a quick example of the system, I installed =neofetch=, switched to the =zsh=
20094shell, and ran a few common commands.
20095
20096#+caption: Test Examples
20097#+attr_html: :alt An example of the date, pwd, and neofetch commands.
20098[[https://img.cleberg.net/blog/20240503-ubuntu-on-macos/test.webp]]
20099
20100** Thoughts
20101
20102The installation, creation, and launch processes are seamless and provide an
20103easy way to test or use Linux on macOS with little to no hassle.
20104
20105The shell and file integrations are incredibly snappy and I have not
20106experienced any lag so far. Further, being able to open files in the
20107Files app allows for me to use macOS native applications, such as
20108TextEdit for quick editing without having to resort to other programs
20109that may not be as familiar to macOS users.
20110
20111I love this method and will be happily looking for further use cases to
20112apply.
20113#+date: <2024-06-19 Wednesday 08:00:00>
20114#+title: Migrating Ubuntu GPG Keys to trusted.gpg.d
20115#+description: Technical instructions to relocate GPG keys from the deprecated trusted.gpg keyring to the supported trusted.gpg.d directory for system security maintenance.
20116#+slug: deprecated-trusted-gpg-fix
20117
20118** System Warning
20119
20120When running an update on an Ubuntu system, you may have run into a system
20121warning that looks like the example below.
20122
20123#+begin_src txt
20124W: https://dl.yarnpkg.com/debian/dists/stable/InRelease: Key is stored in legacy
20125trusted.gpg keyring (/etc/apt/trusted.gpg), see the DEPRECATION section in
20126apt-key(8) for details.
20127#+end_src
20128
20129While this example references the =yarn= package, the warning message is the
20130same for any repository using the deprecated =trusted.gpg= key ring.
20131
20132The issue arises from managing keys with the =apt-key= command, which utilizes
20133the =/etc/apt/trusted.gpg= file by default. Instead, Ubuntu has moved to
20134managing key rings with individual =.gpg= files in the =/etc/apt/trusted.gpg.d/=
20135directory.
20136
20137To fix this issue, let's check to see which keys are using the =trusted.gpg= key
20138ring and move them into their own dedicated key ring.
20139
20140** Finding All Keys in the Keyring
20141
20142Let's start by simply listing the keys used by the =apt= commands. To do this,
20143run the following command.
20144
20145#+begin_src sh
20146sudo apt-key list
20147#+end_src
20148
20149This command will show an output similar to the one below. You may see
20150additional keys in the =/etc/apt/trusted.gpg.d/= directory - this is where we
20151will be moving any keys currently found in the =trusted.gpg= key ring.
20152
20153In the below example, we can see that this system has four different GPG keys
20154stored within the =trusted.gpg= key ring. Let's go ahead and move them into
20155their own files.
20156
20157#+begin_src txt
20158Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead
20159(see apt-key(8)).
20160
20161/etc/apt/trusted.gpg
20162--------------------
20163pub rsa2048 2011-08-19 [SC] [expires: 2027-05-24]
20164 573B FD6B 3D8F BC64 1079 A6AB ABF5 BD82 7BD9 BF62
20165uid [ unknown] nginx signing key <signing-key@nginx.com>
20166
20167pub rsa4096 2016-10-05 [SC]
20168 72EC F46A 56B4 AD39 C907 BBB7 1646 B01B 86E5 0310
20169uid [ unknown] Yarn Packaging <yarn@dan.cx>
20170sub rsa4096 2016-10-05 [E]
20171sub rsa4096 2019-01-02 [S] [expires: 2026-01-23]
20172sub rsa4096 2019-01-11 [S] [expires: 2026-01-23]
20173
20174pub rsa4096 2024-05-29 [SC]
20175 8540 A6F1 8833 A80E 9C16 53A4 2FD2 1310 B49F 6B46
20176uid [ unknown] nginx signing key <signing-key-2@nginx.com>
20177
20178pub rsa4096 2024-05-29 [SC]
20179 9E9B E90E ACBC DE69 FE9B 204C BCDC D8A3 8D88 A2B3
20180uid [ unknown] nginx signing key <signing-key-3@nginx.com>
20181#+end_src
20182
20183** Moving Keys to the Proper Location
20184
20185*** Exporting Keys to New Files
20186
20187Now that we know the keys, we will need to move them into their own key ring. We
20188can do this by copying the last eight (8) characters from the key's signature
20189and exporting it from this key ring into its own.
20190
20191Using the yarn example from the beginning, here's the command to move this key
20192into its own key ring.
20193
20194#+begin_src sh
20195sudo apt-key export 86E50310 | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/yarn.gpg
20196#+end_src
20197
20198You can repeat this process for any other keys, such as the =nginx= keys in the
20199example above.
20200
20201*** Cleaning Up
20202
20203If you run =sudo apt-key list= again, you should see the keys within their own
20204key rings:
20205
20206#+begin_src txt
20207/etc/apt/trusted.gpg.d/nginx-archive-keyring.gpg
20208------------------------------------------------
20209pub rsa4096 2024-05-29 [SC]
20210 8540 A6F1 8833 A80E 9C16 53A4 2FD2 1310 B49F 6B46
20211uid [ unknown] nginx signing key <signing-key-2@nginx.com>
20212
20213pub rsa2048 2011-08-19 [SC] [expires: 2027-05-24]
20214 573B FD6B 3D8F BC64 1079 A6AB ABF5 BD82 7BD9 BF62
20215uid [ unknown] nginx signing key <signing-key@nginx.com>
20216
20217pub rsa4096 2024-05-29 [SC]
20218 9E9B E90E ACBC DE69 FE9B 204C BCDC D8A3 8D88 A2B3
20219uid [ unknown] nginx signing key <signing-key-3@nginx.com>
20220
20221/etc/apt/trusted.gpg.d/yarn.gpg
20222-------------------------------
20223pub rsa4096 2016-10-05 [SC]
20224 72EC F46A 56B4 AD39 C907 BBB7 1646 B01B 86E5 0310
20225uid [ unknown] Yarn Packaging <yarn@dan.cx>
20226sub rsa4096 2016-10-05 [E]
20227sub rsa4096 2019-01-02 [S] [expires: 2026-01-23]
20228sub rsa4096 2019-01-11 [S] [expires: 2026-01-23]
20229#+end_src
20230
20231Once you have verified that the keys are valid and stored in their own key
20232rings, you can archive the =trusted.gpg= file and run a system update to test
20233the new files.
20234
20235#+begin_src sh
20236sudo mv /etc/apt/trusted.gpg /etc/apt/trusted.gpg.bkp
20237sudo apt update
20238#+end_src
20239
20240Once you've verified that updates work as expected and that the keys are working
20241as intended, you can delete the =.bkp= file created above. If you're storing
20242keys that are not easily re-attainable, I suggest keeping the =.bkp= file stored
20243in a safe location until you are positive that you no longer need it.
20244#+date: <2024-07-11 Thursday 20:24:02>
20245#+title: Running Emacs Natively on Apple Silicon iPad
20246#+description: Procedural instructions for installing Emacs on iPadOS devices with Apple Silicon architecture, including configuration and operational guidance.
20247#+slug: emacs-on-ipad
20248
20249This post describes the process to install and use Emacs on the iPad Air 13-inch
20250(M2). The iPad used in this post is running iPadOS 17.6.
20251
20252** Shell Application
20253
20254In order to use Emacs on an iPad, you will need a terminal emulator application.
20255I recommend [[https://apps.apple.com/us/app/ish-shell/id1436902243][iSH]], since it runs a version of Alpine Linux within the app itself
20256and will allow you to install packages that you need.
20257
20258#+caption: iSH Application
20259#+attr_html: :alt The iSH shell on iPad.
20260[[https://img.cleberg.net/blog/20240711-emacs-on-ipad/ish.webp]]
20261
20262** Require Packages
20263
20264I started by adding the required packages directly within iSH. Emacs should
20265install dependencies by default, but I include a few other packages that I use
20266in my terminal as well.
20267
20268#+begin_src sh
20269apk add emacs ripgrep fd findutils
20270#+end_src
20271
20272#+caption: Package Installation
20273#+attr_html: :alt Showing command "apk add emacs ripgrep rf findutils" within iSH.
20274[[https://img.cleberg.net/blog/20240711-emacs-on-ipad/dependencies.webp]]
20275
20276** Emacs
20277
20278Once this is complete, you should be able to run Emacs natively on your iPad.
20279It's effective, but can be slow at times.
20280
20281I attempted to also install Doom Emacs, which technically worked, but was so
20282incredibly slow and buggy that I was not even able to take screenshots. Someone
20283smarter than me could likely get it to work with a little tinkering.
20284
20285*** MELPA
20286
20287You also have to remember to hook up MELPA yourself in the =.emacs= file to be
20288able to search through their 5700+ packages instead of just ELPA packages. If
20289you don't, you will only have access to ELPA packages like the ones below.
20290
20291#+caption: package-install
20292#+attr_html: :alt A full list of all packages available when searching MELPA.
20293[[https://img.cleberg.net/blog/20240711-emacs-on-ipad/melpa.webp]]
20294
20295Once you have MELPA, you can install packages like the =dashboard= package shown
20296below.
20297
20298#+caption: emacs-dashboard
20299#+attr_html: :alt An example of the emacs-dashboard package.
20300[[https://img.cleberg.net/blog/20240711-emacs-on-ipad/dashboard.webp]]
20301
20302*** Speed
20303
20304While Emacs will run on my iPad, it's not perfect. The largest issue on my iPad
20305is speed - loading Emacs takes 6-7 seconds and installing the =magit= package
20306took 129 seconds.
20307
20308I haven't played around enough to optimize loading times and poke around to see
20309why the network requests take so long, but it's a big enough issue that I
20310wouldn't see casual Emacs users dealing with the lag.
20311#+date: <2024-08-12 Mon 12:48:00>
20312#+title: Essential Org-Mode Features for Productivity
20313#+description: Detailed overview of essential Org-Mode functionalities focusing on task organization, note-taking, and workflow optimization.
20314#+slug: org-mode-features
20315
20316* Cycling (Folding)
20317
20318#+begin_quote
20319[[https://github.com/ccleberg/cleberg.net/blob/main/content/blog/2024-08-11-org-mode-features.org?plain=1#L7][View this section]] in org-mode.
20320#+end_quote
20321
20322My absolute top-used feature is [[https://orgmode.org/manual/Global-and-local-cycling.html][global and local cycling]] and it's not even
20323close. Being able to hit =S-TAB= and collapse all headings when opening a file
20324or needing to see a file's headings is an under appreciated trait.
20325
20326** Local Cycling
20327
20328When focused on a heading, you can press =TAB= (=org-cycle=) to cycle that
20329subtree among the states =folded=, =children=, and =subtree=.
20330
20331#+begin_src txt
20332,-> FOLDED -> CHILDREN -> SUBTREE --.
20333'-----------------------------------'
20334#+end_src
20335
20336** Global Cycling
20337
20338If you're looking to cycle more than one subtree or heading at a time, you can
20339press =S-TAB= or =C-u TAB= to cycle the entire buffer among the states
20340=overview=, =contents=, and =show all=.
20341
20342#+begin_src txt
20343,-> OVERVIEW -> CONTENTS -> SHOW ALL --.
20344'--------------------------------------'
20345#+end_src
20346
20347** Default Views
20348
20349If you need to set a default view for a certain file, you can run =C-u C-u TAB=
20350to set the =org-set-startup-visibility=. Alternatively, just put the following
20351at the top of the buffer:
20352
20353#+begin_src org
20354#+STARTUP: overview
20355#+STARTUP: content
20356#+STARTUP: showall
20357#+STARTUP: show2levels
20358#+STARTUP: show3levels
20359#+STARTUP: show4levels
20360#+STARTUP: show5levels
20361#+STARTUP: showeverything
20362#+end_src
20363
20364* TODO Lists
20365
20366#+begin_quote
20367[[https://github.com/ccleberg/cleberg.net/blob/main/content/blog/2024-08-11-org-mode-features.org?plain=1#L55][View this section]] in org-mode.
20368#+end_quote
20369
20370Next up on in this post are [[https://orgmode.org/manual/TODO-Items.html][TODO Lists]]. Being able to combine note taking and
20371reminders was the primary reason I switched to org-mode in the first place.
20372
20373TODO lists are more than just the Markdown equivalent of =- [ ] Task=. Instead,
20374you can utilize org-mode's native functionality to dynamically create TODO
20375items, have fast access to TODO states, log progress on tasks, clocking work
20376time, prioritization, breaking down tasks into subtasks, and even using checkbox
20377formatting as well.
20378
20379** Prioritizing Tasks
20380
20381One of my favorite ways to separate my tasks are to assign priorities. You can
20382do this by placing a priority cookie into the headline of a TODO item.
20383
20384#+begin_src org
20385,*** TODO [#A] My Task
20386,*** TODO [#C] My Other Task
20387#+end_src
20388
20389You can quickly shift priorities with =S-UP= and =S-DOWN=, as well as set the
20390allowed priorities for a buffer with the following syntax:
20391
20392#+begin_src org
20393#+PRIORITIES: A C B
20394#+PRIORITIES: 1 10 5
20395#+end_src
20396
20397** Deadlines
20398
20399Plans can be set per task by settings special keywords with a date. You can set
20400a =DEADLINE= to note when the task will appear in the agenda or set the
20401=SCHEDULED= variable to note when you plan to start working on the task.
20402
20403#+begin_src org
20404,* TODO My Task
20405DEADLINE: <2004-02-29 Sun>
20406
20407,* My Other Task
20408SCHEDULED: <2004-12-25 Sat>
20409#+end_src
20410
20411As a side note, I use [[https://www.beorgapp.com/][Beorg]] on iOS to sync and edit my org-mode files on the go.
20412Within this app, I use the =SCHEDULED= variable by default and rely heavily on
20413that variable to alert me of tasks with iOS native notifications.
20414
20415* Time Stamping
20416
20417#+begin_quote
20418[[https://github.com/ccleberg/cleberg.net/blob/main/content/blog/2024-08-11-org-mode-features.org?plain=1#L106][View this section]] in org-mode.
20419#+end_quote
20420
20421Another great feature are the [[https://orgmode.org/manual/Dates-and-Times.html][dates and times]] functionalities within org-mode.
20422Timestamps follow this format in org-mode:
20423
20424#+begin_src org
20425,* An active timestamp
20426<2006-11-01 Wed 19:15>
20427
20428,* An inactive timestamp
20429[2006-11-01 Wed 19:15>
20430
20431,* Repeated timestamps
20432<2006-11-01 Wed 19:15 +1m>
20433
20434,* Time range
20435<2006-11-02 Thu 10:00-12:00>
20436
20437,* Date range
20438<2004-08-23 Mon>--<2004-08-26 Thu>
20439#+end_src
20440
20441In addition to the deadlines mentioned above, you can insert timestamps into any
20442marker within the buffer with =C-c .= for an active timestamp and =C-c != for an
20443inactive timestamp. You can read more on [[https://orgmode.org/manual/Creating-Timestamps.html][creating timestamps]] to figure out the
20444different options available when creating timestamps.
20445
20446* Tags
20447
20448#+begin_quote
20449[[https://github.com/ccleberg/cleberg.net/blob/main/content/blog/2024-08-11-org-mode-features.org?plain=1#L137][View this section]] in org-mode.
20450#+end_quote
20451
20452Next up are [[https://orgmode.org/manual/Tags.html][Tags]], which provide a way to document contexts within and across
20453buffers and files.
20454
20455You can assign tags directly to headlines or at the file level and can contain
20456letters, numbers, underscores, and the =@= symbol.
20457
20458#+begin_src org
20459,
20460,* Pick fruits off the trees :work:
20461,** Start with the orange grove :orange:tree:
20462,*** TODO Pick bananas last :banana:
20463#+end_src
20464
20465** Filtering and Searching by Tag
20466
20467You can search for tags with =C-c \= or open the org-agenda with =M-x org-agenda
20468m= and search for a tag of your choosing.
20469
20470* Tables
20471
20472#+begin_quote
20473[[https://github.com/ccleberg/cleberg.net/blob/main/content/blog/2024-08-11-org-mode-features.org?plain=1#L162][View this section]] in org-mode.
20474#+end_quote
20475
20476One of the most inconvenient parts of writing in Markdown was dealing with
20477tables. In org-mode, [[https://orgmode.org/manual/Tables.html][Tables]] are a breeze due to Emacs's built-in table editor
20478and formatter.
20479
20480** Built-In Table Editor
20481
20482With the built-in table editor, press =C-c |= to convert the active region to a
20483table or create an empty table.
20484
20485When focused on a table, press =TAB= to re-align the table. There are more
20486commands available to re-align, traverse, and edit cells and regions within
20487tables in Emacs.
20488
20489For example, refer to the following table. This table uses the =TBLFM= variable
20490to define functions that will apply to the cells within the table. When I press
20491=C-c C-c=, the table cells updated to their resulting values.
20492
20493| N | N^2 | N^3 | N^4 | sqrt(n) | sqrt[4](N) |
20494|---+-----+-----+-----+-----------+------------|
20495| / | < | | > | < | > |
20496| 1 | 1 | 1 | 1 | 1 | 1 |
20497| 2 | 4 | 8 | 16 | 1.4142136 | 1.1892071 |
20498| 3 | 9 | 27 | 81 | 1.7320508 | 1.3160740 |
20499|---+-----+-----+-----+-----------+------------|
20500#+TBLFM: $2=$1^2::$3=$1^3::$4=$1^4::$5=sqrt($1)::$6=sqrt(sqrt(($1)))
20501
20502** Spreadsheet
20503
20504If you spend a lot of time in spreadsheets, you'll appreciate the Emacs Calc
20505package, which Emacs uses to implement spreadsheet-like capabilities within
20506org-mode tables.
20507
20508Read through the [[https://orgmode.org/manual/The-Spreadsheet.html][spreadsheet]] documentation to learn more about field references
20509(=@ROW$COLUMN=), range references (=$1..$3=), coordinates, formulas, and more.
20510
20511For example, refer to the following table. Here is a table that computes the
20512Taylor series of degree n at location x for a couple of functions. You can clear
20513the values out of the =Result= column for each row, press =C-c C-c=, and watch
20514as the table re-calculates the results for each.
20515
20516|---+-------------+---+-----+--------------------------------------|
20517| | Func | n | x | Result |
20518|---+-------------+---+-----+--------------------------------------|
20519| # | exp(x) | 1 | x | 1 + x |
20520| # | exp(x) | 2 | x | 1 + x + x^2 / 2 |
20521| # | exp(x) | 3 | x | 1 + x + x^2 / 2 + x^3 / 6 |
20522| # | x^2+sqrt(x) | 2 | x=0 | x*(0.5 / 0) + x^2 (2 - 0.25 / 0) / 2 |
20523| # | x^2+sqrt(x) | 2 | x=1 | 2 + 2.5 x - 2.5 + 0.875 (x - 1)^2 |
20524| * | tan(x) | 3 | x | x pi / 180 + 5.72e-8 x^3 pi^3 |
20525|---+-------------+---+-----+--------------------------------------|
20526#+TBLFM: $5=taylor($2,$4,$3);n3
20527
20528Alternatively, you can also refer to the following table. This table will update
20529to different values based on the =TBLFM= I select when I'm pressing =C-c C-c=.
20530The results are =[1,2]= when focused on the first line and =[2,4]= when focused
20531on the second line.
20532
20533| x | y |
20534|---+---|
20535| 1 | 2 |
20536| 2 | 4 |
20537#+TBLFM: $2=$1*1
20538#+TBLFM: $2=$1*2
20539
20540* Source Code Blocks
20541
20542#+begin_quote
20543[[https://github.com/ccleberg/cleberg.net/blob/main/content/blog/2024-08-11-org-mode-features.org?plain=1#L232][View this section]] in org-mode.
20544#+end_quote
20545
20546** Introduction to Source Code Blocks
20547
20548If you run a technical blog, write code frequently, or like to take notes with
20549source code examples, you will appreciate the [[https://orgmode.org/manual/Working-with-Source-Code.html][source code]] functionality of
20550org-mode.
20551
20552With this feature, org-mode can manage the source code you define with features
20553such as editing, formatting, extracting, exporting, and publishing.
20554Additionally, you can compile and execute source code dynamically within the
20555org-mode file or buffer.
20556
20557A full source code block uses the following structure:
20558
20559#+begin_src org
20560,#+NAME: <name>
20561,#+BEGIN_SRC <language> <switches> <header arguments>
20562 <body>
20563,#+END_SRC
20564#+end_src
20565
20566You can also use inline blocks:
20567
20568#+begin_src org
20569src_<language>[<header arguments>]{<body>}
20570#+end_src
20571
20572** Editing Source Code Blocks
20573
20574When focused on a code block, press =C-c'= to edit the code block in a special
20575editor. Press =C-x C-x= to save the buffer and =C-c '= again to close the
20576buffer.
20577
20578** Syntax Highlighting and Formatting
20579
20580Emacs provides syntax highlighting by default within org-mode's source code
20581blocks. Org-mode will copy the text to a temporary buffer, highlight it using
20582the major mode relevant to the language (e.g., [[https://www.gnu.org/software/emacs/manual/html_mono/ccmode.html][CC Mode]]), and then return the
20583resulting highlighted text.
20584
20585** Buffer Evaluation
20586
20587By default, org-mode will only evaluate Emacs Lisp code blocks. You will need to
20588enable other [[https://orgmode.org/manual/Languages.html][languages]] in order to evaluate code blocks written in languages
20589other than elisp.
20590
20591When focused on a code block, you can execute the block with =C-c C-c=. The
20592results of the evaluation will appear below the code block in a =RESULTS= block.
20593
20594For example, here is a code block in elisp that I have written and evaluated with
20595org-mode inside this blog post. I will press =C-c C-c= and you will see the
20596results below the code block.
20597
20598#+begin_src elisp
20599(defun greet-me ()
20600 (message "Hello, world!"))
20601
20602(greet-me)
20603#+end_src
20604
20605#+begin_src org
20606,#+RESULTS:
20607: Hello, world!
20608#+end_src
20609
20610* Exporting and Publishing
20611
20612#+begin_quote
20613[[https://github.com/ccleberg/cleberg.net/blob/main/content/blog/2024-08-11-org-mode-features.org?plain=1#L302][View this section]] in org-mode.
20614#+end_quote
20615
20616I would be remiss if I did not mention the native [[https://orgmode.org/manual/Exporting.html][exporting and publishing]]
20617functionality within Emacs and org-mode.
20618
20619** Built-In Publishing
20620
20621This functionality allows you to translate Org to the following formats
20622(additional formats may be available via external packages):
20623
20624- ascii (ASCII format)
20625- beamer (LaTeX Beamer format)
20626- html (HTML format)
20627- icalendar (iCalendar format)
20628- latex (LaTeX format)
20629- md (Markdown format)
20630- odt (OpenDocument Text format)
20631- org (Org format)
20632- texinfo (Texinfo format)
20633- man (Man page format)
20634
20635 You can export with =C-c C-h= and then select your preferred destination
20636 format. For example, an export from org to HTML as a file would use =C-c C-e h
20637 h=.
20638
20639** External Publishing Tools
20640
20641If the native functionality isn't useful enough for you, there are a myriad of
20642external [[https://orgmode.org/tools.html][tools]], including [[https://orgmode.org/worg/org-blog-wiki.html][blogs and wikis]], that work with org-mode.
20643
20644* Agenda
20645
20646#+begin_quote
20647[[https://github.com/ccleberg/cleberg.net/blob/main/content/blog/2024-08-11-org-mode-features.org?plain=1#L336][View this section]] in org-mode.
20648#+end_quote
20649
20650Last, but not least, are [[https://orgmode.org/manual/Agenda-Views.html][Agenda Views]]. With agenda views, you can easily view
20651your agenda of TODO items, scheduled items, projects, etc. across your
20652collection of org-mode files.
20653
20654Org-mode will extract all of this information from your org files
20655(=org-agenda-files=) and display it within the agenda buffer.
20656
20657You can open the agenda with =M-x org-agenda=. Once inside the buffer, you can
20658use shortcuts in the [[https://orgmode.org/manual/Agenda-Dispatcher.html][Agenda Dispatcher]] to quickly view calendars, TODOs, create
20659items, filter items, and more.
20660#+date: <2024-08-25 Sunday 09:45:30>
20661#+title: Automate Email Sentiment Analysis with n8n
20662#+description: Detailed procedural steps for configuring an n8n workflow to perform sentiment analysis on incoming email messages, enabling automated classification based on sentiment.
20663#+slug: n8n-sentiment-analysis
20664
20665* n8n
20666
20667This guide will show you how to self-host [[https://n8n.io/][n8n]], a workflow automation platform,
20668and use it to create a workflow that automatically analyzes the sentiment of all
20669incoming emails on your account.
20670
20671This is a completely free process that only requires that you have access to (1)
20672an email account with IMAP/SMTP options and (2) a computer or machine where you
20673can install and use Docker.
20674
20675** Installation
20676
20677To get started, read the [[https://github.com/n8n-io/self-hosted-ai-starter-kit][self-hosted-ai-starter-kit]] project README or simply
20678clone the repository and set it up with Docker using the command below.
20679
20680#+begin_src sh
20681git clone https://github.com/n8n-io/self-hosted-ai-starter-kit.git
20682cd self-hosted-ai-starter-kit
20683docker compose --profile cpu up
20684#+end_src
20685
20686This will clone the repository, start the stack of Docker containers with
20687=compose=, and make the n8n web page available at [[https://localhost:5678]]. The
20688first run will ask you to configure the administrator account for n8n.
20689
20690** Reverse Proxy
20691
20692If you want to use n8n from a public domain name, you'll need to configure a
20693reverse proxy. I use Nginx on my server for reverse proxies. Nginx configuration
20694locations and practices vary by distribution, but the following commands will
20695show you my general reverse proxy configuration.
20696
20697#+begin_src sh
20698cd /etc/nginx/conf.d
20699nano n8n.conf
20700#+end_src
20701
20702#+begin_src conf
20703server {
20704 listen 443 ssl;
20705 listen [::]:443 ssl;
20706 http2 on;
20707 server_name n8n.example.com;
20708
20709 # SSL
20710 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
20711 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
20712 ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
20713
20714 # reverse proxy
20715 location / {
20716 set $upstream_n8n http://127.0.0.1:5678;
20717
20718 proxy_set_header Remote-User $user;
20719 proxy_set_header Remote-Email $email;
20720 proxy_set_header Remote-Groups $groups;
20721
20722 client_body_buffer_size 128k;
20723 proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
20724
20725 send_timeout 5m;
20726 proxy_read_timeout 360;
20727 proxy_send_timeout 360;
20728 proxy_connect_timeout 360;
20729
20730 proxy_set_header Host $host;
20731 proxy_set_header Upgrade $http_upgrade;
20732 proxy_set_header Connection upgrade;
20733 proxy_set_header Accept-Encoding gzip;
20734 proxy_set_header X-Real-IP $remote_addr;
20735 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
20736 proxy_set_header X-Forwarded-Proto $scheme;
20737 proxy_set_header X-Forwarded-Host $http_host;
20738 proxy_set_header X-Forwarded-Uri $request_uri;
20739 proxy_set_header X-Forwarded-Ssl on;
20740 proxy_redirect http:// $scheme://;
20741 proxy_http_version 1.1;
20742 proxy_set_header Connection "";
20743 proxy_cache_bypass $cookie_session;
20744 proxy_no_cache $cookie_session;
20745 proxy_buffers 64 256k;
20746 }
20747
20748# HTTP redirect
20749server {
20750 listen 80;
20751 listen [::]:80;
20752 server_name n8n.example.com;
20753 include custom.d/letsencrypt.conf;
20754
20755 if ($host ~ ^[^.]+\.example\.com$) {
20756 return 301 https://$host$request_uri;
20757 }
20758}
20759#+end_src
20760
20761Simply restart Nginx to ensure the new configuration is in-effect.
20762
20763#+begin_src sh
20764sudo systemctl restart nginx.service
20765#+end_src
20766
20767** Workflow Configuration
20768
20769You can open the included workflow at
20770[[http://localhost:5678/workflow/srOnR8PAY3u4RSwb]] or simply open the web interface
20771and create a new configuration.
20772
20773#+caption: n8n Workflow
20774#+attr_html: :alt A canvas view of the worklow, showing an email trigger, if statement, sentiment analysis via Ollama, and send email action.
20775[[https://img.cleberg.net/blog/20240825-n8n-sentiment-analysis/workflow.webp]]
20776
20777This workflow contains the following nodes:
207781. Email Trigger (IMAP)
20779 + Create an IMAP credential with your email's IMAP settings to allow n8n to
20780 monitor for new emails.
207812. If
20782 + Check ={{ $json.subject }}= to see if it =contains= "n8n Sentiment
20783 Analysis" to ensure we don't re-check our own emails.
207843. Sentiment Analysis
20785 + Analyze the ={{ $json.textPlain }}= input coming from each email.
20786 + Ensure you have =Include Detailed Results= enabled so that we can access
20787 the confidence and strength variables.
207884. Ollama Chat Model
20789 + The Docker stack we used includes Ollama and Llama3, which provide the
20790 easiest way to test this workflow.
207915. Send Email
20792 + Create an SMTP credential to allow n8n to send emails.
20793 + Subject: =n8n Sentiment Analysis: {{ $json.sentimentAnalysis.category }}=
20794 + Email Format: =Text=
20795 + Text (Expression): This can contain anything you want, but be sure to
20796 include the variables ={{ $json.sentimentAnalysis.category }}=, ={{
20797 $json.sentimentAnalysis.strength }}=, and ={{
20798 $json.sentimentAnalysis.confidence }}=.
20799 + In the additional options, I enabled the =Append n8n Attribution= option in
20800 the screenshots below.
20801
20802** Testing
20803
20804You can use the =Test Workflow= button at the bottom of the canvas area to test
20805the workflow. This relies on receiving new messages in your inbox, so be sure to
20806send yourself a test email!
20807
20808** Results
20809
20810After testing each step noted above, n8n provided the results below - it works!
20811
20812#+caption: Positive Results
20813#+attr_html: :alt An example showing that n8n marked an email as "Positive".
20814[[https://img.cleberg.net/blog/20240825-n8n-sentiment-analysis/positive_results.webp]]
20815
20816#+caption: Negative Results
20817#+attr_html: :alt An example showing that n8n marked an email as "Negative".
20818[[https://img.cleberg.net/blog/20240825-n8n-sentiment-analysis/negative_results.webp]]
20819
20820While this isn't anything earth-shattering, it does show easy it is to get
20821started with n8n and large language models in a self-hosted environment.
20822#+date: <2024-09-20 Friday 13:38:52>
20823#+title: Linux Server Monitoring with Prometheus & Grafana Cloud
20824#+description: Stepwise method for deploying Prometheus with Docker to monitor Linux servers and integrating Grafana Cloud for visualization and analysis of collected metrics.
20825#+slug: prometheus-grafana-cloud
20826
20827This tutorial will guide you through the process of:
20828
208291. Configuring a free Grafana cloud account.
208302. Installing Prometheus to store metrics.
208313. Installing Node Exporter to export machine metrics for Prometheus.
208324. Installing Nginx Exporter to export Nginx metrics for Prometheus.
208335. Visualizing data in Grafana dashboards.
208346. Configure alerts based on Grafana metrics.
20835
20836* Grafana Cloud
20837
20838To get started, visit the [[https://grafana.com/auth/sign-up/create-user][Grafana website]] and create a free account.
20839
20840** Prometheus Data Source
20841
20842By default, a Prometheus data source should exist in your data sources page
20843(=$yourOrg.grafana.net/connections/datasources=). If not, add a new data source
20844using the Prometheus type.
20845
20846Once you have a valid Prometheus data source, open the data source and note the
20847following items:
20848
20849| Data | Example |
20850|-----------------------+---------------------------------------------------------------------|
20851| Prometheus Server URL | https://prometheus-prod-13-prod-us-east-0.grafana.net/api/prom/push |
20852|-----------------------+---------------------------------------------------------------------|
20853| User | 1234567 |
20854|-----------------------+---------------------------------------------------------------------|
20855| Password | configured |
20856
20857** Cloud Access Policy Token
20858
20859Now let's create an access token in Grafana. Navigate to the Administration
20860> Users and Access > Cloud Access Policies page and create an access policy.
20861
20862The =metrics > write= scope must be enabled within the access policy you choose.
20863
20864Once you have an access policy with the correct scope, click the Add Token
20865button and be sure to copy and save the token since it will disappear once the
20866modal window is closed.
20867
20868** Dashboards
20869
20870Finally, let's create a couple dashboards so that we can easily explore the data
20871that we will be importing from the server.
20872
20873I recommend importing the following dashboards:
20874
20875- [[https://grafana.com/grafana/dashboards/1860-node-exporter-full/][Node Exporter Full]]
20876- [[https://github.com/nginxinc/nginx-prometheus-exporter/blob/main/grafana][nginx-prometheus-exporter]]
20877- Prometheus 2.0 Stats
20878
20879Refer to the bottom of the post for dashboard screenshots!
20880
20881* Docker
20882
20883On the machine that you want to observe, make sure Docker and Docker Compose are
20884installed. This tutorial will be using Docker Compose to create a group of
20885containers that will work together to send metrics to Grafana.
20886
20887Let's start by creating a working directory.
20888
20889#+begin_src sh
20890mkdir ~/prometheus && \
20891cd ~/prometheus && \
20892nano compose.yml
20893#+end_src
20894
20895Within the =compose.yml= file, let's paste the following:
20896
20897#+begin_src yaml
20898# compose.yml
20899
20900networks:
20901 monitoring:
20902 driver: bridge
20903
20904volumes:
20905 prometheus_data: {}
20906
20907services:
20908 nginx-exporter:
20909 image: nginx/nginx-prometheus-exporter
20910 container_name: nginx-exporter
20911 restart: unless-stopped
20912 command:
20913 - '--nginx.scrape-uri=http://host.docker.internal:8080/stub_status'
20914 expose:
20915 - 9113
20916 networks:
20917 - monitoring
20918 extra_hosts:
20919 - host.docker.internal:host-gateway
20920
20921 node-exporter:
20922 image: prom/node-exporter:latest
20923 container_name: node-exporter
20924 restart: unless-stopped
20925 volumes:
20926 - /proc:/host/proc:ro
20927 - /sys:/host/sys:ro
20928 - /:/rootfs:ro
20929 command:
20930 - '--path.procfs=/host/proc'
20931 - '--path.rootfs=/rootfs'
20932 - '--path.sysfs=/host/sys'
20933 - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
20934 expose:
20935 - 9100
20936 networks:
20937 - monitoring
20938
20939 prometheus:
20940 image: prom/prometheus:latest
20941 container_name: prometheus
20942 restart: unless-stopped
20943 volumes:
20944 - ./prometheus.yml:/etc/prometheus/prometheus.yml
20945 - prometheus_data:/prometheus
20946 command:
20947 - '--config.file=/etc/prometheus/prometheus.yml'
20948 - '--storage.tsdb.path=/prometheus'
20949 - '--web.console.libraries=/etc/prometheus/console_libraries'
20950 - '--web.console.templates=/etc/prometheus/consoles'
20951 - '--web.enable-lifecycle'
20952 expose:
20953 - 9090
20954 networks:
20955 - monitoring
20956#+end_src
20957
20958#+begin_src sh
20959sudo docker compose up -d
20960#+end_src
20961
20962#+begin_quote
20963I'm not sure if it made a difference but I also whitelisted port 8080 on my
20964local firewall with =sudo ufw allow 8080=.
20965#+end_quote
20966
20967Next, let's create a =prometheus.yml= configuration file.
20968
20969#+begin_src sh
20970nano prometheus.yml
20971#+end_src
20972
20973#+begin_src yaml
20974# prometheus.yml
20975
20976global:
20977 scrape_interval: 1m
20978
20979scrape_configs:
20980 - job_name: 'prometheus'
20981 scrape_interval: 1m
20982 static_configs:
20983 - targets: ['localhost:9090']
20984
20985 - job_name: 'node'
20986 static_configs:
20987 - targets: ['node-exporter:9100']
20988
20989 - job_name: 'nginx'
20990 scrape_interval: 5s
20991 static_configs:
20992 - targets: ['nginx-exporter:9113']
20993
20994remote_write:
20995 - url: 'https://prometheus-prod-13-prod-us-east-0.grafana.net/api/prom/push'
20996 basic_auth:
20997 username: 'prometheus-grafana-username'
20998 password: 'access-policy-token'
20999#+end_src
21000
21001** Nginx
21002
21003To enable to the Nginx statistics we need for the nginx-exporter container, we
21004need to modify the Nginx configuration on the host.
21005
21006More specifically, we need to create a path for the =stub_status= to be returned
21007when we query port 8080 on our localhost.
21008
21009#+begin_src sh
21010sudo nano /etc/nginx/conf.d/default.conf
21011#+end_src
21012
21013#+begin_src conf
21014server {
21015 listen 8080;
21016 listen [::]:8080;
21017
21018 location /stub_status {
21019 stub_status;
21020 }
21021}
21022#+end_src
21023
21024#+begin_src sh
21025sudo systemctl restart nginx.service
21026#+end_src
21027
21028** Debugging
21029
21030At this point, everything should be running smoothly. If not, here are a few
21031areas to check and see if any obvious errors exist.
21032
21033Nginx: Curl the stub_status from the Nginx web server on the host machine to see
21034if Nginx and stub_status are working properly.
21035
21036#+begin_src sh
21037curl http://127.0.0.1:8080/stub_status
21038
21039# EXPECTED RESULTS:
21040Active connections: 101
21041server accepts handled requests
21042 7510 7510 9654
21043Reading: 0 Writing: 1 Waiting: 93
21044#+end_src
21045
21046Nginx-Exporter: Curl the exported Nginx metrics.
21047
21048#+begin_src sh
21049# Figure out the IP address of the Docker container
21050sudo docker network inspect grafana_monitoring
21051
21052...
21053"Name": "nginx-exporter",
21054"EndpointID": "ef999a53eb9e0753199a680f8d78db7c2a8d5f442626df0b1bb945f03b73dcdd",
21055"MacAddress": "02:42:c0:a8:40:02",
21056"IPv4Address": "192.168.64.2/20",
21057...
21058
21059# Curl the exported Nginx metrics
21060curl 192.168.64.2:9113/metrics
21061
21062# EXPECTED RESULTS:
21063...
21064# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
21065# TYPE go_gc_duration_seconds summary
21066go_gc_duration_seconds{quantile="0"} 2.9927e-05
21067go_gc_duration_seconds{quantile="0.25"} 4.24e-05
21068go_gc_duration_seconds{quantile="0.5"} 4.8531e-05
21069...
21070#+end_src
21071
21072Node-Exporter: Curl the exporter node machine metrics.
21073
21074#+begin_src sh
21075# Curl the exported Node metrics
21076curl 192.168.64.3:9100/metrics
21077
21078# EXPECTED RESULTS:
21079...
21080# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
21081# TYPE promhttp_metric_handler_requests_total counter
21082promhttp_metric_handler_requests_total{code="200"} 47
21083promhttp_metric_handler_requests_total{code="500"} 0
21084promhttp_metric_handler_requests_total{code="503"} 0
21085...
21086#+end_src
21087
21088Grafana: Open the Explore panel and look to see if any metrics are coming
21089through the Prometheus data source. If not, something on the machine is
21090preventing data from flowing through.
21091
21092* Alerts & IRM
21093
21094Now that we have our data connected and visualized, we can define alerting rules
21095and determine what Grafana should do when an alert is triggered.
21096
21097** OnCall
21098
21099#+caption: OnCall
21100#+attr_html: :alt View of the Grafana OnCall dashboard with metrics.
21101[[https://img.cleberg.net/blog/20240920-prometheus-grafana-cloud/oncall.webp]]
21102
21103Within the Alerts & IRM section of Grafana (=/alerts-and-incidents=), open the
21104Users page.
21105
21106The Users page allows you to configure user connections such as:
21107
21108- Mobile App
21109- Slack
21110- Telegram
21111- MS Teams
21112- iCal
21113- Google Calendar
21114
21115In addition to the connections of each user, you can specify how each user or
21116team is alerted for Default Notifications and Important Notifications.
21117
21118Finally, you can access the Schedules page within the OnCall module to schedule
21119users and teams to be on call for specific date and time ranges. For my
21120purposes, I put myself on-call 24/7 so that I receive all alerts.
21121
21122#+caption: User Information
21123#+attr_html: :alt View of the grafanafd88 user with notification preferences.
21124[[https://img.cleberg.net/blog/20240920-prometheus-grafana-cloud/irm_user_info.webp]]
21125
21126** Alerting
21127
21128#+caption: Alerting Insights
21129#+attr_html: :alt A dashboard with the current grafana-managed alert rules.
21130[[https://img.cleberg.net/blog/20240920-prometheus-grafana-cloud/alerting_insights.webp]]
21131
21132Now that we have defined users and team associated with an on-call schedule and
21133configured to receive the proper alerts, let's define a rule that will generate
21134alerts.
21135
21136Within the Alerting section of the Alerts & IRM module, you can create alert
21137rules, contact points, and notification policies.
21138
21139Let's start by opening the Alert Rules page and click the New Alert Rule button.
21140
21141As shown in the image below, we will create an alert for high CPU temperature by querying the =node_hwmon_temp_celsius= metric from our Prometheus data source.
21142
21143Next, we will set the threshold to be anything above 50 (degrees Celsius).
21144Finally, we will tell Grafana to evaluate this every 1 minute via our Default
21145evaluation group. This is connected to our Grafana email, but can be associated
21146with any notification policy.
21147
21148#+caption: New Alert Rule
21149#+attr_html: :alt All available seetings when creating a new alert.
21150[[https://img.cleberg.net/blog/20240920-prometheus-grafana-cloud/new_alert.webp]]
21151
21152When the alert fires, it will generate an email (or whatever notification policy
21153you assigned) and will look something like the following image.
21154
21155#+caption: Alerting Example
21156#+attr_html: :alt An email showing that "High CPU Temps" alert is firing.
21157[[https://img.cleberg.net/blog/20240920-prometheus-grafana-cloud/email_alert.webp]]
21158
21159** Dashboards
21160
21161As promised above, here are some dashboard screenshots based on the
21162configurations above.
21163
21164#+caption: Nginx Dashboard
21165#+attr_html: :alt Updated dashboard based on the new alert created above.
21166[[https://img.cleberg.net/blog/20240920-prometheus-grafana-cloud/dashboard_nginx.webp]]
21167
21168#+caption: Node Dashboard
21169#+attr_html: :alt Metrics of server nodes, including CPU, network, and memory.
21170[[https://img.cleberg.net/blog/20240920-prometheus-grafana-cloud/dashboard_node.webp]]
21171
21172#+caption: OnCall Dashboard
21173#+attr_html: :alt Full OnCall dashboard with metrics for alerts, mean time to respond, and more.
21174[[https://img.cleberg.net/blog/20240920-prometheus-grafana-cloud/dashboard_oncall.webp]]
21175
21176#+caption: Prometheus Dashboard
21177#+attr_html: :alt Dashboard showing Prometheus metrics, including scrape duration, head shunks, reload count, and more.
21178[[https://img.cleberg.net/blog/20240920-prometheus-grafana-cloud/dashboard_prometheus.webp]]
21179#+date: <2024-09-23 Monday 19:52:20>
21180#+title: Self-Hosting Guide: Transmission BitTorrent Client
21181#+description: Comprehensive setup instructions for installing and configuring the Transmission BitTorrent client on a Linux server, including securing remote access through Nginx reverse proxy.
21182#+slug: self-hosting-transmission
21183
21184#+begin_quote
21185If you're torrenting anything sensitive, I *highly* recommend you use a VPN.
21186Something like mullvad-cli is incredibly simple to use and can be configured to
21187have a "killswitch" or "lockdown mode" to ensure that if the VPN disconnects,
21188your traffic won't be leaked to your ISP.
21189#+end_quote
21190
21191* Transmission
21192
21193[[https://transmissionbt.com/][Transmission]] is a cross-platform bittorrent client that supports running a
21194[[https://linux.die.net/man/1/transmission-remote][remote control utility]], a [[https://linux.die.net/man/1/transmission-daemon][daemon service]] for running as a background service,
21195and a [[https://linux.die.net/man/1/transmission-cli][command-line client]].
21196
21197Since I love torrenting Linux ISOs and providing them back to the community,
21198let's walk through a tutorial of setting up Transmission on a headless server
21199and connecting it to a domain name (=transmission.example.com=) so that we can
21200manage our torrents remotely.
21201
21202This tutorial assumes you have a Linux machine, have Nginx installed, and have a
21203domain name pointing at your Linux machine.
21204
21205** Installation
21206
21207First, let's install a couple Transmission packages on the system. We don't need
21208the GUI components, so we'll only install the daemon and command line interface
21209utilities.
21210
21211#+begin_src sh
21212sudo apt install transmission-cli transmission-common transmission-daemon
21213#+end_src
21214
21215You will need to run the program to initialize the files before you can edit the
21216configurations, so let's run it and end the process.
21217
21218#+begin_src sh
21219# Run the program
21220transmission-daemon -e ~/.local/log/transmission.log
21221
21222# End the program after it finishes running
21223transmission-remote --exit
21224#+end_src
21225
21226** Configuration
21227
21228Now that we've run the program for the first time and initialized the relevant
21229files, let's edit those files.
21230
21231#+begin_quote
21232If you edit the files while Transmission is running, your changes won't be
21233saved! Make sure to end the service, update the configuration files, and restart
21234the service.
21235#+end_quote
21236
21237To start, let's edit the main configuration file.
21238
21239#+begin_src sh
21240nano ~/.config/transmission-daemon/settings.json
21241#+end_src
21242
21243Within this file, I suggesting skimming *every* option and determining if you
21244want to change any of those options.
21245
21246For remote access, we will focus on the following =rpc= options. This
21247configuration will not require authentication, will allow any device with access
21248(I suggest that you have a firewall restricting access) to access the service
21249(="rpc-bind-access": "0.0.0.0"=), will open the service on port =9091=, and will
21250whitelist a few LAN IPs (="rpc-whitelist":
21251"127.0.0.1,::1,192.168.0.98,192.168.0.97"=).
21252
21253#+begin_src json
21254{
21255 ...
21256 "rpc-authentication-required": false,
21257 "rpc-bind-address": "0.0.0.0",
21258 "rpc-enabled": true,
21259 "rpc-host-whitelist": "",
21260 "rpc-host-whitelist-enabled": true,
21261 "rpc-password": "{7fc02520b97e054f7a15274c7cfafe3cd7330169.OQUAUS4",
21262 "rpc-port": 9091,
21263 "rpc-socket-mode": "0750",
21264 "rpc-url": "/transmission/",
21265 "rpc-username": "",
21266 "rpc-whitelist": "127.0.0.1,::1,192.168.0.98,192.168.0.97",
21267 "rpc-whitelist-enabled": true,
21268 ...
21269}
21270#+end_src
21271
21272Once you've finished configuring the service, start the service up again.
21273
21274#+begin_src sh
21275transmission-daemon -e ~/.local/log/transmission.log
21276#+end_src
21277
21278At this point, you should be able to access the website at =localhost:9091= (if
21279you're browsing on the machine where Transmission is running) or
21280=$server_ip:9091= (if you're browsing from a different LAN device).
21281
21282If you want to make further changes to Transmission's configuration, I suggest
21283doing so now. Once you start working on remote access via a reverse proxy,
21284you'll be adding an additional layer of complexity that bring in more confusion
21285when errors occur.
21286
21287#+begin_quote
21288NOTE: If you are trying to initialize =transmission-daemon= via =systemd=
21289instead of using the manually-executed command above, you may notice that the
21290service will timeout and fail to start.
21291#+end_quote
21292
21293To fix this timeout issue, you need to edit the service file and change
21294=Type=notify= to =Type=simple=.
21295
21296#+begin_src shell
21297# Command to edit the service file:
21298sudo systemctl edit --full transmission-daemon.service
21299
21300# Make the edit noted above and then reload the service file with this command:
21301sudo systemctl daemon-reload
21302sudo systemctl enable transmission-daemon.service
21303sudo systemctl start transmission-daemon.service
21304#+end_src
21305
21306* Reverse Proxy
21307
21308Now that the service is running and configured properly, let's work on remote
21309access.
21310
21311This tutorial will use Nginx, but you can use any reverse proxy or something
21312like Cloudflare Tunnels if that's your thing.
21313
21314** Configuration
21315
21316If you have Nginx installed, you should have either the =/etc/nginx/conf.d= or
21317=/etc/nginx/sites-available= directories available to create website
21318configuration files. This tutorial assumes the =conf.d= structure, but it's
21319essentially the same except using the =sites-available= structure requires you
21320to symlink your files into the =sites-enabled= directory.
21321
21322Let's start by creating the website configuration file.
21323
21324#+begin_src sh
21325sudo nano /etc/nginx/conf.d/transmission.conf
21326#+end_src
21327
21328Within the file, you will need a configuration similar to the code below. Note
21329that this uses SSL and requires a valid TLS/SSL certificate. You can use [[https://letsencrypt.org/][Let's
21330Encrypt]] if you don't have a certificate yet.
21331
21332#+begin_src conf
21333server {
21334 listen 443 ssl;
21335 listen [::]:443 ssl;
21336 http2 on;
21337 server_name transmission.example.com;
21338
21339 # SSL
21340 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
21341 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
21342 ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
21343
21344 # reverse proxy
21345 location / {
21346 set $upstream_transmission http://localhost:9091;
21347 proxy_pass $upstream_transmission;
21348 proxy_pass_header X-Transmission-Session-Id;
21349 }
21350}
21351
21352# HTTP redirect
21353server {
21354 listen 80;
21355 listen [::]:80;
21356 server_name transmission.example.com;
21357
21358 if ($host ~ ^[^.]+\.example\.com) {
21359 return 301 https://$host$request_uri;
21360 }
21361}
21362#+end_src
21363
21364Once you've saved the configuration file, restart the Nginx web server to enable
21365the remote access connection.
21366
21367#+begin_src sh
21368sudo systemctl restart nginx.service
21369#+end_src
21370
21371At this point, Transmission should now be available at
21372=transmission.example.com=, same as it's available on the LAN.
21373
21374#+begin_quote
21375Pro Tip: If you dislike something about the website UI, you can edit the
21376website's files in the =/usr/share/transmission/public_html/= directory. You can
21377modify the HTML, CSS, and JS files in this directory.
21378#+end_quote
21379#+date: <2024-10-31 Thursday 11:01:05>
21380#+title: Ollama Code Assistant Setup in VS Codium
21381#+description: Technical overview and configuration guide for incorporating Ollama as a code assistance tool within VS Codium and VS Code editors to enhance development workflows.
21382#+slug: continue-ollama-code-assistant
21383
21384* Background
21385
21386As someone who doesn't do software development full time and intends to actually
21387enjoy it as a hobby, I've been interested in the /concept/ of code assistants
21388for a while. However, I had a few issues:
21389
213901. I'm not a full-time developer, so I wanted to actually learn all the various
21391 features and rules of the projects I was building.
213922. The quality was quite low until recently.
213933. I wanted to use an open-source solution. Ideally, one with a free tier so
21394 that I could test it before paying for a subscription.
21395
21396I recently discovered [[https://www.continue.dev/][Continue]], which allows for local LLM configurations via
21397[[https://ollama.com/][Ollama]], so I've decided to try it out and see how helpful a code assistant can
21398really be for someone like me, who doesn't do this professionally.
21399
21400* Installation
21401
21402Installation is a quick and painless process if you already have Ollama
21403installed, but I'll assume that we're starting from scratch.
21404
21405** Ollama
21406
21407First, install Ollama for your system. For macOS devices, I'd recommend using
21408Homebrew via the following command. However, you can also visit their website
21409and install the software manually.
21410
21411#+begin_src sh
21412brew install ollama
21413brew services start ollama
21414#+end_src
21415
21416Next, we will need to install the models requested by the Continue extension. By
21417default, Continue asks you to install the =llama3.1:8b= and =starcoder2:3b=
21418models. However, you can customize the configuration to specify different
21419models, if preferred.
21420
21421#+begin_src sh
21422ollama pull llama3.1:8b
21423ollama pull starcoder2:3b
21424#+end_src
21425
21426At this point, Ollama should be running on your local machine and have two
21427models available for use.
21428
21429You can test this by visiting =http://localhost:11434/= in your browser or use
21430cURL in your shell:
21431
21432#+begin_src sh
21433curl http://localhost:11434
21434
21435# If running, the response will look like this:
21436# Ollama is running
21437#+end_src
21438
21439** Continue
21440
21441To get started with using Ollama as a code assistant in VS Codium, you'll first
21442need to install the Continue extension. This extension is available for free on
21443the Visual Studio Code marketplace and can be easily installed via the built-in
21444Extension view.
21445
21446Follow these steps:
214471. Open VS Codium and navigate to the Extensions view by clicking on the square
21448 icon next to the file menu or pressing =Ctrl+Shift+X= (Windows/Linux) or
21449 =Cmd+Shift+X= (Mac).
214502. Search for "Continue" in the Extensions marketplace.
214513. Click on the Continue extension and click Install.
21452
21453Once installed, restart VS Codium to ensure that the extension is properly
21454loaded.
21455
21456* Continue: Extension Configuration
21457
21458When you first install Continue, it will ask which service you'll be using. If
21459you select Ollama, it will check to make sure Ollama is running and you have
21460installed the two default models from Ollama.
21461
21462After this initial setup, you can open the =config.json= file by pressing =Cmd +
21463p= and entering => Continue: Open config.json=. This will show a config such as
21464the following:
21465
21466#+begin_src json
21467{
21468 "models": [
21469 {
21470 "title": "Llama 3.1 8B",
21471 "provider": "ollama",
21472 "model": "llama3.1:8b"
21473 },
21474 {
21475 "model": "claude-3-5-sonnet-20240620",
21476 "provider": "anthropic",
21477 "apiKey": "",
21478 "title": "Claude 3.5 Sonnet"
21479 }
21480 ],
21481 "tabAutocompleteModel": {
21482 "title": "Starcoder 3b",
21483 "provider": "ollama",
21484 "model": "starcoder2:3b"
21485 },
21486 "customCommands": [
21487 {
21488 "name": "test",
21489 "prompt": "{{{ input }}}\n\nWrite a comprehensive set of unit tests for the selected code. It should setup, run tests that check for correctness including important edge cases, and teardown. Ensure that the tests are complete and sophisticated. Give the tests just as chat output, don't edit any file.",
21490 "description": "Write unit tests for highlighted code"
21491 }
21492 ],
21493 "contextProviders": [
21494 {
21495 "name": "code",
21496 "params": {}
21497 },
21498 {
21499 "name": "docs",
21500 "params": {}
21501 },
21502 {
21503 "name": "diff",
21504 "params": {}
21505 },
21506 {
21507 "name": "terminal",
21508 "params": {}
21509 },
21510 {
21511 "name": "problems",
21512 "params": {}
21513 },
21514 {
21515 "name": "folder",
21516 "params": {}
21517 },
21518 {
21519 "name": "codebase",
21520 "params": {}
21521 }
21522 ],
21523 "slashCommands": [
21524 {
21525 "name": "edit",
21526 "description": "Edit selected code"
21527 },
21528 {
21529 "name": "comment",
21530 "description": "Write comments for the selected code"
21531 },
21532 {
21533 "name": "share",
21534 "description": "Export the current chat session to markdown"
21535 },
21536 {
21537 "name": "cmd",
21538 "description": "Generate a shell command"
21539 },
21540 {
21541 "name": "commit",
21542 "description": "Generate a git commit message"
21543 }
21544 ],
21545 "embeddingsProvider": {
21546 "provider": "ollama",
21547 "model": "nomic-embed-text"
21548 }
21549}
21550#+end_src
21551
21552You can modify this file with many different customizations. Refer to the
21553[[https://docs.continue.dev/customize/config][Configuration options]] page for more information.
21554
21555* Use Cases
21556
21557While I'm sure there are a ton of use cases that I can't think of, I decided to
21558test it out with this blog and some basic Python scripts I am currently using.
21559Here are the most common ones I've used so far:
21560
21561- Improving README documentation
21562- Refactor my =salary_visualization.py= script to align with PEP8.
21563- Auto-complete thoughts and suggest further ideas for topics in this blog post.
21564
21565As an example, the following list of possible use cases was auto-generated by
21566Continue:
21567
21568- Auto-complete function names and variables: With Ollama enabled, typing a few
21569 characters into the editor will suggest matching functions or variables from
21570 the entire project.
21571- Code suggestions for common tasks: Ollama can provide suggestions for common
21572 programming tasks, such as converting types or formatting code.
21573- Live coding assistance: As you type, Ollama can offer live suggestions and
21574 corrections to help ensure your code is correct.
21575
21576** Screenshots
21577
21578Below are a few screenshots from my current VS Codium window:
21579
21580#+caption: Available Continue Commands
21581#+attr_html: :alt List of commands available from the Continue package.
21582[[https://img.cleberg.net/blog/20241031-continue-ollama-code-assistant/continue_commands.webp]]
21583
21584#+caption: Continue Fullscreen Chat Window
21585#+attr_html: :alt Continue chat window in full screen mode.
21586[[https://img.cleberg.net/blog/20241031-continue-ollama-code-assistant/continue_fullscreen.webp]]
21587
21588#+caption: Inline Hotkeys
21589#+attr_html: :alt View of the Add to chat (Cmd+L) and Edit highlighted code (Cmd+I) options when typing inline.
21590[[https://img.cleberg.net/blog/20241031-continue-ollama-code-assistant/continue_inline_hotkeys.webp]]
21591
21592#+caption: Inline Editing Suggestions
21593#+attr_html: :alt Highlighted red and green sections showing suggestions from the agent.
21594[[https://img.cleberg.net/blog/20241031-continue-ollama-code-assistant/continue_inline.webp]]
21595
21596#+caption: Sidebar Context Window
21597#+attr_html: :alt View of the sidebar context window open side-by-side with an edited file.
21598[[https://img.cleberg.net/blog/20241031-continue-ollama-code-assistant/continue_sidebar.webp]]
21599
21600* Conclusion
21601
21602As it stands, it seems that the current iteration of code completion and review
21603models from Ollama are quite good for my use case. In particular, it is able to
21604suggest logical continuations of my thoughts in a blog post, generate accurate
21605documentation based on my files, explain code to me with references within the
21606project, and align my existing files to standards.
21607
21608However, it is not perfect. I have noticed that it often goes off in a random
21609direction, unrelated to the intent of what I'm writing (either blogging or
21610programming). It also struggles to understand the full context without clear,
21611specific, repeated instructions to refer to certain files, standards, etc. while
21612suggesting improvements.
21613
21614All together, I think it's useful enough to suggest as an add-on tool, but I
21615would be highly skeptical of any suggestions it provides.
21616#+date: <2024-12-29 Sun 17:45:00>
21617#+title: Self-Hosting Guide: The Lounge (IRC)
21618#+description: Step-by-step instructions for installing and configuring The Lounge IRC web client on Linux systems utilizing Docker Compose to provide persistent and secure chat access.
21619#+slug: self-hosting-the-lounge
21620
21621* The Lounge
21622
21623[[https://thelounge.chat/][The Lounge]] is a self-hosted IRC client for the web, which supports a lot of
21624desirable features for a modern IRC client. The Lounge supports push
21625notifications, link previews, file uploads, always connected, multi-user
21626support, and is available as a PWA for mobile devices.
21627
21628I wanted to write this as I had written a post about [[https://cleberg.net/blog/self-hosting-convos.html][self-hosting Convos]] and
21629have recently migrated over to The Lounge instead.
21630
21631If you'd like to try a demo first, head over to [[https://demo.thelounge.chat/][the official demo website]].
21632
21633** Installation (Docker)
21634
21635I install everything I can via Docker, so this tutorial will install The Lounge
21636with the Docker Compose platform.
21637
21638You can find the official docker version of The Lounge's repository on GitHub at
21639[[https://github.com/thelounge/thelounge-docker][thelounge-docker]].
21640
21641To start, let's create a directory for this app and create the =compose.yml= file.
21642
21643#+begin_src shell
21644mkdir thelounge
21645cd thelounge
21646nano compose.yml
21647#+end_src
21648
21649Within this configuration file, you can paste the content below and customize as
21650needed. If you want to use a different port on your machine, change the first
21651port on the =9000:9000= line. Additionally, you may move the volume to a
21652different location if required.
21653
21654#+begin_src yaml
21655services:
21656 thelounge:
21657 image: ghcr.io/thelounge/thelounge:latest
21658 container_name: thelounge
21659 ports:
21660 - "9000:9000"
21661 restart: always
21662 volumes:
21663 - ./.thelounge:/var/opt/thelounge
21664#+end_src
21665
21666Save and close the file and you can now launch the service.
21667
21668#+begin_src shell
21669sudo docker compose up -d
21670#+end_src
21671
21672The service is now available at =localhost:9000= or =machine_ip:9000= if you're
21673browsing from a different device. Don't forget to allow the port through your
21674machine's firewall, if you have one enabled.
21675
21676#+caption: Login
21677#+attr_html: :alt The Lounge login page with username and password fields.
21678[[https://img.cleberg.net/blog/20241229-thelounge/login.webp]]
21679
21680** Nginx Reverse Proxy
21681
21682If you want to access the service via a domain name (=thelounge.example.com=),
21683you can use Nginx as a reverse proxy.
21684
21685First, create the Nginx configuration file.
21686
21687#+begin_src shell
21688sudo nano /etc/nginx/conf.d/
21689#+end_src
21690
21691The configuration below assumes you have a wildcard certificate for HTTPS (:443)
21692traffic via =example.com=. If you don't, you'll need to obtain an SSL
21693certificate to use HTTPS.
21694
21695#+begin_src configuration
21696upstream irc_upstream { server 127.0.0.1:9000; }
21697
21698# HTTP redirect
21699server {
21700 listen 80;
21701 listen [::]:80;
21702 server_name thelounge.example.com;
21703 include custom.d/letsencrypt.conf;
21704
21705 if ($host ~ ^[^.]+\.example\.com) {
21706 return 301 https://$host$request_uri;
21707 }
21708}
21709
21710# HTTPS
21711server {
21712 listen 443 ssl;
21713 listen [::]:443 ssl;
21714 http2 on;
21715 server_name thelounge.example.com;
21716
21717 # SSL
21718 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
21719 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
21720 ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
21721
21722 # reverse proxy
21723 location / {
21724 proxy_pass http://irc_upstream;
21725 client_max_body_size 0;
21726 proxy_set_header X-Request-Base "$scheme://$host/";
21727
21728 # Standard reverse proxy settings
21729 proxy_set_header Host $host;
21730 proxy_set_header Upgrade $http_upgrade;
21731 proxy_set_header Connection upgrade;
21732 proxy_set_header Accept-Encoding gzip;
21733 proxy_set_header X-Real-IP $remote_addr;
21734 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
21735 proxy_set_header X-Forwarded-Proto $scheme;
21736 proxy_set_header X-Forwarded-Host $http_host;
21737 proxy_set_header X-Forwarded-Uri $request_uri;
21738 proxy_set_header X-Forwarded-Ssl on;
21739 proxy_redirect http:// $scheme://;
21740 proxy_http_version 1.1;
21741 proxy_set_header Connection "";
21742 proxy_cache_bypass $cookie_session;
21743 proxy_no_cache $cookie_session;
21744 proxy_buffers 64 256k;
21745 }
21746}
21747#+end_src
21748
21749Finally, restart Nginx to see the effects.
21750
21751#+begin_src shell
21752sudo systemctl restart nginx.service
21753#+end_src
21754
21755** Initial Setup
21756
21757The first thing you'll need to do is create a user. You can do this with the
21758docker container with the following command, which will ask for a password.
21759
21760#+begin_src shell
21761sudo docker exec --user node -it thelounge thelounge add [username]
21762#+end_src
21763
21764Once the user has been created, you'll be able to log in to the web interface.
21765Once created, you can change your password in the settings panel of the web
21766interface.
21767
21768Finally, you can connect to an IRC server with the plus (=+=) button at the
21769bottom of the sidebar and connect to individual channels or users via the plus
21770(=+=) button next to your server's name in the sidebar.
21771
21772#+caption: New Server Connection
21773#+attr_html: :alt Settings for network, proxy, preferences, and authentication.
21774[[https://img.cleberg.net/blog/20241229-thelounge/new_connection.webp]]
21775
21776#+caption: Existing Server Connection
21777#+attr_html: :alt Libera.Chat server connection log and status.
21778[[https://img.cleberg.net/blog/20241229-thelounge/existing_connection.webp]]
21779
21780#+caption: Channel View
21781#+attr_html: :alt View of the #emacs channel on Libera.Chat.
21782[[https://img.cleberg.net/blog/20241229-thelounge/channel.webp]]
21783#+date: <2025-01-23 Thursday 20:44:45>
21784#+title: Self-Hosting Guide: Tandoor Recipe Manager
21785#+description: Detailed tutorial on deploying Tandoor, an open-source recipe management application, including installation steps and configuration for self-hosted operation.
21786#+slug: self-hosting-tandoor
21787
21788* Overview
21789
21790[[https://tandoor.dev/][Tandoor]] is a smart recipe manager web application that allows you to:
21791- Store your recipes
21792- Search for recipes and ingredients
21793- Share recipes with a link and permission system
21794- Put recipes and ingredients into a shopping list
21795- Plan your meals with a built-in calendar
21796- Calculate nutritional values
21797- Import recipes from websites and apps
21798- Load existing recipes
21799
21800* Installation
21801
21802[[https://docs.tandoor.dev/][The Tandoor docs]] contain all of the relevant information needed to get start
21803with the self-hosting process.
21804
21805This tutorial will use Docker Compose and Nginx on Ubuntu 24.04.1.
21806
21807** Docker Compose
21808
21809On your machine, create a directory for Tandoor and copy down the =.env= and
21810=docker-compose.yml= templates:
21811
21812#+begin_src shell
21813mkdir ~/tandoor cd ~/tandoor wget
21814https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env wget
21815# Plain version of docker-compose.yml
21816https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/plain/docker-compose.yml
21817#+end_src
21818
21819Within these files, customize as needed.
21820- =.env=
21821 - Add a =SECRET_KEY=
21822 - Define the =ALLOWED_HOSTS=
21823 - Set the =POSTGRES_PASSWORD=
21824- =docker-compose.yml=
21825 - Update the =ports= since Nginx on my host is already using port 80 (e.g.
21826 =8087:80=)
21827
21828Once you've updated and saved the files, you can launch the container.
21829
21830#+begin_src shell
21831sudo docker compose up -d
21832#+end_src
21833
21834The application is now available at =localhost:8087= or =ip_address:8087= if
21835accessing via a different machine. If accessing via a different machine,
21836remember to allow port =8087= through any existing firewalls.
21837
21838** Nginx Reverse Proxy
21839
21840Now that Tandoor is available locally, let's connect it to the Nginx web server
21841running on the host machine.
21842
21843#+begin_quote
21844Note: I use Nginx configuration files within the =conf.d= directory, but you may
21845need to use the =sites-available= directory, depending on your installation of
21846Nginx.
21847#+end_quote
21848
21849#+begin_src shell
21850cd /etc/nginx/conf.d/
21851sudo nano recipes.conf
21852#+end_src
21853
21854Within this file, define your website configuration. The example below is my
21855default configuration and utilizes a wildcard certificate for =*.example.com=
21856that covers all of my subdomains. If you don't have a wildcard certificate, you
21857will need to generate an SSL certificate for your domain.
21858
21859#+begin_src config
21860server {
21861 listen 443 ssl;
21862 listen [::]:443 ssl;
21863 http2 on;
21864 server_name recipes.example.com;
21865
21866 # SSL
21867 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
21868 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
21869 ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
21870
21871 # reverse proxy
21872 location / {
21873 proxy_pass http://localhost:8087/;
21874 proxy_set_header Host $http_host;
21875 proxy_set_header X-Forwarded-Proto $scheme;
21876 proxy_redirect http://127.0.0.1:8080 https://recipes.example.com;
21877 }
21878
21879 location /media/ {
21880 root /media/;
21881 index index.html index.htm;
21882 }
21883}
21884
21885# HTTP redirect
21886server {
21887 listen 80;
21888 listen [::]:80;
21889 server_name recipes.example.com;
21890 include custom.d/letsencrypt.conf;
21891
21892 if ($host ~ ^[^.]+\.example\.com$) {
21893 return 301 https://$host$request_uri;
21894 }
21895}
21896#+end_src
21897
21898Save and close the configuration file and then restart the web server.
21899
21900#+begin_src shell
21901sudo systemctl restart nginx.service
21902#+end_src
21903
21904The app is now available on your custom domain!
21905
21906** Screenshots
21907
21908#+caption: Login
21909#+attr_html: :alt Tandoor login page.
21910[[https://img.cleberg.net/blog/20250123-self-hosting-tandoor/login.webp]]
21911
21912#+caption: Recipes
21913#+attr_html: :alt Tandoor options to search for recipes.
21914[[https://img.cleberg.net/blog/20250123-self-hosting-tandoor/recipes.webp]]
21915
21916#+caption: Meal Plan
21917#+attr_html: :alt Calendar view to define meal plans.
21918[[https://img.cleberg.net/blog/20250123-self-hosting-tandoor/mealplan.webp]]
21919
21920#+caption: Shopping Lists
21921#+attr_html: :alt Page to create or search for shopping lists.
21922[[https://img.cleberg.net/blog/20250123-self-hosting-tandoor/shopping.webp]]
21923
21924#+caption: Cook Book
21925#+attr_html: :alt Page to create a visual-style cookbook.
21926[[https://img.cleberg.net/blog/20250123-self-hosting-tandoor/cookbook.webp]]
21927
21928#+caption: Import
21929#+attr_html: :alt Options to import recipes from other pages and auto-populate images, keywords, steps, and a description.
21930[[https://img.cleberg.net/blog/20250123-self-hosting-tandoor/import.webp]]
21931
21932#+caption: Administration Panel
21933#+attr_html: :alt Django administration options for the web app.
21934[[https://img.cleberg.net/blog/20250123-self-hosting-tandoor/admin.webp]]
21935#+date: <2025-02-11 Tuesday 11:40:00>
21936#+title: Obscura VPN: A Two-Party Protocol Review
21937#+description: Analytical review outlining the features, implementation, and security aspects of Obscura VPN's two-party protocol in conjunction with Mullvad VPN.
21938#+slug: obscura-vpn
21939
21940#+begin_quote
21941This review is written at a high-level for users, not a technical deep-dive of
21942VPN technologies.
21943#+end_quote
21944
21945* What is Obscura?
21946
21947[[https://obscura.net/][Obscura]] is a new VPN offering that's [[https://mullvad.net/en/blog/mullvad-partnered-with-obscura-vpn][partnered with Mullvad]] to provide a unique
21948service: a VPN that "can't log your activity" due to the fact that it uses
21949Obscura servers for initial traffic routing and then passes off the traffic to
21950Mullvad's WireGuard servers as exit nodes.
21951
21952This two-party VPN protocol allows you to use Obscura to relay your traffic, but
21953they don't see your traffic. On the other hand, the exit nodes from Mullvad will
21954see where you're connecting but cannot tell who you are as your traffic will
21955come directly from Obscura servers with other users.
21956
21957* Privacy Features
21958
21959One of the things I look for when considering a VPN are privacy features
21960intended to benefit users. Here are the features I love about Obscura:
21961
21962- Two-party VPN protocol with Mullvad's servers
21963- No email required for sign-up
21964- No credit card required, crypto options available
21965- Open source [[https://github.com/Sovereign-Engineering/obscuravpn-client][repository]] is available
21966- [[http://ngmmbxlzfpptluh4tbdt57prk3zxmq4ztew7l2whmg7hkqaof2nzf7id.onion/][Tor website available]]
21967- Transparent display of the exit server's public key (see the Location page in
21968 the app) so you can verify with Mullvad that you're connect via a genuine
21969 Mullvad exit node using [[https://mullvad.net/en/servers][Mullvad's server listing]]
21970
21971* Initial Impressions
21972
21973** Available Platforms
21974
21975I am testing out Obscura on macOS, as that is the only available platform. This
21976is the biggest downside, as I would not want to support different VPNs on
21977different devices - if I'm using Obscura, I want to use it on all of my
21978platforms (macOS, iOS, and Ubuntu).
21979
21980However, the macOS app is incredibly easy to install via the =.dmg= file
21981available on Obscura's website. Upon first launch, you're presented with a login
21982option and can create an account now, if you have not done so already.
21983
21984** Payment
21985
21986Once created, the app will ask you to pay if you do not have a subscription. As
21987far as I can tell, there is no free tier available. In my opinion, this is a
21988good thing as I hesitate to understand how privacy-focused apps make money when
21989they offer free services to users.
21990
21991#+caption: Payment
21992#+attr_html: :alt Options to fund your Obscura account with credit card or lightning.
21993[[https://img.cleberg.net/blog/20250211-obscura-vpn/payment.webp]]
21994
21995** Connection
21996
21997Once you've enrolled in either a subscription or one-time payment, Obscura will
21998return you to the app and allow you to connect. You can choose a specific server
21999location or automatically connect.
22000
22001#+caption: Initial Connection
22002#+attr_html: :alt Obscura macOS app on the Connection screen.
22003[[https://img.cleberg.net/blog/20250211-obscura-vpn/connect.webp]]
22004
22005#+caption: Toolbar Utility
22006#+attr_html: :alt The Obscura VPN menu bar app on macOS shows connection status, bandwidth status, and options to debug or quit.
22007[[https://img.cleberg.net/blog/20250211-obscura-vpn/toolbar.webp]]
22008
22009** Speed
22010
22011The connection feels naturally smooth and I haven't experienced any disruptions
22012or lag in my (very short) time testing out the application. To confirm, I
22013checked a speed test while connected to a server location in my country.
22014
22015Speed test results:
22016
22017- 85.49 Mbps (Down)
22018- 217.75 Mbps (Up)
22019- 25 ms (Ping)
22020
22021** Mullvad Verification
22022
22023Lastly, I checked with Mullvad's verification tool (=mullvad.net/en/check=) to
22024verify Obscura's claims that Obscura sends your traffic through Mullvad's exit
22025nodes before connecting to your target.
22026
22027From the image below, we can see that Mullvad confirmed my traffic to be
22028connected through a Mullvad IP address.
22029
22030#+caption: Mullvad Check
22031#+attr_html: :alt The Mullvad connection check web page shows all "green" when on Obscura.
22032[[https://img.cleberg.net/blog/20250211-obscura-vpn/mullvad-check.webp]]
22033
22034* Final Thoughts
22035
22036Overall, I have found the app to be fast and user friendly. The privacy features
22037are certainly compelling, but the platform has a ways to go before it can
22038displace something like Mullvad in the privacy-focused portion of the VPN
22039market.
22040
22041I wish Obscura offered the following features:
22042
22043- Available on more platforms
22044- Specifics on how many devices will be allowed per account
22045- More server locations, especially in countries with stronger data privacy laws
22046- Options to pay with other crypto beside Bitcoin, such as Monero
22047- Whitepapers or videos that walk through the technical details and show how
22048 traffic can be verified to be encrypted and connecting through the various
22049 expected nodes
22050- Verifiable builds
22051
22052However, Obscura is a nice tool for now and I hope to see more information from
22053the technical side to help more users gain comfort around the security and
22054privacy of the tool.
22055
22056Used Obscura? Send me an email and let me know your thoughts!
22057#+date: <2025-02-25 Mon 19:20:05>
22058#+title: How I Transferred 5000+ Emails from Proton Mail to Migadu
22059#+description: Detailed instructions describing the process and considerations involved in transferring email data securely from Proton Mail to Migadu email hosting.
22060#+slug: email-migration
22061
22062* The Setup
22063
22064I recently migrated my emails from Proton Mail to Migadu after a failed attempt
22065to get myself into the Proton ecosystem and wanted to detail my process, as it
22066was far more painful than expected.
22067
22068To give some context: I had nearly 5000 messages stored, accounting for around
220692.5 GB of space.
22070
22071Overall, this process would have taken all day had I done it in one sitting, but
22072I decided to break it up and it lasted a couple days before I was able to say that
22073all my messages were stored in my new account.
22074
22075* Exporting Messages
22076
22077To start, I needed to export my messages from Proton Mail. As I am using macOS,
22078I was able to use the [[https://proton.me/support/proton-mail-export-tool][Proton Mail Export Tool]]. However, the downside is that
22079this dumps every single email on your account into a single folder in the =.eml=
22080format. They also export a JSON file for each message, in the case you're
22081importing back into Proton Mail.
22082
22083This means that anything in my Inbox, Sent, Archive, Trash, and user-created
22084folders were all dumped out into a single folder with incomprehensible names.
22085
22086Without a clear path to easily figure out how to re-organize my emails into a
22087new account, I was left a bit annoyed at Proton's export process.
22088
22089* Importing Messages
22090
22091Left with a pile of messages and no way to discern what they were without
22092opening each one, I decided to try and use Thunderbird to import messages into
22093my new Migadu IMAP account.
22094
22095This led to a dead end as my two methods failed:
22096
220971. [[https://addons.thunderbird.net/en-US/thunderbird/addon/importexporttools-ng/][ImportExportTools NG]] does not work with my version of Thunderbird (135).
220982. Manually dragging the =.eml= files onto a folder in Thunderbird worked for
22099 small batches of files, but seemed to lock up if I tried to import more than
22100 a few hundred at a time. It also seemed a bit buggy, as I ended up with many
22101 duplicate, and sometimes triplicate, messages.
22102
22103At this point, I decided to take a step back and use [[https://github.com/djcb/mu][mu]], a command-line utility
22104that would index my files and sync back and forth with Migadu for me.
22105
22106Using my blog post, [[https://cleberg.net/blog/mu4e.html][Email in Doom Emacs with Mu4e on macOS]], (and skipping the
22107mu4e parts) I was able to set up a minimal directory connected to my Migadu IMAP
22108account. Using my terminal, I simply moved all of my messages into the =mu=
22109directory and synchronized the account, and voila, my messages synchronized
22110successfully to the remote server and my other email clients.
22111
22112However, the remaining issue was that I now had all 5000 messages in the Archive
22113folder and needed to figure out how to organize them back into their proper
22114directories.
22115
22116* Organizing Messages Into Folders
22117
22118As with any problem, I used Python as my hammer to fix the problem. I started by
22119creating the directories required in Thunderbird, fetching them with =mbsync= so
22120that they appeared in my =mu= directory, and using Python to organize my
22121messages into the newly-created sub-folders.
22122
22123** Sent Messages
22124
22125I started by organizing my Sent messages. This required checking each file for
22126the =From= header and moving them to the Sent folder.
22127
22128#+begin_src shell
22129cd ~/.maildir/migadu/Archive/cur
22130nano _sent.py
22131#+end_src
22132
22133#+begin_src python
22134# _sent.py
22135import os
22136import glob
22137import shutil
22138
22139# Loop through all files in the current folder
22140for file in glob.glob("*.eml"):
22141 # Create boolean to check if we should move the file
22142 move = False
22143
22144 # Open the current file
22145 f = open(file, 'r')
22146
22147 # For each line in file, find the From header
22148 for line in f:
22149 if line.startswith("From:"):
22150 # If we find ourself, mark the message for move
22151 if "user@example.com" in line:
22152 move = True
22153
22154 # Close the file
22155 f.close()
22156
22157 # Move the file, if marked for move
22158 if move == True:
22159 filepath = os.path.join("/Users/YOUR_USERNAME/.maildir/migadu/Archive/cur/", file)
22160 new_filepath = os.path.join("/Users/YOUR_USERNAME/.maildir/migadu/Sent/cur/", file)
22161 shutil.move(filepath, new_filepath)
22162#+end_src
22163
22164#+begin_src python
22165python3 _sent.py
22166#+end_src
22167
22168The only downside to my current approach is that it was the quick and dirty
22169option, so I re-ran it while editing the =user@example.com= string for each
22170email I wanted to move. If I had wanted to create a more well-defined solution,
22171I would have created an array of addresses to check for and have the =if=
22172statement check against that array.
22173
22174Regardless, I was able to run this with the addresses I wanted to move to the
22175Sent folder and was soon finished.
22176
22177** Archive Sub-Folders
22178
22179Next, I needed to move the remaining ~3000 messages from the Archive folder into
22180dated sub-folders, organized as such:
22181
22182- Archive/2016
22183- ...
22184- Archive/2025
22185
22186
22187To do this, I followed a similar approach as the method above but check for the
22188=Date= header instead of the =From= header.
22189
22190#+begin_src shell
22191cd ~/.maildir/migadu/Archive/cur
22192nano _archive.py
22193#+end_src
22194
22195This approach requires finding the =X-Pm-Date= header and splitting it by the
22196spaces contained within. Once split into a list, we must select the fourth
22197element, as that contains the year which will match the directory we should move
22198it to.
22199
22200For example, the header =X-Pm-Date: Fri, 07 Feb 2025 16:12:08 +0000= will be
22201split into a list as such:
22202
22203#+begin_src python
22204[
22205 'X-Pm-Date:', # 0
22206 'Fri,', # 1
22207 '07', # 2
22208 'Feb', # 3
22209 '2025', # 4
22210 '16:12:08', # 5
22211 '+0000' # 6
22212]
22213#+end_src
22214
22215From this list, we select the fourth element (=2025=) and use that to build the
22216destination path.
22217
22218#+begin_src python
22219# _archive.py
22220import os
22221import glob
22222import shutil
22223
22224# Loop through all files in the sub-folders under Archive
22225for file in glob.glob("*.eml"):
22226 # Create boolean to check if we should move the file
22227 move = False
22228
22229 # Open the current file
22230 f = open(file, 'r')
22231
22232 # For each line in file, find the X-Pm-Date header
22233 for line in f:
22234 if line.startswith("X-Pm-Date"):
22235 # Split the line into a list by spaces;
22236 # Then select the item that contains the year
22237 year = line.split(" ")[4]
22238 move = True
22239
22240 # Close the file
22241 f.close()
22242
22243 # Move the file, if marked for move
22244 if move == True:
22245 filepath = os.path.join("/Users/YOUR_USERNAME/.maildir/migadu/Archive/cur/", file)
22246 new_filepath = os.path.join(f"/Users/YOUR_USERNAME/.maildir/migadu/Archive/{year}/cur/", file)
22247 shutil.move(filepath, new_filepath)
22248#+end_src
22249
22250#+begin_src python
22251python3 _archive.py
22252#+end_src
22253
22254At this point, we've now moved all Sent messages to the Sent box and organized
22255all messages under the Archive folder into their correct sub-folders.
22256
22257If you exported other files, such as files from your Inbox, Trash, etc., you
22258could follow a similar approach and determine the best header or attribute to
22259identify them for further organization.
22260
22261** Synchronize the Results
22262
22263Before synchronizing the files in their new locations, I needed to remove the
22264characters at the end of the file name since =mu= appends IDs to the end of file
22265names.
22266
22267#+begin_src shell
22268cd ~/.maildir/migadu/Archive
22269nano _sync_prep.py
22270#+end_src
22271
22272This script prepares the =Archive= sub-folders for synchronization, but the same
22273concept applies to the Sent folder, except you'd replace =*/cur/*= with =*= if
22274this script were inside the =Sent/cur= directory.
22275
22276#+begin_src python
22277import glob
22278import shutil
22279
22280# Loop through all files in the sub-folders under Archive
22281for file in glob.glob("*/cur/*"):
22282 # Remove the characters at the end of the file name created by =mu=
22283 new_file = file.split(",U=",1)[0]
22284
22285 # Move the file to the new file name
22286 shutil.move(file, new_file)
22287#+end_src
22288
22289#+begin_src shell
22290python3 _sync_prep.py
22291#+end_src
22292
22293Finally, we can synchronize the results.
22294
22295#+begin_src shell
22296mbsync -aV
22297#+end_src
22298
22299* Removing Duplicates
22300
22301My only remaining issue at the time of writing is identifying and removing
22302duplicate messages. I have toyed with simple Python and command-line solutions
22303to identify duplicate files, but could not get them to effectively define all
22304the duplicates found in any specific directory.
22305
22306I've even tried using the [[https://github.com/pkolaczk/fclones][fclones]] utility, to no avail. It seems that something
22307in the Proton export, my manual Thunderbird method attempt, or possible sync
22308issues between Thunderbird -> Migadu <-> mu caused duplicates where content
22309within the message has been modified.
22310
22311Although I now seem to be wasting space and in need of a deduplication tool, I
22312have all of my messages migrated to my new service.
22313#+date: <2025-04-05 Saturday 23:04:54>
22314#+title: Automating GitHub to GitLab Repository Sync
22315#+description: Technical steps for setting up automated synchronization and mirroring of source code repositories from GitHub to GitLab for redundancy and repository management.
22316#+slug: git-mirror
22317
22318This is a short post detailing how I maintained repositories on GitHub and
22319mirrors on GitLab - including both public and private repositories.
22320
22321Since GitLab locks pull-only mirrors behing their Premium and Ultimate tiers, I
22322found a different solution.
22323
22324* Creating Mirrors
22325
22326I'll skip the setup and just hit the bullet points:
22327
22328- I have a plethora of GitHub repositories.
22329- I used GitLab's mass-import functionality to do the initial import of my
22330 repositories from GitHub.
22331- I made sure all of my GitHub repositories were cloned locally in my =~/git=
22332 directory.
22333
22334* Setting up Mirror Connections
22335
22336To start, I navigated to the =~/git= directory, which holds all of the
22337repositories I have on GitHub and created a shell script.
22338
22339#+begin_src shell
22340cd ~/git
22341nano setup_mirrors.sh
22342#+end_src
22343
22344Within this shell script, I created a loop that will open each repository and
22345add both the GitHub and GitLab SSH-style clone URIs to the =origin= remote.
22346
22347#+begin_src shell
22348for repo in */
22349do
22350 cd $repo
22351 git remote set-url origin --push --add git@github.com:ccleberg/${repo%*/}.git
22352 git remote set-url origin --push --add git@gitlab.com:ccleberg/${repo%*/}.git
22353 git remote -v
22354 cd ..
22355done
22356#+end_src
22357
22358Once complete, I created another shell script to open each repository, pull any
22359remote commits, and push local commits to both remotes.
22360
22361#+begin_src shell
22362nano mirror.sh
22363#+end_src
22364
22365#+begin_src shell
22366for repo in */;
22367do
22368 cd $repo
22369 git pull --rebase origin HEAD
22370 git push
22371 cd ..
22372done
22373#+end_src
22374
22375Finally, enable execution of both scripts before moving on.
22376
22377#+begin_src shell
22378chmod +x setup_mirrors.sh
22379chmod +x mirror.sh
22380#+end_src
22381
22382* Initialize Mirrors
22383
22384To use the =setup_mirrors.sh= script above, simply execute it from a terminal.
22385
22386#+begin_src shell
22387./setup_mirrors.sh
22388#+end_src
22389
22390Once complete, each repository /should/ have one fetch URI (GitHub) and two push
22391URIs (GitHub & GitLab). At this point, any =git pull= or =git fetch= commands
22392will pull from GitHub and any =git push= commands will send updates to both
22393GitHub and GitLab.
22394
22395* Schedule Periodic Checks
22396
22397To utilize the =mirror.sh= script from the previous step, let's use crontab.
22398
22399#+begin_src shell
22400crontab -e
22401#+end_src
22402
22403Within crontab, I used the schedule below to ensure the script is executed
22404daily.
22405
22406#+begin_src text
224070 0 * * * /Users/cmc/git/mirror.sh
22408#+end_src
22409
22410This ensures that the =mirror.sh= file is executed daily and will push any local
22411or GitHub commits to GitLab.
22412
22413I have tested this by manually running =mirror.sh= and watching the results.
22414There are edge cases where I will need to intervene and manually resolve merge
22415conflicts, but it's largely autonomous, so I'm happy with the results.
22416#+date: <2025-05-02 Friday 21:10:00>
22417#+title: Running Asahi Linux on Apple M2 MacBook Pro: My Experience
22418#+description: Documented analysis of installation procedure, system performance, and software compatibility for running Asahi Linux on the Apple M2 MacBook Pro 16-inch model.
22419#+slug: asahi-linux
22420
22421* Trying out Asahi Linux
22422
22423While debating if I should find a replacement laptop and get rid of my MacBook
22424Pro, I decided to try [[https://asahilinux.org/][Asahi Linux]].
22425
22426To put the bottom line up front, I love the experience and support for popular
22427features has come a long way since I originally checked out their website a few
22428years ago.
22429
22430From Asahi's website, as seen below, I noticed that almost all targeted features
22431are now supported. This is a great jump from the state it was at in 2022 - 2023
22432when I last checked.
22433
22434#+caption: Asahi Linux Device Support
22435#+attr_html: :alt Device support for Asahi Linux.
22436[[https://img.cleberg.net/blog/20250502-asahi-linux/device_support.webp]]
22437
22438** Installation
22439
22440Luckily, the installation process is as simple as getting started with a single
22441command:
22442
22443#+begin_src shell
22444curl https://alx.sh | sh
22445#+end_src
22446
22447This script will walk you through the standard installation steps to define the
22448size of the Linux partition, define your user and hostname, and give you
22449instructions on how to boot from the new operating system (OS).
22450
22451Once the script completes, it will ask you to completely shut down the device,
22452boot into the Startup Options menu, and follow a handful of prompts to enable
22453your new OS.
22454
22455** Initial Thoughts
22456
22457By default, the installer suggests using the KDE (as does the online community),
22458so I opted to try KDE. Historically, I have always been a Gnome user when
22459utilizing a fully graphical desktop environment (DE), but I'm up for trying
22460something new.
22461
22462KDE on Asahi works wonderfully and supports all of the generic features I'd
22463expect from a vanilla installation:
22464
22465- Snap to tile
22466- Shell and window themes
22467- Graphical updates
22468- Sensible default applications (Dolphin, Firefox, KMail, Konsole, KWrite)
22469- Support for battery percentages, keyboard backlight control, and miscellaneous
22470 sensor controls
22471- Fast and reliable bluetooth adoption
22472- Native game pad support
22473
22474Here's a quick look at the default desktop. In subsequent screenshots, you'll
22475see I started using the WhiteSur theme as it provides a more comfortable layout
22476for macOS users.
22477
22478#+caption: KDE
22479#+attr_html: :alt A screenshot of a terminal running fastfetch and emacs on Asahi Linux.
22480[[https://img.cleberg.net/blog/20250502-asahi-linux/kde.webp]]
22481
22482When diving into the system information panel, we can see that this machine is
22483running Fedora Linux Asahi Remix 42 on an Apple M2 Pro laptop with 16GB of RAM.
22484
22485#+caption: System Information
22486#+attr_html: :alt System information shows Fedora Linux Asahi Remix 42 with KDE 6.3.4.
22487[[https://img.cleberg.net/blog/20250502-asahi-linux/sysinfo.webp]]
22488
22489As I noted above, the first thing I did after installing Asahi and taking a few
22490screenshots was to install and change themes.
22491
22492I prefer the WhiteSur theme, as I used this theme previously on Linux before I
22493switched to macOS. However, I also like themes that incorporate the gruvbox
22494color palette, as well as the Nothing theme that uses a dark black and red color
22495palette.
22496
22497#+caption: Themes
22498#+attr_html: :alt Themes available by default on Asahi.
22499[[https://img.cleberg.net/blog/20250502-asahi-linux/themes.webp]]
22500
22501One of the things I use my laptop for the most is consuming media (music,
22502television shows, movies, etc.), so I had to make sure that my bluetooth devices
22503would connect properly.
22504
22505As you can see in the screenshot below, my AirPods Pro 2 connect seamlessly and
22506are available for use. By going through the normal button-holding process, I was
22507able to pair my AirPods in about 10 seconds.
22508
22509However, I have noticed that Asahi doesn't support some of the advanced AirPod
22510capabilities like pausing media when you remove an AirPod from your ear. The
22511lack of these advanced features doesn't bother me, but some people may be.
22512
22513#+caption: Bluetooth
22514#+attr_html: :alt Bluetooth settings showing AirPods Pro support.
22515[[https://img.cleberg.net/blog/20250502-asahi-linux/bluetooth.webp]]
22516
22517Next, I was able to connect my PS5 controller via USB-C and Asahi picked up the
22518connect instantly in the settings menu. You can move the sticks and press all of
22519the buttons to see if they're recognized within this settings menu.
22520
22521#+caption: Game Pad Settings
22522#+attr_html: :alt Gamepad settings showing PS5 input and button configurations.
22523[[https://img.cleberg.net/blog/20250502-asahi-linux/gamepad_settings.webp]]
22524
22525I had a bit of an issue launching Steam the first time, as I was getting an AVC
22526Denial error from SELinux. To fix this, I had to manually allow the Steam
22527application access to the requested resources:
22528
22529#+begin_src shell
22530sudo ausearch -c 'passt' --raw | sudo audit2allow -M my-passt
22531sudo semodule -X 300 -i my-passt.pp
22532#+end_src
22533
22534While my MacBook only has 16GB of RAM, which seems to be the bare minimum
22535required for the virtualization overhead, I was able to test one or two games
22536before they crashed due to RAM errors.
22537
22538However, I did confirm that my PS5 controller worked natively with these games
22539on Steam! I don't think I will be gaming much on this laptop anyway, but it
22540seems you should have at least the 32GB models if you want to game on Asahi.
22541
22542** Software Support
22543
22544One thing to keep in mind is that Apple silicon chips (M*) are based on the ARM
22545architecture, which means you will need to install software that supports ARM
22546(i.e., ARM64/aarch64).
22547
22548While I have run into a handful of applications that do not have an ARM package
22549available, it is not as difficult as it used to be to find suitable apps for all
22550purposes on ARM.
22551
22552*** Signal Desktop
22553
22554One specific application that has been causing issues is [[https://signal.org/][Signal]]. The download
22555page on their website will prompt you to install their Debian-based package,
22556which does not work on RPM-based systems (or any other distribution except
22557Debian & downstreams, such as Ubuntu).
22558
22559There are a few different websites offering potential solutions, but I settled
22560on the [[https://github.com/kenballus/asahi-signal-desktop/][asahi-signal-desktop]] repository for now.
22561
22562#+begin_quote
22563One thing to note is that this script does not work with all versions of Signal.
22564I tested out 7.53.0 and found that the installer did not work at all.
22565
22566I had to use 7.44.0, as noted in the project's README, for the script to work.
22567#+end_quote
22568
22569The installation process is simple. You can simply clone the repository and run
22570the script:
22571
22572#+begin_src shell
22573git clone https://github.com/kenballus/asahi-signal-desktop/
22574cd asahi-linux-desktop
22575./install_signal.sh 7.44.0
22576#+end_src
22577
22578At this point, Signal should work. However, I have run into constant crashing
22579issues, so it's not a great solution.
22580
22581[[https://github.com/AsamK/signal-cli][signal-cli]] may work, but I have not tested this yet on Asahi. Before I used
22582macOS, I used [[https://github.com/isamert/scli][scli]], which provides a TUI for the signal-cli utility and it
22583worked wonderfully.
22584
22585* Final Thoughts
22586
22587I am still in the early days of testing out Asahi Linux, so I will be sure to
22588come back and update this post occasionally, but I am very happy with the user
22589experience so far. Everything is smooth and I am still able to experience the
22590great hardware of the Macbook with the software of Linux.
22591
22592In addition to the screenshots above, I have been testing out [[https://swaywm.org/][SwayWM]], which is
22593slightly more difficult to configure after being away for Linux for a couple
22594years. Regardless, it's working on Asahi as well and seems to be suitable for
22595daily driving.
22596
22597Overall, I am glad there's continued support for Asahi and will be supporting
22598them through their Open Collective going forward. Cheers to the Asahi team on
22599such a great product.
22600#+date: <2025-05-30 Friday 10:53:28>
22601#+title: Mastering Emerging Tech for IT Audit Career Growth
22602#+description: Instructional content detailing actionable knowledge areas including artificial intelligence, blockchain, cloud computing, DevOps, and automation relevant for audit professionals.
22603#+slug: it-audit-career
22604
22605* Introduction
22606
22607As trends have shown, the auditing and assurance world is increasingly becoming
22608overwhelmed with topics like [[https://kpmg.com/xx/en/what-we-do/services/ai/trusted-ai-framework.html][AI]], [[https://pcaobus.org/Documents/Audits-Involving-Cryptoassets-Spotlight.pdf][Digital Assets]], [[https://kpmg.com/us/en/articles/2023/building-trust-cloud-environments.html][Cloud]], [[https://kpmg.com/us/en/articles/2023/role-of-internal-audit-devops.html][DevOps]], and [[https://kpmg.com/ch/en/insights/technology/audit-transformation.html][Automation]].
22609
22610While these trends are rapidly transforming businesses and industries, audit
22611firms and departments are working hard to ensure they stay aligned with business
22612objectives.
22613
22614Throughout my career, I've seen topics like "AI" evolve from LLM-based projects
22615used by data scientists to everyday tools accessible to the masses and integrated
22616directly into business operations.
22617
22618As an auditor, skills in emerging technologies are an invaluable asset that can
22619translate into massive career opportunities and long-term job security.
22620
22621I have experienced this firsthand as topics like automation, AI, cloud
22622assurance, and DevOps have kept me in a secure position with significant potential
22623for growth and learning.
22624
22625In this post, I'm going to walk through the process of becoming skilled in an
22626emerging technology and how to translate that into a rewarding audit career.
22627
22628* Step 1: Build a Strong Foundation in Audit & Risk
22629
22630To start, I want to note that learning about emerging technologies is only
22631useful if you already have a base in auditing. You can't audit a cloud
22632environment with cloud knowledge alone — you also need to understand the
22633fundamentals of auditing.
22634
22635If you find that you're highly technical already, focus your time on learning
22636audit standards and guidance.
22637
22638You can customize this to any frameworks applicable to your company or industry,
22639but these are generally the baseline for an audit career:
22640
22641- [[https://www.coso.org/Pages/default.aspx][COSO]]
22642- [[https://www.nist.gov/cyberframework][NIST CSF]]
22643- [[https://www.iso.org/isoiec-27001-information-security.html][ISO/IEC 27001]]
22644- [[https://pcaobus.org/][PCAOB]]
22645- [[https://www.aicpa-cima.com/resources/assurance][AICPA]]
22646
22647The consistent theme throughout these frameworks is that audits rely on
22648independent parties who provide consistent documentation, appropriate
22649considerations of risks and responses, scoping, and reporting.
22650
22651One of the easiest ways to get this knowledge is by doing. While it can be
22652challenging, the concept is straightforward. If you are working in a role within
22653a department like internal audit, external audit, risk, compliance, etc., focus
22654on performing your role exceptionally and continuously challenge yourself to
22655learn more about audit and risk each day you work.
22656
22657* Step 2: Learn the Tech That's Changing the Profession
22658
22659Getting to the point of this post, there's a wide range of technologies transforming the audit
22660world. To accelerate career growth and discover a niche you enjoy, I've compiled
22661a short list of topics that are currently (1) growing in demand, (2) have a need
22662for more experts within the audit field, and (3) are expected to remain relevant.
22663
22664- *Automation & Data Analytics*: Alteryx, Python, Power BI, Tableau
22665- *Cloud*: AWS, Azure, GCP, IBM
22666- *Blockchain*: Decentralization, smart contracts, chain of custody
22667- *DevOps*: Azure DevOps, CI/CD, DevSecOps, GitHub, GitLab, Jenkins
22668- *AI/ML*: AI in business, AI in auditing, AI assurance
22669
22670#+BEGIN_QUOTE
22671*Pro Tip*: Tackle one area at a time. You don't need to become an expert, but
22672make sure you learn enough to conceptualize the technologies and logic behind
22673the topics you're auditing.
22674
22675If you focus on one at a time, you can dedicate time to that sole topic to
22676understand it more deeply before moving on to the next.
22677#+END_QUOTE
22678
22679* Step 3: Earn Certifications That Signal Credibility
22680
22681If you don't have much experience, or can't demonstrate your knowledge in a
22682particular area due to job constraints or otherwise, you can utilize courses
22683and certifications to boost your credibility. This is an effective way to
22684establish credibility and demonstrate your qualifications.
22685
22686- [[https://www.isaca.org/credentialing/cisa][CISA (Certified Information Systems Auditor)]]: Core certification for IT
22687 auditors covering systems, controls, governance, and assurance.
22688- [[https://learn.microsoft.com/en-us/certifications/azure-fundamentals/][Microsoft Azure Fundamentals (AZ-900)]] / [[https://aws.amazon.com/certification/certified-cloud-practitioner/][AWS Certified Cloud Practitioner]]: Entry-level
22689 cloud certifications covering core cloud concepts, services, pricing, and
22690 security.
22691- [[https://www.alteryx.com/certification/designer-core][Alteryx Designer Core Certification]]: Validates your ability to build data
22692 prep, blending, and analytics workflows in Alteryx.
22693- [[https://www.coursera.org/professional-certificates/ibm-data-science][IBM Data Science Professional Certificate]] (via Coursera): Teaches data
22694 science fundamentals, Python, SQL, and AI basics; popular for aspiring data
22695 auditors and analysts.
22696- Entry-level technical certifications:
22697 - [[https://pythoninstitute.org/pcep][PCEP (Certified Entry-Level Python Programmer) by Python Institute]]
22698 - [[https://www.edx.org/learn/artificial-intelligence][AI certifications on edX]] covering foundations, governance, and AI ethics.
22699 - [[https://www.blockchain-council.org/certifications/certified-blockchain-professional-expert/][Certified Blockchain Expert]]
22700
22701I have earned the following certifications and have seen great responses
22702throughout my career due to the credibility they provided in these focus areas:
22703
22704- GitLab Certified Git Associate
22705- GitHub Foundations
22706- Alteryx Designer Core Certified
22707- Microsoft Certified: Azure Fundamentals
22708- Certified Information Systems Auditor (CISA)
22709- Certified Entry-Level Python Programmer (PCEP)
22710- Data Science Professional Certificate
22711- Cybersecurity Audit Certificate
22712
22713* Step 4: Join Emerging Tech Projects and Pilots
22714
22715Once you have the knowledge and, if applicable, the credentials to showcase your
22716skills, you need the chance to network and show what you can do.
22717
22718To do this, I recommend joining a group focused on the topic you're passionate
22719about. Normally, this is easiest within your own company. However, sometimes you
22720may need to seek other opportunities in your community or online to find the
22721project that works best for you.
22722
22723- Seek opportunities internally: AI pilots, automation and data analysis
22724 projects, blockchain initiatives.
22725- If they don't exist — propose them.
22726- Suggest automating risk assessments, piloting AI tools, or testing continuous
22727 assurance routines.
22728
22729*Why this matters*: These projects become resume differentiators and position
22730you for leadership roles.
22731
22732* Step 5: Build a Network of Tech-Savvy Auditors & Consultants
22733
22734As a segue from Step 4, ensure that you have a network of like-minded
22735individuals who can challenge you and expose you to new ideas. This helps keep
22736you sharp and open to innovation. Some suggestions include:
22737
22738- Join ISACA, local AI/ML meetups, blockchain working groups.
22739- Engage with emerging tech communities on LinkedIn — share posts, comment on
22740 thought leadership, or publish insights.
22741
22742* Step 6: Stay Curious and Keep Learning
22743
22744Last, but not least, keep learning! While you may feel like you know everything,
22745there's always more to explore. I enjoy revisiting known problems from different
22746perspectives, as it's helped me grow and realize that alternative approaches
22747often yield better results with experience.
22748
22749- The landscape is changing rapidly — stay current on AI regulation, blockchain
22750 security, cloud assurance frameworks, or whatever interests you.
22751- Subscribe to niche newsletters (e.g., The Cybersecurity 202, AI Ethics
22752 Weekly).
22753- Take one course or certification per year to stay sharp.
22754
22755* Closing
22756
22757You don't need to be an engineer or software developer to thrive in technology —
22758but you do need to understand enough to assess risk. With these steps, I have
22759found a wonderful and fulfilling career, and I know you can too.
22760
22761If you have any questions on how to break into tech risk and audit, feel free to
22762reach out over email or anywhere else — I would love to connect and chat!
22763#+date: <2025-06-03 Tue 11:38:40>
22764#+title: Privacy-First iOS Apps for Minimalists
22765#+description: Curated listing of iOS applications prioritized for privacy preservation and data security, targeted at users requiring minimal data exposure.
22766#+slug: private-ios-apps
22767
22768The world is evolving into a privacy nightmare, where our own devices are being
22769used by numerous parties to constantly track and report on our activities. This
22770is especially prevalent on iOS, where platform restrictions leave users without
22771many options to lock down their data.
22772
22773However, there are apps that exist that can help enhance the privacy of an iOS
22774device. The post below details a number of privacy resources, directories, and
22775tools for iOS.
22776
22777**Resource Table**
22778
22779| Name | Type |
22780|----------------------------------------+-----------------------------|
22781| [[https://github.com/pluja/awesome-privacy][Awesome Privacy]] | App Lists & Directories |
22782| [[https://www.privacyguides.org/en/os/ios-overview/][PrivacyGuides.org]] | App Lists & Directories |
22783| [[https://cyberinsider.com/][CyberInsider]] | Guides & Educational |
22784| [[https://ssd.eff.org/module-categories/further-learning][EFF Surveillance Self-Defense]] | Guides & Educational |
22785| [[https://osintframework.com/][OSINT Framework]] | Guides & Educational |
22786| [[https://securityplanner.consumerreports.org/][Security Planner (Consumer Reports)]] | Guides & Educational |
22787| [[https://discuss.privacyguides.net/][PrivacyGuides Forum]] | Community & Discussion |
22788| [[https://www.reddit.com/r/privacy/][r/Privacy]] | Community & Discussion |
22789| [[https://www.reddit.com/r/iosprivacy/][r/iOSPrivacy]] | Community & Discussion |
22790| [[https://tosdr.org/en/][Terms of Service; Didn't Read (ToS;DR)]] | Policy & Transparency Tools |
22791| [[https://browserleaks.com][BrowserLeaks.com]] | Privacy Testing & Tools |
22792| [[https://mullvad.net/check][Mullvad Privacy Check]] | Privacy Testing & Tools |
22793| [[https://support.apple.com/en-us/HT212025][iOS App Privacy Report (Settings)]] | Privacy Testing & Tools |
22794| [[https://privacytests.org/][PrivacyTests.org]] | Privacy Testing & Tools |
22795
22796#+begin_quote
22797*Note*: This list focuses solely on iOS-compatible resources and tools. No
22798Android comparisons here as I have not used Android in many years.
22799#+end_quote
22800
22801* Email
22802
22803First, I like to consider email as my first app on a new device. However, iOS is
22804\extremely\ lacking in this area. Certain options are private, such as Proton
22805Mail and Tuta, but they have certain restrictions and ecosystem lock-in methods
22806that I try to avoid.
22807
22808Canary Mail was a decent option for a while, although a bit mysterious, but they
22809have recently leaned into the AI hype pretty heavily, which is concerning.
22810However, it's still one of the only options for PGP emails on iOS.
22811
22812- [[https://proton.me/mail][Proton Mail]] - One of the more popular private email options available on iOS.
22813 Allows custom domains, but does not allow for IMAP/SMTP usage, so you're
22814 locked into using their apps. Open source.
22815- [[https://tuta.com/][Tuta]] - Tuta also locks you into their clients and they do not allow you to use
22816 custom domains. Open source.
22817- [[https://canarymail.io/][Canary Mail]] - Closed source, so you can't verify anything about what they are
22818 building into the app. However, it's one of the only options for PGP mail on
22819 iOS. Advanced features are locked behind a paywall.
22820
22821Another suggestion is to use a browser-based web client. You can install browser
22822mail clients as progressive web apps (PWAs). For example, I have been using
22823[[https://webmail.migadu.com][webmail.migadu.com]] as a progressive email app for a while now and it works great
22824for my purposes.
22825
22826This allows for access to your email without installing an application that
22827access native APIs or other potential data sources you may be concerned about.
22828This is especially important if you're concerned about allowing permissions to
22829device APIs, non-reproducible buils from the App Store, or mobile device
22830fingerprints.
22831
22832Lastly, [[https://blog.thunderbird.net/2025/05/thunderbird-for-mobile-april-2025-progress-report/][Thunderbird iOS]] is under development. Keep an eye out for an alpha or
22833beta release later in 2025 or early 2026. I have a feeling that once Thunderbird
22834iOS is available, it will easily become the best option for email on iOS.
22835
22836#+begin_quote
22837*I use*: Web-based email on iOS and Thunderbird on my desktop. Once Thunderbird
22838releases for iOS, I will probably use that.
22839#+end_quote
22840
22841* Browsers
22842
22843Your choise of browser is very important if you're concerned with privacy. Any
22844and all links you click will be opened in your default browser, so you need to
22845make sure you choose the right browser and configure it properly. Trackers, ads,
22846fingerprints, and data leaks are constant threats that should be avoided when
22847possible.
22848
22849- [[https://apps.apple.com/us/app/firefox-focus-privacy-browser/id1055677337][Firefox Focus]] - In my opinion, the best option for privacy on iOS.
22850 Automatically blocks trackers, erases data and history upon app close, and
22851 focused on private usage. However, it won't support your bookmarks or keep you
22852 logged into sites long-term.
22853- [[https://duckduckgo.com/app][DuckDuckGo]] - Another good option, built on chromium. Like Focus, it allows you
22854 to clear all data with a button tap.
22855- [[https://onionbrowser.com/][Onion Browser]] & [[https://orbot.app/][Orbot]] - Tor - what can I say? It's been the most popular
22856 privacy browser for ages for a reason and now it's available on iOS. Onion
22857 Browser is a Tor browser and Orbot can proxy any number of iOS apps through
22858 Tor. As with all Tor traffic, it will be slower than "regular" traffic.
22859- [[https://brave.com/][Brave]] - Another chromium-based privacy browser. Can sync with other Brave
22860 browsers via a secure linking process (no account). Fully-featured and great
22861 privacy defaults. There have been some concerns in the past about the company
22862 behind Brave, but I still think it's a decent option for most peoplel.
22863- [[https://www.apple.com/safari/][Safari]] (with caveats) - Great option if the browsers above don't work for you.
22864 Be sure to read PrivacyGuide's [[https://www.privacyguides.org/en/mobile-browsers/#safari-ios][Safari]] section for more information on what you
22865 need to do to lock it down before relying on it full time.
22866
22867#+begin_quote
22868*I use*: Hardened safari in private mode for every day use, and Onion Browser
22869for anonymous browsing.
22870#+end_quote
22871
22872* Messaging
22873
22874Next up are messaging apps. If you have an iPhone, it's a good bet that you will
22875be messaging other people on it. The threats for messaging apps tend to be
22876metadata/data collection from cellular providers, ISPs, and Apple itself. If you
22877want to protect the privacy of your messages, who your messaging, and the
22878metadata around those messages (time, method, location, etc.), you'll need to
22879think about which apps you're using.
22880
22881- [[https://signal.org/][Signal]] - My personal favorite and still the gold standard for secure and
22882 private messaging. Open source, end-to-end encrypted, and runs its own private
22883 push notification infrastructure so Apple can't read your message content. A
22884 phone number is required to sign up, but you can create a username immediately
22885 after signing up and share that with others instead of sharing your phone
22886 number.
22887- [[https://simplex.chat/][SimpleX]] - A decentralized, phone-number-free messaging system. Uses anonymous
22888 message relays and asymmetric keys.
22889- [[https://getsession.org/][Session]] - A fork of Signal's protocol that eliminates phone numbers entirely.
22890 Routes messages through a decentralized onion network (like Tor).
22891- [[https://element.io/][Element]] - Based on the Matrix protocol, offering decentralized, federated
22892 chat. Great for groups and communities, with optional end-to-end encryption.
22893 Other client options are available for Matrix on iOS, as well.
22894
22895#+begin_quote
22896*I use*: Signal for private chats with known people, and Matrix for group chats.
22897#+end_quote
22898
22899* VPNs & Networking
22900
22901Network traffic is where most surveillance happens. Even with encrypted
22902messaging and browsers, your IP address and DNS queries reveal a lot about you.
22903A good VPN or alternative network routing tool masks this, but not all VPNs are
22904trustworthy. Avoid “free” services or those lacking transparency.
22905
22906- [[https://mullvad.net/][Mullvad]] - A no-logs VPN that doesn't require an email or personal info to
22907 create an account. Consistently audited and privacy-focused. Their app is
22908 excellent on iOS.
22909- [[https://www.torproject.org/][Tor]] - Best for anonymity rather than everyday VPN use. Routes your traffic
22910 over three relays, obscuring both source and destination. Slower, but
22911 unmatched for high-risk browsing.
22912- [[https://yggdrasilnetwork.org/installation][Yggdrasil]] - An experimental, encrypted, peer-to-peer mesh network. Less
22913 polished for mobile but useful for hobbyists or building private networks
22914 between devices.
22915
22916If you can't self-host or build your own mesh, Mullvad is hands-down the easiest
22917option here. There are other VPN options available, but I haven't tested them
22918all so I will simply put my vote for Mullvad here and let you research other
22919options if you don't want to use Mullvad.
22920
22921#+begin_quote
22922*I use*: Mullvad for 24/7 usage, and Tor when anonymity is required.
22923#+end_quote
22924
22925* Password Management
22926
22927Weak and reused passwords are still the biggest risks for personal security. A
22928good password manager makes it possible to use strong, unique credentials
22929without memorizing them all.
22930
22931- [[https://bitwarden.com/][Bitwarden]] - Open source, audited, and free to self-host (e.g., Vaultwarden).
22932 The iOS app integrates with system autofill and Face ID.
22933- [[https://keepassium.com/][KeePassium]] - A KeePass-compatible client for iOS. Local database storage,
22934 optional cloud sync, and no external accounts. Excellent if you want full
22935 control over your credential store.
22936- [[https://keepassxc.org/][KeePassXC]] + Syncing Solution - If you already use KeePass on desktop, sync
22937 your database with a secure method like [[https://cryptomator.org/][Cryptomator]]-protected cloud storage,
22938 Syncthing, or local-only transfers.
22939
22940Good passwords matter are extremely important, and these apps give you control
22941over your vault.
22942
22943#+begin_quote
22944*I use*: Bitwarden Families ($40/year) to protect passwords, passkeys, TOTP
22945codes, and secure notes for my family. If I were using a solution solely for
22946myself, I would prefer KeePassXC + Syncthing.
22947#+end_quote
22948
22949* Multi-Factor Authentication (MFA)
22950
22951MFA is essential, but relying on SMS codes or untrusted proprietary apps defeats
22952the point. Use open, local, encrypted authenticators where possible. Also, use
22953passkeys if you can! I prefer passkeys, then TOTP, and then SMS/email, if other
22954options are not possible.
22955
22956- [[https://bitwarden.com/products/authenticator/][Bitwarden Authenticator]] - Integrates with the password manager or works as a
22957 standalone TOTP app. Optional encrypted backups through your Bitwarden
22958 account.
22959- [[https://ente.io/auth/][Ente Auth]] - Open source, end-to-end encrypted TOTP manager. Syncs encrypted
22960 via Ente's infrastructure.
22961- [[https://www.tofuauth.com/][Tofu]] - Minimal, offline-first TOTP app. No cloud, no telemetry.
22962- [[https://raivo-otp.com/][Raivo OTP]] - Open source, native iOS app with secure iCloud backups. Clean
22963 interface.
22964- [[https://apps.apple.com/us/app/otp-auth/id659877384][OTP Auth]] - A longstanding, trusted TOTP manager with encrypted backups and
22965 Apple Watch support. *Not open source.*
22966
22967I recommend pairing one of these with strong passwords and a VPN for everyday
22968security.
22969
22970#+begin_quote
22971*I use*: Bitwarden Authenticator, previously OTP Auth.
22972#+end_quote
22973
22974* Notes & Personal Data
22975
22976If you're storing sensitive personal notes, account details, or journal entries,
22977opt for encrypted, local-first apps.
22978
22979- [[https://beorgapp.com/][Beorg]] - An Org-mode-compatible outliner and task manager for iOS. Great for
22980 Emacs fans and those managing plaintext files.
22981- [[https://obsidian.md/][Obsidian]] - A local Markdown-based notes app. All data stays on your device
22982 unless you opt for Obsidian Sync (or your own setup).
22983- [[https://standardnotes.com/][Standard Notes]] - End-to-end encrypted notes, with cross-platform sync. Good if
22984 you want a straightforward, secure cloud service.
22985- [[https://joplinapp.org/][Joplin]] - Open source, Markdown notes with optional encryption and cloud sync
22986 (Nextcloud, Dropbox, etc).
22987
22988These options help decouple your data from major cloud platforms while keeping
22989notes portable and encrypted.
22990
22991#+begin_quote
22992*I use*: Beorg, since I love org-mode.
22993#+end_quote
22994
22995* Photos & Media
22996
22997If you're using iCloud Photos, your camera roll quietly feeds metadata and
22998images to iCloud by default. If you want to self-host or encrypt your photo
22999library, here's what works on iOS. At a minimum, I suggest disabling iCloud for
23000the Photos app, so the data stays local on your device.
23001
23002- [[https://immich.app/][Immich (self-hosted)]] - Open source, feature-rich, self-hosted photo manager
23003 with facial recognition and live photo support. Requires a home server.
23004- [[https://ente.io/][Ente Photos]] - End-to-end encrypted photo storage with iOS and web apps. Paid
23005 plans, but privacy-first infrastructure.
23006
23007Good privacy photos apps are rare on iOS — these two are the standout options
23008right now.
23009
23010#+begin_quote
23011*I use*: Immich for all photos.
23012#+end_quote
23013
23014* Encryption Utilities
23015
23016If you're handling sensitive files, you need a proper encryption utility to lock
23017them down.
23018
23019- [[https://cryptomator.org/][Cryptomator]] - Open source, encrypted cloud storage vaults. Integrates with
23020 most cloud providers and works locally.
23021- [[https://apps.apple.com/us/app/instant-pgp/id1497433694][Instant PGP]] - PGP key generation, import/export, and encrypted message/file
23022 creation. Handy for old-school secure comms.
23023
23024If you're serious about privacy, encrypted storage and messaging layers like
23025these are essential.
23026
23027#+begin_quote
23028*I use*: Cryptomator when syncing sensitive data over Syncthing. I no longer
23029require Instant PGP since Migadu's webmail client (SnappyMail) supports PGP.
23030#+end_quote
23031
23032* News & Social
23033
23034News and social apps leak all kinds of usage metadata, even when you're just
23035lurking. These tools let you follow content with less exposure.
23036
23037- [[https://netnewswire.com/][NetNewsWire]] - Free, open source RSS reader for iOS.
23038- [[https://www.talklittle.com/three-cheers/][ThreeCheers]] - Privacy-friendly Tildes client for iOS.
23039- [[https://getvoyager.app/][Voyager]] - Clean, independent Lemmy client.
23040- [[https://joinmastodon.org/][Mastodon]] - Federated, open source alternative to Twitter.
23041- [[https://joinpeertube.org/][PeerTube]] - Decentralized video platform, accessible via web or PWA.
23042- [[https://pixelfed.org/][Pixelfed]] - Federated, open source alternative to Instagram.
23043
23044If you're going to be online, at least let it be on your terms.
23045
23046#+begin_quote
23047*I use*: NetNewsWire (via FreshRSS) for RSS feeds, Voyager for Lemmy, and Three
23048 Cheers for Tildes. I have used all of these apps and they are great, but I am
23049 not very active on social sites.
23050#+end_quote
23051
23052* Final Thoughts
23053
23054Whether you just want to improve your privacy in small steps or you're
23055fashioning a tinfoil hat as we speak, moving to privacy-focused services and
23056apps does two things:
23057
230581. It protects your privacy by ensuring that your data is being protected
23059 through the many methods mentioned above; and
230602. It provides money (for paid apps), support (in terms of download count,
23061 reviews, ratings, etc.), and motivation for the developers and companies
23062 behind these apps that provide a privacy haven for users on iOS.
23063
23064Every app you use, every service you sign into, quietly collects and trades your
23065data. iOS makes true anonymity harder than other platforms, but these tools and
23066services give you a fighting chance to keep your data private.
23067#+date: <2025-09-05 Fri 10:21:00>
23068#+title: Crafting a More Human-Centric Design for My Website
23069#+description: Read this post to understand my latest website redesign and the context for the refresh.
23070#+slug: human-website-design
23071
23072#+begin_quote
23073/Please note that this redesign is a work-in-progress and is not complete. The
23074content below describes the beginning of this journey and where I think it may
23075take me next./
23076#+end_quote
23077
23078#+caption: The Old Theme
23079#+attr_html: :alt A screenshot of the old theme for cleberg.net.
23080https://img.cleberg.net/blog/20250905-human-website-design/before.webp
23081
23082#+caption: The New Theme
23083#+attr_html: :alt A screenshot of the new theme for cleberg.net.
23084https://img.cleberg.net/blog/20250905-human-website-design/after.webp
23085
23086* Inspiration
23087
23088I've written numerous times on this blog about minimaliasm, so it shouldn't
23089surprise any readers here that I've redesigned my website again for even more
23090minimalism. However, it's a bit different this time as I've historically
23091designed my website to be very computer design driven (think monospace fonts and
23092sharp contrasting colors), rather than focusing on designing a website for human
23093readers.
23094
23095I felt inspired recently to minimize my website yet again, but I couldn't figure
23096out why I wanted to do so for a while. Finally, I think I figured it out. I have
23097been inspired lately by concepts, such as [[https://en.wikipedia.org/wiki/Wabi-sabi][wabi-sabi]], [[https://en.wikipedia.org/wiki/Shikata_ga_nai][sho ga nai]], [[https://en.wikipedia.org/wiki/Brutalist_architecture][brutalism]],
23098[[https://en.wikipedia.org/wiki/Minimalism][minimalism]], and related concepts.
23099
23100This piece specifically inspired me to cut out elements on my web page that did
23101not add value. In other places, I simply redesigned how that information was
23102presented (e.g., creating an "About" page rather than putting that information
23103on my home screen).
23104
23105However, that was not the only motivation for redesigning my website. I have
23106also recently found an interest in crafting a more human-centric design for my
23107website.
23108
23109In the past, it has largely been a monospace-only, sharp contrast, bold colored
23110blog. As I grown older, I find myself more interested in naturally-occurring
23111color palettes and focusing on legible content.
23112
23113This blog post will explore the decisions I made and why I chose those
23114decisions.
23115
23116* Function
23117
23118The first step in this journey related to the functionality of the site itself.
23119While my site has not used JavaScript for many years, I have focused on the HTML
23120itself.
23121
23122A common question I challenge myself with is: "Does this element serve a
23123function? Is it useful? Does it provide more value than distraction?"
23124
23125With the most recent redesign, I decided to:
23126
23127- Keep the navigation bar to five elements.
23128- Move the "contact" details from the home page to a dedicated =/about/= page.
23129- Minimize the footer to remove the build timestamp, donation links,
23130 security.txt links, and webring links.
23131
23132This left a focus on navigation, my blog posts, and a short list of links to
23133relevant pages on my website.
23134
23135* Colors
23136
23137Previously, I used a very direct color palette for both light and dark modes, as
23138seen below in the CSS snippet. This used sharp colors and relied heavily on
23139"pure" colors without tints or shades.
23140
23141This allowed for plain blank and white contrasting elements, with basic red and
23142blue accent colors for inline code and links, respectively.
23143
23144#+begin_src css
23145:root {
23146 --bg: #fff;
23147 --bg-bright: #eee;
23148 --fg: #111;
23149 --link: #00f;
23150 --code: #f00;
23151 --border: #ccc;
23152}
23153
23154@media (prefers-color-scheme: dark) {
23155 :root {
23156 --bg: #121212;
23157 --bg-bright: #1e1e1e;
23158 --fg: #e0e0e0;
23159 --link: #8ab4f8;
23160 --code: #f28b82;
23161 --border: #333;
23162 }
23163}
23164#+end_src
23165
23166After the update, the focus of the color scheme is focused on a more natual,
23167paper-like effect. The light mode focuses on a yellow/orange-tinted paper-like
23168substance, with a dark red accent and straightfoward design. Likewise, the dark
23169mode scheme uses a charcoal-like color scheme with very simliar accent colors.
23170
23171#+begin_src css
23172:root {
23173 --bg: #EDEDE7;
23174 --bg-bright: #E8D3BE;
23175 --fg: #201E1D;
23176 --link: #810208;
23177 --code: #810208;
23178 --border: #E8D3BE;
23179}
23180
23181@media (prefers-color-scheme: dark) {
23182 :root {
23183 --bg: #201E1D;
23184 --bg-bright: #2F2F2D;
23185 --fg: #EDEDE7;
23186 --link: #D9C2A6;
23187 --code: #D9C2A6;
23188 --border: #2F2F2D;
23189 }
23190}
23191#+end_src
23192
23193I am still working on the color scheme and don't believe I'm done modifying this
23194yet. Expect more changes, as always.
23195
23196* Space
23197
23198While I haven't updated the margins and padding much /yet/, I did make a few
23199changes that affected the spacing of the site. The intent of this redesign is to
23200keep the focus of the reader's eyes on the intended content, as well as creating
23201a comfortable and intentional page layout.
23202
23203That being said, let's dive into the changes I've made so far:
23204
23205- *Font style*: I've updated the font style from a monospace font stack to a
23206 serif font stack. This created a more natual flow across the screen and less
23207 rigid constraints on the area of the fonts.
23208- *Font size*: Related to font style, I naturally had to update the font size as
23209 well. This allows for natural reading breaks for the eyes, spacing between
23210 lines, emphasized and larger headers for visual cues, as well as fewer words
23211 being displayed on screen at once.
23212- *Date format*: I have moved the visible dates on my pages from the ISO
23213 standard (=YYYY-MM-DD=) to a legible, human date format. The new dates
23214 describe the months in full words, as well as the day of the week on post
23215 pages, so that you may read it as you would say it out loud. The underlying
23216 date tagging within the =<date>= element itself still follows the ISO
23217 standard, as that's meant for computers to read.
23218
23219With these changes, I'm pleasantly surprised at the quality improvement when
23220reading the pages on the site.
23221
23222* Looking Forward
23223
23224As I said at the beginning of the post, I'm not done yet. However, I wanted to
23225stop and reflect on the changes so far.
23226
23227I will continue to think about changes, specifically with the following already
23228in mind:
23229
23230- *Color palettes*: The current scheme is good but not great. I'd love to find a
23231 color palette that feels like reading natural paper.
23232- *Emphasized text*: Are links legible enough? Is there enought contrast between
23233 quotes, inline code, links, etc.?
23234- *Spacing*: Font sizes, padding, and potentially reducing content.
23235- *Layout*: Is there a better structure to this document that would allow for
23236 more comfortable reading?
23237
23238With this phase done, I am content and am joyful at the thought of continuing to
23239refine this website. If not for any readers who may or may not even exist, at
23240least for myself.
23241#+date: <2025-09-25 Thu 21:08:00>
23242#+title: Simplifying the Site: A Minimalist Approach
23243#+description: This post details a complete redesign of a blog post, stripping away extraneous CSS and HTML elements to prioritize readability and user experience.
23244#+slug: minimalist-website-redesign
23245
23246I know what you're thinking: "ANOTHER blog post about redesigning the website?"
23247
23248Yes. Another blog post about redesigning the website. I enjoy it, what can I
23249say?
23250
23251I wrote my latest post about redesigning from a monospace-driven,
23252terminal-styled theme to a more natural paper-inspired theme. However, it left a
23253nagging feeling that something was missing. What if I took it further? Was there
23254really any reason that I had any of that CSS at all?
23255
23256#+caption: Old Theme
23257#+attr_html: :alt A screenshot of the old theme for cleberg.net.
23258https://img.cleberg.net/blog/20250905-human-website-design/after.webp
23259
23260#+caption: New Theme
23261#+attr_html: :alt A screenshot of the new theme for cleberg.net.
23262https://img.cleberg.net/blog/20250925-minimalist-website-redesign/minimal.webp
23263
23264* Inspiration
23265
23266I /briefly/ touched on my inspiration in the last post (hint: wabi-sabi, sho ga
23267nai, brutalism, minimalism), but I didn't expand on the /why/ for these
23268concepts.
23269
23270So, why? The minimalismtic approach to web design has resonated with me for a
23271long time. But why minimalism?
23272
23273Officially, "the concept of minimalist architecture is to strip everything down
23274to its essential quality and achieve simplicity."
23275
23276This is a blog. It also has pages dedicated to describing who I am and what I
23277offer. Therefore, the essential quality is the information delivered by the
23278words and the design on your screen. To effectively communicate this
23279information, I tried to understand how online information is best read.
23280
23281With all of that out of the way, let's get on with it.
23282
23283* Core Changes
23284
23285Below is a quick audit of the original style sheet and markup, and the elements
23286I've removed or added. For more in-depth information, refer to the [[https://github.com/ccleberg/cleberg.net/commit/d7978b4ccf4c6c56b1c2888791f73271f91fd12c][git diff]].
23287
23288*HTML*
23289
23290- HTML is now minified
23291- File tags
23292- Links to site pages are moved from the =index= to the =about= page
23293- Wiki was removed entirely
23294- Added a few =<br>= elements to compensate for some of the CSS removals
23295
23296*CSS*
23297
23298For the CSS, I removed /everything/, except the following rules.
23299
23300#+begin_src css
23301body {
23302 max-width: 50em;
23303 margin: 1.5rem;
23304}
23305
23306nav ul {
23307 list-style-type: none;
23308 display: flex;
23309 padding: 0;
23310}
23311
23312nav ul li {
23313 margin-right: 0.5rem;
23314}
23315
23316img {
23317 border-style: none;
23318 width: 100%;
23319}
23320
23321table {
23322 border: 1px solid #111;
23323 border-collapse: collapse;
23324 width: 100%;
23325}
23326
23327pre,
23328pre>code,
23329code {
23330 font-size: 0.9rem;
23331}
23332
23333pre {
23334 border: 1px solid #111;
23335 margin: 0.5rem 0;
23336 padding: 0.5rem;
23337 overflow-x: auto;
23338}
23339
23340time {
23341 display: block;
23342}
23343
23344:not(pre)>code {
23345 color: #f00;
23346}
23347
23348@media (prefers-color-scheme: dark) {
23349 body {
23350 background-color: #222;
23351 color: #eee;
23352 }
23353
23354 a {
23355 color: #add8e6;
23356 }
23357
23358 pre {
23359 border-color: #eee;
23360 }
23361}
23362#+end_src
23363
23364That's it. No framework, no vendor prefixes, no unnecessary selectors.
23365
23366This gave me an almost-pure HTML site. It's freeing. It's liberating. It's
23367comfortable.
23368
23369* Reading Experience Benefits
23370
23371Recent research shows that line length between 45–75 characters maximizes
23372reading speed and comprehension. By constraining the container width to ~50 rem
23373(≈ 100 characters), we stay close to that range while still staying wide enough
23374for my personal preferences. Additionally, a lean stylesheet eliminates *FOUC*
23375(flash‑of‑unstyled‑content) on slower networks, delivering a smoother
23376first‑paint experience.
23377
23378* Final Thoughts
23379
23380I am very pleased with the new theme and will see how it holds up to time. Based
23381on enabling short-term server logs to debug issues (before promptly wiping it to
23382the ether), I notice that most visitors to my site are via RSS anyway. Either
23383way, I hope you reach out if you have an opinion; I always enjoy hearing from
23384you.
23385#+date: <2025-10-03 Fri 4:23:00>
23386#+title: My Privacy Toolkit
23387#+description: Learn about the tools I use to keep my life private and data secure.
23388#+slug: privacy-toolkit
23389
23390* VPN
23391
23392I use a few different VPNs for different purposes. For general use, I use
23393[[https://mullvad.net/en][Mullvad]] due to its private account creation process, support for private payment
23394options, and rock solid consistency. This is by far the most stable and
23395high-speed VPN I have used over the last ~10 years.
23396
23397I also use [[https://njal.la/vpn/][Njalla]] whenever I need to use a service where Mullvad is blocked. I
23398have found that the Njalla IP I was provisioned is not blocked anywhere, so it's
23399useful for software that seemingly has blocked all major VPN providers. There is
23400no client, but they provide OpenVPN and Wireguard support.
23401
23402Finally, I use [[https://airvpn.org/][AirVPN]] on my server's torrent service due to its support for port
23403forwarding. I don't use it for any other purpose, but it works great with
23404Transmission via Docker.
23405
23406* DNS
23407
23408Using a VPN on every single device is not possible. For all of the other items
23409on my network (IoT, Apple TVs, etc.), I enforce [[https://nextdns.io/][NextDNS]] via the [[https://github.com/nextdns/nextdns/wiki/UnifiOS][UnifiOS script]]
23410they provide. This lets me control the DNS on my network, avoid the ISP's
23411default DNS, and enforce blocklists at a LAN level for my home network.
23412
23413* Data Removal
23414
23415I use [[https://www.easyoptouts.com/][Easy Opt Outs]] due to its low price of $20 per year. There are other
23416options that promise more utility, but I find that this service is adequate.
23417Using this has dramatically reduced the spammy, "<person> info here" results on
23418web searches.
23419
23420Regardless, it's a low price to pay to make sure public information is not used
23421against you and reduces the surface of information available via low-effort
23422searches.
23423
23424* Passwords
23425
23426I use [[https://bitwarden.com/][Bitwarden]] due to its use-friendly interface, which is important to the
23427people I share secrets with. Bitwarden allows for seamless username and password
23428generation, multi-device syncing via its own provided service, built-in password
23429strength and duplicate analysis, wide range of device support, and general UI
23430(after its recent UI refresh).
23431
23432If I were only keeping passwords for myself and didn't need to support
23433non-technical users, I would strongly prefer [[https://keepassxc.org/][KeePassXC]]. This would allow me to
23434use Syncthing or another personal syncing solution that would avoid a
23435centralized server that controls my passwords and authentication to access the
23436passwords.
23437
23438* MFA
23439
23440I use iOS, which is limited in options for great MFA apps. If you use Android,
23441just use [[https://getaegis.app/][Aegis]].
23442
23443For iOS, I currently use [[https://bitwarden.com/products/authenticator/][Bitwarden Authenticator]], which is a different app from
23444Bitwarden. You can use this /without/ linking it to a Bitwarden account and use it
23445as a standalone app, if you're not a fan of storing your passwords and MFA
23446methods in the same location. If you don't care, you can just use the MFA fields
23447within Bitwarden itself.
23448
23449The other options on iOS really aren't worth discussing, so I'll leave it here.
23450
23451* Communications
23452
23453For private instant communications, I use [[https://signal.org/][Signal]]. This is currently the gold
23454standard for private direct messages. It is centralized, but it is very stable,
23455provides forward secrecy for messages, and has a proven track record of not
23456spilling data.
23457
23458If we talk about other communication channels, I usually opt for emails
23459encrypted with [[https://gnupg.org/][GPG (PGP)]]. This isn't the best solution, but it does provide a
23460trustworthy encryption method if you need to use email.
23461
23462I am not a fan of the current landscape of private messaging apps outside of
23463Signal (Matrix, Session, etc.), so I'll leave my thoughts here with Signal and
23464PGP.
23465
23466Want real privacy? Talk to someone in person when possible.
23467
23468* Browser Extensions
23469** Firefox (Desktop)
23470
23471[[https://ublockorigin.com/][uBlock Origin]] is a no-brainer. This add-on provides domain-level blocking for
23472resources, allowing you to block or allow specific domains, scripts, styles, and
23473more with a click.
23474
23475If you prefer to avoid mainstream websites and browse via alternative
23476front-ends, I suggest using [[https://libredirect.github.io/][LibRedirect]]. You can configure this add-on to
23477automatically redirect your requests to privacy-respecting alternatives.
23478
23479** Safari (iOS)
23480
23481Since uBlock Origin doesn't work on Safari, I opt for [[https://apps.apple.com/us/app/ublock-origin-lite/id6745342698?platform=iphone][uBlock Origin Lite]], which
23482is a decent alternative.
23483
23484Similar to LibRedirect, I use [[https://apps.apple.com/us/app/privacy-redirect/id1578144015][Privacy Redirect]] on iOS to redirect websites to
23485private alternative front-ends.
23486
23487* Payments
23488
23489If you're just trying to avoid your bank collecting and using information about
23490your purchases, you can try something like [[https://www.privacy.com/][Privacy.com]], which lets you mask the
23491purchase information from your bank.
23492
23493If you're paying in person, I vote for paying with cash. Withdraw a certain
23494amount per paycheck and use for all in-person payments, whenever possible.
23495#+date: <2025-10-06 Mon 13:11:00>
23496#+title: Playing Rocket League on macOS via CrossOver
23497#+description: Learn how to play Rocket League on macOS via CrossOver.
23498#+slug: rocket-league-macos
23499
23500#+caption: Rocket League on macOS
23501#+attr_html: :alt The Rocket League launch screen on macOS.
23502[[https://img.cleberg.net/blog/20251006-rocket-league-macos/launch.webp]]
23503
23504* Intro
23505
23506Rocket League isn’t natively supported on macOS, but you can run it using
23507CrossOver, a compatibility layer that lets Windows applications run on macOS.
23508This guide walks you through installing CrossOver, setting up the Epic Games
23509Store (or Steam), and getting Rocket League up and running.
23510
23511* Install CrossOver
23512
23513To start, we will install CrossOver with [[https://brew.sh][Homebrew]].
23514
23515#+begin_src shell
23516brew install --cask crossover
23517#+end_src
23518
23519Once installed, launch CrossOver and select the Try Now button. CrossOver offers
23520a 14‑day free trial. After the trial, a full license costs $74 USD.
23521
23522* Set Up the Epic Games Store (or Steam)
23523
23524After opening CrossOver, click the Install button and install the Epic Games
23525Store (or Steam, if you have Rocket League on Steam). You will need to agree to
23526the installation prompts that pop up and it should install in just a couple
23527minutes.
23528
23529When the installer finishes, you’ll see a bottle (a sandboxed Windows
23530environment) like the one below:
23531
23532#+caption: Epic Games Store Bottle
23533#+attr_html: :alt The Epic Games Store bottle within CrossOver.
23534[[https://img.cleberg.net/blog/20251006-rocket-league-macos/bottle.webp]]
23535
23536You can now launch the Epic application. Epic will update itself and then
23537present the login screen:
23538
23539#+caption: Epic Updater
23540#+attr_html: :alt The progress bar for updating the Epic Games Store bottle.
23541[[https://img.cleberg.net/blog/20251006-rocket-league-macos/updater.webp]]
23542
23543#+caption: Epic Login
23544#+attr_html: :alt The Epic Login page.
23545[[https://img.cleberg.net/blog/20251006-rocket-league-macos/login.webp]]
23546
23547* Install Rocket League
23548
23549Sign in to your Epic account, locate Rocket League in your library, and click
23550Install. The download and installation process will complete shortly (depending
23551on your internet connection), and then you should be ready to play.
23552
23553* Play the Game
23554
23555Last but not least, launch the game from the Epic Games Store once you install
23556it. It may take a minute or two when launching for the first time.
23557
23558Take a look at the screenshots I was able to capture while playing on my M2
23559MacBook Pro with a PS5 Dualsense Edge controller:
23560
23561#+caption: Main Menu
23562#+attr_html: :alt The Rocket Leageu main menu.
23563[[https://img.cleberg.net/blog/20251006-rocket-league-macos/menu.webp]]
23564
23565#+caption: In-Game
23566#+attr_html: :alt A screenshot of a replay within a Rocket League match.
23567[[https://img.cleberg.net/blog/20251006-rocket-league-macos/game.webp]]
23568
23569* Tips & Troubleshooting
23570
23571Overall, the controller support for my PS5 Dualsense Edge controller worked out
23572of the box and the performance was excellent. However, issues can happen and I
23573gathered some tips, in case they do.
23574
23575- *Game runs slowly*: In CrossOver, edit the bottle’s settings → Graphics → enable
23576 “Use DirectX 11” and increase the allocated RAM.
23577- *Controller not detected*: Install the Windows driver for your controller inside
23578 the bottle (e.g., Xbox 360 driver).
23579- *Epic Store won’t update*: Delete the EpicGamesLauncher cache folder inside the
23580 bottle (C:\Users\<User>\AppData\Local\EpicGamesLauncher\Saved) and restart the
23581 launcher.
23582- *Crashes on launch*: Ensure you have the latest macOS updates and that CrossOver
23583 is up‑to‑date (check “Check for Updates” in the app menu).
23584
23585* Conclusion
23586
23587That's it! It's a simple process that allows you to try out Rocket League on
23588macOS for 14 and determine if you enjoy this solution enough to pay for it.
23589
23590If you know a way to do this for free without paying for CrossOver, please reach
23591out and let me know! I am hesitant to pay $74 simply to play Rocket League, but
23592we shall see.
23593
23594Happy boosting!
23595#+date: <2025-10-24 Fri 19:41:41>
23596#+title: Digital Garden
23597#+description: A post dedicated to digital gardens, an online space for personal knowledge management, creative exploration, and reflection.
23598#+slug: digital-garden
23599
23600* What's a Digital Garden?
23601
23602Before I start describing digital gardens, I want to show a couple examples that
23603are distinct enough to show how varied digital gardens are.
23604
23605The first is a website ([[https://avas.space][ava's space]]) and the second is a private collection of
23606notes, using [[https://www.orgroam.com/][org-roam]].
23607
23608#+caption: avas.space
23609#+attr_html: :alt Ava's Space, a digital garden and website by Ava.
23610[[https://img.cleberg.net/blog/20251024-digital-garden/ava.webp]]
23611
23612#+caption: Org Roam
23613#+attr_html: :alt A screenshot of an org-roam digital garden and collection of note files.
23614[[https://img.cleberg.net/blog/20251024-digital-garden/orgroam.webp]]
23615
23616A digital garden is a personal space, just for you. It could serve any number of
23617purposes, which I'll get into later, but the key is that it serves a specific
23618purpose for yourself.
23619
23620The primary features of a digital garden, as opposed to a personal website or
23621blog, is that it is not inherently structured in a strict manner, it may follow
23622a free form structure, it may look like a work-in-progress wiki.
23623
23624At the end of the day, it defines a web page where any type of content exists
23625and the author takes time and care to prune it in a specific manner.
23626
23627* Historical Context
23628
23629A lot of people talk about [[https://www.eastgate.com/garden/Enter.html][Hypertext Gardens: Delightful Vistas]] by Mark
23630Bernstein as the first instance of this phrase on the internet. And while this
23631may be true, I want to take a step further back.
23632
23633Have you heard of a [[https://en.wikipedia.org/wiki/Memex][Memex]]? This hypothetical device was described in Vannevar
23634Bush's 1945 article "As We May Think". He envisioned a device that would be able
23635to store all of a person's information for them - books, records, and
23636communications. The person could then reference the Memex for any information
23637and be guided to the correct spot. Essentially, it would the 1940's version of a
23638mind mapping or personal knowledge system.
23639
23640From the Memex to Hypertext Gardens to mind maps to other knowledge management
23641systems, humans seem intent on constantly reinventing the art of information
23642organization.
23643
23644The modern web is the result of 30+ years of redesigning the web to streamline
23645information. However, in this refinement, we've lost the art and beauty of
23646personal websites where people would dump all of the information relevant to
23647themselves.
23648
23649Digital Gardens are a movement to bring these personal websites back and present
23650them in a personalized fashion.
23651
23652* Common Uses
23653
23654Before I get into /how/ to build a digital garden, I want to touch on the
23655potential uses. Everyone has their own reason for building a website and digital
23656gardens are no exception.
23657
23658You may want to build a digital garden for any number of reasons:
23659
23660- Knowledge management
23661- Creative exploration
23662- Collaborative learning
23663- Personal reflection
23664- Artistic expression
23665- Data hoarding
23666- Social interaction
23667
23668* How to Build Your Own Garden
23669
23670If you do want to create your own digital garden, you can get started with a few
23671easy steps:
23672
236731. *Choose a platform*: You can host a website for free on platforms like GitHub
23674 or use a more note-taking platform like Notion or Obsidian.
236752. *Set Up the Structure*: Decide how you want to organize your content. Remember
23676 the purpose of a digital garden! Plant all of the information you care about
23677 and then work to prune and clean them up as they grow.
236783. *Start with Your Interests*: A digital garden should reflect yourself. Find
23679 your passions and start from there.
236804. *Regular Updates*: A garden won't grow if you ignore it. Remember to check in
23681 and think if you want to add more, prune things out, or move things around.
236825. *Interlinking Ideas*: Like plants and ecosystems, a digital garden should link
23683 together. You may use tags or links to connect items on your own site or
23684 perhaps includes links or lists to other gardens you enjoy.
23685
23686By far the most important piece here is regular updates. You can always fix or
23687change things, but the garden won't grow if you don't contribute to it.
23688
23689* Additional Suggestions
23690
23691Digital gardens are such an easy way to manage your knowledge and unleash your
23692creativity. Instead of sticking to the usual blog format, you get a space where
23693your ideas can grow and change over time. Whether you want to document what
23694you’re learning, share thoughts with others, or just reflect on your journey, a
23695digital garden can be the perfect tool.
23696
23697I strongly encourage you to create your own today, even if it starts as private
23698notes instead of a website! Nothing is ever too small and the point of a garden
23699isn't to be perfect, but to be a place where you can experiment and have fun.
23700#+date: <2025-11-02 Sun 10:07:14>
23701#+title: Using aerc on macOS
23702#+description: Learn how I use aerc, a TUI email client, on macOS.
23703#+slug: aerc-macos
23704
23705#+caption: aerc-mail
23706#+attr_html: :alt An image of aerc with an email opened in the reader window.
23707https://img.cleberg.net/blog/20251102-aerc-macos/read.webp
23708
23709Shout-out to [[https://baty.net/posts/2025/10/using-aerc-for-email/][Jack Baty]] for inspiring me to write about my =aerc-mail= client
23710after chatting about it over email recently!
23711
23712* Overview
23713
23714[[https://aerc-mail.org/][aerc]] is a text user interface (TUI) email client with intuitive defaults, easy
23715setup, and hassle-free operations. This post will walk through my personal
23716configuration for aerc on macOS.
23717
23718* Installation
23719
23720Since I am on macOS, I opted to install aerc from Homebrew. There are build
23721options for additional modules if you build from source, but I am okay with the
23722default options.
23723
23724You can view the information about the package prior to installing it, if you
23725wish:
23726
23727#+begin_src sh
23728brew info aerc
23729#+end_src
23730
23731#+begin_src sh
23732==> aerc: stable 0.21.0 (bottled), HEAD
23733Email client that runs in your terminal
23734https://aerc-mail.org/
23735Installed
23736/opt/homebrew/Cellar/aerc/0.21.0 (48 files, 23MB) *
23737 Poured from bottle using the formulae.brew.sh API on 2025-10-05 at 19:51:31
23738From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/a/aerc.rb
23739License: MIT
23740==> Dependencies
23741Build: go ✘, scdoc ✘
23742Required: notmuch ✔
23743==> Options
23744--HEAD
23745 Install HEAD version
23746==> Downloading https://formulae.brew.sh/api/formula/aerc.json
23747==> Analytics
23748install: 136 (30 days), 560 (90 days), 1,772 (365 days)
23749install-on-request: 136 (30 days), 560 (90 days), 1,772 (365 days)
23750build-error: 3 (30 days)
23751#+end_src
23752
23753Once you confirm you want to install this package, run the following:
23754
23755#+begin_src sh
23756brew install aerc
23757#+end_src
23758
23759* First-Time Wizard
23760
23761When you run aerc for the first time, an IMAP & SMTP wizard will walk you
23762through a basic setup for your email account(s).
23763
23764On macOS, aerc will save your configuration files in the following location:
23765=~/Library/Preferences/aerc=.
23766
23767I tried to create a =~/.config/aerc= directory, but the Library directory above
23768took precedence. If you truly need to keep your files in =~/.config=, you could
23769probably symlink that to the Library location. However, I accepted my fate and
23770will just edit the files within the Library directory.
23771
23772* Configuration
23773
23774Once you have installed and setup aerc for the first time, you can configure the
23775client and account settings.
23776
23777As noted above, I will be working within the Library directory that aerc
23778defaults to when installed via brew.
23779
23780#+begin_src sh
23781cd ~/Library/Preferences/aerc
23782#+end_src
23783
23784This directory should contain the following files. You can open the
23785=accounts.conf= file to confirm it contains the information you input during the
23786wizard process.
23787
23788| File | Purpose |
23789|-----------------+-------------------------------------------------------------|
23790| =accounts.conf= | Contains account-specific settings for IMAP & SMTP accounts |
23791| =aerc.conf= | Contains program-wide settings |
23792| =binds.conf= | Contains key bindings for aerc |
23793
23794I'll share my personal configuration, which is close to the defaults that
23795existed at the time of writing this post.
23796
23797=accounts.conf=
23798
23799This is the information from the wizard. The only further customization I
23800provided is the default signature file to be appended to each message I compose.
23801
23802#+begin_src conf
23803[Personal]
23804source = imaps://hello%40cleberg.net:PASSWORD@imap.migadu.com
23805outgoing = smtps://hello%40cleberg.net:PASSWORD@smtp.migadu.com
23806default = INBOX
23807from = Christian Cleberg <hello@cleberg.net>
23808copy-to = Sent
23809cache-headers = true
23810signature-file = /Users/cmc/Library/Preferences/aerc/sigs/default
23811#+end_src
23812
23813=sigs/default=
23814
23815Any content within this file will be appended to my messages, as noted above.
23816
23817#+begin_src text
23818Christian Cleberg <hello@cleberg.net>
23819GPG: 3917 973F B159 BBB8 6194 5385 69451A51 7AC0 CB37
23820#+end_src
23821
23822=aerc.conf=
23823
23824This contains quite a few customizations for my preferences related to
23825downloads, encryption, mouse controls, editor, and message composition.
23826
23827#+begin_src conf
23828[general]
23829# Prefer downloads to be saved to the Downloads folder
23830default-save-path=~/Downloads
23831# Use GPG for PGP encryption
23832pgp-provider=gpg
23833
23834[ui]
23835# Enable mouse controls for scrolling, paging, etc.
23836mouse-enabled=true
23837
23838[statusline]
23839[viewer]
23840# Use emacs instead of vi
23841pager=/usr/local/bin/emacs_viewer
23842
23843[compose]
23844# Use emacs instead of vi
23845editor=emacs
23846# Add Cc as a default field to view each time I compose a message
23847header-layout=To|From,Cc,Subject
23848# Allow me to edit headers when composing (so I can customize From address)
23849edit-headers=true
23850# Warn me if I mentioned these words but don't attach a file
23851empty-subject-warning=true
23852no-attachment-warning=^[^>]*attach(ed|ment)
23853
23854[multipart-converters]
23855
23856[filters]
23857text/plain=colorize
23858text/calendar=calendar
23859message/delivery-status=colorize
23860message/rfc822=colorize
23861text/html=! html
23862.headers=colorize
23863
23864[openers]
23865[hooks]
23866[templates]
23867#+end_src
23868
23869#+begin_quote
23870*IMPORTANT*: If you use the =pager= option above that I'm using, you will need
23871 to create the =emacs_viewer= file as well.
23872#+end_quote
23873
23874You can create the file following my process below, which will create an
23875executable in the location specified in my =aerc.conf= file. I put it in
23876=/usr/local/bin=, but you can put it anywhere as long as it exists in the file
23877path specific within your configuration file.
23878
23879#+begin_src sh
23880sudo nano /usr/local/bin/emacs_viewer
23881#+end_src
23882
23883Paste this inside the file:
23884
23885#+begin_src sh
23886#!/bin/bash
23887emacs -nw - < "$1"
23888#+end_src
23889
23890#+begin_src sh
23891sudo chmod +x /usr/local/bin/emacs_viewer
23892#+end_src
23893
23894=binds.conf=
23895
23896Finally, here are the key bindings. I did not change these, but I'm showing them
23897here in case you have an issue and want to see the defaults.
23898
23899#+begin_src conf
23900# Binds are of the form <key sequence> = <command to run>
23901# To use '=' in a key sequence, substitute it with "Eq": "<Ctrl+Eq>"
23902# If you wish to bind #, you can wrap the key sequence in quotes: "#" = quit
23903<C-p> = :prev-tab<Enter>
23904<C-PgUp> = :prev-tab<Enter>
23905<C-n> = :next-tab<Enter>
23906<C-PgDn> = :next-tab<Enter>
23907\[t = :prev-tab<Enter>
23908\]t = :next-tab<Enter>
23909<C-t> = :term<Enter>
23910? = :help keys<Enter>
23911<C-c> = :prompt 'Quit?' quit<Enter>
23912<C-q> = :prompt 'Quit?' quit<Enter>
23913<C-z> = :suspend<Enter>
23914
23915[messages]
23916q = :prompt 'Quit?' quit<Enter>
23917
23918j = :next<Enter>
23919<Down> = :next<Enter>
23920<C-d> = :next 50%<Enter>
23921<C-f> = :next 100%<Enter>
23922<PgDn> = :next 100%<Enter>
23923
23924k = :prev<Enter>
23925<Up> = :prev<Enter>
23926<C-u> = :prev 50%<Enter>
23927<C-b> = :prev 100%<Enter>
23928<PgUp> = :prev 100%<Enter>
23929g = :select 0<Enter>
23930G = :select -1<Enter>
23931
23932J = :next-folder<Enter>
23933<C-Down> = :next-folder<Enter>
23934K = :prev-folder<Enter>
23935<C-Up> = :prev-folder<Enter>
23936H = :collapse-folder<Enter>
23937<C-Left> = :collapse-folder<Enter>
23938L = :expand-folder<Enter>
23939<C-Right> = :expand-folder<Enter>
23940
23941v = :mark -t<Enter>
23942<Space> = :mark -t<Enter>:next<Enter>
23943V = :mark -v<Enter>
23944
23945T = :toggle-threads<Enter>
23946zc = :fold<Enter>
23947zo = :unfold<Enter>
23948za = :fold -t<Enter>
23949zM = :fold -a<Enter>
23950zR = :unfold -a<Enter>
23951<tab> = :fold -t<Enter>
23952
23953zz = :align center<Enter>
23954zt = :align top<Enter>
23955zb = :align bottom<Enter>
23956
23957<Enter> = :view<Enter>
23958d = :choose -o y 'Really delete this message' delete-message<Enter>
23959D = :delete<Enter>
23960a = :archive flat<Enter>
23961A = :unmark -a<Enter>:mark -T<Enter>:archive flat<Enter>
23962
23963C = :compose<Enter>
23964m = :compose<Enter>
23965
23966b = :bounce<space>
23967
23968rr = :reply -a<Enter>
23969rq = :reply -aq<Enter>
23970Rr = :reply<Enter>
23971Rq = :reply -q<Enter>
23972
23973c = :cf<space>
23974$ = :term<space>
23975! = :term<space>
23976| = :pipe<space>
23977
23978/ = :search<space>
23979\ = :filter<space>
23980n = :next-result<Enter>
23981N = :prev-result<Enter>
23982<Esc> = :clear<Enter>
23983
23984s = :split<Enter>
23985S = :vsplit<Enter>
23986
23987pl = :patch list<Enter>
23988pa = :patch apply <Tab>
23989pd = :patch drop <Tab>
23990pb = :patch rebase<Enter>
23991pt = :patch term<Enter>
23992ps = :patch switch <Tab>
23993
23994[messages:folder=Drafts]
23995<Enter> = :recall<Enter>
23996
23997[view]
23998/ = :toggle-key-passthrough<Enter>/
23999q = :close<Enter>
24000O = :open<Enter>
24001o = :open<Enter>
24002S = :save<space>
24003| = :pipe<space>
24004D = :delete<Enter>
24005A = :archive flat<Enter>
24006
24007<C-y> = :copy-link <space>
24008<C-l> = :open-link <space>
24009
24010f = :forward<Enter>
24011rr = :reply -a<Enter>
24012rq = :reply -aq<Enter>
24013Rr = :reply<Enter>
24014Rq = :reply -q<Enter>
24015
24016H = :toggle-headers<Enter>
24017<C-k> = :prev-part<Enter>
24018<C-Up> = :prev-part<Enter>
24019<C-j> = :next-part<Enter>
24020<C-Down> = :next-part<Enter>
24021J = :next<Enter>
24022<C-Right> = :next<Enter>
24023K = :prev<Enter>
24024<C-Left> = :prev<Enter>
24025
24026[view::passthrough]
24027$noinherit = true
24028$ex = <C-x>
24029<Esc> = :toggle-key-passthrough<Enter>
24030
24031[compose]
24032# Keybindings used when the embedded terminal is not selected in the compose
24033# view
24034$noinherit = true
24035$ex = <C-x>
24036$complete = <C-o>
24037<C-k> = :prev-field<Enter>
24038<C-Up> = :prev-field<Enter>
24039<C-j> = :next-field<Enter>
24040<C-Down> = :next-field<Enter>
24041<A-p> = :switch-account -p<Enter>
24042<C-Left> = :switch-account -p<Enter>
24043<A-n> = :switch-account -n<Enter>
24044<C-Right> = :switch-account -n<Enter>
24045<tab> = :next-field<Enter>
24046<backtab> = :prev-field<Enter>
24047<C-p> = :prev-tab<Enter>
24048<C-PgUp> = :prev-tab<Enter>
24049<C-n> = :next-tab<Enter>
24050<C-PgDn> = :next-tab<Enter>
24051
24052[compose::editor]
24053# Keybindings used when the embedded terminal is selected in the compose view
24054$noinherit = true
24055$ex = <C-x>
24056<C-k> = :prev-field<Enter>
24057<C-Up> = :prev-field<Enter>
24058<C-j> = :next-field<Enter>
24059<C-Down> = :next-field<Enter>
24060<C-p> = :prev-tab<Enter>
24061<C-PgUp> = :prev-tab<Enter>
24062<C-n> = :next-tab<Enter>
24063<C-PgDn> = :next-tab<Enter>
24064
24065[compose::review]
24066# Keybindings used when reviewing a message to be sent
24067# Inline comments are used as descriptions on the review screen
24068y = :send<Enter> # Send
24069n = :abort<Enter> # Abort (discard message, no confirmation)
24070s = :sign<Enter> # Toggle signing
24071x = :encrypt<Enter> # Toggle encryption to all recipients
24072v = :preview<Enter> # Preview message
24073p = :postpone<Enter> # Postpone
24074q = :choose -o d discard abort -o p postpone postpone<Enter> # Abort or postpone
24075e = :edit<Enter> # Edit (body and headers)
24076a = :attach<space> # Add attachment
24077d = :detach<space> # Remove attachment
24078
24079[terminal]
24080$noinherit = true
24081$ex = <C-x>
24082
24083<C-p> = :prev-tab<Enter>
24084<C-n> = :next-tab<Enter>
24085<C-PgUp> = :prev-tab<Enter>
24086<C-PgDn> = :next-tab<Enter>
24087#+end_src
24088
24089* Operation
24090
24091Now that I have configured the program to my liking, I can run =aerc= in my
24092terminal and see my changes reflected!
24093
24094Personally, I keep aerc running in a terminal at all times, since it has the
24095operation to ring a bell when a new message arrives.
24096
24097#+caption: Notification
24098#+attr_html: :alt An image of the bell notification on macOS from aerc.
24099[[https://img.cleberg.net/blog/20251102-aerc-macos/notification.webp]]
24100
24101The mailbox views and composition screens are fairly standard, but it does a
24102great job at displaying information effectively.
24103
24104#+caption: Inbox
24105#+attr_html: :alt An image of the aerc inbox view.
24106[[https://img.cleberg.net/blog/20251102-aerc-macos/inbox.webp]]
24107
24108#+caption: Compose Message
24109#+attr_html: :alt An image of Emacs being used as the composition window for aerc.
24110[[https://img.cleberg.net/blog/20251102-aerc-macos/compose.webp]]
24111
24112#+caption: Send Message
24113#+attr_html: :alt An image of the aerc commands to send an email.
24114[[https://img.cleberg.net/blog/20251102-aerc-macos/send.webp]]
24115
24116#+caption: Read Message
24117#+attr_html: :alt An image of an email opened in aerc's reading window.
24118[[https://img.cleberg.net/blog/20251102-aerc-macos/read.webp]]
24119
24120All in all, I love aerc and far prefer it to my previous client (neomutt). In
24121certain cases, Thunderbird is more convenient, but I don't find myself opening
24122it at all lately. I will definitely update this post in the future if I find
24123anything extraordinary to add about aerc.
24124#+date: <2025-11-14 Fri 19:46:30>
24125#+title: Striving for 100% WCAG 2 Compliance
24126#+description: Read on to learn more about how I've ensured my humble site is compliant with WCAG 2.
24127#+slug: wcag
24128
24129* WCAG 2
24130
24131What are the Web Content Accessibility Guidelines (WCAG) 2 requirements?
24132
24133While the full list of WCAG requirements may seem intimidating, it's actually
24134straightforward for a static site like =cleberg.net= if you are intentional
24135about design. Luckily, they offer a [[https://www.w3.org/WAI/WCAG22/quickref/][Quick Reference]] guide that is much easier to
24136parse than the full specification.
24137
24138So, while it wasn't onerous to push myself over the 100% threshold, I wanted to
24139write about how my current website design contributed to my compliance rate and
24140why WCAG is important.
24141
24142* Why WCAG?
24143
24144Before I dive into the specifics of how my website complies with these
24145requirements, I wanted to touch on the importance of WCAG itself.
24146
24147There are three major reasons I wanted to implement the WCAG requirements on my
24148website:
24149
241501. *Usability*: The main reason, which I've covered in numerous other blog posts
24151 about minimalism and design, is usability. I believe that our tech should
24152 always serve a purpose and I believe that the design of a tool should
24153 encapsulate that idea. Therefore, designing a website with intent leads to
24154 better usability, both by those with and without disabilities.
241552. *Kindness*: What's better than designing a tool accessible by /everyone/?
24156 Personally, I love the idea that no matter who visits my website, they should
24157 experience the same content and same utility intended by the website.
241583. *Future-Proofing*: Technology, include internet tools, are changing
24159 constantly. When a website is well-designed, reachable by all audiences, and
24160 utilitarian, it will be able to last longer or even "decay gracefully".
24161
24162* Implementing WCAG On My Website
24163
24164Since I do not implement JavaScript, forms, or multimedia content on my website,
24165the list of items to implement was short.
24166
24167I'll go through some of the items that I considered during my manual
24168verification of the site. If an item is not on this list, it is either *not
24169applicable* or the native browser functionality covers the requirement and I do
24170not implement any custom element that would require verification.
24171
24172- [1.1]: Text alternatives for non-text content (i.e., images)
24173 - For each image within my website, I made sure that alt-text was
24174 available.
24175- [1.3.1]: Use Accessible Rich Internet Applications (ARIA) landmarks to
24176 identify regions of a page
24177 - ARIA landmarks are available for the navigation menu on my website,
24178 which is the only element I use which is recommended by WCAG to have
24179 an ARIA role.
24180- [1.3.1]: Use semantic HTML elements to identify regions of a page
24181 - I have always used semantic HTML, but I reviewed my HTML templates to
24182 ensure I had coverage. In short, I use elements like =<nav>=,
24183 =<main>=, =<article>=, and =<footer>= instead of =<div>= elements.
24184- [1.4.1]: Use a color with a relative luminance difference of 3:1 or greater
24185 - When choosing the color palettes for this site, I ensured that all
24186 were at least AA compliant and in most cases, AAA compliant.
24187- [1.4.4]: Use em (or rem) units for font sizes
24188 - All sizing on this site uses =rem= sizing.
24189- [2.4.1]: Create links to skip blocks of repeated material
24190 - I created a =skip-to-content= link hidden at the top each web page
24191 that allows keyboard or screen reader users to skip the navigation
24192 block.
24193- [2.4.6]: Provide descriptive headings
24194 - Each page contains hierarchical headers starting at =<h1>=.
24195- [3.1.1]: Use the language attribute on the HTML element
24196 - Each pages is defined with =<html lang=en-us>=.
24197- [3.1.4]: Provide an expansion of an abbreviation on the first occurrence
24198 - To the best of my ability, I have been reviewing all historical posts
24199 on this site and expanding abbreviations upon first use.
24200- [3.2]: Make web pages appear and operate in predictable ways
24201 - This, and many other requirements, are resolved by a combination of
24202 (1) native browser functionality which works as expected since I am
24203 using semantic HTML, and (2) my use of a structure outline and logical
24204 content flow on each page of the site.
24205
24206* Results
24207
24208#+caption: IBM Equal Access Accessibility Checker
24209#+attr_html: :alt Results from the IBM Equal Access Accessibility Checker, showing 94% confirmed compliance, with the remaining manually confirmed.
24210[[https://img.cleberg.net/blog/20251113-wcag/accessibility_assessment.webp]]
24211
24212To confirm compliance, I chose /not/ to use one of the "free" accessibility
24213checker websites, since they often only test one page at a time, take a while to
24214run, and I just generally don't enjoy scanning my website with random services.
24215
24216Instead, I used the [[https://www.ibm.com/able/toolkit/tools/#develop][IBM Equal Access Accessibility Checker]] extension on Firefox.
24217This allowed me to scan each page I wanted, stored my scans, and provided
24218in-depth results with links to guidance and suggestions for fixing each issue.
24219
24220This was great as it showed me a handful of issues I hadn't caught previously.
24221However, the image above shows that my site is 94% compliant through *automated*
24222checks and I manually verified the remaining 6% of checks.
24223
24224I performed these scans for all top-level pages on my website (home, about,
24225blog, services, about, salary, and now), as well as the three latest blog posts
24226to confirm compliance.
24227
24228I'd love to test 100% of every page of my website one day and will continue to
24229strive to enhance my compliance with ARIA and WCAG.
24230#+date: <2025-11-23 17:43:57>
24231#+title: Practical Uses for AI in IT Audit
24232#+description: Learn how I'm currently using AI in my role in Technology Assurance (i.e., IT Audit) at KPMG.
24233#+slug: it-audit-ai
24234
24235* My AI Toolkit
24236
24237As others in the Big 4 will probably tell you, AI is a strategic tool that
24238allows for a far more efficient and impact audit. However, the key is
24239understanding when to use it, how to use it, and how to translate the output
24240from your AI into actions.
24241
24242This post will walk you through how I personally leverage AI in my role.
24243
24244Let's start by stepping through the tools I have at my disposal.
24245
24246** Gemini Enterprise (formerly Agentspace)
24247
24248First and foremost is [[https://cloud.google.com/gemini-enterprise][Gemini Enterprise]], which has a large context window, is
24249connected to the Enterprise's internal documentation and websites, and allows
24250for specialized use of custom agents, [[https://notebooklm.google/][NotebookLM]], and more.
24251
24252I use Gemini for a handful of specific use-cases, notably:
24253
242541. NotebookLM notebooks for team-based usage. For example, you can build a
24255 notebook for a specific client (using public and private information about
24256 that client) and then chat with the data in the notebook, generate an audio
24257 overview, and save documents, such as mind maps or study guides, for future
24258 use.
24259 - I love this because it provides a "run once and save" functionality, where
24260 I can generate things like an audio overview or document, save it, and then
24261 my team can reference it without re-running the prompt or even needing to
24262 open the notebook, if they download it.
242632. Custom Agents for customized personas that benefit from the private
24264 information that Gemini has access to, such as internal SharePoint sites.
24265 - For example, I have a custom agent designed to operate as an IT Audit
24266 Assistant that can help you ask questions that can be answered by the
24267 company's news, documentation sites, and more (e.g., "I'm testing logical
24268 access for AWS and want to know if anyone has created a solution or
24269 guidance for XYZ.").
24270
24271** Microsoft Copilot
24272
24273Likely the most common AI in use at company's today, due to its integration with
24274every Microsoft product, Copilot offers the best AI solution for productivity.
24275However, it's usage is likely also the most volatile due to the large gap in
24276skill sets between new-hires and senior employees.
24277
24278For associates and other early-career employees, I suggest that they use Copilot
24279to perform two primary tasks:
24280
242811. Check their tone, such as making an email or messages more professionalism,
24282 softer, or perhaps to analyze the conversation up to that point to understand
24283 how to respond better. This is something that /should/ come naturally to
24284 senior employees, but is often a learning curve for associates.
242852. For written documentation, such as guides or presentations, use Copilot and
24286 provide it with ample information on the goal of the documentation in order
24287 for Copilot to suggest an outline, gaps in the writer's thinking, or areas
24288 for further research before writing the document. The key here is to use
24289 Copilot to improve your thinking and not to replace it. It shouldn't generate
24290 the content for you, it should remove the tedium of creating boilerplate
24291 structures or to design the structure of a document.
24292
24293** Clara AI
24294
24295[[https://kpmg.com/us/en/capabilities-services/audit-services/kpmg-clara.html][Clara AI]] is KPMG's audit-based AI platform, known as the "KPMG Clara Smart Audit
24296Platform". The major benefit to Clara AI is that it lives within a sandboxed
24297environment, running on white-labelled models, fine-tuned on firm-specific
24298methodologies, and built directly into KPMG's audit tools.
24299
24300As noted in KPMG's Clara AI page linked above, Clara AI is built directly into
24301the Clara audit platform in order to provide systematic risk assessments, drive
24302consistent and efficient audits by providing all audit teams with relevant
24303information for their audit, and connects to other tools (such as KPMG Client
24304Collaboration, where evidence is requested and provided).
24305
24306I *absolutely love this tool*, as it solves some of the bigger problems for
24307auditors at large firms:
24308
243091. Connecting younger staff to the firm's guidance and regulatory material,
24310 lowering the barrier to auditors trying to learn which pieces of regulation
24311 apply and when they apply.
243122. As you can imagine, the audit process for a firm of KPMG's size can be
24313 complex at times and changes depending on the client, the client's industry,
24314 the type of engagement, and more. Clara AI helps break down each of these
24315 challenges and provide detailed, accurate guidance for auditors to learn
24316 about their specific use-case without getting lost in irrelevant data.
24317
24318Clara AI does a lot more than I've written about here and I know teams are using
24319these features beyond what my teams do in Tech Assurance. You can read about
24320more features, such as automation, dynamic dashboards, and more on the product
24321page: [[https://kpmg.com/us/en/capabilities-services/audit-services/kpmg-clara.html][Clara AI]].
24322
24323* Use Cases for Each Tool
24324
24325While this section won't be exhaustive, it provides a few basic examples of what
24326my teams do at KPMG with AI, as well as some suggestions for how other auditors
24327can utilize AI at their own companies.
24328
24329** Gemini
24330*** Client and Project-Based Agents or Notebooks
24331
24332As I mentioned above, one great use-case is to build notebooks for your client
24333team or even for internal teams, such as a team that's formed to increase
24334automation within the audit practice.
24335
24336I build notebooks for these teams, upload as many relevant documents and links
24337as I can, invite my team members, and then save useful artifacts within the
24338notebook so that my team members don't need to recreate the wheel in order to
24339gain access to that same information.
24340
24341*** Technical Research
24342
24343Through purely anecdotal experiences, I have found Gemini to be superior to my
24344other AI tools when it comes to technical topics. It provides more accurate
24345information and better context for my specific role.
24346
24347This has replaced the experience of walking team members through topics, such as
24348Snowflake architecture, AWS logical security, or SQL permissions.
24349
24350In addition, it helps me and others who are more experience dive deeper into
24351subjects that we historically did not have the time or knowledge to research.
24352For example, Gemini recently helped me use my knowledge of similar systems to
24353understand Kafka's architecture and risks as they related to my client's
24354specific implementation of Kafka.
24355
24356*** Data Analysis
24357
24358As I mentioned above, Gemini seems to excel with technical tasks. I've been
24359using this to enhance the data analysis we perform.
24360
24361For example, Gemini is able to help write queries (e.g., SQL or AWS commands) to
24362extract the information we need to test a control. While we have lots of
24363internal documentation for popular platforms, there are always situations where
24364you need data from a system and don't have any guidance to extract it.
24365
24366For users with data, Gemini is able to provide a more natural language querying
24367process where the user can simply describe what analysis they need, or even
24368generate a dashboard for the user. I find this most helpful in identifying
24369relevant metrics or analytics and then have our teams use their skills to build
24370out Alteryx workflows and Power BI dashboards to implement what Gemini has
24371helped them investigate.
24372
24373Lastly, data cleaning is wonderful in Gemini. Gemini is able to help structure,
24374clean and deduplicate data much faster than humans can and seems to have a lower
24375potential for errors than if someone manually combed through the data trying to
24376correct it.
24377
24378** Microsoft Copilot
24379*** Meeting Transcripts
24380
24381One of the built-in features of Copilot in Teams is [[https://support.microsoft.com/en-us/office/view-live-transcription-in-microsoft-teams-meetings-dc1a8f23-2e20-4684-885e-2152e06a4a8b][transcription]]. This is a
24382killer feature for auditors, as one of the biggest pain points for an audit (on
24383both sides) is having to meet multiple times to rehash the same topic or send
24384constant follow-ups throughout the year to re-confirm facts.
24385
24386With a transcript, we can spend one meeting going fully through all questions
24387and simply refer back to the transcript whenever we have a question.
24388
24389If you also save the video recording, you can also save the time of requesting
24390evidence later, as you already have a recording where you walked through the
24391evidence you needed.
24392
24393This saves:
24394
243951. An absolute *ton* of time on both the audit and client sides, in terms of
24396 questions and follow-ups.
243972. Even *more* time by not needing the ask the client to take a million
24398 screenshots. Instead, the auditor can take screenshots from the recording
24399 after the fact. No more stopping every 5 seconds in a meeting waiting for
24400 someone to take a screenshot or sending a request for screenshots after the
24401 fact.
24402
24403*** Meeting Facilitator
24404
24405Another killer feature of Copilot is the [[https://learn.microsoft.com/en-us/microsoftteams/facilitator-teams][Facilitator]]. Facilitator takes the
24406agenda in the meeting invite (or determines the agenda throughout the meeting),
24407tracks progress of the discussion against the agenda, takes notes, and tracks
24408to-do items with assignments for users to take action after the end of the
24409meeting.
24410
24411*** Microsoft Office Integrations
24412
24413I talked about the Microsoft Office integrations above, so I won't dive deep in
24414this section.
24415
24416For auditors, some useful examples are:
24417
24418- Generating boilerplate for an audience you're not familiar with writing for
24419 (e.g., engineers vs. CFOs). Note that you *shouldn't* generate the content,
24420 you should only let Copilot guide you to the proper format and then use your
24421 knowledge to write the content.
24422- Checking your tone, especially if Copilot has access to historical chats, in
24423 order to help you respond professionally and appropriately.
24424- In Excel or Word, helping you analyze your data.
24425
24426I'm not a big fan of generating content with Copilot, so I don't have any
24427examples for that. Perhaps I am naive, but I still think we should be honing our
24428interpersonal skills and the content of our communications with others should
24429still be written by us, not AI.
24430
24431*** Managing Emails, Chats, and More
24432
24433I almost titled this section "Manager Mode", since I'm feeling a different level
24434of intensity from the sheer number of emails, Teams messages, tags in channels,
24435and more that I experience now as a Manager.
24436
24437One small but effective use for Copilot is to check at the end of the day if
24438you've missed anything that day, such as responding to an email or message,
24439actions items assigned to you in meetings, or important announcements that,
24440while not directed at you individually, may require action from you.
24441
24442I currently do not have access to Copilot Studio, but I'd love to build a custom
24443agent around this idea if I could.
24444
24445** Clara AI
24446*** Firm & Methodology Guidance
24447
24448For an auditor who has to deal with SOX, SOC 1-3, AUP, and more engagements, you
24449can imagine the sheer number of potential pitfalls there are when working
24450through an engagement.
24451
24452Being able to have Clara AI break down the regulatory requirements for your
24453specific situation is amazing.
24454
24455For example, how do you risk assess Client ABC, who has unique IT systems and
24456business processes, and operates in the oil & gas industry? What if they are
24457also impacted by the US's tariffs? Even further, what if they experienced a data
24458breach in the prior year and have experienced changes in numerous key roles?
24459
24460Clara AI is able to handle of this and more, guiding team members to the
24461relevant guidance and assisting them in the audit process.
24462
24463*** Workflow Assistance
24464
24465On top of the various regulations and internal guidance frameworks, auditors at
24466the Big 4 also have to learn how to use their firm's specific audit platforms.
24467
24468KPMG built the KPMG Clara Workflow (KCw) for audits and SOC reports, which
24469requires teams to fill out the audit file for their engagement, answer all
24470questions that appear, perform their testing, and abide by the required items.
24471
24472Clara AI helps by connecting auditors to the internal guidance that supports
24473each section of the audit platform so that they can complete their engagement in
24474compliance with their requirements.
24475
24476* A Day with AI
24477
24478Before I finish, I wanted to provide an example of how an auditor might utilize
24479AI throughout their day. You can probably summarize this already from the
24480information provided above, but I wanted to lay out in plain terms here at the
24481end:
24482
24483- Strategic planning in the morning with any AI. Review regulatory news,
24484 summarize key decisions made by the government, industry leaders, and more.
24485- If you have a lot of emails and messages, use Copilot to summarize actions
24486 items and make a prioritized plan of action.
24487- As you test or review items in the audit file, use Clara AI to identify gaps
24488 in approach.
24489- During meetings, enable transcriptions and Facilitator. Use these tools so
24490 that people attending the meeting don't need to take detailed notes. Instead,
24491 each person can be fully invested in the topics they're discussing.
24492- Use Copilot to summarize and draft outlines for emails to the team and/or
24493 client.
24494
24495** Conclusion
24496*** What /Can/ AI Do?
24497
24498AI /can/ increase efficiency, consistency, and audit quality, but there are some
24499big assumptions when we state that. We first need to make sure we, as auditors,
24500are using the tools themselves effectively. For an auditor, a general AI chat
24501won't be very useful. Instead, we should be training and using focused models
24502that solve specific purposes. The model will perform better if it's trained for
24503specific purpose and isn't expected to know everything that's ever existed.
24504
24505But what happens when we /do/ use AI effectively? Well, at that point, we are
24506able to elevate ourselves and automate the manual and tedious tasks that have
24507taken up time historically.
24508
24509Staff who may have been asked to spends hours cleaning data from the client
24510before can now spend a shorter length of time reviewing the data output instead
24511and then using that additional time to enhance their critical thinking. If you
24512are used to performing these manual tasks, think about what your seniors or
24513managers do with your results. Can you take on those tasks instead and elevate
24514yourself into those roles?
24515
24516*** The Role of the Human in an AI World
24517
24518Last, but not least, I want to *highly stress* the importance of human in the
24519loop (HITL) concept. What we do as auditors is strategically important, whether
24520you're an internal or external auditor, and cannot be replaced by the current
24521landscape of AI tools. Therefore, when we're using AI, we need to make sure we
24522validate *everything* coming out the tool. At no point can the AI take
24523responsibility, so we need to take responsibility for our actions when we use
24524AI.
24525
24526However, when we do all of this, we are able to elevate the role of the auditor
24527to one of judgment, critical thinking, validation, and client relationship
24528management. This means faster, higher quality, and more impactful audits
24529throughout the industry.
24530#+date: <2025-12-06 Sat 14:13:00>
24531#+title: Self-Hosting Guide: Tor Websites
24532#+description: Learn how to host your own website on Tor with an Onion Address in a few short steps!
24533#+slug: self-hosting-tor
24534
24535* Installation
24536
24537First, install the Tor package. I'm using Ubuntu, so I'll be using =apt= to
24538install Tor.
24539
24540#+begin_src sh
24541sudo apt install tor
24542#+end_src
24543
24544* Configuration
24545
24546** Tor
24547Next, let's configure the service we'll be serving via Tor. For this, edit the
24548file below and add the lines below to the end of the file.
24549
24550#+begin_src sh
24551sudo nano /etc/tor/torrc
24552#+end_src
24553
24554The following lines you add to the file will define the directory to use and the
24555internet protocol (IP) address and port we will be using.
24556
24557#+begin_src text
24558HiddenServiceDir /var/lib/tor/hidden_service/
24559HiddenServicePort 80 127.0.0.1:5000
24560#+end_src
24561
24562Next, you will need to =cat= the contents of the file below to find the onion
24563address provided to you.
24564
24565#+begin_src sh
24566sudo cat /var/lib/tor/hidden_service/hostname
24567#+end_src
24568
24569** Nginx
24570
24571With this onion address handy, create a file in your Nginx configuration
24572directory. Mine is =conf.d=, but yours may be =sites-available=. If that's the
24573case, don't forget to create a symbolic link from the file you created in
24574=sites-available= to =sites-enabled=.
24575
24576#+begin_src sh
24577sudo nano /etc/nginx/conf.d/tor-website.conf
24578#+end_src
24579
24580Within this Nginx configuration file, I will be using the file below. I have
24581left comments to explain each part.
24582
24583#+begin_src conf
24584server {
24585 # The IP address and port Nginx will listen on
24586 listen 127.0.0.1:80;
24587 # The onion address to listen for
24588 server_name sv3g2dlyvwyk2nvi3eeh55fcrpdvjlclhi6wwsx57cste6lwdjanzyyd.onion;
24589 # The directory where your website files are
24590 root /var/www/tor-website/;
24591
24592 # By default, try to serve the files. Return 404 if a file is not found.
24593 location / {
24594 try_files $uri $uri/ =404;
24595 }
24596
24597 # Put any other configurations here at the end, such as redirects
24598 # ...
24599}
24600#+end_src
24601
24602Finally, we can restart the services and check to see if it's working.
24603
24604#+begin_src sh
24605sudo systemctl restart nginx.service
24606sudo systemctl restart tor.service
24607#+end_src
24608
24609At this point, you should be able to view your Onion address in Tor.
24610
24611[[https://img.cleberg.net/blog/20251206-self-hosting-tor/onion.webp]]
24612#+caption: Onion Address in Tor
24613
24614** Onion-Location
24615
24616Lastly, let's advertise the =Onion-Location= on our clearnet website so that
24617users on Tor can automatically switch to the Onion address if they want to.
24618
24619#+begin_src sh
24620sudo nano /etc/nginx/conf.d/regular_website.conf
24621#+end_src
24622
24623#+begin_src conf
24624server {
24625 server_name example.com;
24626 add_header Onion-Location http://YourOnionAddress.onion$request_uri;
24627}
24628#+end_src
24629
24630Once complete, restart Nginx.
24631
24632#+begin_src sh
24633sudo systemctl restart nginx.service
24634#+end_src
24635
24636You should now see a big purple button in Tor titled =.onion available= when
24637visiting the clearnet web page.
24638
24639[[https://img.cleberg.net/blog/20251206-self-hosting-tor/clearnet.webp]]
24640#+caption: Onion-Location Button
24641
24642If everything worked, you're all done! You now have a website served via an
24643onion address on Tor and an Onion-Location header advertising your new onion
24644address.
theme/static/styles.css +1 −2
@@ -6,8 +6,7 @@ body {
6 max-width: 50em; 6 max-width: 50em;
7} 7}
8 8
9body, 9body {
10footer {
11 padding: 1rem; 10 padding: 1rem;
12} 11}
13 12