Commit 25945b8fea
Unsigned
Layout: unified · split
.gitignore +2 −1
| @@ -1,2 +1,3 @@ | ||
| 1 | 1 | public/ |
| 2 | sitemap.org | |
| 2 | static/syntax* | |
| 3 | .DS_Store | |
README.md added +38
| @@ -0,0 +1,38 @@ | ||
| 1 | # cleberg.net | |
| 2 | ||
| 3 | ## Overview | |
| 4 | ||
| 5 | This website & blog uses Markdown and [Zola](https://www.getzola.org/). | |
| 6 | ||
| 7 | ## Configuration | |
| 8 | ||
| 9 | If you want to replicate this project structure, you'll need to customize your | |
| 10 | =.emacs= file appropriately, or load a custom =.el= file. | |
| 11 | ||
| 12 | If you're within emacs while changing =.emacs=, you'll need to reload the | |
| 13 | configuration with =M-x load-file= and hit enter to reload the current file | |
| 14 | (=.emacs=). | |
| 15 | ||
| 16 | See the [[./elisp/publish.el][publish.el]] file for this repository's | |
| 17 | configuration. | |
| 18 | ||
| 19 | * Building | |
| 20 | ||
| 21 | Local testing: | |
| 22 | ||
| 23 | ```sh | |
| 24 | zola serve | |
| 25 | ``` | |
| 26 | ||
| 27 | Building: | |
| 28 | ||
| 29 | ```sh | |
| 30 | zola build | |
| 31 | ``` | |
| 32 | ||
| 33 | If you want to build locally and push remotely, you can include `zola build` | |
| 34 | in your CI\CD pipeline, or simply use `scp` after building. | |
| 35 | ||
| 36 | ```sh | |
| 37 | scp -r public/* user@remote_host:/var/www/your_website/ | |
| 38 | ``` | |
README.org deleted −35
| @@ -1,35 +0,0 @@ | ||
| 1 | #+title: README | |
| 2 | ||
| 3 | * Overview | |
| 4 | ||
| 5 | This project uses [[https://orgmode.org/][org-mode]] to structure all documents, including the index page | |
| 6 | and sub-pages such as blog posts. | |
| 7 | ||
| 8 | * Configuration | |
| 9 | ||
| 10 | If you want to replicate this project structure, you'll need to customize your | |
| 11 | =.emacs= file appropriately, or load a custom =.el= file. | |
| 12 | ||
| 13 | If you're within emacs while changing =.emacs=, you'll need to reload the | |
| 14 | configuration with =M-x load-file= and hit enter to reload the current file | |
| 15 | (=.emacs=). | |
| 16 | ||
| 17 | See the [[./elisp/publish.el][publish.el]] file for this repository's | |
| 18 | configuration. | |
| 19 | ||
| 20 | * Building | |
| 21 | ||
| 22 | When ready to publish, open any of the =.org= files in the project and execute | |
| 23 | =M-x org-publish-current-project=. | |
| 24 | ||
| 25 | If you want to force emacs to re-publish all files, including files without | |
| 26 | changes, execute =C-u M-x org-publish-current-project=. | |
| 27 | ||
| 28 | The resulting files will be published to the =public/= directory within the | |
| 29 | repository, which is ignored via the =.gitignore= file. If you wish to publish | |
| 30 | the files remotely, you can change the =:publishing-directory= variable in | |
| 31 | =publish.el= or do a sync step outside of emacs: | |
| 32 | ||
| 33 | #+begin_src sh | |
| 34 | scp -r public/* ubuntu:/var/www/cleberg.net/ | |
| 35 | #+end_src | |
blog/2018-11-28-cpp-compiler.org deleted −127
| @@ -1,127 +0,0 @@ | ||
| 1 | #+date:2018-11-28 | |
| 2 | #+title: The C++ Compiler | |
| 3 | ||
| 4 | * A Brief Introduction | |
| 5 | ||
| 6 | [[https://en.wikipedia.org/wiki/C%2B%2B][C++]] is a general-purpose programming language with object-oriented, generic, and | |
| 7 | functional features in addition to facilities for low-level memory manipulation. | |
| 8 | ||
| 9 | The source code, shown in the snippet below, must be compiled before it can be | |
| 10 | executed. There are many steps and intricacies to the compilation process, and | |
| 11 | this post was a personal exercise to learn and remember as much information as I | |
| 12 | can. | |
| 13 | ||
| 14 | #+BEGIN_SRC cpp | |
| 15 | #include <iostream> | |
| 16 | ||
| 17 | int main() | |
| 18 | { | |
| 19 | std::cout << "Hello, world!\n"; | |
| 20 | } | |
| 21 | #+END_SRC | |
| 22 | ||
| 23 | * Compilation Process | |
| 24 | ||
| 25 | ** An Overview | |
| 26 | ||
| 27 | Compiling C++ projects is a frustrating task most days. Seemingly nonexistent | |
| 28 | errors keeping your program from successfully compiling can be annoying | |
| 29 | (especially since you know you wrote it perfectly the first time, right?). | |
| 30 | ||
| 31 | I'm learning more and more about C++ these days and decided to write this | |
| 32 | concept down so that I can cement it even further in my own head. However, C++ | |
| 33 | is not the only compiled language. Check out [[https://en.wikipedia.org/wiki/Compiled_language][the Wikipedia entry for compiled | |
| 34 | languages]] for more examples of compiled languages. | |
| 35 | ||
| 36 | I'll start with a wonderful, graphical way to conceptualize the C++ compiler. | |
| 37 | View [[https://web.archive.org/web/20190419035048/http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html][The C++ Compilation Process]] by Kurt MacMahon, an NIU professor, to see the | |
| 38 | graphic and an explanation. The goal of the compilation process is to take the | |
| 39 | C++ code and produce a shared library, dynamic library, or an executable file. | |
| 40 | ||
| 41 | ** Compilation Phases | |
| 42 | ||
| 43 | Let's break down the compilation process. There are four major steps to | |
| 44 | compiling C++ code. | |
| 45 | ||
| 46 | *** Step 1 | |
| 47 | ||
| 48 | The first step is to expand the source code file to meet all dependencies. The | |
| 49 | C++ preprocessor includes the code from all the header files, such as | |
| 50 | =#include <iostream>=. Now, what does that mean? The previous example includes | |
| 51 | the =iostream= header. This tells the computer that you want to use the | |
| 52 | =iostream= standard library, which contains classes and functions written in the | |
| 53 | core language. This specific header allows you to manipulate input/output | |
| 54 | streams. After all this, you'll end up which a temporary file that contains the | |
| 55 | expanded source code. | |
| 56 | ||
| 57 | In the example of the C++ code above, the =iostream= class would be included | |
| 58 | in the expanded code. | |
| 59 | ||
| 60 | *** Step 2 | |
| 61 | ||
| 62 | After the code is expanded, the compiler comes into play. The compiler takes the | |
| 63 | C++ code and converts this code into the assembly language, understood by the | |
| 64 | platform. You can see this in action if you head over to the [[https://godbolt.org][GodBolt Compiler | |
| 65 | Explorer]], which shows C++ being converted into assembly dynamically. | |
| 66 | ||
| 67 | For example, the =Hello, world!= code snippet above compiles into the following | |
| 68 | assembly code: | |
| 69 | ||
| 70 | #+BEGIN_SRC asm | |
| 71 | .LC0: | |
| 72 | .string "Hello, world!\n" | |
| 73 | main: | |
| 74 | push rbp | |
| 75 | mov rbp, rsp | |
| 76 | mov esi, OFFSET FLAT:.LC0 | |
| 77 | mov edi, OFFSET FLAT:_ZSt4cout | |
| 78 | 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*) | |
| 79 | mov eax, 0 | |
| 80 | pop rbp | |
| 81 | ret | |
| 82 | __static_initialization_and_destruction_0(int, int): | |
| 83 | push rbp | |
| 84 | mov rbp, rsp | |
| 85 | sub rsp, 16 | |
| 86 | mov DWORD PTR [rbp-4], edi | |
| 87 | mov DWORD PTR [rbp-8], esi | |
| 88 | cmp DWORD PTR [rbp-4], 1 | |
| 89 | jne .L5 | |
| 90 | cmp DWORD PTR [rbp-8], 65535 | |
| 91 | jne .L5 | |
| 92 | mov edi, OFFSET FLAT:_ZStL8__ioinit | |
| 93 | call std::ios_base::Init::Init() [complete object constructor] | |
| 94 | mov edx, OFFSET FLAT:__dso_handle | |
| 95 | mov esi, OFFSET FLAT:_ZStL8__ioinit | |
| 96 | mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev | |
| 97 | call __cxa_atexit | |
| 98 | .L5: | |
| 99 | nop | |
| 100 | leave | |
| 101 | ret | |
| 102 | _GLOBAL__sub_I_main: | |
| 103 | push rbp | |
| 104 | mov rbp, rsp | |
| 105 | mov esi, 65535 | |
| 106 | mov edi, 1 | |
| 107 | call __static_initialization_and_destruction_0(int, int) | |
| 108 | pop rbp | |
| 109 | ret | |
| 110 | #+END_SRC | |
| 111 | ||
| 112 | *** Step 3 | |
| 113 | ||
| 114 | Third, the assembly code generated by the compiler is assembled into the object | |
| 115 | code for the platform. Essentially, this is when the compiler takes the assembly | |
| 116 | code and assembles it into machine code in a binary format. After researching | |
| 117 | this online, I figured out that a lot of compilers will allow you to stop | |
| 118 | compilation at this step. This would be useful for compiling each source code | |
| 119 | file separately. This saves time later if a single file changes; only that file | |
| 120 | needs to be recompiled. | |
| 121 | ||
| 122 | *** Step 4 | |
| 123 | ||
| 124 | Finally, the object code file generated by the assembler is linked together with | |
| 125 | the object code files for any library functions used to produce a shared | |
| 126 | library, dynamic library, or an executable file. It replaces all references to | |
| 127 | undefined symbols with the correct addresses. | |
blog/2018-12-08-aes-encryption.org deleted −103
| @@ -1,103 +0,0 @@ | ||
| 1 | #+date: 2018-12-08 | |
| 2 | #+title: AES Encryption | |
| 3 | ||
| 4 | * Basic AES | |
| 5 | ||
| 6 | If you're not familiar with encryption techniques, [[https://en.wikipedia.org/wiki/Advanced_Encryption_Standard][AES]] is the *Advanced | |
| 7 | Encryption Standard*. This specification was established by the National | |
| 8 | Institute of Standards and Technology, sub-selected from the Rijndael family of | |
| 9 | ciphers (128, 192, and 256 bits) in 2001. Furthering its popularity and status, | |
| 10 | the US government chose AES as their default encryption method for top-secret | |
| 11 | data, removing the previous standard which had been in place since 1977. | |
| 12 | ||
| 13 | AES has proven to be an extremely safe encryption method, with 7-round and | |
| 14 | 8-round attacks making no material improvements since the release of this | |
| 15 | encryption standard almost two decades ago. | |
| 16 | ||
| 17 | #+BEGIN_QUOTE | |
| 18 | Though many papers have been published on the cryptanalysis of AES, the | |
| 19 | fastest single-key attacks on round-reduced AES variants [20, 33] so far are | |
| 20 | only slightly more powerful than those proposed 10 years ago [23,24]. | |
| 21 | - [[http://research.microsoft.com/en-us/projects/cryptanalysis/aesbc.pdf][Bogdonav, et al.]] | |
| 22 | #+END_QUOTE | |
| 23 | ||
| 24 | * How Secure is AES? | |
| 25 | ||
| 26 | In theory, AES-256 is non-crackable due to the massive number of combinations | |
| 27 | that can be produced. However, AES-128 is no longer recommended as a viable | |
| 28 | implementation to protect important data. | |
| 29 | ||
| 30 | A 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 | |
| 31 | understand. Basically AES encrypts the data by obscuring the relationship | |
| 32 | between the data and the encrypted data. Additionally, this method spreads the | |
| 33 | message out. Lastly, the key produced by AES is the secret to decrypting | |
| 34 | it. Someone may know the method of AES, but without the key, they are powerless. | |
| 35 | ||
| 36 | To obscure and spread the data out, AES creates a substitution-permutation | |
| 37 | network. 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 | |
| 38 | network sends the data through a set of S boxes (using the unique key) to | |
| 39 | substitute the bits with another block of bits. Then, a P box will permutate, or | |
| 40 | rearrange, the bits. This is done over and over, with the key being derived from | |
| 41 | the last round. For AES, the key size specifies the number of transformation | |
| 42 | rounds: 10, 12, and 14 rounds for 128-bit, 192-bit, and 256-bit keys, | |
| 43 | respectively. | |
| 44 | ||
| 45 | * The Process | |
| 46 | ||
| 47 | 1. *KeyExpansion=: Using [[https://en.m.wikipedia.org/wiki/Advanced_Encryption_Standard][Rijndael's key schedule]], the keys are dynamically | |
| 48 | generated. | |
| 49 | 2. *AddRoundKey*: Each byte of the data is combined with this key using bitwise | |
| 50 | xor. | |
| 51 | 3. *SubBytes*: This is followed by the substitution of each byte of data. | |
| 52 | 4. *ShiftRows*: Then, the final three rows are shifted a certain number of | |
| 53 | steps, dictated by the cipher. | |
| 54 | 5. *MixColumns*: After the rows have been shifted, the columns are mixed and | |
| 55 | combined. | |
| 56 | ||
| 57 | This process does not necessarily stop after one full round. Steps 2 through 5 | |
| 58 | will repeat for the number of rounds specified by the key. However, the final | |
| 59 | round excludes the MixColumns step. As you can see, this is a fairly complex | |
| 60 | process. One must have a solid understanding of general mathematic principles to | |
| 61 | fully understand how the sequence works (and to even attempt to find a | |
| 62 | weakness). | |
| 63 | ||
| 64 | According to research done by Bogdanov et al., it would take billions of years | |
| 65 | to brute force a 126-bit key with current hardware. Additionally, this brute | |
| 66 | force attack would require storing 2^88 bits of data! However, there | |
| 67 | are a few different attacks that have been used to show vulnerabilities with the | |
| 68 | use of this technology. Side-channel attacks use inadvertent leaks of data from | |
| 69 | the hardware or software, which can allow attackers to obtain the key or run | |
| 70 | programs on a user's hardware. | |
| 71 | ||
| 72 | Please note that this is not something you should run out and try to implement | |
| 73 | in your `Hello, World!` app after only a few hours of research. While AES | |
| 74 | (basically all encryption methods) is extremely efficient in what it does, it | |
| 75 | takes a lot of time and patience to understand. If you're looking for something | |
| 76 | which currently implements AES, check out the [[https://www.bouncycastle.org/documentation.html][Legion of the Bouncy Castle]] for | |
| 77 | Java implementations of cryptographic algorithms. | |
| 78 | ||
| 79 | * Why Does Encryption Matter? | |
| 80 | ||
| 81 | There are limitless reasons to enable encryption at-rest or in-transit for | |
| 82 | various aspects of your digital life. You can research specific examples, such | |
| 83 | as [[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 | |
| 84 | list a few basic reasons to always enable encryption, where feasible: | |
| 85 | ||
| 86 | 1. Privacy is a human right and is recognized as a national right in some | |
| 87 | countries (e.g., [[https://www.law.cornell.edu/wex/fourth_amendment][US Fourth Amendment]]). | |
| 88 | 2. "Why not?" Encryption rarely affects performance or speed, so there's usually | |
| 89 | not a reason to avoid it in the first place. | |
| 90 | 3. Your digital identity and activity (texts, emails, phone calls, online | |
| 91 | accounts, etc.) are extremely valuable and can result in terrible | |
| 92 | consequences, such as identity theft, if leaked to other parties. Encrypting | |
| 93 | this data prevents such leaks from ruining lives. | |
| 94 | 4. Wiping or factory-resetting does not actually wipe all data from the storage | |
| 95 | device. There are methods to read data from the physical disks/boards inside | |
| 96 | devices. | |
| 97 | 5. Corporations, governments, and other nefarious groups/individuals are | |
| 98 | actively looking for ways to collect personal information about anyone they | |
| 99 | can. If someone's data is unencrypted, that person may become a target due to | |
| 100 | the ease of data collection. | |
| 101 | ||
| 102 | **Read More:** | |
| 103 | - [[http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf][Federal Information Processing Standards Publication 197]] | |
blog/2019-01-07-useful-css.org deleted −181
| @@ -1,181 +0,0 @@ | ||
| 1 | #+date: 2019-01-07 | |
| 2 | #+title: Useful CSS Snippets | |
| 3 | ||
| 4 | * Introduction to CSS | |
| 5 | ||
| 6 | [[https://en.wikipedia.org/wiki/CSS][CSS]], the language used to markup HTML code and make it "pretty", is one of the | |
| 7 | most effective ways to increase the attractiveness of a website. It can also | |
| 8 | lead to increased user engagement, retention, and satisfaction. In fact, there | |
| 9 | are whole career fields are dedicated to the improvement of user experiences, | |
| 10 | known as UI design and UX design. | |
| 11 | ||
| 12 | Some web developers are used to the common CSS properties, such as element | |
| 13 | sizing, fonts, colors, etc., but are not as well versed in less-used properties | |
| 14 | and values such as =flexbox=, =clip-path=, and =transform=. This article will | |
| 15 | provide some insight into the less-used and unique CSS properties. | |
| 16 | ||
| 17 | * CSS Variables | |
| 18 | ||
| 19 | The first topic today is CSS variables. Variables are not often used by smaller | |
| 20 | developers. CSS variables allow you to give your website a well-defined | |
| 21 | structure, where you can easily reuse CSS properties throughout the project. | |
| 22 | ||
| 23 | You can use variables to define things, such as color palettes. Then, you can | |
| 24 | use these colors for backgrounds anywhere else in the HTML. This could be | |
| 25 | extended, where extra variables could be defined for =primary-text=, | |
| 26 | =quoted-text=, etc. Variables can also be used to define spacing (e.g. =32px= or | |
| 27 | =2rem=), which can then be applied to margins, padding, font sizes, and more. | |
| 28 | ||
| 29 | For example, here are some variables defined at the root of the website, which | |
| 30 | allows for any subsequent CSS rules to use those variables: | |
| 31 | ||
| 32 | #+BEGIN_SRC css | |
| 33 | :root { | |
| 34 | --primary-color: black; | |
| 35 | --secondary-color: white; | |
| 36 | } | |
| 37 | ||
| 38 | body { | |
| 39 | background-color: var(--primary-color); | |
| 40 | color: var(--secondary-color); | |
| 41 | } | |
| 42 | #+END_SRC | |
| 43 | ||
| 44 | * CSS Box Shadows | |
| 45 | ||
| 46 | Box shadows were once my mortal enemy. No matter how hard I tried, I just | |
| 47 | couldn't get them to work how I wanted. Because of this, my favorite discovery | |
| 48 | has been CSSMatic's [[https://www.cssmatic.com/box-shadow][box shadow generator]]. It provides an excellent tool to | |
| 49 | generate box shadows using their simple sliders. Surprisingly, this is the | |
| 50 | reason I learned how box shadows work! You can use the sliders and watch how the | |
| 51 | CSS code changes in the image that is displayed. Through this, you should | |
| 52 | understand that the basic structure for box shadows is: | |
| 53 | ||
| 54 | #+BEGIN_SRC css | |
| 55 | box-shadow: inset horizontal vertical blur spread color; | |
| 56 | #+END_SRC | |
| 57 | ||
| 58 | Now, let's look at some basic examples! You can copy and paste the following | |
| 59 | code into a site like CodePen or your own HTML files. Feel free to play around | |
| 60 | with the code, experiment, and learn. | |
| 61 | ||
| 62 | **Box Shadow #1** | |
| 63 | ||
| 64 | #+BEGIN_SRC html | |
| 65 | <div class="shadow-examples"> | |
| 66 | <div class="box effect1"> | |
| 67 | <h3>Effect 1</h3> | |
| 68 | </div> | |
| 69 | </div> | |
| 70 | #+END_SRC | |
| 71 | ||
| 72 | #+BEGIN_SRC css | |
| 73 | .box h3 { | |
| 74 | text-align: center; | |
| 75 | position: relative; | |
| 76 | top: 80px; | |
| 77 | } | |
| 78 | .box { | |
| 79 | width: 70%; | |
| 80 | height: 200px; | |
| 81 | background: #fff; | |
| 82 | margin: 40px auto; | |
| 83 | } | |
| 84 | .effect1 { | |
| 85 | box-shadow: 0 10px 6px -6px #777; | |
| 86 | } | |
| 87 | #+END_SRC | |
| 88 | ||
| 89 | **Box Shadow #2** | |
| 90 | ||
| 91 | #+BEGIN_SRC html | |
| 92 | <div class="shadow-examples"> | |
| 93 | <div class="box effect2"> | |
| 94 | <h3>Effect 2</h3> | |
| 95 | </div> | |
| 96 | </div> | |
| 97 | #+END_SRC | |
| 98 | ||
| 99 | #+BEGIN_SRC css | |
| 100 | .box h3 { | |
| 101 | text-align: center; | |
| 102 | position: relative; | |
| 103 | top: 80px; | |
| 104 | } | |
| 105 | .box { | |
| 106 | width: 70%; | |
| 107 | height: 200px; | |
| 108 | background: #fff; | |
| 109 | margin: 40px auto; | |
| 110 | } | |
| 111 | .effect2 { | |
| 112 | box-shadow: 10px 10px 5px -5px rgba(0, 0, 0, 0.75); | |
| 113 | } | |
| 114 | #+END_SRC | |
| 115 | ||
| 116 | Try these box shadows out on your own and see how changing each shadow value | |
| 117 | works. | |
| 118 | ||
| 119 | * CSS Flexbox | |
| 120 | ||
| 121 | Now, let's move on to the best part of this article: flexbox. The flexbox is by | |
| 122 | far my favorite new toy. I originally stumbled across this solution after | |
| 123 | looking for more efficient ways of centering content horizontally AND | |
| 124 | vertically. I had used a few hack-ish methods before, but flexbox throws those | |
| 125 | out the window. The best part of it all is that flexbox is /dead simple/. | |
| 126 | ||
| 127 | Flexbox pertains to the parent div of any element. You want the parent to be the | |
| 128 | flexbox in which items are arranged to use the flex methods. It's easier to see | |
| 129 | this in action that explained, so let's see an example. | |
| 130 | ||
| 131 | **Flexbox** | |
| 132 | ||
| 133 | #+BEGIN_SRC html | |
| 134 | <div class="flex-examples"> | |
| 135 | <div class="sm-box"> | |
| 136 | <h3>1</h3> | |
| 137 | </div> | |
| 138 | <div class="sm-box"> | |
| 139 | <h3>2</h3> | |
| 140 | </div> | |
| 141 | </div> | |
| 142 | #+END_SRC | |
| 143 | ||
| 144 | #+BEGIN_SRC css | |
| 145 | .flex-examples { | |
| 146 | display: flex; | |
| 147 | flex-wrap: wrap; | |
| 148 | justify-content: flex-start; | |
| 149 | align-items: center; | |
| 150 | padding: 10px; | |
| 151 | background-color: #f2f2f2; | |
| 152 | } | |
| 153 | .sm-box { | |
| 154 | display: flex; | |
| 155 | justify-content: center; | |
| 156 | align-items: center; | |
| 157 | width: 20%; | |
| 158 | height: 100px; | |
| 159 | background: #fff; | |
| 160 | margin: 40px 10px; | |
| 161 | } | |
| 162 | #+END_SRC | |
| 163 | ||
| 164 | You may notice that we no longer need to use the =top= property for the =h3= | |
| 165 | elements in our code. This is because we set the display box to be a flex | |
| 166 | container for the small boxes, AND we made the small boxes flex containers for | |
| 167 | their elements (the h3 tags). Flex boxes can be nested like this to center | |
| 168 | content that is inside centered content. | |
| 169 | ||
| 170 | For the example above, we designated the =justify-content= property to be | |
| 171 | =flex-start= so that the boxes stack from the left side of the screen. This | |
| 172 | property can be changed to =center= to make the boxes appear in the center of | |
| 173 | the screen. | |
| 174 | ||
| 175 | For an interactive example, [[https://codepen.io/LandonSchropp/pen/KpzzGo][check out this CodePen]] from [[https://codepen.io/LandonSchropp/][LandonScropp]]. Resize the | |
| 176 | window with dice to see how they collapse and re-align. | |
| 177 | ||
| 178 | * Even More CSS | |
| 179 | ||
| 180 | For more inspiration, you can visit [[https://www.codepen.io][CodePen]], [[https://dribbble.com][Dribbble]], or [[https://uimovement.com][UI Movement]] to browse | |
| 181 | the collections of many amazing web designers. | |
blog/2019-09-09-audit-analytics.org deleted −213
| @@ -1,213 +0,0 @@ | ||
| 1 | #+date: 2019-09-09 | |
| 2 | #+title: Data Analysis in Auditing | |
| 3 | ||
| 4 | * What Are Data Analytics? | |
| 5 | ||
| 6 | A quick aside before I dive into this post: `data analytics` is a vague term | |
| 7 | that has become popular in recent years. Think of a `data analytic` as the | |
| 8 | output of any data analysis you perform. For example, a pivot table or a pie | |
| 9 | chart could be a data analytic. | |
| 10 | ||
| 11 | [[https://en.wikipedia.org/wiki/Data_analysis][Data analysis]] is a process that utilizes statistics and other mathematical | |
| 12 | methods to discover useful information within datasets. This involves examining, | |
| 13 | cleaning, transforming, and modeling data so that you can use the data to | |
| 14 | support an opinion, create more useful viewpoints, and gain knowledge to | |
| 15 | implement into audit planning or risk assessments. | |
| 16 | ||
| 17 | One of the common mistakes that managers (and anyone new to the process) make is | |
| 18 | assuming that everything involved with this process is "data analytics". In | |
| 19 | fact, data analytics are only a small part of the process. | |
| 20 | ||
| 21 | See *Figure 1* for a more accurate representation of where data analysis sits | |
| 22 | within the full process. This means that data analysis does not include | |
| 23 | querying or extracting data, selecting samples, or performing audit tests. | |
| 24 | These steps can be necessary for an audit (and may even be performed by the same | |
| 25 | associates), but they are not data analytics. | |
| 26 | ||
| 27 | #+CAPTION: The Intelligence Cycle | |
| 28 | [[https://img.cleberg.net/blog/20190909-data-analysis-in-auditing/intelligence_cycle-min.png]] | |
| 29 | ||
| 30 | * Current Use of Analytics in Auditing | |
| 31 | ||
| 32 | While data analysis has been an integral part of most businesses and departments | |
| 33 | for the better part of the last century, only recently have internal audit | |
| 34 | functions been adopting this practice. The internal audit function works | |
| 35 | exclusively to provide assurance and consulting services to the business areas | |
| 36 | within the firm (except for internal auditing firms who are hired by different | |
| 37 | companies to perform their roles). | |
| 38 | ||
| 39 | #+BEGIN_QUOTE | |
| 40 | Internal Auditing helps an organization accomplish its objectives by bringing | |
| 41 | a systematic, disciplined approach to evaluate and improve the effectiveness | |
| 42 | of risk management, control and governance processes. | |
| 43 | ||
| 44 | - The IIA's Definition of Internal Audit | |
| 45 | #+END_QUOTE | |
| 46 | ||
| 47 | Part of the blame for the slow adoption of data analysis can be attributed to | |
| 48 | the fact that internal auditing is strongly based on tradition and following the | |
| 49 | precedents set by previous auditors. However, there can be no progress without | |
| 50 | auditors who are willing to break the mold and test new audit techniques. In | |
| 51 | fact, 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 | |
| 52 | analytics]] in North America. This number should be as close as possible to | |
| 53 | 100%. I have never been part of an audit that would not have benefited from data | |
| 54 | analytics. | |
| 55 | ||
| 56 | So, how do internal audit functions remedy this situation? It's definitely not | |
| 57 | as easy as walking into work on Monday and telling your Chief Audit Executive | |
| 58 | that you're going to start implementing analytics in the next audit. You need a | |
| 59 | plan and a system to make the analysis process as effective as possible. | |
| 60 | ||
| 61 | * The DELTA Model | |
| 62 | ||
| 63 | One of the easiest ways to experiment with data analytics and gain an | |
| 64 | understanding of the processes is to implement them within your own department. | |
| 65 | But how do we do this if we've never worked with analysis before? One of the | |
| 66 | most common places to start is to research some data analysis models currently | |
| 67 | available. For this post, we'll take a look at the DELTA model. You can take a | |
| 68 | look at **Figure 2** for a quick overview of the model. | |
| 69 | ||
| 70 | The DELTA model sets a few guidelines for areas wanting to implement data | |
| 71 | analytics so that the results can be as comprehensive as possible: | |
| 72 | ||
| 73 | - *Data*: Must be clean, accessible, and (usually) unique. | |
| 74 | - *Enterprise-Wide Focus*: Key data systems and analytical resources must be | |
| 75 | available for use (by the Internal Audit Function). | |
| 76 | - *Leaders*: Must promote a data analytics approach and show the value of | |
| 77 | analytical results. | |
| 78 | - *Targets*: Must be set for key areas and risks that the analytics can be | |
| 79 | compared against (KPIs). | |
| 80 | - *Analysts*: There must be auditors willing and able to perform data analytics | |
| 81 | or else the system cannot be sustained. | |
| 82 | ||
| 83 | #+CAPTION: The Delta Model | |
| 84 | [[https://img.cleberg.net/blog/20190909-data-analysis-in-auditing/delta-min.png]] | |
| 85 | ||
| 86 | * Finding the Proper KPIs | |
| 87 | ||
| 88 | Once the Internal Audit Function has decided that they want to start using data | |
| 89 | analytics internally and have ensured they're properly set up to do so, they | |
| 90 | need to figure out what they will be testing against. Key Performance Indicators | |
| 91 | (KPIs) are qualitative or quantitative factors that can be evaluated and | |
| 92 | assessed to determine if the department is performing well, usually compared to | |
| 93 | historical or industry benchmarks. Once KPIs have been agreed upon and set, | |
| 94 | auditors can use data analytics to assess and report on these KPIs. This allows | |
| 95 | the person performing the analytics the freedom to express opinions on the | |
| 96 | results, whereas the results are ambiguous if no KPIs exist. | |
| 97 | ||
| 98 | It should be noted that tracking KPIs in the department can help ensure you have | |
| 99 | a rigorous Quality Assurance and Improvement Program (QAIP) in accordance with | |
| 100 | some applicable standards, such as IPPF Standard 1300. | |
| 101 | ||
| 102 | #+BEING_QUOTE | |
| 103 | The chief audit executive must develop and maintain a quality assurance and | |
| 104 | improvement program that covers all aspects of the internal audit activity. | |
| 105 | ||
| 106 | - IPPF Standard 1300 | |
| 107 | #+END_QUOTE | |
| 108 | ||
| 109 | Additionally, IPPF Standard 2060 discusses reporting: | |
| 110 | ||
| 111 | #+BEGIN_QUOTE | |
| 112 | The chief audit executive must report periodically to senior management and the | |
| 113 | board on the internal audit activity's purpose, authority, responsibility, and | |
| 114 | performance relative to its plan and on its conformance with the Code of Ethics | |
| 115 | and the Standards. Reporting must also include significant risk and control | |
| 116 | issues, including fraud risks, governance issues, and other matters that require | |
| 117 | the attention of senior management and/or the board. | |
| 118 | ||
| 119 | - IPPF Standard 2060 | |
| 120 | #+END_QUOTE | |
| 121 | ||
| 122 | The hardest part of finding KPIs is to determine which KPIs are appropriate for | |
| 123 | your department. Since every department is different and has different goals, | |
| 124 | KPIs will vary drastically between companies. To give you an idea of where to | |
| 125 | look, here are some ideas I came up with when discussing the topic with a few | |
| 126 | colleagues. | |
| 127 | ||
| 128 | - Efficiency/Budgeting: | |
| 129 | - Audit hours to staff utilization ratio (annual hours divided by total annual | |
| 130 | work hours). | |
| 131 | - Audit hours compared to the number of audits completed. | |
| 132 | - Time between audit steps or to complete the whole audit. E.g., time from | |
| 133 | fieldwork completion to audit report issuance. | |
| 134 | - Reputation: | |
| 135 | - The frequency that management has requested the services of the IAF. | |
| 136 | - Management, audit committee, or external audit satisfaction survey results. | |
| 137 | - Education, experience, certifications, tenure, and training of the auditors | |
| 138 | on staff. | |
| 139 | - Quality: | |
| 140 | - Number and frequency of audit findings. Assign monetary or numerical values, | |
| 141 | if possible. | |
| 142 | - Percentage of recommendations issued and implemented. | |
| 143 | - Planning: | |
| 144 | - Percentage or number of key risks audited per year or per audit. | |
| 145 | - Proportion of audit universe audited per year. | |
| 146 | ||
| 147 | * Data Analysis Tools | |
| 148 | ||
| 149 | Finally, to be able to analyze and report on the data analysis, auditors need to | |
| 150 | evaluate the tools at their disposal. There are many options available, but a | |
| 151 | few of the most common ones can easily get the job done. For example, almost | |
| 152 | every auditor already has access to Microsoft Excel. Excel is more powerful than | |
| 153 | most people give it credit for and can accomplish a lot of basic statistics | |
| 154 | without much work. If you don't know a lot about statistics but still want to | |
| 155 | see some of the more basic results, Excel is a great option. | |
| 156 | ||
| 157 | To perform more in-depth statistical analysis or to explore large datasets that | |
| 158 | Excel cannot handle, auditors will need to explore other options. The big three | |
| 159 | that have had a lot of success in recent years are Python, R, and ACL. ACL can | |
| 160 | be used as either a graphical tool (point and click) or as a scripting tool, | |
| 161 | where the auditor must write the scripts manually. Python and the R-language are | |
| 162 | solely scripting languages. | |
| 163 | ||
| 164 | The general trend in the data analytics environment is that if the tool allows | |
| 165 | you to do everything by clicking buttons or dragging elements, you won't be able | |
| 166 | to fully utilize the analytics you need. The most robust solutions are created | |
| 167 | by those who understand how to write the scripts manually. It should be noted | |
| 168 | that as the utility of a tool increases, it usually means that the learning | |
| 169 | curve for that tool will also be higher. It will take auditors longer to learn | |
| 170 | how to utilize Python, R, or ACL versus learning how to utilize Excel. | |
| 171 | ||
| 172 | * Visualization | |
| 173 | ||
| 174 | Once an auditor has finally found the right data, KPIs, and tools, they must | |
| 175 | report these results so that actions can be taken. Performing in-depth data | |
| 176 | analysis is only useful if the results are understood by the audiences of the | |
| 177 | data. The best way to create this understanding is to visualize the results of | |
| 178 | the data. Let's take a look at some of the best options to visualize and report | |
| 179 | the results you've found. | |
| 180 | ||
| 181 | Some of the most popular commercial tools for visualization are Microsoft | |
| 182 | PowerBI and Tableau Desktop. However, other tools exist such as JMP, Plotly, | |
| 183 | Qlikview, Alteryx, or D3. Some require commercial licenses while others are | |
| 184 | simply free to use. For corporate data, you may want to make sure that the tool | |
| 185 | does not communicate any of the data outside the company (such as cloud | |
| 186 | storage). I won't be going into depth on any of these tools since visualization | |
| 187 | is largely a subjective and creative experience, but remember to constantly | |
| 188 | explore new options as you repeat the process. | |
| 189 | ||
| 190 | Lastly, let's take a look at an example of data visualization. This example | |
| 191 | comes 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 | |
| 192 | of entry-level US jobs that require experience. *Figure 3* shows us an | |
| 193 | easy-to-digest picture of the data. We can quickly tell that only about 12.5% of | |
| 194 | entry-level jobs don't require experience. | |
| 195 | ||
| 196 | This is the kind of result that easily describes the data for you. However, make | |
| 197 | sure to include an explanation of what the results mean. Don't let the reader | |
| 198 | assume what the data means, especially if it relates to a complex subject. /Tell | |
| 199 | a story/ about the data and why the results matter. For example, *Figure 4* | |
| 200 | shows a part of the explanation the author gives to illustrate his point. | |
| 201 | ||
| 202 | #+CAPTION: Entry-Level Visualization | |
| 203 | [[https://img.cleberg.net/blog/20190909-data-analysis-in-auditing/vis_example-min.png]] | |
| 204 | ||
| 205 | #+CAPTION: Visualization Explanation | |
| 206 | [[https://img.cleberg.net/blog/20190909-data-analysis-in-auditing/vis_example_explanation-min.png]] | |
| 207 | ||
| 208 | * Wrap-Up | |
| 209 | ||
| 210 | While this is not an all-encompassing program that you can just adopt into your | |
| 211 | department, it should be enough to get anyone started on the process of | |
| 212 | understanding and implementing data analytics. Always remember to continue | |
| 213 | learning and exploring new options as your processes grow and evolve. | |
blog/2019-12-03-the-ansoff-matrix.org deleted −135
| @@ -1,135 +0,0 @@ | ||
| 1 | #+date: 2019-12-03 | |
| 2 | #+title: The Ansoff Matrix | |
| 3 | ||
| 4 | * Overview | |
| 5 | ||
| 6 | As the world of business evolves, managers must approach business planning and | |
| 7 | strategy with a contemporary mindset. According to Dess, McNamara, Eisner, and | |
| 8 | Lee, managers must be willing to adapt to the modern business environment by | |
| 9 | going beyond "'incremental management', whereby they view their job as making a | |
| 10 | series of small, minor changes to improve the efficiency of the firm's | |
| 11 | operations"[fn:1]. | |
| 12 | ||
| 13 | One reason that strategic management is crucial is because most businesses that | |
| 14 | fail in the United States each year fail due to a lack of strategic focus or | |
| 15 | direction[fn:2]. The rate of failure for businesses with poor strategies shows | |
| 16 | that strategic planning and management are crucial to a business's strength and | |
| 17 | longevity, injecting the critical factors of growth and direction into a | |
| 18 | company's business plan. | |
| 19 | ||
| 20 | One of the most significant strategic planning and management frameworks that | |
| 21 | companies can use is the [[https://en.wikipedia.org/wiki/Ansoff_matrix][Ansoff Matrix]]. While this framework has unique purposes | |
| 22 | and use-cases, it can effectively help an organization grow and | |
| 23 | compete. Specifically, the Ansoff matrix is one of the most effective frameworks | |
| 24 | for companies who want to focus on increasing sales revenue or | |
| 25 | profitability[fn:3]. | |
| 26 | ||
| 27 | This framework uses a two-by-two figure to show the four strategic options for | |
| 28 | companies to use in this framework: market penetration, market development, | |
| 29 | product development, and diversification (see *Figure 1*). The x-axis of the | |
| 30 | matrix focuses on the firm's markets and also determines if the firm is looking | |
| 31 | to enter new markets or innovate in its current markets. The y-axis of the | |
| 32 | matrix focuses on the firm's products and determines if the firm wants to pursue | |
| 33 | strategies around their existing products or explore new products. | |
| 34 | ||
| 35 | #+CAPTION: The Ansoff Matrix by JaisonAbeySabu, Own work, CC BY-SA 3.0 | |
| 36 | [[https://img.cleberg.net/blog/20191203-the-ansoff-matrix/ansoff_matrix-min.png]] | |
| 37 | ||
| 38 | * Strategic Options | |
| 39 | ||
| 40 | ** Market Penetration | |
| 41 | ||
| 42 | The most straightforward strategy in the Ansoff matrix is to focus on existing | |
| 43 | products in existing markets, also known as market penetration[fn:3]. Companies | |
| 44 | such as Coca-Cola have used market penetration successfully by investing a lot | |
| 45 | of money to get further value out of their current markets. Coca-Cola does this | |
| 46 | by introducing new features such as Christmas-themed bottles, personal names on | |
| 47 | the bottles, and other marketing schemes. | |
| 48 | ||
| 49 | ** Market Development | |
| 50 | ||
| 51 | Market development extends existing products into new markets in an attempt to | |
| 52 | increase the number of buyers. One interesting way that Coca-Cola used this | |
| 53 | strategy comes from the stigma that Diet Coke is a woman's drink[fn:4]. Coca-Cola | |
| 54 | introduced Coca-Cola Zero, which contained the same nutritional content as Diet | |
| 55 | Coke, but was packaged in a dark black can to appear more "manly"[fn:4]. | |
| 56 | ||
| 57 | ** Product Development | |
| 58 | ||
| 59 | Product development uses existing markets to introduce new products so that the | |
| 60 | firm can better meet customer needs[fn:4]. The extreme end of diversification is | |
| 61 | home to companies such as Johnson & Johnson, a healthcare company that has | |
| 62 | developed a business portfolio of more than 60,000 different products[fn:5]. | |
| 63 | Johnson & Johnson's dedication to continuous diversification has led them to a | |
| 64 | balance sheet rating of "AAA", industry recognition for diversification, and | |
| 65 | increases in their investor dividends for 57 consecutive years[fn:6]. | |
| 66 | ||
| 67 | ** Related Diversification | |
| 68 | ||
| 69 | Diversification, the final strategy of the Ansoff Matrix, is more difficult than | |
| 70 | the others since it involves exploring both new markets and new products. | |
| 71 | Related diversification is a diversification strategy that closely relates to | |
| 72 | the firm's core business. Coca-Cola's best example of related diversification is | |
| 73 | its acquisition of Glaceau and Vitamin Water, which expanded their drinking | |
| 74 | lines of business[fn:4]. | |
| 75 | ||
| 76 | ** Unrelated Diversification | |
| 77 | ||
| 78 | Unrelated diversification is a diversification strategy that does not really | |
| 79 | relate to the firm's core business but still diversifies their business | |
| 80 | portfolio. A good example of this would be a coffee company who has decided to | |
| 81 | enter the market for bicycle sales. The main purpose of this strategy is to an | |
| 82 | extremely diverse company that will not go bankrupt if one market goes through | |
| 83 | difficult times. However, this requires a lot of independent skills and heavy | |
| 84 | investments since the company most likely cannot easily transfer knowledge | |
| 85 | between the markets they compete in. | |
| 86 | ||
| 87 | * Requirements for Success | |
| 88 | ||
| 89 | To use the Ansoff Matrix framework, managers need to formulate corporate goals | |
| 90 | and objectives. Without goals and direction, management frameworks do not | |
| 91 | present much practical utility. Further, the Ansoff Matrix requires the managers | |
| 92 | involved to make tactical decisions and create a path for the company to take | |
| 93 | toward their goals. Lastly, both the Ansoff Matrix needs to consider both | |
| 94 | internal and external perspectives throughout the strategy formulation process. | |
| 95 | ||
| 96 | One interesting probability is that companies will be using multiple strategic | |
| 97 | planning and management frameworks at the same time. While this may sound like | |
| 98 | it could crowd the management process, there are numerous reasons to do so. For | |
| 99 | example, the Ansoff Matrix and the Balanced Scorecard are relatively popular, | |
| 100 | and they cover entirely different parts of a company's strategy. Using the | |
| 101 | results from the Balanced Scorecard could inform a company of the potential | |
| 102 | product and market demands, such as from customer or supplier survey results, | |
| 103 | to help the company determine which Ansoff Matrix strategy to pursue. | |
| 104 | However, a combined approach at this level would require mature frameworks | |
| 105 | and focused managers who are able to strategize at a high level. | |
| 106 | ||
| 107 | Lastly, it should be noted that the author of the Ansoff matrix, Igor Ansoff, | |
| 108 | often used the term [[https://en.wikipedia.org/wiki/Analysis_paralysis][paralysis by analysis]] to explain the mistake of companies | |
| 109 | who overuse analysis and spend too much time planning. Companies need to | |
| 110 | understand the utility of a strategic management framework while ensuring that | |
| 111 | the company is poised to execute as efficiently as they have planned. | |
| 112 | ||
| 113 | * Footnotes | |
| 114 | ||
| 115 | [fn:1]: Dess, G. G., McNamara, G., Eisner, A. B., Lee, S. H. (2019). Strategic | |
| 116 | management: Text & cases, ninth edition. New York, NY: McGraw-Hill Education. | |
| 117 | ||
| 118 | [fn:2]: Juneja, P. (n.d.). Benefits of strategic management. Management Study | |
| 119 | Guide. Retrieved from | |
| 120 | https://www.managementstudyguide.com/strategic-management-benefits.htm. | |
| 121 | ||
| 122 | [fn:3]: Meldrum M., McDonald M. (1995) The Ansoff matrix. In: Key Marketing | |
| 123 | Concepts. London: Palgrave. | |
| 124 | ||
| 125 | [fn:4]: Oakley, T. (2015). Coca-Cola: The Ansoff matrix. The Marketing Agenda. | |
| 126 | Retrieved from | |
| 127 | https://themarketingagenda.com/2015/03/28/coca-cola-ansoff-matrix/. | |
| 128 | ||
| 129 | [fn:5]: Lemke, T. (2019). The most diversified companies in the stock market. The | |
| 130 | balance. Retrieved from | |
| 131 | https://www.thebalance.com/the-most-diversified-companies-in-the-stock-market-4169730. | |
| 132 | ||
| 133 | [fn:6]: Johnson & Johnson. (2018). 2018 Investor Fact Sheet. [PDF file]. Retrieved | |
| 134 | from | |
| 135 | http://www.investor.jnj.com/\_document/2018-investor-fact-sheet-4-19'id=0000016a-5681-d475-a17f-d78db54a0000. | |
blog/2019-12-16-password-security.org deleted −107
| @@ -1,107 +0,0 @@ | ||
| 1 | #+date: 2019-12-16 | |
| 2 | #+title: Password Security | |
| 3 | ||
| 4 | * Users | |
| 5 | ||
| 6 | ** Why Does It Matter? | |
| 7 | ||
| 8 | Information security, including passwords and identities, has become one of the | |
| 9 | most 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 | |
| 10 | affected by data breaches each year]], there's a greater need to introduce strong | |
| 11 | information security systems. If you think you've been part of a breach, or you | |
| 12 | want to check and see, you can use [[https://haveibeenpwned.com/][Have I Been Pwned]] to see if your email has | |
| 13 | been involved in any public breaches. Remember that there's a possibility that a | |
| 14 | company experienced a breach and did not report it to anyone. | |
| 15 | ||
| 16 | ** How Do I Protect Myself? | |
| 17 | ||
| 18 | The first place to start with any personal security check-up is to gather a list | |
| 19 | of all the different websites, apps, or programs that require you to have login | |
| 20 | credentials. Optionally, once you know where your information is being stored, | |
| 21 | you can sort the list from the most-important items such as banks or government | |
| 22 | logins to less important items such as your favorite meme site. You will want to | |
| 23 | ensure that your critical logins are secure before getting to the others. | |
| 24 | ||
| 25 | Once you think you have a good idea of all your different authentication | |
| 26 | methods, I recommend using a password manager such as [[https://bitwarden.com/][Bitwarden]]. Using a | |
| 27 | password manager allows you to automatically save your logins, create randomized | |
| 28 | passwords, and transfer passwords across devices. However, you'll need to | |
| 29 | memorize your "vault password" that allows you to open the password manager. | |
| 30 | It's important to make this something hard to guess since it would allow anyone | |
| 31 | who has it to access every password you've stored in there. | |
| 32 | ||
| 33 | Personally, I recommend using a [[https://en.wikipedia.org/wiki/Passphrase][passphrase]] instead of a [[https://en.wikipedia.org/wiki/Password][password]] for your vault | |
| 34 | password. Instead of using a string of characters (whether random or simple), | |
| 35 | use a phrase and add in symbols and a number. For example, your vault password | |
| 36 | could be =Racing-Alphabet-Gourd-Parrot3=. Swap the symbols out for whichever | |
| 37 | symbol you want, move the number around, and fine-tune the passphrase until you | |
| 38 | are confident that you can remember it whenever necessary. | |
| 39 | ||
| 40 | Once you've stored your passwords, make sure you continually check up on your | |
| 41 | account and make sure you aren't following bad password practices. Krebs on | |
| 42 | Security has a great [[https://krebsonsecurity.com/password-dos-and-donts/][blog post on password recommendations]]. Any time that a data | |
| 43 | breach happens, make sure you check to see if you were included, and if you need | |
| 44 | to reset any account passwords. | |
| 45 | ||
| 46 | * Developers | |
| 47 | ||
| 48 | ** What Are the Basic Requirements? | |
| 49 | ||
| 50 | When developing any password-protected application, there are a few basic rules | |
| 51 | that anyone should follow even if they do not follow any official guidelines | |
| 52 | such as NIST. The foremost practice is to require users to use passwords that | |
| 53 | are at least 8 characters and cannot easily be guessed. This sounds extremely | |
| 54 | simple, but it requires quite a few different strategies. First, the application | |
| 55 | should check the potential passwords against a dictionary of insecure passwords | |
| 56 | such =password=, =1234abc=, or =application_name=. | |
| 57 | ||
| 58 | Next, the application should offer guidance on the strength of passwords being | |
| 59 | entered during enrollment. Further, NIST officially recommends *not* | |
| 60 | implementing any composition rules that make passwords hard to remember (e.g. | |
| 61 | passwords with letters, numbers, and special characters) and instead encouraging | |
| 62 | the use of long pass phrases which can include spaces. It should be noted that | |
| 63 | to be able to keep spaces within passwords, all unicode characters should be | |
| 64 | supported, and passwords should not be truncated. | |
| 65 | ||
| 66 | ** What Does NIST Recommend? | |
| 67 | ||
| 68 | The National Institute of Standards and Technology ([[https://www.nist.gov][NIST]]) in the US Department | |
| 69 | of Commerce regularly publishes information around information security and | |
| 70 | digital identity guidelines. Recently, NIST published [[https://pages.nist.gov/800-63-3/sp800-63b.html][Special Publication | |
| 71 | 800-63b]]: Digital Identity Guidelines and Authentication and Lifecycle | |
| 72 | Management. | |
| 73 | ||
| 74 | #+BEGIN_QUOTE | |
| 75 | A Memorized Secret authenticator - commonly referred to as a password or, if | |
| 76 | numeric, a PIN - is a secret value intended to be chosen and memorized by the | |
| 77 | user. Memorized secrets need to be of sufficient complexity and secrecy that | |
| 78 | it would be impractical for an attacker to guess or otherwise discover the | |
| 79 | correct secret value. A memorized secret is something you know. | |
| 80 | ||
| 81 | - NIST Special Publication 800-63B | |
| 82 | #+END_QUOTE | |
| 83 | ||
| 84 | NIST offers a lot of guidance on passwords, but I'm going to highlight just a | |
| 85 | few of the important factors: | |
| 86 | ||
| 87 | - Require passwords to be a minimum of 8 characters (6 characters if randomly | |
| 88 | generated and be generated using an approved random bit generator). | |
| 89 | - Compare potential passwords against a list that contains values known to be | |
| 90 | commonly-used, expected, or compromised. | |
| 91 | - Offer guidance on password strength, such as a strength meter. | |
| 92 | - Implement a rate-limiting mechanism to limit the number of failed | |
| 93 | authentication attempts for each user account. | |
| 94 | - Do not require composition rules for passwords and do not require passwords to | |
| 95 | be changed periodically (unless compromised). | |
| 96 | - Allow pasting of user identification and passwords to facilitate the use of | |
| 97 | password managers. | |
| 98 | - Allow users to view the password as it is being entered. | |
| 99 | - Use secure forms of communication and storage, including salting and hashing | |
| 100 | passwords using a one-way key derivation function. | |
| 101 | ||
| 102 | NIST offers further guidance on other devices that require specific security | |
| 103 | policies, querying for passwords, and more. All the information discussed so far | |
| 104 | comes from [[https://pages.nist.gov/800-63-3/sp800-63b.html][NIST SP800-63b]] but NIST offers a lot of information on digital | |
| 105 | identities, enrollment, identity proofing, authentication, lifecycle management, | |
| 106 | federation, and assertions in the total [[https://pages.nist.gov/800-63-3/][NIST SP800-63 Digital Identity | |
| 107 | Guidelines]]. | |
blog/2020-01-25-linux-software.org deleted −250
| @@ -1,250 +0,0 @@ | ||
| 1 | #+date: 2020-01-25 | |
| 2 | #+title: Linux Software | |
| 3 | ||
| 4 | * GUI Applications | |
| 5 | ||
| 6 | ** Etcher | |
| 7 | ||
| 8 | [[https://img.cleberg.net/blog/20200125-the-best-linux-software/etcher.png]] | |
| 9 | ||
| 10 | [[https://www.balena.io/etcher/][Etcher]] is a quick and easy way to burn ISO images to CDs and USB devices. There | |
| 11 | are two different ways you can install this program. First, you can navigate to | |
| 12 | the [[https://www.balena.io/etcher/][official website]] and download the AppImage file, which can run without | |
| 13 | installation. | |
| 14 | ||
| 15 | However, AppImage files are not executable by default, so you'll either need to | |
| 16 | right-click to open the properties of the file and click the "Allow executing | |
| 17 | file as program" box in the Permissions tab or use the following command: | |
| 18 | ||
| 19 | #+BEGIN_SRC sh | |
| 20 | chmod u+x FILE_NAME | |
| 21 | #+END_SRC | |
| 22 | ||
| 23 | If you don't like AppImage files or just prefer repositories, you can use the | |
| 24 | following commands to add the author's repository and install it through the | |
| 25 | command-line only. | |
| 26 | ||
| 27 | First, you'll have to echo the repo and write it to a list file: | |
| 28 | ||
| 29 | #+BEGIN_SRC sh | |
| 30 | echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list | |
| 31 | #+END_SRC | |
| 32 | ||
| 33 | Next, add the application keys to Ubuntu's keyring: | |
| 34 | ||
| 35 | #+BEGIN_SRC sh | |
| 36 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 379CE192D401AB61 | |
| 37 | #+END_SRC | |
| 38 | ||
| 39 | Finally, update the repositories and install the app. | |
| 40 | ||
| 41 | #+BEGIN_SRC sh | |
| 42 | sudo apt update && sudo apt install balena-etcher-electron | |
| 43 | #+END_SRC | |
| 44 | ||
| 45 | Using Arch, Manjaro, or another distro using the AUR? Use this command instead: | |
| 46 | ||
| 47 | #+BEGIN_SRC sh | |
| 48 | sudo pacman -S etcher | |
| 49 | #+END_SRC | |
| 50 | ||
| 51 | ** Atom | |
| 52 | ||
| 53 | [[https://img.cleberg.net/blog/20200125-the-best-linux-software/atom.png]] | |
| 54 | ||
| 55 | [[https://atom.io][Atom]] is the self-proclaimed "hackable text editor for the 21st century". This | |
| 56 | text editor is made by GitHub, [[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 | |
| 57 | add-ons available to customize the layout and abilities of the app. | |
| 58 | ||
| 59 | First, add the Atom repository to your sources. | |
| 60 | ||
| 61 | #+BEGIN_SRC sh | |
| 62 | sudo add-apt-repository ppa:webupd8team/atom | |
| 63 | #+END_SRC | |
| 64 | ||
| 65 | Next, update your package listings and install atom. | |
| 66 | ||
| 67 | #+BEGIN_SRC sh | |
| 68 | sudo apt update && sudo apt install atom | |
| 69 | #+END_SRC | |
| 70 | ||
| 71 | If you have issues updating your packages with the Atom repository, you'll need | |
| 72 | to use the snap package described below instead of the repository. To remove the | |
| 73 | repository we just added, use this command: | |
| 74 | ||
| 75 | #+BEGIN_SRC sh | |
| 76 | sudo add-apt-repository -r ppa:webupd8team/atom | |
| 77 | #+END_SRC | |
| 78 | ||
| 79 | You can also install Atom as a snap package, but it must be installed with the | |
| 80 | `--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 | |
| 81 | about why you need the classic flag. | |
| 82 | ||
| 83 | #+BEGIN_SRC sh | |
| 84 | snap install atom --classic | |
| 85 | #+END_SRC | |
| 86 | ||
| 87 | Using Arch, Manjaro, or another distro using the AUR? Use this command instead: | |
| 88 | ||
| 89 | #+BEGIN_SRC sh | |
| 90 | sudo pacman -S atom | |
| 91 | #+END_SRC | |
| 92 | ||
| 93 | --- | |
| 94 | ||
| 95 | ** Visual Studio Code | |
| 96 | ||
| 97 | [[https://img.cleberg.net/blog/20200125-the-best-linux-software/vscode.png]] | |
| 98 | ||
| 99 | [[https://code.visualstudio.com][Visual Studio Code]] is yet another fantastic choice for programming on Linux, | |
| 100 | especially if you need those extra add-ons to spice up your late-night coding | |
| 101 | sessions. The theme used in the screenshot is [[https://marketplace.visualstudio.com/items?itemName=EliverLara.mars][Mars]] by theme creator [[https://github.com/EliverLara][Eliver Lara]], | |
| 102 | who makes a ton of great themes for VS Code, Atom, and various Linux desktop | |
| 103 | environments. | |
| 104 | ||
| 105 | To install VS Code, you'll need to download the `.deb` file from the official | |
| 106 | website. Once you've downloaded the file, either double-click it to install | |
| 107 | through the Software Center or run the following command: | |
| 108 | ||
| 109 | #+BEGIN_SRC sh | |
| 110 | sudo dpkg -i FILE_NAME.deb | |
| 111 | #+END_SRC | |
| 112 | ||
| 113 | You can also install VS Code as a snap package, but it must be installed with | |
| 114 | the `--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 | |
| 115 | about why you need the classic flag. | |
| 116 | ||
| 117 | #+BEGIN_SRC sh | |
| 118 | snap install code --classic | |
| 119 | #+END_SRC | |
| 120 | ||
| 121 | Using Arch, Manjaro, or another distro using the AUR? Use these commands | |
| 122 | instead: | |
| 123 | ||
| 124 | #+BEGIN_SRC sh | |
| 125 | sudo pacman -S yay binutils make gcc pkg-config fakeroot yay -S visual-studio-code-bin | |
| 126 | #+END_SRC | |
| 127 | ||
| 128 | ** GNOME Tweaks | |
| 129 | ||
| 130 | [[https://img.cleberg.net/blog/20200125-the-best-linux-software/gnome-tweaks.png]] | |
| 131 | ||
| 132 | [[https://gitlab.gnome.org/GNOME/gnome-tweaks][Gnome Tweaks]] is the ultimate tool to use if you want to customize your GNOME | |
| 133 | desktop environment. This is how you can switch application themes (GTK), shell | |
| 134 | themes, icons, fonts, and more. To install GNOME Tweaks on Ubuntu, you just need | |
| 135 | to install the official package. | |
| 136 | ||
| 137 | #+BEGIN_SRC sh | |
| 138 | sudo apt install gnome-tweaks | |
| 139 | #+END_SRC | |
| 140 | ||
| 141 | If you've installed Manjaro or Arch with Gnome, you should have the tweak tool | |
| 142 | pre-installed. If you're on Fedora, this tool is available as an official | |
| 143 | package: | |
| 144 | ||
| 145 | #+BEGIN_SRC sh | |
| 146 | sudo dnf install gnome-tweaks | |
| 147 | #+END_SRC | |
| 148 | ||
| 149 | ** Steam | |
| 150 | ||
| 151 | [[https://img.cleberg.net/blog/20200125-the-best-linux-software/steam.png]] | |
| 152 | ||
| 153 | [[https://steampowered.com][Steam]] is one of the most popular gaming libraries for computers and is one of | |
| 154 | the main reasons that many people have been able to switch to Linux in recent | |
| 155 | years, thanks to Steam Proton, which makes it easier to play games not | |
| 156 | officially created for Linux platforms. | |
| 157 | ||
| 158 | To install Steam on Ubuntu, you just need to install the official package. | |
| 159 | ||
| 160 | #+BEGIN_SRC sh | |
| 161 | sudo apt install steam-installer | |
| 162 | #+END_SRC | |
| 163 | ||
| 164 | For Arch-based systems, you'll simply need to install the =steam= package. | |
| 165 | However, this requires that you enable the =multilib= source. To do so, use the | |
| 166 | following command: | |
| 167 | ||
| 168 | #+BEGIN_SRC sh | |
| 169 | sudo nano /etc/pacman.conf | |
| 170 | #+END_SRC | |
| 171 | ||
| 172 | Now, scroll down and uncomment the =multilib= section. | |
| 173 | ||
| 174 | #+BEGIN_SRC config | |
| 175 | # Before: | |
| 176 | #[multilib] | |
| 177 | #Include = /etc/pacman.d/mirrorlist | |
| 178 | ||
| 179 | # After: | |
| 180 | [multilib] | |
| 181 | Include = /etc/pacman.d/mirrorlist | |
| 182 | #+END_SRC | |
| 183 | ||
| 184 | Finally, install the program: | |
| 185 | ||
| 186 | #+BEGIN_SRC sh | |
| 187 | sudo pacman -S steam | |
| 188 | #+END_SRC | |
| 189 | ||
| 190 | [[./2020-01-26-steam-on-ntfs-drives.html][Problem Launching Steam Games? Click Here.]] | |
| 191 | ||
| 192 | * Command-Line Packages | |
| 193 | ||
| 194 | ** neofetch | |
| 195 | ||
| 196 | [[https://img.cleberg.net/blog/20200125-the-best-linux-software/neofetch.png]] | |
| 197 | ||
| 198 | [[https://github.com/dylanaraps/neofetch][Neofetch]] is a customizable tool used in the command-line to show system | |
| 199 | information. This is exceptionally useful if you want to see your system's | |
| 200 | information quickly without the clutter of some resource-heavy GUI apps. | |
| 201 | ||
| 202 | This is an official package if you're running Ubuntu 17.04 or later, so simply | |
| 203 | use the following command: | |
| 204 | ||
| 205 | #+BEGIN_SRC sh | |
| 206 | sudo apt install neofetch | |
| 207 | #+END_SRC | |
| 208 | ||
| 209 | If you're running Ubuntu 16.10 or earlier, you'll have to use a series of | |
| 210 | commands: | |
| 211 | ||
| 212 | #+BEGIN_SRC sh | |
| 213 | sudo add-apt-repository ppa:dawidd0811/neofetch; sudo apt update; sudo apt install neofetch | |
| 214 | #+END_SRC | |
| 215 | ||
| 216 | Using Arch, Manjaro, or another distro using the AUR? Use this command instead: | |
| 217 | ||
| 218 | #+BEGIN_SRC sh | |
| 219 | sudo pacman -S neofetch | |
| 220 | #+END_SRC | |
| 221 | ||
| 222 | ** yt-dlp | |
| 223 | ||
| 224 | [[https://img.cleberg.net/blog/20200125-the-best-linux-software/yt-dlp.png]] | |
| 225 | ||
| 226 | [[https://github.com/yt-dlp/yt-dlp][yt-dlp]] is an extremely handy command-line tool that allows you to download video | |
| 227 | or audio files from various websites, such as YouTube. There are a ton of | |
| 228 | different options when running this package, so be sure to run `yt-dlp --help` | |
| 229 | first to look through everything you can do (or give up and search for the best | |
| 230 | config online). | |
| 231 | ||
| 232 | While this shouldn't be a problem for most users, yt-dlp requires Python 2.6, | |
| 233 | 2.7, or 3.2+ to work correctly, so install Python if you don't have it already. | |
| 234 | You can check to see if you have Python installed by running: | |
| 235 | ||
| 236 | #+BEGIN_SRC sh | |
| 237 | python -V | |
| 238 | #+END_SRC | |
| 239 | ||
| 240 | To get the youtube-dl package, simply curl the URL and output the results. | |
| 241 | ||
| 242 | #+BEGIN_SRC sh | |
| 243 | sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp | |
| 244 | #+END_SRC | |
| 245 | ||
| 246 | Finally, make the file executable so that it can be run from the command-line. | |
| 247 | ||
| 248 | #+BEGIN_SRC sh | |
| 249 | sudo chmod a+rx /usr/local/bin/yt-dlp | |
| 250 | #+END_SRC | |
blog/2020-01-26-steam-on-ntfs.org deleted −88
| @@ -1,88 +0,0 @@ | ||
| 1 | #+date: 2020-01-26 | |
| 2 | #+title: Linux Gaming Tweak: Steam on NTFS Drives | |
| 3 | ||
| 4 | * Auto-Mount Steam Drives | |
| 5 | ||
| 6 | [[https://img.cleberg.net/blog/20200125-the-best-linux-software/steam.png]] | |
| 7 | ||
| 8 | If you want to see how to install Steam on Linux, see my other post: [[./2020-01-25-linux-software.html][Linux Software]]. | |
| 9 | ||
| 10 | Are you having trouble launching games, even though they've installed correctly? | |
| 11 | This may happen if you're storing your games on an NTFS-formatted drive. This | |
| 12 | shouldn't be an issue if you're storing your games on the same drive that Steam | |
| 13 | is on, but some gamers prefer to put Steam on their main drive and game files on | |
| 14 | another SSD or HDD. | |
| 15 | ||
| 16 | To fix this problem, you'll need to try a few things. First, you'll need to | |
| 17 | install the =ntfs-3g= package, which is meant for better interoperability with | |
| 18 | Linux. | |
| 19 | ||
| 20 | #+BEGIN_SRC sh | |
| 21 | sudo apt install ntfs-3g | |
| 22 | #+END_SRC | |
| 23 | ||
| 24 | Next, you should set up the =/etc/fstab= file to automatically mount your drives | |
| 25 | on boot. To automatically mount your drives when the computer boots up, you'll | |
| 26 | have to create the folders you want to mount your drive to first. I store mine | |
| 27 | in the =/mnt= folder using names that I'll recognize, but you can create your | |
| 28 | folders wherever you want. | |
| 29 | ||
| 30 | #+BEGIN_SRC sh | |
| 31 | mkdir /path/to/folder | |
| 32 | #+END_SRC | |
| 33 | ||
| 34 | For example: | |
| 35 | ||
| 36 | #+BEGIN_SRC sh | |
| 37 | mkdir /mnt/steam_library | |
| 38 | #+END_SRC | |
| 39 | ||
| 40 | To automatically mount drives upon system boot, you will need to collect a few | |
| 41 | items. The UUID is the identification number connected to whichever drive you're | |
| 42 | using to store Steam games. | |
| 43 | ||
| 44 | Drives are usually labeled similar to =/dev/nvme0n1p1= or =/dev/sda1=, so you'll | |
| 45 | need to find the line in the output of the command below that correlates to your | |
| 46 | drive and copy the UUID over to the =/etc/fstab= file. | |
| 47 | ||
| 48 | #+BEGIN_SRC sh | |
| 49 | sudo blkid | grep UUID= | |
| 50 | #+END_SRC | |
| 51 | ||
| 52 | Next, you'll need your =uid= and =gid=. To find these, run the following | |
| 53 | command: | |
| 54 | ||
| 55 | #+BEGIN_SRC sh | |
| 56 | id -u && id -g | |
| 57 | #+END_SRC | |
| 58 | ||
| 59 | Now that you have collected the necessary information, open the `/etc/fstab` | |
| 60 | file: | |
| 61 | ||
| 62 | #+BEGIN_SRC sh | |
| 63 | sudo nano /etc/fstab | |
| 64 | #+END_SRC | |
| 65 | ||
| 66 | Each drive you want to mount on boot should have its own line in the | |
| 67 | =/etc/fstab= file that looks similar to this: | |
| 68 | ||
| 69 | #+BEGIN_SRC config | |
| 70 | UUID=B64E53824E5339F7 /mnt/steam_library ntfs-3g uid=1000,gid=1000 0 0 | |
| 71 | #+END_SRC | |
| 72 | ||
| 73 | Now all you need to do is unmount your drive and re-mount it. You can unmount | |
| 74 | the drive by doing this (be sure to use the correct drive name here): | |
| 75 | ||
| 76 | #+BEGIN_SRC sh | |
| 77 | sudo umount /dev/sdxX | |
| 78 | #+END_SRC | |
| 79 | ||
| 80 | You can re-mount all your drives by executing the following: | |
| 81 | ||
| 82 | #+BEGIN_SRC sh | |
| 83 | sudo mount -a | |
| 84 | #+END_SRC | |
| 85 | ||
| 86 | If you don't know what your drive name is, or you're nervous about unmounting | |
| 87 | and re-mounting, simply reboot your computer, and it will be done for you | |
| 88 | automatically. | |
blog/2020-02-09-cryptography-basics.org deleted −163
| @@ -1,163 +0,0 @@ | ||
| 1 | #+date: 2020-02-09 | |
| 2 | #+title: Cryptography Basics | |
| 3 | ||
| 4 | * Similar Article Available | |
| 5 | ||
| 6 | If you haven't already, feel free to read my post on [[./2018-12-08-aes-encryption.html][AES Encryption]]. | |
| 7 | ||
| 8 | * What is Cryptography? | |
| 9 | ||
| 10 | In layman's terms, cryptography is a process that can change data from a | |
| 11 | readable format into an unreadable format (and vice-versa) through a series of | |
| 12 | processes and secrets. More technically, this is the Internet Security | |
| 13 | Glossary's definition: | |
| 14 | ||
| 15 | #+BEGIN_QUOTE | |
| 16 | [Cryptography is] the mathematical science that deals with transforming data | |
| 17 | to render its meaning unintelligible (i.e., to hide its semantic content), | |
| 18 | prevent its undetected alteration, or prevent its unauthorized use. If the | |
| 19 | transformation is reversible, cryptography also deals with restoring encrypted | |
| 20 | data to an intelligible form. | |
| 21 | ||
| 22 | - [[https://tools.ietf.org/html/rfc2828][Internet Security Glossary (2000)]] | |
| 23 | #+END_QUOTE | |
| 24 | ||
| 25 | Cryptography cannot offer protection against the loss of data; it simply offers | |
| 26 | encryption methods to protect data at-rest and data in-traffic. At a high-level, | |
| 27 | encrypted is when plaintext data is encrypted to ciphertext (a secure form of | |
| 28 | text that cannot be understood unless decrypted back to plaintext). The | |
| 29 | encryption process is completed through the use of a mathematical function that | |
| 30 | utilizes one or more values called keys to encrypt or decrypt the data. | |
| 31 | ||
| 32 | * Key Elements of Cryptographic Systems | |
| 33 | ||
| 34 | To create or evaluate a cryptographic system, you need to know the essential | |
| 35 | pieces to the system: | |
| 36 | ||
| 37 | - *Encryption Algorithm (Primitive):* A mathematical process that encrypts and | |
| 38 | decrypts data. | |
| 39 | - *Encryption Key:* A string of bits used within the encryption algorithm as | |
| 40 | the secret that allows successful encryption or decryption of data. | |
| 41 | - *Key Length (Size):* The maximum number of bits within the encryption key. | |
| 42 | It's important to remember that key size is regulated in many countries. | |
| 43 | - *Message Digest:* A smaller, fixed-size bit string version of the original | |
| 44 | message. This is practically infeasible to reverse, which is why it's commonly | |
| 45 | used to verify integrity. | |
| 46 | ||
| 47 | * Symmetric Systems (Secret Key Cryptography) | |
| 48 | ||
| 49 | Symmetric cryptography utilizes a secret, bidirectional key to perform both | |
| 50 | encryption and decryption of the data. The most common implementation of | |
| 51 | symmetric cryptography is the Advanced Encryption Standard, which uses keys that | |
| 52 | are 128 bits to 256 bits in size. This standard came after the National | |
| 53 | Institute of Standards and Technology (NIST) decided to retire the Data | |
| 54 | Encryption Standard (DES) in 2001. | |
| 55 | ||
| 56 | Since brute force attacks strongly correlate with key length, the 56-bit key | |
| 57 | length of DES was considered insecure after it was publicly broken in under 24 | |
| 58 | hours. However, there is a modern implementation of DES called Triple DES where | |
| 59 | the DES method is applied three times to each data block. | |
| 60 | ||
| 61 | The main advantages to symmetric systems are the ease of use, since only one key | |
| 62 | is required for both encryption and decryption, and the simplicity of the | |
| 63 | algorithms. This helps with bulk data encryption that may unnecessarily waste | |
| 64 | time and power using asymmetric systems. | |
| 65 | ||
| 66 | However, symmetric systems have disadvantages to keep in mind. Since the key is | |
| 67 | private, it can be difficult to safely distribute keys to communication | |
| 68 | partners. Additionally, the key cannot be used to sign messages since it's | |
| 69 | necessary to keep the key private. | |
| 70 | ||
| 71 | * Asymmetric Systems (Public Key Cryptography) | |
| 72 | ||
| 73 | Asymmetric cryptography utilizes two keys within the system: a secret key that | |
| 74 | is privately-held and a public key that can be distributed freely. The | |
| 75 | interesting aspect of asymmetric cryptography is that either key can be used to | |
| 76 | encrypt the data, there's no rule that dictates which key must be used for | |
| 77 | encryption. Once one key is used to encrypt the data, only the other key can be | |
| 78 | used to decrypt the data. This means that if the private key encrypts the data, | |
| 79 | only the public key can decrypt the data. | |
| 80 | ||
| 81 | An advantage of this system is that if you successfully decrypt data using one | |
| 82 | of the keys, you can be sure of the sender since only the other key could have | |
| 83 | encrypted the data. | |
| 84 | ||
| 85 | One of the major implementations of an asymmetric system is a digital signature. | |
| 86 | A digital signature can be generated using the sender's private key, or a | |
| 87 | one-way hash function and is used to provide assurance for the integrity and | |
| 88 | authenticity of the message. A couple common message digest algorithms are | |
| 89 | SHA-256 and SHA-512, which securely compress data and produce a 128-bit message | |
| 90 | digest. | |
| 91 | ||
| 92 | It should be noted that man-in-the-middle attacks are one of the risks with | |
| 93 | digital signatures and public keys. To combat this, applications often use a | |
| 94 | public key infrastructure (PKI) to independently authenticate the validity of | |
| 95 | signatures and keys. | |
| 96 | ||
| 97 | Due to the large key size and [[https://crypto.stackexchange.com/a/591][inefficient mathematical functions]] of asymmetric | |
| 98 | encryption, elliptical curve cryptography (ECC) is often used to increase | |
| 99 | security while using fewer resources. | |
| 100 | ||
| 101 | * Applications of Cryptographic Systems | |
| 102 | ||
| 103 | There are quite a few implementations of cryptographic systems around the world. | |
| 104 | Here are a few popular examples: | |
| 105 | ||
| 106 | *Transport Layer Security (TLS):* One of the most famous cryptographic | |
| 107 | solutions created is TLS, a session-layered or connection-layered internet | |
| 108 | protocol that allows for secure communications between browsers and servers. | |
| 109 | Using handshakes, peer negotiation, and authentication allows TLS to prevent | |
| 110 | eavesdropping and malicious transformation of data. The major reason for TLS | |
| 111 | popularity is that a major vulnerability was found in the SSL protocol in 2014. | |
| 112 | Instead of SSL, TLS can be used with HTTP to form HTTPS and is the preferred | |
| 113 | method for modern web development due to its increased security. | |
| 114 | ||
| 115 | *Secure Hypertext Transfer Protocol (HTTPS):* An application layer protocol | |
| 116 | that allows for secure transport of data between servers and web clients. One of | |
| 117 | the unique parts of HTTPS is that it uses a secured port number instead of the | |
| 118 | default web port address. | |
| 119 | ||
| 120 | *Virtual Private Network (VPN):* VPNs are made to securely extend a private | |
| 121 | network across public networks by utilizing an encrypted layered tunneling | |
| 122 | protocol paired with an authentication method, such as usernames and passwords. | |
| 123 | This technology originally allowed remote employees to access their company's | |
| 124 | data but have evolved into one of the top choices for anyone who wishes to mask | |
| 125 | their sensitive personal data. | |
| 126 | ||
| 127 | *Internet Protocol Security (IPSec):* This protocol suite facilitates | |
| 128 | communication between two or more hosts or subnets by authenticating and | |
| 129 | encrypting the data packets. IPSec is used in a lot of VPNs to establish the VPN | |
| 130 | connection through the transport and tunnel mode encryption methods. IPSec | |
| 131 | encrypts just the data portion of packets in the transport methods, but it | |
| 132 | encrypts both the data and headers in the tunnel method (introducing an | |
| 133 | additional header for authentication). | |
| 134 | ||
| 135 | *Secure Shell (SSH):* SSH is another network protocol used to protect network | |
| 136 | services by authenticating users through a secure channel. This protocol is | |
| 137 | often used for command-line (shell) functions such as remote shell commands, | |
| 138 | logins, and file transfers. | |
| 139 | ||
| 140 | *Kerberos:* Developed by MIT, Kerberos is a computer-network authentication | |
| 141 | protocol that works on the basis of tickets to allow nodes communicating over a | |
| 142 | non-secure network to prove their identity to one another securely. This is most | |
| 143 | commonly used in business environments when used as the authentication and | |
| 144 | encryption method for Windows Active Directory (AD). | |
| 145 | ||
| 146 | * Cybersecurity Controls | |
| 147 | ||
| 148 | If you're someone who needs solutions on how to control risks associated with | |
| 149 | utilizing a crytograhpic system, start with a few basic controls: | |
| 150 | ||
| 151 | - *Policies:* A policy on the use of cryptographic controls for protection | |
| 152 | of information is implemented and is in accordance with organizational | |
| 153 | objectives. | |
| 154 | - *Key management:* A policy on the use, protection and lifetime of | |
| 155 | cryptographic keys is implemented through the entire application lifecycle. | |
| 156 | - *Key size:* The organization has researched the optimal key size for their | |
| 157 | purposes, considering national laws, required processing power, and | |
| 158 | longevity of the solution. | |
| 159 | - *Algorithm selection:* Implemented algorithms are sufficiently appropriate | |
| 160 | for the business of the organization, robust, and align with recommended | |
| 161 | guidelines. | |
| 162 | - *Protocol configuration:* Protocols have been reviewed and configured | |
| 163 | suitable to the purpose of the business. | |
blog/2020-03-25-session-messenger.org deleted −127
| @@ -1,127 +0,0 @@ | ||
| 1 | #+date: 2020-03-25 | |
| 2 | #+title: Session Private Messenger | |
| 3 | ||
| 4 | * Privacy Warning | |
| 5 | ||
| 6 | The company behind Session (Loki Foundation) is from Australia. If you didn't | |
| 7 | know, Australia has introduced [[https://parlinfo.aph.gov.au/parlInfo/download/legislation/bills/r6195_aspassed/toc_pdf/18204b01.pdf][legislation]] mandating companies comply with | |
| 8 | government requests to build backdoor access into applications. For more | |
| 9 | information, read my article on [[./2020-01-25-aes-encryption.html][AES Encryption]]. | |
| 10 | ||
| 11 | * About Session | |
| 12 | ||
| 13 | [[https://getsession.org][Session]] is a private, cross-platform messaging app from the [[https://loki.foundation][Loki Foundation]]. As | |
| 14 | someone who has spent years looking for quality alternatives to major messaging | |
| 15 | apps, I was excited when I first heard about Session. Reading through [[https://arxiv.org/pdf/2002.04609.pdf][Session's | |
| 16 | white paper]], you can learn the technologies behind the Session app. Part of the | |
| 17 | security of Session comes from the Signal protocol, which was forked as the | |
| 18 | origin of Session. | |
| 19 | ||
| 20 | #+BEGIN_QUOTE | |
| 21 | Session is an end-to-end encrypted messenger that removes sensitive metadata | |
| 22 | collection, and is designed for people who want privacy and freedom from any | |
| 23 | forms of surveillance. | |
| 24 | #+END_QUOTE | |
| 25 | ||
| 26 | In general, this app promises security through end-to-end encryption, | |
| 27 | decentralized onion routing, and private identities. The biggest change that the | |
| 28 | Loki Foundation has made to the Signal protocol is removing the need for a phone | |
| 29 | number. Instead, a random identification string is generated for any session you | |
| 30 | create. This means you can create a new session for each device if you want to, | |
| 31 | or link new devices with your ID. | |
| 32 | ||
| 33 | Since Session's website and white paper describe the details of Session's | |
| 34 | security, I'm going to focus on using the app in this post. | |
| 35 | ||
| 36 | * Features | |
| 37 | ||
| 38 | Since most people are looking for an alternative to a popular chat app, I am | |
| 39 | going to list out the features that Session has so that you are able to | |
| 40 | determine if the app would suit your needs: | |
| 41 | ||
| 42 | - Multiple device linking (via QR code or ID) | |
| 43 | - App locking via device screen lock, password, or fingerprint | |
| 44 | - Screenshot blocking | |
| 45 | - Incognito keyboard | |
| 46 | - Read receipts and typing indicators | |
| 47 | - Mobile notification customization | |
| 48 | - Old message deletion and conversation limit | |
| 49 | - Backups | |
| 50 | - Recovery phrase | |
| 51 | - Account deletion, including ID, messages, sessions, and contacts | |
| 52 | ||
| 53 | * Downloads | |
| 54 | ||
| 55 | I have tested this app on Ubuntu 19.10, Android 10, macOS Monterey, and iOS 15. | |
| 56 | All apps have worked well without many issues. | |
| 57 | ||
| 58 | Below is a brief overview of the Session app on Linux. To get this app, you'll | |
| 59 | need to go to the [[https://getsession.org/download/][Downloads]] page and click to link to the operating system | |
| 60 | you're using. | |
| 61 | ||
| 62 | For Linux, it will download an AppImage that you'll need to enable with the | |
| 63 | following command: | |
| 64 | ||
| 65 | #+BEGIN_SRC sh | |
| 66 | sudo chmod u+x session-messenger-desktop-linux-x86_64-1.0.5.AppImage | |
| 67 | #+END_SRC | |
| 68 | ||
| 69 | #+CAPTION: Session Download Options | |
| 70 | [[https://img.cleberg.net/blog/20200325-session-private-messenger/session_downloads.png]] | |
| 71 | ||
| 72 | * Creating an Account | |
| 73 | ||
| 74 | Once you've installed the app, simply run the app and create your unique Session | |
| 75 | ID. It will look something like this: | |
| 76 | =05af1835afdd63c947b47705867501d6373f486aa1ae05b1f2f3fcd24570eba608=. | |
| 77 | ||
| 78 | You'll need to set a display name and, optionally, a password. If you set a | |
| 79 | password, you will need to enter it every time you open the app. | |
| 80 | ||
| 81 | #+CAPTION: Session Login (Linux) | |
| 82 | [[https://img.cleberg.net/blog/20200325-session-private-messenger/session_linux_login.png]] | |
| 83 | ||
| 84 | #+CAPTION: Session Login (macOS) | |
| 85 | [[https://img.cleberg.net/blog/20200325-session-private-messenger/session_macos_login.png]] | |
| 86 | ||
| 87 | #+CAPTION: Password Authentication | |
| 88 | [[https://img.cleberg.net/blog/20200325-session-private-messenger/session_password_authentication.png]] | |
| 89 | ||
| 90 | * Start Messaging | |
| 91 | ||
| 92 | Once you've created your account and set up your profile details, the next step | |
| 93 | is to start messaging other people. To do so, you'll need to share your Session | |
| 94 | ID with other people. From this point, it's fairly straightforward and acts like | |
| 95 | any other messaging app, so I won't dive into much detail here. | |
| 96 | ||
| 97 | ** macOS | |
| 98 | ||
| 99 | #+CAPTION: macOS Conversations | |
| 100 | [[https://img.cleberg.net/blog/20200325-session-private-messenger/session_macos_conversations.png]] | |
| 101 | ||
| 102 | One key feature to note is that the desktop application now provides a helpful | |
| 103 | pop-up box explaining the process that Session uses to hide your IP address: | |
| 104 | ||
| 105 | #+CAPTION: IP Address Help Box | |
| 106 | [[https://img.cleberg.net/blog/20200325-session-private-messenger/session_ip.png]] | |
| 107 | ||
| 108 | ** iOS | |
| 109 | ||
| 110 | The mobile app is quite simple and effective, giving you all the standard mobile | |
| 111 | messaging options you'd expect. | |
| 112 | ||
| 113 | #+CAPTION: iOS App | |
| 114 | [[https://img.cleberg.net/blog/20200325-session-private-messenger/session_ios.png]] | |
| 115 | ||
| 116 | * Potential Issues | |
| 117 | ||
| 118 | I've discovered one annoying issue that would prevent from using this app | |
| 119 | regularly. On a mobile device, there have been issues with receiving messages on | |
| 120 | time. Even with battery optimization disabled and no network restrictions, | |
| 121 | Session notifications sometimes do not display until I open the app or the | |
| 122 | conversation itself and wait a few moments. This is actually one of the reasons | |
| 123 | I stopped using Signal (this seems fixed as of my updates in 2021/2022, | |
| 124 | so I wouldn't worry about this issue anymore). | |
| 125 | ||
| 126 | Looking for another messenger instead of Session? I recommend Signal, Matrix, | |
| 127 | and IRC. | |
blog/2020-05-03-homelab.org deleted −151
| @@ -1,151 +0,0 @@ | ||
| 1 | #+date: 2020-05-03 | |
| 2 | #+title: An Inside Look at My Homelab | |
| 3 | ||
| 4 | * What is a Homelab? | |
| 5 | ||
| 6 | Starting as a developer, I have largely stayed away from hardware-based hobbies | |
| 7 | (other than building a gaming desktop). However, as the quarantine for COVID-19 | |
| 8 | stretches out further and further, I found myself bored and in search of new | |
| 9 | hobbies. After spending the last few months browsing the [[https://www.reddit.com/r/homelab/][r/homelab]] subreddit, I | |
| 10 | decided it was time to jump in and try things out for myself. | |
| 11 | ||
| 12 | Since I am a beginner and just recently graduated from college, everything I've | |
| 13 | done so far in my homelab is fairly low-budget. | |
| 14 | ||
| 15 | * Hardware | |
| 16 | ||
| 17 | #+CAPTION: HomeLab Diagram | |
| 18 | [[https://img.cleberg.net/blog/20200503-homelab/homelab-min.png]] | |
| 19 | ||
| 20 | ** Raspberry Pi 4 | |
| 21 | ||
| 22 | Luckily, I had actually purchased a [[https://www.raspberrypi.org/products/raspberry-pi-4-model-b/][Raspberry Pi 4]] before the quarantine started | |
| 23 | so that I could try to keep Plex Media Center running, even while my desktop | |
| 24 | computer was turned off. I started here, using the Pi to hold Plex and Pi-hole | |
| 25 | until I grew tired with the slow performance. | |
| 26 | ||
| 27 | Here are the specifications for the Pi 4: | |
| 28 | ||
| 29 | - Broadcom BCM2711, Quad core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz | |
| 30 | - 4GB LPDDR4-3200 SDRAM | |
| 31 | - Gigabit Ethernet | |
| 32 | - H.265 (4kp60 decode), H264 (1080p60 decode, 1080p30 encode) | |
| 33 | - 64 GB MicroSD Card | |
| 34 | ||
| 35 | ** Dell Optiplex 5040 | |
| 36 | ||
| 37 | Since I wasn't happy with the Pi as my main server, I turned to Craigslist. I | |
| 38 | know a lot of other homelabbers use Ebay, but I can't seem to ever trust it | |
| 39 | enough to purchase items on there. So I used Craigslist and found a Dell | |
| 40 | Optiplex 5040 desktop computer on sale for $90. While this computer might be | |
| 41 | underpowered, it was one of the few computers under $100 that was available | |
| 42 | during quarantine. | |
| 43 | ||
| 44 | Here are the specifications for the Dell Optiplex 5040: | |
| 45 | ||
| 46 | - Intel Core i3 6100 | |
| 47 | - 8GB RAM DDR3 | |
| 48 | - Intel HD Graphics | |
| 49 | - Gigabit Ethernet | |
| 50 | - 500GB Hard Drive | |
| 51 | ||
| 52 | While this hardware would be awful for a work computer or a gaming rig, it | |
| 53 | turned out to be wonderful for my server purposes. The only limitation I have | |
| 54 | found so far is the CPU. The i3-6100 only has enough power for a single 4k video | |
| 55 | transcode at a time. I haven't tested more than three 1080p streams at a time, | |
| 56 | but the maximum amount of streams I've ever actually used is two. | |
| 57 | ||
| 58 | ** WD easystore 10TB & 8TB | |
| 59 | ||
| 60 | Application storage and temporary files are stored on the internal hard drive of | |
| 61 | the server, but all media files (movies, tv, games, books, etc) are stored | |
| 62 | externally on my WD easystore hard drive. Creating auto-boot configurations in | |
| 63 | the =/etc/fstab= file on my server allows the hard drives to automatically mount | |
| 64 | whenever I need to restart my server. | |
| 65 | ||
| 66 | #+BEGIN_QUOTE | |
| 67 | Update: In March 2022, I shucked the hard drives out of their external cases, | |
| 68 | put some Kapton tape on the third power pin to prevent power shutdowns, and | |
| 69 | stuck them inside my server tower using internal SATA cables. | |
| 70 | #+END_QUOTE | |
| 71 | ||
| 72 | ** Netgear Unmanaged Switch | |
| 73 | ||
| 74 | To manage all the ethernet cords used by my homelab, my desktop, and my living | |
| 75 | room media center, I purchased an 8-port gigabit ethernet switch for $50 at my | |
| 76 | local computer store. This is probably much more than I should have spent on an | |
| 77 | unmanaged switch, but I am comfortable with the choice. | |
| 78 | ||
| 79 | ** TP-Link Managed Switch | |
| 80 | ||
| 81 | Since I use the unmanaged switch to group all living room devices together, I | |
| 82 | use the managed switch to configure VLANs and secure my network. | |
| 83 | ||
| 84 | ** Arris TM1602A Modem & Sagecom Fast 5280 Router | |
| 85 | ||
| 86 | My default modem and router, provided by my ISP, are fairly standard. The Arris | |
| 87 | modem supports DOCSIS 3.0, which is something that I definitely wanted as a | |
| 88 | minimum. The Sagecom router is also standard, no fancy bells or whistles. | |
| 89 | However, it does support DHCP and DHCPv6, which is something you can use to | |
| 90 | route all household traffic through a pi-hole or firewall. | |
| 91 | ||
| 92 | ** TP-Link EAP | |
| 93 | ||
| 94 | In order to gain better control over the network, I use my own wireless access | |
| 95 | point instead of the one included in the Sagecom router above. Now I can control | |
| 96 | and organize all of my ethernet connections through the VLANs on the managed | |
| 97 | switch and wireless connections through the VLANS on the EAP. | |
| 98 | ||
| 99 | ** Generic Printer | |
| 100 | ||
| 101 | The last piece to my homelab is a standard wireless printer. Nothing special | |
| 102 | here. | |
| 103 | ||
| 104 | * Software | |
| 105 | ||
| 106 | ** Ubuntu Server 20.04 | |
| 107 | ||
| 108 | While the 20.04 version of Ubuntu was just released, I always like to experiment | |
| 109 | with new features (and I don't mind breaking my system - it just gives me more | |
| 110 | experience learning how to fix things). So, I have Ubuntu Server 20.04 installed | |
| 111 | on the Dell Optiplex server and Ubuntu Server 19.10 installed on the Raspberry | |
| 112 | Pi. Once I find an acceptable use for the Pi, I will most likely switch the | |
| 113 | operating system. | |
| 114 | ||
| 115 | ** Docker | |
| 116 | ||
| 117 | I am /very/ new to Docker, but I have had a lot of fun playing with it so far. | |
| 118 | Docker is used to create containers that can hold all the contents of a system | |
| 119 | without interfering with other software on the same system. So far, I have | |
| 120 | successfully installed pi-hole, GitLab, Gogs, and Nextcloud in containers. | |
| 121 | However, I opted to delete all of those so that I can reconfigure them more | |
| 122 | professionally at a later time. | |
| 123 | ||
| 124 | ** Plex Media Server | |
| 125 | ||
| 126 | Plex is a media center software that allows you to organize your movies, TV | |
| 127 | shows, music, photos, and videos automatically. It will even download metadata | |
| 128 | for you so that you can easily browse these collections. | |
| 129 | ||
| 130 | ** Pi-hole | |
| 131 | ||
| 132 | Pi-hole is an alternative ad-blocker that runs at the DNS level, allowing you to | |
| 133 | block traffic when it hits your network, so that you can reject any traffic you | |
| 134 | deem to be bad. Pi-hole uses blacklists and whitelists to decide which traffic | |
| 135 | block and, luckily, there are a lot of pre-made lists out there on Reddit, | |
| 136 | GitHub, etc. | |
| 137 | ||
| 138 | ** Nextcloud | |
| 139 | ||
| 140 | While I had trouble with the Docker version of Nextcloud, I was very successful | |
| 141 | when setting up the snap version. Using this, I was able to map Nextcloud to a | |
| 142 | subdomain of a domain I own in Namecheap. Additionally, Nextcloud has an | |
| 143 | integration with Let's Encrypt that allows me to issue certificates | |
| 144 | automatically to any new domain I authorize. | |
| 145 | ||
| 146 | ** Webmin | |
| 147 | ||
| 148 | To monitor my servers, and the processes running on them, I use the Webmin | |
| 149 | dashboard. This was fairly painless to set up, and I currently access it | |
| 150 | straight through the server's IP address. In the future, I will be looking to | |
| 151 | configure Webmin to use a custom domain just like Nextcloud. | |
blog/2020-05-19-customizing-ubuntu.org deleted −185
| @@ -1,185 +0,0 @@ | ||
| 1 | #+date: 2020-05-19 | |
| 2 | #+title: Beginner's Guide: Customizing Ubuntu | |
| 3 | ||
| 4 | * More Information | |
| 5 | ||
| 6 | For inspiration on designing your \*nix computer, check out the [[https://libredd.it/r/unixporn][r/unixporn]] | |
| 7 | subreddit! | |
| 8 | ||
| 9 | * Customizing Ubuntu | |
| 10 | ||
| 11 | New to Linux and want to add a personal touch to your machine? One of the best | |
| 12 | perks of Linux is that it is *extremely* customizable. You can change the | |
| 13 | styles of the windows, shell (status bars/docks), icons, fonts, terminals, and | |
| 14 | more. | |
| 15 | ||
| 16 | In this post, I'm going to go through customization on Ubuntu 20.04 (GNOME) | |
| 17 | since most new users tend to choose Ubuntu-based distros. If you've found a way | |
| 18 | to install Arch with i3-gaps, I'm assuming you know how to find more advanced | |
| 19 | tutorials out there on customizations. | |
| 20 | ||
| 21 | ** Required Tools | |
| 22 | ||
| 23 | #+CAPTION: Gnome Tweaks | |
| 24 | [[https://img.cleberg.net/blog/20200519-customizing-ubuntu/gnome-tweaks-min.png]] | |
| 25 | ||
| 26 | Ubuntu 20.04 ships with the default desktop environment [[https://www.gnome.org/][Gnome]], which includes | |
| 27 | the handy =gnome-tweaks= tool to quickly change designs. To install this, just | |
| 28 | open your terminal and enter the following command: | |
| 29 | ||
| 30 | #+BEGIN_SRC sh | |
| 31 | sudo apt install gnome-tweaks | |
| 32 | #+END_SRC | |
| 33 | ||
| 34 | After you've finished installing the tool, simply launch the Tweaks application, | |
| 35 | and you'll be able to access the various customization options available by | |
| 36 | default on Ubuntu. You might even like some of the pre-installed options. | |
| 37 | ||
| 38 | ** GNOME Application Themes | |
| 39 | ||
| 40 | To change the themes applied to applications in GNOME, you will need to change | |
| 41 | the Applications dropdown in the Appearance section of Tweaks. To add more | |
| 42 | themes, you will have to find your preferred theme online and follow the steps | |
| 43 | below to have it show up in the Tweaks tool. While you may find themes anywhere, | |
| 44 | one of the most popular sites for GNOME themes is [[https://www.gnome-look.org/][gnome-look.org]]. This website | |
| 45 | contains themes for applications, shells, icons, and cursors. | |
| 46 | ||
| 47 | Steps to import themes into Tweaks: | |
| 48 | ||
| 49 | 1. Download the theme. | |
| 50 | 2. These files are usually compressed (.zip, .tar.gz, .tar.xz), so you will need | |
| 51 | to extract the contents. This is easiest when opening the file explorer, | |
| 52 | right-clicking the compressed file, and choosing "Extract here." | |
| 53 | 3. Move the theme folder to =/usr/share/themes/=. You can do so with the | |
| 54 | following command: =sudo mv theme-folder/ /usr/share/themes/=. | |
| 55 | - Icons and cursors will be moved to the =/usr/share/icons/= folder. | |
| 56 | - Fonts will be moved to the =/usr/share/fonts/= folder Alternatively, you | |
| 57 | can move them to the =/usr/share/fonts/opentype/= or | |
| 58 | =/usr/share/fonts/opentype/= folders, if you have a specific font type. | |
| 59 | 4. Close tweaks if it is open. Re-open Tweaks and your new theme will be | |
| 60 | available in the Applications dropdown in the Appearance section of Tweaks. | |
| 61 | ||
| 62 | If the theme is not showing up after you've moved it into the themes folder, you | |
| 63 | may have uncompressed the folder into a sub-folder. You can check this by | |
| 64 | entering the theme folder and listing the contents: | |
| 65 | ||
| 66 | #+BEGIN_SRC sh | |
| 67 | cd /usr/share/themes/Mojave-Dark && ls -la | |
| 68 | #+END_SRC | |
| 69 | ||
| 70 | This is an example of what the contents of your theme folder should look like. | |
| 71 | If you just see another folder there, you should move that folder up into the | |
| 72 | =/usr/share/themes/= folder. | |
| 73 | ||
| 74 | #+BEGIN_SRC sh | |
| 75 | cinnamon COPYING gnome-shell gtk-2.0 gtk-3.0 index.theme metacity-1 plank xfwm4 | |
| 76 | #+END_SRC | |
| 77 | ||
| 78 | ** GNOME Shell Themes | |
| 79 | ||
| 80 | To change the appearance of the title bar, default dock, app menu, and other | |
| 81 | parts of the GNOME shell, you'll need to install the [[https://extensions.gnome.org/extension/19/user-themes/][user themes]] extension on | |
| 82 | [[https://extensions.gnome.org/][Gnome Extensions]]. To be able to install extensions, you will first need to | |
| 83 | install the browser extension that the website instructs you to. See this | |
| 84 | screenshot for the blue box with a link to the extension. | |
| 85 | ||
| 86 | #+CAPTION: Gnome Extensions | |
| 87 | [[https://img.cleberg.net/blog/20200519-customizing-ubuntu/gnome-extensions-min.png]] | |
| 88 | ||
| 89 | After the browser extension is installed, you will need to install the native | |
| 90 | host connector: | |
| 91 | ||
| 92 | #+BEGIN_SRC sh | |
| 93 | sudo apt install chrome-gnome-shell | |
| 94 | #+END_SRC | |
| 95 | ||
| 96 | Finally, you can go the [[https://extensions.gnome.org/extension/19/user-themes/][user themes]] extension page and click the installation | |
| 97 | button. This will enable the Shell option in Tweaks. Now you can move shell | |
| 98 | themes to the =/usr/share/themes= directory, using the same steps mentioned in | |
| 99 | the previous section, and enable the new theme in Tweaks. | |
| 100 | ||
| 101 | ** Icons & Cursors | |
| 102 | ||
| 103 | Icons and cursors are installed exactly the same way, so I'm grouping these | |
| 104 | together in this post. Both of these items will need to follow the same process | |
| 105 | as installing themes, except you will want to move your font folders to the | |
| 106 | =/usr/share/icons/= directory instead. | |
| 107 | ||
| 108 | ** Fonts | |
| 109 | ||
| 110 | Fonts are one of the overlooked parts of customization, but a good font can make | |
| 111 | the whole screen look different. For example, I have installed the [[https://github.com/IBM/plex/releases][IBM Plex]] | |
| 112 | fonts on my system. This follows the same process as installing themes, except | |
| 113 | you will want to move your font folders to the =/usr/share/fonts/= directory | |
| 114 | instead. | |
| 115 | ||
| 116 | ** Terminal | |
| 117 | ||
| 118 | If you spend a lot of time typing commands, you know how important the style and | |
| 119 | functionality of the terminal is. After spending a lot of time using the default | |
| 120 | GNOME terminal with [[https://en.wikipedia.org/wiki/Bash_(Unix_shell)][unix shell]], I decided to try some different options. I ended | |
| 121 | up choosing [[https://terminator-gtk3.readthedocs.io/en/latest/][Terminator]] with [[https://en.wikipedia.org/wiki/Z_shell][zsh]]. | |
| 122 | ||
| 123 | Terminator is great if you need to open multiple terminals at one time by simply | |
| 124 | right-clicking and splitting the screen into as many terminals as you want. | |
| 125 | While this project hasn't been updated in a while, [[https://github.com/gnome-terminator/terminator/issues/1][it is coming under new | |
| 126 | development]]. However, this terminal is great and I haven't experienced any | |
| 127 | errors yet. | |
| 128 | ||
| 129 | For the shell choice, I decided to choose zsh after trying it out on a fresh | |
| 130 | Manjaro installation. Zsh is great if you like to change the themes of your | |
| 131 | terminal, include icons, or add plugins. | |
| 132 | ||
| 133 | The desktop uses the [[https://github.com/zsh-users/zsh-autosuggestions][zsh-autosuggestions]] to suggest past commands as you | |
| 134 | type. In addition, it suggests corrections if you misspell a command. Lastly, it | |
| 135 | uses the =af-magic= theme, which adds dashed lines between commands, moving the | |
| 136 | user@host tag to the right side of the terminal, and changes the colors. There | |
| 137 | are plenty of plugins and themes to choose from. Just figure out what you like | |
| 138 | and add it to your =~/.zshrc= file! | |
| 139 | ||
| 140 | *** Steps to Replicate My Terminal | |
| 141 | ||
| 142 | To install zsh on Ubuntu, enter the following command into a terminal: | |
| 143 | ||
| 144 | #+BEGIN_SRC sh | |
| 145 | sudo apt install zsh | |
| 146 | #+END_SRC | |
| 147 | ||
| 148 | Then, enter the next command to activate zsh: | |
| 149 | ||
| 150 | #+BEGIN_SRC sh | |
| 151 | sudo chsh -s $(which zsh) $(whoami) | |
| 152 | #+END_SRC | |
| 153 | ||
| 154 | To install Terminator on Ubuntu: | |
| 155 | ||
| 156 | #+BEGIN_SRC sh | |
| 157 | sudo apt install terminator | |
| 158 | #+END_SRC | |
| 159 | ||
| 160 | To install Oh My Zsh on Ubuntu: | |
| 161 | ||
| 162 | #+BEGIN_SRC sh | |
| 163 | sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" | |
| 164 | #+END_SRC | |
| 165 | ||
| 166 | To install zsh-autosuggestions via Oh My Zsh: | |
| 167 | ||
| 168 | #+BEGIN_SRC sh | |
| 169 | git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions | |
| 170 | #+END_SRC | |
| 171 | ||
| 172 | Then, add the following plugin wording to your =~/.zshrc= file (the default | |
| 173 | config usually has the =git= plugin activated, so just add any other plugins to | |
| 174 | the parentheses separated by a space): | |
| 175 | ||
| 176 | #+BEGIN_SRC sh | |
| 177 | nano ~/.zshrc | |
| 178 | #+END_SRC | |
| 179 | ||
| 180 | #+BEGIN_SRC sh | |
| 181 | plugins=(git zsh-autosuggestions) | |
| 182 | #+END_SRC | |
| 183 | ||
| 184 | Finally, you need to log out of your computer and log back in so your user shell | |
| 185 | can refresh. | |
blog/2020-07-20-video-game-sales.org deleted −176
| @@ -1,176 +0,0 @@ | ||
| 1 | #+date: 2020-07-20 | |
| 2 | #+title: Data Exploration: Video Game Sales | |
| 3 | ||
| 4 | * Background Information | |
| 5 | ||
| 6 | This dataset (obtained from [[https://www.kaggle.com/gregorut/videogamesales/data][Kaggle]]) contains a list of video games with sales | |
| 7 | greater than 100,000 copies. It was generated by a scrape of vgchartz.com. | |
| 8 | ||
| 9 | Fields include: | |
| 10 | ||
| 11 | - Rank: Ranking of overall sales | |
| 12 | - Name: The game name | |
| 13 | - Platform: Platform of the game release (i.e. PC,PS4, etc.) | |
| 14 | - Year: Year of the game's release | |
| 15 | - Genre: Genre of the game | |
| 16 | - Publisher: Publisher of the game | |
| 17 | - NA_Sales: Sales in North America (in millions) | |
| 18 | - EU_Sales: Sales in Europe (in millions) | |
| 19 | - JP_Sales: Sales in Japan (in millions) | |
| 20 | - Other_Sales: Sales in the rest of the world (in millions) | |
| 21 | - Global_Sales: Total worldwide sales. | |
| 22 | ||
| 23 | There are 16,598 records. 2 records were dropped due to incomplete information. | |
| 24 | ||
| 25 | * Import the Data | |
| 26 | ||
| 27 | #+BEGIN_SRC python | |
| 28 | # Import the Python libraries we will be using | |
| 29 | import pandas as pd | |
| 30 | import numpy as np | |
| 31 | import seaborn as sns; sns.set() | |
| 32 | import matplotlib.pyplot as plt | |
| 33 | ||
| 34 | # Load the file using the path to the downloaded file | |
| 35 | file = r'video_game_sales.csv' | |
| 36 | df = pd.read_csv(file) | |
| 37 | df | |
| 38 | #+END_SRC | |
| 39 | ||
| 40 | #+CAPTION: Dataframe Results | |
| 41 | [[https://img.cleberg.net/blog/20200720-data-exploration-video-game-sales/01_dataframe-min.png]] | |
| 42 | ||
| 43 | * Explore the Data | |
| 44 | ||
| 45 | #+BEGIN_SRC python | |
| 46 | # With the description function, we can see the basic stats. For example, we can also see that the 'Year' column has some incomplete values. | |
| 47 | df.describe() | |
| 48 | #+END_SRC | |
| 49 | ||
| 50 | #+CAPTION: df.describe() | |
| 51 | [[https://img.cleberg.net/blog/20200720-data-exploration-video-game-sales/02_describe-min.png]] | |
| 52 | ||
| 53 | #+BEGIN_SRC python | |
| 54 | # This function shows the rows and columns of NaN values. For example, df[179,3] = nan | |
| 55 | np.where(pd.isnull(df)) | |
| 56 | ||
| 57 | (array([179, ..., 16553], dtype=int64), | |
| 58 | array([3, ..., 5], dtype=int64)) | |
| 59 | #+END_SRC | |
| 60 | ||
| 61 | * Visualize the Data | |
| 62 | ||
| 63 | #+BEGIN_SRC python | |
| 64 | # This function plots the global sales by platform | |
| 65 | sns.catplot(x='Platform', y='Global_Sales', data=df, jitter=False).set_xticklabels(rotation=90) | |
| 66 | #+END_SRC | |
| 67 | ||
| 68 | #+CAPTION: Plot of Global Sales by Platform | |
| 69 | [[https://img.cleberg.net/blog/20200720-data-exploration-video-game-sales/03_plot-min.png]] | |
| 70 | ||
| 71 | #+BEGIN_SRC python | |
| 72 | # This function plots the global sales by genre | |
| 73 | sns.catplot(x='Genre', y='Global_Sales', data=df, jitter=False).set_xticklabels(rotation=45) | |
| 74 | #+END_SRC | |
| 75 | ||
| 76 | #+CAPTION: Plot of Global Sales by Genre | |
| 77 | [[https://img.cleberg.net/blog/20200720-data-exploration-video-game-sales/04_plot-min.png]] | |
| 78 | ||
| 79 | #+BEGIN_SRC python | |
| 80 | # This function plots the global sales by year | |
| 81 | sns.lmplot(x='Year', y='Global_Sales', data=df).set_xticklabels(rotation=45) | |
| 82 | #+END_SRC | |
| 83 | ||
| 84 | #+CAPTION: Plot of Global Sales by Year | |
| 85 | [[https://img.cleberg.net/blog/20200720-data-exploration-video-game-sales/05_plot-min.png]] | |
| 86 | ||
| 87 | #+BEGIN_SRC python | |
| 88 | # This function plots four different lines to show sales from different regions. | |
| 89 | # The global sales plot line is commented-out, but can be included for comparison | |
| 90 | df2 = df.groupby('Year').sum() | |
| 91 | years = range(1980,2019) | |
| 92 | ||
| 93 | a = df2['NA_Sales'] | |
| 94 | b = df2['EU_Sales'] | |
| 95 | c = df2['JP_Sales'] | |
| 96 | d = df2['Other_Sales'] | |
| 97 | # e = df2['Global_Sales'] | |
| 98 | ||
| 99 | fig, ax = plt.subplots(figsize=(12,12)) | |
| 100 | ax.set_ylabel('Region Sales (in Millions)') | |
| 101 | ax.set_xlabel('Year') | |
| 102 | ||
| 103 | ax.plot(years, a, label='NA_Sales') | |
| 104 | ax.plot(years, b, label='EU_Sales') | |
| 105 | ax.plot(years, c, label='JP_Sales') | |
| 106 | ax.plot(years, d, label='Other_Sales') | |
| 107 | # ax.plot(years, e, label='Global_Sales') | |
| 108 | ||
| 109 | ax.legend() | |
| 110 | plt.show() | |
| 111 | #+END_SRC | |
| 112 | ||
| 113 | #+CAPTION: Plot of Regional Sales by Year | |
| 114 | [[https://img.cleberg.net/blog/20200720-data-exploration-video-game-sales/06_plot-min.png]] | |
| 115 | ||
| 116 | * Investigate Outliers | |
| 117 | ||
| 118 | #+BEGIN_SRC python | |
| 119 | # Find the game with the highest sales in North America | |
| 120 | df.loc[df['NA_Sales'].idxmax()] | |
| 121 | ||
| 122 | Rank 1 | |
| 123 | Name Wii Sports | |
| 124 | Platform Wii | |
| 125 | Year 2006 | |
| 126 | Genre Sports | |
| 127 | Publisher Nintendo | |
| 128 | NA_Sales 41.49 | |
| 129 | EU_Sales 29.02 | |
| 130 | JP_Sales 3.77 | |
| 131 | Other_Sales 8.46 | |
| 132 | Global_Sales 82.74 | |
| 133 | Name: 0, dtype: object | |
| 134 | ||
| 135 | # Explore statistics in the year 2006 (highest selling year) | |
| 136 | df3 = df[(df['Year'] == 2006)] | |
| 137 | df3.describe() | |
| 138 | #+END_SRC | |
| 139 | ||
| 140 | #+CAPTION: Descriptive Statistics of 2006 Sales | |
| 141 | [[https://img.cleberg.net/blog/20200720-data-exploration-video-game-sales/07_2006_stats-min.png]] | |
| 142 | ||
| 143 | #+BEGIN_SRC python | |
| 144 | # Plot the results of the previous dataframe (games from 2006) - we can see the year's results were largely carried by Wii Sports | |
| 145 | sns.catplot(x="Genre", y="Global_Sales", data=df3, jitter=False).set_xticklabels(rotation=45) | |
| 146 | #+END_SRC | |
| 147 | ||
| 148 | #+CAPTION: Plot of 2006 Sales | |
| 149 | [[https://img.cleberg.net/blog/20200720-data-exploration-video-game-sales/08_plot-min.png]] | |
| 150 | ||
| 151 | #+BEGIN_SRC python | |
| 152 | # We can see 4 outliers in the graph above, so let's get the top 5 games from that dataframe | |
| 153 | # The results below show that Nintendo had all top 5 games (3 on the Wii and 2 on the DS) | |
| 154 | df3.sort_values(by=['Global_Sales'], ascending=False).head(5) | |
| 155 | #+END_SRC | |
| 156 | ||
| 157 | #+CAPTION: Outliers of 2006 Sales | |
| 158 | [[https://img.cleberg.net/blog/20200720-data-exploration-video-game-sales/09_outliers-min.png]] | |
| 159 | ||
| 160 | * Discussion | |
| 161 | ||
| 162 | The purpose of exploring datasets is to ask questions, answer questions, and | |
| 163 | discover intelligence that can be used to inform decision-making. So, what have | |
| 164 | we found in this dataset? | |
| 165 | ||
| 166 | Today we simply explored a publicly-available dataset to see what kind of | |
| 167 | information it contained. During that exploration, we found that video game | |
| 168 | sales peaked in 2006. That peak was largely due to Nintendo, who sold the top 5 | |
| 169 | games in 2006 and has a number of games in the top-10 list for the years | |
| 170 | 1980-2020. Additionally, the top four platforms by global sales (Wii, NES, GB, | |
| 171 | DS) are owned by Nintendo. | |
| 172 | ||
| 173 | We didn't explore everything this dataset has to offer, but we can tell from a | |
| 174 | brief analysis that Nintendo seems to rule sales in the video gaming world. | |
| 175 | Further analysis could provide insight into which genres, regions, publishers, | |
| 176 | or world events are correlated with sales. | |
blog/2020-08-22-redirect-github-pages.org deleted −120
| @@ -1,120 +0,0 @@ | ||
| 1 | #+date: 2020-08-22 | |
| 2 | #+title: Redirect GitHub Pages from Subdomain to the Top-Level Domain | |
| 3 | ||
| 4 | * Short answer | |
| 5 | ||
| 6 | ** Step 1 | |
| 7 | ||
| 8 | Add a new file CNAME to your GitHub Pages repository containing only one line: | |
| 9 | your top-level domain name. E.g.: =example.com= | |
| 10 | ||
| 11 | ** Step 2 | |
| 12 | ||
| 13 | [Optional] but highly recommended | |
| 14 | ||
| 15 | 2.1: Remove all other top-level records (prefixed with @) of type A from your | |
| 16 | DNS configuration. | |
| 17 | ||
| 18 | 2.2: Remove a CNAME record for the second-level domain www if it is present. | |
| 19 | ||
| 20 | ** Step 3 | |
| 21 | ||
| 22 | Add these 5 entries to the very top of your DNS configuration: | |
| 23 | ||
| 24 | #+BEGIN_SRC txt | |
| 25 | @ A 185.199.108.153 | |
| 26 | @ A 185.199.109.153 | |
| 27 | @ A 185.199.110.153 | |
| 28 | @ A 185.199.111.153 | |
| 29 | www CNAME your_github_username.github.io. | |
| 30 | #+END_SRC | |
| 31 | ||
| 32 | Replace =your_github_username= with your actual GitHub username. | |
| 33 | ||
| 34 | ** Step 4 | |
| 35 | ||
| 36 | Wait for your DNS changes to propagate. DNS changes aren't effective | |
| 37 | immediately. They can take up to a full day to propagate. | |
| 38 | ||
| 39 | * Long answer | |
| 40 | ||
| 41 | This issue has two sides. One is the DNS configuration itself. Another one is | |
| 42 | the way GitHub Pages will forward HTTP requests. | |
| 43 | ||
| 44 | We need to know a few things to understand what GitHub is trying to say in their | |
| 45 | documentation. | |
| 46 | ||
| 47 | ** DNS Entry Types | |
| 48 | ||
| 49 | There are two types of DNS records which interest us: CNAME and A. | |
| 50 | ||
| 51 | =A= is also known as =Apex= or sometimes as =root entry=. It forwards requests | |
| 52 | to a specified fixed IP address. =CNAME= entry forwards requests to a specified | |
| 53 | URL (actual valid plain text URL, not an IP address). | |
| 54 | ||
| 55 | ** DNS Load balancing | |
| 56 | ||
| 57 | GitHub has one central URL address which accepts all DNS requests for GitHub | |
| 58 | Pages: =http://username.github.io=. That URL is resolved to different IP | |
| 59 | addresses based on your geographical location. Website hosted on GitHub Pages is | |
| 60 | a simple collection of =HTML=, =CSS= and =JS= files. GitHub distributes these | |
| 61 | files to different servers across the globe. So that when your browser sends a | |
| 62 | request from Europe, it receives data from a server in Europe. The same is valid | |
| 63 | for the requests from Asia and the USA. | |
| 64 | ||
| 65 | ** What GitHub is trying to say | |
| 66 | ||
| 67 | Since =A= records in DNS must contain IP addresses, and they must be either | |
| 68 | =185.199.108.153= or =185.199.109.153= or =185.199.110.153= or | |
| 69 | =185.199.111.153=, there is no way to forward requests to a server located | |
| 70 | somewhere in Europe or Asia. Your website hosted at GitHub Pages will be | |
| 71 | downloaded from a central GitHub Pages server. There is a minor risk that if | |
| 72 | GitHub Pages DNS servers (=x.x.x.153=) are down for some reason, all custom | |
| 73 | domains which use fixed GitHub Pages IP addresses will not be accessible (their | |
| 74 | DNS requests will not be resolvable). | |
| 75 | ||
| 76 | That is why GitHub strongly suggests to either use a second-level domain for | |
| 77 | your GitHub Pages (e.g. =blog.example.com=) or use a DNS service provider that | |
| 78 | supports a record type =ALIAS= that acts as =A= record but forwards request to a | |
| 79 | URL address (e.g. =username.github.io=) instead of a fixed IP address. | |
| 80 | ||
| 81 | ** How GitHub Pages treats HTTP requests | |
| 82 | ||
| 83 | After a DNS request for =your_github_username.github.io= is resolved into an IP | |
| 84 | address, e.g. =185.199.108.153= your browser sends an HTTP request to that | |
| 85 | server with an HTTP header =Host=. Below are =curl= examples that load the same | |
| 86 | website (these examples might not work if you are behind a proxy server): | |
| 87 | ||
| 88 | #+BEGIN_SRC sh | |
| 89 | curl --header "Host: your_github_username.github.io" http://185.199.108.153/ | |
| 90 | curl --header "Host: www.example.com" http://185.199.108.153/ | |
| 91 | curl --header "Host: example.com" http://185.199.108.153/ | |
| 92 | #+END_SRC | |
| 93 | ||
| 94 | This way GitHub Pages servers know which user website to serve. | |
| 95 | ||
| 96 | #+BEGIN_QUOTE | |
| 97 | GitHub Pages server will automatically redirect HTTP requests to the top-level | |
| 98 | domain if your =CNAME= file contains =example.com= but =www.example.com= is | |
| 99 | requested. | |
| 100 | ||
| 101 | The same is valid if your =CNAME= file contains =www.example.com= but the | |
| 102 | header =Host= in the =HTTP= request contains =example.com=. | |
| 103 | #+END_QUOTE | |
| 104 | ||
| 105 | ** Why can't I add a =CNAME= record entry that accepts a top-level request (=@=) to my DNS configuration? | |
| 106 | ||
| 107 | Quote from the GitHub Pages documentation: | |
| 108 | ||
| 109 | #+BEGIN_QUOTE | |
| 110 | Warning: Do not create a CNAME record for your custom apex domain! Doing so | |
| 111 | may cause issues with other services, such as email, on that domain. | |
| 112 | #+END_QUOTE | |
| 113 | ||
| 114 | * References: | |
| 115 | ||
| 116 | 1. [[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]] | |
| 117 | 2. [[https://docs.github.com/en/github/working-with-github-pages/troubleshooting-custom-domains-and-github-pages][My custom domain isn't working]] | |
| 118 | 3. [[https://serverfault.com/questions/589370/cannot-access-my-github-pages-website-by-ip-address][Cannot access my GitHub Pages website by IP Address]] | |
| 119 | 4. [[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 | |
| 120 | (e.g. www) to the top-level domain (TLD, Apex record)?]] | |
blog/2020-09-01-visual-recognition.org deleted −189
| @@ -1,189 +0,0 @@ | ||
| 1 | #+date: 2020-09-01 | |
| 2 | #+title: IBM Watson Visual Recognition | |
| 3 | ||
| 4 | * What is IBM Watson? | |
| 5 | ||
| 6 | If you've never heard of [[https://www.ibm.com/watson][Watson]], this service is a suite of enterprise-ready AI | |
| 7 | services, applications, and tooling provided by IBM. Watson contains quite a few | |
| 8 | useful tools for data scientists and students, including the subject of this | |
| 9 | post today: visual recognition. | |
| 10 | ||
| 11 | If you'd like to view the official documentation for the Visual Recognition API, | |
| 12 | visit the [[https://cloud.ibm.com/apidocs/visual-recognition/visual-recognition-v3?code=python][API Docs]]. | |
| 13 | ||
| 14 | * Prerequisites | |
| 15 | ||
| 16 | To be able to use Watson Visual Recognition, you'll need the following: | |
| 17 | ||
| 18 | 1. Create a free account on [[https://www.ibm.com/cloud/watson-studio][IBM Watson Studio]]. | |
| 19 | 2. Add the [[https://www.ibm.com/cloud/watson-visual-recognition][Watson Visual Recognition]] service to your IBM Watson account. | |
| 20 | 3. Get your API key and URL. To do this, first go to the [[https://dataplatform.cloud.ibm.com/home2?context=cpdaas][profile dashboard]] for | |
| 21 | your IBM account and click on the Watson Visual Recognition service you | |
| 22 | created. This will be listed in the section titled *Your services*. Then | |
| 23 | click the *Credentials* tab and open the *Auto-generated credentials* | |
| 24 | dropdown. Copy your API key and URL so that you can use them in the Python | |
| 25 | script later. | |
| 26 | 4. *[Optional]* While not required, you can also create the Jupyter Notebook for | |
| 27 | this project right inside [[https://www.ibm.com/cloud/watson-studio][Watson Studio]]. Watson Studio will save your | |
| 28 | notebooks inside an organized project and allow you to use their other | |
| 29 | integrated products, such as storage containers, AI models, documentation, | |
| 30 | external sharing, etc. | |
| 31 | ||
| 32 | * Calling the IBM Watson Visual Recognition API | |
| 33 | ||
| 34 | Okay, now let's get started. | |
| 35 | ||
| 36 | To begin, we need to install the proper Python package for IBM Watson. | |
| 37 | ||
| 38 | #+BEGIN_SRC sh | |
| 39 | pip install --upgrade --user "ibm-watson>=4.5.0" | |
| 40 | #+END_SRC | |
| 41 | ||
| 42 | Next, we need to specify the API key, version, and URL given to us when we | |
| 43 | created the Watson Visual Recognition service. | |
| 44 | ||
| 45 | #+BEGIN_SRC python | |
| 46 | apikey = "<your-apikey>" | |
| 47 | version = "2018-03-19" | |
| 48 | url = "<your-url>" | |
| 49 | #+END_SRC | |
| 50 | ||
| 51 | Now, let's import the necessary libraries and authenticate our service. | |
| 52 | ||
| 53 | #+BEGIN_SRC python | |
| 54 | import json | |
| 55 | from ibm_watson import VisualRecognitionV3 | |
| 56 | from ibm_cloud_sdk_core.authenticators import IAMAuthenticator | |
| 57 | ||
| 58 | authenticator = IAMAuthenticator(apikey) | |
| 59 | visual_recognition = VisualRecognitionV3( | |
| 60 | version=version, | |
| 61 | authenticator=authenticator | |
| 62 | ) | |
| 63 | ||
| 64 | visual_recognition.set_service_url(url) | |
| 65 | #+END_SRC | |
| 66 | ||
| 67 | *[Optional]* If you'd like to tell the API not to use any data to improve | |
| 68 | their products, set the following header. | |
| 69 | ||
| 70 | #+BEGIN_SRC python | |
| 71 | visual_recognition.set_default_headers({'x-watson-learning-opt-out': "true"}) | |
| 72 | #+END_SRC | |
| 73 | ||
| 74 | Now we have our API all set and ready to go. For this example, I'm going to | |
| 75 | include a =dict= of photos to load as we test out the API. | |
| 76 | ||
| 77 | #+BEGIN_SRC python | |
| 78 | data = [ | |
| 79 | { | |
| 80 | "title": "Grizzly Bear", | |
| 81 | "url": "https://example.com/photos/image1.jpg" | |
| 82 | }, | |
| 83 | { | |
| 84 | "title": "Nature Lake", | |
| 85 | "url": "https://example.com/photos/image2.jpg" | |
| 86 | }, | |
| 87 | { | |
| 88 | "title": "Welcome Sign", | |
| 89 | "url": "https://example.com/photos/image3.jpg" | |
| 90 | }, | |
| 91 | { | |
| 92 | "title": "Honey Badger", | |
| 93 | "url": "https://example.com/photos/image4.jpg" | |
| 94 | }, | |
| 95 | { | |
| 96 | "title": "Grand Canyon Lizard", | |
| 97 | "url": "https://example.com/photos/image5.jpg" | |
| 98 | }, | |
| 99 | { | |
| 100 | "title": "Castle", | |
| 101 | "url": "https://example.com/photos/image6.jpg" | |
| 102 | } | |
| 103 | ] | |
| 104 | #+END_SRC | |
| 105 | ||
| 106 | Now that we've set up our libraries and have the photos ready, let's create a | |
| 107 | loop to call the API for each image. The code below shows a loop that calls the | |
| 108 | URL of each image and sends it to the API, requesting results with at least 60% | |
| 109 | confidence. The results are output to the console with dotted lines separating | |
| 110 | each section. | |
| 111 | ||
| 112 | In the case of an API error, the codes and explanations are output to the | |
| 113 | console. | |
| 114 | ||
| 115 | #+BEGIN_SRC python | |
| 116 | from ibm_watson import ApiException | |
| 117 | ||
| 118 | for x in range(len(data)): | |
| 119 | try: | |
| 120 | url = data[x]["url"] | |
| 121 | images_filename = data[x]["title"] | |
| 122 | classes = visual_recognition.classify( | |
| 123 | url=url, | |
| 124 | images_filename=images_filename, | |
| 125 | threshold='0.6', | |
| 126 | owners=["IBM"]).get_result() | |
| 127 | print("-----------------------------------------------") | |
| 128 | print("Image Title: ", data[x]["title"], "\n") | |
| 129 | print("Image URL: ", data[x]["url"], "\n") | |
| 130 | classification_results = classes["images"][0]["classifiers"][0]["classes"] | |
| 131 | for result in classification_results: | |
| 132 | print(result["class"], "(", result["score"], ")") | |
| 133 | print("-----------------------------------------------") | |
| 134 | except ApiException as ex: | |
| 135 | print("Method failed with status code " + str(ex.code) + ": " + ex.message) | |
| 136 | #+END_SRC | |
| 137 | ||
| 138 | * The Results | |
| 139 | ||
| 140 | Here we can see the full result set of our function above. If you view each of | |
| 141 | the URLs that we sent to the API, you'll be able to see that it was remarkably | |
| 142 | accurate. To be fair, these are clear high-resolution, clear photos shot with a | |
| 143 | professional camera. In reality, you will most likely be processing images that | |
| 144 | are lower quality and may have a lot of noise in the photo. | |
| 145 | ||
| 146 | However, we can clearly see the benefit of being able to call this API instead | |
| 147 | of attempting to write our own image recognition function. Each of the | |
| 148 | classifications returned was a fair description of the image. | |
| 149 | ||
| 150 | If you wanted to restrict the results to those that are at least 90% confident | |
| 151 | or greater, you would simply adjust the =threshold= in the | |
| 152 | =visual_recognition.classify()= function. | |
| 153 | ||
| 154 | When your program runs, it should show the output below for each photo you | |
| 155 | provide. | |
| 156 | ||
| 157 | #+BEGIN_SRC txt | |
| 158 | ---------------------------------------------------------------- | |
| 159 | Image Title: Grizzly Bear | |
| 160 | Image URL: https://example.com/photos/image1.jpg | |
| 161 | ||
| 162 | brown bear ( 0.944 ) | |
| 163 | bear ( 1 ) | |
| 164 | carnivore ( 1 ) | |
| 165 | mammal ( 1 ) | |
| 166 | animal ( 1 ) | |
| 167 | Alaskan brown bear ( 0.759 ) | |
| 168 | greenishness color ( 0.975 ) | |
| 169 | ---------------------------------------------------------------- | |
| 170 | #+END_SRC | |
| 171 | ||
| 172 | * Discussion | |
| 173 | ||
| 174 | Now, this was a very minimal implementation of the API. We simply supplied some | |
| 175 | images and looked to see how accurate the results were. However, you could | |
| 176 | implement this type of API into many machine learning (ML) models. | |
| 177 | ||
| 178 | For example, you could be working for a company that scans their warehouses or | |
| 179 | inventory using drones. Would you want to pay employees to sit there and watch | |
| 180 | drone footage all day in order to identify or count things in the video? | |
| 181 | Probably not. Instead, you could use a classification system similar to this one | |
| 182 | in order to train your machine learning model to correctly identify items that | |
| 183 | the drones show through video. More specifically, you could have your machine | |
| 184 | learning model watch a drone fly over a field of sheep in order to count how | |
| 185 | many sheep are living in that field. | |
| 186 | ||
| 187 | There are many ways to implement machine learning functionality, but hopefully | |
| 188 | this post helped inspire some deeper thought about the tools that can help | |
| 189 | propel us further into the future of machine learning and AI. | |
blog/2020-09-22-internal-audit.org deleted −246
| @@ -1,246 +0,0 @@ | ||
| 1 | #+date: 2020-09-22 | |
| 2 | #+title: What is Internal Audit? | |
| 3 | ||
| 4 | #+CAPTION: Internal Audit Overview | |
| 5 | [[https://img.cleberg.net/blog/20200922-what-is-internal-audit/internal-audit-overview.jpg]] | |
| 6 | ||
| 7 | * Definitions | |
| 8 | ||
| 9 | One of the many reasons that Internal Audit needs such thorough explaining to | |
| 10 | non-auditors is that Internal Audit can serve many purposes, depending on the | |
| 11 | organization's size and needs. However, the Institute of Internal Auditors (IIA) | |
| 12 | defines Internal Auditing as: | |
| 13 | ||
| 14 | #+BEGIN_QUOTE | |
| 15 | Internal auditing is an independent, objective assurance and consulting activity | |
| 16 | designed to add value and improve an organization's operations. It helps an | |
| 17 | organization accomplish its objectives by bringing a systematic, disciplined | |
| 18 | approach to evaluate and improve the effectiveness of risk management, control, | |
| 19 | and governance processes. | |
| 20 | #+END_QUOTE | |
| 21 | ||
| 22 | However, this definition uses quite a few terms that aren't clear unless the | |
| 23 | reader already has a solid understanding of the auditing profession. To further | |
| 24 | explain, the following is a list of definitions that can help supplement | |
| 25 | understanding of internal auditing. | |
| 26 | ||
| 27 | ** Independent | |
| 28 | ||
| 29 | Independence is the freedom from conditions that threaten the ability of the | |
| 30 | internal audit activity to carry out internal audit responsibilities in an | |
| 31 | unbiased manner. To achieve the degree of independence necessary to effectively | |
| 32 | carry out the responsibilities of the internal audit activity, the chief audit | |
| 33 | executive has direct and unrestricted access to senior management and the board. | |
| 34 | This can be achieved through a dual-reporting relationship. Threats to | |
| 35 | independence must be managed at the individual auditor, engagement, functional, | |
| 36 | and organizational levels. | |
| 37 | ||
| 38 | ** Objective | |
| 39 | ||
| 40 | Objectivity is an unbiased mental attitude that allows internal auditors to | |
| 41 | perform engagements in such a manner that they believe in their work product and | |
| 42 | that no quality compromises are made. Objectivity requires that internal | |
| 43 | auditors do not subordinate their judgment on audit matters to others. Threats | |
| 44 | to objectivity must be managed at the individual auditor, engagement, | |
| 45 | functional, and organizational levels. | |
| 46 | ||
| 47 | ** Assurance | |
| 48 | ||
| 49 | Assurance services involve the internal auditor's objective assessment of | |
| 50 | evidence to provide opinions or conclusions regarding an entity, operation, | |
| 51 | function, process, system, or other subject matters. The internal auditor | |
| 52 | determines the nature and scope of an assurance engagement. Generally, three | |
| 53 | parties are participants in assurance services: (1) the person or group directly | |
| 54 | involved with the entity, operation, function, process, system, or other | |
| 55 | subject - (the process owner), (2) the person or group making the assessment - | |
| 56 | (the internal auditor), and (3) the person or group using the assessment - (the | |
| 57 | user). | |
| 58 | ||
| 59 | ** Consulting | |
| 60 | ||
| 61 | Consulting services are advisory in nature and are generally performed at the | |
| 62 | specific request of an engagement client. The nature and scope of the consulting | |
| 63 | engagement are subject to agreement with the engagement client. Consulting | |
| 64 | services generally involve two parties: (1) the person or group offering the | |
| 65 | advice (the internal auditor), and (2) the person or group seeking and receiving | |
| 66 | the advice (the engagement client). When performing consulting services, the | |
| 67 | internal auditor should maintain objectivity and not assume management | |
| 68 | responsibility. | |
| 69 | ||
| 70 | ** Governance, Risk Management, & Compliance (GRC) | |
| 71 | ||
| 72 | The integrated collection of capabilities that enable an organization to | |
| 73 | reliably achieve objectives, address uncertainty and act with integrity. | |
| 74 | ||
| 75 | * Audit Charter & Standards | |
| 76 | ||
| 77 | First, it's important to note that not every organization needs internal | |
| 78 | auditors. In fact, it's unwise for an organization to hire internal auditors | |
| 79 | unless they have regulatory requirements for auditing and have the capital to | |
| 80 | support the department. Internal audit is a cost center that can only affect | |
| 81 | revenue indirectly. | |
| 82 | ||
| 83 | Once an organization determines the need for internal assurance services, they | |
| 84 | will hire a Chief Audit Executive and create the audit charter. This charter is | |
| 85 | a document, approved by the company's governing body, that will define internal | |
| 86 | audit's purpose, authority, responsibility, and position within the | |
| 87 | organization. Fortunately, the IIA has model charters available to IIA members | |
| 88 | for those developing or improving their charter. | |
| 89 | ||
| 90 | Beyond the charter and organizational documents, internal auditors follow a few | |
| 91 | different standards in order to perform their job. First is the International | |
| 92 | Professional Practices Framework (IPPF) by the IIA, which is the model of | |
| 93 | standards for internal auditing. In addition, ISACA's Information Technology | |
| 94 | Assurance Framework (ITAF) helps guide auditors in reference to information | |
| 95 | technology (IT) compliance and assurance. Finally, additional standards such as | |
| 96 | FASB, GAAP, and industry-specific standards are used when performing internal | |
| 97 | audit work. | |
| 98 | ||
| 99 | * Three Lines of Defense | |
| 100 | ||
| 101 | [[https://theiia.org][The IIA]] released the original Three Lines of Defense model in 2013, but have | |
| 102 | released an updated version in 2020. Here is what the Three Lines of Defense | |
| 103 | model has historically looked like: | |
| 104 | ||
| 105 | #+CAPTION: 2013 Three Lines of Defense Model | |
| 106 | [[https://img.cleberg.net/blog/20200922-what-is-internal-audit/three_lines_model.png]] | |
| 107 | ||
| 108 | I won't go into depth about the changes made to the model in this article. | |
| 109 | Instead, let's take a look at the most current model. | |
| 110 | ||
| 111 | #+CAPTION: 2020 Three Lines of Defense Model | |
| 112 | [[https://img.cleberg.net/blog/20200922-what-is-internal-audit/updated_three_lines_model.png]] | |
| 113 | ||
| 114 | The updated model forgets the strict idea of areas performing their own | |
| 115 | functions or line of defense. Instead of talking about management, risk, and | |
| 116 | internal audit as 1-2-3, the new model creates a more fluid and cooperative | |
| 117 | model. | |
| 118 | ||
| 119 | Looking at this model from an auditing perspective shows us that auditors will | |
| 120 | need to align, communicate, and collaborate with management, including business | |
| 121 | area managers and chief officers, as well as reporting to the governing body. | |
| 122 | The governing body will instruct internal audit /functionally/ on their goals | |
| 123 | and track their progress periodically. | |
| 124 | ||
| 125 | However, the internal audit department will report /administratively/ to a chief | |
| 126 | officer in the company for the purposes of collaboration, direction, and | |
| 127 | assistance with the business. Note that in most situations, the governing body | |
| 128 | is the audit committee on the company's board of directors. | |
| 129 | ||
| 130 | The result of this structure is that internal audit is an independent and | |
| 131 | objective function that can provide assurance over the topics they audit. | |
| 132 | ||
| 133 | * Audit Process | |
| 134 | ||
| 135 | A normal audit will generally follow the same process, regardless of the topic. | |
| 136 | However, certain special projects or abnormal business areas may call for | |
| 137 | changes to the audit process. The audit process is not set in stone, it's simply | |
| 138 | a set of best practices so that audits can be performed consistently. | |
| 139 | ||
| 140 | #+CAPTION: The Internal Audit Process | |
| 141 | [[https://img.cleberg.net/blog/20200922-what-is-internal-audit/internal-audit-process.jpg]] | |
| 142 | ||
| 143 | While different organizations may tweak the process, it will generally follow | |
| 144 | this flow: | |
| 145 | ||
| 146 | ** 1. Risk Assessment | |
| 147 | ||
| 148 | The risk assessment part of the process has historically been performed | |
| 149 | annually, but many organizations have moved to performing this process much more | |
| 150 | frequently. In fact, some organizations are moving to an agile approach that can | |
| 151 | take new risks into the risk assessment and re-prioritize risk areas on-the-go. | |
| 152 | To perform a risk assessment, leaders in internal audit will research industry | |
| 153 | risks, consult with business leaders around the company, and perform analyses on | |
| 154 | company data. | |
| 155 | ||
| 156 | Once a risk assessment has been documented, the audit department has a | |
| 157 | prioritized list of risks that can be audited. This is usually in the form of | |
| 158 | auditable entities, such as business areas or departments. | |
| 159 | ||
| 160 | ** 2. Planning | |
| 161 | ||
| 162 | During the planning phase of an audit, auditors will meet with the business area | |
| 163 | to discuss the various processes, controls, and risks applicable to the | |
| 164 | business. This helps the auditors determine the scope limits for the audit, as | |
| 165 | well as timing and subject-matter experts. Certain documents will be created in | |
| 166 | this phase that will be used to keep the audit on-track an in-scope as it goes | |
| 167 | forward. | |
| 168 | ||
| 169 | ** 3. Testing | |
| 170 | ||
| 171 | The testing phase, also known as fieldwork or execution, is where internal | |
| 172 | auditors will take the information they've discovered and test it against | |
| 173 | regulations, industry standards, company rules, best practices, as well as | |
| 174 | validating that any processes are complete and accurate. For example, an audit | |
| 175 | of HR would most likely examine processes such as employee on-boarding, employee | |
| 176 | termination, security of personally identifiable information (PII), or the IT | |
| 177 | systems involved in these processes. Company standards would be examined and | |
| 178 | compared against how the processes are actually being performed day-to-day, as | |
| 179 | well as compared against regulations such as the Equal Employment Opportunity | |
| 180 | (EEO), American with Disabilities Act, and National Labor Relations Act. | |
| 181 | ||
| 182 | ** 4. Reporting | |
| 183 | ||
| 184 | Once all the tests have been completed, the audit will enter the reporting | |
| 185 | phase. This is when the audit team will conclude on the evidence they've | |
| 186 | collected, interviews they've held, and any opinions they've formed on the | |
| 187 | controls in place. A summary of the audit findings, conclusions, and specific | |
| 188 | recommendations are officially communicated to the client through a draft | |
| 189 | report. Clients have the opportunity to respond to the report and submit an | |
| 190 | action plan and time frame. These responses become part of the final report | |
| 191 | which is distributed to the appropriate level of administration. | |
| 192 | ||
| 193 | ** 5. Follow-Up | |
| 194 | ||
| 195 | After audits have been completed and management has formed action plans and time | |
| 196 | frames for audit issues, internal audit will follow up once that due date has | |
| 197 | arrived. In most cases, the follow-up will simply consist of a meeting to | |
| 198 | discuss how the action plan has been completed and to request documentation to | |
| 199 | prove it. | |
| 200 | ||
| 201 | * Audit Department Structure | |
| 202 | ||
| 203 | While an internal audit department is most often thought of as a team of | |
| 204 | full-time employees, there are actually many different ways in which a | |
| 205 | department can be structured. As the world becomes more digital and fast-paced, | |
| 206 | outsourcing has become a more attractive option for some organizations. Internal | |
| 207 | audit can be fully outsourced or partially outsourced, allowing for flexibility | |
| 208 | in cases where turnover is high. | |
| 209 | ||
| 210 | In addition, departments can implement a rotational model. This allows for | |
| 211 | interested employees around the organization to rotate into the internal audit | |
| 212 | department for a period of time, allowing them to obtain knowledge of risks and | |
| 213 | controls and allowing the internal audit team to obtain more business area | |
| 214 | knowledge. This program is popular in very large organizations, but | |
| 215 | organizations tend to rotate lower-level audit staff instead of managers. This | |
| 216 | helps prevent any significant knowledge loss as auditors rotate out to business | |
| 217 | areas. | |
| 218 | ||
| 219 | * Consulting | |
| 220 | ||
| 221 | Consulting is not an easy task at any organization, especially for a department | |
| 222 | that can have negative perceptions within the organization as the "compliance | |
| 223 | police." However, once an internal audit department has delivered value to | |
| 224 | organization, adding consulting to their suite of services is a smart move. In | |
| 225 | most cases, Internal Audit can insert themselves into a consulting role without | |
| 226 | affecting the process of project management at the company. This means that | |
| 227 | internal audit can add objective assurance and opinions to business areas as | |
| 228 | they develop new processes, instead of coming in periodically to audit an area | |
| 229 | and file issues that could have been fixed at the beginning. | |
| 230 | ||
| 231 | * Data Science & Data Analytics | |
| 232 | ||
| 233 | #+CAPTION: Data Science Skill Set | |
| 234 | [[https://img.cleberg.net/blog/20200922-what-is-internal-audit/data-science-skillset.png]] | |
| 235 | ||
| 236 | One major piece of the internal audit function in the modern world is data | |
| 237 | science. While the process is data science, most auditors will refer to anything | |
| 238 | in this realm as data analytics. Hot topics such as robotic process automation | |
| 239 | (RPA), machine learning (ML), and data mining have taken over the auditing world | |
| 240 | in recent years. These technologies have been immensely helpful with increasing | |
| 241 | the effectiveness and efficiency of auditors. | |
| 242 | ||
| 243 | For example, mundane and repetitive tasks can be automated in order for auditors | |
| 244 | to make more room in their schedules for labor-intensive work. Further, auditors | |
| 245 | will need to adapt technologies like machine learning in order to extract more | |
| 246 | value from the data they're using to form conclusions. | |
blog/2020-10-12-mediocrity.org deleted −111
| @@ -1,111 +0,0 @@ | ||
| 1 | #+date: 2020-10-12 | |
| 2 | #+title: On the Pursuit of Mediocrity | |
| 3 | ||
| 4 | * Perfect is the Enemy of Good | |
| 5 | ||
| 6 | As the saying goes, "the best is the enemy of the good." As we strive for | |
| 7 | perfection, we often fail to realize the implications of such an undertaking. | |
| 8 | Attempting to reach perfection is often unrealistic. Even worse, it can get in | |
| 9 | the way of achieving a good outcome. In certain situations, we try so hard to | |
| 10 | achieve the ideal solution that we have burned the bridges that would have | |
| 11 | allowed us to reach a lesser yet still superb solution. | |
| 12 | ||
| 13 | Philosophers throughout history have inspected this plight from many viewpoints. | |
| 14 | Greek mythology speaks of the [[https://en.wikipedia.org/wiki/Golden_mean_(philosophy)][golden mean]], which uses the story of Icarus to | |
| 15 | illustrate that sometimes "the middle course" is the best solution. In this | |
| 16 | story, Daedalus, a famous artist of his time, built feathered wings for himself | |
| 17 | and his son so that they might escape the clutches of King Minos. Daedalus warns | |
| 18 | his beloved son whom he loved so much to "fly the middle course", between the | |
| 19 | sea spray and the sun's heat. Icarus did not heed his father; he flew up and up | |
| 20 | until the sun melted the wax off his wings. For not heeding the middle course, | |
| 21 | he fell into the sea and drowned. | |
| 22 | ||
| 23 | More recently, management scholars have explored the [[https://en.wikipedia.org/wiki/Pareto_principle][Pareto principle]] and found | |
| 24 | that as we increase the frequency of something, or strive to perform actions to | |
| 25 | achieve some form of perfection, we run into [[https://en.wikipedia.org/wiki/Diminishing_returns][diminishing returns]]. | |
| 26 | ||
| 27 | Even further, Harold Demsetz is noted as coining the term [[https://en.wikipedia.org/wiki/Nirvana_fallacy][the Nirvana fallacy]] in | |
| 28 | 1969, which shows the fallacy of comparing actual things with unrealistic, | |
| 29 | idealized alternatives. This is another trap that we may fall into, where we are | |
| 30 | constantly thinking of the ultimate solutions to problems, when something more | |
| 31 | realistic needs to be considered. | |
| 32 | ||
| 33 | Over and over throughout history, we've found that perfection is often | |
| 34 | unrealistic and unachievable. However, we push ourselves and our peers to "give | |
| 35 | 100%" or "go the extra mile," while it may be that the better course is to give | |
| 36 | a valuable level of effort while considering the effects of further effort on | |
| 37 | the outcome. Working harder does not always help us achieve loftier goals. | |
| 38 | ||
| 39 | This has presented itself to me most recently during my time studying at my | |
| 40 | university. I was anxious and feeling the stresses of my courses, career, and | |
| 41 | personal life for quite a while, which was greatly affecting how well I was | |
| 42 | doing at school and my level of effort at work. One day, I happened to be | |
| 43 | talking to my father when he said something simple that hit home: | |
| 44 | ||
| 45 | #+BEGIN_QUOTE | |
| 46 | All you can do is show up and do your best. | |
| 47 | Worrying about the outcomes won't affect the outcome itself. | |
| 48 | #+END_QUOTE | |
| 49 | ||
| 50 | The thought was extremely straightforward and uncomplicated, yet it was | |
| 51 | something that I had lost sight of during my stress-filled years at school. Ever | |
| 52 | since then, I've found myself pausing and remembering that quote every time I | |
| 53 | get anxious or stressed. It helps to stop and think "Can I do anything to affect | |
| 54 | the outcome, or am I simply worrying over something I can't change?" | |
| 55 | ||
| 56 | * When Mediocrity Isn't Enough | |
| 57 | ||
| 58 | One problem with the philosophies presented in this post is that they are | |
| 59 | implemented far too often in situations where mediocrity simply isn't adequate. | |
| 60 | For example, let's take a look at digital user data, specifically | |
| 61 | personally-identifiable information (PII). As a cybersecurity auditor in the | |
| 62 | United States, I have found that most companies are concerned more with | |
| 63 | compliance than any actual safeguards over the privacy or protection of user | |
| 64 | data. Other than companies who have built their reputation on privacy and | |
| 65 | security, most companies will use [[https://en.wikipedia.org/wiki/Satisficing][satisficing]] as their primary decision-making | |
| 66 | strategy around user data. | |
| 67 | ||
| 68 | #+BEGIN_QUOTE | |
| 69 | Satisficing is a decision-making strategy or cognitive heuristic that entails | |
| 70 | searching through the available alternatives until an acceptability threshold | |
| 71 | is met. | |
| 72 | #+END_QUOTE | |
| 73 | ||
| 74 | This means that each decision will be met with certain possible solutions until | |
| 75 | one of the solutions meets their minimum acceptable standards. For companies | |
| 76 | that deal with user data, the minimum-acceptable standards come from three | |
| 77 | areas: | |
| 78 | ||
| 79 | 1. Laws and regulations | |
| 80 | 2. Competitive pressure | |
| 81 | 3. Risk of monetary or reputation loss | |
| 82 | ||
| 83 | Working with project management or auditing, the primary concern here is the | |
| 84 | risk of legal ramifications. Since the primary risk comes from laws and | |
| 85 | regulations, companies will require that any project that involves user data | |
| 86 | must follow all the rules of those laws so that the company can protect itself | |
| 87 | from fines or other penalties. | |
| 88 | ||
| 89 | Following this, companies will consider best practices in order to place itself | |
| 90 | in a competitive position (e.g. Google vs. Apple) and review any recent or | |
| 91 | ongoing litigation against companies regarding user data. In a perfect company, | |
| 92 | management would then consider the ethical responsibilities of their | |
| 93 | organization and discuss their responsibilities over things like | |
| 94 | personally-identifiable information. | |
| 95 | ||
| 96 | However, as we mentioned above, most companies follow the idea of satisficing, | |
| 97 | which states that they have met the minimum acceptable standards and can now | |
| 98 | move on to other decisions. Modern business culture in the United States | |
| 99 | dictates that profits are the golden measure of how well a company or manager is | |
| 100 | performing, so we often don't think about our responsibilities beyond these | |
| 101 | basic standards. | |
| 102 | ||
| 103 | Not all situations demand excellence, but I believe that applying any philosophy | |
| 104 | as a broad stroke across one's life can be a mistake. We must be able to think | |
| 105 | critically about what we are doing as we do it and ask ourselves a few | |
| 106 | questions. Have I done everything I can in this situation? Is mediocrity an | |
| 107 | acceptable outcome, or should we strive for perfection, even if we can't attain | |
| 108 | it? | |
| 109 | ||
| 110 | Taking a few moments to think critically throughout our day, as we make | |
| 111 | decisions, can have a tremendous effect on the outcomes we create. | |
blog/2020-12-27-website-redesign.org deleted −87
| @@ -1,87 +0,0 @@ | ||
| 1 | #+date: 2020-12-27 | |
| 2 | #+title: Redesigning My Website: The 5 KB Result | |
| 3 | ||
| 4 | * A Brief History | |
| 5 | ||
| 6 | As a form of continuous learning and entertainment, I've been running a handful | |
| 7 | of websites since 2016 when I took my first programming courses in college. I | |
| 8 | maintain one main website, the place I consider the official website to | |
| 9 | represent me. Under this site, I have a handful of subdirectories and | |
| 10 | subdomains. | |
| 11 | ||
| 12 | One of the parts I've enjoyed the most about web development is the aspect of | |
| 13 | designing an identity for a web page and working to find exciting ways to | |
| 14 | display the site's content. Inevitably, this means I've changed the designs for | |
| 15 | my websites more times than I could possibly count. Since I don't really host | |
| 16 | anything on my main webpage that's vital, it allows me the freedom to change | |
| 17 | things as inspiration strikes. | |
| 18 | ||
| 19 | Historically, I've relied on core utilities for spacing, components, and layouts | |
| 20 | from [[https://getbootstrap.com][Bootstrap]] and added custom CSS for fonts, accents, colors, and other | |
| 21 | items. I also tend to create sites with no border radius on items, visible | |
| 22 | borders, and content that takes up the entire screen (using whitespace inside | |
| 23 | components instead of whitespace around my components). | |
| 24 | ||
| 25 | * The Redesign Process | |
| 26 | ||
| 27 | About a week ago, I found myself wishing for a new design yet again. The prior | |
| 28 | design was largely inspired by IBM's [[https://www.carbondesignsystem.com][Carbon Design System]] and relied on jQuery, | |
| 29 | Bootstrap, along with some compressed [[https://en.wikipedia.org/wiki/WebP][.webp]] images. | |
| 30 | ||
| 31 | To anyone who knows my preferences toward web design - and even in my personal | |
| 32 | life - it should be no surprise that I immediately started looking for | |
| 33 | inspiration on minimalism. While there are some decent minimalistic designs on | |
| 34 | sites 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 | |
| 35 | ask about minimalism. While brutalist web design doesn't have to be minimal, it | |
| 36 | often is. | |
| 37 | ||
| 38 | I suppose, in a way, I did create a brutalist website since my HTML is semantic | |
| 39 | and accessible, hyperlinks are colored and underlined, and all native browser | |
| 40 | functions like scrolling and the back button work as expected. However, I didn't | |
| 41 | think about brutalism while designing these sites. | |
| 42 | ||
| 43 | The new design followed a simple design process. I walked through the screens on | |
| 44 | my blog and asked myself: "Is this element necessary for a user?" This allowed | |
| 45 | me to first start by removing all javascript, which had the sole purpose of | |
| 46 | allowing users to open a collapsed navbar on mobile. Replacing the collapsible | |
| 47 | navbar allowed me to remove both jQuery and Bootstrap's javascript. | |
| 48 | ||
| 49 | Next, I removed things like author names (since I'm literally the only person | |
| 50 | who will ever write on this site), multiple ways to click on a blog post card, | |
| 51 | blog post descriptions, and the scroll-to-top button. It also helped to move all | |
| 52 | categories to a single page, rather than have each category on its own page. | |
| 53 | ||
| 54 | The final big piece to finish the "[[https://en.wikipedia.org/wiki/Marie_Kondo#KonMari_method][KonMari]]"-like part of my process was to | |
| 55 | remove Bootstrap CSS in its entirety. However, this meant pulling out a few very | |
| 56 | useful classes, such as `.img-fluid` and the default font stacks to keep in my | |
| 57 | custom CSS. | |
| 58 | ||
| 59 | After removing all the unnecessary pieces, I was finally able to reorganize my | |
| 60 | content and add a very small amount of custom CSS to make everything pretty. | |
| 61 | This took a brief amount of time, effectively just consisting of me | |
| 62 | converting =<div>= tags into things like =<ul>= lists and choosing accent | |
| 63 | colors. | |
| 64 | ||
| 65 | * The Results | |
| 66 | ||
| 67 | ** Reflection | |
| 68 | ||
| 69 | So, what did all of this reorganizing do to my webpages? Well, first, my | |
| 70 | websites are now *ridiculously fast*. Since the prior designs were also minimal | |
| 71 | and didn't have many images, they measured up in Firefox's Network Monitor | |
| 72 | around 300 KB - 600KB. After making the changes, my main site is at 5 KB | |
| 73 | transferred (22 KB total), and my blog is at 6.5 KB transferred (13 KB | |
| 74 | total). *That means the redesigned pages are less than 2% the size of the old | |
| 75 | designs.* | |
| 76 | ||
| 77 | Google Lighthouse ranks the new webpage as 100 in performance, accessibility, | |
| 78 | and best practices, with SEO at 92 since they think tap targets are not sized | |
| 79 | appropriately for mobile users. First contextual paints of the pages are under | |
| 80 | 0.8 seconds with 0 ms of blocking time. However, the blog subdomain ranks at 100 | |
| 81 | for all four categories! First contextual paints of the blog homepage are under | |
| 82 | 1.0 seconds with 0 ms of blocking time, due to the fact that the CSS for my blog | |
| 83 | is within a separate CSS file, and the CSS for my main website is simply | |
| 84 | embedded in the HTML file. | |
| 85 | ||
| 86 | Now that everything is complete, I can confidently say I'm happy with the result | |
| 87 | and proud to look at the fastest set of websites I've created so far. | |
blog/2020-12-28-neon-drive.org deleted −83
| @@ -1,83 +0,0 @@ | ||
| 1 | #+date: 2020-12-28 | |
| 2 | #+title: Neon Drive: A Nostalgic 80s Arcade Racing Game | |
| 3 | ||
| 4 | * Game Description | |
| 5 | ||
| 6 | [[https://store.steampowered.com/app/433910/Neon_Drive/][Neon Drive]] presents itself as a simple arcade-style game inspired by the arcade | |
| 7 | race games of the 1980s, yet it has managed to take up hours of my life without | |
| 8 | much effort. The game description, directly from the Steam page, is intriguing | |
| 9 | enough to entice anyone who's been looking for a good arcade racing game: | |
| 10 | ||
| 11 | #+BEGIN_QUOTE | |
| 12 | Neon Drive is a slick retro-futuristic arcade game that will make your brain | |
| 13 | melt. You've been warned. From beautiful cityscapes and ocean roads to | |
| 14 | exploding enemy spaceships, Neon Drive has it all. | |
| 15 | #+END_QUOTE | |
| 16 | ||
| 17 | * Gameplay | |
| 18 | ||
| 19 | The game holds true to the [[https://en.wikipedia.org/wiki/Retrofuturism][retro-futurism]] style, including chrome female robots, | |
| 20 | pixelated arcade machines, and [[https://teddit.net/r/outrun/][outrun]] aesthetics. | |
| 21 | ||
| 22 | Each level of the game is shown as a separate arcade machine. Each arcade | |
| 23 | machine lets you play on Normal, Hard, Insane, Practice, and Free Run. To beat | |
| 24 | each arcade, you must reach the end of the level without crashing your car into | |
| 25 | the various obstacles on the course. Basic levels let you move left or right to | |
| 26 | avoid blocks in the road. Later levels put you through other tests, such as | |
| 27 | dodging traffic or blasting asteroids. | |
| 28 | ||
| 29 | The game uses synthwave music to keep you on track to make the correct moves by | |
| 30 | timing the beats of the songs to the correct moves on the screen. It reminds me | |
| 31 | of the early Guitar Hero games, as well as mobile apps like VOEZ - repetition | |
| 32 | and staying on-beat is the only way to win. | |
| 33 | ||
| 34 | * In-Game Screenshots | |
| 35 | ||
| 36 | Taking a look at the main menu, you can see that Neon Drive plays into every | |
| 37 | stereotype you can think of around retro-futuristic, synthwave arcades (in a | |
| 38 | good way). | |
| 39 | ||
| 40 | #+CAPTION: Neon Drive Menu | |
| 41 | [[https://img.cleberg.net/blog/20201228-neon-drive/neon_drive_menu.png]] | |
| 42 | ||
| 43 | Once you get into the first level, we see that the choice of car fits right in | |
| 44 | with 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 | |
| 45 | F40]]. Each new level comes with new color schemes and cars, so you should never | |
| 46 | get tired of the aesthetic. | |
| 47 | ||
| 48 | #+CAPTION: Neon Drive Race | |
| 49 | [[https://img.cleberg.net/blog/20201228-neon-drive/neon_drive_race.png]] | |
| 50 | ||
| 51 | Personally, I love the orange and blue colors used in level 2: | |
| 52 | ||
| 53 | #+CAPTION: Level 2 | |
| 54 | [[https://img.cleberg.net/blog/20201228-neon-drive/neon_drive_level_2.png]] | |
| 55 | ||
| 56 | If you're the competitive type and getting 100% on all arcade machines isn't | |
| 57 | enough, there are leaderboards for the regular part of the game, and the | |
| 58 | endurance game mode. | |
| 59 | ||
| 60 | #+CAPTION: Leaderboard | |
| 61 | [[https://img.cleberg.net/blog/20201228-neon-drive/neon_drive_leaderboard.png]] | |
| 62 | ||
| 63 | * Other Suggestions | |
| 64 | ||
| 65 | Neon Drive sits nicely within the well-founded cult genre of Outrun. Other games | |
| 66 | that I've enjoyed in this same spectrum are: | |
| 67 | ||
| 68 | - [[https://store.steampowered.com/app/233270/Far_Cry_3__Blood_Dragon/][Far Cry 3: Blood Dragon]] | |
| 69 | - [[https://store.steampowered.com/app/1239690/Retrowave/][Retrowave]] | |
| 70 | - [[https://store.steampowered.com/app/732810/Slipstream/][Slipstream]] | |
| 71 | ||
| 72 | Although these games aren't necessarily in the same genre, they do have aspects | |
| 73 | that place them close enough to interest gamers that enjoyed Neon Drive: | |
| 74 | ||
| 75 | - [[https://store.steampowered.com/app/311800/Black_Ice/][Black Ice]] | |
| 76 | - [[https://store.steampowered.com/app/746850/Cloudpunk/][Cloudpunk]] | |
| 77 | - [[https://store.steampowered.com/app/1222680/Need_for_Speed_Heat/][Need for Speed: Heat]] | |
| 78 | - [[https://store.steampowered.com/app/1019310/VirtuaVerse/][VirtuaVerse]] | |
| 79 | ||
| 80 | Of course, if all you really care about is the arcade aspect of these games, you | |
| 81 | can check out the [[https://store.steampowered.com/app/400020/Atari_Vault/][Atari Vault]] or any of the other classic games sold on Steam by | |
| 82 | companies like Namco, Atari. For something like Nintendo, you'd have to settle | |
| 83 | for buying used classic consoles or delve into the world of emulation. | |
blog/2020-12-29-zork.org deleted −80
| @@ -1,80 +0,0 @@ | ||
| 1 | #+date: 2020-12-29 | |
| 2 | #+title: Zork: Let's Explore a Classic | |
| 3 | ||
| 4 | * Download (Free) | |
| 5 | ||
| 6 | Before we get into the game itself, you should know that you can download Zork | |
| 7 | for free from Infocom's [[http://infocom-if.org/downloads/downloads.html][download page]]. So feel free to boot it up and take a | |
| 8 | ride back to the 1980s with this masterpiece. | |
| 9 | ||
| 10 | * Game Description | |
| 11 | ||
| 12 | Zork is an interactive, text-based computer game originally released in 1980. | |
| 13 | This series, split into three separate games, introduced a robust and | |
| 14 | sophisticated text parser to gamers. People were largely used to the simple | |
| 15 | commands used in the popular game [[https://en.wikipedia.org/wiki/Colossal_Cave_Adventure][Colossal Cave Adventure]], but Zork allowed | |
| 16 | users to send more complex commands that included prepositions and conjunctions. | |
| 17 | ||
| 18 | Zork tracks your score as you explore the map, find tools, and collect trophy | |
| 19 | items (e.g., a jewel-encrusted egg). When you place your trophy items in the | |
| 20 | trophy case found in the Living Room area, you gain score points. Collecting the | |
| 21 | Twenty Treasures of Zork and placing them within the trophy case wins the | |
| 22 | game. However, you must explore the map, solve puzzles, and avoid being eaten by | |
| 23 | a grue to collect these treasures. | |
| 24 | ||
| 25 | * The Map | |
| 26 | ||
| 27 | Since Zork is a vast and complex game, it helps to have a map as you explore and | |
| 28 | collect your trophies. However, if you want to play the game as it was truly | |
| 29 | intended, you should try to play it without using the map. | |
| 30 | ||
| 31 | #+CAPTION: Zork Map | |
| 32 | [[https://img.cleberg.net/blog/20201229-zork/zork_map.png]] | |
| 33 | ||
| 34 | /[[https://www.filfre.net/2012/01/exploring-zork-part-1/][Map Source]]/ | |
| 35 | ||
| 36 | * In-Game Screenshots | |
| 37 | ||
| 38 | After playing the game (for the first time ever) for several weeks around 2014, | |
| 39 | I was finally able to beat the game with some online help to find the last | |
| 40 | couple items. As I was writing this post, I installed the game again to grab | |
| 41 | some screenshots to show off the true glory of this game. As noted in [[https://www.filfre.net/2012/01/exploring-zork-part-1/][Jimmy | |
| 42 | Maher's playthrough]], the original Zork games looked quite a bit different due to | |
| 43 | the older hardware of computers like the Apple II and multiple bug fixes that | |
| 44 | Infocom pushed out after the game's initial release. My play-through uses the | |
| 45 | [[https://store.steampowered.com/app/570580/Zork_Anthology/][Zork Anthology]] version, which utilizes DOSBox on Windows. | |
| 46 | ||
| 47 | The first screenshot here shows the introductory information, which doesn't | |
| 48 | include instructions of any kind for the player. If you haven't played text | |
| 49 | adventures before, try to use simple commands like "go west," "look around," or | |
| 50 | "hit troll with elvish sword." | |
| 51 | ||
| 52 | #+CAPTION: Zork Screen, pt. 1 | |
| 53 | [[https://img.cleberg.net/blog/20201229-zork/zork_01.png]] | |
| 54 | ||
| 55 | In this second screenshot, we see the player has entered the house and found the | |
| 56 | trophy case in the living room. The lantern and sword in this room allow the | |
| 57 | player to explore dark areas and attack enemies. If you don't use the lantern, | |
| 58 | you won't be able to see anything in dark areas, and you may be eaten by a grue. | |
| 59 | ||
| 60 | #+CAPTION: Zork Screen, pt. 2 | |
| 61 | [[https://img.cleberg.net/blog/20201229-zork/zork_02.png]] | |
| 62 | ||
| 63 | Finally, we see that the player has found the first treasure: a jewel-encrusted | |
| 64 | egg. These treasures can be taken back to the house and placed in the trophy | |
| 65 | case or carried until you feel like you want to put things away. | |
| 66 | ||
| 67 | #+CAPTION: Zork Screen, pt 3. | |
| 68 | [[https://img.cleberg.net/blog/20201229-zork/zork_03.png]] | |
| 69 | ||
| 70 | * Conclusion | |
| 71 | ||
| 72 | It's been quite a few years since I first played Zork, but I clearly remember | |
| 73 | the late nights and bloodshot eyes that helped me find all the treasures. This | |
| 74 | game is well worth the time and effort, even though the text-based aspect may be | |
| 75 | off-putting to gamers who didn't have to grow up playing games without graphics. | |
| 76 | However, I believe that the strategy and skills learned in early video games | |
| 77 | like Zork can actually help you, even when playing newer games. | |
| 78 | ||
| 79 | If you do decide to play Zork, you can download Zork I, II, and III from | |
| 80 | Infocom's [[http://infocom-if.org/downloads/downloads.html][download page]] for free or search the internet for an online version. | |
blog/2021-10-09-apache-redirect.org deleted −47
| @@ -1,47 +0,0 @@ | ||
| 1 | #+title: Apache Redirect HTML Files to a Directory | |
| 2 | #+date: 2021-10-10 | |
| 3 | ||
| 4 | ** The Problem | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: the-problem | |
| 7 | :END: | |
| 8 | After recently switching static site generators (SSG), my blog URLs | |
| 9 | changed with no option to preserve the classic =.html= extension at the | |
| 10 | end of my blog post URLs. | |
| 11 | ||
| 12 | I really disliked using my old SSG ([[https://jekyllrb.com][Jekyll]]) | |
| 13 | and prefer my new tool ([[https://www.getzola.org][Zola]]) much more, so | |
| 14 | I was determined to figure out a way to get the proper redirect set up | |
| 15 | so that people who find my posts online aren't constantly met by 404 | |
| 16 | errors. | |
| 17 | ||
| 18 | ** The Solution | |
| 19 | :PROPERTIES: | |
| 20 | :CUSTOM_ID: the-solution | |
| 21 | :END: | |
| 22 | To solve this problem, I really needed to solve two pieces: | |
| 23 | ||
| 24 | 1. Redirect all blog post URL requests from =/blog/some-post.html= to | |
| 25 | =/blog/some-post/=. | |
| 26 | 2. Ensure that no other =.html= files are redirected, such as | |
| 27 | =index.html=. | |
| 28 | ||
| 29 | After /a lot/ of tweaking and testing, I believe I have finally found | |
| 30 | the solution. The solution is shown below. | |
| 31 | ||
| 32 | #+begin_src config | |
| 33 | RewriteEngine On | |
| 34 | RewriteCond %{REQUEST_URI} !\index.html$ [NC] | |
| 35 | RewriteRule ^(.*).html$ https://example.com/$1 [R=301,L] | |
| 36 | #+end_src | |
| 37 | ||
| 38 | This piece of code in the Apache =.conf= or =.htaccess= file will do the | |
| 39 | following: | |
| 40 | ||
| 41 | 1. Turn on the RewriteEngine so that we can modify URLs. | |
| 42 | 2. Ignore any =index.html= files from the rule we are about to specify. | |
| 43 | 3. Find any =.html= files within the website directory and redirect it | |
| 44 | to exclude the file extension. | |
| 45 | 4. The final piece is adding the trailing slash (=/=) at the end of the | |
| 46 | URL - you'll notice that I don't have an Apache rule for that since | |
| 47 | Apache handles that automatically. | |
blog/2022-02-17-exiftool.org deleted −67
| @@ -1,67 +0,0 @@ | ||
| 1 | #+title: Stripping Image Metadata with exiftool | |
| 2 | #+date: 2022-02-17 | |
| 3 | ||
| 4 | ** Why Strip Metadata? | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: why-strip-metadata | |
| 7 | :END: | |
| 8 | Okay, so you want to strip metadata from your photos. Perhaps you take | |
| 9 | pictures of very rare birds, and the location metadata is a gold mine | |
| 10 | for poachers, or perhaps you're just privacy-oriented like me and prefer | |
| 11 | to strip metadata from publicly-available images. | |
| 12 | ||
| 13 | There are various components of image metadata that you may want to | |
| 14 | delete before releasing a photo to the public. Here's an incomplete list | |
| 15 | of things I could easily see just by inspecting a photo on my laptop: | |
| 16 | ||
| 17 | - Location (Latitude & Longitude) | |
| 18 | - Dimensions | |
| 19 | - Device Make & Model | |
| 20 | - Color Space | |
| 21 | - Color Profile | |
| 22 | - Focal Length | |
| 23 | - Alpha Channel | |
| 24 | - Red Eye | |
| 25 | - Metering Mode | |
| 26 | - F Number | |
| 27 | ||
| 28 | Regardless of your reasoning, I'm going to explain how I used the | |
| 29 | =exiftool= package in Linux to automatically strip metadata from all | |
| 30 | images in a directory (+ subdirectories). | |
| 31 | ||
| 32 | ** Installing =exiftool= | |
| 33 | :PROPERTIES: | |
| 34 | :CUSTOM_ID: installing-exiftool | |
| 35 | :END: | |
| 36 | First things first: we need to install the tool. I'm running Debian 11 | |
| 37 | on my server (Ubuntu will work the same), so the command is as simple | |
| 38 | as: | |
| 39 | ||
| 40 | #+begin_src sh | |
| 41 | sudo apt install exiftool | |
| 42 | #+end_src | |
| 43 | ||
| 44 | There are different tools that can accomplish the same thing across | |
| 45 | distributions, but I really only care to test out this one package. | |
| 46 | ||
| 47 | ** Recursively Strip Data | |
| 48 | :PROPERTIES: | |
| 49 | :CUSTOM_ID: recursively-strip-data | |
| 50 | :END: | |
| 51 | I actually use this tool extensively to strip any photos uploaded to the | |
| 52 | website that serves all the images for my blog (=img.cleberg.net=). | |
| 53 | ||
| 54 | The following command is incredibly useful and can be modified to | |
| 55 | include any image extensions that =exiftool= supports: | |
| 56 | ||
| 57 | #+begin_src sh | |
| 58 | exiftool -r -all= -ext jpg -ext png /path/to/directory/ | |
| 59 | #+end_src | |
| 60 | ||
| 61 | See below for the results of my most recent usage of =exiftool= after I | |
| 62 | uploaded the image for this blog post. You can see that the command will | |
| 63 | let you know how many directories were scanned, how many images were | |
| 64 | updated, and how many images were unchanged. | |
| 65 | ||
| 66 | #+caption: exiftool results | |
| 67 | [[https://img.cleberg.net/blog/20220217-stripping-metadata-with-exiftool/exiftool.png]] | |
blog/2022-02-20-nginx-caching.org deleted −75
| @@ -1,75 +0,0 @@ | ||
| 1 | #+title: Caching Static Content with Nginx | |
| 2 | #+date: 2022-02-20 | |
| 3 | ||
| 4 | ** Update Your Nginx Config to Cache Static Files | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: update-your-nginx-config-to-cache-static-files | |
| 7 | :END: | |
| 8 | If you run a website on Nginx that serves static content (i.e., content | |
| 9 | that is not dynamic and changing with interactions from the user), you | |
| 10 | would likely benefit from caching that content on the client-side. If | |
| 11 | you're used to Apache and looking for the Nginx equivalent, this post | |
| 12 | should help. | |
| 13 | ||
| 14 | Luckily, setting up the cache is as easy as identifying the file types | |
| 15 | you want to cache and determining the expiration length. To include more | |
| 16 | file types, simply use the bar separator (=|=) and type the new file | |
| 17 | extension you want to include. | |
| 18 | ||
| 19 | #+begin_src config | |
| 20 | server { | |
| 21 | ... | |
| 22 | ||
| 23 | location ~* .(css|js|jpg|jpeg|gif|png|ico)$ { | |
| 24 | expires 30d; | |
| 25 | } | |
| 26 | ||
| 27 | ... | |
| 28 | } | |
| 29 | #+end_src | |
| 30 | ||
| 31 | I have seen some people who prefer to set =expires= as =365d= or even | |
| 32 | =max=, but that is only for stable, infrequently changing websites. As | |
| 33 | my site often changes (i.e., I'm never content with my website), I need | |
| 34 | to know that my readers are seeing the new content without waiting too | |
| 35 | long. | |
| 36 | ||
| 37 | So, I went ahead and set the expiration date at =30d=, which is short | |
| 38 | enough to refresh for readers but long enough that clients/browsers | |
| 39 | won't be re-requesting the static files too often, hopefully resulting | |
| 40 | in faster loading times, as images should be the only thing slowing down | |
| 41 | my site. | |
| 42 | ||
| 43 | ** Testing Results | |
| 44 | :PROPERTIES: | |
| 45 | :CUSTOM_ID: testing-results | |
| 46 | :END: | |
| 47 | To test my changes to the Nginx configuration, I used the | |
| 48 | [[https://addons.mozilla.org/en-US/firefox/addon/http-header-live/][HTTP | |
| 49 | Header Live]] extension on my Gecko browser and used the sidebar to | |
| 50 | inspect the headers of a recent image from my blog. | |
| 51 | ||
| 52 | In the image below, you can see that the =Cache-Control= header is now | |
| 53 | present and set to 2592000, which is 30 days represented in seconds (30 | |
| 54 | days _ 24 hours/day _ 60 minutes/hour * 60 seconds/minute = 2,592,000 | |
| 55 | seconds). | |
| 56 | ||
| 57 | The =Expires= field is now showing 22 March 2022, which is 30 days from | |
| 58 | the day of this post, 20 February 2022. | |
| 59 | ||
| 60 | #+caption: Image Headers | |
| 61 | [[https://img.cleberg.net/blog/20220220-caching-static-content-with-nginx/image_headers.png]] | |
| 62 | ||
| 63 | ** Caveats | |
| 64 | :PROPERTIES: | |
| 65 | :CUSTOM_ID: caveats | |
| 66 | :END: | |
| 67 | Remember that this caching system is *client-side*, which means that | |
| 68 | content is only cached for as long as a client allows it. For example, | |
| 69 | my browser purges all caches, data, etc. upon exit, so this caching | |
| 70 | policy will only work as long as my browser remains open and running. | |
| 71 | ||
| 72 | If you need to test updates to your site, you'll need to clear the cache | |
| 73 | to see updates for any file extension you configured. This can often be | |
| 74 | done with the =Shift + F5= or =Ctrl + F5= key combinations in most | |
| 75 | browsers. | |
blog/2022-02-22-tuesday.org deleted −38
| @@ -1,38 +0,0 @@ | ||
| 1 | #+title: Tuesday | |
| 2 | #+date: 2022-02-22 | |
| 3 | ||
| 4 | ** Tuesday, Twosday | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: tuesday-twosday | |
| 7 | :END: | |
| 8 | I'm taking a break from my usual technology-related posts and writing | |
| 9 | about something a little more enjoyable today. | |
| 10 | ||
| 11 | Today is Tuesday, February 22nd, 2022. Today is 02-22-2022. Today is | |
| 12 | Twosday. | |
| 13 | ||
| 14 | Let's look at everything that fell in place today: | |
| 15 | ||
| 16 | 1. Written in the =m-dd-yy= or =dd-m-yy= formats, today is 2-22-22 or | |
| 17 | 22-2-22, which is a neat little palindrome in either format. (The | |
| 18 | last ubiquitous six-digit palindrome was 1-11-11.) | |
| 19 | 2. Today is Tuesday, which is why everyone is using the nickname Twosday | |
| 20 | to call out these similarities. | |
| 21 | 3. Falling on Tuesday means today is the 2nd day of the week (for most | |
| 22 | cultures. For the US, it's the 3rd day of the week since we start on | |
| 23 | Sunday). | |
| 24 | 4. The only culture I could find with a connection to a =2= is that some | |
| 25 | Slavic languages derived their version of Tuesday from the Old Church | |
| 26 | Slavonic word =въторъ=, meaning "the second." | |
| 27 | 5. Written in the classic monospaced, digital font (think of digital | |
| 28 | clocks from the 80s/90s), there is nice symmetry to the numbers | |
| 29 | ([[https://img.cleberg.net/blog/20220222-tuesday/digital_font.png][view | |
| 30 | the image here]]!). | |
| 31 | 6. This one isn't naturally-occurring, but it seems people around the | |
| 32 | world are celebrating the day. For example, a group is putting | |
| 33 | together | |
| 34 | [[https://www.eventbrite.com/e/2-22-22-a-collective-wedding-ceremony-at-the-state-capitol-tickets-211434605597][a | |
| 35 | wedding of 222 couples at the California State Capitol in | |
| 36 | Sacramento]], concluding at exactly 2:22 PM. These couples will | |
| 37 | record their marriage dates as 2-22-22 2:22 PM. Tickets were on sale | |
| 38 | for $222.22. | |
blog/2022-03-03-financial-database.org deleted −272
| @@ -1,272 +0,0 @@ | ||
| 1 | #+title: Maintaining a Personal Financial Database | |
| 2 | #+date: 2022-03-03 | |
| 3 | ||
| 4 | ** Personal Financial Tracking | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: personal-financial-tracking | |
| 7 | :END: | |
| 8 | For the last 6-ish years, I've tracked my finances in a spreadsheet. | |
| 9 | This is common practice in the business world, but any good dev will | |
| 10 | cringe at the thought of storing long-term data in a spreadsheet. A | |
| 11 | spreadsheet is not for long-term storage or as a source of data to pull | |
| 12 | data/reports. | |
| 13 | ||
| 14 | As I wanted to expand the functionality of my financial data (e.g., | |
| 15 | adding more reports), I decided to migrate the data into a database. To | |
| 16 | run reports, I would query the database and use a language like Python | |
| 17 | or Javascript to process the data, perform calculations, and visualize | |
| 18 | the data. | |
| 19 | ||
| 20 | ** SQLite | |
| 21 | :PROPERTIES: | |
| 22 | :CUSTOM_ID: sqlite | |
| 23 | :END: | |
| 24 | When choosing the type of database I wanted to use for this project, I | |
| 25 | was split between three options: | |
| 26 | ||
| 27 | 1. MySQL: The database I have the most experience with and have used for | |
| 28 | years. | |
| 29 | 2. PostgreSQL: A database I'm new to, but want to learn. | |
| 30 | 3. SQLite: A database that I've used for a couple projects and have | |
| 31 | moderate experience. | |
| 32 | ||
| 33 | I ended up choosing SQLite since it can be maintained within a single | |
| 34 | =.sqlite= file, which allows me more flexibility for storage and backup. | |
| 35 | I keep this file in my cloud storage and pull it up whenever needed. | |
| 36 | ||
| 37 | *** GUI Editing | |
| 38 | :PROPERTIES: | |
| 39 | :CUSTOM_ID: gui-editing | |
| 40 | :END: | |
| 41 | Since I didn't want to try and import 1000--1500 records into my new | |
| 42 | database via the command line, I opted to use | |
| 43 | [[https://sqlitebrowser.org/][DB Browser for SQLite (DB4S)]] as a GUI | |
| 44 | tool. This application is excellent, and I don't see myself going back | |
| 45 | to the CLI when working in this database. | |
| 46 | ||
| 47 | DB4S allows you to copy a range of cells from a spreadsheet and paste it | |
| 48 | straight into the SQL table. I used this process for all 36 accounts, | |
| 49 | 1290 account statements, and 126 pay statements. Overall, I'm guessing | |
| 50 | this took anywhere between 4--8 hours. In comparison, it probably took | |
| 51 | me 2-3 days to initially create the spreadsheet. | |
| 52 | ||
| 53 | #+caption: DB4S | |
| 54 | [[https://img.cleberg.net/blog/20220303-maintaining-a-personal-financial-database/db4s.png]] | |
| 55 | ||
| 56 | *** Schema | |
| 57 | :PROPERTIES: | |
| 58 | :CUSTOM_ID: schema | |
| 59 | :END: | |
| 60 | The schema for this database is actually extremely simple and involves | |
| 61 | only three tables (for now): | |
| 62 | ||
| 63 | 1. Accounts | |
| 64 | 2. Statements | |
| 65 | 3. Payroll | |
| 66 | ||
| 67 | *Accounts* | |
| 68 | ||
| 69 | The Accounts table contains summary information about an account, such | |
| 70 | as a car loan or a credit card. By viewing this table, you can find | |
| 71 | high-level data, such as interest rate, credit line, or owner. | |
| 72 | ||
| 73 | #+begin_src sql | |
| 74 | CREATE TABLE "Accounts" ( | |
| 75 | "AccountID" INTEGER NOT NULL UNIQUE, | |
| 76 | "AccountType" TEXT, | |
| 77 | "AccountName" TEXT, | |
| 78 | "InterestRate" NUMERIC, | |
| 79 | "CreditLine" NUMERIC, | |
| 80 | "State" TEXT, | |
| 81 | "Owner" TEXT, | |
| 82 | "Co-Owner" TEXT, | |
| 83 | PRIMARY KEY("AccountID" AUTOINCREMENT) | |
| 84 | ) | |
| 85 | #+end_src | |
| 86 | ||
| 87 | *Statements* | |
| 88 | ||
| 89 | The Statements table uses the same unique identifier as the Accounts | |
| 90 | table, meaning you can join the tables to find a monthly statement for | |
| 91 | any of the accounts listed in the Accounts table. Each statement has an | |
| 92 | account ID, statement date, and total balance. | |
| 93 | ||
| 94 | #+begin_src sql | |
| 95 | CREATE TABLE "Statements" ( | |
| 96 | "StatementID" INTEGER NOT NULL UNIQUE, | |
| 97 | "AccountID" INTEGER, | |
| 98 | "StatementDate" INTEGER, | |
| 99 | "Balance" NUMERIC, | |
| 100 | PRIMARY KEY("StatementID" AUTOINCREMENT), | |
| 101 | FOREIGN KEY("AccountID") REFERENCES "Accounts"("AccountID") | |
| 102 | ) | |
| 103 | #+end_src | |
| 104 | ||
| 105 | *Payroll* | |
| 106 | ||
| 107 | The Payroll table is a separate entity, unrelated to the Accounts or | |
| 108 | Statements tables. This table contains all information you would find on | |
| 109 | a pay statement from an employer. As you change employers or obtain new | |
| 110 | perks/benefits, just add new columns to adapt to the new data. | |
| 111 | ||
| 112 | #+begin_src sql | |
| 113 | CREATE TABLE "Payroll" ( | |
| 114 | "PaycheckID" INTEGER NOT NULL UNIQUE, | |
| 115 | "PayDate" TEXT, | |
| 116 | "Payee" TEXT, | |
| 117 | "Employer" TEXT, | |
| 118 | "JobTitle" TEXT, | |
| 119 | "IncomeRegular" NUMERIC, | |
| 120 | "IncomePTO" NUMERIC, | |
| 121 | "IncomeHoliday" NUMERIC, | |
| 122 | "IncomeBonus" NUMERIC, | |
| 123 | "IncomePTOPayout" NUMERIC, | |
| 124 | "IncomeReimbursements" NUMERIC, | |
| 125 | "FringeHSA" NUMERIC, | |
| 126 | "FringeStudentLoan" NUMERIC, | |
| 127 | "Fringe401k" NUMERIC, | |
| 128 | "PreTaxMedical" NUMERIC, | |
| 129 | "PreTaxDental" NUMERIC, | |
| 130 | "PreTaxVision" NUMERIC, | |
| 131 | "PreTaxLifeInsurance" NUMERIC, | |
| 132 | "PreTax401k" NUMERIC, | |
| 133 | "PreTaxParking" NUMERIC, | |
| 134 | "PreTaxStudentLoan" NUMERIC, | |
| 135 | "PreTaxOther" NUMERIC, | |
| 136 | "TaxFederal" NUMERIC, | |
| 137 | "TaxSocial" NUMERIC, | |
| 138 | "TaxMedicare" NUMERIC, | |
| 139 | "TaxState" NUMERIC, | |
| 140 | PRIMARY KEY("PaycheckID" AUTOINCREMENT) | |
| 141 | ) | |
| 142 | #+end_src | |
| 143 | ||
| 144 | *** Python Reporting | |
| 145 | :PROPERTIES: | |
| 146 | :CUSTOM_ID: python-reporting | |
| 147 | :END: | |
| 148 | Once I created the database tables and imported all my data, the only | |
| 149 | step left was to create a process to report and visualize on various | |
| 150 | aspects of the data. | |
| 151 | ||
| 152 | In order to explore and create the reports I'm interested in, I utilized | |
| 153 | a two-part process involving Jupyter Notebooks and Python scripts. | |
| 154 | ||
| 155 | **** Step 1: Jupyter Notebooks | |
| 156 | :PROPERTIES: | |
| 157 | :CUSTOM_ID: step-1-jupyter-notebooks | |
| 158 | :END: | |
| 159 | When I need to explore data, try different things, and re-run my code | |
| 160 | cell-by-cell, I use Jupyter Notebooks. For example, I explored the | |
| 161 | =Accounts= table until I found the following useful information: | |
| 162 | ||
| 163 | #+begin_src python | |
| 164 | import sqlite3 | |
| 165 | import pandas as pd | |
| 166 | import matplotlib | |
| 167 | ||
| 168 | # Set up database filename and connect | |
| 169 | db = "finances.sqlite" | |
| 170 | connection = sqlite3.connect(db) | |
| 171 | df = pd.read_sql_query("SELECT * FROM Accounts", connection) | |
| 172 | ||
| 173 | # Set global matplotlib variables | |
| 174 | %matplotlib inline | |
| 175 | matplotlib.rcParams['text.color'] = 'white' | |
| 176 | matplotlib.rcParams['axes.labelcolor'] = 'white' | |
| 177 | matplotlib.rcParams['xtick.color'] = 'white' | |
| 178 | matplotlib.rcParams['ytick.color'] = 'white' | |
| 179 | matplotlib.rcParams['legend.labelcolor'] = 'black' | |
| 180 | ||
| 181 | # Display graph | |
| 182 | df.groupby(['AccountType']).sum().plot.pie(title='Credit Line by Account Type', y='CreditLine', figsize=(5,5), autopct='%1.1f%%') | |
| 183 | #+end_src | |
| 184 | ||
| 185 | **** Step 2: Python Scripts | |
| 186 | :PROPERTIES: | |
| 187 | :CUSTOM_ID: step-2-python-scripts | |
| 188 | :END: | |
| 189 | Once I explored enough through the notebooks and had a list of reports I | |
| 190 | wanted, I moved on to create a Python project with the following | |
| 191 | structure: | |
| 192 | ||
| 193 | #+begin_example | |
| 194 | finance/ | |
| 195 | ├── notebooks/ | |
| 196 | │ │ ├── account_summary.ipynb | |
| 197 | │ │ ├── account_details.ipynb | |
| 198 | │ │ └── payroll.ipynb | |
| 199 | ├── public/ | |
| 200 | │ │ ├── image-01.png | |
| 201 | │ │ └── image-0X.png | |
| 202 | ├── src/ | |
| 203 | │ └── finance.sqlite | |
| 204 | ├── venv/ | |
| 205 | ├── _init.py | |
| 206 | ├── database.py | |
| 207 | ├── process.py | |
| 208 | ├── requirements.txt | |
| 209 | └── README.md | |
| 210 | #+end_example | |
| 211 | ||
| 212 | This structure allows me to: | |
| 213 | ||
| 214 | 1. Compile all required python packages into =requirements.txt= for easy | |
| 215 | installation if I move to a new machine. | |
| 216 | 2. Activate a virtual environment in =venv/= so I don't need to maintain | |
| 217 | a system-wide Python environment just for this project. | |
| 218 | 3. Keep my =notebooks/= folder to continuously explore the data as I see | |
| 219 | fit. | |
| 220 | 4. Maintain a local copy of the database in =src/= for easy access. | |
| 221 | 5. Export reports, images, HTML files, etc. to =public/=. | |
| 222 | ||
| 223 | Now, onto the differences between the code in a Jupyter Notebook and the | |
| 224 | actual Python files. To create the report in the Notebook snippet above, | |
| 225 | I created the following function inside =process.py=: | |
| 226 | ||
| 227 | #+begin_src python | |
| 228 | # Create summary pie chart | |
| 229 | def summary_data(accounts: pandas.DataFrame) -> None: | |
| 230 | accounts_01 = accounts[accounts["Owner"] == "Person01"] | |
| 231 | accounts_02 = accounts[accounts["Owner"] == "Person02"] | |
| 232 | for x in range(1, 4): | |
| 233 | if x == 1: | |
| 234 | df = accounts | |
| 235 | account_string = "All Accounts" | |
| 236 | elif x == 2: | |
| 237 | df = accounts_01 | |
| 238 | account_string = "Person01's Accounts" | |
| 239 | elif x == 3: | |
| 240 | df = accounts_02 | |
| 241 | account_string = "Person02's Accounts" | |
| 242 | print(f"Generating pie chart summary image for {account_string}...") | |
| 243 | summary_chart = ( | |
| 244 | df.groupby(["AccountType"]) | |
| 245 | .sum() | |
| 246 | .plot.pie( | |
| 247 | title=f"Credit Line by Type for {account_string}", | |
| 248 | y="CreditLine", | |
| 249 | autopct="%1.1f%%", | |
| 250 | ) | |
| 251 | ) | |
| 252 | summary_chart.figure.savefig(f"public/summary_chart_{x}.png", dpi=1200) | |
| 253 | #+end_src | |
| 254 | ||
| 255 | The result? A high-quality pie chart that is read directly by the | |
| 256 | =public/index.html= template I use. | |
| 257 | ||
| 258 | #+caption: Summary Pie Chart | |
| 259 | [[https://img.cleberg.net/blog/20220303-maintaining-a-personal-financial-database/summary_chart.png]] | |
| 260 | ||
| 261 | Other charts generated by this project include: | |
| 262 | ||
| 263 | - Charts of account balances over time. | |
| 264 | - Line chart of effective tax rate (taxes divided by taxable income). | |
| 265 | - Salary projections and error limits using past income and inflation | |
| 266 | rates. | |
| 267 | - Multi-line chart of gross income, taxable income, and net income. | |
| 268 | ||
| 269 | The best thing about this project? I can improve it at any given time, | |
| 270 | shaping it into whatever helps me the most for that time. I imagine that | |
| 271 | I will be introducing an asset tracking table soon to track the | |
| 272 | depreciating value of cars, houses, etc. Who knows what's next? | |
blog/2022-03-08-plex-migration.org deleted −270
| @@ -1,270 +0,0 @@ | ||
| 1 | #+title: Migrating Plex to New Server (+ Nvidia Transcoding) | |
| 2 | #+date: 2022-03-08 | |
| 3 | ||
| 4 | ** Migration Phases | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: migration-phases | |
| 7 | :END: | |
| 8 | I recently decided to migrate my server from an old OptiPlex desktop | |
| 9 | machine to a custom-built tower with better hardware in every category. | |
| 10 | In order to do this, I would need to properly migrate a full Plex | |
| 11 | installation. | |
| 12 | ||
| 13 | The second part of this migration is that the new server uses an Nvidia | |
| 14 | GPU and does not have any integrated graphics, which requires extra work | |
| 15 | for installation, but provides much better hardware transcoding options | |
| 16 | for Plex. | |
| 17 | ||
| 18 | Therefore, I have broken this migration down into three phases: | |
| 19 | ||
| 20 | 1. [[#phase-1-configure-the-new-server][Configure the New Server]] | |
| 21 | 2. [[#phase-2-migrate-plex-data-devices][Migrate Plex Data & Devices]] | |
| 22 | 3. [[#phase-3-configure-gpu-transcoding][Configure GPU Transcoding]] | |
| 23 | ||
| 24 | -------------- | |
| 25 | ||
| 26 | ** Phase 1: Configure the New Server | |
| 27 | :PROPERTIES: | |
| 28 | :CUSTOM_ID: phase-1-configure-the-new-server | |
| 29 | :END: | |
| 30 | *** Choosing an OS | |
| 31 | :PROPERTIES: | |
| 32 | :CUSTOM_ID: choosing-an-os | |
| 33 | :END: | |
| 34 | In order to migrate Plex to my new server, I first needed to choose an | |
| 35 | appropriate operating system (OS) and install it on the machine. Given | |
| 36 | that I have encountered numerous issues installing other Linux | |
| 37 | distributions properly with Nvidia graphics, I chose | |
| 38 | [[https://ubuntu.com/download/server][Ubuntu Server]]. | |
| 39 | ||
| 40 | The first step is to create a bootable USB with Ubuntu Server. This is | |
| 41 | easy with [[https://www.balena.io/etcher/][Etcher]], an app that runs on | |
| 42 | many different platforms. Just download the Ubuntu Server =.iso= image, | |
| 43 | launch Etcher, and install the =.iso= on the USB. | |
| 44 | ||
| 45 | Once the USB is created, insert it into my server, reboot, and click | |
| 46 | =Esc= (or any of the =F1-12= keys) until the BIOS menu appears. Finally, | |
| 47 | launch the USB boot drive. | |
| 48 | ||
| 49 | *** Booting with Nvidia | |
| 50 | :PROPERTIES: | |
| 51 | :CUSTOM_ID: booting-with-nvidia | |
| 52 | :END: | |
| 53 | In order to install Ubuntu Server with an Nvidia Graphics card (and no | |
| 54 | integrated graphics on this device for some reason), you'll have to | |
| 55 | configure the boot menu to allow different graphics drivers to be | |
| 56 | loaded. | |
| 57 | ||
| 58 | When booting from the USB, the machine will launch the initial | |
| 59 | installation menu. From this menu, type =e= to view the default command | |
| 60 | options that come with the device - it's a good idea to take a photo of | |
| 61 | this screen, so you can enter these commands on the next screen (along | |
| 62 | with adding support for Nvidia). | |
| 63 | ||
| 64 | Finally, type =Ctrl + C= to enter the command line. From this command | |
| 65 | line, enter the commands found on the =e= screen. *Remember to add | |
| 66 | =nomodeset= to the =linux ...= line so that your Nvidia device will | |
| 67 | display the installation screens properly!* | |
| 68 | ||
| 69 | Here's an example of the commands I pulled from the =e= screen and | |
| 70 | entered on the command line. | |
| 71 | ||
| 72 | #+begin_src sh | |
| 73 | setparams 'Install Ubuntu Server' | |
| 74 | setgfxpayload=keep | |
| 75 | linux /casper/vmlinuz quiet nomodeset --- | |
| 76 | initrd /casper/initrd | |
| 77 | boot | |
| 78 | #+end_src | |
| 79 | ||
| 80 | #+caption: Ubuntu Server Installation 01 | |
| 81 | [[https://img.cleberg.net/blog/20220308-plex-media-server-migration/ubuntu_server_installation_01.png]] | |
| 82 | ||
| 83 | #+caption: Ubuntu Server Installation 02 | |
| 84 | [[https://img.cleberg.net/blog/20220308-plex-media-server-migration/ubuntu_server_installation_02.png]] | |
| 85 | ||
| 86 | Once the machine is rebooted, enter the =e= screen again and add | |
| 87 | =nomodeset= to the =linux ...= line again and press =Ctrl + X= to save | |
| 88 | the boot options. | |
| 89 | ||
| 90 | The machine is now fully installed and can properly display on an | |
| 91 | external display using the Nvidia GPU. | |
| 92 | ||
| 93 | Always remember to update and upgrade on a new installation: | |
| 94 | ||
| 95 | #+begin_src sh | |
| 96 | sudo apt update; sudo apt upgrade -y; sudo apt autoremove -y | |
| 97 | #+end_src | |
| 98 | ||
| 99 | -------------- | |
| 100 | ||
| 101 | ** Phase 2: Migrate Plex Data & Devices | |
| 102 | :PROPERTIES: | |
| 103 | :CUSTOM_ID: phase-2-migrate-plex-data-devices | |
| 104 | :END: | |
| 105 | This phase uses the great Plex article on migrations | |
| 106 | ([[https://support.plex.tv/articles/201370363-move-an-install-to-another-system/][Move | |
| 107 | an Installation to Another System]]) and adds a bit more information to | |
| 108 | help with commands and context. | |
| 109 | ||
| 110 | *** Terminology | |
| 111 | :PROPERTIES: | |
| 112 | :CUSTOM_ID: terminology | |
| 113 | :END: | |
| 114 | *Source:* The original server that is being replaced.\\ | |
| 115 | *Destination:* The new server.\\ | |
| 116 | *Client:* Any application that can be used to modify settings for both | |
| 117 | source/destination. | |
| 118 | ||
| 119 | *** Step 01: [Client] Update Settings | |
| 120 | :PROPERTIES: | |
| 121 | :CUSTOM_ID: step-01-client-update-settings | |
| 122 | :END: | |
| 123 | Open up a Plex app and /disable/ the =Account= > =Library= > | |
| 124 | =Empty trash automatically after every scan= preference for the source | |
| 125 | server. | |
| 126 | ||
| 127 | *** Step 02: [Destination] Install Plex | |
| 128 | :PROPERTIES: | |
| 129 | :CUSTOM_ID: step-02-destination-install-plex | |
| 130 | :END: | |
| 131 | Open up the [[https://www.plex.tv/media-server-downloads/][Plex Media | |
| 132 | Server download page]] and copy the link for the appropriate platform. | |
| 133 | ||
| 134 | Execute the following commands on the destination server to install | |
| 135 | Plex: | |
| 136 | ||
| 137 | #+begin_src sh | |
| 138 | wget <url> | |
| 139 | sudo dpkg -i <filename> | |
| 140 | sudo systemctl stop plexmediaserver.service | |
| 141 | #+end_src | |
| 142 | ||
| 143 | *** Step 03: [Source] Stop Plex & Migrate Data | |
| 144 | :PROPERTIES: | |
| 145 | :CUSTOM_ID: step-03-source-stop-plex-migrate-data | |
| 146 | :END: | |
| 147 | First, stop the Plex service so that no data is created or modified | |
| 148 | during the migration. | |
| 149 | ||
| 150 | #+begin_src sh | |
| 151 | sudo systemctl stop plexmediaserver.service | |
| 152 | #+end_src | |
| 153 | ||
| 154 | Next, copy the data to the new server. To find where the Plex data | |
| 155 | directory is located, Plex has another excellent article available: | |
| 156 | [[https://support.plex.tv/articles/202915258-where-is-the-plex-media-server-data-directory-located/][Where | |
| 157 | is the Plex Media Server data directory located?]]. | |
| 158 | ||
| 159 | There are many ways to copy the data to the new server and will largely | |
| 160 | depend on the size of the folder being copied. Personally, my data | |
| 161 | folder was ~23GB and I opted to simply use the =scp= command to copy the | |
| 162 | files over SSH. | |
| 163 | ||
| 164 | This process was throttled by the old server's slow HDD and ports and | |
| 165 | took approximately 90 minutes to complete. In comparison, moving the | |
| 166 | data from the new server's =home/user/= directory to the | |
| 167 | =/var/.../Plex Media Server= directory took 2-3 minutes. | |
| 168 | ||
| 169 | #+begin_src sh | |
| 170 | scp -r "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server" your_user@xxx.xxx.xxx.xxx:"'/path/to/destination/'" | |
| 171 | #+end_src | |
| 172 | ||
| 173 | *** Step 04: [Destination] Update File Permissions | |
| 174 | :PROPERTIES: | |
| 175 | :CUSTOM_ID: step-04-destination-update-file-permissions | |
| 176 | :END: | |
| 177 | In case you move the data directory to a common area on the new server, | |
| 178 | it will have to be moved to the proper location before Plex can function | |
| 179 | properly: | |
| 180 | ||
| 181 | #+begin_src sh | |
| 182 | mv "Plex Media Server" /var/lib/plexmediaserver/Library/Application Support/ | |
| 183 | #+end_src | |
| 184 | ||
| 185 | To ensure permissions were retained properly, the server will need to | |
| 186 | show that all files and folders in the data directory are owned by | |
| 187 | =plex:plex= (or whichever user is running the Plex application). | |
| 188 | ||
| 189 | #+begin_src sh | |
| 190 | sudo chown -R plex:plex "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server" | |
| 191 | #+end_src | |
| 192 | ||
| 193 | Finally, start the service and check the status. | |
| 194 | ||
| 195 | #+begin_src sh | |
| 196 | sudo systemctl start plexmediaserver.service | |
| 197 | sudo systemctl status plexmediaserver.service | |
| 198 | #+end_src | |
| 199 | ||
| 200 | *** Step 05: [Client] Update Libraries & Metadata | |
| 201 | :PROPERTIES: | |
| 202 | :CUSTOM_ID: step-05-client-update-libraries-metadata | |
| 203 | :END: | |
| 204 | The first step - now that the new server is up and running - is to sign | |
| 205 | out of the client and sign back in. Once this is done, update any | |
| 206 | library locations, if necessary. This was unnecessary in my case since I | |
| 207 | simply moved my storage drives from the source server to the destination | |
| 208 | server. | |
| 209 | ||
| 210 | Next, perform the following actions in the client: | |
| 211 | ||
| 212 | 1. On the left sidebar, click =More= > Three-Dot Menu > | |
| 213 | =Scan Library Files= | |
| 214 | 2. /Enable/ the =Account= > =Library= > | |
| 215 | =Empty trash automatically after every scan= preference for the | |
| 216 | source server. | |
| 217 | 3. On the left sidebar, click =More= > Three-Dot Menu > =Manage Server= | |
| 218 | > =Empty Trash= | |
| 219 | 4. On the left sidebar, click =More= > Three-Dot Menu > =Manage Server= | |
| 220 | > =Clean Bundles= | |
| 221 | 5. On the left sidebar, click =More= > Three-Dot Menu > =Manage Server= | |
| 222 | > =Optimize Database= | |
| 223 | ||
| 224 | Finally, double-check the Remote Access settings to make sure no changes | |
| 225 | have caused issues with accessing the server from outside the network. | |
| 226 | ||
| 227 | In my case, I use a single port forwarding rule in my router and needed | |
| 228 | to update the Local LAN IP Address to the new server IP address. | |
| 229 | ||
| 230 | #+caption: Router port forwarding | |
| 231 | [[https://img.cleberg.net/blog/20220308-plex-media-server-migration/port_forwarding.png]] | |
| 232 | ||
| 233 | -------------- | |
| 234 | ||
| 235 | ** Phase 3: Configure GPU Transcoding | |
| 236 | :PROPERTIES: | |
| 237 | :CUSTOM_ID: phase-3-configure-gpu-transcoding | |
| 238 | :END: | |
| 239 | The final piece to the migration is enabling hardware transcoding so | |
| 240 | that Plex can fully utilize the new Nvidia GPU available in the server. | |
| 241 | The first step is to install Nvidia graphics drivers. This process may | |
| 242 | take a few minutes, but the commands are pretty simple: | |
| 243 | ||
| 244 | #+begin_src sh | |
| 245 | sudo add-apt-repository ppa:graphics-drivers/ppa | |
| 246 | sudo apt update | |
| 247 | sudo apt-get install ubuntu-drivers-common | |
| 248 | sudo ubuntu-drivers autoinstall | |
| 249 | #+end_src | |
| 250 | ||
| 251 | Finally, reboot so that the changes are loaded: | |
| 252 | ||
| 253 | #+begin_src sh | |
| 254 | sudo reboot now | |
| 255 | #+end_src | |
| 256 | ||
| 257 | To ensure that the Nvidia graphics drivers are working properly, run the | |
| 258 | following command to view the available GPUs, statistics, and processes: | |
| 259 | ||
| 260 | #+begin_src sh | |
| 261 | sudo nvidia-smi | |
| 262 | #+end_src | |
| 263 | ||
| 264 | #+caption: nvidia-smi | |
| 265 | [[https://img.cleberg.net/blog/20220308-plex-media-server-migration/nvidia_smi.png]] | |
| 266 | ||
| 267 | Finally, enable hardware transcoding settings in the Plex application: | |
| 268 | ||
| 269 | #+caption: Plex transcoding settings | |
| 270 | [[https://img.cleberg.net/blog/20220308-plex-media-server-migration/plex_transcoding.png]] | |
blog/2022-03-24-server-hardening.org deleted −386
| @@ -1,386 +0,0 @@ | ||
| 1 | #+title: Hardening a Public-Facing Home Server | |
| 2 | #+date: 2022-03-24 | |
| 3 | ||
| 4 | ** Post Updates | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: post-updates | |
| 7 | :END: | |
| 8 | ||
| 9 | #+begin_quote | |
| 10 | After reviewing this post today (2022-10-04), I noticed quite a few gaps | |
| 11 | in my write-up and wanted to add a few things, even though this blog is | |
| 12 | really just a retrospective and knowledge dump for myself. I left things | |
| 13 | intact and simply crossed them out (+like this+) for posterity. | |
| 14 | ||
| 15 | #+end_quote | |
| 16 | ||
| 17 | ** Planning Data Flows & Security | |
| 18 | :PROPERTIES: | |
| 19 | :CUSTOM_ID: planning-data-flows-security | |
| 20 | :END: | |
| 21 | *** My Personal Data Flow | |
| 22 | :PROPERTIES: | |
| 23 | :CUSTOM_ID: my-personal-data-flow | |
| 24 | :END: | |
| 25 | #+begin_src txt | |
| 26 | ┌───────┐ ┌─────────────────┐ | |
| 27 | ┌──► VLAN1 ├───► Private Devices │ | |
| 28 | │ └───────┘ └─────────────────┘ | |
| 29 | ┌──────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ │ | |
| 30 | │ Internet ├───► Router ├───► Firewall ├───► Switch ├──┤ | |
| 31 | └──────────┘ └────────┘ └──────────┘ └────────┘ │ | |
| 32 | │ ┌───────┐ ┌───────────────┐ | |
| 33 | └──► VLAN2 ├───► Public Server │ | |
| 34 | └───────┘ └───────────────┘ | |
| 35 | #+end_src | |
| 36 | ||
| 37 | *** Thought Process | |
| 38 | :PROPERTIES: | |
| 39 | :CUSTOM_ID: thought-process | |
| 40 | :END: | |
| 41 | To serve content from your home server and harden your security posture, | |
| 42 | you have to think about the transport of data from =server= to =client=. | |
| 43 | ||
| 44 | Let's start with the actual server itself. Think about the following: | |
| 45 | ||
| 46 | - Do I have a firewall enabled? Do I need to update this to allow new | |
| 47 | ports or IPs? | |
| 48 | - Do I have an IPS/IDS that may prevent outside traffic? | |
| 49 | - Do I have any other security software installed? | |
| 50 | - Are the services hosted inside Docker containers, behind a reverse | |
| 51 | proxy, or virtualized? If so, are they configured to allow outside | |
| 52 | traffic? | |
| 53 | ||
| 54 | Once the data leaves the server, where does it go? In my case, it goes | |
| 55 | to a managed switch. In this case, I asked the following: | |
| 56 | ||
| 57 | - What configurations is the switch using? | |
| 58 | - Am I using VLANs? | |
| 59 | - Yes, I am using 802.1Q VLANs. | |
| 60 | - Are the VLANs configured properly? | |
| 61 | - Yes, as shown in the [[#switch][Switch]] section below, I have a | |
| 62 | separate VLAN to allow outside traffic to and from the server alone. | |
| 63 | No other devices, except for a service port, and in that VLAN. | |
| 64 | ||
| 65 | At this point, the data has been processed through the switch. Where | |
| 66 | does it go next? In my case, it's pretty simple: it goes to the | |
| 67 | router/modem device. | |
| 68 | ||
| 69 | - Does my ISP block any ports that I need? | |
| 70 | - This is an important step that a lot of people run into when | |
| 71 | self-hosting at home. Use an online port-checker tool for your IP or | |
| 72 | call your ISP if you think ports are blocked. | |
| 73 | - Is there a router firewall? | |
| 74 | - Yes, I checked that it's configured to allow the ports I need to run | |
| 75 | my services publicly. Common web servers and reverse proxies require | |
| 76 | ports 80 and 443, but other services like media servers or games can | |
| 77 | require unique ports, so be sure to check the documentation for your | |
| 78 | service(s). | |
| 79 | - Are there any other settings affecting inbound/outbound traffic? | |
| 80 | - Schedules or access blocks | |
| 81 | - Static Routing | |
| 82 | - QoS | |
| 83 | - Port Forwarding | |
| 84 | - DMZ Hosting | |
| 85 | - Remote Management (this can sometimes mess with services that also | |
| 86 | require the use of ports 80 and 443) | |
| 87 | ||
| 88 | Once the data leaves my router, it goes to the upstream ISP and can be | |
| 89 | accessed publicly. | |
| 90 | ||
| 91 | ** Server | |
| 92 | :PROPERTIES: | |
| 93 | :CUSTOM_ID: server | |
| 94 | :END: | |
| 95 | +The services I run on my server are installed straight into the OS, | |
| 96 | without any use of Docker or VMs, so I don't need any extra application | |
| 97 | configuration to make them accessible to the outside world.+ | |
| 98 | ||
| 99 | As of 2022-10-04, the paragraph above is no longer true as I now run a | |
| 100 | reverse proxy with Nginx and host many services inside Docker. However, | |
| 101 | it doesn't change anything regarding this post as I still just need to | |
| 102 | open ports 80 & 443 and create the necessary website configuration | |
| 103 | files. | |
| 104 | ||
| 105 | When creating new services - either installed directly on bare metal or | |
| 106 | within something like Docker - I ensure that I read through the | |
| 107 | documentation thoroughly to understand a few key things: - What network | |
| 108 | activities should this app perform (if any)? Using which ports and | |
| 109 | protocols? - Does this app require any commands/services to be run as | |
| 110 | =root=? - Does this app log errors, authentication failures/successes, | |
| 111 | or anything else that would be useful for an investigation? | |
| 112 | ||
| 113 | For extra security, I use limit all incoming connections to SSH | |
| 114 | connections through my server firewall (=ufw=) and disable common SSH | |
| 115 | settings. After all of that, I use =fail2ban= as a preventative measure | |
| 116 | against brute-force login attempts. | |
| 117 | ||
| 118 | As another piece of security, you can randomize your SSH port to ensure | |
| 119 | that random scanners or attackers can't easily try to force their way | |
| 120 | into your network. For example, you can edit the port rules in your | |
| 121 | server to block all connection requests to port =22= but forward all | |
| 122 | remote connections from port =12345= to your server's port =22=. Then | |
| 123 | you just need to SSH to your network via your randomized port. | |
| 124 | ||
| 125 | *** =ufw= | |
| 126 | :PROPERTIES: | |
| 127 | :CUSTOM_ID: ufw | |
| 128 | :END: | |
| 129 | To see how to configure =ufw=, see my other post: | |
| 130 | [[/blog/secure-your-network-with-the-uncomplicated-firewall/][Secure | |
| 131 | Your Network with the Uncomplicated Firewall]]. | |
| 132 | ||
| 133 | The general notion with an on-device firewall is that you want to deny | |
| 134 | all incoming connections by default and then selectively open certain | |
| 135 | ports for services or users that you know need access. | |
| 136 | ||
| 137 | If you know that you will only be logging into this server from a | |
| 138 | certain set or list of IPs, you can always set the firewall to only | |
| 139 | allow connections to port 22 from those IPs. | |
| 140 | ||
| 141 | For a quick start to only allow SSH connections to the server, use this: | |
| 142 | ||
| 143 | #+begin_src sh | |
| 144 | sudo ufw default deny incoming | |
| 145 | sudo ufw default allow outgoing | |
| 146 | sudo ufw allow 22 | |
| 147 | sudo ufw enable | |
| 148 | #+end_src | |
| 149 | ||
| 150 | #+caption: ufw | |
| 151 | [[https://img.cleberg.net/blog/20220324-hardening-a-public-facing-home-server/ufw.png]] | |
| 152 | ||
| 153 | *** =ssh= | |
| 154 | :PROPERTIES: | |
| 155 | :CUSTOM_ID: ssh | |
| 156 | :END: | |
| 157 | **** Using SSH Keys | |
| 158 | :PROPERTIES: | |
| 159 | :CUSTOM_ID: using-ssh-keys | |
| 160 | :END: | |
| 161 | First, make sure you have an SSH keypair generated on the device(s) that | |
| 162 | you'll be using to log in to the server. If you don't have an SSH key, | |
| 163 | run this command: | |
| 164 | ||
| 165 | #+begin_src sh | |
| 166 | ssh-keygen | |
| 167 | #+end_src | |
| 168 | ||
| 169 | Now that we have an SSH key, copy it to the server with the following | |
| 170 | command, which will ask for the user's password before accepting the | |
| 171 | key: | |
| 172 | ||
| 173 | #+begin_src sh | |
| 174 | ssh-copy-id my_user@my_server | |
| 175 | #+end_src | |
| 176 | ||
| 177 | If you have multiple keys, you'll need to specify which to use. After | |
| 178 | it's complete, =ssh= back into the server as that user and make sure it | |
| 179 | doesn't ask for a password. | |
| 180 | ||
| 181 | **** Disable Password & Root Authentication | |
| 182 | :PROPERTIES: | |
| 183 | :CUSTOM_ID: disable-password-root-authentication | |
| 184 | :END: | |
| 185 | Now that we can access the server without a password, we will disable | |
| 186 | password authentication and disable anyone from using =ssh= to login as | |
| 187 | =root=. | |
| 188 | ||
| 189 | To do this, open the =sshd_config= file: | |
| 190 | ||
| 191 | #+begin_src sh | |
| 192 | sudo nano /etc/ssh/sshd_config | |
| 193 | #+end_src | |
| 194 | ||
| 195 | You'll need to update the parameters to the values below. If one of | |
| 196 | these rules is commented-out or doesn't exist, create the rule at the | |
| 197 | bottom of the file. | |
| 198 | ||
| 199 | #+begin_src config | |
| 200 | PermitRootLogin no | |
| 201 | PasswordAuthentication no | |
| 202 | PubkeyAuthentication yes | |
| 203 | #+end_src | |
| 204 | ||
| 205 | Finally, restart the =ssh= service: | |
| 206 | ||
| 207 | #+begin_src sh | |
| 208 | sudo systemctl restart sshd.service | |
| 209 | #+end_src | |
| 210 | ||
| 211 | To test that everything's working so far, open ANOTHER terminal and try | |
| 212 | logging in as =root= over SSH. It is very important that you keep your | |
| 213 | current SSH session open and test with an additional session, or you | |
| 214 | will lock yourself out at some point and will need to use a recovery | |
| 215 | method (e.g., hooking monitor up to home server) to get yourself back | |
| 216 | in. | |
| 217 | ||
| 218 | **** Enable MFA for =ssh= | |
| 219 | :PROPERTIES: | |
| 220 | :CUSTOM_ID: enable-mfa-for-ssh | |
| 221 | :END: | |
| 222 | This part is optional, but I highly recommend it. So far, we've ensured | |
| 223 | that no one can log into our user on the server without using our secret | |
| 224 | key, and we've ensured that no one can log in remotely as =root=. Next, | |
| 225 | you can enable MFA authentication for =ssh= connections. | |
| 226 | ||
| 227 | This process involves editing a couple files and installing an MFA | |
| 228 | package, so I will not include all the details in this post. To see how | |
| 229 | to configure MFA for =ssh=, see my other post: | |
| 230 | [[/blog/enable-totp-mfa-for-ssh/][Enabling MFA for SSH]]. | |
| 231 | ||
| 232 | #+caption: SSH MFA | |
| 233 | [[https://img.cleberg.net/blog/20220324-hardening-a-public-facing-home-server/ssh_mfa.png]] | |
| 234 | ||
| 235 | *** =fail2ban= | |
| 236 | :PROPERTIES: | |
| 237 | :CUSTOM_ID: fail2ban | |
| 238 | :END: | |
| 239 | I haven't written a post on how I use =fail2ban=, but it's quite simple. | |
| 240 | I use the default =sshd= jail, but you can always create new jails for | |
| 241 | respective applications or ports. For example, if you use Nginx as your | |
| 242 | web server, you can use the =nginx-http-auth= jail. | |
| 243 | ||
| 244 | In order to get it up and running, use the following commands: | |
| 245 | ||
| 246 | #+begin_src sh | |
| 247 | sudo apt install fail2ban | |
| 248 | sudo fail2ban-client start sshd | |
| 249 | sudo fail2ban-client status sshd | |
| 250 | #+end_src | |
| 251 | ||
| 252 | This should be used as a last-resort defense and shouldn't be a | |
| 253 | replacement for the security measures mentioned above. | |
| 254 | ||
| 255 | #+caption: fail2ban | |
| 256 | [[https://img.cleberg.net/blog/20220324-hardening-a-public-facing-home-server/fail2ban.png]] | |
| 257 | ||
| 258 | ** Switch | |
| 259 | :PROPERTIES: | |
| 260 | :CUSTOM_ID: switch | |
| 261 | :END: | |
| 262 | Between the router and any local devices is my managed switch, which is | |
| 263 | used to create VLANs. The example below shows how I would isolate the | |
| 264 | VLANs if I were starting to host a single service at home. | |
| 265 | ||
| 266 | *** 802.1Q VLAN Configuration | |
| 267 | :PROPERTIES: | |
| 268 | :CUSTOM_ID: q-vlan-configuration | |
| 269 | :END: | |
| 270 | In this configuration, port 8 is the public server that needs to be | |
| 271 | accessed from the outside. Port 23 is my 'dedicated service port' for | |
| 272 | this server. In order to SSH to this server, I need to plug my laptop | |
| 273 | into port 23 or else I cannot SSH. Otherwise, I'd need to hook up a | |
| 274 | monitor and keyboard directly to the server to manage it. | |
| 275 | ||
| 276 | | VLAN ID | VLAN Name | Member Ports | Tagged Ports | Untagged Ports | | |
| 277 | |---------+-----------+--------------+--------------+----------------| | |
| 278 | | 1 | Default | 1-24 | | 1-24 | | |
| 279 | | 2 | Server | 1,8,23 | | 1,8,23 | | |
| 280 | ||
| 281 | *** 802.1Q VLAN PVID Setting | |
| 282 | :PROPERTIES: | |
| 283 | :CUSTOM_ID: q-vlan-pvid-setting | |
| 284 | :END: | |
| 285 | Once the VLAN is created, I simply add the =VLAN ID= of =2= as the | |
| 286 | =PVID= for any related ports (in this case, see that ports =8= and =23= | |
| 287 | have a PVID of =2=). | |
| 288 | ||
| 289 | | Port | PVID | | |
| 290 | |------+------| | |
| 291 | | 1 | 1 | | |
| 292 | | 2 | 1 | | |
| 293 | | 3 | 1 | | |
| 294 | | 4 | 1 | | |
| 295 | | 5 | 1 | | |
| 296 | | 6 | 1 | | |
| 297 | | 7 | 1 | | |
| 298 | | 8 | 2 | | |
| 299 | | 9 | 1 | | |
| 300 | | 10 | 1 | | |
| 301 | | 11 | 1 | | |
| 302 | | 12 | 1 | | |
| 303 | | 13 | 1 | | |
| 304 | | 14 | 1 | | |
| 305 | | 15 | 1 | | |
| 306 | | 16 | 1 | | |
| 307 | | 17 | 1 | | |
| 308 | | 18 | 1 | | |
| 309 | | 19 | 1 | | |
| 310 | | 20 | 1 | | |
| 311 | | 21 | 1 | | |
| 312 | | 22 | 1 | | |
| 313 | | 23 | 2 | | |
| 314 | | 24 | 1 | | |
| 315 | ||
| 316 | ** Router | |
| 317 | :PROPERTIES: | |
| 318 | :CUSTOM_ID: router | |
| 319 | :END: | |
| 320 | On my router, the configuration was as easy as opening the firewall | |
| 321 | settings and unblocking the ports I needed for my services (e.g., | |
| 322 | HTTP/S, Plex, SSH, MySQL, etc.). | |
| 323 | ||
| 324 | +Since I'm relying on an ISP-provided modem/router combo for now (not by | |
| 325 | choice), I do not use any other advanced settings on my router that | |
| 326 | would inhibit any valid traffic to these services.+ | |
| 327 | ||
| 328 | The paragraph above regarding the ISP-owned router is no longer accurate | |
| 329 | as I now use the Ubiquiti Unifi Dream Machine Pro as my router. Within | |
| 330 | this router, I enabled port forwarding/firewall rules, segregate the | |
| 331 | network based on the device, and enable traffic restrictions (e.g., | |
| 332 | silently drop traffic from certain countries and threat categories). | |
| 333 | ||
| 334 | If you have the option with your ISP, I recommend using a personal | |
| 335 | router with software that you are familiar with so that you can explore | |
| 336 | all the options available to you. | |
| 337 | ||
| 338 | ** Physical Security | |
| 339 | :PROPERTIES: | |
| 340 | :CUSTOM_ID: physical-security | |
| 341 | :END: | |
| 342 | One large piece of self-hosting that people generally don't discuss | |
| 343 | online is physical security. However, physical security is very | |
| 344 | important for everyone who hosts a server like this. Exactly /how/ | |
| 345 | important it is depends on the server use/purpose. | |
| 346 | ||
| 347 | If you self-host customer applications that hold protected data (HIPAA, | |
| 348 | GDPR, COPPA, etc.), then physical security is extremely important and | |
| 349 | cannot be ignored. If you simply host a blog and some hobby sites, then | |
| 350 | it's a relatively minor consideration, but one you still need to think | |
| 351 | about. | |
| 352 | ||
| 353 | *** Location | |
| 354 | :PROPERTIES: | |
| 355 | :CUSTOM_ID: location | |
| 356 | :END: | |
| 357 | The first consideration is quite simple: location. - Is the server | |
| 358 | within a property you own or housed on someone else's property? - Is it | |
| 359 | nearby (in your house, in your work office, in your neighbor's garage, | |
| 360 | in a storage unit, etc.)? - Do you have 24/7 access to the server? - Are | |
| 361 | there climate considerations, such as humidity, fires, tornadoes, | |
| 362 | monsoons? - Do you have emergency equipment nearby in case of emergency? | |
| 363 | ||
| 364 | *** Hardware Ownership | |
| 365 | :PROPERTIES: | |
| 366 | :CUSTOM_ID: hardware-ownership | |
| 367 | :END: | |
| 368 | Secondly, consider the hardware itself: - Do you own the server in its | |
| 369 | entirety? - Are any other users able to access the server, even if your | |
| 370 | data/space is segregated? - If you're utilizing a third party, do they | |
| 371 | have any documentation to show responsibility? This could be a SOC 1/2/3 | |
| 372 | report, ISO compliance report, internal security/safety documentation. | |
| 373 | ||
| 374 | *** Physical Controls | |
| 375 | :PROPERTIES: | |
| 376 | :CUSTOM_ID: physical-controls | |
| 377 | :END: | |
| 378 | Regardless of who owns the hardware, ensure that there are adequate | |
| 379 | safeguards in place, if necessary. These usually don't apply to small | |
| 380 | home servers and are usually covered already if you're utilizing a third | |
| 381 | party. | |
| 382 | ||
| 383 | These can include: - Server bezel locks - Server room locks - physical, | |
| 384 | digital, or biometric authentication - Security cameras - Raised | |
| 385 | floors/lowered ceilings with proper guards/gates in-place within the | |
| 386 | floors or ceilings - Security personnel - Log sheets and/or guest badges | |
blog/2022-04-02-nginx-reverse-proxy.org deleted −247
| @@ -1,247 +0,0 @@ | ||
| 1 | #+title: Set-Up a Reverse Proxy with Nginx | |
| 2 | #+date: 2022-04-02 | |
| 3 | ||
| 4 | ** What is a Reverse Proxy? | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: what-is-a-reverse-proxy | |
| 7 | :END: | |
| 8 | A reverse proxy is a server that is placed between local servers or | |
| 9 | services and clients/users (e.g., the internet). The reverse proxy | |
| 10 | intercepts all requests from clients at the network edge and uses its | |
| 11 | configuration files to determine where each request should be sent. | |
| 12 | ||
| 13 | *** A Brief Example | |
| 14 | :PROPERTIES: | |
| 15 | :CUSTOM_ID: a-brief-example | |
| 16 | :END: | |
| 17 | For example, let's say that I run three servers in my home: | |
| 18 | ||
| 19 | - Server_01 (=example.com=) | |
| 20 | - Server_02 (=service01.example.com=) | |
| 21 | - Server_03 (=service02.example.com=) | |
| 22 | ||
| 23 | I also run a reverse proxy in my home that intercepts all public | |
| 24 | traffic: | |
| 25 | ||
| 26 | - Reverse Proxy | |
| 27 | ||
| 28 | Assume that I have a domain name (=example.com=) that allows clients to | |
| 29 | request websites or services from my home servers. | |
| 30 | ||
| 31 | In this case, the reverse proxy will intercept all traffic from | |
| 32 | =example.com= that enters my network and determine if the client is | |
| 33 | requesting valid data, based on my configuration. | |
| 34 | ||
| 35 | If the user is requesting =example.com= and my configuration files say | |
| 36 | that Server_01 holds that data, Nginx will send the user to Server_01. | |
| 37 | If I were to change the configuration so that =example.com= is routed to | |
| 38 | Server_02, that same user would be sent to Server_02 instead. | |
| 39 | ||
| 40 | #+begin_src txt | |
| 41 | ┌──────┐ ┌───────────┐ | |
| 42 | │ User │─┐ ┌──► Server_01 │ | |
| 43 | └──────┘ │ │ └───────────┘ | |
| 44 | │ ┌──────────┐ ┌───────────────┐ │ ┌───────────┐ | |
| 45 | ├────► Internet ├───► Reverse Proxy ├─────├──► Server_02 │ | |
| 46 | │ └──────────┘ └───────────────┘ │ └───────────┘ | |
| 47 | ┌──────┐ │ │ ┌───────────┐ | |
| 48 | │ User │─┘ └──► Server_03 │ | |
| 49 | └──────┘ └───────────┘ | |
| 50 | #+end_src | |
| 51 | ||
| 52 | ** Reverse Proxy Options | |
| 53 | :PROPERTIES: | |
| 54 | :CUSTOM_ID: reverse-proxy-options | |
| 55 | :END: | |
| 56 | There are a lot of options when it comes to reverse proxy servers, so | |
| 57 | I'm just going to list a few of the options I've heard recommended over | |
| 58 | the last few years: | |
| 59 | ||
| 60 | - [[https://nginx.com][Nginx]] | |
| 61 | - [[https://caddyserver.com][Caddy]] | |
| 62 | - [[https://traefik.io/][Traefik]] | |
| 63 | - [[https://www.haproxy.org/][HAProxy]] | |
| 64 | - [[https://ubuntu.com/server/docs/proxy-servers-squid][Squid]] | |
| 65 | ||
| 66 | In this post, we will be using Nginx as our reverse proxy, running on | |
| 67 | Ubuntu Server 20.04.4 LTS. | |
| 68 | ||
| 69 | ** Nginx Reverse Proxy Example | |
| 70 | :PROPERTIES: | |
| 71 | :CUSTOM_ID: nginx-reverse-proxy-example | |
| 72 | :END: | |
| 73 | *** Local Applications | |
| 74 | :PROPERTIES: | |
| 75 | :CUSTOM_ID: local-applications | |
| 76 | :END: | |
| 77 | You may be like me and have a lot of applications running on your local | |
| 78 | network that you'd like to expose publicly with a domain. | |
| 79 | ||
| 80 | In my case, I have services running in multiple Docker containers within | |
| 81 | a single server and want a way to visit those services from anywhere | |
| 82 | with a URL. For example, on my local network, | |
| 83 | [[https://dashy.to][Dashy]] runs through port 4000 (=localhost:4000=) | |
| 84 | and [[https://github.com/louislam/uptime-kuma][Uptime Kuma]] runs | |
| 85 | through port 3001 (=localhost:3001=). | |
| 86 | ||
| 87 | In order to expose these services to the public, I will need to do the | |
| 88 | following: | |
| 89 | ||
| 90 | 1. Set up DNS records for a domain or subdomain (one per service) to | |
| 91 | point toward the IP address of the server. | |
| 92 | 2. Open up the server network's HTTP and HTTPS ports (80 & 443) so that | |
| 93 | the reverse proxy can accept traffic and determine where to send it. | |
| 94 | 3. Install the reverse proxy software. | |
| 95 | 4. Configure the reverse proxy to recognize which service should get | |
| 96 | traffic from any of the domains or subdomains. | |
| 97 | ||
| 98 | *** Step 1: DNS Configuration | |
| 99 | :PROPERTIES: | |
| 100 | :CUSTOM_ID: step-1-dns-configuration | |
| 101 | :END: | |
| 102 | To start, update your DNS configuration so that you have an =A= record | |
| 103 | for each domain or subdomain. | |
| 104 | ||
| 105 | The =A= records should point toward the public IP address of the server. | |
| 106 | If you don't know the public IP address, log in to the server and run | |
| 107 | the following command: | |
| 108 | ||
| 109 | #+begin_src sh | |
| 110 | curl ifconfig.co | |
| 111 | #+end_src | |
| 112 | ||
| 113 | In the DNS example below, =xxx.xxx.xxx.xxx= is the public IP address of | |
| 114 | the server. | |
| 115 | ||
| 116 | #+begin_src config | |
| 117 | example.com A xxx.xxx.xxx.xxx | |
| 118 | uptime.example.com A xxx.xxx.xxx.xxx | |
| 119 | dashy.example.com A xxx.xxx.xxx.xxx | |
| 120 | www CNAME example.com | |
| 121 | #+end_src | |
| 122 | ||
| 123 | Finally, ensure the DNS has propagated correctly with | |
| 124 | [[https://dnschecker.org][DNS Checker]] by entering your domains or | |
| 125 | subdomains in the search box and ensuring the results are showing the | |
| 126 | correct IP address. | |
| 127 | ||
| 128 | *** Step 2: Open Network Ports | |
| 129 | :PROPERTIES: | |
| 130 | :CUSTOM_ID: step-2-open-network-ports | |
| 131 | :END: | |
| 132 | This step will be different depending on which router you have in your | |
| 133 | home. If you're not sure, try to visit | |
| 134 | [[http://192.168.1.1][192.168.1.1]] in your browser. Login credentials | |
| 135 | are usually written on a sticker somewhere on your modem/router. | |
| 136 | ||
| 137 | Once you're able to log in to your router, find the Port Forwarding | |
| 138 | settings. You will need to forward ports =80= and =443= to whichever | |
| 139 | machine is running the reverse proxy. | |
| 140 | ||
| 141 | In my case, the table below shows the port-forwarding rules I've | |
| 142 | created. In this table, =xxx.xxx.xxx.xxx= is the local device IP of the | |
| 143 | reverse proxy server, it will probably be an IP between =192.168.1.1= | |
| 144 | and =192.168.1.255=. | |
| 145 | ||
| 146 | | NAME | FROM | PORT | DEST PORT/IP | ENABLED | | |
| 147 | |-------+------+------+-----------------+---------| | |
| 148 | | HTTP | * | 80 | xxx.xxx.xxx.xxx | TRUE | | |
| 149 | | HTTPS | * | 443 | xxx.xxx.xxx.xxx | TRUE | | |
| 150 | ||
| 151 | Once configured, these rules will direct all web traffic to your reverse | |
| 152 | proxy. | |
| 153 | ||
| 154 | *** Step 3: Nginx Installation | |
| 155 | :PROPERTIES: | |
| 156 | :CUSTOM_ID: step-3-nginx-installation | |
| 157 | :END: | |
| 158 | To install Nginx, simply run the following command: | |
| 159 | ||
| 160 | #+begin_src sh | |
| 161 | sudo apt install nginx | |
| 162 | #+end_src | |
| 163 | ||
| 164 | If you have a firewall enabled, open up ports =80= and =443= on your | |
| 165 | server so that Nginx can accept web traffic from the router. | |
| 166 | ||
| 167 | For example, if you want to use =ufw= for web traffic and SSH, run the | |
| 168 | following commands: | |
| 169 | ||
| 170 | #+begin_src sh | |
| 171 | sudo ufw allow 'Nginx Full' | |
| 172 | sudo ufw allow SSH | |
| 173 | sudo ufw enable | |
| 174 | #+end_src | |
| 175 | ||
| 176 | *** Step 4: Nginx Configuration | |
| 177 | :PROPERTIES: | |
| 178 | :CUSTOM_ID: step-4-nginx-configuration | |
| 179 | :END: | |
| 180 | Now that we have domains pointing toward the server, the only step left | |
| 181 | is to configure the reverse proxy to direct traffic from domains to | |
| 182 | local services. | |
| 183 | ||
| 184 | To start, you'll need to create a configuration file for each domain in | |
| 185 | =/etc/nginx/sites-available/=. They will look identical except for the | |
| 186 | =server_name= variable and the =proxy_pass= port. | |
| 187 | ||
| 188 | Dashy: | |
| 189 | ||
| 190 | #+begin_src sh | |
| 191 | nano /etc/nginx/sites-available/dashy.example.com | |
| 192 | #+end_src | |
| 193 | ||
| 194 | #+begin_src config | |
| 195 | server { | |
| 196 | listen 80; | |
| 197 | server_name dashy.example.com; | |
| 198 | ||
| 199 | location / { | |
| 200 | proxy_pass http://localhost:4000; | |
| 201 | } | |
| 202 | } | |
| 203 | #+end_src | |
| 204 | ||
| 205 | Uptime: | |
| 206 | ||
| 207 | #+begin_src sh | |
| 208 | nano /etc/nginx/sites-available/uptime.example.com | |
| 209 | #+end_src | |
| 210 | ||
| 211 | #+begin_src config | |
| 212 | server { | |
| 213 | listen 80; | |
| 214 | server_name uptime.example.com; | |
| 215 | ||
| 216 | location / { | |
| 217 | proxy_pass http://localhost:3001; | |
| 218 | } | |
| 219 | } | |
| 220 | #+end_src | |
| 221 | ||
| 222 | Once the configuration files are created, you will need to enable them | |
| 223 | with the =symlink= command: | |
| 224 | ||
| 225 | #+begin_src sh | |
| 226 | sudo ln -s /etc/nginx/sites-available/dashy.example.com /etc/nginx/sites-enabled/ | |
| 227 | #+end_src | |
| 228 | ||
| 229 | Voilà! Your local services should now be available through their URLs. | |
| 230 | ||
| 231 | ** HTTPS with Certbot | |
| 232 | :PROPERTIES: | |
| 233 | :CUSTOM_ID: https-with-certbot | |
| 234 | :END: | |
| 235 | If you've followed along, you'll notice that your services are only | |
| 236 | available via HTTP (not HTTPS). | |
| 237 | ||
| 238 | If you want to enable HTTPS for your new domains, you will need to | |
| 239 | generate SSL/TLS certificates for them. The easiest way to generate | |
| 240 | certificates on Nginx is [[https://certbot.eff.org][Certbot]]: | |
| 241 | ||
| 242 | #+begin_src sh | |
| 243 | sudo apt install snapd; sudo snap install core; sudo snap refresh core | |
| 244 | sudo snap install --classic certbot | |
| 245 | sudo ln -s /snap/bin/certbot /usr/bin/certbot | |
| 246 | sudo certbot --nginx | |
| 247 | #+end_src | |
blog/2022-06-24-fedora-i3.org deleted −170
| @@ -1,170 +0,0 @@ | ||
| 1 | #+title: Rebooting My Love Affair with Linux | |
| 2 | #+date: 2022-06-24 | |
| 3 | ||
| 4 | ** Leaving macOS | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: leaving-macos | |
| 7 | :END: | |
| 8 | As I noted [[/blog/foss-macos-apps][in a recent post]], I have been | |
| 9 | planning on migrating from macOS back to a Linux-based OS. I am happy to | |
| 10 | say that I have finally completed my migration and am now stuck in the | |
| 11 | wonderful world of Linux again. | |
| 12 | ||
| 13 | My decision to leave macOS really came down to just a few important | |
| 14 | things: | |
| 15 | ||
| 16 | - Apple Security (Gatekeeper) restricting me from running any software I | |
| 17 | want. Even if you disable Gatekeeper and allow software to bypass the | |
| 18 | rest of the device installation security, you still have to repeat | |
| 19 | that process every time the allowed software is updated. | |
| 20 | - macOS sends out nearly constant connections, pings, telemetry, etc. to | |
| 21 | a myriad of mysterious Apple services. I'm not even going to dive into | |
| 22 | how many macOS apps have constant telemetry on, as well. | |
| 23 | - Lastly, I just /really/ missed the customization and freedom that | |
| 24 | comes with Linux. Being able to switch to entirely new kernel, OS, or | |
| 25 | desktop within minutes is a freedom I took for granted when I switched | |
| 26 | to macOS. | |
| 27 | ||
| 28 | Now that I've covered macOS, I'm going to move on to more exciting | |
| 29 | topics: my personal choice of OS, DE, and various customizations I'm | |
| 30 | using. | |
| 31 | ||
| 32 | ** Fedora | |
| 33 | :PROPERTIES: | |
| 34 | :CUSTOM_ID: fedora | |
| 35 | :END: | |
| 36 | After trying a ton of distros (I think I booted and tested around 20-25 | |
| 37 | distros), I finally landed on [[https://getfedora.org/][Fedora Linux]]. | |
| 38 | I have quite a bit of experience with Fedora and enjoy the =dnf= package | |
| 39 | manager. Fedora allows me to keep up-to-date with recent software (I'm | |
| 40 | looking at you, Debian), but still provides a level of stability you | |
| 41 | don't find in every distro. | |
| 42 | ||
| 43 | In a very close second place was Arch Linux, as well as its spin-off: | |
| 44 | Garuda Linux (Garuda w/ sway is /beautiful/). Arch is great for | |
| 45 | compatibility and the massive community it has, but I have just never | |
| 46 | had the time to properly sit down and learn the methodology behind their | |
| 47 | packaging systems. | |
| 48 | ||
| 49 | Basically, everything else I tested was unacceptable in at least one way | |
| 50 | or another. Void (=glibc=) was great, but doesn't support all the | |
| 51 | software I need. Slackware worked well as a tui, but I wasn't skilled | |
| 52 | enough to get a tiling window manager (WM) working on it. | |
| 53 | ||
| 54 | *** i3 | |
| 55 | :PROPERTIES: | |
| 56 | :CUSTOM_ID: i3 | |
| 57 | :END: | |
| 58 | One of the reasons I settled on Fedora is that it comes with an official | |
| 59 | i3 spin. Being able to use a tiling WM, such as i3 or sway, is one of | |
| 60 | the biggest things I wanted to do as soon as I adopted Linux again. | |
| 61 | ||
| 62 | I will probably set up a dotfile repository soon, so that I don't lose | |
| 63 | any of my configurations, but nothing big has been configured thus far. | |
| 64 | ||
| 65 | The two main things I have updated in i3wm are natural scrolling and | |
| 66 | binding my brightness keys to the =brightnessctl= program. | |
| 67 | ||
| 68 | **** Natural Scrolling | |
| 69 | :PROPERTIES: | |
| 70 | :CUSTOM_ID: natural-scrolling | |
| 71 | :END: | |
| 72 | You can enable natural scrolling by opening the following file: | |
| 73 | ||
| 74 | #+begin_src sh | |
| 75 | sudo nano /usr/share/X11/xorg.conf.d/40-libinput.conf | |
| 76 | #+end_src | |
| 77 | ||
| 78 | Within the =40-libinput.conf= file, find the following input sections | |
| 79 | and enable the natural scrolling option. | |
| 80 | ||
| 81 | This is the =pointer= section: | |
| 82 | ||
| 83 | #+begin_src conf | |
| 84 | Section "InputClass" | |
| 85 | Identifier "libinput pointer catchall" | |
| 86 | MatchIsPointer "on" | |
| 87 | MatchDevicePath "/dev/input/event*" | |
| 88 | Driver "libinput" | |
| 89 | Option "NaturalScrolling" "True" | |
| 90 | EndSection | |
| 91 | #+end_src | |
| 92 | ||
| 93 | This is the =touchpad= section: | |
| 94 | ||
| 95 | #+begin_src conf | |
| 96 | Section "InputClass" | |
| 97 | Identifier "libinput touchpad catchall" | |
| 98 | MatchIsTouchpad "on" | |
| 99 | MatchDevicePath "/dev/input/event*" | |
| 100 | Driver "libinput" | |
| 101 | Option "NaturalScrolling" "True" | |
| 102 | EndSection | |
| 103 | #+end_src | |
| 104 | ||
| 105 | **** Enabling Brightness Keys | |
| 106 | :PROPERTIES: | |
| 107 | :CUSTOM_ID: enabling-brightness-keys | |
| 108 | :END: | |
| 109 | Likewise, enabling brightness key functionality is as simple as binding | |
| 110 | the keys to the =brightnessctl= program. | |
| 111 | ||
| 112 | To do this, open up your i3 config file. Mine is located here: | |
| 113 | ||
| 114 | #+begin_src sh | |
| 115 | nano /home/<my-user>/.config/i3/config | |
| 116 | #+end_src | |
| 117 | ||
| 118 | #+begin_src conf | |
| 119 | # Use brightnessctl to adjust brightness. | |
| 120 | bindsym XF86MonBrightnessDown exec --no-startup-id brightnessctl --min-val=2 -q set 3%- | |
| 121 | bindsym XF86MonBrightnessUp exec --no-startup-id brightnessctl -q set 3%+ | |
| 122 | #+end_src | |
| 123 | ||
| 124 | **** =polybar= | |
| 125 | :PROPERTIES: | |
| 126 | :CUSTOM_ID: polybar | |
| 127 | :END: | |
| 128 | Instead of using the default =i3status= bar, I have opted to use | |
| 129 | =polybar= instead (as you can also see in the screenshot above). | |
| 130 | ||
| 131 | My config for this menu bar is basically just the default settings with | |
| 132 | modified colors and an added battery block to quickly show me the | |
| 133 | machine's battery info. | |
| 134 | ||
| 135 | **** =alacritty= | |
| 136 | :PROPERTIES: | |
| 137 | :CUSTOM_ID: alacritty | |
| 138 | :END: | |
| 139 | Not much to say on this part yet, as I haven't configured it much, but I | |
| 140 | installed =alacritty= as my default terminal, and I am using =zsh= and | |
| 141 | the shell. | |
| 142 | ||
| 143 | ** Software Choices | |
| 144 | :PROPERTIES: | |
| 145 | :CUSTOM_ID: software-choices | |
| 146 | :END: | |
| 147 | Again, I'm not going to say much that I haven't said yet in other blog | |
| 148 | posts, so I'll just do a quick rundown of the apps I installed | |
| 149 | immediately after I set up the environment. | |
| 150 | ||
| 151 | Flatpak Apps: | |
| 152 | ||
| 153 | - Cryptomator | |
| 154 | - pCloud | |
| 155 | - Signal | |
| 156 | ||
| 157 | Fedora Packages: | |
| 158 | ||
| 159 | - gomuks | |
| 160 | - neomutt | |
| 161 | - neofetch | |
| 162 | - Firefox | |
| 163 | - uBlock Origin | |
| 164 | - Bitwarden | |
| 165 | - Stylus | |
| 166 | - Privacy Redirect | |
| 167 | ||
| 168 | Other: | |
| 169 | ||
| 170 | - exiftool | |
blog/2022-07-01-git-server.org deleted −678
| @@ -1,678 +0,0 @@ | ||
| 1 | #+title: Self-Hosting a Personal Git Server | |
| 2 | #+date: 2022-07-01 | |
| 3 | ||
| 4 | ** My Approach to Self-Hosting Git | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: my-approach-to-self-hosting-git | |
| 7 | :END: | |
| 8 | I have often tried to self-host my Git repositories, but have always | |
| 9 | fallen short when I tried to find a suitable web interface to show on | |
| 10 | the front-end. | |
| 11 | ||
| 12 | After a few years, I have finally found a combination of methods that | |
| 13 | allow me to easily self-host my projects, view them on the web, and | |
| 14 | access them from anywhere. | |
| 15 | ||
| 16 | Before I dive into the details, I want to state a high-level summary of | |
| 17 | my self-hosted Git approach: | |
| 18 | ||
| 19 | - This method uses the =ssh://= (read & write) and =git://= (read-only) | |
| 20 | protocols for push and pull access. | |
| 21 | - For the =git://= protocol, I create a =git-daemon-export-ok= file in | |
| 22 | any repository that I want to be cloneable by anyone. | |
| 23 | - The web interface I am using (=cgit=) allows simple HTTP cloning by | |
| 24 | default. I do not disable this setting as I want beginners to be | |
| 25 | able to clone one of my repositories even if they don't know the | |
| 26 | proper method. | |
| 27 | - I am not enabling Smart HTTPS for any repositories. Updates to | |
| 28 | repositories must be pushed via SSH. | |
| 29 | - Beyond the actual repository management, I am using =cgit= for the | |
| 30 | front-end web interface. | |
| 31 | - If you use the =scan-path=<path>= configuration in the =cgitrc= | |
| 32 | configuration file to automatically find repositories, you can't | |
| 33 | exclude a repository from =cgit= if it's stored within the path that | |
| 34 | =cgit= reads. To host private repositories, you'd need to set up | |
| 35 | another directory that =cgit= can't read. | |
| 36 | ||
| 37 | ** Assumptions | |
| 38 | :PROPERTIES: | |
| 39 | :CUSTOM_ID: assumptions | |
| 40 | :END: | |
| 41 | For the purposes of this walkthrough, I am assuming you have a URL | |
| 42 | (=git.example.com=) or IP address (=207.84.26.991=) addressed to the | |
| 43 | server that you will be using to host your git repositories. | |
| 44 | ||
| 45 | ** Adding a Git User | |
| 46 | :PROPERTIES: | |
| 47 | :CUSTOM_ID: adding-a-git-user | |
| 48 | :END: | |
| 49 | In order to use the SSH method associated with git, we will need to add | |
| 50 | a user named =git=. If you have used the SSH method for other git | |
| 51 | hosting sites, you are probably used to the following syntax: | |
| 52 | ||
| 53 | #+begin_src sh | |
| 54 | git clone [user@]server:project.git | |
| 55 | #+end_src | |
| 56 | ||
| 57 | The syntax above is an =scp=-like syntax for using SSH on the =git= user | |
| 58 | on the server to access your repository. | |
| 59 | ||
| 60 | Let's delete any remnants of an old =git= user, if any, and create the | |
| 61 | new user account: | |
| 62 | ||
| 63 | #+begin_src sh | |
| 64 | sudo deluser --remove-home git | |
| 65 | sudo adduser git | |
| 66 | #+end_src | |
| 67 | ||
| 68 | *** Import Your SSH Keys to the Git User | |
| 69 | :PROPERTIES: | |
| 70 | :CUSTOM_ID: import-your-ssh-keys-to-the-git-user | |
| 71 | :END: | |
| 72 | Once the =git= user is created, you will need to copy your public SSH | |
| 73 | key on your local development machine to the =git= user on the server. | |
| 74 | ||
| 75 | If you don't have an SSH key yet, create one with this command: | |
| 76 | ||
| 77 | #+begin_src sh | |
| 78 | ssh-keygen | |
| 79 | #+end_src | |
| 80 | ||
| 81 | Once you create the key pair, the public should be saved to | |
| 82 | =~/.ssh/id_rsa.pub=. | |
| 83 | ||
| 84 | If your server still has password-based authentication available, you | |
| 85 | can copy it over to your user's home directory like this: | |
| 86 | ||
| 87 | #+begin_src sh | |
| 88 | ssh-copy-id git@server | |
| 89 | #+end_src | |
| 90 | ||
| 91 | Otherwise, copy it over to any user that you can access. | |
| 92 | ||
| 93 | #+begin_src sh | |
| 94 | scp ~/.ssh/id_rsa.pub your_user@your_server: | |
| 95 | #+end_src | |
| 96 | ||
| 97 | Once on the server, you will need to copy the contents into the =git= | |
| 98 | user's =authorized_keys= file: | |
| 99 | ||
| 100 | #+begin_src sh | |
| 101 | cat id_rsa.pub > /home/git/.ssh/authorized_keys | |
| 102 | #+end_src | |
| 103 | ||
| 104 | *** (Optional) Disable Password-Based SSH | |
| 105 | :PROPERTIES: | |
| 106 | :CUSTOM_ID: optional-disable-password-based-ssh | |
| 107 | :END: | |
| 108 | If you want to lock down your server and ensure that no one can | |
| 109 | authenticate in via SSH with a password, you will need to edit your SSH | |
| 110 | configuration. | |
| 111 | ||
| 112 | #+begin_src sh | |
| 113 | sudo nano /etc/ssh/sshd_config | |
| 114 | #+end_src | |
| 115 | ||
| 116 | Within this file, find the following settings and set them to the values | |
| 117 | I am showing below: | |
| 118 | ||
| 119 | #+begin_src conf | |
| 120 | PermitRootLogin no | |
| 121 | PasswordAuthentication no | |
| 122 | AuthenticationMethods publickey | |
| 123 | #+end_src | |
| 124 | ||
| 125 | You may have other Authentication Methods required in your personal | |
| 126 | set-up, so the key here is just to ensure that =AuthenticationMethods= | |
| 127 | does not allow passwords. | |
| 128 | ||
| 129 | ** Setting up the Base Directory | |
| 130 | :PROPERTIES: | |
| 131 | :CUSTOM_ID: setting-up-the-base-directory | |
| 132 | :END: | |
| 133 | Now that we have set up a =git= user to handle all transport methods, we | |
| 134 | need to set up the directory that we will be using as our base of all | |
| 135 | repositories. | |
| 136 | ||
| 137 | In my case, I am using =/git= as my source folder. To create this folder | |
| 138 | and assign it to the user we created, execute the following commands: | |
| 139 | ||
| 140 | #+begin_src sh | |
| 141 | sudo mkdir /git | |
| 142 | sudo chown -R git:git /git | |
| 143 | #+end_src | |
| 144 | ||
| 145 | ** Creating a Test Repository | |
| 146 | :PROPERTIES: | |
| 147 | :CUSTOM_ID: creating-a-test-repository | |
| 148 | :END: | |
| 149 | On your server, switch over to the =git= user in order to start managing | |
| 150 | git files. | |
| 151 | ||
| 152 | #+begin_src sh | |
| 153 | su git | |
| 154 | #+end_src | |
| 155 | ||
| 156 | Once logged-in as the =git= user, go to your base directory and create a | |
| 157 | test repository. | |
| 158 | ||
| 159 | #+begin_src sh | |
| 160 | cd /git | |
| 161 | mkdir test.git && cd test.git | |
| 162 | git init --bare | |
| 163 | #+end_src | |
| 164 | ||
| 165 | If you want to make this repo viewable/cloneable to the public via the | |
| 166 | =git://= protocol, you need to create a =git-daemon-export-ok= file | |
| 167 | inside the repository. | |
| 168 | ||
| 169 | #+begin_src sh | |
| 170 | touch git-daemon-export-ok | |
| 171 | #+end_src | |
| 172 | ||
| 173 | ** Change the Login Shell for =git= | |
| 174 | :PROPERTIES: | |
| 175 | :CUSTOM_ID: change-the-login-shell-for-git | |
| 176 | :END: | |
| 177 | To make sure that the =git= user is only used for git operations and | |
| 178 | nothing else, you need to change the user's login shell. To do this, | |
| 179 | simply use the =chsh= command: | |
| 180 | ||
| 181 | #+begin_src sh | |
| 182 | sudo chsh git | |
| 183 | #+end_src | |
| 184 | ||
| 185 | The interactive prompt will ask which shell you want the =git= user to | |
| 186 | use. You must use the following value: | |
| 187 | ||
| 188 | #+begin_src sh | |
| 189 | /usr/bin/git-shell | |
| 190 | #+end_src | |
| 191 | ||
| 192 | Once done, no one will be able to SSH to the =git= user or execute | |
| 193 | commands other than the standard git commands. | |
| 194 | ||
| 195 | ** Opening the Firewall | |
| 196 | :PROPERTIES: | |
| 197 | :CUSTOM_ID: opening-the-firewall | |
| 198 | :END: | |
| 199 | Don't forget to open up ports on the device firewall and network | |
| 200 | firewall if you want to access these repositories publicly. If you're | |
| 201 | using default ports, forward ports =22= (ssh) and =9418= (git) from your | |
| 202 | router to your server's IP address. | |
| 203 | ||
| 204 | If your server also has a firewall, ensure that the firewall allows the | |
| 205 | same ports that are forwarded from the router. For example, if you use | |
| 206 | =ufw=: | |
| 207 | ||
| 208 | #+begin_src sh | |
| 209 | sudo ufw allow 22 | |
| 210 | sudo ufw allow 9418 | |
| 211 | #+end_src | |
| 212 | ||
| 213 | *** Non-Standard SSH Ports | |
| 214 | :PROPERTIES: | |
| 215 | :CUSTOM_ID: non-standard-ssh-ports | |
| 216 | :END: | |
| 217 | If you use a non-standard port for SSH, such as =9876=, you will need to | |
| 218 | create an SSH configuration file on your local development machine in | |
| 219 | order to connect to your server's git repositories. | |
| 220 | ||
| 221 | To do this, you'll need to define your custom port on your client | |
| 222 | machine in your =~/.ssh/config= file: | |
| 223 | ||
| 224 | #+begin_src sh | |
| 225 | nano ~/.ssh/config | |
| 226 | #+end_src | |
| 227 | ||
| 228 | #+begin_src conf | |
| 229 | Host git.example.com | |
| 230 | # HostName can be a URL or an IP address | |
| 231 | HostName git.example.com | |
| 232 | Port 9876 | |
| 233 | User git | |
| 234 | #+end_src | |
| 235 | ||
| 236 | *** Testing SSH | |
| 237 | :PROPERTIES: | |
| 238 | :CUSTOM_ID: testing-ssh | |
| 239 | :END: | |
| 240 | There are two main syntaxes you can use to manage git over SSH: | |
| 241 | ||
| 242 | - =git clone [user@]server:project.git= | |
| 243 | - =git clone ssh://[user@]server/project.git= | |
| 244 | ||
| 245 | I prefer the first, which is an =scp=-like syntax. To test it, try to | |
| 246 | clone the test repository you set up on the server: | |
| 247 | ||
| 248 | #+begin_src sh | |
| 249 | git clone git@git.example.com:/git/test.git | |
| 250 | #+end_src | |
| 251 | ||
| 252 | ** Enabling Read-Only Access | |
| 253 | :PROPERTIES: | |
| 254 | :CUSTOM_ID: enabling-read-only-access | |
| 255 | :END: | |
| 256 | If you want people to be able to clone any repository where you've | |
| 257 | placed a =git-daemon-export-ok= file, you will need to start the git | |
| 258 | daemon. | |
| 259 | ||
| 260 | To do this on a system with =systemd=, create a service file: | |
| 261 | ||
| 262 | #+begin_src sh | |
| 263 | sudo nano /etc/systemd/system/git-daemon.service | |
| 264 | #+end_src | |
| 265 | ||
| 266 | Inside the =git-daemon.service= file, paste the following: | |
| 267 | ||
| 268 | #+begin_src conf | |
| 269 | [Unit] | |
| 270 | Description=Start Git Daemon | |
| 271 | ||
| 272 | [Service] | |
| 273 | ExecStart=/usr/bin/git daemon --reuseaddr --base-path=/git/ /git/ | |
| 274 | ||
| 275 | Restart=always | |
| 276 | RestartSec=500ms | |
| 277 | ||
| 278 | StandardOutput=syslog | |
| 279 | StandardError=syslog | |
| 280 | SyslogIdentifier=git-daemon | |
| 281 | ||
| 282 | User=git | |
| 283 | Group=git | |
| 284 | ||
| 285 | [Install] | |
| 286 | WantedBy=multi-user.target | |
| 287 | #+end_src | |
| 288 | ||
| 289 | Once created, enable and start the service: | |
| 290 | ||
| 291 | #+begin_src sh | |
| 292 | sudo systemctl enable git-daemon.service | |
| 293 | sudo systemctl start git-daemon.service | |
| 294 | #+end_src | |
| 295 | ||
| 296 | To clone read-only via the =git://= protocol, you can use the following | |
| 297 | syntax: | |
| 298 | ||
| 299 | #+begin_src sh | |
| 300 | git clone git://git.example.com/test.git | |
| 301 | #+end_src | |
| 302 | ||
| 303 | ** Migrating Repositories | |
| 304 | :PROPERTIES: | |
| 305 | :CUSTOM_ID: migrating-repositories | |
| 306 | :END: | |
| 307 | At this point, we have a working git server that works with both SSH and | |
| 308 | read-only access. | |
| 309 | ||
| 310 | For each of the repositories I had hosted a different provider, I | |
| 311 | executed the following commands in order to place a copy on my server as | |
| 312 | my new source of truth: | |
| 313 | ||
| 314 | Server: | |
| 315 | ||
| 316 | #+begin_src sh | |
| 317 | su git | |
| 318 | mkdir /git/<REPOSITORY_NAME>.git && cd /git/<REPOSITORY_NAME>.git | |
| 319 | git init --bare | |
| 320 | ||
| 321 | # If you want to make this repo viewable/cloneable to the public | |
| 322 | touch git-daemon-export-ok | |
| 323 | #+end_src | |
| 324 | ||
| 325 | Client: | |
| 326 | ||
| 327 | #+begin_src sh | |
| 328 | git clone git@<PREVIOUS_HOST>:<REPOSITORY_NAME> | |
| 329 | git remote set-url origin git@git.EXAMPLE.COM:/git/<REPOSITORY_NAME>.git | |
| 330 | git push | |
| 331 | #+end_src | |
| 332 | ||
| 333 | ** Optional Web View: =cgit= | |
| 334 | :PROPERTIES: | |
| 335 | :CUSTOM_ID: optional-web-view-cgit | |
| 336 | :END: | |
| 337 | If you want a web viewer for your repositories, you can use various | |
| 338 | tools, such as =gitweb=, =cgit=, or =klaus=. I chose =cgit= due to its | |
| 339 | simple interface and fairly easy set-up (compared to others). Not to | |
| 340 | mention that the [[https://git.kernel.org/][Linux kernel uses =cgit=]]. | |
| 341 | ||
| 342 | *** Docker Compose | |
| 343 | :PROPERTIES: | |
| 344 | :CUSTOM_ID: docker-compose | |
| 345 | :END: | |
| 346 | Instead of using my previous method of using a =docker run= command, | |
| 347 | I've updated this section to use =docker-compose= instead for an easier | |
| 348 | installation and simpler management and configuration. | |
| 349 | ||
| 350 | In order to use Docker Compose, you will set up a =docker-compose.yml= | |
| 351 | file to automatically connect resources like the repositories, =cgitrc=, | |
| 352 | and various files or folders to the =cgit= container you're creating: | |
| 353 | ||
| 354 | #+begin_src sh | |
| 355 | mkdir ~/cgit && cd ~/cgit | |
| 356 | nano docker-compose.yml | |
| 357 | #+end_src | |
| 358 | ||
| 359 | #+begin_src conf | |
| 360 | # docker-compose.yml | |
| 361 | version: '3' | |
| 362 | ||
| 363 | services: | |
| 364 | cgit: | |
| 365 | image: invokr/cgit | |
| 366 | volumes: | |
| 367 | - /git:/git | |
| 368 | - ./cgitrc:/etc/cgitrc | |
| 369 | - ./logo.png:/var/www/htdocs/cgit/logo.png | |
| 370 | - ./favicon.png:/var/www/htdocs/cgit/favicon.png | |
| 371 | - ./filters:/var/www/htdocs/cgit/filters | |
| 372 | ports: | |
| 373 | - "8763:80" | |
| 374 | restart: always | |
| 375 | #+end_src | |
| 376 | ||
| 377 | Then, just start the container: | |
| 378 | ||
| 379 | #+begin_src sh | |
| 380 | sudo docker-compose up -d | |
| 381 | #+end_src | |
| 382 | ||
| 383 | Once it's finished installing, you can access the site at | |
| 384 | =<SERVER_IP>:8763= or use a reverse-proxy service to forward =cgit= to a | |
| 385 | URL, such as =git.example.com=. See the next section for more details on | |
| 386 | reverse proxying a URL to a local port. | |
| 387 | ||
| 388 | *** Nginx Reverse Proxy | |
| 389 | :PROPERTIES: | |
| 390 | :CUSTOM_ID: nginx-reverse-proxy | |
| 391 | :END: | |
| 392 | I am using Nginx as my reverse proxy so that the =cgit= Docker container | |
| 393 | can use =git.example.com= as its URL. To do so, I simply created the | |
| 394 | following configuration file: | |
| 395 | ||
| 396 | #+begin_src sh | |
| 397 | sudo nano /etc/nginx/sites-available/git.example.com | |
| 398 | #+end_src | |
| 399 | ||
| 400 | #+begin_src conf | |
| 401 | server { | |
| 402 | listen 80; | |
| 403 | server_name git.example.com; | |
| 404 | ||
| 405 | if ($host = git.example.com) { | |
| 406 | return 301 https://$host$request_uri; | |
| 407 | } | |
| 408 | ||
| 409 | return 404; | |
| 410 | } | |
| 411 | ||
| 412 | server { | |
| 413 | server_name git.example.com; | |
| 414 | listen 443 ssl http2; | |
| 415 | ||
| 416 | location / { | |
| 417 | # The final `/` is important. | |
| 418 | proxy_pass http://localhost:8763/; | |
| 419 | add_header X-Frame-Options SAMEORIGIN; | |
| 420 | add_header X-XSS-Protection "1; mode=block"; | |
| 421 | proxy_redirect off; | |
| 422 | proxy_buffering off; | |
| 423 | proxy_set_header Host $host; | |
| 424 | proxy_set_header X-Real-IP $remote_addr; | |
| 425 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |
| 426 | proxy_set_header X-Forwarded-Proto $scheme; | |
| 427 | proxy_set_header X-Forwarded-Port $server_port; | |
| 428 | } | |
| 429 | ||
| 430 | # INCLUDE ANY SSL CERTS HERE | |
| 431 | include /etc/letsencrypt/options-ssl-nginx.conf; | |
| 432 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; | |
| 433 | } | |
| 434 | #+end_src | |
| 435 | ||
| 436 | Once created, symlink it and restart the web server. | |
| 437 | ||
| 438 | #+begin_src sh | |
| 439 | sudo ln -s /etc/nginx/sites-available/git.example.com /etc/nginx/sites-enabled/ | |
| 440 | sudo systemctl restart nginx.service | |
| 441 | #+end_src | |
| 442 | ||
| 443 | As we can see below, my site at =git.example.com= is available and | |
| 444 | running: | |
| 445 | ||
| 446 | *** Settings Up Git Details | |
| 447 | :PROPERTIES: | |
| 448 | :CUSTOM_ID: settings-up-git-details | |
| 449 | :END: | |
| 450 | Once you have =cgit= running, you can add some small details, such as | |
| 451 | repository owners and descriptions by editing the following files within | |
| 452 | each repository. | |
| 453 | ||
| 454 | Alternatively, you can use the =cgitrc= file to edit these details if | |
| 455 | you only care to edit them for the purpose of seeing them on your | |
| 456 | website. | |
| 457 | ||
| 458 | The =description= file within the repository on your server will display | |
| 459 | the description online. | |
| 460 | ||
| 461 | #+begin_src sh | |
| 462 | cd /git/example.git | |
| 463 | nano description | |
| 464 | #+end_src | |
| 465 | ||
| 466 | You can add a =[gitweb]= block to the =config= file in order to display | |
| 467 | the owner of the repository. | |
| 468 | ||
| 469 | #+begin_src sh | |
| 470 | cd /git/example.git | |
| 471 | nano config | |
| 472 | #+end_src | |
| 473 | ||
| 474 | #+begin_src conf | |
| 475 | [gitweb] | |
| 476 | owner = "YourName" | |
| 477 | #+end_src | |
| 478 | ||
| 479 | Note that you can ignore the configuration within each repository and | |
| 480 | simply set up this information in the =cgitrc= file, if you want to do | |
| 481 | it that way. | |
| 482 | ||
| 483 | *** Editing =cgit= | |
| 484 | :PROPERTIES: | |
| 485 | :CUSTOM_ID: editing-cgit | |
| 486 | :END: | |
| 487 | In order to edit certain items within =cgit=, you need to edit the | |
| 488 | =cgitrc= file. | |
| 489 | ||
| 490 | #+begin_src sh | |
| 491 | nano ~/cgit/cgitrc | |
| 492 | #+end_src | |
| 493 | ||
| 494 | Below is an example configuration for =cgitrc=. You can find all the | |
| 495 | configuration options within the [configuration manual] | |
| 496 | (https://git.zx2c4.com/cgit/plain/cgitrc.5.txt). | |
| 497 | ||
| 498 | #+begin_src conf | |
| 499 | css=/cgit.css | |
| 500 | logo=/logo.png | |
| 501 | favicon=/favicon.png | |
| 502 | robots=noindex, nofollow | |
| 503 | ||
| 504 | enable-index-links=1 | |
| 505 | enable-commit-graph=1 | |
| 506 | enable-blame=1 | |
| 507 | enable-log-filecount=1 | |
| 508 | enable-log-linecount=1 | |
| 509 | enable-git-config=1 | |
| 510 | ||
| 511 | clone-url=git://git.example.com/$CGIT_REPO_URL ssh://git@git.example.com:/git/$CGIT_REPO_URL | |
| 512 | ||
| 513 | root-title=My Git Website | |
| 514 | root-desc=My personal git repositories. | |
| 515 | ||
| 516 | # Allow download of tar.gz, tar.bz2 and zip-files | |
| 517 | snapshots=tar.gz tar.bz2 zip | |
| 518 | ||
| 519 | ## | |
| 520 | ## List of common mimetypes | |
| 521 | ## | |
| 522 | mimetype.gif=image/gif | |
| 523 | mimetype.html=text/html | |
| 524 | mimetype.jpg=image/jpeg | |
| 525 | mimetype.jpeg=image/jpeg | |
| 526 | mimetype.pdf=application/pdf | |
| 527 | mimetype.png=image/png | |
| 528 | mimetype.svg=image/svg+xml | |
| 529 | ||
| 530 | # Highlight source code | |
| 531 | # source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.sh | |
| 532 | source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.py | |
| 533 | ||
| 534 | # Format markdown, restructuredtext, manpages, text files, and html files | |
| 535 | # through the right converters | |
| 536 | about-filter=/var/www/htdocs/cgit/filters/about-formatting.sh | |
| 537 | ||
| 538 | ## | |
| 539 | ## Search for these files in the root of the default branch of repositories | |
| 540 | ## for coming up with the about page: | |
| 541 | ## | |
| 542 | readme=:README.md | |
| 543 | readme=:readme.md | |
| 544 | readme=:README.mkd | |
| 545 | readme=:readme.mkd | |
| 546 | readme=:README.rst | |
| 547 | readme=:readme.rst | |
| 548 | readme=:README.html | |
| 549 | readme=:readme.html | |
| 550 | readme=:README.htm | |
| 551 | readme=:readme.htm | |
| 552 | readme=:README.txt | |
| 553 | readme=:readme.txt | |
| 554 | readme=:README | |
| 555 | readme=:readme | |
| 556 | ||
| 557 | # Repositories | |
| 558 | ||
| 559 | # Uncomment the following line to scan a path instead of adding repositories manually | |
| 560 | # scan-path=/git | |
| 561 | ||
| 562 | ## Test Section | |
| 563 | section=git/test-section | |
| 564 | ||
| 565 | repo.url=test.git | |
| 566 | repo.path=/git/test.git | |
| 567 | repo.readme=:README.md | |
| 568 | repo.owner=John Doe | |
| 569 | repo.desc=An example repository! | |
| 570 | #+end_src | |
| 571 | ||
| 572 | *** Final Fixes: Syntax Highlighting & README Rendering | |
| 573 | :PROPERTIES: | |
| 574 | :CUSTOM_ID: final-fixes-syntax-highlighting-readme-rendering | |
| 575 | :END: | |
| 576 | After completing my initial install and playing around with it for a few | |
| 577 | days, I noticed two issues: | |
| 578 | ||
| 579 | 1. Syntax highlighting did not work when viewing the source code within | |
| 580 | a file. | |
| 581 | 2. The =about= tab within a repository was not rendered to HTML. | |
| 582 | ||
| 583 | The following process fixes these issues. To start, let's go to the | |
| 584 | =cgit= directory where we were editing our configuration file earlier. | |
| 585 | ||
| 586 | #+begin_src sh | |
| 587 | cd ~/cgit | |
| 588 | #+end_src | |
| 589 | ||
| 590 | In here, create two folders that will hold our syntax files: | |
| 591 | ||
| 592 | #+begin_src sh | |
| 593 | mkdir filters && mkdir filters/html-converters && cd filters | |
| 594 | #+end_src | |
| 595 | ||
| 596 | Next, download the default filters: | |
| 597 | ||
| 598 | #+begin_src sh | |
| 599 | curl https://git.zx2c4.com/cgit/plain/filters/about-formatting.sh > about-formatting.sh | |
| 600 | chmod 755 about-formatting.sh | |
| 601 | curl https://git.zx2c4.com/cgit/plain/filters/syntax-highlighting.py > syntax-highlighting.py | |
| 602 | chmod 755 syntax-highlighting.py | |
| 603 | #+end_src | |
| 604 | ||
| 605 | Finally, download the HTML conversion files you need. The example below | |
| 606 | downloads the Markdown converter: | |
| 607 | ||
| 608 | #+begin_src sh | |
| 609 | cd html-converters | |
| 610 | curl https://git.zx2c4.com/cgit/plain/filters/html-converters/md2html > md2html | |
| 611 | chmod 755 md2html | |
| 612 | #+end_src | |
| 613 | ||
| 614 | If you need other filters or html-converters found within | |
| 615 | [[https://git.zx2c4.com/cgit/tree/filters][the cgit project files]], | |
| 616 | repeat the =curl= and =chmod= process above for whichever files you | |
| 617 | need. | |
| 618 | ||
| 619 | However, formatting will not work quite yet since the Docker cgit | |
| 620 | container we're using doesn't have the formatting package installed. You | |
| 621 | can install this easily by install Python 3+ and the =pygments= package: | |
| 622 | ||
| 623 | #+begin_src sh | |
| 624 | # Enter the container's command line | |
| 625 | sudo docker exec -it cgit bash | |
| 626 | #+end_src | |
| 627 | ||
| 628 | #+begin_src sh | |
| 629 | # Install the necessary packages and then exit | |
| 630 | yum update -y && \ | |
| 631 | yum upgrade -y && \ | |
| 632 | yum install python3 python3-pip -y && \ | |
| 633 | pip3 install markdown pygments && \ | |
| 634 | exit | |
| 635 | #+end_src | |
| 636 | ||
| 637 | *You will need to enter the cgit docker container and re-run these =yum= | |
| 638 | commands every time you kill and restart the container!* | |
| 639 | ||
| 640 | If not done already, we need to add the following variables to our | |
| 641 | =cgitrc= file in order for =cgit= to know where our filtering files are: | |
| 642 | ||
| 643 | #+begin_src conf | |
| 644 | # Highlight source code with python pygments-based highlighter | |
| 645 | source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.py | |
| 646 | ||
| 647 | # Format markdown, restructuredtext, manpages, text files, and html files | |
| 648 | # through the right converters | |
| 649 | about-filter=/var/www/htdocs/cgit/filters/about-formatting.sh | |
| 650 | #+end_src | |
| 651 | ||
| 652 | Now you should see that syntax highlighting and README rendering to the | |
| 653 | =about= tab is fixed. | |
| 654 | ||
| 655 | *** Theming | |
| 656 | :PROPERTIES: | |
| 657 | :CUSTOM_ID: theming | |
| 658 | :END: | |
| 659 | I won't go into much detail in this section, but you can fully theme | |
| 660 | your installation of =cgit= since you have access to the =cgit.css= file | |
| 661 | in your web root. This is another file you can add as a volume to the | |
| 662 | =docker-compose.yml= file if you want to edit this without entering the | |
| 663 | container's command line. | |
| 664 | ||
| 665 | ** :warning: Remember to Back Up Your Data! | |
| 666 | :PROPERTIES: | |
| 667 | :CUSTOM_ID: warning-remember-to-back-up-your-data | |
| 668 | :END: | |
| 669 | The last thing to note is that running services on your own equipment | |
| 670 | means that you're assuming a level of risk that exists regarding data | |
| 671 | loss, catastrophes, etc. In order to reduce the impact of any such | |
| 672 | occurrence, I suggest backing up your data regularly. | |
| 673 | ||
| 674 | Backups can be automated via =cron=, by hooking your base directory up | |
| 675 | to a cloud provider, or even setting up hooks to push all repository | |
| 676 | info to git mirrors on other git hosts. Whatever the method, make sure | |
| 677 | that your data doesn't vanish in the event that your drives or servers | |
| 678 | fail. | |
blog/2022-07-25-curseradio.org deleted −114
| @@ -1,114 +0,0 @@ | ||
| 1 | #+title: CurseRadio: Listening to the Radio on the Command Line | |
| 2 | #+date: 2022-07-25 | |
| 3 | ||
| 4 | ** Overview | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: overview | |
| 7 | :END: | |
| 8 | While exploring some interesting Linux applications, I stumbled across | |
| 9 | [[https://github.com/chronitis/curseradio][curseradio]], a command-line | |
| 10 | radio player based on Python. | |
| 11 | ||
| 12 | This application is fantastic and incredibly easy to install, so I | |
| 13 | wanted to dedicate a post today to this app. Let's look at the features | |
| 14 | within the app and then walk through the installation process I took to | |
| 15 | get =curseradio= working. | |
| 16 | ||
| 17 | ** Features | |
| 18 | :PROPERTIES: | |
| 19 | :CUSTOM_ID: features | |
| 20 | :END: | |
| 21 | #+caption: curseradio | |
| 22 | [[https://img.cleberg.net/blog/20220725-curseradio/curseradio.png]] | |
| 23 | ||
| 24 | The radio player itself is quite minimal. As you can see in the | |
| 25 | screenshot above, it contains a simple plaintext list of all available | |
| 26 | categories, which can be broken down further and further. In addition, | |
| 27 | radio shows are available for listening, alongside regular radio | |
| 28 | stations. | |
| 29 | ||
| 30 | For example, the =Sports= > =Pro Basketball= > =Shows= category contains | |
| 31 | a number of specific shows related to Professional Basketball. | |
| 32 | ||
| 33 | Aside from being able to play any of the listed stations/shows, you can | |
| 34 | make a channel your favorite by pressing =f=. It will now show up at the | |
| 35 | top of the radio player in the =Favourites= category. | |
| 36 | ||
| 37 | *** Commands/Shortcuts | |
| 38 | :PROPERTIES: | |
| 39 | :CUSTOM_ID: commandsshortcuts | |
| 40 | :END: | |
| 41 | | Key(s) | Command | | |
| 42 | |------------+---------------------------------| | |
| 43 | | ↑, ↓ | navigate | | |
| 44 | | PgUp, PgDn | navigate quickly | | |
| 45 | | Home, End | to top/bottom | | |
| 46 | | Enter | open/close folders, play stream | | |
| 47 | | k | stop playing stream | | |
| 48 | | q | quit | | |
| 49 | | f | toggle favourite | | |
| 50 | ||
| 51 | ** Installation | |
| 52 | :PROPERTIES: | |
| 53 | :CUSTOM_ID: installation | |
| 54 | :END: | |
| 55 | *** Dependencies | |
| 56 | :PROPERTIES: | |
| 57 | :CUSTOM_ID: dependencies | |
| 58 | :END: | |
| 59 | Before installing =curseradio=, a handful of system and Python packages | |
| 60 | are required. To get started, install =python3=, =pip3=, and =mpv= on | |
| 61 | your system. In this example, I'm using Fedora Linux, which uses the | |
| 62 | =dnf= package manager. You may need to adjust this if you're using a | |
| 63 | different system. | |
| 64 | ||
| 65 | #+begin_src sh | |
| 66 | sudo dnf install python3 pip3 mpv | |
| 67 | #+end_src | |
| 68 | ||
| 69 | Next, use =pip3= to install =requests=, =xdg=, and =lxml=: | |
| 70 | ||
| 71 | #+begin_src sh | |
| 72 | pip3 install requests xdg lxml | |
| 73 | #+end_src | |
| 74 | ||
| 75 | *** Repository Source Installation | |
| 76 | :PROPERTIES: | |
| 77 | :CUSTOM_ID: repository-source-installation | |
| 78 | :END: | |
| 79 | Once all the dependencies are installed, we can clone the source code | |
| 80 | and enter that directory: | |
| 81 | ||
| 82 | #+begin_src sh | |
| 83 | git clone https://github.com/chronitis/curseradio && cd curseradio | |
| 84 | #+end_src | |
| 85 | ||
| 86 | Once you're within the =curseradio= directory, you can install the | |
| 87 | application with the provided =setup.py= script. | |
| 88 | ||
| 89 | #+begin_src sh | |
| 90 | sudo python3 setup.py install | |
| 91 | #+end_src | |
| 92 | ||
| 93 | In my case, I ran into a few errors and needed to create the folders | |
| 94 | that curseradio wanted to use for its installation. If you don't get any | |
| 95 | errors, you can skip this and run the app. | |
| 96 | ||
| 97 | #+begin_src sh | |
| 98 | sudo mkdir /usr/local/lib/python3.10/ | |
| 99 | sudo mkdir /usr/local/lib/python3.10/site-packages/ | |
| 100 | #+end_src | |
| 101 | ||
| 102 | #+begin_src sh | |
| 103 | sudo chown -R $USER:$USER /usr/local/lib/python3.10/ | |
| 104 | #+end_src | |
| 105 | ||
| 106 | ** Run the Application | |
| 107 | :PROPERTIES: | |
| 108 | :CUSTOM_ID: run-the-application | |
| 109 | :END: | |
| 110 | Once fully installed without errors, you can run the application! | |
| 111 | ||
| 112 | #+begin_src sh | |
| 113 | python3 /usr/local/bin/curseradio | |
| 114 | #+end_src | |
blog/2022-09-21-graphene-os.org deleted −186
| @@ -1,186 +0,0 @@ | ||
| 1 | #+title: Installing Graphene OS on the Pixel 6 Pro | |
| 2 | #+date: 2022-09-21 | |
| 3 | ||
| 4 | ** Introduction | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: introduction | |
| 7 | :END: | |
| 8 | After using iOS for a couple of years, I finally took the plunge and | |
| 9 | purchased a Pixel 6 Pro in order to test and use [GrapheneOS] | |
| 10 | (https://grapheneos.org). | |
| 11 | ||
| 12 | The installation process was rather quick once you have the tools and | |
| 13 | files you need. Overall, it can be done in just a few minutes. | |
| 14 | ||
| 15 | ** Gathering Tools & Files | |
| 16 | :PROPERTIES: | |
| 17 | :CUSTOM_ID: gathering-tools-files | |
| 18 | :END: | |
| 19 | *** Android Tools | |
| 20 | :PROPERTIES: | |
| 21 | :CUSTOM_ID: android-tools | |
| 22 | :END: | |
| 23 | First, in order to interact with the device, we will need the | |
| 24 | [[https://developer.android.com/studio/releases/platform-tools.html][Android | |
| 25 | platform tools]]. Find the Linux download and save the ZIP folder to | |
| 26 | your preferred location. | |
| 27 | ||
| 28 | Once we've downloaded the files, we will need to unzip them, enter the | |
| 29 | directory, and move the necessary executables to a central location, | |
| 30 | such as =/usr/bin/=. For this installation, we only need the =fastboot= | |
| 31 | and =adb= executables. | |
| 32 | ||
| 33 | #+begin_src sh | |
| 34 | cd ~/Downloads | |
| 35 | #+end_src | |
| 36 | ||
| 37 | #+begin_src sh | |
| 38 | unzip platform-tools_r33.0.3-linux.zip | |
| 39 | cd platform-tools | |
| 40 | sudo mv fastboot /usr/bin/ | |
| 41 | sudo mv adb /usr/bin | |
| 42 | #+end_src | |
| 43 | ||
| 44 | *** GrapheneOS Files | |
| 45 | :PROPERTIES: | |
| 46 | :CUSTOM_ID: grapheneos-files | |
| 47 | :END: | |
| 48 | Next, we need the [[https://grapheneos.org/releases][GrapheneOS files]] | |
| 49 | for our device and model. For example, the Pixel 6 Pro is codenamed | |
| 50 | =raven= on the release page. | |
| 51 | ||
| 52 | Once we have the links, let's download them to our working directory: | |
| 53 | ||
| 54 | #+begin_src sh | |
| 55 | curl -O https://releases.grapheneos.org/factory.pub | |
| 56 | curl -0 https://releases.grapheneos.org/raven-factory-2022091400.zip | |
| 57 | curl -0 https://releases.grapheneos.org/raven-factory-2022091400.zip.sig | |
| 58 | #+end_src | |
| 59 | ||
| 60 | **** Validate Integrity | |
| 61 | :PROPERTIES: | |
| 62 | :CUSTOM_ID: validate-integrity | |
| 63 | :END: | |
| 64 | In order to validate the integrity of the downloaded files, we will need | |
| 65 | the =signify= package and Graphene's =factory.pub= file. | |
| 66 | ||
| 67 | #+begin_src sh | |
| 68 | sudo dnf install signify | |
| 69 | #+end_src | |
| 70 | ||
| 71 | #+begin_src sh | |
| 72 | curl -O https://releases.grapheneos.org/factory.pub | |
| 73 | #+end_src | |
| 74 | ||
| 75 | Then we can validate the files and ensure that no data was corrupted or | |
| 76 | modified before it was saved to our device. | |
| 77 | ||
| 78 | #+begin_src sh | |
| 79 | signify -Cqp factory.pub -x raven-factory-2022091400.zip.sig && echo verified | |
| 80 | #+end_src | |
| 81 | ||
| 82 | **** Unzip Files | |
| 83 | :PROPERTIES: | |
| 84 | :CUSTOM_ID: unzip-files | |
| 85 | :END: | |
| 86 | Once the files are verified, we can unzip the Graphene image and enter | |
| 87 | the directory: | |
| 88 | ||
| 89 | #+begin_src sh | |
| 90 | unzip raven-factory-2022091400.zip && cd raven-factory-2022091400 | |
| 91 | #+end_src | |
| 92 | ||
| 93 | ** Installation Process | |
| 94 | :PROPERTIES: | |
| 95 | :CUSTOM_ID: installation-process | |
| 96 | :END: | |
| 97 | *** Enable Developer Debugging & OEM Unlock | |
| 98 | :PROPERTIES: | |
| 99 | :CUSTOM_ID: enable-developer-debugging-oem-unlock | |
| 100 | :END: | |
| 101 | Before we can actually flash anything to the phone, we will need to | |
| 102 | enable OEM Unlocking, as well as either USB Debugging or Wireless | |
| 103 | Debugging, depending on which method we will be using. | |
| 104 | ||
| 105 | To start, enable developer mode by going to =Settings= > =About= and | |
| 106 | tapping =Build Number= seven (7) times. You may need to enter your PIN | |
| 107 | to enable this mode. | |
| 108 | ||
| 109 | Once developer mode is enabled, go to =Settings= > =System= > | |
| 110 | =Devloper Options= and enable OEM Unlocking, as well as USB or Wireless | |
| 111 | Debugging. In my case, I chose USB Debugging and performed all actions | |
| 112 | via USB cable. | |
| 113 | ||
| 114 | Once these options are enabled, plug the phone into the computer and | |
| 115 | execute the following command: | |
| 116 | ||
| 117 | #+begin_src sh | |
| 118 | adb devices | |
| 119 | #+end_src | |
| 120 | ||
| 121 | If an unauthorized error occurs, make sure the USB mode on the phone is | |
| 122 | changed from charging to something like "File Transfer" or "PTP." You | |
| 123 | can find the USB mode in the notification tray. | |
| 124 | ||
| 125 | *** Reboot Device | |
| 126 | :PROPERTIES: | |
| 127 | :CUSTOM_ID: reboot-device | |
| 128 | :END: | |
| 129 | Once we have found the device via =adb=, we can either boot into the | |
| 130 | bootloader interface by holding the volume down button while the phone | |
| 131 | reboots or by executing the following command: | |
| 132 | ||
| 133 | #+begin_src sh | |
| 134 | adb reboot bootloader | |
| 135 | #+end_src | |
| 136 | ||
| 137 | *** Unlock the Bootloader | |
| 138 | :PROPERTIES: | |
| 139 | :CUSTOM_ID: unlock-the-bootloader | |
| 140 | :END: | |
| 141 | The phone will reboot and load the bootloader screen upon startup. At | |
| 142 | this point, we are ready to start the actual flashing of GrapheneOS onto | |
| 143 | the device. | |
| 144 | ||
| 145 | *NOTE*: In my situation, I needed to use =sudo= with every =fastboot= | |
| 146 | command, but not with =adb= commands. I am not sure if this is standard | |
| 147 | or a Fedora quirk, but I'm documenting my commands verbatim in this | |
| 148 | post. | |
| 149 | ||
| 150 | First, we start by unlocking the bootloader so that we can load other | |
| 151 | ROMs: | |
| 152 | ||
| 153 | #+begin_src sh | |
| 154 | sudo fastboot flashing unlock | |
| 155 | #+end_src | |
| 156 | ||
| 157 | *** Flashing Factory Images | |
| 158 | :PROPERTIES: | |
| 159 | :CUSTOM_ID: flashing-factory-images | |
| 160 | :END: | |
| 161 | Once the phone is unlocked, we can flash it with the =flash-all.sh= | |
| 162 | script found inside the =raven-factory-2022091400= folder we entered | |
| 163 | earlier: | |
| 164 | ||
| 165 | #+begin_src sh | |
| 166 | sudo ./flash-all.sh | |
| 167 | #+end_src | |
| 168 | ||
| 169 | This process should take a few minutes and will print informational | |
| 170 | messages as things progress. Avoid doing anything on the phone while | |
| 171 | this process is operating. | |
| 172 | ||
| 173 | *** Lock the Bootloader | |
| 174 | :PROPERTIES: | |
| 175 | :CUSTOM_ID: lock-the-bootloader | |
| 176 | :END: | |
| 177 | If everything was successful, the phone should reboot a few times and | |
| 178 | finally land back on the bootloader screen. At this point, we can | |
| 179 | re-lock the bootloader to enable full verified boot and protect the | |
| 180 | device from unwanted flashing or erasure of data. | |
| 181 | ||
| 182 | #+begin_src sh | |
| 183 | sudo fastboot flashing lock | |
| 184 | #+end_src | |
| 185 | ||
| 186 | Once done, the device will be wiped and ready for a fresh set-up! | |
blog/2022-10-22-alpine-linux.org deleted −301
| @@ -1,301 +0,0 @@ | ||
| 1 | #+title: Alpine Linux: My New Server OS | |
| 2 | #+date: 2022-10-22 | |
| 3 | ||
| 4 | ** Alpine Linux | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: alpine-linux | |
| 7 | :END: | |
| 8 | [[https://alpinelinux.org][Alpine Linux]] is a very small distro, built | |
| 9 | on musl libc and busybox. It uses ash as the default shell, OpenRC as | |
| 10 | the init system, and apk as the package manager. According to their | |
| 11 | website, an Alpine container "requires no more than 8 MB and a minimal | |
| 12 | installation to disk requires around 130 MB of storage." An actual bare | |
| 13 | metal machine is recommended to have 100 MB of RAM and 0-700 MB of | |
| 14 | storage space. | |
| 15 | ||
| 16 | Historically, I've used Ubuntu's minimal installation image as my server | |
| 17 | OS for the last five years. Ubuntu worked well and helped as my original | |
| 18 | server contained an nVidia GPU and no onboard graphics, so quite a few | |
| 19 | distros won't boot or install without a lot of tinkering. | |
| 20 | ||
| 21 | Alpine has given me a huge increase in performance across my Docker apps | |
| 22 | and Nginx websites. CPU load for the new server I'm using to test Alpine | |
| 23 | hovers around 0-5% on average with an Intel(R) Core(TM) i3-6100 CPU @ | |
| 24 | 3.70GHz. | |
| 25 | ||
| 26 | The only services I haven't moved over to Alpine are Plex Media Server | |
| 27 | and Syncthing, which may increase CPU load quite a bit depending on how | |
| 28 | many streams are running. | |
| 29 | ||
| 30 | *** Installation | |
| 31 | :PROPERTIES: | |
| 32 | :CUSTOM_ID: installation | |
| 33 | :END: | |
| 34 | In terms of installation, Alpine has an incredibly useful | |
| 35 | [[https://wiki.alpinelinux.org/wiki/Installation][wiki]] that will guide | |
| 36 | a user throughout the installation and post-installation processes, as | |
| 37 | well as various other articles and guides. | |
| 38 | ||
| 39 | To install Alpine, find an appropriate | |
| 40 | [[https://alpinelinux.org/downloads/][image to download]] and flash it | |
| 41 | to a USB using software such as Rufus or Etcher. I opted to use the | |
| 42 | Standard image for my x86_64 architecture. | |
| 43 | ||
| 44 | Once the USB is ready, plug it into the machine and reboot. Note that | |
| 45 | you may have to use a key such as =Esc= or =F1-12= to access the boot | |
| 46 | menu. The Alpine Linux terminal will load quickly and for a login. | |
| 47 | ||
| 48 | To log in to the installation image, use the =root= account; there is no | |
| 49 | password. Once logged-in, execute the setup command: | |
| 50 | ||
| 51 | #+begin_src sh | |
| 52 | setup-alpine | |
| 53 | #+end_src | |
| 54 | ||
| 55 | The setup script will ask a series of questions to configure the system. | |
| 56 | Be sure to answer carefully or else you may have to re-configure the | |
| 57 | system after boot. | |
| 58 | ||
| 59 | - Keyboard Layout (Local keyboard language and usage mode, e.g., us and | |
| 60 | variant of us-nodeadkeys.) | |
| 61 | - Hostname (The name for the computer.) | |
| 62 | - Network (For example, automatic IP address discovery with the "DHCP" | |
| 63 | protocol.) | |
| 64 | - DNS Servers (Domain Name Servers to query. For privacy reasons, it is | |
| 65 | NOT recommended to route every local request to servers like Google's | |
| 66 | 8.8.8.8 .) | |
| 67 | - Timezone | |
| 68 | - Proxy (Proxy server to use for accessing the web. Use "none" for | |
| 69 | direct connections to the internet.) | |
| 70 | - Mirror (From where to download packages. Choose the organization you | |
| 71 | trust giving your usage patterns to.) | |
| 72 | - SSH (Secure SHell remote access server. "Openssh" is part of the | |
| 73 | default install image. Use "none" to disable remote login, e.g. on | |
| 74 | laptops.) | |
| 75 | - NTP (Network Time Protocol client used for keeping the system clock in | |
| 76 | sync with a time-server. Package "chrony" is part of the default | |
| 77 | install image.) | |
| 78 | - Disk Mode (Select between diskless (disk="none"), "data" or "sys", as | |
| 79 | described above.) | |
| 80 | ||
| 81 | Once the setup script is finished, be sure to reboot the machine and | |
| 82 | remove the USB device. | |
| 83 | ||
| 84 | #+begin_src sh | |
| 85 | reboot | |
| 86 | #+end_src | |
| 87 | ||
| 88 | *** Post-Installation | |
| 89 | :PROPERTIES: | |
| 90 | :CUSTOM_ID: post-installation | |
| 91 | :END: | |
| 92 | There are many things you can do once your Alpine Linux system is up and | |
| 93 | running, and it largely depends on what you'll use the machine for. I'm | |
| 94 | going to walk through my personal post-installation setup for my web | |
| 95 | server. | |
| 96 | ||
| 97 | **** Upgrade the System | |
| 98 | :PROPERTIES: | |
| 99 | :CUSTOM_ID: upgrade-the-system | |
| 100 | :END: | |
| 101 | First, login as =root= in order to update and upgrade the system: | |
| 102 | ||
| 103 | #+begin_src sh | |
| 104 | apk -U upgrade | |
| 105 | #+end_src | |
| 106 | ||
| 107 | **** Adding a User | |
| 108 | :PROPERTIES: | |
| 109 | :CUSTOM_ID: adding-a-user | |
| 110 | :END: | |
| 111 | I needed to add a user so that I don't need to log in as root. Note that | |
| 112 | if you're used to using the =sudo= command, you will now need to use the | |
| 113 | =doas= command on Alpine Linux. | |
| 114 | ||
| 115 | #+begin_src sh | |
| 116 | apk add doas | |
| 117 | adduser <username> | |
| 118 | adduser <username> wheel | |
| 119 | #+end_src | |
| 120 | ||
| 121 | You can now log out and log back in using the newly-created user: | |
| 122 | ||
| 123 | #+begin_src sh | |
| 124 | exit | |
| 125 | #+end_src | |
| 126 | ||
| 127 | **** Enable Community Packages | |
| 128 | :PROPERTIES: | |
| 129 | :CUSTOM_ID: enable-community-packages | |
| 130 | :END: | |
| 131 | In order to install more common packages that aren't found in the =main= | |
| 132 | repository, you will need to enable the =community= repository: | |
| 133 | ||
| 134 | #+begin_src sh | |
| 135 | doas nano /etc/apk/repositories | |
| 136 | #+end_src | |
| 137 | ||
| 138 | Uncomment the community line for whichever version of Alpine you're | |
| 139 | running: | |
| 140 | ||
| 141 | #+begin_src sh | |
| 142 | /media/usb/apks | |
| 143 | http://dl-cdn.alpinelinux.org/alpine/v3.16/main | |
| 144 | http://dl-cdn.alpinelinux.org/alpine/v3.16/community | |
| 145 | #http://dl-cdn.alpinelinux.org/alpine/edge/main | |
| 146 | #http://dl-cdn.alpinelinux.org/alpine/edge/community | |
| 147 | #http://dl-cdn.alpinelinux.org/alpine/edge/testing | |
| 148 | #+end_src | |
| 149 | ||
| 150 | **** Install Required Packages | |
| 151 | :PROPERTIES: | |
| 152 | :CUSTOM_ID: install-required-packages | |
| 153 | :END: | |
| 154 | Now that the community packages are available, you can install any | |
| 155 | packages you need. In my case, I installed the web server packages I | |
| 156 | need for my services: | |
| 157 | ||
| 158 | #+begin_src sh | |
| 159 | doas apk add nano nginx docker docker-compose ufw | |
| 160 | #+end_src | |
| 161 | ||
| 162 | **** SSH | |
| 163 | :PROPERTIES: | |
| 164 | :CUSTOM_ID: ssh | |
| 165 | :END: | |
| 166 | If you didn't install OpenSSH as part of the installation, you can do so | |
| 167 | now: | |
| 168 | ||
| 169 | #+begin_src sh | |
| 170 | doas apk add openssh | |
| 171 | #+end_src | |
| 172 | ||
| 173 | Next, either create a new key or copy your SSH key to the server from | |
| 174 | your current machines: | |
| 175 | ||
| 176 | #+begin_src sh | |
| 177 | # Create a new key | |
| 178 | ssh-keygen | |
| 179 | #+end_src | |
| 180 | ||
| 181 | If you need to copy an existing SSH key from a current machine: | |
| 182 | ||
| 183 | #+begin_src sh | |
| 184 | # Copy key from existing machines | |
| 185 | ssh-copy-id <username>@<ip_address> | |
| 186 | #+end_src | |
| 187 | ||
| 188 | **** Firewall | |
| 189 | :PROPERTIES: | |
| 190 | :CUSTOM_ID: firewall | |
| 191 | :END: | |
| 192 | Lastly, I installed =ufw= above as my firewall. To set up, default to | |
| 193 | deny incoming and allow outgoing connections. Then selectively allow | |
| 194 | other ports or apps as needed. | |
| 195 | ||
| 196 | #+begin_src sh | |
| 197 | doas ufw default deny incoming | |
| 198 | doas ufw default allow outgoing | |
| 199 | doas ufw allow SSH | |
| 200 | doas ufw allow "WWW Full" | |
| 201 | doas ufw allow 9418 # Git server port | |
| 202 | #+end_src | |
| 203 | ||
| 204 | **** Change Hostname | |
| 205 | :PROPERTIES: | |
| 206 | :CUSTOM_ID: change-hostname | |
| 207 | :END: | |
| 208 | If you don't like the hostname set during installation, you just need to | |
| 209 | edit two files. First, edit the simple hostname file: | |
| 210 | ||
| 211 | #+begin_src sh | |
| 212 | doas nano /etc/hostname | |
| 213 | #+end_src | |
| 214 | ||
| 215 | #+begin_src sh | |
| 216 | <hostname> | |
| 217 | #+end_src | |
| 218 | ||
| 219 | Next, edit the =hosts= file: | |
| 220 | ||
| 221 | #+begin_src sh | |
| 222 | doas nano /etc/hosts | |
| 223 | #+end_src | |
| 224 | ||
| 225 | #+begin_src sh | |
| 226 | 127.0.0.1 <hostname>.local <hostname> localhost.local localhost | |
| 227 | ::1 <hostname> <hostname>.local | |
| 228 | #+end_src | |
| 229 | ||
| 230 | ** Nginx Web Server | |
| 231 | :PROPERTIES: | |
| 232 | :CUSTOM_ID: nginx-web-server | |
| 233 | :END: | |
| 234 | To set up my web server, I simply created the =www= user and created the | |
| 235 | necessary files. | |
| 236 | ||
| 237 | #+begin_src sh | |
| 238 | doas adduser -D -g 'www' www | |
| 239 | mkdir /www | |
| 240 | doas mkdir /www | |
| 241 | doas chown -R www:www /var/lib/nginx/ | |
| 242 | doas chown -R www:www /www | |
| 243 | #+end_src | |
| 244 | ||
| 245 | If you're running a simple webroot, you can alter the main =nginx.conf= | |
| 246 | file. Otherwise, you can drop configuration files in the following | |
| 247 | directory. You don't need to enable or symlink the configuration file | |
| 248 | like you do in other systems. | |
| 249 | ||
| 250 | #+begin_src sh | |
| 251 | doas nano /etc/nginx/http.d/example_website.conf | |
| 252 | #+end_src | |
| 253 | ||
| 254 | Once the configuration is set and pointed at the =/www= directory to | |
| 255 | serve files, enable the Nginx service: | |
| 256 | ||
| 257 | #+begin_src sh | |
| 258 | # Note that 'default' must be included or Nginx will not start on boot | |
| 259 | doas rc-update add nginx default | |
| 260 | #+end_src | |
| 261 | ||
| 262 | ** Docker Containers | |
| 263 | :PROPERTIES: | |
| 264 | :CUSTOM_ID: docker-containers | |
| 265 | :END: | |
| 266 | Docker works exactly the same as other systems. Either execute a | |
| 267 | =docker run= command or create a =docker-compose.yml= file and do | |
| 268 | =docker-compose up -d=. | |
| 269 | ||
| 270 | ** Git Server | |
| 271 | :PROPERTIES: | |
| 272 | :CUSTOM_ID: git-server | |
| 273 | :END: | |
| 274 | I went in-depth on how to self-host a git server in another post: | |
| 275 | [[/blog/git-server/][Self-Hosting a Personal Git Server]]. | |
| 276 | ||
| 277 | However, there are a few differences with Alpine. First note that in | |
| 278 | order to change the =git= user's shell, you must do a few things a | |
| 279 | little different: | |
| 280 | ||
| 281 | #+begin_src sh | |
| 282 | doas apk add libuser | |
| 283 | doas touch /etc/login.defs | |
| 284 | doas mkdir /etc/default | |
| 285 | doas touch /etc/default/useradd | |
| 286 | doas lchsh git | |
| 287 | #+end_src | |
| 288 | ||
| 289 | ** Thoughts on Alpine | |
| 290 | :PROPERTIES: | |
| 291 | :CUSTOM_ID: thoughts-on-alpine | |
| 292 | :END: | |
| 293 | So far, I love Alpine Linux. I have no complaints about anything at this | |
| 294 | point, but I'm not completely finished with the migration yet. Once I'm | |
| 295 | able to upgrade my hardware to a rack-mounted server, I will migrate | |
| 296 | Plex and Syncthing over to Alpine as well - possibly putting Plex into a | |
| 297 | container or VM. | |
| 298 | ||
| 299 | The performance is stellar, the =apk= package manager is seamless, and | |
| 300 | system administration tasks are effortless. My only regret is that I | |
| 301 | didn't install Alpine sooner. | |
blog/2022-10-30-linux-display-manager.org deleted −85
| @@ -1,85 +0,0 @@ | ||
| 1 | #+title: How to Disable or Change the Display Manager on Void Linux | |
| 2 | #+date: 2022-10-30 | |
| 3 | ||
| 4 | ** Display Manager Services | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: display-manager-services | |
| 7 | :END: | |
| 8 | In order to change the | |
| 9 | [[https://en.wikipedia.org/wiki/Display_manager][display manager]] on | |
| 10 | Void Linux - or any other Linux distro - you need to identify the | |
| 11 | currently enabled display manager. | |
| 12 | ||
| 13 | *** Disabling the Current Display Manager | |
| 14 | :PROPERTIES: | |
| 15 | :CUSTOM_ID: disabling-the-current-display-manager | |
| 16 | :END: | |
| 17 | Void Linux only has one ISO available for download with a pre-built | |
| 18 | display manager at the time of this post: the XFCE ISO. If you've | |
| 19 | installed this version, the pre-assigned display manager is =lxdm=. If | |
| 20 | you installed another display manager, replace =lxdm= in the following | |
| 21 | command with the display manager you have installed. | |
| 22 | ||
| 23 | To disable =lxdm=, simply remove the service symlink: | |
| 24 | ||
| 25 | #+begin_src sh | |
| 26 | sudo rm /var/service/lxdm | |
| 27 | #+end_src | |
| 28 | ||
| 29 | *** Enabling a New Display Manager | |
| 30 | :PROPERTIES: | |
| 31 | :CUSTOM_ID: enabling-a-new-display-manager | |
| 32 | :END: | |
| 33 | If you want to enable a new display manager, you can do so after =lxdm= | |
| 34 | is disabled. Make sure to replace =<new_display_manager>= with your new | |
| 35 | DM, such as =gdm=, =xdm=, etc. | |
| 36 | ||
| 37 | #+begin_src sh | |
| 38 | sudo ln -s /etc/sv/<new_display_manager> /var/service | |
| 39 | #+end_src | |
| 40 | ||
| 41 | ** Set Up =.xinitrc= | |
| 42 | :PROPERTIES: | |
| 43 | :CUSTOM_ID: set-up-.xinitrc | |
| 44 | :END: | |
| 45 | Depending on your setup, you may need to create a few X files, such as | |
| 46 | =~/.xinitrc=. For my personal set-up, I created this file to launch the | |
| 47 | i3wm as my desktop. | |
| 48 | ||
| 49 | #+begin_src sh | |
| 50 | nano ~/.xinitrc | |
| 51 | #+end_src | |
| 52 | ||
| 53 | #+begin_src sh | |
| 54 | #!/bin/sh | |
| 55 | ||
| 56 | exec i3 | |
| 57 | #+end_src | |
| 58 | ||
| 59 | If you run a desktop other than i3, simply replace =i3= with the shell | |
| 60 | command that launches that desktop. | |
| 61 | ||
| 62 | ** Set Up Your Shell Profile | |
| 63 | :PROPERTIES: | |
| 64 | :CUSTOM_ID: set-up-your-shell-profile | |
| 65 | :END: | |
| 66 | Finally, in order to automatically launch an X session upon login, you | |
| 67 | will need to edit the =.bash_profile= (bash) or =.zprofile= (zsh) files | |
| 68 | for your shell: | |
| 69 | ||
| 70 | #+begin_src sh | |
| 71 | nano ~/.zprofile | |
| 72 | #+end_src | |
| 73 | ||
| 74 | Add the following snippet to the end of the shell profile file. This | |
| 75 | will execute the =startx= command upon login. | |
| 76 | ||
| 77 | #+begin_src sh | |
| 78 | if [ -z "${DISPLAY}" ] && [ "${XDG_VTNR}" -eq 1 ]; then | |
| 79 | exec startx | |
| 80 | fi | |
| 81 | #+end_src | |
| 82 | ||
| 83 | Alternatively, you can ignore this step and simply choose to manually | |
| 84 | execute =startx= upon login. This can be useful if you have issues with | |
| 85 | your desktop or like to manually launch different desktops by choice. | |
blog/2022-12-01-nginx-compression.org deleted −83
| @@ -1,83 +0,0 @@ | ||
| 1 | #+title: Enable GZIP Compression on Nginx | |
| 2 | #+date: 2022-12-01 | |
| 3 | ||
| 4 | ** Text Compression | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: text-compression | |
| 7 | :ID: 24C2F6E7-129E-42F3-91F0-A6C6C519FA01 | |
| 8 | :END: | |
| 9 | Text compression allows a web server to serve text-based resources | |
| 10 | faster than uncompressed data. This can speed up things like First | |
| 11 | Contentful Paint, Tie to Interactive, and Speed Index. | |
| 12 | ||
| 13 | ** Enable Nginx Compression with gzip | |
| 14 | :PROPERTIES: | |
| 15 | :CUSTOM_ID: enable-nginx-compression-with-gzip | |
| 16 | :ID: C018C991-E253-4779-A702-AEB69614499F | |
| 17 | :END: | |
| 18 | In order to enable text compression on Nginx, we need to enable it | |
| 19 | within the configuration file: | |
| 20 | ||
| 21 | #+begin_src sh | |
| 22 | nano /etc/nginx/nginx.conf | |
| 23 | #+end_src | |
| 24 | ||
| 25 | Within the =http= block, find the section that shows something like the | |
| 26 | block below. This is the default gzip configuration I found in my | |
| 27 | =nginx.conf= file on Alpine Linux 3.17. Yours may look slightly | |
| 28 | different, just make sure that you're not creating any duplicate gzip | |
| 29 | options. | |
| 30 | ||
| 31 | #+begin_src conf | |
| 32 | # Enable gzipping of responses. | |
| 33 | #gzip on; | |
| 34 | ||
| 35 | # Set the Vary HTTP header as defined in the RFC 2616. Default is 'off'. | |
| 36 | gzip_vary on; | |
| 37 | #+end_src | |
| 38 | ||
| 39 | Remove the default gzip lines and replace them with the following: | |
| 40 | ||
| 41 | #+begin_src conf | |
| 42 | # Enable gzipping of responses. | |
| 43 | gzip on; | |
| 44 | gzip_vary on; | |
| 45 | gzip_min_length 10240; | |
| 46 | gzip_proxied expired no-cache no-store private auth; | |
| 47 | gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; | |
| 48 | gzip_disable "MSIE [1-6]\."; | |
| 49 | #+end_src | |
| 50 | ||
| 51 | ** Explanations of ngx_http_gzip_module Options | |
| 52 | :PROPERTIES: | |
| 53 | :CUSTOM_ID: explanations-of-ngx_http_gzip_module-options | |
| 54 | :ID: C180752B-E1B9-49A5-B180-84488068A76A | |
| 55 | :END: | |
| 56 | Each of the lines above enables a different aspect of the gzip response | |
| 57 | for Nginx. Here are the full explanations: | |
| 58 | ||
| 59 | - =gzip= -- Enables or disables gzipping of responses. | |
| 60 | - =gzip_vary= -- Enables or disables inserting the "Vary: | |
| 61 | Accept-Encoding" response header field if the directives gzip, | |
| 62 | gzip_static, or gunzip are active. | |
| 63 | - =gzip_min_length= -- Sets the minimum length of a response that will | |
| 64 | be gzipped. The length is determined only from the "Content-Length" | |
| 65 | response header field. | |
| 66 | - =gzip_proxied= -- Enables or disables gzipping of responses for | |
| 67 | proxied requests depending on the request and response. The fact that | |
| 68 | the request is proxied is determined by the presence of the "Via" | |
| 69 | request header field. | |
| 70 | - =gzip_types= -- Enables gzipping of responses for the specified MIME | |
| 71 | types in addition to "text/html". The special value “*” matches any | |
| 72 | MIME type (0.8.29). Responses with the "text/html" type are always | |
| 73 | compressed. | |
| 74 | - =gzip_disable= -- Disables gzipping of responses for requests with | |
| 75 | "User-Agent" header fields matching any of the specified regular | |
| 76 | expressions. | |
| 77 | - The special mask "msie6" (0.7.12) corresponds to the regular | |
| 78 | expression "MSIE [4-6].", but works faster. Starting from version | |
| 79 | 0.8.11, "MSIE 6.0; ... SV1" is excluded from this mask. | |
| 80 | ||
| 81 | More information on these directives and their options can be found on | |
| 82 | the [[https://nginx.org/en/docs/http/ngx_http_gzip_module.html][Module | |
| 83 | ngx_http_gzip_module]] page in Nginx's documentation. | |
blog/2022-12-17-st.org deleted −100
| @@ -1,100 +0,0 @@ | ||
| 1 | #+title: Simple Terminal | |
| 2 | #+date: 2022-12-17 | |
| 3 | ||
| 4 | ** st | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: st | |
| 7 | :END: | |
| 8 | [[https://st.suckless.org][st]] standards for Simple Terminal, a simple | |
| 9 | terminal implementation for X made by the | |
| 10 | [[https://suckless.org][suckless]] team. | |
| 11 | ||
| 12 | This post walks through the dependencies needed and process to build and | |
| 13 | install =st= on Fedora Workstation. | |
| 14 | ||
| 15 | *** Obtain Files | |
| 16 | :PROPERTIES: | |
| 17 | :CUSTOM_ID: obtain-files | |
| 18 | :END: | |
| 19 | To start, obtain the source files for =st= via =git clone=. | |
| 20 | ||
| 21 | #+begin_src sh | |
| 22 | mkdir ~/suckless && cd ~/suckless | |
| 23 | git clone https://git.suckless.org/st && cd st | |
| 24 | #+end_src | |
| 25 | ||
| 26 | *** Dependencies | |
| 27 | :PROPERTIES: | |
| 28 | :CUSTOM_ID: dependencies | |
| 29 | :END: | |
| 30 | Once you have the files and are in the =st= directory, ensure the | |
| 31 | following packages are installed. | |
| 32 | ||
| 33 | #+begin_src sh | |
| 34 | sudo dnf update && sudo dnf upgrade | |
| 35 | sudo dnf install gcc patch libX11-devel libXft-devel | |
| 36 | #+end_src | |
| 37 | ||
| 38 | *** Building | |
| 39 | :PROPERTIES: | |
| 40 | :CUSTOM_ID: building | |
| 41 | :END: | |
| 42 | Before building, ensure that you read the README file. | |
| 43 | ||
| 44 | #+begin_src sh | |
| 45 | cat README | |
| 46 | #+end_src | |
| 47 | ||
| 48 | Once you've read the instructions, open the =config.mk= file and ensure | |
| 49 | it matches your setup. If you're not sure, leave the default options | |
| 50 | within the file. | |
| 51 | ||
| 52 | Finally, you can build =st= with the following command. Ensure you run | |
| 53 | as root (e.g., =sudo=) or else you may not end up with a usable | |
| 54 | application file. | |
| 55 | ||
| 56 | #+begin_src sh | |
| 57 | sudo make clean install | |
| 58 | #+end_src | |
| 59 | ||
| 60 | *** Customization (Patches) | |
| 61 | :PROPERTIES: | |
| 62 | :CUSTOM_ID: customization-patches | |
| 63 | :END: | |
| 64 | Note that customizing =st= requires you to modify the source files or to | |
| 65 | download one of the [[https://st.suckless.org/patches/][available | |
| 66 | patches]] for suckless.org. | |
| 67 | ||
| 68 | If you've already installed =st= and want to customize or install a | |
| 69 | patch, start by uninstalling the current program. | |
| 70 | ||
| 71 | #+begin_src sh | |
| 72 | cd ~/suckless/st | |
| 73 | sudo make uninstall | |
| 74 | #+end_src | |
| 75 | ||
| 76 | Next, grab the =<path>.diff= file from the page of the patch you chose. | |
| 77 | For example, I will be using the | |
| 78 | [[https://st.suckless.org/patches/defaultfontsize/][defaultfontsize]] | |
| 79 | patch in the below example. | |
| 80 | ||
| 81 | #+begin_src sh | |
| 82 | wget https://st.suckless.org/patches/defaultfontsize/st-defaultfontsize-20210225-4ef0cbd.diff | |
| 83 | #+end_src | |
| 84 | ||
| 85 | Once the file is downloaded inside the =st= folder, apply the patch and | |
| 86 | re-install the program. You may need to install the =patch= command if | |
| 87 | you don't have it installed already (you should have installed it | |
| 88 | above). | |
| 89 | ||
| 90 | #+begin_src sh | |
| 91 | patch -i st-defaultfontsize-20210225-4ef0cbd.diff | |
| 92 | sudo make clean install | |
| 93 | #+end_src | |
| 94 | ||
| 95 | Once installed, you can use the default font size patch to launch =st= | |
| 96 | with any font size you wish: | |
| 97 | ||
| 98 | #+begin_src sh | |
| 99 | st -z 16 | |
| 100 | #+end_src | |
blog/2023-02-02-exploring-hare.org deleted −189
| @@ -1,189 +0,0 @@ | ||
| 1 | #+title: Exploring the Hare Programming Language | |
| 2 | #+date: 2023-02-02 | |
| 3 | ||
| 4 | ** A Quick Note | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: a-quick-note | |
| 7 | :END: | |
| 8 | By no means am I a professional developer, so this post will be rather | |
| 9 | short. I won't be going into depth on the specification or anything that | |
| 10 | technical. | |
| 11 | ||
| 12 | Instead, I will simply be talking about how I (a relatively basic | |
| 13 | hobbyist programmer) have been playing with Hare and what intrigues me | |
| 14 | about the language. | |
| 15 | ||
| 16 | ** Hare | |
| 17 | :PROPERTIES: | |
| 18 | :CUSTOM_ID: hare | |
| 19 | :END: | |
| 20 | The [[https://harelang.org][Hare]] programming language is a | |
| 21 | straightforward language that should look familiar if you've ever | |
| 22 | programmed with C, Rust, or other languages that aim to build software | |
| 23 | at the system-level. | |
| 24 | ||
| 25 | The Hare homepage states the following: | |
| 26 | ||
| 27 | #+begin_quote | |
| 28 | Hare is a systems programming language designed to be simple, stable, | |
| 29 | and robust. Hare uses a static type system, manual memory management, | |
| 30 | and minimal runtime. It is well-suited to writing operating systems, | |
| 31 | system tools, compilers, networking software, and other low-level, high | |
| 32 | performance tasks. | |
| 33 | ||
| 34 | #+end_quote | |
| 35 | ||
| 36 | I have found this all to be true while playing with it for the first | |
| 37 | time today. In the next few sections, I'm going to walk through my | |
| 38 | installation and first program. | |
| 39 | ||
| 40 | *** Installation | |
| 41 | :PROPERTIES: | |
| 42 | :CUSTOM_ID: installation | |
| 43 | :END: | |
| 44 | I'm currently running Alpine Linux on my Thinkpad, so the installation | |
| 45 | was quite easy as there is a package for Hare in the =apk= repositories. | |
| 46 | ||
| 47 | #+begin_src sh | |
| 48 | doas apk add hare hare-doc | |
| 49 | #+end_src | |
| 50 | ||
| 51 | However, I was able to install Hare from scratch on Fedora Linux a short | |
| 52 | while ago, which was also very easy to do. If you need further | |
| 53 | instructions and Hare doesn't have a package on your system, take a look | |
| 54 | at the [[https://harelang.org/installation/][Hare Installation]] page. | |
| 55 | ||
| 56 | *** Creating a Test Project | |
| 57 | :PROPERTIES: | |
| 58 | :CUSTOM_ID: creating-a-test-project | |
| 59 | :END: | |
| 60 | In order to play with the language, I created | |
| 61 | [[https://git.sr.ht/~cmc/hare-projects][hare-test]] and will be putting | |
| 62 | any of my Hare-related adventures in here. | |
| 63 | ||
| 64 | #+begin_quote | |
| 65 | *Update:* I also created a simple Hare program for creating a file from | |
| 66 | user input: | |
| 67 | [[https://git.sr.ht/~cmc/hare-projects/tree/main/item/files/files.ha][files.ha]] | |
| 68 | ||
| 69 | #+end_quote | |
| 70 | ||
| 71 | Luckily, Hare doesn't require any complex set-up tools or build | |
| 72 | environment. Once you have Hare installed, you simply need to create a | |
| 73 | file ending with =.ha= and you can run a Hare program. | |
| 74 | ||
| 75 | I created a file called =rgb.ha= in order to test out the random number | |
| 76 | generation and passing parameters between functions. | |
| 77 | ||
| 78 | #+begin_src sh | |
| 79 | nano rgb.ha | |
| 80 | #+end_src | |
| 81 | ||
| 82 | Within this file, I was able to easily import a few of the | |
| 83 | [[https://harelang.org/tutorials/stdlib/][standard library modules]]: | |
| 84 | =fmt=, =math::random=, and =datetime=. | |
| 85 | ||
| 86 | With these modules, I created two functions: | |
| 87 | ||
| 88 | 1. =main=: This function calls the =generate_rgb= function and then | |
| 89 | prints out the returned values. | |
| 90 | 2. =generate_rgb=: This function uses the current Unix epoch time to | |
| 91 | generate a pseudo-random value and uses this value to create three | |
| 92 | more random values between 0 and 255. These three numbers represent a | |
| 93 | color in RGB format. | |
| 94 | ||
| 95 | #+begin_quote | |
| 96 | *Note*: Some syntax coloring may look odd, as Zola currently doesn't | |
| 97 | have a syntax highlighting theme for Hare. Instead, I'm using the C | |
| 98 | theme, which may not be exactly accurate when coloring the code below. | |
| 99 | ||
| 100 | #+end_quote | |
| 101 | ||
| 102 | #+begin_src C | |
| 103 | use datetime; | |
| 104 | use fmt; | |
| 105 | use math::random; | |
| 106 | ||
| 107 | export fn main() void = { | |
| 108 | const rgb = generate_rgb(); | |
| 109 | fmt::printfln("RGB: ({}, {}, {})", rgb[0], rgb[1], rgb[2])!; | |
| 110 | }; | |
| 111 | ||
| 112 | fn generate_rgb() []u64 = { | |
| 113 | // Use the current Unix epoch time as the seed value | |
| 114 | let datetime = datetime::epochunix(&datetime::now()); | |
| 115 | ||
| 116 | // Generate initial pseudo-random value | |
| 117 | // You must cast the datetime from int to u64 | |
| 118 | let x = random::init(datetime: u64); | |
| 119 | ||
| 120 | // Generate RGB values between (0, 255) using pseudo-random init value | |
| 121 | let r = random::u64n(&x, 255); | |
| 122 | let g = random::u64n(&x, 255); | |
| 123 | let b = random::u64n(&x, 255); | |
| 124 | ||
| 125 | // Structure data as array and return | |
| 126 | let rgb_array: [3]u64 = [r, g, b]; | |
| 127 | return rgb_array; | |
| 128 | }; | |
| 129 | #+end_src | |
| 130 | ||
| 131 | *** Running a Program | |
| 132 | :PROPERTIES: | |
| 133 | :CUSTOM_ID: running-a-program | |
| 134 | :END: | |
| 135 | Once you have a Hare file written and ready to run, you simply need to | |
| 136 | run it: | |
| 137 | ||
| 138 | #+begin_src sh | |
| 139 | hare run file.ha | |
| 140 | #+end_src | |
| 141 | ||
| 142 | You can also compile the program into an executable: | |
| 143 | ||
| 144 | #+begin_src sh | |
| 145 | hare build -o example file.ha | |
| 146 | ./example | |
| 147 | #+end_src | |
| 148 | ||
| 149 | *** Initial Thoughts | |
| 150 | :PROPERTIES: | |
| 151 | :CUSTOM_ID: initial-thoughts | |
| 152 | :END: | |
| 153 | **** Documentation Improvements Would Help | |
| 154 | :PROPERTIES: | |
| 155 | :CUSTOM_ID: documentation-improvements-would-help | |
| 156 | :END: | |
| 157 | While I was able to piece everything together eventually, the biggest | |
| 158 | downfall right now in Hare's documentation. For such a new project, the | |
| 159 | documentation is in a great spot. However, bare specifications don't | |
| 160 | help as much as a brief examples section would. | |
| 161 | ||
| 162 | For example, it took me a while to figure out what the =u64n= function | |
| 163 | was looking for. I could tell that it took two parameters and the second | |
| 164 | was my max value (255), but couldn't figure out what the first value | |
| 165 | should be. Eventually, I inspected the =random.ha= file in the | |
| 166 | [[https://git.sr.ht/~sircmpwn/hare/tree/master/item/math/random/random.ha][Hare | |
| 167 | source code]] and found the test suite that helped me discover that it | |
| 168 | needed an =init()= value in the form of =&var=. | |
| 169 | ||
| 170 | **** More Basic Modules | |
| 171 | :PROPERTIES: | |
| 172 | :CUSTOM_ID: more-basic-modules | |
| 173 | :END: | |
| 174 | This is another point that comes from Hare being new and awaiting more | |
| 175 | contributions, but there are some basic functions that I would | |
| 176 | personally enjoy seeing in Hare, such as one to convert decimal | |
| 177 | (base 10) values to hexadecimal (base 16). | |
| 178 | ||
| 179 | If I'm feeling comfortable with my math, I may work on the list of | |
| 180 | functions I want and see if any can make it into the Hare source code. | |
| 181 | ||
| 182 | **** Overall Thoughts | |
| 183 | :PROPERTIES: | |
| 184 | :CUSTOM_ID: overall-thoughts | |
| 185 | :END: | |
| 186 | Overall, I actually really enjoy Hare. It's not as tedious to get a | |
| 187 | project up and running as Rust, but it's also simpler and more | |
| 188 | user-friendly than learning C. I am going to continue playing with it | |
| 189 | and see if I can make anything of particular value. | |
blog/2023-06-18-unifi-ip-blocklist.org deleted −89
| @@ -1,89 +0,0 @@ | ||
| 1 | #+title: Block IP Addresses and Subnets with the Unifi Network Firewall | |
| 2 | #+date: 2023-06-18 | |
| 3 | ||
| 4 | ** Identifying Abusive IPs | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: identifying-abusive-ips | |
| 7 | :END: | |
| 8 | If you're like me and use Unifi network equipment at the edge of the | |
| 9 | network you manage, you may know that Unifi is only somewhat decent at | |
| 10 | identifying and blocking IPs that represent abusive or threat actors. | |
| 11 | ||
| 12 | While Unifi has a | |
| 13 | [[https://help.ui.com/hc/en-us/articles/360006893234-UniFi-Gateway-Threat-Management][threat | |
| 14 | management]] tool inside their Network application, it can be lacking in | |
| 15 | functionality and identification. For example, I have my UDM Pro set to | |
| 16 | identify and block almost all categories of threats available within the | |
| 17 | Unifi settings. However, I regularly identify abusive actors on my web | |
| 18 | server via the server logs. | |
| 19 | ||
| 20 | In addition, I have identified IP addresses and subnets directly within | |
| 21 | Unifi's logs that the UDM did not block for whatever reason. | |
| 22 | ||
| 23 | This guide is meant to be another step in the process to manually block | |
| 24 | abusive IP addresses or subnets that you have identified but are not | |
| 25 | being automatically blocked yet. | |
| 26 | ||
| 27 | ** Create an IP Group Profile | |
| 28 | :PROPERTIES: | |
| 29 | :CUSTOM_ID: create-an-ip-group-profile | |
| 30 | :END: | |
| 31 | To start, login to the Unifi machine's web GUI and navigate to the | |
| 32 | Network app > Settings > Profiles. | |
| 33 | ||
| 34 | Within this page, choose the =IP Groups= tab and click =Create New=. | |
| 35 | ||
| 36 | #+caption: Network Profiles | |
| 37 | [[https://img.cleberg.net/blog/20230618-unifi-ip-blocklist/unifi_profiles.png]] | |
| 38 | ||
| 39 | Each IP Group profile can be used as one of three options: | |
| 40 | ||
| 41 | 1. Port Group | |
| 42 | 2. IPv4 Address/Subnet | |
| 43 | 3. IPv6 Address/Subnet | |
| 44 | ||
| 45 | In this example, I'm creating an IPv4 Address/Subnet group and adding a | |
| 46 | few different IP addresses and a subnet. Once you've added all IP | |
| 47 | addresses and subnets, click the =Apply= button that should appear at | |
| 48 | the bottom. | |
| 49 | ||
| 50 | #+caption: Network Profile IPs | |
| 51 | [[https://img.cleberg.net/blog/20230618-unifi-ip-blocklist/abusive_ips.png]] | |
| 52 | ||
| 53 | At this point, the IPv4 Address/Subnet has been created but not yet | |
| 54 | used. | |
| 55 | ||
| 56 | ** Drop IP Group Profile via the Unifi Firewall | |
| 57 | :PROPERTIES: | |
| 58 | :CUSTOM_ID: drop-ip-group-profile-via-the-unifi-firewall | |
| 59 | :END: | |
| 60 | To instruct the Unifi machine to block the profile we just created, we | |
| 61 | need to navigate to the Network app > Settings > Firewall & Security. | |
| 62 | ||
| 63 | Within this screen, find the Firewall Rules table and click | |
| 64 | =Create Entry=. This entry should contain the following settings: | |
| 65 | ||
| 66 | - Type: =Internet In= | |
| 67 | - Description: =<Your Custom Rule>= | |
| 68 | - Rule Applied: =Before Predefined Rules= | |
| 69 | - Action: =Drop= | |
| 70 | - Source Type: =Port/IP Group= | |
| 71 | - IPv4 Address Group: =<Name of the Group Profile You Created Above>= | |
| 72 | ||
| 73 | Customize the remaining configurations to your liking, and then save and | |
| 74 | enable the firewall rule. | |
| 75 | ||
| 76 | #+caption: Firewall Rule | |
| 77 | [[https://img.cleberg.net/blog/20230618-unifi-ip-blocklist/firewall_drop_rule.png]] | |
| 78 | ||
| 79 | Once enabled, the Unifi machine will be able to drop all incoming | |
| 80 | connections from the defined IP addresses and subnets within the created | |
| 81 | profile. | |
| 82 | ||
| 83 | #+begin_quote | |
| 84 | As a personal aside to this topic, I'm looking for a convenient way to | |
| 85 | update the firewall rules or profiles remotely (within the LAN) from the | |
| 86 | web server to accelerate this process. If you have an idea on how to | |
| 87 | automatically update Unifi IP groups or firewall rules, let me know! | |
| 88 | ||
| 89 | #+end_quote | |
blog/2023-06-20-audit-review-cheatsheet.org deleted −81
| @@ -1,81 +0,0 @@ | ||
| 1 | #+title: Audit Review Checklist | |
| 2 | #+date: 2023-06-20 | |
| 3 | ||
| 4 | ||
| 5 | ** Overview | |
| 6 | :PROPERTIES: | |
| 7 | :CUSTOM_ID: overview | |
| 8 | :END: | |
| 9 | This post is a /very/ brief overview on the basic process to review | |
| 10 | audit test results, focusing on work done as part of a financial | |
| 11 | statement audit (FSA) or service organization controls (SOC) report. | |
| 12 | ||
| 13 | While there are numerous different things to review and look for - all | |
| 14 | varying wildly depending on the report, client, and tester - this list | |
| 15 | serves as a solid base foundation for a reviewer. | |
| 16 | ||
| 17 | I have used this throughout my career as a starting point to my reviews, | |
| 18 | and it has worked wonders for creating a consistent and objective | |
| 19 | template to my reviews. The goal is to keep this base high-level enough | |
| 20 | to be used on a wide variety of engagements, while still ensuring that | |
| 21 | all key areas are covered. | |
| 22 | ||
| 23 | ** Cheatsheet | |
| 24 | :PROPERTIES: | |
| 25 | :CUSTOM_ID: cheatsheet | |
| 26 | :END: | |
| 27 | 1. [ ] Check all documents for spelling and grammar. | |
| 28 | 2. [ ] Ensure all acronyms are fully explained upon first use. | |
| 29 | 3. [ ] For all people referenced, use their full names and job titles | |
| 30 | upon first use. | |
| 31 | 4. [ ] All supporting documents must cross-reference to the lead sheet | |
| 32 | and vice-versa. | |
| 33 | 5. [ ] Verify that the control has been adequately tested: | |
| 34 | - [ ] *Test of Design*: Did the tester obtain information regarding | |
| 35 | how the control should perform normally and abnormally (e.g., | |
| 36 | emergency scenarios)? | |
| 37 | - [ ] *Test of Operating Effectiveness*: Did the tester inquire, | |
| 38 | observe, inspect, or re-perform sufficient evidence to support | |
| 39 | their conclusion over the control? Inquiry alone is not adequate! | |
| 40 | 6. [ ] For any information used in the control, whether by the control | |
| 41 | operator or by the tester, did the tester appropriately document the | |
| 42 | source (system or person), extraction method, parameters, and | |
| 43 | completeness and accuracy (C&A)? | |
| 44 | - [ ] For any reports, queries, etc. used in the extraction, did the | |
| 45 | tester include a copy and notate C&A considerations? | |
| 46 | 7. [ ] Did the tester document the specific criteria that the control is | |
| 47 | being tested against? | |
| 48 | 8. [ ] Did the tester notate in the supporting documents where each | |
| 49 | criterion was satisfied? | |
| 50 | 9. [ ] If testing specific policies or procedures, are the documents | |
| 51 | adequate? | |
| 52 | - [ ] e.g., a test to validate that a review of policy XYZ occurs | |
| 53 | periodically should also evaluate the sufficiency of the policy | |
| 54 | itself, if meant to cover the risk that such a policy does not | |
| 55 | exist and is not reviewed. | |
| 56 | 10. [ ] Does the test cover the appropriate period under review? | |
| 57 | - [ ] If the test is meant to cover only a portion of the audit | |
| 58 | period, do other controls exist to mitigate the risks that exist | |
| 59 | for the remainder of the period? | |
| 60 | 11. [ ] For any computer-aided audit tools (CAATs) or other automation | |
| 61 | techniques used in the test, is the use of such tools explained and | |
| 62 | appropriately documented? | |
| 63 | 12. [ ] If prior-period documentation exists, are there any missing | |
| 64 | pieces of evidence that would further enhance the quality of the | |
| 65 | test? | |
| 66 | 13. [ ] Was any information discovered during the walkthrough or inquiry | |
| 67 | phase that was not incorporated into the test? | |
| 68 | 14. [ ] Are there new rules or expectations from your company's internal | |
| 69 | guidance or your regulatory bodies that would affect the audit | |
| 70 | approach for this control? | |
| 71 | 15. [ ] Was an exception, finding, or deficiency identified as a result | |
| 72 | of this test? | |
| 73 | - [ ] Was the control deficient in design, operation, or both? | |
| 74 | - [ ] What was the root cause of the finding? | |
| 75 | - [ ] Does the finding indicate other findings or potential fraud? | |
| 76 | - [ ] What's the severity and scope of the finding? | |
| 77 | - [ ] Do other controls exist as a form of compensation against the | |
| 78 | finding's severity, and do they mitigate the risk within the | |
| 79 | control objective? | |
| 80 | - [ ] Does the finding exist at the end of the period, or was it | |
| 81 | resolved within the audit period? | |
blog/2023-06-28-backblaze-b2.org deleted −207
| @@ -1,207 +0,0 @@ | ||
| 1 | #+title: Getting Started with Backblaze B2 Cloud Storage | |
| 2 | #+date: 2023-06-28 | |
| 3 | ||
| 4 | ** Overview | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: overview | |
| 7 | :END: | |
| 8 | Backblaze [[https://www.backblaze.com/b2/cloud-storage.html][B2 Cloud | |
| 9 | Storage]] is an inexpensive and reliable on-demand cloud storage and | |
| 10 | backup solution. | |
| 11 | ||
| 12 | The service starts at $5/TB/month ($0.005/GB/month) with a download rate | |
| 13 | of $0.01/GB/month. | |
| 14 | ||
| 15 | However, there are free tiers: | |
| 16 | ||
| 17 | - The first 10 GB of storage is free. | |
| 18 | - The first 1 GB of data downloaded each day is free. | |
| 19 | - Class A transactions are free. | |
| 20 | - The first 2500 Class B transactions each day are free. | |
| 21 | - The first 2500 Class C transactions each day are free. | |
| 22 | ||
| 23 | You can see which API calls fall into categories A, B, or C here: | |
| 24 | [[https://www.backblaze.com/b2/b2-transactions-price.html][Pricing | |
| 25 | Organized by API Calls]]. | |
| 26 | ||
| 27 | For someone like me, who wants an offsite backup of their server's | |
| 28 | =/home/= directory and various other server configs that fall under 10 | |
| 29 | GB total, Backblaze is a great solution from a financial perspective. | |
| 30 | ||
| 31 | ** Create An Account | |
| 32 | :PROPERTIES: | |
| 33 | :CUSTOM_ID: create-an-account | |
| 34 | :END: | |
| 35 | To start with Backblaze, you'll need to | |
| 36 | [[https://www.backblaze.com/b2/sign-up.html][create a free account]] - | |
| 37 | no payment method is required to sign up. | |
| 38 | ||
| 39 | Once you have an account, you can test out the service with their web | |
| 40 | GUI, their mobile app, or their CLI tool. I'm going to use the CLI tool | |
| 41 | below to test a file upload and then sync an entire directory to my | |
| 42 | Backblaze bucket. | |
| 43 | ||
| 44 | ** Create a Bucket | |
| 45 | :PROPERTIES: | |
| 46 | :CUSTOM_ID: create-a-bucket | |
| 47 | :END: | |
| 48 | Before you can start uploading, you need to create a bucket. If you're | |
| 49 | familiar with other object storage services, this will feel familiar. If | |
| 50 | not, it's pretty simple to create one. | |
| 51 | ||
| 52 | As their webpage says: | |
| 53 | ||
| 54 | #+begin_quote | |
| 55 | A bucket is a container that holds files that are uploaded into B2 Cloud | |
| 56 | Storage. The bucket name must be globally unique and must have a minimum | |
| 57 | of 6 characters. A limit of 100 buckets may be created per account. An | |
| 58 | unlimited number of files may be uploaded into a bucket. | |
| 59 | ||
| 60 | #+end_quote | |
| 61 | ||
| 62 | Once you click the =Create a Bucket= button on their webpage or mobile | |
| 63 | app, you need to provide the following: | |
| 64 | ||
| 65 | - Bucket Unique Name | |
| 66 | - Files in Bucket are: =Private= or =Public= | |
| 67 | - Default Encryption: =Disable= or =Enable= | |
| 68 | - Object Lock: =Disable= or =Enable= | |
| 69 | ||
| 70 | For my bucket, I created a private bucket with encryption enabled and | |
| 71 | object lock disabled. | |
| 72 | ||
| 73 | Once your bucket is created, you can test the upload/download feature on | |
| 74 | their web GUI or mobile app! At this point, you have a fully functional | |
| 75 | bucket and account. | |
| 76 | ||
| 77 | ** Linux CLI Tool | |
| 78 | :PROPERTIES: | |
| 79 | :CUSTOM_ID: linux-cli-tool | |
| 80 | :END: | |
| 81 | *** Installation | |
| 82 | :PROPERTIES: | |
| 83 | :CUSTOM_ID: installation | |
| 84 | :END: | |
| 85 | To install the =b2= CLI tool, you'll need to download it from the | |
| 86 | [[https://www.backblaze.com/docs/cloud-storage-command-line-tools][CLI | |
| 87 | Tools]] page. I recommend copying the URL from the link that says | |
| 88 | =Linux= and using wget to download it, as shown below. | |
| 89 | ||
| 90 | Once downloaded, make the file executable and move it to a location on | |
| 91 | your =$PATH=, so that you can execute that command from anywhere on the | |
| 92 | machine. | |
| 93 | ||
| 94 | #+begin_src sh | |
| 95 | wget <b2_cli_url> | |
| 96 | chmod +x b2_linux | |
| 97 | mv b2_linux /usr/bin/b2 | |
| 98 | #+end_src | |
| 99 | ||
| 100 | *** Log In | |
| 101 | :PROPERTIES: | |
| 102 | :CUSTOM_ID: log-in | |
| 103 | :END: | |
| 104 | The first step after installation is to log in. To do this, execute the | |
| 105 | following command and provide your =<applicationKeyId>= and | |
| 106 | =<applicationKey>=. | |
| 107 | ||
| 108 | If you don't want to provide these values in the command itself, you can | |
| 109 | simply execute the base command and it will request them in an | |
| 110 | interactive prompt. | |
| 111 | ||
| 112 | #+begin_src sh | |
| 113 | # if you want to provide the keys directly: | |
| 114 | b2 authorize-account [<applicationKeyId>] [<applicationKey>] | |
| 115 | ||
| 116 | # or, if you don't want your keys in your shell history: | |
| 117 | b2 authorize-account | |
| 118 | #+end_src | |
| 119 | ||
| 120 | *** Upload a Test File | |
| 121 | :PROPERTIES: | |
| 122 | :CUSTOM_ID: upload-a-test-file | |
| 123 | :END: | |
| 124 | In order to test the functionality of the CLI tool, I'll start by | |
| 125 | uploading a single test file to the bucket I created above. We can do | |
| 126 | this with the =upload_file= function. | |
| 127 | ||
| 128 | The command is issued as follows: | |
| 129 | ||
| 130 | #+begin_src sh | |
| 131 | b2 upload_file <bucket_name> <local_file> <remote_file> | |
| 132 | #+end_src | |
| 133 | ||
| 134 | In my situation, I executed the following command with my username. | |
| 135 | ||
| 136 | #+begin_src sh | |
| 137 | b2 upload_file my_unique_bucket /home/<user>/test.md test.md | |
| 138 | #+end_src | |
| 139 | ||
| 140 | To confirm that the file was uploaded successfully, list the files in | |
| 141 | your bucket: | |
| 142 | ||
| 143 | #+begin_src sh | |
| 144 | b2 ls <bucket_name> | |
| 145 | #+end_src | |
| 146 | ||
| 147 | #+begin_src txt | |
| 148 | test.md | |
| 149 | #+end_src | |
| 150 | ||
| 151 | *** Sync a Directory | |
| 152 | :PROPERTIES: | |
| 153 | :CUSTOM_ID: sync-a-directory | |
| 154 | :END: | |
| 155 | If you have numerous files, you can use the =sync= function to perform | |
| 156 | functionality similar to =rsync=, where you can check what's in your | |
| 157 | bucket and sync anything that is new or modified. | |
| 158 | ||
| 159 | The command is issued as follows: | |
| 160 | ||
| 161 | #+begin_src sh | |
| 162 | b2 sync <source file location> <B2 bucket destination> | |
| 163 | #+end_src | |
| 164 | ||
| 165 | In my case, I can sync my user's entire home directory to my bucket | |
| 166 | without specifying any of the files directly: | |
| 167 | ||
| 168 | #+begin_src sh | |
| 169 | b2 sync /home/<user>/ "b2://<bucketName>/home/<user>" | |
| 170 | #+end_src | |
| 171 | ||
| 172 | ** Caveats | |
| 173 | :PROPERTIES: | |
| 174 | :CUSTOM_ID: caveats | |
| 175 | :END: | |
| 176 | *** Timing of Updates to the Web GUI | |
| 177 | :PROPERTIES: | |
| 178 | :CUSTOM_ID: timing-of-updates-to-the-web-gui | |
| 179 | :END: | |
| 180 | When performing actions over a bucket, there is a slight delay in the | |
| 181 | web GUI when inspecting a bucket or its file. Note that simple actions | |
| 182 | such as uploading or deleting files may have a delay of a few minutes up | |
| 183 | to 24 hours. In my experience (<10 GB and ~20,000 files), any actions | |
| 184 | took only a few minutes to update across clients. | |
| 185 | ||
| 186 | *** Symlinks | |
| 187 | :PROPERTIES: | |
| 188 | :CUSTOM_ID: symlinks | |
| 189 | :END: | |
| 190 | Note that symlinks are resolved by b2, so if you have a link from | |
| 191 | =/home/<user>/nas-storage= that symlinks out to a =/mnt/nas-storage= | |
| 192 | folder that has 10TB of data, =b2= will resolve that link and start | |
| 193 | uploading all 10TB of data linked within the folder. | |
| 194 | ||
| 195 | If you're not sure if you have any symlinks, a symlink will look like | |
| 196 | this (note the =->= symbol): | |
| 197 | ||
| 198 | #+begin_src sh | |
| 199 | > ls -lha | |
| 200 | lrwxrwxrwx 1 root root 20 Jun 28 13:32 nas -> /mnt/nas-storage/ | |
| 201 | #+end_src | |
| 202 | ||
| 203 | You can recursively find symlink in a path with the following command: | |
| 204 | ||
| 205 | #+begin_src sh | |
| 206 | ls -lR /path/to/search | grep '^l' | |
| 207 | #+end_src | |
blog/2023-08-18-agile-auditing.org deleted −172
| @@ -1,172 +0,0 @@ | ||
| 1 | #+title: Agile Auditing: An Introduction | |
| 2 | #+date: 2023-08-18 | |
| 3 | ||
| 4 | ** What is Agile Auditing? | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: what-is-agile-auditing | |
| 7 | :END: | |
| 8 | [[https://en.wikipedia.org/wiki/Agile_software_development][Agile]], the | |
| 9 | collaborative philosophy behind many software development methods, has | |
| 10 | been picking up steam as a beneficial tool to use in the external and | |
| 11 | internal auditing world. | |
| 12 | ||
| 13 | This blog post will walk through commonly used terms within Agile, | |
| 14 | Scrum, and Kanban in order to translate these terms and roles into | |
| 15 | audit-specific terms. | |
| 16 | ||
| 17 | Whether your team is in charge of a financial statement audit, an | |
| 18 | attestation (SOC 1, SOC 2, etc.), or a unique internal audit, the terms | |
| 19 | used throughout this post should still apply. | |
| 20 | ||
| 21 | ** Agile | |
| 22 | :PROPERTIES: | |
| 23 | :CUSTOM_ID: agile | |
| 24 | :END: | |
| 25 | To start, I'll take a look at Agile. | |
| 26 | ||
| 27 | #+begin_quote | |
| 28 | The Agile methodology is a project management approach that involves | |
| 29 | breaking the project into phases and emphasizes continuous collaboration | |
| 30 | and improvement. Teams follow a cycle of planning, executing, and | |
| 31 | evaluating. | |
| 32 | ||
| 33 | #+end_quote | |
| 34 | ||
| 35 | While this approach may seem familiar to what audit teams have | |
| 36 | historically done, an audit team must make distinct changes in their | |
| 37 | mentality and how they approach and manage a project. | |
| 38 | ||
| 39 | *** Agile Values | |
| 40 | :PROPERTIES: | |
| 41 | :CUSTOM_ID: agile-values | |
| 42 | :END: | |
| 43 | The Agile Manifesto, written in 2001 at a summit in Utah, contain a set | |
| 44 | of four main values that comprise the Agile approach: | |
| 45 | ||
| 46 | 1. Individuals and interactions over processes and tools. | |
| 47 | 2. Working software over comprehensive documentation. | |
| 48 | 3. Customer collaboration over contract negotiation. | |
| 49 | 4. Responding to change over following a plan. | |
| 50 | ||
| 51 | Beyond the four values, | |
| 52 | [[https://agilemanifesto.org/principles.html][twelve principles]] were | |
| 53 | also written as part of the summit. | |
| 54 | ||
| 55 | In order to relate these values to an audit or attestation engagement, | |
| 56 | we need to shift the focus from software development to the main goal of | |
| 57 | an engagement: completing sufficient audit testing to address to | |
| 58 | relevant risks over the processes and controls at hand. | |
| 59 | ||
| 60 | Audit Examples: | |
| 61 | ||
| 62 | - Engagement teams must value the team members, client contacts, and | |
| 63 | their interactions over the historical processes and tools that have | |
| 64 | been used. | |
| 65 | - Engagement teams must value a final report that contains sufficient | |
| 66 | audit documentation over excessive documentation or scope creep. | |
| 67 | - Engagement teams must collaborate with the audit clients as much as | |
| 68 | feasible to ensure that both sides are constantly updated with current | |
| 69 | knowledge of the engagement's status and any potential findings, | |
| 70 | rather than waiting for pre-set meetings or the end of the engagement | |
| 71 | to communicate. | |
| 72 | - Engagement teams must be able to respond to change in an engagement's | |
| 73 | schedule, scope, or environment to ensure that the project is | |
| 74 | completed in a timely manner and that all relevant areas are tested. | |
| 75 | - In terms of an audit department's portfolio, they must be able to | |
| 76 | respond to changes in their company's or client's environment and be | |
| 77 | able to dynamically change their audit plan accordingly. | |
| 78 | ||
| 79 | ** Scrum | |
| 80 | :PROPERTIES: | |
| 81 | :CUSTOM_ID: scrum | |
| 82 | :END: | |
| 83 | The above section discusses the high-level details of the Agile | |
| 84 | philosophy and how an audit team can potentially mold that mindset into | |
| 85 | the audit world, but how does a team implement these ideas? | |
| 86 | ||
| 87 | There are many methods that use an Agile mindset, but I prefer | |
| 88 | [[https://en.wikipedia.org/wiki/Scrum_(software_development)][Scrum]]. | |
| 89 | Scrum is a framework based on Agile that enables a team to work through | |
| 90 | a project through a series of roles, ceremonies, artifacts, and values. | |
| 91 | ||
| 92 | Let's dive into each of these individually. | |
| 93 | ||
| 94 | *** Scrum Team | |
| 95 | :PROPERTIES: | |
| 96 | :CUSTOM_ID: scrum-team | |
| 97 | :END: | |
| 98 | A scrum project is only as good as the team running the project. | |
| 99 | Standard scrum teams are separated into three distinct areas: | |
| 100 | ||
| 101 | 1. *Product Owner (Client Contact)*: The client contact is the audit | |
| 102 | equivalent of the product owner in Scrum. They are responsible for | |
| 103 | partnering with the engagement or audit team to ensure progress is | |
| 104 | being made, priorities are established, and clear guidance is given | |
| 105 | when questions or findings arise within each sprint. | |
| 106 | 2. *Scrum Master (Engagement Lead)*: The engagement or audit team lead | |
| 107 | is responsible for coaching the team and the client contact on the | |
| 108 | scrum process, tracking team progress against plan, scheduling | |
| 109 | necessary resources, and helping remove obstacles. | |
| 110 | 3. *Scrum Developers (Engagement Members)*: The engagement or audit team | |
| 111 | is the set of team members responsible for getting the work done. | |
| 112 | These team members will work on each task, report progress, resolve | |
| 113 | obstacles, and collaborate with other team members and the client | |
| 114 | contact to ensure goals are being met. | |
| 115 | ||
| 116 | *** Scrum Ceremonies | |
| 117 | :PROPERTIES: | |
| 118 | :CUSTOM_ID: scrum-ceremonies | |
| 119 | :END: | |
| 120 | Scrum ceremonies are events that are performed on a regular basis. | |
| 121 | ||
| 122 | 1. *Sprint Planning*: The team works together to plan the upcoming | |
| 123 | sprint goal and which user stories (tasks) will be added to the | |
| 124 | sprint to achieve that goal. | |
| 125 | 2. *Sprint*: The time period, typically at least one week and no more | |
| 126 | than one month in length, where the team works on the stories and | |
| 127 | anything in the backlog. | |
| 128 | 3. *Daily Scrum*: A very short meeting held each day, typically 15 | |
| 129 | minutes, to quickly emphasize alignment on the sprint goal and plan | |
| 130 | the next 24 hours. Each team member may share what they did the day | |
| 131 | before, what they'll do today, and any obstacles to their work. | |
| 132 | 4. *Sprint Review*: At the end of each sprint, the team will gather and | |
| 133 | discuss the progress, obstacles, and backlog from the previous | |
| 134 | sprint. | |
| 135 | 5. *Sprint Retrospective*: More specific than the sprint review, the | |
| 136 | retrospective is meant to discuss what worked and what did not work | |
| 137 | during the sprint. This may be processes, tools, people, or even | |
| 138 | things related to the Scrum ceremonies. | |
| 139 | ||
| 140 | One additional ceremony that may be applicable is organizing the | |
| 141 | backlog. This is typically the responsibility of the engagement leader | |
| 142 | and is meant to prioritize and clarify what needs to be done to complete | |
| 143 | items in the backlog. | |
| 144 | ||
| 145 | *** Artifacts | |
| 146 | :PROPERTIES: | |
| 147 | :CUSTOM_ID: artifacts | |
| 148 | :END: | |
| 149 | While artifacts are generally not customizable in the audit world (i.e., | |
| 150 | each control test must include some kind of working paper with evidence | |
| 151 | supporting the test results), I wanted to include some quick notes on | |
| 152 | associating scrum artifact terms with an audit. | |
| 153 | ||
| 154 | 1. *Product Backlog*: This is the overall backlog of unfinished audit | |
| 155 | tasks from all prior sprints. | |
| 156 | 2. *Sprint Backlog*: This is the backlog of unfinished audit tasks from | |
| 157 | one individual sprint. | |
| 158 | 3. *Increment*: This is the output of each sprint - generally this is | |
| 159 | best thought of as any documentation prepared during the sprint, such | |
| 160 | as risk assessments, control working papers, deficiency analysis, | |
| 161 | etc. | |
| 162 | ||
| 163 | ** Kanban | |
| 164 | :PROPERTIES: | |
| 165 | :CUSTOM_ID: kanban | |
| 166 | :END: | |
| 167 | Last but not least, Kanban is a methodology that relies on boards to | |
| 168 | categorize work into distinct, descriptive categories that allow an | |
| 169 | agile or scrum team to effectively plan the work of a sprint or project. | |
| 170 | ||
| 171 | See Atlassian's [[https://www.atlassian.com/agile/kanban][Kanban]] page | |
| 172 | for more information. | |
blog/2023-10-04-digital-minimalism.org deleted −110
| @@ -1,110 +0,0 @@ | ||
| 1 | #+title: Digital Minimalism | |
| 2 | #+date: 2023-10-05 | |
| 3 | ||
| 4 | I've written [[/notes/minimalism][a note about minimalism]] before, but I wanted to dedicate some time | |
| 5 | to reflect on digital minimalism and how I've been able to minimize the impact | |
| 6 | of digital devices in my life. | |
| 7 | ||
| 8 | #+begin_quote | |
| 9 | These changes crept up on us and happened fast, before we had a chance to step | |
| 10 | back and ask what we really wanted out of the rapid advances of the past | |
| 11 | decade. We added new technologies to the periphery of our experience for minor | |
| 12 | reasons, then woke one morning to discover that they had colonized the core of | |
| 13 | our daily life. We didn't, in other words, sign up for the digital world in | |
| 14 | which we're currently entrenched; we seem to have stumbled backward into | |
| 15 | it. | |
| 16 | ||
| 17 | /(Digital Minimalism, 2019)/ | |
| 18 | #+end_quote | |
| 19 | ||
| 20 | ** The Principles of Digital Minimalism | |
| 21 | :PROPERTIES: | |
| 22 | :CUSTOM_ID: the-principles-of-digital-minimalism | |
| 23 | :END: | |
| 24 | As noted in Cal Newport's book, /Digital Minimalism/, there are three main | |
| 25 | principles to digital minimalism that I tend to agree with: | |
| 26 | ||
| 27 | 1. Clutter is costly. | |
| 28 | - Digital minimalists recognize that cluttering their time and attention with | |
| 29 | too many devices, apps, and services creates an overall negative cost that | |
| 30 | can swamp the small benefits that each individual item provides in | |
| 31 | isolation. | |
| 32 | 2. Optimization is important. | |
| 33 | - Digital minimalists believe that deciding a particular technology supports | |
| 34 | something they value is only the first step. To truly extract its full | |
| 35 | potential benefit, it's necessary to think carefully about how they'll use | |
| 36 | the technology. | |
| 37 | 3. Intentionality is satisfying. | |
| 38 | - Digital minimalists derive significant satisfaction from their general | |
| 39 | commitment to being more intentional about how they engage with new | |
| 40 | technologies. This source of satisfaction is independent of the specific | |
| 41 | decisions they make and is one of the biggest reasons that minimalism tends | |
| 42 | to be immensely meaningful to its practitioners. | |
| 43 | ||
| 44 | ** Taking Action | |
| 45 | :PROPERTIES: | |
| 46 | :CUSTOM_ID: taking-action | |
| 47 | :END: | |
| 48 | In order to put the logic into practice, I've created a few new habits and | |
| 49 | continued performing old habits that are working well: | |
| 50 | ||
| 51 | *** Using Devices With Intention | |
| 52 | :PROPERTIES: | |
| 53 | :CUSTOM_ID: using-devices-with-intention | |
| 54 | :END: | |
| 55 | - I already rarely use "social media", mostly limited to forums such as Hacker | |
| 56 | News and Tildes, so I've just tweaked my behavior to stop looking for content | |
| 57 | in those places when I'm bored. | |
| 58 | - Use devices with intention. Each time I pick up a digital device, there should | |
| 59 | be an intention to use the device to improve my current situation. No more | |
| 60 | endless scrolling or searching for something to interest me. | |
| 61 | ||
| 62 | *** Prevent Distractions | |
| 63 | :PROPERTIES: | |
| 64 | :CUSTOM_ID: prevent-distractions | |
| 65 | :END: | |
| 66 | - Disable (most) notifications on all devices. I spent 15-30 minutes going | |
| 67 | through the notifications on my phone, watch, and computer to ensure that only | |
| 68 | a select few apps have the ability to interrupt me: Calendar, Messages, Phone, | |
| 69 | Reminders, & Signal. | |
| 70 | - Disable badges for any apps except the ones mentioned in the bullet above. | |
| 71 | - Set-up focus profiles across devices so that I can enable different modes, | |
| 72 | such as Personal when I only want to see notifications from people I care | |
| 73 | about or Do Not Disturb, where absolutely nothing can interrupt me. | |
| 74 | - Clean up my home screens. This one was quite easy as I already maintain a | |
| 75 | minimalist set-up, but I went extreme by limiting my phone to just eight apps | |
| 76 | on the home screen and four in the dock. If I need another app, I'll have to | |
| 77 | search or use the app library. | |
| 78 | - Remove the work profile from my phone. This was a tough decision as having my | |
| 79 | work profile on my device definitely makes my life easier at times, but it | |
| 80 | also has quite a negative effect when I'm "always online" and can see the | |
| 81 | notifications and team activity 24/7. I believe creating a distinct barrier | |
| 82 | between my work and personal devices will be beneficial in the end. | |
| 83 | ||
| 84 | *** Creating Alternative Activities | |
| 85 | :PROPERTIES: | |
| 86 | :CUSTOM_ID: creating-alternative-activities | |
| 87 | :END: | |
| 88 | This is the most difficult piece, as most of my hobbies and interests lie in the | |
| 89 | digital world. However, I'm making a concerted effort to put devices down unless | |
| 90 | necessary and force myself to perform other activities in the physical world | |
| 91 | instead. | |
| 92 | ||
| 93 | I've started with a few basics that are always readily available to me: | |
| 94 | ||
| 95 | - Do a chore, such as organizing or cleaning. | |
| 96 | - Read a book, study a piece of art, etc. | |
| 97 | - Exercise or get outdoors. | |
| 98 | - Participate in a hobby, such as photography, birding, disc golf, etc. | |
| 99 | - Let yourself be bored and wander into creativity. | |
| 100 | ||
| 101 | ** Making Progress | |
| 102 | :PROPERTIES: | |
| 103 | :CUSTOM_ID: making-progress | |
| 104 | :END: | |
| 105 | I'll be taking notes as I continue down this journey and hope to see positive | |
| 106 | trends. I've always been a minimalist in the physical world and it feels | |
| 107 | refreshing to filter out the clutter that has come to dominate my digital life | |
| 108 | over the years. | |
| 109 | ||
| 110 | I'm excited to see where this journey leads. | |
blog/2023-11-08-scli.org deleted −153
| @@ -1,153 +0,0 @@ | ||
| 1 | #+title: Installing scli on Alpine Linux (musl) | |
| 2 | #+date: 2023-11-08 | |
| 3 | ||
| 4 | [[https://github.com/isamert/scli][scli]] is a command-line tool that allows you to connect to your Signal messenger | |
| 5 | account. This program utilizes a two-pane display that shows you chats on the | |
| 6 | left and the focused conversation on the right. | |
| 7 | ||
| 8 | This guide will show you how to install =scli= and its dependencies on Alpine | |
| 9 | Linux, which requires some extra work due to musl. | |
| 10 | ||
| 11 | If you're using a non-musl system, you can likely following the =scli= README | |
| 12 | and download the packaged binaries for an easier installation process. | |
| 13 | ||
| 14 | ** Dependencies | |
| 15 | :PROPERTIES: | |
| 16 | :CUSTOM_ID: dependencies | |
| 17 | :END: | |
| 18 | In order to use =scli=, you need a few dependencies: | |
| 19 | ||
| 20 | - =openjdk17-jre= - Used as a dependency for the =signal-cli= tool. | |
| 21 | Version may vary. | |
| 22 | - =signal-cli= - Used as the backbone of the =scli= tool. | |
| 23 | - =findutils= - Replaces the standard Busybox version of =xargs=. | |
| 24 | - =urwid= - A console user interface library for Python. | |
| 25 | - =urwid-readline= - For GNU emacs-like keybinds on the input line. | |
| 26 | - =qrencode= - Displays a QR code in the terminal to link the device | |
| 27 | using your phone. Not necessary if you're only linking on desktop and | |
| 28 | can copy/paste the connection URL. | |
| 29 | ||
| 30 | Let's start by installing the packages available via Alpine's | |
| 31 | repositories. Be sure to install the latest version of =openjdk=. If you | |
| 32 | run into Java-related issues, uninstall =openjdk= and install an older | |
| 33 | version. | |
| 34 | ||
| 35 | #+begin_src sh | |
| 36 | doas apk add openjdk17-jre findutils qrencode | |
| 37 | #+end_src | |
| 38 | ||
| 39 | Next, let's install =signal-cli=. Be sure to export the version of | |
| 40 | =signal-cli= that you want. I use version =0.12.4= below, but that may | |
| 41 | be outdated by the time you're reading this. | |
| 42 | ||
| 43 | #+begin_src sh | |
| 44 | export VERSION="0.12.4" | |
| 45 | wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}".tar.gz | |
| 46 | doas tar xf signal-cli-"${VERSION}".tar.gz -C /opt | |
| 47 | doas ln -sf /opt/signal-cli-${VERSION}"/bin/signal/cli /usr/local/bin | |
| 48 | #+end_src | |
| 49 | ||
| 50 | Finally, install the =urwid= packages using the Python packaging | |
| 51 | utility. | |
| 52 | ||
| 53 | #+begin_src sh | |
| 54 | pip3 install urwid urwid-readline | |
| 55 | #+end_src | |
| 56 | ||
| 57 | ** Installation | |
| 58 | :PROPERTIES: | |
| 59 | :CUSTOM_ID: installation | |
| 60 | :END: | |
| 61 | Now that we have all of the dependencies we need, we can install =scli=. | |
| 62 | Start by simply cloning the repository. | |
| 63 | ||
| 64 | #+begin_src sh | |
| 65 | git clone https://github.com/isamert/scli | |
| 66 | #+end_src | |
| 67 | ||
| 68 | When I cloned this repository on 2023-11-08, I found a bug in the logic | |
| 69 | that required a fix. You must edit the =scli= file and replace the one | |
| 70 | instance of =RLIMIT_OFILE= with =RLIMIT_NOFILE=. | |
| 71 | ||
| 72 | #+begin_src sh | |
| 73 | cd scli | |
| 74 | nano scli | |
| 75 | #+end_src | |
| 76 | ||
| 77 | Once complete, you can move this program to anywhere on your =$PATH=. I | |
| 78 | chose the following directory. | |
| 79 | ||
| 80 | #+begin_src sh | |
| 81 | doas mv scli /usr/local/bin/scli | |
| 82 | #+end_src | |
| 83 | ||
| 84 | ** Initial Setup | |
| 85 | :PROPERTIES: | |
| 86 | :CUSTOM_ID: initial-setup | |
| 87 | :END: | |
| 88 | Now that everything is installed, we can login and configure the client. | |
| 89 | Start by generating a connection link. | |
| 90 | ||
| 91 | #+begin_src sh | |
| 92 | signal-cli link -n "YOUR-DEVICE-NICKNAME" | tee >(xargs -L 1 qrencode -t utf8) | |
| 93 | #+end_src | |
| 94 | ||
| 95 | This will generate a connection link and related QR code for you to use | |
| 96 | to link the devices together. Once complete, *wait patiently* for the | |
| 97 | connection process to finish. | |
| 98 | ||
| 99 | Once it completes, it will exit and return you to the prompt. From here, | |
| 100 | you need to perform an initial =receive= command to start things off. | |
| 101 | The =USERNAME= variable should be your phone number, such as | |
| 102 | =+15551237890=. | |
| 103 | ||
| 104 | #+begin_src sh | |
| 105 | signal-cli -u USERNAME receive | |
| 106 | #+end_src | |
| 107 | ||
| 108 | Also be sure to test the daemon to ensure it works properly. If no | |
| 109 | errors occur, it's working. If you run into errors because you're not | |
| 110 | running a DBUS session, see my notes below. | |
| 111 | ||
| 112 | #+begin_src sh | |
| 113 | signal-cli -u USERNAME daemon | |
| 114 | #+end_src | |
| 115 | ||
| 116 | Once the initial reception is complete, you are ready to use =scli=. | |
| 117 | ||
| 118 | This process will differ depending on your desktop environment (DE). If | |
| 119 | you are running a DE, you likely have a DBUS session running already and | |
| 120 | can simply launch the program. | |
| 121 | ||
| 122 | However, if you're like me and running your computer straight on the TTY | |
| 123 | without a DE, you'll need to start a DBUS session for this program. | |
| 124 | ||
| 125 | #+begin_src sh | |
| 126 | # If you're not running a DBUS session yet, you need to start one for scli | |
| 127 | dbus-run-session -- scli | |
| 128 | ||
| 129 | # OR - If you're already running a DBUS session, simply run scli | |
| 130 | scli | |
| 131 | #+end_src | |
| 132 | ||
| 133 | ** Configuration | |
| 134 | :PROPERTIES: | |
| 135 | :CUSTOM_ID: configuration | |
| 136 | :END: | |
| 137 | Lastly, there are a number of configuration options that you can pass | |
| 138 | via the command or in the =~/.config/sclirc= file. See the Github README | |
| 139 | for more information on configuration options. | |
| 140 | ||
| 141 | #+begin_src sh | |
| 142 | nano ~/.config/sclirc | |
| 143 | #+end_src | |
| 144 | ||
| 145 | #+begin_src conf | |
| 146 | # ~/.config/sclirc | |
| 147 | ||
| 148 | wrap-at = 80 | |
| 149 | enable-notifications = true | |
| 150 | #+end_src | |
| 151 | ||
| 152 | That's it! Following this guide, I have a functional =scli= program that | |
| 153 | successfully sends messages to my contacts and myself! | |
blog/index.org deleted −152
| @@ -1,152 +0,0 @@ | ||
| 1 | #+title: Blog | |
| 2 | #+OPTIONS: toc:nil | |
| 3 | ||
| 4 | Use =C-f= to search blog post titles for keywords. | |
| 5 | ||
| 6 | * 2023 | |
| 7 | :PROPERTIES: | |
| 8 | :ID: 21779F03-E65E-4A80-A958-3D08723EB8AD | |
| 9 | :PUBDATE: 2023-12-03 Sun 22:35 | |
| 10 | :END: | |
| 11 | ||
| 12 | - 2023-12-03 [[./2023-12-03-unifi-nextdns.org][How to Install NextDNS on the Unifi Dream Machine]] | |
| 13 | - 2023-11-08 [[./2023-11-08-scli.org][Installing scli on Alpine Linux (musl)]] | |
| 14 | - 2023-10-17 [[./2023-10-17-self-hosting-anonymousoverflow.org][Self-Hosting AnonymousOverflow]] | |
| 15 | - 2023-10-15 [[./2023-10-15-alpine-ssh-hardening.org][SSH Hardening for Alpine Linux]] | |
| 16 | - 2023-10-11 [[./2023-10-11-self-hosting-authelia.org][Self-Hosting Authelia]] | |
| 17 | - 2023-10-04 [[./2023-10-04-digital-minimalism.org][Digital Minimalism]] | |
| 18 | - 2023-09-19 [[./2023-09-19-audit-sql-scripts.org][Useful SQL Scripts for Auditing Logical Access]] | |
| 19 | - 2023-09-15 [[./2023-09-15-gitweb.org][Gitweb via Nginx]] | |
| 20 | - 2023-08-18 [[./2023-08-18-agile-auditing.org][Agile Auditing: An Introduction]] | |
| 21 | - 2023-07-19 [[./2023-07-19-plex-transcoder-errors.org][How to Fix Plex Error: 'Conversion failed. The transcoder failed to start up.']] | |
| 22 | - 2023-07-12 [[./2023-07-12-mullvad-wireguard-lan.org][Enabling LAN Access in Mullvad Wireguard Conf Files]] | |
| 23 | - 2023-06-30 [[./2023-06-30-self-hosting-voyager-lemmy-client.org][Self Hosting Voyager - A Lemmy Web Client]] | |
| 24 | - 2023-06-28 [[./2023-06-28-backblaze-b2.org][Getting Started with Backblaze B2 Cloud Storage]] | |
| 25 | - 2023-06-23 [[./2023-06-23-self-hosting-convos-irc.org][Self-Hosting Convos IRC Web Client]] | |
| 26 | - 2023-06-23 [[./2023-06-23-byobu.org][Byobu]] | |
| 27 | - 2023-06-20 [[./2023-06-20-audit-review-cheatsheet.org][Cheatsheet: Review Audit Test Results]] | |
| 28 | - 2023-06-18 [[./2023-06-18-unifi-ip-blocklist.org][Block IP Addresses and Subnets with Unifi Network Firewall]] | |
| 29 | - 2023-06-08 [[./2023-06-08-self-hosting-baikal-server.org][Self-Hosting Baïkal Server (CalDAV & CardDAV)]] | |
| 30 | - 2023-06-08 [[./2023-06-08-goaccess-geoip.org][Inspecting Nginx Logs with GoAccess and MaxMind GeoIP Data]] | |
| 31 | - 2023-05-22 [[./2023-05-22-burnout.org][Burnout]] | |
| 32 | - 2023-02-02 [[./2023-02-02-exploring-hare.org][Exploring the Hare Programming Language]] | |
| 33 | - 2023-01-28 [[./2023-01-28-self-hosting-wger-workout-manager.org][Self-Hosting Wger Workout Manager]] | |
| 34 | - 2023-01-23 [[./2023-01-23-random-mullvad-wireguard.org][Connecting to a Random Mullvad Wireguard Host]] | |
| 35 | - 2023-01-21 [[./2023-01-21-running-flatpak-apps-easily.org][Running Flatpak Apps with Symlinks]] | |
| 36 | - 2023-01-08 [[./2023-01-08-fedora-remove-login-manager.org][Remove the Login Manager from Fedora i3]] | |
| 37 | - 2023-01-05 [[./2023-01-05-mass-unlike-tumblr-posts.org][How to Easily Mass Unlike Tumblr Posts With Javascript]] | |
| 38 | - 2023-01-03 [[./2023-01-03-recent-website-changes.org][Recent Website Changes]] | |
| 39 | ||
| 40 | * 2022 | |
| 41 | :PROPERTIES: | |
| 42 | :ID: 3B281087-A3B6-46B7-8A29-77F74DAD5F15 | |
| 43 | :PUBDATE: 2023-12-03 Sun 22:35 | |
| 44 | :END: | |
| 45 | ||
| 46 | - 2022-12-23 [[./2022-12-23-alpine-desktop.org][Alpine Linux as a Desktop OS]] | |
| 47 | - 2022-12-17 [[./2022-12-17-st.org][Simple Terminal]] | |
| 48 | - 2022-12-07 [[./2022-12-07-nginx-wildcard-redirect.org][Redirect Nginx Subdomains & Trailing Content with Regex]] | |
| 49 | - 2022-12-01 [[./2022-12-01-nginx-compression.org][Enable GZIP Compression on Nginx]] | |
| 50 | - 2022-11-29 [[./2022-11-29-nginx-referrer-ban-list.org][Creating a Referrer Ban List on Nginx]] | |
| 51 | - 2022-11-27 [[./2022-11-27-server-build.org][Building a Custom Rack-Mount Server]] | |
| 52 | - 2022-11-11 [[./2022-11-11-nginx-tmp-errors.org][Fixing Permission Errors in /var/lib/nginx]] | |
| 53 | - 2022-11-07 [[./2022-11-07-matrix-synapse.org][Self-Hosting Matrix Synapse on Alpine Linux]] | |
| 54 | - 2022-10-30 [[./2022-10-30-linux-display-manager.org][How to Disable or Change the Display Manager on Void Linux]] | |
| 55 | - 2022-10-22 [[./2022-10-22-alpine-linux.org][Alpine Linux: My New Server OS]] | |
| 56 | - 2022-10-20 [[./2022-10-20-syncthing.org][Syncthing: A Minimal Self-Hosted Cloud Storage Solution]] | |
| 57 | - 2022-10-04 [[./2022-10-04-mtp-linux.org][How to Mount an MTP Mobile Device on Linux]] | |
| 58 | - 2022-09-21 [[./2022-09-21-graphene-os.org][Installing Graphene OS on the Pixel 6 Pro]] | |
| 59 | - 2022-09-17 [[./2022-09-17-serenity-os.org][Serenity OS: Testing Out a Unique System]] | |
| 60 | - 2022-08-31 [[./2022-08-31-privacy.com-changes.org][Concerning Changes on Privacy.com]] | |
| 61 | - 2022-07-31 [[./2022-07-31-bash-it.org][Upgrade Bash with Bash-It & Ble.sh]] | |
| 62 | - 2022-07-30 [[./2022-07-30-flac-to-opus.org][Recursive Command-Line FLAC to Opus Conversion]] | |
| 63 | - 2022-07-25 [[./2022-07-25-curseradio.org][CurseRadio: Listening to the Radio on the Command Line]] | |
| 64 | - 2022-07-14 [[./2022-07-14-gnupg.org][GNU Privacy Guard (GPG)]] | |
| 65 | - 2022-07-01 [[./2022-07-01-git-server.org][Self-Hosting a Personal Git Server]] | |
| 66 | - 2022-06-24 [[./2022-06-24-fedora-i3.org][Rebooting My Love Affair with Linux]] | |
| 67 | - 2022-06-22 [[./2022-06-22-daily-poetry.org][Daily Plaintext Poetry via Email]] | |
| 68 | - 2022-06-16 [[./2022-06-16-terminal-lifestyle.org][A Terminal Lifestyle]] | |
| 69 | - 2022-06-07 [[./2022-06-07-freshrss.org][Self-Hosting FreshRSS]] | |
| 70 | - 2022-06-04 [[./2022-06-04-njalla-dns-api.org][Dynamic DNS with Njalla API]] | |
| 71 | - 2022-06-01 [[./2022-06-01-ditching-cloudflare.org][Ditching Cloudflare for Njalla]] | |
| 72 | - 2022-04-09 [[./2022-04-09-pinetime.org][PineTime: An Open-Source SmartWatch]] | |
| 73 | - 2022-04-02 [[./2022-04-02-nginx-reverse-proxy.org][Set-Up a Reverse Proxy with Nginx]] | |
| 74 | - 2022-03-26 [[./2022-03-26-ssh-mfa.org][Enable TOTP MFA for SSH]] | |
| 75 | - 2022-03-24 [[./2022-03-24-server-hardening.org][Hardening a Public-Facing Home Server]] | |
| 76 | - 2022-03-23 [[./2022-03-23-nextcloud-on-ubuntu.org][Installing Nextcloud on Ubuntu]] | |
| 77 | - 2022-03-23 [[./2022-03-23-cloudflare-dns-api.org][Dynamic DNS with Cloudflare API]] | |
| 78 | - 2022-03-08 [[./2022-03-08-plex-migration.org][Migrating Plex to New Server (+ Nvidia Transcoding)]] | |
| 79 | - 2022-03-03 [[./2022-03-03-financial-database.org][Maintaining a Personal Financial Database]] | |
| 80 | - 2022-03-02 [[./2022-03-02-note-taking.org][Easy, Reliable Note-Taking]] | |
| 81 | - 2022-02-22 [[./2022-02-22-tuesday.org][Tuesday]] | |
| 82 | - 2022-02-20 [[./2022-02-20-nginx-caching.org][Caching Static Content with Nginx]] | |
| 83 | - 2022-02-17 [[./2022-02-17-exiftool.org][Stripping Image Metadata with exiftool]] | |
| 84 | - 2022-02-16 [[./2022-02-16-debian-and-nginx.org][Migrating to a New Web Server Setup with Debian, Nginx, and Agate]] | |
| 85 | - 2022-02-10 [[./2022-02-10-leaving-the-office.org][Leaving Office-Based Work In the Past]] | |
| 86 | ||
| 87 | * 2021 | |
| 88 | :PROPERTIES: | |
| 89 | :ID: 7603D51F-513F-4407-9CDC-F55C555D35D4 | |
| 90 | :PUBDATE: 2023-12-03 Sun 22:35 | |
| 91 | :END: | |
| 92 | ||
| 93 | - 2021-12-04 [[./2021-12-04-cisa.org][I Passed the CISA!]] | |
| 94 | - 2021-10-09 [[./2021-10-09-apache-redirect.org][Apache Redirect HTML Files to a Directory]] | |
| 95 | - 2021-08-25 [[./2021-08-25-audit-sampling.org][Audit Sampling with Python]] | |
| 96 | - 2021-07-15 [[./2021-07-15-delete-gitlab-repos.org][How to Delete All GitLab Repositories]] | |
| 97 | - 2021-05-30 [[./2021-05-30-changing-git-authors.org][Changing Git Authors]] | |
| 98 | - 2021-04-28 [[./2021-04-28-photography.org][Jumping Back Into Photography]] | |
| 99 | - 2021-04-23 [[./2021-04-23-php-comment-system.org][Roll Your Own Static Commenting System in PHP]] | |
| 100 | - 2021-04-17 [[./2021-04-17-gemini-server.org][Hosting a Gemini Server]] | |
| 101 | - 2021-03-30 [[./2021-03-30-vps-web-server.org][How to Set Up a VPS Web Server]] | |
| 102 | - 2021-03-28 [[./2021-03-28-vaporwave-vs-outrun.org][Vaporwave vs Outrun]] | |
| 103 | - 2021-03-28 [[./2021-03-28-gemini-capsule.org][Launching a Gemini Capsule]] | |
| 104 | - 2021-03-19 [[./2021-03-19-clone-github-repos.org][How to Clone All Repositories from a GitHub or Sourcehut Account]] | |
| 105 | - 2021-02-19 [[./2021-02-19-macos.org][macOS: Testing Out A New OS]] | |
| 106 | - 2021-01-07 [[./2021-01-07-ufw.org][Secure Your Network with the Uncomplicated Firewall]] | |
| 107 | - 2021-01-04 [[./2021-01-04-fediverse.org][A Simple Guide to the Fediverse]] | |
| 108 | - 2021-01-01 [[./2021-01-01-seum.org][SEUM: Speedrunners from Hell]] | |
| 109 | ||
| 110 | * 2020 | |
| 111 | :PROPERTIES: | |
| 112 | :ID: 8442803D-FB00-498D-9936-51029E80B367 | |
| 113 | :PUBDATE: 2023-12-03 Sun 22:35 | |
| 114 | :END: | |
| 115 | ||
| 116 | - 2020-12-29 [[./2020-12-29-zork.org][Zork: Let's Explore a Classic]] | |
| 117 | - 2020-12-28 [[./2020-12-28-neon-drive.org][Neon Drive: A Nostalgic 80s Arcade Racing Game]] | |
| 118 | - 2020-12-27 [[./2020-12-27-website-redesign.org][Redesigning My Website: The 5 KB Result]] | |
| 119 | - 2020-10-12 [[./2020-10-12-mediocrity.org][On the Pursuit of Mediocrity]] | |
| 120 | - 2020-09-25 [[./2020-09-25-happiness-map.org][Data Visualization: World Choropleth Map of Happiness]] | |
| 121 | - 2020-09-22 [[./2020-09-22-internal-audit.org][What is Internal Audit?]] | |
| 122 | - 2020-09-01 [[./2020-09-01-visual-recognition.org][IBM Watson Visual Recognition]] | |
| 123 | - 2020-08-29 [[./2020-08-29-php-auth-flow.org][PHP Authentication Flow]] | |
| 124 | - 2020-08-22 [[./2020-08-22-redirect-github-pages.org][Redirect GitHub Pages from Subdomain to the Top-Level Domain]] | |
| 125 | - 2020-07-26 [[./2020-07-26-business-analysis.org][Algorithmically Analyzing Local Businesses]] | |
| 126 | - 2020-07-20 [[./2020-07-20-video-game-sales.org][Data Exploration: Video Game Sales]] | |
| 127 | - 2020-05-19 [[./2020-05-19-customizing-ubuntu.org][Beginner's Guide: Customizing Ubuntu]] | |
| 128 | - 2020-05-03 [[./2020-05-03-homelab.org][An Inside Look at My Homelab]] | |
| 129 | - 2020-03-25 [[./2020-03-25-session-messenger.org][Session Private Messenger]] | |
| 130 | - 2020-02-09 [[./2020-02-09-cryptography-basics.org][Cryptography Basics]] | |
| 131 | - 2020-01-26 [[./2020-01-26-steam-on-ntfs.org][Linux Gaming Tweak: Steam on NTFS Drives]] | |
| 132 | - 2020-01-25 [[./2020-01-25-linux-software.org][Linux Software]] | |
| 133 | ||
| 134 | * 2019 | |
| 135 | :PROPERTIES: | |
| 136 | :ID: CC0E4E85-6094-48AD-B78A-68DE97E56A17 | |
| 137 | :PUBDATE: 2023-12-03 Sun 22:35 | |
| 138 | :END: | |
| 139 | ||
| 140 | - 2019-12-16 [[./2019-12-16-password-security.org][Password Security]] | |
| 141 | - 2019-12-03 [[./2019-12-03-the-ansoff-matrix.org][The Ansoff Matrix]] | |
| 142 | - 2019-09-09 [[./2019-09-09-audit-analytics.org][Data Analysis in Auditing]] | |
| 143 | - 2019-01-07 [[./2019-01-07-useful-css.org][Useful CSS Snippets]] | |
| 144 | ||
| 145 | * 2018 | |
| 146 | :PROPERTIES: | |
| 147 | :ID: 4FE32AD6-AC36-43DD-ACEF-B2D98450FB2D | |
| 148 | :PUBDATE: 2023-12-03 Sun 22:35 | |
| 149 | :END: | |
| 150 | ||
| 151 | - 2018-12-08 [[./2018-12-08-aes-encryption.org][AES Encryption]] | |
| 152 | - 2018-11-28 [[./2018-11-28-cpp-compiler.org][The C++ Compiler]] | |
blog/rss.org deleted −108
| @@ -1,108 +0,0 @@ | ||
| 1 | #+TITLE: cleberg.net | |
| 2 | ||
| 3 | - [[file:2023-12-03-unifi-nextdns.org][How to Install NextDNS on the Unifi Dream Machine]] | |
| 4 | - [[file:2021-03-28-vaporwave-vs-outrun.org][2021-03-28-vaporwave-vs-outrun]] | |
| 5 | - [[file:2023-06-23-self-hosting-convos-irc.org][2023-06-23-self-hosting-convos-irc]] | |
| 6 | - [[file:2020-07-26-business-analysis.org][Algorithmically Analyzing Local Businesses]] | |
| 7 | - [[file:2023-07-12-mullvad-wireguard-lan.org][2023-07-12-mullvad-wireguard-lan]] | |
| 8 | - [[file:2022-11-11-nginx-tmp-errors.org][2022-11-11-nginx-tmp-errors]] | |
| 9 | - [[file:2022-02-16-debian-and-nginx.org][2022-02-16-debian-and-nginx]] | |
| 10 | - [[file:2023-06-20-audit-review-cheatsheet.org][2023-06-20-audit-review-cheatsheet]] | |
| 11 | - [[file:2020-08-22-redirect-github-pages.org][Redirect GitHub Pages from Subdomain to the Top-Level Domain]] | |
| 12 | - [[file:2020-08-29-php-auth-flow.org][PHP Authentication Flow]] | |
| 13 | - [[file:2023-05-22-burnout.org][2023-05-22-burnout]] | |
| 14 | - [[file:2022-09-21-graphene-os.org][2022-09-21-graphene-os]] | |
| 15 | - [[file:2021-04-17-gemini-server.org][2021-04-17-gemini-server]] | |
| 16 | - [[file:2023-11-08-scli.org][2023-11-08-scli]] | |
| 17 | - [[file:2021-03-28-gemini-capsule.org][2021-03-28-gemini-capsule]] | |
| 18 | - [[file:2022-04-09-pinetime.org][2022-04-09-pinetime]] | |
| 19 | - [[file:2023-06-08-self-hosting-baikal-server.org][2023-06-08-self-hosting-baikal-server]] | |
| 20 | - [[file:2022-07-14-gnupg.org][2022-07-14-gnupg]] | |
| 21 | - [[file:2022-12-17-st.org][2022-12-17-st]] | |
| 22 | - [[file:2020-05-19-customizing-ubuntu.org][Beginner's Guide: Customizing Ubuntu]] | |
| 23 | - [[file:2022-03-23-cloudflare-dns-api.org][2022-03-23-cloudflare-dns-api]] | |
| 24 | - [[file:2020-09-25-happiness-map.org][Data Visualization: World Choropleth Map of Happiness]] | |
| 25 | - [[file:2022-03-24-server-hardening.org][2022-03-24-server-hardening]] | |
| 26 | - [[file:2022-02-22-tuesday.org][2022-02-22-tuesday]] | |
| 27 | - [[file:2021-07-15-delete-gitlab-repos.org][2021-07-15-delete-gitlab-repos]] | |
| 28 | - [[file:2019-12-03-the-ansoff-matrix.org][The Ansoff Matrix]] | |
| 29 | - [[file:2023-06-28-backblaze-b2.org][2023-06-28-backblaze-b2]] | |
| 30 | - [[file:2022-12-07-nginx-wildcard-redirect.org][2022-12-07-nginx-wildcard-redirect]] | |
| 31 | - [[file:2023-01-05-mass-unlike-tumblr-posts.org][2023-01-05-mass-unlike-tumblr-posts]] | |
| 32 | - [[file:2023-01-08-fedora-remove-login-manager.org][2023-01-08-fedora-remove-login-manager]] | |
| 33 | - [[file:2018-12-08-aes-encryption.org][AES Encryption]] | |
| 34 | - [[file:2020-05-03-homelab.org][An Inside Look at My Homelab]] | |
| 35 | - [[file:2023-09-15-gitweb.org][2023-09-15-gitweb]] | |
| 36 | - [[file:2022-11-29-nginx-referrer-ban-list.org][2022-11-29-nginx-referrer-ban-list]] | |
| 37 | - [[file:2022-03-26-ssh-mfa.org][2022-03-26-ssh-mfa]] | |
| 38 | - [[file:2022-12-23-alpine-desktop.org][2022-12-23-alpine-desktop]] | |
| 39 | - [[file:2020-01-25-linux-software.org][Linux Software]] | |
| 40 | - [[file:2022-02-17-exiftool.org][2022-02-17-exiftool]] | |
| 41 | - [[file:2021-12-04-cisa.org][2021-12-04-cisa]] | |
| 42 | - [[file:2020-02-09-cryptography-basics.org][Cryptography Basics]] | |
| 43 | - [[file:2022-07-30-flac-to-opus.org][2022-07-30-flac-to-opus]] | |
| 44 | - [[file:2022-06-24-fedora-i3.org][2022-06-24-fedora-i3]] | |
| 45 | - [[file:2020-07-20-video-game-sales.org][Data Exploration: Video Game Sales]] | |
| 46 | - [[file:2022-11-07-matrix-synapse.org][2022-11-07-matrix-synapse]] | |
| 47 | - [[file:2020-10-12-mediocrity.org][On the Pursuit of Mediocrity]] | |
| 48 | - [[file:2021-02-19-macos.org][2021-02-19-macos]] | |
| 49 | - [[file:2020-01-26-steam-on-ntfs.org][Linux Gaming Tweak: Steam on NTFS Drives]] | |
| 50 | - [[file:2019-01-07-useful-css.org][Useful CSS Snippets]] | |
| 51 | - [[file:2023-01-28-self-hosting-wger-workout-manager.org][2023-01-28-self-hosting-wger-workout-manager]] | |
| 52 | - [[file:2022-11-27-server-build.org][2022-11-27-server-build]] | |
| 53 | - [[file:2023-10-04-digital-minimalism.org][2023-10-04-digital-minimalism]] | |
| 54 | - [[file:2020-12-27-website-redesign.org][Redesigning My Website: The 5 KB Result]] | |
| 55 | - [[file:2023-09-19-audit-sql-scripts.org][2023-09-19-audit-sql-scripts]] | |
| 56 | - [[file:2020-12-28-neon-drive.org][Neon Drive: A Nostalgic 80s Arcade Racing Game]] | |
| 57 | - [[file:2023-02-02-exploring-hare.org][2023-02-02-exploring-hare]] | |
| 58 | - [[file:2022-09-17-serenity-os.org][2022-09-17-serenity-os]] | |
| 59 | - [[file:2021-10-09-apache-redirect.org][2021-10-09-apache-redirect]] | |
| 60 | - [[file:2022-03-08-plex-migration.org][2022-03-08-plex-migration]] | |
| 61 | - [[file:2023-01-21-running-flatpak-apps-easily.org][2023-01-21-running-flatpak-apps-easily]] | |
| 62 | - [[file:2022-06-16-terminal-lifestyle.org][2022-06-16-terminal-lifestyle]] | |
| 63 | - [[file:2022-10-22-alpine-linux.org][2022-10-22-alpine-linux]] | |
| 64 | - [[file:2019-09-09-audit-analytics.org][Data Analysis in Auditing]] | |
| 65 | - [[file:2023-08-18-agile-auditing.org][2023-08-18-agile-auditing]] | |
| 66 | - [[file:2023-01-23-random-mullvad-wireguard.org][2023-01-23-random-mullvad-wireguard]] | |
| 67 | - [[file:2022-02-20-nginx-caching.org][2022-02-20-nginx-caching]] | |
| 68 | - [[file:2022-08-31-privacy.com-changes.org][2022-08-31-privacy.com-changes]] | |
| 69 | - [[file:2022-03-02-note-taking.org][2022-03-02-note-taking]] | |
| 70 | - [[file:2021-04-28-photography.org][2021-04-28-photography]] | |
| 71 | - [[file:2020-09-22-internal-audit.org][What is Internal Audit?]] | |
| 72 | - [[file:2022-07-25-curseradio.org][2022-07-25-curseradio]] | |
| 73 | - [[file:2022-10-30-linux-display-manager.org][2022-10-30-linux-display-manager]] | |
| 74 | - [[file:2022-06-01-ditching-cloudflare.org][2022-06-01-ditching-cloudflare]] | |
| 75 | - [[file:2023-06-08-goaccess-geoip.org][2023-06-08-goaccess-geoip]] | |
| 76 | - [[file:2019-12-16-password-security.org][Password Security]] | |
| 77 | - [[file:2021-01-07-ufw.org][2021-01-07-ufw]] | |
| 78 | - [[file:2022-07-01-git-server.org][2022-07-01-git-server]] | |
| 79 | - [[file:2023-06-23-byobu.org][2023-06-23-byobu]] | |
| 80 | - [[file:2020-03-25-session-messenger.org][Session Private Messenger]] | |
| 81 | - [[file:2023-01-03-recent-website-changes.org][2023-01-03-recent-website-changes]] | |
| 82 | - [[file:2023-06-30-self-hosting-voyager-lemmy-client.org][2023-06-30-self-hosting-voyager-lemmy-client]] | |
| 83 | - [[file:2021-05-30-changing-git-authors.org][2021-05-30-changing-git-authors]] | |
| 84 | - [[file:2023-10-11-self-hosting-authelia.org][2023-10-11-self-hosting-authelia]] | |
| 85 | - [[file:2021-01-04-fediverse.org][2021-01-04-fediverse]] | |
| 86 | - [[file:2021-04-23-php-comment-system.org][2021-04-23-php-comment-system]] | |
| 87 | - [[file:2023-07-19-plex-transcoder-errors.org][2023-07-19-plex-transcoder-errors]] | |
| 88 | - [[file:2022-07-31-bash-it.org][2022-07-31-bash-it]] | |
| 89 | - [[file:2023-10-15-alpine-ssh-hardening.org][2023-10-15-alpine-ssh-hardening]] | |
| 90 | - [[file:2022-10-04-mtp-linux.org][2022-10-04-mtp-linux]] | |
| 91 | - [[file:2022-06-04-njalla-dns-api.org][2022-06-04-njalla-dns-api]] | |
| 92 | - [[file:2022-03-23-nextcloud-on-ubuntu.org][2022-03-23-nextcloud-on-ubuntu]] | |
| 93 | - [[file:2021-03-19-clone-github-repos.org][2021-03-19-clone-github-repos]] | |
| 94 | - [[file:2022-04-02-nginx-reverse-proxy.org][2022-04-02-nginx-reverse-proxy]] | |
| 95 | - [[file:2023-10-17-self-hosting-anonymousoverflow.org][2023-10-17-self-hosting-anonymousoverflow]] | |
| 96 | - [[file:2022-06-22-daily-poetry.org][2022-06-22-daily-poetry]] | |
| 97 | - [[file:2018-11-28-cpp-compiler.org][The C++ Compiler]] | |
| 98 | - [[file:2021-03-30-vps-web-server.org][2021-03-30-vps-web-server]] | |
| 99 | - [[file:2021-01-01-seum.org][2021-01-01-seum]] | |
| 100 | - [[file:2022-02-10-leaving-the-office.org][2022-02-10-leaving-the-office]] | |
| 101 | - [[file:2020-09-01-visual-recognition.org][IBM Watson Visual Recognition]] | |
| 102 | - [[file:2023-06-18-unifi-ip-blocklist.org][2023-06-18-unifi-ip-blocklist]] | |
| 103 | - [[file:2022-10-20-syncthing.org][2022-10-20-syncthing]] | |
| 104 | - [[file:2022-12-01-nginx-compression.org][2022-12-01-nginx-compression]] | |
| 105 | - [[file:2022-03-03-financial-database.org][2022-03-03-financial-database]] | |
| 106 | - [[file:2021-08-25-audit-sampling.org][2021-08-25-audit-sampling]] | |
| 107 | - [[file:2022-06-07-freshrss.org][2022-06-07-freshrss]] | |
| 108 | - [[file:2020-12-29-zork.org][Zork: Let's Explore a Classic]] | |
| \ No newline at end of file | ||
config.toml added +18
| @@ -0,0 +1,18 @@ | ||
| 1 | # Site settings | |
| 2 | base_url = "https://cleberg.net" | |
| 3 | title = "cleberg.net" | |
| 4 | description = "The personal website of Christian Cleberg." | |
| 5 | author = "Christian Cleberg" | |
| 6 | ||
| 7 | # RSS settings | |
| 8 | generate_feed = true | |
| 9 | feed_filename = "atom.xml" | |
| 10 | ||
| 11 | [markdown] | |
| 12 | highlight_code = true | |
| 13 | highlight_theme = "css" | |
| 14 | highlight_themes_css = [{ theme = "dracula", filename = "syntax-theme-dark.css" }] | |
| 15 | external_links_target_blank = true | |
| 16 | lazy_async_image = true | |
| 17 | ||
| 18 | [extra] | |
content/blog/2018-11-28-cpp-compiler.md added +138
| @@ -0,0 +1,138 @@ | ||
| 1 | +++ | |
| 2 | date = 2018-11-28 | |
| 3 | title = "The C++ Compiler" | |
| 4 | description = "Learn basics about the C++ compilation process." | |
| 5 | +++ | |
| 6 | ||
| 7 | # A Brief Introduction | |
| 8 | ||
| 9 | [C++](https://en.wikipedia.org/wiki/C%2B%2B) is a general-purpose | |
| 10 | programming language with object-oriented, generic, and functional | |
| 11 | features in addition to facilities for low-level memory manipulation. | |
| 12 | ||
| 13 | The source code, shown in the snippet below, must be compiled before it | |
| 14 | can be executed. There are many steps and intricacies to the compilation | |
| 15 | process, and this post was a personal exercise to learn and remember as | |
| 16 | much information as I can. | |
| 17 | ||
| 18 | ```cpp | |
| 19 | #include <iostream> | |
| 20 | ||
| 21 | int main() | |
| 22 | { | |
| 23 | std::cout << "Hello, world!\n"; | |
| 24 | } | |
| 25 | ``` | |
| 26 | ||
| 27 | # Compilation Process | |
| 28 | ||
| 29 | ## An Overview | |
| 30 | ||
| 31 | Compiling C++ projects is a frustrating task most days. Seemingly | |
| 32 | nonexistent errors keeping your program from successfully compiling can | |
| 33 | be annoying (especially since you know you wrote it perfectly the first | |
| 34 | time, right?). | |
| 35 | ||
| 36 | I'm learning more and more about C++ these days and decided to write | |
| 37 | this concept down so that I can cement it even further in my own head. | |
| 38 | However, C++ is not the only compiled language. Check out [the Wikipedia | |
| 39 | entry for compiled | |
| 40 | languages](https://en.wikipedia.org/wiki/Compiled_language) for more | |
| 41 | examples of compiled languages. | |
| 42 | ||
| 43 | I'll start with a wonderful, graphical way to conceptualize the C++ | |
| 44 | compiler. View [The C++ Compilation | |
| 45 | Process](https://web.archive.org/web/20190419035048/http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html) | |
| 46 | by Kurt MacMahon, an NIU professor, to see the graphic and an | |
| 47 | explanation. The goal of the compilation process is to take the C++ code | |
| 48 | and produce a shared library, dynamic library, or an executable file. | |
| 49 | ||
| 50 | ## Compilation Phases | |
| 51 | ||
| 52 | Let's break down the compilation process. There are four major steps to | |
| 53 | compiling C++ code. | |
| 54 | ||
| 55 | ### Step 1 | |
| 56 | ||
| 57 | The first step is to expand the source code file to meet all | |
| 58 | dependencies. The C++ preprocessor includes the code from all the header | |
| 59 | files, such as `#include <iostream>`. Now, what does that | |
| 60 | mean? The previous example includes the `iostream` header. | |
| 61 | This tells the computer that you want to use the `iostream` | |
| 62 | standard library, which contains classes and functions written in the | |
| 63 | core language. This specific header allows you to manipulate | |
| 64 | input/output streams. After all this, you'll end up which a temporary | |
| 65 | file that contains the expanded source code. | |
| 66 | ||
| 67 | In the example of the C++ code above, the `iostream` class | |
| 68 | would be included in the expanded code. | |
| 69 | ||
| 70 | ### Step 2 | |
| 71 | ||
| 72 | After the code is expanded, the compiler comes into play. The compiler | |
| 73 | takes the C++ code and converts this code into the assembly language, | |
| 74 | understood by the platform. You can see this in action if you head over | |
| 75 | to the [GodBolt Compiler Explorer](https://godbolt.org), which shows C++ | |
| 76 | being converted into assembly dynamically. | |
| 77 | ||
| 78 | For example, the `Hello, world!` code snippet above compiles | |
| 79 | into the following assembly code: | |
| 80 | ||
| 81 | ```asm | |
| 82 | .LC0: | |
| 83 | .string "Hello, world!\n" | |
| 84 | main: | |
| 85 | push rbp | |
| 86 | mov rbp, rsp | |
| 87 | mov esi, OFFSET FLAT:.LC0 | |
| 88 | mov edi, OFFSET FLAT:_ZSt4cout | |
| 89 | 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*) | |
| 90 | mov eax, 0 | |
| 91 | pop rbp | |
| 92 | ret | |
| 93 | __static_initialization_and_destruction_0(int, int): | |
| 94 | push rbp | |
| 95 | mov rbp, rsp | |
| 96 | sub rsp, 16 | |
| 97 | mov DWORD PTR [rbp-4], edi | |
| 98 | mov DWORD PTR [rbp-8], esi | |
| 99 | cmp DWORD PTR [rbp-4], 1 | |
| 100 | jne .L5 | |
| 101 | cmp DWORD PTR [rbp-8], 65535 | |
| 102 | jne .L5 | |
| 103 | mov edi, OFFSET FLAT:_ZStL8__ioinit | |
| 104 | call std::ios_base::Init::Init() [complete object constructor] | |
| 105 | mov edx, OFFSET FLAT:__dso_handle | |
| 106 | mov esi, OFFSET FLAT:_ZStL8__ioinit | |
| 107 | mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev | |
| 108 | call __cxa_atexit | |
| 109 | .L5: | |
| 110 | nop | |
| 111 | leave | |
| 112 | ret | |
| 113 | _GLOBAL__sub_I_main: | |
| 114 | push rbp | |
| 115 | mov rbp, rsp | |
| 116 | mov esi, 65535 | |
| 117 | mov edi, 1 | |
| 118 | call __static_initialization_and_destruction_0(int, int) | |
| 119 | pop rbp | |
| 120 | ret | |
| 121 | ``` | |
| 122 | ||
| 123 | ### Step 3 | |
| 124 | ||
| 125 | Third, the assembly code generated by the compiler is assembled into the | |
| 126 | object code for the platform. Essentially, this is when the compiler | |
| 127 | takes the assembly code and assembles it into machine code in a binary | |
| 128 | format. After researching this online, I figured out that a lot of | |
| 129 | compilers will allow you to stop compilation at this step. This would be | |
| 130 | useful for compiling each source code file separately. This saves time | |
| 131 | later if a single file changes; only that file needs to be recompiled. | |
| 132 | ||
| 133 | ### Step 4 | |
| 134 | ||
| 135 | Finally, the object code file generated by the assembler is linked | |
| 136 | together with the object code files for any library functions used to | |
| 137 | produce a shared library, dynamic library, or an executable file. It | |
| 138 | replaces all references to undefined symbols with the correct addresses. | |
content/blog/2018-12-08-aes-encryption.md added +124
| @@ -0,0 +1,124 @@ | ||
| 1 | +++ | |
| 2 | date = 2018-12-08 | |
| 3 | title = "AES Encryption" | |
| 4 | description = "An exploration of the AES encryption standard." | |
| 5 | +++ | |
| 6 | ||
| 7 | # Basic AES | |
| 8 | ||
| 9 | If you're not familiar with encryption techniques, | |
| 10 | [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) is the | |
| 11 | **Advanced Encryption Standard**. This specification was established by | |
| 12 | the National Institute of Standards and Technology, sub-selected from | |
| 13 | the Rijndael family of ciphers (128, 192, and 256 bits) in 2001. | |
| 14 | Furthering its popularity and status, the US government chose AES as | |
| 15 | their default encryption method for top-secret data, removing the | |
| 16 | previous standard which had been in place since 1977. | |
| 17 | ||
| 18 | AES has proven to be an extremely safe encryption method, with 7-round | |
| 19 | and 8-round attacks making no material improvements since the release of | |
| 20 | this encryption standard almost two decades ago. | |
| 21 | ||
| 22 | > Though many papers have been published on the cryptanalysis of AES, | |
| 23 | > the fastest single-key attacks on round-reduced AES variants [20, | |
| 24 | > 33] so far are only slightly more powerful than those proposed 10 | |
| 25 | > years ago [23,24]. | |
| 26 | > | |
| 27 | > - [Bogdonav, et | |
| 28 | > al.](http://research.microsoft.com/en-us/projects/cryptanalysis/aesbc.pdf) | |
| 29 | ||
| 30 | # How Secure is AES? | |
| 31 | ||
| 32 | In theory, AES-256 is non-crackable due to the massive number of | |
| 33 | combinations that can be produced. However, AES-128 is no longer | |
| 34 | recommended as a viable implementation to protect important data. | |
| 35 | ||
| 36 | A semi-short [comic | |
| 37 | strip](http://www.moserware.com/2009/09/stick-figure-guide-to-advanced.html) | |
| 38 | from Moserware quickly explains AES for the public to understand. | |
| 39 | Basically AES encrypts the data by obscuring the relationship between | |
| 40 | the data and the encrypted data. Additionally, this method spreads the | |
| 41 | message out. Lastly, the key produced by AES is the secret to decrypting | |
| 42 | it. Someone may know the method of AES, but without the key, they are | |
| 43 | powerless. | |
| 44 | ||
| 45 | To obscure and spread the data out, AES creates a | |
| 46 | substitution-permutation network. Wikipedia has a wonderful [example of | |
| 47 | an SP | |
| 48 | network](https://upload.wikimedia.org/wikipedia/commons/thumb/c/cd/SubstitutionPermutationNetwork2.png/468px-SubstitutionPermutationNetwork2.png) | |
| 49 | available. This network sends the data through a set of S boxes (using | |
| 50 | the unique key) to substitute the bits with another block of bits. Then, | |
| 51 | a P box will permutate, or rearrange, the bits. This is done over and | |
| 52 | over, with the key being derived from the last round. For AES, the key | |
| 53 | size specifies the number of transformation rounds: 10, 12, and 14 | |
| 54 | rounds for 128-bit, 192-bit, and 256-bit keys, respectively. | |
| 55 | ||
| 56 | # The Process | |
| 57 | ||
| 58 | 1. \*KeyExpansion=: Using [Rijndael's key | |
| 59 | schedule](https://en.m.wikipedia.org/wiki/Advanced_Encryption_Standard), | |
| 60 | the keys are dynamically generated. | |
| 61 | 2. **AddRoundKey**: Each byte of the data is combined with this key | |
| 62 | using bitwise xor. | |
| 63 | 3. **SubBytes**: This is followed by the substitution of each byte of | |
| 64 | data. | |
| 65 | 4. **ShiftRows**: Then, the final three rows are shifted a certain | |
| 66 | number of steps, dictated by the cipher. | |
| 67 | 5. **MixColumns**: After the rows have been shifted, the columns are | |
| 68 | mixed and combined. | |
| 69 | ||
| 70 | This process does not necessarily stop after one full round. Steps 2 | |
| 71 | through 5 will repeat for the number of rounds specified by the key. | |
| 72 | However, the final round excludes the MixColumns step. As you can see, | |
| 73 | this is a fairly complex process. One must have a solid understanding of | |
| 74 | general mathematic principles to fully understand how the sequence works | |
| 75 | (and to even attempt to find a weakness). | |
| 76 | ||
| 77 | According to research done by Bogdanov et al., it would take billions of | |
| 78 | years to brute force a 126-bit key with current hardware. Additionally, | |
| 79 | this brute force attack would require storing 2^88^ bits of data! | |
| 80 | However, there are a few different attacks that have been used to show | |
| 81 | vulnerabilities with the use of this technology. Side-channel attacks | |
| 82 | use inadvertent leaks of data from the hardware or software, which can | |
| 83 | allow attackers to obtain the key or run programs on a user's hardware. | |
| 84 | ||
| 85 | Please note that this is not something you should run out and try to | |
| 86 | implement in your `Hello, World!` app after only a few hours of | |
| 87 | research. While AES (basically all encryption methods) is extremely | |
| 88 | efficient in what it does, it takes a lot of time and patience to | |
| 89 | understand. If you're looking for something which currently implements | |
| 90 | AES, check out the [Legion of the Bouncy | |
| 91 | Castle](https://www.bouncycastle.org/documentation.html) for Java | |
| 92 | implementations of cryptographic algorithms. | |
| 93 | ||
| 94 | # Why Does Encryption Matter? | |
| 95 | ||
| 96 | There are limitless reasons to enable encryption at-rest or in-transit | |
| 97 | for various aspects of your digital life. You can research specific | |
| 98 | examples, such as [Australia passes new law to thwart strong | |
| 99 | encryption](https://arstechnica.com/tech-policy/2018/12/australia-passes-new-law-to-thwart-strong-encryption/). | |
| 100 | However, I will simply list a few basic reasons to always enable | |
| 101 | encryption, where feasible: | |
| 102 | ||
| 103 | 1. Privacy is a human right and is recognized as a national right in | |
| 104 | some countries (e.g., [US Fourth | |
| 105 | Amendment](https://www.law.cornell.edu/wex/fourth_amendment)). | |
| 106 | 2. "Why not?" Encryption rarely affects performance or speed, so | |
| 107 | there's usually not a reason to avoid it in the first place. | |
| 108 | 3. Your digital identity and activity (texts, emails, phone calls, | |
| 109 | online accounts, etc.) are extremely valuable and can result in | |
| 110 | terrible consequences, such as identity theft, if leaked to other | |
| 111 | parties. Encrypting this data prevents such leaks from ruining | |
| 112 | lives. | |
| 113 | 4. Wiping or factory-resetting does not actually wipe all data from the | |
| 114 | storage device. There are methods to read data from the physical | |
| 115 | disks/boards inside devices. | |
| 116 | 5. Corporations, governments, and other nefarious groups/individuals | |
| 117 | are actively looking for ways to collect personal information about | |
| 118 | anyone they can. If someone's data is unencrypted, that person may | |
| 119 | become a target due to the ease of data collection. | |
| 120 | ||
| 121 | ****Read More:**** | |
| 122 | ||
| 123 | - [Federal Information Processing Standards Publication | |
| 124 | 197](http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf) | |
content/blog/2019-01-07-useful-css.md added +199
| @@ -0,0 +1,199 @@ | ||
| 1 | +++ | |
| 2 | date = 2019-01-07 | |
| 3 | title = "Useful CSS Snippets" | |
| 4 | description = "Explore some useful CSS snippets." | |
| 5 | +++ | |
| 6 | ||
| 7 | # Introduction to CSS | |
| 8 | ||
| 9 | [CSS](https://en.wikipedia.org/wiki/CSS), the language used to markup | |
| 10 | HTML code and make it "pretty", is one of the most effective ways to | |
| 11 | increase the attractiveness of a website. It can also lead to increased | |
| 12 | user engagement, retention, and satisfaction. In fact, there are whole | |
| 13 | career fields are dedicated to the improvement of user experiences, | |
| 14 | known as UI design and UX design. | |
| 15 | ||
| 16 | Some web developers are used to the common CSS properties, such as | |
| 17 | element sizing, fonts, colors, etc., but are not as well versed in | |
| 18 | less-used properties and values such as `flexbox`, | |
| 19 | `clip-path`, and `transform`. This article will | |
| 20 | provide some insight into the less-used and unique CSS properties. | |
| 21 | ||
| 22 | # CSS Variables | |
| 23 | ||
| 24 | The first topic today is CSS variables. Variables are not often used by | |
| 25 | smaller developers. CSS variables allow you to give your website a | |
| 26 | well-defined structure, where you can easily reuse CSS properties | |
| 27 | throughout the project. | |
| 28 | ||
| 29 | You can use variables to define things, such as color palettes. Then, | |
| 30 | you can use these colors for backgrounds anywhere else in the HTML. This | |
| 31 | could be extended, where extra variables could be defined for | |
| 32 | `primary-text`, `quoted-text`, etc. Variables can | |
| 33 | also be used to define spacing (e.g. `32px` or | |
| 34 | `2rem`), which can then be applied to margins, padding, font | |
| 35 | sizes, and more. | |
| 36 | ||
| 37 | For example, here are some variables defined at the root of the website, | |
| 38 | which allows for any subsequent CSS rules to use those variables: | |
| 39 | ||
| 40 | ```css | |
| 41 | :root { | |
| 42 | --primary-color: black; | |
| 43 | --secondary-color: white; | |
| 44 | } | |
| 45 | ||
| 46 | body { | |
| 47 | background-color: var(--primary-color); | |
| 48 | color: var(--secondary-color); | |
| 49 | } | |
| 50 | ``` | |
| 51 | ||
| 52 | # CSS Box Shadows | |
| 53 | ||
| 54 | Box shadows were once my mortal enemy. No matter how hard I tried, I | |
| 55 | just couldn't get them to work how I wanted. Because of this, my | |
| 56 | favorite discovery has been CSSMatic's [box shadow | |
| 57 | generator](https://www.cssmatic.com/box-shadow). It provides an | |
| 58 | excellent tool to generate box shadows using their simple sliders. | |
| 59 | Surprisingly, this is the reason I learned how box shadows work! You can | |
| 60 | use the sliders and watch how the CSS code changes in the image that is | |
| 61 | displayed. Through this, you should understand that the basic structure | |
| 62 | for box shadows is: | |
| 63 | ||
| 64 | ```css | |
| 65 | box-shadow: inset horizontal vertical blur spread color; | |
| 66 | ``` | |
| 67 | ||
| 68 | Now, let's look at some basic examples! You can copy and paste the | |
| 69 | following code into a site like CodePen or your own HTML files. Feel | |
| 70 | free to play around with the code, experiment, and learn. | |
| 71 | ||
| 72 | ****Box Shadow #1**** | |
| 73 | ||
| 74 | ```html | |
| 75 | <div class="shadow-examples"> | |
| 76 | <div class="box effect1"> | |
| 77 | <h3>Effect 1</h3> | |
| 78 | </div> | |
| 79 | </div> | |
| 80 | ``` | |
| 81 | ||
| 82 | ```css | |
| 83 | .box h3 { | |
| 84 | text-align: center; | |
| 85 | position: relative; | |
| 86 | top: 80px; | |
| 87 | } | |
| 88 | .box { | |
| 89 | width: 70%; | |
| 90 | height: 200px; | |
| 91 | background: #fff; | |
| 92 | margin: 40px auto; | |
| 93 | } | |
| 94 | .effect1 { | |
| 95 | box-shadow: 0 10px 6px -6px #777; | |
| 96 | } | |
| 97 | ``` | |
| 98 | ||
| 99 | ****Box Shadow #2**** | |
| 100 | ||
| 101 | ```html | |
| 102 | <div class="shadow-examples"> | |
| 103 | <div class="box effect2"> | |
| 104 | <h3>Effect 2</h3> | |
| 105 | </div> | |
| 106 | </div> | |
| 107 | ``` | |
| 108 | ||
| 109 | ```css | |
| 110 | .box h3 { | |
| 111 | text-align: center; | |
| 112 | position: relative; | |
| 113 | top: 80px; | |
| 114 | } | |
| 115 | .box { | |
| 116 | width: 70%; | |
| 117 | height: 200px; | |
| 118 | background: #fff; | |
| 119 | margin: 40px auto; | |
| 120 | } | |
| 121 | .effect2 { | |
| 122 | box-shadow: 10px 10px 5px -5px rgba(0, 0, 0, 0.75); | |
| 123 | } | |
| 124 | ``` | |
| 125 | ||
| 126 | Try these box shadows out on your own and see how changing each shadow | |
| 127 | value works. | |
| 128 | ||
| 129 | # CSS Flexbox | |
| 130 | ||
| 131 | Now, let's move on to the best part of this article: flexbox. The | |
| 132 | flexbox is by far my favorite new toy. I originally stumbled across this | |
| 133 | solution after looking for more efficient ways of centering content | |
| 134 | horizontally AND vertically. I had used a few hack-ish methods before, | |
| 135 | but flexbox throws those out the window. The best part of it all is that | |
| 136 | flexbox is *dead simple*. | |
| 137 | ||
| 138 | Flexbox pertains to the parent div of any element. You want the parent | |
| 139 | to be the flexbox in which items are arranged to use the flex methods. | |
| 140 | It's easier to see this in action that explained, so let's see an | |
| 141 | example. | |
| 142 | ||
| 143 | ****Flexbox**** | |
| 144 | ||
| 145 | ```html | |
| 146 | <div class="flex-examples"> | |
| 147 | <div class="sm-box"> | |
| 148 | <h3>1</h3> | |
| 149 | </div> | |
| 150 | <div class="sm-box"> | |
| 151 | <h3>2</h3> | |
| 152 | </div> | |
| 153 | </div> | |
| 154 | ``` | |
| 155 | ||
| 156 | ```css | |
| 157 | .flex-examples { | |
| 158 | display: flex; | |
| 159 | flex-wrap: wrap; | |
| 160 | justify-content: flex-start; | |
| 161 | align-items: center; | |
| 162 | padding: 10px; | |
| 163 | background-color: #f2f2f2; | |
| 164 | } | |
| 165 | .sm-box { | |
| 166 | display: flex; | |
| 167 | justify-content: center; | |
| 168 | align-items: center; | |
| 169 | width: 20%; | |
| 170 | height: 100px; | |
| 171 | background: #fff; | |
| 172 | margin: 40px 10px; | |
| 173 | } | |
| 174 | ``` | |
| 175 | ||
| 176 | You may notice that we no longer need to use the `top` | |
| 177 | property for the `h3` elements in our code. This is because | |
| 178 | we set the display box to be a flex container for the small boxes, AND | |
| 179 | we made the small boxes flex containers for their elements (the h3 | |
| 180 | tags). Flex boxes can be nested like this to center content that is | |
| 181 | inside centered content. | |
| 182 | ||
| 183 | For the example above, we designated the `justify-content` | |
| 184 | property to be `flex-start` so that the boxes stack from the | |
| 185 | left side of the screen. This property can be changed to | |
| 186 | `center` to make the boxes appear in the center of the | |
| 187 | screen. | |
| 188 | ||
| 189 | For an interactive example, [check out this | |
| 190 | CodePen](https://codepen.io/LandonSchropp/pen/KpzzGo) from | |
| 191 | [LandonScropp](https://codepen.io/LandonSchropp/). Resize the window | |
| 192 | with dice to see how they collapse and re-align. | |
| 193 | ||
| 194 | # Even More CSS | |
| 195 | ||
| 196 | For more inspiration, you can visit [CodePen](https://www.codepen.io), | |
| 197 | [Dribbble](https://dribbble.com), or [UI | |
| 198 | Movement](https://uimovement.com) to browse the collections of many | |
| 199 | amazing web designers. | |
content/blog/2019-09-09-audit-analytics.md added +237
| @@ -0,0 +1,237 @@ | ||
| 1 | +++ | |
| 2 | date = 2019-09-09 | |
| 3 | title = "Data Analysis in Auditing" | |
| 4 | description = "Learn how to use data analysis in the world of auditing." | |
| 5 | +++ | |
| 6 | ||
| 7 | # What Are Data Analytics? | |
| 8 | ||
| 9 | A quick aside before I dive into this post: `data analytics` is a | |
| 10 | vague term that has become popular in recent years. Think of a `data | |
| 11 | analytic` as the output of any data analysis you perform. For example, | |
| 12 | a pivot table or a pie chart could be a data analytic. | |
| 13 | ||
| 14 | [Data analysis](https://en.wikipedia.org/wiki/Data_analysis) is a | |
| 15 | process that utilizes statistics and other mathematical methods to | |
| 16 | discover useful information within datasets. This involves examining, | |
| 17 | cleaning, transforming, and modeling data so that you can use the data | |
| 18 | to support an opinion, create more useful viewpoints, and gain knowledge | |
| 19 | to implement into audit planning or risk assessments. | |
| 20 | ||
| 21 | One of the common mistakes that managers (and anyone new to the process) | |
| 22 | make is assuming that everything involved with this process is "data | |
| 23 | analytics". In fact, data analytics are only a small part of the | |
| 24 | process. | |
| 25 | ||
| 26 | See **Figure 1** for a more accurate representation of where data | |
| 27 | analysis sits within the full process. This means that data analysis | |
| 28 | does not include querying or extracting data, selecting samples, or | |
| 29 | performing audit tests. These steps can be necessary for an audit (and | |
| 30 | may even be performed by the same associates), but they are not data | |
| 31 | analytics. | |
| 32 | ||
| 33 |  | |
| 35 | ||
| 36 | # Current Use of Analytics in Auditing | |
| 37 | ||
| 38 | While data analysis has been an integral part of most businesses and | |
| 39 | departments for the better part of the last century, only recently have | |
| 40 | internal audit functions been adopting this practice. The internal audit | |
| 41 | function works exclusively to provide assurance and consulting services | |
| 42 | to the business areas within the firm (except for internal auditing | |
| 43 | firms who are hired by different companies to perform their roles). | |
| 44 | ||
| 45 | > Internal Auditing helps an organization accomplish its objectives by | |
| 46 | > bringing a systematic, disciplined approach to evaluate and improve | |
| 47 | > the effectiveness of risk management, control and governance | |
| 48 | > processes. | |
| 49 | > | |
| 50 | > - The IIA's Definition of Internal Audit | |
| 51 | ||
| 52 | Part of the blame for the slow adoption of data analysis can be | |
| 53 | attributed to the fact that internal auditing is strongly based on | |
| 54 | tradition and following the precedents set by previous auditors. | |
| 55 | However, there can be no progress without auditors who are willing to | |
| 56 | break the mold and test new audit techniques. In fact, as of 2018, [only | |
| 57 | 63% of internal audit departments currently utilize data | |
| 58 | analytics](https://www.cpapracticeadvisor.com/accounting-audit/news/12404086/internal-audit-groups-are-lagging-in-data-analytics) | |
| 59 | in North America. This number should be as close as possible to 100%. I | |
| 60 | have never been part of an audit that would not have benefited from data | |
| 61 | analytics. | |
| 62 | ||
| 63 | So, how do internal audit functions remedy this situation? It's | |
| 64 | definitely not as easy as walking into work on Monday and telling your | |
| 65 | Chief Audit Executive that you're going to start implementing analytics | |
| 66 | in the next audit. You need a plan and a system to make the analysis | |
| 67 | process as effective as possible. | |
| 68 | ||
| 69 | # The DELTA Model | |
| 70 | ||
| 71 | One of the easiest ways to experiment with data analytics and gain an | |
| 72 | understanding of the processes is to implement them within your own | |
| 73 | department. But how do we do this if we've never worked with analysis | |
| 74 | before? One of the most common places to start is to research some data | |
| 75 | analysis models currently available. For this post, we'll take a look | |
| 76 | at the DELTA model. You can take a look at ****Figure 2**** for a quick | |
| 77 | overview of the model. | |
| 78 | ||
| 79 | The DELTA model sets a few guidelines for areas wanting to implement | |
| 80 | data analytics so that the results can be as comprehensive as possible: | |
| 81 | ||
| 82 | - **Data**: Must be clean, accessible, and (usually) unique. | |
| 83 | - **Enterprise-Wide Focus**: Key data systems and analytical resources | |
| 84 | must be available for use (by the Internal Audit Function). | |
| 85 | - **Leaders**: Must promote a data analytics approach and show the | |
| 86 | value of analytical results. | |
| 87 | - **Targets**: Must be set for key areas and risks that the analytics | |
| 88 | can be compared against (KPIs). | |
| 89 | - **Analysts**: There must be auditors willing and able to perform | |
| 90 | data analytics or else the system cannot be sustained. | |
| 91 | ||
| 92 |  | |
| 94 | ||
| 95 | # Finding the Proper KPIs | |
| 96 | ||
| 97 | Once the Internal Audit Function has decided that they want to start | |
| 98 | using data analytics internally and have ensured they're properly set | |
| 99 | up to do so, they need to figure out what they will be testing against. | |
| 100 | Key Performance Indicators (KPIs) are qualitative or quantitative | |
| 101 | factors that can be evaluated and assessed to determine if the | |
| 102 | department is performing well, usually compared to historical or | |
| 103 | industry benchmarks. Once KPIs have been agreed upon and set, auditors | |
| 104 | can use data analytics to assess and report on these KPIs. This allows | |
| 105 | the person performing the analytics the freedom to express opinions on | |
| 106 | the results, whereas the results are ambiguous if no KPIs exist. | |
| 107 | ||
| 108 | It should be noted that tracking KPIs in the department can help ensure | |
| 109 | you have a rigorous Quality Assurance and Improvement Program (QAIP) in | |
| 110 | accordance with some applicable standards, such as IPPF Standard 1300. | |
| 111 | ||
| 112 | ```{=org} | |
| 113 | #+BEING_QUOTE | |
| 114 | ``` | |
| 115 | The chief audit executive must develop and maintain a quality assurance | |
| 116 | and improvement program that covers all aspects of the internal audit | |
| 117 | activity. | |
| 118 | ||
| 119 | - IPPF Standard 1300 | |
| 120 | ||
| 121 | ```{=org} | |
| 122 | #+END_QUOTE | |
| 123 | ``` | |
| 124 | Additionally, IPPF Standard 2060 discusses reporting: | |
| 125 | ||
| 126 | > The chief audit executive must report periodically to senior | |
| 127 | > management and the board on the internal audit activity's purpose, | |
| 128 | > authority, responsibility, and performance relative to its plan and on | |
| 129 | > its conformance with the Code of Ethics and the Standards. Reporting | |
| 130 | > must also include significant risk and control issues, including fraud | |
| 131 | > risks, governance issues, and other matters that require the attention | |
| 132 | > of senior management and/or the board. | |
| 133 | > | |
| 134 | > - IPPF Standard 2060 | |
| 135 | ||
| 136 | The hardest part of finding KPIs is to determine which KPIs are | |
| 137 | appropriate for your department. Since every department is different and | |
| 138 | has different goals, KPIs will vary drastically between companies. To | |
| 139 | give you an idea of where to look, here are some ideas I came up with | |
| 140 | when discussing the topic with a few colleagues. | |
| 141 | ||
| 142 | - Efficiency/Budgeting: | |
| 143 | - Audit hours to staff utilization ratio (annual hours divided by | |
| 144 | total annual work hours). | |
| 145 | - Audit hours compared to the number of audits completed. | |
| 146 | - Time between audit steps or to complete the whole audit. E.g., | |
| 147 | time from fieldwork completion to audit report issuance. | |
| 148 | - Reputation: | |
| 149 | - The frequency that management has requested the services of the | |
| 150 | IAF. | |
| 151 | - Management, audit committee, or external audit satisfaction | |
| 152 | survey results. | |
| 153 | - Education, experience, certifications, tenure, and training of | |
| 154 | the auditors on staff. | |
| 155 | - Quality: | |
| 156 | - Number and frequency of audit findings. Assign monetary or | |
| 157 | numerical values, if possible. | |
| 158 | - Percentage of recommendations issued and implemented. | |
| 159 | - Planning: | |
| 160 | - Percentage or number of key risks audited per year or per audit. | |
| 161 | - Proportion of audit universe audited per year. | |
| 162 | ||
| 163 | # Data Analysis Tools | |
| 164 | ||
| 165 | Finally, to be able to analyze and report on the data analysis, auditors | |
| 166 | need to evaluate the tools at their disposal. There are many options | |
| 167 | available, but a few of the most common ones can easily get the job | |
| 168 | done. For example, almost every auditor already has access to Microsoft | |
| 169 | Excel. Excel is more powerful than most people give it credit for and | |
| 170 | can accomplish a lot of basic statistics without much work. If you | |
| 171 | don't know a lot about statistics but still want to see some of the | |
| 172 | more basic results, Excel is a great option. | |
| 173 | ||
| 174 | To perform more in-depth statistical analysis or to explore large | |
| 175 | datasets that Excel cannot handle, auditors will need to explore other | |
| 176 | options. The big three that have had a lot of success in recent years | |
| 177 | are Python, R, and ACL. ACL can be used as either a graphical tool | |
| 178 | (point and click) or as a scripting tool, where the auditor must write | |
| 179 | the scripts manually. Python and the R-language are solely scripting | |
| 180 | languages. | |
| 181 | ||
| 182 | The general trend in the data analytics environment is that if the tool | |
| 183 | allows you to do everything by clicking buttons or dragging elements, | |
| 184 | you won't be able to fully utilize the analytics you need. The most | |
| 185 | robust solutions are created by those who understand how to write the | |
| 186 | scripts manually. It should be noted that as the utility of a tool | |
| 187 | increases, it usually means that the learning curve for that tool will | |
| 188 | also be higher. It will take auditors longer to learn how to utilize | |
| 189 | Python, R, or ACL versus learning how to utilize Excel. | |
| 190 | ||
| 191 | # Visualization | |
| 192 | ||
| 193 | Once an auditor has finally found the right data, KPIs, and tools, they | |
| 194 | must report these results so that actions can be taken. Performing | |
| 195 | in-depth data analysis is only useful if the results are understood by | |
| 196 | the audiences of the data. The best way to create this understanding is | |
| 197 | to visualize the results of the data. Let's take a look at some of the | |
| 198 | best options to visualize and report the results you've found. | |
| 199 | ||
| 200 | Some of the most popular commercial tools for visualization are | |
| 201 | Microsoft PowerBI and Tableau Desktop. However, other tools exist such | |
| 202 | as JMP, Plotly, Qlikview, Alteryx, or D3. Some require commercial | |
| 203 | licenses while others are simply free to use. For corporate data, you | |
| 204 | may want to make sure that the tool does not communicate any of the data | |
| 205 | outside the company (such as cloud storage). I won't be going into | |
| 206 | depth on any of these tools since visualization is largely a subjective | |
| 207 | and creative experience, but remember to constantly explore new options | |
| 208 | as you repeat the process. | |
| 209 | ||
| 210 | Lastly, let's take a look at an example of data visualization. This | |
| 211 | example comes from a [blog post written by Kushal | |
| 212 | Chakrabarti](https://talent.works/2018/03/28/the-science-of-the-job-search-part-iii-61-of-entry-level-jobs-require-3-years-of-experience/) | |
| 213 | in 2018 about the percent of entry-level US jobs that require | |
| 214 | experience. **Figure 3** shows us an easy-to-digest picture of the data. | |
| 215 | We can quickly tell that only about 12.5% of entry-level jobs don't | |
| 216 | require experience. | |
| 217 | ||
| 218 | This is the kind of result that easily describes the data for you. | |
| 219 | However, make sure to include an explanation of what the results mean. | |
| 220 | Don't let the reader assume what the data means, especially if it | |
| 221 | relates to a complex subject. *Tell a story* about the data and why the | |
| 222 | results matter. For example, **Figure 4** shows a part of the | |
| 223 | explanation the author gives to illustrate his point. | |
| 224 | ||
| 225 |  | |
| 227 | ||
| 228 |  | |
| 230 | ||
| 231 | # Wrap-Up | |
| 232 | ||
| 233 | While this is not an all-encompassing program that you can just adopt | |
| 234 | into your department, it should be enough to get anyone started on the | |
| 235 | process of understanding and implementing data analytics. Always | |
| 236 | remember to continue learning and exploring new options as your | |
| 237 | processes grow and evolve. | |
content/blog/2019-12-03-the-ansoff-matrix.md added +205
| @@ -0,0 +1,205 @@ | ||
| 1 | +++ | |
| 2 | date = 2019-12-03 | |
| 3 | title = "The Ansoff Matrix" | |
| 4 | description = "Learn about the Ansoff Matrix, a strategic management tool." | |
| 5 | +++ | |
| 6 | ||
| 7 | # Overview | |
| 8 | ||
| 9 | As the world of business evolves, managers must approach business | |
| 10 | planning and strategy with a contemporary mindset. According to Dess, | |
| 11 | McNamara, Eisner, and Lee, managers must be willing to adapt to the | |
| 12 | modern business environment by going beyond "'incremental | |
| 13 | management', whereby they view their job as making a series of small, | |
| 14 | minor changes to improve the efficiency of the firm's operations"[^1]. | |
| 15 | ||
| 16 | One reason that strategic management is crucial is because most | |
| 17 | businesses that fail in the United States each year fail due to a lack | |
| 18 | of strategic focus or direction[^2]. The rate of failure for businesses | |
| 19 | with poor strategies shows that strategic planning and management are | |
| 20 | crucial to a business's strength and longevity, injecting the critical | |
| 21 | factors of growth and direction into a company's business plan. | |
| 22 | ||
| 23 | One of the most significant strategic planning and management frameworks | |
| 24 | that companies can use is the [Ansoff | |
| 25 | Matrix](https://en.wikipedia.org/wiki/Ansoff_matrix). While this | |
| 26 | framework has unique purposes and use-cases, it can effectively help an | |
| 27 | organization grow and compete. Specifically, the Ansoff matrix is one of | |
| 28 | the most effective frameworks for companies who want to focus on | |
| 29 | increasing sales revenue or profitability[^3]. | |
| 30 | ||
| 31 | This framework uses a two-by-two figure to show the four strategic | |
| 32 | options for companies to use in this framework: market penetration, | |
| 33 | market development, product development, and diversification (see | |
| 34 | **Figure 1**). The x-axis of the matrix focuses on the firm's markets | |
| 35 | and also determines if the firm is looking to enter new markets or | |
| 36 | innovate in its current markets. The y-axis of the matrix focuses on the | |
| 37 | firm's products and determines if the firm wants to pursue strategies | |
| 38 | around their existing products or explore new products. | |
| 39 | ||
| 40 |  | |
| 42 | ||
| 43 | # Strategic Options | |
| 44 | ||
| 45 | ## Market Penetration | |
| 46 | ||
| 47 | The most straightforward strategy in the Ansoff matrix is to focus on | |
| 48 | existing products in existing markets, also known as market | |
| 49 | penetration[^4]. Companies such as Coca-Cola have used market | |
| 50 | penetration successfully by investing a lot of money to get further | |
| 51 | value out of their current markets. Coca-Cola does this by introducing | |
| 52 | new features such as Christmas-themed bottles, personal names on the | |
| 53 | bottles, and other marketing schemes. | |
| 54 | ||
| 55 | ## Market Development | |
| 56 | ||
| 57 | Market development extends existing products into new markets in an | |
| 58 | attempt to increase the number of buyers. One interesting way that | |
| 59 | Coca-Cola used this strategy comes from the stigma that Diet Coke is a | |
| 60 | woman's drink[^5]. Coca-Cola introduced Coca-Cola Zero, which contained | |
| 61 | the same nutritional content as Diet Coke, but was packaged in a dark | |
| 62 | black can to appear more "manly"[^6]. | |
| 63 | ||
| 64 | ## Product Development | |
| 65 | ||
| 66 | Product development uses existing markets to introduce new products so | |
| 67 | that the firm can better meet customer needs[^7]. The extreme end of | |
| 68 | diversification is home to companies such as Johnson & Johnson, a | |
| 69 | healthcare company that has developed a business portfolio of more than | |
| 70 | 60,000 different products[^8]. Johnson & Johnson's dedication to | |
| 71 | continuous diversification has led them to a balance sheet rating of | |
| 72 | "AAA", industry recognition for diversification, and increases in | |
| 73 | their investor dividends for 57 consecutive years[^9]. | |
| 74 | ||
| 75 | ## Related Diversification | |
| 76 | ||
| 77 | Diversification, the final strategy of the Ansoff Matrix, is more | |
| 78 | difficult than the others since it involves exploring both new markets | |
| 79 | and new products. Related diversification is a diversification strategy | |
| 80 | that closely relates to the firm's core business. Coca-Cola's best | |
| 81 | example of related diversification is its acquisition of Glaceau and | |
| 82 | Vitamin Water, which expanded their drinking lines of business[^10]. | |
| 83 | ||
| 84 | ## Unrelated Diversification | |
| 85 | ||
| 86 | Unrelated diversification is a diversification strategy that does not | |
| 87 | really relate to the firm's core business but still diversifies their | |
| 88 | business portfolio. A good example of this would be a coffee company who | |
| 89 | has decided to enter the market for bicycle sales. The main purpose of | |
| 90 | this strategy is to an extremely diverse company that will not go | |
| 91 | bankrupt if one market goes through difficult times. However, this | |
| 92 | requires a lot of independent skills and heavy investments since the | |
| 93 | company most likely cannot easily transfer knowledge between the markets | |
| 94 | they compete in. | |
| 95 | ||
| 96 | # Requirements for Success | |
| 97 | ||
| 98 | To use the Ansoff Matrix framework, managers need to formulate corporate | |
| 99 | goals and objectives. Without goals and direction, management frameworks | |
| 100 | do not present much practical utility. Further, the Ansoff Matrix | |
| 101 | requires the managers involved to make tactical decisions and create a | |
| 102 | path for the company to take toward their goals. Lastly, both the Ansoff | |
| 103 | Matrix needs to consider both internal and external perspectives | |
| 104 | throughout the strategy formulation process. | |
| 105 | ||
| 106 | One interesting probability is that companies will be using multiple | |
| 107 | strategic planning and management frameworks at the same time. While | |
| 108 | this may sound like it could crowd the management process, there are | |
| 109 | numerous reasons to do so. For example, the Ansoff Matrix and the | |
| 110 | Balanced Scorecard are relatively popular, and they cover entirely | |
| 111 | different parts of a company's strategy. Using the results from the | |
| 112 | Balanced Scorecard could inform a company of the potential product and | |
| 113 | market demands, such as from customer or supplier survey results, to | |
| 114 | help the company determine which Ansoff Matrix strategy to pursue. | |
| 115 | However, a combined approach at this level would require mature | |
| 116 | frameworks and focused managers who are able to strategize at a high | |
| 117 | level. | |
| 118 | ||
| 119 | Lastly, it should be noted that the author of the Ansoff matrix, Igor | |
| 120 | Ansoff, often used the term [paralysis by | |
| 121 | analysis](https://en.wikipedia.org/wiki/Analysis_paralysis) to explain | |
| 122 | the mistake of companies who overuse analysis and spend too much time | |
| 123 | planning. Companies need to understand the utility of a strategic | |
| 124 | management framework while ensuring that the company is poised to | |
| 125 | execute as efficiently as they have planned. | |
| 126 | ||
| 127 | # Footnotes | |
| 128 | ||
| 129 | [^1]: | |
| 130 | ```example | |
| 131 | Dess, G. G., McNamara, G., Eisner, A. B., Lee, S. H. (2019). Strategic | |
| 132 | ``` | |
| 133 | ||
| 134 | management: Text & cases, ninth edition. New York, NY: McGraw-Hill | |
| 135 | Education. | |
| 136 | ||
| 137 | [^2]: | |
| 138 | ```example | |
| 139 | Juneja, P. (n.d.). Benefits of strategic management. Management Study | |
| 140 | ``` | |
| 141 | ||
| 142 | Guide. Retrieved from | |
| 143 | <https://www.managementstudyguide.com/strategic-management-benefits.htm>. | |
| 144 | ||
| 145 | [^3]: | |
| 146 | ```example | |
| 147 | Meldrum M., McDonald M. (1995) The Ansoff matrix. In: Key Marketing | |
| 148 | ``` | |
| 149 | ||
| 150 | Concepts. London: Palgrave. | |
| 151 | ||
| 152 | [^4]: | |
| 153 | ```example | |
| 154 | Meldrum M., McDonald M. (1995) The Ansoff matrix. In: Key Marketing | |
| 155 | ``` | |
| 156 | ||
| 157 | Concepts. London: Palgrave. | |
| 158 | ||
| 159 | [^5]: | |
| 160 | ```example | |
| 161 | Oakley, T. (2015). Coca-Cola: The Ansoff matrix. The Marketing Agenda. | |
| 162 | ``` | |
| 163 | ||
| 164 | Retrieved from | |
| 165 | <https://themarketingagenda.com/2015/03/28/coca-cola-ansoff-matrix/>. | |
| 166 | ||
| 167 | [^6]: | |
| 168 | ```example | |
| 169 | Oakley, T. (2015). Coca-Cola: The Ansoff matrix. The Marketing Agenda. | |
| 170 | ``` | |
| 171 | ||
| 172 | Retrieved from | |
| 173 | <https://themarketingagenda.com/2015/03/28/coca-cola-ansoff-matrix/>. | |
| 174 | ||
| 175 | [^7]: | |
| 176 | ```example | |
| 177 | Oakley, T. (2015). Coca-Cola: The Ansoff matrix. The Marketing Agenda. | |
| 178 | ``` | |
| 179 | ||
| 180 | Retrieved from | |
| 181 | <https://themarketingagenda.com/2015/03/28/coca-cola-ansoff-matrix/>. | |
| 182 | ||
| 183 | [^8]: | |
| 184 | ```example | |
| 185 | Lemke, T. (2019). The most diversified companies in the stock market. The | |
| 186 | ``` | |
| 187 | ||
| 188 | balance. Retrieved from | |
| 189 | <https://www.thebalance.com/the-most-diversified-companies-in-the-stock-market-4169730>. | |
| 190 | ||
| 191 | [^9]: | |
| 192 | ```example | |
| 193 | Johnson & Johnson. (2018). 2018 Investor Fact Sheet. [PDF file]. Retrieved | |
| 194 | ``` | |
| 195 | ||
| 196 | from | |
| 197 | [http://www.investor.jnj.com/_document/2018-investor-fact-sheet-4-19'id=0000016a-5681-d475-a17f-d78db54a0000](http://www.investor.jnj.com/\_document/2018-investor-fact-sheet-4-19'id=0000016a-5681-d475-a17f-d78db54a0000). | |
| 198 | ||
| 199 | [^10]: | |
| 200 | ```example | |
| 201 | Oakley, T. (2015). Coca-Cola: The Ansoff matrix. The Marketing Agenda. | |
| 202 | ``` | |
| 203 | ||
| 204 | Retrieved from | |
| 205 | <https://themarketingagenda.com/2015/03/28/coca-cola-ansoff-matrix/>. | |
content/blog/2019-12-16-password-security.md added +126
| @@ -0,0 +1,126 @@ | ||
| 1 | +++ | |
| 2 | date = 2019-12-16 | |
| 3 | title = "Password Security" | |
| 4 | description = "Password security basics." | |
| 5 | +++ | |
| 6 | ||
| 7 | # Users | |
| 8 | ||
| 9 | ## Why Does It Matter? | |
| 10 | ||
| 11 | Information security, including passwords and identities, has become one | |
| 12 | of the most important digital highlights of the last decade. With | |
| 13 | [billions of people affected by data breaches each | |
| 14 | year](https://www.usatoday.com/story/money/2018/12/28/data-breaches-2018-billions-hit-growing-number-cyberattacks/2413411002/), | |
| 15 | there's a greater need to introduce strong information security | |
| 16 | systems. If you think you've been part of a breach, or you want to | |
| 17 | check and see, you can use [Have I Been | |
| 18 | Pwned](https://haveibeenpwned.com/) to see if your email has been | |
| 19 | involved in any public breaches. Remember that there's a possibility | |
| 20 | that a company experienced a breach and did not report it to anyone. | |
| 21 | ||
| 22 | ## How Do I Protect Myself? | |
| 23 | ||
| 24 | The first place to start with any personal security check-up is to | |
| 25 | gather a list of all the different websites, apps, or programs that | |
| 26 | require you to have login credentials. Optionally, once you know where | |
| 27 | your information is being stored, you can sort the list from the | |
| 28 | most-important items such as banks or government logins to less | |
| 29 | important items such as your favorite meme site. You will want to ensure | |
| 30 | that your critical logins are secure before getting to the others. | |
| 31 | ||
| 32 | Once you think you have a good idea of all your different authentication | |
| 33 | methods, I recommend using a password manager such as | |
| 34 | [Bitwarden](https://bitwarden.com/). Using a password manager allows you | |
| 35 | to automatically save your logins, create randomized passwords, and | |
| 36 | transfer passwords across devices. However, you'll need to memorize | |
| 37 | your "vault password" that allows you to open the password manager. | |
| 38 | It's important to make this something hard to guess since it would | |
| 39 | allow anyone who has it to access every password you've stored in | |
| 40 | there. | |
| 41 | ||
| 42 | Personally, I recommend using a | |
| 43 | [passphrase](https://en.wikipedia.org/wiki/Passphrase) instead of a | |
| 44 | [password](https://en.wikipedia.org/wiki/Password) for your vault | |
| 45 | password. Instead of using a string of characters (whether random or | |
| 46 | simple), use a phrase and add in symbols and a number. For example, your | |
| 47 | vault password could be `Racing-Alphabet-Gourd-Parrot3`. Swap | |
| 48 | the symbols out for whichever symbol you want, move the number around, | |
| 49 | and fine-tune the passphrase until you are confident that you can | |
| 50 | remember it whenever necessary. | |
| 51 | ||
| 52 | Once you've stored your passwords, make sure you continually check up | |
| 53 | on your account and make sure you aren't following bad password | |
| 54 | practices. Krebs on Security has a great [blog post on password | |
| 55 | recommendations](https://krebsonsecurity.com/password-dos-and-donts/). | |
| 56 | Any time that a data breach happens, make sure you check to see if you | |
| 57 | were included, and if you need to reset any account passwords. | |
| 58 | ||
| 59 | # Developers | |
| 60 | ||
| 61 | ## What Are the Basic Requirements? | |
| 62 | ||
| 63 | When developing any password-protected application, there are a few | |
| 64 | basic rules that anyone should follow even if they do not follow any | |
| 65 | official guidelines such as NIST. The foremost practice is to require | |
| 66 | users to use passwords that are at least 8 characters and cannot easily | |
| 67 | be guessed. This sounds extremely simple, but it requires quite a few | |
| 68 | different strategies. First, the application should check the potential | |
| 69 | passwords against a dictionary of insecure passwords such | |
| 70 | `password`, `1234abc`, or | |
| 71 | `application_name`. | |
| 72 | ||
| 73 | Next, the application should offer guidance on the strength of passwords | |
| 74 | being entered during enrollment. Further, NIST officially recommends | |
| 75 | **not** implementing any composition rules that make passwords hard to | |
| 76 | remember (e.g. passwords with letters, numbers, and special characters) | |
| 77 | and instead encouraging the use of long pass phrases which can include | |
| 78 | spaces. It should be noted that to be able to keep spaces within | |
| 79 | passwords, all unicode characters should be supported, and passwords | |
| 80 | should not be truncated. | |
| 81 | ||
| 82 | ## What Does NIST Recommend? | |
| 83 | ||
| 84 | The National Institute of Standards and Technology | |
| 85 | ([NIST](https://www.nist.gov)) in the US Department of Commerce | |
| 86 | regularly publishes information around information security and digital | |
| 87 | identity guidelines. Recently, NIST published [Special Publication | |
| 88 | 800-63b](https://pages.nist.gov/800-63-3/sp800-63b.html): Digital | |
| 89 | Identity Guidelines and Authentication and Lifecycle Management. | |
| 90 | ||
| 91 | > A Memorized Secret authenticator - commonly referred to as a password | |
| 92 | > or, if numeric, a PIN - is a secret value intended to be chosen and | |
| 93 | > memorized by the user. Memorized secrets need to be of sufficient | |
| 94 | > complexity and secrecy that it would be impractical for an attacker to | |
| 95 | > guess or otherwise discover the correct secret value. A memorized | |
| 96 | > secret is something you know. | |
| 97 | > | |
| 98 | > - NIST Special Publication 800-63B | |
| 99 | ||
| 100 | NIST offers a lot of guidance on passwords, but I'm going to highlight | |
| 101 | just a few of the important factors: | |
| 102 | ||
| 103 | - Require passwords to be a minimum of 8 characters (6 characters if | |
| 104 | randomly generated and be generated using an approved random bit | |
| 105 | generator). | |
| 106 | - Compare potential passwords against a list that contains values | |
| 107 | known to be commonly-used, expected, or compromised. | |
| 108 | - Offer guidance on password strength, such as a strength meter. | |
| 109 | - Implement a rate-limiting mechanism to limit the number of failed | |
| 110 | authentication attempts for each user account. | |
| 111 | - Do not require composition rules for passwords and do not require | |
| 112 | passwords to be changed periodically (unless compromised). | |
| 113 | - Allow pasting of user identification and passwords to facilitate the | |
| 114 | use of password managers. | |
| 115 | - Allow users to view the password as it is being entered. | |
| 116 | - Use secure forms of communication and storage, including salting and | |
| 117 | hashing passwords using a one-way key derivation function. | |
| 118 | ||
| 119 | NIST offers further guidance on other devices that require specific | |
| 120 | security policies, querying for passwords, and more. All the information | |
| 121 | discussed so far comes from [NIST | |
| 122 | SP800-63b](https://pages.nist.gov/800-63-3/sp800-63b.html) but NIST | |
| 123 | offers a lot of information on digital identities, enrollment, identity | |
| 124 | proofing, authentication, lifecycle management, federation, and | |
| 125 | assertions in the total [NIST SP800-63 Digital Identity | |
| 126 | Guidelines](https://pages.nist.gov/800-63-3/). | |
content/blog/2020-01-25-linux-software.md added +275
| @@ -0,0 +1,275 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-01-25 | |
| 3 | title = "Linux Software" | |
| 4 | description = "A look at some useful Linux applications." | |
| 5 | +++ | |
| 6 | ||
| 7 | # GUI Applications | |
| 8 | ||
| 9 | ## Etcher | |
| 10 | ||
| 11 |  | |
| 12 | ||
| 13 | [Etcher](https://www.balena.io/etcher/) is a quick and easy way to burn | |
| 14 | ISO images to CDs and USB devices. There are two different ways you can | |
| 15 | install this program. First, you can navigate to the [official | |
| 16 | website](https://www.balena.io/etcher/) and download the AppImage file, | |
| 17 | which can run without installation. | |
| 18 | ||
| 19 | However, AppImage files are not executable by default, so you'll either | |
| 20 | need to right-click to open the properties of the file and click the | |
| 21 | "Allow executing file as program" box in the Permissions tab or use | |
| 22 | the following command: | |
| 23 | ||
| 24 | ```sh | |
| 25 | chmod u+x FILE_NAME | |
| 26 | ``` | |
| 27 | ||
| 28 | If you don't like AppImage files or just prefer repositories, you can | |
| 29 | use the following commands to add the author's repository and install | |
| 30 | it through the command-line only. | |
| 31 | ||
| 32 | First, you'll have to echo the repo and write it to a list file: | |
| 33 | ||
| 34 | ```sh | |
| 35 | echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list | |
| 36 | ``` | |
| 37 | ||
| 38 | Next, add the application keys to Ubuntu's keyring: | |
| 39 | ||
| 40 | ```sh | |
| 41 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 379CE192D401AB61 | |
| 42 | ``` | |
| 43 | ||
| 44 | Finally, update the repositories and install the app. | |
| 45 | ||
| 46 | ```sh | |
| 47 | sudo apt update && sudo apt install balena-etcher-electron | |
| 48 | ``` | |
| 49 | ||
| 50 | Using Arch, Manjaro, or another distro using the AUR? Use this command | |
| 51 | instead: | |
| 52 | ||
| 53 | ```sh | |
| 54 | sudo pacman -S etcher | |
| 55 | ``` | |
| 56 | ||
| 57 | ## Atom | |
| 58 | ||
| 59 |  | |
| 60 | ||
| 61 | [Atom](https://atom.io) is the self-proclaimed "hackable text editor | |
| 62 | for the 21st century". This text editor is made by GitHub, [now owned | |
| 63 | by | |
| 64 | Microsoft](https://news.microsoft.com/2018/06/04/microsoft-to-acquire-github-for-7-5-billion/), | |
| 65 | and has some of the best add-ons available to customize the layout and | |
| 66 | abilities of the app. | |
| 67 | ||
| 68 | First, add the Atom repository to your sources. | |
| 69 | ||
| 70 | ```sh | |
| 71 | sudo add-apt-repository ppa:webupd8team/atom | |
| 72 | ``` | |
| 73 | ||
| 74 | Next, update your package listings and install atom. | |
| 75 | ||
| 76 | ```sh | |
| 77 | sudo apt update && sudo apt install atom | |
| 78 | ``` | |
| 79 | ||
| 80 | If you have issues updating your packages with the Atom repository, | |
| 81 | you'll need to use the snap package described below instead of the | |
| 82 | repository. To remove the repository we just added, use this command: | |
| 83 | ||
| 84 | ```sh | |
| 85 | sudo add-apt-repository -r ppa:webupd8team/atom | |
| 86 | ``` | |
| 87 | ||
| 88 | You can also install Atom as a snap package, but it must be installed | |
| 89 | with the `--classic` flag. A [full explanation is | |
| 90 | available](https://language-bash.com/blog/how-to-snap-introducing-classic-confinement) | |
| 91 | if you'd like to read more about why you need the classic flag. | |
| 92 | ||
| 93 | ```sh | |
| 94 | snap install atom --classic | |
| 95 | ``` | |
| 96 | ||
| 97 | Using Arch, Manjaro, or another distro using the AUR? Use this command | |
| 98 | instead: | |
| 99 | ||
| 100 | ```sh | |
| 101 | sudo pacman -S atom | |
| 102 | ``` | |
| 103 | ||
| 104 | --- | |
| 105 | ||
| 106 | ## Visual Studio Code | |
| 107 | ||
| 108 |  | |
| 109 | ||
| 110 | [Visual Studio Code](https://code.visualstudio.com) is yet another | |
| 111 | fantastic choice for programming on Linux, especially if you need those | |
| 112 | extra add-ons to spice up your late-night coding sessions. The theme | |
| 113 | used in the screenshot is | |
| 114 | [Mars](https://marketplace.visualstudio.com/items?itemName=EliverLara.mars) | |
| 115 | by theme creator [Eliver Lara](https://github.com/EliverLara), who makes | |
| 116 | a ton of great themes for VS Code, Atom, and various Linux desktop | |
| 117 | environments. | |
| 118 | ||
| 119 | To install VS Code, you'll need to download the `.deb` file from the | |
| 120 | official website. Once you've downloaded the file, either double-click | |
| 121 | it to install through the Software Center or run the following command: | |
| 122 | ||
| 123 | ```sh | |
| 124 | sudo dpkg -i FILE_NAME.deb | |
| 125 | ``` | |
| 126 | ||
| 127 | You can also install VS Code as a snap package, but it must be installed | |
| 128 | with the `--classic` flag. A [full explanation is | |
| 129 | available](https://language-bash.com/blog/how-to-snap-introducing-classic-confinement) | |
| 130 | if you'd like to read more about why you need the classic flag. | |
| 131 | ||
| 132 | ```sh | |
| 133 | snap install code --classic | |
| 134 | ``` | |
| 135 | ||
| 136 | Using Arch, Manjaro, or another distro using the AUR? Use these commands | |
| 137 | instead: | |
| 138 | ||
| 139 | ```sh | |
| 140 | sudo pacman -S yay binutils make gcc pkg-config fakeroot yay -S visual-studio-code-bin | |
| 141 | ``` | |
| 142 | ||
| 143 | ## GNOME Tweaks | |
| 144 | ||
| 145 |  | |
| 146 | ||
| 147 | [Gnome Tweaks](https://gitlab.gnome.org/GNOME/gnome-tweaks) is the | |
| 148 | ultimate tool to use if you want to customize your GNOME desktop | |
| 149 | environment. This is how you can switch application themes (GTK), shell | |
| 150 | themes, icons, fonts, and more. To install GNOME Tweaks on Ubuntu, you | |
| 151 | just need to install the official package. | |
| 152 | ||
| 153 | ```sh | |
| 154 | sudo apt install gnome-tweaks | |
| 155 | ``` | |
| 156 | ||
| 157 | If you've installed Manjaro or Arch with Gnome, you should have the | |
| 158 | tweak tool pre-installed. If you're on Fedora, this tool is available | |
| 159 | as an official package: | |
| 160 | ||
| 161 | ```sh | |
| 162 | sudo dnf install gnome-tweaks | |
| 163 | ``` | |
| 164 | ||
| 165 | ## Steam | |
| 166 | ||
| 167 |  | |
| 168 | ||
| 169 | [Steam](https://steampowered.com) is one of the most popular gaming | |
| 170 | libraries for computers and is one of the main reasons that many people | |
| 171 | have been able to switch to Linux in recent years, thanks to Steam | |
| 172 | Proton, which makes it easier to play games not officially created for | |
| 173 | Linux platforms. | |
| 174 | ||
| 175 | To install Steam on Ubuntu, you just need to install the official | |
| 176 | package. | |
| 177 | ||
| 178 | ```sh | |
| 179 | sudo apt install steam-installer | |
| 180 | ``` | |
| 181 | ||
| 182 | For Arch-based systems, you'll simply need to install the | |
| 183 | `steam` package. However, this requires that you enable the | |
| 184 | `multilib` source. To do so, use the following command: | |
| 185 | ||
| 186 | ```sh | |
| 187 | sudo nano /etc/pacman.conf | |
| 188 | ``` | |
| 189 | ||
| 190 | Now, scroll down and uncomment the `multilib` section. | |
| 191 | ||
| 192 | ```config | |
| 193 | # Before: | |
| 194 | #[multilib] | |
| 195 | #Include = /etc/pacman.d/mirrorlist | |
| 196 | ||
| 197 | # After: | |
| 198 | [multilib] | |
| 199 | Include = /etc/pacman.d/mirrorlist | |
| 200 | ``` | |
| 201 | ||
| 202 | Finally, install the program: | |
| 203 | ||
| 204 | ```sh | |
| 205 | sudo pacman -S steam | |
| 206 | ``` | |
| 207 | ||
| 208 | [Problem Launching Steam Games? Click | |
| 209 | Here.](./2020-01-26-steam-on-ntfs-drives.html) | |
| 210 | ||
| 211 | # Command-Line Packages | |
| 212 | ||
| 213 | ## neofetch | |
| 214 | ||
| 215 |  | |
| 216 | ||
| 217 | [Neofetch](https://github.com/dylanaraps/neofetch) is a customizable | |
| 218 | tool used in the command-line to show system information. This is | |
| 219 | exceptionally useful if you want to see your system's information | |
| 220 | quickly without the clutter of some resource-heavy GUI apps. | |
| 221 | ||
| 222 | This is an official package if you're running Ubuntu 17.04 or later, so | |
| 223 | simply use the following command: | |
| 224 | ||
| 225 | ```sh | |
| 226 | sudo apt install neofetch | |
| 227 | ``` | |
| 228 | ||
| 229 | If you're running Ubuntu 16.10 or earlier, you'll have to use a series | |
| 230 | of commands: | |
| 231 | ||
| 232 | ```sh | |
| 233 | sudo add-apt-repository ppa:dawidd0811/neofetch; sudo apt update; sudo apt install neofetch | |
| 234 | ``` | |
| 235 | ||
| 236 | Using Arch, Manjaro, or another distro using the AUR? Use this command | |
| 237 | instead: | |
| 238 | ||
| 239 | ```sh | |
| 240 | sudo pacman -S neofetch | |
| 241 | ``` | |
| 242 | ||
| 243 | ## yt-dlp | |
| 244 | ||
| 245 |  | |
| 246 | ||
| 247 | [yt-dlp](https://github.com/yt-dlp/yt-dlp) is an extremely handy | |
| 248 | command-line tool that allows you to download video or audio files from | |
| 249 | various websites, such as YouTube. There are a ton of different options | |
| 250 | when running this package, so be sure to run `yt-dlp --help` first to | |
| 251 | look through everything you can do (or give up and search for the best | |
| 252 | config online). | |
| 253 | ||
| 254 | While this shouldn't be a problem for most users, yt-dlp requires | |
| 255 | Python 2.6, 2.7, or 3.2+ to work correctly, so install Python if you | |
| 256 | don't have it already. You can check to see if you have Python | |
| 257 | installed by running: | |
| 258 | ||
| 259 | ```sh | |
| 260 | python -V | |
| 261 | ``` | |
| 262 | ||
| 263 | To get the youtube-dl package, simply curl the URL and output the | |
| 264 | results. | |
| 265 | ||
| 266 | ```sh | |
| 267 | sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp | |
| 268 | ``` | |
| 269 | ||
| 270 | Finally, make the file executable so that it can be run from the | |
| 271 | command-line. | |
| 272 | ||
| 273 | ```sh | |
| 274 | sudo chmod a+rx /usr/local/bin/yt-dlp | |
| 275 | ``` | |
content/blog/2020-01-26-steam-on-ntfs.md added +95
| @@ -0,0 +1,95 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-01-26 | |
| 3 | title = "Linux Gaming Tweak: Steam on NTFS Drives" | |
| 4 | description = "Learn how to fix Steam NTFS issues on Linux." | |
| 5 | +++ | |
| 6 | ||
| 7 | # Auto-Mount Steam Drives | |
| 8 | ||
| 9 |  | |
| 10 | ||
| 11 | If you want to see how to install Steam on Linux, see my other post: | |
| 12 | [Linux Software](/blog/linux-software/). | |
| 13 | ||
| 14 | Are you having trouble launching games, even though they've installed | |
| 15 | correctly? This may happen if you're storing your games on an | |
| 16 | NTFS-formatted drive. This shouldn't be an issue if you're storing | |
| 17 | your games on the same drive that Steam is on, but some gamers prefer to | |
| 18 | put Steam on their main drive and game files on another SSD or HDD. | |
| 19 | ||
| 20 | To fix this problem, you'll need to try a few things. First, you'll | |
| 21 | need to install the `ntfs-3g` package, which is meant for | |
| 22 | better interoperability with Linux. | |
| 23 | ||
| 24 | ```sh | |
| 25 | sudo apt install ntfs-3g | |
| 26 | ``` | |
| 27 | ||
| 28 | Next, you should set up the `/etc/fstab` file to | |
| 29 | automatically mount your drives on boot. To automatically mount your | |
| 30 | drives when the computer boots up, you'll have to create the folders | |
| 31 | you want to mount your drive to first. I store mine in the | |
| 32 | `/mnt` folder using names that I'll recognize, but you can | |
| 33 | create your folders wherever you want. | |
| 34 | ||
| 35 | ```sh | |
| 36 | mkdir /path/to/folder | |
| 37 | ``` | |
| 38 | ||
| 39 | For example: | |
| 40 | ||
| 41 | ```sh | |
| 42 | mkdir /mnt/steam_library | |
| 43 | ``` | |
| 44 | ||
| 45 | To automatically mount drives upon system boot, you will need to collect | |
| 46 | a few items. The UUID is the identification number connected to | |
| 47 | whichever drive you're using to store Steam games. | |
| 48 | ||
| 49 | Drives are usually labeled similar to `/dev/nvme0n1p1` or | |
| 50 | `/dev/sda1`, so you'll need to find the line in the output | |
| 51 | of the command below that correlates to your drive and copy the UUID | |
| 52 | over to the `/etc/fstab` file. | |
| 53 | ||
| 54 | ```sh | |
| 55 | sudo blkid | grep UUID= | |
| 56 | ``` | |
| 57 | ||
| 58 | Next, you'll need your `uid` and `gid`. To find | |
| 59 | these, run the following command: | |
| 60 | ||
| 61 | ```sh | |
| 62 | id -u && id -g | |
| 63 | ``` | |
| 64 | ||
| 65 | Now that you have collected the necessary information, open the | |
| 66 | `/etc/fstab` file: | |
| 67 | ||
| 68 | ```sh | |
| 69 | sudo nano /etc/fstab | |
| 70 | ``` | |
| 71 | ||
| 72 | Each drive you want to mount on boot should have its own line in the | |
| 73 | `/etc/fstab` file that looks similar to this: | |
| 74 | ||
| 75 | ```config | |
| 76 | UUID=B64E53824E5339F7 /mnt/steam_library ntfs-3g uid=1000,gid=1000 0 0 | |
| 77 | ``` | |
| 78 | ||
| 79 | Now all you need to do is unmount your drive and re-mount it. You can | |
| 80 | unmount the drive by doing this (be sure to use the correct drive name | |
| 81 | here): | |
| 82 | ||
| 83 | ```sh | |
| 84 | sudo umount /dev/sdxX | |
| 85 | ``` | |
| 86 | ||
| 87 | You can re-mount all your drives by executing the following: | |
| 88 | ||
| 89 | ```sh | |
| 90 | sudo mount -a | |
| 91 | ``` | |
| 92 | ||
| 93 | If you don't know what your drive name is, or you're nervous about | |
| 94 | unmounting and re-mounting, simply reboot your computer, and it will be | |
| 95 | done for you automatically. | |
content/blog/2020-02-09-cryptography-basics.md added +178
| @@ -0,0 +1,178 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-02-09 | |
| 3 | title = "Cryptography Basics" | |
| 4 | description = "Learn about the basics of cryptography." | |
| 5 | +++ | |
| 6 | ||
| 7 | # Similar Article Available | |
| 8 | ||
| 9 | If you haven't already, feel free to read my post on [AES | |
| 10 | Encryption](/blog/aes-encryption/). | |
| 11 | ||
| 12 | # What is Cryptography? | |
| 13 | ||
| 14 | In layman's terms, cryptography is a process that can change data from | |
| 15 | a readable format into an unreadable format (and vice-versa) through a | |
| 16 | series of processes and secrets. More technically, this is the Internet | |
| 17 | Security Glossary's definition: | |
| 18 | ||
| 19 | > [Cryptography is] the mathematical science that deals with | |
| 20 | > transforming data to render its meaning unintelligible (i.e., to hide | |
| 21 | > its semantic content), prevent its undetected alteration, or prevent | |
| 22 | > its unauthorized use. If the transformation is reversible, | |
| 23 | > cryptography also deals with restoring encrypted data to an | |
| 24 | > intelligible form. | |
| 25 | > | |
| 26 | > - [Internet Security Glossary | |
| 27 | > (2000)](https://tools.ietf.org/html/rfc2828) | |
| 28 | ||
| 29 | Cryptography cannot offer protection against the loss of data; it simply | |
| 30 | offers encryption methods to protect data at-rest and data in-traffic. | |
| 31 | At a high-level, encrypted is when plaintext data is encrypted to | |
| 32 | ciphertext (a secure form of text that cannot be understood unless | |
| 33 | decrypted back to plaintext). The encryption process is completed | |
| 34 | through the use of a mathematical function that utilizes one or more | |
| 35 | values called keys to encrypt or decrypt the data. | |
| 36 | ||
| 37 | # Key Elements of Cryptographic Systems | |
| 38 | ||
| 39 | To create or evaluate a cryptographic system, you need to know the | |
| 40 | essential pieces to the system: | |
| 41 | ||
| 42 | - **Encryption Algorithm (Primitive):** A mathematical process that | |
| 43 | encrypts and decrypts data. | |
| 44 | - **Encryption Key:** A string of bits used within the encryption | |
| 45 | algorithm as the secret that allows successful encryption or | |
| 46 | decryption of data. | |
| 47 | - **Key Length (Size):** The maximum number of bits within the | |
| 48 | encryption key. It's important to remember that key size is | |
| 49 | regulated in many countries. | |
| 50 | - **Message Digest:** A smaller, fixed-size bit string version of the | |
| 51 | original message. This is practically infeasible to reverse, which | |
| 52 | is why it's commonly used to verify integrity. | |
| 53 | ||
| 54 | # Symmetric Systems (Secret Key Cryptography) | |
| 55 | ||
| 56 | Symmetric cryptography utilizes a secret, bidirectional key to perform | |
| 57 | both encryption and decryption of the data. The most common | |
| 58 | implementation of symmetric cryptography is the Advanced Encryption | |
| 59 | Standard, which uses keys that are 128 bits to 256 bits in size. This | |
| 60 | standard came after the National Institute of Standards and Technology | |
| 61 | (NIST) decided to retire the Data Encryption Standard (DES) in 2001. | |
| 62 | ||
| 63 | Since brute force attacks strongly correlate with key length, the 56-bit | |
| 64 | key length of DES was considered insecure after it was publicly broken | |
| 65 | in under 24 hours. However, there is a modern implementation of DES | |
| 66 | called Triple DES where the DES method is applied three times to each | |
| 67 | data block. | |
| 68 | ||
| 69 | The main advantages to symmetric systems are the ease of use, since only | |
| 70 | one key is required for both encryption and decryption, and the | |
| 71 | simplicity of the algorithms. This helps with bulk data encryption that | |
| 72 | may unnecessarily waste time and power using asymmetric systems. | |
| 73 | ||
| 74 | However, symmetric systems have disadvantages to keep in mind. Since the | |
| 75 | key is private, it can be difficult to safely distribute keys to | |
| 76 | communication partners. Additionally, the key cannot be used to sign | |
| 77 | messages since it's necessary to keep the key private. | |
| 78 | ||
| 79 | # Asymmetric Systems (Public Key Cryptography) | |
| 80 | ||
| 81 | Asymmetric cryptography utilizes two keys within the system: a secret | |
| 82 | key that is privately-held and a public key that can be distributed | |
| 83 | freely. The interesting aspect of asymmetric cryptography is that either | |
| 84 | key can be used to encrypt the data, there's no rule that dictates | |
| 85 | which key must be used for encryption. Once one key is used to encrypt | |
| 86 | the data, only the other key can be used to decrypt the data. This means | |
| 87 | that if the private key encrypts the data, only the public key can | |
| 88 | decrypt the data. | |
| 89 | ||
| 90 | An advantage of this system is that if you successfully decrypt data | |
| 91 | using one of the keys, you can be sure of the sender since only the | |
| 92 | other key could have encrypted the data. | |
| 93 | ||
| 94 | One of the major implementations of an asymmetric system is a digital | |
| 95 | signature. A digital signature can be generated using the sender's | |
| 96 | private key, or a one-way hash function and is used to provide assurance | |
| 97 | for the integrity and authenticity of the message. A couple common | |
| 98 | message digest algorithms are SHA-256 and SHA-512, which securely | |
| 99 | compress data and produce a 128-bit message digest. | |
| 100 | ||
| 101 | It should be noted that man-in-the-middle attacks are one of the risks | |
| 102 | with digital signatures and public keys. To combat this, applications | |
| 103 | often use a public key infrastructure (PKI) to independently | |
| 104 | authenticate the validity of signatures and keys. | |
| 105 | ||
| 106 | Due to the large key size and [inefficient mathematical | |
| 107 | functions](https://crypto.stackexchange.com/a/591) of asymmetric | |
| 108 | encryption, elliptical curve cryptography (ECC) is often used to | |
| 109 | increase security while using fewer resources. | |
| 110 | ||
| 111 | # Applications of Cryptographic Systems | |
| 112 | ||
| 113 | There are quite a few implementations of cryptographic systems around | |
| 114 | the world. Here are a few popular examples: | |
| 115 | ||
| 116 | **Transport Layer Security (TLS):** One of the most famous cryptographic | |
| 117 | solutions created is TLS, a session-layered or connection-layered | |
| 118 | internet protocol that allows for secure communications between browsers | |
| 119 | and servers. Using handshakes, peer negotiation, and authentication | |
| 120 | allows TLS to prevent eavesdropping and malicious transformation of | |
| 121 | data. The major reason for TLS popularity is that a major vulnerability | |
| 122 | was found in the SSL protocol in 2014. Instead of SSL, TLS can be used | |
| 123 | with HTTP to form HTTPS and is the preferred method for modern web | |
| 124 | development due to its increased security. | |
| 125 | ||
| 126 | **Secure Hypertext Transfer Protocol (HTTPS):** An application layer | |
| 127 | protocol that allows for secure transport of data between servers and | |
| 128 | web clients. One of the unique parts of HTTPS is that it uses a secured | |
| 129 | port number instead of the default web port address. | |
| 130 | ||
| 131 | **Virtual Private Network (VPN):** VPNs are made to securely extend a | |
| 132 | private network across public networks by utilizing an encrypted layered | |
| 133 | tunneling protocol paired with an authentication method, such as | |
| 134 | usernames and passwords. This technology originally allowed remote | |
| 135 | employees to access their company's data but have evolved into one of | |
| 136 | the top choices for anyone who wishes to mask their sensitive personal | |
| 137 | data. | |
| 138 | ||
| 139 | **Internet Protocol Security (IPSec):** This protocol suite facilitates | |
| 140 | communication between two or more hosts or subnets by authenticating and | |
| 141 | encrypting the data packets. IPSec is used in a lot of VPNs to establish | |
| 142 | the VPN connection through the transport and tunnel mode encryption | |
| 143 | methods. IPSec encrypts just the data portion of packets in the | |
| 144 | transport methods, but it encrypts both the data and headers in the | |
| 145 | tunnel method (introducing an additional header for authentication). | |
| 146 | ||
| 147 | **Secure Shell (SSH):** SSH is another network protocol used to protect | |
| 148 | network services by authenticating users through a secure channel. This | |
| 149 | protocol is often used for command-line (shell) functions such as remote | |
| 150 | shell commands, logins, and file transfers. | |
| 151 | ||
| 152 | **Kerberos:** Developed by MIT, Kerberos is a computer-network | |
| 153 | authentication protocol that works on the basis of tickets to allow | |
| 154 | nodes communicating over a non-secure network to prove their identity to | |
| 155 | one another securely. This is most commonly used in business | |
| 156 | environments when used as the authentication and encryption method for | |
| 157 | Windows Active Directory (AD). | |
| 158 | ||
| 159 | # Cybersecurity Controls | |
| 160 | ||
| 161 | If you're someone who needs solutions on how to control risks | |
| 162 | associated with utilizing a crytograhpic system, start with a few basic | |
| 163 | controls: | |
| 164 | ||
| 165 | - **Policies:** A policy on the use of cryptographic controls for | |
| 166 | protection of information is implemented and is in accordance with | |
| 167 | organizational objectives. | |
| 168 | - **Key management:** A policy on the use, protection and lifetime of | |
| 169 | cryptographic keys is implemented through the entire application | |
| 170 | lifecycle. | |
| 171 | - **Key size:** The organization has researched the optimal key size | |
| 172 | for their purposes, considering national laws, required processing | |
| 173 | power, and longevity of the solution. | |
| 174 | - **Algorithm selection:** Implemented algorithms are sufficiently | |
| 175 | appropriate for the business of the organization, robust, and align | |
| 176 | with recommended guidelines. | |
| 177 | - **Protocol configuration:** Protocols have been reviewed and | |
| 178 | configured suitable to the purpose of the business. | |
content/blog/2020-03-25-session-messenger.md added +136
| @@ -0,0 +1,136 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-03-25 | |
| 3 | title = "Session Private Messenger" | |
| 4 | description = "Exploring the Session Private Messenger application." | |
| 5 | +++ | |
| 6 | ||
| 7 | # Privacy Warning | |
| 8 | ||
| 9 | The company behind Session (Loki Foundation) is from Australia. If you | |
| 10 | didn't know, Australia has introduced | |
| 11 | [legislation](https://parlinfo.aph.gov.au/parlInfo/download/legislation/bills/r6195_aspassed/toc_pdf/18204b01.pdf) | |
| 12 | mandating companies comply with government requests to build backdoor | |
| 13 | access into applications. For more information, read my article on [AES | |
| 14 | Encryption](./2020-01-25-aes-encryption.html). | |
| 15 | ||
| 16 | # About Session | |
| 17 | ||
| 18 | [Session](https://getsession.org) is a private, cross-platform messaging | |
| 19 | app from the [Loki Foundation](https://loki.foundation). As someone who | |
| 20 | has spent years looking for quality alternatives to major messaging | |
| 21 | apps, I was excited when I first heard about Session. Reading through | |
| 22 | [Session's white paper](https://arxiv.org/pdf/2002.04609.pdf), you can | |
| 23 | learn the technologies behind the Session app. Part of the security of | |
| 24 | Session comes from the Signal protocol, which was forked as the origin | |
| 25 | of Session. | |
| 26 | ||
| 27 | > Session is an end-to-end encrypted messenger that removes sensitive | |
| 28 | > metadata collection, and is designed for people who want privacy and | |
| 29 | > freedom from any forms of surveillance. | |
| 30 | ||
| 31 | In general, this app promises security through end-to-end encryption, | |
| 32 | decentralized onion routing, and private identities. The biggest change | |
| 33 | that the Loki Foundation has made to the Signal protocol is removing the | |
| 34 | need for a phone number. Instead, a random identification string is | |
| 35 | generated for any session you create. This means you can create a new | |
| 36 | session for each device if you want to, or link new devices with your | |
| 37 | ID. | |
| 38 | ||
| 39 | Since Session's website and white paper describe the details of | |
| 40 | Session's security, I'm going to focus on using the app in this post. | |
| 41 | ||
| 42 | # Features | |
| 43 | ||
| 44 | Since most people are looking for an alternative to a popular chat app, | |
| 45 | I am going to list out the features that Session has so that you are | |
| 46 | able to determine if the app would suit your needs: | |
| 47 | ||
| 48 | - Multiple device linking (via QR code or ID) | |
| 49 | - App locking via device screen lock, password, or fingerprint | |
| 50 | - Screenshot blocking | |
| 51 | - Incognito keyboard | |
| 52 | - Read receipts and typing indicators | |
| 53 | - Mobile notification customization | |
| 54 | - Old message deletion and conversation limit | |
| 55 | - Backups | |
| 56 | - Recovery phrase | |
| 57 | - Account deletion, including ID, messages, sessions, and contacts | |
| 58 | ||
| 59 | # Downloads | |
| 60 | ||
| 61 | I have tested this app on Ubuntu 19.10, Android 10, macOS Monterey, and | |
| 62 | iOS 15. All apps have worked well without many issues. | |
| 63 | ||
| 64 | Below is a brief overview of the Session app on Linux. To get this app, | |
| 65 | you'll need to go to the [Downloads](https://getsession.org/download/) | |
| 66 | page and click to link to the operating system you're using. | |
| 67 | ||
| 68 | For Linux, it will download an AppImage that you'll need to enable with | |
| 69 | the following command: | |
| 70 | ||
| 71 | ```sh | |
| 72 | sudo chmod u+x session-messenger-desktop-linux-x86_64-1.0.5.AppImage | |
| 73 | ``` | |
| 74 | ||
| 75 |  | |
| 77 | ||
| 78 | # Creating an Account | |
| 79 | ||
| 80 | Once you've installed the app, simply run the app and create your | |
| 81 | unique Session ID. It will look something like this: | |
| 82 | `05af1835afdd63c947b47705867501d6373f486aa1ae05b1f2f3fcd24570eba608`. | |
| 83 | ||
| 84 | You'll need to set a display name and, optionally, a password. If you | |
| 85 | set a password, you will need to enter it every time you open the app. | |
| 86 | ||
| 87 |  | |
| 89 | ||
| 90 |  | |
| 92 | ||
| 93 |  | |
| 95 | ||
| 96 | # Start Messaging | |
| 97 | ||
| 98 | Once you've created your account and set up your profile details, the | |
| 99 | next step is to start messaging other people. To do so, you'll need to | |
| 100 | share your Session ID with other people. From this point, it's fairly | |
| 101 | straightforward and acts like any other messaging app, so I won't dive | |
| 102 | into much detail here. | |
| 103 | ||
| 104 | ## macOS | |
| 105 | ||
| 106 |  | |
| 108 | ||
| 109 | One key feature to note is that the desktop application now provides a | |
| 110 | helpful pop-up box explaining the process that Session uses to hide your | |
| 111 | IP address: | |
| 112 | ||
| 113 |  | |
| 115 | ||
| 116 | ## iOS | |
| 117 | ||
| 118 | The mobile app is quite simple and effective, giving you all the | |
| 119 | standard mobile messaging options you'd expect. | |
| 120 | ||
| 121 |  | |
| 123 | ||
| 124 | # Potential Issues | |
| 125 | ||
| 126 | I've discovered one annoying issue that would prevent from using this | |
| 127 | app regularly. On a mobile device, there have been issues with receiving | |
| 128 | messages on time. Even with battery optimization disabled and no network | |
| 129 | restrictions, Session notifications sometimes do not display until I | |
| 130 | open the app or the conversation itself and wait a few moments. This is | |
| 131 | actually one of the reasons I stopped using Signal (this seems fixed as | |
| 132 | of my updates in 2021/2022, so I wouldn't worry about this issue | |
| 133 | anymore). | |
| 134 | ||
| 135 | Looking for another messenger instead of Session? I recommend Signal, | |
| 136 | Matrix, and IRC. | |
content/blog/2020-05-03-homelab.md added +165
| @@ -0,0 +1,165 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-05-03 | |
| 3 | title = "An Inside Look at My Homelab" | |
| 4 | description = "A retrospective on the first iteration of my home lab." | |
| 5 | +++ | |
| 6 | ||
| 7 | # What is a Homelab? | |
| 8 | ||
| 9 | Starting as a developer, I have largely stayed away from hardware-based | |
| 10 | hobbies (other than building a gaming desktop). However, as the | |
| 11 | quarantine for COVID-19 stretches out further and further, I found | |
| 12 | myself bored and in search of new hobbies. After spending the last few | |
| 13 | months browsing the [r/homelab](https://www.reddit.com/r/homelab/) | |
| 14 | subreddit, I decided it was time to jump in and try things out for | |
| 15 | myself. | |
| 16 | ||
| 17 | Since I am a beginner and just recently graduated from college, | |
| 18 | everything I've done so far in my homelab is fairly low-budget. | |
| 19 | ||
| 20 | # Hardware | |
| 21 | ||
| 22 |  | |
| 24 | ||
| 25 | ## Raspberry Pi 4 | |
| 26 | ||
| 27 | Luckily, I had actually purchased a [Raspberry Pi | |
| 28 | 4](https://www.raspberrypi.org/products/raspberry-pi-4-model-b/) before | |
| 29 | the quarantine started so that I could try to keep Plex Media Center | |
| 30 | running, even while my desktop computer was turned off. I started here, | |
| 31 | using the Pi to hold Plex and Pi-hole until I grew tired with the slow | |
| 32 | performance. | |
| 33 | ||
| 34 | Here are the specifications for the Pi 4: | |
| 35 | ||
| 36 | - Broadcom BCM2711, Quad core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz | |
| 37 | - 4GB LPDDR4-3200 SDRAM | |
| 38 | - Gigabit Ethernet | |
| 39 | - H.265 (4kp60 decode), H264 (1080p60 decode, 1080p30 encode) | |
| 40 | - 64 GB MicroSD Card | |
| 41 | ||
| 42 | ## Dell Optiplex 5040 | |
| 43 | ||
| 44 | Since I wasn't happy with the Pi as my main server, I turned to | |
| 45 | Craigslist. I know a lot of other homelabbers use Ebay, but I can't | |
| 46 | seem to ever trust it enough to purchase items on there. So I used | |
| 47 | Craigslist and found a Dell Optiplex 5040 desktop computer on sale for | |
| 48 | \$90. While this computer might be underpowered, it was one of the few | |
| 49 | computers under \$100 that was available during quarantine. | |
| 50 | ||
| 51 | Here are the specifications for the Dell Optiplex 5040: | |
| 52 | ||
| 53 | - Intel Core i3 6100 | |
| 54 | - 8GB RAM DDR3 | |
| 55 | - Intel HD Graphics | |
| 56 | - Gigabit Ethernet | |
| 57 | - 500GB Hard Drive | |
| 58 | ||
| 59 | While this hardware would be awful for a work computer or a gaming rig, | |
| 60 | it turned out to be wonderful for my server purposes. The only | |
| 61 | limitation I have found so far is the CPU. The i3-6100 only has enough | |
| 62 | power for a single 4k video transcode at a time. I haven't tested more | |
| 63 | than three 1080p streams at a time, but the maximum amount of streams | |
| 64 | I've ever actually used is two. | |
| 65 | ||
| 66 | ## WD easystore 10TB & 8TB | |
| 67 | ||
| 68 | Application storage and temporary files are stored on the internal hard | |
| 69 | drive of the server, but all media files (movies, tv, games, books, etc) | |
| 70 | are stored externally on my WD easystore hard drive. Creating auto-boot | |
| 71 | configurations in the `/etc/fstab` file on my server allows | |
| 72 | the hard drives to automatically mount whenever I need to restart my | |
| 73 | server. | |
| 74 | ||
| 75 | > Update: In March 2022, I shucked the hard drives out of their external | |
| 76 | > cases, put some Kapton tape on the third power pin to prevent power | |
| 77 | > shutdowns, and stuck them inside my server tower using internal SATA | |
| 78 | > cables. | |
| 79 | ||
| 80 | ## Netgear Unmanaged Switch | |
| 81 | ||
| 82 | To manage all the ethernet cords used by my homelab, my desktop, and my | |
| 83 | living room media center, I purchased an 8-port gigabit ethernet switch | |
| 84 | for \$50 at my local computer store. This is probably much more than I | |
| 85 | should have spent on an unmanaged switch, but I am comfortable with the | |
| 86 | choice. | |
| 87 | ||
| 88 | ## TP-Link Managed Switch | |
| 89 | ||
| 90 | Since I use the unmanaged switch to group all living room devices | |
| 91 | together, I use the managed switch to configure VLANs and secure my | |
| 92 | network. | |
| 93 | ||
| 94 | ## Arris TM1602A Modem & Sagecom Fast 5280 Router | |
| 95 | ||
| 96 | My default modem and router, provided by my ISP, are fairly standard. | |
| 97 | The Arris modem supports DOCSIS 3.0, which is something that I | |
| 98 | definitely wanted as a minimum. The Sagecom router is also standard, no | |
| 99 | fancy bells or whistles. However, it does support DHCP and DHCPv6, which | |
| 100 | is something you can use to route all household traffic through a | |
| 101 | pi-hole or firewall. | |
| 102 | ||
| 103 | ## TP-Link EAP | |
| 104 | ||
| 105 | In order to gain better control over the network, I use my own wireless | |
| 106 | access point instead of the one included in the Sagecom router above. | |
| 107 | Now I can control and organize all of my ethernet connections through | |
| 108 | the VLANs on the managed switch and wireless connections through the | |
| 109 | VLANS on the EAP. | |
| 110 | ||
| 111 | ## Generic Printer | |
| 112 | ||
| 113 | The last piece to my homelab is a standard wireless printer. Nothing | |
| 114 | special here. | |
| 115 | ||
| 116 | # Software | |
| 117 | ||
| 118 | ## Ubuntu Server 20.04 | |
| 119 | ||
| 120 | While the 20.04 version of Ubuntu was just released, I always like to | |
| 121 | experiment with new features (and I don't mind breaking my system - it | |
| 122 | just gives me more experience learning how to fix things). So, I have | |
| 123 | Ubuntu Server 20.04 installed on the Dell Optiplex server and Ubuntu | |
| 124 | Server 19.10 installed on the Raspberry Pi. Once I find an acceptable | |
| 125 | use for the Pi, I will most likely switch the operating system. | |
| 126 | ||
| 127 | ## Docker | |
| 128 | ||
| 129 | I am *very* new to Docker, but I have had a lot of fun playing with it | |
| 130 | so far. Docker is used to create containers that can hold all the | |
| 131 | contents of a system without interfering with other software on the same | |
| 132 | system. So far, I have successfully installed pi-hole, GitLab, Gogs, and | |
| 133 | Nextcloud in containers. However, I opted to delete all of those so that | |
| 134 | I can reconfigure them more professionally at a later time. | |
| 135 | ||
| 136 | ## Plex Media Server | |
| 137 | ||
| 138 | Plex is a media center software that allows you to organize your movies, | |
| 139 | TV shows, music, photos, and videos automatically. It will even download | |
| 140 | metadata for you so that you can easily browse these collections. | |
| 141 | ||
| 142 | ## Pi-hole | |
| 143 | ||
| 144 | Pi-hole is an alternative ad-blocker that runs at the DNS level, | |
| 145 | allowing you to block traffic when it hits your network, so that you can | |
| 146 | reject any traffic you deem to be bad. Pi-hole uses blacklists and | |
| 147 | whitelists to decide which traffic block and, luckily, there are a lot | |
| 148 | of pre-made lists out there on Reddit, GitHub, etc. | |
| 149 | ||
| 150 | ## Nextcloud | |
| 151 | ||
| 152 | While I had trouble with the Docker version of Nextcloud, I was very | |
| 153 | successful when setting up the snap version. Using this, I was able to | |
| 154 | map Nextcloud to a subdomain of a domain I own in Namecheap. | |
| 155 | Additionally, Nextcloud has an integration with Let's Encrypt that | |
| 156 | allows me to issue certificates automatically to any new domain I | |
| 157 | authorize. | |
| 158 | ||
| 159 | ## Webmin | |
| 160 | ||
| 161 | To monitor my servers, and the processes running on them, I use the | |
| 162 | Webmin dashboard. This was fairly painless to set up, and I currently | |
| 163 | access it straight through the server's IP address. In the future, I | |
| 164 | will be looking to configure Webmin to use a custom domain just like | |
| 165 | Nextcloud. | |
content/blog/2020-05-19-customizing-ubuntu.md added +210
| @@ -0,0 +1,210 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-05-19 | |
| 3 | title = "Beginner's Guide: Customizing Ubuntu" | |
| 4 | description = "A beginner's guide to customizing the Ubuntu operating system." | |
| 5 | +++ | |
| 6 | ||
| 7 | # More Information | |
| 8 | ||
| 9 | For inspiration on designing your *nix computer, check out the | |
| 10 | [r/unixporn](https://libredd.it/r/unixporn) subreddit! | |
| 11 | ||
| 12 | # Customizing Ubuntu | |
| 13 | ||
| 14 | New to Linux and want to add a personal touch to your machine? One of | |
| 15 | the best perks of Linux is that it is **extremely** customizable. You | |
| 16 | can change the styles of the windows, shell (status bars/docks), icons, | |
| 17 | fonts, terminals, and more. | |
| 18 | ||
| 19 | In this post, I'm going to go through customization on Ubuntu 20.04 | |
| 20 | (GNOME) since most new users tend to choose Ubuntu-based distros. If | |
| 21 | you've found a way to install Arch with i3-gaps, I'm assuming you know | |
| 22 | how to find more advanced tutorials out there on customizations. | |
| 23 | ||
| 24 | ## Required Tools | |
| 25 | ||
| 26 |  | |
| 28 | ||
| 29 | Ubuntu 20.04 ships with the default desktop environment | |
| 30 | [Gnome](https://www.gnome.org/), which includes the handy | |
| 31 | `gnome-tweaks` tool to quickly change designs. To install | |
| 32 | this, just open your terminal and enter the following command: | |
| 33 | ||
| 34 | ```sh | |
| 35 | sudo apt install gnome-tweaks | |
| 36 | ``` | |
| 37 | ||
| 38 | After you've finished installing the tool, simply launch the Tweaks | |
| 39 | application, and you'll be able to access the various customization | |
| 40 | options available by default on Ubuntu. You might even like some of the | |
| 41 | pre-installed options. | |
| 42 | ||
| 43 | ## GNOME Application Themes | |
| 44 | ||
| 45 | To change the themes applied to applications in GNOME, you will need to | |
| 46 | change the Applications dropdown in the Appearance section of Tweaks. To | |
| 47 | add more themes, you will have to find your preferred theme online and | |
| 48 | follow the steps below to have it show up in the Tweaks tool. While you | |
| 49 | may find themes anywhere, one of the most popular sites for GNOME themes | |
| 50 | is [gnome-look.org](https://www.gnome-look.org/). This website contains | |
| 51 | themes for applications, shells, icons, and cursors. | |
| 52 | ||
| 53 | Steps to import themes into Tweaks: | |
| 54 | ||
| 55 | 1. Download the theme. | |
| 56 | 2. These files are usually compressed (.zip, .tar.gz, .tar.xz), so you | |
| 57 | will need to extract the contents. This is easiest when opening the | |
| 58 | file explorer, right-clicking the compressed file, and choosing | |
| 59 | "Extract here." | |
| 60 | 3. Move the theme folder to `/usr/share/themes/`. You can do | |
| 61 | so with the following command: | |
| 62 | `sudo mv theme-folder/ /usr/share/themes/`. | |
| 63 | - Icons and cursors will be moved to the | |
| 64 | `/usr/share/icons/` folder. | |
| 65 | - Fonts will be moved to the `/usr/share/fonts/` folder | |
| 66 | Alternatively, you can move them to the | |
| 67 | `/usr/share/fonts/opentype/` or | |
| 68 | `/usr/share/fonts/opentype/` folders, if you have a | |
| 69 | specific font type. | |
| 70 | 4. Close tweaks if it is open. Re-open Tweaks and your new theme will | |
| 71 | be available in the Applications dropdown in the Appearance section | |
| 72 | of Tweaks. | |
| 73 | ||
| 74 | If the theme is not showing up after you've moved it into the themes | |
| 75 | folder, you may have uncompressed the folder into a sub-folder. You can | |
| 76 | check this by entering the theme folder and listing the contents: | |
| 77 | ||
| 78 | ```sh | |
| 79 | cd /usr/share/themes/Mojave-Dark && ls -la | |
| 80 | ``` | |
| 81 | ||
| 82 | This is an example of what the contents of your theme folder should look | |
| 83 | like. If you just see another folder there, you should move that folder | |
| 84 | up into the `/usr/share/themes/` folder. | |
| 85 | ||
| 86 | ```sh | |
| 87 | cinnamon COPYING gnome-shell gtk-2.0 gtk-3.0 index.theme metacity-1 plank xfwm4 | |
| 88 | ``` | |
| 89 | ||
| 90 | ## GNOME Shell Themes | |
| 91 | ||
| 92 | To change the appearance of the title bar, default dock, app menu, and | |
| 93 | other parts of the GNOME shell, you'll need to install the [user | |
| 94 | themes](https://extensions.gnome.org/extension/19/user-themes/) | |
| 95 | extension on [Gnome Extensions](https://extensions.gnome.org/). To be | |
| 96 | able to install extensions, you will first need to install the browser | |
| 97 | extension that the website instructs you to. See this screenshot for the | |
| 98 | blue box with a link to the extension. | |
| 99 | ||
| 100 |  | |
| 102 | ||
| 103 | After the browser extension is installed, you will need to install the | |
| 104 | native host connector: | |
| 105 | ||
| 106 | ```sh | |
| 107 | sudo apt install chrome-gnome-shell | |
| 108 | ``` | |
| 109 | ||
| 110 | Finally, you can go the [user | |
| 111 | themes](https://extensions.gnome.org/extension/19/user-themes/) | |
| 112 | extension page and click the installation button. This will enable the | |
| 113 | Shell option in Tweaks. Now you can move shell themes to the | |
| 114 | `/usr/share/themes` directory, using the same steps mentioned | |
| 115 | in the previous section, and enable the new theme in Tweaks. | |
| 116 | ||
| 117 | ## Icons & Cursors | |
| 118 | ||
| 119 | Icons and cursors are installed exactly the same way, so I'm grouping | |
| 120 | these together in this post. Both of these items will need to follow the | |
| 121 | same process as installing themes, except you will want to move your | |
| 122 | font folders to the `/usr/share/icons/` directory instead. | |
| 123 | ||
| 124 | ## Fonts | |
| 125 | ||
| 126 | Fonts are one of the overlooked parts of customization, but a good font | |
| 127 | can make the whole screen look different. For example, I have installed | |
| 128 | the [IBM Plex](https://github.com/IBM/plex/releases) fonts on my system. | |
| 129 | This follows the same process as installing themes, except you will want | |
| 130 | to move your font folders to the `/usr/share/fonts/` | |
| 131 | directory instead. | |
| 132 | ||
| 133 | ## Terminal | |
| 134 | ||
| 135 | If you spend a lot of time typing commands, you know how important the | |
| 136 | style and functionality of the terminal is. After spending a lot of time | |
| 137 | using the default GNOME terminal with [unix | |
| 138 | shell](https://en.wikipedia.org/wiki/Bash_(Unix_shell)), I decided to | |
| 139 | try some different options. I ended up choosing | |
| 140 | [Terminator](https://terminator-gtk3.readthedocs.io/en/latest/) with | |
| 141 | [zsh](https://en.wikipedia.org/wiki/Z_shell). | |
| 142 | ||
| 143 | Terminator is great if you need to open multiple terminals at one time | |
| 144 | by simply right-clicking and splitting the screen into as many terminals | |
| 145 | as you want. While this project hasn't been updated in a while, [it is | |
| 146 | coming under new | |
| 147 | development](https://github.com/gnome-terminator/terminator/issues/1). | |
| 148 | However, this terminal is great and I haven't experienced any errors | |
| 149 | yet. | |
| 150 | ||
| 151 | For the shell choice, I decided to choose zsh after trying it out on a | |
| 152 | fresh Manjaro installation. Zsh is great if you like to change the | |
| 153 | themes of your terminal, include icons, or add plugins. | |
| 154 | ||
| 155 | The desktop uses the | |
| 156 | [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions) | |
| 157 | to suggest past commands as you type. In addition, it suggests | |
| 158 | corrections if you misspell a command. Lastly, it uses the | |
| 159 | `af-magic` theme, which adds dashed lines between commands, | |
| 160 | moving the user@host tag to the right side of the terminal, and changes | |
| 161 | the colors. There are plenty of plugins and themes to choose from. Just | |
| 162 | figure out what you like and add it to your `~/.zshrc` file! | |
| 163 | ||
| 164 | ### Steps to Replicate My Terminal | |
| 165 | ||
| 166 | To install zsh on Ubuntu, enter the following command into a terminal: | |
| 167 | ||
| 168 | ```sh | |
| 169 | sudo apt install zsh | |
| 170 | ``` | |
| 171 | ||
| 172 | Then, enter the next command to activate zsh: | |
| 173 | ||
| 174 | ```sh | |
| 175 | sudo chsh -s $(which zsh) $(whoami) | |
| 176 | ``` | |
| 177 | ||
| 178 | To install Terminator on Ubuntu: | |
| 179 | ||
| 180 | ```sh | |
| 181 | sudo apt install terminator | |
| 182 | ``` | |
| 183 | ||
| 184 | To install Oh My Zsh on Ubuntu: | |
| 185 | ||
| 186 | ```sh | |
| 187 | sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" | |
| 188 | ``` | |
| 189 | ||
| 190 | To install zsh-autosuggestions via Oh My Zsh: | |
| 191 | ||
| 192 | ```sh | |
| 193 | git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions | |
| 194 | ``` | |
| 195 | ||
| 196 | Then, add the following plugin wording to your `~/.zshrc` | |
| 197 | file (the default config usually has the `git` plugin | |
| 198 | activated, so just add any other plugins to the parentheses separated by | |
| 199 | a space): | |
| 200 | ||
| 201 | ```sh | |
| 202 | nano ~/.zshrc | |
| 203 | ``` | |
| 204 | ||
| 205 | ```sh | |
| 206 | plugins=(git zsh-autosuggestions) | |
| 207 | ``` | |
| 208 | ||
| 209 | Finally, you need to log out of your computer and log back in so your | |
| 210 | user shell can refresh. | |
content/blog/2020-07-20-video-game-sales.md added +181
| @@ -0,0 +1,181 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-07-20 | |
| 3 | title = "Data Exploration: Video Game Sales" | |
| 4 | description = "Exploring and visualizing data with Python." | |
| 5 | +++ | |
| 6 | ||
| 7 | # Background Information | |
| 8 | ||
| 9 | This dataset (obtained from | |
| 10 | [Kaggle](https://www.kaggle.com/gregorut/videogamesales/data)) contains | |
| 11 | a list of video games with sales greater than 100,000 copies. It was | |
| 12 | generated by a scrape of vgchartz.com. | |
| 13 | ||
| 14 | Fields include: | |
| 15 | ||
| 16 | - Rank: Ranking of overall sales | |
| 17 | - Name: The game name | |
| 18 | - Platform: Platform of the game release (i.e. PC,PS4, etc.) | |
| 19 | - Year: Year of the game's release | |
| 20 | - Genre: Genre of the game | |
| 21 | - Publisher: Publisher of the game | |
| 22 | - NA~Sales~: Sales in North America (in millions) | |
| 23 | - EU~Sales~: Sales in Europe (in millions) | |
| 24 | - JP~Sales~: Sales in Japan (in millions) | |
| 25 | - Other~Sales~: Sales in the rest of the world (in millions) | |
| 26 | - Global~Sales~: Total worldwide sales. | |
| 27 | ||
| 28 | There are 16,598 records. 2 records were dropped due to incomplete | |
| 29 | information. | |
| 30 | ||
| 31 | # Import the Data | |
| 32 | ||
| 33 | ```python | |
| 34 | # Import the Python libraries we will be using | |
| 35 | import pandas as pd | |
| 36 | import numpy as np | |
| 37 | import seaborn as sns; sns.set() | |
| 38 | import matplotlib.pyplot as plt | |
| 39 | ||
| 40 | # Load the file using the path to the downloaded file | |
| 41 | file = r'video_game_sales.csv' | |
| 42 | df = pd.read_csv(file) | |
| 43 | df | |
| 44 | ``` | |
| 45 | ||
| 46 |  | |
| 48 | ||
| 49 | # Explore the Data | |
| 50 | ||
| 51 | ```python | |
| 52 | # With the description function, we can see the basic stats. For example, we can also see that the 'Year' column has some incomplete values. | |
| 53 | df.describe() | |
| 54 | ``` | |
| 55 | ||
| 56 |  | |
| 57 | ||
| 58 | ```python | |
| 59 | # This function shows the rows and columns of NaN values. For example, df[179,3] = nan | |
| 60 | np.where(pd.isnull(df)) | |
| 61 | ||
| 62 | (array([179, ..., 16553], dtype=int64), | |
| 63 | array([3, ..., 5], dtype=int64)) | |
| 64 | ``` | |
| 65 | ||
| 66 | # Visualize the Data | |
| 67 | ||
| 68 | ```python | |
| 69 | # This function plots the global sales by platform | |
| 70 | sns.catplot(x='Platform', y='Global_Sales', data=df, jitter=False).set_xticklabels(rotation=90) | |
| 71 | ``` | |
| 72 | ||
| 73 |  | |
| 75 | ||
| 76 | ```python | |
| 77 | # This function plots the global sales by genre | |
| 78 | sns.catplot(x='Genre', y='Global_Sales', data=df, jitter=False).set_xticklabels(rotation=45) | |
| 79 | ``` | |
| 80 | ||
| 81 |  | |
| 83 | ||
| 84 | ```python | |
| 85 | # This function plots the global sales by year | |
| 86 | sns.lmplot(x='Year', y='Global_Sales', data=df).set_xticklabels(rotation=45) | |
| 87 | ``` | |
| 88 | ||
| 89 |  | |
| 91 | ||
| 92 | ```python | |
| 93 | # This function plots four different lines to show sales from different regions. | |
| 94 | # The global sales plot line is commented-out, but can be included for comparison | |
| 95 | df2 = df.groupby('Year').sum() | |
| 96 | years = range(1980,2019) | |
| 97 | ||
| 98 | a = df2['NA_Sales'] | |
| 99 | b = df2['EU_Sales'] | |
| 100 | c = df2['JP_Sales'] | |
| 101 | d = df2['Other_Sales'] | |
| 102 | # e = df2['Global_Sales'] | |
| 103 | ||
| 104 | fig, ax = plt.subplots(figsize=(12,12)) | |
| 105 | ax.set_ylabel('Region Sales (in Millions)') | |
| 106 | ax.set_xlabel('Year') | |
| 107 | ||
| 108 | ax.plot(years, a, label='NA_Sales') | |
| 109 | ax.plot(years, b, label='EU_Sales') | |
| 110 | ax.plot(years, c, label='JP_Sales') | |
| 111 | ax.plot(years, d, label='Other_Sales') | |
| 112 | # ax.plot(years, e, label='Global_Sales') | |
| 113 | ||
| 114 | ax.legend() | |
| 115 | plt.show() | |
| 116 | ``` | |
| 117 | ||
| 118 |  | |
| 120 | ||
| 121 | # Investigate Outliers | |
| 122 | ||
| 123 | ```python | |
| 124 | # Find the game with the highest sales in North America | |
| 125 | df.loc[df['NA_Sales'].idxmax()] | |
| 126 | ||
| 127 | Rank 1 | |
| 128 | Name Wii Sports | |
| 129 | Platform Wii | |
| 130 | Year 2006 | |
| 131 | Genre Sports | |
| 132 | Publisher Nintendo | |
| 133 | NA_Sales 41.49 | |
| 134 | EU_Sales 29.02 | |
| 135 | JP_Sales 3.77 | |
| 136 | Other_Sales 8.46 | |
| 137 | Global_Sales 82.74 | |
| 138 | Name: 0, dtype: object | |
| 139 | ||
| 140 | # Explore statistics in the year 2006 (highest selling year) | |
| 141 | df3 = df[(df['Year'] == 2006)] | |
| 142 | df3.describe() | |
| 143 | ``` | |
| 144 | ||
| 145 |  | |
| 147 | ||
| 148 | ```python | |
| 149 | # Plot the results of the previous dataframe (games from 2006) - we can see the year's results were largely carried by Wii Sports | |
| 150 | sns.catplot(x="Genre", y="Global_Sales", data=df3, jitter=False).set_xticklabels(rotation=45) | |
| 151 | ``` | |
| 152 | ||
| 153 |  | |
| 155 | ||
| 156 | ```python | |
| 157 | # We can see 4 outliers in the graph above, so let's get the top 5 games from that dataframe | |
| 158 | # The results below show that Nintendo had all top 5 games (3 on the Wii and 2 on the DS) | |
| 159 | df3.sort_values(by=['Global_Sales'], ascending=False).head(5) | |
| 160 | ``` | |
| 161 | ||
| 162 |  | |
| 164 | ||
| 165 | # Discussion | |
| 166 | ||
| 167 | The purpose of exploring datasets is to ask questions, answer questions, | |
| 168 | and discover intelligence that can be used to inform decision-making. | |
| 169 | So, what have we found in this dataset? | |
| 170 | ||
| 171 | Today we simply explored a publicly-available dataset to see what kind | |
| 172 | of information it contained. During that exploration, we found that | |
| 173 | video game sales peaked in 2006. That peak was largely due to Nintendo, | |
| 174 | who sold the top 5 games in 2006 and has a number of games in the top-10 | |
| 175 | list for the years 1980-2020. Additionally, the top four platforms by | |
| 176 | global sales (Wii, NES, GB, DS) are owned by Nintendo. | |
| 177 | ||
| 178 | We didn't explore everything this dataset has to offer, but we can tell | |
| 179 | from a brief analysis that Nintendo seems to rule sales in the video | |
| 180 | gaming world. Further analysis could provide insight into which genres, | |
| 181 | regions, publishers, or world events are correlated with sales. | |
blog/2020-07-26-business-analysis.org → content/blog/2020-07-26-business-analysis.md renamed +114 −103
| @@ -1,28 +1,31 @@ | ||
| 1 | #+date: 2020-07-26 | |
| 2 | #+title: Algorithmically Analyzing Local Businesses | |
| 1 | +++ | |
| 2 | date = 2020-07-26 | |
| 3 | title = "Algorithmically Analyzing Local Businesses" | |
| 4 | description = "Exploring and visualizing data with Python." | |
| 5 | +++ | |
| 3 | 6 | |
| 4 | * Background Information | |
| 7 | # Background Information | |
| 5 | 8 | |
| 6 | This project aims to help investors learn more about a random city in order to | |
| 7 | determine optimal locations for business investments. The data used in this | |
| 8 | project was obtained using Foursquare's developer API. | |
| 9 | This project aims to help investors learn more about a random city in | |
| 10 | order to determine optimal locations for business investments. The data | |
| 11 | used in this project was obtained using Foursquare's developer API. | |
| 9 | 12 | |
| 10 | 13 | Fields include: |
| 11 | 14 | |
| 12 | - Venue Name | |
| 13 | - Venue Category | |
| 14 | - Venue Latitude | |
| 15 | - Venue Longitude | |
| 15 | - Venue Name | |
| 16 | - Venue Category | |
| 17 | - Venue Latitude | |
| 18 | - Venue Longitude | |
| 16 | 19 | |
| 17 | There are 232 records found using the center of Lincoln as the area of interest | |
| 18 | with a radius of 10,000. | |
| 20 | There are 232 records found using the center of Lincoln as the area of | |
| 21 | interest with a radius of 10,000. | |
| 19 | 22 | |
| 20 | * Import the Data | |
| 23 | # Import the Data | |
| 21 | 24 | |
| 22 | The first step is the simplest: import the applicable libraries. We will be | |
| 23 | using the libraries below for this project. | |
| 25 | The first step is the simplest: import the applicable libraries. We will | |
| 26 | be using the libraries below for this project. | |
| 24 | 27 | |
| 25 | #+BEGIN_SRC python | |
| 28 | ```python | |
| 26 | 29 | # Import the Python libraries we will be using |
| 27 | 30 | import pandas as pd |
| 28 | 31 | import requests |
| @@ -31,32 +34,32 @@ import math | ||
| 31 | 34 | import json |
| 32 | 35 | from pandas.io.json import json_normalize |
| 33 | 36 | from sklearn.cluster import KMeans |
| 34 | #+END_SRC | |
| 37 | ``` | |
| 35 | 38 | |
| 36 | To begin our analysis, we need to import the data for this project. The data we | |
| 37 | are using in this project comes directly from the Foursquare API. The first step | |
| 38 | is to get the latitude and longitude of the city being studied (Lincoln, NE) and | |
| 39 | setting up the folium map. | |
| 39 | To begin our analysis, we need to import the data for this project. The | |
| 40 | data we are using in this project comes directly from the Foursquare | |
| 41 | API. The first step is to get the latitude and longitude of the city | |
| 42 | being studied (Lincoln, NE) and setting up the folium map. | |
| 40 | 43 | |
| 41 | #+BEGIN_SRC python | |
| 44 | ```python | |
| 42 | 45 | # Define the latitude and longitude, then map the results |
| 43 | 46 | latitude = 40.806862 |
| 44 | 47 | longitude = -96.681679 |
| 45 | 48 | map_LNK = folium.Map(location=[latitude, longitude], zoom_start=12) |
| 46 | 49 | |
| 47 | 50 | map_LNK |
| 48 | #+END_SRC | |
| 51 | ``` | |
| 49 | 52 | |
| 50 | #+CAPTION: Blank Map | |
| 51 | [[https://img.cleberg.net/blog/20200726-ibm-data-science/01_blank_map-min.png]] | |
| 53 |  | |
| 52 | 55 | |
| 53 | Now that we have defined our city and created the map, we need to go get the | |
| 54 | business data. The Foursquare API will limit the results to 100 per API call, so | |
| 55 | we use our first API call below to determine the total results that Foursquare | |
| 56 | has found. Since the total results are 232, we perform the API fetching process | |
| 57 | three times (100 + 100 + 32 = 232). | |
| 56 | Now that we have defined our city and created the map, we need to go get | |
| 57 | the business data. The Foursquare API will limit the results to 100 per | |
| 58 | API call, so we use our first API call below to determine the total | |
| 59 | results that Foursquare has found. Since the total results are 232, we | |
| 60 | perform the API fetching process three times (100 + 100 + 32 = 232). | |
| 58 | 61 | |
| 59 | #+BEGIN_SRC python | |
| 62 | ```python | |
| 60 | 63 | # Foursquare API credentials |
| 61 | 64 | CLIENT_ID = 'your-client-id' |
| 62 | 65 | CLIENT_SECRET = 'your-client-secret' |
| @@ -114,18 +117,19 @@ url3 = 'https://api.foursquare.com/v2/venues/explore?&client_id={}&client_secret | ||
| 114 | 117 | |
| 115 | 118 | # Fetch the final results (201 - 232) |
| 116 | 119 | results3 = requests.get(url3).json() |
| 117 | #+END_SRC | |
| 120 | ``` | |
| 118 | 121 | |
| 119 | * Clean the Data | |
| 122 | # Clean the Data | |
| 120 | 123 | |
| 121 | Now that we have our data in three separate dataframes, we need to combine them | |
| 122 | into a single dataframe and make sure to reset the index so that we have a | |
| 123 | unique ID for each business. The `get_category_type` function below will pull | |
| 124 | the categories and name from each business's entry in the Foursquare data | |
| 125 | automatically. Once all the data has been labeled and combined, the results are | |
| 126 | stored in the =nearby_venues= dataframe. | |
| 124 | Now that we have our data in three separate dataframes, we need to | |
| 125 | combine them into a single dataframe and make sure to reset the index so | |
| 126 | that we have a unique ID for each business. The `get~categorytype~` | |
| 127 | function below will pull the categories and name from each business's | |
| 128 | entry in the Foursquare data automatically. Once all the data has been | |
| 129 | labeled and combined, the results are stored in the | |
| 130 | `nearby_venues` dataframe. | |
| 127 | 131 | |
| 128 | #+BEGIN_SRC python | |
| 132 | ```python | |
| 129 | 133 | # This function will extract the category of the venue from the API dictionary |
| 130 | 134 | def get_category_type(row): |
| 131 | 135 | try: |
| @@ -188,18 +192,18 @@ nearby_venues3.columns = [col.split(".")[-1] for col in nearby_venues3.columns] | ||
| 188 | 192 | nearby_venues = nearby_venues.append(nearby_venues3) |
| 189 | 193 | nearby_venues = nearby_venues.reset_index(drop=True) |
| 190 | 194 | nearby_venues |
| 191 | #+END_SRC | |
| 195 | ``` | |
| 192 | 196 | |
| 193 | #+CAPTION: Clean Data | |
| 194 | [[https://img.cleberg.net/blog/20200726-ibm-data-science/02_clean_data-min.png]] | |
| 197 |  | |
| 195 | 199 | |
| 196 | * Visualize the Data | |
| 200 | # Visualize the Data | |
| 197 | 201 | |
| 198 | We now have a complete, clean data set. The next step is to visualize this data | |
| 199 | onto the map we created earlier. We will be using folium's =CircleMarker()= | |
| 200 | function to do this. | |
| 202 | We now have a complete, clean data set. The next step is to visualize | |
| 203 | this data onto the map we created earlier. We will be using folium's | |
| 204 | `CircleMarker()` function to do this. | |
| 201 | 205 | |
| 202 | #+BEGIN_SRC python | |
| 206 | ```python | |
| 203 | 207 | # add markers to map |
| 204 | 208 | for lat, lng, name, categories in zip(nearby_venues['lat'], nearby_venues['lng'], nearby_venues['name'], nearby_venues['categories']): |
| 205 | 209 | label = '{} ({})'.format(name, categories) |
| @@ -215,22 +219,25 @@ for lat, lng, name, categories in zip(nearby_venues['lat'], nearby_venues['lng'] | ||
| 215 | 219 | ).add_to(map_LNK) |
| 216 | 220 | |
| 217 | 221 | map_LNK |
| 218 | #+END_SRC | |
| 222 | ``` | |
| 219 | 223 | |
| 220 |  | |
| 224 | \ | |
| 221 | 227 | |
| 222 | * Clustering: /k-means/ | |
| 228 | # Clustering: *k-means* | |
| 223 | 229 | |
| 224 | To cluster the data, we will be using the /k-means/ algorithm. This algorithm is | |
| 225 | iterative and will automatically make sure that data points in each cluster are | |
| 226 | as close as possible to each other, while being as far as possible away from | |
| 227 | other clusters. | |
| 230 | To cluster the data, we will be using the *k-means* algorithm. This | |
| 231 | algorithm is iterative and will automatically make sure that data points | |
| 232 | in each cluster are as close as possible to each other, while being as | |
| 233 | far as possible away from other clusters. | |
| 228 | 234 | |
| 229 | However, we first have to figure out how many clusters to use (defined as the | |
| 230 | variable /'k'/). To do so, we will use the next two functions to calculate the | |
| 231 | sum of squares within clusters and then return the optimal number of clusters. | |
| 235 | However, we first have to figure out how many clusters to use (defined | |
| 236 | as the variable *'k'*). To do so, we will use the next two functions | |
| 237 | to calculate the sum of squares within clusters and then return the | |
| 238 | optimal number of clusters. | |
| 232 | 239 | |
| 233 | #+BEGIN_SRC python | |
| 240 | ```python | |
| 234 | 241 | # This function will return the sum of squares found in the data |
| 235 | 242 | def calculate_wcss(data): |
| 236 | 243 | wcss = [] |
| @@ -264,13 +271,13 @@ def optimal_number_of_clusters(wcss): | ||
| 264 | 271 | |
| 265 | 272 | # calculating the optimal number of clusters |
| 266 | 273 | n = optimal_number_of_clusters(sum_of_squares) |
| 267 | #+END_SRC | |
| 274 | ``` | |
| 268 | 275 | |
| 269 | Now that we have found that our optimal number of clusters is six, we need to | |
| 270 | perform k-means clustering. When this clustering occurs, each business is | |
| 271 | assigned a cluster number from 0 to 5 in the dataframe. | |
| 276 | Now that we have found that our optimal number of clusters is six, we | |
| 277 | need to perform k-means clustering. When this clustering occurs, each | |
| 278 | business is assigned a cluster number from 0 to 5 in the dataframe. | |
| 272 | 279 | |
| 273 | #+BEGIN_SRC python | |
| 280 | ```python | |
| 274 | 281 | # set number of clusters equal to the optimal number |
| 275 | 282 | kclusters = n |
| 276 | 283 | |
| @@ -279,12 +286,13 @@ kmeans = KMeans(n_clusters=kclusters, random_state=0).fit(cluster_df) | ||
| 279 | 286 | |
| 280 | 287 | # add clustering labels to dataframe |
| 281 | 288 | nearby_venues.insert(0, 'Cluster Labels', kmeans.labels_) |
| 282 | #+END_SRC | |
| 289 | ``` | |
| 283 | 290 | |
| 284 | Success! We now have a dataframe with clean business data, along with a cluster | |
| 285 | number for each business. Now let's map the data using six different colors. | |
| 291 | Success! We now have a dataframe with clean business data, along with a | |
| 292 | cluster number for each business. Now let's map the data using six | |
| 293 | different colors. | |
| 286 | 294 | |
| 287 | #+BEGIN_SRC python | |
| 295 | ```python | |
| 288 | 296 | # create map with clusters |
| 289 | 297 | map_clusters = folium.Map(location=[latitude, longitude], zoom_start=12) |
| 290 | 298 | colors = ['#0F9D58', '#DB4437', '#4285F4', '#800080', '#ce12c0', '#171717'] |
| @@ -303,36 +311,37 @@ for lat, lng, name, categories, cluster in zip(nearby_venues['lat'], nearby_venu | ||
| 303 | 311 | fill_opacity=0.7).add_to(map_clusters) |
| 304 | 312 | |
| 305 | 313 | map_clusters |
| 306 | #+END_SRC | |
| 314 | ``` | |
| 307 | 315 | |
| 308 | #+CAPTION: Clustered Map | |
| 309 | [[https://img.cleberg.net/blog/20200726-ibm-data-science/04_clusters-min.png]] | |
| 316 |  | |
| 310 | 318 | |
| 311 | * Investigate Clusters | |
| 319 | # Investigate Clusters | |
| 312 | 320 | |
| 313 | Now that we have figured out our clusters, let's do a little more analysis to | |
| 314 | provide more insight into the clusters. With the information below, we can see | |
| 315 | which clusters are more popular for businesses and which are less popular. The | |
| 316 | results below show us that clusters 0 through 3 are popular, while clusters 4 | |
| 317 | and 5 are not very popular at all. | |
| 321 | Now that we have figured out our clusters, let's do a little more | |
| 322 | analysis to provide more insight into the clusters. With the information | |
| 323 | below, we can see which clusters are more popular for businesses and | |
| 324 | which are less popular. The results below show us that clusters 0 | |
| 325 | through 3 are popular, while clusters 4 and 5 are not very popular at | |
| 326 | all. | |
| 318 | 327 | |
| 319 | #+BEGIN_SRC python | |
| 328 | ```python | |
| 320 | 329 | # Show how many venues are in each cluster |
| 321 | 330 | color_names = ['Dark Green', 'Red', 'Blue', 'Purple', 'Pink', 'Black'] |
| 322 | 331 | for x in range(0,6): |
| 323 | 332 | print("Color of Cluster", x, ":", color_names[x]) |
| 324 | 333 | print("Venues found in Cluster", x, ":", nearby_venues.loc[nearby_venues['Cluster Labels'] == x, nearby_venues.columns[:]].shape[0]) |
| 325 | 334 | print("---") |
| 326 | #+END_SRC | |
| 335 | ``` | |
| 327 | 336 | |
| 328 | #+CAPTION: Venues per Cluster | |
| 329 | [[https://img.cleberg.net/blog/20200726-ibm-data-science/05_venues_per_cluster-min.png]] | |
| 337 |  | |
| 330 | 339 | |
| 331 | Our last piece of analysis is to summarize the categories of businesses within | |
| 332 | each cluster. With these results, we can clearly see that restaurants, coffee | |
| 333 | shops, and grocery stores are the most popular. | |
| 340 | Our last piece of analysis is to summarize the categories of businesses | |
| 341 | within each cluster. With these results, we can clearly see that | |
| 342 | restaurants, coffee shops, and grocery stores are the most popular. | |
| 334 | 343 | |
| 335 | #+BEGIN_SRC python | |
| 344 | ```python | |
| 336 | 345 | # Calculate how many venues there are in each category |
| 337 | 346 | # Sort from largest to smallest |
| 338 | 347 | temp_df = nearby_venues.drop(columns=['name', 'lat', 'lng']) |
| @@ -352,27 +361,29 @@ with pd.option_context('display.max_rows', None, 'display.max_columns', None): | ||
| 352 | 361 | print("\n\n", "Cluster 3:", "\n", cluster3_grouped.loc[cluster3_grouped['Cluster Labels'] > 1]) |
| 353 | 362 | print("\n\n", "Cluster 4:", "\n", cluster4_grouped.loc[cluster4_grouped['Cluster Labels'] > 1]) |
| 354 | 363 | print("\n\n", "Cluster 5:", "\n", cluster5_grouped.loc[cluster5_grouped['Cluster Labels'] > 1]) |
| 355 | #+END_SRC | |
| 364 | ``` | |
| 356 | 365 | |
| 357 | #+CAPTION: Venues per Cluster, pt. 1 | |
| 358 | [[https://img.cleberg.net/blog/20200726-ibm-data-science/06_categories_per_cluster_pt1-min.png]] | |
| 366 |  | |
| 359 | 368 | |
| 360 | #+CAPTION: Venues per Cluster, pt. 2 | |
| 361 | [[https://img.cleberg.net/blog/20200726-ibm-data-science/07_categories_per_cluster_pt2-min.png]] | |
| 369 |  | |
| 362 | 371 | |
| 363 | * Discussion | |
| 372 | # Discussion | |
| 364 | 373 | |
| 365 | In this project, we gathered location data for Lincoln, Nebraska, USA and | |
| 366 | clustered the data using the k-means algorithm in order to identify the unique | |
| 367 | clusters of businesses in Lincoln. Through these actions, we found that there | |
| 368 | are six unique business clusters in Lincoln and that two of the clusters are | |
| 369 | likely unsuitable for investors. The remaining four clusters have a variety of | |
| 370 | businesses, but are largely dominated by restaurants and grocery stores. | |
| 374 | In this project, we gathered location data for Lincoln, Nebraska, USA | |
| 375 | and clustered the data using the k-means algorithm in order to identify | |
| 376 | the unique clusters of businesses in Lincoln. Through these actions, we | |
| 377 | found that there are six unique business clusters in Lincoln and that | |
| 378 | two of the clusters are likely unsuitable for investors. The remaining | |
| 379 | four clusters have a variety of businesses, but are largely dominated by | |
| 380 | restaurants and grocery stores. | |
| 371 | 381 | |
| 372 | Using this project, investors can now make more informed decisions when deciding | |
| 373 | the location and category of business in which to invest. | |
| 382 | Using this project, investors can now make more informed decisions when | |
| 383 | deciding the location and category of business in which to invest. | |
| 374 | 384 | |
| 375 | Further studies may involve other attributes for business locations, such as | |
| 376 | population density, average wealth across the city, or crime rates. In addition, | |
| 377 | further studies may include additional location data and businesses by utilizing | |
| 378 | multiple sources, such as Google Maps and OpenStreetMap. | |
| 385 | Further studies may involve other attributes for business locations, | |
| 386 | such as population density, average wealth across the city, or crime | |
| 387 | rates. In addition, further studies may include additional location data | |
| 388 | and businesses by utilizing multiple sources, such as Google Maps and | |
| 389 | OpenStreetMap. | |
content/blog/2020-08-22-redirect-github-pages.md added +136
| @@ -0,0 +1,136 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-08-22 | |
| 3 | title = "Redirect GitHub Pages from Subdomain to the Top-Level Domain" | |
| 4 | description = "Learn how to redirect Github pages to the TLD." | |
| 5 | +++ | |
| 6 | ||
| 7 | # Short answer | |
| 8 | ||
| 9 | ## Step 1 | |
| 10 | ||
| 11 | Add a new file CNAME to your GitHub Pages repository containing only one | |
| 12 | line: your top-level domain name. E.g.: `example.com` | |
| 13 | ||
| 14 | ## Step 2 | |
| 15 | ||
| 16 | [Optional] but highly recommended | |
| 17 | ||
| 18 | 2.1: Remove all other top-level records (prefixed with @) of type A from | |
| 19 | your DNS configuration. | |
| 20 | ||
| 21 | 2.2: Remove a CNAME record for the second-level domain www if it is | |
| 22 | present. | |
| 23 | ||
| 24 | ## Step 3 | |
| 25 | ||
| 26 | Add these 5 entries to the very top of your DNS configuration: | |
| 27 | ||
| 28 | ```txt | |
| 29 | @ A 185.199.108.153 | |
| 30 | @ A 185.199.109.153 | |
| 31 | @ A 185.199.110.153 | |
| 32 | @ A 185.199.111.153 | |
| 33 | www CNAME your_github_username.github.io. | |
| 34 | ``` | |
| 35 | ||
| 36 | Replace `your_github_username` with your actual GitHub | |
| 37 | username. | |
| 38 | ||
| 39 | ## Step 4 | |
| 40 | ||
| 41 | Wait for your DNS changes to propagate. DNS changes aren't effective | |
| 42 | immediately. They can take up to a full day to propagate. | |
| 43 | ||
| 44 | # Long answer | |
| 45 | ||
| 46 | This issue has two sides. One is the DNS configuration itself. Another | |
| 47 | one is the way GitHub Pages will forward HTTP requests. | |
| 48 | ||
| 49 | We need to know a few things to understand what GitHub is trying to say | |
| 50 | in their documentation. | |
| 51 | ||
| 52 | ## DNS Entry Types | |
| 53 | ||
| 54 | There are two types of DNS records which interest us: CNAME and A. | |
| 55 | ||
| 56 | `A` is also known as `Apex` or sometimes as | |
| 57 | `root entry`. It forwards requests to a specified fixed IP | |
| 58 | address. `CNAME` entry forwards requests to a specified URL | |
| 59 | (actual valid plain text URL, not an IP address). | |
| 60 | ||
| 61 | ## DNS Load balancing | |
| 62 | ||
| 63 | GitHub has one central URL address which accepts all DNS requests for | |
| 64 | GitHub Pages: `http://username.github.io`. That URL is | |
| 65 | resolved to different IP addresses based on your geographical location. | |
| 66 | Website hosted on GitHub Pages is a simple collection of | |
| 67 | `HTML`, `CSS` and `JS` files. GitHub | |
| 68 | distributes these files to different servers across the globe. So that | |
| 69 | when your browser sends a request from Europe, it receives data from a | |
| 70 | server in Europe. The same is valid for the requests from Asia and the | |
| 71 | USA. | |
| 72 | ||
| 73 | ## What GitHub is trying to say | |
| 74 | ||
| 75 | Since `A` records in DNS must contain IP addresses, and they | |
| 76 | must be either `185.199.108.153` or | |
| 77 | `185.199.109.153` or `185.199.110.153` or | |
| 78 | `185.199.111.153`, there is no way to forward requests to a | |
| 79 | server located somewhere in Europe or Asia. Your website hosted at | |
| 80 | GitHub Pages will be downloaded from a central GitHub Pages server. | |
| 81 | There is a minor risk that if GitHub Pages DNS servers | |
| 82 | (`x.x.x.153`) are down for some reason, all custom domains | |
| 83 | which use fixed GitHub Pages IP addresses will not be accessible (their | |
| 84 | DNS requests will not be resolvable). | |
| 85 | ||
| 86 | That is why GitHub strongly suggests to either use a second-level domain | |
| 87 | for your GitHub Pages (e.g. `blog.example.com`) or use a DNS | |
| 88 | service provider that supports a record type `ALIAS` that | |
| 89 | acts as `A` record but forwards request to a URL address | |
| 90 | (e.g. `username.github.io`) instead of a fixed IP address. | |
| 91 | ||
| 92 | ## How GitHub Pages treats HTTP requests | |
| 93 | ||
| 94 | After a DNS request for `your_github_username.github.io` is | |
| 95 | resolved into an IP address, e.g. `185.199.108.153` your | |
| 96 | browser sends an HTTP request to that server with an HTTP header | |
| 97 | `Host`. Below are `curl` examples that load the | |
| 98 | same website (these examples might not work if you are behind a proxy | |
| 99 | server): | |
| 100 | ||
| 101 | ```sh | |
| 102 | curl --header "Host: your_github_username.github.io" http://185.199.108.153/ | |
| 103 | curl --header "Host: www.example.com" http://185.199.108.153/ | |
| 104 | curl --header "Host: example.com" http://185.199.108.153/ | |
| 105 | ``` | |
| 106 | ||
| 107 | This way GitHub Pages servers know which user website to serve. | |
| 108 | ||
| 109 | > GitHub Pages server will automatically redirect HTTP requests to the | |
| 110 | > top-level domain if your `CNAME` file contains | |
| 111 | > `example.com` but `www.example.com` is | |
| 112 | > requested. | |
| 113 | > | |
| 114 | > The same is valid if your `CNAME` file contains | |
| 115 | > `www.example.com` but the header `Host` in the | |
| 116 | > `HTTP` request contains `example.com`. | |
| 117 | ||
| 118 | ## Why can't I add a `CNAME` record entry that accepts a top-level request (`@`) to my DNS configuration? | |
| 119 | ||
| 120 | Quote from the GitHub Pages documentation: | |
| 121 | ||
| 122 | > Warning: Do not create a CNAME record for your custom apex domain! | |
| 123 | > Doing so may cause issues with other services, such as email, on that | |
| 124 | > domain. | |
| 125 | ||
| 126 | # References: | |
| 127 | ||
| 128 | 1. [Setting up a custom domain with GitHub | |
| 129 | Pages](https://docs.github.com/en/github/working-with-github-pages/configuring-a-custom-domain-for-your-github-pages-site) | |
| 130 | 2. [My custom domain isn't | |
| 131 | working](https://docs.github.com/en/github/working-with-github-pages/troubleshooting-custom-domains-and-github-pages) | |
| 132 | 3. [Cannot access my GitHub Pages website by IP | |
| 133 | Address](https://serverfault.com/questions/589370/cannot-access-my-github-pages-website-by-ip-address) | |
| 134 | 4. [How do I set up GitHub Pages to redirect DNS requests from a | |
| 135 | subdomain (e.g. www) to the top-level domain (TLD, Apex | |
| 136 | record)?](https://stackoverflow.com/questions/23375422/how-do-i-set-up-github-pages-to-redirect-dns-requests-from-a-subdomain-e-g-www) | |
blog/2020-08-29-php-auth-flow.org → content/blog/2020-08-29-php-auth-flow.md renamed +75 −65
| @@ -1,19 +1,23 @@ | ||
| 1 | #+date: 2020-08-29 | |
| 2 | #+title: PHP Authentication Flow | |
| 3 | ||
| 4 | * Introduction | |
| 5 | ||
| 6 | When creating websites that will allow users to create accounts, the developer | |
| 7 | always needs to consider the proper authentication flow for their app. For | |
| 8 | example, some developers will utilize an API for authentication, some will use | |
| 9 | OAuth, and some may just use their own simple database. | |
| 10 | ||
| 11 | For those using pre-built libraries, authentication may simply be a problem of | |
| 12 | copying and pasting the code from their library's documentation. For example, | |
| 13 | here's the code I use to authenticate users with the Tumblr OAuth API for my | |
| 14 | Tumblr client, Vox Populi: | |
| 15 | ||
| 16 | #+BEGIN_SRC php | |
| 1 | +++ | |
| 2 | date = 2020-08-29 | |
| 3 | title = "PHP Authentication Flow" | |
| 4 | description = "Learn how to establish and maintain a basic user authentication flow in PHP." | |
| 5 | +++ | |
| 6 | ||
| 7 | # Introduction | |
| 8 | ||
| 9 | When creating websites that will allow users to create accounts, the | |
| 10 | developer always needs to consider the proper authentication flow for | |
| 11 | their app. For example, some developers will utilize an API for | |
| 12 | authentication, some will use OAuth, and some may just use their own | |
| 13 | simple database. | |
| 14 | ||
| 15 | For those using pre-built libraries, authentication may simply be a | |
| 16 | problem of copying and pasting the code from their library's | |
| 17 | documentation. For example, here's the code I use to authenticate users | |
| 18 | with the Tumblr OAuth API for my Tumblr client, Vox Populi: | |
| 19 | ||
| 20 | ```php | |
| 17 | 21 | // Start the session |
| 18 | 22 | session_start(); |
| 19 | 23 | |
| @@ -34,27 +38,28 @@ $client = new Tumblr\API\Client( | ||
| 34 | 38 | $token, |
| 35 | 39 | $token_secret |
| 36 | 40 | ); |
| 37 | #+END_SRC | |
| 41 | ``` | |
| 38 | 42 | |
| 39 | However, developers creating authentication flows from scratch will need to | |
| 40 | think carefully about when to make sure a web page will check the user's | |
| 41 | authenticity. | |
| 43 | However, developers creating authentication flows from scratch will need | |
| 44 | to think carefully about when to make sure a web page will check the | |
| 45 | user's authenticity. | |
| 42 | 46 | |
| 43 | In this article, we're going to look at a simple authentication flow using a | |
| 44 | MySQL database and PHP. | |
| 47 | In this article, we're going to look at a simple authentication flow | |
| 48 | using a MySQL database and PHP. | |
| 45 | 49 | |
| 46 | * Creating User Accounts | |
| 50 | # Creating User Accounts | |
| 47 | 51 | |
| 48 | The beginning to any type of user authentication is to create a user account. | |
| 49 | This process can take many formats, but the simplest is to accept user input | |
| 50 | from a form (e.g., username and password) and send it over to your database. | |
| 51 | For example, here's a snippet that shows how to get username and password | |
| 52 | parameters that would come when a user submits a form to your PHP script. | |
| 52 | The beginning to any type of user authentication is to create a user | |
| 53 | account. This process can take many formats, but the simplest is to | |
| 54 | accept user input from a form (e.g., username and password) and send it | |
| 55 | over to your database. For example, here's a snippet that shows how to | |
| 56 | get username and password parameters that would come when a user submits | |
| 57 | a form to your PHP script. | |
| 53 | 58 | |
| 54 | *Note*: Ensure that your password column is large enough to hold the hashed | |
| 55 | value (at least 60 characters or longer). | |
| 59 | **Note**: Ensure that your password column is large enough to hold the | |
| 60 | hashed value (at least 60 characters or longer). | |
| 56 | 61 | |
| 57 | #+BEGIN_SRC php | |
| 62 | ```php | |
| 58 | 63 | // Get the values from the URL |
| 59 | 64 | $username = $_POST['username']; |
| 60 | 65 | $raw_password = $_POST['password']; |
| @@ -87,15 +92,15 @@ if ($conn->query($sql) === TRUE) { | ||
| 87 | 92 | } |
| 88 | 93 | |
| 89 | 94 | $conn->close(); |
| 90 | #+END_SRC | |
| 95 | ``` | |
| 91 | 96 | |
| 92 | * Validate Returning Users | |
| 97 | # Validate Returning Users | |
| 93 | 98 | |
| 94 | To be able to verify that a returning user has a valid username and password in | |
| 95 | your database is as simple as having users fill out a form and comparing their | |
| 96 | inputs to your database. | |
| 99 | To be able to verify that a returning user has a valid username and | |
| 100 | password in your database is as simple as having users fill out a form | |
| 101 | and comparing their inputs to your database. | |
| 97 | 102 | |
| 98 | #+BEGIN_SRC php | |
| 103 | ```php | |
| 99 | 104 | // Query the database for username and password |
| 100 | 105 | // ... |
| 101 | 106 | |
| @@ -106,41 +111,42 @@ if(password_verify($password_input, $hashed_password)) { | ||
| 106 | 111 | |
| 107 | 112 | // Else, Redirect them back to the login page. |
| 108 | 113 | ... |
| 109 | #+END_SRC | |
| 114 | ``` | |
| 110 | 115 | |
| 111 | * Storing Authentication State | |
| 116 | # Storing Authentication State | |
| 112 | 117 | |
| 113 | Once you've created the user's account, now you're ready to initialize the | |
| 114 | user's session. *You will need to do this on every page you load while the user | |
| 115 | is logged in.* To do so, simply enter the following code snippet: | |
| 118 | Once you've created the user's account, now you're ready to | |
| 119 | initialize the user's session. **You will need to do this on every page | |
| 120 | you load while the user is logged in.** To do so, simply enter the | |
| 121 | following code snippet: | |
| 116 | 122 | |
| 117 | #+BEGIN_SRC php | |
| 123 | ```php | |
| 118 | 124 | session_start(); |
| 119 | #+END_SRC | |
| 125 | ``` | |
| 120 | 126 | |
| 121 | Once you've initialized the session, the next step is to store the session in a | |
| 122 | cookie so that you can access it later. | |
| 127 | Once you've initialized the session, the next step is to store the | |
| 128 | session in a cookie so that you can access it later. | |
| 123 | 129 | |
| 124 | #+BEGIN_SRC php | |
| 130 | ```php | |
| 125 | 131 | setcookie(session_name()); |
| 126 | #+END_SRC | |
| 132 | ``` | |
| 127 | 133 | |
| 128 | Now that the session name has been stored, you'll be able to check if there's an | |
| 129 | active session whenever you load a page. | |
| 134 | Now that the session name has been stored, you'll be able to check if | |
| 135 | there's an active session whenever you load a page. | |
| 130 | 136 | |
| 131 | #+BEGIN_SRC php | |
| 137 | ```php | |
| 132 | 138 | if(isset(session_name())) { |
| 133 | 139 | // The session is active |
| 134 | 140 | } |
| 135 | #+END_SRC | |
| 141 | ``` | |
| 136 | 142 | |
| 137 | * Removing User Authentication | |
| 143 | # Removing User Authentication | |
| 138 | 144 | |
| 139 | The next logical step is to give your users the option to log out once they are | |
| 140 | done using your application. This can be tricky in PHP since a few of the | |
| 141 | standard ways do not always work. | |
| 145 | The next logical step is to give your users the option to log out once | |
| 146 | they are done using your application. This can be tricky in PHP since a | |
| 147 | few of the standard ways do not always work. | |
| 142 | 148 | |
| 143 | #+BEGIN_SRC php | |
| 149 | ```php | |
| 144 | 150 | // Initialize the session. |
| 145 | 151 | // If you are using session_name("something"), don't forget it now! |
| 146 | 152 | session_start(); |
| @@ -172,14 +178,18 @@ session_write_close(); | ||
| 172 | 178 | // Go back to sign-in page |
| 173 | 179 | header('Location: https://example.com/logged-out/'); |
| 174 | 180 | die(); |
| 175 | #+END_SRC | |
| 181 | ``` | |
| 176 | 182 | |
| 177 | * Wrapping Up | |
| 183 | # Wrapping Up | |
| 178 | 184 | |
| 179 | Now you should be ready to begin your authentication programming with PHP. You | |
| 180 | can create user accounts, create sessions for users across different pages of | |
| 181 | your site, and then destroy the user data when they're ready to leave. | |
| 185 | Now you should be ready to begin your authentication programming with | |
| 186 | PHP. You can create user accounts, create sessions for users across | |
| 187 | different pages of your site, and then destroy the user data when | |
| 188 | they're ready to leave. | |
| 182 | 189 | |
| 183 | For more information on this subject, I recommend reading the [[https://www.php.net/][PHP | |
| 184 | Documentation]]. Specifically, you may want to look at [[https://www.php.net/manual/en/features.http-auth.php][HTTP Authentication with | |
| 185 | PHP]], [[https://www.php.net/manual/en/book.session.php][session handling]], and [[https://www.php.net/manual/en/function.hash.php][hash]]. | |
| 190 | For more information on this subject, I recommend reading the [PHP | |
| 191 | Documentation](https://www.php.net/). Specifically, you may want to look | |
| 192 | at [HTTP Authentication with | |
| 193 | PHP](https://www.php.net/manual/en/features.http-auth.php), [session | |
| 194 | handling](https://www.php.net/manual/en/book.session.php), and | |
| 195 | [hash](https://www.php.net/manual/en/function.hash.php). | |
content/blog/2020-09-01-visual-recognition.md added +204
| @@ -0,0 +1,204 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-09-01 | |
| 3 | title = "IBM Watson Visual Recognition" | |
| 4 | description = "Exploring and visualizing data with Python." | |
| 5 | +++ | |
| 6 | ||
| 7 | # What is IBM Watson? | |
| 8 | ||
| 9 | If you've never heard of [Watson](https://www.ibm.com/watson), this | |
| 10 | service is a suite of enterprise-ready AI services, applications, and | |
| 11 | tooling provided by IBM. Watson contains quite a few useful tools for | |
| 12 | data scientists and students, including the subject of this post today: | |
| 13 | visual recognition. | |
| 14 | ||
| 15 | If you'd like to view the official documentation for the Visual | |
| 16 | Recognition API, visit the [API | |
| 17 | Docs](https://cloud.ibm.com/apidocs/visual-recognition/visual-recognition-v3?code=python). | |
| 18 | ||
| 19 | # Prerequisites | |
| 20 | ||
| 21 | To be able to use Watson Visual Recognition, you'll need the following: | |
| 22 | ||
| 23 | 1. Create a free account on [IBM Watson | |
| 24 | Studio](https://www.ibm.com/cloud/watson-studio). | |
| 25 | 2. Add the [Watson Visual | |
| 26 | Recognition](https://www.ibm.com/cloud/watson-visual-recognition) | |
| 27 | service to your IBM Watson account. | |
| 28 | 3. Get your API key and URL. To do this, first go to the [profile | |
| 29 | dashboard](https://dataplatform.cloud.ibm.com/home2?context=cpdaas) | |
| 30 | for your IBM account and click on the Watson Visual Recognition | |
| 31 | service you created. This will be listed in the section titled | |
| 32 | **Your services**. Then click the **Credentials** tab and open the | |
| 33 | **Auto-generated credentials** dropdown. Copy your API key and URL | |
| 34 | so that you can use them in the Python script later. | |
| 35 | 4. **[Optional]** While not required, you can also create the Jupyter | |
| 36 | Notebook for this project right inside [Watson | |
| 37 | Studio](https://www.ibm.com/cloud/watson-studio). Watson Studio will | |
| 38 | save your notebooks inside an organized project and allow you to use | |
| 39 | their other integrated products, such as storage containers, AI | |
| 40 | models, documentation, external sharing, etc. | |
| 41 | ||
| 42 | # Calling the IBM Watson Visual Recognition API | |
| 43 | ||
| 44 | Okay, now let's get started. | |
| 45 | ||
| 46 | To begin, we need to install the proper Python package for IBM Watson. | |
| 47 | ||
| 48 | ```sh | |
| 49 | pip install --upgrade --user "ibm-watson>=4.5.0" | |
| 50 | ``` | |
| 51 | ||
| 52 | Next, we need to specify the API key, version, and URL given to us when | |
| 53 | we created the Watson Visual Recognition service. | |
| 54 | ||
| 55 | ```python | |
| 56 | apikey = "<your-apikey>" | |
| 57 | version = "2018-03-19" | |
| 58 | url = "<your-url>" | |
| 59 | ``` | |
| 60 | ||
| 61 | Now, let's import the necessary libraries and authenticate our service. | |
| 62 | ||
| 63 | ```python | |
| 64 | import json | |
| 65 | from ibm_watson import VisualRecognitionV3 | |
| 66 | from ibm_cloud_sdk_core.authenticators import IAMAuthenticator | |
| 67 | ||
| 68 | authenticator = IAMAuthenticator(apikey) | |
| 69 | visual_recognition = VisualRecognitionV3( | |
| 70 | version=version, | |
| 71 | authenticator=authenticator | |
| 72 | ) | |
| 73 | ||
| 74 | visual_recognition.set_service_url(url) | |
| 75 | ``` | |
| 76 | ||
| 77 | **[Optional]** If you'd like to tell the API not to use any data to | |
| 78 | improve their products, set the following header. | |
| 79 | ||
| 80 | ```python | |
| 81 | visual_recognition.set_default_headers({'x-watson-learning-opt-out': "true"}) | |
| 82 | ``` | |
| 83 | ||
| 84 | Now we have our API all set and ready to go. For this example, I'm | |
| 85 | going to include a `dict` of photos to load as we test out | |
| 86 | the API. | |
| 87 | ||
| 88 | ```python | |
| 89 | data = [ | |
| 90 | { | |
| 91 | "title": "Grizzly Bear", | |
| 92 | "url": "https://example.com/photos/image1.jpg" | |
| 93 | }, | |
| 94 | { | |
| 95 | "title": "Nature Lake", | |
| 96 | "url": "https://example.com/photos/image2.jpg" | |
| 97 | }, | |
| 98 | { | |
| 99 | "title": "Welcome Sign", | |
| 100 | "url": "https://example.com/photos/image3.jpg" | |
| 101 | }, | |
| 102 | { | |
| 103 | "title": "Honey Badger", | |
| 104 | "url": "https://example.com/photos/image4.jpg" | |
| 105 | }, | |
| 106 | { | |
| 107 | "title": "Grand Canyon Lizard", | |
| 108 | "url": "https://example.com/photos/image5.jpg" | |
| 109 | }, | |
| 110 | { | |
| 111 | "title": "Castle", | |
| 112 | "url": "https://example.com/photos/image6.jpg" | |
| 113 | } | |
| 114 | ] | |
| 115 | ``` | |
| 116 | ||
| 117 | Now that we've set up our libraries and have the photos ready, let's | |
| 118 | create a loop to call the API for each image. The code below shows a | |
| 119 | loop that calls the URL of each image and sends it to the API, | |
| 120 | requesting results with at least 60% confidence. The results are output | |
| 121 | to the console with dotted lines separating each section. | |
| 122 | ||
| 123 | In the case of an API error, the codes and explanations are output to | |
| 124 | the console. | |
| 125 | ||
| 126 | ```python | |
| 127 | from ibm_watson import ApiException | |
| 128 | ||
| 129 | for x in range(len(data)): | |
| 130 | try: | |
| 131 | url = data[x]["url"] | |
| 132 | images_filename = data[x]["title"] | |
| 133 | classes = visual_recognition.classify( | |
| 134 | url=url, | |
| 135 | images_filename=images_filename, | |
| 136 | threshold='0.6', | |
| 137 | owners=["IBM"]).get_result() | |
| 138 | print("-----------------------------------------------") | |
| 139 | print("Image Title: ", data[x]["title"], "\n") | |
| 140 | print("Image URL: ", data[x]["url"], "\n") | |
| 141 | classification_results = classes["images"][0]["classifiers"][0]["classes"] | |
| 142 | for result in classification_results: | |
| 143 | print(result["class"], "(", result["score"], ")") | |
| 144 | print("-----------------------------------------------") | |
| 145 | except ApiException as ex: | |
| 146 | print("Method failed with status code " + str(ex.code) + ": " + ex.message) | |
| 147 | ``` | |
| 148 | ||
| 149 | # The Results | |
| 150 | ||
| 151 | Here we can see the full result set of our function above. If you view | |
| 152 | each of the URLs that we sent to the API, you'll be able to see that it | |
| 153 | was remarkably accurate. To be fair, these are clear high-resolution, | |
| 154 | clear photos shot with a professional camera. In reality, you will most | |
| 155 | likely be processing images that are lower quality and may have a lot of | |
| 156 | noise in the photo. | |
| 157 | ||
| 158 | However, we can clearly see the benefit of being able to call this API | |
| 159 | instead of attempting to write our own image recognition function. Each | |
| 160 | of the classifications returned was a fair description of the image. | |
| 161 | ||
| 162 | If you wanted to restrict the results to those that are at least 90% | |
| 163 | confident or greater, you would simply adjust the `threshold` | |
| 164 | in the `visual_recognition.classify()` function. | |
| 165 | ||
| 166 | When your program runs, it should show the output below for each photo | |
| 167 | you provide. | |
| 168 | ||
| 169 | ```txt | |
| 170 | ---------------------------------------------------------------- | |
| 171 | Image Title: Grizzly Bear | |
| 172 | Image URL: https://example.com/photos/image1.jpg | |
| 173 | ||
| 174 | brown bear ( 0.944 ) | |
| 175 | bear ( 1 ) | |
| 176 | carnivore ( 1 ) | |
| 177 | mammal ( 1 ) | |
| 178 | animal ( 1 ) | |
| 179 | Alaskan brown bear ( 0.759 ) | |
| 180 | greenishness color ( 0.975 ) | |
| 181 | ---------------------------------------------------------------- | |
| 182 | ``` | |
| 183 | ||
| 184 | # Discussion | |
| 185 | ||
| 186 | Now, this was a very minimal implementation of the API. We simply | |
| 187 | supplied some images and looked to see how accurate the results were. | |
| 188 | However, you could implement this type of API into many machine learning | |
| 189 | (ML) models. | |
| 190 | ||
| 191 | For example, you could be working for a company that scans their | |
| 192 | warehouses or inventory using drones. Would you want to pay employees to | |
| 193 | sit there and watch drone footage all day in order to identify or count | |
| 194 | things in the video? Probably not. Instead, you could use a | |
| 195 | classification system similar to this one in order to train your machine | |
| 196 | learning model to correctly identify items that the drones show through | |
| 197 | video. More specifically, you could have your machine learning model | |
| 198 | watch a drone fly over a field of sheep in order to count how many sheep | |
| 199 | are living in that field. | |
| 200 | ||
| 201 | There are many ways to implement machine learning functionality, but | |
| 202 | hopefully this post helped inspire some deeper thought about the tools | |
| 203 | that can help propel us further into the future of machine learning and | |
| 204 | AI. | |
content/blog/2020-09-22-internal-audit.md added +263
| @@ -0,0 +1,263 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-09-22 | |
| 3 | title = "What is Internal Audit?" | |
| 4 | description = "Learn about the Internal Audit function and their purpose." | |
| 5 | +++ | |
| 6 | ||
| 7 |  | |
| 9 | ||
| 10 | # Definitions | |
| 11 | ||
| 12 | One of the many reasons that Internal Audit needs such thorough | |
| 13 | explaining to non-auditors is that Internal Audit can serve many | |
| 14 | purposes, depending on the organization's size and needs. However, the | |
| 15 | Institute of Internal Auditors (IIA) defines Internal Auditing as: | |
| 16 | ||
| 17 | > Internal auditing is an independent, objective assurance and | |
| 18 | > consulting activity designed to add value and improve an | |
| 19 | > organization's operations. It helps an organization accomplish its | |
| 20 | > objectives by bringing a systematic, disciplined approach to evaluate | |
| 21 | > and improve the effectiveness of risk management, control, and | |
| 22 | > governance processes. | |
| 23 | ||
| 24 | However, this definition uses quite a few terms that aren't clear | |
| 25 | unless the reader already has a solid understanding of the auditing | |
| 26 | profession. To further explain, the following is a list of definitions | |
| 27 | that can help supplement understanding of internal auditing. | |
| 28 | ||
| 29 | ## Independent | |
| 30 | ||
| 31 | Independence is the freedom from conditions that threaten the ability of | |
| 32 | the internal audit activity to carry out internal audit responsibilities | |
| 33 | in an unbiased manner. To achieve the degree of independence necessary | |
| 34 | to effectively carry out the responsibilities of the internal audit | |
| 35 | activity, the chief audit executive has direct and unrestricted access | |
| 36 | to senior management and the board. This can be achieved through a | |
| 37 | dual-reporting relationship. Threats to independence must be managed at | |
| 38 | the individual auditor, engagement, functional, and organizational | |
| 39 | levels. | |
| 40 | ||
| 41 | ## Objective | |
| 42 | ||
| 43 | Objectivity is an unbiased mental attitude that allows internal auditors | |
| 44 | to perform engagements in such a manner that they believe in their work | |
| 45 | product and that no quality compromises are made. Objectivity requires | |
| 46 | that internal auditors do not subordinate their judgment on audit | |
| 47 | matters to others. Threats to objectivity must be managed at the | |
| 48 | individual auditor, engagement, functional, and organizational levels. | |
| 49 | ||
| 50 | ## Assurance | |
| 51 | ||
| 52 | Assurance services involve the internal auditor's objective assessment | |
| 53 | of evidence to provide opinions or conclusions regarding an entity, | |
| 54 | operation, function, process, system, or other subject matters. The | |
| 55 | internal auditor determines the nature and scope of an assurance | |
| 56 | engagement. Generally, three parties are participants in assurance | |
| 57 | services: (1) the person or group directly involved with the entity, | |
| 58 | operation, function, process, system, or other subject - (the process | |
| 59 | owner), (2) the person or group making the assessment - (the internal | |
| 60 | auditor), and (3) the person or group using the assessment - (the user). | |
| 61 | ||
| 62 | ## Consulting | |
| 63 | ||
| 64 | Consulting services are advisory in nature and are generally performed | |
| 65 | at the specific request of an engagement client. The nature and scope of | |
| 66 | the consulting engagement are subject to agreement with the engagement | |
| 67 | client. Consulting services generally involve two parties: (1) the | |
| 68 | person or group offering the advice (the internal auditor), and (2) the | |
| 69 | person or group seeking and receiving the advice (the engagement | |
| 70 | client). When performing consulting services, the internal auditor | |
| 71 | should maintain objectivity and not assume management responsibility. | |
| 72 | ||
| 73 | ## Governance, Risk Management, & Compliance (GRC) | |
| 74 | ||
| 75 | The integrated collection of capabilities that enable an organization to | |
| 76 | reliably achieve objectives, address uncertainty and act with integrity. | |
| 77 | ||
| 78 | # Audit Charter & Standards | |
| 79 | ||
| 80 | First, it's important to note that not every organization needs | |
| 81 | internal auditors. In fact, it's unwise for an organization to hire | |
| 82 | internal auditors unless they have regulatory requirements for auditing | |
| 83 | and have the capital to support the department. Internal audit is a cost | |
| 84 | center that can only affect revenue indirectly. | |
| 85 | ||
| 86 | Once an organization determines the need for internal assurance | |
| 87 | services, they will hire a Chief Audit Executive and create the audit | |
| 88 | charter. This charter is a document, approved by the company's | |
| 89 | governing body, that will define internal audit's purpose, authority, | |
| 90 | responsibility, and position within the organization. Fortunately, the | |
| 91 | IIA has model charters available to IIA members for those developing or | |
| 92 | improving their charter. | |
| 93 | ||
| 94 | Beyond the charter and organizational documents, internal auditors | |
| 95 | follow a few different standards in order to perform their job. First is | |
| 96 | the International Professional Practices Framework (IPPF) by the IIA, | |
| 97 | which is the model of standards for internal auditing. In addition, | |
| 98 | ISACA's Information Technology Assurance Framework (ITAF) helps guide | |
| 99 | auditors in reference to information technology (IT) compliance and | |
| 100 | assurance. Finally, additional standards such as FASB, GAAP, and | |
| 101 | industry-specific standards are used when performing internal audit | |
| 102 | work. | |
| 103 | ||
| 104 | # Three Lines of Defense | |
| 105 | ||
| 106 | [The IIA](https://theiia.org) released the original Three Lines of | |
| 107 | Defense model in 2013, but have released an updated version in 2020. | |
| 108 | Here is what the Three Lines of Defense model has historically looked | |
| 109 | like: | |
| 110 | ||
| 111 |  | |
| 113 | ||
| 114 | I won't go into depth about the changes made to the model in this | |
| 115 | article. Instead, let's take a look at the most current model. | |
| 116 | ||
| 117 |  | |
| 119 | ||
| 120 | The updated model forgets the strict idea of areas performing their own | |
| 121 | functions or line of defense. Instead of talking about management, risk, | |
| 122 | and internal audit as 1-2-3, the new model creates a more fluid and | |
| 123 | cooperative model. | |
| 124 | ||
| 125 | Looking at this model from an auditing perspective shows us that | |
| 126 | auditors will need to align, communicate, and collaborate with | |
| 127 | management, including business area managers and chief officers, as well | |
| 128 | as reporting to the governing body. The governing body will instruct | |
| 129 | internal audit *functionally* on their goals and track their progress | |
| 130 | periodically. | |
| 131 | ||
| 132 | However, the internal audit department will report *administratively* to | |
| 133 | a chief officer in the company for the purposes of collaboration, | |
| 134 | direction, and assistance with the business. Note that in most | |
| 135 | situations, the governing body is the audit committee on the company's | |
| 136 | board of directors. | |
| 137 | ||
| 138 | The result of this structure is that internal audit is an independent | |
| 139 | and objective function that can provide assurance over the topics they | |
| 140 | audit. | |
| 141 | ||
| 142 | # Audit Process | |
| 143 | ||
| 144 | A normal audit will generally follow the same process, regardless of the | |
| 145 | topic. However, certain special projects or abnormal business areas may | |
| 146 | call for changes to the audit process. The audit process is not set in | |
| 147 | stone, it's simply a set of best practices so that audits can be | |
| 148 | performed consistently. | |
| 149 | ||
| 150 |  | |
| 152 | ||
| 153 | While different organizations may tweak the process, it will generally | |
| 154 | follow this flow: | |
| 155 | ||
| 156 | ## 1. Risk Assessment | |
| 157 | ||
| 158 | The risk assessment part of the process has historically been performed | |
| 159 | annually, but many organizations have moved to performing this process | |
| 160 | much more frequently. In fact, some organizations are moving to an agile | |
| 161 | approach that can take new risks into the risk assessment and | |
| 162 | re-prioritize risk areas on-the-go. To perform a risk assessment, | |
| 163 | leaders in internal audit will research industry risks, consult with | |
| 164 | business leaders around the company, and perform analyses on company | |
| 165 | data. | |
| 166 | ||
| 167 | Once a risk assessment has been documented, the audit department has a | |
| 168 | prioritized list of risks that can be audited. This is usually in the | |
| 169 | form of auditable entities, such as business areas or departments. | |
| 170 | ||
| 171 | ## 2. Planning | |
| 172 | ||
| 173 | During the planning phase of an audit, auditors will meet with the | |
| 174 | business area to discuss the various processes, controls, and risks | |
| 175 | applicable to the business. This helps the auditors determine the scope | |
| 176 | limits for the audit, as well as timing and subject-matter experts. | |
| 177 | Certain documents will be created in this phase that will be used to | |
| 178 | keep the audit on-track an in-scope as it goes forward. | |
| 179 | ||
| 180 | ## 3. Testing | |
| 181 | ||
| 182 | The testing phase, also known as fieldwork or execution, is where | |
| 183 | internal auditors will take the information they've discovered and test | |
| 184 | it against regulations, industry standards, company rules, best | |
| 185 | practices, as well as validating that any processes are complete and | |
| 186 | accurate. For example, an audit of HR would most likely examine | |
| 187 | processes such as employee on-boarding, employee termination, security | |
| 188 | of personally identifiable information (PII), or the IT systems involved | |
| 189 | in these processes. Company standards would be examined and compared | |
| 190 | against how the processes are actually being performed day-to-day, as | |
| 191 | well as compared against regulations such as the Equal Employment | |
| 192 | Opportunity (EEO), American with Disabilities Act, and National Labor | |
| 193 | Relations Act. | |
| 194 | ||
| 195 | ## 4. Reporting | |
| 196 | ||
| 197 | Once all the tests have been completed, the audit will enter the | |
| 198 | reporting phase. This is when the audit team will conclude on the | |
| 199 | evidence they've collected, interviews they've held, and any opinions | |
| 200 | they've formed on the controls in place. A summary of the audit | |
| 201 | findings, conclusions, and specific recommendations are officially | |
| 202 | communicated to the client through a draft report. Clients have the | |
| 203 | opportunity to respond to the report and submit an action plan and time | |
| 204 | frame. These responses become part of the final report which is | |
| 205 | distributed to the appropriate level of administration. | |
| 206 | ||
| 207 | ## 5. Follow-Up | |
| 208 | ||
| 209 | After audits have been completed and management has formed action plans | |
| 210 | and time frames for audit issues, internal audit will follow up once | |
| 211 | that due date has arrived. In most cases, the follow-up will simply | |
| 212 | consist of a meeting to discuss how the action plan has been completed | |
| 213 | and to request documentation to prove it. | |
| 214 | ||
| 215 | # Audit Department Structure | |
| 216 | ||
| 217 | While an internal audit department is most often thought of as a team of | |
| 218 | full-time employees, there are actually many different ways in which a | |
| 219 | department can be structured. As the world becomes more digital and | |
| 220 | fast-paced, outsourcing has become a more attractive option for some | |
| 221 | organizations. Internal audit can be fully outsourced or partially | |
| 222 | outsourced, allowing for flexibility in cases where turnover is high. | |
| 223 | ||
| 224 | In addition, departments can implement a rotational model. This allows | |
| 225 | for interested employees around the organization to rotate into the | |
| 226 | internal audit department for a period of time, allowing them to obtain | |
| 227 | knowledge of risks and controls and allowing the internal audit team to | |
| 228 | obtain more business area knowledge. This program is popular in very | |
| 229 | large organizations, but organizations tend to rotate lower-level audit | |
| 230 | staff instead of managers. This helps prevent any significant knowledge | |
| 231 | loss as auditors rotate out to business areas. | |
| 232 | ||
| 233 | # Consulting | |
| 234 | ||
| 235 | Consulting is not an easy task at any organization, especially for a | |
| 236 | department that can have negative perceptions within the organization as | |
| 237 | the "compliance police." However, once an internal audit department | |
| 238 | has delivered value to organization, adding consulting to their suite of | |
| 239 | services is a smart move. In most cases, Internal Audit can insert | |
| 240 | themselves into a consulting role without affecting the process of | |
| 241 | project management at the company. This means that internal audit can | |
| 242 | add objective assurance and opinions to business areas as they develop | |
| 243 | new processes, instead of coming in periodically to audit an area and | |
| 244 | file issues that could have been fixed at the beginning. | |
| 245 | ||
| 246 | # Data Science & Data Analytics | |
| 247 | ||
| 248 |  | |
| 250 | ||
| 251 | One major piece of the internal audit function in the modern world is | |
| 252 | data science. While the process is data science, most auditors will | |
| 253 | refer to anything in this realm as data analytics. Hot topics such as | |
| 254 | robotic process automation (RPA), machine learning (ML), and data mining | |
| 255 | have taken over the auditing world in recent years. These technologies | |
| 256 | have been immensely helpful with increasing the effectiveness and | |
| 257 | efficiency of auditors. | |
| 258 | ||
| 259 | For example, mundane and repetitive tasks can be automated in order for | |
| 260 | auditors to make more room in their schedules for labor-intensive work. | |
| 261 | Further, auditors will need to adapt technologies like machine learning | |
| 262 | in order to extract more value from the data they're using to form | |
| 263 | conclusions. | |
blog/2020-09-25-happiness-map.org → content/blog/2020-09-25-happiness-map.md renamed +88 −80
| @@ -1,88 +1,94 @@ | ||
| 1 | #+date: 2020-09-25 | |
| 2 | #+title: Data Visualization: World Choropleth Map of Happiness | |
| 1 | +++ | |
| 2 | date = 2020-09-25 | |
| 3 | title = "Data Visualization: World Choropleth Map of Happiness" | |
| 4 | description = "Exploring and visualizing data with Python." | |
| 5 | +++ | |
| 3 | 6 | |
| 4 | * Background Information | |
| 7 | # Background Information | |
| 5 | 8 | |
| 6 | The dataset (obtained from [[https://www.kaggle.com/unsdsn/world-happiness][Kaggle]]) used in this article contains a list of | |
| 7 | countries around the world, their happiness rankings and scores, as well as | |
| 8 | other national scoring measures. | |
| 9 | The dataset (obtained from | |
| 10 | [Kaggle](https://www.kaggle.com/unsdsn/world-happiness)) used in this | |
| 11 | article contains a list of countries around the world, their happiness | |
| 12 | rankings and scores, as well as other national scoring measures. | |
| 9 | 13 | |
| 10 | 14 | Fields include: |
| 11 | 15 | |
| 12 | - Overall rank | |
| 13 | - Country or region | |
| 14 | - GDP per capita | |
| 15 | - Social support | |
| 16 | - Healthy life expectancy | |
| 17 | - Freedom to make life choices | |
| 18 | - Generosity | |
| 19 | - Perceptions of corruption | |
| 16 | - Overall rank | |
| 17 | - Country or region | |
| 18 | - GDP per capita | |
| 19 | - Social support | |
| 20 | - Healthy life expectancy | |
| 21 | - Freedom to make life choices | |
| 22 | - Generosity | |
| 23 | - Perceptions of corruption | |
| 20 | 24 | |
| 21 | There are 156 records. Since there are ~195 countries in the world, we can see | |
| 22 | that around 40 countries will be missing from this dataset. | |
| 25 | There are 156 records. Since there are \~195 countries in the world, we | |
| 26 | can see that around 40 countries will be missing from this dataset. | |
| 23 | 27 | |
| 24 | * Install Packages | |
| 28 | # Install Packages | |
| 25 | 29 | |
| 26 | As always, run the =install= command for all packages needed to perform | |
| 27 | analysis. | |
| 30 | As always, run the `install` command for all packages needed | |
| 31 | to perform analysis. | |
| 28 | 32 | |
| 29 | #+BEGIN_SRC python | |
| 33 | ```python | |
| 30 | 34 | !pip install folium geopandas matplotlib numpy pandas |
| 31 | #+END_SRC | |
| 35 | ``` | |
| 32 | 36 | |
| 33 | * Import the Data | |
| 37 | # Import the Data | |
| 34 | 38 | |
| 35 | We only need a couple packages to create a choropleth map. We will use [[https://python-visualization.github.io/folium/][Folium]], | |
| 36 | which provides map visualizations in Python. We will also use geopandas and | |
| 37 | pandas to wrangle our data before we put it on a map. | |
| 39 | We only need a couple packages to create a choropleth map. We will use | |
| 40 | [Folium](https://python-visualization.github.io/folium/), which provides | |
| 41 | map visualizations in Python. We will also use geopandas and pandas to | |
| 42 | wrangle our data before we put it on a map. | |
| 38 | 43 | |
| 39 | #+BEGIN_SRC python | |
| 44 | ```python | |
| 40 | 45 | # Import the necessary Python packages |
| 41 | 46 | import folium |
| 42 | 47 | import geopandas as gpd |
| 43 | 48 | import pandas as pd |
| 44 | #+END_SRC | |
| 49 | ``` | |
| 45 | 50 | |
| 46 | To get anything to show up on a map, we need a file that will specify the | |
| 47 | boundaries of each country. Luckily, GeoJSON files exist (for free!) on the | |
| 48 | internet. To get the boundaries of every country in the world, we will use the | |
| 49 | GeoJSON link shown below. | |
| 51 | To get anything to show up on a map, we need a file that will specify | |
| 52 | the boundaries of each country. Luckily, GeoJSON files exist (for free!) | |
| 53 | on the internet. To get the boundaries of every country in the world, we | |
| 54 | will use the GeoJSON link shown below. | |
| 50 | 55 | |
| 51 | GeoPandas will take this data and load it into a dataframe so that we can easily | |
| 52 | match it to the data we're trying to analyze. Let's look at the GeoJSON | |
| 53 | dataframe: | |
| 56 | GeoPandas will take this data and load it into a dataframe so that we | |
| 57 | can easily match it to the data we're trying to analyze. Let's look at | |
| 58 | the GeoJSON dataframe: | |
| 54 | 59 | |
| 55 | #+BEGIN_SRC python | |
| 60 | ```python | |
| 56 | 61 | # Load the GeoJSON data with geopandas |
| 57 | 62 | geo_data = gpd.read_file('https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson') |
| 58 | 63 | geo_data.head() |
| 59 | #+END_SRC | |
| 64 | ``` | |
| 60 | 65 | |
| 61 | #+CAPTION: GeoJSON Dataframe | |
| 62 | [[https://img.cleberg.net/blog/20200925-world-choropleth-map/geojson_df.png]] | |
| 66 |  | |
| 63 | 68 | |
| 64 | Next, let's load the data from the Kaggle dataset. I've downloaded this file, so | |
| 65 | update the file path if you have it somewhere else. After loading, let's take a | |
| 66 | look at this dataframe: | |
| 69 | Next, let's load the data from the Kaggle dataset. I've downloaded | |
| 70 | this file, so update the file path if you have it somewhere else. After | |
| 71 | loading, let's take a look at this dataframe: | |
| 67 | 72 | |
| 68 | #+BEGIN_SRC python | |
| 73 | ```python | |
| 69 | 74 | # Load the world happiness data with pandas |
| 70 | 75 | happy_data = pd.read_csv(r'~/Downloads/world_happiness_data_2019.csv') |
| 71 | 76 | happy_data.head() |
| 72 | #+END_SRC | |
| 77 | ``` | |
| 73 | 78 | |
| 74 | #+CAPTION: Happiness Dataframe | |
| 75 | [[https://img.cleberg.net/blog/20200925-world-choropleth-map/happiness_df.png]] | |
| 79 |  | |
| 76 | 81 | |
| 77 | * Clean the Data | |
| 82 | # Clean the Data | |
| 78 | 83 | |
| 79 | Some countries need to be renamed, or they will be lost when you merge the | |
| 80 | happiness and GeoJSON dataframes. This is something I discovered when the map | |
| 81 | below showed empty countries. I searched both data frames for the missing | |
| 82 | countries to see the naming differences. Any countries that do not have records | |
| 83 | in the =happy_data= df will not show up on the map. | |
| 84 | Some countries need to be renamed, or they will be lost when you merge | |
| 85 | the happiness and GeoJSON dataframes. This is something I discovered | |
| 86 | when the map below showed empty countries. I searched both data frames | |
| 87 | for the missing countries to see the naming differences. Any countries | |
| 88 | that do not have records in the `happy_data` df will not show | |
| 89 | up on the map. | |
| 84 | 90 | |
| 85 | #+BEGIN_SRC python | |
| 91 | ```python | |
| 86 | 92 | # Rename some countries to match our GeoJSON data |
| 87 | 93 | |
| 88 | 94 | # Rename USA |
| @@ -100,37 +106,38 @@ happy_data.at[republic_congo_index, 'Country or region'] = 'Republic of Congo' | ||
| 100 | 106 | # Rename the DRC |
| 101 | 107 | democratic_congo_index = happy_data.index[happy_data['Country or region'] == 'Congo (Kinshasa)'] |
| 102 | 108 | happy_data.at[democratic_congo_index, 'Country or region'] = 'Democratic Republic of the Congo' |
| 103 | #+END_SRC | |
| 109 | ``` | |
| 104 | 110 | |
| 105 | * Merge the Data | |
| 111 | # Merge the Data | |
| 106 | 112 | |
| 107 | 113 | Now that we have clean data, we need to merge the GeoJSON data with the |
| 108 | happiness data. Since we've stored them both in dataframes, we just need to call | |
| 109 | the =.merge()= function. | |
| 114 | happiness data. Since we've stored them both in dataframes, we just | |
| 115 | need to call the `.merge()` function. | |
| 110 | 116 | |
| 111 | We will also rename a couple columns, just so that they're a little easier to | |
| 112 | use when we create the map. | |
| 117 | We will also rename a couple columns, just so that they're a little | |
| 118 | easier to use when we create the map. | |
| 113 | 119 | |
| 114 | #+BEGIN_SRC python | |
| 120 | ```python | |
| 115 | 121 | # Merge the two previous dataframes into a single geopandas dataframe |
| 116 | 122 | merged_df = geo_data.merge(happy_data,left_on='ADMIN', right_on='Country or region') |
| 117 | 123 | |
| 118 | 124 | # Rename columns for ease of use |
| 119 | 125 | merged_df = merged_df.rename(columns = {'ADMIN':'GeoJSON_Country'}) |
| 120 | 126 | merged_df = merged_df.rename(columns = {'Country or region':'Country'}) |
| 121 | #+END_SRC | |
| 127 | ``` | |
| 122 | 128 | |
| 123 | #+CAPTION: Merged Dataframe | |
| 124 | [[https://img.cleberg.net/blog/20200925-world-choropleth-map/merged_df.png]] | |
| 129 |  | |
| 125 | 131 | |
| 126 | * Create the Map | |
| 132 | # Create the Map | |
| 127 | 133 | |
| 128 | 134 | The data is finally ready to be added to a map. The code below shows the |
| 129 | simplest way to find the center of the map and create a Folium map object. The | |
| 130 | important part is to remember to reference the merged dataframe for our GeoJSON | |
| 131 | data and value data. The columns specify which geo data and value data to use. | |
| 135 | simplest way to find the center of the map and create a Folium map | |
| 136 | object. The important part is to remember to reference the merged | |
| 137 | dataframe for our GeoJSON data and value data. The columns specify which | |
| 138 | geo data and value data to use. | |
| 132 | 139 | |
| 133 | #+BEGIN_SRC python | |
| 140 | ```python | |
| 134 | 141 | # Assign centroids to map |
| 135 | 142 | x_map = merged_df.centroid.x.mean() |
| 136 | 143 | y_map = merged_df.centroid.y.mean() |
| @@ -154,20 +161,21 @@ folium.Choropleth( | ||
| 154 | 161 | smooth_factor=0, |
| 155 | 162 | highlight=True |
| 156 | 163 | ).add_to(world_map) |
| 157 | #+END_SRC | |
| 164 | ``` | |
| 158 | 165 | |
| 159 | 166 | Let's look at the resulting map. |
| 160 | 167 | |
| 161 | #+CAPTION: Choropleth Map | |
| 162 | [[https://img.cleberg.net/blog/20200925-world-choropleth-map/map.png]] | |
| 168 |  | |
| 163 | 170 | |
| 164 | * Create a Tooltip on Hover | |
| 171 | # Create a Tooltip on Hover | |
| 165 | 172 | |
| 166 | Now that we have a map set up, we could stop. However, I want to add a tooltip | |
| 167 | so that I can see more information about each country. The =tooltip_data= code | |
| 168 | below will show a popup on hover with all the data fields shown. | |
| 173 | Now that we have a map set up, we could stop. However, I want to add a | |
| 174 | tooltip so that I can see more information about each country. The | |
| 175 | `tooltip_data` code below will show a popup on hover with all | |
| 176 | the data fields shown. | |
| 169 | 177 | |
| 170 | #+BEGIN_SRC python | |
| 178 | ```python | |
| 171 | 179 | # Adding labels to map |
| 172 | 180 | style_function = lambda x: {'fillColor': '#ffffff', |
| 173 | 181 | 'color':'#000000', |
| @@ -208,10 +216,10 @@ folium.LayerControl().add_to(world_map) | ||
| 208 | 216 | |
| 209 | 217 | # Display the map |
| 210 | 218 | world_map |
| 211 | #+END_SRC | |
| 219 | ``` | |
| 212 | 220 | |
| 213 | The final image below will show you what the tooltip looks like whenever you | |
| 214 | hover over a country. | |
| 221 | The final image below will show you what the tooltip looks like whenever | |
| 222 | you hover over a country. | |
| 215 | 223 | |
| 216 | #+CAPTION: Choropleth Map Tooltip | |
| 217 | [[https://img.cleberg.net/blog/20200925-world-choropleth-map/tooltip_map.png]] | |
| 224 |  | |
content/blog/2020-10-12-mediocrity.md added +120
| @@ -0,0 +1,120 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-10-12 | |
| 3 | title = "On the Pursuit of Mediocrity" | |
| 4 | description = "Musings on mediocrity." | |
| 5 | +++ | |
| 6 | ||
| 7 | # Perfect is the Enemy of Good | |
| 8 | ||
| 9 | As the saying goes, "the best is the enemy of the good." As we strive | |
| 10 | for perfection, we often fail to realize the implications of such an | |
| 11 | undertaking. Attempting to reach perfection is often unrealistic. Even | |
| 12 | worse, it can get in the way of achieving a good outcome. In certain | |
| 13 | situations, we try so hard to achieve the ideal solution that we have | |
| 14 | burned the bridges that would have allowed us to reach a lesser yet | |
| 15 | still superb solution. | |
| 16 | ||
| 17 | Philosophers throughout history have inspected this plight from many | |
| 18 | viewpoints. Greek mythology speaks of the [golden | |
| 19 | mean](https://en.wikipedia.org/wiki/Golden_mean_(philosophy)), which | |
| 20 | uses the story of Icarus to illustrate that sometimes "the middle | |
| 21 | course" is the best solution. In this story, Daedalus, a famous artist | |
| 22 | of his time, built feathered wings for himself and his son so that they | |
| 23 | might escape the clutches of King Minos. Daedalus warns his beloved son | |
| 24 | whom he loved so much to "fly the middle course", between the sea | |
| 25 | spray and the sun's heat. Icarus did not heed his father; he flew up | |
| 26 | and up until the sun melted the wax off his wings. For not heeding the | |
| 27 | middle course, he fell into the sea and drowned. | |
| 28 | ||
| 29 | More recently, management scholars have explored the [Pareto | |
| 30 | principle](https://en.wikipedia.org/wiki/Pareto_principle) and found | |
| 31 | that as we increase the frequency of something, or strive to perform | |
| 32 | actions to achieve some form of perfection, we run into [diminishing | |
| 33 | returns](https://en.wikipedia.org/wiki/Diminishing_returns). | |
| 34 | ||
| 35 | Even further, Harold Demsetz is noted as coining the term [the Nirvana | |
| 36 | fallacy](https://en.wikipedia.org/wiki/Nirvana_fallacy) in 1969, which | |
| 37 | shows the fallacy of comparing actual things with unrealistic, idealized | |
| 38 | alternatives. This is another trap that we may fall into, where we are | |
| 39 | constantly thinking of the ultimate solutions to problems, when | |
| 40 | something more realistic needs to be considered. | |
| 41 | ||
| 42 | Over and over throughout history, we've found that perfection is often | |
| 43 | unrealistic and unachievable. However, we push ourselves and our peers | |
| 44 | to "give 100%" or "go the extra mile," while it may be that the | |
| 45 | better course is to give a valuable level of effort while considering | |
| 46 | the effects of further effort on the outcome. Working harder does not | |
| 47 | always help us achieve loftier goals. | |
| 48 | ||
| 49 | This has presented itself to me most recently during my time studying at | |
| 50 | my university. I was anxious and feeling the stresses of my courses, | |
| 51 | career, and personal life for quite a while, which was greatly affecting | |
| 52 | how well I was doing at school and my level of effort at work. One day, | |
| 53 | I happened to be talking to my father when he said something simple that | |
| 54 | hit home: | |
| 55 | ||
| 56 | > All you can do is show up and do your best. Worrying about the | |
| 57 | > outcomes won't affect the outcome itself. | |
| 58 | ||
| 59 | The thought was extremely straightforward and uncomplicated, yet it was | |
| 60 | something that I had lost sight of during my stress-filled years at | |
| 61 | school. Ever since then, I've found myself pausing and remembering that | |
| 62 | quote every time I get anxious or stressed. It helps to stop and think | |
| 63 | "Can I do anything to affect the outcome, or am I simply worrying over | |
| 64 | something I can't change?" | |
| 65 | ||
| 66 | # When Mediocrity Isn't Enough | |
| 67 | ||
| 68 | One problem with the philosophies presented in this post is that they | |
| 69 | are implemented far too often in situations where mediocrity simply | |
| 70 | isn't adequate. For example, let's take a look at digital user data, | |
| 71 | specifically personally-identifiable information (PII). As a | |
| 72 | cybersecurity auditor in the United States, I have found that most | |
| 73 | companies are concerned more with compliance than any actual safeguards | |
| 74 | over the privacy or protection of user data. Other than companies who | |
| 75 | have built their reputation on privacy and security, most companies will | |
| 76 | use [satisficing](https://en.wikipedia.org/wiki/Satisficing) as their | |
| 77 | primary decision-making strategy around user data. | |
| 78 | ||
| 79 | > Satisficing is a decision-making strategy or cognitive heuristic that | |
| 80 | > entails searching through the available alternatives until an | |
| 81 | > acceptability threshold is met. | |
| 82 | ||
| 83 | This means that each decision will be met with certain possible | |
| 84 | solutions until one of the solutions meets their minimum acceptable | |
| 85 | standards. For companies that deal with user data, the | |
| 86 | minimum-acceptable standards come from three areas: | |
| 87 | ||
| 88 | 1. Laws and regulations | |
| 89 | 2. Competitive pressure | |
| 90 | 3. Risk of monetary or reputation loss | |
| 91 | ||
| 92 | Working with project management or auditing, the primary concern here is | |
| 93 | the risk of legal ramifications. Since the primary risk comes from laws | |
| 94 | and regulations, companies will require that any project that involves | |
| 95 | user data must follow all the rules of those laws so that the company | |
| 96 | can protect itself from fines or other penalties. | |
| 97 | ||
| 98 | Following this, companies will consider best practices in order to place | |
| 99 | itself in a competitive position (e.g. Google vs. Apple) and review any | |
| 100 | recent or ongoing litigation against companies regarding user data. In a | |
| 101 | perfect company, management would then consider the ethical | |
| 102 | responsibilities of their organization and discuss their | |
| 103 | responsibilities over things like personally-identifiable information. | |
| 104 | ||
| 105 | However, as we mentioned above, most companies follow the idea of | |
| 106 | satisficing, which states that they have met the minimum acceptable | |
| 107 | standards and can now move on to other decisions. Modern business | |
| 108 | culture in the United States dictates that profits are the golden | |
| 109 | measure of how well a company or manager is performing, so we often | |
| 110 | don't think about our responsibilities beyond these basic standards. | |
| 111 | ||
| 112 | Not all situations demand excellence, but I believe that applying any | |
| 113 | philosophy as a broad stroke across one's life can be a mistake. We | |
| 114 | must be able to think critically about what we are doing as we do it and | |
| 115 | ask ourselves a few questions. Have I done everything I can in this | |
| 116 | situation? Is mediocrity an acceptable outcome, or should we strive for | |
| 117 | perfection, even if we can't attain it? | |
| 118 | ||
| 119 | Taking a few moments to think critically throughout our day, as we make | |
| 120 | decisions, can have a tremendous effect on the outcomes we create. | |
content/blog/2020-12-27-website-redesign.md added +102
| @@ -0,0 +1,102 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-12-27 | |
| 3 | title = "Redesigning My Website: The 5 KB Result" | |
| 4 | description = "A retrospective on my recent website redesign." | |
| 5 | +++ | |
| 6 | ||
| 7 | # A Brief History | |
| 8 | ||
| 9 | As a form of continuous learning and entertainment, I've been running a | |
| 10 | handful of websites since 2016 when I took my first programming courses | |
| 11 | in college. I maintain one main website, the place I consider the | |
| 12 | official website to represent me. Under this site, I have a handful of | |
| 13 | subdirectories and subdomains. | |
| 14 | ||
| 15 | One of the parts I've enjoyed the most about web development is the | |
| 16 | aspect of designing an identity for a web page and working to find | |
| 17 | exciting ways to display the site's content. Inevitably, this means | |
| 18 | I've changed the designs for my websites more times than I could | |
| 19 | possibly count. Since I don't really host anything on my main webpage | |
| 20 | that's vital, it allows me the freedom to change things as inspiration | |
| 21 | strikes. | |
| 22 | ||
| 23 | Historically, I've relied on core utilities for spacing, components, | |
| 24 | and layouts from [Bootstrap](https://getbootstrap.com) and added custom | |
| 25 | CSS for fonts, accents, colors, and other items. I also tend to create | |
| 26 | sites with no border radius on items, visible borders, and content that | |
| 27 | takes up the entire screen (using whitespace inside components instead | |
| 28 | of whitespace around my components). | |
| 29 | ||
| 30 | # The Redesign Process | |
| 31 | ||
| 32 | About a week ago, I found myself wishing for a new design yet again. The | |
| 33 | prior design was largely inspired by IBM's [Carbon Design | |
| 34 | System](https://www.carbondesignsystem.com) and relied on jQuery, | |
| 35 | Bootstrap, along with some compressed | |
| 36 | [.webp](https://en.wikipedia.org/wiki/WebP) images. | |
| 37 | ||
| 38 | To anyone who knows my preferences toward web design - and even in my | |
| 39 | personal life - it should be no surprise that I immediately started | |
| 40 | looking for inspiration on minimalism. While there are some decent | |
| 41 | minimalistic designs on sites like | |
| 42 | [Dribbble](https://dribbble.com/search/shots/popular/web-design?q=minimalism), | |
| 43 | people seem to mostly discuss [brutalist web | |
| 44 | design](https://brutalist-web.design) when you ask about minimalism. | |
| 45 | While brutalist web design doesn't have to be minimal, it often is. | |
| 46 | ||
| 47 | I suppose, in a way, I did create a brutalist website since my HTML is | |
| 48 | semantic and accessible, hyperlinks are colored and underlined, and all | |
| 49 | native browser functions like scrolling and the back button work as | |
| 50 | expected. However, I didn't think about brutalism while designing these | |
| 51 | sites. | |
| 52 | ||
| 53 | The new design followed a simple design process. I walked through the | |
| 54 | screens on my blog and asked myself: "Is this element necessary for a | |
| 55 | user?" This allowed me to first start by removing all javascript, which | |
| 56 | had the sole purpose of allowing users to open a collapsed navbar on | |
| 57 | mobile. Replacing the collapsible navbar allowed me to remove both | |
| 58 | jQuery and Bootstrap's javascript. | |
| 59 | ||
| 60 | Next, I removed things like author names (since I'm literally the only | |
| 61 | person who will ever write on this site), multiple ways to click on a | |
| 62 | blog post card, blog post descriptions, and the scroll-to-top button. It | |
| 63 | also helped to move all categories to a single page, rather than have | |
| 64 | each category on its own page. | |
| 65 | ||
| 66 | The final big piece to finish the | |
| 67 | "[KonMari](https://en.wikipedia.org/wiki/Marie_Kondo#KonMari_method)"-like | |
| 68 | part of my process was to remove Bootstrap CSS in its entirety. However, | |
| 69 | this meant pulling out a few very useful classes, such as `.img-fluid` | |
| 70 | and the default font stacks to keep in my custom CSS. | |
| 71 | ||
| 72 | After removing all the unnecessary pieces, I was finally able to | |
| 73 | reorganize my content and add a very small amount of custom CSS to make | |
| 74 | everything pretty. This took a brief amount of time, effectively just | |
| 75 | consisting of me converting `<div>` tags into things like | |
| 76 | `<ul>` lists and choosing accent colors. | |
| 77 | ||
| 78 | # The Results | |
| 79 | ||
| 80 | ## Reflection | |
| 81 | ||
| 82 | So, what did all of this reorganizing do to my webpages? Well, first, my | |
| 83 | websites are now **ridiculously fast**. Since the prior designs were | |
| 84 | also minimal and didn't have many images, they measured up in | |
| 85 | Firefox's Network Monitor around 300 KB - 600KB. After making the | |
| 86 | changes, my main site is at 5 KB transferred (22 KB total), and my blog | |
| 87 | is at 6.5 KB transferred (13 KB total). **That means the redesigned | |
| 88 | pages are less than 2% the size of the old designs.** | |
| 89 | ||
| 90 | Google Lighthouse ranks the new webpage as 100 in performance, | |
| 91 | accessibility, and best practices, with SEO at 92 since they think tap | |
| 92 | targets are not sized appropriately for mobile users. First contextual | |
| 93 | paints of the pages are under 0.8 seconds with 0 ms of blocking time. | |
| 94 | However, the blog subdomain ranks at 100 for all four categories! First | |
| 95 | contextual paints of the blog homepage are under 1.0 seconds with 0 ms | |
| 96 | of blocking time, due to the fact that the CSS for my blog is within a | |
| 97 | separate CSS file, and the CSS for my main website is simply embedded in | |
| 98 | the HTML file. | |
| 99 | ||
| 100 | Now that everything is complete, I can confidently say I'm happy with | |
| 101 | the result and proud to look at the fastest set of websites I've | |
| 102 | created so far. | |
content/blog/2020-12-28-neon-drive.md added +94
| @@ -0,0 +1,94 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-12-28 | |
| 3 | title = "Neon Drive: A Nostalgic 80s Arcade Racing Game" | |
| 4 | description = "A video game review for Neon Drive." | |
| 5 | +++ | |
| 6 | ||
| 7 | # Game Description | |
| 8 | ||
| 9 | [Neon Drive](https://store.steampowered.com/app/433910/Neon_Drive/) | |
| 10 | presents itself as a simple arcade-style game inspired by the arcade | |
| 11 | race games of the 1980s, yet it has managed to take up hours of my life | |
| 12 | without much effort. The game description, directly from the Steam page, | |
| 13 | is intriguing enough to entice anyone who's been looking for a good | |
| 14 | arcade racing game: | |
| 15 | ||
| 16 | > Neon Drive is a slick retro-futuristic arcade game that will make your | |
| 17 | > brain melt. You've been warned. From beautiful cityscapes and ocean | |
| 18 | > roads to exploding enemy spaceships, Neon Drive has it all. | |
| 19 | ||
| 20 | # Gameplay | |
| 21 | ||
| 22 | The game holds true to the | |
| 23 | [retro-futurism](https://en.wikipedia.org/wiki/Retrofuturism) style, | |
| 24 | including chrome female robots, pixelated arcade machines, and | |
| 25 | [outrun](https://teddit.net/r/outrun/) aesthetics. | |
| 26 | ||
| 27 | Each level of the game is shown as a separate arcade machine. Each | |
| 28 | arcade machine lets you play on Normal, Hard, Insane, Practice, and Free | |
| 29 | Run. To beat each arcade, you must reach the end of the level without | |
| 30 | crashing your car into the various obstacles on the course. Basic levels | |
| 31 | let you move left or right to avoid blocks in the road. Later levels put | |
| 32 | you through other tests, such as dodging traffic or blasting asteroids. | |
| 33 | ||
| 34 | The game uses synthwave music to keep you on track to make the correct | |
| 35 | moves by timing the beats of the songs to the correct moves on the | |
| 36 | screen. It reminds me of the early Guitar Hero games, as well as mobile | |
| 37 | apps like VOEZ - repetition and staying on-beat is the only way to win. | |
| 38 | ||
| 39 | # In-Game Screenshots | |
| 40 | ||
| 41 | Taking a look at the main menu, you can see that Neon Drive plays into | |
| 42 | every stereotype you can think of around retro-futuristic, synthwave | |
| 43 | arcades (in a good way). | |
| 44 | ||
| 45 |  | |
| 47 | ||
| 48 | Once you get into the first level, we see that the choice of car fits | |
| 49 | right in with the stereotypical cars of the 80s, like the | |
| 50 | [DeLorean](https://en.wikipedia.org/wiki/DMC_DeLorean) or the [Ferrari | |
| 51 | F40](https://en.wikipedia.org/wiki/Ferrari_F40). Each new level comes | |
| 52 | with new color schemes and cars, so you should never get tired of the | |
| 53 | aesthetic. | |
| 54 | ||
| 55 |  | |
| 57 | ||
| 58 | Personally, I love the orange and blue colors used in level 2: | |
| 59 | ||
| 60 |  | |
| 62 | ||
| 63 | If you're the competitive type and getting 100% on all arcade machines | |
| 64 | isn't enough, there are leaderboards for the regular part of the game, | |
| 65 | and the endurance game mode. | |
| 66 | ||
| 67 |  | |
| 68 | ||
| 69 | # Other Suggestions | |
| 70 | ||
| 71 | Neon Drive sits nicely within the well-founded cult genre of Outrun. | |
| 72 | Other games that I've enjoyed in this same spectrum are: | |
| 73 | ||
| 74 | - [Far Cry 3: Blood | |
| 75 | Dragon](https://store.steampowered.com/app/233270/Far_Cry_3__Blood_Dragon/) | |
| 76 | - [Retrowave](https://store.steampowered.com/app/1239690/Retrowave/) | |
| 77 | - [Slipstream](https://store.steampowered.com/app/732810/Slipstream/) | |
| 78 | ||
| 79 | Although these games aren't necessarily in the same genre, they do have | |
| 80 | aspects that place them close enough to interest gamers that enjoyed | |
| 81 | Neon Drive: | |
| 82 | ||
| 83 | - [Black Ice](https://store.steampowered.com/app/311800/Black_Ice/) | |
| 84 | - [Cloudpunk](https://store.steampowered.com/app/746850/Cloudpunk/) | |
| 85 | - [Need for Speed: | |
| 86 | Heat](https://store.steampowered.com/app/1222680/Need_for_Speed_Heat/) | |
| 87 | - [VirtuaVerse](https://store.steampowered.com/app/1019310/VirtuaVerse/) | |
| 88 | ||
| 89 | Of course, if all you really care about is the arcade aspect of these | |
| 90 | games, you can check out the [Atari | |
| 91 | Vault](https://store.steampowered.com/app/400020/Atari_Vault/) or any of | |
| 92 | the other classic games sold on Steam by companies like Namco, Atari. | |
| 93 | For something like Nintendo, you'd have to settle for buying used | |
| 94 | classic consoles or delve into the world of emulation. | |
content/blog/2020-12-29-zork.md added +95
| @@ -0,0 +1,95 @@ | ||
| 1 | +++ | |
| 2 | date = 2020-12-29 | |
| 3 | title = "Zork: Let's Explore a Classic" | |
| 4 | description = "A video game review for Zork." | |
| 5 | +++ | |
| 6 | ||
| 7 | # Download (Free) | |
| 8 | ||
| 9 | Before we get into the game itself, you should know that you can | |
| 10 | download Zork for free from Infocom's [download | |
| 11 | page](http://infocom-if.org/downloads/downloads.html). So feel free to | |
| 12 | boot it up and take a ride back to the 1980s with this masterpiece. | |
| 13 | ||
| 14 | # Game Description | |
| 15 | ||
| 16 | Zork is an interactive, text-based computer game originally released in | |
| 17 | 1980. This series, split into three separate games, introduced a robust | |
| 18 | and sophisticated text parser to gamers. People were largely used to the | |
| 19 | simple commands used in the popular game [Colossal Cave | |
| 20 | Adventure](https://en.wikipedia.org/wiki/Colossal_Cave_Adventure), but | |
| 21 | Zork allowed users to send more complex commands that included | |
| 22 | prepositions and conjunctions. | |
| 23 | ||
| 24 | Zork tracks your score as you explore the map, find tools, and collect | |
| 25 | trophy items (e.g., a jewel-encrusted egg). When you place your trophy | |
| 26 | items in the trophy case found in the Living Room area, you gain score | |
| 27 | points. Collecting the Twenty Treasures of Zork and placing them within | |
| 28 | the trophy case wins the game. However, you must explore the map, solve | |
| 29 | puzzles, and avoid being eaten by a grue to collect these treasures. | |
| 30 | ||
| 31 | # The Map | |
| 32 | ||
| 33 | Since Zork is a vast and complex game, it helps to have a map as you | |
| 34 | explore and collect your trophies. However, if you want to play the game | |
| 35 | as it was truly intended, you should try to play it without using the | |
| 36 | map. | |
| 37 | ||
| 38 |  | |
| 39 | ||
| 40 | *[Map Source](https://www.filfre.net/2012/01/exploring-zork-part-1/)* | |
| 41 | ||
| 42 | # In-Game Screenshots | |
| 43 | ||
| 44 | After playing the game (for the first time ever) for several weeks | |
| 45 | around 2014, I was finally able to beat the game with some online help | |
| 46 | to find the last couple items. As I was writing this post, I installed | |
| 47 | the game again to grab some screenshots to show off the true glory of | |
| 48 | this game. As noted in [Jimmy Maher's | |
| 49 | playthrough](https://www.filfre.net/2012/01/exploring-zork-part-1/), the | |
| 50 | original Zork games looked quite a bit different due to the older | |
| 51 | hardware of computers like the Apple II and multiple bug fixes that | |
| 52 | Infocom pushed out after the game's initial release. My play-through | |
| 53 | uses the [Zork | |
| 54 | Anthology](https://store.steampowered.com/app/570580/Zork_Anthology/) | |
| 55 | version, which utilizes DOSBox on Windows. | |
| 56 | ||
| 57 | The first screenshot here shows the introductory information, which | |
| 58 | doesn't include instructions of any kind for the player. If you | |
| 59 | haven't played text adventures before, try to use simple commands like | |
| 60 | "go west," "look around," or "hit troll with elvish sword." | |
| 61 | ||
| 62 |  | |
| 64 | ||
| 65 | In this second screenshot, we see the player has entered the house and | |
| 66 | found the trophy case in the living room. The lantern and sword in this | |
| 67 | room allow the player to explore dark areas and attack enemies. If you | |
| 68 | don't use the lantern, you won't be able to see anything in dark | |
| 69 | areas, and you may be eaten by a grue. | |
| 70 | ||
| 71 |  | |
| 73 | ||
| 74 | Finally, we see that the player has found the first treasure: a | |
| 75 | jewel-encrusted egg. These treasures can be taken back to the house and | |
| 76 | placed in the trophy case or carried until you feel like you want to put | |
| 77 | things away. | |
| 78 | ||
| 79 |  | |
| 81 | ||
| 82 | # Conclusion | |
| 83 | ||
| 84 | It's been quite a few years since I first played Zork, but I clearly | |
| 85 | remember the late nights and bloodshot eyes that helped me find all the | |
| 86 | treasures. This game is well worth the time and effort, even though the | |
| 87 | text-based aspect may be off-putting to gamers who didn't have to grow | |
| 88 | up playing games without graphics. However, I believe that the strategy | |
| 89 | and skills learned in early video games like Zork can actually help you, | |
| 90 | even when playing newer games. | |
| 91 | ||
| 92 | If you do decide to play Zork, you can download Zork I, II, and III from | |
| 93 | Infocom's [download | |
| 94 | page](http://infocom-if.org/downloads/downloads.html) for free or search | |
| 95 | the internet for an online version. | |
blog/2021-01-01-seum.org → content/blog/2021-01-01-seum.md renamed +42 −57
| @@ -1,32 +1,27 @@ | ||
| 1 | #+title: SEUM: Speedrunners from Hell | |
| 2 | #+date: 2021-01-01 | |
| 3 | ||
| 4 | ** Game Description | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: game-description | |
| 7 | :ID: 809DA4E6-4E0D-4179-B8FF-6C9E767EC74D | |
| 8 | :END: | |
| 9 | [[https://store.steampowered.com/app/457210/SEUM_Speedrunners_from_Hell/][SEUM: | |
| 10 | Speedrunners from Hell]] is an incredibly fast-paced mashup of a puzzle | |
| 11 | game and a free-running game. Created by [[https://pinestudio.co][Pine | |
| 12 | Studio]] in early 2016 and first released as a | |
| 13 | [[https://gamejolt.com/games/seum-speedrunners-from-hell-demo/154868][demo | |
| 14 | on GameJolt]], this game was successfully green-lit on Steam and has | |
| 15 | amassed a cult following on multiple consoles. | |
| 1 | +++ | |
| 2 | date = 2021-01-01 | |
| 3 | title = "SEUM: Speedrunners from Hell" | |
| 4 | description = "A video game review for SEUM: Speedrunners from Hell." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Game Description {#game-description id="809DA4E6-4E0D-4179-B8FF-6C9E767EC74D"} | |
| 8 | ||
| 9 | [SEUM: Speedrunners from | |
| 10 | Hell](https://store.steampowered.com/app/457210/SEUM_Speedrunners_from_Hell/) | |
| 11 | is an incredibly fast-paced mashup of a puzzle game and a free-running | |
| 12 | game. Created by [Pine Studio](https://pinestudio.co) in early 2016 and | |
| 13 | first released as a [demo on | |
| 14 | GameJolt](https://gamejolt.com/games/seum-speedrunners-from-hell-demo/154868), | |
| 15 | this game was successfully green-lit on Steam and has amassed a cult | |
| 16 | following on multiple consoles. | |
| 16 | 17 | |
| 17 | 18 | Here's the game description straight from the developers: |
| 18 | 19 | |
| 19 | #+begin_quote | |
| 20 | Like a bastard child of Quake 3 and Super Meat Boy, SEUM: Speedrunners | |
| 21 | from Hell is truly hardcore and focuses on speed and fast reaction. | |
| 20 | > Like a bastard child of Quake 3 and Super Meat Boy, SEUM: Speedrunners | |
| 21 | > from Hell is truly hardcore and focuses on speed and fast reaction. | |
| 22 | 22 | |
| 23 | #+end_quote | |
| 23 | ## Story {#story id="BF401145-763D-4399-922B-7D73322B5B40"} | |
| 24 | 24 | |
| 25 | ** Story | |
| 26 | :PROPERTIES: | |
| 27 | :CUSTOM_ID: story | |
| 28 | :ID: BF401145-763D-4399-922B-7D73322B5B40 | |
| 29 | :END: | |
| 30 | 25 | SEUM does a great job setting the stage when you open the game for the |
| 31 | 26 | first time, playing an animated video in the form of comic book images. |
| 32 | 27 | You see Marty, the protagonist, sitting around drinking his beer as |
| @@ -38,14 +33,10 @@ use all the powers in the game. | ||
| 38 | 33 | Check out the screenshot below for one of the first panels of the |
| 39 | 34 | storyline: |
| 40 | 35 | |
| 41 | #+caption: SEUM Story | |
| 42 | [[https://img.cleberg.net/blog/20210101-seum/seum_story.png]] | |
| 36 |  | |
| 37 | ||
| 38 | ## Gameplay {#gameplay id="A55CB1AF-E818-4CA7-BF69-E8141369C269"} | |
| 43 | 39 | |
| 44 | ** Gameplay | |
| 45 | :PROPERTIES: | |
| 46 | :CUSTOM_ID: gameplay | |
| 47 | :ID: A55CB1AF-E818-4CA7-BF69-E8141369C269 | |
| 48 | :END: | |
| 49 | 40 | To accomplish each level, you will need to get to the final blue portal |
| 50 | 41 | under a certain time limit. You can beat a level by getting to the |
| 51 | 42 | portal before the time meter runs out or "Dominate" a level by beating |
| @@ -58,50 +49,44 @@ to go through any existing orange portals, light all yellow beacons, | ||
| 58 | 49 | avoid things like fireballs and blades, or use any satanic power orbs |
| 59 | 50 | lying around. These special abilities include: |
| 60 | 51 | |
| 61 | - Gravity | |
| 62 | - Teleport | |
| 63 | - Rewind | |
| 64 | - Spawn platform | |
| 65 | - Roar (DLC) | |
| 66 | - Rocket (DLC) | |
| 67 | - Shadow world (DLC) | |
| 52 | - Gravity | |
| 53 | - Teleport | |
| 54 | - Rewind | |
| 55 | - Spawn platform | |
| 56 | - Roar (DLC) | |
| 57 | - Rocket (DLC) | |
| 58 | - Shadow world (DLC) | |
| 68 | 59 | |
| 69 | 60 | For the main storyline, there are nine floors to beat. Each floor |
| 70 | 61 | contains nine regular levels, one boss level, and one bonus level; |
| 71 | although you don't technically need to beat all levels to advance to the | |
| 72 | next floor. | |
| 62 | although you don't technically need to beat all levels to advance to | |
| 63 | the next floor. | |
| 64 | ||
| 65 |  | |
| 73 | 67 | |
| 74 | #+caption: SEUM Floor Menu | |
| 75 | [[https://img.cleberg.net/blog/20210101-seum/seum_floor.png]] | |
| 68 | ## In-Game Screenshots {#in-game-screenshots id="C779F008-4C8A-4BA4-AA31-60A1BF5A3EE3"} | |
| 76 | 69 | |
| 77 | ** In-Game Screenshots | |
| 78 | :PROPERTIES: | |
| 79 | :CUSTOM_ID: in-game-screenshots | |
| 80 | :ID: C779F008-4C8A-4BA4-AA31-60A1BF5A3EE3 | |
| 81 | :END: | |
| 82 | 70 | The main menu gives you plenty of options for tuning your system, |
| 83 | 71 | playing main levels, playing the DLC, or exploring the other game modes. |
| 84 | 72 | |
| 85 | #+caption: SEUM Main Menu | |
| 86 | [[https://img.cleberg.net/blog/20210101-seum/seum_menu.png]] | |
| 73 |  | |
| 87 | 75 | |
| 88 | 76 | Once you enter a level, you can look around and strategize before |
| 89 | 77 | starting. Clicking any button will start the menu, and you'll have to |
| 90 | 78 | restart if you die. One of the great things about SEUM is that it has |
| 91 | great keyboard shortcuts. You can quickly smash =R= to restart the level | |
| 92 | or =M= to return to the level menu. | |
| 79 | great keyboard shortcuts. You can quickly smash `R` to | |
| 80 | restart the level or `M` to return to the level menu. | |
| 93 | 81 | |
| 94 | #+caption: SEUM Level | |
| 95 | [[https://img.cleberg.net/blog/20210101-seum/seum_level.png]] | |
| 82 |  | |
| 96 | 83 | |
| 97 | 84 | Once you're able to win a level, you'll see the high scores and can |
| 98 | 85 | watch replays of the best scores. |
| 99 | 86 | |
| 100 | #+caption: SEUM Win | |
| 101 | [[https://img.cleberg.net/blog/20210101-seum/seum_win.png]] | |
| 87 |  | |
| 102 | 88 | |
| 103 | 89 | Each regular level contains a beer in a disguised location that may take |
| 104 | 90 | some out-of-the-box thinking. |
| 105 | 91 | |
| 106 | #+caption: SEUM Beer | |
| 107 | [[https://img.cleberg.net/blog/20210101-seum/seum_beer.png]] | |
| 92 |  | |
blog/2021-01-04-fediverse.org → content/blog/2021-01-04-fediverse.md renamed +58 −75
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: A Simple Guide to the Fediverse | |
| 2 | #+date: 2021-01-04 | |
| 1 | +++ | |
| 2 | date = 2021-01-04 | |
| 3 | title = "A Simple Guide to the Fediverse" | |
| 4 | description = "Learn about the basics of the Fediverse." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## What is the Fediverse? | |
| 3 | 8 | |
| 4 | ** What is the Fediverse? | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: what-is-the-fediverse | |
| 7 | :END: | |
| 8 | 9 | The fediverse is a federated universe of servers commonly used for |
| 9 | 10 | sharing content, like social media. So, instead of having to rely on a |
| 10 | 11 | single organization to run the server (e.g. Facebook), the fediverse is |
| @@ -21,8 +22,8 @@ and communicate back and forth without having to be on the same server. | ||
| 21 | 22 | However, responsible email admins are there to set rules and control the |
| 22 | 23 | traffic going in/out of the server. |
| 23 | 24 | |
| 24 | #+caption: Federated services diagram | |
| 25 | [[https://img.cleberg.net/blog/20210104-a-simple-guide-to-the-fediverse/federated-example.svg]] | |
| 25 |  | |
| 26 | 27 | |
| 27 | 28 | The main objective of this architecture is to decentralize the control |
| 28 | 29 | within the internet connections. For example, if you run your own |
| @@ -35,12 +36,13 @@ This strategy is great for making sure control of the social web isn't | ||
| 35 | 36 | controlled by a single organization, but it also has some downsides. If |
| 36 | 37 | I create a Mastodon instance and get a ton of users to sign up, I can |
| 37 | 38 | shut the server down at any time. That means you're at risk of losing |
| 38 | the content you've created unless you back it up, or the server backs it | |
| 39 | up for you. Also, depending on the software used (e.g. Mastodon, | |
| 39 | the content you've created unless you back it up, or the server backs | |
| 40 | it up for you. Also, depending on the software used (e.g. Mastodon, | |
| 40 | 41 | Pixelfed, etc.), censorship may still be an issue if the server admins |
| 41 | 42 | decide they want to censor their users. Now, censorship isn't always a |
| 42 | bad thing and can even benefit the community as a whole, but you'll want | |
| 43 | to determine which servers align with your idea of proper censorship. | |
| 43 | bad thing and can even benefit the community as a whole, but you'll | |
| 44 | want to determine which servers align with your idea of proper | |
| 45 | censorship. | |
| 44 | 46 | |
| 45 | 47 | However, these are risks that we take when we sign up for any online |
| 46 | 48 | platform. Whatever your reason is for trying out federated social |
| @@ -50,74 +52,55 @@ with the increased difficulty understanding and signing up for these | ||
| 50 | 52 | platforms. Perhaps increased regulation and litigation against current |
| 51 | 53 | social media sites will push more users into the fediverse. |
| 52 | 54 | |
| 53 | ** Federated Alternatives to Popular Sites | |
| 54 | :PROPERTIES: | |
| 55 | :CUSTOM_ID: federated-alternatives-to-popular-sites | |
| 56 | :END: | |
| 55 | ## Federated Alternatives to Popular Sites | |
| 56 | ||
| 57 | 57 | The list below is a small guide that will show you federated |
| 58 | 58 | alternatives to current popular websites. There are many more out there, |
| 59 | 59 | so go and explore: you might just find the perfect home. |
| 60 | 60 | |
| 61 | ||
| 62 | :PROPERTIES: | |
| 63 | :CUSTOM_ID: reddit | |
| 64 | :END: | |
| 65 | - [[https://lemmy.ml/instances][Lemmy]] | |
| 66 | ||
| 67 | *** Twitter/Facebook/Tumblr | |
| 68 | :PROPERTIES: | |
| 69 | :CUSTOM_ID: twitterfacebooktumblr | |
| 70 | :END: | |
| 71 | - [[https://joinmastodon.org][Mastodon]] | |
| 72 | - [[https://diasporafoundation.org][Diaspora]] | |
| 73 | - [[https://friendi.ca][Friendica]] | |
| 74 | - [[https://gnusocial.network][GNU Social]] | |
| 75 | - [[https://pleroma.social][Pleroma]] | |
| 76 | ||
| 77 | ||
| 78 | :PROPERTIES: | |
| 79 | :CUSTOM_ID: instagram | |
| 80 | :END: | |
| 81 | - [[https://pixelfed.org][Pixelfed]] | |
| 82 | ||
| 83 | *** Slack/Discord | |
| 84 | :PROPERTIES: | |
| 85 | :CUSTOM_ID: slackdiscord | |
| 86 | :END: | |
| 87 | - [[https://element.io][Matrix]] | |
| 88 | ||
| 89 | *** Youtube/Vimeo | |
| 90 | :PROPERTIES: | |
| 91 | :CUSTOM_ID: youtubevimeo | |
| 92 | :END: | |
| 93 | - [[https://joinpeertube.org][Peertube]] | |
| 94 | ||
| 95 | *** Spotify/Soundcloud | |
| 96 | :PROPERTIES: | |
| 97 | :CUSTOM_ID: spotifysoundcloud | |
| 98 | :END: | |
| 99 | - [[https://funkwhale.audio][Funkwhale]] | |
| 100 | ||
| 101 | *** Podcasting | |
| 102 | :PROPERTIES: | |
| 103 | :CUSTOM_ID: podcasting | |
| 104 | :END: | |
| 105 | - [[https://pubcast.pub][Pubcast]] | |
| 106 | ||
| 107 | *** Medium/Blogger | |
| 108 | :PROPERTIES: | |
| 109 | :CUSTOM_ID: mediumblogger | |
| 110 | :END: | |
| 111 | - [[https://writefreely.org][WriteFreely]] | |
| 112 | ||
| 113 | ** Get Started | |
| 114 | :PROPERTIES: | |
| 115 | :CUSTOM_ID: get-started | |
| 116 | :END: | |
| 61 | ||
| 62 | ||
| 63 | - [Lemmy](https://lemmy.ml/instances) | |
| 64 | ||
| 65 | ### Twitter/Facebook/Tumblr | |
| 66 | ||
| 67 | - [Mastodon](https://joinmastodon.org) | |
| 68 | - [Diaspora](https://diasporafoundation.org) | |
| 69 | - [Friendica](https://friendi.ca) | |
| 70 | - [GNU Social](https://gnusocial.network) | |
| 71 | - [Pleroma](https://pleroma.social) | |
| 72 | ||
| 73 | ||
| 74 | ||
| 75 | - [Pixelfed](https://pixelfed.org) | |
| 76 | ||
| 77 | ### Slack/Discord | |
| 78 | ||
| 79 | - [Matrix](https://element.io) | |
| 80 | ||
| 81 | ### Youtube/Vimeo | |
| 82 | ||
| 83 | - [Peertube](https://joinpeertube.org) | |
| 84 | ||
| 85 | ### Spotify/Soundcloud | |
| 86 | ||
| 87 | - [Funkwhale](https://funkwhale.audio) | |
| 88 | ||
| 89 | ### Podcasting | |
| 90 | ||
| 91 | - [Pubcast](https://pubcast.pub) | |
| 92 | ||
| 93 | ### Medium/Blogger | |
| 94 | ||
| 95 | - [WriteFreely](https://writefreely.org) | |
| 96 | ||
| 97 | ## Get Started | |
| 98 | ||
| 117 | 99 | The best way to get started is to simply sign up and learn as you go. If |
| 118 | 100 | you're comfortable signing up through a Mastodon, Pleroma, or Friendica |
| 119 | server, here is [[https://fediverse.party/en/portal/servers][a list of | |
| 120 | themed servers]] to choose from. If you're looking for something else, | |
| 121 | try a web search for a federated alternative to your favorite sites. | |
| 101 | server, here is [a list of themed | |
| 102 | servers](https://fediverse.party/en/portal/servers) to choose from. If | |
| 103 | you're looking for something else, try a web search for a federated | |
| 104 | alternative to your favorite sites. | |
| 122 | 105 | |
| 123 | 106 | Find a server that focuses on your passions and start there! |
blog/2021-01-07-ufw.org → content/blog/2021-01-07-ufw.md renamed +86 −100
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: Secure Your Network with the Uncomplicated Firewall | |
| 2 | #+date: 2021-01-07 | |
| 1 | +++ | |
| 2 | date = 2021-01-07 | |
| 3 | title = "Secure Your Network with the Uncomplicated Firewall" | |
| 4 | description = "A simple guide to the UFW." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Uncomplicated Firewall | |
| 3 | 8 | |
| 4 | ** Uncomplicated Firewall | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: uncomplicated-firewall | |
| 7 | :END: | |
| 8 | 9 | Uncomplicated Firewall, also known as ufw, is a convenient and |
| 9 | 10 | beginner-friendly way to enforce OS-level firewall rules. For those who |
| 10 | 11 | are hosting servers or any device that is accessible to the world (i.e., |
| @@ -14,160 +15,147 @@ implemented and active. | ||
| 14 | 15 | Ufw is available by default in all Ubuntu installations after 8.04 LTS. |
| 15 | 16 | For other distributions, you can look to install ufw or check if there |
| 16 | 17 | are alternative firewalls installed already. There are usually |
| 17 | alternatives available, such as Fedora's =firewall= and the package | |
| 18 | available on most distributions: =iptables=. Ufw is considered a | |
| 19 | beginner-friendly front-end to iptables. | |
| 18 | alternatives available, such as Fedora's `firewall` and the | |
| 19 | package available on most distributions: `iptables`. Ufw is | |
| 20 | considered a beginner-friendly front-end to iptables. | |
| 20 | 21 | |
| 21 | [[https://gufw.org][Gufw]] is available as a graphical user interface | |
| 22 | [Gufw](https://gufw.org) is available as a graphical user interface | |
| 22 | 23 | (GUI) application for users who are uncomfortable setting up a firewall |
| 23 | 24 | through a terminal. |
| 24 | 25 | |
| 25 | [[https://img.cleberg.net/blog/20210107-secure-your-network-with-the-uncomplicated-firewall/gufw.png]] | |
| 26 |  | |
| 27 | ||
| 28 | ## Getting Help | |
| 26 | 29 | |
| 27 | ** Getting Help | |
| 28 | :PROPERTIES: | |
| 29 | :CUSTOM_ID: getting-help | |
| 30 | :END: | |
| 31 | 30 | If you need help figuring out commands, remember that you can run the |
| 32 | =--help= flag to get a list of options. | |
| 31 | `--help` flag to get a list of options. | |
| 33 | 32 | |
| 34 | #+begin_src sh | |
| 33 | ```sh | |
| 35 | 34 | sudo ufw --help |
| 36 | #+end_src | |
| 35 | ``` | |
| 36 | ||
| 37 | ## Set Default State | |
| 37 | 38 | |
| 38 | ** Set Default State | |
| 39 | :PROPERTIES: | |
| 40 | :CUSTOM_ID: set-default-state | |
| 41 | :END: | |
| 42 | 39 | The proper way to run a firewall is to set a strict default state and |
| 43 | 40 | slowly open up ports that you want to allow. This helps prevent anything |
| 44 | 41 | malicious from slipping through the cracks. The following command |
| 45 | 42 | prevents all incoming traffic (other than the rules we specify later), |
| 46 | 43 | but you can also set this for outgoing connections, if necessary. |
| 47 | 44 | |
| 48 | #+begin_src sh | |
| 45 | ```sh | |
| 49 | 46 | sudo ufw default deny incoming |
| 50 | #+end_src | |
| 47 | ``` | |
| 51 | 48 | |
| 52 | 49 | You should also allow outgoing traffic if you want to allow the device |
| 53 | 50 | to communicate back to you or other parties. For example, media servers |
| 54 | 51 | like Plex need to be able to send out data related to streaming the |
| 55 | 52 | media. |
| 56 | 53 | |
| 57 | #+begin_src sh | |
| 54 | ```sh | |
| 58 | 55 | sudo ufw default allow outgoing |
| 59 | #+end_src | |
| 56 | ``` | |
| 60 | 57 | |
| 61 | ** Adding Port Rules | |
| 62 | :PROPERTIES: | |
| 63 | :CUSTOM_ID: adding-port-rules | |
| 64 | :END: | |
| 65 | Now that we've disabled all incoming traffic by default, we need to open | |
| 66 | up some ports (or else no traffic would be able to come in). If you need | |
| 67 | to be able to =ssh= into the machine, you'll need to open up port 22. | |
| 58 | ## Adding Port Rules | |
| 68 | 59 | |
| 69 | #+begin_src sh | |
| 60 | Now that we've disabled all incoming traffic by default, we need to | |
| 61 | open up some ports (or else no traffic would be able to come in). If you | |
| 62 | need to be able to `ssh` into the machine, you'll need to | |
| 63 | open up port 22. | |
| 64 | ||
| 65 | ```sh | |
| 70 | 66 | sudo ufw allow 22 |
| 71 | #+end_src | |
| 67 | ``` | |
| 72 | 68 | |
| 73 | 69 | You can also issue more restrictive rules. The following rule will allow |
| 74 | =ssh= connections only from machines on the local subnet. | |
| 70 | `ssh` connections only from machines on the local subnet. | |
| 75 | 71 | |
| 76 | #+begin_src sh | |
| 72 | ```sh | |
| 77 | 73 | sudo ufw allow proto tcp from 192.168.0.0/24 to any port 22 |
| 78 | #+end_src | |
| 74 | ``` | |
| 79 | 75 | |
| 80 | 76 | If you need to set a rule that isn't tcp, just append your connection |
| 81 | 77 | type to the end of the rule. |
| 82 | 78 | |
| 83 | #+begin_src sh | |
| 79 | ```sh | |
| 84 | 80 | sudo ufw allow 1900/udp |
| 85 | #+end_src | |
| 81 | ``` | |
| 82 | ||
| 83 | ## Enable ufw | |
| 86 | 84 | |
| 87 | ** Enable ufw | |
| 88 | :PROPERTIES: | |
| 89 | :CUSTOM_ID: enable-ufw | |
| 90 | :END: | |
| 91 | 85 | Now that the firewall is configured and ready to go, you can enable the |
| 92 | 86 | firewall. |
| 93 | 87 | |
| 94 | #+begin_src sh | |
| 88 | ```sh | |
| 95 | 89 | sudo ufw enable |
| 96 | #+end_src | |
| 90 | ``` | |
| 97 | 91 | |
| 98 | 92 | A restart may be required for the firewall to begin operating. |
| 99 | 93 | |
| 100 | #+begin_src sh | |
| 94 | ```sh | |
| 101 | 95 | sudo reboot now |
| 102 | #+end_src | |
| 96 | ``` | |
| 97 | ||
| 98 | ## Checking Status | |
| 103 | 99 | |
| 104 | ** Checking Status | |
| 105 | :PROPERTIES: | |
| 106 | :CUSTOM_ID: checking-status | |
| 107 | :END: | |
| 108 | 100 | Now that the firewall is enabled, let's check and see what the rules |
| 109 | 101 | look like. |
| 110 | 102 | |
| 111 | #+begin_src sh | |
| 103 | ```sh | |
| 112 | 104 | sudo ufw status numbered |
| 113 | #+end_src | |
| 105 | ``` | |
| 114 | 106 | |
| 115 | #+begin_src txt | |
| 107 | ```txt | |
| 116 | 108 | Status: active |
| 117 | 109 | |
| 118 | 110 | To Action From |
| 119 | 111 | -- ------ ---- |
| 120 | 112 | [ 1] 22 ALLOW IN Anywhere |
| 121 | 113 | [ 2] 22 (v6) ALLOW IN Anywhere (v6) |
| 122 | #+end_src | |
| 114 | ``` | |
| 115 | ||
| 116 | ## Deleting Rules | |
| 123 | 117 | |
| 124 | ** Deleting Rules | |
| 125 | :PROPERTIES: | |
| 126 | :CUSTOM_ID: deleting-rules | |
| 127 | :END: | |
| 128 | 118 | If you need to delete a rule, you need to know the number associated |
| 129 | 119 | with that rule. Let's delete the first rule in the table above. You'll |
| 130 | 120 | be asked to confirm the deletion as part of this process. |
| 131 | 121 | |
| 132 | #+begin_src sh | |
| 122 | ```sh | |
| 133 | 123 | sudo ufw delete 1 |
| 134 | #+end_src | |
| 124 | ``` | |
| 125 | ||
| 126 | ## Managing App Rules | |
| 135 | 127 | |
| 136 | ** Managing App Rules | |
| 137 | :PROPERTIES: | |
| 138 | :CUSTOM_ID: managing-app-rules | |
| 139 | :END: | |
| 140 | 128 | Luckily, there's a convenient way for installed applications to create |
| 141 | files that ufw can easily implement so that you don't have to search and | |
| 142 | find which ports your application requires. To see if your device has | |
| 143 | any applications with pre-installed ufw rules, execute the following | |
| 129 | files that ufw can easily implement so that you don't have to search | |
| 130 | and find which ports your application requires. To see if your device | |
| 131 | has any applications with pre-installed ufw rules, execute the following | |
| 144 | 132 | command: |
| 145 | 133 | |
| 146 | #+begin_src sh | |
| 134 | ```sh | |
| 147 | 135 | sudo ufw app list |
| 148 | #+end_src | |
| 136 | ``` | |
| 149 | 137 | |
| 150 | 138 | The results should look something like this: |
| 151 | 139 | |
| 152 | #+begin_src txt | |
| 140 | ```txt | |
| 153 | 141 | Available applications: |
| 154 | 142 | OpenSSH |
| 155 | 143 | Samba |
| 156 | 144 | plexmediaserver |
| 157 | 145 | plexmediaserver-all |
| 158 | 146 | plexmediaserver-dlna |
| 159 | #+end_src | |
| 147 | ``` | |
| 160 | 148 | |
| 161 | 149 | If you want to get more information on a specific app rule, use the |
| 162 | =info= command. | |
| 150 | `info` command. | |
| 163 | 151 | |
| 164 | #+begin_src sh | |
| 152 | ```sh | |
| 165 | 153 | sudo ufw app info plexmediaserver-dlna |
| 166 | #+end_src | |
| 154 | ``` | |
| 167 | 155 | |
| 168 | 156 | You'll get a blurb of info back like this: |
| 169 | 157 | |
| 170 | #+begin_src txt | |
| 158 | ```txt | |
| 171 | 159 | Profile: plexmediaserver-dlna |
| 172 | 160 | Title: Plex Media Server (DLNA) |
| 173 | 161 | Description: The Plex Media Server (additional DLNA capability only) |
| @@ -175,31 +163,29 @@ Description: The Plex Media Server (additional DLNA capability only) | ||
| 175 | 163 | Ports: |
| 176 | 164 | 1900/udp |
| 177 | 165 | 32469/tcp |
| 178 | #+end_src | |
| 166 | ``` | |
| 179 | 167 | |
| 180 | 168 | You can add or delete app rules the same way that you'd add or delete |
| 181 | 169 | specific port rules. |
| 182 | 170 | |
| 183 | #+begin_src sh | |
| 171 | ```sh | |
| 184 | 172 | sudo ufw allow plexmediaserver-dlna |
| 185 | #+end_src | |
| 173 | ``` | |
| 186 | 174 | |
| 187 | #+begin_src sh | |
| 175 | ```sh | |
| 188 | 176 | sudo ufw delete RULE|NUM |
| 189 | #+end_src | |
| 177 | ``` | |
| 178 | ||
| 179 | ## Creating App Rules | |
| 190 | 180 | |
| 191 | ** Creating App Rules | |
| 192 | :PROPERTIES: | |
| 193 | :CUSTOM_ID: creating-app-rules | |
| 194 | :END: | |
| 195 | 181 | If you'd like to create you own app rule, you'll need to create a file |
| 196 | in the =/etc/ufw/applications.d= directory. Within the file you create, | |
| 197 | you need to make sure the content is properly formatted. | |
| 182 | in the `/etc/ufw/applications.d` directory. Within the file | |
| 183 | you create, you need to make sure the content is properly formatted. | |
| 198 | 184 | |
| 199 | For example, here are the contents my =plexmediaserver= file, which | |
| 200 | creates three distinct app rules for ufw: | |
| 185 | For example, here are the contents my `plexmediaserver` file, | |
| 186 | which creates three distinct app rules for ufw: | |
| 201 | 187 | |
| 202 | #+begin_src config | |
| 188 | ```config | |
| 203 | 189 | [plexmediaserver] |
| 204 | 190 | title=Plex Media Server (Standard) |
| 205 | 191 | description=The Plex Media Server |
| @@ -214,24 +200,24 @@ ports=1900/udp|32469/tcp | ||
| 214 | 200 | title=Plex Media Server (Standard + DLNA) |
| 215 | 201 | description=The Plex Media Server (with additional DLNA capability) |
| 216 | 202 | ports=32400/tcp|3005/tcp|5353/udp|8324/tcp|32410:32414/udp|1900/udp|32469/tcp |
| 217 | #+end_src | |
| 203 | ``` | |
| 218 | 204 | |
| 219 | So, if I wanted to create a custom app rule called "mycustomrule," I'd | |
| 220 | create a file and add my content like this: | |
| 205 | So, if I wanted to create a custom app rule called "mycustomrule," | |
| 206 | I'd create a file and add my content like this: | |
| 221 | 207 | |
| 222 | #+begin_src sh | |
| 208 | ```sh | |
| 223 | 209 | sudo nano /etc/ufw/applications.d/mycustomrule |
| 224 | #+end_src | |
| 210 | ``` | |
| 225 | 211 | |
| 226 | #+begin_src config | |
| 212 | ```config | |
| 227 | 213 | [mycustomrule] |
| 228 | 214 | title=My Custom Rule |
| 229 | 215 | description=This is a temporary ufw app rule. |
| 230 | 216 | ports=88/tcp|9100/udp |
| 231 | #+end_src | |
| 217 | ``` | |
| 232 | 218 | |
| 233 | 219 | Then, I would just enable this rule in ufw. |
| 234 | 220 | |
| 235 | #+begin_src sh | |
| 221 | ```sh | |
| 236 | 222 | sudo ufw allow mycustomrule |
| 237 | #+end_src | |
| 223 | ``` | |
blog/2021-02-19-macos.org → content/blog/2021-02-19-macos.md renamed +100 −111
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: macOS: Testing Out A New OS | |
| 2 | #+date: 2021-02-19 | |
| 1 | +++ | |
| 2 | date = 2021-02-19 | |
| 3 | title = "macOS: Testing Out A New OS" | |
| 4 | description = "A retrospective on my migration from Linux to macOS." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Diving into macOS | |
| 3 | 8 | |
| 4 | ** Diving into macOS | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: diving-into-macos | |
| 7 | :END: | |
| 8 | 9 | After spending nearly 15 years working with Windows and 8 years on |
| 9 | 10 | Linux, I have experienced macOS for the first time. By chance, my spouse |
| 10 | 11 | happened to buy a new MacBook and gifted me their 2013 model. Of course, |
| @@ -13,24 +14,22 @@ around for gaming needs, but over the past week I've found myself using | ||
| 13 | 14 | the MacBook more and more for things that don't require gaming specs or |
| 14 | 15 | advanced dev tools. |
| 15 | 16 | |
| 16 | ** Initial Thoughts | |
| 17 | :PROPERTIES: | |
| 18 | :CUSTOM_ID: initial-thoughts | |
| 19 | :END: | |
| 17 | ## Initial Thoughts | |
| 18 | ||
| 20 | 19 | Before I move on to the technical aspects of my set-up, I want to take |
| 21 | 20 | some time and express my thoughts on the overall OS. |
| 22 | 21 | |
| 23 | #+caption: macOS Desktop | |
| 24 | [[https://img.cleberg.net/blog/20210219-macos-testing-out-a-new-os/macos-desktop.png]] | |
| 22 |  | |
| 25 | 24 | |
| 26 | 25 | As expected, the initial computer setup is a breeze with Mac's guided |
| 27 | 26 | GUI installer. |
| 28 | 27 | |
| 29 | 28 | The desktop itself reminds me of GNOME more than anything else I've |
| 30 | seen: even Pantheon from [[https://elementary.io/][ElementaryOS]], which | |
| 29 | seen: even Pantheon from [ElementaryOS](https://elementary.io/), which | |
| 31 | 30 | people commonly refer to as the closest Linux distro to macOS. The |
| 32 | 31 | desktop toolbar is great and far surpasses the utility of the GNOME |
| 33 | toolbar due to the fact that the extensions and icons /actually work/. I | |
| 32 | toolbar due to the fact that the extensions and icons *actually work*. I | |
| 34 | 33 | launch macOS and immediately see my shortcuts for Tresorit, Bitwarden, |
| 35 | 34 | and Mullvad pop up as the computer loads. |
| 36 | 35 | |
| @@ -52,10 +51,8 @@ The rest of this post explains the technicalities of how I set up my CLI | ||
| 52 | 51 | environment to make me feel more at-home, similar to the environments I |
| 53 | 52 | set up on Fedora, Ubuntu, etc. |
| 54 | 53 | |
| 55 | ** Making it Feel Like Home | |
| 56 | :PROPERTIES: | |
| 57 | :CUSTOM_ID: making-it-feel-like-home | |
| 58 | :END: | |
| 54 | ## Making it Feel Like Home | |
| 55 | ||
| 59 | 56 | If you're someone who uses Linux primarily, no doubt your first thought |
| 60 | 57 | when booting macOS will be the same as mine was: "Where is the terminal |
| 61 | 58 | and how do I set up my favorite utilities?" |
| @@ -63,163 +60,155 @@ and how do I set up my favorite utilities?" | ||
| 63 | 60 | Luckily, macOS hasn't completely hidden away the development tools from |
| 64 | 61 | the average user. You can easily find the Terminal app in the Launchpad |
| 65 | 62 | area, but it's probably not what you're used to. I was surprised (and |
| 66 | happy) to see that the default shell is =zsh=, the shell I use on all of | |
| 67 | my Linux distros. However, the commands are not the same - even the ones | |
| 68 | you may think are native to the shell. Commands like =dir= do not exist, | |
| 69 | so other native commands like =ls -la= or =pwd= are more useful here. | |
| 63 | happy) to see that the default shell is `zsh`, the shell I | |
| 64 | use on all of my Linux distros. However, the commands are not the same - | |
| 65 | even the ones you may think are native to the shell. Commands like | |
| 66 | `dir` do not exist, so other native commands like | |
| 67 | `ls -la` or `pwd` are more useful here. | |
| 70 | 68 | |
| 71 | 69 | With only a few minutes of installing and tweaking a few packages, I was |
| 72 | 70 | able to recreate a terminal environment that I feel very comfortable |
| 73 | 71 | using. See the image below for a preview of the iTerm2 app with a split |
| 74 | 72 | view between my macOS desktop shell and an SSH session into my server. |
| 75 | 73 | |
| 76 | #+caption: iTerm2 | |
| 77 | [[https://img.cleberg.net/blog/20210219-macos-testing-out-a-new-os/iterm2.png]] | |
| 78 | ||
| 79 | ** Xcode | |
| 80 | :PROPERTIES: | |
| 81 | :CUSTOM_ID: xcode | |
| 82 | :END: | |
| 83 | My first step was to search the web for any hints on how to get =zsh= | |
| 84 | back up to the state I like, with extensions, themes, etc. My first step | |
| 85 | was to install the CLI tools for | |
| 86 | [[https://developer.apple.com/xcode/][Xcode]], Apple's suite of | |
| 74 |  | |
| 75 | ||
| 76 | ## Xcode | |
| 77 | ||
| 78 | My first step was to search the web for any hints on how to get | |
| 79 | `zsh` back up to the state I like, with extensions, themes, | |
| 80 | etc. My first step was to install the CLI tools for | |
| 81 | [Xcode](https://developer.apple.com/xcode/), Apple's suite of | |
| 87 | 82 | development tools. |
| 88 | 83 | |
| 89 | #+begin_src sh | |
| 84 | ```sh | |
| 90 | 85 | sudo xcode-select -r |
| 91 | #+end_src | |
| 86 | ``` | |
| 92 | 87 | |
| 93 | #+begin_src sh | |
| 88 | ```sh | |
| 94 | 89 | sudo xcode-select --install |
| 95 | #+end_src | |
| 90 | ``` | |
| 96 | 91 | |
| 97 | ** Homebrew | |
| 98 | :PROPERTIES: | |
| 99 | :CUSTOM_ID: homebrew | |
| 100 | :END: | |
| 101 | Next up is to install [[https://brew.sh][Homebrew]], a nifty package | |
| 92 | ## Homebrew | |
| 93 | ||
| 94 | Next up is to install [Homebrew](https://brew.sh), a nifty package | |
| 102 | 95 | manager for macOS. |
| 103 | 96 | |
| 104 | #+begin_src sh | |
| 97 | ```sh | |
| 105 | 98 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" |
| 106 | #+end_src | |
| 99 | ``` | |
| 107 | 100 | |
| 108 | 101 | I ran into a permission error when installing Homebrew: |
| 109 | 102 | |
| 110 | #+begin_src sh | |
| 103 | ```sh | |
| 111 | 104 | Error: Failed to link all completions, docs and manpages: |
| 112 | 105 | Permission denied @ rb_file_s_symlink - (../../../Homebrew/completions/zsh/_brew, /usr/local/share/zsh/site-functions/_brew) |
| 113 | 106 | Failed during: /usr/local/bin/brew update --force --quiet |
| 114 | #+end_src | |
| 107 | ``` | |
| 115 | 108 | |
| 116 | 109 | I found that the following permission modification worked like a charm. |
| 117 | 110 | However, I noted that some users online discussed the fact that this |
| 118 | 111 | solution may not work if your system has multiple users who use |
| 119 | 112 | Homebrew. |
| 120 | 113 | |
| 121 | #+begin_src sh | |
| 114 | ```sh | |
| 122 | 115 | sudo chown -R $(whoami) $(brew --prefix)/* |
| 123 | #+end_src | |
| 116 | ``` | |
| 124 | 117 | |
| 125 | 118 | Next up is to ensure Homebrew is updated and cleaned. |
| 126 | 119 | |
| 127 | #+begin_src sh | |
| 120 | ```sh | |
| 128 | 121 | brew update |
| 129 | #+end_src | |
| 122 | ``` | |
| 130 | 123 | |
| 131 | #+begin_src sh | |
| 124 | ```sh | |
| 132 | 125 | brew cleanup |
| 133 | #+end_src | |
| 126 | ``` | |
| 127 | ||
| 128 | ## iTerm2 | |
| 134 | 129 | |
| 135 | ** iTerm2 | |
| 136 | :PROPERTIES: | |
| 137 | :CUSTOM_ID: iterm2 | |
| 138 | :END: | |
| 139 | 130 | Now that I've installed the basic utilities for development, I moved |
| 140 | 131 | onto installing iTerm2, a much better terminal than the default. |
| 141 | 132 | |
| 142 | #+begin_src sh | |
| 133 | ```sh | |
| 143 | 134 | brew install --cask iterm2 |
| 144 | #+end_src | |
| 135 | ``` | |
| 145 | 136 | |
| 146 | I also used the =Make iTerm2 Default Term= and | |
| 147 | =Install Shell Integration= options in the iTerm2 application menu to | |
| 148 | make sure I don't run into any issues later on with different terminals. | |
| 137 | I also used the `Make iTerm2 Default Term` and | |
| 138 | `Install Shell Integration` options in the iTerm2 application | |
| 139 | menu to make sure I don't run into any issues later on with different | |
| 140 | terminals. | |
| 149 | 141 | |
| 150 | We will also install =zsh= so we can use it in iTerm2. | |
| 142 | We will also install `zsh` so we can use it in iTerm2. | |
| 151 | 143 | |
| 152 | #+begin_src sh | |
| 144 | ```sh | |
| 153 | 145 | brew install zsh |
| 154 | #+end_src | |
| 146 | ``` | |
| 155 | 147 | |
| 156 | ** Oh-My-Zsh | |
| 157 | :PROPERTIES: | |
| 158 | :CUSTOM_ID: oh-my-zsh | |
| 159 | :END: | |
| 160 | I've shown the great aspects of [[https://ohmyz.sh][Oh My Zsh]] in other | |
| 161 | blog posts, so I'll skip over that speech for now. Simply install it and | |
| 162 | run an update. | |
| 148 | ## Oh-My-Zsh | |
| 163 | 149 | |
| 164 | #+begin_src sh | |
| 150 | I've shown the great aspects of [Oh My Zsh](https://ohmyz.sh) in other | |
| 151 | blog posts, so I'll skip over that speech for now. Simply install it | |
| 152 | and run an update. | |
| 153 | ||
| 154 | ```sh | |
| 165 | 155 | sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" |
| 166 | #+end_src | |
| 156 | ``` | |
| 167 | 157 | |
| 168 | #+begin_src sh | |
| 158 | ```sh | |
| 169 | 159 | omz update |
| 170 | #+end_src | |
| 160 | ``` | |
| 171 | 161 | |
| 172 | 162 | Finally, restart the iTerm2 application to ensure all changes go into |
| 173 | 163 | effect. |
| 174 | 164 | |
| 175 | ** Oh-My-Zsh Themes | |
| 176 | :PROPERTIES: | |
| 177 | :CUSTOM_ID: oh-my-zsh-themes | |
| 178 | :END: | |
| 165 | ## Oh-My-Zsh Themes | |
| 166 | ||
| 179 | 167 | Let's change the theme of the terminal to make it a little more |
| 180 | 168 | friendly. |
| 181 | 169 | |
| 182 | #+begin_src sh | |
| 170 | ```sh | |
| 183 | 171 | open ~/.zshrc |
| 184 | #+end_src | |
| 172 | ``` | |
| 185 | 173 | |
| 186 | 174 | The third section of this file should contain a line like the code |
| 187 | below. Change that theme to | |
| 188 | [[https://github.com/ohmyzsh/ohmyzsh/wiki/Themes][any theme you want]], | |
| 189 | save the file, and exit. | |
| 175 | below. Change that theme to [any theme you | |
| 176 | want](https://github.com/ohmyzsh/ohmyzsh/wiki/Themes), save the file, | |
| 177 | and exit. | |
| 190 | 178 | |
| 191 | #+begin_src sh | |
| 179 | ```sh | |
| 192 | 180 | ZSH_THEME="af-magic" |
| 193 | #+end_src | |
| 194 | ||
| 195 | After changing the =.zshrc= file, you'll need to close your terminal and | |
| 196 | re-open it to see the changes. Optionally, just open a new tab if you're | |
| 197 | using iTerm2, and you'll see the new shell config. | |
| 198 | ||
| 199 | ** Oh-My-Zsh Plugins | |
| 200 | :PROPERTIES: | |
| 201 | :CUSTOM_ID: oh-my-zsh-plugins | |
| 202 | :END: | |
| 203 | Of course, my customization of =zsh= would not be complete without | |
| 204 | [[https://github.com/zsh-users/zsh-autosuggestions][zsh-autosuggestions]]. | |
| 205 | This will bring up commands you've run in the past as you type them. For | |
| 206 | example, if you've run =ssh user@192.168.1.99= before, the terminal will | |
| 207 | show this command as soon as you start typing it (e.g. =zsh u=), and you | |
| 208 | can hit the right arrow to autocomplete the command. | |
| 209 | ||
| 210 | #+begin_src sh | |
| 181 | ``` | |
| 182 | ||
| 183 | After changing the `.zshrc` file, you'll need to close your | |
| 184 | terminal and re-open it to see the changes. Optionally, just open a new | |
| 185 | tab if you're using iTerm2, and you'll see the new shell config. | |
| 186 | ||
| 187 | ## Oh-My-Zsh Plugins | |
| 188 | ||
| 189 | Of course, my customization of `zsh` would not be complete | |
| 190 | without | |
| 191 | [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions). | |
| 192 | This will bring up commands you've run in the past as you type them. | |
| 193 | For example, if you've run `ssh user@192.168.1.99` before, | |
| 194 | the terminal will show this command as soon as you start typing it | |
| 195 | (e.g. =zsh u=), and you can hit the right arrow to autocomplete the | |
| 196 | command. | |
| 197 | ||
| 198 | ```sh | |
| 211 | 199 | git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions |
| 212 | #+end_src | |
| 200 | ``` | |
| 213 | 201 | |
| 214 | #+begin_src sh | |
| 202 | ```sh | |
| 215 | 203 | open ~/.zshrc |
| 216 | #+end_src | |
| 204 | ``` | |
| 217 | 205 | |
| 218 | #+begin_src sh | |
| 206 | ```sh | |
| 219 | 207 | # Scroll down the script and edit this line to add zsh-autosuggestions |
| 220 | 208 | plugins=(git zsh-autosuggestions) |
| 221 | #+end_src | |
| 209 | ``` | |
| 222 | 210 | |
| 223 | Remember: After changing the =.zshrc= file, you'll need to close your | |
| 224 | terminal and re-open it to see the changes. Optionally, just open a new | |
| 225 | tab if you're using iTerm2, and you'll see the new shell config. | |
| 211 | Remember: After changing the `.zshrc` file, you'll need to | |
| 212 | close your terminal and re-open it to see the changes. Optionally, just | |
| 213 | open a new tab if you're using iTerm2, and you'll see the new shell | |
| 214 | config. | |
blog/2021-03-19-clone-github-repos.org → content/blog/2021-03-19-clone-github-repos.md renamed +49 −49
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: How to Clone All Repositories from a GitHub or Sourcehut Account | |
| 2 | #+date: 2021-03-19 | |
| 1 | +++ | |
| 2 | date = 2021-03-19 | |
| 3 | title = "How to Clone All Repositories from a GitHub or Sourcehut Account" | |
| 4 | description = "Learn how to clone all GitHub or Sourcehut repositories." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Cloning from GitHub | |
| 3 | 8 | |
| 4 | ** Cloning from GitHub | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: cloning-from-github | |
| 7 | :END: | |
| 8 | 9 | If you're like me and use a lot of different devices (and sometimes |
| 9 | 10 | decide to just wipe your device and start with a new OS), you probably |
| 10 | 11 | know the pain of cloning all your old code repositories down to your |
| @@ -12,48 +13,47 @@ local file system. | ||
| 12 | 13 | |
| 13 | 14 | If you're using GitHub, you can easily clone all of your code back down |
| 14 | 15 | in just seconds. First, create a bash script. I do so by opening a new |
| 15 | file in =nano=, but you can use =gedit=, =vim=, or something else: | |
| 16 | file in `nano`, but you can use `gedit`, | |
| 17 | `vim`, or something else: | |
| 16 | 18 | |
| 17 | #+begin_src sh | |
| 19 | ```sh | |
| 18 | 20 | nano clone_github_repos.sh |
| 19 | #+end_src | |
| 21 | ``` | |
| 20 | 22 | |
| 21 | 23 | Next, paste in the following information. Note that you can replace the |
| 22 | word =users= in the first line with =orgs= and type an organization's | |
| 23 | name instead of a user's name. | |
| 24 | word `users` in the first line with `orgs` and | |
| 25 | type an organization's name instead of a user's name. | |
| 24 | 26 | |
| 25 | #+begin_src sh | |
| 27 | ```sh | |
| 26 | 28 | CNTX=users; NAME=YOUR-USERNAME; PAGE=1 |
| 27 | 29 | curl "https://api.github.com/$CNTX/$NAME/repos?page=$PAGE&per_page=100" | |
| 28 | 30 | grep -e 'git_url*' | |
| 29 | cut -d \" -f 4 | | |
| 31 | cut -d " -f 4 | | |
| 30 | 32 | xargs -L1 git clone |
| 31 | #+end_src | |
| 33 | ``` | |
| 32 | 34 | |
| 33 | 35 | Finally, save the bash script and make it executable. |
| 34 | 36 | |
| 35 | #+begin_src sh | |
| 37 | ```sh | |
| 36 | 38 | chmod a+x clone_github_repos.sh |
| 37 | #+end_src | |
| 39 | ``` | |
| 38 | 40 | |
| 39 | 41 | Now you can run the script and should see the cloning process begin. |
| 40 | 42 | |
| 41 | #+begin_src sh | |
| 43 | ```sh | |
| 42 | 44 | ./clone_github_repos.sh |
| 43 | #+end_src | |
| 45 | ``` | |
| 46 | ||
| 47 | ## Cloning from Sourcehut | |
| 44 | 48 | |
| 45 | ** Cloning from Sourcehut | |
| 46 | :PROPERTIES: | |
| 47 | :CUSTOM_ID: cloning-from-sourcehut | |
| 48 | :END: | |
| 49 | 49 | I haven't fully figured out how to directly incorporate Sourcehut's |
| 50 | 50 | GraphQL API into a bash script yet, so this one will take two steps. |
| 51 | 51 | |
| 52 | First, log-in to Sourcehut and go to their | |
| 53 | [[https://git.sr.ht/graphql][GraphQL playground for Git]]. Next, paste | |
| 54 | the following query into the left box: | |
| 52 | First, log-in to Sourcehut and go to their [GraphQL playground for | |
| 53 | Git](https://git.sr.ht/graphql). Next, paste the following query into | |
| 54 | the left box: | |
| 55 | 55 | |
| 56 | #+begin_src sh | |
| 56 | ```sh | |
| 57 | 57 | query { |
| 58 | 58 | me { |
| 59 | 59 | canonicalName |
| @@ -65,7 +65,7 @@ query { | ||
| 65 | 65 | } |
| 66 | 66 | } |
| 67 | 67 | } |
| 68 | #+end_src | |
| 68 | ``` | |
| 69 | 69 | |
| 70 | 70 | The output on the right side will give you an object of all your |
| 71 | 71 | repositories. Just grab that text and remove all the characters such as |
| @@ -74,18 +74,19 @@ space-separated values for the next step. | ||
| 74 | 74 | |
| 75 | 75 | Now let's create the bash script: |
| 76 | 76 | |
| 77 | #+begin_src sh | |
| 77 | ```sh | |
| 78 | 78 | nano clone_sourcehut_repos.sh |
| 79 | #+end_src | |
| 79 | ``` | |
| 80 | 80 | |
| 81 | 81 | Next, paste the following bash script in with the list of repositories |
| 82 | you obtained above and replace =your-username= with your username. | |
| 82 | you obtained above and replace `your-username` with your | |
| 83 | username. | |
| 83 | 84 | |
| 84 | 85 | Note that this uses the SSH-based Git cloning method |
| 85 | 86 | (e.g. =git@git...=), so you'll need to ensure you have set up Sourcehut |
| 86 | 87 | with your SSH key. |
| 87 | 88 | |
| 88 | #+begin_src sh | |
| 89 | ```sh | |
| 89 | 90 | repos=(repo1 repo2 repo3) |
| 90 | 91 | |
| 91 | 92 | # List all sub-directories in the current directory |
| @@ -94,38 +95,37 @@ do | ||
| 94 | 95 | # Clone |
| 95 | 96 | git clone git@git.sr.ht:~your-username/$repo |
| 96 | 97 | done |
| 97 | #+end_src | |
| 98 | ``` | |
| 98 | 99 | |
| 99 | 100 | Finally, save the bash script and make it executable. |
| 100 | 101 | |
| 101 | #+begin_src sh | |
| 102 | ```sh | |
| 102 | 103 | chmod a+x clone_sourcehut_repos.sh |
| 103 | #+end_src | |
| 104 | ``` | |
| 104 | 105 | |
| 105 | 106 | Now you can run the script and should see the cloning process begin. |
| 106 | 107 | |
| 107 | #+begin_src sh | |
| 108 | ```sh | |
| 108 | 109 | ./clone_sourcehut_repos.sh |
| 109 | #+end_src | |
| 110 | ``` | |
| 111 | ||
| 112 | ## Moving Repositories to a New Host | |
| 110 | 113 | |
| 111 | ** Moving Repositories to a New Host | |
| 112 | :PROPERTIES: | |
| 113 | :CUSTOM_ID: moving-repositories-to-a-new-host | |
| 114 | :END: | |
| 115 | 114 | Now that you have all of your code repositories cloned to your local |
| 116 | 115 | computer, you may want to change the remote host (e.g., moving from |
| 117 | 116 | GitHub to GitLab). To do this, let's create another bash script: |
| 118 | 117 | |
| 119 | #+begin_src sh | |
| 118 | ```sh | |
| 120 | 119 | nano change_remote_urls.sh |
| 121 | #+end_src | |
| 120 | ``` | |
| 122 | 121 | |
| 123 | 122 | Past the following information and be sure to change the URL information |
| 124 | 123 | to whichever host you are moving to. For this example, I am looping |
| 125 | 124 | through all of my cloned GitHub directories and changing them to |
| 126 | Sourcehut (e.g. =<YOUR_NEW_REMOTE_URL>= -> =git@git.sr.ht:~myusername=). | |
| 125 | Sourcehut (e.g. `<YOUR_NEW_REMOTE_URL>` -\> | |
| 126 | `git@git.sr.ht:~myusername`). | |
| 127 | 127 | |
| 128 | #+begin_src sh | |
| 128 | ```sh | |
| 129 | 129 | # List all sub-directories in the current directory |
| 130 | 130 | for dir in */ |
| 131 | 131 | do |
| @@ -140,16 +140,16 @@ do | ||
| 140 | 140 | # Go back to main directory |
| 141 | 141 | cd .. |
| 142 | 142 | done |
| 143 | #+end_src | |
| 143 | ``` | |
| 144 | 144 | |
| 145 | 145 | Finally, save the bash script and make it executable. |
| 146 | 146 | |
| 147 | #+begin_src sh | |
| 147 | ```sh | |
| 148 | 148 | chmod a+x change_remote_urls.sh |
| 149 | #+end_src | |
| 149 | ``` | |
| 150 | 150 | |
| 151 | 151 | Now you can run the script and should see the cloning process begin. |
| 152 | 152 | |
| 153 | #+begin_src sh | |
| 153 | ```sh | |
| 154 | 154 | ./change_remote_urls.sh |
| 155 | #+end_src | |
| 155 | ``` | |
blog/2021-03-28-gemini-capsule.org → content/blog/2021-03-28-gemini-capsule.md renamed +70 −75
| @@ -1,29 +1,30 @@ | ||
| 1 | #+title: Launching a Gemini Capsule | |
| 2 | #+date: 2021-03-28 | |
| 3 | ||
| 4 | ** What is Gemini? | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: what-is-gemini | |
| 7 | :END: | |
| 8 | [[https://gemini.circumlunar.space/][Gemini]] is an internet protocol | |
| 1 | +++ | |
| 2 | date = 2021-03-28 | |
| 3 | title = "Launching a Gemini Capsule" | |
| 4 | description = "A guide to self-hosting a Gemini capsule on your own server." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## What is Gemini? | |
| 8 | ||
| 9 | [Gemini](https://gemini.circumlunar.space/) is an internet protocol | |
| 9 | 10 | introduced in June 2019 as an alternative to HTTP(S) or Gopher. In |
| 10 | 11 | layman's terms, it's an alternative way to browse sites (called |
| 11 | 12 | capsules) that requires a special browser. Since Gemini is not |
| 12 | 13 | standardized as an internet standard, normal web browsers won't be able |
| 13 | to load a Gemini capsule. Instead, you'll need to use | |
| 14 | [[https://gemini.%20circumlunar.space/clients.html][a Gemini-specific | |
| 15 | browser]]. | |
| 14 | to load a Gemini capsule. Instead, you'll need to use [a | |
| 15 | Gemini-specific | |
| 16 | browser](https://gemini.%20circumlunar.space/clients.html). | |
| 16 | 17 | |
| 17 | 18 | The content found within a Gemini page is called |
| 18 | [[https://gemini.circumlunar.space/docs/cheatsheet.gmi][Gemtext]] and is | |
| 19 | /extremely/ basic (on purpose). Gemini only processes the text, no media | |
| 20 | content like images. However, you're able to style 3 levels of headings, | |
| 21 | regular text, links (which will display on their own line), quotes, and | |
| 22 | an unordered list. | |
| 19 | [Gemtext](https://gemini.circumlunar.space/docs/cheatsheet.gmi) and is | |
| 20 | *extremely* basic (on purpose). Gemini only processes the text, no media | |
| 21 | content like images. However, you're able to style 3 levels of | |
| 22 | headings, regular text, links (which will display on their own line), | |
| 23 | quotes, and an unordered list. | |
| 23 | 24 | |
| 24 | 25 | Here's a complete listing of valid Gemtext: |
| 25 | 26 | |
| 26 | #+begin_src txt | |
| 27 | ```txt | |
| 27 | 28 | # Heading 1 |
| 28 | 29 | ## Heading 2 |
| 29 | 30 | ### Heading 3 |
| @@ -40,12 +41,10 @@ My List: | ||
| 40 | 41 | * Item |
| 41 | 42 | |
| 42 | 43 | ```Anything between three backticks will be rendered as code.``` |
| 43 | #+end_src | |
| 44 | ``` | |
| 45 | ||
| 46 | ## Free Option | |
| 44 | 47 | |
| 45 | ** Free Option | |
| 46 | :PROPERTIES: | |
| 47 | :CUSTOM_ID: free-option | |
| 48 | :END: | |
| 49 | 48 | There are probably numerous websites that allow you to create your |
| 50 | 49 | personal Gemini capsule, but I'm going to focus on the two sites that I |
| 51 | 50 | have personally tested. The first option below, Midnight Pub, allows you |
| @@ -55,20 +54,18 @@ second option below, Sourcehut, allows you to use a Git repository and | ||
| 55 | 54 | automatic build process to deploy your personal Gemini capsule every |
| 56 | 55 | time you push a commit. |
| 57 | 56 | |
| 58 | *** Midnight Pub - Beginner Friendly | |
| 59 | :PROPERTIES: | |
| 60 | :CUSTOM_ID: midnight-pub---beginner-friendly | |
| 61 | :END: | |
| 62 | [[https://midnight.pub/][Midnight Pub]] is a small, virtual community | |
| 57 | ### Midnight Pub - Beginner Friendly | |
| 58 | ||
| 59 | [Midnight Pub](https://midnight.pub/) is a small, virtual community | |
| 63 | 60 | meant to reflect the atmosphere of wandering into a small alley pub. The |
| 64 | 61 | site is built in Gemtext and has a server-side process to convert |
| 65 | 62 | Gemtext to HTML if someone loads the site in an HTTP(S) browser. |
| 66 | 63 | |
| 67 | 64 | To create an account, you'll need to email the owner of the website to |
| 68 | 65 | obtain a key. You can find their email on the Midnight Pub homepage. |
| 69 | Once registered, head to [[https://midnight.pub/account][your account]] | |
| 70 | and select [[https://midnight.pub/site][manage site]]. This is the | |
| 71 | screen where you can upload or create any files to be displayed on the | |
| 66 | Once registered, head to [your account](https://midnight.pub/account) | |
| 67 | and select [manage site](https://midnight.pub/site). This is the screen | |
| 68 | where you can upload or create any files to be displayed on the | |
| 72 | 69 | internet. |
| 73 | 70 | |
| 74 | 71 | For example, I've created both an HTML file and a Gemini file. Remember |
| @@ -76,25 +73,22 @@ that Gemini is automatically converted to HTML on the Pub, so you don't | ||
| 76 | 73 | need an HTML version. For example, I created an HTML version to add in |
| 77 | 74 | some extra styling. |
| 78 | 75 | |
| 79 | All you need to do is create a page like =index.gmi= and use your Gemini | |
| 80 | browser to head over to your-username.midnight.pub to see the result. | |
| 76 | All you need to do is create a page like `index.gmi` and use | |
| 77 | your Gemini browser to head over to your-username.midnight.pub to see | |
| 78 | the result. | |
| 81 | 79 | |
| 82 | 80 | That's all there is to it! Easy enough, right? Let's check out a more |
| 83 | 81 | advanced version in the next section. |
| 84 | 82 | |
| 85 | ** Paid Option | |
| 86 | :PROPERTIES: | |
| 87 | :CUSTOM_ID: paid-option | |
| 88 | :END: | |
| 83 | ## Paid Option | |
| 84 | ||
| 89 | 85 | As of 2021, Sourcehut has decided to require users to have a paid |
| 90 | 86 | account in order to utilize their automated build system. For now, paid |
| 91 | accounts can be as low as $2/month. | |
| 87 | accounts can be as low as \$2/month. | |
| 88 | ||
| 89 | ### Sourcehut | |
| 92 | 90 | |
| 93 | *** Sourcehut | |
| 94 | :PROPERTIES: | |
| 95 | :CUSTOM_ID: sourcehut | |
| 96 | :END: | |
| 97 | [[https://sourcehut.org/][Sourcehut]] is a collection of software | |
| 91 | [Sourcehut](https://sourcehut.org/) is a collection of software | |
| 98 | 92 | development tools, but mostly surrounds their hosted Git repository |
| 99 | 93 | service. Simply put, it's a minimal and more private alternative to |
| 100 | 94 | services like GitHub. |
| @@ -106,43 +100,43 @@ instead. | ||
| 106 | 100 | |
| 107 | 101 | The first thing you'll need to do is create an SSH key pair, if you |
| 108 | 102 | don't already have one on your system. Once created, grab the contents |
| 109 | of =id_rsa.pub= and add it to your Sourcehut account settings - this | |
| 110 | will allow you to push and pull code changes without using a | |
| 111 | username/password. | |
| 103 | of `id_rsa.pub` and add it to your Sourcehut account | |
| 104 | settings - this will allow you to push and pull code changes without | |
| 105 | using a username/password. | |
| 112 | 106 | |
| 113 | #+begin_src sh | |
| 107 | ```sh | |
| 114 | 108 | ssh keygen |
| 115 | #+end_src | |
| 109 | ``` | |
| 116 | 110 | |
| 117 | 111 | Next up, let's create a repository with the proper name so that the |
| 118 | 112 | Sourcehut build system will know we want them to host a website for us. |
| 119 | 113 | Use the following format exactly: |
| 120 | 114 | |
| 121 | #+begin_src sh | |
| 115 | ```sh | |
| 122 | 116 | mkdir your-username.srht.site && cd your-username.srht.site |
| 123 | #+end_src | |
| 117 | ``` | |
| 124 | 118 | |
| 125 | Now that we've created the repo, let's initialize Git and add the proper | |
| 126 | remote URL. | |
| 119 | Now that we've created the repo, let's initialize Git and add the | |
| 120 | proper remote URL. | |
| 127 | 121 | |
| 128 | #+begin_src sh | |
| 122 | ```sh | |
| 129 | 123 | git init |
| 130 | #+end_src | |
| 124 | ``` | |
| 131 | 125 | |
| 132 | #+begin_src sh | |
| 126 | ```sh | |
| 133 | 127 | git remote add origin git@git.sr.ht:~your-username/your-username.srht.site |
| 134 | #+end_src | |
| 128 | ``` | |
| 135 | 129 | |
| 136 | 130 | Now that our repository is set up and configured, we will need to create |
| 137 | 131 | at least two files: |
| 138 | 132 | |
| 139 | - =index.gmi= | |
| 140 | - =.build.yml= | |
| 133 | - `index.gmi` | |
| 134 | - `.build.yml` | |
| 141 | 135 | |
| 142 | For your =.build.yml= file, use the following content and be sure to | |
| 143 | update the =site= line with your username! | |
| 136 | For your `.build.yml` file, use the following content and be | |
| 137 | sure to update the `site` line with your username! | |
| 144 | 138 | |
| 145 | #+begin_src yaml | |
| 139 | ```yaml | |
| 146 | 140 | image: alpine/latest |
| 147 | 141 | oauth: pages.sr.ht/PAGES:RW |
| 148 | 142 | environment: |
| @@ -153,38 +147,39 @@ tasks: | ||
| 153 | 147 | tar -cvz . > ../site.tar.gz |
| 154 | 148 | - upload: | |
| 155 | 149 | acurl -f https://pages.sr.ht/publish/$site -Fcontent=@site.tar.gz -Fprotocol=GEMINI |
| 156 | #+end_src | |
| 150 | ``` | |
| 157 | 151 | |
| 158 | For the =index.gmi= file, put whatever you want in there and save it. | |
| 159 | You could even just copy and paste the Gemtext cheatsheet. | |
| 152 | For the `index.gmi` file, put whatever you want in there and | |
| 153 | save it. You could even just copy and paste the Gemtext cheatsheet. | |
| 160 | 154 | |
| 161 | 155 | If you want to serve both HTML and Gemini files from this repository, |
| 162 | just add a second command to the =upload= section: | |
| 156 | just add a second command to the `upload` section: | |
| 163 | 157 | |
| 164 | #+begin_src yaml | |
| 158 | ```yaml | |
| 165 | 159 | - upload: | |
| 166 | 160 | acurl -f https://pages.sr.ht/publish/$site -Fcontent=@site.tar.gz -Fprotocol=GEMINI |
| 167 | 161 | acurl -f https://pages.sr.ht/publish/$site -Fcontent=@site.tar.gz |
| 168 | #+end_src | |
| 162 | ``` | |
| 169 | 163 | |
| 170 | 164 | Lastly, commit your changes and push them to the remote repo. |
| 171 | 165 | |
| 172 | #+begin_src sh | |
| 166 | ```sh | |
| 173 | 167 | git add .; git commit -m "initial commit"; git push --set-upstream origin HEAD |
| 174 | #+end_src | |
| 168 | ``` | |
| 175 | 169 | |
| 176 | If you've successfully created the files with the proper format, you'll | |
| 177 | see the terminal print a message that lets you know where the automatic | |
| 178 | build is taking place. For example, here's what the terminal tells me: | |
| 170 | If you've successfully created the files with the proper format, | |
| 171 | you'll see the terminal print a message that lets you know where the | |
| 172 | automatic build is taking place. For example, here's what the terminal | |
| 173 | tells me: | |
| 179 | 174 | |
| 180 | #+begin_src sh | |
| 175 | ```sh | |
| 181 | 176 | remote: Build started: |
| 182 | 177 | remote: https://builds.sr.ht/~user/job/689803 [.build.yml] |
| 183 | #+end_src | |
| 178 | ``` | |
| 184 | 179 | |
| 185 | 180 | Now that you've properly built your Sourcehut page, you can browse to |
| 186 | 181 | your-username.srht.site in a Gemini browser and view the final results. |
| 187 | 182 | Take a look at the image below for my Sourcehut Gemini capsule. |
| 188 | 183 | |
| 189 | #+caption: Gemini page on the amfora browser | |
| 190 | [[https://img.cleberg.net/blog/20210328-launching-a-gemini-capsule/amfora.png]] | |
| 184 |  | |
blog/2021-03-28-vaporwave-vs-outrun.org → content/blog/2021-03-28-vaporwave-vs-outrun.md renamed +58 −79
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: Vaporwave vs Outrun | |
| 2 | #+date: 2021-03-28 | |
| 1 | +++ | |
| 2 | date = 2021-03-28 | |
| 3 | title = "Vaporwave vs Outrun" | |
| 4 | description = "Learn the differences between the vaporwave and Outrun aesthetics." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Overview | |
| 3 | 8 | |
| 4 | ** Overview | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: overview | |
| 7 | :END: | |
| 8 | 9 | When it comes to an aesthetic that originated primarily online, there |
| 9 | 10 | tends to be a lot of confusion around what falls into each specific |
| 10 | 11 | genre. This post discusses Vaporwave and Outrun, which have become |
| @@ -12,69 +13,58 @@ almost synonymous in the minds of those online who aren't hardcore into | ||
| 12 | 13 | these genres of media. More specifically, Outrun is largely an unknown |
| 13 | 14 | term while all aesthetics in these two genres are usually attributed to |
| 14 | 15 | Vaporwave. For example, take a look at the image search results for |
| 15 | Vaporwave: the results include *a lot* of Outrun-themed images. You'll | |
| 16 | find a similar trend almost everywhere. | |
| 16 | Vaporwave: the results include **a lot** of Outrun-themed images. | |
| 17 | You'll find a similar trend almost everywhere. | |
| 18 | ||
| 19 |  | |
| 17 | 20 | |
| 18 | [[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/vaporwave-search-results.png]] | |
| 21 | ## Vaporwave | |
| 19 | 22 | |
| 20 | ** Vaporwave | |
| 21 | :PROPERTIES: | |
| 22 | :CUSTOM_ID: vaporwave | |
| 23 | :END: | |
| 24 | [[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/macintosh-plus.png]] | |
| 23 |  | |
| 25 | 24 | |
| 26 | 25 | Okay, so what is Vaporwave? I'm going to just copy-and-paste some |
| 27 | 26 | general info from the Wikipedia article on |
| 28 | [[https://en.wikipedia.org/wiki/Vaporwave][Vaporwave]], so that I'm not | |
| 27 | [Vaporwave](https://en.wikipedia.org/wiki/Vaporwave), so that I'm not | |
| 29 | 28 | repeating everything you can already search for online: |
| 30 | 29 | |
| 31 | #+begin_quote | |
| 32 | Vaporwave is a microgenre of electronic music, a visual art style, and | |
| 33 | an Internet meme that emerged in the early 2010s. It is defined partly | |
| 34 | by its slowed-down, chopped and screwed samples of smooth jazz, | |
| 35 | elevator, R&B, and lounge music from the 1980s and 1990s. The | |
| 36 | surrounding subculture is sometimes associated with an ambiguous or | |
| 37 | satirical take on consumer capitalism and pop culture, and tends to be | |
| 38 | characterized by a nostalgic or surrealist engagement with the popular | |
| 39 | entertainment, technology and advertising of previous decades. Visually, | |
| 40 | it incorporates early Internet imagery, late 1990s web design, glitch | |
| 41 | art, anime, 3D-rendered objects, and cyberpunk tropes in its cover | |
| 42 | artwork and music videos. | |
| 43 | ||
| 44 | #+end_quote | |
| 30 | > Vaporwave is a microgenre of electronic music, a visual art style, and | |
| 31 | > an Internet meme that emerged in the early 2010s. It is defined partly | |
| 32 | > by its slowed-down, chopped and screwed samples of smooth jazz, | |
| 33 | > elevator, R&B, and lounge music from the 1980s and 1990s. The | |
| 34 | > surrounding subculture is sometimes associated with an ambiguous or | |
| 35 | > satirical take on consumer capitalism and pop culture, and tends to be | |
| 36 | > characterized by a nostalgic or surrealist engagement with the popular | |
| 37 | > entertainment, technology and advertising of previous decades. | |
| 38 | > Visually, it incorporates early Internet imagery, late 1990s web | |
| 39 | > design, glitch art, anime, 3D-rendered objects, and cyberpunk tropes | |
| 40 | > in its cover artwork and music videos. | |
| 45 | 41 | |
| 46 | 42 | This is an excellent summary, and it helps address my point here: there |
| 47 | 43 | are specific aspects that make Vaporwave unique: |
| 48 | 44 | |
| 49 | *** Time Frame | |
| 50 | :PROPERTIES: | |
| 51 | :CUSTOM_ID: time-frame | |
| 52 | :END: | |
| 45 | ### Time Frame | |
| 46 | ||
| 53 | 47 | The time frame for references, logos, etc. focuses mostly on the 1990s |
| 54 | 48 | in Vaporwave. You'll see old school Pepsi logos, Microsoft 95 screens, |
| 55 | 49 | tropical plants, classic marble sculptures, and many references from |
| 56 | 50 | Japan's influence in the 90s. |
| 57 | 51 | |
| 58 | *** Art | |
| 59 | :PROPERTIES: | |
| 60 | :CUSTOM_ID: art | |
| 61 | :END: | |
| 52 | ### Art | |
| 53 | ||
| 62 | 54 | The color scheme is generally a soft, light palette that uses pastel |
| 63 | 55 | colors. The backdrop will often be in a light-pink or teal blue. |
| 64 | 56 | |
| 65 | *** Music | |
| 66 | :PROPERTIES: | |
| 67 | :CUSTOM_ID: music | |
| 68 | :END: | |
| 57 | ### Music | |
| 58 | ||
| 69 | 59 | The musical genre of Vaporwave incorporates soft tunes, lounge music, |
| 70 | 60 | and sound effects that will make the listener reminisce of the 90s. The |
| 71 | 61 | sounds of the genre are generally slower-paced and calming. The major |
| 72 | breakthrough artist for Vaporwave was | |
| 73 | [[https://en.wikipedia.org/wiki/Vektroid][Macintosh Plus]], who released | |
| 74 | the album | |
| 75 | [[https://archive.org/details/MACINTOSHPLUS-FLORALSHOPPE_complete][Floral | |
| 76 | Shoppe]] in 2011. Another more recent example is the artist | |
| 77 | [[https://sadbert.bandcamp.com/][sadbert]], whose latest album | |
| 62 | breakthrough artist for Vaporwave was [Macintosh | |
| 63 | Plus](https://en.wikipedia.org/wiki/Vektroid), who released the album | |
| 64 | [Floral | |
| 65 | Shoppe](https://archive.org/details/MACINTOSHPLUS-FLORALSHOPPE_complete) | |
| 66 | in 2011. Another more recent example is the artist | |
| 67 | [sadbert](https://sadbert.bandcamp.com/), whose latest album | |
| 78 | 68 | incorporates the upbeat tones of the 1999 Dilbert TV series. |
| 79 | 69 | |
| 80 | 70 | Notice that Vaporwave doesn't include things like racing cars, |
| @@ -84,30 +74,23 @@ was becoming common in households, a reality that we have already | ||
| 84 | 74 | experienced. Focus on the most aesthetically-pleasing parts of that past |
| 85 | 75 | is a large part of Vaporwave. |
| 86 | 76 | |
| 87 | ** Outrun | |
| 88 | :PROPERTIES: | |
| 89 | :CUSTOM_ID: outrun | |
| 90 | :END: | |
| 91 | #+caption: Outrun | |
| 92 | [[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/outrun.png]] | |
| 77 | ## Outrun | |
| 78 | ||
| 79 |  | |
| 93 | 80 | |
| 94 | 81 | Now, let's get to Outrun. This one is a little trickier since the genre |
| 95 | 82 | has largely been lumped under the Vaporwave title for so long. However, |
| 96 | it stems from the [[https://en.wikipedia.org/wiki/Synthwave][Synthwave]] | |
| 97 | music genre and is likely named after the 1986 racer game, | |
| 98 | [[https://en.wikipedia.org/wiki/Out_Run][Out Run]]. | |
| 99 | ||
| 100 | *** Time Frame | |
| 101 | :PROPERTIES: | |
| 102 | :CUSTOM_ID: time-frame-1 | |
| 103 | :END: | |
| 83 | it stems from the [Synthwave](https://en.wikipedia.org/wiki/Synthwave) | |
| 84 | music genre and is likely named after the 1986 racer game, [Out | |
| 85 | Run](https://en.wikipedia.org/wiki/Out_Run). | |
| 86 | ||
| 87 | ### Time Frame | |
| 88 | ||
| 104 | 89 | Outrun can be thought of as a retro-futuristic aesthetic born from the |
| 105 | 90 | 1980s. |
| 106 | 91 | |
| 107 | *** Art | |
| 108 | :PROPERTIES: | |
| 109 | :CUSTOM_ID: art-1 | |
| 110 | :END: | |
| 92 | ### Art | |
| 93 | ||
| 111 | 94 | The color scheme uses a very dark color palette with the main colors |
| 112 | 95 | being deep oranges, blues, and purples. Red edges are common around |
| 113 | 96 | objects in Outrun art. The background of the Outrun aesthetic is almost |
| @@ -118,10 +101,8 @@ Classic sports cars, chrome robots, computer generated graphics and | ||
| 118 | 101 | fonts, and the occasional use of rain or palm trees can be found in |
| 119 | 102 | Outrun art. |
| 120 | 103 | |
| 121 | *** Music | |
| 122 | :PROPERTIES: | |
| 123 | :CUSTOM_ID: music-1 | |
| 124 | :END: | |
| 104 | ### Music | |
| 105 | ||
| 125 | 106 | This aesthetic has a more aggressive and fast-paced style of music, |
| 126 | 107 | which tends to match the subject of the art in this aesthetic. |
| 127 | 108 | |
| @@ -129,22 +110,20 @@ Outrun enthusiasts love what people in the 1980s thought the future | ||
| 129 | 110 | would look like. Take a look at a common video game discussed in Outrun |
| 130 | 111 | circles, Far Cry 3: Blood Dragon: |
| 131 | 112 | |
| 132 | #+caption: Fry Cry 3: Blood Dragon | |
| 133 | [[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/far-cry.png]] | |
| 113 |  | |
| 134 | 115 | |
| 135 | 116 | Another example that doesn't force the color scheme as hard as some |
| 136 | 117 | online art does is Kung Fury: |
| 137 | 118 | |
| 138 | #+caption: Kung Fury | |
| 139 | [[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/kung-fury.png]] | |
| 119 |  | |
| 121 | ||
| 122 |  | |
| 140 | 124 | |
| 141 | #+caption: Kung Fury Hacker Scene | |
| 142 | [[https://img.cleberg.net/blog/20210328-vaporwave-vs-outrun/kung-fury-hacker.png]] | |
| 125 | ## Conclusion | |
| 143 | 126 | |
| 144 | ** Conclusion | |
| 145 | :PROPERTIES: | |
| 146 | :CUSTOM_ID: conclusion | |
| 147 | :END: | |
| 148 | 127 | While Vaporwave and Outrun share similarities, they are two distinct |
| 149 | 128 | aesthetics with many important distinctions. Someone who enjoys one may |
| 150 | 129 | not necessarily enjoy the other, so it's important to make sure we |
blog/2021-03-30-vps-web-server.org → content/blog/2021-03-30-vps-web-server.md renamed +185 −201
| @@ -1,35 +1,35 @@ | ||
| 1 | #+title: How to Set Up a VPS Web Server | |
| 2 | #+date: 2021-03-30 | |
| 1 | +++ | |
| 2 | date = 2021-03-30 | |
| 3 | title = "How to Set Up a VPS Web Server" | |
| 4 | description = "A beginner's guide to setting up a virtual private server (VPS)." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Shared Hosting vs. VPS | |
| 3 | 8 | |
| 4 | ** Shared Hosting vs. VPS | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: shared-hosting-vs.-vps | |
| 7 | :END: | |
| 8 | 9 | Choosing a place to host a website is one of the most confusing |
| 9 | 10 | decisions for beginner web developers. Even for experienced web devs, |
| 10 | 11 | choosing between different forms of web hosting can be a daunting |
| 11 | 12 | choice. |
| 12 | 13 | |
| 13 | First, let's take a look at | |
| 14 | [[https://en.wikipedia.org/wiki/Shared_web_hosting_service][shared web | |
| 15 | hosting]]. Shared web hosting is a product where you are purchasing a | |
| 16 | small piece of a web server that is being shared between many websites. | |
| 17 | As a result, the cost of shared hosting is extremely low. You won't have | |
| 18 | access to the server itself, which means you can't install your own | |
| 19 | software on the server, such as Docker. Usually, you are simply allowed | |
| 20 | to connect your domains to the server, set up domain security, and other | |
| 21 | small utilities. | |
| 22 | ||
| 23 | In contrast, a | |
| 24 | [[https://en.wikipedia.org/wiki/Virtual_private_server][virtual private | |
| 25 | server]] (VPS) is a virtual machine that replicates the environment of | |
| 26 | having a dedicated server to yourself. You are able to control the | |
| 27 | virtual server's host names, base file system, package manager, etc. | |
| 28 | Another great upside of a VPS is that since it's virtual, the company | |
| 29 | providing the VPS can dynamically increase the disk size, RAM size, or | |
| 30 | number of CPUs at any time. However, the virtual server is still | |
| 31 | physically located on a server that is shared between multiple virtual | |
| 32 | servers. | |
| 14 | First, let's take a look at [shared web | |
| 15 | hosting](https://en.wikipedia.org/wiki/Shared_web_hosting_service). | |
| 16 | Shared web hosting is a product where you are purchasing a small piece | |
| 17 | of a web server that is being shared between many websites. As a result, | |
| 18 | the cost of shared hosting is extremely low. You won't have access to | |
| 19 | the server itself, which means you can't install your own software on | |
| 20 | the server, such as Docker. Usually, you are simply allowed to connect | |
| 21 | your domains to the server, set up domain security, and other small | |
| 22 | utilities. | |
| 23 | ||
| 24 | In contrast, a [virtual private | |
| 25 | server](https://en.wikipedia.org/wiki/Virtual_private_server) (VPS) is a | |
| 26 | virtual machine that replicates the environment of having a dedicated | |
| 27 | server to yourself. You are able to control the virtual server's host | |
| 28 | names, base file system, package manager, etc. Another great upside of a | |
| 29 | VPS is that since it's virtual, the company providing the VPS can | |
| 30 | dynamically increase the disk size, RAM size, or number of CPUs at any | |
| 31 | time. However, the virtual server is still physically located on a | |
| 32 | server that is shared between multiple virtual servers. | |
| 33 | 33 | |
| 34 | 34 | The choice between shared hosting and VPS mostly depends on your skill |
| 35 | 35 | level with system administration. If you're comforting working on a |
| @@ -38,10 +38,8 @@ a VPS is usually a better option. However, shared hosting is a fantastic | ||
| 38 | 38 | option for people who don't want to have to learn how to manage their |
| 39 | 39 | server. |
| 40 | 40 | |
| 41 | ** My Situation | |
| 42 | :PROPERTIES: | |
| 43 | :CUSTOM_ID: my-situation | |
| 44 | :END: | |
| 41 | ## My Situation | |
| 42 | ||
| 45 | 43 | I had used shared hosting for approximately 5 years before trying my |
| 46 | 44 | first VPS. I manage a homelab and have had success running a server and |
| 47 | 45 | performing typical sysadmin duties, but I was still hesitant to get a |
| @@ -50,54 +48,49 @@ networking part of a server - DNS and hostname configurations were not | ||
| 50 | 48 | my friend. |
| 51 | 49 | |
| 52 | 50 | As a little bit of background, I originally used |
| 53 | [[https://www.siteground.com][Siteground]] for my initially shared | |
| 54 | hosting and stayed on that platform for at least a year. However, the UI | |
| 55 | was clunky, and I didn't like how they handled certain technical | |
| 56 | aspects, so I switched to [[https://www.namecheap.com][Namecheap]]. | |
| 57 | Namecheap was great because it is the service I primarily use for | |
| 58 | purchasing domain names, which made it incredibly easy to link them to | |
| 59 | my hosting service. However, it was still mediocre shared hosting, and | |
| 60 | Namecheap is notorious for not letting you use | |
| 61 | [[https://letsencrypt.org][Let's Encrypt]] to obtain free SSL/TLS | |
| 62 | certificates; Namecheap wants to make you purchase certificates through | |
| 63 | their store. | |
| 64 | ||
| 65 | Finally, I settled down with [[https://www.iwebfusion.net][iWebFusion]] | |
| 51 | [Siteground](https://www.siteground.com) for my initially shared hosting | |
| 52 | and stayed on that platform for at least a year. However, the UI was | |
| 53 | clunky, and I didn't like how they handled certain technical aspects, | |
| 54 | so I switched to [Namecheap](https://www.namecheap.com). Namecheap was | |
| 55 | great because it is the service I primarily use for purchasing domain | |
| 56 | names, which made it incredibly easy to link them to my hosting service. | |
| 57 | However, it was still mediocre shared hosting, and Namecheap is | |
| 58 | notorious for not letting you use [Let's | |
| 59 | Encrypt](https://letsencrypt.org) to obtain free SSL/TLS certificates; | |
| 60 | Namecheap wants to make you purchase certificates through their store. | |
| 61 | ||
| 62 | Finally, I settled down with [iWebFusion](https://www.iwebfusion.net) | |
| 66 | 63 | for about the last year of my shared hosting. This service was pretty |
| 67 | 64 | great, came with free SSL/TLS, and I never had any complaints. |
| 68 | 65 | |
| 69 | 66 | However, I finally grew tired of not being able to install software on |
| 70 | 67 | my own web server. I wanted to be able to try out things like |
| 71 | [[https://postmill.xyz][Postmill]] or [[https://matrix.org][Matrix]]. | |
| 72 | This is possible with a VPS, so I decided to grab a new domain name to | |
| 73 | try it out. | |
| 74 | ||
| 75 | ** Getting Started: Buying a VPS | |
| 76 | :PROPERTIES: | |
| 77 | :CUSTOM_ID: getting-started-buying-a-vps | |
| 78 | :END: | |
| 68 | [Postmill](https://postmill.xyz) or [Matrix](https://matrix.org). This | |
| 69 | is possible with a VPS, so I decided to grab a new domain name to try it | |
| 70 | out. | |
| 71 | ||
| 72 | ## Getting Started: Buying a VPS | |
| 73 | ||
| 79 | 74 | The first step to moving over to a VPS is (you guessed it): finding a |
| 80 | VPS provider. For my VPSs, I use [[https://1984hosting.com][1984]] and | |
| 75 | VPS provider. For my VPSs, I use [1984](https://1984hosting.com) and | |
| 81 | 76 | prefer their services much more than any alternative, due to their |
| 82 | location (Iceland), their [[https://1984hosting.com/GDPR/][privacy | |
| 83 | policy]], their respect for GDPR, and the ability to remain anonymous if | |
| 84 | you pay in Bitcoin or Monero. | |
| 77 | location (Iceland), their [privacy | |
| 78 | policy](https://1984hosting.com/GDPR/), their respect for GDPR, and the | |
| 79 | ability to remain anonymous if you pay in Bitcoin or Monero. | |
| 85 | 80 | |
| 86 | [[https://njal.la][Njalla]] is another good, privacy-oriented option for | |
| 81 | [Njalla](https://njal.la) is another good, privacy-oriented option for | |
| 87 | 82 | VPS services. |
| 88 | 83 | |
| 89 | 84 | You'll have to decide what specifications you want on your VPS. For me, |
| 90 | 85 | I only build and deploy low-resource HTML, PHP, and Python websites. |
| 91 | 86 | This means I can survive on the smallest VPS: 1 CPU, 1GB of RAM, and |
| 92 | 25GB SSD for $5.00 per month. | |
| 87 | 25GB SSD for \$5.00 per month. | |
| 93 | 88 | |
| 94 | 89 | As noted above, the great thing about a VPS is you can request your |
| 95 | 90 | provider to increase the resources at any time. |
| 96 | 91 | |
| 97 | ** Configuring DNS Settings | |
| 98 | :PROPERTIES: | |
| 99 | :CUSTOM_ID: configuring-dns-settings | |
| 100 | :END: | |
| 92 | ## Configuring DNS Settings | |
| 93 | ||
| 101 | 94 | Okay, so now let's get into some actual work that has to be done to get |
| 102 | 95 | content moved from a shared host to a VPS. At this point, I'm assuming |
| 103 | 96 | you have a shared host with website content that you can still access, |
| @@ -105,147 +98,143 @@ and you've purchased a new VPS and can SSH into that server. | ||
| 105 | 98 | |
| 106 | 99 | The first change is minor, but it should be done immediately in order to |
| 107 | 100 | get things moving: DNS settings. Go to wherever your DNS settings are |
| 108 | handled. If your shared host also managed your DNS settings, you'll need | |
| 109 | to first move that DNS over to your new VPS provider. For me, I route my | |
| 110 | DNS through [[https://www.gandi.net][Gandi]]. | |
| 101 | handled. If your shared host also managed your DNS settings, you'll | |
| 102 | need to first move that DNS over to your new VPS provider. For me, I | |
| 103 | route my DNS through [Gandi](https://www.gandi.net). | |
| 111 | 104 | |
| 112 | Once you know where your DNS settings are, go ahead and update the =A= | |
| 113 | records to match the public IP address of your VPS. For example: | |
| 105 | Once you know where your DNS settings are, go ahead and update the | |
| 106 | `A` records to match the public IP address of your VPS. For | |
| 107 | example: | |
| 114 | 108 | |
| 115 | #+begin_src txt | |
| 109 | ```txt | |
| 116 | 110 | A example.com xxx.xxx.xxx.xxx |
| 117 | 111 | A subdomain xxx.xxx.xxx.xxx |
| 118 | 112 | CNAME www example.com. |
| 119 | #+end_src | |
| 113 | ``` | |
| 120 | 114 | |
| 121 | 115 | If you have any other records that require updates, such as MX or TXT |
| 122 | 116 | records for a mail server, be sure to update those accordingly. |
| 123 | 117 | Personally, I don't host my own mail server. I route all mail on my |
| 124 | custom domains to [[https://www.migadu.com][Migadu]]. Hosting your own | |
| 118 | custom domains to [Migadu](https://www.migadu.com). Hosting your own | |
| 125 | 119 | email server can become complex quickly and is not for beginners. |
| 126 | 120 | |
| 127 | 121 | DNS changes can take up to 48 hours to propagate, so be sure to give it |
| 128 | 122 | some time before assuming you've made an error. |
| 129 | 123 | |
| 130 | ** Server Updates and Packages | |
| 131 | :PROPERTIES: | |
| 132 | :CUSTOM_ID: server-updates-and-packages | |
| 133 | :END: | |
| 124 | ## Server Updates and Packages | |
| 125 | ||
| 134 | 126 | Now that the DNS settings have been changed, let's set up our server |
| 135 | 127 | while we wait for the DNS to propagate. First up is to ssh into your |
| 136 | 128 | server. If you've signed up with a service like DigitalOcean, you can |
| 137 | 129 | add your SSH key to your account and to your VPS droplet so that you |
| 138 | 130 | don't need a password in order to SSH. |
| 139 | 131 | |
| 140 | #+begin_src sh | |
| 132 | ```sh | |
| 141 | 133 | ssh root@xxx.xxx.xxx.xxx |
| 142 | #+end_src | |
| 134 | ``` | |
| 143 | 135 | |
| 144 | 136 | The VPS that is used in this blog post runs Ubuntu 20.04 with an Apache |
| 145 | 137 | web server. If you're working on a different operating system (OS) or |
| 146 | want a different web server, such as Nginx, you'll have to use different | |
| 147 | commands to set it up. | |
| 138 | want a different web server, such as Nginx, you'll have to use | |
| 139 | different commands to set it up. | |
| 148 | 140 | |
| 149 | 141 | First, let's update and upgrade our server. |
| 150 | 142 | |
| 151 | *NOTE:* Since we have logged in to the server as =root= for now, we | |
| 152 | don't need to use the =sudo= modifier before our commands. | |
| 143 | **NOTE:** Since we have logged in to the server as `root` for | |
| 144 | now, we don't need to use the `sudo` modifier before our | |
| 145 | commands. | |
| 153 | 146 | |
| 154 | #+begin_src sh | |
| 147 | ```sh | |
| 155 | 148 | apt update && apt upgrade -y |
| 156 | #+end_src | |
| 149 | ``` | |
| 150 | ||
| 151 | ## Create A User Account | |
| 157 | 152 | |
| 158 | ** Create A User Account | |
| 159 | :PROPERTIES: | |
| 160 | :CUSTOM_ID: create-a-user-account | |
| 161 | :END: | |
| 162 | While being able to use =root= can be beneficial at times, you shouldn't | |
| 163 | use =root= unless you have to. | |
| 153 | While being able to use `root` can be beneficial at times, | |
| 154 | you shouldn't use `root` unless you have to. | |
| 164 | 155 | |
| 165 | So let's set up a new user in our system. The =-m= option below tells | |
| 166 | the OS to create a home directory for the new user. | |
| 156 | So let's set up a new user in our system. The `-m` option | |
| 157 | below tells the OS to create a home directory for the new user. | |
| 167 | 158 | |
| 168 | #+begin_src sh | |
| 159 | ```sh | |
| 169 | 160 | adduser USERNAME |
| 170 | #+end_src | |
| 161 | ``` | |
| 171 | 162 | |
| 172 | 163 | Now, create a password for that user. |
| 173 | 164 | |
| 174 | #+begin_src sh | |
| 165 | ```sh | |
| 175 | 166 | passwd USERNAME |
| 176 | #+end_src | |
| 167 | ``` | |
| 177 | 168 | |
| 178 | 169 | Finally, add the user to the sudoers file, so they can perform |
| 179 | 170 | priveleged commands. |
| 180 | 171 | |
| 181 | #+begin_src sh | |
| 172 | ```sh | |
| 182 | 173 | usermod -a -G sudo USERNAME |
| 183 | #+end_src | |
| 174 | ``` | |
| 184 | 175 | |
| 185 | 176 | If you are using SSH keys and not passwords, you'll need to copy your |
| 186 | 177 | SSH key from your local machine to the VPS. If you haven't disabled |
| 187 | password-based SSH yet, the easiest way to do this is =ssh-copy-id= from | |
| 188 | your local computer (not from the VPS): | |
| 178 | password-based SSH yet, the easiest way to do this is | |
| 179 | `ssh-copy-id` from your local computer (not from the VPS): | |
| 189 | 180 | |
| 190 | #+begin_src sh | |
| 181 | ```sh | |
| 191 | 182 | ssh-copy-id testuser@xxx.xxx.xxx.xxx |
| 192 | #+end_src | |
| 183 | ``` | |
| 193 | 184 | |
| 194 | If you've disabled password-based SSH, you'll need to manually copy your | |
| 195 | SSH key into the =~/.ssh/authorized_keys= file. | |
| 185 | If you've disabled password-based SSH, you'll need to manually copy | |
| 186 | your SSH key into the `~/.ssh/authorized_keys` file. | |
| 187 | ||
| 188 | ## Install Software | |
| 196 | 189 | |
| 197 | ** Install Software | |
| 198 | :PROPERTIES: | |
| 199 | :CUSTOM_ID: install-software | |
| 200 | :END: | |
| 201 | 190 | Our goal here is to host a web server, so the next step is to install |
| 202 | 191 | the Apache web server and any other packages we need. |
| 203 | 192 | |
| 204 | From this point on, I will be logged in as a user (not =root=) and will | |
| 205 | need to use the =sudo= modifier for most commands. | |
| 193 | From this point on, I will be logged in as a user (not | |
| 194 | `root`) and will need to use the `sudo` modifier | |
| 195 | for most commands. | |
| 206 | 196 | |
| 207 | #+begin_src sh | |
| 197 | ```sh | |
| 208 | 198 | sudo apt update; sudo apt upgrade -y; sudo apt autoremove -y |
| 209 | 199 | sudo apt install apache2 |
| 210 | #+end_src | |
| 200 | ``` | |
| 211 | 201 | |
| 212 | 202 | If you need other language support, such as PHP, you'll need to install |
| 213 | 203 | that too. |
| 214 | 204 | |
| 215 | #+begin_src sh | |
| 205 | ```sh | |
| 216 | 206 | sudo apt install libapache2-mod-php php-dom |
| 217 | 207 | sudo a2enmod php |
| 218 | 208 | sudo systemctl restart apache2 |
| 219 | #+end_src | |
| 209 | ``` | |
| 210 | ||
| 211 | ## Website Files & Folders | |
| 220 | 212 | |
| 221 | ** Website Files & Folders | |
| 222 | :PROPERTIES: | |
| 223 | :CUSTOM_ID: website-files-folders | |
| 224 | :END: | |
| 225 | 213 | Next up is to create the directories for the domain(s) we want to be |
| 226 | 214 | hosted on this web server. |
| 227 | 215 | |
| 228 | #+begin_src sh | |
| 216 | ```sh | |
| 229 | 217 | cd /var/www |
| 230 | 218 | sudo mkdir example.com |
| 231 | #+end_src | |
| 219 | ``` | |
| 232 | 220 | |
| 233 | We have a folder for =example.com= now, so let's add an =index.html= | |
| 234 | file and put it within a specific =public_html= folder. You don't need | |
| 235 | this =public_html= if you don't want it, but it helps with organizing | |
| 236 | items related to =example.com= that you don't want to publish to the | |
| 237 | internet. | |
| 221 | We have a folder for `example.com` now, so let's add an | |
| 222 | `index.html` file and put it within a specific | |
| 223 | `public_html` folder. You don't need this | |
| 224 | `public_html` if you don't want it, but it helps with | |
| 225 | organizing items related to `example.com` that you don't | |
| 226 | want to publish to the internet. | |
| 238 | 227 | |
| 239 | #+begin_src sh | |
| 228 | ```sh | |
| 240 | 229 | cd example.com |
| 241 | 230 | sudo mkdir public_html && cd public_html |
| 242 | 231 | sudo nano index.html |
| 243 | #+end_src | |
| 232 | ``` | |
| 244 | 233 | |
| 245 | You can put anything you want in this =index.html= file. If you can't | |
| 246 | think of anything, paste this in there: | |
| 234 | You can put anything you want in this `index.html` file. If | |
| 235 | you can't think of anything, paste this in there: | |
| 247 | 236 | |
| 248 | #+begin_src html | |
| 237 | ```html | |
| 249 | 238 | <!DOCTYPE html> |
| 250 | 239 | <html lang="en"> |
| 251 | 240 | <head> |
| @@ -257,37 +246,36 @@ think of anything, paste this in there: | ||
| 257 | 246 | <h1>Hello, world!</h1> |
| 258 | 247 | </body> |
| 259 | 248 | </html> |
| 260 | #+end_src | |
| 249 | ``` | |
| 261 | 250 | |
| 262 | If you want something to be served at =example.com/page01/file.txt=, | |
| 263 | you'll have to create the =page01= directory under the =example.com= | |
| 251 | If you want something to be served at | |
| 252 | `example.com/page01/file.txt`, you'll have to create the | |
| 253 | `page01` directory under the `example.com` | |
| 264 | 254 | directory. For example: |
| 265 | 255 | |
| 266 | #+begin_src sh | |
| 256 | ```sh | |
| 267 | 257 | cd /var/www/example.com/public_html |
| 268 | 258 | sudo mkdir page01 |
| 269 | 259 | sudo nano file.txt |
| 270 | #+end_src | |
| 260 | ``` | |
| 261 | ||
| 262 | ## Apache Configuration | |
| 271 | 263 | |
| 272 | ** Apache Configuration | |
| 273 | :PROPERTIES: | |
| 274 | :CUSTOM_ID: apache-configuration | |
| 275 | :END: | |
| 276 | 264 | Now, let's set up the files that will tell the server where to find the |
| 277 | files for =example.com=. We will copy the default configuration file and | |
| 278 | create our own. | |
| 265 | files for `example.com`. We will copy the default | |
| 266 | configuration file and create our own. | |
| 279 | 267 | |
| 280 | #+begin_src sh | |
| 268 | ```sh | |
| 281 | 269 | cd /etc/apache2/sites-available |
| 282 | 270 | sudo cp 000-default.conf example.com.conf |
| 283 | 271 | sudo nano example.com.conf |
| 284 | #+end_src | |
| 272 | ``` | |
| 285 | 273 | |
| 286 | 274 | This configuration file will have a few default lines, but you'll need |
| 287 | 275 | to edit it to look similar to this (settings may change based on your |
| 288 | 276 | personal needs): |
| 289 | 277 | |
| 290 | #+begin_src config | |
| 278 | ```config | |
| 291 | 279 | <VirtualHost *:80> |
| 292 | 280 | ServerAdmin your-email@email-provider.com |
| 293 | 281 | ServerName example.com |
| @@ -296,85 +284,85 @@ personal needs): | ||
| 296 | 284 | ErrorLog ${APACHE_LOG_DIR}/error.log |
| 297 | 285 | CustomLog ${APACHE_LOG_DIR}/access.log combined |
| 298 | 286 | </VirtualHost> |
| 299 | #+end_src | |
| 287 | ``` | |
| 300 | 288 | |
| 301 | 289 | Now, enable the configuration for your new site, disable the default |
| 302 | 290 | configuration, and reload the web server. |
| 303 | 291 | |
| 304 | #+begin_src sh | |
| 292 | ```sh | |
| 305 | 293 | sudo a2ensite example.com.conf |
| 306 | 294 | sudo a2dissite 000-default.conf |
| 307 | 295 | sudo systemctl reload apache2 |
| 308 | #+end_src | |
| 296 | ``` | |
| 309 | 297 | |
| 310 | 298 | You can always run a test to make sure no errors or warnings are found |
| 311 | 299 | in your configuration files. |
| 312 | 300 | |
| 313 | #+begin_src sh | |
| 301 | ```sh | |
| 314 | 302 | sudo apache2ctl configtest |
| 315 | #+end_src | |
| 303 | ``` | |
| 316 | 304 | |
| 317 | 305 | Now, restart the web server entirely. After this, you should be able to |
| 318 | browse to =http://example.com= and see the HTML content you provided | |
| 319 | earlier. Note that SSL/TLS has not been enabled yet, so you won't be | |
| 320 | able to use the secure version yet (=https://example.com=). | |
| 306 | browse to `http://example.com` and see the HTML content you | |
| 307 | provided earlier. Note that SSL/TLS has not been enabled yet, so you | |
| 308 | won't be able to use the secure version yet | |
| 309 | (`https://example.com`). | |
| 321 | 310 | |
| 322 | #+begin_src sh | |
| 311 | ```sh | |
| 323 | 312 | sudo systemctl restart apache2 |
| 324 | #+end_src | |
| 313 | ``` | |
| 325 | 314 | |
| 326 | 315 | You can repeat this for as many websites as you need. Just create the |
| 327 | domain folders in =/var/www/=, add the configuration file, enable the | |
| 328 | configuration, and restart =apache2=. | |
| 316 | domain folders in `/var/www/`, add the configuration file, | |
| 317 | enable the configuration, and restart `apache2`. | |
| 318 | ||
| 319 | ## SSL/TLS Certificates: Serve Websites Over HTTPS | |
| 329 | 320 | |
| 330 | ** SSL/TLS Certificates: Serve Websites Over HTTPS | |
| 331 | :PROPERTIES: | |
| 332 | :CUSTOM_ID: ssltls-certificates-serve-websites-over-https | |
| 333 | :END: | |
| 334 | 321 | In order to serve secure content, you'll need to obtain SSL/TLS |
| 335 | 322 | certificates. Luckily, there's a free tool called |
| 336 | [[https://certbot.eff.org][Certbot]] that helps us with the process. | |
| 323 | [Certbot](https://certbot.eff.org) that helps us with the process. | |
| 337 | 324 | |
| 338 | The first step is to install =snapd= and =core= for Ubuntu. | |
| 325 | The first step is to install `snapd` and `core` | |
| 326 | for Ubuntu. | |
| 339 | 327 | |
| 340 | #+begin_src sh | |
| 328 | ```sh | |
| 341 | 329 | sudo apt install snapd |
| 342 | 330 | sudo snap install core |
| 343 | 331 | sudo snap refresh core |
| 344 | #+end_src | |
| 332 | ``` | |
| 345 | 333 | |
| 346 | Next, install the =certbot= snap package. | |
| 334 | Next, install the `certbot` snap package. | |
| 347 | 335 | |
| 348 | #+begin_src sh | |
| 336 | ```sh | |
| 349 | 337 | sudo snap install --classic certbot |
| 350 | #+end_src | |
| 338 | ``` | |
| 351 | 339 | |
| 352 | Execute the following command to ensure that the =certbot= command can | |
| 353 | be run. | |
| 340 | Execute the following command to ensure that the `certbot` | |
| 341 | command can be run. | |
| 354 | 342 | |
| 355 | #+begin_src sh | |
| 343 | ```sh | |
| 356 | 344 | sudo ln -s /snap/bin/certbot /usr/bin/certbot |
| 357 | #+end_src | |
| 345 | ``` | |
| 358 | 346 | |
| 359 | Finally, you can run =certbot= one of two ways: | |
| 347 | Finally, you can run `certbot` one of two ways: | |
| 360 | 348 | |
| 361 | 1. run it and let it alter your Apache configuration files automatically | |
| 362 | to enable HTTPS redirects. | |
| 363 | 2. run it and only allow it to create certificates. You'll need to | |
| 364 | manually alter the config files to enable HTTPS redirects. | |
| 349 | 1. run it and let it alter your Apache configuration files | |
| 350 | automatically to enable HTTPS redirects. | |
| 351 | 2. run it and only allow it to create certificates. You'll need to | |
| 352 | manually alter the config files to enable HTTPS redirects. | |
| 365 | 353 | |
| 366 | 354 | Run certbot and allow automatic config changes: |
| 367 | 355 | |
| 368 | #+begin_src sh | |
| 356 | ```sh | |
| 369 | 357 | sudo certbot --apache |
| 370 | #+end_src | |
| 358 | ``` | |
| 371 | 359 | |
| 372 | 360 | Run certbot for certificates only and don't allow it to alter config |
| 373 | 361 | files: |
| 374 | 362 | |
| 375 | #+begin_src sh | |
| 363 | ```sh | |
| 376 | 364 | sudo certbot certonly --apache |
| 377 | #+end_src | |
| 365 | ``` | |
| 378 | 366 | |
| 379 | 367 | The Certbot packages on your system come with a cron job or systemd |
| 380 | 368 | timer that will renew your certificates automatically before they |
| @@ -382,52 +370,48 @@ expire. You will not need to run Certbot again unless you change your | ||
| 382 | 370 | configuration. You can test automatic renewal for your certificates by |
| 383 | 371 | running this command: |
| 384 | 372 | |
| 385 | #+begin_src sh | |
| 373 | ```sh | |
| 386 | 374 | sudo certbot renew --dry-run |
| 387 | #+end_src | |
| 375 | ``` | |
| 376 | ||
| 377 | Now, test your domains by going to `https://example.com`. | |
| 388 | 378 | |
| 389 | Now, test your domains by going to =https://example.com=. | |
| 379 | ## Firewall Security | |
| 390 | 380 | |
| 391 | ** Firewall Security | |
| 392 | :PROPERTIES: | |
| 393 | :CUSTOM_ID: firewall-security | |
| 394 | :END: | |
| 395 | 381 | To enable better security on your server, you'll need to enable a basic |
| 396 | firewall. For Ubuntu, we'll use | |
| 397 | [[https://cleberg.net/blog/secure-your-network-with-the-uncomplicated-firewall.html][the | |
| 398 | uncomplicated firewall]]. | |
| 382 | firewall. For Ubuntu, we'll use [the uncomplicated | |
| 383 | firewall](https://cleberg.net/blog/secure-your-network-with-the-uncomplicated-firewall.html). | |
| 399 | 384 | |
| 400 | 385 | Now, add the following rules to the firewall allow SSH, Apache, and |
| 401 | 386 | HTTP(S) connections. If you need to, you can enable different ports for |
| 402 | 387 | specifics applications, SFTP, etc. |
| 403 | 388 | |
| 404 | #+begin_src sh | |
| 389 | ```sh | |
| 405 | 390 | sudo ufw default deny incoming |
| 406 | 391 | sudo ufw default allow outgoing |
| 407 | 392 | sudo ufw allow OpenSSH |
| 408 | 393 | sudo ufw allow Apache |
| 409 | 394 | sudo ufw allow proto tcp from any to any port 80,443 |
| 410 | #+end_src | |
| 395 | ``` | |
| 411 | 396 | |
| 412 | 397 | Once you've added all the rules you need, enable the firewall. |
| 413 | 398 | |
| 414 | #+begin_src sh | |
| 399 | ```sh | |
| 415 | 400 | sudo ufw enable |
| 416 | #+end_src | |
| 401 | ``` | |
| 402 | ||
| 403 | ## Troubleshooting | |
| 417 | 404 | |
| 418 | ** Troubleshooting | |
| 419 | :PROPERTIES: | |
| 420 | :CUSTOM_ID: troubleshooting | |
| 421 | :END: | |
| 422 | 405 | If you run into any issues during your VPS set-up, be sure to walk back |
| 423 | 406 | through your actions and make sure you didn't miss any steps. |
| 424 | 407 | |
| 425 | 408 | Many websites have fantastic guides to setting up various web servers. |
| 426 | This is one of the areas | |
| 427 | [[https://www.digitalocean.%20com/community/tutorials][where | |
| 428 | DigitalOcean shines]]. For simpler or more Linux-oriented questions, I | |
| 429 | suggest using [Linuxize] (https://linuxize.com). | |
| 409 | This is one of the areas [where DigitalOcean | |
| 410 | shines](https://www.digitalocean.%20com/community/tutorials). For | |
| 411 | simpler or more Linux-oriented questions, I suggest using [Linuxize] | |
| 412 | (<https://linuxize.com>). | |
| 430 | 413 | |
| 431 | 414 | If you're getting certain errors (e.g. =500 Internal Server Error=) and |
| 432 | need to debug locally, you can view the =access.log= and =error.log= | |
| 433 | files in the =/var/log/apache/= directory. | |
| 415 | need to debug locally, you can view the `access.log` and | |
| 416 | `error.log` files in the `/var/log/apache/` | |
| 417 | directory. | |
blog/2021-04-17-gemini-server.org → content/blog/2021-04-17-gemini-server.md renamed +78 −95
| @@ -1,181 +1,164 @@ | ||
| 1 | #+title: Hosting a Gemini Server | |
| 2 | #+date: 2021-04-17 | |
| 1 | +++ | |
| 2 | date = 2021-04-17 | |
| 3 | title = "Hosting a Gemini Server" | |
| 4 | description = "A guide to self-hosting a Gemini web server on your own server." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Similar Article Available | |
| 3 | 8 | |
| 4 | ** Similar Article Available | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: similar-article-available | |
| 7 | :END: | |
| 8 | 9 | To read more about Gemini and ways to test out this new protocol without |
| 9 | your own server, see my previous post | |
| 10 | [[/blog/launching-a-gemini-capsule/][Launching a Gemini Capsule]]. | |
| 10 | your own server, see my previous post [Launching a Gemini | |
| 11 | Capsule](/blog/launching-a-gemini-capsule/). | |
| 12 | ||
| 13 | ## Preparation | |
| 11 | 14 | |
| 12 | ** Preparation | |
| 13 | :PROPERTIES: | |
| 14 | :CUSTOM_ID: preparation | |
| 15 | :END: | |
| 16 | 15 | This guide assumes you have access to a server accessible to the world |
| 17 | 16 | through a public IP address and that you own a domain name used for this |
| 18 | 17 | Gemini capsule. |
| 19 | 18 | |
| 20 | ** Getting Started with Agate | |
| 21 | :PROPERTIES: | |
| 22 | :CUSTOM_ID: getting-started-with-agate | |
| 23 | :END: | |
| 24 | We are going to use [[https://github.com/mbrubeck/agate][Agate]] for | |
| 25 | this tutorial. This is a basic Gemini server written in Rust. It takes | |
| 26 | very little time and maintenance to get it running. | |
| 27 | ||
| 28 | ** Install Dependencies | |
| 29 | :PROPERTIES: | |
| 30 | :CUSTOM_ID: install-dependencies | |
| 31 | :END: | |
| 19 | ## Getting Started with Agate | |
| 20 | ||
| 21 | We are going to use [Agate](https://github.com/mbrubeck/agate) for this | |
| 22 | tutorial. This is a basic Gemini server written in Rust. It takes very | |
| 23 | little time and maintenance to get it running. | |
| 24 | ||
| 25 | ## Install Dependencies | |
| 26 | ||
| 32 | 27 | First, you will need to install the Rust package for your system. On |
| 33 | Ubuntu, use the following commands (remember to use =sudo= if you are | |
| 34 | not the root user). The Rust installation will give you options to | |
| 35 | customize the installation; I used the default installation options. | |
| 28 | Ubuntu, use the following commands (remember to use `sudo` if | |
| 29 | you are not the root user). The Rust installation will give you options | |
| 30 | to customize the installation; I used the default installation options. | |
| 36 | 31 | |
| 37 | #+begin_src sh | |
| 32 | ```sh | |
| 38 | 33 | sudo apt update && sudo apt upgrade -y |
| 39 | 34 | curl https://sh.rustup.rs -sSf | sh |
| 40 | #+end_src | |
| 35 | ``` | |
| 41 | 36 | |
| 42 | 37 | Remember to configure your shell with the new configuration: |
| 43 | 38 | |
| 44 | #+begin_src sh | |
| 39 | ```sh | |
| 45 | 40 | source $HOME/.cargo/env |
| 46 | #+end_src | |
| 41 | ``` | |
| 47 | 42 | |
| 48 | Before we install agate, make sure you have the =gcc= package installed: | |
| 43 | Before we install agate, make sure you have the `gcc` package | |
| 44 | installed: | |
| 49 | 45 | |
| 50 | #+begin_src sh | |
| 46 | ```sh | |
| 51 | 47 | sudo apt install gcc |
| 52 | #+end_src | |
| 48 | ``` | |
| 53 | 49 | |
| 54 | 50 | Next, you'll need to install the agate executable with Rust's Cargo |
| 55 | 51 | package maintainer: |
| 56 | 52 | |
| 57 | #+begin_src sh | |
| 53 | ```sh | |
| 58 | 54 | cargo install agate |
| 59 | #+end_src | |
| 55 | ``` | |
| 56 | ||
| 57 | ## Create Symlinks | |
| 60 | 58 | |
| 61 | ** Create Symlinks | |
| 62 | :PROPERTIES: | |
| 63 | :CUSTOM_ID: create-symlinks | |
| 64 | :END: | |
| 65 | 59 | Once Cargo has finished installing all the required packages, symlink |
| 66 | the executable to your $PATH. | |
| 60 | the executable to your \$PATH. | |
| 67 | 61 | |
| 68 | #+begin_src sh | |
| 62 | ```sh | |
| 69 | 63 | sudo ln -s $HOME/.cargo/bin/agate /usr/local/bin/agate |
| 70 | #+end_src | |
| 64 | ``` | |
| 65 | ||
| 66 | ## Using Agate's Built-In Installation Tool | |
| 71 | 67 | |
| 72 | ** Using Agate's Built-In Installation Tool | |
| 73 | :PROPERTIES: | |
| 74 | :CUSTOM_ID: using-agates-built-in-installation-tool | |
| 75 | :END: | |
| 76 | 68 | If you're running Ubuntu or Debian, use the Debian installation script |
| 77 | found in Agate's GitHub repository, under the =tools/debian= folder. | |
| 69 | found in Agate's GitHub repository, under the `tools/debian` | |
| 70 | folder. | |
| 78 | 71 | |
| 79 | #+begin_src sh | |
| 72 | ```sh | |
| 80 | 73 | git clone https://github.com/mbrubeck/agate |
| 81 | 74 | cd agate/tools/debian |
| 82 | 75 | sudo ./install.sh |
| 83 | #+end_src | |
| 76 | ``` | |
| 77 | ||
| 78 | ## Configure the Gemini Service | |
| 84 | 79 | |
| 85 | ** Configure the Gemini Service | |
| 86 | :PROPERTIES: | |
| 87 | :CUSTOM_ID: configure-the-gemini-service | |
| 88 | :END: | |
| 89 | 80 | We have a little more to do, but since this script tries to immediately |
| 90 | 81 | run the service, it will likely fail with an exit code. Let's add our |
| 91 | 82 | finishing touches. Edit the following file and replace the hostname with |
| 92 | 83 | your desired URL. You can also change the directory where content will |
| 93 | 84 | be served. |
| 94 | 85 | |
| 95 | #+begin_src sh | |
| 86 | ```sh | |
| 96 | 87 | sudo nano /etc/systemd/system/gemini.service |
| 97 | #+end_src | |
| 88 | ``` | |
| 98 | 89 | |
| 99 | #+begin_src sh | |
| 90 | ```sh | |
| 100 | 91 | # Edit these lines to whatever you want - see the next code block for my personal configuration. |
| 101 | 92 | WorkingDirectory=/srv/gemini |
| 102 | 93 | ExecStart=agate --hostname $(uname -n) --lang en |
| 103 | #+end_src | |
| 94 | ``` | |
| 104 | 95 | |
| 105 | 96 | This is my personal config: |
| 106 | 97 | |
| 107 | #+begin_src sh | |
| 98 | ```sh | |
| 108 | 99 | WorkingDirectory=/var/gemini/ |
| 109 | 100 | ExecStart=agate --hostname gemini.example.com --lang en |
| 110 | #+end_src | |
| 101 | ``` | |
| 111 | 102 | |
| 112 | 103 | Since we've altered the systemd configuration files, we have to reload |
| 113 | 104 | the daemon. Let's do that, restart our service, and check its status. |
| 114 | 105 | |
| 115 | #+begin_src sh | |
| 106 | ```sh | |
| 116 | 107 | sudo systemctl daemon-reload |
| 117 | 108 | sudo systemctl restart gemini.service |
| 118 | 109 | sudo systemctl status gemini.service |
| 119 | #+end_src | |
| 110 | ``` | |
| 111 | ||
| 112 | ## Fixing Systemd Errors | |
| 120 | 113 | |
| 121 | ** Fixing Systemd Errors | |
| 122 | :PROPERTIES: | |
| 123 | :CUSTOM_ID: fixing-systemd-errors | |
| 124 | :END: | |
| 125 | 114 | If you're still getting errors, the installation process may not have |
| 126 | 115 | properly enabled the gemini service. Fix it with the following commands. |
| 127 | 116 | |
| 128 | #+begin_src sh | |
| 117 | ```sh | |
| 129 | 118 | sudo systemctl enable gemini.service |
| 130 | 119 | sudo systemctl restart gemini.service |
| 131 | 120 | sudo systemctl status gemini.service |
| 132 | #+end_src | |
| 121 | ``` | |
| 122 | ||
| 123 | ## Firewall Rules | |
| 133 | 124 | |
| 134 | ** Firewall Rules | |
| 135 | :PROPERTIES: | |
| 136 | :CUSTOM_ID: firewall-rules | |
| 137 | :END: | |
| 138 | 125 | Great! Our server is now functional and running. The first consideration |
| 139 | 126 | now is that you need to be able to access port 1965 on the server. If |
| 140 | 127 | you have a firewall enabled, you'll need to open that port up. |
| 141 | 128 | |
| 142 | #+begin_src sh | |
| 129 | ```sh | |
| 143 | 130 | sudo ufw allow 1965 |
| 144 | 131 | sudo ufw reload |
| 145 | #+end_src | |
| 132 | ``` | |
| 133 | ||
| 134 | ## Creating Content | |
| 146 | 135 | |
| 147 | ** Creating Content | |
| 148 | :PROPERTIES: | |
| 149 | :CUSTOM_ID: creating-content | |
| 150 | :END: | |
| 151 | 136 | Let's create the Gemini capsule. Note that wherever you set the |
| 152 | 137 | WorkingDirectory variable to earlier, Agate will expect you to put your |
| 153 | Gemini capsule contents in a sub-folder called "content." So, I place my | |
| 154 | files in "/var/gmi/content." I'm going to create that folder now and put | |
| 155 | a file in there. | |
| 138 | Gemini capsule contents in a sub-folder called "content." So, I place | |
| 139 | my files in "/var/gmi/content." I'm going to create that folder now | |
| 140 | and put a file in there. | |
| 156 | 141 | |
| 157 | #+begin_src sh | |
| 142 | ```sh | |
| 158 | 143 | sudo mkdir /var/gemini/content |
| 159 | 144 | sudo nano /var/gemini/content/index.gmi |
| 160 | #+end_src | |
| 145 | ``` | |
| 161 | 146 | |
| 162 | 147 | You can put whatever you want in the "index.gmi" file, just make sure |
| 163 | 148 | it's valid Gemtext. |
| 164 | 149 | |
| 165 | ** The Results | |
| 166 | :PROPERTIES: | |
| 167 | :CUSTOM_ID: the-results | |
| 168 | :END: | |
| 150 | ## The Results | |
| 151 | ||
| 169 | 152 | Here are some screenshots of the Gemini page I just created in the |
| 170 | [[https://gmi.skyjake.fi/lagrange/][Lagrange]] browser and the | |
| 171 | [[https://github.com/makeworld-the-better-one/amfora][amfora]] browser. | |
| 153 | [Lagrange](https://gmi.skyjake.fi/lagrange/) browser and the | |
| 154 | [amfora](https://github.com/makeworld-the-better-one/amfora) browser. | |
| 172 | 155 | |
| 173 | #+caption: GUI Gemini browser | |
| 174 | [[https://img.cleberg.net/blog/20210417-hosting-a-gemini-server/lagrange.png]] | |
| 156 |  | |
| 175 | 158 | |
| 176 | /Lagrange/ | |
| 159 | *Lagrange* | |
| 177 | 160 | |
| 178 | #+caption: CLI Gemini browser | |
| 179 | [[https://img.cleberg.net/blog/20210417-hosting-a-gemini-server/amfora.png]] | |
| 161 |  | |
| 180 | 163 | |
| 181 | /Amfora/ | |
| 164 | *Amfora* | |
blog/2021-04-23-php-comment-system.org → content/blog/2021-04-23-php-comment-system.md renamed +95 −103
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: Roll Your Own Static Commenting System in PHP | |
| 2 | #+date: 2021-04-23 | |
| 1 | +++ | |
| 2 | date = 2021-04-23 | |
| 3 | title = "Roll Your Own Static Commenting System in PHP" | |
| 4 | description = "A simple guide to creating a commenting system in PHP." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## The Terrible-ness of Commenting Systems | |
| 3 | 8 | |
| 4 | ** The Terrible-ness of Commenting Systems | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: the-terrible-ness-of-commenting-systems | |
| 7 | :END: | |
| 8 | 9 | The current state of affairs regarding interactive comment systems is, |
| 9 | 10 | well, terrible. It is especially awful if you're a privacy conscious |
| 10 | 11 | person who does not generally load third-party scripts or frames on the |
| @@ -15,26 +16,24 @@ something that should be standard. | ||
| 15 | 16 | |
| 16 | 17 | Of course, there are some really terrible options: |
| 17 | 18 | |
| 18 | - Facebook Comments | |
| 19 | - Discourse | |
| 19 | - Facebook Comments | |
| 20 | - Discourse | |
| 20 | 21 | |
| 21 | 22 | There are some options that are better but still use too many scripts, |
| 22 | 23 | frames, or social integrations on your web page that could impact some |
| 23 | 24 | users: |
| 24 | 25 | |
| 25 | - Disqus | |
| 26 | - Isso | |
| 27 | - Remark42 | |
| 26 | - Disqus | |
| 27 | - Isso | |
| 28 | - Remark42 | |
| 28 | 29 | |
| 29 | 30 | Lastly, I looked into a few unique ways of generating blog comments, |
| 30 | 31 | such as using Twitter threads or GitHub issues to automatically post |
| 31 | 32 | issues. However, these both rely on external third-party sites that I |
| 32 | 33 | don't currently use. |
| 33 | 34 | |
| 34 | ** Stay Static with Server-Side Comments | |
| 35 | :PROPERTIES: | |
| 36 | :CUSTOM_ID: stay-static-with-server-side-comments | |
| 37 | :END: | |
| 35 | ## Stay Static with Server-Side Comments | |
| 36 | ||
| 38 | 37 | The main issue for my personal use-case is that my blog is completely, |
| 39 | 38 | 100% static. I use PHP on the back-end but website visitors only see |
| 40 | 39 | HTML and a single CSS file. No external javascript and no embedded |
| @@ -42,14 +41,15 @@ frames. | ||
| 42 | 41 | |
| 43 | 42 | So, how do we keep a site static and still allow users to interact with |
| 44 | 43 | blog posts? The key actually pretty simple - I'm already using PHP, so |
| 45 | why not rely on the classic HTML =<form>= and a PHP script to save the | |
| 46 | comments somewhere? As it turns out, this was a perfect solution for me. | |
| 44 | why not rely on the classic HTML `<form>` and a PHP script to | |
| 45 | save the comments somewhere? As it turns out, this was a perfect | |
| 46 | solution for me. | |
| 47 | 47 | |
| 48 | 48 | The second issue for my personal use-case is that I am trying to keep |
| 49 | 49 | the contents of my website accessible over time, as described by |
| 50 | [cite/t:@brandur], in his post entitled | |
| 51 | [[https://brandur.org/fragments/graceful-degradation-time][Blog with | |
| 52 | Markdown + Git, and degrade gracefully through time]] . | |
| 50 | @brandur, in his post entitled [Blog with Markdown + Git, and degrade | |
| 51 | gracefully through | |
| 52 | time](https://brandur.org/fragments/graceful-degradation-time) . | |
| 53 | 53 | |
| 54 | 54 | This means I cannot rely on a database for comments, since I do not rely |
| 55 | 55 | on a database for any other part of my websites. |
| @@ -59,37 +59,35 @@ that future readers will be able to see the source data long after I'm | ||
| 59 | 59 | gone, or the website has gone offline. However, I still haven't |
| 60 | 60 | committed any images served on my blog to Git, as I'm not entirely sold |
| 61 | 61 | on Git LFS yet - for now, images can be found at |
| 62 | [[https://img.cleberg.net][img.cleberg.net]]. | |
| 62 | [img.cleberg.net](https://img.cleberg.net). | |
| 63 | 63 | |
| 64 | 64 | Saving my comments back to the Git repository ensures that another |
| 65 | 65 | aspect of my site will degrade gracefully. |
| 66 | 66 | |
| 67 | ** Create a Comment Form | |
| 68 | :PROPERTIES: | |
| 69 | :CUSTOM_ID: create-a-comment-form | |
| 70 | :END: | |
| 67 | ## Create a Comment Form | |
| 68 | ||
| 71 | 69 | Okay, let's get started. The first step is to create an HTML form that |
| 72 | 70 | users can see and utilize to submit comments. This is fairly easy and |
| 73 | 71 | can be changed depending on your personal preferences. |
| 74 | 72 | |
| 75 | 73 | Take a look at the code block below for the form I currently use. Note |
| 76 | that =<current-url>= is replaced automatically in PHP with the current | |
| 77 | post's URL, so that my PHP script used later will know which blog post | |
| 78 | the comment is related to. | |
| 74 | that `<current-url>` is replaced automatically in PHP with | |
| 75 | the current post's URL, so that my PHP script used later will know | |
| 76 | which blog post the comment is related to. | |
| 79 | 77 | |
| 80 | 78 | The form contains the following structure: |
| 81 | 79 | |
| 82 | 1. =<form>= - This is the form and will determine which PHP script to | |
| 83 | send the comment to. | |
| 84 | 2. =<section hidden>= - This section is hidden from the user and is used | |
| 85 | to ensure that we know which blog post sent the comment. | |
| 86 | 3. =<section>= Display Name (Optional) - Used to accept a display name, | |
| 87 | if entered. | |
| 88 | 4. =<section>= Comment (Required) - Used to accept the user's full | |
| 89 | comment. Markdown is allowed. | |
| 90 | 5. =<button>= - A button to submit the form. | |
| 91 | ||
| 92 | #+begin_src html | |
| 80 | 1. `<form>` - This is the form and will determine which PHP | |
| 81 | script to send the comment to. | |
| 82 | 2. `<section hidden>` - This section is hidden from the user | |
| 83 | and is used to ensure that we know which blog post sent the comment. | |
| 84 | 3. `<section>` Display Name (Optional) - Used to accept a | |
| 85 | display name, if entered. | |
| 86 | 4. `<section>` Comment (Required) - Used to accept the | |
| 87 | user's full comment. Markdown is allowed. | |
| 88 | 5. `<button>` - A button to submit the form. | |
| 89 | ||
| 90 | ```html | |
| 93 | 91 | <form action="/comment.php" method="POST"> |
| 94 | 92 | <h3>Leave a Comment</h3> |
| 95 | 93 | <section hidden> |
| @@ -129,33 +127,32 @@ The form contains the following structure: | ||
| 129 | 127 | </section> |
| 130 | 128 | <button type="submit">Submit</button> |
| 131 | 129 | </form> |
| 132 | #+end_src | |
| 130 | ``` | |
| 131 | ||
| 132 | ## Handle Comments via POST | |
| 133 | 133 | |
| 134 | ** Handle Comments via POST | |
| 135 | :PROPERTIES: | |
| 136 | :CUSTOM_ID: handle-comments-via-post | |
| 137 | :END: | |
| 138 | 134 | Now that we have a form and can submit comments, we need to create a PHP |
| 139 | 135 | script so that the server can fetch the comment data and save it. Make |
| 140 | sure your PHP script name matches the name you entered in the =action= | |
| 141 | field in your form. | |
| 142 | ||
| 143 | See the code block below for the contents of my =comment.php= script. We | |
| 144 | perform the following tasks in this script: | |
| 145 | ||
| 146 | 1. Grab the POST data from the HTML form. | |
| 147 | 2. Sanitize the comment data with =htmlentities=. | |
| 148 | 3. Set the display name to =Anonymous= if it was left blank. | |
| 149 | 4. Create a PHP object that combines all of this data. | |
| 150 | 5. Check if our destination file =comments.json= exists. | |
| 151 | 6. If so, convert the PHP object to JSON and save it to the file. | |
| 152 | 7. If the =comments.json= file does not exist, the script will exit with | |
| 153 | an error. You can alter this to ensure it creates the script, but my | |
| 154 | source code includes the file by default, so it will always exist. | |
| 155 | 8. Finally, send the user back to the =#comments= section of the blog | |
| 156 | post they just read. | |
| 157 | ||
| 158 | #+begin_src php | |
| 136 | sure your PHP script name matches the name you entered in the | |
| 137 | `action` field in your form. | |
| 138 | ||
| 139 | See the code block below for the contents of my `comment.php` | |
| 140 | script. We perform the following tasks in this script: | |
| 141 | ||
| 142 | 1. Grab the POST data from the HTML form. | |
| 143 | 2. Sanitize the comment data with `htmlentities`. | |
| 144 | 3. Set the display name to `Anonymous` if it was left blank. | |
| 145 | 4. Create a PHP object that combines all of this data. | |
| 146 | 5. Check if our destination file `comments.json` exists. | |
| 147 | 6. If so, convert the PHP object to JSON and save it to the file. | |
| 148 | 7. If the `comments.json` file does not exist, the script | |
| 149 | will exit with an error. You can alter this to ensure it creates the | |
| 150 | script, but my source code includes the file by default, so it will | |
| 151 | always exist. | |
| 152 | 8. Finally, send the user back to the `#comments` section of | |
| 153 | the blog post they just read. | |
| 154 | ||
| 155 | ```php | |
| 159 | 156 | // Get the content sent from the comment form |
| 160 | 157 | $comment = htmlentities($_POST['userContent']); |
| 161 | 158 | $post_url = $_POST['postURL']; |
| @@ -190,35 +187,33 @@ if (file_exists($file_name)) { | ||
| 190 | 187 | |
| 191 | 188 | // Send the user back |
| 192 | 189 | header('Location: ' . $post_url . '#comments'); |
| 193 | #+end_src | |
| 190 | ``` | |
| 194 | 191 | |
| 195 | If you're using Apache, make sure the =www-data= user on your server has | |
| 196 | the correct permissions to your website directory or else it will not be | |
| 197 | able to write to =comments.json=. | |
| 192 | If you're using Apache, make sure the `www-data` user on | |
| 193 | your server has the correct permissions to your website directory or | |
| 194 | else it will not be able to write to `comments.json`. | |
| 198 | 195 | |
| 199 | #+begin_src sh | |
| 196 | ```sh | |
| 200 | 197 | chgrp -R www-data /path/to/website/ |
| 201 | 198 | chmod -R g+w comments.json |
| 202 | #+end_src | |
| 199 | ``` | |
| 200 | ||
| 201 | ## Display User Comments | |
| 203 | 202 | |
| 204 | ** Display User Comments | |
| 205 | :PROPERTIES: | |
| 206 | :CUSTOM_ID: display-user-comments | |
| 207 | :END: | |
| 208 | 203 | Now that we can submit comments, and they are saved to the |
| 209 | =comments.json= file, let's make sure we can show those comments in each | |
| 210 | blog post. | |
| 204 | `comments.json` file, let's make sure we can show those | |
| 205 | comments in each blog post. | |
| 211 | 206 | |
| 212 | 207 | The code block below shows the function I use to decode my |
| 213 | =comments.json= file, check if the comments apply to the current post, | |
| 214 | and then display them. | |
| 208 | `comments.json` file, check if the comments apply to the | |
| 209 | current post, and then display them. | |
| 215 | 210 | |
| 216 | This piece of code should *really* be inside a function (or at least in | |
| 217 | an organized PHP workflow). Don't just copy-and-paste and expect it to | |
| 218 | work. You need to at least supply a =$query= variable depending on the | |
| 219 | page visited. | |
| 211 | This piece of code should **really** be inside a function (or at least | |
| 212 | in an organized PHP workflow). Don't just copy-and-paste and expect it | |
| 213 | to work. You need to at least supply a `$query` variable | |
| 214 | depending on the page visited. | |
| 220 | 215 | |
| 221 | #+begin_src php | |
| 216 | ```php | |
| 222 | 217 | $query = 'your-blog-post.html'; |
| 223 | 218 | |
| 224 | 219 | // Load saved comments |
| @@ -250,35 +245,32 @@ foreach ($comments as $comment) { | ||
| 250 | 245 | } |
| 251 | 246 | |
| 252 | 247 | echo $comment_section; |
| 253 | #+end_src | |
| 248 | ``` | |
| 249 | ||
| 250 | ## Bonus: Create a 'Recent Comments' Page | |
| 254 | 251 | |
| 255 | ** Bonus: Create a 'Recent Comments' Page | |
| 256 | :PROPERTIES: | |
| 257 | :CUSTOM_ID: bonus-create-a-recent-comments-page | |
| 258 | :END: | |
| 259 | 252 | Finally, the last part of my current system is to create a Recent |
| 260 | 253 | Comments page so that I can easily check-in on my blog and moderate any |
| 261 | spam. As an alternative, you could use PHP's =mail()= function to email | |
| 262 | you for each blog comment. | |
| 254 | spam. As an alternative, you could use PHP's `mail()` | |
| 255 | function to email you for each blog comment. | |
| 263 | 256 | |
| 264 | 257 | The code to do this is literally the same code as the previous section, |
| 265 | 258 | I just make sure it is printed when someone visits |
| 266 | =https://example.com/comments/=. | |
| 259 | `https://example.com/comments/`. | |
| 260 | ||
| 261 | ## Possible Enhancements | |
| 267 | 262 | |
| 268 | ** Possible Enhancements | |
| 269 | :PROPERTIES: | |
| 270 | :CUSTOM_ID: possible-enhancements | |
| 271 | :END: | |
| 272 | 263 | This comment system is by no means a fully-developed system. I have |
| 273 | 264 | noted a few possible enhancements here that I may implement in the |
| 274 | 265 | future: |
| 275 | 266 | |
| 276 | - Create a secure moderator page with user authentication at | |
| 277 | =https://blog.example.com/mod/=. This page could have the option to | |
| 278 | edit or delete any comment found in =comments.json=. | |
| 279 | - Create a temporary file, such as =pending_comments.json=, that will | |
| 280 | store newly-submitted comments and won't display on blog posts until | |
| 281 | approved by a moderator. | |
| 282 | - Create a =/modlog/= page with a chronological log, showing which | |
| 283 | moderator approved which comments and why certain comments were | |
| 284 | rejected. | |
| 267 | - Create a secure moderator page with user authentication at | |
| 268 | `https://blog.example.com/mod/`. This page could have the | |
| 269 | option to edit or delete any comment found in | |
| 270 | `comments.json`. | |
| 271 | - Create a temporary file, such as `pending_comments.json`, | |
| 272 | that will store newly-submitted comments and won't display on blog | |
| 273 | posts until approved by a moderator. | |
| 274 | - Create a `/modlog/` page with a chronological log, | |
| 275 | showing which moderator approved which comments and why certain | |
| 276 | comments were rejected. | |
blog/2021-04-28-photography.org → content/blog/2021-04-28-photography.md renamed +34 −37
| @@ -1,71 +1,68 @@ | ||
| 1 | #+title: Jumping Back Into Photography | |
| 2 | #+date: 2021-04-28 | |
| 1 | +++ | |
| 2 | date = 2021-04-28 | |
| 3 | title = "Jumping Back Into Photography" | |
| 4 | description = "Some thoughts on photography." | |
| 5 | +++ | |
| 3 | 6 | |
| 4 | ** Why Photography? | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: why-photography | |
| 7 | :END: | |
| 8 | I've often wondered why photography is as enticing as it is. You can see | |
| 9 | billions of people around the world taking photographs every single | |
| 7 | ## Why Photography? | |
| 8 | ||
| 9 | I've often wondered why photography is as enticing as it is. You can | |
| 10 | see billions of people around the world taking photographs every single | |
| 10 | 11 | moment of the day. New technology often boasts about their photographic |
| 11 | 12 | capabilities, especially smartphones. I would even assume that we live |
| 12 | 13 | in a world where there is never a moment in which a photograph is not |
| 13 | 14 | being taken somewhere on Earth. |
| 14 | 15 | |
| 15 | 16 | As for myself, I would simply say that I enjoy preserving a memory in |
| 16 | physical (or digital) form. I've never had the best memory when it comes | |
| 17 | to recalling details of places and people gone by, so it helps to have a | |
| 18 | frame of reference lying around. | |
| 17 | physical (or digital) form. I've never had the best memory when it | |
| 18 | comes to recalling details of places and people gone by, so it helps to | |
| 19 | have a frame of reference lying around. | |
| 19 | 20 | |
| 20 | 21 | Regardless of the reason, I think most people would agree that you |
| 21 | 22 | simply cannot have too many hobbies. |
| 22 | 23 | |
| 23 | ** Older Cameras | |
| 24 | :PROPERTIES: | |
| 25 | :CUSTOM_ID: older-cameras | |
| 26 | :END: | |
| 24 | ## Older Cameras | |
| 25 | ||
| 27 | 26 | I started playing around with the idea of photography when my family |
| 28 | 27 | purchased a Fujifilm camera for family-specific events. I don't recall |
| 29 | 28 | the specific model, but I do recall it was a point-and-shoot camera |
| 30 | 29 | without an interchangeable lens. However, it was of great value to |
| 31 | someone, like myself, who couldn't afford any other camera. I took about | |
| 32 | 10,000 shots with that camera over a 3-5 year span. Most notably, all of | |
| 33 | my trips to California were documented through this camera. | |
| 30 | someone, like myself, who couldn't afford any other camera. I took | |
| 31 | about 10,000 shots with that camera over a 3-5 year span. Most notably, | |
| 32 | all of my trips to California were documented through this camera. | |
| 34 | 33 | |
| 35 | 34 | When possible, I would borrow my sister's camera, which is a Sony |
| 36 | 35 | SLT-A58. This camera was great and allowed for some of my best early |
| 37 | 36 | shots, especially those taken in Utah's and Nevada's parks. |
| 38 | 37 | |
| 39 | ** My Current Kit | |
| 40 | :PROPERTIES: | |
| 41 | :CUSTOM_ID: my-current-kit | |
| 42 | :END: | |
| 38 | ## My Current Kit | |
| 39 | ||
| 43 | 40 | I've finally come to a point in my life where I have the disposable |
| 44 | 41 | income to invest in a solid photography kit. I played around with the |
| 45 | 42 | idea of a lot of different cameras, different types, new vs used, etc. |
| 46 | Finally, I settled on the | |
| 47 | [[https://en.wikipedia.org/wiki/Sony_%CE%B17_III][Sony α7 III]]. This | |
| 48 | camera is mirror-less and uses a full-frame image sensor at 24 | |
| 49 | megapixels. I don't create large prints, and I am mostly focused on | |
| 50 | preserving memories in high quality for the next 5-10 years with this | |
| 51 | camera, so the specifications here are just perfect for me. | |
| 43 | Finally, I settled on the [Sony α7 | |
| 44 | III](https://en.wikipedia.org/wiki/Sony_%CE%B17_III). This camera is | |
| 45 | mirror-less and uses a full-frame image sensor at 24 megapixels. I | |
| 46 | don't create large prints, and I am mostly focused on preserving | |
| 47 | memories in high quality for the next 5-10 years with this camera, so | |
| 48 | the specifications here are just perfect for me. | |
| 52 | 49 | |
| 53 | 50 | For lenses, I decided to buy two lenses that could carry me through most |
| 54 | 51 | situations: |
| 55 | 52 | |
| 56 | - [[https://electronics.sony.com/imaging/lenses/full-frame-e-mount/p/sel2470z][Vario-Tessar | |
| 57 | T* FE 24-70 mm F4 ZA OSS]] | |
| 58 | - [[https://www.tamron-usa.com/product/lenses/a047.html][Tamron 70-300mm | |
| 59 | f4.5-6.3 Di III RXD]] | |
| 53 | - [Vario-Tessar T\* FE 24-70 mm F4 ZA | |
| 54 | OSS](https://electronics.sony.com/imaging/lenses/full-frame-e-mount/p/sel2470z) | |
| 55 | - [Tamron 70-300mm f4.5-6.3 Di III | |
| 56 | RXD](https://www.tamron-usa.com/product/lenses/a047.html) | |
| 60 | 57 | |
| 61 | In addition, I grabbed a couple | |
| 62 | [[https://www.promaster.com/Product/6725][HGX Prime 67mm]] protection | |
| 63 | filters for the lenses. | |
| 58 | In addition, I grabbed a couple [HGX Prime | |
| 59 | 67mm](https://www.promaster.com/Product/6725) protection filters for the | |
| 60 | lenses. | |
| 64 | 61 | |
| 65 | 62 | As I delve further into photography and pick up more skills, I will most |
| 66 | 63 | likely go back and grab a lens with a higher f-stop value, such as |
| 67 | f/1.8. I toyed with the idea of grabbing a 50 mm at =f/1.8=, but decided | |
| 68 | to keep things in a reasonable price range instead. | |
| 64 | f/1.8. I toyed with the idea of grabbing a 50 mm at `f/1.8`, | |
| 65 | but decided to keep things in a reasonable price range instead. | |
| 69 | 66 | |
| 70 | 67 | Finally, I made sure to buy a photography-specific backpack with a rain |
| 71 | 68 | guard, and the zipper on the back panel, to protect the equipment while |
blog/2021-05-30-changing-git-authors.org → content/blog/2021-05-30-changing-git-authors.md renamed +18 −17
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: Changing Git Authors | |
| 2 | #+date: 2021-05-30 | |
| 1 | +++ | |
| 2 | date = 2021-05-30 | |
| 3 | title = "Changing Git Authors" | |
| 4 | description = "A guide to change Git author names and emails in old commits." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Changing Git Author/Email Based on Previously Committed Email | |
| 3 | 8 | |
| 4 | ** Changing Git Author/Email Based on Previously Committed Email | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: changing-git-authoremail-based-on-previously-committed-email | |
| 7 | :END: | |
| 8 | 9 | Here's the dilemma: You've been committing changes to your git |
| 9 | 10 | repository with an incorrect name or email (or multiple repositories), |
| 10 | 11 | and now you want to fix it. Luckily, there's a semi-reliable way to fix |
| @@ -14,18 +15,18 @@ of commits, so use this method only if you're okay accepting that risk. | ||
| 14 | 15 | |
| 15 | 16 | Okay, let's create the bash script: |
| 16 | 17 | |
| 17 | #+begin_src sh | |
| 18 | ```sh | |
| 18 | 19 | nano change_git_authors.sh |
| 19 | #+end_src | |
| 20 | ``` | |
| 20 | 21 | |
| 21 | 22 | The following information can be pasted directly into your bash script. |
| 22 | 23 | The only changes you need to make are to the following variables: |
| 23 | 24 | |
| 24 | - =OLD_EMAIL= | |
| 25 | - =CORRECT_NAME= | |
| 26 | - =CORRECT_EMAIL= | |
| 25 | - `OLD_EMAIL` | |
| 26 | - `CORRECT_NAME` | |
| 27 | - `CORRECT_EMAIL` | |
| 27 | 28 | |
| 28 | #+begin_src sh | |
| 29 | ```sh | |
| 29 | 30 | #!/bin/sh |
| 30 | 31 | |
| 31 | 32 | # List all sub-directories in the current directory |
| @@ -58,16 +59,16 @@ do | ||
| 58 | 59 | |
| 59 | 60 | cd .. |
| 60 | 61 | done |
| 61 | #+end_src | |
| 62 | ``` | |
| 62 | 63 | |
| 63 | 64 | Finally, save the bash script and make it executable. |
| 64 | 65 | |
| 65 | #+begin_src sh | |
| 66 | ```sh | |
| 66 | 67 | chmod a+x change_git_authors.sh |
| 67 | #+end_src | |
| 68 | ``` | |
| 68 | 69 | |
| 69 | 70 | Now you can run the script and should see the process begin. |
| 70 | 71 | |
| 71 | #+begin_src sh | |
| 72 | ```sh | |
| 72 | 73 | ./change_git_authors.sh |
| 73 | #+end_src | |
| 74 | ``` | |
blog/2021-07-15-delete-gitlab-repos.org → content/blog/2021-07-15-delete-gitlab-repos.md renamed +34 −39
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: How to Delete All GitLab Repositories | |
| 2 | #+date: 2021-07-15 | |
| 1 | +++ | |
| 2 | date = 2021-07-15 | |
| 3 | title = "How to Delete All GitLab Repositories" | |
| 4 | description = "Learn how to delete all GitLab repositories in your account." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Background | |
| 3 | 8 | |
| 4 | ** Background | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: background | |
| 7 | :END: | |
| 8 | 9 | Have you ever used GitLab to host your source code, moved to a different |
| 9 | 10 | host, and wanted to delete everything from your GitLab account? Well, |
| 10 | 11 | this post covers any scenario where you would want to delete all |
| @@ -15,43 +16,37 @@ manually delete them whenever I switch host. GitHub has a few different | ||
| 15 | 16 | tools online to delete all repositories for you, but I have not found |
| 16 | 17 | anything similar for GitLab, so I needed an alternative solution. |
| 17 | 18 | |
| 18 | ** Use a Python Script | |
| 19 | :PROPERTIES: | |
| 20 | :CUSTOM_ID: use-a-python-script | |
| 21 | :END: | |
| 22 | *** Requirements | |
| 23 | :PROPERTIES: | |
| 24 | :CUSTOM_ID: requirements | |
| 25 | :END: | |
| 19 | ## Use a Python Script | |
| 20 | ||
| 21 | ### Requirements | |
| 22 | ||
| 26 | 23 | Before we look at the script, make sure you know your GitLab username. |
| 27 | Next, [[https://gitlab.com/-/profile/personal_access_tokens][create an | |
| 28 | authorization token]] so that the Python script can delete your | |
| 29 | repositories. Don't lose this token or else you'll need to create a new | |
| 30 | one. | |
| 31 | ||
| 32 | *** Create the Script | |
| 33 | :PROPERTIES: | |
| 34 | :CUSTOM_ID: create-the-script | |
| 35 | :END: | |
| 24 | Next, [create an authorization | |
| 25 | token](https://gitlab.com/-/profile/personal_access_tokens) so that the | |
| 26 | Python script can delete your repositories. Don't lose this token or | |
| 27 | else you'll need to create a new one. | |
| 28 | ||
| 29 | ### Create the Script | |
| 30 | ||
| 36 | 31 | To run a Python script, you must first create it. Open a terminal and |
| 37 | 32 | enter the following commands in whichever directory you prefer to store |
| 38 | 33 | the script. You can do the same things in a file manager if you prefer. |
| 39 | 34 | |
| 40 | #+begin_src sh | |
| 35 | ```sh | |
| 41 | 36 | mkdir delete-gitlab |
| 42 | #+end_src | |
| 37 | ``` | |
| 43 | 38 | |
| 44 | #+begin_src sh | |
| 39 | ```sh | |
| 45 | 40 | cd delete-gitlab |
| 46 | #+end_src | |
| 41 | ``` | |
| 47 | 42 | |
| 48 | #+begin_src sh | |
| 43 | ```sh | |
| 49 | 44 | nano main.py |
| 50 | #+end_src | |
| 45 | ``` | |
| 51 | 46 | |
| 52 | Enter the following code into your =main.py= script. | |
| 47 | Enter the following code into your `main.py` script. | |
| 53 | 48 | |
| 54 | #+begin_src python | |
| 49 | ```python | |
| 55 | 50 | import request |
| 56 | 51 | import json |
| 57 | 52 | |
| @@ -105,16 +100,16 @@ def main(): | ||
| 105 | 100 | |
| 106 | 101 | if __name__ == "__main__": |
| 107 | 102 | main() |
| 108 | #+end_src | |
| 103 | ``` | |
| 109 | 104 | |
| 110 | Now that you have the proper information, replace ={user-id}= with your | |
| 111 | GitLab username and ={auth-token}= with the authorization token you | |
| 112 | created earlier. | |
| 105 | Now that you have the proper information, replace `{user-id}` | |
| 106 | with your GitLab username and `{auth-token}` with the | |
| 107 | authorization token you created earlier. | |
| 113 | 108 | |
| 114 | 109 | Finally, simply run the script and watch the output. You can also use |
| 115 | PyCharm Community Edition to edit and run the Python script if you don't | |
| 116 | want to work in a terminal. | |
| 110 | PyCharm Community Edition to edit and run the Python script if you | |
| 111 | don't want to work in a terminal. | |
| 117 | 112 | |
| 118 | #+begin_src sh | |
| 113 | ```sh | |
| 119 | 114 | python3 main.py |
| 120 | #+end_src | |
| 115 | ``` | |
blog/2021-08-25-audit-sampling.org → content/blog/2021-08-25-audit-sampling.md renamed +87 −102
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: Audit Sampling with Python | |
| 2 | #+date: 2021-08-25 | |
| 1 | +++ | |
| 2 | date = 2021-08-25 | |
| 3 | title = "Audit Sampling with Python" | |
| 4 | description = "Learn how to sample populations with Python." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Introduction | |
| 3 | 8 | |
| 4 | ** Introduction | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: introduction | |
| 7 | :END: | |
| 8 | 9 | For anyone who is familiar with internal auditing, external auditing, or |
| 9 | 10 | consulting, you will understand how tedious audit testing can become |
| 10 | 11 | when you are required to test large swaths of data. When we cannot |
| @@ -18,38 +19,34 @@ quite a lot of people. While some audit-focused tools have introduced | ||
| 18 | 19 | sampling functionality (e.g. Wdesk), many audit departments and firms |
| 19 | 20 | cannot use software like this due to certain constraints, such as the |
| 20 | 21 | team's budget or knowledge. Here is where this article comes in: we're |
| 21 | going to use [[https://www.python.org][Python]], a free and open-source | |
| 22 | going to use [Python](https://www.python.org), a free and open-source | |
| 22 | 23 | programming language, to generate random samples from a dataset in order |
| 23 | 24 | to suffice numerous audit situations. |
| 24 | 25 | |
| 25 | ** Audit Requirements for Sampling | |
| 26 | :PROPERTIES: | |
| 27 | :CUSTOM_ID: audit-requirements-for-sampling | |
| 28 | :END: | |
| 26 | ## Audit Requirements for Sampling | |
| 27 | ||
| 29 | 28 | Before we get into the details of how to sample with Python, I want to |
| 30 | 29 | make sure I discuss the different requirements that auditors may have of |
| 31 | 30 | samples used within their projects. |
| 32 | 31 | |
| 33 | *** Randomness | |
| 34 | :PROPERTIES: | |
| 35 | :CUSTOM_ID: randomness | |
| 36 | :END: | |
| 37 | First, let's discuss randomness. When testing out new technology to help | |
| 38 | assist with audit sampling, you need to understand exactly how your | |
| 32 | ### Randomness | |
| 33 | ||
| 34 | First, let's discuss randomness. When testing out new technology to | |
| 35 | help assist with audit sampling, you need to understand exactly how your | |
| 39 | 36 | samples are being generated. For example, if the underlying function is |
| 40 | 37 | just picking every 57th element from a list, that's not truly random; |
| 41 | 38 | it's a systematic form of sampling. Luckily, since Python is |
| 42 | 39 | open-source, we have access to its codebase. Through this blog post, I |
| 43 | will be using the [[https://pandas.pydata.org][pandas]] module in order | |
| 44 | to generate the random samples. More specifically, I will be using the | |
| 45 | [[https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.sample.html][pandas.DataFrame.sample]] | |
| 40 | will be using the [pandas](https://pandas.pydata.org) module in order to | |
| 41 | generate the random samples. More specifically, I will be using the | |
| 42 | [pandas.DataFrame.sample](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.sample.html) | |
| 46 | 43 | function provided by Pandas. |
| 47 | 44 | |
| 48 | 45 | Now that you know what you're using, you can always check out the code |
| 49 | behind =pandas.DataFrame.sample=. This function does a lot of work, but | |
| 50 | we really only care about the following snippets of code: | |
| 46 | behind `pandas.DataFrame.sample`. This function does a lot of | |
| 47 | work, but we really only care about the following snippets of code: | |
| 51 | 48 | |
| 52 | #+begin_src python | |
| 49 | ```python | |
| 53 | 50 | # Process random_state argument |
| 54 | 51 | rs = com.random_state(random_state) |
| 55 | 52 | |
| @@ -61,25 +58,24 @@ if ignore_index: | ||
| 61 | 58 | result.index = ibase.default_index(len(result)) |
| 62 | 59 | |
| 63 | 60 | return result |
| 64 | #+end_src | |
| 65 | ||
| 66 | The block of code above shows you that if you assign a =random_state= | |
| 67 | argument when you run the function, that will be used as a seed number | |
| 68 | in the random generation and will allow you to reproduce a sample, given | |
| 69 | that nothing else changes. This is critical to the posterity of audit | |
| 70 | work. After all, how can you say your audit process is adequately | |
| 71 | documented if the next person can't run the code and get the same | |
| 72 | sample? The final piece here on randomness is to look at the | |
| 73 | [[https://docs.%20python.org/3/library/random.html#random.choice][choice]] | |
| 61 | ``` | |
| 62 | ||
| 63 | The block of code above shows you that if you assign a | |
| 64 | `random_state` argument when you run the function, that will | |
| 65 | be used as a seed number in the random generation and will allow you to | |
| 66 | reproduce a sample, given that nothing else changes. This is critical to | |
| 67 | the posterity of audit work. After all, how can you say your audit | |
| 68 | process is adequately documented if the next person can't run the code | |
| 69 | and get the same sample? The final piece here on randomness is to look | |
| 70 | at the | |
| 71 | [choice](https://docs.%20python.org/3/library/random.html#random.choice) | |
| 74 | 72 | function used above. This is the crux of the generation and can also be |
| 75 | 73 | examined for more detailed analysis on its reliability. As far as |
| 76 | 74 | auditing goes, we will trust that these functions are mathematically |
| 77 | 75 | random. |
| 78 | 76 | |
| 79 | *** Sample Sizes | |
| 80 | :PROPERTIES: | |
| 81 | :CUSTOM_ID: sample-sizes | |
| 82 | :END: | |
| 77 | ### Sample Sizes | |
| 78 | ||
| 83 | 79 | As mentioned in the intro, sampling is only an effective method of |
| 84 | 80 | auditing when it truly represents the entire population. While some |
| 85 | 81 | audit departments or firms may consider certain judgmental sample sizes |
| @@ -98,38 +94,35 @@ proprietary, table that will instruct auditors which sample sizes to | ||
| 98 | 94 | choose. This allows for uniform testing and reduces overall workload. |
| 99 | 95 | See the table below for a common implementation of sample sizes: |
| 100 | 96 | |
| 101 | | Control Frequency | Sample Size - High Risk | Sample Size - Low Risk | | |
| 102 | |-------------------+-------------------------+------------------------| | |
| 103 | | More Than Daily | 40 | 25 | | |
| 104 | | Daily | 40 | 25 | | |
| 105 | | Weekly | 12 | 5 | | |
| 106 | | Monthly | 5 | 3 | | |
| 107 | | Quarterly | 2 | 2 | | |
| 108 | | Semi-Annually | 1 | 1 | | |
| 109 | | Annually | 1 | 1 | | |
| 110 | | Ad-hoc | 1 | 1 | | |
| 111 | ||
| 112 | ** Sampling with Python & Pandas | |
| 113 | :PROPERTIES: | |
| 114 | :CUSTOM_ID: sampling-with-python-pandas | |
| 115 | :END: | |
| 97 | Control Frequency Sample Size - High Risk Sample Size - Low Risk | |
| 98 | ------------------- ------------------------- ------------------------ | |
| 99 | More Than Daily 40 25 | |
| 100 | Daily 40 25 | |
| 101 | Weekly 12 5 | |
| 102 | Monthly 5 3 | |
| 103 | Quarterly 2 2 | |
| 104 | Semi-Annually 1 1 | |
| 105 | Annually 1 1 | |
| 106 | Ad-hoc 1 1 | |
| 107 | ||
| 108 | ## Sampling with Python & Pandas | |
| 109 | ||
| 116 | 110 | In this section, I am going to cover a few basic audit situations that |
| 117 | 111 | require sampling. While some situations may require more effort, the |
| 118 | 112 | syntax, organization, and intellect used remain largely the same. If |
| 119 | you've never used Python before, note that lines starting with a '=#=' | |
| 120 | symbol are called comments, and they will be skipped by Python. I highly | |
| 121 | recommend taking a quick tutorial online to understand the basics of | |
| 122 | Python if any of the code below is confusing to you. | |
| 123 | ||
| 124 | *** Simple Random Sample | |
| 125 | :PROPERTIES: | |
| 126 | :CUSTOM_ID: simple-random-sample | |
| 127 | :END: | |
| 113 | you've never used Python before, note that lines starting with a | |
| 114 | '`#`' symbol are called comments, and they will be skipped | |
| 115 | by Python. I highly recommend taking a quick tutorial online to | |
| 116 | understand the basics of Python if any of the code below is confusing to | |
| 117 | you. | |
| 118 | ||
| 119 | ### Simple Random Sample | |
| 120 | ||
| 128 | 121 | First, let's look at a simple, random sample. The code block below will |
| 129 | import the =pandas= module, load a data file, sample the data, and | |
| 130 | export the sample to a file. | |
| 122 | import the `pandas` module, load a data file, sample the | |
| 123 | data, and export the sample to a file. | |
| 131 | 124 | |
| 132 | #+begin_src python | |
| 125 | ```python | |
| 133 | 126 | # Import the Pandas module |
| 134 | 127 | import pandas |
| 135 | 128 | |
| @@ -147,16 +140,14 @@ sample = df.sample(n=25, random_state=0) | ||
| 147 | 140 | |
| 148 | 141 | # Save the sample to Excel |
| 149 | 142 | sample.to_excel(file_output) |
| 150 | #+end_src | |
| 143 | ``` | |
| 144 | ||
| 145 | ### Simple Random Sample: Using Multiple Input Files | |
| 151 | 146 | |
| 152 | *** Simple Random Sample: Using Multiple Input Files | |
| 153 | :PROPERTIES: | |
| 154 | :CUSTOM_ID: simple-random-sample-using-multiple-input-files | |
| 155 | :END: | |
| 156 | 147 | Now that we've created a simple sample, let's create a sample from |
| 157 | 148 | multiple files. |
| 158 | 149 | |
| 159 | #+begin_src python | |
| 150 | ```python | |
| 160 | 151 | # Import the Pandas module |
| 161 | 152 | import pandas |
| 162 | 153 | |
| @@ -183,17 +174,15 @@ sample = pandas.concat([sample_01, sample_02, sample_03], ignore_index=True) | ||
| 183 | 174 | |
| 184 | 175 | # Save the sample to Excel |
| 185 | 176 | sample.to_excel(file_output) |
| 186 | #+end_src | |
| 177 | ``` | |
| 178 | ||
| 179 | ### Stratified Random Sample | |
| 187 | 180 | |
| 188 | *** Stratified Random Sample | |
| 189 | :PROPERTIES: | |
| 190 | :CUSTOM_ID: stratified-random-sample | |
| 191 | :END: | |
| 192 | 181 | Well, what if you need to sample distinct parts of a single file? For |
| 193 | 182 | example, let's write some code to separate our data by "Region" and |
| 194 | 183 | sample those regions independently. |
| 195 | 184 | |
| 196 | #+begin_src python | |
| 185 | ```python | |
| 197 | 186 | # Import the Pandas module |
| 198 | 187 | import pandas |
| 199 | 188 | |
| @@ -219,18 +208,16 @@ sample = pandas.concat([sample_east, sample_west], ignore_index=True) | ||
| 219 | 208 | |
| 220 | 209 | # Save the sample to Excel |
| 221 | 210 | sample.to_excel(file_output) |
| 222 | #+end_src | |
| 211 | ``` | |
| 212 | ||
| 213 | ### Stratified Systematic Sample | |
| 223 | 214 | |
| 224 | *** Stratified Systematic Sample | |
| 225 | :PROPERTIES: | |
| 226 | :CUSTOM_ID: stratified-systematic-sample | |
| 227 | :END: | |
| 228 | 215 | This next example is quite useful if you need audit coverage over a |
| 229 | 216 | certain time period. This code will generate samples for each month in |
| 230 | 217 | the data and combine them all together at the end. Obviously, this code |
| 231 | 218 | can be modified to stratify by something other than months, if needed. |
| 232 | 219 | |
| 233 | #+begin_src python | |
| 220 | ```python | |
| 234 | 221 | # Import the Pandas module |
| 235 | 222 | import pandas |
| 236 | 223 | |
| @@ -270,23 +257,21 @@ def monthly_stratified_sample(df: pandas.DataFrame, date_column: str, num_select | ||
| 270 | 257 | sample_size = 3 |
| 271 | 258 | sample = monthly_stratified_sample(df, 'Date of Sale', sample_size) |
| 272 | 259 | sample.to_excel(file_output) |
| 273 | #+end_src | |
| 274 | ||
| 275 | ** Documenting the Results | |
| 276 | :PROPERTIES: | |
| 277 | :CUSTOM_ID: documenting-the-results | |
| 278 | :END: | |
| 279 | Once you've generated a proper sample, there are a few things left to do | |
| 280 | in order to properly ensure your process is reproducible. | |
| 281 | ||
| 282 | 1. Document the sample. Make sure the resulting file is readable and | |
| 283 | includes the documentation listed in the next bullet. | |
| 284 | 2. Include documentation around the data source, extraction techniques, | |
| 285 | any modifications made to the data, and be sure to include a copy of | |
| 286 | the script itself. | |
| 287 | 3. Whenever possible, perform a completeness and accuracy test to ensure | |
| 288 | your sample is coming from a complete and accurate population. To | |
| 289 | ensure completeness, compare the record count from the data source to | |
| 290 | the record count loaded into Python. To ensure accuracy, test a small | |
| 291 | sample against the source data (e.g., test 5 sales against the | |
| 292 | database to see if the details are accurate). | |
| 260 | ``` | |
| 261 | ||
| 262 | ## Documenting the Results | |
| 263 | ||
| 264 | Once you've generated a proper sample, there are a few things left to | |
| 265 | do in order to properly ensure your process is reproducible. | |
| 266 | ||
| 267 | 1. Document the sample. Make sure the resulting file is readable and | |
| 268 | includes the documentation listed in the next bullet. | |
| 269 | 2. Include documentation around the data source, extraction techniques, | |
| 270 | any modifications made to the data, and be sure to include a copy of | |
| 271 | the script itself. | |
| 272 | 3. Whenever possible, perform a completeness and accuracy test to | |
| 273 | ensure your sample is coming from a complete and accurate | |
| 274 | population. To ensure completeness, compare the record count from | |
| 275 | the data source to the record count loaded into Python. To ensure | |
| 276 | accuracy, test a small sample against the source data (e.g., test 5 | |
| 277 | sales against the database to see if the details are accurate). | |
content/blog/2021-10-09-apache-redirect.md added +46
| @@ -0,0 +1,46 @@ | ||
| 1 | +++ | |
| 2 | date = 2021-10-09 | |
| 3 | title = "Apache Redirect HTML Files to a Directory" | |
| 4 | description = "A guide on redirecting HTML files to directory in Apache." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## The Problem | |
| 8 | ||
| 9 | After recently switching static site generators (SSG), my blog URLs | |
| 10 | changed with no option to preserve the classic `.html` | |
| 11 | extension at the end of my blog post URLs. | |
| 12 | ||
| 13 | I really disliked using my old SSG ([Jekyll](https://jekyllrb.com)) and | |
| 14 | prefer my new tool ([Zola](https://www.getzola.org)) much more, so I was | |
| 15 | determined to figure out a way to get the proper redirect set up so that | |
| 16 | people who find my posts online aren't constantly met by 404 errors. | |
| 17 | ||
| 18 | ## The Solution | |
| 19 | ||
| 20 | To solve this problem, I really needed to solve two pieces: | |
| 21 | ||
| 22 | 1. Redirect all blog post URL requests from | |
| 23 | `/blog/some-post.html` to `/blog/some-post/`. | |
| 24 | 2. Ensure that no other `.html` files are redirected, such | |
| 25 | as `index.html`. | |
| 26 | ||
| 27 | After *a lot* of tweaking and testing, I believe I have finally found | |
| 28 | the solution. The solution is shown below. | |
| 29 | ||
| 30 | ```conf | |
| 31 | RewriteEngine On | |
| 32 | RewriteCond %{REQUEST_URI} !\index.html$ [NC] | |
| 33 | RewriteRule ^(.*).html$ https://example.com/$1 [R=301,L] | |
| 34 | ``` | |
| 35 | ||
| 36 | This piece of code in the Apache `.conf` or | |
| 37 | `.htaccess` file will do the following: | |
| 38 | ||
| 39 | 1. Turn on the RewriteEngine so that we can modify URLs. | |
| 40 | 2. Ignore any `index.html` files from the rule we are about | |
| 41 | to specify. | |
| 42 | 3. Find any `.html` files within the website directory and | |
| 43 | redirect it to exclude the file extension. | |
| 44 | 4. The final piece is adding the trailing slash (`/`) at the | |
| 45 | end of the URL - you'll notice that I don't have an Apache rule | |
| 46 | for that since Apache handles that automatically. | |
blog/2021-12-04-cisa.org → content/blog/2021-12-04-cisa.md renamed +88 −98
| @@ -1,19 +1,21 @@ | ||
| 1 | #+title: I Passed the CISA! | |
| 2 | #+date: 2021-12-04 | |
| 1 | +++ | |
| 2 | date = 2021-12-04 | |
| 3 | title = "I Passed the CISA!" | |
| 4 | description = "A recap of the CISA certification exam and my results." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## What is the CISA? | |
| 3 | 8 | |
| 4 | ** What is the CISA? | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: what-is-the-cisa | |
| 7 | :END: | |
| 8 | 9 | For those of you lucky enough not to be knee-deep in the world of IT/IS |
| 9 | Auditing, [[https://www.isaca.org/credentialing/cisa][CISA]] stands for | |
| 10 | Auditing, [CISA](https://www.isaca.org/credentialing/cisa) stands for | |
| 10 | 11 | Certified Information Systems Auditor. This certification and exam are |
| 11 | part of ISACA's suite of certifications. As I often explain it to people | |
| 12 | like my family, it basically means you're employed to use your knowledge | |
| 13 | of information systems, regulations, common threats, risks, etc. in | |
| 14 | order to assess an organization's current control of their risk. If a | |
| 15 | risk isn't controlled (and the company doesn't want to accept the risk), | |
| 16 | an IS auditor will suggest implementing a control to address that risk. | |
| 12 | part of ISACA's suite of certifications. As I often explain it to | |
| 13 | people like my family, it basically means you're employed to use your | |
| 14 | knowledge of information systems, regulations, common threats, risks, | |
| 15 | etc. in order to assess an organization's current control of their | |
| 16 | risk. If a risk isn't controlled (and the company doesn't want to | |
| 17 | accept the risk), an IS auditor will suggest implementing a control to | |
| 18 | address that risk. | |
| 17 | 19 | |
| 18 | 20 | Now, the CISA certification itself is, in my opinion, the main |
| 19 | 21 | certification for this career. While certifications such as the CPA or |
| @@ -25,13 +27,13 @@ However, to be honest, I am a skeptic of most certifications. I | ||
| 25 | 27 | understand the value they hold in terms of how much you need to commit |
| 26 | 28 | to studying or learning on the job, as well as the market value for |
| 27 | 29 | certifications such as the CISA. But I also have known some very |
| 28 | +incompetent+ /less than stellar/ auditors who have CPAs, CISAs, CIAs, | |
| 30 | ~~incompetent~~ *less than stellar* auditors who have CPAs, CISAs, CIAs, | |
| 29 | 31 | etc. |
| 30 | 32 | |
| 31 | 33 | The same goes for most industries: if a person is good at studying, they |
| 32 | 34 | can earn the certification. However, that knowledge means nothing unless |
| 33 | you're actually able to use it in real life and perform as expected of a | |
| 34 | certification holder. The challenge comes when people are hired or | |
| 35 | you're actually able to use it in real life and perform as expected of | |
| 36 | a certification holder. The challenge comes when people are hired or | |
| 35 | 37 | connected strictly because of their certifications or resume; you need |
| 36 | 38 | to see a person work before you can assume them having a CISA means |
| 37 | 39 | they're better than someone without the CISA. |
| @@ -40,56 +42,50 @@ Okay, rant over. Certifications are generally accepted as a measuring | ||
| 40 | 42 | stick of commitment and quality of an employee, so I am accepting it |
| 41 | 43 | too. |
| 42 | 44 | |
| 43 | ** Exam Content | |
| 44 | :PROPERTIES: | |
| 45 | :CUSTOM_ID: exam-content | |
| 46 | :END: | |
| 45 | ## Exam Content | |
| 46 | ||
| 47 | 47 | The CISA is broken down into five sections, each weighted with a |
| 48 | 48 | percentage of test questions that may appear. |
| 49 | 49 | |
| 50 | #+caption: CISA exam sections | |
| 51 | [[https://img.cleberg.net/blog/20211204-i-passed-the-cisa/cisa-exam-sections.png]] | |
| 50 |  | |
| 52 | 52 | |
| 53 | 53 | Since the exam contains 150 questions, here's how those sections break |
| 54 | 54 | down: |
| 55 | 55 | |
| 56 | | Exam Section | Percentage of Exam | Questions | | |
| 57 | |---------------+--------------------+-----------| | |
| 58 | | 1 | 21% | 32 | | |
| 59 | | 2 | 17% | 26 | | |
| 60 | | 3 | 12% | 18 | | |
| 61 | | 4 | 23% | 34 | | |
| 62 | | 5 | 27% | 40 | | |
| 63 | | *Grand Total* | *100%* | *150* | | |
| 64 | ||
| 65 | ** My Studying Habits | |
| 66 | :PROPERTIES: | |
| 67 | :CUSTOM_ID: my-studying-habits | |
| 68 | :END: | |
| 56 | Exam Section Percentage of Exam Questions | |
| 57 | ----------------- -------------------- ----------- | |
| 58 | 1 21% 32 | |
| 59 | 2 17% 26 | |
| 60 | 3 12% 18 | |
| 61 | 4 23% 34 | |
| 62 | 5 27% 40 | |
| 63 | **Grand Total** **100%** **150** | |
| 64 | ||
| 65 | ## My Studying Habits | |
| 66 | ||
| 69 | 67 | This part is a little hard for me to break down into specific detail due |
| 70 | 68 | to the craziness of the last year. While I officially purchased my |
| 71 | studying materials in December 2020 and opened them to "start studying" | |
| 72 | in January 2021, I really wasn't able to study much due to the demands | |
| 73 | of my job and personal life. | |
| 69 | studying materials in December 2020 and opened them to "start | |
| 70 | studying" in January 2021, I really wasn't able to study much due to | |
| 71 | the demands of my job and personal life. | |
| 74 | 72 | |
| 75 | 73 | Let me approach this from a few different viewpoints. |
| 76 | 74 | |
| 77 | *** Study Materials | |
| 78 | :PROPERTIES: | |
| 79 | :CUSTOM_ID: study-materials | |
| 80 | :END: | |
| 75 | ### Study Materials | |
| 76 | ||
| 81 | 77 | Let's start by discussing the study materials I purchased. I'll be |
| 82 | 78 | referring to #1 as the CRM and #2 as the QAE. |
| 83 | 79 | |
| 84 | 1. [[https://store.isaca.org/s/store#/store/browse/detail/a2S4w000004KoCbEAK][CISA | |
| 85 | Review Manual, 27th Edition | Print]] | |
| 86 | 2. [[https://store.isaca.org/s/store#/store/browse/detail/a2S4w000004KoCcEAK][CISA | |
| 87 | Review Questions, Answers & Explanations Manual, 12th Edition | | |
| 88 | Print]] | |
| 80 | 1. [CISA Review Manual, 27th Edition \| | |
| 81 | Print](https://store.isaca.org/s/store#/store/browse/detail/a2S4w000004KoCbEAK) | |
| 82 | 2. [[<https://store.isaca.org/s/store#/store/browse/detail/a2S4w000004KoCcEAK>][CISA | |
| 83 | Review Questions, Answers & Explanations Manual, 12th Edition \| | |
| 84 | Print]] | |
| 89 | 85 | |
| 90 | 86 | The CRM is an excellent source of information and could honestly be used |
| 91 | 87 | as a reference for most IS auditors as a learning reference during their |
| 92 | daily audit responsibilities. However, it is *full* of information and | |
| 88 | daily audit responsibilities. However, it is **full** of information and | |
| 93 | 89 | can be overloading if you're not good at filtering out useless |
| 94 | 90 | information while studying. |
| 95 | 91 | |
| @@ -99,10 +95,10 @@ complaint about the QAE is that each question is immediately followed | ||
| 99 | 95 | with the correct answer and explanations below it, which means I had to |
| 100 | 96 | use something to constantly cover the answers while I was studying. |
| 101 | 97 | |
| 102 | I didn't use the online database version of the QAE, but I've heard that | |
| 103 | it's easier to use than the printed book. However, it is more expensive | |
| 104 | ($299 database vs $129 book) which might be important if you're paying | |
| 105 | for materials yourself. | |
| 98 | I didn't use the online database version of the QAE, but I've heard | |
| 99 | that it's easier to use than the printed book. However, it is more | |
| 100 | expensive (\$299 database vs \$129 book) which might be important if | |
| 101 | you're paying for materials yourself. | |
| 106 | 102 | |
| 107 | 103 | In terms of question difficulty, I felt that the QAE was a good |
| 108 | 104 | representation of the actual exam. I've seen a lot of people online say |
| @@ -115,10 +111,8 @@ topics you struggle with), and use the QAE to continue practicing | ||
| 115 | 111 | exam-like questions, you should be fine. I didn't use any online |
| 116 | 112 | courses, videos, etc. - the ISACA materials are more than enough. |
| 117 | 113 | |
| 118 | *** Studying Process | |
| 119 | :PROPERTIES: | |
| 120 | :CUSTOM_ID: studying-process | |
| 121 | :END: | |
| 114 | ### Studying Process | |
| 115 | ||
| 122 | 116 | While I was able to briefly read through sections 1 and 2 in early 2021, |
| 123 | 117 | I had to stop and take a break from February/March to September. I |
| 124 | 118 | switched jobs in September, which allowed me a lot more free time to |
| @@ -130,62 +124,60 @@ practice exam from the QAE manual and scored 70% (105/150). | ||
| 130 | 124 | |
| 131 | 125 | Here's a breakdown of my initial practice exam: |
| 132 | 126 | |
| 133 | | Exam Section | Incorrect | Correct | Grand Total | Percent | | |
| 134 | |---------------+-----------+---------+-------------+---------| | |
| 135 | | 1 | 8 | 25 | 33 | 76% | | |
| 136 | | 2 | 5 | 20 | 25 | 80% | | |
| 137 | | 3 | 6 | 12 | 18 | 67% | | |
| 138 | | 4 | 10 | 23 | 33 | 70% | | |
| 139 | | 5 | 16 | 25 | 41 | 61% | | |
| 140 | | *Grand Total* | *45* | *105* | *150* | *70%* | | |
| 127 | Exam Section Incorrect Correct Grand Total Percent | |
| 128 | ----------------- ----------- --------- ------------- --------- | |
| 129 | 1 8 25 33 76% | |
| 130 | 2 5 20 25 80% | |
| 131 | 3 6 12 18 67% | |
| 132 | 4 10 23 33 70% | |
| 133 | 5 16 25 41 61% | |
| 134 | **Grand Total** **45** **105** **150** **70%** | |
| 141 | 135 | |
| 142 | 136 | As I expected, my toughest sections were related to project management, |
| 143 | 137 | development, implementation, and security. |
| 144 | 138 | |
| 145 | 139 | This just leaves October and November. For these months, I tried to |
| 146 | 140 | practice every few days, doing 10 questions for each section, until the |
| 147 | exam. This came out to 13 practice sessions, ~140 questions per section, | |
| 148 | and ~700 questions total. | |
| 141 | exam. This came out to 13 practice sessions, \~140 questions per | |
| 142 | section, and \~700 questions total. | |
| 149 | 143 | |
| 150 | 144 | While some practice sessions were worse and some were better, the final |
| 151 | 145 | results were similar to my practice exam results. As you can see below, |
| 152 | 146 | my averages were slightly worse than my practice exam. However, I got in |
| 153 | over 700 questions of practice and, most importantly, *I read through | |
| 147 | over 700 questions of practice and, most importantly, \*I read through | |
| 154 | 148 | the explanations every time I answered incorrectly and learned from my |
| 155 | mistakes*. | |
| 156 | ||
| 157 | | Exam Section | Incorrect | Correct | Grand Total | Percent | | |
| 158 | |---------------+-----------+---------+-------------+---------| | |
| 159 | | 1 | 33 | 108 | 141 | 77% | | |
| 160 | | 2 | 33 | 109 | 142 | 77% | | |
| 161 | | 3 | 55 | 89 | 144 | 62% | | |
| 162 | | 4 | 52 | 88 | 140 | 63% | | |
| 163 | | 5 | 55 | 85 | 140 | 61% | | |
| 164 | | *Grand Total* | *228* | *479* | *707* | *68%* | | |
| 165 | ||
| 166 | #+caption: CISA practice question results | |
| 167 | [[https://img.cleberg.net/blog/20211204-i-passed-the-cisa/cisa-practice-questions-results.png]] | |
| 168 | ||
| 169 | ** Results | |
| 170 | :PROPERTIES: | |
| 171 | :CUSTOM_ID: results | |
| 172 | :END: | |
| 149 | mistakes\*. | |
| 150 | ||
| 151 | Exam Section Incorrect Correct Grand Total Percent | |
| 152 | ----------------- ----------- --------- ------------- --------- | |
| 153 | 1 33 108 141 77% | |
| 154 | 2 33 109 142 77% | |
| 155 | 3 55 89 144 62% | |
| 156 | 4 52 88 140 63% | |
| 157 | 5 55 85 140 61% | |
| 158 | **Grand Total** **228** **479** **707** **68%** | |
| 159 | ||
| 160 |  | |
| 162 | ||
| 163 | ## Results | |
| 164 | ||
| 173 | 165 | Now, how do the practice scores reflect my actual results? After all, |
| 174 | 166 | it's hard to tell how good a practice regimen is unless you see how it |
| 175 | 167 | turns out. |
| 176 | 168 | |
| 177 | | Exam Section | Section Name | Score | | |
| 178 | |--------------+------------------------------------------------------------------+-------| | |
| 179 | | 1 | Information Systems Auditing Process | 678 | | |
| 180 | | 2 | Governance and Management of IT | 590 | | |
| 181 | | 3 | Information Systems Acquisition, Development, and Implementation | 721 | | |
| 182 | | 4 | Information Systems Operations and Business Resilience | 643 | | |
| 183 | | 5 | Protection of Information Assets | 511 | | |
| 184 | | *TOTAL* | | *616* | | |
| 169 | Exam Section Section Name Score | |
| 170 | -------------- ------------------------------------------------------------------ --------- | |
| 171 | 1 Information Systems Auditing Process 678 | |
| 172 | 2 Governance and Management of IT 590 | |
| 173 | 3 Information Systems Acquisition, Development, and Implementation 721 | |
| 174 | 4 Information Systems Operations and Business Resilience 643 | |
| 175 | 5 Protection of Information Assets 511 | |
| 176 | **TOTAL** **616** | |
| 185 | 177 | |
| 186 | 178 | Now, in order to pass the CISA, you need at least 450 on a sliding scale |
| 187 | 179 | of 200-800. Personally, I really have no clue what an average CISA score |
| 188 | is. After a /very/ brief look online, I can see that the high end is | |
| 180 | is. After a *very* brief look online, I can see that the high end is | |
| 189 | 181 | usually in the low 700s. In addition, only about 50-60% of people pass |
| 190 | 182 | the exam. |
| 191 | 183 | |
| @@ -204,10 +196,8 @@ CISA topics at all, the most important part of studying is doing | ||
| 204 | 196 | practice questions. You really need to understand how to read the |
| 205 | 197 | questions critically and pick the best answer. |
| 206 | 198 | |
| 207 | ** Looking Forward | |
| 208 | :PROPERTIES: | |
| 209 | :CUSTOM_ID: looking-forward | |
| 210 | :END: | |
| 199 | ## Looking Forward | |
| 200 | ||
| 211 | 201 | I am extremely happy that I was finally able to pass the CISA. Looking |
| 212 | 202 | to the future, I'm not sure what's next in terms of professional |
| 213 | 203 | learning. My current company offers internal learning courses, so I will |
blog/2022-02-10-leaving-the-office.org → content/blog/2022-02-10-leaving-the-office.md renamed +56 −88
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: Leaving Office-Based Work In the Past | |
| 2 | #+date: 2022-02-10 | |
| 1 | +++ | |
| 2 | date = 2022-02-10 | |
| 3 | title = "Leaving Office-Based Work in the Past" | |
| 4 | description = "My thoughts on the current surge of remote work and what that means for full-time office-based roles." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## The Working World is Changing | |
| 3 | 8 | |
| 4 | ** The Working World is Changing | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: the-working-world-is-changing | |
| 7 | :END: | |
| 8 | 9 | There has been a trend for the past few years of companies slowly |
| 9 | 10 | realizing that the pandemic is not just a temporary state that will go |
| 10 | 11 | away eventually and let everything return to the way it was before. In |
| @@ -22,14 +23,10 @@ I decided to take a look back at my relatively short career so far and | ||
| 22 | 23 | compare the positive and negative effects of the different work |
| 23 | 24 | environments I've been in. |
| 24 | 25 | |
| 25 | ** In-Person Offices | |
| 26 | :PROPERTIES: | |
| 27 | :CUSTOM_ID: in-person-offices | |
| 28 | :END: | |
| 29 | *** Retail Internship | |
| 30 | :PROPERTIES: | |
| 31 | :CUSTOM_ID: retail-internship | |
| 32 | :END: | |
| 26 | ## In-Person Offices | |
| 27 | ||
| 28 | ### Retail Internship | |
| 29 | ||
| 33 | 30 | I started my first job as a management intern at a busy retail pharmacy, |
| 34 | 31 | working my 40-hour weeks on my feet. As these retail stores don't |
| 35 | 32 | believe in resting or sitting down, you can guarantee that you will |
| @@ -45,12 +42,10 @@ However, if you are able to operate a retail store with a limited crew | ||
| 45 | 42 | and provide enough comfort and support, I believe these jobs could be |
| 46 | 43 | both comfortable and efficient. |
| 47 | 44 | |
| 48 | *** Semi-Private Cubicles | |
| 49 | :PROPERTIES: | |
| 50 | :CUSTOM_ID: semi-private-cubicles | |
| 51 | :END: | |
| 52 | #+caption: Semi-Private Cubicles | |
| 53 | [[https://img.cleberg.net/blog/20220210-leaving-office-based-work-in-the-past/private_cubicles.png]] | |
| 45 | ### Semi-Private Cubicles | |
| 46 | ||
| 47 |  | |
| 54 | 49 | |
| 55 | 50 | After about a year, I was able to find another internship - this time, |
| 56 | 51 | it was in my field of interest: internal auditing. This was for a life |
| @@ -65,8 +60,8 @@ formed into cubicles that house 4 desks/employees. These "pods" of | ||
| 65 | 60 | 4-person cubicles are linked throughout each floor of the headquarters |
| 66 | 61 | (except the sales people, who had that open-floor concept going on). The |
| 67 | 62 | walls of the cubicle were tall and provided a lot of privacy and |
| 68 | sound-proofing, except when I used the standing desk feature (I'm over 6 | |
| 69 | feet tall, so probably not an issue for most people). | |
| 63 | sound-proofing, except when I used the standing desk feature (I'm over | |
| 64 | 6 feet tall, so probably not an issue for most people). | |
| 70 | 65 | |
| 71 | 66 | I loved this environment, it allowed me to focus on my work with minimal |
| 72 | 67 | distractions, but also allowed easy access, so I could spin around in my |
| @@ -74,12 +69,10 @@ chair and chat with my friends without leaving my chair. This is the | ||
| 74 | 69 | closest I've been to a home office environment (which is my personal |
| 75 | 70 | favorite, as I'll get to later in this post). |
| 76 | 71 | |
| 77 | *** Semi-Open Floor Concept | |
| 78 | :PROPERTIES: | |
| 79 | :CUSTOM_ID: semi-open-floor-concept | |
| 80 | :END: | |
| 81 | #+caption: Semi-Open Floor Concept | |
| 82 | [[https://img.cleberg.net/blog/20220210-leaving-office-based-work-in-the-past/semi_open_office.png]] | |
| 72 | ### Semi-Open Floor Concept | |
| 73 | ||
| 74 |  | |
| 83 | 76 | |
| 84 | 77 | When I shifted to my first full-time internal audit job out of college, |
| 85 | 78 | I was working at a company that was headquartered on a floor in a |
| @@ -87,8 +80,8 @@ downtown high-rise building. The company was only about 20 years old | ||
| 87 | 80 | when I worked there and were trying a lot of new things to attract young |
| 88 | 81 | talent, one of which was a semi-open floor concept for the office. My |
| 89 | 82 | department worked just around the hallway corner from the executive |
| 90 | offices and used that "modern" layout young tech companies started using | |
| 91 | in the 2000s/2010s. | |
| 83 | offices and used that "modern" layout young tech companies started | |
| 84 | using in the 2000s/2010s. | |
| 92 | 85 | |
| 93 | 86 | Each desk was brief, and you could look most coworkers in the face |
| 94 | 87 | without moving from your chair, I hated this so much. Directly to my |
| @@ -104,18 +97,17 @@ conversation would be constant throughout the day while you try to work. | ||
| 104 | 97 | For someone like me, who needs silence to get work done, that was a |
| 105 | 98 | non-starter. |
| 106 | 99 | |
| 107 | *** Hotel Office Concept | |
| 108 | :PROPERTIES: | |
| 109 | :CUSTOM_ID: hotel-office-concept | |
| 110 | :END: | |
| 111 | #+caption: Hotel Office Concept | |
| 112 | [[https://img.cleberg.net/blog/20220210-leaving-office-based-work-in-the-past/hotel_desks.png]] | |
| 100 | ### Hotel Office Concept | |
| 101 | ||
| 102 |  | |
| 113 | 104 | |
| 114 | 105 | I currently work for a company remotely (for now) and travel to the |
| 115 | 106 | office every once in a while for events and to help coach the staff |
| 116 | 107 | underneath me. The office I visit uses the hotel desk concept, where you |
| 117 | need to check in at a touch screen when you enter the office and "rent" | |
| 118 | a desk for the day. The same goes for offices and meeting rooms. | |
| 108 | need to check in at a touch screen when you enter the office and | |
| 109 | "rent" a desk for the day. The same goes for offices and meeting | |
| 110 | rooms. | |
| 119 | 111 | |
| 120 | 112 | These desks are flat-top only and do not have any walls at all. In |
| 121 | 113 | addition, they're stacked with one row of 4 desks facing another row of |
| @@ -133,12 +125,10 @@ Luckily, you can rent offices with doors that offer quiet and privacy, | ||
| 133 | 125 | which can be very nice if you have a lot of meetings or webinars on a |
| 134 | 126 | certain day. |
| 135 | 127 | |
| 136 | ** Home Office | |
| 137 | :PROPERTIES: | |
| 138 | :CUSTOM_ID: home-office | |
| 139 | :END: | |
| 140 | #+caption: Home Office | |
| 141 | [[https://img.cleberg.net/blog/20220210-leaving-office-based-work-in-the-past/home_office.png]] | |
| 128 | ## Home Office | |
| 129 | ||
| 130 |  | |
| 142 | 132 | |
| 143 | 133 | Okay, now let's finally get to the home office concept. I have worked |
| 144 | 134 | from home for a little over two years at this point, across three |
| @@ -150,16 +140,11 @@ These things might not apply to you, and that's fine. Everyone has a | ||
| 150 | 140 | different situation, and I really don't think you'll know what works |
| 151 | 141 | until you try. |
| 152 | 142 | |
| 153 | *** Tip #1 | |
| 154 | :PROPERTIES: | |
| 155 | :CUSTOM_ID: tip-1 | |
| 156 | :END: | |
| 157 | Let's start with my top rule for a home office: | |
| 143 | ### Tip #1 | |
| 158 | 144 | |
| 159 | #+begin_quote | |
| 160 | If you live with others, working in a shared space is not effective. | |
| 145 | Let's start with my top rule for a home office: | |
| 161 | 146 | |
| 162 | #+end_quote | |
| 147 | > If you live with others, working in a shared space is not effective. | |
| 163 | 148 | |
| 164 | 149 | It just does not work. If you have another person sleeping in your |
| 165 | 150 | bedroom, it is difficult to manage your work schedule with their |
| @@ -172,17 +157,12 @@ likely won't work either. Distractions will come far more frequently: | ||
| 172 | 157 | televisions, cooking, cleaning, deliveries, etc. If you're like me, |
| 173 | 158 | you'll end up playing a game instead of actually doing any work. |
| 174 | 159 | |
| 175 | *** Tip #2 | |
| 176 | :PROPERTIES: | |
| 177 | :CUSTOM_ID: tip-2 | |
| 178 | :END: | |
| 179 | Okay, the second thing I've discovered that works for me: | |
| 160 | ### Tip #2 | |
| 180 | 161 | |
| 181 | #+begin_quote | |
| 182 | Use the pomodoro method (or something similar) to balance work tasks | |
| 183 | with personal tasks. | |
| 162 | Okay, the second thing I've discovered that works for me: | |
| 184 | 163 | |
| 185 | #+end_quote | |
| 164 | > Use the pomodoro method (or something similar) to balance work tasks | |
| 165 | > with personal tasks. | |
| 186 | 166 | |
| 187 | 167 | I use a very casual version of the pomodoro method where I will work for |
| 188 | 168 | 1-2 hours (usually set in strict intervals like 1, 1.5, 2 hours) and |
| @@ -196,18 +176,13 @@ clothes in the washer, get the mail, etc. If you're in a convenient | ||
| 196 | 176 | location, this usually gives time for things like getting groceries (as |
| 197 | 177 | long as you're not a slow shopper). |
| 198 | 178 | |
| 199 | *** Tip #3 | |
| 200 | :PROPERTIES: | |
| 201 | :CUSTOM_ID: tip-3 | |
| 202 | :END: | |
| 179 | ### Tip #3 | |
| 180 | ||
| 203 | 181 | While I listed this one as number three, I don't think I'd accomplish |
| 204 | 182 | anything without it: |
| 205 | 183 | |
| 206 | #+begin_quote | |
| 207 | Document everything: even things you didn't before - such as task lists | |
| 208 | and notes from casual calls or meetings. | |
| 209 | ||
| 210 | #+end_quote | |
| 184 | > Document everything: even things you didn't before - such as task | |
| 185 | > lists and notes from casual calls or meetings. | |
| 211 | 186 | |
| 212 | 187 | I've noticed that staying in an office gave me more constant reminders |
| 213 | 188 | of outstanding tasks or facts I had learned in a conversation. |
| @@ -221,16 +196,11 @@ has improved my retention immensely. Beyond helping my mental | ||
| 221 | 196 | recollection, it has saved me numerous times when I need to do a keyword |
| 222 | 197 | search for some topic that was discussed 6+ months ago. |
| 223 | 198 | |
| 224 | *** Tip #4 | |
| 225 | :PROPERTIES: | |
| 226 | :CUSTOM_ID: tip-4 | |
| 227 | :END: | |
| 228 | Okay, last one for now. | |
| 199 | ### Tip #4 | |
| 229 | 200 | |
| 230 | #+begin_quote | |
| 231 | Keep your work area clean. | |
| 201 | Okay, last one for now. | |
| 232 | 202 | |
| 233 | #+end_quote | |
| 203 | > Keep your work area clean. | |
| 234 | 204 | |
| 235 | 205 | This one is straightforward, but I know some people struggle with |
| 236 | 206 | cleanliness or may not believe it makes a difference. Trust me, keeping |
| @@ -241,8 +211,8 @@ Just think about it, you walk into your home office and see a clean desk | ||
| 241 | 211 | with a laptop, dock, monitors, keyboard, mouse, and a notepad with a pen |
| 242 | 212 | on top. |
| 243 | 213 | |
| 244 | Now imagine the opposite, there's an office with the same equipment, but | |
| 245 | there are clothes hanging on the chair, empty drink bottles, candy | |
| 214 | Now imagine the opposite, there's an office with the same equipment, | |
| 215 | but there are clothes hanging on the chair, empty drink bottles, candy | |
| 246 | 216 | wrappers and dirty plates. This can take both a mental and emotional |
| 247 | 217 | toll by bringing constant disarray and stress into your working |
| 248 | 218 | environment. |
| @@ -250,14 +220,12 @@ environment. | ||
| 250 | 220 | Just keep things clean each day, and you won't need to do any big |
| 251 | 221 | cleaning days to recover. |
| 252 | 222 | |
| 253 | ** My Preferences | |
| 254 | :PROPERTIES: | |
| 255 | :CUSTOM_ID: my-preferences | |
| 256 | :END: | |
| 223 | ## My Preferences | |
| 224 | ||
| 257 | 225 | I've talked about the different environments I've worked in and |
| 258 | 226 | expressed some honest thoughts on pros or cons to each, but what do I |
| 259 | prefer? Well, if you're reading along, you should be able to tell that I | |
| 260 | much prefer a home office above all else. | |
| 227 | prefer? Well, if you're reading along, you should be able to tell that | |
| 228 | I much prefer a home office above all else. | |
| 261 | 229 | |
| 262 | 230 | Being able to control my own day and allot my time as needed has brought |
| 263 | 231 | a calmness to my life and has allowed me to maximize each day. I feel |
| @@ -270,5 +238,5 @@ my work done. | ||
| 270 | 238 | |
| 271 | 239 | Cubicles are good! I agree with Alice (from the comic Dilbert): |
| 272 | 240 | |
| 273 | #+caption: Dilbert comic strip | |
| 274 | [[https://img.cleberg.net/blog/20220210-leaving-office-based-work-in-the-past/dilbert_120109.png]] | |
| 241 |  | |
blog/2022-02-16-debian-and-nginx.org → content/blog/2022-02-16-debian-and-nginx.md renamed +73 −75
| @@ -1,12 +1,13 @@ | ||
| 1 | #+title: Migrating to a New Web Server Setup with Debian, Nginx, and Agate | |
| 2 | #+date: 2022-02-16 | |
| 1 | +++ | |
| 2 | date = 2022-02-16 | |
| 3 | title = "Migrating to a New Web Server Setup with Debian, Nginx, and Agate" | |
| 4 | description = "A retrospective on my recent server migration." | |
| 5 | +++ | |
| 3 | 6 | |
| 4 | ** Server OS: Debian | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: server-os-debian | |
| 7 | :END: | |
| 8 | #+caption: Debian + neofetch | |
| 9 | [[https://img.cleberg.net/blog/20220216-migrating-to-debian-and-nginx/neofetch.png]] | |
| 7 | ## Server OS: Debian | |
| 8 | ||
| 9 |  | |
| 10 | 11 | |
| 11 | 12 | I've used various Linux distributions throughout the years, but I've |
| 12 | 13 | never used anything except Ubuntu for my servers. Why? I really have no |
| @@ -15,64 +16,62 @@ idea, mostly just comfort around the commands and software availability. | ||
| 15 | 16 | However, I have always wanted to try Debian as a server OS after testing |
| 16 | 17 | it out in a VM a few years ago (side-note: I'd love to try Alpine too, |
| 17 | 18 | but I always struggle with compatibility). So, I decided to launch a new |
| 18 | VPS and use [[https://www.debian.org][Debian]] 11 as the OS. Spoiler | |
| 19 | VPS and use [Debian](https://www.debian.org) 11 as the OS. Spoiler | |
| 19 | 20 | alert: it feels identical to Ubuntu for my purposes. |
| 20 | 21 | |
| 21 | 22 | I did the normal things when first launching the VPS, such as adding a |
| 22 | 23 | new user, locking down SSH, etc. If you want to see that level of |
| 23 | detail, read my other post about | |
| 24 | [[https://cleberg.net/blog/how-to-set-up-a-vps-web-server/][How to Set | |
| 25 | Up a VPS Web Server]]. | |
| 24 | detail, read my other post about [How to Set Up a VPS Web | |
| 25 | Server](https://cleberg.net/blog/how-to-set-up-a-vps-web-server/). | |
| 26 | 26 | |
| 27 | 27 | All of this has been similar, apart from small things such as the |
| 28 | 28 | location of users' home folders. No complaints at all from me - Debian |
| 29 | 29 | seems great. |
| 30 | 30 | |
| 31 | ** Web Server: Nginx | |
| 32 | :PROPERTIES: | |
| 33 | :CUSTOM_ID: web-server-nginx | |
| 34 | :END: | |
| 35 | #+caption: Nginx status | |
| 36 | [[https://img.cleberg.net/blog/20220216-migrating-to-debian-and-nginx/nginx.png]] | |
| 31 | ## Web Server: Nginx | |
| 32 | ||
| 33 |  | |
| 37 | 35 | |
| 38 | 36 | Once I had the baseline server configuration set-up for Debian, I moved |
| 39 | on to trying out [[https://nginx.org][Nginx]] as my web server software. | |
| 40 | This required me to install the =nginx= and =ufw= packages, as well as | |
| 41 | setting up the initial UFW config: | |
| 37 | on to trying out [Nginx](https://nginx.org) as my web server software. | |
| 38 | This required me to install the `nginx` and `ufw` | |
| 39 | packages, as well as setting up the initial UFW config: | |
| 42 | 40 | |
| 43 | #+begin_src sh | |
| 41 | ```sh | |
| 44 | 42 | sudo apt install nginx ufw |
| 45 | 43 | sudo ufw allow 'Nginx Full' |
| 46 | 44 | sudo ufw allow SSH |
| 47 | 45 | sudo ufw enable |
| 48 | 46 | sudo ufw status |
| 49 | 47 | sudo systemctl status nginx |
| 50 | #+end_src | |
| 48 | ``` | |
| 51 | 49 | |
| 52 | 50 | Once I had the firewall set, I moved on to creating the directories and |
| 53 | 51 | files for my website. This is very easy and is basically the same as |
| 54 | 52 | setting up an Apache server, so no struggles here. |
| 55 | 53 | |
| 56 | #+begin_src sh | |
| 54 | ```sh | |
| 57 | 55 | sudo mkdir -p /var/www/your_domain/html |
| 58 | 56 | sudo chown -R $USER:$USER /var/www/your_domain/html |
| 59 | 57 | sudo chmod -R 755 /var/www/your_domain |
| 60 | 58 | nano /var/www/your_domain/html/index.html |
| 61 | #+end_src | |
| 59 | ``` | |
| 62 | 60 | |
| 63 | 61 | The next part, creating the Nginx configuration files, is quite a bit |
| 64 | 62 | different from Apache. First, you need to create the files in the |
| 65 | =sites-available= folder and symlink it the =sites-enabled= folder. | |
| 63 | `sites-available` folder and symlink it the | |
| 64 | `sites-enabled` folder. | |
| 66 | 65 | |
| 67 | 66 | Creating the config file for your domain: |
| 68 | 67 | |
| 69 | #+begin_src sh | |
| 68 | ```sh | |
| 70 | 69 | sudo nano /etc/nginx/sites-available/your_domain |
| 71 | #+end_src | |
| 70 | ``` | |
| 72 | 71 | |
| 73 | 72 | Default content for an Nginx config file: |
| 74 | 73 | |
| 75 | #+begin_src sh | |
| 74 | ```sh | |
| 76 | 75 | server { |
| 77 | 76 | listen 80; |
| 78 | 77 | listen [::]:80; |
| @@ -86,43 +85,45 @@ server { | ||
| 86 | 85 | try_files $uri $uri/ =404; |
| 87 | 86 | } |
| 88 | 87 | } |
| 89 | #+end_src | |
| 88 | ``` | |
| 90 | 89 | |
| 91 | 90 | Finally, symlink it together: |
| 92 | 91 | |
| 93 | #+begin_src sh | |
| 92 | ```sh | |
| 94 | 93 | sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/ |
| 95 | #+end_src | |
| 94 | ``` | |
| 96 | 95 | |
| 97 | 96 | This will make your site available to the public (as long as you have |
| 98 | =your_domain= DNS records pointed at the server's IP address)! | |
| 97 | `your_domain` DNS records pointed at the server's IP | |
| 98 | address)! | |
| 99 | 99 | |
| 100 | Next, I used [[https://certbot.eff.org/][certbot]] to issue an HTTPS | |
| 100 | Next, I used [certbot](https://certbot.eff.org/) to issue an HTTPS | |
| 101 | 101 | certificate for my domains using the following commands: |
| 102 | 102 | |
| 103 | #+begin_src sh | |
| 103 | ```sh | |
| 104 | 104 | sudo apt install snapd; sudo snap install core; sudo snap refresh core |
| 105 | 105 | sudo snap install --classic certbot |
| 106 | 106 | sudo ln -s /snap/bin/certbot /usr/bin/certbot |
| 107 | 107 | sudo certbot --nginx |
| 108 | #+end_src | |
| 108 | ``` | |
| 109 | 109 | |
| 110 | 110 | Now that certbot ran successfully and updated my Nginx config files to |
| 111 | include a =443= server block of code, I went back in and edited the | |
| 112 | config file to include security HTTP headers. This part is optional, but | |
| 113 | is recommended for security purposes; you can even test a website's HTTP | |
| 114 | header security at [[https://securityheaders.com/][Security Headers]]. | |
| 111 | include a `443` server block of code, I went back in and | |
| 112 | edited the config file to include security HTTP headers. This part is | |
| 113 | optional, but is recommended for security purposes; you can even test a | |
| 114 | website's HTTP header security at [Security | |
| 115 | Headers](https://securityheaders.com/). | |
| 115 | 116 | |
| 116 | 117 | The configuration below shows a set-up where you only want your website |
| 117 | 118 | to serve content from its own domain, except for images and scripts, |
| 118 | which may come from =nullitics.com=. All other content would be blocked | |
| 119 | from loading in a browser. | |
| 119 | which may come from `nullitics.com`. All other content would | |
| 120 | be blocked from loading in a browser. | |
| 120 | 121 | |
| 121 | #+begin_src sh | |
| 122 | ```sh | |
| 122 | 123 | sudo nano /etc/nginx/sites-available/your_domain |
| 123 | #+end_src | |
| 124 | ``` | |
| 124 | 125 | |
| 125 | #+begin_src sh | |
| 126 | ```sh | |
| 126 | 127 | server { |
| 127 | 128 | ... |
| 128 | 129 | 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'"; |
| @@ -133,50 +134,47 @@ server { | ||
| 133 | 134 | add_header Referrer-Policy "no-referrer"; |
| 134 | 135 | ... |
| 135 | 136 | } |
| 136 | #+end_src | |
| 137 | ``` | |
| 137 | 138 | |
| 138 | #+begin_src sh | |
| 139 | ```sh | |
| 139 | 140 | sudo systemctl restart nginx |
| 140 | #+end_src | |
| 141 | ``` | |
| 142 | ||
| 143 | ### Nginx vs. Apache | |
| 141 | 144 | |
| 142 | *** Nginx vs. Apache | |
| 143 | :PROPERTIES: | |
| 144 | :CUSTOM_ID: nginx-vs.-apache | |
| 145 | :END: | |
| 146 | 145 | As I stated at the beginning, my historical hesitation with trying Nginx |
| 147 | 146 | was that the differences in configuration formats scared me away from |
| 148 | 147 | leaving Apache. However, I prefer Nginx to Apache for a few reasons: |
| 149 | 148 | |
| 150 | 1. Nginx uses only one config file (=your_domain=) vs. Apache's two-file | |
| 151 | approach for HTTP vs. HTTPS (=your_domain.conf= and | |
| 152 | =your_domain-le-ssl.conf=). | |
| 153 | 2. Symlinking new configurations files and reloading Nginx are way | |
| 154 | easier than Apache's process of having to enable headers with | |
| 155 | =a2enmod mod_headers=, enable PHP with =a2enmod php= (plus any other | |
| 156 | mods you need), and then enabling sites with =a2ensite=, and THEN | |
| 157 | reloading Apache. | |
| 158 | 3. The contents of the Nginx config files seem more organized and | |
| 159 | logical with the curly-bracket approach. This is a minor reason, but | |
| 160 | everything just felt cleaner while I was installing my sites and that | |
| 161 | had a big quality of life impact on the installation for me. | |
| 149 | 1. Nginx uses only one config file (`your_domain`) | |
| 150 | vs. Apache's two-file approach for HTTP vs. HTTPS | |
| 151 | (`your_domain.conf` and | |
| 152 | `your_domain-le-ssl.conf`). | |
| 153 | 2. Symlinking new configurations files and reloading Nginx are way | |
| 154 | easier than Apache's process of having to enable headers with | |
| 155 | `a2enmod mod_headers`, enable PHP with | |
| 156 | `a2enmod php` (plus any other mods you need), and then | |
| 157 | enabling sites with `a2ensite`, and THEN reloading | |
| 158 | Apache. | |
| 159 | 3. The contents of the Nginx config files seem more organized and | |
| 160 | logical with the curly-bracket approach. This is a minor reason, but | |
| 161 | everything just felt cleaner while I was installing my sites and | |
| 162 | that had a big quality of life impact on the installation for me. | |
| 162 | 163 | |
| 163 | 164 | They're both great software packages, but Nginx just seems more |
| 164 | 165 | organized and easier to use these days. I will certainly be exploring |
| 165 | 166 | the Nginx docs to see what other fun things I can do with all of this. |
| 166 | 167 | |
| 167 | ** Gemini Server: Agate | |
| 168 | :PROPERTIES: | |
| 169 | :CUSTOM_ID: gemini-server-agate | |
| 170 | :END: | |
| 171 | #+caption: Agate status | |
| 172 | [[https://img.cleberg.net/blog/20220216-migrating-to-debian-and-nginx/agate.png]] | |
| 168 | ## Gemini Server: Agate | |
| 169 | ||
| 170 |  | |
| 173 | 172 | |
| 174 | 173 | Finally, I set up the Agate software on this server again to host my |
| 175 | 174 | Gemini server content, using Rust as I have before. You can read my |
| 176 | other post for more information on installing Agate: | |
| 177 | [[https://cleberg.net/blog/hosting-a-gemini-server/][Hosting a Gemini | |
| 178 | Server]]. | |
| 175 | other post for more information on installing Agate: [Hosting a Gemini | |
| 176 | Server](https://cleberg.net/blog/hosting-a-gemini-server/). | |
| 179 | 177 | |
| 180 | 178 | All in all, Debian + Nginx is very slick and I prefer it over my old |
| 181 | combination of Ubuntu + Apache (although it's really just Nginx > Apache | |
| 182 | for me, since Debian seems mostly the same as Ubuntu is so far). | |
| 179 | combination of Ubuntu + Apache (although it's really just Nginx \> | |
| 180 | Apache for me, since Debian seems mostly the same as Ubuntu is so far). | |
content/blog/2022-02-17-exiftool.md added +67
| @@ -0,0 +1,67 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-02-17 | |
| 3 | title = "Stripping Image Metadata with exiftool" | |
| 4 | description = "A simple guide to remove exif data with exiftool." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Why Strip Metadata? | |
| 8 | ||
| 9 | Okay, so you want to strip metadata from your photos. Perhaps you take | |
| 10 | pictures of very rare birds, and the location metadata is a gold mine | |
| 11 | for poachers, or perhaps you're just privacy-oriented like me and | |
| 12 | prefer to strip metadata from publicly-available images. | |
| 13 | ||
| 14 | There are various components of image metadata that you may want to | |
| 15 | delete before releasing a photo to the public. Here's an incomplete | |
| 16 | list of things I could easily see just by inspecting a photo on my | |
| 17 | laptop: | |
| 18 | ||
| 19 | - Location (Latitude & Longitude) | |
| 20 | - Dimensions | |
| 21 | - Device Make & Model | |
| 22 | - Color Space | |
| 23 | - Color Profile | |
| 24 | - Focal Length | |
| 25 | - Alpha Channel | |
| 26 | - Red Eye | |
| 27 | - Metering Mode | |
| 28 | - F Number | |
| 29 | ||
| 30 | Regardless of your reasoning, I'm going to explain how I used the | |
| 31 | `exiftool` package in Linux to automatically strip metadata | |
| 32 | from all images in a directory (+ subdirectories). | |
| 33 | ||
| 34 | ## Installing `exiftool` | |
| 35 | ||
| 36 | First things first: we need to install the tool. I'm running Debian 11 | |
| 37 | on my server (Ubuntu will work the same), so the command is as simple | |
| 38 | as: | |
| 39 | ||
| 40 | ```sh | |
| 41 | sudo apt install exiftool | |
| 42 | ``` | |
| 43 | ||
| 44 | There are different tools that can accomplish the same thing across | |
| 45 | distributions, but I really only care to test out this one package. | |
| 46 | ||
| 47 | ## Recursively Strip Data | |
| 48 | ||
| 49 | I actually use this tool extensively to strip any photos uploaded to the | |
| 50 | website that serves all the images for my blog | |
| 51 | (`img.cleberg.net`). | |
| 52 | ||
| 53 | The following command is incredibly useful and can be modified to | |
| 54 | include any image extensions that `exiftool` supports: | |
| 55 | ||
| 56 | ```sh | |
| 57 | exiftool -r -all= -ext jpg -ext png /path/to/directory/ | |
| 58 | ``` | |
| 59 | ||
| 60 | See below for the results of my most recent usage of | |
| 61 | `exiftool` after I uploaded the image for this blog post. You | |
| 62 | can see that the command will let you know how many directories were | |
| 63 | scanned, how many images were updated, and how many images were | |
| 64 | unchanged. | |
| 65 | ||
| 66 |  | |
content/blog/2022-02-20-nginx-caching.md added +72
| @@ -0,0 +1,72 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-02-20 | |
| 3 | title = "Caching Static Content with Nginx" | |
| 4 | description = "Learn how to enable the static content cache in Nginx." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Update Your Nginx Config to Cache Static Files | |
| 8 | ||
| 9 | If you run a website on Nginx that serves static content (i.e., content | |
| 10 | that is not dynamic and changing with interactions from the user), you | |
| 11 | would likely benefit from caching that content on the client-side. If | |
| 12 | you're used to Apache and looking for the Nginx equivalent, this post | |
| 13 | should help. | |
| 14 | ||
| 15 | Luckily, setting up the cache is as easy as identifying the file types | |
| 16 | you want to cache and determining the expiration length. To include more | |
| 17 | file types, simply use the bar separator (`|`) and type the | |
| 18 | new file extension you want to include. | |
| 19 | ||
| 20 | ```config | |
| 21 | server { | |
| 22 | ... | |
| 23 | ||
| 24 | location ~* .(css|js|jpg|jpeg|gif|png|ico)$ { | |
| 25 | expires 30d; | |
| 26 | } | |
| 27 | ||
| 28 | ... | |
| 29 | } | |
| 30 | ``` | |
| 31 | ||
| 32 | I have seen some people who prefer to set `expires` as | |
| 33 | `365d` or even `max`, but that is only for stable, | |
| 34 | infrequently changing websites. As my site often changes (i.e., I'm | |
| 35 | never content with my website), I need to know that my readers are | |
| 36 | seeing the new content without waiting too long. | |
| 37 | ||
| 38 | So, I went ahead and set the expiration date at `30d`, which | |
| 39 | is short enough to refresh for readers but long enough that | |
| 40 | clients/browsers won't be re-requesting the static files too often, | |
| 41 | hopefully resulting in faster loading times, as images should be the | |
| 42 | only thing slowing down my site. | |
| 43 | ||
| 44 | ## Testing Results | |
| 45 | ||
| 46 | To test my changes to the Nginx configuration, I used the [HTTP Header | |
| 47 | Live](https://addons.mozilla.org/en-US/firefox/addon/http-header-live/) | |
| 48 | extension on my Gecko browser and used the sidebar to inspect the | |
| 49 | headers of a recent image from my blog. | |
| 50 | ||
| 51 | In the image below, you can see that the `Cache-Control` | |
| 52 | header is now present and set to 2592000, which is 30 days represented | |
| 53 | in seconds (30 days \_ 24 hours/day \_ 60 minutes/hour \* 60 | |
| 54 | seconds/minute = 2,592,000 seconds). | |
| 55 | ||
| 56 | The `Expires` field is now showing 22 March 2022, which is 30 | |
| 57 | days from the day of this post, 20 February 2022. | |
| 58 | ||
| 59 |  | |
| 61 | ||
| 62 | ## Caveats | |
| 63 | ||
| 64 | Remember that this caching system is **client-side**, which means that | |
| 65 | content is only cached for as long as a client allows it. For example, | |
| 66 | my browser purges all caches, data, etc. upon exit, so this caching | |
| 67 | policy will only work as long as my browser remains open and running. | |
| 68 | ||
| 69 | If you need to test updates to your site, you'll need to clear the | |
| 70 | cache to see updates for any file extension you configured. This can | |
| 71 | often be done with the `Shift + F5` or `Ctrl + F5` | |
| 72 | key combinations in most browsers. | |
content/blog/2022-02-22-tuesday.md added +41
| @@ -0,0 +1,41 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-02-22 | |
| 3 | title = "Tuesday" | |
| 4 | description = "2-22-22 || 22-2-22" | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Tuesday, Twosday | |
| 8 | ||
| 9 | I'm taking a break from my usual technology-related posts and writing | |
| 10 | about something a little more enjoyable today. | |
| 11 | ||
| 12 | Today is Tuesday, February 22nd, 2022. Today is 02-22-2022. Today is | |
| 13 | Twosday. | |
| 14 | ||
| 15 | Let's look at everything that fell in place today: | |
| 16 | ||
| 17 | 1. Written in the `m-dd-yy` or `dd-m-yy` formats, | |
| 18 | today is 2-22-22 or 22-2-22, which is a neat little palindrome in | |
| 19 | either format. (The last ubiquitous six-digit palindrome was | |
| 20 | 1-11-11.) | |
| 21 | 2. Today is Tuesday, which is why everyone is using the nickname | |
| 22 | Twosday to call out these similarities. | |
| 23 | 3. Falling on Tuesday means today is the 2nd day of the week (for most | |
| 24 | cultures. For the US, it's the 3rd day of the week since we start | |
| 25 | on Sunday). | |
| 26 | 4. The only culture I could find with a connection to a `2` | |
| 27 | is that some Slavic languages derived their version of Tuesday from | |
| 28 | the Old Church Slavonic word `въторъ`, meaning "the | |
| 29 | second." | |
| 30 | 5. Written in the classic monospaced, digital font (think of digital | |
| 31 | clocks from the 80s/90s), there is nice symmetry to the numbers | |
| 32 | ([view the image | |
| 33 | here](https://img.cleberg.net/blog/20220222-tuesday/digital_font.png)!). | |
| 34 | 6. This one isn't naturally-occurring, but it seems people around the | |
| 35 | world are celebrating the day. For example, a group is putting | |
| 36 | together | |
| 37 | [[<https://www.eventbrite.com/e/2-22-22-a-collective-wedding-ceremony-at-the-state-capitol-tickets-211434605597>][a | |
| 38 | wedding of 222 couples at the California State Capitol in | |
| 39 | Sacramento]], concluding at exactly 2:22 PM. These couples will | |
| 40 | record their marriage dates as 2-22-22 2:22 PM. Tickets were on sale | |
| 41 | for \$222.22. | |
blog/2022-03-02-note-taking.org → content/blog/2022-03-02-note-taking.md renamed +49 −69
| @@ -1,19 +1,16 @@ | ||
| 1 | #+title: Easy, Reliable Note-Taking | |
| 2 | #+date: 2022-03-02 | |
| 1 | +++ | |
| 2 | date = 2022-03-02 | |
| 3 | title = "Easy, Reliable Note-Taking" | |
| 4 | description = "My personal preferences to ensure notes are clear, portable, and long-lasting." | |
| 5 | +++ | |
| 3 | 6 | |
| 4 | ** Choosing Durable File Formats | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: choosing-durable-file-formats | |
| 7 | :END: | |
| 7 | ## Choosing Durable File Formats | |
| 8 | 8 | |
| 9 | #+begin_quote | |
| 10 | TL;DR: Write in a format that can be easily rendered and read in | |
| 11 | plain-text mode (e.g., =.txt=, =.md=, etc.). | |
| 9 | > TL;DR: Write in a format that can be easily rendered and read in | |
| 10 | > plain-text mode (e.g., `.txt`, `.md`, etc.). | |
| 12 | 11 | |
| 13 | #+end_quote | |
| 14 | ||
| 15 | As I've written more and more over the years, I've found that my love of | |
| 16 | note-taking is always growing. Everything I learn or need to remember | |
| 12 | As I've written more and more over the years, I've found that my love | |
| 13 | of note-taking is always growing. Everything I learn or need to remember | |
| 17 | 14 | can be written down in a note and saved digitally, with no cost to |
| 18 | 15 | myself. Unlike paper copies that need physical storage space, digital |
| 19 | 16 | files simply need space on your local disk or cloud storage, which is |
| @@ -25,8 +22,8 @@ styling. This meant that I had to go through each note during migration | ||
| 25 | 22 | and edit the file to look presentable again. |
| 26 | 23 | |
| 27 | 24 | For the last year or two, I have written everything exclusively in |
| 28 | [[https://en.wikipedia.org/wiki/Markdown][Markdown]] format. Small | |
| 29 | notes, long-form writings, and even these blog posts are all written in | |
| 25 | [Markdown](https://en.wikipedia.org/wiki/Markdown) format. Small notes, | |
| 26 | long-form writings, and even these blog posts are all written in | |
| 30 | 27 | Markdown. |
| 31 | 28 | |
| 32 | 29 | Why Markdown? While I do appreciate the simplicity of plain-text files |
| @@ -34,21 +31,19 @@ without any formatting, I often need visual cues such as heading and | ||
| 34 | 31 | code blocks to keep my thoughts straight. Markdown provides a minimal |
| 35 | 32 | set of styling indicators for me to style my notes without adding any |
| 36 | 33 | proprietary, embedded data into the files. If I want a top-level |
| 37 | heading, I simply add a hash (=#=) before the line. An added bonus is | |
| 38 | that even if a system doesn't understand Markdown, it will render it as | |
| 39 | plain-text and I can read it just as easily. | |
| 34 | heading, I simply add a hash (`#`) before the line. An added | |
| 35 | bonus is that even if a system doesn't understand Markdown, it will | |
| 36 | render it as plain-text and I can read it just as easily. | |
| 40 | 37 | |
| 41 | 38 | For example, here's how TextEdit on macOS will open and display a |
| 42 | 39 | Markdown file in plain-text, since it does contain any features to |
| 43 | 40 | preview Markdown as HTML: |
| 44 | 41 | |
| 45 | #+caption: Plain Text Markdown | |
| 46 | [[https://img.cleberg.net/blog/20220302-easy-reliable-note-taking/plain_markdown.png]] | |
| 42 |  | |
| 44 | ||
| 45 | ## Saving & Syncing Files | |
| 47 | 46 | |
| 48 | ** Saving & Syncing Files | |
| 49 | :PROPERTIES: | |
| 50 | :CUSTOM_ID: saving-syncing-files | |
| 51 | :END: | |
| 52 | 47 | In order to read and edit my notes across platforms, I use my personal |
| 53 | 48 | cloud storage through Tresorit due to its native integration with macOS |
| 54 | 49 | and iOS file managers. In addition, Tresorit works well on Debian-based |
| @@ -64,23 +59,19 @@ me to sync anything manually or kick-off a sync job to update my files. | ||
| 64 | 59 | This means that I can edit on mobile, and it takes about 5-10 seconds to |
| 65 | 60 | see the changes on desktop. |
| 66 | 61 | |
| 67 | *** Version Control with Git | |
| 68 | :PROPERTIES: | |
| 69 | :CUSTOM_ID: version-control-with-git | |
| 70 | :END: | |
| 62 | ### Version Control with Git | |
| 63 | ||
| 71 | 64 | A different approach I've contemplated is storing my notes and |
| 72 | 65 | attachments is using a hosted Git repository to track changes to the |
| 73 | 66 | files. However, I don't want to rely on an external service that could |
| 74 | 67 | potentially see into my data, even if the repository is private. |
| 75 | 68 | |
| 76 | I might just do =git init= locally and then commit my changes each time | |
| 77 | I write or update a note, but that seems to be a lot of work just for | |
| 78 | tracking changes - which I don't necessarily care to know. | |
| 69 | I might just do `git init` locally and then commit my changes | |
| 70 | each time I write or update a note, but that seems to be a lot of work | |
| 71 | just for tracking changes - which I don't necessarily care to know. | |
| 72 | ||
| 73 | ### Backups! | |
| 79 | 74 | |
| 80 | *** Backups! | |
| 81 | :PROPERTIES: | |
| 82 | :CUSTOM_ID: backups | |
| 83 | :END: | |
| 84 | 75 | One small addition to the storage/sync conversation is the idea of |
| 85 | 76 | backups. Personally, I manually create periodic backups of my entire |
| 86 | 77 | cloud storage, compress it into an archive, and store it on my home |
| @@ -90,18 +81,12 @@ To improve my workflow, I am going to be exploring options to | ||
| 90 | 81 | automatically compress the mounted cloud directory and send it over to |
| 91 | 82 | my server on a set schedule. |
| 92 | 83 | |
| 93 | ** Writing on Desktop | |
| 94 | :PROPERTIES: | |
| 95 | :CUSTOM_ID: writing-on-desktop | |
| 96 | :END: | |
| 84 | ## Writing on Desktop | |
| 97 | 85 | |
| 98 | #+begin_quote | |
| 99 | *Update (06.14.22)*: Since writing this post, I have reverted to simply | |
| 100 | keeping my =notes= folder open and opening notes individually in | |
| 101 | TextEdit for a more minimal and relaxing writing experience on the | |
| 102 | desktop. | |
| 103 | ||
| 104 | #+end_quote | |
| 86 | > **Update (06.14.22)**: Since writing this post, I have reverted to | |
| 87 | > simply keeping my `notes` folder open and opening notes | |
| 88 | > individually in TextEdit for a more minimal and relaxing writing | |
| 89 | > experience on the desktop. | |
| 105 | 90 | |
| 106 | 91 | The bulk of my writing occurs in a desktop environment, with a full |
| 107 | 92 | keyboard layout and wide screen. I don't illustrate with a smart pen, I |
| @@ -109,36 +94,31 @@ rarely use embedded images, and I love being able to see all of my | ||
| 109 | 94 | notes/directories in a sidebar. |
| 110 | 95 | |
| 111 | 96 | With this simple set of requirements, I chose |
| 112 | [[https://obsidian.md][Obsidian]] as my desktop text editor. Obsidian | |
| 113 | has some in-depth tools like a graph view, command palette, mentions, | |
| 114 | etc., but I've found that using it as a simple Markdown editor is | |
| 115 | incredibly easy and straightforward. | |
| 97 | [Obsidian](https://obsidian.md) as my desktop text editor. Obsidian has | |
| 98 | some in-depth tools like a graph view, command palette, mentions, etc., | |
| 99 | but I've found that using it as a simple Markdown editor is incredibly | |
| 100 | easy and straightforward. | |
| 116 | 101 | |
| 117 | 102 | Here's an example of how my Markdown notes look when opened in |
| 118 | 103 | plain-text mode: |
| 119 | 104 | |
| 120 | #+caption: Obsidian Markdown Source Mode | |
| 121 | [[https://img.cleberg.net/blog/20220302-easy-reliable-note-taking/obsidian_source_mode.png]] | |
| 105 |  | |
| 107 | ||
| 108 | Here's the "live preview" version, where the Markdown is rendered | |
| 109 | into its HTML format: | |
| 122 | 110 | |
| 123 | Here's the "live preview" version, where the Markdown is rendered into | |
| 124 | its HTML format: | |
| 111 |  | |
| 125 | 113 | |
| 126 | #+caption: Obsidian Markdown Live Preview | |
| 127 | [[https://img.cleberg.net/blog/20220302-easy-reliable-note-taking/obsidian_live_preview.png]] | |
| 114 | ### Programming on Desktop | |
| 128 | 115 | |
| 129 | *** Programming on Desktop | |
| 130 | :PROPERTIES: | |
| 131 | :CUSTOM_ID: programming-on-desktop | |
| 132 | :END: | |
| 133 | 116 | While I was writing this, I realized I should specify that I don't use |
| 134 | 117 | the same editor for writing notes and for writing code. For programming |
| 135 | purposes, I use [[https://vscodium.com][VSCodium]] as my development | |
| 136 | IDE. | |
| 118 | purposes, I use [VSCodium](https://vscodium.com) as my development IDE. | |
| 119 | ||
| 120 | ## Writing on Mobile | |
| 137 | 121 | |
| 138 | ** Writing on Mobile | |
| 139 | :PROPERTIES: | |
| 140 | :CUSTOM_ID: writing-on-mobile | |
| 141 | :END: | |
| 142 | 122 | Personally, I write very little on mobile, except when needing to take |
| 143 | 123 | important notes on-the-go. Any long-form writing, journals, etc. are |
| 144 | 124 | done at home, where I always have my laptop available. |
| @@ -146,9 +126,9 @@ done at home, where I always have my laptop available. | ||
| 146 | 126 | I wanted a simple and foolproof editor for iOS, preferably open-source. |
| 147 | 127 | After a long journey of testing the few (& terrible) open-source iOS |
| 148 | 128 | note-taking apps, I finally found a phenomenal one: |
| 149 | [[https://github.com/simonbs/runestone][Runestone]]. This app is | |
| 150 | fantastic for note-taking, has plenty of optional features, and | |
| 151 | integrates natively with the iOS file manager. | |
| 129 | [Runestone](https://github.com/simonbs/runestone). This app is fantastic | |
| 130 | for note-taking, has plenty of optional features, and integrates | |
| 131 | natively with the iOS file manager. | |
| 152 | 132 | |
| 153 | 133 | This app opens the iOS file manager and allows you to click any file you |
| 154 | 134 | want, opens it up in an editor, and lets me save and close out of that |
content/blog/2022-03-03-financial-database.md added +271
| @@ -0,0 +1,271 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-03-03 | |
| 3 | title = "Maintaining a Personal Financial Database" | |
| 4 | description = "An example project showing to build and maintain a simple financial database." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Personal Financial Tracking | |
| 8 | ||
| 9 | For the last 6-ish years, I've tracked my finances in a spreadsheet. | |
| 10 | This is common practice in the business world, but any good dev will | |
| 11 | cringe at the thought of storing long-term data in a spreadsheet. A | |
| 12 | spreadsheet is not for long-term storage or as a source of data to pull | |
| 13 | data/reports. | |
| 14 | ||
| 15 | As I wanted to expand the functionality of my financial data (e.g., | |
| 16 | adding more reports), I decided to migrate the data into a database. To | |
| 17 | run reports, I would query the database and use a language like Python | |
| 18 | or Javascript to process the data, perform calculations, and visualize | |
| 19 | the data. | |
| 20 | ||
| 21 | ## SQLite | |
| 22 | ||
| 23 | When choosing the type of database I wanted to use for this project, I | |
| 24 | was split between three options: | |
| 25 | ||
| 26 | 1. MySQL: The database I have the most experience with and have used | |
| 27 | for years. | |
| 28 | 2. PostgreSQL: A database I'm new to, but want to learn. | |
| 29 | 3. SQLite: A database that I've used for a couple projects and have | |
| 30 | moderate experience. | |
| 31 | ||
| 32 | I ended up choosing SQLite since it can be maintained within a single | |
| 33 | `.sqlite` file, which allows me more flexibility for storage | |
| 34 | and backup. I keep this file in my cloud storage and pull it up whenever | |
| 35 | needed. | |
| 36 | ||
| 37 | ### GUI Editing | |
| 38 | ||
| 39 | Since I didn't want to try and import 1000--1500 records into my new | |
| 40 | database via the command line, I opted to use [DB Browser for SQLite | |
| 41 | (DB4S)](https://sqlitebrowser.org/) as a GUI tool. This application is | |
| 42 | excellent, and I don't see myself going back to the CLI when working in | |
| 43 | this database. | |
| 44 | ||
| 45 | DB4S allows you to copy a range of cells from a spreadsheet and paste it | |
| 46 | straight into the SQL table. I used this process for all 36 accounts, | |
| 47 | 1290 account statements, and 126 pay statements. Overall, I'm guessing | |
| 48 | this took anywhere between 4--8 hours. In comparison, it probably took | |
| 49 | me 2-3 days to initially create the spreadsheet. | |
| 50 | ||
| 51 |  | |
| 52 | ||
| 53 | ### Schema | |
| 54 | ||
| 55 | The schema for this database is actually extremely simple and involves | |
| 56 | only three tables (for now): | |
| 57 | ||
| 58 | 1. Accounts | |
| 59 | 2. Statements | |
| 60 | 3. Payroll | |
| 61 | ||
| 62 | **Accounts** | |
| 63 | ||
| 64 | The Accounts table contains summary information about an account, such | |
| 65 | as a car loan or a credit card. By viewing this table, you can find | |
| 66 | high-level data, such as interest rate, credit line, or owner. | |
| 67 | ||
| 68 | ```sql | |
| 69 | CREATE TABLE "Accounts" ( | |
| 70 | "AccountID" INTEGER NOT NULL UNIQUE, | |
| 71 | "AccountType" TEXT, | |
| 72 | "AccountName" TEXT, | |
| 73 | "InterestRate" NUMERIC, | |
| 74 | "CreditLine" NUMERIC, | |
| 75 | "State" TEXT, | |
| 76 | "Owner" TEXT, | |
| 77 | "Co-Owner" TEXT, | |
| 78 | PRIMARY KEY("AccountID" AUTOINCREMENT) | |
| 79 | ) | |
| 80 | ``` | |
| 81 | ||
| 82 | **Statements** | |
| 83 | ||
| 84 | The Statements table uses the same unique identifier as the Accounts | |
| 85 | table, meaning you can join the tables to find a monthly statement for | |
| 86 | any of the accounts listed in the Accounts table. Each statement has an | |
| 87 | account ID, statement date, and total balance. | |
| 88 | ||
| 89 | ```sql | |
| 90 | CREATE TABLE "Statements" ( | |
| 91 | "StatementID" INTEGER NOT NULL UNIQUE, | |
| 92 | "AccountID" INTEGER, | |
| 93 | "StatementDate" INTEGER, | |
| 94 | "Balance" NUMERIC, | |
| 95 | PRIMARY KEY("StatementID" AUTOINCREMENT), | |
| 96 | FOREIGN KEY("AccountID") REFERENCES "Accounts"("AccountID") | |
| 97 | ) | |
| 98 | ``` | |
| 99 | ||
| 100 | **Payroll** | |
| 101 | ||
| 102 | The Payroll table is a separate entity, unrelated to the Accounts or | |
| 103 | Statements tables. This table contains all information you would find on | |
| 104 | a pay statement from an employer. As you change employers or obtain new | |
| 105 | perks/benefits, just add new columns to adapt to the new data. | |
| 106 | ||
| 107 | ```sql | |
| 108 | CREATE TABLE "Payroll" ( | |
| 109 | "PaycheckID" INTEGER NOT NULL UNIQUE, | |
| 110 | "PayDate" TEXT, | |
| 111 | "Payee" TEXT, | |
| 112 | "Employer" TEXT, | |
| 113 | "JobTitle" TEXT, | |
| 114 | "IncomeRegular" NUMERIC, | |
| 115 | "IncomePTO" NUMERIC, | |
| 116 | "IncomeHoliday" NUMERIC, | |
| 117 | "IncomeBonus" NUMERIC, | |
| 118 | "IncomePTOPayout" NUMERIC, | |
| 119 | "IncomeReimbursements" NUMERIC, | |
| 120 | "FringeHSA" NUMERIC, | |
| 121 | "FringeStudentLoan" NUMERIC, | |
| 122 | "Fringe401k" NUMERIC, | |
| 123 | "PreTaxMedical" NUMERIC, | |
| 124 | "PreTaxDental" NUMERIC, | |
| 125 | "PreTaxVision" NUMERIC, | |
| 126 | "PreTaxLifeInsurance" NUMERIC, | |
| 127 | "PreTax401k" NUMERIC, | |
| 128 | "PreTaxParking" NUMERIC, | |
| 129 | "PreTaxStudentLoan" NUMERIC, | |
| 130 | "PreTaxOther" NUMERIC, | |
| 131 | "TaxFederal" NUMERIC, | |
| 132 | "TaxSocial" NUMERIC, | |
| 133 | "TaxMedicare" NUMERIC, | |
| 134 | "TaxState" NUMERIC, | |
| 135 | PRIMARY KEY("PaycheckID" AUTOINCREMENT) | |
| 136 | ) | |
| 137 | ``` | |
| 138 | ||
| 139 | ### Python Reporting | |
| 140 | ||
| 141 | Once I created the database tables and imported all my data, the only | |
| 142 | step left was to create a process to report and visualize on various | |
| 143 | aspects of the data. | |
| 144 | ||
| 145 | In order to explore and create the reports I'm interested in, I | |
| 146 | utilized a two-part process involving Jupyter Notebooks and Python | |
| 147 | scripts. | |
| 148 | ||
| 149 | 1. Step 1: Jupyter Notebooks | |
| 150 | ||
| 151 | When I need to explore data, try different things, and re-run my | |
| 152 | code cell-by-cell, I use Jupyter Notebooks. For example, I explored | |
| 153 | the `Accounts` table until I found the following useful | |
| 154 | information: | |
| 155 | ||
| 156 | ```python | |
| 157 | import sqlite3 | |
| 158 | import pandas as pd | |
| 159 | import matplotlib | |
| 160 | ||
| 161 | # Set up database filename and connect | |
| 162 | db = "finances.sqlite" | |
| 163 | connection = sqlite3.connect(db) | |
| 164 | df = pd.read_sql_query("SELECT * FROM Accounts", connection) | |
| 165 | ||
| 166 | # Set global matplotlib variables | |
| 167 | %matplotlib inline | |
| 168 | matplotlib.rcParams['text.color'] = 'white' | |
| 169 | matplotlib.rcParams['axes.labelcolor'] = 'white' | |
| 170 | matplotlib.rcParams['xtick.color'] = 'white' | |
| 171 | matplotlib.rcParams['ytick.color'] = 'white' | |
| 172 | matplotlib.rcParams['legend.labelcolor'] = 'black' | |
| 173 | ||
| 174 | # Display graph | |
| 175 | df.groupby(['AccountType']).sum().plot.pie(title='Credit Line by Account Type', y='CreditLine', figsize=(5,5), autopct='%1.1f%%') | |
| 176 | ``` | |
| 177 | ||
| 178 | 2. Step 2: Python Scripts | |
| 179 | ||
| 180 | Once I explored enough through the notebooks and had a list of | |
| 181 | reports I wanted, I moved on to create a Python project with the | |
| 182 | following structure: | |
| 183 | ||
| 184 | ```example | |
| 185 | finance/ | |
| 186 | ├── notebooks/ | |
| 187 | │ │ ├── account_summary.ipynb | |
| 188 | │ │ ├── account_details.ipynb | |
| 189 | │ │ └── payroll.ipynb | |
| 190 | ├── public/ | |
| 191 | │ │ ├── image-01.png | |
| 192 | │ │ └── image-0X.png | |
| 193 | ├── src/ | |
| 194 | │ └── finance.sqlite | |
| 195 | ├── venv/ | |
| 196 | ├── _init.py | |
| 197 | ├── database.py | |
| 198 | ├── process.py | |
| 199 | ├── requirements.txt | |
| 200 | └── README.md | |
| 201 | ``` | |
| 202 | ||
| 203 | This structure allows me to: | |
| 204 | ||
| 205 | 1. Compile all required python packages into | |
| 206 | `requirements.txt` for easy installation if I move to | |
| 207 | a new machine. | |
| 208 | 2. Activate a virtual environment in `venv/` so I don't | |
| 209 | need to maintain a system-wide Python environment just for this | |
| 210 | project. | |
| 211 | 3. Keep my `notebooks/` folder to continuously explore | |
| 212 | the data as I see fit. | |
| 213 | 4. Maintain a local copy of the database in `src/` for | |
| 214 | easy access. | |
| 215 | 5. Export reports, images, HTML files, etc. to | |
| 216 | `public/`. | |
| 217 | ||
| 218 | Now, onto the differences between the code in a Jupyter Notebook and | |
| 219 | the actual Python files. To create the report in the Notebook | |
| 220 | snippet above, I created the following function inside | |
| 221 | `process.py`: | |
| 222 | ||
| 223 | ```python | |
| 224 | # Create summary pie chart | |
| 225 | def summary_data(accounts: pandas.DataFrame) -> None: | |
| 226 | accounts_01 = accounts[accounts["Owner"] == "Person01"] | |
| 227 | accounts_02 = accounts[accounts["Owner"] == "Person02"] | |
| 228 | for x in range(1, 4): | |
| 229 | if x == 1: | |
| 230 | df = accounts | |
| 231 | account_string = "All Accounts" | |
| 232 | elif x == 2: | |
| 233 | df = accounts_01 | |
| 234 | account_string = "Person01's Accounts" | |
| 235 | elif x == 3: | |
| 236 | df = accounts_02 | |
| 237 | account_string = "Person02's Accounts" | |
| 238 | print(f"Generating pie chart summary image for {account_string}...") | |
| 239 | summary_chart = ( | |
| 240 | df.groupby(["AccountType"]) | |
| 241 | .sum() | |
| 242 | .plot.pie( | |
| 243 | title=f"Credit Line by Type for {account_string}", | |
| 244 | y="CreditLine", | |
| 245 | autopct="%1.1f%%", | |
| 246 | ) | |
| 247 | ) | |
| 248 | summary_chart.figure.savefig(f"public/summary_chart_{x}.png", dpi=1200) | |
| 249 | ``` | |
| 250 | ||
| 251 | The result? A high-quality pie chart that is read directly by the | |
| 252 | `public/index.html` template I use. | |
| 253 | ||
| 254 |  | |
| 256 | ||
| 257 | Other charts generated by this project include: | |
| 258 | ||
| 259 | - Charts of account balances over time. | |
| 260 | - Line chart of effective tax rate (taxes divided by taxable | |
| 261 | income). | |
| 262 | - Salary projections and error limits using past income and | |
| 263 | inflation rates. | |
| 264 | - Multi-line chart of gross income, taxable income, and net | |
| 265 | income. | |
| 266 | ||
| 267 | The best thing about this project? I can improve it at any given | |
| 268 | time, shaping it into whatever helps me the most for that time. I | |
| 269 | imagine that I will be introducing an asset tracking table soon to | |
| 270 | track the depreciating value of cars, houses, etc. Who knows what's | |
| 271 | next? | |
content/blog/2022-03-08-plex-migration.md added +253
| @@ -0,0 +1,253 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-03-08 | |
| 3 | title = "Migrating Plex to a New Server (& Nvidia Transcoding)" | |
| 4 | description = "A retrospective on migrating Plex between servers and enabling Nvidia GPU transcoding." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Migration Phases | |
| 8 | ||
| 9 | I recently decided to migrate my server from an old OptiPlex desktop | |
| 10 | machine to a custom-built tower with better hardware in every category. | |
| 11 | In order to do this, I would need to properly migrate a full Plex | |
| 12 | installation. | |
| 13 | ||
| 14 | The second part of this migration is that the new server uses an Nvidia | |
| 15 | GPU and does not have any integrated graphics, which requires extra work | |
| 16 | for installation, but provides much better hardware transcoding options | |
| 17 | for Plex. | |
| 18 | ||
| 19 | Therefore, I have broken this migration down into three phases: | |
| 20 | ||
| 21 | 1. [Configure the New Server](#phase-1-configure-the-new-server) | |
| 22 | 2. [Migrate Plex Data & Devices](#phase-2-migrate-plex-data-devices) | |
| 23 | 3. [Configure GPU Transcoding](#phase-3-configure-gpu-transcoding) | |
| 24 | ||
| 25 | ------------------------------------------------------------------------ | |
| 26 | ||
| 27 | ## Phase 1: Configure the New Server | |
| 28 | ||
| 29 | ### Choosing an OS | |
| 30 | ||
| 31 | In order to migrate Plex to my new server, I first needed to choose an | |
| 32 | appropriate operating system (OS) and install it on the machine. Given | |
| 33 | that I have encountered numerous issues installing other Linux | |
| 34 | distributions properly with Nvidia graphics, I chose [Ubuntu | |
| 35 | Server](https://ubuntu.com/download/server). | |
| 36 | ||
| 37 | The first step is to create a bootable USB with Ubuntu Server. This is | |
| 38 | easy with [Etcher](https://www.balena.io/etcher/), an app that runs on | |
| 39 | many different platforms. Just download the Ubuntu Server | |
| 40 | `.iso` image, launch Etcher, and install the | |
| 41 | `.iso` on the USB. | |
| 42 | ||
| 43 | Once the USB is created, insert it into my server, reboot, and click | |
| 44 | `Esc` (or any of the `F1-12` keys) until the BIOS | |
| 45 | menu appears. Finally, launch the USB boot drive. | |
| 46 | ||
| 47 | ### Booting with Nvidia | |
| 48 | ||
| 49 | In order to install Ubuntu Server with an Nvidia Graphics card (and no | |
| 50 | integrated graphics on this device for some reason), you'll have to | |
| 51 | configure the boot menu to allow different graphics drivers to be | |
| 52 | loaded. | |
| 53 | ||
| 54 | When booting from the USB, the machine will launch the initial | |
| 55 | installation menu. From this menu, type `e` to view the | |
| 56 | default command options that come with the device - it's a good idea to | |
| 57 | take a photo of this screen, so you can enter these commands on the next | |
| 58 | screen (along with adding support for Nvidia). | |
| 59 | ||
| 60 | Finally, type `Ctrl + C` to enter the command line. From this | |
| 61 | command line, enter the commands found on the `e` screen. | |
| 62 | \*Remember to add `nomodeset` to the `linux ...` | |
| 63 | line so that your Nvidia device will display the installation screens | |
| 64 | properly!\* | |
| 65 | ||
| 66 | Here's an example of the commands I pulled from the `e` | |
| 67 | screen and entered on the command line. | |
| 68 | ||
| 69 | ```sh | |
| 70 | setparams 'Install Ubuntu Server' | |
| 71 | setgfxpayload=keep | |
| 72 | linux /casper/vmlinuz quiet nomodeset --- | |
| 73 | initrd /casper/initrd | |
| 74 | boot | |
| 75 | ``` | |
| 76 | ||
| 77 |  | |
| 79 | ||
| 80 |  | |
| 82 | ||
| 83 | Once the machine is rebooted, enter the `e` screen again and | |
| 84 | add `nomodeset` to the `linux ...` line again and | |
| 85 | press `Ctrl + X` to save the boot options. | |
| 86 | ||
| 87 | The machine is now fully installed and can properly display on an | |
| 88 | external display using the Nvidia GPU. | |
| 89 | ||
| 90 | Always remember to update and upgrade on a new installation: | |
| 91 | ||
| 92 | ```sh | |
| 93 | sudo apt update; sudo apt upgrade -y; sudo apt autoremove -y | |
| 94 | ``` | |
| 95 | ||
| 96 | ------------------------------------------------------------------------ | |
| 97 | ||
| 98 | ## Phase 2: Migrate Plex Data & Devices | |
| 99 | ||
| 100 | This phase uses the great Plex article on migrations ([Move an | |
| 101 | Installation to Another | |
| 102 | System](https://support.plex.tv/articles/201370363-move-an-install-to-another-system/)) | |
| 103 | and adds a bit more information to help with commands and context. | |
| 104 | ||
| 105 | ### Terminology | |
| 106 | ||
| 107 | **Source:** The original server that is being replaced.\ | |
| 108 | **Destination:** The new server.\ | |
| 109 | **Client:** Any application that can be used to modify settings for both | |
| 110 | source/destination. | |
| 111 | ||
| 112 | ### Step 01: [Client] Update Settings | |
| 113 | ||
| 114 | Open up a Plex app and *disable* the `Account` \> | |
| 115 | `Library` \> | |
| 116 | `Empty trash automatically after every scan` preference for | |
| 117 | the source server. | |
| 118 | ||
| 119 | ### Step 02: [Destination] Install Plex | |
| 120 | ||
| 121 | Open up the [Plex Media Server download | |
| 122 | page](https://www.plex.tv/media-server-downloads/) and copy the link for | |
| 123 | the appropriate platform. | |
| 124 | ||
| 125 | Execute the following commands on the destination server to install | |
| 126 | Plex: | |
| 127 | ||
| 128 | ```sh | |
| 129 | wget <url> | |
| 130 | sudo dpkg -i <filename> | |
| 131 | sudo systemctl stop plexmediaserver.service | |
| 132 | ``` | |
| 133 | ||
| 134 | ### Step 03: [Source] Stop Plex & Migrate Data | |
| 135 | ||
| 136 | First, stop the Plex service so that no data is created or modified | |
| 137 | during the migration. | |
| 138 | ||
| 139 | ```sh | |
| 140 | sudo systemctl stop plexmediaserver.service | |
| 141 | ``` | |
| 142 | ||
| 143 | Next, copy the data to the new server. To find where the Plex data | |
| 144 | directory is located, Plex has another excellent article available: | |
| 145 | [Where is the Plex Media Server data directory | |
| 146 | located?](https://support.plex.tv/articles/202915258-where-is-the-plex-media-server-data-directory-located/). | |
| 147 | ||
| 148 | There are many ways to copy the data to the new server and will largely | |
| 149 | depend on the size of the folder being copied. Personally, my data | |
| 150 | folder was \~23GB and I opted to simply use the `scp` command | |
| 151 | to copy the files over SSH. | |
| 152 | ||
| 153 | This process was throttled by the old server's slow HDD and ports and | |
| 154 | took approximately 90 minutes to complete. In comparison, moving the | |
| 155 | data from the new server's `home/user/` directory to the | |
| 156 | `/var/.../Plex Media Server` directory took 2-3 minutes. | |
| 157 | ||
| 158 | ```sh | |
| 159 | scp -r "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server" your_user@xxx.xxx.xxx.xxx:"'/path/to/destination/'" | |
| 160 | ``` | |
| 161 | ||
| 162 | ### Step 04: [Destination] Update File Permissions | |
| 163 | ||
| 164 | In case you move the data directory to a common area on the new server, | |
| 165 | it will have to be moved to the proper location before Plex can function | |
| 166 | properly: | |
| 167 | ||
| 168 | ```sh | |
| 169 | mv "Plex Media Server" /var/lib/plexmediaserver/Library/Application Support/ | |
| 170 | ``` | |
| 171 | ||
| 172 | To ensure permissions were retained properly, the server will need to | |
| 173 | show that all files and folders in the data directory are owned by | |
| 174 | `plex:plex` (or whichever user is running the Plex | |
| 175 | application). | |
| 176 | ||
| 177 | ```sh | |
| 178 | sudo chown -R plex:plex "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server" | |
| 179 | ``` | |
| 180 | ||
| 181 | Finally, start the service and check the status. | |
| 182 | ||
| 183 | ```sh | |
| 184 | sudo systemctl start plexmediaserver.service | |
| 185 | sudo systemctl status plexmediaserver.service | |
| 186 | ``` | |
| 187 | ||
| 188 | ### Step 05: [Client] Update Libraries & Metadata | |
| 189 | ||
| 190 | The first step - now that the new server is up and running - is to sign | |
| 191 | out of the client and sign back in. Once this is done, update any | |
| 192 | library locations, if necessary. This was unnecessary in my case since I | |
| 193 | simply moved my storage drives from the source server to the destination | |
| 194 | server. | |
| 195 | ||
| 196 | Next, perform the following actions in the client: | |
| 197 | ||
| 198 | 1. On the left sidebar, click `More` \> Three-Dot Menu \> | |
| 199 | `Scan Library Files` | |
| 200 | 2. *Enable* the `Account` \> `Library` \> | |
| 201 | `Empty trash automatically after every scan` preference | |
| 202 | for the source server. | |
| 203 | 3. On the left sidebar, click `More` \> Three-Dot Menu \> | |
| 204 | `Manage Server` \> `Empty Trash` | |
| 205 | 4. On the left sidebar, click `More` \> Three-Dot Menu \> | |
| 206 | `Manage Server` \> `Clean Bundles` | |
| 207 | 5. On the left sidebar, click `More` \> Three-Dot Menu \> | |
| 208 | `Manage Server` \> `Optimize Database` | |
| 209 | ||
| 210 | Finally, double-check the Remote Access settings to make sure no changes | |
| 211 | have caused issues with accessing the server from outside the network. | |
| 212 | ||
| 213 | In my case, I use a single port forwarding rule in my router and needed | |
| 214 | to update the Local LAN IP Address to the new server IP address. | |
| 215 | ||
| 216 |  | |
| 218 | ||
| 219 | ------------------------------------------------------------------------ | |
| 220 | ||
| 221 | ## Phase 3: Configure GPU Transcoding | |
| 222 | ||
| 223 | The final piece to the migration is enabling hardware transcoding so | |
| 224 | that Plex can fully utilize the new Nvidia GPU available in the server. | |
| 225 | The first step is to install Nvidia graphics drivers. This process may | |
| 226 | take a few minutes, but the commands are pretty simple: | |
| 227 | ||
| 228 | ```sh | |
| 229 | sudo add-apt-repository ppa:graphics-drivers/ppa | |
| 230 | sudo apt update | |
| 231 | sudo apt-get install ubuntu-drivers-common | |
| 232 | sudo ubuntu-drivers autoinstall | |
| 233 | ``` | |
| 234 | ||
| 235 | Finally, reboot so that the changes are loaded: | |
| 236 | ||
| 237 | ```sh | |
| 238 | sudo reboot now | |
| 239 | ``` | |
| 240 | ||
| 241 | To ensure that the Nvidia graphics drivers are working properly, run the | |
| 242 | following command to view the available GPUs, statistics, and processes: | |
| 243 | ||
| 244 | ```sh | |
| 245 | sudo nvidia-smi | |
| 246 | ``` | |
| 247 | ||
| 248 |  | |
| 249 | ||
| 250 | Finally, enable hardware transcoding settings in the Plex application: | |
| 251 | ||
| 252 |  | |
blog/2022-03-23-cloudflare-dns-api.org → content/blog/2022-03-23-cloudflare-dns-api.md renamed +50 −46
| @@ -1,40 +1,41 @@ | ||
| 1 | #+title: Dynamic DNS with Cloudflare API | |
| 2 | #+date: 2022-03-23 | |
| 3 | ||
| 4 | ** DDNS: Dynamic DNS | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: ddns-dynamic-dns | |
| 7 | :END: | |
| 8 | If you're hosting a service from a location with dynamic DNS (where your | |
| 9 | IP may change at any time), you must have a solution to update the DNS | |
| 10 | so that you can access your service even when the IP of the server | |
| 1 | +++ | |
| 2 | date = 2022-03-23 | |
| 3 | title = "Dynamic DNS with Cloudflare API" | |
| 4 | description = "Learn how to dynamically update DNS records for changing IPs with Cloudflare." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## DDNS: Dynamic DNS | |
| 8 | ||
| 9 | If you're hosting a service from a location with dynamic DNS (where | |
| 10 | your IP may change at any time), you must have a solution to update the | |
| 11 | DNS so that you can access your service even when the IP of the server | |
| 11 | 12 | changes. |
| 12 | 13 | |
| 13 | The process below uses the [[https://api.cloudflare.com/][Cloudflare | |
| 14 | API]] to update DNS =A= and =AAAA= records with the server's current IP. | |
| 15 | If you use another DNS provider, you will have to find a way to update | |
| 16 | your DNS (or find a way to get a static IP). | |
| 14 | The process below uses the [Cloudflare API](https://api.cloudflare.com/) | |
| 15 | to update DNS `A` and `AAAA` records with the | |
| 16 | server's current IP. If you use another DNS provider, you will have to | |
| 17 | find a way to update your DNS (or find a way to get a static IP). | |
| 17 | 18 | |
| 18 | First, install =jq= since we will use it in the next script: | |
| 19 | First, install `jq` since we will use it in the next script: | |
| 19 | 20 | |
| 20 | #+begin_src sh | |
| 21 | ```sh | |
| 21 | 22 | sudo apt install jq |
| 22 | #+end_src | |
| 23 | ``` | |
| 23 | 24 | |
| 24 | 25 | Next, create a location for your DDNS update scripts and open the first |
| 25 | 26 | script: |
| 26 | 27 | |
| 27 | #+begin_src sh | |
| 28 | ```sh | |
| 28 | 29 | mkdir ~/ddns |
| 29 | 30 | nano ~/ddns/update.sh |
| 30 | #+end_src | |
| 31 | ``` | |
| 31 | 32 | |
| 32 | The following =update.sh= script will take all of your domains and | |
| 33 | subdomains and check Cloudflare to see if the current =A= and =AAAA= | |
| 34 | records match your server's IP address. If not, it will update the | |
| 35 | records. | |
| 33 | The following `update.sh` script will take all of your | |
| 34 | domains and subdomains and check Cloudflare to see if the current | |
| 35 | `A` and `AAAA` records match your server's IP | |
| 36 | address. If not, it will update the records. | |
| 36 | 37 | |
| 37 | #+begin_src sh | |
| 38 | ```sh | |
| 38 | 39 | # file: update.sh |
| 39 | 40 | #!/bin/bash |
| 40 | 41 | |
| @@ -56,20 +57,22 @@ do | ||
| 56 | 57 | echo -e "\nUpdating $subdomain..." |
| 57 | 58 | zone_name=$domain dns_record=$subdomain /home/<your-username>/ddns/ddns.sh |
| 58 | 59 | done |
| 59 | #+end_src | |
| 60 | ``` | |
| 60 | 61 | |
| 61 | Next, open up the =ddns.sh= script. Paste the following into the script | |
| 62 | and update the =api_token= and =email= variables. | |
| 62 | Next, open up the `ddns.sh` script. Paste the following into | |
| 63 | the script and update the `api_token` and `email` | |
| 64 | variables. | |
| 63 | 65 | |
| 64 | #+begin_src sh | |
| 66 | ```sh | |
| 65 | 67 | nano ~/ddns/ddns.sh |
| 66 | #+end_src | |
| 68 | ``` | |
| 67 | 69 | |
| 68 | :warning: *Note*: If you want your DNS records to be proxied through | |
| 69 | Cloudflare, find and update the following snippet: =\"proxied\":false}"= | |
| 70 | to say =true= instead of =false=. | |
| 70 | :warning: **Note**: If you want your DNS records to be proxied through | |
| 71 | Cloudflare, find and update the following snippet: | |
| 72 | `"proxied":false}"` to say `true` instead of | |
| 73 | `false`. | |
| 71 | 74 | |
| 72 | #+begin_src sh | |
| 75 | ```sh | |
| 73 | 76 | # file: ddns.sh |
| 74 | 77 | #!/bin/bash |
| 75 | 78 | # based on https://gist.github.com/Tras2/cba88201b17d765ec065ccbedfb16d9a |
| @@ -121,7 +124,7 @@ then | ||
| 121 | 124 | -H "Content-Type: application/json" \ |
| 122 | 125 | -H "X-Auth-Email: $email" \ |
| 123 | 126 | -H "Authorization: Bearer $api_token" \ |
| 124 | --data "{\"type\":\"A\",\"name\":\"$dns_record\",\"content\":\"$ipv4\",\"ttl\":1,\"proxied\":false}" \ | |
| 127 | --data "{"type":"A","name":"$dns_record","content":"$ipv4","ttl":1,"proxied":false}" \ | |
| 125 | 128 | | jq -r '.errors' |
| 126 | 129 | else |
| 127 | 130 | echo "The current IPv4 and DNS record IPv4 are the same." |
| @@ -148,7 +151,7 @@ then | ||
| 148 | 151 | -H "Content-Type: application/json" \ |
| 149 | 152 | -H "X-Auth-Email: $email" \ |
| 150 | 153 | -H "Authorization: Bearer $api_token" \ |
| 151 | --data "{\"type\":\"AAAA\",\"name\":\"$dns_record\",\"content\":\"$ipv6\",\"ttl\":1,\"proxied\":false}" \ | |
| 154 | --data "{"type":"AAAA","name":"$dns_record","content":"$ipv6","ttl":1,"proxied":false}" \ | |
| 152 | 155 | | jq -r '.errors' |
| 153 | 156 | else |
| 154 | 157 | echo "The current IPv6 and DNS record IPv6 are the same." |
| @@ -162,30 +165,31 @@ then | ||
| 162 | 165 | else |
| 163 | 166 | echo "There is a problem with either the email or the password" |
| 164 | 167 | fi |
| 165 | #+end_src | |
| 168 | ``` | |
| 166 | 169 | |
| 167 | 170 | Once the script is saved and closed, make the scripts executable: |
| 168 | 171 | |
| 169 | #+begin_src sh | |
| 172 | ```sh | |
| 170 | 173 | chmod +x ~/ddns/ddns.sh |
| 171 | 174 | chmod +x ~/ddns/update.sh |
| 172 | #+end_src | |
| 175 | ``` | |
| 173 | 176 | |
| 174 | 177 | You can test the script by running it manually: |
| 175 | 178 | |
| 176 | #+begin_src sh | |
| 179 | ```sh | |
| 177 | 180 | ./update.sh |
| 178 | #+end_src | |
| 181 | ``` | |
| 179 | 182 | |
| 180 | To make sure the scripts run automatically, add it to the =cron= file so | |
| 181 | that it will run on a schedule. To do this, open the cron file: | |
| 183 | To make sure the scripts run automatically, add it to the | |
| 184 | `cron` file so that it will run on a schedule. To do this, | |
| 185 | open the cron file: | |
| 182 | 186 | |
| 183 | #+begin_src sh | |
| 187 | ```sh | |
| 184 | 188 | crontab -e |
| 185 | #+end_src | |
| 189 | ``` | |
| 186 | 190 | |
| 187 | 191 | In the cron file, paste the following at the bottom of the editor: |
| 188 | 192 | |
| 189 | #+begin_src sh | |
| 193 | ```sh | |
| 190 | 194 | */5 * * * * bash /home/<your_username>/ddns/update.sh |
| 191 | #+end_src | |
| 195 | ``` | |
blog/2022-03-23-nextcloud-on-ubuntu.org → content/blog/2022-03-23-nextcloud-on-ubuntu.md renamed +68 −80
| @@ -1,90 +1,83 @@ | ||
| 1 | #+title: Installing Nextcloud on Ubuntu | |
| 2 | #+date: 2022-03-23 | |
| 3 | ||
| 4 | ** What is Nextcloud? | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: what-is-nextcloud | |
| 7 | :END: | |
| 8 | [[https://nextcloud.com/][Nextcloud]] is a self-hosted solution for | |
| 1 | +++ | |
| 2 | date = 2022-03-23 | |
| 3 | title = "Installing Nextcloud on Ubuntu" | |
| 4 | description = "A guide to self-hosting the NextCloud application on your own server." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## What is Nextcloud? | |
| 8 | ||
| 9 | [Nextcloud](https://nextcloud.com/) is a self-hosted solution for | |
| 9 | 10 | storage, communications, editing, calendar, contacts, and more. |
| 10 | 11 | |
| 11 | 12 | This tutorial assumes that you have an Ubuntu server and a domain name |
| 12 | 13 | configured to point toward the server. |
| 13 | 14 | |
| 14 | ** Install Dependencies | |
| 15 | :PROPERTIES: | |
| 16 | :CUSTOM_ID: install-dependencies | |
| 17 | :END: | |
| 15 | ## Install Dependencies | |
| 16 | ||
| 18 | 17 | To start, you will need to install the packages that Nextcloud requires: |
| 19 | 18 | |
| 20 | #+begin_src sh | |
| 19 | ```sh | |
| 21 | 20 | sudo apt install apache2 mariadb-server libapache2-mod-php7.4 |
| 22 | 21 | sudo apt install php7.4-gd php7.4-mysql php7.4-curl php7.4-mbstring php7.4-intl |
| 23 | 22 | sudo apt install php7.4-gmp php7.4-bcmath php-imagick php7.4-xml php7.4-zip |
| 24 | #+end_src | |
| 23 | ``` | |
| 25 | 24 | |
| 26 | ** Set Up MySQL | |
| 27 | :PROPERTIES: | |
| 28 | :CUSTOM_ID: set-up-mysql | |
| 29 | :END: | |
| 30 | Next, you will need to log in to MySQL as the =root= user of the | |
| 31 | machine. | |
| 25 | ## Set Up MySQL | |
| 32 | 26 | |
| 33 | #+begin_src sh | |
| 27 | Next, you will need to log in to MySQL as the `root` user of | |
| 28 | the machine. | |
| 29 | ||
| 30 | ```sh | |
| 34 | 31 | sudo mysql -uroot -p |
| 35 | #+end_src | |
| 32 | ``` | |
| 36 | 33 | |
| 37 | 34 | Once you've logged in, you must create a new user so that Nextcloud can |
| 38 | manage the database. You will also create a =nextcloud= database and | |
| 39 | assign privileges: | |
| 35 | manage the database. You will also create a `nextcloud` | |
| 36 | database and assign privileges: | |
| 40 | 37 | |
| 41 | #+begin_src sql | |
| 38 | ```sql | |
| 42 | 39 | CREATE USER 'username'@'localhost' IDENTIFIED BY 'password'; |
| 43 | 40 | CREATE DATABASE IF NOT EXISTS nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; |
| 44 | 41 | GRANT ALL PRIVILEGES ON nextcloud.* TO 'username'@'localhost'; |
| 45 | 42 | FLUSH PRIVILEGES; |
| 46 | 43 | quit; |
| 47 | #+end_src | |
| 44 | ``` | |
| 45 | ||
| 46 | ## Download & Install Nextcloud | |
| 48 | 47 | |
| 49 | ** Download & Install Nextcloud | |
| 50 | :PROPERTIES: | |
| 51 | :CUSTOM_ID: download-install-nextcloud | |
| 52 | :END: | |
| 53 | To download Nextcloud, go the | |
| 54 | [[https://nextcloud.com/install/#instructions-server][Nextcloud | |
| 55 | downloads page]], click on =Archive File= and right-click the big blue | |
| 56 | button to copy the link. | |
| 48 | To download Nextcloud, go the [Nextcloud downloads | |
| 49 | page](https://nextcloud.com/install/#instructions-server), click on | |
| 50 | `Archive File` and right-click the big blue button to copy | |
| 51 | the link. | |
| 57 | 52 | |
| 58 | 53 | Then, go to your server and enter the following commands to download, |
| 59 | 54 | unzip, and move the files to your destination directory. This example |
| 60 | uses =example.com= as the destination, but you can put it wherever you | |
| 61 | want to server your files from. | |
| 55 | uses `example.com` as the destination, but you can put it | |
| 56 | wherever you want to server your files from. | |
| 62 | 57 | |
| 63 | #+begin_src sh | |
| 58 | ```sh | |
| 64 | 59 | wget https://download.nextcloud.com/server/releases/nextcloud-23.0.3.zip |
| 65 | 60 | sudo apt install unzip |
| 66 | 61 | unzip nextcloud-23.0.3.zip |
| 67 | 62 | sudo cp -r nextcloud /var/www/example.com |
| 68 | #+end_src | |
| 63 | ``` | |
| 64 | ||
| 65 | ## Configure the Apache Web Server | |
| 69 | 66 | |
| 70 | ** Configure the Apache Web Server | |
| 71 | :PROPERTIES: | |
| 72 | :CUSTOM_ID: configure-the-apache-web-server | |
| 73 | :END: | |
| 74 | 67 | Now that the database is set up and Nextcloud is installed, you need to |
| 75 | 68 | set up the Apache configuration files to tell the server how to handle |
| 76 | requests for =example.com/nextcloud=. | |
| 69 | requests for `example.com/nextcloud`. | |
| 77 | 70 | |
| 78 | 71 | First, open the following file in the editor: |
| 79 | 72 | |
| 80 | #+begin_src sh | |
| 73 | ```sh | |
| 81 | 74 | sudo nano /etc/apache2/sites-available/nextcloud.conf |
| 82 | #+end_src | |
| 75 | ``` | |
| 83 | 76 | |
| 84 | 77 | Once the editor is open, paste the following information in. Then, save |
| 85 | 78 | and close the file. |
| 86 | 79 | |
| 87 | #+begin_src config | |
| 80 | ```config | |
| 88 | 81 | <VirtualHost *:80> |
| 89 | 82 | DocumentRoot /var/www/example.com |
| 90 | 83 | ServerName example.com |
| @@ -103,79 +96,74 @@ and close the file. | ||
| 103 | 96 | </IfModule> |
| 104 | 97 | </Directory> |
| 105 | 98 | </VirtualHost> |
| 106 | #+end_src | |
| 99 | ``` | |
| 107 | 100 | |
| 108 | 101 | Once the file is saved, enable it with Apache: |
| 109 | 102 | |
| 110 | #+begin_src sh | |
| 103 | ```sh | |
| 111 | 104 | sudo a2ensite nextcloud.conf |
| 112 | #+end_src | |
| 105 | ``` | |
| 113 | 106 | |
| 114 | 107 | Next, enable the Apache mods required by Nextcloud: |
| 115 | 108 | |
| 116 | #+begin_src sh | |
| 109 | ```sh | |
| 117 | 110 | sudo a2enmod rewrite headers env dir mime |
| 118 | #+end_src | |
| 111 | ``` | |
| 119 | 112 | |
| 120 | 113 | Finally, restart Apache. If any errors arise, you must solve those |
| 121 | 114 | before continuing. |
| 122 | 115 | |
| 123 | #+begin_src sh | |
| 116 | ```sh | |
| 124 | 117 | sudo systemctl restart apache2 |
| 125 | #+end_src | |
| 118 | ``` | |
| 126 | 119 | |
| 127 | 120 | For the app to work, you must have the correct file permissions on your |
| 128 | =nextcloud= directory. Set the owner to be =www-data=: | |
| 121 | `nextcloud` directory. Set the owner to be | |
| 122 | `www-data`: | |
| 129 | 123 | |
| 130 | #+begin_src sh | |
| 124 | ```sh | |
| 131 | 125 | sudo chown -R www-data:www-data /var/www/example.com/nextcloud/ |
| 132 | #+end_src | |
| 126 | ``` | |
| 127 | ||
| 128 | ## DNS | |
| 133 | 129 | |
| 134 | ** DNS | |
| 135 | :PROPERTIES: | |
| 136 | :CUSTOM_ID: dns | |
| 137 | :END: | |
| 138 | 130 | If you do not have a static IP address, you will need to update your DNS |
| 139 | 131 | settings (at your DNS provider) whenever your dynamic IP address |
| 140 | 132 | changes. |
| 141 | 133 | |
| 142 | 134 | For an example on how I do that with Cloudflare, see my other post: |
| 143 | [[/blog/updating-dynamic-dns-with-cloudflare-api/][Updating Dynamic DNS | |
| 144 | with Cloudflare API]] | |
| 135 | [Updating Dynamic DNS with Cloudflare | |
| 136 | API](/blog/updating-dynamic-dns-with-cloudflare-api/) | |
| 137 | ||
| 138 | ## Certbot | |
| 145 | 139 | |
| 146 | ** Certbot | |
| 147 | :PROPERTIES: | |
| 148 | :CUSTOM_ID: certbot | |
| 149 | :END: | |
| 150 | 140 | If you want to serve Nextcloud from HTTPS rather than plain HTTP, use |
| 151 | 141 | the following commands to issue Let's Encrypt SSL certificates: |
| 152 | 142 | |
| 153 | #+begin_src sh | |
| 143 | ```sh | |
| 154 | 144 | sudo apt install snapd |
| 155 | 145 | sudo snap install core |
| 156 | 146 | sudo snap refresh core |
| 157 | 147 | sudo snap install --classic certbot |
| 158 | 148 | sudo ln -s /snap/bin/certbot /usr/bin/certbot |
| 159 | 149 | sudo certbot --apache |
| 160 | #+end_src | |
| 150 | ``` | |
| 151 | ||
| 152 | ## Results | |
| 161 | 153 | |
| 162 | ** Results | |
| 163 | :PROPERTIES: | |
| 164 | :CUSTOM_ID: results | |
| 165 | :END: | |
| 166 | 154 | Voilà! You're all done and should be able to access Nextcloud from your |
| 167 | 155 | domain or IP address. |
| 168 | 156 | |
| 169 | 157 | See the screenshots below for the dashboard and a settings page on my |
| 170 | instance of Nextcloud, using the =Breeze Dark= theme I installed from | |
| 171 | the Apps page. | |
| 158 | instance of Nextcloud, using the `Breeze Dark` theme I | |
| 159 | installed from the Apps page. | |
| 172 | 160 | |
| 173 | #+caption: Nextcloud Dashboard | |
| 174 | [[https://img.cleberg.net/blog/20220323-installing-nextcloud-on-ubuntu/nextcloud_dashboard.png]] | |
| 161 |  | |
| 175 | 163 | |
| 176 | /Figure 01: Nextcloud Dashboard/ | |
| 164 | *Figure 01: Nextcloud Dashboard* | |
| 177 | 165 | |
| 178 | #+caption: Nextcloud Settings | |
| 179 | [[https://img.cleberg.net/blog/20220323-installing-nextcloud-on-ubuntu/nextcloud_settings.png]] | |
| 166 |  | |
| 180 | 168 | |
| 181 | /Figure 02: Nextcloud Security Settings/ | |
| 169 | *Figure 02: Nextcloud Security Settings* | |
content/blog/2022-03-24-server-hardening.md added +353
| @@ -0,0 +1,353 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-03-24 | |
| 3 | title = "Hardening a Public-Facing Home Server" | |
| 4 | description = "Learn some basic techniques to harden a home server and network." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Post Updates | |
| 8 | ||
| 9 | > After reviewing this post today (2022-10-04), I noticed quite a few | |
| 10 | > gaps in my write-up and wanted to add a few things, even though this | |
| 11 | > blog is really just a retrospective and knowledge dump for myself. I | |
| 12 | > left things intact and simply crossed them out (~~like this~~) for | |
| 13 | > posterity. | |
| 14 | ||
| 15 | ## Planning Data Flows & Security | |
| 16 | ||
| 17 | ### My Personal Data Flow | |
| 18 | ||
| 19 | ```txt | |
| 20 | ┌───────┐ ┌─────────────────┐ | |
| 21 | ┌──► VLAN1 ├───► Private Devices │ | |
| 22 | │ └───────┘ └─────────────────┘ | |
| 23 | ┌──────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ │ | |
| 24 | │ Internet ├───► Router ├───► Firewall ├───► Switch ├──┤ | |
| 25 | └──────────┘ └────────┘ └──────────┘ └────────┘ │ | |
| 26 | │ ┌───────┐ ┌───────────────┐ | |
| 27 | └──► VLAN2 ├───► Public Server │ | |
| 28 | └───────┘ └───────────────┘ | |
| 29 | ``` | |
| 30 | ||
| 31 | ### Thought Process | |
| 32 | ||
| 33 | To serve content from your home server and harden your security posture, | |
| 34 | you have to think about the transport of data from `server` | |
| 35 | to `client`. | |
| 36 | ||
| 37 | Let's start with the actual server itself. Think about the following: | |
| 38 | ||
| 39 | - Do I have a firewall enabled? Do I need to update this to allow new | |
| 40 | ports or IPs? | |
| 41 | - Do I have an IPS/IDS that may prevent outside traffic? | |
| 42 | - Do I have any other security software installed? | |
| 43 | - Are the services hosted inside Docker containers, behind a reverse | |
| 44 | proxy, or virtualized? If so, are they configured to allow outside | |
| 45 | traffic? | |
| 46 | ||
| 47 | Once the data leaves the server, where does it go? In my case, it goes | |
| 48 | to a managed switch. In this case, I asked the following: | |
| 49 | ||
| 50 | - What configurations is the switch using? | |
| 51 | - Am I using VLANs? | |
| 52 | - Yes, I am using 802.1Q VLANs. | |
| 53 | - Are the VLANs configured properly? | |
| 54 | - Yes, as shown in the [Switch](#switch) section below, I have a | |
| 55 | separate VLAN to allow outside traffic to and from the server | |
| 56 | alone. No other devices, except for a service port, and in that | |
| 57 | VLAN. | |
| 58 | ||
| 59 | At this point, the data has been processed through the switch. Where | |
| 60 | does it go next? In my case, it's pretty simple: it goes to the | |
| 61 | router/modem device. | |
| 62 | ||
| 63 | - Does my ISP block any ports that I need? | |
| 64 | - This is an important step that a lot of people run into when | |
| 65 | self-hosting at home. Use an online port-checker tool for your | |
| 66 | IP or call your ISP if you think ports are blocked. | |
| 67 | - Is there a router firewall? | |
| 68 | - Yes, I checked that it's configured to allow the ports I need | |
| 69 | to run my services publicly. Common web servers and reverse | |
| 70 | proxies require ports 80 and 443, but other services like media | |
| 71 | servers or games can require unique ports, so be sure to check | |
| 72 | the documentation for your service(s). | |
| 73 | - Are there any other settings affecting inbound/outbound traffic? | |
| 74 | - Schedules or access blocks | |
| 75 | - Static Routing | |
| 76 | - QoS | |
| 77 | - Port Forwarding | |
| 78 | - DMZ Hosting | |
| 79 | - Remote Management (this can sometimes mess with services that | |
| 80 | also require the use of ports 80 and 443) | |
| 81 | ||
| 82 | Once the data leaves my router, it goes to the upstream ISP and can be | |
| 83 | accessed publicly. | |
| 84 | ||
| 85 | ## Server | |
| 86 | ||
| 87 | +The services I run on my server are installed straight into the OS, | |
| 88 | without any use of Docker or VMs, so I don't need any extra application | |
| 89 | configuration to make them accessible to the outside world.+ | |
| 90 | ||
| 91 | As of 2022-10-04, the paragraph above is no longer true as I now run a | |
| 92 | reverse proxy with Nginx and host many services inside Docker. However, | |
| 93 | it doesn't change anything regarding this post as I still just need to | |
| 94 | open ports 80 & 443 and create the necessary website configuration | |
| 95 | files. | |
| 96 | ||
| 97 | When creating new services - either installed directly on bare metal or | |
| 98 | within something like Docker - I ensure that I read through the | |
| 99 | documentation thoroughly to understand a few key things: - What network | |
| 100 | activities should this app perform (if any)? Using which ports and | |
| 101 | protocols? - Does this app require any commands/services to be run as | |
| 102 | `root`? - Does this app log errors, authentication | |
| 103 | failures/successes, or anything else that would be useful for an | |
| 104 | investigation? | |
| 105 | ||
| 106 | For extra security, I use limit all incoming connections to SSH | |
| 107 | connections through my server firewall (`ufw`) and disable | |
| 108 | common SSH settings. After all of that, I use `fail2ban` as a | |
| 109 | preventative measure against brute-force login attempts. | |
| 110 | ||
| 111 | As another piece of security, you can randomize your SSH port to ensure | |
| 112 | that random scanners or attackers can't easily try to force their way | |
| 113 | into your network. For example, you can edit the port rules in your | |
| 114 | server to block all connection requests to port `22` but | |
| 115 | forward all remote connections from port `12345` to your | |
| 116 | server's port `22`. Then you just need to SSH to your | |
| 117 | network via your randomized port. | |
| 118 | ||
| 119 | ### `ufw` | |
| 120 | ||
| 121 | To see how to configure `ufw`, see my other post: [Secure | |
| 122 | Your Network with the Uncomplicated | |
| 123 | Firewall](/blog/secure-your-network-with-the-uncomplicated-firewall/). | |
| 124 | ||
| 125 | The general notion with an on-device firewall is that you want to deny | |
| 126 | all incoming connections by default and then selectively open certain | |
| 127 | ports for services or users that you know need access. | |
| 128 | ||
| 129 | If you know that you will only be logging into this server from a | |
| 130 | certain set or list of IPs, you can always set the firewall to only | |
| 131 | allow connections to port 22 from those IPs. | |
| 132 | ||
| 133 | For a quick start to only allow SSH connections to the server, use this: | |
| 134 | ||
| 135 | ```sh | |
| 136 | sudo ufw default deny incoming | |
| 137 | sudo ufw default allow outgoing | |
| 138 | sudo ufw allow 22 | |
| 139 | sudo ufw enable | |
| 140 | ``` | |
| 141 | ||
| 142 |  | |
| 143 | ||
| 144 | ### `ssh` | |
| 145 | ||
| 146 | 1. Using SSH Keys | |
| 147 | ||
| 148 | First, make sure you have an SSH keypair generated on the device(s) | |
| 149 | that you'll be using to log in to the server. If you don't have an | |
| 150 | SSH key, run this command: | |
| 151 | ||
| 152 | ```sh | |
| 153 | ssh-keygen | |
| 154 | ``` | |
| 155 | ||
| 156 | Now that we have an SSH key, copy it to the server with the | |
| 157 | following command, which will ask for the user's password before | |
| 158 | accepting the key: | |
| 159 | ||
| 160 | ```sh | |
| 161 | ssh-copy-id my_user@my_server | |
| 162 | ``` | |
| 163 | ||
| 164 | If you have multiple keys, you'll need to specify which to use. | |
| 165 | After it's complete, `ssh` back into the server as that | |
| 166 | user and make sure it doesn't ask for a password. | |
| 167 | ||
| 168 | 2. Disable Password & Root Authentication | |
| 169 | ||
| 170 | Now that we can access the server without a password, we will | |
| 171 | disable password authentication and disable anyone from using | |
| 172 | `ssh` to login as `root`. | |
| 173 | ||
| 174 | To do this, open the `sshd_config` file: | |
| 175 | ||
| 176 | ```sh | |
| 177 | sudo nano /etc/ssh/sshd_config | |
| 178 | ``` | |
| 179 | ||
| 180 | You'll need to update the parameters to the values below. If one of | |
| 181 | these rules is commented-out or doesn't exist, create the rule at | |
| 182 | the bottom of the file. | |
| 183 | ||
| 184 | ```config | |
| 185 | PermitRootLogin no | |
| 186 | PasswordAuthentication no | |
| 187 | PubkeyAuthentication yes | |
| 188 | ``` | |
| 189 | ||
| 190 | Finally, restart the `ssh` service: | |
| 191 | ||
| 192 | ```sh | |
| 193 | sudo systemctl restart sshd.service | |
| 194 | ``` | |
| 195 | ||
| 196 | To test that everything's working so far, open ANOTHER terminal and | |
| 197 | try logging in as `root` over SSH. It is very important | |
| 198 | that you keep your current SSH session open and test with an | |
| 199 | additional session, or you will lock yourself out at some point and | |
| 200 | will need to use a recovery method (e.g., hooking monitor up to home | |
| 201 | server) to get yourself back in. | |
| 202 | ||
| 203 | 3. Enable MFA for `ssh` | |
| 204 | ||
| 205 | This part is optional, but I highly recommend it. So far, we've | |
| 206 | ensured that no one can log into our user on the server without | |
| 207 | using our secret key, and we've ensured that no one can log in | |
| 208 | remotely as `root`. Next, you can enable MFA | |
| 209 | authentication for `ssh` connections. | |
| 210 | ||
| 211 | This process involves editing a couple files and installing an MFA | |
| 212 | package, so I will not include all the details in this post. To see | |
| 213 | how to configure MFA for `ssh`, see my other post: | |
| 214 | [Enabling MFA for SSH](/blog/enable-totp-mfa-for-ssh/). | |
| 215 | ||
| 216 |  | |
| 218 | ||
| 219 | ### `fail2ban` | |
| 220 | ||
| 221 | I haven't written a post on how I use `fail2ban`, but it's | |
| 222 | quite simple. I use the default `sshd` jail, but you can | |
| 223 | always create new jails for respective applications or ports. For | |
| 224 | example, if you use Nginx as your web server, you can use the | |
| 225 | `nginx-http-auth` jail. | |
| 226 | ||
| 227 | In order to get it up and running, use the following commands: | |
| 228 | ||
| 229 | ```sh | |
| 230 | sudo apt install fail2ban | |
| 231 | sudo fail2ban-client start sshd | |
| 232 | sudo fail2ban-client status sshd | |
| 233 | ``` | |
| 234 | ||
| 235 | This should be used as a last-resort defense and shouldn't be a | |
| 236 | replacement for the security measures mentioned above. | |
| 237 | ||
| 238 |  | |
| 239 | ||
| 240 | ## Switch | |
| 241 | ||
| 242 | Between the router and any local devices is my managed switch, which is | |
| 243 | used to create VLANs. The example below shows how I would isolate the | |
| 244 | VLANs if I were starting to host a single service at home. | |
| 245 | ||
| 246 | ### 802.1Q VLAN Configuration | |
| 247 | ||
| 248 | In this configuration, port 8 is the public server that needs to be | |
| 249 | accessed from the outside. Port 23 is my 'dedicated service port' for | |
| 250 | this server. In order to SSH to this server, I need to plug my laptop | |
| 251 | into port 23 or else I cannot SSH. Otherwise, I'd need to hook up a | |
| 252 | monitor and keyboard directly to the server to manage it. | |
| 253 | ||
| 254 | VLAN ID VLAN Name Member Ports Tagged Ports Untagged Ports | |
| 255 | --------- ----------- -------------- -------------- ---------------- | |
| 256 | 1 Default 1-24 1-24 | |
| 257 | 2 Server 1,8,23 1,8,23 | |
| 258 | ||
| 259 | ### 802.1Q VLAN PVID Setting | |
| 260 | ||
| 261 | Once the VLAN is created, I simply add the `VLAN ID` of | |
| 262 | `2` as the `PVID` for any related ports (in this | |
| 263 | case, see that ports `8` and `23` have a PVID of | |
| 264 | `2`). | |
| 265 | ||
| 266 | Port PVID | |
| 267 | ------ ------ | |
| 268 | 1 1 | |
| 269 | 2 1 | |
| 270 | 3 1 | |
| 271 | 4 1 | |
| 272 | 5 1 | |
| 273 | 6 1 | |
| 274 | 7 1 | |
| 275 | 8 2 | |
| 276 | 9 1 | |
| 277 | 10 1 | |
| 278 | 11 1 | |
| 279 | 12 1 | |
| 280 | 13 1 | |
| 281 | 14 1 | |
| 282 | 15 1 | |
| 283 | 16 1 | |
| 284 | 17 1 | |
| 285 | 18 1 | |
| 286 | 19 1 | |
| 287 | 20 1 | |
| 288 | 21 1 | |
| 289 | 22 1 | |
| 290 | 23 2 | |
| 291 | 24 1 | |
| 292 | ||
| 293 | ## Router | |
| 294 | ||
| 295 | On my router, the configuration was as easy as opening the firewall | |
| 296 | settings and unblocking the ports I needed for my services (e.g., | |
| 297 | HTTP/S, Plex, SSH, MySQL, etc.). | |
| 298 | ||
| 299 | +Since I'm relying on an ISP-provided modem/router combo for now (not | |
| 300 | by choice), I do not use any other advanced settings on my router that | |
| 301 | would inhibit any valid traffic to these services.+ | |
| 302 | ||
| 303 | The paragraph above regarding the ISP-owned router is no longer accurate | |
| 304 | as I now use the Ubiquiti Unifi Dream Machine Pro as my router. Within | |
| 305 | this router, I enabled port forwarding/firewall rules, segregate the | |
| 306 | network based on the device, and enable traffic restrictions (e.g., | |
| 307 | silently drop traffic from certain countries and threat categories). | |
| 308 | ||
| 309 | If you have the option with your ISP, I recommend using a personal | |
| 310 | router with software that you are familiar with so that you can explore | |
| 311 | all the options available to you. | |
| 312 | ||
| 313 | ## Physical Security | |
| 314 | ||
| 315 | One large piece of self-hosting that people generally don't discuss | |
| 316 | online is physical security. However, physical security is very | |
| 317 | important for everyone who hosts a server like this. Exactly *how* | |
| 318 | important it is depends on the server use/purpose. | |
| 319 | ||
| 320 | If you self-host customer applications that hold protected data (HIPAA, | |
| 321 | GDPR, COPPA, etc.), then physical security is extremely important and | |
| 322 | cannot be ignored. If you simply host a blog and some hobby sites, then | |
| 323 | it's a relatively minor consideration, but one you still need to think | |
| 324 | about. | |
| 325 | ||
| 326 | ### Location | |
| 327 | ||
| 328 | The first consideration is quite simple: location. - Is the server | |
| 329 | within a property you own or housed on someone else's property? - Is it | |
| 330 | nearby (in your house, in your work office, in your neighbor's garage, | |
| 331 | in a storage unit, etc.)? - Do you have 24/7 access to the server? - Are | |
| 332 | there climate considerations, such as humidity, fires, tornadoes, | |
| 333 | monsoons? - Do you have emergency equipment nearby in case of emergency? | |
| 334 | ||
| 335 | ### Hardware Ownership | |
| 336 | ||
| 337 | Secondly, consider the hardware itself: - Do you own the server in its | |
| 338 | entirety? - Are any other users able to access the server, even if your | |
| 339 | data/space is segregated? - If you're utilizing a third party, do they | |
| 340 | have any documentation to show responsibility? This could be a SOC 1/2/3 | |
| 341 | report, ISO compliance report, internal security/safety documentation. | |
| 342 | ||
| 343 | ### Physical Controls | |
| 344 | ||
| 345 | Regardless of who owns the hardware, ensure that there are adequate | |
| 346 | safeguards in place, if necessary. These usually don't apply to small | |
| 347 | home servers and are usually covered already if you're utilizing a | |
| 348 | third party. | |
| 349 | ||
| 350 | These can include: - Server bezel locks - Server room locks - physical, | |
| 351 | digital, or biometric authentication - Security cameras - Raised | |
| 352 | floors/lowered ceilings with proper guards/gates in-place within the | |
| 353 | floors or ceilings - Security personnel - Log sheets and/or guest badges | |
blog/2022-03-26-ssh-mfa.org → content/blog/2022-03-26-ssh-mfa.md renamed +72 −82
| @@ -1,88 +1,83 @@ | ||
| 1 | #+title: Enable TOTP MFA for SSH | |
| 2 | #+date: 2022-03-26 | |
| 1 | +++ | |
| 2 | date = 2022-03-26 | |
| 3 | title = "Enable TOTP MFA for SSH" | |
| 4 | description = "Learn how to enable timed one-time passcodes for SSH." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Why Do I Need MFA for SSH? | |
| 3 | 8 | |
| 4 | ** Why Do I Need MFA for SSH? | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: why-do-i-need-mfa-for-ssh | |
| 7 | :END: | |
| 8 | 9 | If you are a sysadmin of a server anywhere (that includes at home!), you |
| 9 | 10 | may want an added layer of protection against intruders. This is not a |
| 10 | 11 | replacement for other security measures, such as: |
| 11 | 12 | |
| 12 | - Disable root SSH | |
| 13 | - Disable SSH password authentication | |
| 14 | - Allow only certain users to login via SSH | |
| 15 | - Allow SSH only from certain IPs | |
| 13 | - Disable root SSH | |
| 14 | - Disable SSH password authentication | |
| 15 | - Allow only certain users to login via SSH | |
| 16 | - Allow SSH only from certain IPs | |
| 16 | 17 | |
| 17 | 18 | However, MFA can be added as an additional security measure to ensure |
| 18 | 19 | that your server is protected. This is especially important if you need |
| 19 | 20 | to allow password authentication for SSH. |
| 20 | 21 | |
| 21 | 22 | For more guidance on server security measures, see my other post: |
| 22 | [[/blog/hardening-a-public-facing-home-server/][Hardening a | |
| 23 | Public-Facing Home Server]]. | |
| 23 | [Hardening a Public-Facing Home | |
| 24 | Server](/blog/hardening-a-public-facing-home-server/). | |
| 25 | ||
| 26 | ## Install MFA PAM Module | |
| 24 | 27 | |
| 25 | ** Install MFA PAM Module | |
| 26 | :PROPERTIES: | |
| 27 | :CUSTOM_ID: install-mfa-pam-module | |
| 28 | :END: | |
| 29 | 28 | PAM, which stands for Pluggable Authentication Module, is an |
| 30 | 29 | authentication infrastructure used on Linux systems to authenticate a |
| 31 | 30 | user. In order to use this technology, let's install the |
| 32 | =libpam-google-authenticator= package: | |
| 31 | `libpam-google-authenticator` package: | |
| 33 | 32 | |
| 34 | #+begin_src sh | |
| 33 | ```sh | |
| 35 | 34 | sudo apt-get update |
| 36 | #+end_src | |
| 35 | ``` | |
| 37 | 36 | |
| 38 | #+begin_src sh | |
| 37 | ```sh | |
| 39 | 38 | sudo apt-get install libpam-google-authenticator |
| 40 | #+end_src | |
| 41 | ||
| 42 | ** Initialize the PAM Module | |
| 43 | :PROPERTIES: | |
| 44 | :CUSTOM_ID: initialize-the-pam-module | |
| 45 | :END: | |
| 46 | *** Interactive Method | |
| 47 | :PROPERTIES: | |
| 48 | :CUSTOM_ID: interactive-method | |
| 49 | :END: | |
| 39 | ``` | |
| 40 | ||
| 41 | ## Initialize the PAM Module | |
| 42 | ||
| 43 | ### Interactive Method | |
| 44 | ||
| 50 | 45 | Once the package is installed, initialize it and following the |
| 51 | 46 | interactive prompts to generate your OTP or TOTP: |
| 52 | 47 | |
| 53 | #+begin_src sh | |
| 48 | ```sh | |
| 54 | 49 | google-authenticator |
| 55 | #+end_src | |
| 50 | ``` | |
| 56 | 51 | |
| 57 | 52 | If you are not sure how to answer, read the prompts carefully and think |
| 58 | 53 | about having to how each situation would affect your normal login |
| 59 | 54 | attempts. If you are still not sure, use my default responses below. |
| 60 | 55 | |
| 61 | #+begin_src txt | |
| 56 | ```txt | |
| 62 | 57 | OUTPUT |
| 63 | 58 | |
| 64 | 59 | Do you want authentication tokens to be time-based (y/n) y |
| 65 | #+end_src | |
| 60 | ``` | |
| 66 | 61 | |
| 67 | 62 | At this point, use an authenticator app somewhere one of your devices to |
| 68 | 63 | scan the QR code. Any future login attempts after our upcoming |
| 69 | 64 | configuration changes will require that TOTP. |
| 70 | 65 | |
| 71 | #+begin_src txt | |
| 66 | ```txt | |
| 72 | 67 | OUTPUT |
| 73 | 68 | |
| 74 | 69 | Do you want me to update your "/home/user/.google_authenticator" file? (y/n) y |
| 75 | #+end_src | |
| 70 | ``` | |
| 76 | 71 | |
| 77 | #+begin_src txt | |
| 72 | ```txt | |
| 78 | 73 | OUTPUT |
| 79 | 74 | |
| 80 | 75 | Do you want to disallow multiple uses of the same authentication |
| 81 | 76 | token? This restricts you to one login about every 30s, but it increases |
| 82 | 77 | your chances to notice or even prevent man-in-the-middle attacks (y/n) y |
| 83 | #+end_src | |
| 78 | ``` | |
| 84 | 79 | |
| 85 | #+begin_src txt | |
| 80 | ```txt | |
| 86 | 81 | OUTPUT |
| 87 | 82 | |
| 88 | 83 | By default, a new token is generated every 30 seconds by the mobile app. |
| @@ -95,32 +90,30 @@ code, the next code) to 17 permitted codes (the 8 previous codes, the current | ||
| 95 | 90 | code, and the 8 next codes). This will permit for a time skew of up to 4 minutes |
| 96 | 91 | between client and server. |
| 97 | 92 | Do you want to do so? (y/n) n |
| 98 | #+end_src | |
| 93 | ``` | |
| 99 | 94 | |
| 100 | #+begin_src txt | |
| 95 | ```txt | |
| 101 | 96 | OUTPUT |
| 102 | 97 | |
| 103 | 98 | If the computer that you are logging into isn't hardened against brute-force |
| 104 | 99 | login attempts, you can enable rate-limiting for the authentication module. |
| 105 | 100 | By default, this limits attackers to no more than 3 login attempts every 30s. |
| 106 | 101 | Do you want to enable rate-limiting? (y/n) y |
| 107 | #+end_src | |
| 102 | ``` | |
| 103 | ||
| 104 | ### Non-Interactive Method | |
| 108 | 105 | |
| 109 | *** Non-Interactive Method | |
| 110 | :PROPERTIES: | |
| 111 | :CUSTOM_ID: non-interactive-method | |
| 112 | :END: | |
| 113 | 106 | If you need to do this quickly, know your responses to the prompts, or |
| 114 | 107 | are setting this up for numerous users, the non-interactive method can |
| 115 | 108 | be much faster: |
| 116 | 109 | |
| 117 | #+begin_src sh | |
| 110 | ```sh | |
| 118 | 111 | google-authenticator -t -d -f -r 3 -R 30 -w 3 |
| 119 | #+end_src | |
| 112 | ``` | |
| 120 | 113 | |
| 121 | 114 | The options referenced above are as follows: |
| 122 | 115 | |
| 123 | #+begin_src txt | |
| 116 | ```txt | |
| 124 | 117 | google-authenticator [<options>] |
| 125 | 118 | -h, --help Print this message |
| 126 | 119 | -c, --counter-based Set up counter-based (HOTP) verification |
| @@ -140,69 +133,66 @@ google-authenticator [<options>] | ||
| 140 | 133 | -w, --window-size=W Set window of concurrently valid codes |
| 141 | 134 | -W, --minimal-window Disable window of concurrently valid codes |
| 142 | 135 | -e, --emergency-codes=N Number of emergency codes to generate |
| 143 | #+end_src | |
| 136 | ``` | |
| 144 | 137 | |
| 145 | 138 | This fully configures the authenticator, saves it to a file, and then |
| 146 | 139 | outputs the secret key, QR code, and recovery codes. (If you add the |
| 147 | flag =-q=, then there won't be any output). If you use this command in | |
| 148 | an automated fashion, make sure your script captures the secret key | |
| 149 | and/or recovery codes and makes them available to the user. | |
| 150 | ||
| 151 | ** PAM Configuration Settings | |
| 152 | :PROPERTIES: | |
| 153 | :CUSTOM_ID: pam-configuration-settings | |
| 154 | :END: | |
| 140 | flag `-q`, then there won't be any output). If you use this | |
| 141 | command in an automated fashion, make sure your script captures the | |
| 142 | secret key and/or recovery codes and makes them available to the user. | |
| 143 | ||
| 144 | ## PAM Configuration Settings | |
| 145 | ||
| 155 | 146 | Once you've enabled the T/OTP and have it saved to an MFA app on your |
| 156 | phone or other device, open the PAM =sshd= file: | |
| 147 | phone or other device, open the PAM `sshd` file: | |
| 157 | 148 | |
| 158 | #+begin_src sh | |
| 149 | ```sh | |
| 159 | 150 | sudo nano /etc/pam.d/sshd |
| 160 | #+end_src | |
| 151 | ``` | |
| 161 | 152 | |
| 162 | 153 | You need to do two things in this file. First, add the following lines |
| 163 | 154 | to the bottom of the file: |
| 164 | 155 | |
| 165 | #+begin_src config | |
| 156 | ```config | |
| 166 | 157 | auth required pam_google_authenticator.so nullok |
| 167 | 158 | auth required pam_permit.so |
| 168 | #+end_src | |
| 159 | ``` | |
| 169 | 160 | |
| 170 | 161 | Second, comment-out the following line near the top of the file. |
| 171 | 162 | |
| 172 | 163 | If you leave this line uncommented, every SSH login attempt will ask for |
| 173 | 164 | the following three authentication factors: |
| 174 | 165 | |
| 175 | 1. Publickey | |
| 176 | 2. Password | |
| 177 | 3. T/OTP code | |
| 166 | 1. Publickey | |
| 167 | 2. Password | |
| 168 | 3. T/OTP code | |
| 178 | 169 | |
| 179 | #+begin_src config | |
| 170 | ```config | |
| 180 | 171 | #@include common-auth |
| 181 | #+end_src | |
| 172 | ``` | |
| 173 | ||
| 174 | ## SSH Configuration Settings | |
| 182 | 175 | |
| 183 | ** SSH Configuration Settings | |
| 184 | :PROPERTIES: | |
| 185 | :CUSTOM_ID: ssh-configuration-settings | |
| 186 | :END: | |
| 187 | Finally, edit the =sshd_config= file again: | |
| 176 | Finally, edit the `sshd_config` file again: | |
| 188 | 177 | |
| 189 | #+begin_src sh | |
| 178 | ```sh | |
| 190 | 179 | sudo nano /etc/ssh/sshd_config |
| 191 | #+end_src | |
| 180 | ``` | |
| 192 | 181 | |
| 193 | You'll need to change =ChallengeResponseAuthentication= to yes and add | |
| 194 | the =AuthenticationMethods= line to the bottom of the file. | |
| 182 | You'll need to change `ChallengeResponseAuthentication` to | |
| 183 | yes and add the `AuthenticationMethods` line to the bottom of | |
| 184 | the file. | |
| 195 | 185 | |
| 196 | #+begin_src config | |
| 186 | ```config | |
| 197 | 187 | ChallengeResponseAuthentication yes |
| 198 | 188 | AuthenticationMethods publickey,password publickey,keyboard-interactive |
| 199 | #+end_src | |
| 189 | ``` | |
| 200 | 190 | |
| 201 | Finally, restart the =ssh= service: | |
| 191 | Finally, restart the `ssh` service: | |
| 202 | 192 | |
| 203 | #+begin_src sh | |
| 193 | ```sh | |
| 204 | 194 | sudo systemctl restart sshd.service |
| 205 | #+end_src | |
| 195 | ``` | |
| 206 | 196 | |
| 207 | 197 | The next time you log in, you should be greeted with a verification code |
| 208 | 198 | request! |
content/blog/2022-04-02-nginx-reverse-proxy.md added +234
| @@ -0,0 +1,234 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-04-02 | |
| 3 | title = "Set-Up a Reverse Proxy with Nginx" | |
| 4 | description = "Learn how to set-up an Nginx reverse proxy from scratch." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## What is a Reverse Proxy? | |
| 8 | ||
| 9 | A reverse proxy is a server that is placed between local servers or | |
| 10 | services and clients/users (e.g., the internet). The reverse proxy | |
| 11 | intercepts all requests from clients at the network edge and uses its | |
| 12 | configuration files to determine where each request should be sent. | |
| 13 | ||
| 14 | ### A Brief Example | |
| 15 | ||
| 16 | For example, let's say that I run three servers in my home: | |
| 17 | ||
| 18 | - Server~01~ (`example.com`) | |
| 19 | - Server~02~ (`service01.example.com`) | |
| 20 | - Server~03~ (`service02.example.com`) | |
| 21 | ||
| 22 | I also run a reverse proxy in my home that intercepts all public | |
| 23 | traffic: | |
| 24 | ||
| 25 | - Reverse Proxy | |
| 26 | ||
| 27 | Assume that I have a domain name (`example.com`) that allows | |
| 28 | clients to request websites or services from my home servers. | |
| 29 | ||
| 30 | In this case, the reverse proxy will intercept all traffic from | |
| 31 | `example.com` that enters my network and determine if the | |
| 32 | client is requesting valid data, based on my configuration. | |
| 33 | ||
| 34 | If the user is requesting `example.com` and my configuration | |
| 35 | files say that Server~01~ holds that data, Nginx will send the user to | |
| 36 | Server~01~. If I were to change the configuration so that | |
| 37 | `example.com` is routed to Server~02~, that same user would | |
| 38 | be sent to Server~02~ instead. | |
| 39 | ||
| 40 | ```txt | |
| 41 | ┌──────┐ ┌───────────┐ | |
| 42 | │ User │─┐ ┌──► Server_01 │ | |
| 43 | └──────┘ │ │ └───────────┘ | |
| 44 | │ ┌──────────┐ ┌───────────────┐ │ ┌───────────┐ | |
| 45 | ├────► Internet ├───► Reverse Proxy ├─────├──► Server_02 │ | |
| 46 | │ └──────────┘ └───────────────┘ │ └───────────┘ | |
| 47 | ┌──────┐ │ │ ┌───────────┐ | |
| 48 | │ User │─┘ └──► Server_03 │ | |
| 49 | └──────┘ └───────────┘ | |
| 50 | ``` | |
| 51 | ||
| 52 | ## Reverse Proxy Options | |
| 53 | ||
| 54 | There are a lot of options when it comes to reverse proxy servers, so | |
| 55 | I'm just going to list a few of the options I've heard recommended | |
| 56 | over the last few years: | |
| 57 | ||
| 58 | - [Nginx](https://nginx.com) | |
| 59 | - [Caddy](https://caddyserver.com) | |
| 60 | - [Traefik](https://traefik.io/) | |
| 61 | - [HAProxy](https://www.haproxy.org/) | |
| 62 | - [Squid](https://ubuntu.com/server/docs/proxy-servers-squid) | |
| 63 | ||
| 64 | In this post, we will be using Nginx as our reverse proxy, running on | |
| 65 | Ubuntu Server 20.04.4 LTS. | |
| 66 | ||
| 67 | ## Nginx Reverse Proxy Example | |
| 68 | ||
| 69 | ### Local Applications | |
| 70 | ||
| 71 | You may be like me and have a lot of applications running on your local | |
| 72 | network that you'd like to expose publicly with a domain. | |
| 73 | ||
| 74 | In my case, I have services running in multiple Docker containers within | |
| 75 | a single server and want a way to visit those services from anywhere | |
| 76 | with a URL. For example, on my local network, [Dashy](https://dashy.to) | |
| 77 | runs through port 4000 (`localhost:4000`) and [Uptime | |
| 78 | Kuma](https://github.com/louislam/uptime-kuma) runs through port 3001 | |
| 79 | (`localhost:3001`). | |
| 80 | ||
| 81 | In order to expose these services to the public, I will need to do the | |
| 82 | following: | |
| 83 | ||
| 84 | 1. Set up DNS records for a domain or subdomain (one per service) to | |
| 85 | point toward the IP address of the server. | |
| 86 | 2. Open up the server network's HTTP and HTTPS ports (80 & 443) so | |
| 87 | that the reverse proxy can accept traffic and determine where to | |
| 88 | send it. | |
| 89 | 3. Install the reverse proxy software. | |
| 90 | 4. Configure the reverse proxy to recognize which service should get | |
| 91 | traffic from any of the domains or subdomains. | |
| 92 | ||
| 93 | ### Step 1: DNS Configuration | |
| 94 | ||
| 95 | To start, update your DNS configuration so that you have an | |
| 96 | `A` record for each domain or subdomain. | |
| 97 | ||
| 98 | The `A` records should point toward the public IP address of | |
| 99 | the server. If you don't know the public IP address, log in to the | |
| 100 | server and run the following command: | |
| 101 | ||
| 102 | ```sh | |
| 103 | curl ifconfig.co | |
| 104 | ``` | |
| 105 | ||
| 106 | In the DNS example below, `xxx.xxx.xxx.xxx` is the public IP | |
| 107 | address of the server. | |
| 108 | ||
| 109 | ```config | |
| 110 | example.com A xxx.xxx.xxx.xxx | |
| 111 | uptime.example.com A xxx.xxx.xxx.xxx | |
| 112 | dashy.example.com A xxx.xxx.xxx.xxx | |
| 113 | www CNAME example.com | |
| 114 | ``` | |
| 115 | ||
| 116 | Finally, ensure the DNS has propagated correctly with [DNS | |
| 117 | Checker](https://dnschecker.org) by entering your domains or subdomains | |
| 118 | in the search box and ensuring the results are showing the correct IP | |
| 119 | address. | |
| 120 | ||
| 121 | ### Step 2: Open Network Ports | |
| 122 | ||
| 123 | This step will be different depending on which router you have in your | |
| 124 | home. If you're not sure, try to visit | |
| 125 | [192.168.1.1](http://192.168.1.1) in your browser. Login credentials are | |
| 126 | usually written on a sticker somewhere on your modem/router. | |
| 127 | ||
| 128 | Once you're able to log in to your router, find the Port Forwarding | |
| 129 | settings. You will need to forward ports `80` and | |
| 130 | `443` to whichever machine is running the reverse proxy. | |
| 131 | ||
| 132 | In my case, the table below shows the port-forwarding rules I've | |
| 133 | created. In this table, `xxx.xxx.xxx.xxx` is the local device | |
| 134 | IP of the reverse proxy server, it will probably be an IP between | |
| 135 | `192.168.1.1` and `192.168.1.255`. | |
| 136 | ||
| 137 | NAME FROM PORT DEST PORT/IP ENABLED | |
| 138 | ------- ------ ------ ----------------- --------- | |
| 139 | HTTP \* 80 xxx.xxx.xxx.xxx TRUE | |
| 140 | HTTPS \* 443 xxx.xxx.xxx.xxx TRUE | |
| 141 | ||
| 142 | Once configured, these rules will direct all web traffic to your reverse | |
| 143 | proxy. | |
| 144 | ||
| 145 | ### Step 3: Nginx Installation | |
| 146 | ||
| 147 | To install Nginx, simply run the following command: | |
| 148 | ||
| 149 | ```sh | |
| 150 | sudo apt install nginx | |
| 151 | ``` | |
| 152 | ||
| 153 | If you have a firewall enabled, open up ports `80` and | |
| 154 | `443` on your server so that Nginx can accept web traffic | |
| 155 | from the router. | |
| 156 | ||
| 157 | For example, if you want to use `ufw` for web traffic and | |
| 158 | SSH, run the following commands: | |
| 159 | ||
| 160 | ```sh | |
| 161 | sudo ufw allow 'Nginx Full' | |
| 162 | sudo ufw allow SSH | |
| 163 | sudo ufw enable | |
| 164 | ``` | |
| 165 | ||
| 166 | ### Step 4: Nginx Configuration | |
| 167 | ||
| 168 | Now that we have domains pointing toward the server, the only step left | |
| 169 | is to configure the reverse proxy to direct traffic from domains to | |
| 170 | local services. | |
| 171 | ||
| 172 | To start, you'll need to create a configuration file for each domain in | |
| 173 | `/etc/nginx/sites-available/`. They will look identical | |
| 174 | except for the `server_name` variable and the | |
| 175 | `proxy_pass` port. | |
| 176 | ||
| 177 | Dashy: | |
| 178 | ||
| 179 | ```sh | |
| 180 | nano /etc/nginx/sites-available/dashy.example.com | |
| 181 | ``` | |
| 182 | ||
| 183 | ```config | |
| 184 | server { | |
| 185 | listen 80; | |
| 186 | server_name dashy.example.com; | |
| 187 | ||
| 188 | location / { | |
| 189 | proxy_pass http://localhost:4000; | |
| 190 | } | |
| 191 | } | |
| 192 | ``` | |
| 193 | ||
| 194 | Uptime: | |
| 195 | ||
| 196 | ```sh | |
| 197 | nano /etc/nginx/sites-available/uptime.example.com | |
| 198 | ``` | |
| 199 | ||
| 200 | ```config | |
| 201 | server { | |
| 202 | listen 80; | |
| 203 | server_name uptime.example.com; | |
| 204 | ||
| 205 | location / { | |
| 206 | proxy_pass http://localhost:3001; | |
| 207 | } | |
| 208 | } | |
| 209 | ``` | |
| 210 | ||
| 211 | Once the configuration files are created, you will need to enable them | |
| 212 | with the `symlink` command: | |
| 213 | ||
| 214 | ```sh | |
| 215 | sudo ln -s /etc/nginx/sites-available/dashy.example.com /etc/nginx/sites-enabled/ | |
| 216 | ``` | |
| 217 | ||
| 218 | Voilà! Your local services should now be available through their URLs. | |
| 219 | ||
| 220 | ## HTTPS with Certbot | |
| 221 | ||
| 222 | If you've followed along, you'll notice that your services are only | |
| 223 | available via HTTP (not HTTPS). | |
| 224 | ||
| 225 | If you want to enable HTTPS for your new domains, you will need to | |
| 226 | generate SSL/TLS certificates for them. The easiest way to generate | |
| 227 | certificates on Nginx is [Certbot](https://certbot.eff.org): | |
| 228 | ||
| 229 | ```sh | |
| 230 | sudo apt install snapd; sudo snap install core; sudo snap refresh core | |
| 231 | sudo snap install --classic certbot | |
| 232 | sudo ln -s /snap/bin/certbot /usr/bin/certbot | |
| 233 | sudo certbot --nginx | |
| 234 | ``` | |
blog/2022-04-09-pinetime.org → content/blog/2022-04-09-pinetime.md renamed +77 −98
| @@ -1,53 +1,44 @@ | ||
| 1 | #+title: PineTime: An Open-Source SmartWatch | |
| 2 | #+date: 2022-04-09 | |
| 3 | ||
| 4 | ** PineTime Product Information | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: pinetime-product-information | |
| 7 | :END: | |
| 8 | *** Overview | |
| 9 | :PROPERTIES: | |
| 10 | :CUSTOM_ID: overview | |
| 11 | :END: | |
| 12 | The [[https://www.pine64.org/pinetime/][PineTime]] is an open-source | |
| 13 | smartwatch, created by [[https://www.pine64.org][PINE64]]. Originally | |
| 1 | +++ | |
| 2 | date = 2022-04-09 | |
| 3 | title = "PineTime: An Open-Source Smart Watch" | |
| 4 | description = "Playing with the PineTime smart watch." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## PineTime Product Information | |
| 8 | ||
| 9 | ### Overview | |
| 10 | ||
| 11 | The [PineTime](https://www.pine64.org/pinetime/) is an open-source | |
| 12 | smartwatch, created by [PINE64](https://www.pine64.org). Originally | |
| 14 | 13 | announced in September 2019, this ARM-based watch is a fantastic option |
| 15 | 14 | for users who want the benefits of a modern smartwatch with the backing |
| 16 | 15 | of open-source components and software. |
| 17 | 16 | |
| 18 | *** Product Specifications | |
| 19 | :PROPERTIES: | |
| 20 | :CUSTOM_ID: product-specifications | |
| 21 | :END: | |
| 22 | I won't dive into too many details that you can find on | |
| 23 | [[https://www.pine64.org/pinetime/][the product page]], but I wanted to | |
| 24 | point out the prices for each watch and the primary functions: | |
| 25 | ||
| 26 | **** Price: | |
| 27 | :PROPERTIES: | |
| 28 | :CUSTOM_ID: price | |
| 29 | :END: | |
| 30 | - $26.99 (Sealed) | |
| 31 | - $24.99 (Dev Kit) | |
| 32 | - $51.98 (One Sealed + One Dev Kit) | |
| 33 | ||
| 34 | **** Primary Functionality: | |
| 35 | :PROPERTIES: | |
| 36 | :CUSTOM_ID: primary-functionality | |
| 37 | :END: | |
| 38 | - Clock (+ Smartphone Sync) | |
| 39 | - Pedometer | |
| 40 | - Heart Rate Monitor | |
| 41 | - Sleep Monitor | |
| 42 | - Calories burned | |
| 43 | - Messaging | |
| 44 | - Smartphone Notifications | |
| 45 | - Media Controls | |
| 46 | ||
| 47 | ** Unboxing | |
| 48 | :PROPERTIES: | |
| 49 | :CUSTOM_ID: unboxing | |
| 50 | :END: | |
| 17 | ### Product Specifications | |
| 18 | ||
| 19 | I won't dive into too many details that you can find on [the product | |
| 20 | page](https://www.pine64.org/pinetime/), but I wanted to point out the | |
| 21 | prices for each watch and the primary functions: | |
| 22 | ||
| 23 | 1. Price: | |
| 24 | ||
| 25 | - \$26.99 (Sealed) | |
| 26 | - \$24.99 (Dev Kit) | |
| 27 | - \$51.98 (One Sealed + One Dev Kit) | |
| 28 | ||
| 29 | 2. Primary Functionality: | |
| 30 | ||
| 31 | - Clock (+ Smartphone Sync) | |
| 32 | - Pedometer | |
| 33 | - Heart Rate Monitor | |
| 34 | - Sleep Monitor | |
| 35 | - Calories burned | |
| 36 | - Messaging | |
| 37 | - Smartphone Notifications | |
| 38 | - Media Controls | |
| 39 | ||
| 40 | ## Unboxing | |
| 41 | ||
| 51 | 42 | Now, my PineTime was ordered on 2022-02-17, shipped on 2022-02-22, and |
| 52 | 43 | was delivered on 2022-03-23. With the current delays on shipping times |
| 53 | 44 | around the world (and the semiconductor shortage), a month for delivery |
| @@ -60,35 +51,31 @@ USB wall adapter). | ||
| 60 | 51 | The watch itself was able to turn on immediately when I pulled it out of |
| 61 | 52 | the box, but the battery was depleted and required charging right away. |
| 62 | 53 | |
| 63 | #+caption: PineTime Box Contents | |
| 64 | [[https://img.cleberg.net/blog/20220409-pinetime-smart-watch/pinetime_box_contents.png]] | |
| 65 | ||
| 66 | ** Software | |
| 67 | :PROPERTIES: | |
| 68 | :CUSTOM_ID: software | |
| 69 | :END: | |
| 70 | *** Watch OS: InfiniTime | |
| 71 | :PROPERTIES: | |
| 72 | :CUSTOM_ID: watch-os-infinitime | |
| 73 | :END: | |
| 54 |  | |
| 56 | ||
| 57 | ## Software | |
| 58 | ||
| 59 | ### Watch OS: InfiniTime | |
| 60 | ||
| 74 | 61 | While turning on the watch for the first time, some of the main design |
| 75 | 62 | choices you can see in the watch OS, |
| 76 | [[https://wiki.pine64.org/wiki/InfiniTime][InfiniTime]], are: | |
| 63 | [InfiniTime](https://wiki.pine64.org/wiki/InfiniTime), are: | |
| 77 | 64 | |
| 78 | - A square bezel, not too thin against the sides of the watch. | |
| 79 | - A simple, rubber band. | |
| 80 | - Basic font and screen pixel design. | |
| 81 | - Swipe gestures to access other screens. | |
| 65 | - A square bezel, not too thin against the sides of the watch. | |
| 66 | - A simple, rubber band. | |
| 67 | - Basic font and screen pixel design. | |
| 68 | - Swipe gestures to access other screens. | |
| 82 | 69 | |
| 83 | #+caption: PineTime Screens | |
| 84 | [[https://img.cleberg.net/blog/20220409-pinetime-smart-watch/pinetime.png]] | |
| 70 |  | |
| 85 | 72 | |
| 86 | 73 | The OS itself is fantastic in terms of functionality for me. It does |
| 87 | 74 | exactly what a smartwatch should do - track time, steps, heart rates, |
| 88 | 75 | and connect to another smart device, without being overly burdensome to |
| 89 | 76 | the user. |
| 90 | 77 | |
| 91 | My only gripe so far is that it's /really/ difficult to swipe to | |
| 78 | My only gripe so far is that it's *really* difficult to swipe to | |
| 92 | 79 | different screens, such as pulling down the notification tray. I'm not |
| 93 | 80 | sure if this is an OS or hardware issue, but it makes it quite hard to |
| 94 | 81 | quickly move around the screens. |
| @@ -99,52 +86,46 @@ accidentally turn on the screen. With other watches, I absolutely hated | ||
| 99 | 86 | not being able to turn off the raise-to-wake or wave features (i.e., |
| 100 | 87 | blinding myself while wearing a watch at night because I moved my arm). |
| 101 | 88 | |
| 102 | *** iOS App: InfiniLink | |
| 103 | :PROPERTIES: | |
| 104 | :CUSTOM_ID: ios-app-infinilink | |
| 105 | :END: | |
| 89 | ### iOS App: InfiniLink | |
| 90 | ||
| 106 | 91 | Since I am using iOS as my primary mobile device OS, I am using the |
| 107 | [[https://github.com/xan-m/InfiniLink][InfiniLink]] app to connect my | |
| 92 | [InfiniLink](https://github.com/xan-m/InfiniLink) app to connect my | |
| 108 | 93 | watch. |
| 109 | 94 | |
| 110 | 95 | This app provides the following for PineTime owners: |
| 111 | 96 | |
| 112 | - Firmware updates | |
| 113 | - Steps | |
| 114 | - Charts | |
| 115 | - Notifications | |
| 97 | - Firmware updates | |
| 98 | - Steps | |
| 99 | - Charts | |
| 100 | - Notifications | |
| 116 | 101 | |
| 117 | 102 | I mashed up a few screenshots to show off the home page, menu, firmware |
| 118 | 103 | update, and notification test screens: |
| 119 | 104 | |
| 120 | #+caption: InfiniLink Home | |
| 121 | [[https://img.cleberg.net/blog/20220409-pinetime-smart-watch/infinilink_home.png]] | |
| 105 |  | |
| 122 | 107 | |
| 123 | 108 | Another big feature of InfiniLink is the ability to track pedometer |
| 124 | 109 | steps in a collection of beautiful graphs, with the option to change |
| 125 | 110 | your step goal and add in manual steps. |
| 126 | 111 | |
| 127 | #+caption: InfiniLink Steps | |
| 128 | [[https://img.cleberg.net/blog/20220409-pinetime-smart-watch/infinilink_steps.png]] | |
| 112 |  | |
| 129 | 114 | |
| 130 | 115 | Finally, there are charts to display the battery percentage and heart |
| 131 | 116 | rates over time. This area also comes with an option to clear data. |
| 132 | 117 | |
| 133 | #+caption: InfiniLink Charts | |
| 134 | [[https://img.cleberg.net/blog/20220409-pinetime-smart-watch/infinilink_charts.png]] | |
| 135 | ||
| 136 | ** Final Thoughts | |
| 137 | :PROPERTIES: | |
| 138 | :CUSTOM_ID: final-thoughts | |
| 139 | :END: | |
| 140 | *** Pros | |
| 141 | :PROPERTIES: | |
| 142 | :CUSTOM_ID: pros | |
| 143 | :END: | |
| 118 |  | |
| 120 | ||
| 121 | ## Final Thoughts | |
| 122 | ||
| 123 | ### Pros | |
| 124 | ||
| 144 | 125 | After wearing my watch for a few weeks, I have mostly positive thoughts |
| 145 | 126 | about the watch so far. In the past, I have owned smartwatches by |
| 146 | FitBit, Fossil, Apple, etc. - *but I prefer the PineTime over all of | |
| 147 | those watches*. | |
| 127 | FitBit, Fossil, Apple, etc. - **but I prefer the PineTime over all of | |
| 128 | those watches**. | |
| 148 | 129 | |
| 149 | 130 | The PineTime strips out all the unnecessary features and performs the |
| 150 | 131 | functions that it provides effectively and efficiently. |
| @@ -152,14 +133,12 @@ functions that it provides effectively and efficiently. | ||
| 152 | 133 | The battery life is amazing on this device. By default, the watch seems |
| 153 | 134 | to last anywhere from a few days to a week before dying. |
| 154 | 135 | |
| 155 | And of course, it's open source and backed by some of the most dedicated | |
| 156 | enthusiasts and developers I've seen. Watching the Matrix channel, | |
| 157 | forums, and website have been exciting to see. | |
| 136 | And of course, it's open source and backed by some of the most | |
| 137 | dedicated enthusiasts and developers I've seen. Watching the Matrix | |
| 138 | channel, forums, and website have been exciting to see. | |
| 139 | ||
| 140 | ### Cons | |
| 158 | 141 | |
| 159 | *** Cons | |
| 160 | :PROPERTIES: | |
| 161 | :CUSTOM_ID: cons | |
| 162 | :END: | |
| 163 | 142 | If I had to complain about anything, it would simply be the small bugs |
| 164 | 143 | in some features that can be contributed to the companion apps more than |
| 165 | 144 | the watch itself. |
blog/2022-06-01-ditching-cloudflare.org → content/blog/2022-06-01-ditching-cloudflare.md renamed +44 −55
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: Ditching Cloudflare for Njalla | |
| 2 | #+date: 2022-06-01 | |
| 1 | +++ | |
| 2 | date = 2022-06-01 | |
| 3 | title = "Ditching Cloudflare for Njalla" | |
| 4 | description = "A retrospective on my decision to leave Cloudflare and move to Njalla for domain registration and DNS." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Registrar | |
| 3 | 8 | |
| 4 | ** Registrar | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: registrar | |
| 7 | :END: | |
| 8 | 9 | After spending a year or so using Cloudflare for DNS only - no proxying |
| 9 | 10 | or applications - I spent the last few months using Cloudflare Tunnels |
| 10 | 11 | and Cloudflare Access to protect my self-hosted websites and |
| @@ -14,27 +15,22 @@ However, I have never liked using Cloudflare due to their increasingly | ||
| 14 | 15 | large share of control over web traffic, as well as their business model |
| 15 | 16 | of being a MITM for all of your traffic. |
| 16 | 17 | |
| 17 | So, as of today, I have switched over to [[https://njal.la][Njalla]] as | |
| 18 | my registrar and DNS manager. I was able to easily transfer my domains | |
| 19 | over rapidly, with only one domain taking more than 15-30 minutes to | |
| 18 | So, as of today, I have switched over to [Njalla](https://njal.la) as my | |
| 19 | registrar and DNS manager. I was able to easily transfer my domains over | |
| 20 | rapidly, with only one domain taking more than 15-30 minutes to | |
| 20 | 21 | propagate. |
| 21 | 22 | |
| 22 | 23 | +I do still have two domains sitting at Cloudflare for the moment while |
| 23 | 24 | I decide if they're worth the higher rates (one domain is 30€ and the |
| 24 | 25 | other is 45€).+ |
| 25 | 26 | |
| 26 | #+begin_quote | |
| 27 | *Update (2022.06.03)*: I ended up transferring my final two domains over | |
| 28 | to Njalla, clearing my Cloudflare account of personal data, and deleting | |
| 29 | the Cloudflare account entirely. /I actually feel relieved to have moved | |
| 30 | on to a provider I trust./ | |
| 27 | > **Update (2022.06.03)**: I ended up transferring my final two domains | |
| 28 | > over to Njalla, clearing my Cloudflare account of personal data, and | |
| 29 | > deleting the Cloudflare account entirely. *I actually feel relieved to | |
| 30 | > have moved on to a provider I trust.* | |
| 31 | 31 | |
| 32 | #+end_quote | |
| 32 | ## DNS | |
| 33 | 33 | |
| 34 | ** DNS | |
| 35 | :PROPERTIES: | |
| 36 | :CUSTOM_ID: dns | |
| 37 | :END: | |
| 38 | 34 | As noted above, I'm using Njalla exclusively for DNS configurations on |
| 39 | 35 | my domains. |
| 40 | 36 | |
| @@ -46,55 +42,48 @@ back in one-by-one. | ||
| 46 | 42 | |
| 47 | 43 | This would be much simpler if I were able to edit the plain-text format |
| 48 | 44 | of the DNS configuration. I was able to do that at a past registrar |
| 49 | (perhaps it was [[https://gandi.net/][Gandi.net]]?) and it made life a | |
| 50 | lot easier. | |
| 51 | ||
| 52 | *** Dynamic DNS Updates | |
| 53 | :PROPERTIES: | |
| 54 | :CUSTOM_ID: dynamic-dns-updates | |
| 55 | :END: | |
| 56 | I have built an easy Python script to run (or set-up in =cron= to run | |
| 57 | automatically) that will check my server's IPv4 and IPv6, compare it to | |
| 58 | Njalla, and update the DNS records if they don't match. You can see the | |
| 59 | full script and process in my other post: | |
| 60 | [[/blog/njalla-dns-api/][Updating Dynamic DNS with Njalla API]]. | |
| 45 | (perhaps it was [Gandi.net](https://gandi.net/)?) and it made life a lot | |
| 46 | easier. | |
| 47 | ||
| 48 | ### Dynamic DNS Updates | |
| 49 | ||
| 50 | I have built an easy Python script to run (or set-up in | |
| 51 | `cron` to run automatically) that will check my server's | |
| 52 | IPv4 and IPv6, compare it to Njalla, and update the DNS records if they | |
| 53 | don't match. You can see the full script and process in my other post: | |
| 54 | [Updating Dynamic DNS with Njalla API](/blog/njalla-dns-api/). | |
| 61 | 55 | |
| 62 | 56 | I haven't used this other method, but I do know that you can create |
| 63 | =Dynamic= DNS records with Njalla that | |
| 64 | [[https://njal.la/docs/ddns/][work for updating dynamic subdomains]]. | |
| 65 | ||
| 66 | *** Njalla's DNS Tool | |
| 67 | :PROPERTIES: | |
| 68 | :CUSTOM_ID: njallas-dns-tool | |
| 69 | :END: | |
| 70 | One neat upside to Njalla is that they have a | |
| 71 | [[https://check.njal.la/dns/][DNS lookup tool]] that provides a lot of | |
| 72 | great information for those of you (AKA: me) who hate using the =dig= | |
| 73 | command. | |
| 57 | `Dynamic` DNS records with Njalla that [work for updating | |
| 58 | dynamic subdomains](https://njal.la/docs/ddns/). | |
| 59 | ||
| 60 | ### Njalla's DNS Tool | |
| 61 | ||
| 62 | One neat upside to Njalla is that they have a [DNS lookup | |
| 63 | tool](https://check.njal.la/dns/) that provides a lot of great | |
| 64 | information for those of you (AKA: me) who hate using the | |
| 65 | `dig` command. | |
| 74 | 66 | |
| 75 | 67 | This was very useful for monitoring a couple of my transferred domains |
| 76 | 68 | to see when the changes in nameservers, records, and DNSSEC went into |
| 77 | 69 | effect. |
| 78 | 70 | |
| 79 | ** Tunnel | |
| 80 | :PROPERTIES: | |
| 81 | :CUSTOM_ID: tunnel | |
| 82 | :END: | |
| 71 | ## Tunnel | |
| 72 | ||
| 83 | 73 | Cloudflare Tunnel is a service that acts as a reverse-proxy (hosted on |
| 84 | 74 | Cloudflare's servers) and allowed me to mask the private IP address of |
| 85 | 75 | the server hosting my various websites and apps. |
| 86 | 76 | |
| 87 | 77 | However, as I was moving away from Cloudflare, I was not able to find a |
| 88 | 78 | suitable replacement that was both inexpensive and simple. So, I simply |
| 89 | went back to hosting [[/blog/set-up-nginx-reverse-proxy/][my own reverse | |
| 90 | proxy with Nginx]]. With the recent additions of Unifi hardware in my | |
| 91 | server/network rack, I am much more protected against spam and malicious | |
| 92 | attacks at the network edge than I was before I switched to Cloudflare. | |
| 93 | ||
| 94 | ** Access | |
| 95 | :PROPERTIES: | |
| 96 | :CUSTOM_ID: access | |
| 97 | :END: | |
| 79 | went back to hosting [my own reverse proxy with | |
| 80 | Nginx](/blog/set-up-nginx-reverse-proxy/). With the recent | |
| 81 | additions of Unifi hardware in my server/network rack, I am much more | |
| 82 | protected against spam and malicious attacks at the network edge than I | |
| 83 | was before I switched to Cloudflare. | |
| 84 | ||
| 85 | ## Access | |
| 86 | ||
| 98 | 87 | Cloudflare Access, another app I used in combination with Cloudflare |
| 99 | 88 | Tunnel, provided an authentication screen that required you to enter |
| 100 | 89 | valid credentials before Cloudflare would forward you to the actual |
blog/2022-06-04-njalla-dns-api.org → content/blog/2022-06-04-njalla-dns-api.md renamed +69 −76
| @@ -1,14 +1,15 @@ | ||
| 1 | #+title: Dynamic DNS with Njalla API | |
| 2 | #+date: 2022-06-04 | |
| 3 | ||
| 4 | ** Njalla's API | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: njallas-api | |
| 7 | :END: | |
| 8 | As noted in my recent post about [[/blog/ditching-cloudflare/][switching | |
| 9 | to Njalla from Cloudflare]], I was searching for a way to replace my | |
| 10 | very easy-to-use bash script to [[/blog/cloudflare-dns-api/][update | |
| 11 | Cloudflare's DNS via their API]]. | |
| 1 | +++ | |
| 2 | date = 2022-02-10 | |
| 3 | title = "Dynamic DNS with Njalla API" | |
| 4 | description = "Learn how to dynamically update DNS records for changing IPs with Njalla." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Njalla's API | |
| 8 | ||
| 9 | As noted in my recent post about [switching to Njalla from | |
| 10 | Cloudflare](/blog/ditching-cloudflare/), I was searching for a | |
| 11 | way to replace my very easy-to-use bash script to [update Cloudflare's | |
| 12 | DNS via their API](/blog/cloudflare-dns-api/). | |
| 12 | 13 | |
| 13 | 14 | To reiterate what I said in those posts, this is a common necessity for |
| 14 | 15 | those of us who have non-static IP addresses that can change at any |
| @@ -21,93 +22,88 @@ update their domain's DNS records if it changes. | ||
| 21 | 22 | This post explains how to use Python to update Njalla's DNS records |
| 22 | 23 | whenever a machine's public IP address changes. |
| 23 | 24 | |
| 24 | *** Creating a Token | |
| 25 | :PROPERTIES: | |
| 26 | :CUSTOM_ID: creating-a-token | |
| 27 | :END: | |
| 25 | ### Creating a Token | |
| 26 | ||
| 28 | 27 | To use Njalla's API, you will first need to create a token that will be |
| 29 | 28 | used to authenticate you every time you call the API. Luckily, this is |
| 30 | 29 | very easy to do if you have an account with Njalla. |
| 31 | 30 | |
| 32 | Simply go the [[https://njal.la/settings/api/][API Settings]] page and | |
| 33 | click the =Add Token= button. Next, enter a name for the token and click | |
| 34 | =Add=. | |
| 31 | Simply go the [API Settings](https://njal.la/settings/api/) page and | |
| 32 | click the `Add Token` button. Next, enter a name for the | |
| 33 | token and click `Add`. | |
| 35 | 34 | |
| 36 | Finally, click the =Manage= button next to your newly created token and | |
| 37 | copy the =API Token= field. | |
| 35 | Finally, click the `Manage` button next to your newly created | |
| 36 | token and copy the `API Token` field. | |
| 37 | ||
| 38 | ### Finding the Correct API Request | |
| 38 | 39 | |
| 39 | *** Finding the Correct API Request | |
| 40 | :PROPERTIES: | |
| 41 | :CUSTOM_ID: finding-the-correct-api-request | |
| 42 | :END: | |
| 43 | 40 | Once you have a token, you're ready to call the Njalla API for any |
| 44 | 41 | number of requests. For a full listing of available requests, see the |
| 45 | [[https://njal.la/api/][Njalla API Documentation]]. | |
| 42 | [Njalla API Documentation](https://njal.la/api/). | |
| 46 | 43 | |
| 47 | For this demo, we are using the =list-records= and =edit-record= | |
| 48 | requests. | |
| 44 | For this demo, we are using the `list-records` and | |
| 45 | `edit-record` requests. | |
| 49 | 46 | |
| 50 | The =list-records= request requires the following payload to be sent | |
| 51 | when calling the API: | |
| 47 | The `list-records` request requires the following payload to | |
| 48 | be sent when calling the API: | |
| 52 | 49 | |
| 53 | #+begin_src txt | |
| 50 | ```txt | |
| 54 | 51 | params: { |
| 55 | 52 | domain: string |
| 56 | 53 | } |
| 57 | #+end_src | |
| 54 | ``` | |
| 58 | 55 | |
| 59 | The =edit-record= request requires the following payload to be sent when | |
| 60 | calling the API: | |
| 56 | The `edit-record` request requires the following payload to | |
| 57 | be sent when calling the API: | |
| 61 | 58 | |
| 62 | #+begin_src txt | |
| 59 | ```txt | |
| 63 | 60 | params: { |
| 64 | 61 | domain: string |
| 65 | 62 | id: int |
| 66 | 63 | content: string |
| 67 | 64 | } |
| 68 | #+end_src | |
| 65 | ``` | |
| 66 | ||
| 67 | ## Server Set-Up | |
| 69 | 68 | |
| 70 | ** Server Set-Up | |
| 71 | :PROPERTIES: | |
| 72 | :CUSTOM_ID: server-set-up | |
| 73 | :END: | |
| 74 | 69 | To create this script, we will be using Python. By default, I use Python |
| 75 | 70 | 3 on my servers, so please note that I did not test this in Python 2, |
| 76 | 71 | and I do not know if Python 2 will work for this. |
| 77 | 72 | |
| 78 | *** Creating the Script | |
| 79 | :PROPERTIES: | |
| 80 | :CUSTOM_ID: creating-the-script | |
| 81 | :END: | |
| 73 | ### Creating the Script | |
| 74 | ||
| 82 | 75 | First, find a suitable place to create your script. Personally, I just |
| 83 | create a directory called =ddns= in my home directory: | |
| 76 | create a directory called `ddns` in my home directory: | |
| 84 | 77 | |
| 85 | #+begin_src sh | |
| 78 | ```sh | |
| 86 | 79 | mkdir ~/ddns |
| 87 | #+end_src | |
| 80 | ``` | |
| 88 | 81 | |
| 89 | 82 | Next, create a Python script file: |
| 90 | 83 | |
| 91 | #+begin_src sh | |
| 84 | ```sh | |
| 92 | 85 | nano ~/ddns/ddns.py |
| 93 | #+end_src | |
| 86 | ``` | |
| 94 | 87 | |
| 95 | 88 | The following code snippet is quite long, so I won't go into depth on |
| 96 | 89 | each part. However, I suggest you read through the entire script before |
| 97 | 90 | running it; it is quite simple and contains comments to help explain |
| 98 | 91 | each code block. |
| 99 | 92 | |
| 100 | :warning: *Note*: You will need to update the following variables for | |
| 93 | :warning: **Note**: You will need to update the following variables for | |
| 101 | 94 | this to work: |
| 102 | 95 | |
| 103 | - =token=: This is the Njalla API token you created earlier. | |
| 104 | - =user_domain=: This is the top-level domain you want to modify. | |
| 105 | - =include_subdomains=: Set this to =True= if you also want to modify | |
| 106 | subdomains found under the TLD. | |
| 107 | - =subdomains=: If =include_subdomains= = =True=, you can include your | |
| 108 | list of subdomains to be modified here. | |
| 109 | ||
| 110 | #+begin_src python | |
| 96 | - `token`: This is the Njalla API token you created | |
| 97 | earlier. | |
| 98 | - `user_domain`: This is the top-level domain you want to | |
| 99 | modify. | |
| 100 | - `include_subdomains`: Set this to `True` if | |
| 101 | you also want to modify subdomains found under the TLD. | |
| 102 | - `subdomains`: If `include_subdomains` = | |
| 103 | `True`, you can include your list of subdomains to be | |
| 104 | modified here. | |
| 105 | ||
| 106 | ```python | |
| 111 | 107 | #!/usr/bin/python |
| 112 | 108 | # -*- coding: utf-8 -*- |
| 113 | 109 | # Import Python modules |
| @@ -173,7 +169,7 @@ for record in data['records']: | ||
| 173 | 169 | ) |
| 174 | 170 | else: |
| 175 | 171 | print('IPv4 of', ipv4, |
| 176 | 'does not match Njalla\'s value of', | |
| 172 | 'does not match Njalla's value of', | |
| 177 | 173 | record['content'], '. Updating...') |
| 178 | 174 | update_record(user_domain, record['id'], ipv4) |
| 179 | 175 | elif record['type'] == 'AAAA': |
| @@ -183,36 +179,33 @@ for record in data['records']: | ||
| 183 | 179 | ) |
| 184 | 180 | else: |
| 185 | 181 | print('IPv6 of', ipv6, |
| 186 | 'does not match Njalla\'s value of', | |
| 182 | 'does not match Njalla's value of', | |
| 187 | 183 | record['content'], '. Updating...') |
| 188 | 184 | update_record(user_domain, record['id'], ipv6) |
| 189 | #+end_src | |
| 185 | ``` | |
| 186 | ||
| 187 | ### Running the Script | |
| 190 | 188 | |
| 191 | *** Running the Script | |
| 192 | :PROPERTIES: | |
| 193 | :CUSTOM_ID: running-the-script | |
| 194 | :END: | |
| 195 | 189 | Once you've created the script and are ready to test it, run the |
| 196 | 190 | following command: |
| 197 | 191 | |
| 198 | #+begin_src sh | |
| 192 | ```sh | |
| 199 | 193 | python3 ~/ddns/ddns.py |
| 200 | #+end_src | |
| 194 | ``` | |
| 195 | ||
| 196 | ### Setting the Script to Run Automatically | |
| 201 | 197 | |
| 202 | *** Setting the Script to Run Automatically | |
| 203 | :PROPERTIES: | |
| 204 | :CUSTOM_ID: setting-the-script-to-run-automatically | |
| 205 | :END: | |
| 206 | To make sure the scripts run automatically, add it to the =cron= file so | |
| 207 | that it will run on a schedule. To do this, open the =cron= file: | |
| 198 | To make sure the scripts run automatically, add it to the | |
| 199 | `cron` file so that it will run on a schedule. To do this, | |
| 200 | open the `cron` file: | |
| 208 | 201 | |
| 209 | #+begin_src sh | |
| 202 | ```sh | |
| 210 | 203 | crontab -e |
| 211 | #+end_src | |
| 204 | ``` | |
| 212 | 205 | |
| 213 | 206 | In the cron file, paste the following at the bottom of the editor in |
| 214 | 207 | order to check the IP every five minutes: |
| 215 | 208 | |
| 216 | #+begin_src sh | |
| 209 | ```sh | |
| 217 | 210 | */5 * * * * python3 /home/<your_username>/ddns/ddns.py |
| 218 | #+end_src | |
| 211 | ``` | |
blog/2022-06-07-freshrss.org → content/blog/2022-06-07-freshrss.md renamed +116 −125
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: Self-Hosting FreshRSS | |
| 2 | #+date: 2022-06-07 | |
| 1 | +++ | |
| 2 | date = 2022-06-07 | |
| 3 | title = "Self-Hosting FreshRSS" | |
| 4 | description = "A guide to self-hosting the FreshRSS application on your own server." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Why RSS? | |
| 3 | 8 | |
| 4 | ** Why RSS? | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: why-rss | |
| 7 | :END: | |
| 8 | 9 | After noticing that I have collected 50+ blogs as bookmarks, I decided |
| 9 | 10 | to migrate back to using RSS feeds to stay up-to-date with my favorite |
| 10 | 11 | websites. Using RSS allows me to read all of these posts in a single app |
| @@ -15,61 +16,56 @@ However, I ran into one issue: syncing subscriptions and read/unread | ||
| 15 | 16 | posts across devices. Since I want to be able to easily read on both |
| 16 | 17 | mobile and desktop, I decided to look for a self-hosted RSS solution. |
| 17 | 18 | |
| 18 | Thus, I found [[https://www.freshrss.org/][FreshRSS]] and was able to | |
| 19 | Thus, I found [FreshRSS](https://www.freshrss.org/) and was able to | |
| 19 | 20 | successfully install it on my server in about 30 minutes. |
| 20 | 21 | |
| 21 | ** Documentation | |
| 22 | :PROPERTIES: | |
| 23 | :CUSTOM_ID: documentation | |
| 24 | :END: | |
| 25 | While it's certainly not robust, the | |
| 26 | [[https://freshrss.github.io/FreshRSS/][FreshRSS documentation]] is | |
| 27 | helpful for figuring out basic information about the service. | |
| 22 | ## Documentation | |
| 23 | ||
| 24 | While it's certainly not robust, the [FreshRSS | |
| 25 | documentation](https://freshrss.github.io/FreshRSS/) is helpful for | |
| 26 | figuring out basic information about the service. | |
| 28 | 27 | |
| 29 | 28 | However, I wanted to install this service as a Docker container and |
| 30 | stumbled across the | |
| 31 | [[https://github.com/FreshRSS/FreshRSS/tree/edge/Docker][Docker README]] | |
| 32 | within the GitHub repository. | |
| 29 | stumbled across the [Docker | |
| 30 | README](https://github.com/FreshRSS/FreshRSS/tree/edge/Docker) within | |
| 31 | the GitHub repository. | |
| 33 | 32 | |
| 34 | 33 | This README was the documentation I actually needed. However, as you'll |
| 35 | see below, I still had to manually edit one file (=config.php=) to | |
| 36 | access the API externally via my RSS apps. | |
| 37 | ||
| 38 | ** Installation | |
| 39 | :PROPERTIES: | |
| 40 | :CUSTOM_ID: installation | |
| 41 | :END: | |
| 42 | *** DNS | |
| 43 | :PROPERTIES: | |
| 44 | :CUSTOM_ID: dns | |
| 45 | :END: | |
| 34 | see below, I still had to manually edit one file | |
| 35 | (`config.php`) to access the API externally via my RSS apps. | |
| 36 | ||
| 37 | ## Installation | |
| 38 | ||
| 39 | ### DNS | |
| 40 | ||
| 46 | 41 | The first step, as required by any external web service, was assigning a |
| 47 | domain name to use. I chose to use a subdomain, like =rss.example.com=. | |
| 42 | domain name to use. I chose to use a subdomain, like | |
| 43 | `rss.example.com`. | |
| 48 | 44 | |
| 49 | To assign this, I created an =A= record in my DNS settings with the IPv4 | |
| 50 | address of the server and an =AAAA= record with the IPv6 address of the | |
| 51 | server. Note: assigning an IPv6 (=AAAA=) record is optional, but I like | |
| 52 | to enable IPV6 for my services. | |
| 45 | To assign this, I created an `A` record in my DNS settings | |
| 46 | with the IPv4 address of the server and an `AAAA` record with | |
| 47 | the IPv6 address of the server. Note: assigning an IPv6 | |
| 48 | (`AAAA`) record is optional, but I like to enable IPV6 for my | |
| 49 | services. | |
| 53 | 50 | |
| 54 | #+begin_src config | |
| 51 | ```config | |
| 55 | 52 | rss.example.com A xxx.xxx.xxx.xxx |
| 56 | 53 | rss.example.com AAAA xxxx:xxxx: ... :xxxx |
| 57 | #+end_src | |
| 58 | ||
| 59 | *** Docker | |
| 60 | :PROPERTIES: | |
| 61 | :CUSTOM_ID: docker | |
| 62 | :END: | |
| 63 | I initially tried to set up a =docker-compose.yml= file with a =.env= | |
| 64 | file because I prefer to have a file I can look back at later to see how | |
| 65 | I initially started the container, but it simply wouldn't work for me. | |
| 66 | I'm not sure why, but I assume I wasn't telling =docker-compose= where | |
| 67 | the =.env= file was. | |
| 68 | ||
| 69 | Regardless, I chose to simply run the service with =docker run=. See the | |
| 70 | following command for my =docker run= configuration: | |
| 71 | ||
| 72 | #+begin_src sh | |
| 54 | ``` | |
| 55 | ||
| 56 | ### Docker | |
| 57 | ||
| 58 | I initially tried to set up a `docker-compose.yml` file with | |
| 59 | a `.env` file because I prefer to have a file I can look back | |
| 60 | at later to see how I initially started the container, but it simply | |
| 61 | wouldn't work for me. I'm not sure why, but I assume I wasn't telling | |
| 62 | `docker-compose` where the `.env` file was. | |
| 63 | ||
| 64 | Regardless, I chose to simply run the service with | |
| 65 | `docker run`. See the following command for my | |
| 66 | `docker run` configuration: | |
| 67 | ||
| 68 | ```sh | |
| 73 | 69 | sudo docker run -d --restart unless-stopped --log-opt max-size=10m \ |
| 74 | 70 | -p 8080:80 \ |
| 75 | 71 | -e TZ=America/Chicago \ |
| @@ -78,46 +74,44 @@ sudo docker run -d --restart unless-stopped --log-opt max-size=10m \ | ||
| 78 | 74 | -v freshrss_extensions:/var/www/FreshRSS/extensions \ |
| 79 | 75 | --name freshrss \ |
| 80 | 76 | freshrss/freshrss |
| 81 | #+end_src | |
| 77 | ``` | |
| 82 | 78 | |
| 83 | 79 | This started the container successfully and allowed me to visit the |
| 84 | FreshRSS instance at =localhost:8080=. | |
| 80 | FreshRSS instance at `localhost:8080`. | |
| 81 | ||
| 82 | ### Fresh RSS Set-Up | |
| 85 | 83 | |
| 86 | *** Fresh RSS Set-Up | |
| 87 | :PROPERTIES: | |
| 88 | :CUSTOM_ID: fresh-rss-set-up | |
| 89 | :END: | |
| 90 | I *HIGHLY* suggest that you set up your user account prior to exposing | |
| 84 | I **HIGHLY** suggest that you set up your user account prior to exposing | |
| 91 | 85 | this service to the public. It's unlikely that someone is trying to |
| 92 | access the exact domain or IP/port you're assigning here, but as soon as | |
| 93 | you expose this service, the first person to open the URL will be able | |
| 94 | to create the admin user. | |
| 86 | access the exact domain or IP/port you're assigning here, but as soon | |
| 87 | as you expose this service, the first person to open the URL will be | |
| 88 | able to create the admin user. | |
| 95 | 89 | |
| 96 | In order to set up your FreshRSS service, open the =localhost:8080= URL | |
| 97 | in your browser (you may need to use a local IP instead of =localhost= | |
| 98 | if you're accessing the page from a different machine on the network - | |
| 99 | e.g., =192.168.1.20:8080=). | |
| 90 | In order to set up your FreshRSS service, open the | |
| 91 | `localhost:8080` URL in your browser (you may need to use a | |
| 92 | local IP instead of `localhost` if you're accessing the page | |
| 93 | from a different machine on the network - e.g., | |
| 94 | `192.168.1.20:8080`). | |
| 100 | 95 | |
| 101 | 96 | Once the page loads, set up your default user with a strong username and |
| 102 | 97 | password. You may also choose to configure other settings prior to |
| 103 | 98 | exposing this service. |
| 104 | 99 | |
| 105 | *** Nginx Reverse-Proxy | |
| 106 | :PROPERTIES: | |
| 107 | :CUSTOM_ID: nginx-reverse-proxy | |
| 108 | :END: | |
| 100 | ### Nginx Reverse-Proxy | |
| 101 | ||
| 109 | 102 | In order to access this service outside my home, I needed to set up a |
| 110 | reverse-proxy to connect =localhost:8080= to =rss.example.com=. | |
| 103 | reverse-proxy to connect `localhost:8080` to | |
| 104 | `rss.example.com`. | |
| 111 | 105 | |
| 112 | 106 | First, I created a new Nginx configuration file: |
| 113 | 107 | |
| 114 | #+begin_src sh | |
| 108 | ```sh | |
| 115 | 109 | sudo nano /etc/nginx/sites-available/rss.example.com |
| 116 | #+end_src | |
| 110 | ``` | |
| 117 | 111 | |
| 118 | 112 | Within the config file, I pasted the following code: |
| 119 | 113 | |
| 120 | #+begin_src config | |
| 114 | ```config | |
| 121 | 115 | upstream freshrss { |
| 122 | 116 | server 127.0.0.1:8080; |
| 123 | 117 | keepalive 64; |
| @@ -146,91 +140,87 @@ server { | ||
| 146 | 140 | proxy_pass_header Authorization; |
| 147 | 141 | } |
| 148 | 142 | } |
| 149 | #+end_src | |
| 143 | ``` | |
| 150 | 144 | |
| 151 | 145 | Finally, restart Nginx and you will be able to access your service via |
| 152 | 146 | HTTP: |
| 153 | 147 | |
| 154 | #+begin_src sh | |
| 148 | ```sh | |
| 155 | 149 | sudo systemctl restart nginx.service |
| 156 | #+end_src | |
| 150 | ``` | |
| 151 | ||
| 152 | ### HTTPS | |
| 157 | 153 | |
| 158 | *** HTTPS | |
| 159 | :PROPERTIES: | |
| 160 | :CUSTOM_ID: https | |
| 161 | :END: | |
| 162 | 154 | However, I don't want to access my RSS feeds via HTTP. I want it |
| 163 | 155 | available only via HTTPS. In order to do this, I ran the |
| 164 | [[https://certbot.eff.org/][certbot]] program to generate SSL | |
| 165 | certificates for me: | |
| 156 | [certbot](https://certbot.eff.org/) program to generate SSL certificates | |
| 157 | for me: | |
| 166 | 158 | |
| 167 | #+begin_src sh | |
| 159 | ```sh | |
| 168 | 160 | sudo certbot --nginx |
| 169 | #+end_src | |
| 161 | ``` | |
| 170 | 162 | |
| 171 | 163 | This process will automatically generate an SSL certificate for you and |
| 172 | 164 | modify the Nginx configuration file to include a redirect from HTTP to |
| 173 | 165 | HTTPS. |
| 174 | 166 | |
| 175 | ** Post-Installation Fixes | |
| 176 | :PROPERTIES: | |
| 177 | :CUSTOM_ID: post-installation-fixes | |
| 178 | :END: | |
| 167 | ## Post-Installation Fixes | |
| 168 | ||
| 179 | 169 | At this point, we have a functional FreshRSS website, available from |
| 180 | 170 | anywhere and secured with HTTPS. However, attempting to connect this |
| 181 | 171 | service to an RSS app resulted in many errors regarding unavailable URLs |
| 182 | 172 | and incorrect credentials. |
| 183 | 173 | |
| 184 | *** API Set-Up | |
| 185 | :PROPERTIES: | |
| 186 | :CUSTOM_ID: api-set-up | |
| 187 | :END: | |
| 188 | First, you need to open your user profile in FreshRSS (=Settings= > | |
| 189 | =Profile=) and set an API password in the field at the bottom. This is | |
| 190 | the password you will need to provide to your RSS apps. | |
| 174 | ### API Set-Up | |
| 175 | ||
| 176 | First, you need to open your user profile in FreshRSS | |
| 177 | (`Settings` > `Profile`) and set an API password | |
| 178 | in the field at the bottom. This is the password you will need to | |
| 179 | provide to your RSS apps. | |
| 191 | 180 | |
| 192 | 181 | Once that is set and saved, click the link below the API password field |
| 193 | 182 | to open the API check tool. It should look something like |
| 194 | =https://localhost:8080/api/= or =https://rss.example.com/api/=. | |
| 183 | `https://localhost:8080/api/` or | |
| 184 | `https://rss.example.com/api/`. | |
| 195 | 185 | |
| 196 | Within this page, you /should/ see your correct external URL and "PASS" | |
| 197 | at the bottom of each API type. This would mean everything is set up | |
| 198 | correctly, and you can now move on and login to any RSS apps that | |
| 186 | Within this page, you *should* see your correct external URL and | |
| 187 | "PASS" at the bottom of each API type. This would mean everything is | |
| 188 | set up correctly, and you can now move on and login to any RSS apps that | |
| 199 | 189 | support self-hosted options. |
| 200 | 190 | |
| 201 | 191 | In my case, the URL showed an internal URL and I had a warning that the |
| 202 | =base_url= variable may be misconfigured. If this is the case, see the | |
| 203 | next section for a fix. | |
| 192 | `base_url` variable may be misconfigured. If this is the | |
| 193 | case, see the next section for a fix. | |
| 194 | ||
| 195 | ### Base URL Fix | |
| 204 | 196 | |
| 205 | *** Base URL Fix | |
| 206 | :PROPERTIES: | |
| 207 | :CUSTOM_ID: base-url-fix | |
| 208 | :END: | |
| 209 | In order to fix the =base_url= for the API, I opened up my docker | |
| 210 | container with the following command: | |
| 197 | In order to fix the `base_url` for the API, I opened up my | |
| 198 | docker container with the following command: | |
| 211 | 199 | |
| 212 | #+begin_src sh | |
| 200 | ```sh | |
| 213 | 201 | sudo docker exec -it freshrss bash |
| 214 | #+end_src | |
| 202 | ``` | |
| 215 | 203 | |
| 216 | 204 | Within this container, update the packages and install an editor: |
| 217 | 205 | |
| 218 | #+begin_src sh | |
| 206 | ```sh | |
| 219 | 207 | apt-get update |
| 220 | 208 | apt-get install nano |
| 221 | #+end_src | |
| 209 | ``` | |
| 222 | 210 | |
| 223 | Finally, open up =config.php= in the =data= directory: | |
| 211 | Finally, open up `config.php` in the `data` | |
| 212 | directory: | |
| 224 | 213 | |
| 225 | #+begin_src sh | |
| 214 | ```sh | |
| 226 | 215 | nano data/config.php |
| 227 | #+end_src | |
| 216 | ``` | |
| 228 | 217 | |
| 229 | Within =config.php=, you will need to update the =base_url= variable and | |
| 230 | update it to match your external URL. In my case, I simply commented-out | |
| 231 | the incorrect URL with =//= and added the correct one on a new line: | |
| 218 | Within `config.php`, you will need to update the | |
| 219 | `base_url` variable and update it to match your external URL. | |
| 220 | In my case, I simply commented-out the incorrect URL with | |
| 221 | `//` and added the correct one on a new line: | |
| 232 | 222 | |
| 233 | #+begin_src php | |
| 223 | ```php | |
| 234 | 224 | <?php |
| 235 | 225 | return array ( |
| 236 | 226 | ... |
| @@ -239,25 +229,26 @@ the incorrect URL with =//= and added the correct one on a new line: | ||
| 239 | 229 | ... |
| 240 | 230 | ) |
| 241 | 231 | > |
| 242 | #+end_src | |
| 232 | ``` | |
| 243 | 233 | |
| 244 | You can now exit the file with =Ctrl + x=, press =y= to save the file, | |
| 245 | and then click =Enter= to keep the same file name. | |
| 234 | You can now exit the file with `Ctrl + x`, press | |
| 235 | `y` to save the file, and then click `Enter` to | |
| 236 | keep the same file name. | |
| 246 | 237 | |
| 247 | 238 | Finally, just exit out of the docker container: |
| 248 | 239 | |
| 249 | #+begin_src sh | |
| 240 | ```sh | |
| 250 | 241 | exit |
| 251 | #+end_src | |
| 242 | ``` | |
| 252 | 243 | |
| 253 | 244 | Next, just restart the container: |
| 254 | 245 | |
| 255 | #+begin_src sh | |
| 246 | ```sh | |
| 256 | 247 | sudo docker restart freshrss |
| 257 | #+end_src | |
| 248 | ``` | |
| 258 | 249 | |
| 259 | 250 | Voilà! Your API check should now "PASS" and you should be able to use |
| 260 | 251 | one of the API URLs in your RSS apps. |
| 261 | 252 | |
| 262 | In my case, I use [[https://netnewswire.com][NetNewsWire]] on my desktop | |
| 253 | In my case, I use [NetNewsWire](https://netnewswire.com) on my desktop | |
| 263 | 254 | and phone. |
blog/2022-06-16-terminal-lifestyle.org → content/blog/2022-06-16-terminal-lifestyle.md renamed +88 −103
| @@ -1,24 +1,25 @@ | ||
| 1 | #+title: A Terminal Lifestyle | |
| 2 | #+date: 2022-06-16 | |
| 1 | +++ | |
| 2 | date = 2022-06-16 | |
| 3 | title = "A Terminal Lifestyle" | |
| 4 | description = "Explaining how I live my digital life mostly in the console/terminal." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Text-Based Simplicity | |
| 3 | 8 | |
| 4 | ** Text-Based Simplicity | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: text-based-simplicity | |
| 7 | :END: | |
| 8 | 9 | I've detailed my views on web-based minimalism and related topics in |
| 9 | 10 | other posts throughout the years; e.g., JavaScript/CSS bloat slowing |
| 10 | 11 | down websites that are essentially a text document. However, I have |
| 11 | 12 | never really expanded beyond talking about the web and describing how I |
| 12 | 13 | focus on minimizing distractions in other digital environments. |
| 13 | 14 | |
| 14 | This post is going to set the baseline for how I /try/ to live my | |
| 15 | This post is going to set the baseline for how I *try* to live my | |
| 15 | 16 | digital life. It does not necessarily get into my physical life, which |
| 16 | 17 | is often harder to control and contain all the noise in our modern |
| 17 | 18 | world. |
| 18 | 19 | |
| 19 | 20 | While there are new things to do every day in our digital world, I find |
| 20 | 21 | that keeping a core set of values and interests can ground you and keep |
| 21 | you mindful of /why/ you are participating in the digital world. For | |
| 22 | you mindful of *why* you are participating in the digital world. For | |
| 22 | 23 | example, if - at your core - you have no interest in what strangers |
| 23 | 24 | think about random topics, it would be unwise to start participating in |
| 24 | 25 | social media. However, I am someone who has been dragged in by effective |
| @@ -29,14 +30,12 @@ I won't dive much further into explaining the philosophy of all this, | ||
| 29 | 30 | but I will link a few helpful articles that may pique your interest if |
| 30 | 31 | you're in search of more meaningful experiences: |
| 31 | 32 | |
| 32 | - [[https://en.wikipedia.org/wiki/Mindfulness][Mindfulness]] | |
| 33 | - [[https://en.wikipedia.org/wiki/Minimalism][Minimalism]] | |
| 34 | - [[https://en.wikipedia.org/wiki/Stoicism][Stoicism]] | |
| 33 | - [Mindfulness](https://en.wikipedia.org/wiki/Mindfulness) | |
| 34 | - [Minimalism](https://en.wikipedia.org/wiki/Minimalism) | |
| 35 | - [Stoicism](https://en.wikipedia.org/wiki/Stoicism) | |
| 36 | ||
| 37 | ## Living Life in the Terminal | |
| 35 | 38 | |
| 36 | ** Living Life in the Terminal | |
| 37 | :PROPERTIES: | |
| 38 | :CUSTOM_ID: living-life-in-the-terminal | |
| 39 | :END: | |
| 40 | 39 | My personal approach to reducing digital distractions and increasing my |
| 41 | 40 | focus on the task at hand is to use a terminal for as much as I possibly |
| 42 | 41 | can. |
| @@ -44,12 +43,12 @@ can. | ||
| 44 | 43 | Most days, this means that I have a few tabs open constantly in my |
| 45 | 44 | terminal: |
| 46 | 45 | |
| 47 | 1. A web browser | |
| 48 | 2. A chat client | |
| 49 | 3. An email client | |
| 50 | 4. An RSS feed reader | |
| 51 | 5. A local shell for navigating my computer's files | |
| 52 | 6. A remote shell for managing servers and other machines | |
| 46 | 1. A web browser | |
| 47 | 2. A chat client | |
| 48 | 3. An email client | |
| 49 | 4. An RSS feed reader | |
| 50 | 5. A local shell for navigating my computer's files | |
| 51 | 6. A remote shell for managing servers and other machines | |
| 53 | 52 | |
| 54 | 53 | Beyond this, I rarely open other tabs or GUI applications, unless |
| 55 | 54 | absolutely necessary. If you look, you may be surprised what can be |
| @@ -58,61 +57,56 @@ accomplished in the terminal. | ||
| 58 | 57 | For example, I have moved my music and entertainment downloads to the |
| 59 | 58 | terminal, along with my device VPN connections. I am exploring options |
| 60 | 59 | for moving my RSS subscriptions to something like |
| 61 | [[https://newsboat.org/][Newsboat]], so that I can read my daily | |
| 62 | articles without all the fuss. | |
| 60 | [Newsboat](https://newsboat.org/), so that I can read my daily articles | |
| 61 | without all the fuss. | |
| 63 | 62 | |
| 64 | 63 | Now that we have some examples out of the way, let's dive into the |
| 65 | 64 | specifics. |
| 66 | 65 | |
| 67 | *** Browsing the Web | |
| 68 | :PROPERTIES: | |
| 69 | :CUSTOM_ID: browsing-the-web | |
| 70 | :END: | |
| 71 | I'm going to start off with a hard topic for those who prefer to live in | |
| 72 | the terminal: web browsing. This task is made hard mostly by websites | |
| 66 | ### Browsing the Web | |
| 67 | ||
| 68 | I'm going to start off with a hard topic for those who prefer to live | |
| 69 | in the terminal: web browsing. This task is made hard mostly by websites | |
| 73 | 70 | and web apps that require JavaScript to run. The other difficult part is |
| 74 | that if you're using a text-based browser, that means images won't load | |
| 75 | (hopefully that's obvious). | |
| 71 | that if you're using a text-based browser, that means images won't | |
| 72 | load (hopefully that's obvious). | |
| 76 | 73 | |
| 77 | I am using [[https://lynx.invisible-island.net][Lynx]], a text-based | |
| 74 | I am using [Lynx](https://lynx.invisible-island.net), a text-based | |
| 78 | 75 | browser that runs quickly and easily in the terminal. Lynx allows me to |
| 79 | browser most websites by simply typing =g= and then typing in the URL I | |
| 80 | want. | |
| 76 | browser most websites by simply typing `g` and then typing in | |
| 77 | the URL I want. | |
| 81 | 78 | |
| 82 | #+caption: Lynx | |
| 83 | [[https://img.cleberg.net/blog/20220616-terminal-lifestyle/lynx.png]] | |
| 79 |  | |
| 84 | 80 | |
| 85 | If you need a search engine while in Lynx, I recommend | |
| 86 | [[https://lite.duckduckgo.com/lite/][DuckDuckGo (Lite)]], which allows | |
| 87 | you to search the web using their text-only interface. | |
| 81 | If you need a search engine while in Lynx, I recommend [DuckDuckGo | |
| 82 | (Lite)](https://lite.duckduckgo.com/lite/), which allows you to search | |
| 83 | the web using their text-only interface. | |
| 88 | 84 | |
| 89 | #+caption: DuckDuckGo !Lite | |
| 90 | [[https://img.cleberg.net/blog/20220616-terminal-lifestyle/ddg.png]] | |
| 85 |  | |
| 91 | 87 | |
| 92 | 88 | Eventually, you will run into websites that don't work (or are just too |
| 93 | ugly and messy) in a text-only mode, and you'll be forced to switch over | |
| 94 | to a GUI browser to look at that site. Personally, I don't mind this as | |
| 95 | it doesn't happen as often as I thought it would. | |
| 89 | ugly and messy) in a text-only mode, and you'll be forced to switch | |
| 90 | over to a GUI browser to look at that site. Personally, I don't mind | |
| 91 | this as it doesn't happen as often as I thought it would. | |
| 96 | 92 | |
| 97 | 93 | The only time I need to do this is when I want to browse an |
| 98 | 94 | image/video-focused webpage or if I need to log in to a site, and it |
| 99 | doesn't support a text-only login page. For example, I am able to easily | |
| 100 | log in to [[https://sr.ht][Sourcehut]] in lynx. | |
| 95 | doesn't support a text-only login page. For example, I am able to | |
| 96 | easily log in to [Sourcehut](https://sr.ht) in lynx. | |
| 97 | ||
| 98 | ### Chatting with Friends | |
| 101 | 99 | |
| 102 | *** Chatting with Friends | |
| 103 | :PROPERTIES: | |
| 104 | :CUSTOM_ID: chatting-with-friends | |
| 105 | :END: | |
| 106 | 100 | After web browsing activities, my main form of terminal communication is |
| 107 | Matrix. I use the [[https://docs.mau.fi/gomuks/][gomuks]] client | |
| 101 | Matrix. I use the [gomuks](https://docs.mau.fi/gomuks/) client | |
| 108 | 102 | currently. |
| 109 | 103 | |
| 110 | 104 | This was incredibly easy to install on macOS (but I will need to see if |
| 111 | 105 | it'll be just as easy on Linux when my new laptop arrives): |
| 112 | 106 | |
| 113 | #+begin_src sh | |
| 107 | ```sh | |
| 114 | 108 | brew install gomuks |
| 115 | #+end_src | |
| 109 | ``` | |
| 116 | 110 | |
| 117 | 111 | Once you launch gomuks, it will sync and require your username and |
| 118 | 112 | password to login. After doing so, the only problem I ran into was |
| @@ -120,111 +114,102 @@ verifying my gomuks client so that I could participate in rooms with | ||
| 120 | 114 | E2EE. |
| 121 | 115 | |
| 122 | 116 | Finally, I was able to verify the session by opening the Element desktop |
| 123 | app (I assume you can do this in the browser and mobile app too, but I'm | |
| 124 | not sure) and manually verifying myself with this process: | |
| 117 | app (I assume you can do this in the browser and mobile app too, but | |
| 118 | I'm not sure) and manually verifying myself with this process: | |
| 125 | 119 | |
| 126 | 1. Open the Element desktop app | |
| 127 | 2. Open a room I was a member of | |
| 128 | 3. Open the =Room Info= pane | |
| 129 | 4. Open the =People= menu and search for myself | |
| 130 | 5. Click on my profile name | |
| 131 | 6. Click on the session link under the =Security= section and follow the | |
| 132 | prompts to manually verify the session | |
| 120 | 1. Open the Element desktop app | |
| 121 | 2. Open a room I was a member of | |
| 122 | 3. Open the `Room Info` pane | |
| 123 | 4. Open the `People` menu and search for myself | |
| 124 | 5. Click on my profile name | |
| 125 | 6. Click on the session link under the `Security` section | |
| 126 | and follow the prompts to manually verify the session | |
| 133 | 127 | |
| 134 | 128 | Overall, I like gomuks and am able to enjoy all the features I was using |
| 135 | 129 | in Element. The only hiccup I have occurred is manually downloading |
| 136 | 130 | images to view them, which can be annoying. |
| 137 | 131 | |
| 138 | #+caption: gomuks | |
| 139 | [[https://img.cleberg.net/blog/20220616-terminal-lifestyle/gomuks.png]] | |
| 132 |  | |
| 133 | ||
| 134 | ||
| 140 | 135 | |
| 141 | ||
| 142 | :PROPERTIES: | |
| 143 | :CUSTOM_ID: email | |
| 144 | :END: | |
| 145 | 136 | Moving email to the terminal has been the hardest of the tasks for me. |
| 146 | 137 | Unlike web browsing, where I can simply decide to not look at a website |
| 147 | 138 | that does not work in the terminal, I cannot simply ignore emails sent |
| 148 | 139 | to me. |
| 149 | 140 | |
| 150 | Personally, I am experimenting with [[https://neomutt.org/][neomutt]] as | |
| 151 | a potential email client. | |
| 141 | Personally, I am experimenting with [neomutt](https://neomutt.org/) as a | |
| 142 | potential email client. | |
| 152 | 143 | |
| 153 | However, this requires a *TON* of configuration and tweaking to get | |
| 144 | However, this requires a **TON** of configuration and tweaking to get | |
| 154 | 145 | right. Even when I was able to set up neomutt, configure my email |
| 155 | 146 | account, and customize a few personal preferences, a lot of emails still |
| 156 | 147 | do not display correctly (mostly due to HTML and images). |
| 157 | 148 | |
| 158 | I won't get into the details of configuring =neomutt=; I mostly followed | |
| 159 | this blog post: | |
| 160 | [[https://gideonwolfe.com/posts/workflow/neomutt/intro/][Email in the | |
| 161 | Terminal: Configuring Neomutt]]. | |
| 149 | I won't get into the details of configuring `neomutt`; I | |
| 150 | mostly followed this blog post: [Email in the Terminal: Configuring | |
| 151 | Neomutt](https://gideonwolfe.com/posts/workflow/neomutt/intro/). | |
| 162 | 152 | |
| 163 | 153 | Finally, I have yet to figure out how to connect my GPG keys to |
| 164 | =neomutt=, but that's a problem for another day. | |
| 154 | `neomutt`, but that's a problem for another day. | |
| 155 | ||
| 156 | ### RSS Feed Reader | |
| 165 | 157 | |
| 166 | *** RSS Feed Reader | |
| 167 | :PROPERTIES: | |
| 168 | :CUSTOM_ID: rss-feed-reader | |
| 169 | :END: | |
| 170 | I have just started using [[https://newsboat.org/][Newsboat]] to read | |
| 158 | I have just started using [Newsboat](https://newsboat.org/) to read | |
| 171 | 159 | articles in my terminal and have found quick success with it. |
| 172 | 160 | |
| 173 | 161 | I'll show you a quick screenshot first: |
| 174 | 162 | |
| 175 | #+caption: Newsboat | |
| 176 | [[https://img.cleberg.net/blog/20220616-terminal-lifestyle/newsboat.png]] | |
| 163 |  | |
| 177 | 164 | |
| 178 | 165 | The configuration was super easy for this app; I simply installed the |
| 179 | 166 | app, created a file for URLs, and imported my OPML subscriptions that I |
| 180 | 167 | had exported out of my old feed reader: |
| 181 | 168 | |
| 182 | #+begin_src sh | |
| 169 | ```sh | |
| 183 | 170 | brew install newsboat |
| 184 | #+end_src | |
| 171 | ``` | |
| 185 | 172 | |
| 186 | #+begin_src sh | |
| 173 | ```sh | |
| 187 | 174 | touch ~/.newsboat/urls |
| 188 | #+end_src | |
| 175 | ``` | |
| 189 | 176 | |
| 190 | #+begin_src sh | |
| 177 | ```sh | |
| 191 | 178 | newsboat -i=my_subscriptions.opml |
| 192 | #+end_src | |
| 179 | ``` | |
| 180 | ||
| 181 | ### Writing & Programming | |
| 193 | 182 | |
| 194 | *** Writing & Programming | |
| 195 | :PROPERTIES: | |
| 196 | :CUSTOM_ID: writing-programming | |
| 197 | :END: | |
| 198 | 183 | Unfortunately, the weak link in my terminal-based environment right now |
| 199 | 184 | is my grasp of the possibilities of editing files within a shell. |
| 200 | 185 | |
| 201 | 186 | I am used to the easy extensions found in VSCodium and Kate, so I am |
| 202 | 187 | slowly learning how to mold the default editing tools to my needs. |
| 203 | Currently, this means I am using =nano= with the following | |
| 188 | Currently, this means I am using `nano` with the following | |
| 204 | 189 | configuration: |
| 205 | 190 | |
| 206 | #+begin_src config | |
| 191 | ```config | |
| 207 | 192 | set breaklonglines |
| 208 | 193 | set autoindent |
| 209 | 194 | set linenumbers |
| 210 | 195 | set tabstospaces |
| 211 | 196 | set tabsize 2 |
| 212 | 197 | set fill 80 |
| 213 | #+end_src | |
| 198 | ``` | |
| 214 | 199 | |
| 215 | 200 | This configuration allows nano to automatically hard-wrap lines at 80 |
| 216 | 201 | characters, autoindent the wrapped lines (if the previous line was |
| 217 | 202 | indented), use 2 spaces per tab, and display line numbers within each |
| 218 | 203 | file I open. |
| 219 | 204 | |
| 220 | I am currently looking to see if =vim= or =emacs= would be more useful | |
| 221 | for my current needs, but I'm not in any rush, so I don't expect to find | |
| 222 | an answer anytime soon. | |
| 205 | I am currently looking to see if `vim` or `emacs` | |
| 206 | would be more useful for my current needs, but I'm not in any rush, so | |
| 207 | I don't expect to find an answer anytime soon. | |
| 223 | 208 | |
| 224 | 209 | With my current life demands, I am not programming at the moment and |
| 225 | 210 | have not explored the best terminal set-up for programming. However, I |
| 226 | have seen many peers find success configuring =vim= and =emacs=, so | |
| 227 | that's where I will start when I pick my projects back up. | |
| 211 | have seen many peers find success configuring `vim` and | |
| 212 | `emacs`, so that's where I will start when I pick my | |
| 213 | projects back up. | |
| 228 | 214 | |
| 229 | #+caption: nano | |
| 230 | [[https://img.cleberg.net/blog/20220616-terminal-lifestyle/nano.png]] | |
| 215 |  | |
blog/2022-06-22-daily-poetry.org → content/blog/2022-06-22-daily-poetry.md renamed +79 −99
| @@ -1,18 +1,17 @@ | ||
| 1 | #+title: Daily Plaintext Poetry via Email | |
| 2 | #+date: 2022-06-22 | |
| 1 | +++ | |
| 2 | date = 2022-06-22 | |
| 3 | title = "Daily Plaintext Poetry via Email" | |
| 4 | description = "A small project to automatically deliver poetry to your inbox daily." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Source Code | |
| 3 | 8 | |
| 4 | ** Source Code | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: source-code | |
| 7 | :END: | |
| 8 | 9 | I don't want to bury the lede here, so if you'd like to see the full |
| 9 | 10 | source code I use to email myself plaintext poems daily, visit the |
| 10 | repository: [[https://git.sr.ht/~cmc/daily-poem/][daily-poem]]. | |
| 11 | repository: [daily-poem](https://git.sr.ht/~cmc/daily-poem/). | |
| 12 | ||
| 13 | ## My Daily Dose of Poetry | |
| 11 | 14 | |
| 12 | ** My Daily Dose of Poetry | |
| 13 | :PROPERTIES: | |
| 14 | :CUSTOM_ID: my-daily-dose-of-poetry | |
| 15 | :END: | |
| 16 | 15 | Most of my programming projects are small, random projects that are made |
| 17 | 16 | strictly to fix some small problem I have or enhance my quality of life. |
| 18 | 17 | |
| @@ -26,55 +25,47 @@ each morning, as I know I would simply give up and stop reading daily. | ||
| 26 | 25 | Thus, I found a way to deliver poetry to myself in plain-text format, on |
| 27 | 26 | a daily basis, and scheduled to deliver automatically. |
| 28 | 27 | |
| 29 | ** Prerequisites | |
| 30 | :PROPERTIES: | |
| 31 | :CUSTOM_ID: prerequisites | |
| 32 | :END: | |
| 28 | ## Prerequisites | |
| 29 | ||
| 33 | 30 | This solution uses Python and email, so the following process requires |
| 34 | 31 | the following to be installed: |
| 35 | 32 | |
| 36 | 1. An SMTP server, which can be as easy as installing =mailutils= if | |
| 37 | you're on a Debian-based distro. | |
| 38 | 2. Python (& pip!) | |
| 39 | 3. The following Python packages: =email=, =smtplib=, =json=, and | |
| 40 | =requests= | |
| 33 | 1. An SMTP server, which can be as easy as installing | |
| 34 | `mailutils` if you're on a Debian-based distro. | |
| 35 | 2. Python (& pip!) | |
| 36 | 3. The following Python packages: `email`, | |
| 37 | `smtplib`, `json`, and `requests` | |
| 38 | ||
| 39 | ## Breaking Down the Logic | |
| 41 | 40 | |
| 42 | ** Breaking Down the Logic | |
| 43 | :PROPERTIES: | |
| 44 | :CUSTOM_ID: breaking-down-the-logic | |
| 45 | :END: | |
| 46 | 41 | I want to break down the logic for this program, as it's quite simple |
| 47 | 42 | and informational. |
| 48 | 43 | |
| 49 | *** Required Packages | |
| 50 | :PROPERTIES: | |
| 51 | :CUSTOM_ID: required-packages | |
| 52 | :END: | |
| 44 | ### Required Packages | |
| 45 | ||
| 53 | 46 | This program starts with a simple import of the required packages, so I |
| 54 | 47 | wanted to explain why each package is used: |
| 55 | 48 | |
| 56 | #+begin_src py | |
| 49 | ```python | |
| 57 | 50 | from email.mime.text import MIMEText # Required for translating MIMEText |
| 58 | 51 | import smtplib # Required to process the SMTP mail delivery |
| 59 | 52 | import json # Required to parse the poetry API results |
| 60 | 53 | import requests # Required to send out a request to the API |
| 61 | #+end_src | |
| 54 | ``` | |
| 55 | ||
| 56 | ### Sending the API Request | |
| 62 | 57 | |
| 63 | *** Sending the API Request | |
| 64 | :PROPERTIES: | |
| 65 | :CUSTOM_ID: sending-the-api-request | |
| 66 | :END: | |
| 67 | 58 | Next, we need to actually send the API request. In my case, I'm calling |
| 68 | 59 | a random poem from the entire API. If you want, you can call specific |
| 69 | 60 | poems or authors from this API. |
| 70 | 61 | |
| 71 | #+begin_src py | |
| 62 | ```python | |
| 72 | 63 | json_data = requests.get('https://poetrydb.org/random').json() |
| 73 | #+end_src | |
| 64 | ``` | |
| 74 | 65 | |
| 75 | 66 | This gives us the following result in JSON: |
| 76 | 67 | |
| 77 | #+begin_src json | |
| 68 | ```json | |
| 78 | 69 | [ |
| 79 | 70 | { |
| 80 | 71 | "title": "Sonnet XXII: With Fools and Children", |
| @@ -100,46 +91,40 @@ This gives us the following result in JSON: | ||
| 100 | 91 | "linecount": "15" |
| 101 | 92 | } |
| 102 | 93 | ] |
| 103 | #+end_src | |
| 94 | ``` | |
| 104 | 95 | |
| 105 | *** Parsing the API Results | |
| 106 | :PROPERTIES: | |
| 107 | :CUSTOM_ID: parsing-the-api-results | |
| 108 | :END: | |
| 109 | In order to parse this into a readable format, we need to use the =json= | |
| 110 | package and extract the fields we want. In the example below, I am | |
| 111 | grabbing every field presented by the API. | |
| 112 | ||
| 113 | For the actual poem content, we need to loop over each line in the | |
| 114 | =lines= variable since each line is a separate string by default. | |
| 96 | ### Parsing the API Results | |
| 115 | 97 | |
| 116 | #+begin_quote | |
| 117 | You /could/ also extract the title or author and make another call out | |
| 118 | to the API to avoid having to build the plaintext poem with a loop, but | |
| 119 | it just doesn't make sense to me to send multiple requests when we can | |
| 120 | create a simple loop on our local machine to work with the data we | |
| 121 | already have. | |
| 98 | In order to parse this into a readable format, we need to use the | |
| 99 | `json` package and extract the fields we want. In the example | |
| 100 | below, I am grabbing every field presented by the API. | |
| 122 | 101 | |
| 123 | For | |
| 124 | [[https://poetrydb.org/title/Sonnet%20XXII:%20With%20Fools%20and%20Children/lines.text][example]], | |
| 125 | look at the raw data response of this link to see the poem's lines | |
| 126 | returned in plaintext. | |
| 127 | ||
| 128 | #+end_quote | |
| 129 | ||
| 130 | #+begin_src py | |
| 102 | For the actual poem content, we need to loop over each line in the | |
| 103 | `lines` variable since each line is a separate string by | |
| 104 | default. | |
| 105 | ||
| 106 | > You *could* also extract the title or author and make another call out | |
| 107 | > to the API to avoid having to build the plaintext poem with a loop, | |
| 108 | > but it just doesn't make sense to me to send multiple requests when | |
| 109 | > we can create a simple loop on our local machine to work with the data | |
| 110 | > we already have. | |
| 111 | > | |
| 112 | > For | |
| 113 | > [example](https://poetrydb.org/title/Sonnet%20XXII:%20With%20Fools%20and%20Children/lines.text), | |
| 114 | > look at the raw data response of this link to see the poem's lines | |
| 115 | > returned in plaintext. | |
| 116 | ||
| 117 | ```python | |
| 131 | 118 | title = json_data[0]['title'] |
| 132 | 119 | author = json_data[0]['author'] |
| 133 | 120 | line_count = json_data[0]['linecount'] |
| 134 | 121 | lines = '' |
| 135 | 122 | for line in json_data[0]['lines']: |
| 136 | 123 | lines = lines + line + "\n" |
| 137 | #+end_src | |
| 124 | ``` | |
| 125 | ||
| 126 | ### Composing the Email | |
| 138 | 127 | |
| 139 | *** Composing the Email | |
| 140 | :PROPERTIES: | |
| 141 | :CUSTOM_ID: composing-the-email | |
| 142 | :END: | |
| 143 | 128 | Now that I have all the data I need, I just need to compose it into a |
| 144 | 129 | message and prepare the message metadata. |
| 145 | 130 | |
| @@ -148,47 +133,43 @@ by the author, then a blank line, and finally the full poem. This code | ||
| 148 | 133 | snippet combines that data and packages it into a MIMEText container, |
| 149 | 134 | ready to be emailed. |
| 150 | 135 | |
| 151 | #+begin_src py | |
| 136 | ```python | |
| 152 | 137 | msg_body = title + "\n" + author + "\n\n" + lines |
| 153 | 138 | msg = MIMEText(msg_body) |
| 154 | #+end_src | |
| 139 | ``` | |
| 155 | 140 | |
| 156 | 141 | Before we send the email, we need to prepare the metadata (subject, |
| 157 | 142 | from, to, etc.): |
| 158 | 143 | |
| 159 | #+begin_src py | |
| 144 | ```python | |
| 160 | 145 | sender_email = 'example@server.local' |
| 161 | 146 | recipient_emails = ['user@example.com'] |
| 162 | 147 | msg['Subject'] = 'Your Daily Poem (' + line_count + ' lines)' |
| 163 | 148 | msg['From'] = sender_email |
| 164 | 149 | msg['To'] = recipient_email |
| 165 | #+end_src | |
| 150 | ``` | |
| 151 | ||
| 152 | ### Sending the Email | |
| 166 | 153 | |
| 167 | *** Sending the Email | |
| 168 | :PROPERTIES: | |
| 169 | :CUSTOM_ID: sending-the-email | |
| 170 | :END: | |
| 171 | 154 | Now that I have everything ready to be emailed, the last step is to |
| 172 | 155 | simply connect to an SMTP server and send the email out to the |
| 173 | recipients. In my case, I installed =mailutils= on Ubuntu and let my | |
| 174 | SMTP server be =localhost=. | |
| 156 | recipients. In my case, I installed `mailutils` on Ubuntu and | |
| 157 | let my SMTP server be `localhost`. | |
| 175 | 158 | |
| 176 | #+begin_src py | |
| 159 | ```python | |
| 177 | 160 | smtp_server = 'localhost' |
| 178 | 161 | s = smtplib.SMTP(smtp_server) |
| 179 | 162 | s.sendmail(sender_email, recipient_emails, msg.as_string()) |
| 180 | 163 | s.quit() |
| 181 | #+end_src | |
| 164 | ``` | |
| 165 | ||
| 166 | ## The Result! | |
| 182 | 167 | |
| 183 | ** The Result! | |
| 184 | :PROPERTIES: | |
| 185 | :CUSTOM_ID: the-result | |
| 186 | :END: | |
| 187 | Instead of including a screenshot, I've copied the contents of the email | |
| 188 | that was delivered to my inbox below since I set this process up in | |
| 189 | plaintext format. | |
| 168 | Instead of including a screenshot, I've copied the contents of the | |
| 169 | email that was delivered to my inbox below since I set this process up | |
| 170 | in plaintext format. | |
| 190 | 171 | |
| 191 | #+begin_src txt | |
| 172 | ```txt | |
| 192 | 173 | Date: Wed, 22 Jun 2022 14:37:19 +0000 (UTC) |
| 193 | 174 | From: REDACTED |
| 194 | 175 | To: REDACTED |
| @@ -214,26 +195,25 @@ You that behold us, laugh us not to scorn; | ||
| 214 | 195 | Give Nature thanks you are not such as we. |
| 215 | 196 | Yet fools and children sometimes tell in play |
| 216 | 197 | Some, wise in show, more fools indeed than they. |
| 217 | #+end_src | |
| 198 | ``` | |
| 199 | ||
| 200 | ## Scheduling the Daily Email | |
| 218 | 201 | |
| 219 | ** Scheduling the Daily Email | |
| 220 | :PROPERTIES: | |
| 221 | :CUSTOM_ID: scheduling-the-daily-email | |
| 222 | :END: | |
| 223 | Last, but not least, is scheduling this Python script with =crontab=. To | |
| 224 | schedule a script to run daily, you can add it to the =crontab= file. To | |
| 225 | do this, open =crontab= in editing mode: | |
| 202 | Last, but not least, is scheduling this Python script with | |
| 203 | `crontab`. To schedule a script to run daily, you can add it | |
| 204 | to the `crontab` file. To do this, open `crontab` | |
| 205 | in editing mode: | |
| 226 | 206 | |
| 227 | #+begin_src sh | |
| 207 | ```sh | |
| 228 | 208 | crontab -e |
| 229 | #+end_src | |
| 209 | ``` | |
| 230 | 210 | |
| 231 | 211 | In the file, simply paste the following snippet at the bottom of the |
| 232 | 212 | file and ensure that the file path is correctly pointing to wherever you |
| 233 | 213 | saved your Python script: |
| 234 | 214 | |
| 235 | #+begin_src config | |
| 215 | ```config | |
| 236 | 216 | 0 8 * * * python3 /home/<your_user>/dailypoem/main.py |
| 237 | #+end_src | |
| 217 | ``` | |
| 238 | 218 | |
| 239 | 219 | We have now set up the script and scheduled it to run daily at 08:00! |
content/blog/2022-06-24-fedora-i3.md added +158
| @@ -0,0 +1,158 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-06-24 | |
| 3 | title = "Rebooting My Love Affair with Linux" | |
| 4 | description = "A retrospective on moving from macOS to Linux." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Leaving macOS | |
| 8 | ||
| 9 | As I noted [in a recent post](/blog/foss-macos-apps), I have been | |
| 10 | planning on migrating from macOS back to a Linux-based OS. I am happy to | |
| 11 | say that I have finally completed my migration and am now stuck in the | |
| 12 | wonderful world of Linux again. | |
| 13 | ||
| 14 | My decision to leave macOS really came down to just a few important | |
| 15 | things: | |
| 16 | ||
| 17 | - Apple Security (Gatekeeper) restricting me from running any software | |
| 18 | I want. Even if you disable Gatekeeper and allow software to bypass | |
| 19 | the rest of the device installation security, you still have to | |
| 20 | repeat that process every time the allowed software is updated. | |
| 21 | - macOS sends out nearly constant connections, pings, telemetry, etc. | |
| 22 | to a myriad of mysterious Apple services. I'm not even going to | |
| 23 | dive into how many macOS apps have constant telemetry on, as well. | |
| 24 | - Lastly, I just *really* missed the customization and freedom that | |
| 25 | comes with Linux. Being able to switch to entirely new kernel, OS, | |
| 26 | or desktop within minutes is a freedom I took for granted when I | |
| 27 | switched to macOS. | |
| 28 | ||
| 29 | Now that I've covered macOS, I'm going to move on to more exciting | |
| 30 | topics: my personal choice of OS, DE, and various customizations I'm | |
| 31 | using. | |
| 32 | ||
| 33 | ## Fedora | |
| 34 | ||
| 35 | After trying a ton of distros (I think I booted and tested around 20-25 | |
| 36 | distros), I finally landed on [Fedora Linux](https://getfedora.org/). I | |
| 37 | have quite a bit of experience with Fedora and enjoy the | |
| 38 | `dnf` package manager. Fedora allows me to keep up-to-date | |
| 39 | with recent software (I'm looking at you, Debian), but still provides a | |
| 40 | level of stability you don't find in every distro. | |
| 41 | ||
| 42 | In a very close second place was Arch Linux, as well as its spin-off: | |
| 43 | Garuda Linux (Garuda w/ sway is *beautiful*). Arch is great for | |
| 44 | compatibility and the massive community it has, but I have just never | |
| 45 | had the time to properly sit down and learn the methodology behind their | |
| 46 | packaging systems. | |
| 47 | ||
| 48 | Basically, everything else I tested was unacceptable in at least one way | |
| 49 | or another. Void (`glibc`) was great, but doesn't support | |
| 50 | all the software I need. Slackware worked well as a tui, but I wasn't | |
| 51 | skilled enough to get a tiling window manager (WM) working on it. | |
| 52 | ||
| 53 | ### i3 | |
| 54 | ||
| 55 | One of the reasons I settled on Fedora is that it comes with an official | |
| 56 | i3 spin. Being able to use a tiling WM, such as i3 or sway, is one of | |
| 57 | the biggest things I wanted to do as soon as I adopted Linux again. | |
| 58 | ||
| 59 | I will probably set up a dotfile repository soon, so that I don't lose | |
| 60 | any of my configurations, but nothing big has been configured thus far. | |
| 61 | ||
| 62 | The two main things I have updated in i3wm are natural scrolling and | |
| 63 | binding my brightness keys to the `brightnessctl` program. | |
| 64 | ||
| 65 | 1. Natural Scrolling | |
| 66 | ||
| 67 | You can enable natural scrolling by opening the following file: | |
| 68 | ||
| 69 | ```sh | |
| 70 | sudo nano /usr/share/X11/xorg.conf.d/40-libinput.conf | |
| 71 | ``` | |
| 72 | ||
| 73 | Within the `40-libinput.conf` file, find the following | |
| 74 | input sections and enable the natural scrolling option. | |
| 75 | ||
| 76 | This is the `pointer` section: | |
| 77 | ||
| 78 | ```conf | |
| 79 | Section "InputClass" | |
| 80 | Identifier "libinput pointer catchall" | |
| 81 | MatchIsPointer "on" | |
| 82 | MatchDevicePath "/dev/input/event*" | |
| 83 | Driver "libinput" | |
| 84 | Option "NaturalScrolling" "True" | |
| 85 | EndSection | |
| 86 | ``` | |
| 87 | ||
| 88 | This is the `touchpad` section: | |
| 89 | ||
| 90 | ```conf | |
| 91 | Section "InputClass" | |
| 92 | Identifier "libinput touchpad catchall" | |
| 93 | MatchIsTouchpad "on" | |
| 94 | MatchDevicePath "/dev/input/event*" | |
| 95 | Driver "libinput" | |
| 96 | Option "NaturalScrolling" "True" | |
| 97 | EndSection | |
| 98 | ``` | |
| 99 | ||
| 100 | 2. Enabling Brightness Keys | |
| 101 | ||
| 102 | Likewise, enabling brightness key functionality is as simple as | |
| 103 | binding the keys to the `brightnessctl` program. | |
| 104 | ||
| 105 | To do this, open up your i3 config file. Mine is located here: | |
| 106 | ||
| 107 | ```sh | |
| 108 | nano /home/<my-user>/.config/i3/config | |
| 109 | ``` | |
| 110 | ||
| 111 | ```conf | |
| 112 | # Use brightnessctl to adjust brightness. | |
| 113 | bindsym XF86MonBrightnessDown exec --no-startup-id brightnessctl --min-val=2 -q set 3%- | |
| 114 | bindsym XF86MonBrightnessUp exec --no-startup-id brightnessctl -q set 3%+ | |
| 115 | ``` | |
| 116 | ||
| 117 | 3. `polybar` | |
| 118 | ||
| 119 | Instead of using the default `i3status` bar, I have opted | |
| 120 | to use `polybar` instead (as you can also see in the | |
| 121 | screenshot above). | |
| 122 | ||
| 123 | My config for this menu bar is basically just the default settings | |
| 124 | with modified colors and an added battery block to quickly show me | |
| 125 | the machine's battery info. | |
| 126 | ||
| 127 | 4. `alacritty` | |
| 128 | ||
| 129 | Not much to say on this part yet, as I haven't configured it much, | |
| 130 | but I installed `alacritty` as my default terminal, and I | |
| 131 | am using `zsh` and the shell. | |
| 132 | ||
| 133 | ## Software Choices | |
| 134 | ||
| 135 | Again, I'm not going to say much that I haven't said yet in other blog | |
| 136 | posts, so I'll just do a quick rundown of the apps I installed | |
| 137 | immediately after I set up the environment. | |
| 138 | ||
| 139 | Flatpak Apps: | |
| 140 | ||
| 141 | - Cryptomator | |
| 142 | - pCloud | |
| 143 | - Signal | |
| 144 | ||
| 145 | Fedora Packages: | |
| 146 | ||
| 147 | - gomuks | |
| 148 | - neomutt | |
| 149 | - neofetch | |
| 150 | - Firefox | |
| 151 | - uBlock Origin | |
| 152 | - Bitwarden | |
| 153 | - Stylus | |
| 154 | - Privacy Redirect | |
| 155 | ||
| 156 | Other: | |
| 157 | ||
| 158 | - exiftool | |
content/blog/2022-07-01-git-server.md added +654
| @@ -0,0 +1,654 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-07-01 | |
| 3 | title = "Self-Hosting a Personal Git Server" | |
| 4 | description = "A guide to self-hosting a Git server on your own server." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## My Approach to Self-Hosting Git | |
| 8 | ||
| 9 | I have often tried to self-host my Git repositories, but have always | |
| 10 | fallen short when I tried to find a suitable web interface to show on | |
| 11 | the front-end. | |
| 12 | ||
| 13 | After a few years, I have finally found a combination of methods that | |
| 14 | allow me to easily self-host my projects, view them on the web, and | |
| 15 | access them from anywhere. | |
| 16 | ||
| 17 | Before I dive into the details, I want to state a high-level summary of | |
| 18 | my self-hosted Git approach: | |
| 19 | ||
| 20 | - This method uses the `ssh://` (read & write) and | |
| 21 | `git://` (read-only) protocols for push and pull access. | |
| 22 | - For the `git://` protocol, I create a | |
| 23 | `git-daemon-export-ok` file in any repository that I | |
| 24 | want to be cloneable by anyone. | |
| 25 | - The web interface I am using (`cgit`) allows simple | |
| 26 | HTTP cloning by default. I do not disable this setting as I want | |
| 27 | beginners to be able to clone one of my repositories even if | |
| 28 | they don't know the proper method. | |
| 29 | - I am not enabling Smart HTTPS for any repositories. Updates to | |
| 30 | repositories must be pushed via SSH. | |
| 31 | - Beyond the actual repository management, I am using | |
| 32 | `cgit` for the front-end web interface. | |
| 33 | - If you use the `scan-path=<path>` configuration in | |
| 34 | the `cgitrc` configuration file to automatically find | |
| 35 | repositories, you can't exclude a repository from | |
| 36 | `cgit` if it's stored within the path that | |
| 37 | `cgit` reads. To host private repositories, you'd | |
| 38 | need to set up another directory that `cgit` can't | |
| 39 | read. | |
| 40 | ||
| 41 | ## Assumptions | |
| 42 | ||
| 43 | For the purposes of this walkthrough, I am assuming you have a URL | |
| 44 | (`git.example.com`) or IP address | |
| 45 | (`207.84.26.991`) addressed to the server that you will be | |
| 46 | using to host your git repositories. | |
| 47 | ||
| 48 | ## Adding a Git User | |
| 49 | ||
| 50 | In order to use the SSH method associated with git, we will need to add | |
| 51 | a user named `git`. If you have used the SSH method for other | |
| 52 | git hosting sites, you are probably used to the following syntax: | |
| 53 | ||
| 54 | ```sh | |
| 55 | git clone [user@]server:project.git | |
| 56 | ``` | |
| 57 | ||
| 58 | The syntax above is an `scp`-like syntax for using SSH on the | |
| 59 | `git` user on the server to access your repository. | |
| 60 | ||
| 61 | Let's delete any remnants of an old `git` user, if any, and | |
| 62 | create the new user account: | |
| 63 | ||
| 64 | ```sh | |
| 65 | sudo deluser --remove-home git | |
| 66 | sudo adduser git | |
| 67 | ``` | |
| 68 | ||
| 69 | ### Import Your SSH Keys to the Git User | |
| 70 | ||
| 71 | Once the `git` user is created, you will need to copy your | |
| 72 | public SSH key on your local development machine to the `git` | |
| 73 | user on the server. | |
| 74 | ||
| 75 | If you don't have an SSH key yet, create one with this command: | |
| 76 | ||
| 77 | ```sh | |
| 78 | ssh-keygen | |
| 79 | ``` | |
| 80 | ||
| 81 | Once you create the key pair, the public should be saved to | |
| 82 | `~/.ssh/id_rsa.pub`. | |
| 83 | ||
| 84 | If your server still has password-based authentication available, you | |
| 85 | can copy it over to your user's home directory like this: | |
| 86 | ||
| 87 | ```sh | |
| 88 | ssh-copy-id git@server | |
| 89 | ``` | |
| 90 | ||
| 91 | Otherwise, copy it over to any user that you can access. | |
| 92 | ||
| 93 | ```sh | |
| 94 | scp ~/.ssh/id_rsa.pub your_user@your_server: | |
| 95 | ``` | |
| 96 | ||
| 97 | Once on the server, you will need to copy the contents into the | |
| 98 | `git` user's `authorized_keys` file: | |
| 99 | ||
| 100 | ```sh | |
| 101 | cat id_rsa.pub > /home/git/.ssh/authorized_keys | |
| 102 | ``` | |
| 103 | ||
| 104 | ### (Optional) Disable Password-Based SSH | |
| 105 | ||
| 106 | If you want to lock down your server and ensure that no one can | |
| 107 | authenticate in via SSH with a password, you will need to edit your SSH | |
| 108 | configuration. | |
| 109 | ||
| 110 | ```sh | |
| 111 | sudo nano /etc/ssh/sshd_config | |
| 112 | ``` | |
| 113 | ||
| 114 | Within this file, find the following settings and set them to the values | |
| 115 | I am showing below: | |
| 116 | ||
| 117 | ```conf | |
| 118 | PermitRootLogin no | |
| 119 | PasswordAuthentication no | |
| 120 | AuthenticationMethods publickey | |
| 121 | ``` | |
| 122 | ||
| 123 | You may have other Authentication Methods required in your personal | |
| 124 | set-up, so the key here is just to ensure that | |
| 125 | `AuthenticationMethods` does not allow passwords. | |
| 126 | ||
| 127 | ## Setting up the Base Directory | |
| 128 | ||
| 129 | Now that we have set up a `git` user to handle all transport | |
| 130 | methods, we need to set up the directory that we will be using as our | |
| 131 | base of all repositories. | |
| 132 | ||
| 133 | In my case, I am using `/git` as my source folder. To create | |
| 134 | this folder and assign it to the user we created, execute the following | |
| 135 | commands: | |
| 136 | ||
| 137 | ```sh | |
| 138 | sudo mkdir /git | |
| 139 | sudo chown -R git:git /git | |
| 140 | ``` | |
| 141 | ||
| 142 | ## Creating a Test Repository | |
| 143 | ||
| 144 | On your server, switch over to the `git` user in order to | |
| 145 | start managing git files. | |
| 146 | ||
| 147 | ```sh | |
| 148 | su git | |
| 149 | ``` | |
| 150 | ||
| 151 | Once logged-in as the `git` user, go to your base directory | |
| 152 | and create a test repository. | |
| 153 | ||
| 154 | ```sh | |
| 155 | cd /git | |
| 156 | mkdir test.git && cd test.git | |
| 157 | git init --bare | |
| 158 | ``` | |
| 159 | ||
| 160 | If you want to make this repo viewable/cloneable to the public via the | |
| 161 | `git://` protocol, you need to create a | |
| 162 | `git-daemon-export-ok` file inside the repository. | |
| 163 | ||
| 164 | ```sh | |
| 165 | touch git-daemon-export-ok | |
| 166 | ``` | |
| 167 | ||
| 168 | ## Change the Login Shell for `git` | |
| 169 | ||
| 170 | To make sure that the `git` user is only used for git | |
| 171 | operations and nothing else, you need to change the user's login shell. | |
| 172 | To do this, simply use the `chsh` command: | |
| 173 | ||
| 174 | ```sh | |
| 175 | sudo chsh git | |
| 176 | ``` | |
| 177 | ||
| 178 | The interactive prompt will ask which shell you want the | |
| 179 | `git` user to use. You must use the following value: | |
| 180 | ||
| 181 | ```sh | |
| 182 | /usr/bin/git-shell | |
| 183 | ``` | |
| 184 | ||
| 185 | Once done, no one will be able to SSH to the `git` user or | |
| 186 | execute commands other than the standard git commands. | |
| 187 | ||
| 188 | ## Opening the Firewall | |
| 189 | ||
| 190 | Don't forget to open up ports on the device firewall and network | |
| 191 | firewall if you want to access these repositories publicly. If you're | |
| 192 | using default ports, forward ports `22` (ssh) and | |
| 193 | `9418` (git) from your router to your server's IP address. | |
| 194 | ||
| 195 | If your server also has a firewall, ensure that the firewall allows the | |
| 196 | same ports that are forwarded from the router. For example, if you use | |
| 197 | `ufw`: | |
| 198 | ||
| 199 | ```sh | |
| 200 | sudo ufw allow 22 | |
| 201 | sudo ufw allow 9418 | |
| 202 | ``` | |
| 203 | ||
| 204 | ### Non-Standard SSH Ports | |
| 205 | ||
| 206 | If you use a non-standard port for SSH, such as `9876`, you | |
| 207 | will need to create an SSH configuration file on your local development | |
| 208 | machine in order to connect to your server's git repositories. | |
| 209 | ||
| 210 | To do this, you'll need to define your custom port on your client | |
| 211 | machine in your `~/.ssh/config` file: | |
| 212 | ||
| 213 | ```sh | |
| 214 | nano ~/.ssh/config | |
| 215 | ``` | |
| 216 | ||
| 217 | ```conf | |
| 218 | Host git.example.com | |
| 219 | # HostName can be a URL or an IP address | |
| 220 | HostName git.example.com | |
| 221 | Port 9876 | |
| 222 | User git | |
| 223 | ``` | |
| 224 | ||
| 225 | ### Testing SSH | |
| 226 | ||
| 227 | There are two main syntaxes you can use to manage git over SSH: | |
| 228 | ||
| 229 | - `git clone [user@]server:project.git` | |
| 230 | - `git clone ssh://[user@]server/project.git` | |
| 231 | ||
| 232 | I prefer the first, which is an `scp`-like syntax. To test | |
| 233 | it, try to clone the test repository you set up on the server: | |
| 234 | ||
| 235 | ```sh | |
| 236 | git clone git@git.example.com:/git/test.git | |
| 237 | ``` | |
| 238 | ||
| 239 | ## Enabling Read-Only Access | |
| 240 | ||
| 241 | If you want people to be able to clone any repository where you've | |
| 242 | placed a `git-daemon-export-ok` file, you will need to start | |
| 243 | the git daemon. | |
| 244 | ||
| 245 | To do this on a system with `systemd`, create a service file: | |
| 246 | ||
| 247 | ```sh | |
| 248 | sudo nano /etc/systemd/system/git-daemon.service | |
| 249 | ``` | |
| 250 | ||
| 251 | Inside the `git-daemon.service` file, paste the following: | |
| 252 | ||
| 253 | ```conf | |
| 254 | [Unit] | |
| 255 | Description=Start Git Daemon | |
| 256 | ||
| 257 | [Service] | |
| 258 | ExecStart=/usr/bin/git daemon --reuseaddr --base-path=/git/ /git/ | |
| 259 | ||
| 260 | Restart=always | |
| 261 | RestartSec=500ms | |
| 262 | ||
| 263 | StandardOutput=syslog | |
| 264 | StandardError=syslog | |
| 265 | SyslogIdentifier=git-daemon | |
| 266 | ||
| 267 | User=git | |
| 268 | Group=git | |
| 269 | ||
| 270 | [Install] | |
| 271 | WantedBy=multi-user.target | |
| 272 | ``` | |
| 273 | ||
| 274 | Once created, enable and start the service: | |
| 275 | ||
| 276 | ```sh | |
| 277 | sudo systemctl enable git-daemon.service | |
| 278 | sudo systemctl start git-daemon.service | |
| 279 | ``` | |
| 280 | ||
| 281 | To clone read-only via the `git://` protocol, you can use the | |
| 282 | following syntax: | |
| 283 | ||
| 284 | ```sh | |
| 285 | git clone git://git.example.com/test.git | |
| 286 | ``` | |
| 287 | ||
| 288 | ## Migrating Repositories | |
| 289 | ||
| 290 | At this point, we have a working git server that works with both SSH and | |
| 291 | read-only access. | |
| 292 | ||
| 293 | For each of the repositories I had hosted a different provider, I | |
| 294 | executed the following commands in order to place a copy on my server as | |
| 295 | my new source of truth: | |
| 296 | ||
| 297 | Server: | |
| 298 | ||
| 299 | ```sh | |
| 300 | su git | |
| 301 | mkdir /git/<REPOSITORY_NAME>.git && cd /git/<REPOSITORY_NAME>.git | |
| 302 | git init --bare | |
| 303 | ||
| 304 | # If you want to make this repo viewable/cloneable to the public | |
| 305 | touch git-daemon-export-ok | |
| 306 | ``` | |
| 307 | ||
| 308 | Client: | |
| 309 | ||
| 310 | ```sh | |
| 311 | git clone git@<PREVIOUS_HOST>:<REPOSITORY_NAME> | |
| 312 | git remote set-url origin git@git.EXAMPLE.COM:/git/<REPOSITORY_NAME>.git | |
| 313 | git push | |
| 314 | ``` | |
| 315 | ||
| 316 | ## Optional Web View: `cgit` | |
| 317 | ||
| 318 | If you want a web viewer for your repositories, you can use various | |
| 319 | tools, such as `gitweb`, `cgit`, or | |
| 320 | `klaus`. I chose `cgit` due to its simple | |
| 321 | interface and fairly easy set-up (compared to others). Not to mention | |
| 322 | that the [Linux kernel uses `cgit`](https://git.kernel.org/). | |
| 323 | ||
| 324 | ### Docker Compose | |
| 325 | ||
| 326 | Instead of using my previous method of using a `docker run` | |
| 327 | command, I've updated this section to use `docker-compose` | |
| 328 | instead for an easier installation and simpler management and | |
| 329 | configuration. | |
| 330 | ||
| 331 | In order to use Docker Compose, you will set up a | |
| 332 | `docker-compose.yml` file to automatically connect resources | |
| 333 | like the repositories, `cgitrc`, and various files or folders | |
| 334 | to the `cgit` container you're creating: | |
| 335 | ||
| 336 | ```sh | |
| 337 | mkdir ~/cgit && cd ~/cgit | |
| 338 | nano docker-compose.yml | |
| 339 | ``` | |
| 340 | ||
| 341 | ```conf | |
| 342 | # docker-compose.yml | |
| 343 | version: '3' | |
| 344 | ||
| 345 | services: | |
| 346 | cgit: | |
| 347 | image: invokr/cgit | |
| 348 | volumes: | |
| 349 | - /git:/git | |
| 350 | - ./cgitrc:/etc/cgitrc | |
| 351 | - ./logo.png:/var/www/htdocs/cgit/logo.png | |
| 352 | - ./favicon.png:/var/www/htdocs/cgit/favicon.png | |
| 353 | - ./filters:/var/www/htdocs/cgit/filters | |
| 354 | ports: | |
| 355 | - "8763:80" | |
| 356 | restart: always | |
| 357 | ``` | |
| 358 | ||
| 359 | Then, just start the container: | |
| 360 | ||
| 361 | ```sh | |
| 362 | sudo docker-compose up -d | |
| 363 | ``` | |
| 364 | ||
| 365 | Once it's finished installing, you can access the site at | |
| 366 | `<SERVER_IP>:8763` or use a reverse-proxy service to forward | |
| 367 | `cgit` to a URL, such as `git.example.com`. See | |
| 368 | the next section for more details on reverse proxying a URL to a local | |
| 369 | port. | |
| 370 | ||
| 371 | ### Nginx Reverse Proxy | |
| 372 | ||
| 373 | I am using Nginx as my reverse proxy so that the `cgit` | |
| 374 | Docker container can use `git.example.com` as its URL. To do | |
| 375 | so, I simply created the following configuration file: | |
| 376 | ||
| 377 | ```sh | |
| 378 | sudo nano /etc/nginx/sites-available/git.example.com | |
| 379 | ``` | |
| 380 | ||
| 381 | ```conf | |
| 382 | server { | |
| 383 | listen 80; | |
| 384 | server_name git.example.com; | |
| 385 | ||
| 386 | if ($host = git.example.com) { | |
| 387 | return 301 https://$host$request_uri; | |
| 388 | } | |
| 389 | ||
| 390 | return 404; | |
| 391 | } | |
| 392 | ||
| 393 | server { | |
| 394 | server_name git.example.com; | |
| 395 | listen 443 ssl http2; | |
| 396 | ||
| 397 | location / { | |
| 398 | # The final `/` is important. | |
| 399 | proxy_pass http://localhost:8763/; | |
| 400 | add_header X-Frame-Options SAMEORIGIN; | |
| 401 | add_header X-XSS-Protection "1; mode=block"; | |
| 402 | proxy_redirect off; | |
| 403 | proxy_buffering off; | |
| 404 | proxy_set_header Host $host; | |
| 405 | proxy_set_header X-Real-IP $remote_addr; | |
| 406 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |
| 407 | proxy_set_header X-Forwarded-Proto $scheme; | |
| 408 | proxy_set_header X-Forwarded-Port $server_port; | |
| 409 | } | |
| 410 | ||
| 411 | # INCLUDE ANY SSL CERTS HERE | |
| 412 | include /etc/letsencrypt/options-ssl-nginx.conf; | |
| 413 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; | |
| 414 | } | |
| 415 | ``` | |
| 416 | ||
| 417 | Once created, symlink it and restart the web server. | |
| 418 | ||
| 419 | ```sh | |
| 420 | sudo ln -s /etc/nginx/sites-available/git.example.com /etc/nginx/sites-enabled/ | |
| 421 | sudo systemctl restart nginx.service | |
| 422 | ``` | |
| 423 | ||
| 424 | As we can see below, my site at `git.example.com` is | |
| 425 | available and running: | |
| 426 | ||
| 427 | ### Settings Up Git Details | |
| 428 | ||
| 429 | Once you have `cgit` running, you can add some small details, | |
| 430 | such as repository owners and descriptions by editing the following | |
| 431 | files within each repository. | |
| 432 | ||
| 433 | Alternatively, you can use the `cgitrc` file to edit these | |
| 434 | details if you only care to edit them for the purpose of seeing them on | |
| 435 | your website. | |
| 436 | ||
| 437 | The `description` file within the repository on your server | |
| 438 | will display the description online. | |
| 439 | ||
| 440 | ```sh | |
| 441 | cd /git/example.git | |
| 442 | nano description | |
| 443 | ``` | |
| 444 | ||
| 445 | You can add a `[gitweb]` block to the `config` | |
| 446 | file in order to display the owner of the repository. | |
| 447 | ||
| 448 | ```sh | |
| 449 | cd /git/example.git | |
| 450 | nano config | |
| 451 | ``` | |
| 452 | ||
| 453 | ```conf | |
| 454 | [gitweb] | |
| 455 | owner = "YourName" | |
| 456 | ``` | |
| 457 | ||
| 458 | Note that you can ignore the configuration within each repository and | |
| 459 | simply set up this information in the `cgitrc` file, if you | |
| 460 | want to do it that way. | |
| 461 | ||
| 462 | ### Editing `cgit` | |
| 463 | ||
| 464 | In order to edit certain items within `cgit`, you need to | |
| 465 | edit the `cgitrc` file. | |
| 466 | ||
| 467 | ```sh | |
| 468 | nano ~/cgit/cgitrc | |
| 469 | ``` | |
| 470 | ||
| 471 | Below is an example configuration for `cgitrc`. You can find | |
| 472 | all the configuration options within the [configuration manual] | |
| 473 | (<https://git.zx2c4.com/cgit/plain/cgitrc.5.txt>). | |
| 474 | ||
| 475 | ```conf | |
| 476 | css=/cgit.css | |
| 477 | logo=/logo.png | |
| 478 | favicon=/favicon.png | |
| 479 | robots=noindex, nofollow | |
| 480 | ||
| 481 | enable-index-links=1 | |
| 482 | enable-commit-graph=1 | |
| 483 | enable-blame=1 | |
| 484 | enable-log-filecount=1 | |
| 485 | enable-log-linecount=1 | |
| 486 | enable-git-config=1 | |
| 487 | ||
| 488 | clone-url=git://git.example.com/$CGIT_REPO_URL ssh://git@git.example.com:/git/$CGIT_REPO_URL | |
| 489 | ||
| 490 | root-title=My Git Website | |
| 491 | root-desc=My personal git repositories. | |
| 492 | ||
| 493 | # Allow download of tar.gz, tar.bz2 and zip-files | |
| 494 | snapshots=tar.gz tar.bz2 zip | |
| 495 | ||
| 496 | ## | |
| 497 | ## List of common mimetypes | |
| 498 | ## | |
| 499 | mimetype.gif=image/gif | |
| 500 | mimetype.html=text/html | |
| 501 | mimetype.jpg=image/jpeg | |
| 502 | mimetype.jpeg=image/jpeg | |
| 503 | mimetype.pdf=application/pdf | |
| 504 | mimetype.png=image/png | |
| 505 | mimetype.svg=image/svg+xml | |
| 506 | ||
| 507 | # Highlight source code | |
| 508 | # source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.sh | |
| 509 | source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.py | |
| 510 | ||
| 511 | # Format markdown, restructuredtext, manpages, text files, and html files | |
| 512 | # through the right converters | |
| 513 | about-filter=/var/www/htdocs/cgit/filters/about-formatting.sh | |
| 514 | ||
| 515 | ## | |
| 516 | ## Search for these files in the root of the default branch of repositories | |
| 517 | ## for coming up with the about page: | |
| 518 | ## | |
| 519 | readme=:README.md | |
| 520 | readme=:readme.md | |
| 521 | readme=:README.mkd | |
| 522 | readme=:readme.mkd | |
| 523 | readme=:README.rst | |
| 524 | readme=:readme.rst | |
| 525 | readme=:README.html | |
| 526 | readme=:readme.html | |
| 527 | readme=:README.htm | |
| 528 | readme=:readme.htm | |
| 529 | readme=:README.txt | |
| 530 | readme=:readme.txt | |
| 531 | readme=:README | |
| 532 | readme=:readme | |
| 533 | ||
| 534 | # Repositories | |
| 535 | ||
| 536 | # Uncomment the following line to scan a path instead of adding repositories manually | |
| 537 | # scan-path=/git | |
| 538 | ||
| 539 | ## Test Section | |
| 540 | section=git/test-section | |
| 541 | ||
| 542 | repo.url=test.git | |
| 543 | repo.path=/git/test.git | |
| 544 | repo.readme=:README.md | |
| 545 | repo.owner=John Doe | |
| 546 | repo.desc=An example repository! | |
| 547 | ``` | |
| 548 | ||
| 549 | ### Final Fixes: Syntax Highlighting & README Rendering | |
| 550 | ||
| 551 | After completing my initial install and playing around with it for a few | |
| 552 | days, I noticed two issues: | |
| 553 | ||
| 554 | 1. Syntax highlighting did not work when viewing the source code within | |
| 555 | a file. | |
| 556 | 2. The `about` tab within a repository was not rendered to | |
| 557 | HTML. | |
| 558 | ||
| 559 | The following process fixes these issues. To start, let's go to the | |
| 560 | `cgit` directory where we were editing our configuration file | |
| 561 | earlier. | |
| 562 | ||
| 563 | ```sh | |
| 564 | cd ~/cgit | |
| 565 | ``` | |
| 566 | ||
| 567 | In here, create two folders that will hold our syntax files: | |
| 568 | ||
| 569 | ```sh | |
| 570 | mkdir filters && mkdir filters/html-converters && cd filters | |
| 571 | ``` | |
| 572 | ||
| 573 | Next, download the default filters: | |
| 574 | ||
| 575 | ```sh | |
| 576 | curl https://git.zx2c4.com/cgit/plain/filters/about-formatting.sh > about-formatting.sh | |
| 577 | chmod 755 about-formatting.sh | |
| 578 | curl https://git.zx2c4.com/cgit/plain/filters/syntax-highlighting.py > syntax-highlighting.py | |
| 579 | chmod 755 syntax-highlighting.py | |
| 580 | ``` | |
| 581 | ||
| 582 | Finally, download the HTML conversion files you need. The example below | |
| 583 | downloads the Markdown converter: | |
| 584 | ||
| 585 | ```sh | |
| 586 | cd html-converters | |
| 587 | curl https://git.zx2c4.com/cgit/plain/filters/html-converters/md2html > md2html | |
| 588 | chmod 755 md2html | |
| 589 | ``` | |
| 590 | ||
| 591 | If you need other filters or html-converters found within [the cgit | |
| 592 | project files](https://git.zx2c4.com/cgit/tree/filters), repeat the | |
| 593 | `curl` and `chmod` process above for whichever | |
| 594 | files you need. | |
| 595 | ||
| 596 | However, formatting will not work quite yet since the Docker cgit | |
| 597 | container we're using doesn't have the formatting package installed. | |
| 598 | You can install this easily by install Python 3+ and the | |
| 599 | `pygments` package: | |
| 600 | ||
| 601 | ```sh | |
| 602 | # Enter the container's command line | |
| 603 | sudo docker exec -it cgit bash | |
| 604 | ``` | |
| 605 | ||
| 606 | ```sh | |
| 607 | # Install the necessary packages and then exit | |
| 608 | yum update -y && \ | |
| 609 | yum upgrade -y && \ | |
| 610 | yum install python3 python3-pip -y && \ | |
| 611 | pip3 install markdown pygments && \ | |
| 612 | exit | |
| 613 | ``` | |
| 614 | ||
| 615 | **You will need to enter the cgit docker container and re-run these | |
| 616 | `yum` commands every time you kill and restart the | |
| 617 | container!** | |
| 618 | ||
| 619 | If not done already, we need to add the following variables to our | |
| 620 | `cgitrc` file in order for `cgit` to know where | |
| 621 | our filtering files are: | |
| 622 | ||
| 623 | ```conf | |
| 624 | # Highlight source code with python pygments-based highlighter | |
| 625 | source-filter=/var/www/htdocs/cgit/filters/syntax-highlighting.py | |
| 626 | ||
| 627 | # Format markdown, restructuredtext, manpages, text files, and html files | |
| 628 | # through the right converters | |
| 629 | about-filter=/var/www/htdocs/cgit/filters/about-formatting.sh | |
| 630 | ``` | |
| 631 | ||
| 632 | Now you should see that syntax highlighting and README rendering to the | |
| 633 | `about` tab is fixed. | |
| 634 | ||
| 635 | ### Theming | |
| 636 | ||
| 637 | I won't go into much detail in this section, but you can fully theme | |
| 638 | your installation of `cgit` since you have access to the | |
| 639 | `cgit.css` file in your web root. This is another file you | |
| 640 | can add as a volume to the `docker-compose.yml` file if you | |
| 641 | want to edit this without entering the container's command line. | |
| 642 | ||
| 643 | ## :warning: Remember to Back Up Your Data! | |
| 644 | ||
| 645 | The last thing to note is that running services on your own equipment | |
| 646 | means that you're assuming a level of risk that exists regarding data | |
| 647 | loss, catastrophes, etc. In order to reduce the impact of any such | |
| 648 | occurrence, I suggest backing up your data regularly. | |
| 649 | ||
| 650 | Backups can be automated via `cron`, by hooking your base | |
| 651 | directory up to a cloud provider, or even setting up hooks to push all | |
| 652 | repository info to git mirrors on other git hosts. Whatever the method, | |
| 653 | make sure that your data doesn't vanish in the event that your drives | |
| 654 | or servers fail. | |
blog/2022-07-14-gnupg.org → content/blog/2022-07-14-gnupg.md renamed +114 −129
| @@ -1,31 +1,30 @@ | ||
| 1 | #+title: GNU Privacy Guard (GPG) | |
| 2 | #+date: 2022-07-14 | |
| 3 | ||
| 4 | ** The History of GPG | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: the-history-of-gpg | |
| 7 | :END: | |
| 8 | [[https://gnupg.org/][GNU Privacy Guard]], also known as GnuPG and GPG, | |
| 9 | is a free ("free" as in both speech and beer) software that fully | |
| 10 | implements the OpenPGP Message Format documented in | |
| 11 | [[https://www.rfc-editor.org/rfc/rfc4880][RFC 4880]]. | |
| 1 | +++ | |
| 2 | date = 2022-07-14 | |
| 3 | title = "GNU Privacy Guard (GPG)" | |
| 4 | description = "Learn how to create a PGP key with GNU Privacy Guard (GPG)." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## The History of GPG | |
| 8 | ||
| 9 | [GNU Privacy Guard](https://gnupg.org/), also known as GnuPG and GPG, is | |
| 10 | a free ("free" as in both speech and beer) software that fully | |
| 11 | implements the OpenPGP Message Format documented in [RFC | |
| 12 | 4880](https://www.rfc-editor.org/rfc/rfc4880). | |
| 12 | 13 | |
| 13 | 14 | I won't go in-depth on the full history of the software in this post, |
| 14 | 15 | but it is important to understand that GPG is not the same as PGP |
| 15 | 16 | (Pretty Good Privacy), which is a different implementation of RFC 4880. |
| 16 | 17 | However, GPG was designed to interoperate with PGP. |
| 17 | 18 | |
| 18 | GPG was originally developed in the late 1990s by | |
| 19 | [[https://en.wikipedia.org/wiki/Werner_Koch][Werner Koch]] and has | |
| 20 | historically been funded generously by the German government. | |
| 19 | GPG was originally developed in the late 1990s by [Werner | |
| 20 | Koch](https://en.wikipedia.org/wiki/Werner_Koch) and has historically | |
| 21 | been funded generously by the German government. | |
| 22 | ||
| 23 | Now that we have all the high-level info out of the way, let's dive | |
| 24 | into the different aspects of GPG and its uses. | |
| 21 | 25 | |
| 22 | Now that we have all the high-level info out of the way, let's dive into | |
| 23 | the different aspects of GPG and its uses. | |
| 26 | ## Encryption Algorithms | |
| 24 | 27 | |
| 25 | ** Encryption Algorithms | |
| 26 | :PROPERTIES: | |
| 27 | :CUSTOM_ID: encryption-algorithms | |
| 28 | :END: | |
| 29 | 28 | GPG supports a wide range of different encryption algorithms, including |
| 30 | 29 | public-key, cipher, hash, and compression algorithms. The support for |
| 31 | 30 | these algorithms has grown since the adoption of the Libgcrypt library |
| @@ -35,7 +34,7 @@ As you will be able to see below in an example of a full key generation | ||
| 35 | 34 | with the GPG command line tool, GPG recommends the following algorithms |
| 36 | 35 | to new users: |
| 37 | 36 | |
| 38 | #+begin_src sh | |
| 37 | ```sh | |
| 39 | 38 | Please select what kind of key you want: |
| 40 | 39 | (1) RSA and RSA |
| 41 | 40 | (2) DSA and Elgamal |
| @@ -43,75 +42,72 @@ Please select what kind of key you want: | ||
| 43 | 42 | (4) RSA (sign only) |
| 44 | 43 | (9) ECC (sign and encrypt) *default* |
| 45 | 44 | (10) ECC (sign only) |
| 46 | #+end_src | |
| 45 | ``` | |
| 47 | 46 | |
| 48 | 47 | I am not doing an in-depth explanation here in order to keep the focus |
| 49 | 48 | on GPG and not encryption algorithms. If you want a deep dive into |
| 50 | 49 | cryptography or encryption algorithms, please read my other posts: |
| 51 | 50 | |
| 52 | - [[/blog/aes-encryption/][AES Encryption]] (2018) | |
| 53 | - [[/blog/cryptography-basics/][Cryptography Basics]] (2020) | |
| 51 | - [AES Encryption](/blog/aes-encryption/) (2018) | |
| 52 | - [Cryptography Basics](/blog/cryptography-basics/) (2020) | |
| 53 | ||
| 54 | ### Vulnerabilities | |
| 54 | 55 | |
| 55 | *** Vulnerabilities | |
| 56 | :PROPERTIES: | |
| 57 | :CUSTOM_ID: vulnerabilities | |
| 58 | :END: | |
| 59 | 56 | As of 2022-07-14, there are a few different vulnerabilities associated |
| 60 | 57 | with GPG or the libraries it uses: |
| 61 | 58 | |
| 62 | - GPG versions 1.0.2--1.2.3 contains a bug where "as soon as one | |
| 63 | (GPG-generated) ElGamal signature of an arbitrary message is released, | |
| 64 | one can recover the signer's private key in less than a second on a | |
| 65 | PC." ([[https://www.di.ens.fr/~pnguyen/pub_Ng04.htm][Source]]) | |
| 66 | - GPG versions prior to 1.4.2.1 contain a false positive signature | |
| 67 | verification bug. | |
| 68 | ([[https://lists.gnupg.%20org/pipermail/gnupg-announce/2006q1/000211.html][Source]]) | |
| 69 | - GPG versions prior to 1.4.2.2 cannot detect injection of unsigned | |
| 70 | data. ( | |
| 71 | [[https://lists.gnupg.org/pipermail/gnupg-announce/2006q1/000218.html][Source]]) | |
| 72 | - Libgcrypt, a library used by GPG, contained a bug which enabled full | |
| 73 | key recovery for RSA-1024 and some RSA-2048 keys. This was resolved in | |
| 74 | a GPG update in 2017. ([[https://lwn.net/Articles/727179/][Source]]) | |
| 75 | - The [[https://en.wikipedia.org/wiki/ROCA_vulnerability][ROCA | |
| 76 | Vulnerability]] affects RSA keys generated by YubiKey 4 tokens. | |
| 77 | ([[https://crocs.fi.%20muni.cz/_media/public/papers/nemec_roca_ccs17_preprint.pdf][Source]]) | |
| 78 | - The [[https://en.wikipedia.org/wiki/SigSpoof][SigSpoof Attack]] allows | |
| 79 | an attacker to spoof digital signatures. | |
| 80 | ([[https://arstechnica.%20com/information-technology/2018/06/decades-old-pgp-bug-allowed-hackers-to-spoof-just-about-anyones-signature/][Source]]) | |
| 81 | - Libgcrypt 1.9.0 contains a severe flaw related to a heap buffer | |
| 82 | overflow, fixed in Libgcrypt 1.9.1 | |
| 83 | ([[https://web.archive.%20org/web/20210221012505/https://www.theregister.com/2021/01/29/severe_libgcrypt_bug/][Source]]) | |
| 84 | ||
| 85 | ** Platforms | |
| 86 | :PROPERTIES: | |
| 87 | :CUSTOM_ID: platforms | |
| 88 | :END: | |
| 89 | Originally developed as a command-line program for *nix systems, GPG now | |
| 90 | has a wealth of front-end applications and libraries available for | |
| 59 | - GPG versions 1.0.2--1.2.3 contains a bug where "as soon as one | |
| 60 | (GPG-generated) ElGamal signature of an arbitrary message is | |
| 61 | released, one can recover the signer's private key in less than a | |
| 62 | second on a PC." | |
| 63 | ([Source](https://www.di.ens.fr/~pnguyen/pub_Ng04.htm)) | |
| 64 | - GPG versions prior to 1.4.2.1 contain a false positive signature | |
| 65 | verification bug. | |
| 66 | ([Source](https://lists.gnupg.%20org/pipermail/gnupg-announce/2006q1/000211.html)) | |
| 67 | - GPG versions prior to 1.4.2.2 cannot detect injection of unsigned | |
| 68 | data. ( | |
| 69 | [Source](https://lists.gnupg.org/pipermail/gnupg-announce/2006q1/000218.html)) | |
| 70 | - Libgcrypt, a library used by GPG, contained a bug which enabled full | |
| 71 | key recovery for RSA-1024 and some RSA-2048 keys. This was resolved | |
| 72 | in a GPG update in 2017. | |
| 73 | ([Source](https://lwn.net/Articles/727179/)) | |
| 74 | - The [ROCA | |
| 75 | Vulnerability](https://en.wikipedia.org/wiki/ROCA_vulnerability) | |
| 76 | affects RSA keys generated by YubiKey 4 tokens. | |
| 77 | ([Source](https://crocs.fi.%20muni.cz/_media/public/papers/nemec_roca_ccs17_preprint.pdf)) | |
| 78 | - The [SigSpoof Attack](https://en.wikipedia.org/wiki/SigSpoof) allows | |
| 79 | an attacker to spoof digital signatures. | |
| 80 | ([Source](https://arstechnica.%20com/information-technology/2018/06/decades-old-pgp-bug-allowed-hackers-to-spoof-just-about-anyones-signature/)) | |
| 81 | - Libgcrypt 1.9.0 contains a severe flaw related to a heap buffer | |
| 82 | overflow, fixed in Libgcrypt 1.9.1 | |
| 83 | ([Source](https://web.archive.%20org/web/20210221012505/https://www.theregister.com/2021/01/29/severe_libgcrypt_bug/)) | |
| 84 | ||
| 85 | ## Platforms | |
| 86 | ||
| 87 | Originally developed as a command-line program for *nix systems, GPG | |
| 88 | now has a wealth of front-end applications and libraries available for | |
| 91 | 89 | end-users. However, the most recommended programs remain the same: |
| 92 | 90 | |
| 93 | - [[https://gnupg.org][GnuPG]] for Linux (depending on distro) | |
| 94 | - [[https://gpg4win.org][Gpg4win]] for Windows | |
| 95 | - [[https://gpgtools.org][GPGTools]] for macOS | |
| 91 | - [GnuPG](https://gnupg.org) for Linux (depending on distro) | |
| 92 | - [Gpg4win](https://gpg4win.org) for Windows | |
| 93 | - [GPGTools](https://gpgtools.org) for macOS | |
| 94 | ||
| 95 | ## Creating a Key Pair | |
| 96 | 96 | |
| 97 | ** Creating a Key Pair | |
| 98 | :PROPERTIES: | |
| 99 | :CUSTOM_ID: creating-a-key-pair | |
| 100 | :END: | |
| 101 | 97 | In order to create a GPG key pair, a user would first need to install |
| 102 | GPG on their system. If we're assuming that the user is on Fedora Linux, | |
| 103 | they would execute the following: | |
| 98 | GPG on their system. If we're assuming that the user is on Fedora | |
| 99 | Linux, they would execute the following: | |
| 104 | 100 | |
| 105 | #+begin_src sh | |
| 101 | ```sh | |
| 106 | 102 | sudo dnf install gpg |
| 107 | #+end_src | |
| 103 | ``` | |
| 108 | 104 | |
| 109 | 105 | Once installed, a user can create a new key pair with the following |
| 110 | 106 | command(s): |
| 111 | 107 | |
| 112 | #+begin_src sh | |
| 108 | ```sh | |
| 113 | 109 | gpg --full-generate-key |
| 114 | #+end_src | |
| 110 | ``` | |
| 115 | 111 | |
| 116 | 112 | GPG will walk the user through an interactive setup that asks for an |
| 117 | 113 | algorithm preference, expiration date, name, and email to associate with |
| @@ -120,7 +116,7 @@ this key. | ||
| 120 | 116 | See the following example key set-up for a default key generation using |
| 121 | 117 | the GnuPG command-line interface: |
| 122 | 118 | |
| 123 | #+begin_src sh | |
| 119 | ```sh | |
| 124 | 120 | gpg (GnuPG) 2.3.6; Copyright (C) 2021 Free Software Foundation, Inc. |
| 125 | 121 | This is free software: you are free to change and redistribute it. |
| 126 | 122 | There is NO WARRANTY, to the extent permitted by law. |
| @@ -172,22 +168,20 @@ pub ed25519 2022-07-14 [SC] | ||
| 172 | 168 | E955B7700FFC11EF51C2BA1FE096AACDD4C32E9C |
| 173 | 169 | uid John Doe (test key) <johndoe@example.com> |
| 174 | 170 | sub cv25519 2022-07-14 [E] |
| 175 | #+end_src | |
| 171 | ``` | |
| 176 | 172 | |
| 177 | 173 | Please note that GUI apps may differ slightly from the GPG command-line |
| 178 | 174 | interface. |
| 179 | 175 | |
| 180 | ** Common Usage | |
| 181 | :PROPERTIES: | |
| 182 | :CUSTOM_ID: common-usage | |
| 183 | :END: | |
| 176 | ## Common Usage | |
| 177 | ||
| 184 | 178 | As noted in RFC 4880, the general functions of OpenPGP are as follows: |
| 185 | 179 | |
| 186 | - digital signatures | |
| 187 | - encryption | |
| 188 | - compression | |
| 189 | - Radix-64 conversion | |
| 190 | - key management and certificate services | |
| 180 | - digital signatures | |
| 181 | - encryption | |
| 182 | - compression | |
| 183 | - Radix-64 conversion | |
| 184 | - key management and certificate services | |
| 191 | 185 | |
| 192 | 186 | From this, you can probably gather that the main use of GPG is for |
| 193 | 187 | encrypting data and/or signing the data with a key. The purpose of |
| @@ -196,10 +190,8 @@ recipient(s) can access the data. | ||
| 196 | 190 | |
| 197 | 191 | Let's explore some specific GPG use-cases. |
| 198 | 192 | |
| 199 | ||
| 200 | :PROPERTIES: | |
| 201 | :CUSTOM_ID: email | |
| 202 | :END: | |
| 193 | ||
| 194 | ||
| 203 | 195 | One of the more popular uses of GPG is to sign and/or encrypt emails. |
| 204 | 196 | With the use of a GPG keypair, you can encrypt a message, its subject, |
| 205 | 197 | and even the attachments within. |
| @@ -214,8 +206,8 @@ The second process, regarding the actual encryption of the message and | ||
| 214 | 206 | its contents, works by using a combination of the sender's keys and the |
| 215 | 207 | recipient's keys. This process may vary slightly by implementation, but |
| 216 | 208 | it most commonly uses asymmetric cryptography, also known as public-key |
| 217 | cryptography. In this version of encryption, the sender's private key to | |
| 218 | sign the message and a combination of the sender's keys and the | |
| 209 | cryptography. In this version of encryption, the sender's private key | |
| 210 | to sign the message and a combination of the sender's keys and the | |
| 219 | 211 | recipient's public key to encrypt the message. |
| 220 | 212 | |
| 221 | 213 | If two people each have their own private keys and exchange their public |
| @@ -227,66 +219,61 @@ Implementation of email encryption varies greatly between email clients, | ||
| 227 | 219 | so you will need to reference your email client's documentation to |
| 228 | 220 | ensure you are setting it up correctly for that specific client. |
| 229 | 221 | |
| 230 | *** File Encryption | |
| 231 | :PROPERTIES: | |
| 232 | :CUSTOM_ID: file-encryption | |
| 233 | :END: | |
| 222 | ### File Encryption | |
| 223 | ||
| 234 | 224 | As noted in the section above regarding emails, GPG enables users to be |
| 235 | 225 | able to send a message to each other if they are both set-up with GPG |
| 236 | 226 | keys. In this example, I am going to show how a user could send a file |
| 237 | called =example_file.txt= to another user via the recipient's email. | |
| 227 | called `example_file.txt` to another user via the | |
| 228 | recipient's email. | |
| 238 | 229 | |
| 239 | 230 | The sender would find the file they want to send and execute the |
| 240 | 231 | following command: |
| 241 | 232 | |
| 242 | #+begin_src sh | |
| 233 | ```sh | |
| 243 | 234 | gpg --encrypt --output example_file.txt.gpg --recipient \ |
| 244 | 235 | recipient@example.com example_file.txt |
| 245 | #+end_src | |
| 236 | ``` | |
| 246 | 237 | |
| 247 | 238 | Once received, the recipient can decrypt the file with the following |
| 248 | 239 | command: |
| 249 | 240 | |
| 250 | #+begin_src sh | |
| 241 | ```sh | |
| 251 | 242 | gpg --decrypt --output example_file.txt example_file.txt.gpg |
| 252 | #+end_src | |
| 243 | ``` | |
| 244 | ||
| 245 | ### Ownership Signatures | |
| 253 | 246 | |
| 254 | *** Ownership Signatures | |
| 255 | :PROPERTIES: | |
| 256 | :CUSTOM_ID: ownership-signatures | |
| 257 | :END: | |
| 258 | 247 | One important aspect of GPG, especially for developers, is the ability |
| 259 | 248 | to sign data without encrypting it. For example, developers often sign |
| 260 | 249 | code changes when they commit the changes back to a central repository, |
| 261 | 250 | in order to display ownership of who made the changes. This allows other |
| 262 | 251 | users to look at a code change and determine that the change was valid. |
| 263 | 252 | |
| 264 | In order to do this using [[https://git-scm.com][Git]], the developer | |
| 265 | simply needs to alter the =git commit= command to include the =-S= flag. | |
| 266 | Here's an example: | |
| 253 | In order to do this using [Git](https://git-scm.com), the developer | |
| 254 | simply needs to alter the `git commit` command to include the | |
| 255 | `-S` flag. Here's an example: | |
| 267 | 256 | |
| 268 | #+begin_src sh | |
| 257 | ```sh | |
| 269 | 258 | git commit -S -m "my commit message" |
| 270 | #+end_src | |
| 259 | ``` | |
| 271 | 260 | |
| 272 | 261 | As an expansion of the example above, Git users can configure their |
| 273 | 262 | environment with a default key to use by adding their GPG signature: |
| 274 | 263 | |
| 275 | #+begin_src sh | |
| 264 | ```sh | |
| 276 | 265 | git config --global user.signingkey XXXXXXXXXXXXXXXX |
| 277 | #+end_src | |
| 266 | ``` | |
| 278 | 267 | |
| 279 | If you're not sure what your signature is, you can find it titled =sig= | |
| 280 | in the output of this command: | |
| 268 | If you're not sure what your signature is, you can find it titled | |
| 269 | `sig` in the output of this command: | |
| 281 | 270 | |
| 282 | #+begin_src sh | |
| 271 | ```sh | |
| 283 | 272 | gpg --list-signatures |
| 284 | #+end_src | |
| 273 | ``` | |
| 274 | ||
| 275 | ### File Integrity | |
| 285 | 276 | |
| 286 | *** File Integrity | |
| 287 | :PROPERTIES: | |
| 288 | :CUSTOM_ID: file-integrity | |
| 289 | :END: | |
| 290 | 277 | When a person generates a signature for data, they are allowing users |
| 291 | 278 | the ability to verify the signature on that data in the future to ensure |
| 292 | 279 | the data has not been corrupted. This is most common with software |
| @@ -296,26 +283,24 @@ replaced with dangerous software. | ||
| 296 | 283 | |
| 297 | 284 | In order to verify signed data, a user needs to have: |
| 298 | 285 | |
| 299 | 1. The signed data | |
| 300 | 2. A signature file | |
| 301 | 3. The public GPG key of the signer | |
| 286 | 1. The signed data | |
| 287 | 2. A signature file | |
| 288 | 3. The public GPG key of the signer | |
| 302 | 289 | |
| 303 | Once the signer's public key is imported on the user's system, and they | |
| 304 | have the data and signature, they can verify the data with the following | |
| 305 | commands: | |
| 290 | Once the signer's public key is imported on the user's system, and | |
| 291 | they have the data and signature, they can verify the data with the | |
| 292 | following commands: | |
| 306 | 293 | |
| 307 | #+begin_src sh | |
| 294 | ```sh | |
| 308 | 295 | # If the signature is attached to the data |
| 309 | 296 | gpg --verify [signature-file] |
| 310 | 297 | |
| 311 | 298 | # If the signature is detached as a separate file from the data |
| 312 | 299 | gpg --verify [signature-file] [original-file] |
| 313 | #+end_src | |
| 300 | ``` | |
| 301 | ||
| 302 | ## Finding Public Keys | |
| 314 | 303 | |
| 315 | ** Finding Public Keys | |
| 316 | :PROPERTIES: | |
| 317 | :CUSTOM_ID: finding-public-keys | |
| 318 | :END: | |
| 319 | 304 | In order to use GPG with others, a user needs to know the other user(s) |
| 320 | 305 | keys. This is easy to do if the user knows the other user(s) in person, |
| 321 | 306 | but may be hard if the relationship is strictly digital. Luckily, there |
| @@ -324,5 +309,5 @@ social pages if they have them. | ||
| 324 | 309 | |
| 325 | 310 | Otherwise, the best option is to use a keyserver, such as: |
| 326 | 311 | |
| 327 | - [[https://pgp.mit.edu][pgp.mit.edu]] | |
| 328 | - [[https://keys.openpgp.org][keys.openpgp.org]] | |
| 312 | - [pgp.mit.edu](https://pgp.mit.edu) | |
| 313 | - [keys.openpgp.org](https://keys.openpgp.org) | |
content/blog/2022-07-25-curseradio.md added +106
| @@ -0,0 +1,106 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-07-25 | |
| 3 | title = "CurseRadio: Listening to the Radio on the Comand Line" | |
| 4 | description = "Use Curse Radio to listen to radio on the command-line." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Overview | |
| 8 | ||
| 9 | While exploring some interesting Linux applications, I stumbled across | |
| 10 | [curseradio](https://github.com/chronitis/curseradio), a command-line | |
| 11 | radio player based on Python. | |
| 12 | ||
| 13 | This application is fantastic and incredibly easy to install, so I | |
| 14 | wanted to dedicate a post today to this app. Let's look at the features | |
| 15 | within the app and then walk through the installation process I took to | |
| 16 | get `curseradio` working. | |
| 17 | ||
| 18 | ## Features | |
| 19 | ||
| 20 |  | |
| 21 | ||
| 22 | The radio player itself is quite minimal. As you can see in the | |
| 23 | screenshot above, it contains a simple plaintext list of all available | |
| 24 | categories, which can be broken down further and further. In addition, | |
| 25 | radio shows are available for listening, alongside regular radio | |
| 26 | stations. | |
| 27 | ||
| 28 | For example, the `Sports` > `Pro Basketball` > | |
| 29 | `Shows` category contains a number of specific shows related | |
| 30 | to Professional Basketball. | |
| 31 | ||
| 32 | Aside from being able to play any of the listed stations/shows, you can | |
| 33 | make a channel your favorite by pressing `f`. It will now | |
| 34 | show up at the top of the radio player in the `Favourites` | |
| 35 | category. | |
| 36 | ||
| 37 | ### Commands/Shortcuts | |
| 38 | ||
| 39 | Key(s) Command | |
| 40 | ------------ --------------------------------- | |
| 41 | ↑, ↓ navigate | |
| 42 | PgUp, PgDn navigate quickly | |
| 43 | Home, End to top/bottom | |
| 44 | Enter open/close folders, play stream | |
| 45 | k stop playing stream | |
| 46 | q quit | |
| 47 | f toggle favourite | |
| 48 | ||
| 49 | ## Installation | |
| 50 | ||
| 51 | ### Dependencies | |
| 52 | ||
| 53 | Before installing `curseradio`, a handful of system and | |
| 54 | Python packages are required. To get started, install | |
| 55 | `python3`, `pip3`, and `mpv` on your | |
| 56 | system. In this example, I'm using Fedora Linux, which uses the | |
| 57 | `dnf` package manager. You may need to adjust this if you're | |
| 58 | using a different system. | |
| 59 | ||
| 60 | ```sh | |
| 61 | sudo dnf install python3 pip3 mpv | |
| 62 | ``` | |
| 63 | ||
| 64 | Next, use `pip3` to install `requests`, | |
| 65 | `xdg`, and `lxml`: | |
| 66 | ||
| 67 | ```sh | |
| 68 | pip3 install requests xdg lxml | |
| 69 | ``` | |
| 70 | ||
| 71 | ### Repository Source Installation | |
| 72 | ||
| 73 | Once all the dependencies are installed, we can clone the source code | |
| 74 | and enter that directory: | |
| 75 | ||
| 76 | ```sh | |
| 77 | git clone https://github.com/chronitis/curseradio && cd curseradio | |
| 78 | ``` | |
| 79 | ||
| 80 | Once you're within the `curseradio` directory, you can | |
| 81 | install the application with the provided `setup.py` script. | |
| 82 | ||
| 83 | ```sh | |
| 84 | sudo python3 setup.py install | |
| 85 | ``` | |
| 86 | ||
| 87 | In my case, I ran into a few errors and needed to create the folders | |
| 88 | that curseradio wanted to use for its installation. If you don't get | |
| 89 | any errors, you can skip this and run the app. | |
| 90 | ||
| 91 | ```sh | |
| 92 | sudo mkdir /usr/local/lib/python3.10/ | |
| 93 | sudo mkdir /usr/local/lib/python3.10/site-packages/ | |
| 94 | ``` | |
| 95 | ||
| 96 | ```sh | |
| 97 | sudo chown -R $USER:$USER /usr/local/lib/python3.10/ | |
| 98 | ``` | |
| 99 | ||
| 100 | ## Run the Application | |
| 101 | ||
| 102 | Once fully installed without errors, you can run the application! | |
| 103 | ||
| 104 | ```sh | |
| 105 | python3 /usr/local/bin/curseradio | |
| 106 | ``` | |
blog/2022-07-30-flac-to-opus.org → content/blog/2022-07-30-flac-to-opus.md renamed +65 −70
| @@ -1,72 +1,70 @@ | ||
| 1 | #+title: Recursive Command-Line FLAC to Opus Conversion | |
| 2 | #+date: 2022-07-30 | |
| 1 | +++ | |
| 2 | date = 2022-07-30 | |
| 3 | title = "Recursive Command-Line FLAC to Opus Conversion" | |
| 4 | description = "Learn how to convert all FLAC files to Opus, including recursive files in subdirectories." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Converting FLAC to OPUS | |
| 3 | 8 | |
| 4 | ** Converting FLAC to OPUS | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: converting-flac-to-opus | |
| 7 | :END: | |
| 8 | 9 | I am currently rebuilding my music library from scratch so that I can |
| 9 | effectively archive all the music I own in the | |
| 10 | [[https://en.wikipedia.org/wiki/FLAC][FLAC file format]], a lossless | |
| 11 | audio codec. | |
| 10 | effectively archive all the music I own in the [FLAC file | |
| 11 | format](https://en.wikipedia.org/wiki/FLAC), a lossless audio codec. | |
| 12 | 12 | |
| 13 | 13 | However, streaming FLAC files outside the home can be difficult due to |
| 14 | 14 | the size of the files, especially if you're using a weak connection. |
| 15 | 15 | |
| 16 | 16 | So, in order to archive the music in a lossless format and still be able |
| 17 | 17 | to stream it easily, I opted to create a copy of my FLAC files in the |
| 18 | [[https://en.wikipedia.org/wiki/Opus_(audio_format)][Opus audio codec]]. | |
| 18 | [Opus audio codec](https://en.wikipedia.org/wiki/Opus_(audio_format)). | |
| 19 | 19 | This allows me to archive a quality, lossless version of the music and |
| 20 | 20 | then point my streaming service to the smaller, stream-ready version. |
| 21 | 21 | |
| 22 | *** Dependencies | |
| 23 | :PROPERTIES: | |
| 24 | :CUSTOM_ID: dependencies | |
| 25 | :END: | |
| 26 | The process I follow utilizes the =opus-tools= package in Ubuntu. Before | |
| 27 | proceeding, install the package: | |
| 22 | ### Dependencies | |
| 23 | ||
| 24 | The process I follow utilizes the `opus-tools` package in | |
| 25 | Ubuntu. Before proceeding, install the package: | |
| 28 | 26 | |
| 29 | #+begin_src sh | |
| 27 | ```sh | |
| 30 | 28 | sudo apt install opus-tools |
| 31 | #+end_src | |
| 29 | ``` | |
| 32 | 30 | |
| 33 | If you want to use a different conversion method, such as =ffmpeg= or | |
| 34 | =avconv=, simply install that package instead. | |
| 31 | If you want to use a different conversion method, such as | |
| 32 | `ffmpeg` or `avconv`, simply install that package | |
| 33 | instead. | |
| 34 | ||
| 35 | ### Conversion Process | |
| 35 | 36 | |
| 36 | *** Conversion Process | |
| 37 | :PROPERTIES: | |
| 38 | :CUSTOM_ID: conversion-process | |
| 39 | :END: | |
| 40 | 37 | The script I'm using is stored in my home directory, but feel free to |
| 41 | 38 | create it wherever you want. It does not need to be in the same |
| 42 | 39 | directory as your music files. |
| 43 | 40 | |
| 44 | #+begin_src sh | |
| 41 | ```sh | |
| 45 | 42 | cd ~ && nano transform.sh |
| 46 | #+end_src | |
| 43 | ``` | |
| 47 | 44 | |
| 48 | 45 | Once you have your new bash script opened in an editor, go ahead and |
| 49 | 46 | paste the following logic into the script. |
| 50 | 47 | |
| 51 | You *MUST* edit the following variables in order for it to work: | |
| 48 | You **MUST** edit the following variables in order for it to work: | |
| 52 | 49 | |
| 53 | - =source=: The source directory where your FLAC files are stored. | |
| 54 | - =dest=: The destination directory where you want the resulting Opus | |
| 55 | files to be stored. | |
| 50 | - `source`: The source directory where your FLAC files are | |
| 51 | stored. | |
| 52 | - `dest`: The destination directory where you want the | |
| 53 | resulting Opus files to be stored. | |
| 56 | 54 | |
| 57 | You *MAY* want to edit the following variables to suit your needs: | |
| 55 | You **MAY** want to edit the following variables to suit your needs: | |
| 58 | 56 | |
| 59 | - =filename=: If you are converting to a file format other than Opus, | |
| 60 | you'll need to edit this so that your resulting files have the correct | |
| 61 | filename extension. | |
| 62 | - =reldir=: This variable can be edited to strip out more leading | |
| 63 | directories in the file path. As you'll see later, I ignore this for | |
| 64 | now and simply clean it up afterward. | |
| 65 | - =opusenc=: This is the actual conversion process. You may want to edit | |
| 66 | the bitrate to suit your needs. I set mine at 128 but some prefer 160 | |
| 67 | or higher. | |
| 57 | - `filename`: If you are converting to a file format other | |
| 58 | than Opus, you'll need to edit this so that your resulting files | |
| 59 | have the correct filename extension. | |
| 60 | - `reldir`: This variable can be edited to strip out more | |
| 61 | leading directories in the file path. As you'll see later, I ignore | |
| 62 | this for now and simply clean it up afterward. | |
| 63 | - `opusenc`: This is the actual conversion process. You may | |
| 64 | want to edit the bitrate to suit your needs. I set mine at 128 but | |
| 65 | some prefer 160 or higher. | |
| 68 | 66 | |
| 69 | #+begin_src sh | |
| 67 | ```sh | |
| 70 | 68 | #!/bin/bash |
| 71 | 69 | ## - The IFS takes care of spaces in file and dirnames |
| 72 | 70 | ## - your folders may vary |
| @@ -128,55 +126,52 @@ opusenc --vbr --bitrate 128 --date "$DATE" \ | ||
| 128 | 126 | ## just for testing |
| 129 | 127 | # sleep 1 |
| 130 | 128 | done |
| 131 | #+end_src | |
| 129 | ``` | |
| 132 | 130 | |
| 133 | 131 | Once you're done, simply save the file and exit your editor. Don't |
| 134 | 132 | forget to enable execution of the script: |
| 135 | 133 | |
| 136 | #+begin_src sh | |
| 134 | ```sh | |
| 137 | 135 | chmod +x transform.sh |
| 138 | #+end_src | |
| 136 | ``` | |
| 139 | 137 | |
| 140 | 138 | Finally, you may now run the script: |
| 141 | 139 | |
| 142 | #+begin_src sh | |
| 140 | ```sh | |
| 143 | 141 | ./transform.sh |
| 144 | #+end_src | |
| 145 | ||
| 146 | If you used =opusenc=, you'll see the conversions happen within the | |
| 147 | terminal as it progresses. You will also see variables printed if you | |
| 148 | uncommented any of the bash script's comments. | |
| 149 | ||
| 150 | *** Cleanup | |
| 151 | :PROPERTIES: | |
| 152 | :CUSTOM_ID: cleanup | |
| 153 | :END: | |
| 154 | As I noted above, I didn't customize my =reldir= variable in the script, | |
| 155 | which caused my output directory to be =/mnt/music/library/archives= | |
| 156 | instead of =/mnt/music/library=. So, I moved the output up one level and | |
| 142 | ``` | |
| 143 | ||
| 144 | If you used `opusenc`, you'll see the conversions happen | |
| 145 | within the terminal as it progresses. You will also see variables | |
| 146 | printed if you uncommented any of the bash script's comments. | |
| 147 | ||
| 148 | ### Cleanup | |
| 149 | ||
| 150 | As I noted above, I didn't customize my `reldir` variable in | |
| 151 | the script, which caused my output directory to be | |
| 152 | `/mnt/music/library/archives` instead of | |
| 153 | `/mnt/music/library`. So, I moved the output up one level and | |
| 157 | 154 | deleted the accidental directory. |
| 158 | 155 | |
| 159 | #+begin_src sh | |
| 156 | ```sh | |
| 160 | 157 | cd /mnt/music/library |
| 161 | 158 | mv archives/* . |
| 162 | 159 | rm -rf archives |
| 163 | #+end_src | |
| 160 | ``` | |
| 161 | ||
| 162 | ### Check the Resulting Size | |
| 164 | 163 | |
| 165 | *** Check the Resulting Size | |
| 166 | :PROPERTIES: | |
| 167 | :CUSTOM_ID: check-the-resulting-size | |
| 168 | :END: | |
| 169 | If you want to see what kind of file size savings you've gained, you can | |
| 170 | always use the =du= command to check: | |
| 164 | If you want to see what kind of file size savings you've gained, you | |
| 165 | can always use the `du` command to check: | |
| 171 | 166 | |
| 172 | #+begin_src sh | |
| 167 | ```sh | |
| 173 | 168 | cd /mnt/music |
| 174 | 169 | du -h --max-depth=1 . |
| 175 | #+end_src | |
| 170 | ``` | |
| 176 | 171 | |
| 177 | 172 | In my case, my small library went from 78GB to 6.3GB! |
| 178 | 173 | |
| 179 | #+begin_src txt | |
| 174 | ```txt | |
| 180 | 175 | 78G ./archives |
| 181 | 176 | 6.3G ./library |
| 182 | #+end_src | |
| 177 | ``` | |
blog/2022-07-31-bash-it.org → content/blog/2022-07-31-bash-it.md renamed +106 −122
| @@ -1,126 +1,117 @@ | ||
| 1 | #+title: Upgrade Bash with Bash-It & Ble.sh | |
| 2 | #+date: 2022-07-31 | |
| 1 | +++ | |
| 2 | date = 2022-07-31 | |
| 3 | title = "Upgrade Bash with Bash-It & Ble.sh" | |
| 4 | description = "Learn how to increase the power of bash with Bash-It and Ble.sh." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Bash | |
| 3 | 8 | |
| 4 | ** Bash | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: bash | |
| 7 | :END: | |
| 8 | 9 | For those who are not familiar, |
| 9 | [[https://en.wikipedia.org/wiki/Bash_(Unix_shell)][Bash]] is a Unix | |
| 10 | shell that is used as the default login shell for most Linux | |
| 11 | distributions. This shell and command processor should be familiar if | |
| 12 | you've used Linux (or older version of macOS) before. | |
| 10 | [Bash](https://en.wikipedia.org/wiki/Bash_(Unix_shell)) is a Unix shell | |
| 11 | that is used as the default login shell for most Linux distributions. | |
| 12 | This shell and command processor should be familiar if you've used | |
| 13 | Linux (or older version of macOS) before. | |
| 13 | 14 | |
| 14 | 15 | However, bash is not the only option. There are numerous other shells |
| 15 | 16 | that exist. Here are some popular examples: |
| 16 | 17 | |
| 17 | - [[https://en.wikipedia.org/wiki/Z_shell][zsh]] | |
| 18 | - [[https://en.wikipedia.org/wiki/Fish_(Unix_shell)][fish]] | |
| 19 | - [[https://github.com/ibara/oksh][oksh]] | |
| 20 | - [[https://wiki.gentoo.org/wiki/Mksh][mksh]] | |
| 21 | - [[https://en.wikipedia.org/wiki/Debian_Almquist_shell][dash]] | |
| 18 | - [zsh](https://en.wikipedia.org/wiki/Z_shell) | |
| 19 | - [fish](https://en.wikipedia.org/wiki/Fish_(Unix_shell)) | |
| 20 | - [oksh](https://github.com/ibara/oksh) | |
| 21 | - [mksh](https://wiki.gentoo.org/wiki/Mksh) | |
| 22 | - [dash](https://en.wikipedia.org/wiki/Debian_Almquist_shell) | |
| 22 | 23 | |
| 23 | 24 | While each shell has its differences, bash is POSIX compliant and the |
| 24 | 25 | default for many Linux users. Because of this, I am going to explore a |
| 25 | program called =bash-it= below that helps bash users increase the | |
| 26 | utility of their shell without installing a completely new shell. | |
| 26 | program called `bash-it` below that helps bash users increase | |
| 27 | the utility of their shell without installing a completely new shell. | |
| 28 | ||
| 29 | ### Installation | |
| 27 | 30 | |
| 28 | *** Installation | |
| 29 | :PROPERTIES: | |
| 30 | :CUSTOM_ID: installation | |
| 31 | :END: | |
| 32 | 31 | First, if bash is not already installed on your system, you can |
| 33 | [[https://www.gnu.org/software/bash/][download bash from GNU]] or use | |
| 34 | your package manager to install it. | |
| 32 | [download bash from GNU](https://www.gnu.org/software/bash/) or use your | |
| 33 | package manager to install it. | |
| 35 | 34 | |
| 36 | 35 | For example, this is how you can install bash on Fedora Linux: |
| 37 | 36 | |
| 38 | #+begin_src sh | |
| 37 | ```sh | |
| 39 | 38 | sudo dnf install bash |
| 40 | #+end_src | |
| 39 | ``` | |
| 41 | 40 | |
| 42 | If you are not using bash as your default shell, use the =chsh= command | |
| 43 | to change your shell: | |
| 41 | If you are not using bash as your default shell, use the | |
| 42 | `chsh` command to change your shell: | |
| 44 | 43 | |
| 45 | #+begin_src sh | |
| 44 | ```sh | |
| 46 | 45 | chsh |
| 47 | #+end_src | |
| 46 | ``` | |
| 48 | 47 | |
| 49 | You should see a prompt like the one below. If the brackets (=[]=) | |
| 50 | contain =bash= already, you're done, and you can simply continue by | |
| 51 | hitting the Enter key. | |
| 48 | You should see a prompt like the one below. If the brackets | |
| 49 | (`[]`) contain `bash` already, you're done, and | |
| 50 | you can simply continue by hitting the Enter key. | |
| 52 | 51 | |
| 53 | 52 | If the brackets contain another shell path (e.g. =/usr/bin/zsh=), enter |
| 54 | the path to the bash program on your system (it's most likely located at | |
| 55 | =/usr/bin/bash=). | |
| 53 | the path to the bash program on your system (it's most likely located | |
| 54 | at `/usr/bin/bash`). | |
| 56 | 55 | |
| 57 | #+begin_src sh | |
| 56 | ```sh | |
| 58 | 57 | Changing shell for <user>. |
| 59 | 58 | New shell [/usr/bin/bash]: |
| 60 | #+end_src | |
| 59 | ``` | |
| 61 | 60 | |
| 62 | 61 | You must log out or restart the machine in order for the login shell to |
| 63 | 62 | be refreshed. You can do it now or wait until you're finished |
| 64 | 63 | customizing the shell. |
| 65 | 64 | |
| 66 | #+begin_src sh | |
| 65 | ```sh | |
| 67 | 66 | sudo reboot now |
| 68 | #+end_src | |
| 67 | ``` | |
| 69 | 68 | |
| 70 | ** Bash-it | |
| 71 | :PROPERTIES: | |
| 72 | :CUSTOM_ID: bash-it | |
| 73 | :END: | |
| 74 | As noted on the [[https://github.com/Bash-it/bash-it][Bash-it]] | |
| 75 | repository: | |
| 69 | ## Bash-it | |
| 76 | 70 | |
| 77 | #+begin_quote | |
| 78 | Bash-it is a collection of community Bash commands and scripts for Bash | |
| 79 | 3.2+. (And a shameless ripoff of oh-my-zsh 😃) | |
| 71 | As noted on the [Bash-it](https://github.com/Bash-it/bash-it) | |
| 72 | repository: | |
| 80 | 73 | |
| 81 | #+end_quote | |
| 74 | > Bash-it is a collection of community Bash commands and scripts for | |
| 75 | > Bash 3.2+. (And a shameless ripoff of oh-my-zsh 😃) | |
| 82 | 76 | |
| 83 | 77 | Bash-it makes it easy to install plugins, set up aliases for common |
| 84 | 78 | commands, and easily change the visual theme of your shell. |
| 85 | 79 | |
| 86 | *** Installation | |
| 87 | :PROPERTIES: | |
| 88 | :CUSTOM_ID: installation-1 | |
| 89 | :END: | |
| 80 | ### Installation | |
| 81 | ||
| 90 | 82 | To install the framework, simply copy the repository files and use the |
| 91 | =install.sh= script provided. If you want, you can (and should!) inspect | |
| 92 | the contents of the installation script before you run it. | |
| 83 | `install.sh` script provided. If you want, you can (and | |
| 84 | should!) inspect the contents of the installation script before you run | |
| 85 | it. | |
| 93 | 86 | |
| 94 | #+begin_src sh | |
| 87 | ```sh | |
| 95 | 88 | git clone --depth=1 https://github.com/Bash-it/bash-it.git ~/.bash_it |
| 96 | 89 | ~/.bash_it/install.sh |
| 97 | #+end_src | |
| 90 | ``` | |
| 98 | 91 | |
| 99 | 92 | If you didn't restart your session after making bash the default, and |
| 100 | 93 | are currently working within another shell, be sure to enter a bash |
| 101 | session before using =bash-it=: | |
| 94 | session before using `bash-it`: | |
| 102 | 95 | |
| 103 | #+begin_src sh | |
| 96 | ```sh | |
| 104 | 97 | bash |
| 105 | #+end_src | |
| 98 | ``` | |
| 99 | ||
| 100 | ### Aliases | |
| 106 | 101 | |
| 107 | *** Aliases | |
| 108 | :PROPERTIES: | |
| 109 | :CUSTOM_ID: aliases | |
| 110 | :END: | |
| 111 | 102 | Bash-it contains a number of aliases for common commands to help improve |
| 112 | 103 | efficiency in the terminal. To list all available options, use the |
| 113 | 104 | following command: |
| 114 | 105 | |
| 115 | #+begin_src sh | |
| 106 | ```sh | |
| 116 | 107 | bash-it show aliases |
| 117 | #+end_src | |
| 108 | ``` | |
| 118 | 109 | |
| 119 | 110 | This will provide you a list that looks like the following text block. |
| 120 | 111 | Within this screen, you will be able to see all available options and |
| 121 | 112 | which ones are currently enabled. |
| 122 | 113 | |
| 123 | #+begin_src txt | |
| 114 | ```txt | |
| 124 | 115 | Alias Enabled? Description |
| 125 | 116 | ag [ ] the silver searcher (ag) aliases |
| 126 | 117 | ansible [ ] ansible abbreviations |
| @@ -133,35 +124,33 @@ clipboard [ ] xclip shortcuts | ||
| 133 | 124 | composer [ ] common composer abbreviations |
| 134 | 125 | curl [x] Curl aliases for convenience. |
| 135 | 126 | ... |
| 136 | #+end_src | |
| 127 | ``` | |
| 137 | 128 | |
| 138 | 129 | To enable an alias, do: |
| 139 | 130 | |
| 140 | #+begin_src sh | |
| 131 | ```sh | |
| 141 | 132 | bash-it enable alias <alias name> [alias name]... -or- $ bash-it enable alias all |
| 142 | #+end_src | |
| 133 | ``` | |
| 143 | 134 | |
| 144 | 135 | To disable an alias, do: |
| 145 | 136 | |
| 146 | #+begin_src sh | |
| 137 | ```sh | |
| 147 | 138 | bash-it disable alias <alias name> [alias name]... -or- $ bash-it disable alias all |
| 148 | #+end_src | |
| 139 | ``` | |
| 140 | ||
| 141 | ### Plugins | |
| 149 | 142 | |
| 150 | *** Plugins | |
| 151 | :PROPERTIES: | |
| 152 | :CUSTOM_ID: plugins | |
| 153 | :END: | |
| 154 | 143 | Similar to aliases, plugins are available with bash-it. You can find a |
| 155 | 144 | complete list of plugins in the same way as aliases. Simply execute the |
| 156 | 145 | following: |
| 157 | 146 | |
| 158 | #+begin_src sh | |
| 147 | ```sh | |
| 159 | 148 | bash-it show plugins |
| 160 | #+end_src | |
| 149 | ``` | |
| 161 | 150 | |
| 162 | 151 | You will see the following output showing enabled and disabled plugins: |
| 163 | 152 | |
| 164 | #+begin_src txt | |
| 153 | ```txt | |
| 165 | 154 | Plugin Enabled? Description |
| 166 | 155 | alias-completion [ ] |
| 167 | 156 | autojump [ ] Autojump configuration, see https://github.com/wting/autojump for more details |
| @@ -172,87 +161,82 @@ battery [x] display info about your battery charge level | ||
| 172 | 161 | blesh [ ] load ble.sh, the Bash line editor! |
| 173 | 162 | boot2docker [ ] Helpers to get Docker setup correctly for boot2docker |
| 174 | 163 | browser [ ] render commandline output in your browser |
| 175 | #+end_src | |
| 164 | ``` | |
| 176 | 165 | |
| 177 | 166 | To enable a plugin, do: |
| 178 | 167 | |
| 179 | #+begin_src sh | |
| 168 | ```sh | |
| 180 | 169 | bash-it enable plugin <plugin name> [plugin name]... -or- $ bash-it enable plugin all |
| 181 | #+end_src | |
| 170 | ``` | |
| 182 | 171 | |
| 183 | 172 | To disable a plugin, do: |
| 184 | 173 | |
| 185 | #+begin_src sh | |
| 174 | ```sh | |
| 186 | 175 | bash-it disable plugin <plugin name> [plugin name]... -or- $ bash-it disable plugin all |
| 187 | #+end_src | |
| 176 | ``` | |
| 177 | ||
| 178 | ### Themes | |
| 188 | 179 | |
| 189 | *** Themes | |
| 190 | :PROPERTIES: | |
| 191 | :CUSTOM_ID: themes | |
| 192 | :END: | |
| 193 | 180 | There are quite a few pre-defined |
| 194 | [[https://bash-it.readthedocs.io/en/latest/themes-list/#list-of-themes][themes]] | |
| 181 | [themes](https://bash-it.readthedocs.io/en/latest/themes-list/#list-of-themes) | |
| 195 | 182 | available with bash-it. |
| 196 | 183 | |
| 197 | 184 | To list all themes: |
| 198 | 185 | |
| 199 | #+begin_src sh | |
| 186 | ```sh | |
| 200 | 187 | ls ~/.bash_it/themes/ |
| 201 | #+end_src | |
| 188 | ``` | |
| 202 | 189 | |
| 203 | To use a new theme, you'll need to edit =.bashrc= and alter the | |
| 204 | =BASH_IT_THEME= variable to your desired theme. For example, I am using | |
| 205 | the =zork= theme. | |
| 190 | To use a new theme, you'll need to edit `.bashrc` and alter | |
| 191 | the `BASH_IT_THEME` variable to your desired theme. For | |
| 192 | example, I am using the `zork` theme. | |
| 206 | 193 | |
| 207 | #+begin_src sh | |
| 194 | ```sh | |
| 208 | 195 | nano ~/.bashrc |
| 209 | #+end_src | |
| 196 | ``` | |
| 210 | 197 | |
| 211 | #+begin_src sh | |
| 198 | ```sh | |
| 212 | 199 | export BASH_IT_THEME='zork' |
| 213 | #+end_src | |
| 200 | ``` | |
| 214 | 201 | |
| 215 | 202 | Once you save your changes, you just need to exit your terminal and |
| 216 | create a new one in order to see your changes to the =.bashrc= file. You | |
| 217 | can also =source= the file to see changes, but I recommend starting a | |
| 218 | completely new shell instead. | |
| 219 | ||
| 220 | ** ble.sh | |
| 221 | :PROPERTIES: | |
| 222 | :CUSTOM_ID: ble.sh | |
| 223 | :END: | |
| 224 | One big feature I was missing in Bash that both =zsh= and =fish= have is | |
| 225 | an autosuggestion feature. To explain: as you type, an autosuggestion | |
| 226 | feature in the shell will offer suggestions in a lighter font color | |
| 227 | beyond the characters already typed. Once you see the command you want, | |
| 228 | you can click the right arrow and have the shell auto-complete that line | |
| 229 | for you. | |
| 230 | ||
| 231 | Luckily, the [[https://github.com/akinomyoga/ble.sh][Bash Line Editor]] | |
| 203 | create a new one in order to see your changes to the | |
| 204 | `.bashrc` file. You can also `source` the file to | |
| 205 | see changes, but I recommend starting a completely new shell instead. | |
| 206 | ||
| 207 | ## ble.sh | |
| 208 | ||
| 209 | One big feature I was missing in Bash that both `zsh` and | |
| 210 | `fish` have is an autosuggestion feature. To explain: as you | |
| 211 | type, an autosuggestion feature in the shell will offer suggestions in a | |
| 212 | lighter font color beyond the characters already typed. Once you see the | |
| 213 | command you want, you can click the right arrow and have the shell | |
| 214 | auto-complete that line for you. | |
| 215 | ||
| 216 | Luckily, the [Bash Line Editor](https://github.com/akinomyoga/ble.sh) | |
| 232 | 217 | (ble.sh) exists! This program provides a wonderful autosuggestions |
| 233 | 218 | feature perfectly, among other features that I haven't tested yet. |
| 234 | 219 | |
| 235 | 220 | In order to install ble.sh, execute the following: |
| 236 | 221 | |
| 237 | #+begin_src sh | |
| 222 | ```sh | |
| 238 | 223 | git clone --recursive https://github.com/akinomyoga/ble.sh.git |
| 239 | 224 | make -C ble.sh install PREFIX=~/.local |
| 240 | 225 | echo 'source ~/.local/share/blesh/ble.sh' >> ~/.bashrc |
| 241 | #+end_src | |
| 226 | ``` | |
| 242 | 227 | |
| 243 | 228 | Again, exit the terminal and open a new one in order to see the |
| 244 | 229 | newly-configured shell. |
| 245 | 230 | |
| 246 | ** Restart the Session | |
| 247 | :PROPERTIES: | |
| 248 | :CUSTOM_ID: restart-the-session | |
| 249 | :END: | |
| 231 | ## Restart the Session | |
| 232 | ||
| 250 | 233 | Finally, as mentioned above, you'll need to restart the session to |
| 251 | 234 | ensure that your user is using bash by default. |
| 252 | 235 | |
| 253 | 236 | You will also need to exit and re-open a shell (e.g., terminal or |
| 254 | terminal tab) any time you make changes to the =.bashrc= file. | |
| 237 | terminal tab) any time you make changes to the `.bashrc` | |
| 238 | file. | |
| 255 | 239 | |
| 256 | #+begin_src sh | |
| 240 | ```sh | |
| 257 | 241 | sudo reboot now |
| 258 | #+end_src | |
| 242 | ``` | |
blog/2022-08-31-privacy.com-changes.org → content/blog/2022-08-31-privacy.com-changes.md renamed +34 −46
| @@ -1,25 +1,24 @@ | ||
| 1 | #+title: Concerning Changes on Privacy.com | |
| 2 | #+date: 2022-08-31 | |
| 1 | +++ | |
| 2 | date = 2022-08-31 | |
| 3 | title = "Concering Changes on Privacy.com" | |
| 4 | description = "My thoughts on Privacy.com changing from a prepaid model to a credit charge model." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Privacy.com Changes Their Terms | |
| 3 | 8 | |
| 4 | ** Privacy.com Changes Their Terms | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: privacy.com-changes-their-terms | |
| 7 | :END: | |
| 8 | 9 | Recently, Privacy.com reached out to their customers regarding a change |
| 9 | 10 | in their terms of use. Further, all customers are required to agree to |
| 10 | 11 | the changes in order to continue using their accounts. |
| 11 | 12 | |
| 12 | [[https://privacy.com/commercial-cardholder-agreement][You can view the | |
| 13 | new cardholder agreement here]]. | |
| 13 | [You can view the new cardholder agreement | |
| 14 | here](https://privacy.com/commercial-cardholder-agreement). | |
| 14 | 15 | |
| 15 | 16 | When you log in, you'll be greeted with a pop-up window asking you to |
| 16 | 17 | review and agree to the new terms of use. You will also not be able to |
| 17 | 18 | open any new cards until the terms are agreed to. |
| 18 | 19 | |
| 19 | *** Changing from a "Prepaid Debit" Model to a "Charge Card" Model | |
| 20 | :PROPERTIES: | |
| 21 | :CUSTOM_ID: changing-from-a-prepaid-debit-model-to-a-charge-card-model | |
| 22 | :END: | |
| 20 | ### Changing from a "Prepaid Debit" Model to a "Charge Card" Model | |
| 21 | ||
| 23 | 22 | The actual content of the changes is interesting. While the historical |
| 24 | 23 | model of using Privacy.com was akin to prepaid debit cards, the new |
| 25 | 24 | model is very similar to a credit card (they use the term "charge |
| @@ -38,17 +37,15 @@ charges through Privacy.com and set the merchant as one of their pre-set | ||
| 38 | 37 | options, such as "Smiley's Corner Store" or "NSA Gift Shop." |
| 39 | 38 | |
| 40 | 39 | The new model still works with a bank account as a funding source, but |
| 41 | the model is changed so that you get a "line of credit" set according to | |
| 42 | a 14-day billing cycle. It seems that Privacy.com will now allow charges | |
| 43 | to be incurred without being immediately paid. | |
| 44 | ||
| 45 | *** Daily Payments and Available Credit | |
| 46 | :PROPERTIES: | |
| 47 | :CUSTOM_ID: daily-payments-and-available-credit | |
| 48 | :END: | |
| 40 | the model is changed so that you get a "line of credit" set according | |
| 41 | to a 14-day billing cycle. It seems that Privacy.com will now allow | |
| 42 | charges to be incurred without being immediately paid. | |
| 43 | ||
| 44 | ### Daily Payments and Available Credit | |
| 45 | ||
| 49 | 46 | Instead of paying as charges are incurred, you must make a "Daily |
| 50 | Payment" and your "Available Credit" will be locked until you make that | |
| 51 | payment. There are also "End of Billing Cycle Payments" that are | |
| 47 | Payment" and your "Available Credit" will be locked until you make | |
| 48 | that payment. There are also "End of Billing Cycle Payments" that are | |
| 52 | 49 | assigned a due date. |
| 53 | 50 | |
| 54 | 51 | Further, Privacy.com will decline charges that would cause you to exceed |
| @@ -56,23 +53,18 @@ your Available Credit or Credit Limit. | ||
| 56 | 53 | |
| 57 | 54 | One particular interesting section states the following: |
| 58 | 55 | |
| 59 | #+begin_quote | |
| 60 | YOUR OBLIGATION TO PAY US BACK FOR ALL CARD TRANSACTIONS AND OTHER | |
| 61 | OBLIGATIONS YOU INCUR IS SECURED BY THE SECURED ACCOUNT. IF YOU DO NOT | |
| 62 | PAY US BACK FOR ANY AMOUNT YOU OWE US WHEN YOUR PAYMENTS ARE DUE, WE | |
| 63 | WILL EXERCISE OUR INTEREST AND DEBIT THE SECURED ACCOUNT, AND YOU WILL | |
| 64 | LOSE THE MONEY IN THE SECURED ACCOUNT. SEE SECTION 8: SECURITY AGREEMENT | |
| 65 | FOR MORE INFORMATION. | |
| 56 | > YOUR OBLIGATION TO PAY US BACK FOR ALL CARD TRANSACTIONS AND OTHER | |
| 57 | > OBLIGATIONS YOU INCUR IS SECURED BY THE SECURED ACCOUNT. IF YOU DO NOT | |
| 58 | > PAY US BACK FOR ANY AMOUNT YOU OWE US WHEN YOUR PAYMENTS ARE DUE, WE | |
| 59 | > WILL EXERCISE OUR INTEREST AND DEBIT THE SECURED ACCOUNT, AND YOU WILL | |
| 60 | > LOSE THE MONEY IN THE SECURED ACCOUNT. SEE SECTION 8: SECURITY | |
| 61 | > AGREEMENT FOR MORE INFORMATION. | |
| 66 | 62 | |
| 67 | #+end_quote | |
| 63 | ### Personal Information | |
| 68 | 64 | |
| 69 | *** Personal Information | |
| 70 | :PROPERTIES: | |
| 71 | :CUSTOM_ID: personal-information | |
| 72 | :END: | |
| 73 | 65 | Now that Privacy.com is more of a financial institution, they are |
| 74 | obligated to comply with the | |
| 75 | [[https://en.wikipedia.org/wiki/Know_your_customer][know your customer]] | |
| 66 | obligated to comply with the [know your | |
| 67 | customer](https://en.wikipedia.org/wiki/Know_your_customer) | |
| 76 | 68 | guidelines/laws. |
| 77 | 69 | |
| 78 | 70 | I did not proceed with the change to my Privacy.com account, but I have |
| @@ -81,17 +73,13 @@ to be submitted, such as SSN. I am not aware of all new personal | ||
| 81 | 73 | information required or if the funding source is now required to only be |
| 82 | 74 | a bank account. |
| 83 | 75 | |
| 84 | *** Fees | |
| 85 | :PROPERTIES: | |
| 86 | :CUSTOM_ID: fees | |
| 87 | :END: | |
| 76 | ### Fees | |
| 77 | ||
| 88 | 78 | Luckily, the fees section did not change much. The subscription fees for |
| 89 | 79 | a premium account are still the only fees. |
| 90 | 80 | |
| 91 | ** My Thoughts | |
| 92 | :PROPERTIES: | |
| 93 | :CUSTOM_ID: my-thoughts | |
| 94 | :END: | |
| 81 | ## My Thoughts | |
| 82 | ||
| 95 | 83 | Personally, I wiped my personal information from my account and then |
| 96 | 84 | permanently deleted it when I heard about these changes. I have no |
| 97 | 85 | interest in yet another method of credit lending offered by private |
| @@ -105,6 +93,6 @@ This type of change toward a "buy it now, pay us later" model is | ||
| 105 | 93 | concerning, and I will be watching Privacy.com to see if they further |
| 106 | 94 | their interests in the credit model as time goes on. |
| 107 | 95 | |
| 108 | Could we see them start charging interest, fees, etc.? I'm not sure, but | |
| 109 | this change does not inspire confidence in their mission as a | |
| 96 | Could we see them start charging interest, fees, etc.? I'm not sure, | |
| 97 | but this change does not inspire confidence in their mission as a | |
| 110 | 98 | privacy-focused company. |
blog/2022-09-17-serenity-os.org → content/blog/2022-09-17-serenity-os.md renamed +55 −66
| @@ -1,11 +1,12 @@ | ||
| 1 | #+title: Serenity OS: Testing Out a Unique System | |
| 2 | #+date: 2022-09-17 | |
| 3 | ||
| 4 | ** Overview | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: overview | |
| 7 | :END: | |
| 8 | [[https://serenityos.org][SerenityOS]] is a unique operating system (OS) | |
| 1 | +++ | |
| 2 | date = 2022-09-17 | |
| 3 | title = "Serenity OS: Testing Out a Unique System" | |
| 4 | description = "A quick look at the Serenity operating system." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Overview | |
| 8 | ||
| 9 | [SerenityOS](https://serenityos.org) is a unique operating system (OS) | |
| 9 | 10 | that I have seen pop up in my news feed a few times over the last few |
| 10 | 11 | years, but I have never had time to test it out until now. |
| 11 | 12 | |
| @@ -15,51 +16,42 @@ to my youth. | ||
| 15 | 16 | |
| 16 | 17 | Per their website: |
| 17 | 18 | |
| 18 | #+begin_quote | |
| 19 | A graphical Unix-like operating system for desktop computers! | |
| 20 | ||
| 21 | SerenityOS is a love letter to '90s user interfaces with a custom | |
| 22 | Unix-like core. It flatters with sincerity by stealing beautiful ideas | |
| 23 | from various other systems. | |
| 24 | ||
| 25 | Roughly speaking, the goal is a marriage between the aesthetic of | |
| 26 | late-1990s productivity software and the power-user accessibility of | |
| 27 | late-2000s *nix. | |
| 19 | > A graphical Unix-like operating system for desktop computers! | |
| 20 | > | |
| 21 | > SerenityOS is a love letter to '90s user interfaces with a custom | |
| 22 | > Unix-like core. It flatters with sincerity by stealing beautiful ideas | |
| 23 | > from various other systems. | |
| 24 | > | |
| 25 | > Roughly speaking, the goal is a marriage between the aesthetic of | |
| 26 | > late-1990s productivity software and the power-user accessibility of | |
| 27 | > late-2000s *nix. | |
| 28 | > | |
| 29 | > This is a system by us, for us, based on the things we like. | |
| 28 | 30 | |
| 29 | This is a system by us, for us, based on the things we like. | |
| 31 | ## Building | |
| 30 | 32 | |
| 31 | #+end_quote | |
| 33 | Your first question may be "Where's the iso?" and the answer is... | |
| 34 | there are none. SerenityOS does not provide pre-built images for | |
| 35 | testing. You must build the images yourself. This seems intentionally to | |
| 36 | limit participation to only those who are truly interested enough to | |
| 37 | learn how to build the OS. | |
| 32 | 38 | |
| 33 | ** Building | |
| 34 | :PROPERTIES: | |
| 35 | :CUSTOM_ID: building | |
| 36 | :END: | |
| 37 | Your first question may be "Where's the iso?" and the answer is... there | |
| 38 | are none. SerenityOS does not provide pre-built images for testing. You | |
| 39 | must build the images yourself. This seems intentionally to limit | |
| 40 | participation to only those who are truly interested enough to learn how | |
| 41 | to build the OS. | |
| 39 | ### Clone | |
| 42 | 40 | |
| 43 | *** Clone | |
| 44 | :PROPERTIES: | |
| 45 | :CUSTOM_ID: clone | |
| 46 | :END: | |
| 47 | 41 | In order to get started, you'll need to clone the source repository: |
| 48 | 42 | |
| 49 | #+begin_src sh | |
| 43 | ```sh | |
| 50 | 44 | git clone https://github.com/SerenityOS/serenity && cd serenity |
| 51 | #+end_src | |
| 52 | ||
| 53 | *** Build | |
| 54 | :PROPERTIES: | |
| 55 | :CUSTOM_ID: build | |
| 56 | :END: | |
| 57 | Note that I followed the | |
| 58 | [[https://github.com/SerenityOS/serenity/blob/master/Documentation/BuildInstructions.md][Build | |
| 59 | Instructions]] in the SerenityOS repository as of commit | |
| 60 | =660d2b53b1206e868d5470eee80b5e62d7e30da7=. Things may have changed | |
| 61 | since my installation, and you should double-check the instructions | |
| 62 | first. | |
| 45 | ``` | |
| 46 | ||
| 47 | ### Build | |
| 48 | ||
| 49 | Note that I followed the [Build | |
| 50 | Instructions](https://github.com/SerenityOS/serenity/blob/master/Documentation/BuildInstructions.md) | |
| 51 | in the SerenityOS repository as of commit | |
| 52 | `660d2b53b1206e868d5470eee80b5e62d7e30da7`. Things may have | |
| 53 | changed since my installation, and you should double-check the | |
| 54 | instructions first. | |
| 63 | 55 | |
| 64 | 56 | Regardless, I want to repeat my steps here to illustrate any errors or |
| 65 | 57 | differing commands I needed to run in order to build and run SerenityOS. |
| @@ -67,61 +59,58 @@ differing commands I needed to run in order to build and run SerenityOS. | ||
| 67 | 59 | Since I am running Fedora, I needed to install these packages in order |
| 68 | 60 | to build the OS images: |
| 69 | 61 | |
| 70 | #+begin_src sh | |
| 62 | ```sh | |
| 71 | 63 | sudo 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 |
| 72 | #+end_src | |
| 64 | ``` | |
| 73 | 65 | |
| 74 | Next, make sure you're inside the =serenity= directory created earlier | |
| 75 | during the git cloning process and process to build the toolchain: | |
| 66 | Next, make sure you're inside the `serenity` directory | |
| 67 | created earlier during the git cloning process and process to build the | |
| 68 | toolchain: | |
| 76 | 69 | |
| 77 | #+begin_src sh | |
| 70 | ```sh | |
| 78 | 71 | Meta/serenity.sh rebuild-toolchain |
| 79 | #+end_src | |
| 72 | ``` | |
| 80 | 73 | |
| 81 | 74 | Once the toolchain is built, you can Build and run the OS! |
| 82 | 75 | |
| 83 | #+begin_src sh | |
| 76 | ```sh | |
| 84 | 77 | Meta/serenity.sh run |
| 85 | #+end_src | |
| 78 | ``` | |
| 86 | 79 | |
| 87 | 80 | After this process is completed, the image should run automatically and |
| 88 | 81 | launch. |
| 89 | 82 | |
| 90 | ** Issues | |
| 91 | :PROPERTIES: | |
| 92 | :CUSTOM_ID: issues | |
| 93 | :END: | |
| 83 | ## Issues | |
| 84 | ||
| 94 | 85 | I played around in SerenityOS for an hour or two in order to see what I |
| 95 | 86 | could do and had a lot of fun with it. The only issue I ran into was a |
| 96 | 87 | lack of working internet. I didn't try very hard, but I could tell that |
| 97 | 88 | the main network link wasn't connecting to my Fedora host properly. |
| 98 | 89 | |
| 99 | ** Screenshots | |
| 100 | :PROPERTIES: | |
| 101 | :CUSTOM_ID: screenshots | |
| 102 | :END: | |
| 90 | ## Screenshots | |
| 91 | ||
| 103 | 92 | The initial launch of the image displays the SerenityOS desktop, with a |
| 104 | 93 | simple terminal already launched: |
| 105 | 94 | |
| 106 | [[https://img.cleberg.net/blog/20220917-serenityos/initial_launch.png]] | |
| 95 |  | |
| 107 | 96 | |
| 108 | 97 | Here you can see the Fire application (literally just shows fire |
| 109 | 98 | burning), a browser with the local Serenity Browser page loaded, and a |
| 110 | 99 | text editor. |
| 111 | 100 | |
| 112 | [[https://img.cleberg.net/blog/20220917-serenityos/basic_apps.png]] | |
| 101 |  | |
| 113 | 102 | |
| 114 | 103 | I also poked around the system utilities and found most tools you'd |
| 115 | 104 | expect to find within a standard desktop. |
| 116 | 105 | |
| 117 | [[https://img.cleberg.net/blog/20220917-serenityos/system_monitor.png]] | |
| 106 |  | |
| 118 | 107 | |
| 119 | 108 | Lastly, I noted that the default desktop contains numerous pre-defined |
| 120 | 109 | themes to choose from. This is a small piece, but it's actually |
| 121 | 110 | wonderful to see desktop developers consider theming directly out of the |
| 122 | 111 | box rather than using an addon-based mentality. |
| 123 | 112 | |
| 124 | [[https://img.cleberg.net/blog/20220917-serenityos/themes.png]] | |
| 113 |  | |
| 125 | 114 | |
| 126 | 115 | I didn't take a screenshot of the other pre-installed games, but I did |
| 127 | 116 | spend nearly 30 minutes playing Solitaire before remembering that I was |
content/blog/2022-09-21-graphene-os.md added +167
| @@ -0,0 +1,167 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-09-21 | |
| 3 | title = "Installing Graphene OS on the Pixel 6 Pro" | |
| 4 | description = "A retrospective on the successful command-line installation of Graphene OS on a Pixel 6 Pro." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Introduction | |
| 8 | ||
| 9 | After using iOS for a couple of years, I finally took the plunge and | |
| 10 | purchased a Pixel 6 Pro in order to test and use [GrapheneOS] | |
| 11 | (<https://grapheneos.org>). | |
| 12 | ||
| 13 | The installation process was rather quick once you have the tools and | |
| 14 | files you need. Overall, it can be done in just a few minutes. | |
| 15 | ||
| 16 | ## Gathering Tools & Files | |
| 17 | ||
| 18 | ### Android Tools | |
| 19 | ||
| 20 | First, in order to interact with the device, we will need the [Android | |
| 21 | platform | |
| 22 | tools](https://developer.android.com/studio/releases/platform-tools.html). | |
| 23 | Find the Linux download and save the ZIP folder to your preferred | |
| 24 | location. | |
| 25 | ||
| 26 | Once we've downloaded the files, we will need to unzip them, enter the | |
| 27 | directory, and move the necessary executables to a central location, | |
| 28 | such as `/usr/bin/`. For this installation, we only need the | |
| 29 | `fastboot` and `adb` executables. | |
| 30 | ||
| 31 | ```sh | |
| 32 | cd ~/Downloads | |
| 33 | ``` | |
| 34 | ||
| 35 | ```sh | |
| 36 | unzip platform-tools_r33.0.3-linux.zip | |
| 37 | cd platform-tools | |
| 38 | sudo mv fastboot /usr/bin/ | |
| 39 | sudo mv adb /usr/bin | |
| 40 | ``` | |
| 41 | ||
| 42 | ### GrapheneOS Files | |
| 43 | ||
| 44 | Next, we need the [GrapheneOS files](https://grapheneos.org/releases) | |
| 45 | for our device and model. For example, the Pixel 6 Pro is codenamed | |
| 46 | `raven` on the release page. | |
| 47 | ||
| 48 | Once we have the links, let's download them to our working directory: | |
| 49 | ||
| 50 | ```sh | |
| 51 | curl -O https://releases.grapheneos.org/factory.pub | |
| 52 | curl -0 https://releases.grapheneos.org/raven-factory-2022091400.zip | |
| 53 | curl -0 https://releases.grapheneos.org/raven-factory-2022091400.zip.sig | |
| 54 | ``` | |
| 55 | ||
| 56 | 1. Validate Integrity | |
| 57 | ||
| 58 | In order to validate the integrity of the downloaded files, we will | |
| 59 | need the `signify` package and Graphene's | |
| 60 | `factory.pub` file. | |
| 61 | ||
| 62 | ```sh | |
| 63 | sudo dnf install signify | |
| 64 | ``` | |
| 65 | ||
| 66 | ```sh | |
| 67 | curl -O https://releases.grapheneos.org/factory.pub | |
| 68 | ``` | |
| 69 | ||
| 70 | Then we can validate the files and ensure that no data was corrupted | |
| 71 | or modified before it was saved to our device. | |
| 72 | ||
| 73 | ```sh | |
| 74 | signify -Cqp factory.pub -x raven-factory-2022091400.zip.sig && echo verified | |
| 75 | ``` | |
| 76 | ||
| 77 | 2. Unzip Files | |
| 78 | ||
| 79 | Once the files are verified, we can unzip the Graphene image and | |
| 80 | enter the directory: | |
| 81 | ||
| 82 | ```sh | |
| 83 | unzip raven-factory-2022091400.zip && cd raven-factory-2022091400 | |
| 84 | ``` | |
| 85 | ||
| 86 | ## Installation Process | |
| 87 | ||
| 88 | ### Enable Developer Debugging & OEM Unlock | |
| 89 | ||
| 90 | Before we can actually flash anything to the phone, we will need to | |
| 91 | enable OEM Unlocking, as well as either USB Debugging or Wireless | |
| 92 | Debugging, depending on which method we will be using. | |
| 93 | ||
| 94 | To start, enable developer mode by going to `Settings` > | |
| 95 | `About` and tapping `Build Number` seven (7) | |
| 96 | times. You may need to enter your PIN to enable this mode. | |
| 97 | ||
| 98 | Once developer mode is enabled, go to `Settings` > | |
| 99 | `System` > `Devloper Options` and enable OEM | |
| 100 | Unlocking, as well as USB or Wireless Debugging. In my case, I chose USB | |
| 101 | Debugging and performed all actions via USB cable. | |
| 102 | ||
| 103 | Once these options are enabled, plug the phone into the computer and | |
| 104 | execute the following command: | |
| 105 | ||
| 106 | ```sh | |
| 107 | adb devices | |
| 108 | ``` | |
| 109 | ||
| 110 | If an unauthorized error occurs, make sure the USB mode on the phone is | |
| 111 | changed from charging to something like "File Transfer" or "PTP." | |
| 112 | You can find the USB mode in the notification tray. | |
| 113 | ||
| 114 | ### Reboot Device | |
| 115 | ||
| 116 | Once we have found the device via `adb`, we can either boot | |
| 117 | into the bootloader interface by holding the volume down button while | |
| 118 | the phone reboots or by executing the following command: | |
| 119 | ||
| 120 | ```sh | |
| 121 | adb reboot bootloader | |
| 122 | ``` | |
| 123 | ||
| 124 | ### Unlock the Bootloader | |
| 125 | ||
| 126 | The phone will reboot and load the bootloader screen upon startup. At | |
| 127 | this point, we are ready to start the actual flashing of GrapheneOS onto | |
| 128 | the device. | |
| 129 | ||
| 130 | **NOTE**: In my situation, I needed to use `sudo` with every | |
| 131 | `fastboot` command, but not with `adb` commands. I | |
| 132 | am not sure if this is standard or a Fedora quirk, but I'm documenting | |
| 133 | my commands verbatim in this post. | |
| 134 | ||
| 135 | First, we start by unlocking the bootloader so that we can load other | |
| 136 | ROMs: | |
| 137 | ||
| 138 | ```sh | |
| 139 | sudo fastboot flashing unlock | |
| 140 | ``` | |
| 141 | ||
| 142 | ### Flashing Factory Images | |
| 143 | ||
| 144 | Once the phone is unlocked, we can flash it with the | |
| 145 | `flash-all.sh` script found inside the | |
| 146 | `raven-factory-2022091400` folder we entered earlier: | |
| 147 | ||
| 148 | ```sh | |
| 149 | sudo ./flash-all.sh | |
| 150 | ``` | |
| 151 | ||
| 152 | This process should take a few minutes and will print informational | |
| 153 | messages as things progress. Avoid doing anything on the phone while | |
| 154 | this process is operating. | |
| 155 | ||
| 156 | ### Lock the Bootloader | |
| 157 | ||
| 158 | If everything was successful, the phone should reboot a few times and | |
| 159 | finally land back on the bootloader screen. At this point, we can | |
| 160 | re-lock the bootloader to enable full verified boot and protect the | |
| 161 | device from unwanted flashing or erasure of data. | |
| 162 | ||
| 163 | ```sh | |
| 164 | sudo fastboot flashing lock | |
| 165 | ``` | |
| 166 | ||
| 167 | Once done, the device will be wiped and ready for a fresh set-up! | |
blog/2022-10-04-mtp-linux.org → content/blog/2022-10-04-mtp-linux.md renamed +36 −41
| @@ -1,14 +1,16 @@ | ||
| 1 | #+title: How to Mount an MTP Mobile Device on Linux | |
| 2 | #+date: 2022-10-04 | |
| 1 | +++ | |
| 2 | date = 2022-10-04 | |
| 3 | title = "How to Mount an MTP Mobile Device on Fedora Linux" | |
| 4 | description = "Learn how to mount an MTP mobile device on Fedora Linux." | |
| 5 | +++ | |
| 3 | 6 | |
| 4 | I recently ran into trouble attempting to mount my GrapheneOS phone to my laptop | |
| 5 | running Fedora Linux via the [[https://en.wikipedia.org/wiki/Media_transfer_protocol][Media Transfer Protocol]] (MTP) and discovered a | |
| 6 | simple and effective solution. | |
| 7 | I recently ran into trouble attempting to mount my GrapheneOS phone to | |
| 8 | my laptop running Fedora Linux via the [Media Transfer | |
| 9 | Protocol](https://en.wikipedia.org/wiki/Media_transfer_protocol) (MTP) | |
| 10 | and discovered a simple and effective solution. | |
| 11 | ||
| 12 | ## Use a USB 3.0 Port | |
| 7 | 13 | |
| 8 | ** Use a USB 3.0 Port | |
| 9 | :PROPERTIES: | |
| 10 | :CUSTOM_ID: use-a-usb-3.0-port | |
| 11 | :END: | |
| 12 | 14 | First, ensure that the device was plugged in to the laptop through a USB |
| 13 | 15 | 3.0 port, if possible. From a brief glance online, it seems that USB 2.0 |
| 14 | 16 | ports may cause issues with dropped connections over MTP. This is purely |
| @@ -16,70 +18,63 @@ anecdotal since I don't have any evidence to link showing that USB 2.0 | ||
| 16 | 18 | causes issues, but I can confirm that switching to a USB 3.0 port seemed |
| 17 | 19 | to cut out most of my issues. |
| 18 | 20 | |
| 19 | ** Switch USB Preferences to MTP | |
| 20 | :PROPERTIES: | |
| 21 | :CUSTOM_ID: switch-usb-preferences-to-mtp | |
| 22 | :END: | |
| 21 | ## Switch USB Preferences to MTP | |
| 22 | ||
| 23 | 23 | Secondly, you need to ensure that the phone's USB preferences/mode is |
| 24 | 24 | changed to MTP or File Transfer once the phone is plugged in. Other |
| 25 | 25 | modes will not allow you to access the phone's file system. |
| 26 | 26 | |
| 27 | ** Install =jmtpfs= | |
| 28 | :PROPERTIES: | |
| 29 | :CUSTOM_ID: install-jmtpfs | |
| 30 | :END: | |
| 31 | Next, I used the =jmtpfs= package to mount my phone to my laptop. There | |
| 32 | are other packages that exist, but this one worked perfectly for me. On | |
| 33 | Fedora Linux, you can install it like this: | |
| 27 | ## Install `jmtpfs` | |
| 28 | ||
| 29 | Next, I used the `jmtpfs` package to mount my phone to my | |
| 30 | laptop. There are other packages that exist, but this one worked | |
| 31 | perfectly for me. On Fedora Linux, you can install it like this: | |
| 34 | 32 | |
| 35 | #+begin_src sh | |
| 33 | ```sh | |
| 36 | 34 | sudo dnf install jmtpfs -y |
| 37 | #+end_src | |
| 35 | ``` | |
| 36 | ||
| 37 | ## Create a Mount Point | |
| 38 | 38 | |
| 39 | ** Create a Mount Point | |
| 40 | :PROPERTIES: | |
| 41 | :CUSTOM_ID: create-a-mount-point | |
| 42 | :END: | |
| 43 | 39 | Once you have the package installed, you just need to create a folder |
| 44 | for the device to use as a mount point. In my case, I used =/mnt/pixel=: | |
| 40 | for the device to use as a mount point. In my case, I used | |
| 41 | `/mnt/pixel`: | |
| 45 | 42 | |
| 46 | #+begin_src sh | |
| 43 | ```sh | |
| 47 | 44 | sudo mkdir /mnt/pixel |
| 48 | 45 | sudo chown -R $USER:$USER /mnt/pixel |
| 49 | #+end_src | |
| 46 | ``` | |
| 47 | ||
| 48 | ## Mount & Access the Phone's File System | |
| 50 | 49 | |
| 51 | ** Mount & Access the Phone's File System | |
| 52 | :PROPERTIES: | |
| 53 | :CUSTOM_ID: mount-access-the-phones-file-system | |
| 54 | :END: | |
| 55 | 50 | Finally, plug-in and mount the device, and you should be able to see all |
| 56 | 51 | storage (internal and external) inside your new folder! |
| 57 | 52 | |
| 58 | #+begin_src sh | |
| 53 | ```sh | |
| 59 | 54 | jmtpfs /mnt/pixel |
| 60 | #+end_src | |
| 55 | ``` | |
| 61 | 56 | |
| 62 | 57 | The output should look something like this: |
| 63 | 58 | |
| 64 | #+begin_src sh | |
| 59 | ```sh | |
| 65 | 60 | Device 0 (VID=18d1 and PID=4ee1) is a Google Inc Nexus/Pixel (MTP). |
| 66 | 61 | Android device detected, assigning default bug flags |
| 67 | #+end_src | |
| 62 | ``` | |
| 68 | 63 | |
| 69 | 64 | Now you are mounted and can do anything you'd like with the device's |
| 70 | 65 | files: |
| 71 | 66 | |
| 72 | #+begin_src sh | |
| 67 | ```sh | |
| 73 | 68 | cd /mnt/pixel |
| 74 | 69 | ls -lha |
| 75 | #+end_src | |
| 70 | ``` | |
| 76 | 71 | |
| 77 | 72 | From here, you will be able to see any internal or external storage |
| 78 | 73 | available on the device: |
| 79 | 74 | |
| 80 | #+begin_src sh | |
| 75 | ```sh | |
| 81 | 76 | total 0 |
| 82 | 77 | drwxr-xr-x. 3 user user 0 Jan 1 1970 . |
| 83 | 78 | drwxr-xr-x. 1 root root 10 Oct 4 13:29 .. |
| 84 | 79 | drwxr-xr-x. 16 user user 0 Apr 21 4426383 'Internal shared storage' |
| 85 | #+end_src | |
| 80 | ``` | |
blog/2022-10-20-syncthing.org → content/blog/2022-10-20-syncthing.md renamed +91 −111
| @@ -1,83 +1,70 @@ | ||
| 1 | #+title: Syncthing: A Minimal Self-Hosted Cloud Storage Solution | |
| 2 | #+date: 2022-10-20 | |
| 1 | +++ | |
| 2 | date = 2022-10-04 | |
| 3 | title = "Syncthing: A Minimal Self-Hosted Cloud Storage Solution" | |
| 4 | description = "" | |
| 5 | +++ | |
| 6 | ||
| 7 | ## An Overview of Syncthing | |
| 3 | 8 | |
| 4 | ** An Overview of Syncthing | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: an-overview-of-syncthing | |
| 7 | :END: | |
| 8 | 9 | If you've been looking around the self-hosted cloud storage space for a |
| 9 | 10 | while, you've undoubtedly run into someone suggesting |
| 10 | [[https://syncthing.net][Syncthing]] as an option. However, it is an | |
| 11 | [Syncthing](https://syncthing.net) as an option. However, it is an | |
| 11 | 12 | unusual alternative for those users out there who are used to having a |
| 12 | centralized cloud server that serves as the "controller" of the data and | |
| 13 | interacts with clients on devices to fetch files. | |
| 13 | centralized cloud server that serves as the "controller" of the data | |
| 14 | and interacts with clients on devices to fetch files. | |
| 14 | 15 | |
| 15 | 16 | This post is a walkthrough of the Syncthing software, how I set up my |
| 16 | 17 | personal storage, and some pros and cons of using the software. |
| 17 | 18 | |
| 18 | ** Installing Syncthing | |
| 19 | :PROPERTIES: | |
| 20 | :CUSTOM_ID: installing-syncthing | |
| 21 | :END: | |
| 19 | ## Installing Syncthing | |
| 20 | ||
| 22 | 21 | To install Syncthing, visit the |
| 23 | [[https://syncthing.net/downloads/][Downloads]] page or install via your | |
| 22 | [Downloads](https://syncthing.net/downloads/) page or install via your | |
| 24 | 23 | device's package manager. |
| 25 | 24 | |
| 26 | *** Server & Desktop | |
| 27 | :PROPERTIES: | |
| 28 | :CUSTOM_ID: server-desktop | |
| 29 | :END: | |
| 25 | ### Server & Desktop | |
| 26 | ||
| 30 | 27 | You can install Syncthing on servers and desktops via the Downloads page |
| 31 | 28 | linked above or via the command-line. |
| 32 | 29 | |
| 33 | 30 | For Debian-based distros: |
| 34 | 31 | |
| 35 | #+begin_src sh | |
| 32 | ```sh | |
| 36 | 33 | sudo apt install syncthing |
| 37 | #+end_src | |
| 34 | ``` | |
| 38 | 35 | |
| 39 | 36 | For Fedora-based distros: |
| 40 | 37 | |
| 41 | #+begin_src sh | |
| 38 | ```sh | |
| 42 | 39 | sudo dnf install syncthing |
| 43 | #+end_src | |
| 40 | ``` | |
| 41 | ||
| 42 | ### Mobile | |
| 44 | 43 | |
| 45 | *** Mobile | |
| 46 | :PROPERTIES: | |
| 47 | :CUSTOM_ID: mobile | |
| 48 | :END: | |
| 49 | 44 | Syncthing for Android is available on |
| 50 | [[https://f-droid.org/packages/com.nutomic.syncthingandroid/][F-Droid]] | |
| 51 | and | |
| 52 | [[https://play.google.com/store/apps/details?id=com.nutomic.syncthingandroid][Google | |
| 53 | Play]]. Syncthing does not have an official iOS client, but there is a | |
| 54 | third-party client called | |
| 55 | [[https://apps.apple.com/us/app/m%C3%B6bius-sync/id1539203216][Möbius | |
| 56 | Sync]]. | |
| 57 | ||
| 58 | ** How Does Syncthing Work? | |
| 59 | :PROPERTIES: | |
| 60 | :CUSTOM_ID: how-does-syncthing-work | |
| 61 | :END: | |
| 45 | [F-Droid](https://f-droid.org/packages/com.nutomic.syncthingandroid/) | |
| 46 | and [Google | |
| 47 | Play](https://play.google.com/store/apps/details?id=com.nutomic.syncthingandroid). | |
| 48 | Syncthing does not have an official iOS client, but there is a | |
| 49 | third-party client called [Möbius | |
| 50 | Sync](https://apps.apple.com/us/app/m%C3%B6bius-sync/id1539203216). | |
| 51 | ||
| 52 | ## How Does Syncthing Work? | |
| 53 | ||
| 62 | 54 | To start, I wanted to include the main marketing blurb from their |
| 63 | 55 | website: |
| 64 | 56 | |
| 65 | #+begin_quote | |
| 66 | Syncthing is a continuous file synchronization program. It synchronizes | |
| 67 | files between two or more computers in real time, safely protected from | |
| 68 | prying eyes. Your data is your data alone, and you deserve to choose | |
| 69 | where it is stored, whether it is shared with some third party, and how | |
| 70 | it's transmitted over the internet. | |
| 71 | ||
| 72 | #+end_quote | |
| 57 | > Syncthing is a continuous file synchronization program. It | |
| 58 | > synchronizes files between two or more computers in real time, safely | |
| 59 | > protected from prying eyes. Your data is your data alone, and you | |
| 60 | > deserve to choose where it is stored, whether it is shared with some | |
| 61 | > third party, and how it's transmitted over the internet. | |
| 73 | 62 | |
| 74 | 63 | Let's break this apart and add in some other details to help explain |
| 75 | 64 | what exactly Syncthing does in order to sync files between devices. |
| 76 | 65 | |
| 77 | *** Local Syncthing Server(s) | |
| 78 | :PROPERTIES: | |
| 79 | :CUSTOM_ID: local-syncthing-servers | |
| 80 | :END: | |
| 66 | ### Local Syncthing Server(s) | |
| 67 | ||
| 81 | 68 | Syncthing syncs files between multiple devices by creating a local |
| 82 | 69 | server on each device. These local servers handle a few different |
| 83 | 70 | things, such as watching files and directories for changes, hosting an |
| @@ -85,43 +72,40 @@ administrative GUI website, and authenticating with connected devices. | ||
| 85 | 72 | |
| 86 | 73 | You can also start, stop, and restart the Syncthing server via the |
| 87 | 74 | command-line or web dashboard. If you're running Syncthing on a device |
| 88 | with =systemd=, you can use the following commands: | |
| 75 | with `systemd`, you can use the following commands: | |
| 89 | 76 | |
| 90 | #+begin_src sh | |
| 77 | ```sh | |
| 91 | 78 | sudo systemctl start syncthing@username.service |
| 92 | 79 | sudo systemctl restart syncthing@username.service |
| 93 | 80 | sudo systemctl stop syncthing@username.service |
| 94 | #+end_src | |
| 81 | ``` | |
| 82 | ||
| 83 | ### Syncthing Dashboard | |
| 95 | 84 | |
| 96 | *** Syncthing Dashboard | |
| 97 | :PROPERTIES: | |
| 98 | :CUSTOM_ID: syncthing-dashboard | |
| 99 | :END: | |
| 100 | 85 | This biggest part of Syncthing is the admin GUI website that runs on |
| 101 | 86 | each device (note that mobile devices will use the Syncthing app rather |
| 102 | 87 | than the web GUI). The admin GUI is available through the web browser on |
| 103 | 88 | the local device that is running Syncthing - simply go to |
| 104 | =http://localhost:8384= or =http://127.0.0.1:8384=. This web page is the | |
| 105 | place where you will change settings, add/modify synced files, and | |
| 106 | add/modify connected devices. | |
| 89 | `http://localhost:8384` or | |
| 90 | `http://127.0.0.1:8384`. This web page is the place where you | |
| 91 | will change settings, add/modify synced files, and add/modify connected | |
| 92 | devices. | |
| 107 | 93 | |
| 108 | 94 | Here's an example web GUI dashboard: |
| 109 | 95 | |
| 110 | #+caption: Syncthing Dashboard | |
| 111 | [[https://img.cleberg.net/blog/20221020-syncthing/syncthing_gui.png]] | |
| 96 |  | |
| 98 | ||
| 99 | ### Remote Devices | |
| 112 | 100 | |
| 113 | *** Remote Devices | |
| 114 | :PROPERTIES: | |
| 115 | :CUSTOM_ID: remote-devices | |
| 116 | :END: | |
| 117 | 101 | A cloud storage solution wouldn't be very useful if you aren't able to |
| 118 | 102 | share data among various devices. Syncthing does this by sharing Device |
| 119 | 103 | IDs to connect servers, and then by manually sharing Folders with |
| 120 | 104 | devices that have been connected. |
| 121 | 105 | |
| 122 | 106 | For instance, if you have a laptop running Syncthing and then install |
| 123 | the Syncthing mobile app on a phone, you could scan the laptop's QR code | |
| 124 | for Device ID and then accept the authentication on the laptop's | |
| 107 | the Syncthing mobile app on a phone, you could scan the laptop's QR | |
| 108 | code for Device ID and then accept the authentication on the laptop's | |
| 125 | 109 | dashboard. Next, you can use either device to select a folder for |
| 126 | 110 | sharing and dictating which device should send, receive, or both. |
| 127 | 111 | |
| @@ -130,10 +114,8 @@ which can add devices from the introducer to the device list, for | ||
| 130 | 114 | mutually shared folders. You can also configure Auto Accept, |
| 131 | 115 | compression, rate limits, and more settings per device. |
| 132 | 116 | |
| 133 | ** My Personal Cloud Storage Set-up | |
| 134 | :PROPERTIES: | |
| 135 | :CUSTOM_ID: my-personal-cloud-storage-set-up | |
| 136 | :END: | |
| 117 | ## My Personal Cloud Storage Set-up | |
| 118 | ||
| 137 | 119 | Personally, I use a model similar to a traditional cloud storage |
| 138 | 120 | service. I have a "centralized" server running 24/7 that acts as an |
| 139 | 121 | Introducer for my Syncthing network. I think of this as my main storage |
| @@ -147,52 +129,50 @@ network, both running intermittently as they are not powered-on 24/7. | ||
| 147 | 129 | The initial set-up of the software was easy enough, but data transfer |
| 148 | 130 | rates were incredibly slow for me due to the Wi-Fi. Instead, I plugged |
| 149 | 131 | my laptop into the ethernet network that my server is on and manually |
| 150 | copied my folders over to the server with =scp=. Once complete, | |
| 151 | Syncthing validated that all files were there and not missing, and it | |
| 152 | did not need to transfer any data through the WAN. | |
| 132 | copied my folders over to the server with `scp`. Once | |
| 133 | complete, Syncthing validated that all files were there and not missing, | |
| 134 | and it did not need to transfer any data through the WAN. | |
| 153 | 135 | |
| 154 | 136 | As slow as the transfer was going, this probably saved me a few days of |
| 155 | 137 | waiting for my ~100GB sync. |
| 156 | 138 | |
| 157 | ** Pros & Cons | |
| 158 | :PROPERTIES: | |
| 159 | :CUSTOM_ID: pros-cons | |
| 160 | :END: | |
| 161 | I've put together a short list of pros and cons for Syncthing. I thought | |
| 162 | about my experiences with Nextcloud, WebDAV, proprietary services | |
| 163 | (Google Drive, iCloud, etc.), and privacy-focused cloud solutions | |
| 164 | (pCloud, Tresorit, etc.). | |
| 165 | ||
| 166 | *Pros:* | |
| 167 | ||
| 168 | - I've faced no data loss at all through my two-month trial run. | |
| 169 | - No third-parties store your data on their servers. | |
| 170 | - You have full control over your data and can take your data and leave | |
| 171 | at any time. | |
| 172 | - It's possible to encrypt client-side easily with software like | |
| 173 | Cryptomator. | |
| 174 | - No proprietary clients or mounted volumes, just plain files and | |
| 175 | folders. | |
| 176 | ||
| 177 | *Cons:* | |
| 178 | ||
| 179 | - The learning curve is steeper than traditional cloud services and is | |
| 180 | focused on a technical audience. | |
| 181 | - If a device needs to modify files in a Folder, the devices will need | |
| 182 | to sync ALL files from the folder, which may be large. To avoid size | |
| 183 | restraints, split large folders into smaller folders for syncing. | |
| 184 | - Syncing can be slow due to the clients/servers initially connecting or | |
| 185 | re-connecting after sleeping. | |
| 186 | - Multiple personal devices are required and require the user to own or | |
| 187 | rent them as no third-party servers are involved in the storage of | |
| 188 | data. | |
| 189 | ||
| 190 | Overall, I've had a great experience with Syncthing so far. I've had no | |
| 191 | data loss, syncing has been quick and easy when changes are made to | |
| 139 | ## Pros & Cons | |
| 140 | ||
| 141 | I've put together a short list of pros and cons for Syncthing. I | |
| 142 | thought about my experiences with Nextcloud, WebDAV, proprietary | |
| 143 | services (Google Drive, iCloud, etc.), and privacy-focused cloud | |
| 144 | solutions (pCloud, Tresorit, etc.). | |
| 145 | ||
| 146 | **Pros:** | |
| 147 | ||
| 148 | - I've faced no data loss at all through my two-month trial run. | |
| 149 | - No third-parties store your data on their servers. | |
| 150 | - You have full control over your data and can take your data and | |
| 151 | leave at any time. | |
| 152 | - It's possible to encrypt client-side easily with software like | |
| 153 | Cryptomator. | |
| 154 | - No proprietary clients or mounted volumes, just plain files and | |
| 155 | folders. | |
| 156 | ||
| 157 | **Cons:** | |
| 158 | ||
| 159 | - The learning curve is steeper than traditional cloud services and is | |
| 160 | focused on a technical audience. | |
| 161 | - If a device needs to modify files in a Folder, the devices will need | |
| 162 | to sync ALL files from the folder, which may be large. To avoid size | |
| 163 | restraints, split large folders into smaller folders for syncing. | |
| 164 | - Syncing can be slow due to the clients/servers initially connecting | |
| 165 | or re-connecting after sleeping. | |
| 166 | - Multiple personal devices are required and require the user to own | |
| 167 | or rent them as no third-party servers are involved in the storage | |
| 168 | of data. | |
| 169 | ||
| 170 | Overall, I've had a great experience with Syncthing so far. I've had | |
| 171 | no data loss, syncing has been quick and easy when changes are made to | |
| 192 | 172 | files, device connections are reliable, and I love the freedom of |
| 193 | 173 | controlling the clients and servers as I choose. |
| 194 | 174 | |
| 195 | 175 | Not to mention that I appreciate that I - or someone else - could pull |
| 196 | the Syncthing [[https://github.com/syncthing][source code]] and continue | |
| 176 | the Syncthing [source code](https://github.com/syncthing) and continue | |
| 197 | 177 | development/support if the Syncthing Foundation decides to stop |
| 198 | 178 | developing the software or sells the business. |
content/blog/2022-10-22-alpine-linux.md added +280
| @@ -0,0 +1,280 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-10-22 | |
| 3 | title = "Alpine Linux: My New Server OS" | |
| 4 | description = "A retrospective on installing and configuring Alpine Linux as my new server operating system." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Alpine Linux | |
| 8 | ||
| 9 | [Alpine Linux](https://alpinelinux.org) is a very small distro, built on | |
| 10 | musl libc and busybox. It uses ash as the default shell, OpenRC as the | |
| 11 | init system, and apk as the package manager. According to their website, | |
| 12 | an Alpine container "requires no more than 8 MB and a minimal | |
| 13 | installation to disk requires around 130 MB of storage." An actual bare | |
| 14 | metal machine is recommended to have 100 MB of RAM and 0-700 MB of | |
| 15 | storage space. | |
| 16 | ||
| 17 | Historically, I've used Ubuntu's minimal installation image as my | |
| 18 | server OS for the last five years. Ubuntu worked well and helped as my | |
| 19 | original server contained an nVidia GPU and no onboard graphics, so | |
| 20 | quite a few distros won't boot or install without a lot of tinkering. | |
| 21 | ||
| 22 | Alpine has given me a huge increase in performance across my Docker apps | |
| 23 | and Nginx websites. CPU load for the new server I'm using to test | |
| 24 | Alpine hovers around 0-5% on average with an Intel(R) Core(TM) i3-6100 | |
| 25 | CPU @ 3.70GHz. | |
| 26 | ||
| 27 | The only services I haven't moved over to Alpine are Plex Media Server | |
| 28 | and Syncthing, which may increase CPU load quite a bit depending on how | |
| 29 | many streams are running. | |
| 30 | ||
| 31 | ### Installation | |
| 32 | ||
| 33 | In terms of installation, Alpine has an incredibly useful | |
| 34 | [wiki](https://wiki.alpinelinux.org/wiki/Installation) that will guide a | |
| 35 | user throughout the installation and post-installation processes, as | |
| 36 | well as various other articles and guides. | |
| 37 | ||
| 38 | To install Alpine, find an appropriate [image to | |
| 39 | download](https://alpinelinux.org/downloads/) and flash it to a USB | |
| 40 | using software such as Rufus or Etcher. I opted to use the Standard | |
| 41 | image for my x86~64~ architecture. | |
| 42 | ||
| 43 | Once the USB is ready, plug it into the machine and reboot. Note that | |
| 44 | you may have to use a key such as `Esc` or `F1-12` | |
| 45 | to access the boot menu. The Alpine Linux terminal will load quickly and | |
| 46 | for a login. | |
| 47 | ||
| 48 | To log in to the installation image, use the `root` account; | |
| 49 | there is no password. Once logged-in, execute the setup command: | |
| 50 | ||
| 51 | ```sh | |
| 52 | setup-alpine | |
| 53 | ``` | |
| 54 | ||
| 55 | The setup script will ask a series of questions to configure the system. | |
| 56 | Be sure to answer carefully or else you may have to re-configure the | |
| 57 | system after boot. | |
| 58 | ||
| 59 | - Keyboard Layout (Local keyboard language and usage mode, e.g., us | |
| 60 | and variant of us-nodeadkeys.) | |
| 61 | - Hostname (The name for the computer.) | |
| 62 | - Network (For example, automatic IP address discovery with the | |
| 63 | "DHCP" protocol.) | |
| 64 | - DNS Servers (Domain Name Servers to query. For privacy reasons, it | |
| 65 | is NOT recommended to route every local request to servers like | |
| 66 | Google's 8.8.8.8 .) | |
| 67 | - Timezone | |
| 68 | - Proxy (Proxy server to use for accessing the web. Use "none" for | |
| 69 | direct connections to the internet.) | |
| 70 | - Mirror (From where to download packages. Choose the organization you | |
| 71 | trust giving your usage patterns to.) | |
| 72 | - SSH (Secure SHell remote access server. "Openssh" is part of the | |
| 73 | default install image. Use "none" to disable remote login, e.g. on | |
| 74 | laptops.) | |
| 75 | - NTP (Network Time Protocol client used for keeping the system clock | |
| 76 | in sync with a time-server. Package "chrony" is part of the | |
| 77 | default install image.) | |
| 78 | - Disk Mode (Select between diskless (disk="none"), "data" or | |
| 79 | "sys", as described above.) | |
| 80 | ||
| 81 | Once the setup script is finished, be sure to reboot the machine and | |
| 82 | remove the USB device. | |
| 83 | ||
| 84 | ```sh | |
| 85 | reboot | |
| 86 | ``` | |
| 87 | ||
| 88 | ### Post-Installation | |
| 89 | ||
| 90 | There are many things you can do once your Alpine Linux system is up and | |
| 91 | running, and it largely depends on what you'll use the machine for. | |
| 92 | I'm going to walk through my personal post-installation setup for my | |
| 93 | web server. | |
| 94 | ||
| 95 | 1. Upgrade the System | |
| 96 | ||
| 97 | First, login as `root` in order to update and upgrade the | |
| 98 | system: | |
| 99 | ||
| 100 | ```sh | |
| 101 | apk -U upgrade | |
| 102 | ``` | |
| 103 | ||
| 104 | 2. Adding a User | |
| 105 | ||
| 106 | I needed to add a user so that I don't need to log in as root. Note | |
| 107 | that if you're used to using the `sudo` command, you | |
| 108 | will now need to use the `doas` command on Alpine Linux. | |
| 109 | ||
| 110 | ```sh | |
| 111 | apk add doas | |
| 112 | adduser <username> | |
| 113 | adduser <username> wheel | |
| 114 | ``` | |
| 115 | ||
| 116 | You can now log out and log back in using the newly-created user: | |
| 117 | ||
| 118 | ```sh | |
| 119 | exit | |
| 120 | ``` | |
| 121 | ||
| 122 | 3. Enable Community Packages | |
| 123 | ||
| 124 | In order to install more common packages that aren't found in the | |
| 125 | `main` repository, you will need to enable the | |
| 126 | `community` repository: | |
| 127 | ||
| 128 | ```sh | |
| 129 | doas nano /etc/apk/repositories | |
| 130 | ``` | |
| 131 | ||
| 132 | Uncomment the community line for whichever version of Alpine you're | |
| 133 | running: | |
| 134 | ||
| 135 | ```sh | |
| 136 | /media/usb/apks | |
| 137 | http://dl-cdn.alpinelinux.org/alpine/v3.16/main | |
| 138 | http://dl-cdn.alpinelinux.org/alpine/v3.16/community | |
| 139 | #http://dl-cdn.alpinelinux.org/alpine/edge/main | |
| 140 | #http://dl-cdn.alpinelinux.org/alpine/edge/community | |
| 141 | #http://dl-cdn.alpinelinux.org/alpine/edge/testing | |
| 142 | ``` | |
| 143 | ||
| 144 | 4. Install Required Packages | |
| 145 | ||
| 146 | Now that the community packages are available, you can install any | |
| 147 | packages you need. In my case, I installed the web server packages I | |
| 148 | need for my services: | |
| 149 | ||
| 150 | ```sh | |
| 151 | doas apk add nano nginx docker docker-compose ufw | |
| 152 | ``` | |
| 153 | ||
| 154 | 5. SSH | |
| 155 | ||
| 156 | If you didn't install OpenSSH as part of the installation, you can | |
| 157 | do so now: | |
| 158 | ||
| 159 | ```sh | |
| 160 | doas apk add openssh | |
| 161 | ``` | |
| 162 | ||
| 163 | Next, either create a new key or copy your SSH key to the server | |
| 164 | from your current machines: | |
| 165 | ||
| 166 | ```sh | |
| 167 | # Create a new key | |
| 168 | ssh-keygen | |
| 169 | ``` | |
| 170 | ||
| 171 | If you need to copy an existing SSH key from a current machine: | |
| 172 | ||
| 173 | ```sh | |
| 174 | # Copy key from existing machines | |
| 175 | ssh-copy-id <username>@<ip_address> | |
| 176 | ``` | |
| 177 | ||
| 178 | 6. Firewall | |
| 179 | ||
| 180 | Lastly, I installed `ufw` above as my firewall. To set | |
| 181 | up, default to deny incoming and allow outgoing connections. Then | |
| 182 | selectively allow other ports or apps as needed. | |
| 183 | ||
| 184 | ```sh | |
| 185 | doas ufw default deny incoming | |
| 186 | doas ufw default allow outgoing | |
| 187 | doas ufw allow SSH | |
| 188 | doas ufw allow "WWW Full" | |
| 189 | doas ufw allow 9418 # Git server port | |
| 190 | ``` | |
| 191 | ||
| 192 | 7. Change Hostname | |
| 193 | ||
| 194 | If you don't like the hostname set during installation, you just | |
| 195 | need to edit two files. First, edit the simple hostname file: | |
| 196 | ||
| 197 | ```sh | |
| 198 | doas nano /etc/hostname | |
| 199 | ``` | |
| 200 | ||
| 201 | ```sh | |
| 202 | <hostname> | |
| 203 | ``` | |
| 204 | ||
| 205 | Next, edit the `hosts` file: | |
| 206 | ||
| 207 | ```sh | |
| 208 | doas nano /etc/hosts | |
| 209 | ``` | |
| 210 | ||
| 211 | ```sh | |
| 212 | 127.0.0.1 <hostname>.local <hostname> localhost.local localhost | |
| 213 | ::1 <hostname> <hostname>.local | |
| 214 | ``` | |
| 215 | ||
| 216 | ## Nginx Web Server | |
| 217 | ||
| 218 | To set up my web server, I simply created the `www` user and | |
| 219 | created the necessary files. | |
| 220 | ||
| 221 | ```sh | |
| 222 | doas adduser -D -g 'www' www | |
| 223 | mkdir /www | |
| 224 | doas mkdir /www | |
| 225 | doas chown -R www:www /var/lib/nginx/ | |
| 226 | doas chown -R www:www /www | |
| 227 | ``` | |
| 228 | ||
| 229 | If you're running a simple webroot, you can alter the main | |
| 230 | `nginx.conf` file. Otherwise, you can drop configuration | |
| 231 | files in the following directory. You don't need to enable or symlink | |
| 232 | the configuration file like you do in other systems. | |
| 233 | ||
| 234 | ```sh | |
| 235 | doas nano /etc/nginx/http.d/example_website.conf | |
| 236 | ``` | |
| 237 | ||
| 238 | Once the configuration is set and pointed at the `/www` | |
| 239 | directory to serve files, enable the Nginx service: | |
| 240 | ||
| 241 | ```sh | |
| 242 | # Note that 'default' must be included or Nginx will not start on boot | |
| 243 | doas rc-update add nginx default | |
| 244 | ``` | |
| 245 | ||
| 246 | ## Docker Containers | |
| 247 | ||
| 248 | Docker works exactly the same as other systems. Either execute a | |
| 249 | `docker run` command or create a | |
| 250 | `docker-compose.yml` file and do | |
| 251 | `docker-compose up -d`. | |
| 252 | ||
| 253 | ## Git Server | |
| 254 | ||
| 255 | I went in-depth on how to self-host a git server in another post: | |
| 256 | [Self-Hosting a Personal Git Server](/blog/git-server/). | |
| 257 | ||
| 258 | However, there are a few differences with Alpine. First note that in | |
| 259 | order to change the `git` user's shell, you must do a few | |
| 260 | things a little different: | |
| 261 | ||
| 262 | ```sh | |
| 263 | doas apk add libuser | |
| 264 | doas touch /etc/login.defs | |
| 265 | doas mkdir /etc/default | |
| 266 | doas touch /etc/default/useradd | |
| 267 | doas lchsh git | |
| 268 | ``` | |
| 269 | ||
| 270 | ## Thoughts on Alpine | |
| 271 | ||
| 272 | So far, I love Alpine Linux. I have no complaints about anything at this | |
| 273 | point, but I'm not completely finished with the migration yet. Once | |
| 274 | I'm able to upgrade my hardware to a rack-mounted server, I will | |
| 275 | migrate Plex and Syncthing over to Alpine as well - possibly putting | |
| 276 | Plex into a container or VM. | |
| 277 | ||
| 278 | The performance is stellar, the `apk` package manager is | |
| 279 | seamless, and system administration tasks are effortless. My only regret | |
| 280 | is that I didn't install Alpine sooner. | |
content/blog/2022-10-30-linux-display-manager.md added +81
| @@ -0,0 +1,81 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-10-30 | |
| 3 | title = "How to Disable or Change the Display Manager on Void Linux" | |
| 4 | description = "Learn how to remove or modify the display manager on Void Linux." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Display Manager Services | |
| 8 | ||
| 9 | In order to change the [display | |
| 10 | manager](https://en.wikipedia.org/wiki/Display_manager) on Void Linux - | |
| 11 | or any other Linux distro - you need to identify the currently enabled | |
| 12 | display manager. | |
| 13 | ||
| 14 | ### Disabling the Current Display Manager | |
| 15 | ||
| 16 | Void Linux only has one ISO available for download with a pre-built | |
| 17 | display manager at the time of this post: the XFCE ISO. If you've | |
| 18 | installed this version, the pre-assigned display manager is | |
| 19 | `lxdm`. If you installed another display manager, replace | |
| 20 | `lxdm` in the following command with the display manager you | |
| 21 | have installed. | |
| 22 | ||
| 23 | To disable `lxdm`, simply remove the service symlink: | |
| 24 | ||
| 25 | ```sh | |
| 26 | sudo rm /var/service/lxdm | |
| 27 | ``` | |
| 28 | ||
| 29 | ### Enabling a New Display Manager | |
| 30 | ||
| 31 | If you want to enable a new display manager, you can do so after | |
| 32 | `lxdm` is disabled. Make sure to replace | |
| 33 | `<new_display_manager>` with your new DM, such as | |
| 34 | `gdm`, `xdm`, etc. | |
| 35 | ||
| 36 | ```sh | |
| 37 | sudo ln -s /etc/sv/<new_display_manager> /var/service | |
| 38 | ``` | |
| 39 | ||
| 40 | ## Set Up `.xinitrc` | |
| 41 | ||
| 42 | Depending on your setup, you may need to create a few X files, such as | |
| 43 | `~/.xinitrc`. For my personal set-up, I created this file to | |
| 44 | launch the i3wm as my desktop. | |
| 45 | ||
| 46 | ```sh | |
| 47 | nano ~/.xinitrc | |
| 48 | ``` | |
| 49 | ||
| 50 | ```sh | |
| 51 | #!/bin/sh | |
| 52 | ||
| 53 | exec i3 | |
| 54 | ``` | |
| 55 | ||
| 56 | If you run a desktop other than i3, simply replace `i3` with | |
| 57 | the shell command that launches that desktop. | |
| 58 | ||
| 59 | ## Set Up Your Shell Profile | |
| 60 | ||
| 61 | Finally, in order to automatically launch an X session upon login, you | |
| 62 | will need to edit the `.bash_profile` (bash) or | |
| 63 | `.zprofile` (zsh) files for your shell: | |
| 64 | ||
| 65 | ```sh | |
| 66 | nano ~/.zprofile | |
| 67 | ``` | |
| 68 | ||
| 69 | Add the following snippet to the end of the shell profile file. This | |
| 70 | will execute the `startx` command upon login. | |
| 71 | ||
| 72 | ```sh | |
| 73 | if [ -z "${DISPLAY}" ] && [ "${XDG_VTNR}" -eq 1 ]; then | |
| 74 | exec startx | |
| 75 | fi | |
| 76 | ``` | |
| 77 | ||
| 78 | Alternatively, you can ignore this step and simply choose to manually | |
| 79 | execute `startx` upon login. This can be useful if you have | |
| 80 | issues with your desktop or like to manually launch different desktops | |
| 81 | by choice. | |
blog/2022-11-07-matrix-synapse.org → content/blog/2022-11-07-matrix-synapse.md renamed +82 −92
| @@ -1,114 +1,106 @@ | ||
| 1 | #+title: Self-Hosting Matrix Synapse on Alpine Linux | |
| 2 | #+date: 2022-11-07 | |
| 1 | +++ | |
| 2 | date = 2022-11-07 | |
| 3 | title = "Self-Hosting Matrix Synapse on Alpine Linux" | |
| 4 | description = "A guide to self-hosting the Matrix Synapse application on your own server." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Synpase | |
| 3 | 8 | |
| 4 | ** Synpase | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: synpase | |
| 7 | :END: | |
| 8 | 9 | If you're reading this, you likely know that |
| 9 | [[https://github.com/matrix-org/synapse/][Synapse]] is a popular | |
| 10 | [[https://matrix.org/][Matrix]] home server software that allows users | |
| 11 | to run their own Matrix home server. | |
| 10 | [Synapse](https://github.com/matrix-org/synapse/) is a popular | |
| 11 | [Matrix](https://matrix.org/) home server software that allows users to | |
| 12 | run their own Matrix home server. | |
| 12 | 13 | |
| 13 | 14 | This post is a short guide describing how I was able to get Synapse |
| 14 | 15 | working in a minimally-usable state on Alpine Linux. |
| 15 | 16 | |
| 16 | ** Installation Process | |
| 17 | :PROPERTIES: | |
| 18 | :CUSTOM_ID: installation-process | |
| 19 | :END: | |
| 20 | *** Dependencies | |
| 21 | :PROPERTIES: | |
| 22 | :CUSTOM_ID: dependencies | |
| 23 | :END: | |
| 17 | ## Installation Process | |
| 18 | ||
| 19 | ### Dependencies | |
| 20 | ||
| 24 | 21 | First, since there is no Alpine-specific package for Synapse, we need to |
| 25 | 22 | ensure that Alpine has the required dependencies for the Python-based |
| 26 | 23 | installation method. |
| 27 | 24 | |
| 28 | #+begin_src sh | |
| 25 | ```sh | |
| 29 | 26 | doas apk -U update |
| 30 | 27 | doas apk add python3 py3-virtualenv |
| 31 | #+end_src | |
| 28 | ``` | |
| 32 | 29 | |
| 33 | 30 | Next, we need to set up a Python virtual environment for Synapse: |
| 34 | 31 | |
| 35 | #+begin_src sh | |
| 32 | ```sh | |
| 36 | 33 | mkdir -p ~/synapse && cd ~/synapse |
| 37 | 34 | virtualenv -p python3 ~/synapse/env |
| 38 | 35 | source ~/synapse/env/bin/activate |
| 39 | 36 | pip install --upgrade pip |
| 40 | 37 | pip install --upgrade setuptools |
| 41 | 38 | pip install matrix-synapse |
| 42 | #+end_src | |
| 39 | ``` | |
| 40 | ||
| 41 | ### Running Synapse | |
| 43 | 42 | |
| 44 | *** Running Synapse | |
| 45 | :PROPERTIES: | |
| 46 | :CUSTOM_ID: running-synapse | |
| 47 | :END: | |
| 48 | 43 | Once installed, running Synapse is easy. Simply execute the following |
| 49 | command, replacing =example.com= with the domain name that will be used | |
| 50 | with this home server. This will generate the configuration files needed | |
| 51 | to run the server. | |
| 44 | command, replacing `example.com` with the domain name that | |
| 45 | will be used with this home server. This will generate the configuration | |
| 46 | files needed to run the server. | |
| 52 | 47 | |
| 53 | #+begin_src sh | |
| 48 | ```sh | |
| 54 | 49 | python -m synapse.app.homeserver \ |
| 55 | 50 | --server-name example.com \ |
| 56 | 51 | --config-path homeserver.yaml \ |
| 57 | 52 | --generate-config \ |
| 58 | 53 | --report-stats=no |
| 59 | #+end_src | |
| 54 | ``` | |
| 60 | 55 | |
| 61 | 56 | Once the configuration is generated, we can start up the Synapse server: |
| 62 | 57 | |
| 63 | #+begin_src sh | |
| 58 | ```sh | |
| 64 | 59 | synctl start |
| 65 | #+end_src | |
| 60 | ``` | |
| 61 | ||
| 62 | ### Configuring Synapse | |
| 66 | 63 | |
| 67 | *** Configuring Synapse | |
| 68 | :PROPERTIES: | |
| 69 | :CUSTOM_ID: configuring-synapse | |
| 70 | :END: | |
| 71 | To make any change to Synapse, we need to edit the =YAML= configuration | |
| 72 | file: | |
| 64 | To make any change to Synapse, we need to edit the `YAML` | |
| 65 | configuration file: | |
| 73 | 66 | |
| 74 | #+begin_src sh | |
| 67 | ```sh | |
| 75 | 68 | nano ~/synapse/homeserver.yaml |
| 76 | #+end_src | |
| 69 | ``` | |
| 77 | 70 | |
| 78 | For now, we just need to ensure the =server_name= is accurate. However, | |
| 79 | there are a lot of other configuration options found in the | |
| 80 | [[https://matrix-org.github.io/synapse/develop/usage/configuration/config_documentation.html][Configuring | |
| 81 | Synapse]] documentation that can be enabled/disabled at any point. | |
| 71 | For now, we just need to ensure the `server_name` is | |
| 72 | accurate. However, there are a lot of other configuration options found | |
| 73 | in the [Configuring | |
| 74 | Synapse](https://matrix-org.github.io/synapse/develop/usage/configuration/config_documentation.html) | |
| 75 | documentation that can be enabled/disabled at any point. | |
| 82 | 76 | |
| 83 | #+begin_src yaml | |
| 77 | ```yaml | |
| 84 | 78 | server_name: "example.com" |
| 85 | #+end_src | |
| 79 | ``` | |
| 86 | 80 | |
| 87 | 81 | Make sure to restart Synapse when you make changes to the configuration: |
| 88 | 82 | |
| 89 | #+begin_src sh | |
| 83 | ```sh | |
| 90 | 84 | synctl restart |
| 91 | #+end_src | |
| 85 | ``` | |
| 86 | ||
| 87 | ### Nginx Reverse-Proxy | |
| 92 | 88 | |
| 93 | *** Nginx Reverse-Proxy | |
| 94 | :PROPERTIES: | |
| 95 | :CUSTOM_ID: nginx-reverse-proxy | |
| 96 | :END: | |
| 97 | 89 | To ensure that Synapse is reachable from the public, we need to connect |
| 98 | 90 | our domain to the Synapse server. In my case, I use a Nginx |
| 99 | 91 | reverse-proxy for this purpose. |
| 100 | 92 | |
| 101 | 93 | To use Nginx, we need to create a reverse-proxy configuration file: |
| 102 | 94 | |
| 103 | #+begin_src sh | |
| 95 | ```sh | |
| 104 | 96 | doas nano /etc/nginx/http.d/example.com.conf |
| 105 | #+end_src | |
| 97 | ``` | |
| 106 | 98 | |
| 107 | If you already have TLS certificates for this domain (=example.com=), | |
| 108 | you can simply use the SSL configuration and point toward your TLS | |
| 109 | certificates. | |
| 99 | If you already have TLS certificates for this domain | |
| 100 | (`example.com`), you can simply use the SSL configuration and | |
| 101 | point toward your TLS certificates. | |
| 110 | 102 | |
| 111 | #+begin_src conf | |
| 103 | ```conf | |
| 112 | 104 | server { |
| 113 | 105 | listen 443 ssl http2; |
| 114 | 106 | listen [::]:443 ssl http2; |
| @@ -147,15 +139,15 @@ server { | ||
| 147 | 139 | listen 80; |
| 148 | 140 | return 404; |
| 149 | 141 | } |
| 150 | #+end_src | |
| 142 | ``` | |
| 151 | 143 | |
| 152 | 144 | If you need to generate TLS certificates (I recommend |
| 153 | [[https://certbot.eff.org/][Certbot]]), you'll need a more minimal Nginx | |
| 145 | [Certbot](https://certbot.eff.org/)), you'll need a more minimal Nginx | |
| 154 | 146 | conf file before you can use the TLS-enabled example above. Instead, use |
| 155 | 147 | this configuration file during the Certbot certificate generation |
| 156 | 148 | process: |
| 157 | 149 | |
| 158 | #+begin_src conf | |
| 150 | ```conf | |
| 159 | 151 | server { |
| 160 | 152 | server_name example.com; |
| 161 | 153 | location / { |
| @@ -163,66 +155,64 @@ server { | ||
| 163 | 155 | } |
| 164 | 156 | listen 80; |
| 165 | 157 | } |
| 166 | #+end_src | |
| 158 | ``` | |
| 167 | 159 | |
| 168 | 160 | Once you're done editing the Nginx conf file, restart Nginx: |
| 169 | 161 | |
| 170 | #+begin_src sh | |
| 162 | ```sh | |
| 171 | 163 | doas rc-service nginx restart |
| 172 | #+end_src | |
| 164 | ``` | |
| 173 | 165 | |
| 174 | If you still need to generate TLS certificates, run =certbot= now and | |
| 175 | obtain the certificates. Certbot will ask if you want to use a webroot | |
| 176 | or spin up a temporary web server. I *highly* recommend using the | |
| 177 | temporary web server due to the many issues with using a webroot. | |
| 166 | If you still need to generate TLS certificates, run `certbot` | |
| 167 | now and obtain the certificates. Certbot will ask if you want to use a | |
| 168 | webroot or spin up a temporary web server. I **highly** recommend using | |
| 169 | the temporary web server due to the many issues with using a webroot. | |
| 178 | 170 | |
| 179 | 171 | You will need to stop Nginx in order to user the temporary web server |
| 180 | 172 | option with Certbot: |
| 181 | 173 | |
| 182 | #+begin_src sh | |
| 174 | ```sh | |
| 183 | 175 | # Stop Nginx so certbot can spin up a temp webserver for cert generation |
| 184 | 176 | doas rc-service nginx stop |
| 185 | 177 | doas certbot certonly -v |
| 186 | 178 | doas rc-service nginx start |
| 187 | #+end_src | |
| 188 | ||
| 189 | *** Open Firewall & Router Ports | |
| 190 | :PROPERTIES: | |
| 191 | :CUSTOM_ID: open-firewall-router-ports | |
| 192 | :END: | |
| 193 | If you use a firewall on the server, open the =8448= port for discovery | |
| 194 | and federation, as well as the normal web server ports if you're using a | |
| 195 | reverse proxy. If you want additional services, such as voice calls, you | |
| 196 | will need to read the Synapse documentation to see which ports need to | |
| 197 | be opened for those features. | |
| 179 | ``` | |
| 180 | ||
| 181 | ### Open Firewall & Router Ports | |
| 182 | ||
| 183 | If you use a firewall on the server, open the `8448` port for | |
| 184 | discovery and federation, as well as the normal web server ports if | |
| 185 | you're using a reverse proxy. If you want additional services, such as | |
| 186 | voice calls, you will need to read the Synapse documentation to see | |
| 187 | which ports need to be opened for those features. | |
| 198 | 188 | |
| 199 | 189 | Here's an example of the Universal Firewall (UFW) software: |
| 200 | 190 | |
| 201 | #+begin_src sh | |
| 191 | ```sh | |
| 202 | 192 | # Matrix port |
| 203 | 193 | doas ufw allow 8448 |
| 204 | 194 | # Standard web server ports |
| 205 | 195 | doas ufw allow "Nginx Full" |
| 206 | #+end_src | |
| 196 | ``` | |
| 197 | ||
| 198 | Remember to forward any Synapse ports, such as `8448`, | |
| 199 | `80`, and `443`, in your Router from the internet | |
| 200 | to your server's IP address. | |
| 207 | 201 | |
| 208 | Remember to forward any Synapse ports, such as =8448=, =80=, and =443=, | |
| 209 | in your Router from the internet to your server's IP address. | |
| 202 | ### Adding Matrix Users | |
| 210 | 203 | |
| 211 | *** Adding Matrix Users | |
| 212 | :PROPERTIES: | |
| 213 | :CUSTOM_ID: adding-matrix-users | |
| 214 | :END: | |
| 215 | 204 | Finally, if you didn't enable public registration in the |
| 216 | =homeserver.yaml= file, you can manually create users via the | |
| 205 | `homeserver.yaml` file, you can manually create users via the | |
| 217 | 206 | command-line: |
| 218 | 207 | |
| 219 | #+begin_src sh | |
| 208 | ```sh | |
| 220 | 209 | cd ~/synapse |
| 221 | 210 | register_new_matrix_user -c homeserver.yaml |
| 222 | #+end_src | |
| 211 | ``` | |
| 223 | 212 | |
| 224 | 213 | Remember that the format for federated Matrix usernames is |
| 225 | =@username:example.com= when logging in to client applications. | |
| 214 | `@username:example.com` when logging in to client | |
| 215 | applications. | |
| 226 | 216 | |
| 227 | 217 | Once Synapse is running, and you have a username, you are ready to log |
| 228 | 218 | in to a Matrix client and start sending messages, joining rooms, and |
blog/2022-11-11-nginx-tmp-errors.org → content/blog/2022-11-11-nginx-tmp-errors.md renamed +35 −35
| @@ -1,13 +1,14 @@ | ||
| 1 | #+title: Fixing Permission Errors in /var/lib/nginx" description | |
| 2 | #+date: 2022-11-11 | |
| 1 | +++ | |
| 2 | date = 2022-11-11 | |
| 3 | title = "Fixing Permission Errors in /var/lib/nginx" | |
| 4 | description = "Learn how to fix permission errors related to the Nginx temporary file storage." | |
| 5 | +++ | |
| 3 | 6 | |
| 4 | /This is a brief post so that I personally remember the solution as it | |
| 5 | has occurred multiple times for me./ | |
| 7 | *This is a brief post so that I personally remember the solution as it | |
| 8 | has occurred multiple times for me.* | |
| 9 | ||
| 10 | ## The Problem | |
| 6 | 11 | |
| 7 | ** The Problem | |
| 8 | :PROPERTIES: | |
| 9 | :CUSTOM_ID: the-problem | |
| 10 | :END: | |
| 11 | 12 | After migrating to a new server OS, I started receiving quite a few |
| 12 | 13 | permission errors like the one below. These popped up for various |
| 13 | 14 | different websites I'm serving via Nginx on this server, but did not |
| @@ -15,47 +16,46 @@ prevent the website from loading. | ||
| 15 | 16 | |
| 16 | 17 | I found the errors in the standard log file: |
| 17 | 18 | |
| 18 | #+begin_src sh | |
| 19 | ```sh | |
| 19 | 20 | cat /var/log/nginx/error.log |
| 20 | #+end_src | |
| 21 | ``` | |
| 21 | 22 | |
| 22 | #+begin_src sh | |
| 23 | ```sh | |
| 23 | 24 | 2022/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/" |
| 24 | #+end_src | |
| 25 | ``` | |
| 25 | 26 | |
| 26 | You can see that the error is =13: Permission denied= and it occurs in | |
| 27 | the =/var/lib/nginx/tmp/= directory. In my case, I had thousands of | |
| 28 | errors where Nginx was denied permission to read/write files in this | |
| 29 | directory. | |
| 27 | You can see that the error is `13: Permission denied` and it | |
| 28 | occurs in the `/var/lib/nginx/tmp/` directory. In my case, I | |
| 29 | had thousands of errors where Nginx was denied permission to read/write | |
| 30 | files in this directory. | |
| 30 | 31 | |
| 31 | 32 | So how do I fix it? |
| 32 | 33 | |
| 33 | ** The Solution | |
| 34 | :PROPERTIES: | |
| 35 | :CUSTOM_ID: the-solution | |
| 36 | :END: | |
| 37 | In order to resolve the issue, I had to ensure the =/var/lib/nginx= | |
| 38 | directory is owned by Nginx. Mine was owned by the =www= user and Nginx | |
| 39 | was not able to read or write files within that directory. This | |
| 40 | prevented Nginx from caching temporary files. | |
| 34 | ## The Solution | |
| 35 | ||
| 36 | In order to resolve the issue, I had to ensure the | |
| 37 | `/var/lib/nginx` directory is owned by Nginx. Mine was owned | |
| 38 | by the `www` user and Nginx was not able to read or write | |
| 39 | files within that directory. This prevented Nginx from caching temporary | |
| 40 | files. | |
| 41 | 41 | |
| 42 | #+begin_src sh | |
| 42 | ```sh | |
| 43 | 43 | # Alpine Linux |
| 44 | 44 | doas chown -R nginx:nginx /var/lib/nginx |
| 45 | 45 | |
| 46 | 46 | # Other Distros |
| 47 | 47 | sudo chown -R nginx:nginx /var/lib/nginx |
| 48 | #+end_src | |
| 48 | ``` | |
| 49 | 49 | |
| 50 | You /may/ also be able to change the =proxy_temp_path= in your Nginx | |
| 51 | config, but I did not try this. Here's a suggestion I found online that | |
| 52 | may work if the above solution does not: | |
| 50 | You *may* also be able to change the `proxy_temp_path` in | |
| 51 | your Nginx config, but I did not try this. Here's a suggestion I found | |
| 52 | online that may work if the above solution does not: | |
| 53 | 53 | |
| 54 | #+begin_src sh | |
| 54 | ```sh | |
| 55 | 55 | nano /etc/nginx/http.d/example.com.conf |
| 56 | #+end_src | |
| 56 | ``` | |
| 57 | 57 | |
| 58 | #+begin_src conf | |
| 58 | ```conf | |
| 59 | 59 | server { |
| 60 | 60 | ... |
| 61 | 61 | |
| @@ -65,15 +65,15 @@ server { | ||
| 65 | 65 | |
| 66 | 66 | ... |
| 67 | 67 | } |
| 68 | #+end_src | |
| 68 | ``` | |
| 69 | 69 | |
| 70 | 70 | Finally, restart Nginx and your server should be able to cache temporary |
| 71 | 71 | files again. |
| 72 | 72 | |
| 73 | #+begin_src sh | |
| 73 | ```sh | |
| 74 | 74 | # Alpine Linux (OpenRC) |
| 75 | 75 | doas rc-service nginx restart |
| 76 | 76 | |
| 77 | 77 | # Other Distros (systemd) |
| 78 | 78 | sudo systemctl restart nginx |
| 79 | #+end_src | |
| 79 | ``` | |
blog/2022-11-27-server-build.org → content/blog/2022-11-27-server-build.md renamed +51 −61
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: Building a Custom Rack-Mount Server | |
| 2 | #+date: 2022-11-27 | |
| 1 | +++ | |
| 2 | date = 2022-11-27 | |
| 3 | title = "Building a Custom Rack-Mounted Server" | |
| 4 | description = "A retrospective on building my own rack-mounted server." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## The Dilemma | |
| 3 | 8 | |
| 4 | ** The Dilemma | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: the-dilemma | |
| 7 | :END: | |
| 8 | 9 | For years, I have been using desktops and a Raspberry Pi as the backbone |
| 9 | 10 | of my homelab. I have always wanted to move toward a single dedicated |
| 10 | 11 | server that could handle all of my tasks, but was often put off by the |
| @@ -20,37 +21,32 @@ After returning the R720XD, I decided that I wanted to build my own | ||
| 20 | 21 | server with modern, consumer-grade PC components. This time, I am very |
| 21 | 22 | happy with the results of my server. |
| 22 | 23 | |
| 23 | ** Components | |
| 24 | :PROPERTIES: | |
| 25 | :CUSTOM_ID: components | |
| 26 | :END: | |
| 24 | ## Components | |
| 25 | ||
| 27 | 26 | I'll start by listing all the components I used for this server build: |
| 28 | 27 | |
| 29 | - *Case*: | |
| 30 | [[https://www.rosewill.com/rosewill-rsv-r4100u-black/p/9SIA072GJ92825][Rosewill | |
| 31 | RSV-R4100U 4U Server Chassis Rackmount Case]] | |
| 32 | - *Motherboard*: [[https://nzxt.com/product/n7-b550][NZXT B550]] | |
| 33 | - *CPU*: AMD Ryzen 7 5700G with Radeon Graphics | |
| 34 | - *GPU*: N/A - I specifically chose one of the few AMD CPUs that support | |
| 35 | onboard graphics. | |
| 36 | - *RAM*: 64GB RAM (2x32GB) /Max of 128GB RAM on this motherboard/ | |
| 37 | - *Boot Drive*: Western Digital 500GB M.2 NVME SSD | |
| 38 | - *HDD Bay*: | |
| 39 | - 10TB WD White /(shucked, moved from previous server)/ | |
| 40 | - 8TB WD White /(shucked, moved from previous server)/ | |
| 41 | - 2 x 8TB WD Red Plus /(Black Friday lined up perfectly with this | |
| 42 | build, so I grabbed two of these)/ | |
| 43 | - *PSU*: Corsair RM850 PSU | |
| 44 | - *Extras*: | |
| 45 | - Corsair TM3Q Thermal Paste | |
| 46 | - Noctua 120mm fan /(replacement for front case fan)/ | |
| 47 | - 2 x Noctua 80mm fans /(replacement for rear case fans)/ | |
| 48 | - CableMatters 6Gbps SATA Cables | |
| 49 | ||
| 50 | ** Building the Server | |
| 51 | :PROPERTIES: | |
| 52 | :CUSTOM_ID: building-the-server | |
| 53 | :END: | |
| 28 | - **Case**: [Rosewill RSV-R4100U 4U Server Chassis Rackmount | |
| 29 | Case](https://www.rosewill.com/rosewill-rsv-r4100u-black/p/9SIA072GJ92825) | |
| 30 | - **Motherboard**: [NZXT B550](https://nzxt.com/product/n7-b550) | |
| 31 | - **CPU**: AMD Ryzen 7 5700G with Radeon Graphics | |
| 32 | - **GPU**: N/A - I specifically chose one of the few AMD CPUs that | |
| 33 | support onboard graphics. | |
| 34 | - **RAM**: 64GB RAM (2x32GB) *Max of 128GB RAM on this motherboard* | |
| 35 | - **Boot Drive**: Western Digital 500GB M.2 NVME SSD | |
| 36 | - **HDD Bay**: | |
| 37 | - 10TB WD White *(shucked, moved from previous server)* | |
| 38 | - 8TB WD White *(shucked, moved from previous server)* | |
| 39 | - 2 x 8TB WD Red Plus *(Black Friday lined up perfectly with this | |
| 40 | build, so I grabbed two of these)* | |
| 41 | - **PSU**: Corsair RM850 PSU | |
| 42 | - **Extras**: | |
| 43 | - Corsair TM3Q Thermal Paste | |
| 44 | - Noctua 120mm fan *(replacement for front case fan)* | |
| 45 | - 2 x Noctua 80mm fans *(replacement for rear case fans)* | |
| 46 | - CableMatters 6Gbps SATA Cables | |
| 47 | ||
| 48 | ## Building the Server | |
| 49 | ||
| 54 | 50 | This took quite a while for me to build (in my opinion of time), |
| 55 | 51 | totaling around 3 hours from start to finish. The case has some peculiar |
| 56 | 52 | construction, so you have to completely remove the ODD & HDD cages to |
| @@ -59,8 +55,8 @@ install the motherboard and other components first. | ||
| 59 | 55 | Now, I've never built a computer of any kind before, so I was quite |
| 60 | 56 | nervous. Personally, the only challenging part was getting the CPU |
| 61 | 57 | cooler to screw into the motherboard without sliding the thermal paste |
| 62 | around too much underneath. I'm still not entirely sure if I did a great | |
| 63 | job of it, but nothing's broken yet. | |
| 58 | around too much underneath. I'm still not entirely sure if I did a | |
| 59 | great job of it, but nothing's broken yet. | |
| 64 | 60 | |
| 65 | 61 | The main components were all fine and simple. However, installing the |
| 66 | 62 | hard drives is slightly tedious as I need to power off the server and |
| @@ -72,19 +68,15 @@ used to in other machines. | ||
| 72 | 68 | Seeing that the cases with hot-swap bays were 3-4x the price, I'm okay |
| 73 | 69 | dealing with the tedium of removing the cage to install new drives. |
| 74 | 70 | |
| 75 | ** Software | |
| 76 | :PROPERTIES: | |
| 77 | :CUSTOM_ID: software | |
| 78 | :END: | |
| 71 | ## Software | |
| 72 | ||
| 79 | 73 | I'm not going to dive into the software as I have done so in other |
| 80 | 74 | recent posts. However, I wanted to note that I am using Alpine Linux on |
| 81 | 75 | this server and hosting most services inside Docker. No virtual machines |
| 82 | 76 | (VMs) and very few bare-metal services. |
| 83 | 77 | |
| 84 | ** The Results | |
| 85 | :PROPERTIES: | |
| 86 | :CUSTOM_ID: the-results | |
| 87 | :END: | |
| 78 | ## The Results | |
| 79 | ||
| 88 | 80 | How did my build turn out? Well, after migrating my other servers and |
| 89 | 81 | their services over, I found that my server is blazing fast. The |
| 90 | 82 | heaviest of my applications, Plex, is handled with ease. Even 4k |
| @@ -94,10 +86,8 @@ I am very happy with the results and will likely continue to improve on | ||
| 94 | 86 | this server as the years go by rather than buying another used server |
| 95 | 87 | online. |
| 96 | 88 | |
| 97 | *** Mistakes I Made | |
| 98 | :PROPERTIES: | |
| 99 | :CUSTOM_ID: mistakes-i-made | |
| 100 | :END: | |
| 89 | ### Mistakes I Made | |
| 90 | ||
| 101 | 91 | This post wouldn't be complete unless I wrote about the mistakes I made |
| 102 | 92 | while building. The only real mistake I made beyond a "whoops I dropped |
| 103 | 93 | a screw" related to airflow and fan direction. |
| @@ -106,19 +96,19 @@ While installing the two new hard drives that showed up on 2022-11-30 | ||
| 106 | 96 | and getting ready to install the case in my rack, I noticed that the |
| 107 | 97 | hard drive temperatures were quite high. |
| 108 | 98 | |
| 109 | I used the =smartctl= command for each of my drives (=/dev/sda= through | |
| 110 | =/dev/sdd=): | |
| 99 | I used the `smartctl` command for each of my drives | |
| 100 | (`/dev/sda` through `/dev/sdd`): | |
| 111 | 101 | |
| 112 | #+begin_src sh | |
| 102 | ```sh | |
| 113 | 103 | doas smartctl -a /dev/sda | grep Temperature_Celsius |
| 114 | #+end_src | |
| 104 | ``` | |
| 115 | 105 | |
| 116 | The results were unusual - all four drives were idling at ~44-46 degrees | |
| 117 | Celsius. The only drive that was cooler was my 10TB drive, which was at | |
| 118 | 38 degrees Celsius. I noted that this 10TB drive was also closest to the | |
| 119 | case fan. | |
| 106 | The results were unusual - all four drives were idling at ~44-46 | |
| 107 | degrees Celsius. The only drive that was cooler was my 10TB drive, which | |
| 108 | was at 38 degrees Celsius. I noted that this 10TB drive was also closest | |
| 109 | to the case fan. | |
| 120 | 110 | |
| 121 | #+begin_src sh | |
| 111 | ```sh | |
| 122 | 112 | ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE |
| 123 | 113 | # /dev/sda |
| 124 | 114 | 194 Temperature_Celsius 0x0002 147 147 000 Old_age Always - 44 (Min/Max 22/46) |
| @@ -128,7 +118,7 @@ ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_ | ||
| 128 | 118 | 194 Temperature_Celsius 0x0002 144 144 000 Old_age Always - 45 (Min/Max 19/61) |
| 129 | 119 | # /dev/sdd |
| 130 | 120 | 194 Temperature_Celsius 0x0002 171 171 000 Old_age Always - 38 (Min/Max 14/56) |
| 131 | #+end_src | |
| 121 | ``` | |
| 132 | 122 | |
| 133 | 123 | After looking to see if I could fit more fans into the case, I noticed |
| 134 | 124 | that the 120mm fan used for intake from the front of the case was |
| @@ -140,7 +130,7 @@ dropped immediately! They are now idling at ~31-33 degrees Celsius. A | ||
| 140 | 130 | single fan spinning the wrong way caused my drives to idle 10-15 degrees |
| 141 | 131 | higher than they should have. |
| 142 | 132 | |
| 143 | #+begin_src sh | |
| 133 | ```sh | |
| 144 | 134 | ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE |
| 145 | 135 | # /dev/sda |
| 146 | 136 | 194 Temperature_Celsius 0x0002 209 209 000 Old_age Always - 31 (Min/Max 14/56) |
| @@ -150,7 +140,7 @@ ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_ | ||
| 150 | 140 | 194 Temperature_Celsius 0x0002 203 203 000 Old_age Always - 32 (Min/Max 21/48) |
| 151 | 141 | # /dev/sdd |
| 152 | 142 | 194 Temperature_Celsius 0x0002 196 196 000 Old_age Always - 33 (Min/Max 22/46) |
| 153 | #+end_src | |
| 143 | ``` | |
| 154 | 144 | |
| 155 | 145 | This was a silly error to make, but I'm glad I found it today before I |
| 156 | 146 | screwed the case into the rack and made things a lot more tedious to |
blog/2022-11-29-nginx-referrer-ban-list.org → content/blog/2022-11-29-nginx-referrer-ban-list.md renamed +54 −60
| @@ -1,24 +1,25 @@ | ||
| 1 | #+title: Creating a Referrer Ban List on Nginx | |
| 2 | #+date: 2022-11-29 | |
| 1 | +++ | |
| 2 | date = 2022-11-29 | |
| 3 | title = "Creating a Referrer Ban List in Nginx" | |
| 4 | description = "Learn how to create a ban list for referring sites in Nginx." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Creating the Ban List | |
| 3 | 8 | |
| 4 | ** Creating the Ban List | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: creating-the-ban-list | |
| 7 | :END: | |
| 8 | 9 | In order to ban list referral domains or websites with Nginx, you need |
| 9 | 10 | to create a ban list file. The file below will accept regexes for |
| 10 | 11 | different domains or websites you wish to block. |
| 11 | 12 | |
| 12 | 13 | First, create the file in your nginx directory: |
| 13 | 14 | |
| 14 | #+begin_src sh | |
| 15 | ```sh | |
| 15 | 16 | doas nano /etc/nginx/banlist.conf |
| 16 | #+end_src | |
| 17 | ``` | |
| 17 | 18 | |
| 18 | 19 | Next, paste the following contents in and fill out the regexes with |
| 19 | 20 | whichever domains you're blocking. |
| 20 | 21 | |
| 21 | #+begin_src conf | |
| 22 | ```conf | |
| 22 | 23 | # /etc/nginx/banlist.conf |
| 23 | 24 | |
| 24 | 25 | map $http_referer $bad_referer { |
| @@ -29,23 +30,21 @@ map $http_referer $bad_referer { | ||
| 29 | 30 | # Put regexes for undesired referrers here |
| 30 | 31 | "~news.ycombinator.com" 1; |
| 31 | 32 | } |
| 32 | #+end_src | |
| 33 | ``` | |
| 34 | ||
| 35 | ## Configuring Nginx | |
| 33 | 36 | |
| 34 | ** Configuring Nginx | |
| 35 | :PROPERTIES: | |
| 36 | :CUSTOM_ID: configuring-nginx | |
| 37 | :END: | |
| 38 | 37 | In order for the ban list to work, Nginx needs to know it exists and how |
| 39 | to handle it. For this, edit the =nginx.conf= file. | |
| 38 | to handle it. For this, edit the `nginx.conf` file. | |
| 40 | 39 | |
| 41 | #+begin_src sh | |
| 40 | ```sh | |
| 42 | 41 | doas nano /etc/nginx/nginx.conf |
| 43 | #+end_src | |
| 42 | ``` | |
| 44 | 43 | |
| 45 | Within this file, find the =http= block and add your ban list file | |
| 46 | location to the end of the block. | |
| 44 | Within this file, find the `http` block and add your ban list | |
| 45 | file location to the end of the block. | |
| 47 | 46 | |
| 48 | #+begin_src conf | |
| 47 | ```conf | |
| 49 | 48 | # /etc/nginx/nginx.conf |
| 50 | 49 | |
| 51 | 50 | http { |
| @@ -54,31 +53,30 @@ http { | ||
| 54 | 53 | # Include ban list |
| 55 | 54 | include /etc/nginx/banlist.conf; |
| 56 | 55 | } |
| 57 | #+end_src | |
| 56 | ``` | |
| 57 | ||
| 58 | ## Enabling the Ban List | |
| 58 | 59 | |
| 59 | ** Enabling the Ban List | |
| 60 | :PROPERTIES: | |
| 61 | :CUSTOM_ID: enabling-the-ban-list | |
| 62 | :END: | |
| 63 | 60 | Finally, we need to take action when a bad referral site is found. To do |
| 64 | 61 | so, edit the configuration file for your website. For example, I have |
| 65 | all website configuration files in the =http.d= directory. You may have | |
| 66 | them in the =sites-available= directory on some distributions. | |
| 62 | all website configuration files in the `http.d` directory. | |
| 63 | You may have them in the `sites-available` directory on some | |
| 64 | distributions. | |
| 67 | 65 | |
| 68 | #+begin_src sh | |
| 66 | ```sh | |
| 69 | 67 | doas nano /etc/nginx/http.d/example.com.conf |
| 70 | #+end_src | |
| 68 | ``` | |
| 71 | 69 | |
| 72 | Within each website's configuration file, edit the =server= blocks that | |
| 73 | are listening to ports 80 and 443 and create a check for the | |
| 74 | =$bad_referrer= variable we created in the ban list file. | |
| 70 | Within each website's configuration file, edit the `server` | |
| 71 | blocks that are listening to ports 80 and 443 and create a check for the | |
| 72 | `$bad_referrer` variable we created in the ban list file. | |
| 75 | 73 | |
| 76 | If a matching site is found, you can return any | |
| 77 | [[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes][HTTP Status | |
| 78 | Code]] you want. Code 403 (Forbidden) is logical in this case since you | |
| 79 | are preventing a client connection due to a banned domain. | |
| 74 | If a matching site is found, you can return any [HTTP Status | |
| 75 | Code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) you want. | |
| 76 | Code 403 (Forbidden) is logical in this case since you are preventing a | |
| 77 | client connection due to a banned domain. | |
| 80 | 78 | |
| 81 | #+begin_src conf | |
| 79 | ```conf | |
| 82 | 80 | server { |
| 83 | 81 | ... |
| 84 | 82 | |
| @@ -86,49 +84,45 @@ server { | ||
| 86 | 84 | if ($bad_referer) { |
| 87 | 85 | return 403; |
| 88 | 86 | } |
| 89 | ||
| 87 | ||
| 90 | 88 | ... |
| 91 | 89 | } |
| 92 | #+end_src | |
| 90 | ``` | |
| 91 | ||
| 92 | ## Restart Nginx | |
| 93 | 93 | |
| 94 | ** Restart Nginx | |
| 95 | :PROPERTIES: | |
| 96 | :CUSTOM_ID: restart-nginx | |
| 97 | :END: | |
| 98 | 94 | Lastly, restart Nginx to enable all changes made. |
| 99 | 95 | |
| 100 | #+begin_src sh | |
| 96 | ```sh | |
| 101 | 97 | doas rc-service nginx restart |
| 102 | #+end_src | |
| 98 | ``` | |
| 99 | ||
| 100 | ## Testing Results | |
| 103 | 101 | |
| 104 | ** Testing Results | |
| 105 | :PROPERTIES: | |
| 106 | :CUSTOM_ID: testing-results | |
| 107 | :END: | |
| 108 | 102 | In order to test the results, let's curl the contents of our site. To |
| 109 | 103 | start, I'll curl the site normally: |
| 110 | 104 | |
| 111 | #+begin_src sh | |
| 105 | ```sh | |
| 112 | 106 | curl https://cleberg.net |
| 113 | #+end_src | |
| 107 | ``` | |
| 114 | 108 | |
| 115 | 109 | The HTML contents of the page come back successfully: |
| 116 | 110 | |
| 117 | #+begin_src html | |
| 111 | ```html | |
| 118 | 112 | <!doctype html>...</html> |
| 119 | #+end_src | |
| 113 | ``` | |
| 120 | 114 | |
| 121 | 115 | Next, let's include a banned referrer: |
| 122 | 116 | |
| 123 | #+begin_src sh | |
| 117 | ```sh | |
| 124 | 118 | curl --referer https://news.ycombinator.com https://cleberg.net |
| 125 | #+end_src | |
| 119 | ``` | |
| 126 | 120 | |
| 127 | This time, I'm met with a 403 Forbidden response page. That means we are | |
| 128 | successful and any clients being referred from a banned domain will be | |
| 129 | met with this same response code. | |
| 121 | This time, I'm met with a 403 Forbidden response page. That means we | |
| 122 | are successful and any clients being referred from a banned domain will | |
| 123 | be met with this same response code. | |
| 130 | 124 | |
| 131 | #+begin_src html | |
| 125 | ```html | |
| 132 | 126 | <html> |
| 133 | 127 | <head><title>403 Forbidden</title></head> |
| 134 | 128 | <body> |
| @@ -136,4 +130,4 @@ met with this same response code. | ||
| 136 | 130 | <hr><center>nginx</center> |
| 137 | 131 | </body> |
| 138 | 132 | </html> |
| 139 | #+end_src | |
| 133 | ``` | |
content/blog/2022-12-01-nginx-compression.md added +79
| @@ -0,0 +1,79 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-12-01 | |
| 3 | title = "Enable GZIP Compression in Nginx" | |
| 4 | description = "Learn how to enable compression in Nginx." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Text Compression | |
| 8 | ||
| 9 | Text compression allows a web server to serve text-based resources | |
| 10 | faster than uncompressed data. This can speed up things like First | |
| 11 | Contentful Paint, Tie to Interactive, and Speed Index. | |
| 12 | ||
| 13 | ## Enable Nginx Compression with gzip | |
| 14 | ||
| 15 | In order to enable text compression on Nginx, we need to enable it | |
| 16 | within the configuration file: | |
| 17 | ||
| 18 | ```sh | |
| 19 | nano /etc/nginx/nginx.conf | |
| 20 | ``` | |
| 21 | ||
| 22 | Within the `http` block, find the section that shows | |
| 23 | something like the block below. This is the default gzip configuration I | |
| 24 | found in my `nginx.conf` file on Alpine Linux 3.17. Yours may | |
| 25 | look slightly different, just make sure that you're not creating any | |
| 26 | duplicate gzip options. | |
| 27 | ||
| 28 | ```conf | |
| 29 | # Enable gzipping of responses. | |
| 30 | #gzip on; | |
| 31 | ||
| 32 | # Set the Vary HTTP header as defined in the RFC 2616. Default is 'off'. | |
| 33 | gzip_vary on; | |
| 34 | ``` | |
| 35 | ||
| 36 | Remove the default gzip lines and replace them with the following: | |
| 37 | ||
| 38 | ```conf | |
| 39 | # Enable gzipping of responses. | |
| 40 | gzip on; | |
| 41 | gzip_vary on; | |
| 42 | gzip_min_length 10240; | |
| 43 | gzip_proxied expired no-cache no-store private auth; | |
| 44 | gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; | |
| 45 | gzip_disable "MSIE [1-6]"; | |
| 46 | ``` | |
| 47 | ||
| 48 | ## Explanations of ngx~httpgzipmodule~ Options | |
| 49 | ||
| 50 | Each of the lines above enables a different aspect of the gzip response | |
| 51 | for Nginx. Here are the full explanations: | |
| 52 | ||
| 53 | - `gzip` -- Enables or disables gzipping of responses. | |
| 54 | - `gzip_vary` -- Enables or disables inserting the "Vary: | |
| 55 | Accept-Encoding" response header field if the directives gzip, | |
| 56 | gzip~static~, or gunzip are active. | |
| 57 | - `gzip_min_length` -- Sets the minimum length of a | |
| 58 | response that will be gzipped. The length is determined only from | |
| 59 | the "Content-Length" response header field. | |
| 60 | - `gzip_proxied` -- Enables or disables gzipping of | |
| 61 | responses for proxied requests depending on the request and | |
| 62 | response. The fact that the request is proxied is determined by the | |
| 63 | presence of the "Via" request header field. | |
| 64 | - `gzip_types` -- Enables gzipping of responses for the | |
| 65 | specified MIME types in addition to "text/html". The special value | |
| 66 | "*" matches any MIME type (0.8.29). Responses with the | |
| 67 | "text/html" type are always compressed. | |
| 68 | - `gzip_disable` -- Disables gzipping of responses for | |
| 69 | requests with "User-Agent" header fields matching any of the | |
| 70 | specified regular expressions. | |
| 71 | - The special mask "msie6" (0.7.12) corresponds to the regular | |
| 72 | expression "MSIE [4-6].", but works faster. Starting from | |
| 73 | version 0.8.11, "MSIE 6.0; ... SV1" is excluded from this | |
| 74 | mask. | |
| 75 | ||
| 76 | More information on these directives and their options can be found on | |
| 77 | the [Module | |
| 78 | ngx~httpgzipmodule~](https://nginx.org/en/docs/http/ngx_http_gzip_module.html) | |
| 79 | page in Nginx's documentation. | |
blog/2022-12-07-nginx-wildcard-redirect.org → content/blog/2022-12-07-nginx-wildcard-redirect.md renamed +36 −37
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: Redirect Nginx Subdomains & Trailing Content with Regex | |
| 2 | #+date: 2022-12-07 | |
| 1 | +++ | |
| 2 | date = 2022-12-07 | |
| 3 | title = "Redirect Nginx Subdomains & Trailing Content with Regex" | |
| 4 | description = "A simple Nginx configuration to redirect all subdomains and trailing content." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Problem | |
| 3 | 8 | |
| 4 | ** Problem | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: problem | |
| 7 | :END: | |
| 8 | 9 | I recently migrated domains and replaced the old webpage with a simple |
| 9 | 10 | info page with instructions to users on how to edit their bookmarks and |
| 10 | 11 | URLs to get to the page they were seeking. |
| @@ -12,15 +13,13 @@ URLs to get to the page they were seeking. | ||
| 12 | 13 | This was not ideal as it left the work up to the user and may have |
| 13 | 14 | caused friction for users who accessed my RSS feed. |
| 14 | 15 | |
| 15 | ** Solution | |
| 16 | :PROPERTIES: | |
| 17 | :CUSTOM_ID: solution | |
| 18 | :END: | |
| 16 | ## Solution | |
| 17 | ||
| 19 | 18 | Instead, I finally found a solution that allows me to redirect both |
| 20 | 19 | subdomains AND trailing content. For example, both of these URLs now |
| 21 | 20 | redirect properly using the logic I'll explain below: |
| 22 | 21 | |
| 23 | #+begin_src txt | |
| 22 | ```txt | |
| 24 | 23 | # Example 1 - Simple base domain redirect with trailing content |
| 25 | 24 | https://domain1.com/blog/alpine-linux/ -> https://domain2.com/blog/alpine-linux/ |
| 26 | 25 | |
| @@ -28,25 +27,23 @@ https://domain1.com/blog/alpine-linux/ -> https://domain2.com/blog/alpine-linux/ | ||
| 28 | 27 | https://libreddit.domain1.com/r/history/comments/7z8cbg/new_discovery_mode_turns_video_game_assassins/ |
| 29 | 28 | -> |
| 30 | 29 | https://libreddit.domain2.com/r/history/comments/7z8cbg/new_discovery_mode_turns_video_game_assassins/ |
| 31 | #+end_src | |
| 30 | ``` | |
| 32 | 31 | |
| 33 | 32 | Go ahead, try the URLs if you want to test them. |
| 34 | 33 | |
| 35 | *** Nginx Config | |
| 36 | :PROPERTIES: | |
| 37 | :CUSTOM_ID: nginx-config | |
| 38 | :END: | |
| 34 | ### Nginx Config | |
| 35 | ||
| 39 | 36 | To make this possible. I needed to configure a proper redirect scheme in |
| 40 | 37 | my Nginx configuration. |
| 41 | 38 | |
| 42 | #+begin_src sh | |
| 39 | ```sh | |
| 43 | 40 | doas nano /etc/nginx/http.d/domain1.conf |
| 44 | #+end_src | |
| 41 | ``` | |
| 45 | 42 | |
| 46 | 43 | Within this file, I had one block configured to redirect HTTP requests |
| 47 | 44 | to HTTPS for the base domain and all subdomains. |
| 48 | 45 | |
| 49 | #+begin_src conf | |
| 46 | ```conf | |
| 50 | 47 | server { |
| 51 | 48 | listen [::]:80; |
| 52 | 49 | listen 80; |
| @@ -62,14 +59,14 @@ server { | ||
| 62 | 59 | |
| 63 | 60 | return 404; |
| 64 | 61 | } |
| 65 | #+end_src | |
| 62 | ``` | |
| 66 | 63 | |
| 67 | For the base domain, I have another =server= block dedicated to | |
| 68 | redirecting all base domain requests. You can see that the =rewrite= | |
| 69 | line is instructing Nginx to gather all trailing content and append it | |
| 70 | to the new =domain2.com= URL. | |
| 64 | For the base domain, I have another `server` block dedicated | |
| 65 | to redirecting all base domain requests. You can see that the | |
| 66 | `rewrite` line is instructing Nginx to gather all trailing | |
| 67 | content and append it to the new `domain2.com` URL. | |
| 71 | 68 | |
| 72 | #+begin_src conf | |
| 69 | ```conf | |
| 73 | 70 | server { |
| 74 | 71 | listen [::]:443 ssl http2; |
| 75 | 72 | listen 443 ssl http2; |
| @@ -81,21 +78,23 @@ server { | ||
| 81 | 78 | ssl_certificate /etc/letsencrypt/live/domain1.com/fullchain.pem; |
| 82 | 79 | ssl_certificate_key /etc/letsencrypt/live/domain1.com/privkey.pem; |
| 83 | 80 | } |
| 84 | #+end_src | |
| 81 | ``` | |
| 85 | 82 | |
| 86 | 83 | Finally, the tricky part is figuring out how to tell Nginx to redirect |
| 87 | 84 | while keeping both a subdomain and trailing content intact. I found that |
| 88 | the easiest way to do this is to give it a =server= block of its own. | |
| 85 | the easiest way to do this is to give it a `server` block of | |
| 86 | its own. | |
| 89 | 87 | |
| 90 | Within this block, we need to do some regex on the =server_name= line | |
| 91 | before we can rewrite anything. This creates a variable called | |
| 92 | =subdomain=. | |
| 88 | Within this block, we need to do some regex on the | |
| 89 | `server_name` line before we can rewrite anything. This | |
| 90 | creates a variable called `subdomain`. | |
| 93 | 91 | |
| 94 | Once the server gets to the =rewrite= line, it pulls the =subdomain= | |
| 95 | variable from above and uses it on the new =domain2.com= domain before | |
| 96 | appending the trailing content (=$request_uri=). | |
| 92 | Once the server gets to the `rewrite` line, it pulls the | |
| 93 | `subdomain` variable from above and uses it on the new | |
| 94 | `domain2.com` domain before appending the trailing content | |
| 95 | (`$request_uri`). | |
| 97 | 96 | |
| 98 | #+begin_src conf | |
| 97 | ```conf | |
| 99 | 98 | server { |
| 100 | 99 | listen [::]:443 ssl http2; |
| 101 | 100 | listen 443 ssl http2; |
| @@ -107,14 +106,14 @@ server { | ||
| 107 | 106 | ssl_certificate /etc/letsencrypt/live/domain1.com/fullchain.pem; |
| 108 | 107 | ssl_certificate_key /etc/letsencrypt/live/domain1.com/privkey.pem; |
| 109 | 108 | } |
| 110 | #+end_src | |
| 109 | ``` | |
| 111 | 110 | |
| 112 | 111 | That's all there is to it. With this, I simply restarted Nginx and |
| 113 | 112 | watched the redirections work in-action. |
| 114 | 113 | |
| 115 | #+begin_src sh | |
| 114 | ```sh | |
| 116 | 115 | doas rc-service nginx restart |
| 117 | #+end_src | |
| 116 | ``` | |
| 118 | 117 | |
| 119 | 118 | Looking back on it, I wish I had done this sooner. Who knows how many |
| 120 | 119 | people went looking for my sites or bookmarks and gave up when they saw |
content/blog/2022-12-17-st.md added +94
| @@ -0,0 +1,94 @@ | ||
| 1 | +++ | |
| 2 | date = 2022-12-17 | |
| 3 | title = "Simple Terminal" | |
| 4 | description = "Instructions for building simple terminal on Fedora." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## st | |
| 8 | ||
| 9 | [st](https://st.suckless.org) standards for Simple Terminal, a simple | |
| 10 | terminal implementation for X made by the | |
| 11 | [suckless](https://suckless.org) team. | |
| 12 | ||
| 13 | This post walks through the dependencies needed and process to build and | |
| 14 | install `st` on Fedora Workstation. | |
| 15 | ||
| 16 | ### Obtain Files | |
| 17 | ||
| 18 | To start, obtain the source files for `st` via | |
| 19 | `git clone`. | |
| 20 | ||
| 21 | ```sh | |
| 22 | mkdir ~/suckless && cd ~/suckless | |
| 23 | git clone https://git.suckless.org/st && cd st | |
| 24 | ``` | |
| 25 | ||
| 26 | ### Dependencies | |
| 27 | ||
| 28 | Once you have the files and are in the `st` directory, ensure | |
| 29 | the following packages are installed. | |
| 30 | ||
| 31 | ```sh | |
| 32 | sudo dnf update && sudo dnf upgrade | |
| 33 | sudo dnf install gcc patch libX11-devel libXft-devel | |
| 34 | ``` | |
| 35 | ||
| 36 | ### Building | |
| 37 | ||
| 38 | Before building, ensure that you read the README file. | |
| 39 | ||
| 40 | ```sh | |
| 41 | cat README | |
| 42 | ``` | |
| 43 | ||
| 44 | Once you've read the instructions, open the `config.mk` file | |
| 45 | and ensure it matches your setup. If you're not sure, leave the default | |
| 46 | options within the file. | |
| 47 | ||
| 48 | Finally, you can build `st` with the following command. | |
| 49 | Ensure you run as root (e.g., `sudo`) or else you may not end | |
| 50 | up with a usable application file. | |
| 51 | ||
| 52 | ```sh | |
| 53 | sudo make clean install | |
| 54 | ``` | |
| 55 | ||
| 56 | ### Customization (Patches) | |
| 57 | ||
| 58 | Note that customizing `st` requires you to modify the source | |
| 59 | files or to download one of the [available | |
| 60 | patches](https://st.suckless.org/patches/) for suckless.org. | |
| 61 | ||
| 62 | If you've already installed `st` and want to customize or | |
| 63 | install a patch, start by uninstalling the current program. | |
| 64 | ||
| 65 | ```sh | |
| 66 | cd ~/suckless/st | |
| 67 | sudo make uninstall | |
| 68 | ``` | |
| 69 | ||
| 70 | Next, grab the `<path>.diff` file from the page of the patch | |
| 71 | you chose. For example, I will be using the | |
| 72 | [defaultfontsize](https://st.suckless.org/patches/defaultfontsize/) | |
| 73 | patch in the below example. | |
| 74 | ||
| 75 | ```sh | |
| 76 | wget https://st.suckless.org/patches/defaultfontsize/st-defaultfontsize-20210225-4ef0cbd.diff | |
| 77 | ``` | |
| 78 | ||
| 79 | Once the file is downloaded inside the `st` folder, apply the | |
| 80 | patch and re-install the program. You may need to install the | |
| 81 | `patch` command if you don't have it installed already (you | |
| 82 | should have installed it above). | |
| 83 | ||
| 84 | ```sh | |
| 85 | patch -i st-defaultfontsize-20210225-4ef0cbd.diff | |
| 86 | sudo make clean install | |
| 87 | ``` | |
| 88 | ||
| 89 | Once installed, you can use the default font size patch to launch | |
| 90 | `st` with any font size you wish: | |
| 91 | ||
| 92 | ```sh | |
| 93 | st -z 16 | |
| 94 | ``` | |
blog/2022-12-23-alpine-desktop.org → content/blog/2022-12-23-alpine-desktop.md renamed +91 −102
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: Alpine Linux as a Desktop OS | |
| 2 | #+date: 2022-12-23 | |
| 1 | +++ | |
| 2 | date = 2022-12-23 | |
| 3 | title = "Alpine Linux as a Desktop OS" | |
| 4 | description = "Learn how to set up Alpine Linux with Sway to use as a desktop operating system." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Isn't Alpine Linux for Servers? | |
| 3 | 8 | |
| 4 | ** Isn't Alpine Linux for Servers? | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: isnt-alpine-linux-for-servers | |
| 7 | :END: | |
| 8 | 9 | This is a question I see a lot when people are presented with an example |
| 9 | 10 | of Alpine Linux running as a desktop OS. |
| 10 | 11 | |
| @@ -14,82 +15,76 @@ functioning at a productive level for desktop users. | ||
| 14 | 15 | This post is documentation of how I installed and modified Alpine Linux |
| 15 | 16 | to become my daily desktop OS. |
| 16 | 17 | |
| 17 | ** Installation | |
| 18 | :PROPERTIES: | |
| 19 | :CUSTOM_ID: installation | |
| 20 | :END: | |
| 18 | ## Installation | |
| 19 | ||
| 21 | 20 | Note that I cover the installation of Alpine Linux in my other post, so |
| 22 | I won't repeat it here: [[/blog/alpine-linux/][Alpine Linux: My New | |
| 23 | Server OS]]. | |
| 21 | I won't repeat it here: [Alpine Linux: My New Server | |
| 22 | OS](/blog/alpine-linux/). | |
| 24 | 23 | |
| 25 | 24 | Basically, get a bootable USB or whatever you prefer with Alpine on it, |
| 26 | 25 | boot the ISO, and run the setup script. |
| 27 | 26 | |
| 28 | #+begin_src sh | |
| 27 | ```sh | |
| 29 | 28 | setup-alpine |
| 30 | #+end_src | |
| 29 | ``` | |
| 31 | 30 | |
| 32 | 31 | Once you have gone through all the options and installer finishes |
| 33 | 32 | without errors, reboot. |
| 34 | 33 | |
| 35 | #+begin_src sh | |
| 34 | ```sh | |
| 36 | 35 | reboot |
| 37 | #+end_src | |
| 36 | ``` | |
| 37 | ||
| 38 | ## Initial Setup | |
| 38 | 39 | |
| 39 | ** Initial Setup | |
| 40 | :PROPERTIES: | |
| 41 | :CUSTOM_ID: initial-setup | |
| 42 | :END: | |
| 43 | 40 | Once Alpine is installed and the machine has rebooted, login is as root |
| 44 | initially or =su= to root once you log in as your user. From here, you | |
| 45 | should start by updating and upgrading the system in case the ISO was | |
| 46 | not fully up-to-date. | |
| 41 | initially or `su` to root once you log in as your user. From | |
| 42 | here, you should start by updating and upgrading the system in case the | |
| 43 | ISO was not fully up-to-date. | |
| 47 | 44 | |
| 48 | #+begin_src sh | |
| 45 | ```sh | |
| 49 | 46 | # Update and upgrade system |
| 50 | 47 | apk -U update && apk -U upgrade |
| 51 | 48 | |
| 52 | 49 | # Add an editor so we can enable the community repository |
| 53 | 50 | apk add nano |
| 54 | #+end_src | |
| 51 | ``` | |
| 55 | 52 | |
| 56 | You need to uncomment the =community= repository for your version of | |
| 57 | Alpine Linux. | |
| 53 | You need to uncomment the `community` repository for your | |
| 54 | version of Alpine Linux. | |
| 58 | 55 | |
| 59 | For v3.17, the =repositories= file should look like this: | |
| 56 | For v3.17, the `repositories` file should look like this: | |
| 60 | 57 | |
| 61 | #+begin_src sh | |
| 58 | ```sh | |
| 62 | 59 | nano /etc/apk/repositories |
| 63 | #+end_src | |
| 60 | ``` | |
| 64 | 61 | |
| 65 | #+begin_src conf | |
| 62 | ```conf | |
| 66 | 63 | #/media/sda/apks |
| 67 | 64 | http://mirrors.gigenet.com/alpinelinux/v3.17/main |
| 68 | 65 | http://mirrors.gigenet.com/alpinelinux/v3.17/community |
| 69 | 66 | #http://mirrors.gigenet.com/alpinelinux/edge/main |
| 70 | 67 | #http://mirrors.gigenet.com/alpinelinux/edge/community |
| 71 | 68 | #http://mirrors.gigenet.com/alpinelinux/edge/testing |
| 72 | #+end_src | |
| 69 | ``` | |
| 73 | 70 | |
| 74 | #+begin_src sh | |
| 71 | ```sh | |
| 75 | 72 | # Add the rest of your packages |
| 76 | 73 | apk add linux-firmware iwd doas git curl wget |
| 77 | 74 | |
| 78 | 75 | # Add yourself to the wheel group so you can use the doas command |
| 79 | 76 | adduser $USER wheel |
| 80 | #+end_src | |
| 77 | ``` | |
| 81 | 78 | |
| 82 | ** Window Manager (Desktop) | |
| 83 | :PROPERTIES: | |
| 84 | :CUSTOM_ID: window-manager-desktop | |
| 85 | :END: | |
| 86 | The [[https://wiki.alpinelinux.org/wiki/Sway][Sway installation guide]] | |
| 79 | ## Window Manager (Desktop) | |
| 80 | ||
| 81 | The [Sway installation guide](https://wiki.alpinelinux.org/wiki/Sway) | |
| 87 | 82 | has everything you need to get Sway working on Alpine. |
| 88 | 83 | |
| 89 | 84 | However, I'll include a brief list of the commands I ran and their |
| 90 | 85 | purpose for posterity here. |
| 91 | 86 | |
| 92 | #+begin_src sh | |
| 87 | ```sh | |
| 93 | 88 | # Add eudev and set it up |
| 94 | 89 | apk add eudev |
| 95 | 90 | setup-devd udev |
| @@ -126,21 +121,22 @@ apk add \ # Install optional dependencies: | ||
| 126 | 121 | swaylock swaylockd \ # lockscreen tool |
| 127 | 122 | swaybg \ # wallpaper daemon |
| 128 | 123 | swayidle # idle management (DPMS) daemon |
| 129 | #+end_src | |
| 124 | ``` | |
| 130 | 125 | |
| 131 | 126 | Once you have the packages installed and set-up, you need to export the |
| 132 | =XDG_RUNTIME_DIR= upon login. To do this, edit your =.profile= file. | |
| 127 | `XDG_RUNTIME_DIR` upon login. To do this, edit your | |
| 128 | `.profile` file. | |
| 133 | 129 | |
| 134 | If you use another shell, such as =zsh=, you need to edit that shell's | |
| 135 | profile (e.g., =~/.zprofile=)! | |
| 130 | If you use another shell, such as `zsh`, you need to edit | |
| 131 | that shell's profile (e.g., `~/.zprofile`)! | |
| 136 | 132 | |
| 137 | #+begin_src sh | |
| 133 | ```sh | |
| 138 | 134 | nano ~/.profile |
| 139 | #+end_src | |
| 135 | ``` | |
| 140 | 136 | |
| 141 | 137 | Within the file, paste this: |
| 142 | 138 | |
| 143 | #+begin_src sh | |
| 139 | ```sh | |
| 144 | 140 | if test -z "${XDG_RUNTIME_DIR}"; then |
| 145 | 141 | export XDG_RUNTIME_DIR=/tmp/$(id -u)-runtime-dir |
| 146 | 142 | if ! test -d "${XDG_RUNTIME_DIR}"; then |
| @@ -148,22 +144,20 @@ if test -z "${XDG_RUNTIME_DIR}"; then | ||
| 148 | 144 | chmod 0700 "${XDG_RUNTIME_DIR}" |
| 149 | 145 | fi |
| 150 | 146 | fi |
| 151 | #+end_src | |
| 147 | ``` | |
| 152 | 148 | |
| 153 | 149 | Once that's complete, you can launch Sway manually. |
| 154 | 150 | |
| 155 | #+begin_src sh | |
| 151 | ```sh | |
| 156 | 152 | dbus-run-session -- sway |
| 157 | #+end_src | |
| 153 | ``` | |
| 154 | ||
| 155 | ## Personal Touches | |
| 158 | 156 | |
| 159 | ** Personal Touches | |
| 160 | :PROPERTIES: | |
| 161 | :CUSTOM_ID: personal-touches | |
| 162 | :END: | |
| 163 | 157 | I also added the following packages, per my personal preferences and |
| 164 | 158 | situation. |
| 165 | 159 | |
| 166 | #+begin_src sh | |
| 160 | ```sh | |
| 167 | 161 | doas apk add brightnessctl \ # Brightness controller |
| 168 | 162 | zsh \ # Shell |
| 169 | 163 | firefox \ # Browser |
| @@ -173,98 +167,93 @@ doas apk add brightnessctl \ # Brightness controller | ||
| 173 | 167 | neomutt \ # CLI email client |
| 174 | 168 | thunderbird \ # GUI email client |
| 175 | 169 | gnupg # GPG key manager |
| 176 | #+end_src | |
| 170 | ``` | |
| 177 | 171 | |
| 178 | 172 | From here, I use my Syncthing storage to pull all the configuration |
| 179 | files I stored from prior desktops, such as | |
| 180 | [[https://git.sr.ht/~cmc/dotfiles][my dotfiles]]. | |
| 181 | ||
| 182 | ** Resolving Issues | |
| 183 | :PROPERTIES: | |
| 184 | :CUSTOM_ID: resolving-issues | |
| 185 | :END: | |
| 186 | *** WiFi Issues | |
| 187 | :PROPERTIES: | |
| 188 | :CUSTOM_ID: wifi-issues | |
| 189 | :END: | |
| 190 | I initially tried to set up my Wi-Fi the standard way with =iwd=, but it | |
| 191 | didn't work. | |
| 192 | ||
| 193 | Here is what I initially tried (I did all of this as =root=): | |
| 194 | ||
| 195 | #+begin_src sh | |
| 173 | files I stored from prior desktops, such as [my | |
| 174 | dotfiles](https://git.sr.ht/~cmc/dotfiles). | |
| 175 | ||
| 176 | ## Resolving Issues | |
| 177 | ||
| 178 | ### WiFi Issues | |
| 179 | ||
| 180 | I initially tried to set up my Wi-Fi the standard way with | |
| 181 | `iwd`, but it didn't work. | |
| 182 | ||
| 183 | Here is what I initially tried (I did all of this as `root`): | |
| 184 | ||
| 185 | ```sh | |
| 196 | 186 | apk add iwd |
| 197 | 187 | rc-service iwd start |
| 198 | 188 | iwctl station wlan0 connect <SSID> # This will prompt for the password |
| 199 | 189 | rc-update add iwd boot && rc-update add dbus boot |
| 200 | #+end_src | |
| 190 | ``` | |
| 201 | 191 | |
| 202 | 192 | Then, I added the Wi-Fi entry to the bottom of the networking interface |
| 203 | 193 | file: |
| 204 | 194 | |
| 205 | #+begin_src sh | |
| 195 | ```sh | |
| 206 | 196 | nano /etc/network/interfaces |
| 207 | #+end_src | |
| 197 | ``` | |
| 208 | 198 | |
| 209 | #+begin_src conf | |
| 199 | ```conf | |
| 210 | 200 | auto wlan0 |
| 211 | 201 | iface wlan0 inet dhcp |
| 212 | #+end_src | |
| 202 | ``` | |
| 213 | 203 | |
| 214 | 204 | Finally, restart the networking service: |
| 215 | 205 | |
| 216 | #+begin_src sh | |
| 206 | ```sh | |
| 217 | 207 | rc-service networking restart |
| 218 | #+end_src | |
| 208 | ``` | |
| 219 | 209 | |
| 220 | 210 | My Wi-Fi interface would receive an IP address from the router, but it |
| 221 | 211 | could not ping anything in the network. To solve the Wi-Fi issues, I |
| 222 | originally upgraded to Alpine's =edge= repositories, which was | |
| 223 | unnecessary. | |
| 212 | originally upgraded to Alpine's `edge` repositories, which | |
| 213 | was unnecessary. | |
| 224 | 214 | |
| 225 | Really, the solution was to enable the =NameResolvingService=resolvconf= | |
| 226 | in =/etc/iwd/main.conf=. | |
| 215 | Really, the solution was to enable the | |
| 216 | `NameResolvingService=resolvconf` in | |
| 217 | `/etc/iwd/main.conf`. | |
| 227 | 218 | |
| 228 | #+begin_src sh | |
| 219 | ```sh | |
| 229 | 220 | doas nano /etc/iwd/main.conf |
| 230 | #+end_src | |
| 221 | ``` | |
| 231 | 222 | |
| 232 | #+begin_src conf | |
| 223 | ```conf | |
| 233 | 224 | [Network] |
| 234 | 225 | |
| 235 | 226 | NameResolvingService=resolvconf |
| 236 | #+end_src | |
| 227 | ``` | |
| 237 | 228 | |
| 238 | 229 | Once I finished this process, my Wi-Fi is working flawlessly. |
| 239 | 230 | |
| 240 | *** Sound Issues | |
| 241 | :PROPERTIES: | |
| 242 | :CUSTOM_ID: sound-issues | |
| 243 | :END: | |
| 231 | ### Sound Issues | |
| 232 | ||
| 244 | 233 | Same as with the Wi-Fi, I had no sound and could not control the |
| 245 | 234 | mute/unmute or volume buttons on my laptop. |
| 246 | 235 | |
| 247 | 236 | To resolve this, I installed |
| 248 | [[https://wiki.alpinelinux.org/wiki/PipeWire][pipewire]]. | |
| 237 | [pipewire](https://wiki.alpinelinux.org/wiki/PipeWire). | |
| 249 | 238 | |
| 250 | #+begin_src sh | |
| 239 | ```sh | |
| 251 | 240 | # Add your user to the following groups |
| 252 | 241 | addgroup $USER audio |
| 253 | 242 | addgroup $USER video |
| 254 | 243 | |
| 255 | 244 | # Install pipewire and other useful packages |
| 256 | 245 | apk add pipewire wireplumber pipewire-pulse pipewire-jack pipewire-alsa |
| 257 | #+end_src | |
| 246 | ``` | |
| 258 | 247 | |
| 259 | Finally, I needed to add =/usr/libexec/pipewire-launcher= to my | |
| 260 | =.config/sway/config= file so that Pipewire would run every time I | |
| 261 | launched sway. | |
| 248 | Finally, I needed to add `/usr/libexec/pipewire-launcher` to | |
| 249 | my `.config/sway/config` file so that Pipewire would run | |
| 250 | every time I launched sway. | |
| 262 | 251 | |
| 263 | #+begin_src sh | |
| 252 | ```sh | |
| 264 | 253 | nano ~/.config/sway/config |
| 265 | #+end_src | |
| 254 | ``` | |
| 266 | 255 | |
| 267 | #+begin_src conf | |
| 256 | ```conf | |
| 268 | 257 | # Run pipewire audio server |
| 269 | 258 | exec /usr/libexec/pipewire-launcher |
| 270 | 259 | |
| @@ -273,7 +262,7 @@ bindsym XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT | ||
| 273 | 262 | bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -5% |
| 274 | 263 | bindsym XF86AudioMute exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ toggle |
| 275 | 264 | bindsym XF86AudioMicMute exec --no-startup-id pactl set-source-mute @DEFAULT_SOURCE@ toggle |
| 276 | #+end_src | |
| 265 | ``` | |
| 277 | 266 | |
| 278 | 267 | Note that I do not use bluetooth or screen sharing, so I won't cover |
| 279 | 268 | those options in this post. |
blog/2023-01-03-recent-website-changes.org → content/blog/2023-01-03-recent-website-changes.md renamed +42 −50
| @@ -1,46 +1,43 @@ | ||
| 1 | #+title: Recent Website Changes | |
| 2 | #+date: 2023-01-03 | |
| 1 | +++ | |
| 2 | date = 2023-01-03 | |
| 3 | title = "Recent Website Changes" | |
| 4 | description = "A retrospective of recent changes to my website." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## The State of This Website | |
| 3 | 8 | |
| 4 | ** The State of This Website | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: the-state-of-this-website | |
| 7 | :END: | |
| 8 | 9 | Over the years, this website has changed URLs, styles, content focus, |
| 9 | 10 | and much more. However, it seems that I am never done, as I am yet again |
| 10 | 11 | changing this page to fit my current needs and wants. |
| 11 | 12 | |
| 12 | While this site was already minimal (~12kb), it contained a lot of | |
| 13 | While this site was already minimal (\~12kb), it contained a lot of | |
| 13 | 14 | disorganized content and some poorly chosen color schemes. |
| 14 | 15 | |
| 15 | 16 | The recent updates attempt to fix these items while focusing on what I |
| 16 | 17 | truly care about here: the content within each page. |
| 17 | 18 | |
| 18 | ** Recent Changes | |
| 19 | :PROPERTIES: | |
| 20 | :CUSTOM_ID: recent-changes | |
| 21 | :END: | |
| 19 | ## Recent Changes | |
| 20 | ||
| 22 | 21 | I've made quite a few changes to the style of this website today, both |
| 23 | 22 | in the CSS and in the HTML. |
| 24 | 23 | |
| 25 | 24 | Here are some of the key takeaways from today's changes: |
| 26 | 25 | |
| 27 | 1. Reduce the width of the website from =60em= to =40em=. | |
| 28 | 2. Remove breadcrumb navigation and replaced with a simple "Return Home" | |
| 29 | link when visiting anything other than the homepage. | |
| 30 | 3. Remove syntax highlighting from code blocks. CSS now loads from a | |
| 31 | single file. | |
| 32 | 4. Move blog posts on the homepage to the top, pushing tertiary content | |
| 33 | down. | |
| 34 | 5. Update font-family from =monospace= to =sans-serif= for readability. | |
| 35 | ||
| 36 | ** Future Focus | |
| 37 | :PROPERTIES: | |
| 38 | :CUSTOM_ID: future-focus | |
| 39 | :END: | |
| 40 | *** Accessibility | |
| 41 | :PROPERTIES: | |
| 42 | :CUSTOM_ID: accessibility | |
| 43 | :END: | |
| 26 | 1. Reduce the width of the website from `60em` to | |
| 27 | `40em`. | |
| 28 | 2. Remove breadcrumb navigation and replaced with a simple "Return | |
| 29 | Home" link when visiting anything other than the homepage. | |
| 30 | 3. Remove syntax highlighting from code blocks. CSS now loads from a | |
| 31 | single file. | |
| 32 | 4. Move blog posts on the homepage to the top, pushing tertiary content | |
| 33 | down. | |
| 34 | 5. Update font-family from `monospace` to | |
| 35 | `sans-serif` for readability. | |
| 36 | ||
| 37 | ## Future Focus | |
| 38 | ||
| 39 | ### Accessibility | |
| 40 | ||
| 44 | 41 | My main focus amidst all the changes this past year was accessibility. I |
| 45 | 42 | tried to improve the accessibility of all pages on this site as well as |
| 46 | 43 | I know how. For example, I added aria labels to identify different |
| @@ -49,10 +46,8 @@ navigation menus in November. | ||
| 49 | 46 | I want to continue this in 2023 and learn more about the accessibility |
| 50 | 47 | features I can utilize to help those who may need them. |
| 51 | 48 | |
| 52 | *** Content First | |
| 53 | :PROPERTIES: | |
| 54 | :CUSTOM_ID: content-first | |
| 55 | :END: | |
| 49 | ### Content First | |
| 50 | ||
| 56 | 51 | Beyond accessibility, I am making a concerted effort to bring the |
| 57 | 52 | content of this site to the forefront and push the tertiary information |
| 58 | 53 | (e.g., About Me, Services, etc.) down below the content. |
| @@ -61,33 +56,30 @@ Further, I want to review and edit previous blog posts for grammar, | ||
| 61 | 56 | accuracy, dead links, and more. Where necessary, I may even add a blurb |
| 62 | 57 | at the top of old posts that are no longer accurate or helpful. |
| 63 | 58 | |
| 64 | *** Style | |
| 65 | :PROPERTIES: | |
| 66 | :CUSTOM_ID: style | |
| 67 | :END: | |
| 59 | ### Style | |
| 60 | ||
| 68 | 61 | As always, I will be searching for ways to reduce distractions and bring |
| 69 | 62 | a minimalistic approach to this site. While there are certainly more |
| 70 | drastic measures I could take, such as making this a | |
| 71 | [[https://shinobi.bt.ht][Shinobi Website]] or a [[https://nocss.club][No | |
| 72 | CSS Website]], I prefer to keep some modern features here. | |
| 63 | drastic measures I could take, such as making this a [Shinobi | |
| 64 | Website](https://shinobi.bt.ht) or a [No CSS | |
| 65 | Website](https://nocss.club), I prefer to keep some modern features | |
| 66 | here. | |
| 73 | 67 | |
| 74 | 68 | Two ideas have been floating around in my head, but I am not sure how I |
| 75 | 69 | feel about these approaches yet: |
| 76 | 70 | |
| 77 | 1. Going all-in with Emacs org-mode and creating a script to | |
| 78 | auto-publish all my blog posts from org-mode to plain HTML files and | |
| 79 | then letting my CSS style it after the fact. | |
| 80 | 2. Focus on publishing for Gemini or Gopher and then utilize a | |
| 81 | conversion program to translate the pages to HTML. | |
| 71 | 1. Going all-in with Emacs org-mode and creating a script to | |
| 72 | auto-publish all my blog posts from org-mode to plain HTML files and | |
| 73 | then letting my CSS style it after the fact. | |
| 74 | 2. Focus on publishing for Gemini or Gopher and then utilize a | |
| 75 | conversion program to translate the pages to HTML. | |
| 76 | ||
| 77 | ## Looking Back | |
| 82 | 78 | |
| 83 | ** Looking Back | |
| 84 | :PROPERTIES: | |
| 85 | :CUSTOM_ID: looking-back | |
| 86 | :END: | |
| 87 | 79 | As I sit here and finalize the changes, as well as this post, I am happy |
| 88 | 80 | with the current state of this website. It's certainly a long way from |
| 89 | my first attempts (parallax background images, anyone?) and it's good to | |
| 90 | see the progress. | |
| 81 | my first attempts (parallax background images, anyone?) and it's good | |
| 82 | to see the progress. | |
| 91 | 83 | |
| 92 | 84 | I can only hope and work hard to ensure that I make more meaningful |
| 93 | 85 | progress by this time next year. |
blog/2023-01-05-mass-unlike-tumblr-posts.org → content/blog/2023-01-05-mass-unlike-tumblr-posts.md renamed +40 −46
| @@ -1,19 +1,18 @@ | ||
| 1 | #+title: How to Easily Mass Unlike Tumblr Posts With Javascript | |
| 2 | #+date: 2023-01-05 | |
| 1 | +++ | |
| 2 | date = 2023-01-05 | |
| 3 | title = "How to Easily Mass Unlike Tumblr Posts with Javascript" | |
| 4 | description = "Learn how to unlike Tumblr posts en masse in the browser." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## The Dilemma | |
| 3 | 8 | |
| 4 | ** The Dilemma | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: the-dilemma | |
| 7 | :END: | |
| 8 | 9 | The dilemma I had was pretty simple: I wanted to unlike all the posts I |
| 9 | 10 | have liked on Tumblr so that I could follow a new focus on blogs and |
| 10 | 11 | start fresh. Otherwise, Tumblr will keep recommending content based on |
| 11 | 12 | your previous likes. |
| 12 | 13 | |
| 13 | ** The Solution | |
| 14 | :PROPERTIES: | |
| 15 | :CUSTOM_ID: the-solution | |
| 16 | :END: | |
| 14 | ## The Solution | |
| 15 | ||
| 17 | 16 | I searched the web for a while and noted that most solutions referenced |
| 18 | 17 | Tumblr setting and dashboard pages that no longer exist. Additionally, I |
| 19 | 18 | did not want to install a third party extension to do this, as some |
| @@ -23,15 +22,13 @@ Luckily, I used Javascript for a while a few years ago and figured it | ||
| 23 | 22 | would be easy enough to script a solution, as long as Tumblr had a |
| 24 | 23 | system for the unlike buttons. |
| 25 | 24 | |
| 26 | *** Identifying Unlike Buttons | |
| 27 | :PROPERTIES: | |
| 28 | :CUSTOM_ID: identifying-unlike-buttons | |
| 29 | :END: | |
| 25 | ### Identifying Unlike Buttons | |
| 26 | ||
| 30 | 27 | Tumblr's unlike buttons are structured as you can see in the following |
| 31 | code block. All unlike buttons have an =aria-label= with a value of | |
| 32 | =Unlike=. | |
| 28 | code block. All unlike buttons have an `aria-label` with a | |
| 29 | value of `Unlike`. | |
| 33 | 30 | |
| 34 | #+begin_src html | |
| 31 | ```html | |
| 35 | 32 | <button class="TRX6J" aria-label="Unlike"> |
| 36 | 33 | <span class="EvhBA B1Z5w ztpfZ" tabindex="-1"> |
| 37 | 34 | <svg xmlns="http://www.w3.org/2000/svg" height="21" width="23" |
| @@ -40,36 +37,35 @@ code block. All unlike buttons have an =aria-label= with a value of | ||
| 40 | 37 | </svg> |
| 41 | 38 | </span> |
| 42 | 39 | </button> |
| 43 | #+end_src | |
| 40 | ``` | |
| 44 | 41 | |
| 45 | *** Running a Script to Unlike All Likes | |
| 46 | :PROPERTIES: | |
| 47 | :CUSTOM_ID: running-a-script-to-unlike-all-likes | |
| 48 | :END: | |
| 49 | To run this script, you will need to load the | |
| 50 | [[https://www.tumblr.com/likes][Likes | Tumblr]] page while logged in to | |
| 51 | your account. | |
| 42 | ### Running a Script to Unlike All Likes | |
| 43 | ||
| 44 | To run this script, you will need to load the [Likes \| | |
| 45 | Tumblr](https://www.tumblr.com/likes) page while logged in to your | |
| 46 | account. | |
| 52 | 47 | |
| 53 | 48 | Further, be sure to scroll down to the bottom and force Tumblr to load |
| 54 | 49 | more posts so that this script unlikes more posts at a time. |
| 55 | 50 | |
| 56 | 51 | Once you are logged in and the page is loaded, open the Developer Tools |
| 57 | and be sure you're on the "Console" tab. It should look something like | |
| 58 | this (this is in Firefox, Chromium should be similar): | |
| 52 | and be sure you're on the "Console" tab. It should look something | |
| 53 | like this (this is in Firefox, Chromium should be similar): | |
| 59 | 54 | |
| 60 | #+caption: Firefox Dev !Tools | |
| 61 | [[https:///img.cleberg.net/blog/20230105-mass-unlike-tumblr-posts/dev_console.png]] | |
| 55 |  | |
| 62 | 57 | |
| 63 | 58 | All you need to do is paste the following snippet into the dev console. |
| 64 | This code will collect all unlike buttons (=elements=) and then click | |
| 65 | each button to unlike it. | |
| 59 | This code will collect all unlike buttons (`elements`) and | |
| 60 | then click each button to unlike it. | |
| 66 | 61 | |
| 67 | Optionally, you can comment-out the line =elements[i].click();= and | |
| 68 | uncomment the =console.log()= lines to simply print out information | |
| 69 | without performing any actions. This can be useful to debug issues or | |
| 70 | confirm that the code below isn't doing anything you don't want it to. | |
| 62 | Optionally, you can comment-out the line | |
| 63 | `elements[i].click();` and uncomment the | |
| 64 | `console.log()` lines to simply print out information without | |
| 65 | performing any actions. This can be useful to debug issues or confirm | |
| 66 | that the code below isn't doing anything you don't want it to. | |
| 71 | 67 | |
| 72 | #+begin_src javascript | |
| 68 | ```javascript | |
| 73 | 69 | const elements = document.querySelectorAll('[aria-label="Unlike"]'); |
| 74 | 70 | // console.log(elements); // 👉 [button] |
| 75 | 71 | |
| @@ -77,22 +73,20 @@ for (let i=0; i < elements.length; i++) { | ||
| 77 | 73 | // console.log(elements[i]); |
| 78 | 74 | elements[i].click(); |
| 79 | 75 | } |
| 80 | #+end_src | |
| 76 | ``` | |
| 77 | ||
| 78 | ## Results | |
| 81 | 79 | |
| 82 | ** Results | |
| 83 | :PROPERTIES: | |
| 84 | :CUSTOM_ID: results | |
| 85 | :END: | |
| 86 | The results were quick for my situation, as it unliked ~200 posts within | |
| 87 | 2-3 seconds. I am not sure how this will perform on larger sets of likes | |
| 88 | (or if Tumblr has a limit to unliking posts). | |
| 80 | The results were quick for my situation, as it unliked \~200 posts | |
| 81 | within 2-3 seconds. I am not sure how this will perform on larger sets | |
| 82 | of likes (or if Tumblr has a limit to unliking posts). | |
| 89 | 83 | |
| 90 | 84 | You can see the below screenshot showing that I pasted the snippet into |
| 91 | 85 | the console, pressed Enter, and then the posts are automatically |
| 92 | 86 | unliked. |
| 93 | 87 | |
| 94 | #+caption: Script !Results | |
| 95 | [[https:///img.cleberg.net/blog/20230105-mass-unlike-tumblr-posts/script_results.png]] | |
| 88 |  | |
| 96 | 90 | |
| 97 | 91 | Thinking about this further, I would bet that this would be fairly |
| 98 | 92 | simple to package into a browser add-on so that users could install the |
blog/2023-01-08-fedora-remove-login-manager.org → content/blog/2023-01-08-fedora-remove-login-manager.md renamed +21 −24
| @@ -1,10 +1,11 @@ | ||
| 1 | #+title: Remove the Login Manager from Fedora i3 | |
| 2 | #+date: 2023-01-08 | |
| 1 | +++ | |
| 2 | date = 2023-01-08 | |
| 3 | title = "How to Remove the Login Manager from Fedora i3" | |
| 4 | description = "Learn how to completely remove the login manager from Fedora i3." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Fedora i3's Login Manager | |
| 3 | 8 | |
| 4 | ** Fedora i3's Login Manager | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: fedora-i3s-login-manager | |
| 7 | :END: | |
| 8 | 9 | Since I use the i3 spin of Fedora Workstation, I don't like to have a |
| 9 | 10 | login manager installed by default. As of the current version of Fedora |
| 10 | 11 | i3, the default login manager is LightDM. |
| @@ -13,35 +14,31 @@ If this is no longer the case, you can search for currently-installed | ||
| 13 | 14 | packages with the following command and see if you can identify a |
| 14 | 15 | different login manager. |
| 15 | 16 | |
| 16 | #+begin_src sh | |
| 17 | ```sh | |
| 17 | 18 | sudo dnf list installed |
| 18 | #+end_src | |
| 19 | ``` | |
| 20 | ||
| 21 | ## Removing the Login Manager | |
| 19 | 22 | |
| 20 | ** Removing the Login Manager | |
| 21 | :PROPERTIES: | |
| 22 | :CUSTOM_ID: removing-the-login-manager | |
| 23 | :END: | |
| 24 | 23 | In order to remove the login manager, simply uninstall the package. |
| 25 | 24 | |
| 26 | #+begin_src sh | |
| 25 | ```sh | |
| 27 | 26 | sudo dnf remove lightdm |
| 28 | #+end_src | |
| 27 | ``` | |
| 28 | ||
| 29 | ## Launching i3 Manually | |
| 29 | 30 | |
| 30 | ** Launching i3 Manually | |
| 31 | :PROPERTIES: | |
| 32 | :CUSTOM_ID: launching-i3-manually | |
| 33 | :END: | |
| 34 | 31 | In order to launch i3 manually, you need to set up your X session |
| 35 | properly. To start, create or edit the =~/.xinitrc= file to include the | |
| 36 | following at the bottom. | |
| 32 | properly. To start, create or edit the `~/.xinitrc` file to | |
| 33 | include the following at the bottom. | |
| 37 | 34 | |
| 38 | #+begin_src config | |
| 35 | ```config | |
| 39 | 36 | exec i3 |
| 40 | #+end_src | |
| 37 | ``` | |
| 41 | 38 | |
| 42 | 39 | Now, whenever you log in to the TTY, you can launch your desktop with |
| 43 | 40 | the following command. |
| 44 | 41 | |
| 45 | #+begin_src sh | |
| 42 | ```sh | |
| 46 | 43 | startx |
| 47 | #+end_src | |
| 44 | ``` | |
blog/2023-01-21-running-flatpak-apps-easily.org → content/blog/2023-01-21-running-flatpak-apps-easily.md renamed +25 −25
| @@ -1,50 +1,50 @@ | ||
| 1 | #+title: Running Flatpak Apps with Symlinks | |
| 2 | #+date: 2023-01-21 | |
| 1 | +++ | |
| 2 | date = 2023-01-21 | |
| 3 | title = "Running Flatpak Apps with Symlinks" | |
| 4 | description = "Learn how to run Flatpak apps through menu launchers with symlinks." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Running Flatpak Apps Should Be Faster | |
| 3 | 8 | |
| 4 | ** Running Flatpak Apps Should Be Faster | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: running-flatpak-apps-should-be-faster | |
| 7 | :END: | |
| 8 | 9 | If you're like me and use Flatpak for those pesky apps that cannot run |
| 9 | 10 | on your system for one reason or another, you likely get annoyed with |
| 10 | 11 | opening a terminal and manually running the Flatpak app with the lengthy |
| 11 | =flatpak run ...= command. | |
| 12 | `flatpak run ...` command. | |
| 12 | 13 | |
| 13 | In the past, I manually created aliases in my =.zshrc= file for certain | |
| 14 | apps. For example, an alias would look like the example below. | |
| 14 | In the past, I manually created aliases in my `.zshrc` file | |
| 15 | for certain apps. For example, an alias would look like the example | |
| 16 | below. | |
| 15 | 17 | |
| 16 | 18 | This would allow me to run the command fast within the terminal, but it |
| 17 | 19 | wouldn't allow me to run it in an application launcher. |
| 18 | 20 | |
| 19 | #+begin_src sh | |
| 21 | ```sh | |
| 20 | 22 | # ~/.zshrc |
| 21 | 23 | alias librewolf = "flatpak run io.gitlab.librewolf-community" |
| 22 | #+end_src | |
| 24 | ``` | |
| 23 | 25 | |
| 24 | 26 | However, I now use a much faster and better method that integrates with |
| 25 | the tiling WMs I use and their application launchers - =dmenu= and | |
| 26 | =bemenu=. | |
| 27 | the tiling WMs I use and their application launchers - | |
| 28 | `dmenu` and `bemenu`. | |
| 29 | ||
| 30 | ## Creating Symlinks for Flatpak Apps | |
| 27 | 31 | |
| 28 | ** Creating Symlinks for Flatpak Apps | |
| 29 | :PROPERTIES: | |
| 30 | :CUSTOM_ID: creating-symlinks-for-flatpak-apps | |
| 31 | :END: | |
| 32 | 32 | Let's use the example of Librewolf below. I can install the application |
| 33 | 33 | like so: |
| 34 | 34 | |
| 35 | #+begin_src sh | |
| 35 | ```sh | |
| 36 | 36 | flatpak install flathub io.gitlab.librewolf-community |
| 37 | #+end_src | |
| 37 | ``` | |
| 38 | 38 | |
| 39 | 39 | Once installed, I can create a symlink to link the flatpak app to my new |
| 40 | 40 | symlink in a location commonly included in your PATH. In this case, I |
| 41 | chose =/usr/bin=. You may need to choose a different location if | |
| 42 | =/usr/bin= isn't in your PATH. | |
| 41 | chose `/usr/bin`. You may need to choose a different location | |
| 42 | if `/usr/bin` isn't in your PATH. | |
| 43 | 43 | |
| 44 | #+begin_src sh | |
| 44 | ```sh | |
| 45 | 45 | ln -s /var/lib/flatpak/exports/bin/io.gitlab.librewolf-community /usr/bin/librewolf |
| 46 | #+end_src | |
| 46 | ``` | |
| 47 | 47 | |
| 48 | 48 | Once complete, you should be able to launch the app using the command |
| 49 | name you chose above in the symlink (=librewolf=) from a terminal or | |
| 50 | from your application launcher! | |
| 49 | name you chose above in the symlink (`librewolf`) from a | |
| 50 | terminal or from your application launcher! | |
blog/2023-01-23-random-mullvad-wireguard.org → content/blog/2023-01-23-random-mullvad-wireguard.md renamed +44 −47
| @@ -1,17 +1,17 @@ | ||
| 1 | #+title: Connecting to a Random Mullvad Wireguard Host | |
| 2 | #+date: 2023-01-23 | |
| 1 | +++ | |
| 2 | date = 2023-01-23 | |
| 3 | title = "Connecting to a Random Mullvad Wireguard Host on Boot" | |
| 4 | description = "Learn how to connect to a random Mullvad Wireguard host on boot." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Mullvad Wireguard | |
| 3 | 8 | |
| 4 | ** Mullvad Wireguard | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: mullvad-wireguard | |
| 7 | :END: | |
| 8 | 9 | If you're using an OS that does not support one of Mullvad's apps, |
| 9 | 10 | you're likely using the Wireguard configuration files instead. |
| 10 | 11 | |
| 11 | If not, the first step is to visit Mullvad's | |
| 12 | [[https://mullvad.net/en/account/#/wireguard-config][Wireguard | |
| 13 | configuration files]] page and download a ZIP of the configuration files | |
| 14 | you want to use. | |
| 12 | If not, the first step is to visit Mullvad's [Wireguard configuration | |
| 13 | files](https://mullvad.net/en/account/#/wireguard-config) page and | |
| 14 | download a ZIP of the configuration files you want to use. | |
| 15 | 15 | |
| 16 | 16 | Personally, I downloaded all configuration files across the world and |
| 17 | 17 | chose my connections using the script below. |
| @@ -19,31 +19,29 @@ chose my connections using the script below. | ||
| 19 | 19 | Once the files are downloaded, unzip them and move them to your |
| 20 | 20 | preferred location: |
| 21 | 21 | |
| 22 | #+begin_src sh | |
| 22 | ```sh | |
| 23 | 23 | cd Downloads |
| 24 | 24 | unzip mullvad_wireguard_linux_all_all.zip |
| 25 | 25 | mkdir ~/mullvad && mv ~/Downloads/*.conf ~/mullvad/ |
| 26 | #+end_src | |
| 26 | ``` | |
| 27 | ||
| 28 | ## Creating a Script to Connect to a Random Host | |
| 27 | 29 | |
| 28 | ** Creating a Script to Connect to a Random Host | |
| 29 | :PROPERTIES: | |
| 30 | :CUSTOM_ID: creating-a-script-to-connect-to-a-random-host | |
| 31 | :END: | |
| 32 | 30 | Once you have a folder of Wireguard configuration files from Mullvad, |
| 33 | 31 | you can create a script to randomly connect to any one of the locations. |
| 34 | 32 | |
| 35 | Start by creating a shell script - mine is called =vpn.sh=. | |
| 33 | Start by creating a shell script - mine is called `vpn.sh`. | |
| 36 | 34 | |
| 37 | #+begin_src sh | |
| 35 | ```sh | |
| 38 | 36 | nano ~/vpn.sh |
| 39 | #+end_src | |
| 37 | ``` | |
| 40 | 38 | |
| 41 | 39 | Within this script, you can paste the following info. Note that I |
| 42 | specify =us-*= in my script, which means that it will only consider | |
| 43 | US-based VPN locations. You can alter this or simply change it =*= to | |
| 44 | consider all locations. | |
| 40 | specify `us-*` in my script, which means that it will only | |
| 41 | consider US-based VPN locations. You can alter this or simply change it | |
| 42 | `*` to consider all locations. | |
| 45 | 43 | |
| 46 | #+begin_src sh | |
| 44 | ```sh | |
| 47 | 45 | #!/bin/sh |
| 48 | 46 | |
| 49 | 47 | ls /home/$USER/mullvad/us-* |sort -R |tail -n 1 |while read file; do |
| @@ -54,19 +52,19 @@ ls /home/$USER/mullvad/us-* |sort -R |tail -n 1 |while read file; do | ||
| 54 | 52 | printf "\n\nPrinting new IP info:\n" |
| 55 | 53 | curl https://am.i.mullvad.net/connected |
| 56 | 54 | done |
| 57 | #+end_src | |
| 55 | ``` | |
| 58 | 56 | |
| 59 | 57 | Once you've modified the script to your liking, add executable |
| 60 | 58 | permissions and run the script: |
| 61 | 59 | |
| 62 | #+begin_src sh | |
| 60 | ```sh | |
| 63 | 61 | chmod +x ~/vpn.sh |
| 64 | 62 | ~/vpn.sh |
| 65 | #+end_src | |
| 63 | ``` | |
| 66 | 64 | |
| 67 | 65 | The output should look like the following: |
| 68 | 66 | |
| 69 | #+begin_src txt | |
| 67 | ```txt | |
| 70 | 68 | doas (user@host) password: |
| 71 | 69 | |
| 72 | 70 | # ... The script will process all of the iptables and wg commands here |
| @@ -75,45 +73,44 @@ Created Mullvad wireguard connection with file: /home/user/mullvad/us-nyc-wg-210 | ||
| 75 | 73 | |
| 76 | 74 | Printing new IP info: |
| 77 | 75 | You are connected to Mullvad (server country-city-wg-num). Your IP address is 12.345.678.99 |
| 78 | #+end_src | |
| 76 | ``` | |
| 79 | 77 | |
| 80 | 78 | That's all there is to it. You can see your new location and IP via the |
| 81 | =printf= and =curl= commands included in the script. | |
| 79 | `printf` and `curl` commands included in the | |
| 80 | script. | |
| 81 | ||
| 82 | You can also go to the [Connection Check \| | |
| 83 | Mullvad](https://mullvad.net/en/check/) page to see if you are fully | |
| 84 | connected to Mullvad and if any leaks exist. | |
| 82 | 85 | |
| 83 | You can also go to the [[https://mullvad.net/en/check/][Connection Check | |
| 84 | | Mullvad]] page to see if you are fully connected to Mullvad and if any | |
| 85 | leaks exist. | |
| 86 |  | |
| 86 | 88 | |
| 87 | #+caption: Mullvad Connection Check | |
| 88 | [[https://img.cleberg.net/blog/20230123-random-mullvad-wireguard/mullvad_check.png]] | |
| 89 | ## Disconnecting from the Wireguard Connection | |
| 89 | 90 | |
| 90 | ** Disconnecting from the Wireguard Connection | |
| 91 | :PROPERTIES: | |
| 92 | :CUSTOM_ID: disconnecting-from-the-wireguard-connection | |
| 93 | :END: | |
| 94 | 91 | If you forget which connection you're using, you can execute the |
| 95 | 92 | following command to see where Wireguard is currently connected: |
| 96 | 93 | |
| 97 | #+begin_src sh | |
| 94 | ```sh | |
| 98 | 95 | wg show |
| 99 | #+end_src | |
| 96 | ``` | |
| 100 | 97 | |
| 101 | 98 | This command will show you the Wireguard interfaces and should output a |
| 102 | connection like so: =interface: us-lax-wg-104=. | |
| 99 | connection like so: `interface: us-lax-wg-104`. | |
| 103 | 100 | |
| 104 | 101 | Once you have this, just disconnect using that files' full path: |
| 105 | 102 | |
| 106 | #+begin_src sh | |
| 103 | ```sh | |
| 107 | 104 | wg-quick down /home/user/mullvad/us-lax-wg-104.conf |
| 108 | #+end_src | |
| 105 | ``` | |
| 109 | 106 | |
| 110 | 107 | I have a TODO item on figuring out how to easily export an environment |
| 111 | variable that contains the configuration file's full name, so that I can | |
| 112 | just execute the following: | |
| 108 | variable that contains the configuration file's full name, so that I | |
| 109 | can just execute the following: | |
| 113 | 110 | |
| 114 | #+begin_src sh | |
| 111 | ```sh | |
| 115 | 112 | # Ideal situation if I can export the $file variable to the environment |
| 116 | 113 | wg-quick down $file |
| 117 | #+end_src | |
| 114 | ``` | |
| 118 | 115 | |
| 119 | 116 | If you have an idea on how to do this, email me! |
blog/2023-01-28-self-hosting-wger-workout-manager.org → content/blog/2023-01-28-self-hosting-wger-workout-manager.md renamed +79 −96
| @@ -1,118 +1,106 @@ | ||
| 1 | #+title: Self-Hosting Wger Workout Manager | |
| 2 | #+date: 2023-01-28 | |
| 3 | ||
| 4 | ** Wger: The Self-Hosted Workout Manager | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: wger-the-self-hosted-workout-manager | |
| 7 | :END: | |
| 8 | [[https://wger.de][Wger Workout Manager]] is a fitness tracking tool for | |
| 1 | +++ | |
| 2 | date = 2023-01-28 | |
| 3 | title = "Self-Hosting Wger Workout Manager" | |
| 4 | description = "A guide to self-hosting the Wger application on your own server." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Wger: The Self-Hosted Workout Manager | |
| 8 | ||
| 9 | [Wger Workout Manager](https://wger.de) is a fitness tracking tool for | |
| 9 | 10 | those who enjoy self-hosting their data. You can also register an |
| 10 | 11 | account on their main website if you'd prefer to try without |
| 11 | 12 | self-hosting. |
| 12 | 13 | |
| 13 | *** Features | |
| 14 | :PROPERTIES: | |
| 15 | :CUSTOM_ID: features | |
| 16 | :END: | |
| 14 | ### Features | |
| 15 | ||
| 17 | 16 | I didn't see a full listing of features anywhere, so I compiled this |
| 18 | 17 | list of my own after installing wger: |
| 19 | 18 | |
| 20 | **** Dashboard | |
| 21 | :PROPERTIES: | |
| 22 | :CUSTOM_ID: dashboard | |
| 23 | :END: | |
| 24 | - Dashboard view of Workout Schedule, Nutrition Plan, Weight Graph, & | |
| 25 | last 5 Weight Logs | |
| 26 | ||
| 27 | **** Training | |
| 28 | :PROPERTIES: | |
| 29 | :CUSTOM_ID: training | |
| 30 | :END: | |
| 31 | - Workout Log | |
| 32 | - Workout Schedule | |
| 33 | - Calendar (shows weight logs and Bad/Neutral/Good days) | |
| 34 | - Gallery (shows images you upload) | |
| 35 | - Workout templates | |
| 36 | - Public templates | |
| 37 | - Exercises | |
| 38 | ||
| 39 | **** Nutrition | |
| 40 | :PROPERTIES: | |
| 41 | :CUSTOM_ID: nutrition | |
| 42 | :END: | |
| 43 | - Nutrition plans | |
| 44 | - BMI calculator | |
| 45 | - Daily calories calculator | |
| 46 | - Ingredient overview | |
| 47 | ||
| 48 | **** Body Weight | |
| 49 | :PROPERTIES: | |
| 50 | :CUSTOM_ID: body-weight | |
| 51 | :END: | |
| 52 | - Weight overview | |
| 53 | ||
| 54 | *** Documentation | |
| 55 | :PROPERTIES: | |
| 56 | :CUSTOM_ID: documentation | |
| 57 | :END: | |
| 19 | 1. Dashboard | |
| 20 | ||
| 21 | - Dashboard view of Workout Schedule, Nutrition Plan, Weight | |
| 22 | Graph, & last 5 Weight Logs | |
| 23 | ||
| 24 | 2. Training | |
| 25 | ||
| 26 | - Workout Log | |
| 27 | - Workout Schedule | |
| 28 | - Calendar (shows weight logs and Bad/Neutral/Good days) | |
| 29 | - Gallery (shows images you upload) | |
| 30 | - Workout templates | |
| 31 | - Public templates | |
| 32 | - Exercises | |
| 33 | ||
| 34 | 3. Nutrition | |
| 35 | ||
| 36 | - Nutrition plans | |
| 37 | - BMI calculator | |
| 38 | - Daily calories calculator | |
| 39 | - Ingredient overview | |
| 40 | ||
| 41 | 4. Body Weight | |
| 42 | ||
| 43 | - Weight overview | |
| 44 | ||
| 45 | ### Documentation | |
| 46 | ||
| 58 | 47 | In order to self-host wger, I opted to use the Docker version of the |
| 59 | 48 | application. You can read the README within the |
| 60 | [[https://github.com/wger-project/docker][wger-project/docker]] project | |
| 61 | on GitHub for information and installation instructions. | |
| 49 | [wger-project/docker](https://github.com/wger-project/docker) project on | |
| 50 | GitHub for information and installation instructions. | |
| 51 | ||
| 52 | ### Installation | |
| 62 | 53 | |
| 63 | *** Installation | |
| 64 | :PROPERTIES: | |
| 65 | :CUSTOM_ID: installation | |
| 66 | :END: | |
| 67 | 54 | To start the installation, I created a folder for wger and started |
| 68 | 55 | creating the three necessary files: |
| 69 | 56 | |
| 70 | #+begin_src sh | |
| 57 | ```sh | |
| 71 | 58 | mkdir ~/wger && mkdir ~/wger/config |
| 72 | 59 | touch ~/wger/docker-compose.yml && \ |
| 73 | 60 | touch ~/wger/config/prod.env && \ |
| 74 | 61 | touch ~/wger/config/nginx.conf |
| 75 | #+end_src | |
| 62 | ``` | |
| 76 | 63 | |
| 77 | 64 | Once you have the folders and files created, you will need to copy the |
| 78 | contents of the =docker-compose.yml=, =prod.env=, and =nginx.conf= from | |
| 79 | the GitHub link above. | |
| 65 | contents of the `docker-compose.yml`, `prod.env`, | |
| 66 | and `nginx.conf` from the GitHub link above. | |
| 80 | 67 | |
| 81 | 68 | A few notes to explain the changes I made to the default files: |
| 82 | 69 | |
| 83 | - I updated the =ALLOW_REGISTRAION= variable in =prod.env= to =False= | |
| 84 | after I created an account via my LAN connection, *before* I connected | |
| 85 | this app to a publicly-available domain. | |
| 86 | - I uncommented and updated =CSRF_TRUSTED_ORIGINS= to be equal to the | |
| 87 | public version of this app: =https://wger.example.com=. | |
| 88 | - I updated the port within =docker-compose.yml=, within the =nginx= | |
| 89 | block. The port I updated this to will be reflected in my nginx | |
| 90 | configuration file on the server (NOT the wger nginx.conf file). | |
| 91 | ||
| 92 | *** Deploy | |
| 93 | :PROPERTIES: | |
| 94 | :CUSTOM_ID: deploy | |
| 95 | :END: | |
| 70 | - I updated the `ALLOW_REGISTRAION` variable in | |
| 71 | `prod.env` to `False` after I created an | |
| 72 | account via my LAN connection, **before** I connected this app to a | |
| 73 | publicly-available domain. | |
| 74 | - I uncommented and updated `CSRF_TRUSTED_ORIGINS` to be | |
| 75 | equal to the public version of this app: | |
| 76 | `https://wger.example.com`. | |
| 77 | - I updated the port within `docker-compose.yml`, within | |
| 78 | the `nginx` block. The port I updated this to will be | |
| 79 | reflected in my nginx configuration file on the server (NOT the wger | |
| 80 | nginx.conf file). | |
| 81 | ||
| 82 | ### Deploy | |
| 83 | ||
| 96 | 84 | Once all files are created and modified to your needs, simply start the |
| 97 | 85 | container. |
| 98 | 86 | |
| 99 | #+begin_src sh | |
| 87 | ```sh | |
| 100 | 88 | docker-compose up -d |
| 101 | #+end_src | |
| 89 | ``` | |
| 102 | 90 | |
| 103 | 91 | You can now visit the website on your LAN by going to |
| 104 | =localhost:YOUR_PORT= or by the server's IP, if you're not on the same | |
| 105 | machine that is running the container. | |
| 92 | `localhost:YOUR_PORT` or by the server's IP, if you're not | |
| 93 | on the same machine that is running the container. | |
| 106 | 94 | |
| 107 | 95 | If you wish to connect this app to a public domain name, you'll need to |
| 108 | point an =A= DNS record from the domain to your server's public IP. | |
| 109 | You'll then need to create a configuration file for whichever web server | |
| 110 | or reverse proxy you're using. | |
| 96 | point an `A` DNS record from the domain to your server's | |
| 97 | public IP. You'll then need to create a configuration file for | |
| 98 | whichever web server or reverse proxy you're using. | |
| 111 | 99 | |
| 112 | 100 | Wger's README suggests the following reverse proxy configuration for |
| 113 | 101 | Nginx: |
| 114 | 102 | |
| 115 | #+begin_src conf | |
| 103 | ```conf | |
| 116 | 104 | upstream wger { |
| 117 | 105 | # This port should match the port in the `nginx` block of docker-compose.yml |
| 118 | 106 | # If the container is running on this same machine, replace this with |
| @@ -139,18 +127,16 @@ server { | ||
| 139 | 127 | ssl_certificate /path/to/https/certificate.crt; |
| 140 | 128 | ssl_certificate_key /path/to/https/certificate.key; |
| 141 | 129 | } |
| 142 | #+end_src | |
| 130 | ``` | |
| 143 | 131 | |
| 144 | ** Thoughts on Wger | |
| 145 | :PROPERTIES: | |
| 146 | :CUSTOM_ID: thoughts-on-wger | |
| 147 | :END: | |
| 148 | I'm still playing around with the app itself, but it seems to be a solid | |
| 149 | all-around workout manager, weight log, and food log. | |
| 132 | ## Thoughts on Wger | |
| 133 | ||
| 134 | I'm still playing around with the app itself, but it seems to be a | |
| 135 | solid all-around workout manager, weight log, and food log. | |
| 150 | 136 | |
| 151 | 137 | I like that the weight log graph is fluid and updates quickly. You can |
| 152 | also import or export data in CSV format if you'd like to move your data | |
| 153 | elsewhere. | |
| 138 | also import or export data in CSV format if you'd like to move your | |
| 139 | data elsewhere. | |
| 154 | 140 | |
| 155 | 141 | The workout manager is slightly odd, as it requires you to enter sets |
| 156 | 142 | and reps for each exercise when you enter it into the plan. Then, when |
| @@ -160,11 +146,8 @@ actually performed, in terms of reps and weight. | ||
| 160 | 146 | I haven't tried the food log yet and I likely will not, at least for a |
| 161 | 147 | while. I have no need for a food log or calorie tracker at the moment. |
| 162 | 148 | |
| 163 | *** Screenshot Example | |
| 164 | :PROPERTIES: | |
| 165 | :CUSTOM_ID: screenshot-example | |
| 166 | :END: | |
| 149 | ### Screenshot Example | |
| 150 | ||
| 167 | 151 | You can see an example of a dashboard with placeholder data here: |
| 168 | 152 | |
| 169 | #+caption: wger dashboard | |
| 170 | [[https://img.cleberg.net/blog/20230128-wger/wger.png]] | |
| 153 |  | |
content/blog/2023-02-02-exploring-hare.md added +171
| @@ -0,0 +1,171 @@ | ||
| 1 | +++ | |
| 2 | date = 2023-02-02 | |
| 3 | title = "Exploring the Hare Programming Language" | |
| 4 | description = "A retrospective on my first time using the Hare Programming Language." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## A Quick Note | |
| 8 | ||
| 9 | By no means am I a professional developer, so this post will be rather | |
| 10 | short. I won't be going into depth on the specification or anything | |
| 11 | that technical. | |
| 12 | ||
| 13 | Instead, I will simply be talking about how I (a relatively basic | |
| 14 | hobbyist programmer) have been playing with Hare and what intrigues me | |
| 15 | about the language. | |
| 16 | ||
| 17 | ## Hare | |
| 18 | ||
| 19 | The [Hare](https://harelang.org) programming language is a | |
| 20 | straightforward language that should look familiar if you've ever | |
| 21 | programmed with C, Rust, or other languages that aim to build software | |
| 22 | at the system-level. | |
| 23 | ||
| 24 | The Hare homepage states the following: | |
| 25 | ||
| 26 | > Hare is a systems programming language designed to be simple, stable, | |
| 27 | > and robust. Hare uses a static type system, manual memory management, | |
| 28 | > and minimal runtime. It is well-suited to writing operating systems, | |
| 29 | > system tools, compilers, networking software, and other low-level, | |
| 30 | > high performance tasks. | |
| 31 | ||
| 32 | I have found this all to be true while playing with it for the first | |
| 33 | time today. In the next few sections, I'm going to walk through my | |
| 34 | installation and first program. | |
| 35 | ||
| 36 | ### Installation | |
| 37 | ||
| 38 | I'm currently running Alpine Linux on my Thinkpad, so the installation | |
| 39 | was quite easy as there is a package for Hare in the `apk` | |
| 40 | repositories. | |
| 41 | ||
| 42 | ```sh | |
| 43 | doas apk add hare hare-doc | |
| 44 | ``` | |
| 45 | ||
| 46 | However, I was able to install Hare from scratch on Fedora Linux a short | |
| 47 | while ago, which was also very easy to do. If you need further | |
| 48 | instructions and Hare doesn't have a package on your system, take a | |
| 49 | look at the [Hare Installation](https://harelang.org/installation/) | |
| 50 | page. | |
| 51 | ||
| 52 | ### Creating a Test Project | |
| 53 | ||
| 54 | In order to play with the language, I created | |
| 55 | [hare-test](https://git.sr.ht/~cmc/hare-projects) and will be putting | |
| 56 | any of my Hare-related adventures in here. | |
| 57 | ||
| 58 | > **Update:** I also created a simple Hare program for creating a file | |
| 59 | > from user input: | |
| 60 | > [files.ha](https://git.sr.ht/~cmc/hare-projects/tree/main/item/files/files.ha) | |
| 61 | ||
| 62 | Luckily, Hare doesn't require any complex set-up tools or build | |
| 63 | environment. Once you have Hare installed, you simply need to create a | |
| 64 | file ending with `.ha` and you can run a Hare program. | |
| 65 | ||
| 66 | I created a file called `rgb.ha` in order to test out the | |
| 67 | random number generation and passing parameters between functions. | |
| 68 | ||
| 69 | ```sh | |
| 70 | nano rgb.ha | |
| 71 | ``` | |
| 72 | ||
| 73 | Within this file, I was able to easily import a few of the [standard | |
| 74 | library modules](https://harelang.org/tutorials/stdlib/): | |
| 75 | `fmt`, `math::random`, and `datetime`. | |
| 76 | ||
| 77 | With these modules, I created two functions: | |
| 78 | ||
| 79 | 1. `main`: This function calls the `generate_rgb` | |
| 80 | function and then prints out the returned values. | |
| 81 | 2. `generate_rgb`: This function uses the current Unix epoch | |
| 82 | time to generate a pseudo-random value and uses this value to create | |
| 83 | three more random values between 0 and 255. These three numbers | |
| 84 | represent a color in RGB format. | |
| 85 | ||
| 86 | > **Note**: Some syntax coloring may look odd, as Zola currently | |
| 87 | > doesn't have a syntax highlighting theme for Hare. Instead, I'm | |
| 88 | > using the C theme, which may not be exactly accurate when coloring the | |
| 89 | > code below. | |
| 90 | ||
| 91 | ```c | |
| 92 | use datetime; | |
| 93 | use fmt; | |
| 94 | use math::random; | |
| 95 | ||
| 96 | export fn main() void = { | |
| 97 | const rgb = generate_rgb(); | |
| 98 | fmt::printfln("RGB: ({}, {}, {})", rgb[0], rgb[1], rgb[2])!; | |
| 99 | }; | |
| 100 | ||
| 101 | fn generate_rgb() []u64 = { | |
| 102 | // Use the current Unix epoch time as the seed value | |
| 103 | let datetime = datetime::epochunix(&datetime::now()); | |
| 104 | ||
| 105 | // Generate initial pseudo-random value | |
| 106 | // You must cast the datetime from int to u64 | |
| 107 | let x = random::init(datetime: u64); | |
| 108 | ||
| 109 | // Generate RGB values between (0, 255) using pseudo-random init value | |
| 110 | let r = random::u64n(&x, 255); | |
| 111 | let g = random::u64n(&x, 255); | |
| 112 | let b = random::u64n(&x, 255); | |
| 113 | ||
| 114 | // Structure data as array and return | |
| 115 | let rgb_array: [3]u64 = [r, g, b]; | |
| 116 | return rgb_array; | |
| 117 | }; | |
| 118 | ``` | |
| 119 | ||
| 120 | ### Running a Program | |
| 121 | ||
| 122 | Once you have a Hare file written and ready to run, you simply need to | |
| 123 | run it: | |
| 124 | ||
| 125 | ```sh | |
| 126 | hare run file.ha | |
| 127 | ``` | |
| 128 | ||
| 129 | You can also compile the program into an executable: | |
| 130 | ||
| 131 | ```sh | |
| 132 | hare build -o example file.ha | |
| 133 | ./example | |
| 134 | ``` | |
| 135 | ||
| 136 | ### Initial Thoughts | |
| 137 | ||
| 138 | 1. Documentation Improvements Would Help | |
| 139 | ||
| 140 | While I was able to piece everything together eventually, the | |
| 141 | biggest downfall right now in Hare's documentation. For such a new | |
| 142 | project, the documentation is in a great spot. However, bare | |
| 143 | specifications don't help as much as a brief examples section | |
| 144 | would. | |
| 145 | ||
| 146 | For example, it took me a while to figure out what the | |
| 147 | `u64n` function was looking for. I could tell that it | |
| 148 | took two parameters and the second was my max value (255), but | |
| 149 | couldn't figure out what the first value should be. Eventually, I | |
| 150 | inspected the `random.ha` file in the [Hare source | |
| 151 | code](https://git.sr.ht/~sircmpwn/hare/tree/master/item/math/random/random.ha) | |
| 152 | and found the test suite that helped me discover that it needed an | |
| 153 | `init()` value in the form of `&var`. | |
| 154 | ||
| 155 | 2. More Basic Modules | |
| 156 | ||
| 157 | This is another point that comes from Hare being new and awaiting | |
| 158 | more contributions, but there are some basic functions that I would | |
| 159 | personally enjoy seeing in Hare, such as one to convert decimal | |
| 160 | (base 10) values to hexadecimal (base 16). | |
| 161 | ||
| 162 | If I'm feeling comfortable with my math, I may work on the list of | |
| 163 | functions I want and see if any can make it into the Hare source | |
| 164 | code. | |
| 165 | ||
| 166 | 3. Overall Thoughts | |
| 167 | ||
| 168 | Overall, I actually really enjoy Hare. It's not as tedious to get a | |
| 169 | project up and running as Rust, but it's also simpler and more | |
| 170 | user-friendly than learning C. I am going to continue playing with | |
| 171 | it and see if I can make anything of particular value. | |
blog/2023-05-22-burnout.org → content/blog/2023-05-22-burnout.md renamed +21 −25
| @@ -1,21 +1,19 @@ | ||
| 1 | #+title: Burnout | |
| 2 | #+date: 2023-05-22 | |
| 3 | ||
| 4 | ** RE: Burnout | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: re-burnout | |
| 7 | :END: | |
| 8 | I recently read | |
| 9 | [[https://drewdevault.com/2023/05/01/2023-05-01-Burnout.html][Drew | |
| 10 | DeVault's post on burnout]] around the same time I was pulling out of a | |
| 11 | burnout rut myself earlier this month. Finally, seeing the light at the | |
| 12 | end of my burnout tunnel made me want to write my first post back on | |
| 13 | this topic. | |
| 14 | ||
| 15 | ** Busy Seasons on Busy Seasons | |
| 16 | :PROPERTIES: | |
| 17 | :CUSTOM_ID: busy-seasons-on-busy-seasons | |
| 18 | :END: | |
| 1 | +++ | |
| 2 | date = 2023-05-22 | |
| 3 | title = "RE: Burnout" | |
| 4 | description = "A response to Drew DeVault's burnout post." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## RE: Burnout | |
| 8 | ||
| 9 | I recently read [Drew DeVault's post on | |
| 10 | burnout](https://drewdevault.com/2023/05/01/2023-05-01-Burnout.html) | |
| 11 | around the same time I was pulling out of a burnout rut myself earlier | |
| 12 | this month. Finally, seeing the light at the end of my burnout tunnel | |
| 13 | made me want to write my first post back on this topic. | |
| 14 | ||
| 15 | ## Busy Seasons on Busy Seasons | |
| 16 | ||
| 19 | 17 | My career deals with busy seasons, generally driven by client demand. |
| 20 | 18 | This last year, I dealt with a harsh busy season from Aug to Oct 2022 to |
| 21 | 19 | issue a few SOC reports for the period ending 2022-09-30. Immediately |
| @@ -30,14 +28,12 @@ I have a brief break and need to focus on the 2023-09-30 SOC reports | ||
| 30 | 28 | again. While auditing and consulting always involve a busy season, this |
| 31 | 29 | is the first time I've had one last 9+ months without a break. |
| 32 | 30 | |
| 33 | While it's been tough, I have a handful of breaks pre-planned throughout | |
| 34 | this next cycle and should be able to moderate the level of commitment | |
| 35 | required for each client. | |
| 31 | While it's been tough, I have a handful of breaks pre-planned | |
| 32 | throughout this next cycle and should be able to moderate the level of | |
| 33 | commitment required for each client. | |
| 34 | ||
| 35 | ## Refocusing | |
| 36 | 36 | |
| 37 | ** Refocusing | |
| 38 | :PROPERTIES: | |
| 39 | :CUSTOM_ID: refocusing | |
| 40 | :END: | |
| 41 | 37 | Outside of work, I finally have time to work on hobbies such as this |
| 42 | 38 | website, programming, athletics, games, etc. |
| 43 | 39 | |
blog/2023-06-08-goaccess-geoip.org → content/blog/2023-06-08-goaccess-geoip.md renamed +32 −37
| @@ -1,51 +1,48 @@ | ||
| 1 | #+title: Inspecting Nginx Logs with GoAccess and MaxMind GeoIP Data | |
| 2 | #+date: 2023-06-08 | |
| 3 | ||
| 4 | ** Overview | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: overview | |
| 7 | :END: | |
| 8 | [[https://goaccess.io/][GoAccess]] is an open source real-time web log | |
| 9 | analyzer and interactive viewer that runs in a terminal in *nix systems | |
| 1 | +++ | |
| 2 | date = 2023-06-08 | |
| 3 | title = "Inspecting Nginx Logs with GoAccess and MaxMind GeoIP Data" | |
| 4 | description = "Learn how to use GoAccess and MaxMind to evaluate visitors to your web server." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Overview | |
| 8 | ||
| 9 | [GoAccess](https://goaccess.io/) is an open source real-time web log | |
| 10 | analyzer and interactive viewer that runs in a terminal in \*nix systems | |
| 10 | 11 | or through your browser. |
| 11 | 12 | |
| 12 | ** Installation | |
| 13 | :PROPERTIES: | |
| 14 | :CUSTOM_ID: installation | |
| 15 | :END: | |
| 16 | To start, you'll need to install GoAccess for your OS. Here's an example | |
| 17 | for Debian-based distros: | |
| 13 | ## Installation | |
| 14 | ||
| 15 | To start, you'll need to install GoAccess for your OS. Here's an | |
| 16 | example for Debian-based distros: | |
| 18 | 17 | |
| 19 | #+begin_src sh | |
| 18 | ```sh | |
| 20 | 19 | sudo apt install goaccess |
| 21 | #+end_src | |
| 20 | ``` | |
| 22 | 21 | |
| 23 | 22 | Next, find any number of the MaxMind GeoIP database files on GitHub or |
| 24 | 23 | another file hosting website. We're going to use P3TERX's version in |
| 25 | 24 | this example: |
| 26 | 25 | |
| 27 | #+begin_src sh | |
| 26 | ```sh | |
| 28 | 27 | wget https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-City.mmdb |
| 29 | #+end_src | |
| 28 | ``` | |
| 30 | 29 | |
| 31 | 30 | Be sure to save this file in an easy to remember location! |
| 32 | 31 | |
| 33 | ** Usage | |
| 34 | :PROPERTIES: | |
| 35 | :CUSTOM_ID: usage | |
| 36 | :END: | |
| 32 | ## Usage | |
| 33 | ||
| 37 | 34 | In order to utilize the full capabilities of GoAccess and MMDB, start |
| 38 | 35 | with the command template below and customize as necessary. This will |
| 39 | 36 | export an HTML view of the GoAccess dashboard, showing all relevant |
| 40 | 37 | information related to that site's access log. You can also omit the |
| 41 | =-o output.html= parameter if you prefer to view the data within the CLI | |
| 42 | instead of creating an HTML file. | |
| 38 | `-o output.html` parameter if you prefer to view the data | |
| 39 | within the CLI instead of creating an HTML file. | |
| 43 | 40 | |
| 44 | 41 | With the addition of the GeoIP Database parameter, section |
| 45 | =16 - Geo Location= will be added with the various countries that are | |
| 46 | associated with the collected IP addresses. | |
| 42 | `16 - Geo Location` will be added with the various countries | |
| 43 | that are associated with the collected IP addresses. | |
| 47 | 44 | |
| 48 | #+begin_src sh | |
| 45 | ```sh | |
| 49 | 46 | zcat /var/log/nginx/example.access.log.*.gz | goaccess \ |
| 50 | 47 | --geoip-database=/home/user/GeoLite2-City.mmdb \ |
| 51 | 48 | --date-format=%d/%b/%Y \ |
| @@ -53,22 +50,20 @@ zcat /var/log/nginx/example.access.log.*.gz | goaccess \ | ||
| 53 | 50 | --log-format=COMBINED \ |
| 54 | 51 | -o output.html \ |
| 55 | 52 | /var/log/nginx/example.access.log - |
| 56 | #+end_src | |
| 53 | ``` | |
| 54 | ||
| 55 | ### Example Output | |
| 57 | 56 | |
| 58 | *** Example Output | |
| 59 | :PROPERTIES: | |
| 60 | :CUSTOM_ID: example-output | |
| 61 | :END: | |
| 62 | 57 | See below for an example of the HTML output: |
| 63 | 58 | |
| 64 | #+caption: GoAccess HTML | |
| 65 | [[https://img.cleberg.net/blog/20230608-goaccess/goaccess-dashboard.png]] | |
| 59 |  | |
| 66 | 61 | |
| 67 | 62 | You can also see the GeoIP card created by the integration of the |
| 68 | 63 | MaxMind database information. |
| 69 | 64 | |
| 70 | #+caption: GoAccess GeoIP | |
| 71 | [[https://img.cleberg.net/blog/20230608-goaccess/goaccess-geoip.png]] | |
| 65 |  | |
| 72 | 67 | |
| 73 | 68 | That's all there is to it! Informational data is provided in an |
| 74 | 69 | organized fashion with minimal effort. |
blog/2023-06-08-self-hosting-baikal-server.org → content/blog/2023-06-08-self-hosting-baikal-server.md renamed +58 −65
| @@ -1,34 +1,34 @@ | ||
| 1 | #+title: Self-Hosting Baïkal Server | |
| 2 | #+date: 2023-06-08 | |
| 3 | ||
| 4 | ** What is Baïkal? | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: what-is-baïkal | |
| 7 | :END: | |
| 8 | [[https://sabre.io/baikal/][Baïkal]] is a lightweight CalDAV + CardDAV | |
| 1 | +++ | |
| 2 | date = 2023-06-08 | |
| 3 | title = "Self-Hosting Baïkal Server (CalDAV & CardDAV)" | |
| 4 | description = "A guide to self-hosting the Baïkal application on your own server." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## What is Baïkal? | |
| 8 | ||
| 9 | [Baïkal](https://sabre.io/baikal/) is a lightweight CalDAV + CardDAV | |
| 9 | 10 | server that you can self-host on your own machine. While I have tried (& |
| 10 | 11 | failed) to get this CalDAV + CardDAV server running before, it was quite |
| 11 | 12 | easy this time. Not really sure what I did differently this time, but |
| 12 | 13 | I'm documenting my process here to ensure I don't forget. |
| 13 | 14 | |
| 14 | ** Installation | |
| 15 | :PROPERTIES: | |
| 16 | :CUSTOM_ID: installation | |
| 17 | :END: | |
| 18 | First, create a folder on your server and open a =docker-compose.yml= | |
| 19 | file for editing: | |
| 15 | ## Installation | |
| 20 | 16 | |
| 21 | #+begin_src sh | |
| 17 | First, create a folder on your server and open a | |
| 18 | `docker-compose.yml` file for editing: | |
| 19 | ||
| 20 | ```sh | |
| 22 | 21 | mkdir baikal && cd baikal |
| 23 | 22 | nano docker-compose.yml |
| 24 | #+end_src | |
| 23 | ``` | |
| 25 | 24 | |
| 26 | 25 | Within this file, you'll need to paste the information below. You can |
| 27 | customize the =ports= section to use any port on your server to pass | |
| 28 | through to port 80 in the container. You can also edit the =volumes= | |
| 29 | section to use docker volumes instead of local folders. | |
| 26 | customize the `ports` section to use any port on your server | |
| 27 | to pass through to port 80 in the container. You can also edit the | |
| 28 | `volumes` section to use docker volumes instead of local | |
| 29 | folders. | |
| 30 | 30 | |
| 31 | #+begin_src conf | |
| 31 | ```conf | |
| 32 | 32 | version: "2" |
| 33 | 33 | services: |
| 34 | 34 | baikal: |
| @@ -39,50 +39,45 @@ services: | ||
| 39 | 39 | volumes: |
| 40 | 40 | - ./config:/var/www/baikal/config |
| 41 | 41 | - ./data:/var/www/baikal/Specific |
| 42 | #+end_src | |
| 42 | ``` | |
| 43 | 43 | |
| 44 | 44 | Once finished with editing, save and close the file. Then, launch the |
| 45 | 45 | docker container: |
| 46 | 46 | |
| 47 | #+begin_src sh | |
| 47 | ```sh | |
| 48 | 48 | sudo docker-compose up -d |
| 49 | #+end_src | |
| 49 | ``` | |
| 50 | ||
| 51 | ## Intial Setup | |
| 50 | 52 | |
| 51 | ** Intial Setup | |
| 52 | :PROPERTIES: | |
| 53 | :CUSTOM_ID: intial-setup | |
| 54 | :END: | |
| 55 | 53 | As long as no issues came up when starting the container, you should be |
| 56 | able to visit the server's set-up page at =http://<server_ip>:<port>=. | |
| 57 | The application will ask you to create an administrator account and | |
| 58 | choose the database type for your storage. Personally, I opted to use | |
| 59 | SQLite. | |
| 54 | able to visit the server's set-up page at | |
| 55 | `http://<server_ip>:<port>`. The application will ask you to | |
| 56 | create an administrator account and choose the database type for your | |
| 57 | storage. Personally, I opted to use SQLite. | |
| 60 | 58 | |
| 61 | 59 | Make sure the administrator credentials are adequate to protect against |
| 62 | 60 | common attacks. |
| 63 | 61 | |
| 64 | ** Creating Users | |
| 65 | :PROPERTIES: | |
| 66 | :CUSTOM_ID: creating-users | |
| 67 | :END: | |
| 68 | Once you've set up the application, you will be greeted by the Dashboard | |
| 69 | page, which will show the version of the app, status of the | |
| 62 | ## Creating Users | |
| 63 | ||
| 64 | Once you've set up the application, you will be greeted by the | |
| 65 | Dashboard page, which will show the version of the app, status of the | |
| 70 | 66 | admin/CalDAV/CardDAV services, and the number of users, calendars, |
| 71 | 67 | events, address books, and contacts. |
| 72 | 68 | |
| 73 | #+caption: Baïkal Dashboard | |
| 74 | [[https://img.cleberg.net/blog/20230608-baikal/baikal-dashboard.png]] | |
| 69 |  | |
| 75 | 71 | |
| 76 | To create a new user, navigate to the =Users and resources= page. This | |
| 77 | process is as simple as entering a username, password, and email. | |
| 72 | To create a new user, navigate to the `Users and resources` | |
| 73 | page. This process is as simple as entering a username, password, and | |
| 74 | email. | |
| 78 | 75 | |
| 79 | 76 | Once a user has been created, you can create any number of calendars and |
| 80 | 77 | address books for user, as well as inspect their information. |
| 81 | 78 | |
| 82 | ** Setting Up a Public URL | |
| 83 | :PROPERTIES: | |
| 84 | :CUSTOM_ID: setting-up-a-public-url | |
| 85 | :END: | |
| 79 | ## Setting Up a Public URL | |
| 80 | ||
| 86 | 81 | Once your application is working locally, you can open access remotely |
| 87 | 82 | via a URL by using a reverse-proxy like Nginx. |
| 88 | 83 | |
| @@ -92,15 +87,15 @@ point a domain name to the server hosting Baïkal. | ||
| 92 | 87 | Start by navigating to your web server's configuration directory and |
| 93 | 88 | create a new file for this application. |
| 94 | 89 | |
| 95 | #+begin_src sh | |
| 90 | ```sh | |
| 96 | 91 | cd /etc/nginx/sites-available/ |
| 97 | 92 | nano dav |
| 98 | #+end_src | |
| 93 | ``` | |
| 99 | 94 | |
| 100 | 95 | Within this file, paste in the configuration from below and change |
| 101 | =dav.example.com= to match the URL you'll be using. | |
| 96 | `dav.example.com` to match the URL you'll be using. | |
| 102 | 97 | |
| 103 | #+begin_src conf | |
| 98 | ```conf | |
| 104 | 99 | server { |
| 105 | 100 | if ($host ~ ^[^.]+\.example\.com$) { |
| 106 | 101 | return 301 https://$host$request_uri; |
| @@ -130,37 +125,35 @@ server { | ||
| 130 | 125 | include /etc/letsencrypt/options-ssl-nginx.conf; |
| 131 | 126 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; |
| 132 | 127 | } |
| 133 | #+end_src | |
| 128 | ``` | |
| 134 | 129 | |
| 135 | 130 | For Nginx on Ubuntu, you'll need to symlink the configuration file to |
| 136 | the =sites-enabled= directory and then restart Nginx. | |
| 131 | the `sites-enabled` directory and then restart Nginx. | |
| 137 | 132 | |
| 138 | #+begin_src sh | |
| 133 | ```sh | |
| 139 | 134 | sudo ln -s /etc/nginx/sites-available/dav /etc/nginx/sites-enabled/dav |
| 140 | 135 | sudo systemctl restart nginx.service |
| 141 | #+end_src | |
| 136 | ``` | |
| 142 | 137 | |
| 143 | 138 | At this point, the Baïkal server should be available over the internet |
| 144 | 139 | at the URL configured above! |
| 145 | 140 | |
| 146 | ** Configuring Clients | |
| 147 | :PROPERTIES: | |
| 148 | :CUSTOM_ID: configuring-clients | |
| 149 | :END: | |
| 141 | ## Configuring Clients | |
| 142 | ||
| 150 | 143 | Lastly, configuring clients and applications is essential to making sure |
| 151 | 144 | the DAV server is being fully utilized. |
| 152 | 145 | |
| 153 | You can also use the =Users and resources= page to inspect a user's | |
| 154 | personal link to their calendars and address books by clicking the info | |
| 155 | (i) button. It will show a URI like | |
| 156 | =/dav.php/calendars/your-user/default/=. | |
| 146 | You can also use the `Users and resources` page to inspect a | |
| 147 | user's personal link to their calendars and address books by clicking | |
| 148 | the info (i) button. It will show a URI like | |
| 149 | `/dav.php/calendars/your-user/default/`. | |
| 157 | 150 | |
| 158 | 151 | However, I found that the following URL works for most applications: |
| 159 | =/dav.php/principals/your-user/=. | |
| 152 | `/dav.php/principals/your-user/`. | |
| 160 | 153 | |
| 161 | I used the =principals= URL above for Thunderbird (calendar, tasks, and | |
| 162 | contacts), as well as iOS (calendar, tasks, and contacts) and everything | |
| 163 | works flawlessly so far. | |
| 154 | I used the `principals` URL above for Thunderbird (calendar, | |
| 155 | tasks, and contacts), as well as iOS (calendar, tasks, and contacts) and | |
| 156 | everything works flawlessly so far. | |
| 164 | 157 | |
| 165 | 158 | Syncing is quick between the server and clients, and I haven't seen any |
| 166 | 159 | disruptions in the service or data integrity. |
content/blog/2023-06-18-unifi-ip-blocklist.md added +87
| @@ -0,0 +1,87 @@ | ||
| 1 | +++ | |
| 2 | date = 2023-06-18 | |
| 3 | title = "Block IP Addresses and Subnets with Unifi Network Firewall" | |
| 4 | description = "Learn how to use the Unifi Network Firewall to block IP addresses and subnets." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Identifying Abusive IPs | |
| 8 | ||
| 9 | If you're like me and use Unifi network equipment at the edge of the | |
| 10 | network you manage, you may know that Unifi is only somewhat decent at | |
| 11 | identifying and blocking IPs that represent abusive or threat actors. | |
| 12 | ||
| 13 | While Unifi has a [threat | |
| 14 | management](https://help.ui.com/hc/en-us/articles/360006893234-UniFi-Gateway-Threat-Management) | |
| 15 | tool inside their Network application, it can be lacking in | |
| 16 | functionality and identification. For example, I have my UDM Pro set to | |
| 17 | identify and block almost all categories of threats available within the | |
| 18 | Unifi settings. However, I regularly identify abusive actors on my web | |
| 19 | server via the server logs. | |
| 20 | ||
| 21 | In addition, I have identified IP addresses and subnets directly within | |
| 22 | Unifi's logs that the UDM did not block for whatever reason. | |
| 23 | ||
| 24 | This guide is meant to be another step in the process to manually block | |
| 25 | abusive IP addresses or subnets that you have identified but are not | |
| 26 | being automatically blocked yet. | |
| 27 | ||
| 28 | ## Create an IP Group Profile | |
| 29 | ||
| 30 | To start, login to the Unifi machine's web GUI and navigate to the | |
| 31 | Network app \> Settings \> Profiles. | |
| 32 | ||
| 33 | Within this page, choose the `IP Groups` tab and click | |
| 34 | `Create New`. | |
| 35 | ||
| 36 |  | |
| 38 | ||
| 39 | Each IP Group profile can be used as one of three options: | |
| 40 | ||
| 41 | 1. Port Group | |
| 42 | 2. IPv4 Address/Subnet | |
| 43 | 3. IPv6 Address/Subnet | |
| 44 | ||
| 45 | In this example, I'm creating an IPv4 Address/Subnet group and adding a | |
| 46 | few different IP addresses and a subnet. Once you've added all IP | |
| 47 | addresses and subnets, click the `Apply` button that should | |
| 48 | appear at the bottom. | |
| 49 | ||
| 50 |  | |
| 52 | ||
| 53 | At this point, the IPv4 Address/Subnet has been created but not yet | |
| 54 | used. | |
| 55 | ||
| 56 | ## Drop IP Group Profile via the Unifi Firewall | |
| 57 | ||
| 58 | To instruct the Unifi machine to block the profile we just created, we | |
| 59 | need to navigate to the Network app \> Settings \> Firewall & Security. | |
| 60 | ||
| 61 | Within this screen, find the Firewall Rules table and click | |
| 62 | `Create Entry`. This entry should contain the following | |
| 63 | settings: | |
| 64 | ||
| 65 | - Type: `Internet In` | |
| 66 | - Description: `<Your Custom Rule>` | |
| 67 | - Rule Applied: `Before Predefined Rules` | |
| 68 | - Action: `Drop` | |
| 69 | - Source Type: `Port/IP Group` | |
| 70 | - IPv4 Address Group: | |
| 71 | `<Name of the Group Profile You Created Above>` | |
| 72 | ||
| 73 | Customize the remaining configurations to your liking, and then save and | |
| 74 | enable the firewall rule. | |
| 75 | ||
| 76 |  | |
| 78 | ||
| 79 | Once enabled, the Unifi machine will be able to drop all incoming | |
| 80 | connections from the defined IP addresses and subnets within the created | |
| 81 | profile. | |
| 82 | ||
| 83 | > As a personal aside to this topic, I'm looking for a convenient way | |
| 84 | > to update the firewall rules or profiles remotely (within the LAN) | |
| 85 | > from the web server to accelerate this process. If you have an idea on | |
| 86 | > how to automatically update Unifi IP groups or firewall rules, let me | |
| 87 | > know! | |
content/blog/2023-06-20-audit-review-template.md added +80
| @@ -0,0 +1,80 @@ | ||
| 1 | +++ | |
| 2 | date = 2023-06-20 | |
| 3 | title = "Audit Testing Review Template" | |
| 4 | description = "A handy reference template for audit reviews." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Overview | |
| 8 | ||
| 9 | This post is a *very* brief overview on the basic process to review | |
| 10 | audit test results, focusing on work done as part of a financial | |
| 11 | statement audit (FSA) or service organization controls (SOC) report. | |
| 12 | ||
| 13 | While there are numerous different things to review and look for - all | |
| 14 | varying wildly depending on the report, client, and tester - this list | |
| 15 | serves as a solid base foundation for a reviewer. | |
| 16 | ||
| 17 | I have used this throughout my career as a starting point to my reviews, | |
| 18 | and it has worked wonders for creating a consistent and objective | |
| 19 | template to my reviews. The goal is to keep this base high-level enough | |
| 20 | to be used on a wide variety of engagements, while still ensuring that | |
| 21 | all key areas are covered. | |
| 22 | ||
| 23 | ## Review Template | |
| 24 | ||
| 25 | 1. [ ] Check all documents for spelling and grammar. | |
| 26 | 2. [ ] Ensure all acronyms are fully explained upon first use. | |
| 27 | 3. [ ] For all people referenced, use their full names and job titles | |
| 28 | upon first use. | |
| 29 | 4. [ ] All supporting documents must cross-reference to the lead sheet | |
| 30 | and vice-versa. | |
| 31 | 5. [ ] Verify that the control has been adequately tested: | |
| 32 | - [ ] **Test of Design**: Did the tester obtain information | |
| 33 | regarding how the control should perform normally and abnormally | |
| 34 | (e.g., emergency scenarios)? | |
| 35 | - [ ] **Test of Operating Effectiveness**: Did the tester inquire, | |
| 36 | observe, inspect, or re-perform sufficient evidence to support | |
| 37 | their conclusion over the control? Inquiry alone is not | |
| 38 | adequate! | |
| 39 | 6. [ ] For any information used in the control, whether by the control | |
| 40 | operator or by the tester, did the tester appropriately document the | |
| 41 | source (system or person), extraction method, parameters, and | |
| 42 | completeness and accuracy (C&A)? | |
| 43 | - [ ] For any reports, queries, etc. used in the extraction, did | |
| 44 | the tester include a copy and notate C&A considerations? | |
| 45 | 7. [ ] Did the tester document the specific criteria that the control | |
| 46 | is being tested against? | |
| 47 | 8. [ ] Did the tester notate in the supporting documents where each | |
| 48 | criterion was satisfied? | |
| 49 | 9. [ ] If testing specific policies or procedures, are the documents | |
| 50 | adequate? | |
| 51 | - [ ] e.g., a test to validate that a review of policy XYZ occurs | |
| 52 | periodically should also evaluate the sufficiency of the policy | |
| 53 | itself, if meant to cover the risk that such a policy does not | |
| 54 | exist and is not reviewed. | |
| 55 | 10. [ ] Does the test cover the appropriate period under review? | |
| 56 | - [ ] If the test is meant to cover only a portion of the audit | |
| 57 | period, do other controls exist to mitigate the risks that exist | |
| 58 | for the remainder of the period? | |
| 59 | 11. [ ] For any computer-aided audit tools (CAATs) or other automation | |
| 60 | techniques used in the test, is the use of such tools explained and | |
| 61 | appropriately documented? | |
| 62 | 12. [ ] If prior-period documentation exists, are there any missing | |
| 63 | pieces of evidence that would further enhance the quality of the | |
| 64 | test? | |
| 65 | 13. [ ] Was any information discovered during the walkthrough or inquiry | |
| 66 | phase that was not incorporated into the test? | |
| 67 | 14. [ ] Are there new rules or expectations from your company's | |
| 68 | internal guidance or your regulatory bodies that would affect the | |
| 69 | audit approach for this control? | |
| 70 | 15. [ ] Was an exception, finding, or deficiency identified as a result | |
| 71 | of this test? | |
| 72 | - [ ] Was the control deficient in design, operation, or both? | |
| 73 | - [ ] What was the root cause of the finding? | |
| 74 | - [ ] Does the finding indicate other findings or potential fraud? | |
| 75 | - [ ] What's the severity and scope of the finding? | |
| 76 | - [ ] Do other controls exist as a form of compensation against | |
| 77 | the finding's severity, and do they mitigate the risk within | |
| 78 | the control objective? | |
| 79 | - [ ] Does the finding exist at the end of the period, or was it | |
| 80 | resolved within the audit period? | |
blog/2023-06-23-byobu.org → content/blog/2023-06-23-byobu.md renamed +20 −24
| @@ -1,40 +1,36 @@ | ||
| 1 | #+title: Byobu | |
| 2 | #+date: 2023-06-23 | |
| 1 | +++ | |
| 2 | date = 2023-06-23 | |
| 3 | title = "Byobu" | |
| 4 | description = "Learning about the Byobu application for terminals." | |
| 5 | +++ | |
| 3 | 6 | |
| 4 | ** Byobu | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: byobu | |
| 7 | :END: | |
| 8 | [[https://www.byobu.org][byobu]] is a command-line tool that allows you | |
| 9 | to use numerous screens within a single terminal emulator instance. More | |
| 10 | specifically, it's a text based window manager, using either =screen= or | |
| 11 | =tmux=. | |
| 7 | ## Byobu | |
| 8 | ||
| 9 | [byobu](https://www.byobu.org) is a command-line tool that allows you to | |
| 10 | use numerous screens within a single terminal emulator instance. More | |
| 11 | specifically, it's a text based window manager, using either | |
| 12 | `screen` or `tmux`. | |
| 12 | 13 | |
| 13 | 14 | This post is mostly just a self-reference as I explore byobu, so I may |
| 14 | 15 | come back later and update this post with more content. |
| 15 | 16 | |
| 16 | *** Screenshot | |
| 17 | :PROPERTIES: | |
| 18 | :CUSTOM_ID: screenshot | |
| 19 | :END: | |
| 17 | ### Screenshot | |
| 18 | ||
| 20 | 19 | Take a look below at my current multi-window set-up in byobu while I |
| 21 | 20 | write this blog post: |
| 22 | 21 | |
| 23 | #+caption: byobu | |
| 24 | [[https://img.cleberg.net/blog/20230623-byobu/byobu.png]] | |
| 22 |  | |
| 23 | ||
| 24 | ## Keybindings | |
| 25 | 25 | |
| 26 | ** Keybindings | |
| 27 | :PROPERTIES: | |
| 28 | :CUSTOM_ID: keybindings | |
| 29 | :END: | |
| 30 | 26 | You can open the help menu with either of the following commands; they |
| 31 | 27 | will both open the same manpage: |
| 32 | 28 | |
| 33 | #+begin_src sh | |
| 29 | ```sh | |
| 34 | 30 | byobu --help |
| 35 | 31 | # or |
| 36 | 32 | man byobu |
| 37 | #+end_src | |
| 33 | ``` | |
| 38 | 34 | |
| 39 | 35 | While the manpage contains a ton of information about the functionality |
| 40 | 36 | of byobu (such as status notifications, sessions, and windows), the |
| @@ -42,7 +38,7 @@ first location to explore should be the keybindings section. | ||
| 42 | 38 | |
| 43 | 39 | The keybindings are configured as follows: |
| 44 | 40 | |
| 45 | #+begin_src txt | |
| 41 | ```txt | |
| 46 | 42 | byobu keybindings can be user defined in /usr/share/byobu/keybindings/ (or |
| 47 | 43 | within .screenrc if byobu-export was used). The common key bindings are: |
| 48 | 44 | |
| @@ -70,4 +66,4 @@ Ctrl-a R - Reload profile | ||
| 70 | 66 | Ctrl-a ! - Toggle key bindings on and off |
| 71 | 67 | Ctrl-a k - Kill the current window |
| 72 | 68 | Ctrl-a ~ - Save the current window's scrollback buffer |
| 73 | #+end_src | |
| 69 | ``` | |
blog/2023-06-23-self-hosting-convos-irc.org → content/blog/2023-06-23-self-hosting-convos-irc.md renamed +73 −74
| @@ -1,43 +1,43 @@ | ||
| 1 | #+title: Self-Hosting Convos: IRC Web Client | |
| 2 | #+date: 2023-06-23 | |
| 3 | ||
| 4 | ** Convos | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: convos | |
| 7 | :END: | |
| 8 | [[https://convos.chat/][Convos]] is an always-online web client for IRC. | |
| 1 | +++ | |
| 2 | date = 2023-06-23 | |
| 3 | title = "Self-Hosting Convos IRC Web Client" | |
| 4 | description = "A guide to self-hosting the Convos application on your own server." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Convos | |
| 8 | ||
| 9 | [Convos](https://convos.chat/) is an always-online web client for IRC. | |
| 9 | 10 | It has a few features that made it attractive to me as a self-hosted |
| 10 | 11 | option: |
| 11 | 12 | |
| 12 | - Extremely simple Docker Compose installation method. | |
| 13 | - Runs in the background and monitors chats even while you're not logged | |
| 14 | in. | |
| 15 | - Neatly organized sidebar for conversation and client settings. | |
| 16 | - Ability to connect to different hosts and create profiles for hosts. | |
| 17 | - By default, registration is closed to the public. You can enable | |
| 18 | public registration on the Settings page or generate invitation links | |
| 19 | on the Users page. | |
| 20 | - Customization of the client theme, organization name and URL, admin | |
| 21 | email, and video service. | |
| 22 | ||
| 23 | ** Docker Installation | |
| 24 | :PROPERTIES: | |
| 25 | :CUSTOM_ID: docker-installation | |
| 26 | :END: | |
| 27 | To install Convos, simply create a directory and a =docker-compose.yml= | |
| 28 | file: | |
| 29 | ||
| 30 | #+begin_src sh | |
| 13 | - Extremely simple Docker Compose installation method. | |
| 14 | - Runs in the background and monitors chats even while you're not | |
| 15 | logged in. | |
| 16 | - Neatly organized sidebar for conversation and client settings. | |
| 17 | - Ability to connect to different hosts and create profiles for hosts. | |
| 18 | - By default, registration is closed to the public. You can enable | |
| 19 | public registration on the Settings page or generate invitation | |
| 20 | links on the Users page. | |
| 21 | - Customization of the client theme, organization name and URL, admin | |
| 22 | email, and video service. | |
| 23 | ||
| 24 | ## Docker Installation | |
| 25 | ||
| 26 | To install Convos, simply create a directory and a | |
| 27 | `docker-compose.yml` file: | |
| 28 | ||
| 29 | ```sh | |
| 31 | 30 | mkdir ~/convos && cd ~/convos |
| 32 | 31 | nano docker-compose.yml |
| 33 | #+end_src | |
| 32 | ``` | |
| 34 | 33 | |
| 35 | With the =docker-compose.yml= file open, paste the configuration below | |
| 36 | into the file. You can customize the host port to be something unique, | |
| 37 | such as =21897:3000=. You can also change the =data= folder to be a | |
| 38 | docker volume instead, if you prefer. | |
| 34 | With the `docker-compose.yml` file open, paste the | |
| 35 | configuration below into the file. You can customize the host port to be | |
| 36 | something unique, such as `21897:3000`. You can also change | |
| 37 | the `data` folder to be a docker volume instead, if you | |
| 38 | prefer. | |
| 39 | 39 | |
| 40 | #+begin_src config | |
| 40 | ```config | |
| 41 | 41 | version: '3' |
| 42 | 42 | |
| 43 | 43 | services: |
| @@ -50,34 +50,33 @@ services: | ||
| 50 | 50 | environment: |
| 51 | 51 | - CONVOS_REVERSE_PROXY=1 |
| 52 | 52 | restart: always |
| 53 | #+end_src | |
| 53 | ``` | |
| 54 | 54 | |
| 55 | Save the =docker-compose.yml= file and bring the container up: | |
| 55 | Save the `docker-compose.yml` file and bring the container | |
| 56 | up: | |
| 56 | 57 | |
| 57 | #+begin_src sh | |
| 58 | ```sh | |
| 58 | 59 | sudo docker-compose up -d |
| 59 | #+end_src | |
| 60 | ``` | |
| 61 | ||
| 62 | At this point, Convos is available at `<server_ip>:3000` but | |
| 63 | not available to the public. | |
| 60 | 64 | |
| 61 | At this point, Convos is available at =<server_ip>:3000= but not | |
| 62 | available to the public. | |
| 65 | ## Nginx Reverse Proxy | |
| 63 | 66 | |
| 64 | ** Nginx Reverse Proxy | |
| 65 | :PROPERTIES: | |
| 66 | :CUSTOM_ID: nginx-reverse-proxy | |
| 67 | :END: | |
| 68 | 67 | If you're using Nginx, you can create a configuration file for the |
| 69 | 68 | convos application. Start by opening the file: |
| 70 | 69 | |
| 71 | #+begin_src sh | |
| 70 | ```sh | |
| 72 | 71 | nano /etc/nginx/sites-available/convos |
| 73 | #+end_src | |
| 72 | ``` | |
| 74 | 73 | |
| 75 | 74 | Within the nginx configuration file, paste the following content and be |
| 76 | sure to update =convos.example.com= to match your domain and | |
| 77 | =127.0.0.1:3000= to match the port you opened in the | |
| 78 | =docker-compose.yml= file. | |
| 75 | sure to update `convos.example.com` to match your domain and | |
| 76 | `127.0.0.1:3000` to match the port you opened in the | |
| 77 | `docker-compose.yml` file. | |
| 79 | 78 | |
| 80 | #+begin_src config | |
| 79 | ```config | |
| 81 | 80 | # Host and port where convos is running |
| 82 | 81 | upstream convos_upstream { server 127.0.0.1:3000; } |
| 83 | 82 | |
| @@ -115,59 +114,59 @@ server { | ||
| 115 | 114 | include /etc/letsencrypt/options-ssl-nginx.conf; |
| 116 | 115 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; |
| 117 | 116 | } |
| 118 | #+end_src | |
| 117 | ``` | |
| 119 | 118 | |
| 120 | Once the file is saved, link it to the =sites-enabled= directory and | |
| 121 | restart Nginx. | |
| 119 | Once the file is saved, link it to the `sites-enabled` | |
| 120 | directory and restart Nginx. | |
| 122 | 121 | |
| 123 | #+begin_src sh | |
| 122 | ```sh | |
| 124 | 123 | sudo ln -s /etc/nginx/sites-available/convos /etc/nginx/sites-enabled/convos |
| 125 | 124 | sudo systemctl restart nginx.service |
| 126 | #+end_src | |
| 125 | ``` | |
| 126 | ||
| 127 | ## Screenshots | |
| 127 | 128 | |
| 128 | ** Screenshots | |
| 129 | :PROPERTIES: | |
| 130 | :CUSTOM_ID: screenshots | |
| 131 | :END: | |
| 132 | 129 | See the screenshots below for an example of the Convos homepage and an |
| 133 | 130 | excerpt of the chat screen. |
| 134 | 131 | |
| 135 | 132 | There are numerous themes to choose from; the theme shown in the images |
| 136 | 133 | below is Dracula. |
| 137 | 134 | |
| 135 | ```{=org} | |
| 138 | 136 | #+caption: Convos Home |
| 139 | [[https://img.cleberg.net/blog/20230623-convos/convos_home.png%20%22Convos%20Home%22]] | |
| 137 | ``` | |
| 138 | <https://img.cleberg.net/blog/20230623-convos/convos_home.png%20%22Convos%20Home%22> | |
| 140 | 139 | |
| 140 | ```{=org} | |
| 141 | 141 | #+caption: Convos Chat |
| 142 | [[https://img.cleberg.net/blog/20230623-convos/convos_chat.png%20%22Convos%20Chat%22]] | |
| 142 | ``` | |
| 143 | <https://img.cleberg.net/blog/20230623-convos/convos_chat.png%20%22Convos%20Chat%22> | |
| 144 | ||
| 145 | ## Registering a Nickname | |
| 143 | 146 | |
| 144 | ** Registering a Nickname | |
| 145 | :PROPERTIES: | |
| 146 | :CUSTOM_ID: registering-a-nickname | |
| 147 | :END: | |
| 148 | 147 | Hop into the server channel so that you can message NickServ. In the |
| 149 | 148 | case of Convos, the default sever is libera.chat. Simply click the |
| 150 | =libera= conversation at the top of the sidebar to open it. Once the | |
| 151 | chat is open, you can claim a nickname by typing: | |
| 149 | `libera` conversation at the top of the sidebar to open it. | |
| 150 | Once the chat is open, you can claim a nickname by typing: | |
| 152 | 151 | |
| 153 | #+begin_src txt | |
| 152 | ```txt | |
| 154 | 153 | /nick <nick> |
| 155 | #+end_src | |
| 154 | ``` | |
| 156 | 155 | |
| 157 | If the nickname is available, and you'd like to register the nickname to | |
| 158 | yourself, you'll need to type another command: | |
| 156 | If the nickname is available, and you'd like to register the nickname | |
| 157 | to yourself, you'll need to type another command: | |
| 159 | 158 | |
| 160 | #+begin_src txt | |
| 159 | ```txt | |
| 161 | 160 | /msg NickServ REGISTER |
| 162 | 161 | <password> <email> |
| 163 | #+end_src | |
| 162 | ``` | |
| 164 | 163 | |
| 165 | 164 | On libera.chat, the server will send a confirmation email with a command |
| 166 | 165 | that you must message in IRC to verify registration of the nickname: |
| 167 | 166 | |
| 168 | #+begin_src txt | |
| 167 | ```txt | |
| 169 | 168 | /msg NickServ VERIFY REGISTER <nick> <verification_code> |
| 170 | #+end_src | |
| 169 | ``` | |
| 171 | 170 | |
| 172 | 171 | Once entered, the server should confirm registration of the nickname to |
| 173 | 172 | the supplied email with the password specified. |
content/blog/2023-06-28-backblaze-b2.md added +188
| @@ -0,0 +1,188 @@ | ||
| 1 | +++ | |
| 2 | date = 2023-06-28 | |
| 3 | title = "Getting Started with Backblaze B2 Cloud Storage" | |
| 4 | description = "An introduction to the free ttier of Backblaze B2 Cloud Storage." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Overview | |
| 8 | ||
| 9 | Backblaze [B2 Cloud | |
| 10 | Storage](https://www.backblaze.com/b2/cloud-storage.html) is an | |
| 11 | inexpensive and reliable on-demand cloud storage and backup solution. | |
| 12 | ||
| 13 | The service starts at \$5/TB/month (\$0.005/GB/month) with a download | |
| 14 | rate of \$0.01/GB/month. | |
| 15 | ||
| 16 | However, there are free tiers: | |
| 17 | ||
| 18 | - The first 10 GB of storage is free. | |
| 19 | - The first 1 GB of data downloaded each day is free. | |
| 20 | - Class A transactions are free. | |
| 21 | - The first 2500 Class B transactions each day are free. | |
| 22 | - The first 2500 Class C transactions each day are free. | |
| 23 | ||
| 24 | You can see which API calls fall into categories A, B, or C here: | |
| 25 | [Pricing Organized by API | |
| 26 | Calls](https://www.backblaze.com/b2/b2-transactions-price.html). | |
| 27 | ||
| 28 | For someone like me, who wants an offsite backup of their server's | |
| 29 | `/home/` directory and various other server configs that fall | |
| 30 | under 10 GB total, Backblaze is a great solution from a financial | |
| 31 | perspective. | |
| 32 | ||
| 33 | ## Create An Account | |
| 34 | ||
| 35 | To start with Backblaze, you'll need to [create a free | |
| 36 | account](https://www.backblaze.com/b2/sign-up.html) - no payment method | |
| 37 | is required to sign up. | |
| 38 | ||
| 39 | Once you have an account, you can test out the service with their web | |
| 40 | GUI, their mobile app, or their CLI tool. I'm going to use the CLI tool | |
| 41 | below to test a file upload and then sync an entire directory to my | |
| 42 | Backblaze bucket. | |
| 43 | ||
| 44 | ## Create a Bucket | |
| 45 | ||
| 46 | Before you can start uploading, you need to create a bucket. If you're | |
| 47 | familiar with other object storage services, this will feel familiar. If | |
| 48 | not, it's pretty simple to create one. | |
| 49 | ||
| 50 | As their webpage says: | |
| 51 | ||
| 52 | > A bucket is a container that holds files that are uploaded into B2 | |
| 53 | > Cloud Storage. The bucket name must be globally unique and must have a | |
| 54 | > minimum of 6 characters. A limit of 100 buckets may be created per | |
| 55 | > account. An unlimited number of files may be uploaded into a bucket. | |
| 56 | ||
| 57 | Once you click the `Create a Bucket` button on their webpage | |
| 58 | or mobile app, you need to provide the following: | |
| 59 | ||
| 60 | - Bucket Unique Name | |
| 61 | - Files in Bucket are: `Private` or `Public` | |
| 62 | - Default Encryption: `Disable` or `Enable` | |
| 63 | - Object Lock: `Disable` or `Enable` | |
| 64 | ||
| 65 | For my bucket, I created a private bucket with encryption enabled and | |
| 66 | object lock disabled. | |
| 67 | ||
| 68 | Once your bucket is created, you can test the upload/download feature on | |
| 69 | their web GUI or mobile app! At this point, you have a fully functional | |
| 70 | bucket and account. | |
| 71 | ||
| 72 | ## Linux CLI Tool | |
| 73 | ||
| 74 | ### Installation | |
| 75 | ||
| 76 | To install the `b2` CLI tool, you'll need to download it | |
| 77 | from the [CLI | |
| 78 | Tools](https://www.backblaze.com/docs/cloud-storage-command-line-tools) | |
| 79 | page. I recommend copying the URL from the link that says | |
| 80 | `Linux` and using wget to download it, as shown below. | |
| 81 | ||
| 82 | Once downloaded, make the file executable and move it to a location on | |
| 83 | your `$PATH`, so that you can execute that command from | |
| 84 | anywhere on the machine. | |
| 85 | ||
| 86 | ```sh | |
| 87 | wget <b2_cli_url> | |
| 88 | chmod +x b2_linux | |
| 89 | mv b2_linux /usr/bin/b2 | |
| 90 | ``` | |
| 91 | ||
| 92 | ### Log In | |
| 93 | ||
| 94 | The first step after installation is to log in. To do this, execute the | |
| 95 | following command and provide your `<applicationKeyId>` and | |
| 96 | `<applicationKey>`. | |
| 97 | ||
| 98 | If you don't want to provide these values in the command itself, you | |
| 99 | can simply execute the base command and it will request them in an | |
| 100 | interactive prompt. | |
| 101 | ||
| 102 | ```sh | |
| 103 | # if you want to provide the keys directly: | |
| 104 | b2 authorize-account [<applicationKeyId>] [<applicationKey>] | |
| 105 | ||
| 106 | # or, if you don't want your keys in your shell history: | |
| 107 | b2 authorize-account | |
| 108 | ``` | |
| 109 | ||
| 110 | ### Upload a Test File | |
| 111 | ||
| 112 | In order to test the functionality of the CLI tool, I'll start by | |
| 113 | uploading a single test file to the bucket I created above. We can do | |
| 114 | this with the `upload_file` function. | |
| 115 | ||
| 116 | The command is issued as follows: | |
| 117 | ||
| 118 | ```sh | |
| 119 | b2 upload_file <bucket_name> <local_file> <remote_file> | |
| 120 | ``` | |
| 121 | ||
| 122 | In my situation, I executed the following command with my username. | |
| 123 | ||
| 124 | ```sh | |
| 125 | b2 upload_file my_unique_bucket /home/<user>/test.md test.md | |
| 126 | ``` | |
| 127 | ||
| 128 | To confirm that the file was uploaded successfully, list the files in | |
| 129 | your bucket: | |
| 130 | ||
| 131 | ```sh | |
| 132 | b2 ls <bucket_name> | |
| 133 | ``` | |
| 134 | ||
| 135 | ```txt | |
| 136 | test.md | |
| 137 | ``` | |
| 138 | ||
| 139 | ### Sync a Directory | |
| 140 | ||
| 141 | If you have numerous files, you can use the `sync` function | |
| 142 | to perform functionality similar to `rsync`, where you can | |
| 143 | check what's in your bucket and sync anything that is new or modified. | |
| 144 | ||
| 145 | The command is issued as follows: | |
| 146 | ||
| 147 | ```sh | |
| 148 | b2 sync <source file location> <B2 bucket destination> | |
| 149 | ``` | |
| 150 | ||
| 151 | In my case, I can sync my user's entire home directory to my bucket | |
| 152 | without specifying any of the files directly: | |
| 153 | ||
| 154 | ```sh | |
| 155 | b2 sync /home/<user>/ "b2://<bucketName>/home/<user>" | |
| 156 | ``` | |
| 157 | ||
| 158 | ## Caveats | |
| 159 | ||
| 160 | ### Timing of Updates to the Web GUI | |
| 161 | ||
| 162 | When performing actions over a bucket, there is a slight delay in the | |
| 163 | web GUI when inspecting a bucket or its file. Note that simple actions | |
| 164 | such as uploading or deleting files may have a delay of a few minutes up | |
| 165 | to 24 hours. In my experience (\<10 GB and \~20,000 files), any actions | |
| 166 | took only a few minutes to update across clients. | |
| 167 | ||
| 168 | ### Symlinks | |
| 169 | ||
| 170 | Note that symlinks are resolved by b2, so if you have a link from | |
| 171 | `/home/<user>/nas-storage` that symlinks out to a | |
| 172 | `/mnt/nas-storage` folder that has 10TB of data, | |
| 173 | `b2` will resolve that link and start uploading all 10TB of | |
| 174 | data linked within the folder. | |
| 175 | ||
| 176 | If you're not sure if you have any symlinks, a symlink will look like | |
| 177 | this (note the `->` symbol): | |
| 178 | ||
| 179 | ```sh | |
| 180 | > ls -lha | |
| 181 | lrwxrwxrwx 1 root root 20 Jun 28 13:32 nas -> /mnt/nas-storage/ | |
| 182 | ``` | |
| 183 | ||
| 184 | You can recursively find symlink in a path with the following command: | |
| 185 | ||
| 186 | ```sh | |
| 187 | ls -lR /path/to/search | grep '^l' | |
| 188 | ``` | |
blog/2023-06-30-self-hosting-voyager-lemmy-client.org → content/blog/2023-06-30-self-hosting-voyager-lemmy-client.md renamed +48 −51
| @@ -1,57 +1,55 @@ | ||
| 1 | #+title: Self-Hosting Voyager: A Lemmy Web Client | |
| 2 | #+date: 2023-06-30 | |
| 1 | +++ | |
| 2 | date = 2023-06-30 | |
| 3 | title = "Self-Hosting Voyager - A Lemmy Web Client" | |
| 4 | description = "A guide to self-hosting the Voyager application on your own server." | |
| 5 | +++ | |
| 3 | 6 | |
| 4 | ** Installation Guide | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: installation-guide | |
| 7 | :END: | |
| 8 | [[https://github.com/aeharding/voyager][Voyager]] is a mobile-first | |
| 9 | Lemmy web client, based on iOS design standards. It follows very closely | |
| 10 | to Apollo's design. | |
| 7 | ## Installation Guide | |
| 8 | ||
| 9 | [Voyager](https://github.com/aeharding/voyager) is a mobile-first Lemmy | |
| 10 | web client, based on iOS design standards. It follows very closely to | |
| 11 | Apollo's design. | |
| 11 | 12 | |
| 12 | 13 | This post is a guide showing how I was able to build and launch my own |
| 13 | 14 | instance of Voyager via Docker Compose. |
| 14 | 15 | |
| 15 | *** Clone the Repository | |
| 16 | :PROPERTIES: | |
| 17 | :CUSTOM_ID: clone-the-repository | |
| 18 | :END: | |
| 16 | ### Clone the Repository | |
| 17 | ||
| 19 | 18 | Start by cloning the repository and entering it: |
| 20 | 19 | |
| 21 | #+begin_src sh | |
| 20 | ```sh | |
| 22 | 21 | git clone https://github.com/aeharding/voyager |
| 23 | 22 | cd voyager |
| 24 | #+end_src | |
| 23 | ``` | |
| 24 | ||
| 25 | ### Build the Image | |
| 25 | 26 | |
| 26 | *** Build the Image | |
| 27 | :PROPERTIES: | |
| 28 | :CUSTOM_ID: build-the-image | |
| 29 | :END: | |
| 30 | 27 | With this repository, you can build the image yourself without any |
| 31 | 28 | further configuration. When complete, it'll give you the image ID for |
| 32 | 29 | you to run. |
| 33 | 30 | |
| 34 | #+begin_src sh | |
| 31 | ```sh | |
| 35 | 32 | sudo docker build . |
| 36 | 33 | # Successfully built 5f00723cb5be |
| 37 | #+end_src | |
| 34 | ``` | |
| 38 | 35 | |
| 39 | 36 | With the image ID above, you can run the container and pass the |
| 40 | requested port =5314= through or use a custom port, if you wish. | |
| 37 | requested port `5314` through or use a custom port, if you | |
| 38 | wish. | |
| 41 | 39 | |
| 42 | You can also set the =CUSTOM_LEMMY_SERVERS= environment variable if you | |
| 43 | want to add to the default suggested login servers. This must be set | |
| 44 | with a comma separated list of suggested servers. The first instance in | |
| 45 | the list will be the default view for logged-out users. | |
| 40 | You can also set the `CUSTOM_LEMMY_SERVERS` environment | |
| 41 | variable if you want to add to the default suggested login servers. This | |
| 42 | must be set with a comma separated list of suggested servers. The first | |
| 43 | instance in the list will be the default view for logged-out users. | |
| 46 | 44 | |
| 47 | I will be using a =docker-compose.yml= file to run this container, | |
| 48 | instead of a =docker run= command. | |
| 45 | I will be using a `docker-compose.yml` file to run this | |
| 46 | container, instead of a `docker run` command. | |
| 49 | 47 | |
| 50 | #+begin_src sh | |
| 48 | ```sh | |
| 51 | 49 | nano docker-compose.yml |
| 52 | #+end_src | |
| 50 | ``` | |
| 53 | 51 | |
| 54 | #+begin_src conf | |
| 52 | ```conf | |
| 55 | 53 | version: "2" |
| 56 | 54 | services: |
| 57 | 55 | voyager: |
| @@ -61,32 +59,30 @@ services: | ||
| 61 | 59 | - "<custom_port>:5314" |
| 62 | 60 | environment: |
| 63 | 61 | - CUSTOM_LEMMY_SERVERS=lemmy.dbzer0.com,lemmy.world,lemmy.ml,beehaw.org |
| 64 | #+end_src | |
| 62 | ``` | |
| 65 | 63 | |
| 66 | #+begin_src sh | |
| 64 | ```sh | |
| 67 | 65 | sudo docker-compose up -d |
| 68 | #+end_src | |
| 66 | ``` | |
| 69 | 67 | |
| 70 | 68 | The web app will now be available at the following address: |
| 71 | =<machine_ip>:<custom_port>=. If you are running it on your local | |
| 72 | device, try =localhost:<custom_port>=. | |
| 69 | `<machine_ip>:<custom_port>`. If you are running it on your | |
| 70 | local device, try `localhost:<custom_port>`. | |
| 71 | ||
| 72 | ### Reverse Proxy | |
| 73 | 73 | |
| 74 | *** Reverse Proxy | |
| 75 | :PROPERTIES: | |
| 76 | :CUSTOM_ID: reverse-proxy | |
| 77 | :END: | |
| 78 | If you want to visit this app via an external URL or domain name, you'll | |
| 79 | need to set up a reverse proxy. The example below uses Nginx as a | |
| 80 | reverse proxy. | |
| 74 | If you want to visit this app via an external URL or domain name, | |
| 75 | you'll need to set up a reverse proxy. The example below uses Nginx as | |
| 76 | a reverse proxy. | |
| 81 | 77 | |
| 82 | 78 | Simply create the configuration file, paste the contents below, save the |
| 83 | 79 | file, symlink the file, and restart Nginx. |
| 84 | 80 | |
| 85 | #+begin_src sh | |
| 81 | ```sh | |
| 86 | 82 | sudo nano /etc/nginx/sites-available/voyager |
| 87 | #+end_src | |
| 83 | ``` | |
| 88 | 84 | |
| 89 | #+begin_src conf | |
| 85 | ```conf | |
| 90 | 86 | server { |
| 91 | 87 | if ($host ~ ^[^.]+\.example\.com$) { |
| 92 | 88 | return 301 https://$host$request_uri; |
| @@ -116,14 +112,15 @@ server { | ||
| 116 | 112 | include /etc/letsencrypt/options-ssl-nginx.conf; |
| 117 | 113 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; |
| 118 | 114 | } |
| 119 | #+end_src | |
| 115 | ``` | |
| 120 | 116 | |
| 121 | #+begin_src sh | |
| 117 | ```sh | |
| 122 | 118 | sudo ln sudo ln -s /etc/nginx/sites-available/voyager /etc/nginx/sites-enabled/voyager |
| 123 | 119 | sudo systemctl restart nginx.service |
| 124 | #+end_src | |
| 120 | ``` | |
| 125 | 121 | |
| 126 | The site will now be available at the =server_name= you specified above! | |
| 122 | The site will now be available at the `server_name` you | |
| 123 | specified above! | |
| 127 | 124 | |
| 128 | 125 | You can visit my instance at |
| 129 | [[https://voyager.cleberg.net][voyager.cleberg.net]] for an example. | |
| 126 | [voyager.cleberg.net](https://voyager.cleberg.net) for an example. | |
blog/2023-07-12-mullvad-wireguard-lan.org → content/blog/2023-07-12-mullvad-wireguard-lan.md renamed +54 −61
| @@ -1,32 +1,30 @@ | ||
| 1 | #+title: Enabling LAN Access in Mullvad Wireguard Conf Files | |
| 2 | #+date: 2023-07-12 | |
| 3 | ||
| 4 | ** Download Configuration Files from Mullvad | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: download-configuration-files-from-mullvad | |
| 7 | :END: | |
| 8 | To begin, you'll need | |
| 9 | [[https://mullvad.net/account/wireguard-config][Wireguard configuration | |
| 10 | files from Mullvad]]. You can choose any of the options as you download | |
| 11 | them. For example, I enabled the kill switch, selected all countries, | |
| 12 | and selected a few content filters. | |
| 1 | +++ | |
| 2 | date = 2023-07-12 | |
| 3 | title = "Enable LAN Access in Mullvad Wireguard Conf Files" | |
| 4 | description = "Learn how to enable LAN access manually in Mullvad configuration files." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Download Configuration Files from Mullvad | |
| 8 | ||
| 9 | To begin, you'll need [Wireguard configuration files from | |
| 10 | Mullvad](https://mullvad.net/account/wireguard-config). You can choose | |
| 11 | any of the options as you download them. For example, I enabled the kill | |
| 12 | switch, selected all countries, and selected a few content filters. | |
| 13 | 13 | |
| 14 | 14 | Once downloaded, unzip the files and move them to the Wireguard folder |
| 15 | 15 | on your system. |
| 16 | 16 | |
| 17 | #+begin_src sh | |
| 17 | ```sh | |
| 18 | 18 | cd ~/Downloads |
| 19 | 19 | unzip mullvad_wireguard_linux_all_all.zip |
| 20 | 20 | doas mv *.conf /etc/wireguard/ |
| 21 | #+end_src | |
| 21 | ``` | |
| 22 | ||
| 23 | ### Configuration File Layout | |
| 22 | 24 | |
| 23 | *** Configuration File Layout | |
| 24 | :PROPERTIES: | |
| 25 | :CUSTOM_ID: configuration-file-layout | |
| 26 | :END: | |
| 27 | 25 | The default configuration files will look something like this: |
| 28 | 26 | |
| 29 | #+begin_src conf | |
| 27 | ```conf | |
| 30 | 28 | [Interface] |
| 31 | 29 | # Device: <redacted> |
| 32 | 30 | PrivateKey = <redacted> |
| @@ -39,50 +37,45 @@ PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m ad | ||
| 39 | 37 | PublicKey = <redacted> |
| 40 | 38 | AllowedIPs = <redacted> |
| 41 | 39 | Endpoint = <redacted> |
| 42 | #+end_src | |
| 40 | ``` | |
| 43 | 41 | |
| 44 | #+begin_quote | |
| 45 | Note: If you didn't select the kill switch option, you won't see the | |
| 46 | =PostUp= and =PreDown= lines. In this case, you'll need to modify the | |
| 47 | script below to simply append those lines to the =[Interface]= block. | |
| 42 | > Note: If you didn't select the kill switch option, you won't see the | |
| 43 | > `PostUp` and `PreDown` lines. In this case, | |
| 44 | > you'll need to modify the script below to simply append those lines | |
| 45 | > to the `[Interface]` block. | |
| 48 | 46 | |
| 49 | #+end_quote | |
| 47 | ## Editing the Configuration Files | |
| 50 | 48 | |
| 51 | ** Editing the Configuration Files | |
| 52 | :PROPERTIES: | |
| 53 | :CUSTOM_ID: editing-the-configuration-files | |
| 54 | :END: | |
| 55 | 49 | Once you have the files, you'll need to edit them and replace the |
| 56 | =PostUp= and =PreDown= lines to enable LAN access. | |
| 50 | `PostUp` and `PreDown` lines to enable LAN access. | |
| 57 | 51 | |
| 58 | 52 | I recommend that you do this process as root, since you'll need to be |
| 59 | able to access files in =/etc/wireguard=, which are generally owned by | |
| 60 | root. You can also try using =sudo= or =doas=, but I didn't test that | |
| 61 | scenario so you may need to adjust, as necessary. | |
| 53 | able to access files in `/etc/wireguard`, which are generally | |
| 54 | owned by root. You can also try using `sudo` or | |
| 55 | `doas`, but I didn't test that scenario so you may need to | |
| 56 | adjust, as necessary. | |
| 62 | 57 | |
| 63 | #+begin_src sh | |
| 58 | ```sh | |
| 64 | 59 | su |
| 65 | #+end_src | |
| 60 | ``` | |
| 66 | 61 | |
| 67 | 62 | Create the Python file that we'll be using to update the Wireguard |
| 68 | 63 | configuration files. |
| 69 | 64 | |
| 70 | #+begin_src sh | |
| 65 | ```sh | |
| 71 | 66 | nano replace.py |
| 72 | #+end_src | |
| 67 | ``` | |
| 73 | 68 | |
| 74 | 69 | Within the Python file, copy and paste the logic below. This script will |
| 75 | 70 | open a directory, loop through every configuration file within the |
| 76 | directory, and replace the =PostUp= and =PreDown= lines with the new | |
| 77 | LAN-enabled iptables commands. | |
| 78 | ||
| 79 | #+begin_quote | |
| 80 | Note: If your LAN is on a subnet other than =192.168.1.0/24=, you'll | |
| 81 | need to update the Python script below appropriately. | |
| 71 | directory, and replace the `PostUp` and `PreDown` | |
| 72 | lines with the new LAN-enabled iptables commands. | |
| 82 | 73 | |
| 83 | #+end_quote | |
| 74 | > Note: If your LAN is on a subnet other than | |
| 75 | > `192.168.1.0/24`, you'll need to update the Python script | |
| 76 | > below appropriately. | |
| 84 | 77 | |
| 85 | #+begin_src python | |
| 78 | ```python | |
| 86 | 79 | import os |
| 87 | 80 | import fileinput |
| 88 | 81 | |
| @@ -101,25 +94,25 @@ for file in os.listdir(dir): | ||
| 101 | 94 | print(line, end="") |
| 102 | 95 | |
| 103 | 96 | print("--- done ---") |
| 104 | #+end_src | |
| 97 | ``` | |
| 105 | 98 | |
| 106 | 99 | Once you're done, save and close the file. You can now run the Python |
| 107 | 100 | script and watch as each file is updated. |
| 108 | 101 | |
| 109 | #+begin_src sh | |
| 102 | ```sh | |
| 110 | 103 | python3 replace.py |
| 111 | #+end_src | |
| 104 | ``` | |
| 112 | 105 | |
| 113 | To confirm it worked, you can =cat= one of the configuration files to | |
| 114 | inspect the new logic and connect to one to test it out. | |
| 106 | To confirm it worked, you can `cat` one of the configuration | |
| 107 | files to inspect the new logic and connect to one to test it out. | |
| 115 | 108 | |
| 116 | #+begin_src sh | |
| 109 | ```sh | |
| 117 | 110 | cat /etc/wireguard/us-chi-wg-001.conf |
| 118 | #+end_src | |
| 111 | ``` | |
| 119 | 112 | |
| 120 | 113 | The configuration files should now look like this: |
| 121 | 114 | |
| 122 | #+begin_src conf | |
| 115 | ```conf | |
| 123 | 116 | [Interface] |
| 124 | 117 | # Device: <redacted> |
| 125 | 118 | PrivateKey = <redacted> |
| @@ -132,20 +125,20 @@ PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m ad | ||
| 132 | 125 | PublicKey = <redacted> |
| 133 | 126 | AllowedIPs = <redacted> |
| 134 | 127 | Endpoint = <redacted> |
| 135 | #+end_src | |
| 128 | ``` | |
| 136 | 129 | |
| 137 | If you connect to a Wireguard interface, such as =us-chi-wg-001=, you | |
| 138 | can test your SSH functionality and see that it works even while on the | |
| 139 | VPN. | |
| 130 | If you connect to a Wireguard interface, such as | |
| 131 | `us-chi-wg-001`, you can test your SSH functionality and see | |
| 132 | that it works even while on the VPN. | |
| 140 | 133 | |
| 141 | #+begin_src sh | |
| 134 | ```sh | |
| 142 | 135 | wg-quick up us-chi-wg-001 |
| 143 | 136 | ssh user@lan-host |
| 144 | #+end_src | |
| 137 | ``` | |
| 145 | 138 | |
| 146 | 139 | To confirm your VPN connection, you can curl Mullvad's connection API: |
| 147 | 140 | |
| 148 | #+begin_src sh | |
| 141 | ```sh | |
| 149 | 142 | curl https://am.i.mullvad.net/connected |
| 150 | 143 | # You are connected to Mullvad (server us-chi-wg-001). Your IP address is <redacted> |
| 151 | #+end_src | |
| 144 | ``` | |
blog/2023-07-19-plex-transcoder-errors.org → content/blog/2023-07-19-plex-transcoder-errors.md renamed +32 −36
| @@ -1,67 +1,63 @@ | ||
| 1 | #+title: How to Fix Plex Error: 'Conversion failed. The transcoder failed to start up.' | |
| 2 | #+date: 2023-07-19 | |
| 1 | +++ | |
| 2 | date = 2023-07-19 | |
| 3 | title = "How to Avoid Plex Error: 'Conversion failed. The transcoder failed to start up.'" | |
| 4 | description = "Learn how to avoid Plex conversion errors caused by subtitles." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Plex Transcoder Error | |
| 3 | 8 | |
| 4 | ** Plex Transcoder Error | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: plex-transcoder-error | |
| 7 | :END: | |
| 8 | 9 | Occasionally, you may see an error in your Plex client that references a |
| 9 | 10 | failure with the transcoder conversion process. The specific error |
| 10 | 11 | wording is: |
| 11 | 12 | |
| 12 | #+begin_src txt | |
| 13 | ```txt | |
| 13 | 14 | Conversion failed. The transcoder failed to start up. |
| 14 | #+end_src | |
| 15 | ``` | |
| 16 | ||
| 17 | ## Debugging the Cause | |
| 15 | 18 | |
| 16 | ** Debugging the Cause | |
| 17 | :PROPERTIES: | |
| 18 | :CUSTOM_ID: debugging-the-cause | |
| 19 | :END: | |
| 20 | In order to get a better look at what is causing the error, I'm going to | |
| 21 | observe the Plex console while the error occurs. To do this, open the | |
| 22 | Plex web client, go to =Settings= > =Manage= > =Console=. Now, try to | |
| 23 | play the title again and watch to see which errors occur. | |
| 19 | In order to get a better look at what is causing the error, I'm going | |
| 20 | to observe the Plex console while the error occurs. To do this, open the | |
| 21 | Plex web client, go to `Settings` > `Manage` > | |
| 22 | `Console`. Now, try to play the title again and watch to see | |
| 23 | which errors occur. | |
| 24 | 24 | |
| 25 | 25 | In my case, you can see the errors below are related to a subtitle file |
| 26 | (=.srt=) causing the transcoder to crash. | |
| 26 | (`.srt`) causing the transcoder to crash. | |
| 27 | 27 | |
| 28 | #+begin_src txt | |
| 28 | ```txt | |
| 29 | 29 | Jul 19, 2023 16:49:34.945 [140184571120440] Error — Couldn't find the file to stream: /movies/Movie Title (2021)/Movie Title (2021).srt |
| 30 | 30 | Jul 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 |
| 31 | 31 | Jul 19, 2023 16:49:35.225 [140184532732728] Warning — [Req#760d/Transcode] Got a request to stop a transcode session without a valid session GUID. |
| 32 | 32 | Jul 19, 2023 16:49:45.561 [140184532732728] Info — [Req#7648] AutoUpdate: no updates available |
| 33 | 33 | Jul 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" |
| 34 | #+end_src | |
| 34 | ``` | |
| 35 | ||
| 36 | ## Solving the Error | |
| 35 | 37 | |
| 36 | ** Solving the Error | |
| 37 | :PROPERTIES: | |
| 38 | :CUSTOM_ID: solving-the-error | |
| 39 | :END: | |
| 40 | 38 | In my case, I simply removed the subtitle file because it was not |
| 41 | 39 | critical to keep. You may also avoid this by turning off subtitles if |
| 42 | 40 | you don't want to delete the file. |
| 43 | 41 | |
| 44 | #+begin_src sh | |
| 42 | ```sh | |
| 45 | 43 | rm -rf "/movies/Movie Title (2021)/Movie Title (2021).srt" |
| 46 | #+end_src | |
| 44 | ``` | |
| 47 | 45 | |
| 48 | 46 | Once the subtitle is removed from the directory or subtitles are turned |
| 49 | 47 | off, try to play the title again. At this point, it should play without |
| 50 | 48 | error. If not, reopen or refresh your Plex client and the Plex server: |
| 51 | 49 | |
| 52 | #+begin_src sh | |
| 50 | ```sh | |
| 53 | 51 | sudo systemctl restart plexmediaserver.service |
| 54 | #+end_src | |
| 52 | ``` | |
| 53 | ||
| 54 | ## Related Discussion | |
| 55 | 55 | |
| 56 | ** Related Discussion | |
| 57 | :PROPERTIES: | |
| 58 | :CUSTOM_ID: related-discussion | |
| 59 | :END: | |
| 60 | Looking at | |
| 61 | [[https://forums.plex.tv/t/subtitles-crashing-plex-transcoder-samsung-q80-tv-with-or-without-hardware-transcode/741441/2][a | |
| 62 | related Plex forum post]], it seems that =.srt= subtitles are the core | |
| 63 | issue here. However, Plex does not seem to have a resolution that fixes | |
| 64 | these errors. | |
| 56 | Looking at [a related Plex forum | |
| 57 | post](https://forums.plex.tv/t/subtitles-crashing-plex-transcoder-samsung-q80-tv-with-or-without-hardware-transcode/741441/2), | |
| 58 | it seems that `.srt` subtitles are the core issue here. | |
| 59 | However, Plex does not seem to have a resolution that fixes these | |
| 60 | errors. | |
| 65 | 61 | |
| 66 | 62 | Unfortunately, I would suggest converting subtitle formats, burning the |
| 67 | 63 | subtitles into the title, or simply removing subtitles when they cause |
content/blog/2023-08-18-agile-auditing.md added +158
| @@ -0,0 +1,158 @@ | ||
| 1 | +++ | |
| 2 | date = 2023-08-18 | |
| 3 | title = "Agile Auditing: An Introduction" | |
| 4 | description = "A quick introduction to using the Agile methodology in an audit." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## What is Agile Auditing? | |
| 8 | ||
| 9 | [Agile](https://en.wikipedia.org/wiki/Agile_software_development), the | |
| 10 | collaborative philosophy behind many software development methods, has | |
| 11 | been picking up steam as a beneficial tool to use in the external and | |
| 12 | internal auditing world. | |
| 13 | ||
| 14 | This blog post will walk through commonly used terms within Agile, | |
| 15 | Scrum, and Kanban in order to translate these terms and roles into | |
| 16 | audit-specific terms. | |
| 17 | ||
| 18 | Whether your team is in charge of a financial statement audit, an | |
| 19 | attestation (SOC 1, SOC 2, etc.), or a unique internal audit, the terms | |
| 20 | used throughout this post should still apply. | |
| 21 | ||
| 22 | ## Agile | |
| 23 | ||
| 24 | To start, I'll take a look at Agile. | |
| 25 | ||
| 26 | > The Agile methodology is a project management approach that involves | |
| 27 | > breaking the project into phases and emphasizes continuous | |
| 28 | > collaboration and improvement. Teams follow a cycle of planning, | |
| 29 | > executing, and evaluating. | |
| 30 | ||
| 31 | While this approach may seem familiar to what audit teams have | |
| 32 | historically done, an audit team must make distinct changes in their | |
| 33 | mentality and how they approach and manage a project. | |
| 34 | ||
| 35 | ### Agile Values | |
| 36 | ||
| 37 | The Agile Manifesto, written in 2001 at a summit in Utah, contain a set | |
| 38 | of four main values that comprise the Agile approach: | |
| 39 | ||
| 40 | 1. Individuals and interactions over processes and tools. | |
| 41 | 2. Working software over comprehensive documentation. | |
| 42 | 3. Customer collaboration over contract negotiation. | |
| 43 | 4. Responding to change over following a plan. | |
| 44 | ||
| 45 | Beyond the four values, [twelve | |
| 46 | principles](https://agilemanifesto.org/principles.html) were also | |
| 47 | written as part of the summit. | |
| 48 | ||
| 49 | In order to relate these values to an audit or attestation engagement, | |
| 50 | we need to shift the focus from software development to the main goal of | |
| 51 | an engagement: completing sufficient audit testing to address to | |
| 52 | relevant risks over the processes and controls at hand. | |
| 53 | ||
| 54 | Audit Examples: | |
| 55 | ||
| 56 | - Engagement teams must value the team members, client contacts, and | |
| 57 | their interactions over the historical processes and tools that have | |
| 58 | been used. | |
| 59 | - Engagement teams must value a final report that contains sufficient | |
| 60 | audit documentation over excessive documentation or scope creep. | |
| 61 | - Engagement teams must collaborate with the audit clients as much as | |
| 62 | feasible to ensure that both sides are constantly updated with | |
| 63 | current knowledge of the engagement's status and any potential | |
| 64 | findings, rather than waiting for pre-set meetings or the end of the | |
| 65 | engagement to communicate. | |
| 66 | - Engagement teams must be able to respond to change in an | |
| 67 | engagement's schedule, scope, or environment to ensure that the | |
| 68 | project is completed in a timely manner and that all relevant areas | |
| 69 | are tested. | |
| 70 | - In terms of an audit department's portfolio, they must be able | |
| 71 | to respond to changes in their company's or client's | |
| 72 | environment and be able to dynamically change their audit plan | |
| 73 | accordingly. | |
| 74 | ||
| 75 | ## Scrum | |
| 76 | ||
| 77 | The above section discusses the high-level details of the Agile | |
| 78 | philosophy and how an audit team can potentially mold that mindset into | |
| 79 | the audit world, but how does a team implement these ideas? | |
| 80 | ||
| 81 | There are many methods that use an Agile mindset, but I prefer | |
| 82 | [Scrum](https://en.wikipedia.org/wiki/Scrum_(software_development)). | |
| 83 | Scrum is a framework based on Agile that enables a team to work through | |
| 84 | a project through a series of roles, ceremonies, artifacts, and values. | |
| 85 | ||
| 86 | Let's dive into each of these individually. | |
| 87 | ||
| 88 | ### Scrum Team | |
| 89 | ||
| 90 | A scrum project is only as good as the team running the project. | |
| 91 | Standard scrum teams are separated into three distinct areas: | |
| 92 | ||
| 93 | 1. **Product Owner (Client Contact)**: The client contact is the audit | |
| 94 | equivalent of the product owner in Scrum. They are responsible for | |
| 95 | partnering with the engagement or audit team to ensure progress is | |
| 96 | being made, priorities are established, and clear guidance is given | |
| 97 | when questions or findings arise within each sprint. | |
| 98 | 2. **Scrum Master (Engagement Lead)**: The engagement or audit team | |
| 99 | lead is responsible for coaching the team and the client contact on | |
| 100 | the scrum process, tracking team progress against plan, scheduling | |
| 101 | necessary resources, and helping remove obstacles. | |
| 102 | 3. **Scrum Developers (Engagement Members)**: The engagement or audit | |
| 103 | team is the set of team members responsible for getting the work | |
| 104 | done. These team members will work on each task, report progress, | |
| 105 | resolve obstacles, and collaborate with other team members and the | |
| 106 | client contact to ensure goals are being met. | |
| 107 | ||
| 108 | ### Scrum Ceremonies | |
| 109 | ||
| 110 | Scrum ceremonies are events that are performed on a regular basis. | |
| 111 | ||
| 112 | 1. **Sprint Planning**: The team works together to plan the upcoming | |
| 113 | sprint goal and which user stories (tasks) will be added to the | |
| 114 | sprint to achieve that goal. | |
| 115 | 2. **Sprint**: The time period, typically at least one week and no more | |
| 116 | than one month in length, where the team works on the stories and | |
| 117 | anything in the backlog. | |
| 118 | 3. **Daily Scrum**: A very short meeting held each day, typically 15 | |
| 119 | minutes, to quickly emphasize alignment on the sprint goal and plan | |
| 120 | the next 24 hours. Each team member may share what they did the day | |
| 121 | before, what they'll do today, and any obstacles to their work. | |
| 122 | 4. **Sprint Review**: At the end of each sprint, the team will gather | |
| 123 | and discuss the progress, obstacles, and backlog from the previous | |
| 124 | sprint. | |
| 125 | 5. **Sprint Retrospective**: More specific than the sprint review, the | |
| 126 | retrospective is meant to discuss what worked and what did not work | |
| 127 | during the sprint. This may be processes, tools, people, or even | |
| 128 | things related to the Scrum ceremonies. | |
| 129 | ||
| 130 | One additional ceremony that may be applicable is organizing the | |
| 131 | backlog. This is typically the responsibility of the engagement leader | |
| 132 | and is meant to prioritize and clarify what needs to be done to complete | |
| 133 | items in the backlog. | |
| 134 | ||
| 135 | ### Artifacts | |
| 136 | ||
| 137 | While artifacts are generally not customizable in the audit world (i.e., | |
| 138 | each control test must include some kind of working paper with evidence | |
| 139 | supporting the test results), I wanted to include some quick notes on | |
| 140 | associating scrum artifact terms with an audit. | |
| 141 | ||
| 142 | 1. **Product Backlog**: This is the overall backlog of unfinished audit | |
| 143 | tasks from all prior sprints. | |
| 144 | 2. **Sprint Backlog**: This is the backlog of unfinished audit tasks | |
| 145 | from one individual sprint. | |
| 146 | 3. **Increment**: This is the output of each sprint - generally this is | |
| 147 | best thought of as any documentation prepared during the sprint, | |
| 148 | such as risk assessments, control working papers, deficiency | |
| 149 | analysis, etc. | |
| 150 | ||
| 151 | ## Kanban | |
| 152 | ||
| 153 | Last but not least, Kanban is a methodology that relies on boards to | |
| 154 | categorize work into distinct, descriptive categories that allow an | |
| 155 | agile or scrum team to effectively plan the work of a sprint or project. | |
| 156 | ||
| 157 | See Atlassian's [Kanban](https://www.atlassian.com/agile/kanban) page | |
| 158 | for more information. | |
blog/2023-09-15-gitweb.org → content/blog/2023-09-15-gitweb.md renamed +29 −34
| @@ -1,13 +1,15 @@ | ||
| 1 | #+title: GitWeb via Nginx | |
| 2 | #+date: 2023-09-16 | |
| 3 | ||
| 4 | ** Overview | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: overview | |
| 7 | :END: | |
| 8 | [[https://git-scm.com/book/en/v2/Git-on-the-Server-GitWeb][GitWeb]] is a | |
| 1 | +++ | |
| 2 | date = 2023-09-15 | |
| 3 | title = "GitWeb via Nginx" | |
| 4 | description = "A guide to self-hosting GitWeb using the Nginx web server." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Overview | |
| 8 | ||
| 9 | [GitWeb](https://git-scm.com/book/en/v2/Git-on-the-Server-GitWeb) is a | |
| 9 | 10 | simple web-based visualizer for git repositories. By default, GitWeb |
| 10 | will only run with the =lighttpd= or =webrick= web servers. | |
| 11 | will only run with the `lighttpd` or `webrick` web | |
| 12 | servers. | |
| 11 | 13 | |
| 12 | 14 | However, this guide will show you how to keep GitWeb running in the |
| 13 | 15 | background and display information for all repositories in a chosen |
| @@ -15,30 +17,25 @@ directory. | ||
| 15 | 17 | |
| 16 | 18 | See below for the final result: |
| 17 | 19 | |
| 18 | #+caption: Gitweb | |
| 19 | [[https://img.cleberg.net/blog/20230915-gitweb/gitweb.png]] | |
| 20 |  | |
| 21 | ||
| 22 | ## Install Dependencies | |
| 20 | 23 | |
| 21 | ** Install Dependencies | |
| 22 | :PROPERTIES: | |
| 23 | :CUSTOM_ID: install-dependencies | |
| 24 | :END: | |
| 25 | 24 | To start, you'll need install the following packages: |
| 26 | 25 | |
| 27 | #+begin_src sh | |
| 26 | ```sh | |
| 28 | 27 | sudo apt install git gitweb fcgiwrap nginx |
| 29 | #+end_src | |
| 28 | ``` | |
| 29 | ||
| 30 | ## Configure Nginx | |
| 30 | 31 | |
| 31 | ** Configure Nginx | |
| 32 | :PROPERTIES: | |
| 33 | :CUSTOM_ID: configure-nginx | |
| 34 | :END: | |
| 35 | 32 | Once installed, create an Nginx configuration file. |
| 36 | 33 | |
| 37 | #+begin_src sh | |
| 34 | ```sh | |
| 38 | 35 | sudo nano /etc/nginx/sites-available/git.example.com |
| 39 | #+end_src | |
| 36 | ``` | |
| 40 | 37 | |
| 41 | #+begin_src conf | |
| 38 | ```conf | |
| 42 | 39 | server { |
| 43 | 40 | listen 80; |
| 44 | 41 | server_name example.com; |
| @@ -57,26 +54,24 @@ server { | ||
| 57 | 54 | index index.cgi; |
| 58 | 55 | } |
| 59 | 56 | } |
| 60 | #+end_src | |
| 57 | ``` | |
| 61 | 58 | |
| 62 | 59 | To make the configuration active, you need to symlink it and then |
| 63 | 60 | restart Nginx. |
| 64 | 61 | |
| 65 | #+begin_src sh | |
| 62 | ```sh | |
| 66 | 63 | sudo ln -s /etc/nginx/sites-available/git.example.com /etc/nginx/sites-enabled/git.example.com |
| 67 | 64 | sudo systemctl restart nginx.service |
| 68 | #+end_src | |
| 65 | ``` | |
| 69 | 66 | |
| 70 | 67 | The GitWeb application should now be available via the URL you set in |
| 71 | 68 | the Nginx configuration above. |
| 72 | 69 | |
| 73 | ** Customize GitWeb | |
| 74 | :PROPERTIES: | |
| 75 | :CUSTOM_ID: customize-gitweb | |
| 76 | :END: | |
| 70 | ## Customize GitWeb | |
| 71 | ||
| 77 | 72 | If you need to, you can customize many things about Gitweb by editing |
| 78 | the [[https://git-scm.com/docs/gitweb.conf][gitweb.conf]] file. | |
| 73 | the [gitweb.conf](https://git-scm.com/docs/gitweb.conf) file. | |
| 79 | 74 | |
| 80 | #+begin_src sh | |
| 75 | ```sh | |
| 81 | 76 | sudo nano /etc/gitweb.conf |
| 82 | #+end_src | |
| 77 | ``` | |
blog/2023-09-19-audit-sql-scripts.org → content/blog/2023-09-19-audit-sql-scripts.md renamed +49 −55
| @@ -1,25 +1,24 @@ | ||
| 1 | #+title: Useful SQL Scripts for Auditing Logical Access | |
| 2 | #+date: 2023-09-20 | |
| 1 | +++ | |
| 2 | date = 2023-09-19 | |
| 3 | title = "Useful SQL Scripts for Auditing Logical Access" | |
| 4 | description = "A reference of SQL scripts for auditing logical access for common databases." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Overview | |
| 3 | 8 | |
| 4 | ** Overview | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: overview | |
| 7 | :END: | |
| 8 | 9 | When you have to scope a database into your engagement, you may be |
| 9 | 10 | curious how to best extract the information from the database. While |
| 10 | 11 | there are numerous different methods to extract this type of |
| 11 | information, I'm going to show an example of how to gather all users and | |
| 12 | privileges from three main database types: Oracle, Microsoft SQL, and | |
| 13 | MySQL. | |
| 12 | information, I'm going to show an example of how to gather all users | |
| 13 | and privileges from three main database types: Oracle, Microsoft SQL, | |
| 14 | and MySQL. | |
| 15 | ||
| 16 | ## Oracle | |
| 14 | 17 | |
| 15 | ** Oracle | |
| 16 | :PROPERTIES: | |
| 17 | :CUSTOM_ID: oracle | |
| 18 | :END: | |
| 19 | 18 | You can use the following SQL script to see all users and their |
| 20 | 19 | privileges in an Oracle database: |
| 21 | 20 | |
| 22 | #+begin_src sql | |
| 21 | ```sql | |
| 23 | 22 | SELECT |
| 24 | 23 | grantee AS "User", |
| 25 | 24 | privilege AS "Privilege" |
| @@ -35,43 +34,40 @@ FROM | ||
| 35 | 34 | dba_tab_privs |
| 36 | 35 | WHERE |
| 37 | 36 | grantee IN (SELECT DISTINCT grantee FROM dba_tab_privs); |
| 38 | #+end_src | |
| 37 | ``` | |
| 39 | 38 | |
| 40 | This script queries the =dba_sys_privs= and =dba_tab_privs= views to | |
| 41 | retrieve system and table-level privileges respectively. It then | |
| 42 | combines the results using =UNION ALL= to show all users and their | |
| 43 | associated privileges. Please note that this method does not extract | |
| 44 | information from the =dba_role_privs= table - use the method below for | |
| 39 | This script queries the `dba_sys_privs` and | |
| 40 | `dba_tab_privs` views to retrieve system and table-level | |
| 41 | privileges respectively. It then combines the results using | |
| 42 | `UNION ALL` to show all users and their associated | |
| 43 | privileges. Please note that this method does not extract information | |
| 44 | from the `dba_role_privs` table - use the method below for | |
| 45 | 45 | that data. |
| 46 | 46 | |
| 47 | 47 | Please note that you might need appropriate privileges (e.g., DBA |
| 48 | 48 | privileges) to access these views, and you should exercise caution when |
| 49 | 49 | querying system tables in a production Oracle database. |
| 50 | 50 | |
| 51 | *** Alternative Oracle Query | |
| 52 | :PROPERTIES: | |
| 53 | :CUSTOM_ID: alternative-oracle-query | |
| 54 | :END: | |
| 51 | ### Alternative Oracle Query | |
| 52 | ||
| 55 | 53 | You can also extract each table's information separately and perform |
| 56 | 54 | processing outside the database to explore and determine the information |
| 57 | 55 | necessary for the audit: |
| 58 | 56 | |
| 59 | #+begin_src sql | |
| 57 | ```sql | |
| 60 | 58 | SELECT * FROM sys.dba_role_privs; |
| 61 | 59 | SELECT * FROM sys.dba_sys_privs; |
| 62 | 60 | SELECT * FROM sys.dba_tab_privs; |
| 63 | 61 | SELECT * FROM sys.dba_users; |
| 64 | #+end_src | |
| 62 | ``` | |
| 63 | ||
| 64 | ## Microsoft SQL | |
| 65 | 65 | |
| 66 | ** Microsoft SQL | |
| 67 | :PROPERTIES: | |
| 68 | :CUSTOM_ID: microsoft-sql | |
| 69 | :END: | |
| 70 | 66 | You can use the following SQL script to see all users and their |
| 71 | 67 | privileges in a Microsoft SQL Server database |
| 72 | ([[https://stackoverflow.com/a/30040784][source]]): | |
| 68 | ([source](https://stackoverflow.com/a/30040784)): | |
| 73 | 69 | |
| 74 | #+begin_src sql | |
| 70 | ```sql | |
| 75 | 71 | /* |
| 76 | 72 | Security Audit Report |
| 77 | 73 | 1) List all access provisioned to a sql user or windows user/group directly |
| @@ -216,47 +212,45 @@ ORDER BY | ||
| 216 | 212 | perm.[permission_name], |
| 217 | 213 | perm.[state_desc], |
| 218 | 214 | obj.type_desc--perm.[class_desc] |
| 219 | #+end_src | |
| 215 | ``` | |
| 216 | ||
| 217 | ## MySQL | |
| 220 | 218 | |
| 221 | ** MySQL | |
| 222 | :PROPERTIES: | |
| 223 | :CUSTOM_ID: mysql | |
| 224 | :END: | |
| 225 | 219 | You can use the following SQL script to see all users and their |
| 226 | 220 | privileges in a MySQL database: |
| 227 | 221 | |
| 228 | #+begin_src shell | |
| 222 | ```sh | |
| 229 | 223 | mysql -u root -p |
| 230 | #+end_src | |
| 224 | ``` | |
| 231 | 225 | |
| 232 | 226 | Find all users and hosts with access to the database: |
| 233 | 227 | |
| 234 | #+begin_src sql | |
| 228 | ```sql | |
| 235 | 229 | SELECT * FROM information_schema.user_privileges; |
| 236 | #+end_src | |
| 230 | ``` | |
| 237 | 231 | |
| 238 | 232 | This script retrieves user information and their associated |
| 239 | database-level privileges from the =information_schema.user_privileges= | |
| 240 | table in MySQL. It lists various privileges such as SELECT, INSERT, | |
| 241 | UPDATE, DELETE, CREATE, and more for each user and database combination. | |
| 233 | database-level privileges from the | |
| 234 | `information_schema.user_privileges` table in MySQL. It lists | |
| 235 | various privileges such as SELECT, INSERT, UPDATE, DELETE, CREATE, and | |
| 236 | more for each user and database combination. | |
| 242 | 237 | |
| 243 | Please note that you may need appropriate privileges (e.g., =SELECT= | |
| 244 | privileges on =information_schema.user_privileges=) to access this | |
| 238 | Please note that you may need appropriate privileges (e.g., | |
| 239 | `SELECT` privileges on | |
| 240 | `information_schema.user_privileges`) to access this | |
| 245 | 241 | information in a MySQL database. Additionally, some privileges like |
| 246 | 242 | GRANT OPTION, EXECUTE, EVENT, and TRIGGER may not be relevant for all |
| 247 | 243 | users and databases. |
| 248 | 244 | |
| 249 | *** Alternative MySQL Query | |
| 250 | :PROPERTIES: | |
| 251 | :CUSTOM_ID: alternative-mysql-query | |
| 252 | :END: | |
| 245 | ### Alternative MySQL Query | |
| 246 | ||
| 253 | 247 | You can also grab individual sets of data from MySQL if you prefer to |
| 254 | 248 | join them after extraction. I have marked the queries below with |
| 255 | =SELECT ...= and excluded most =WHERE= clauses for brevity. You should | |
| 256 | determine the relevant privileges in-scope and query for those | |
| 257 | privileges to reduce the length of time to query. | |
| 249 | `SELECT ...` and excluded most `WHERE` clauses for | |
| 250 | brevity. You should determine the relevant privileges in-scope and query | |
| 251 | for those privileges to reduce the length of time to query. | |
| 258 | 252 | |
| 259 | #+begin_src sql | |
| 253 | ```sql | |
| 260 | 254 | -- Global Permissions |
| 261 | 255 | SELECT ... FROM mysql.user; |
| 262 | 256 | |
| @@ -275,4 +269,4 @@ WHERE db = @db_name; | ||
| 275 | 269 | -- Password Configuration |
| 276 | 270 | SHOW GLOBAL VARIABLES LIKE 'validate_password%'; |
| 277 | 271 | SHOW VARIABLES LIKE 'validate_password%'; |
| 278 | #+end_src | |
| 272 | ``` | |
content/blog/2023-10-04-digital-minimalism.md added +106
| @@ -0,0 +1,106 @@ | ||
| 1 | +++ | |
| 2 | date = 2023-10-04 | |
| 3 | title = "Digital Minimalism" | |
| 4 | description = "My personal retrospective on digital minimalism." | |
| 5 | +++ | |
| 6 | ||
| 7 | I've written [a note about minimalism](/wiki/#digital-garden) | |
| 8 | before, but I wanted to dedicate some time to reflect on digital | |
| 9 | minimalism and how I've been able to minimize the impact of digital | |
| 10 | devices in my life. | |
| 11 | ||
| 12 | > These changes crept up on us and happened fast, before we had a chance | |
| 13 | > to step back and ask what we really wanted out of the rapid advances | |
| 14 | > of the past decade. We added new technologies to the periphery of our | |
| 15 | > experience for minor reasons, then woke one morning to discover that | |
| 16 | > they had colonized the core of our daily life. We didn't, in other | |
| 17 | > words, sign up for the digital world in which we're currently | |
| 18 | > entrenched; we seem to have stumbled backward into it. | |
| 19 | > | |
| 20 | > *(Digital Minimalism, 2019)* | |
| 21 | ||
| 22 | ## The Principles of Digital Minimalism | |
| 23 | ||
| 24 | As noted in Cal Newport's book, *Digital Minimalism*, there are three | |
| 25 | main principles to digital minimalism that I tend to agree with: | |
| 26 | ||
| 27 | 1. Clutter is costly. | |
| 28 | - Digital minimalists recognize that cluttering their time and | |
| 29 | attention with too many devices, apps, and services creates an | |
| 30 | overall negative cost that can swamp the small benefits that | |
| 31 | each individual item provides in isolation. | |
| 32 | 2. Optimization is important. | |
| 33 | - Digital minimalists believe that deciding a particular | |
| 34 | technology supports something they value is only the first step. | |
| 35 | To truly extract its full potential benefit, it's necessary to | |
| 36 | think carefully about how they'll use the technology. | |
| 37 | 3. Intentionality is satisfying. | |
| 38 | - Digital minimalists derive significant satisfaction from their | |
| 39 | general commitment to being more intentional about how they | |
| 40 | engage with new technologies. This source of satisfaction is | |
| 41 | independent of the specific decisions they make and is one of | |
| 42 | the biggest reasons that minimalism tends to be immensely | |
| 43 | meaningful to its practitioners. | |
| 44 | ||
| 45 | ## Taking Action | |
| 46 | ||
| 47 | In order to put the logic into practice, I've created a few new habits | |
| 48 | and continued performing old habits that are working well: | |
| 49 | ||
| 50 | ### Using Devices With Intention | |
| 51 | ||
| 52 | - I already rarely use "social media", mostly limited to forums such | |
| 53 | as Hacker News and Tildes, so I've just tweaked my behavior to stop | |
| 54 | looking for content in those places when I'm bored. | |
| 55 | - Use devices with intention. Each time I pick up a digital device, | |
| 56 | there should be an intention to use the device to improve my current | |
| 57 | situation. No more endless scrolling or searching for something to | |
| 58 | interest me. | |
| 59 | ||
| 60 | ### Prevent Distractions | |
| 61 | ||
| 62 | - Disable (most) notifications on all devices. I spent 15-30 minutes | |
| 63 | going through the notifications on my phone, watch, and computer to | |
| 64 | ensure that only a select few apps have the ability to interrupt me: | |
| 65 | Calendar, Messages, Phone, Reminders, & Signal. | |
| 66 | - Disable badges for any apps except the ones mentioned in the bullet | |
| 67 | above. | |
| 68 | - Set-up focus profiles across devices so that I can enable different | |
| 69 | modes, such as Personal when I only want to see notifications from | |
| 70 | people I care about or Do Not Disturb, where absolutely nothing can | |
| 71 | interrupt me. | |
| 72 | - Clean up my home screens. This one was quite easy as I already | |
| 73 | maintain a minimalist set-up, but I went extreme by limiting my | |
| 74 | phone to just eight apps on the home screen and four in the dock. If | |
| 75 | I need another app, I'll have to search or use the app library. | |
| 76 | - Remove the work profile from my phone. This was a tough decision as | |
| 77 | having my work profile on my device definitely makes my life easier | |
| 78 | at times, but it also has quite a negative effect when I'm "always | |
| 79 | online" and can see the notifications and team activity 24/7. I | |
| 80 | believe creating a distinct barrier between my work and personal | |
| 81 | devices will be beneficial in the end. | |
| 82 | ||
| 83 | ### Creating Alternative Activities | |
| 84 | ||
| 85 | This is the most difficult piece, as most of my hobbies and interests | |
| 86 | lie in the digital world. However, I'm making a concerted effort to put | |
| 87 | devices down unless necessary and force myself to perform other | |
| 88 | activities in the physical world instead. | |
| 89 | ||
| 90 | I've started with a few basics that are always readily available to me: | |
| 91 | ||
| 92 | - Do a chore, such as organizing or cleaning. | |
| 93 | - Read a book, study a piece of art, etc. | |
| 94 | - Exercise or get outdoors. | |
| 95 | - Participate in a hobby, such as photography, birding, disc golf, | |
| 96 | etc. | |
| 97 | - Let yourself be bored and wander into creativity. | |
| 98 | ||
| 99 | ## Making Progress | |
| 100 | ||
| 101 | I'll be taking notes as I continue down this journey and hope to see | |
| 102 | positive trends. I've always been a minimalist in the physical world | |
| 103 | and it feels refreshing to filter out the clutter that has come to | |
| 104 | dominate my digital life over the years. | |
| 105 | ||
| 106 | I'm excited to see where this journey leads. | |
blog/2023-10-11-self-hosting-authelia.org → content/blog/2023-10-11-self-hosting-authelia.md renamed +127 −133
| @@ -1,59 +1,56 @@ | ||
| 1 | #+title: Self-Hosting Authelia | |
| 2 | #+date: 2023-10-11 | |
| 1 | +++ | |
| 2 | date = 2023-10-11 | |
| 3 | title = "Self-Hosting Authelia" | |
| 4 | description = "A guide to self-hosting the Authelia application on your own server." | |
| 5 | +++ | |
| 3 | 6 | |
| 4 | ** Overview | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: overview | |
| 7 | :END: | |
| 8 | [[https://www.authelia.com/][Authelia]] is an open-source authentication service that allows you to place a | |
| 9 | portal between end users on the internet and self-hosted services on your | |
| 10 | server. | |
| 7 | ## Overview | |
| 11 | 8 | |
| 12 | You can require one factor (username+password) or two factor authentication for | |
| 13 | any such user before allowing them to access a specific service on your domain. | |
| 9 | [Authelia](https://www.authelia.com/) is an open-source authentication | |
| 10 | service that allows you to place a portal between end users on the | |
| 11 | internet and self-hosted services on your server. | |
| 12 | ||
| 13 | You can require one factor (username+password) or two factor | |
| 14 | authentication for any such user before allowing them to access a | |
| 15 | specific service on your domain. | |
| 14 | 16 | |
| 15 | 17 | This guide will walk through a standard installation of Authelia for |
| 16 | =example.com=, using =auth.example.com= as Authelia's authentication domain and | |
| 17 | =teddit.example.com= as the website we want to protect behind the authentication | |
| 18 | portal. | |
| 19 | ||
| 20 | ** Prerequisites | |
| 21 | :PROPERTIES: | |
| 22 | :CUSTOM_ID: prerequisites | |
| 23 | :END: | |
| 18 | `example.com`, using `auth.example.com` as | |
| 19 | Authelia's authentication domain and `teddit.example.com` as | |
| 20 | the website we want to protect behind the authentication portal. | |
| 21 | ||
| 22 | ## Prerequisites | |
| 23 | ||
| 24 | 24 | This guide assumes you have the following already set-up: |
| 25 | 25 | |
| 26 | - A registered domain with DNS pointing to your server. | |
| 27 | - A subdomain for Authelia (=auth.example.com=) and a subdomain to | |
| 28 | protect via Authelia (=app.example.com=). | |
| 29 | - A working Nginx web server. | |
| 30 | - Docker and docker-compose installed. | |
| 31 | ||
| 32 | ** Installation | |
| 33 | :PROPERTIES: | |
| 34 | :CUSTOM_ID: installation | |
| 35 | :END: | |
| 36 | This guide will walk through each installation step one-by-one, starting with | |
| 37 | the container and finishing by cleaning up external access via an Nginx reverse | |
| 38 | proxy. | |
| 39 | ||
| 40 | *** Docker-Compose | |
| 41 | :PROPERTIES: | |
| 42 | :CUSTOM_ID: docker-compose | |
| 43 | :END: | |
| 26 | - A registered domain with DNS pointing to your server. | |
| 27 | - A subdomain for Authelia (`auth.example.com`) and a | |
| 28 | subdomain to protect via Authelia (`app.example.com`). | |
| 29 | - A working Nginx web server. | |
| 30 | - Docker and docker-compose installed. | |
| 31 | ||
| 32 | ## Installation | |
| 33 | ||
| 34 | This guide will walk through each installation step one-by-one, starting | |
| 35 | with the container and finishing by cleaning up external access via an | |
| 36 | Nginx reverse proxy. | |
| 37 | ||
| 38 | ### Docker-Compose | |
| 39 | ||
| 44 | 40 | To start, create a directory for Authelia and create a |
| 45 | =docker-compose.yml= file. | |
| 41 | `docker-compose.yml` file. | |
| 46 | 42 | |
| 47 | #+begin_src sh | |
| 43 | ```sh | |
| 48 | 44 | mkdir ~/authelia |
| 49 | 45 | nano ~/authelia/docker-compose.yml |
| 50 | #+end_src | |
| 46 | ``` | |
| 51 | 47 | |
| 52 | Within this file, paste the following content. If you prefer a different local | |
| 53 | port, modify the port on the left side of the colon on the =9091:9091= line. Be | |
| 54 | sure to modify the =TZ= variable to your timezone. | |
| 48 | Within this file, paste the following content. If you prefer a different | |
| 49 | local port, modify the port on the left side of the colon on the | |
| 50 | `9091:9091` line. Be sure to modify the `TZ` | |
| 51 | variable to your timezone. | |
| 55 | 52 | |
| 56 | #+begin_src yml | |
| 53 | ```yml | |
| 57 | 54 | version: '3.3' |
| 58 | 55 | |
| 59 | 56 | services: |
| @@ -66,52 +63,55 @@ services: | ||
| 66 | 63 | - 9091:9091 |
| 67 | 64 | environment: |
| 68 | 65 | - TZ=America/Chicago |
| 69 | #+end_src | |
| 66 | ``` | |
| 70 | 67 | |
| 71 | 68 | Start the container with docker-compose: |
| 72 | 69 | |
| 73 | #+begin_src sh | |
| 70 | ```sh | |
| 74 | 71 | sudo docker-compose up -d |
| 75 | #+end_src | |
| 72 | ``` | |
| 73 | ||
| 74 | After the first start, the container will automatically exit and require | |
| 75 | you to modify the app's configuration files before continuing. Read on | |
| 76 | to learn more. | |
| 76 | 77 | |
| 77 | After the first start, the container will automatically exit and require you to | |
| 78 | modify the app's configuration files before continuing. Read on to learn more. | |
| 78 | ### Authelia Configuration | |
| 79 | 79 | |
| 80 | *** Authelia Configuration | |
| 81 | :PROPERTIES: | |
| 82 | :CUSTOM_ID: authelia-configuration | |
| 83 | :END: | |
| 84 | To configure Authelia before we restart the container, we need to open the | |
| 85 | =config= directory and modify the files. Start by editing the | |
| 86 | =configuration.yml= file, where all of Authelia's settings are stored. | |
| 80 | To configure Authelia before we restart the container, we need to open | |
| 81 | the `config` directory and modify the files. Start by editing | |
| 82 | the `configuration.yml` file, where all of Authelia's | |
| 83 | settings are stored. | |
| 87 | 84 | |
| 88 | My personal preference is to copy the original configuration file to a backup | |
| 89 | file and edit a fresh copy. | |
| 85 | My personal preference is to copy the original configuration file to a | |
| 86 | backup file and edit a fresh copy. | |
| 90 | 87 | |
| 91 | #+begin_src sh | |
| 88 | ```sh | |
| 92 | 89 | sudo cp ~/authelia/config/configuration.yml ~/authelia/config/configuration.yml.bk |
| 93 | 90 | sudo nano ~/authelia/config/configuration.yml |
| 94 | #+end_src | |
| 91 | ``` | |
| 95 | 92 | |
| 96 | Within the blank =configuration.yml= file, paste the following information. You | |
| 97 | will need to make quite a few updates, so be sure to read each line carefully | |
| 98 | and modify as necessary. | |
| 93 | Within the blank `configuration.yml` file, paste the | |
| 94 | following information. You will need to make quite a few updates, so be | |
| 95 | sure to read each line carefully and modify as necessary. | |
| 99 | 96 | |
| 100 | 97 | The major required changes are: |
| 101 | 98 | |
| 102 | - Any instances of =example.com= should be replaced by your domain. | |
| 103 | - =jwt_secret= - Use the =pwgen 40 1= command to generate a secret for yourself. | |
| 104 | - =access_control= - Set the Authelia domain to bypass here, as well as any | |
| 105 | subdomains you want to protect. | |
| 106 | - =session= > =secret= - Use the =pwgen 40 1= command to generate a secret for | |
| 107 | yourself. | |
| 108 | - =regulation= - Set the variables here to restrict login attempts and bans. | |
| 109 | - =storage= > =encryption_key= - Use the =pwgen 40 1= command to generate a | |
| 110 | secret for yourself. | |
| 111 | - =smtp= - If you have access to an SMTP service, set up the information here to | |
| 112 | active outgoing emails. | |
| 113 | ||
| 114 | #+begin_src yml | |
| 99 | - Any instances of `example.com` should be replaced by your | |
| 100 | domain. | |
| 101 | - `jwt_secret` - Use the `pwgen 40 1` command to | |
| 102 | generate a secret for yourself. | |
| 103 | - `access_control` - Set the Authelia domain to bypass | |
| 104 | here, as well as any subdomains you want to protect. | |
| 105 | - `session` > `secret` - Use the | |
| 106 | `pwgen 40 1` command to generate a secret for yourself. | |
| 107 | - `regulation` - Set the variables here to restrict login | |
| 108 | attempts and bans. | |
| 109 | - `storage` > `encryption_key` - Use the | |
| 110 | `pwgen 40 1` command to generate a secret for yourself. | |
| 111 | - `smtp` - If you have access to an SMTP service, set up | |
| 112 | the information here to active outgoing emails. | |
| 113 | ||
| 114 | ```yml | |
| 115 | 115 | # yamllint disable rule:comments-indentation |
| 116 | 116 | --- |
| 117 | 117 | ############################################################################### |
| @@ -200,27 +200,26 @@ notifier: | ||
| 200 | 200 | skip_verify: false |
| 201 | 201 | minimum_version: TLS1.2 |
| 202 | 202 | ... |
| 203 | #+end_src | |
| 203 | ``` | |
| 204 | ||
| 205 | ### Authelia Users | |
| 204 | 206 | |
| 205 | *** Authelia Users | |
| 206 | :PROPERTIES: | |
| 207 | :CUSTOM_ID: authelia-users | |
| 208 | :END: | |
| 209 | 207 | Next, create the users file for authentication. |
| 210 | 208 | |
| 211 | #+begin_src sh | |
| 209 | ```sh | |
| 212 | 210 | sudo nano ~/authelia/config/users_database.yml |
| 213 | #+end_src | |
| 211 | ``` | |
| 214 | 212 | |
| 215 | Within the file, you will need to create an entry for each user that needs | |
| 216 | access to Authelia. The =my_username= entry will be the username used on the | |
| 217 | login page. | |
| 213 | Within the file, you will need to create an entry for each user that | |
| 214 | needs access to Authelia. The `my_username` entry will be the | |
| 215 | username used on the login page. | |
| 218 | 216 | |
| 219 | To generate the password, go to [[https://argon2.online][Argon2 Hash Generator]], generate a random salt, | |
| 220 | and make sure the rest of the settings match the =authentication_backend= | |
| 221 | section of =configuration.yml= file. | |
| 217 | To generate the password, go to [Argon2 Hash | |
| 218 | Generator](https://argon2.online), generate a random salt, and make sure | |
| 219 | the rest of the settings match the `authentication_backend` | |
| 220 | section of `configuration.yml` file. | |
| 222 | 221 | |
| 223 | #+begin_src yml | |
| 222 | ```yaml | |
| 224 | 223 | users: |
| 225 | 224 | my_username: |
| 226 | 225 | displayname: "My User" |
| @@ -231,33 +230,32 @@ users: | ||
| 231 | 230 | groups: |
| 232 | 231 | - admins |
| 233 | 232 | - dev |
| 234 | #+end_src | |
| 233 | ``` | |
| 235 | 234 | |
| 236 | 235 | Once the app is configured, restart the container from scratch. |
| 237 | 236 | |
| 238 | #+begin_src sh | |
| 237 | ```sh | |
| 239 | 238 | cd ~/authelia |
| 240 | 239 | sudo docker-compose down && sudo docker-compose up -d |
| 241 | #+end_src | |
| 240 | ``` | |
| 242 | 241 | |
| 243 | *** Nginx: Authelia Domain | |
| 244 | :PROPERTIES: | |
| 245 | :CUSTOM_ID: nginx-authelia-domain | |
| 246 | :END: | |
| 247 | Once the container is running and configured, the final step is to configure | |
| 248 | external access to the server via Nginx reverse proxy. | |
| 242 | ### Nginx: Authelia Domain | |
| 243 | ||
| 244 | Once the container is running and configured, the final step is to | |
| 245 | configure external access to the server via Nginx reverse proxy. | |
| 249 | 246 | |
| 250 | 247 | Start by creating the Authelia domain. |
| 251 | 248 | |
| 252 | #+begin_src sh | |
| 249 | ```sh | |
| 253 | 250 | sudo nano /etc/nginx/sites-available/auth |
| 254 | #+end_src | |
| 251 | ``` | |
| 255 | 252 | |
| 256 | 253 | Within this file, paste the following information and be sure to update |
| 257 | =example.com= to your domain. Make sure the =$upstream_authelia= variable | |
| 258 | matches the location of your Authelia container. | |
| 254 | `example.com` to your domain. Make sure the | |
| 255 | `$upstream_authelia` variable matches the location of your | |
| 256 | Authelia container. | |
| 259 | 257 | |
| 260 | #+begin_src conf | |
| 258 | ```conf | |
| 261 | 259 | server { |
| 262 | 260 | if ($host ~ ^[^.]+\.example\.com$) { |
| 263 | 261 | return 301 https://$host$request_uri; |
| @@ -309,32 +307,30 @@ server { | ||
| 309 | 307 | } |
| 310 | 308 | |
| 311 | 309 | } |
| 312 | #+end_src | |
| 310 | ``` | |
| 313 | 311 | |
| 314 | Next, symlink the file and restart Nginx. If there are errors, be sure to | |
| 315 | resolve those before moving on. | |
| 312 | Next, symlink the file and restart Nginx. If there are errors, be sure | |
| 313 | to resolve those before moving on. | |
| 316 | 314 | |
| 317 | #+begin_src sh | |
| 315 | ```sh | |
| 318 | 316 | sudo ln -s /etc/nginx/sites-available/auth /etc/nginx/sites-enabled/auth |
| 319 | 317 | sudo systemctl restart nginx.service |
| 320 | #+end_src | |
| 318 | ``` | |
| 319 | ||
| 320 | ### Nginx: Protected Domain(s) | |
| 321 | 321 | |
| 322 | *** Nginx: Protected Domain(s) | |
| 323 | :PROPERTIES: | |
| 324 | :CUSTOM_ID: nginx-protected-domains | |
| 325 | :END: | |
| 326 | Now that Authelia is accessible externally, you need to configure the domain you | |
| 327 | intend to protect with Authelia. In this example, I'm protecting | |
| 328 | =teddit.example.com=. | |
| 322 | Now that Authelia is accessible externally, you need to configure the | |
| 323 | domain you intend to protect with Authelia. In this example, I'm | |
| 324 | protecting `teddit.example.com`. | |
| 329 | 325 | |
| 330 | 326 | Similar to the process above, paste the content and update the relevant |
| 331 | 327 | variables. |
| 332 | 328 | |
| 333 | #+begin_src sh | |
| 329 | ```sh | |
| 334 | 330 | sudo nano /etc/nginx/sites-available/teddit |
| 335 | #+end_src | |
| 331 | ``` | |
| 336 | 332 | |
| 337 | #+begin_src conf | |
| 333 | ```conf | |
| 338 | 334 | server { |
| 339 | 335 | if ($host ~ ^[^.]+\.example\.com$) { |
| 340 | 336 | return 301 https://$host$request_uri; |
| @@ -436,28 +432,26 @@ server { | ||
| 436 | 432 | proxy_buffers 64 256k; |
| 437 | 433 | } |
| 438 | 434 | } |
| 439 | #+end_src | |
| 435 | ``` | |
| 440 | 436 | |
| 441 | 437 | Same as before, symlink the file and restart Nginx. |
| 442 | 438 | |
| 443 | #+begin_src sh | |
| 439 | ```sh | |
| 444 | 440 | sudo ln -s /etc/nginx/sites-available/teddit /etc/nginx/sites-enabled/teddit |
| 445 | 441 | sudo systemctl restart nginx.service |
| 446 | #+end_src | |
| 442 | ``` | |
| 443 | ||
| 444 | ## Results | |
| 447 | 445 | |
| 448 | ** Results | |
| 449 | :PROPERTIES: | |
| 450 | :CUSTOM_ID: results | |
| 451 | :END: | |
| 452 | 446 | When visiting the protected domain, you will now be redirected to your |
| 453 | 447 | authentication domain and presented with the Authelia login portal. |
| 454 | 448 | |
| 455 | #+caption: Authelia Portal | |
| 456 | [[https://img.cleberg.net/blog/20231010-authelia/authelia_portal.png]] | |
| 449 |  | |
| 457 | 451 | |
| 458 | Once you've successfully authenticated, you can visit your authentication domain | |
| 459 | directly and see that you're currently authenticated to any domain protected by | |
| 460 | Authelia. | |
| 452 | Once you've successfully authenticated, you can visit your | |
| 453 | authentication domain directly and see that you're currently | |
| 454 | authenticated to any domain protected by Authelia. | |
| 461 | 455 | |
| 462 | #+caption: Authelia Success | |
| 463 | [[https://img.cleberg.net/blog/20231010-authelia/authelia_success.png]] | |
| 456 |  | |
blog/2023-10-15-alpine-ssh-hardening.org → content/blog/2023-10-15-alpine-ssh-hardening.md renamed +40 −40
| @@ -1,74 +1,74 @@ | ||
| 1 | #+title: SSH Hardening for Alpine Linux | |
| 2 | #+date: 2023-10-16 | |
| 1 | +++ | |
| 2 | date = 2023-10-15 | |
| 3 | title = "SSH Hardening for Alpine Linux" | |
| 4 | description = "A quick guide to harden SSH configuration on Alpine." | |
| 5 | +++ | |
| 6 | ||
| 7 | ## Overview | |
| 3 | 8 | |
| 4 | ** Overview | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: overview | |
| 7 | :END: | |
| 8 | 9 | This guide follows the standard |
| 9 | [[https://www.ssh-audit.com/hardening_guides.html][ssh-audit]] hardening | |
| 10 | [ssh-audit](https://www.ssh-audit.com/hardening_guides.html) hardening | |
| 10 | 11 | guide, tweaked for Alpine Linux. |
| 11 | 12 | |
| 12 | ** Hardening Guide | |
| 13 | :PROPERTIES: | |
| 14 | :CUSTOM_ID: hardening-guide | |
| 15 | :END: | |
| 16 | These steps must be performed as root. You can try to use =doas= or | |
| 17 | =sudo=, but there may be issues. | |
| 13 | ## Hardening Guide | |
| 14 | ||
| 15 | These steps must be performed as root. You can try to use | |
| 16 | `doas` or `sudo`, but there may be issues. | |
| 18 | 17 | |
| 19 | 1. Re-generate the RSA and ED25519 keys | |
| 18 | 1. Re-generate the RSA and ED25519 keys | |
| 20 | 19 | |
| 21 | #+begin_src sh | |
| 20 | ```sh | |
| 22 | 21 | rm /etc/ssh/ssh_host_* |
| 23 | 22 | ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N "" |
| 24 | 23 | ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N "" |
| 25 | #+end_src | |
| 24 | ``` | |
| 26 | 25 | |
| 27 | 2. [@2] Remove small Diffie-Hellman moduli | |
| 26 | 2. Remove small Diffie-Hellman moduli | |
| 28 | 27 | |
| 29 | #+begin_src sh | |
| 28 | ```sh | |
| 30 | 29 | awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe |
| 31 | 30 | mv /etc/ssh/moduli.safe /etc/ssh/moduli |
| 32 | #+end_src | |
| 31 | ``` | |
| 33 | 32 | |
| 34 | 3. [@3] Enable the RSA and ED25519 HostKey directives in the | |
| 35 | /etc/ssh/sshd_config file | |
| 33 | 3. Enable the RSA and ED25519 HostKey directives in the | |
| 34 | /etc/ssh/sshd~config~ file | |
| 36 | 35 | |
| 37 | #+begin_src sh | |
| 36 | ```sh | |
| 38 | 37 | sed -i 's/^\#HostKey \/etc\/ssh\/ssh_host_\(rsa\|ed25519\)_key$/HostKey \/etc\/ssh\/ssh_host_\1_key/g' /etc/ssh/sshd_config |
| 39 | #+end_src | |
| 38 | ``` | |
| 40 | 39 | |
| 41 | 4. [@4] Restrict supported key exchange, cipher, and MAC algorithms | |
| 40 | 4. Restrict supported key exchange, cipher, and MAC algorithms | |
| 42 | 41 | |
| 43 | #+begin_src sh | |
| 42 | ```sh | |
| 44 | 43 | echo -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 |
| 45 | #+end_src | |
| 44 | ``` | |
| 46 | 45 | |
| 47 | 5. [@5] Include the /etc/ssh/sshd_config.d directory | |
| 46 | 5. Include the /etc/ssh/sshd~config~.d directory | |
| 48 | 47 | |
| 49 | #+begin_src sh | |
| 48 | ```sh | |
| 50 | 49 | echo -e "Include /etc/ssh/sshd_config.d/*.conf" > /etc/ssh/sshd_config |
| 51 | #+end_src | |
| 50 | ``` | |
| 52 | 51 | |
| 53 | 6. [@6] Restart OpenSSH server | |
| 52 | 6. Restart OpenSSH server | |
| 54 | 53 | |
| 55 | #+begin_src sh | |
| 54 | ```sh | |
| 56 | 55 | rc-service sshd restart |
| 57 | #+end_src | |
| 56 | ``` | |
| 58 | 57 | |
| 59 | ** Testing SSH | |
| 60 | :PROPERTIES: | |
| 61 | :CUSTOM_ID: testing-ssh | |
| 62 | :END: | |
| 63 | You can test the results with the =ssh-audit= python script. | |
| 58 | ## Testing SSH | |
| 64 | 59 | |
| 65 | #+begin_src sh | |
| 60 | You can test the results with the `ssh-audit` python script. | |
| 61 | ||
| 62 | ```sh | |
| 66 | 63 | pip3 install ssh-audit |
| 67 | 64 | ssh-audit localhost |
| 68 | #+end_src | |
| 65 | ``` | |
| 69 | 66 | |
| 70 | 67 | If everything succeeded, the results will show as all green. If anything |
| 71 | 68 | is yellow, orange, or red, you may need to tweak additional settings. |
| 72 | 69 | |
| 70 | ```txt | |
| 73 | 71 | #+caption: ssh audit |
| 74 | [[https://img.cleberg.net/blog/20231015-ssh-hardening/ssh-audit.png%20%22ssh-audit%22]] | |
| 72 | ``` | |
| 73 | ||
| 74 |  | |
blog/2023-10-17-self-hosting-anonymousoverflow.org → content/blog/2023-10-17-self-hosting-anonymousoverflow.md renamed +38 −41
| @@ -1,37 +1,35 @@ | ||
| 1 | #+title: Self-Hosting AnonymousOverflow | |
| 2 | #+date: 2023-10-17 | |
| 1 | +++ | |
| 2 | date = 2023-10-17 | |
| 3 | title = "Self-Hosting AnonymousOverflow" | |
| 4 | description = "A guide to self-hosting the AnonymousOverflow application on your own server." | |
| 5 | +++ | |
| 3 | 6 | |
| 4 | ** Overview | |
| 5 | :PROPERTIES: | |
| 6 | :CUSTOM_ID: overview | |
| 7 | :END: | |
| 8 | I recently launched an instance of AnonymousOverflow at [[https://ao.cleberg.net][ao.cleberg.net]] and | |
| 9 | wanted to write a brief post on how easy it is to install with Docker Compose | |
| 10 | and Nginx. | |
| 7 | ## Overview | |
| 8 | ||
| 9 | I recently launched an instance of AnonymousOverflow at | |
| 10 | [ao.cleberg.net](https://ao.cleberg.net) and wanted to write a brief | |
| 11 | post on how easy it is to install with Docker Compose and Nginx. | |
| 11 | 12 | |
| 12 | 13 | This guide uses Ubuntu server, Docker Compose, and Nginx as a reverse |
| 13 | 14 | proxy. |
| 14 | 15 | |
| 15 | ** Installation | |
| 16 | :PROPERTIES: | |
| 17 | :CUSTOM_ID: installation | |
| 18 | :END: | |
| 19 | *** Docker Compose | |
| 20 | :PROPERTIES: | |
| 21 | :CUSTOM_ID: docker-compose | |
| 22 | :END: | |
| 16 | ## Installation | |
| 17 | ||
| 18 | ### Docker Compose | |
| 19 | ||
| 23 | 20 | To install AnonymousOverflow, start by creating a directory for the |
| 24 | application and create its =docker-compose.yml= file. | |
| 21 | application and create its `docker-compose.yml` file. | |
| 25 | 22 | |
| 26 | #+begin_src sh | |
| 23 | ```sh | |
| 27 | 24 | mkdir ~/anonymousoverflow && cd ~/anonymousoverflow |
| 28 | 25 | nano docker-compose.yml |
| 29 | #+end_src | |
| 26 | ``` | |
| 30 | 27 | |
| 31 | 28 | Within this file, paste the following information. Be sure to change the |
| 32 | =APP_URL=, =JWT_SIGNING_SECRET=, and =ports= to match your needs. | |
| 29 | `APP_URL`, `JWT_SIGNING_SECRET`, and | |
| 30 | `ports` to match your needs. | |
| 33 | 31 | |
| 34 | #+begin_src yaml | |
| 32 | ```yaml | |
| 35 | 33 | version: '3' |
| 36 | 34 | |
| 37 | 35 | services: |
| @@ -44,33 +42,32 @@ services: | ||
| 44 | 42 | ports: |
| 45 | 43 | - '9380:8080' |
| 46 | 44 | restart: 'always' |
| 47 | #+end_src | |
| 45 | ``` | |
| 48 | 46 | |
| 49 | 47 | Save and exit the file when complete. You can now launch the container |
| 50 | 48 | and access it via your local network. |
| 51 | 49 | |
| 52 | #+begin_src sh | |
| 50 | ```sh | |
| 53 | 51 | sudo docker-compose up -d |
| 54 | #+end_src | |
| 52 | ``` | |
| 53 | ||
| 54 | ### Nginx Reverse Proxy | |
| 55 | 55 | |
| 56 | *** Nginx Reverse Proxy | |
| 57 | :PROPERTIES: | |
| 58 | :CUSTOM_ID: nginx-reverse-proxy | |
| 59 | :END: | |
| 60 | 56 | If you want to access this service outside the local network, I |
| 61 | 57 | recommend using Nginx as a reverse proxy. |
| 62 | 58 | |
| 63 | 59 | Let's start by creating a configuration file. |
| 64 | 60 | |
| 65 | #+begin_src sh | |
| 61 | ```sh | |
| 66 | 62 | sudo nano /etc/nginx/sites-available/ao |
| 67 | #+end_src | |
| 63 | ``` | |
| 68 | 64 | |
| 69 | 65 | Within this file, paste the following content and repace |
| 70 | =ao.example.com= with your URL. You may need to update the SSL | |
| 71 | certificate statements if your certificates are in a different location. | |
| 66 | `ao.example.com` with your URL. You may need to update the | |
| 67 | SSL certificate statements if your certificates are in a different | |
| 68 | location. | |
| 72 | 69 | |
| 73 | #+begin_src conf | |
| 70 | ```conf | |
| 74 | 71 | server { |
| 75 | 72 | if ($host ~ ^[^.]+\.cleberg\.net$) { |
| 76 | 73 | return 301 https://$host$request_uri; |
| @@ -122,15 +119,15 @@ server { | ||
| 122 | 119 | proxy_buffers 64 256k; |
| 123 | 120 | } |
| 124 | 121 | } |
| 125 | #+end_src | |
| 122 | ``` | |
| 126 | 123 | |
| 127 | Save and exit the file when complete. On Ubuntu, you will need to symlink the | |
| 128 | configuration file before it will be recognized by Nginx. Once complete, simply | |
| 129 | restart the web server. | |
| 124 | Save and exit the file when complete. On Ubuntu, you will need to | |
| 125 | symlink the configuration file before it will be recognized by Nginx. | |
| 126 | Once complete, simply restart the web server. | |
| 130 | 127 | |
| 131 | #+begin_src sh | |
| 128 | ```sh | |
| 132 | 129 | sudo ln -s /etc/nginx/sites-available/ao /etc/nginx/sites-enabled/ao |
| 133 | 130 | sudo systemctl restart nginx.service |
| 134 | #+end_src | |
| 131 | ``` | |
| 135 | 132 | |
| 136 | The website will now be available publicly. Visit [[https://ao.cleberg.net][my instance]] for an example. | |
| 133 | The website will now be available publicly. Visit [my instance](https://ao.cleberg.net) for an example. | |
content/blog/2023-11-08-scli.md added +157
| @@ -0,0 +1,157 @@ | ||
| 1 | +++ | |
| 2 | date = 2023-11-08 | |
| 3 | title = "Installing scli on Alpine Linux (musl)" | |
| 4 | description = "Learn how to properly configure scli for musl-based systems." | |
| 5 | +++ | |
| 6 | ||
| 7 | [scli](https://github.com/isamert/scli) is a command-line tool that | |
| 8 | allows you to connect to your Signal messenger account. This program | |
| 9 | utilizes a two-pane display that shows you chats on the left and the | |
| 10 | focused conversation on the right. | |
| 11 | ||
| 12 | This guide will show you how to install `scli` and its | |
| 13 | dependencies on Alpine Linux, which requires some extra work due to | |
| 14 | musl. | |
| 15 | ||
| 16 | If you're using a non-musl system, you can likely following the | |
| 17 | `scli` README and download the packaged binaries for an | |
| 18 | easier installation process. | |
| 19 | ||
| 20 | ## Dependencies | |
| 21 | ||
| 22 | In order to use `scli`, you need a few dependencies: | |
| 23 | ||
| 24 | - `openjdk17-jre` - Used as a dependency for the | |
| 25 | `signal-cli` tool. Version may vary. | |
| 26 | - `signal-cli` - Used as the backbone of the | |
| 27 | `scli` tool. | |
| 28 | - `findutils` - Replaces the standard Busybox version of | |
| 29 | `xargs`. | |
| 30 | - `urwid` - A console user interface library for Python. | |
| 31 | - `urwid-readline` - For GNU emacs-like keybinds on the | |
| 32 | input line. | |
| 33 | - `qrencode` - Displays a QR code in the terminal to link | |
| 34 | the device using your phone. Not necessary if you're only linking | |
| 35 | on desktop and can copy/paste the connection URL. | |
| 36 | ||
| 37 | Let's start by installing the packages available via Alpine's | |
| 38 | repositories. Be sure to install the latest version of | |
| 39 | `openjdk`. If you run into Java-related issues, uninstall | |
| 40 | `openjdk` and install an older version. | |
| 41 | ||
| 42 | ```sh | |
| 43 | doas apk add openjdk17-jre findutils qrencode | |
| 44 | ``` | |
| 45 | ||
| 46 | Next, let's install `signal-cli`. Be sure to export the | |
| 47 | version of `signal-cli` that you want. I use version | |
| 48 | `0.12.4` below, but that may be outdated by the time you're | |
| 49 | reading this. | |
| 50 | ||
| 51 | ```sh | |
| 52 | export VERSION="0.12.4" | |
| 53 | wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}".tar.gz | |
| 54 | doas tar xf signal-cli-"${VERSION}".tar.gz -C /opt | |
| 55 | doas ln -sf /opt/signal-cli-${VERSION}"/bin/signal/cli /usr/local/bin | |
| 56 | ``` | |
| 57 | ||
| 58 | Finally, install the `urwid` packages using the Python | |
| 59 | packaging utility. | |
| 60 | ||
| 61 | ```sh | |
| 62 | pip3 install urwid urwid-readline | |
| 63 | ``` | |
| 64 | ||
| 65 | ## Installation | |
| 66 | ||
| 67 | Now that we have all of the dependencies we need, we can install | |
| 68 | `scli`. Start by simply cloning the repository. | |
| 69 | ||
| 70 | ```sh | |
| 71 | git clone https://github.com/isamert/scli | |
| 72 | ``` | |
| 73 | ||
| 74 | When I cloned this repository on 2023-11-08, I found a bug in the logic | |
| 75 | that required a fix. You must edit the `scli` file and | |
| 76 | replace the one instance of `RLIMIT_OFILE` with | |
| 77 | `RLIMIT_NOFILE`. | |
| 78 | ||
| 79 | ```sh | |
| 80 | cd scli | |
| 81 | nano scli | |
| 82 | ``` | |
| 83 | ||
| 84 | Once complete, you can move this program to anywhere on your | |
| 85 | `$PATH`. I chose the following directory. | |
| 86 | ||
| 87 | ```sh | |
| 88 | doas mv scli /usr/local/bin/scli | |
| 89 | ``` | |
| 90 | ||
| 91 | ## Initial Setup | |
| 92 | ||
| 93 | Now that everything is installed, we can login and configure the client. | |
| 94 | Start by generating a connection link. | |
| 95 | ||
| 96 | ```sh | |
| 97 | signal-cli link -n "YOUR-DEVICE-NICKNAME" | tee >(xargs -L 1 qrencode -t utf8) | |
| 98 | ``` | |
| 99 | ||
| 100 | This will generate a connection link and related QR code for you to use | |
| 101 | to link the devices together. Once complete, **wait patiently** for the | |
| 102 | connection process to finish. | |
| 103 | ||
| 104 | Once it completes, it will exit and return you to the prompt. From here, | |
| 105 | you need to perform an initial `receive` command to start | |
| 106 | things off. The `USERNAME` variable should be your phone | |
| 107 | number, such as `+15551237890`. | |
| 108 | ||
| 109 | ```sh | |
| 110 | signal-cli -u USERNAME receive | |
| 111 | ``` | |
| 112 | ||
| 113 | Also be sure to test the daemon to ensure it works properly. If no | |
| 114 | errors occur, it's working. If you run into errors because you're not | |
| 115 | running a DBUS session, see my notes below. | |
| 116 | ||
| 117 | ```sh | |
| 118 | signal-cli -u USERNAME daemon | |
| 119 | ``` | |
| 120 | ||
| 121 | Once the initial reception is complete, you are ready to use | |
| 122 | `scli`. | |
| 123 | ||
| 124 | This process will differ depending on your desktop environment (DE). If | |
| 125 | you are running a DE, you likely have a DBUS session running already and | |
| 126 | can simply launch the program. | |
| 127 | ||
| 128 | However, if you're like me and running your computer straight on the | |
| 129 | TTY without a DE, you'll need to start a DBUS session for this program. | |
| 130 | ||
| 131 | ```sh | |
| 132 | # If you're not running a DBUS session yet, you need to start one for scli | |
| 133 | dbus-run-session -- scli | |
| 134 | ||
| 135 | # OR - If you're already running a DBUS session, simply run scli | |
| 136 | scli | |
| 137 | ``` | |
| 138 | ||
| 139 | ## Configuration | |
| 140 | ||
| 141 | Lastly, there are a number of configuration options that you can pass | |
| 142 | via the command or in the `~/.config/sclirc` file. See the | |
| 143 | Github README for more information on configuration options. | |
| 144 | ||
| 145 | ```sh | |
| 146 | nano ~/.config/sclirc | |
| 147 | ``` | |
| 148 | ||
| 149 | ```conf | |
| 150 | # ~/.config/sclirc | |
| 151 | ||
| 152 | wrap-at = 80 | |
| 153 | enable-notifications = true | |
| 154 | ``` | |
| 155 | ||
| 156 | That's it! Following this guide, I have a functional `scli` | |
| 157 | program that successfully sends messages to my contacts and myself! | |
blog/2023-12-03-unifi-nextdns.org → content/blog/2023-12-03-unifi-nextdns.md renamed +66 −90
| @@ -1,128 +1,104 @@ | ||
| 1 | #+title: How to Install NextDNS on the Unifi Dream Machine | |
| 2 | #+date: 2023-12-03 | |
| 1 | +++ | |
| 2 | date = 2023-12-03 | |
| 3 | title = "How to Install NextDNS on the Unifi Dream Machine" | |
| 4 | description = "A guide to properly install the NextDNS client on the UDM Pro." | |
| 5 | +++ | |
| 3 | 6 | |
| 4 | * Overview | |
| 5 | :PROPERTIES: | |
| 6 | :ID: 711309AE-955D-4B2D-B716-CFD700079157 | |
| 7 | :PUBDATE: 2023-12-03 Sun 22:32 | |
| 8 | :END: | |
| 7 | # Overview | |
| 9 | 8 | |
| 10 | I recently installed NextDNS on my Unifi Dream Machine router using the | |
| 11 | [[https://github.com/nextdns/nextdns/wiki/UnifiOS][UnifiOS]] wiki page on | |
| 9 | I recently installed NextDNS on my Unifi Dream Machine router using the | |
| 10 | [UnifiOS](https://github.com/nextdns/nextdns/wiki/UnifiOS) wiki page on | |
| 12 | 11 | NextDNS's GitHub repository. |
| 13 | 12 | |
| 14 | As a result of this, I wanted to write down the process in case the wiki or | |
| 15 | installer ever gets lost. | |
| 13 | As a result of this, I wanted to write down the process in case the wiki | |
| 14 | or installer ever gets lost. | |
| 16 | 15 | |
| 17 | * Wiki | |
| 18 | :PROPERTIES: | |
| 19 | :ID: 8457B7A9-AE62-448D-B092-C04759F8D468 | |
| 20 | :PUBDATE: 2023-12-03 Sun 22:32 | |
| 21 | :END: | |
| 16 | # Wiki | |
| 22 | 17 | |
| 23 | The following is copied from the wiki page linked above, with one difference in | |
| 24 | the =ssh= command. | |
| 18 | The following is copied from the wiki page linked above, with one | |
| 19 | difference in the `ssh` command. | |
| 25 | 20 | |
| 26 | Install instructions for Unifi Dream Machine (UDM) standard and pro routers. | |
| 21 | Install instructions for Unifi Dream Machine (UDM) standard and pro | |
| 22 | routers. | |
| 27 | 23 | |
| 28 | ** Install | |
| 29 | :PROPERTIES: | |
| 30 | :ID: 907F595C-6D53-409D-AB97-2B830D09B62E | |
| 31 | :END: | |
| 24 | ## Install | |
| 32 | 25 | |
| 33 | 26 | Enable SSH: |
| 34 | 27 | |
| 35 | - Go to your unifi admin interface and select your device (not the controller | |
| 36 | settings, but the Dream Machine settings) | |
| 37 | - Click on "Settings" at the bottom of the page | |
| 38 | - Go to the "Advanced" section on the left pan | |
| 39 | - Enable SSH | |
| 40 | - Set a SSH password | |
| 28 | - Go to your unifi admin interface and select your device (not the | |
| 29 | controller settings, but the Dream Machine settings) | |
| 30 | - Click on "Settings" at the bottom of the page | |
| 31 | - Go to the "Advanced" section on the left pan | |
| 32 | - Enable SSH | |
| 33 | - Set a SSH password | |
| 41 | 34 | |
| 42 | Connect to your router using =ssh root@xxx.xxx.xxx.xxx= with the password you | |
| 43 | configured. | |
| 35 | Connect to your router using `ssh root@xxx.xxx.xxx.xxx` with | |
| 36 | the password you configured. | |
| 44 | 37 | |
| 45 | 38 | Run the following command and follow the instructions: |
| 46 | 39 | |
| 47 | #+begin_src sh | |
| 40 | ```sh | |
| 48 | 41 | sh -c 'sh -c "$(curl -sL https://nextdns.io/install)"' |
| 49 | #+end_src | |
| 42 | ``` | |
| 50 | 43 | |
| 51 | Note: Queries from the UDM itself won’t be routed to NextDNS nor encrypted due | |
| 52 | to current system limitation. All traffic from other devices on then network | |
| 53 | will. | |
| 44 | Note: Queries from the UDM itself won't be routed to NextDNS nor | |
| 45 | encrypted due to current system limitation. All traffic from other | |
| 46 | devices on then network will. | |
| 54 | 47 | |
| 55 | ** Upgrade | |
| 56 | :PROPERTIES: | |
| 57 | :ID: 3836C070-2C7C-456D-AB3B-15DDE60D2F99 | |
| 58 | :END: | |
| 48 | ## Upgrade | |
| 59 | 49 | |
| 60 | To upgrade to the last version, simply re-run the installer above. If a new | |
| 61 | version is available, the upgrade action will added to the list of possible | |
| 62 | actions. | |
| 50 | To upgrade to the last version, simply re-run the installer above. If a | |
| 51 | new version is available, the upgrade action will added to the list of | |
| 52 | possible actions. | |
| 63 | 53 | |
| 64 | ** Uninstall | |
| 65 | :PROPERTIES: | |
| 66 | :ID: DFD1BA2F-EC4E-4B5C-9CEA-098DFF0AD62F | |
| 67 | :END: | |
| 54 | ## Uninstall | |
| 68 | 55 | |
| 69 | To uninstall, re-run the installer above and select "Remove" in the menu. | |
| 56 | To uninstall, re-run the installer above and select "Remove" in the | |
| 57 | menu. | |
| 70 | 58 | |
| 71 | ** Troubleshooting | |
| 72 | :PROPERTIES: | |
| 73 | :ID: 0B0F4E70-FACC-4C7C-8EB9-55398AA8476F | |
| 74 | :END: | |
| 59 | ## Troubleshooting | |
| 75 | 60 | |
| 76 | If the installation fail, please the installer in debug mode and contact us at | |
| 77 | team@nextdns.io with the transcript of the installation: | |
| 61 | If the installation fail, please the installer in debug mode and contact | |
| 62 | us at team@nextdns.io with the transcript of the installation: | |
| 78 | 63 | |
| 79 | #+begin_src sh | |
| 64 | ```sh | |
| 80 | 65 | sh -c 'DEBUG=1 sh -c "$(curl -sL https://nextdns.io/install)"' |
| 81 | #+end_src | |
| 66 | ``` | |
| 82 | 67 | |
| 83 | *** Content Filtering Conflict | |
| 84 | :PROPERTIES: | |
| 85 | :ID: BCDD3267-4C04-45AC-BAAD-9D15F414ED4D | |
| 86 | :END: | |
| 68 | ### Content Filtering Conflict | |
| 87 | 69 | |
| 88 | NextDNS CLI and the UDM Content Filtering or the Ad Blocking features are | |
| 89 | incompatible. If you want to use NextDNS CLI, please make sure they are | |
| 90 | disabled. | |
| 70 | NextDNS CLI and the UDM Content Filtering or the Ad Blocking features | |
| 71 | are incompatible. If you want to use NextDNS CLI, please make sure they | |
| 72 | are disabled. | |
| 91 | 73 | |
| 92 | To disable Content Filtering, go to Settings > Network, then for each network, | |
| 93 | set the Content Filtering feature to None | |
| 74 | To disable Content Filtering, go to Settings > Network, then for each | |
| 75 | network, set the Content Filtering feature to None | |
| 94 | 76 | |
| 95 | To disable Ad Blocking, go to Settings > Application Firewall. In the General | |
| 96 | tab, uncheck the Ad Blocking checkbox. | |
| 77 | To disable Ad Blocking, go to Settings > Application Firewall. In the | |
| 78 | General tab, uncheck the Ad Blocking checkbox. | |
| 97 | 79 | |
| 98 | *** APT Error | |
| 99 | :PROPERTIES: | |
| 100 | :ID: 83244A13-7154-475A-8193-064A1B81D3AE | |
| 101 | :END: | |
| 80 | ### APT Error | |
| 102 | 81 | |
| 103 | 82 | If you get an apt error as follow: |
| 104 | 83 | |
| 105 | #+begin_src sh | |
| 84 | ```sh | |
| 106 | 85 | E: Failed to fetch http://security.debian.org/dists/stretch/updates/main/binary-arm64/Packages 404 Not Found [IP: 151.101.70.132 80] |
| 107 | #+end_src | |
| 86 | ``` | |
| 108 | 87 | |
| 109 | 88 | You may try to following: |
| 110 | 89 | |
| 111 | #+begin_src sh | |
| 90 | ```sh | |
| 112 | 91 | sed -i -e 's/deb.debian.org/archive.debian.org/g' \ |
| 113 | 92 | -e 's|security.debian.org|archive.debian.org/|g' \ |
| 114 | 93 | -e '/stretch-updates/d' /etc/apt/sources.list |
| 115 | #+end_src | |
| 94 | ``` | |
| 116 | 95 | |
| 117 | * install.sh | |
| 118 | :PROPERTIES: | |
| 119 | :ID: 91DE0DD0-8177-4594-B067-373ABF99C343 | |
| 120 | :PUBDATE: 2023-12-03 Sun 22:34 | |
| 121 | :END: | |
| 96 | # install.sh | |
| 122 | 97 | |
| 123 | Here are the contents of the =install.sh= file used above, as of 2023-12-03: | |
| 98 | Here are the contents of the `install.sh` file used above, as | |
| 99 | of 2023-12-03: | |
| 124 | 100 | |
| 125 | #+begin_src sh | |
| 101 | ```sh | |
| 126 | 102 | #!/bin/sh |
| 127 | 103 | |
| 128 | 104 | main() { |
| @@ -226,13 +202,13 @@ uninstall() { | ||
| 226 | 202 | precheck() { |
| 227 | 203 | if [ -e "/data/unifi" ] && [ -f "/run/dnsfilter/dnsfilter" ]; then |
| 228 | 204 | log_warn "UDM Content Filtering and/or Ad Blocking feature is enabled." |
| 229 | log_warn "Please disable it to use NextDNS." | |
| 205 | log_warn "Please disable it to use NextDNS." | |
| 230 | 206 | log_warn "" |
| 231 | 207 | log_warn " To disable Content Filtering, go to Settings > Network." |
| 232 | 208 | log_warn " For each network, set the Content Filtering feature to None." |
| 233 | log_warn "" | |
| 234 | log_warn " To disable Ad Blocking, go to Settings > Application Firewall" | |
| 235 | log_warn " In the General tab, uncheck the Ad Blocking checkbox." | |
| 209 | log_warn "" | |
| 210 | log_warn " To disable Ad Blocking, go to Settings > Application Firewall" | |
| 211 | log_warn " In the General tab, uncheck the Ad Blocking checkbox." | |
| 236 | 212 | log_warn "" |
| 237 | 213 | while [ -f "/run/dnsfilter/dnsfilter" ]; do |
| 238 | 214 | sleep 1 |
| @@ -359,7 +335,7 @@ install_bin() { | ||
| 359 | 335 | esac |
| 360 | 336 | log_debug "Downloading $url" |
| 361 | 337 | asroot mkdir -p "$(dirname "$bin_path")" && |
| 362 | curl -sL "$url" | asroot sh -c "tar Ozxf - nextdns > \"$bin_path\"" && | |
| 338 | curl -sL "$url" | asroot sh -c "tar Ozxf - nextdns > "$bin_path"" && | |
| 363 | 339 | asroot chmod 755 "$bin_path" |
| 364 | 340 | } |
| 365 | 341 | |
| @@ -1163,9 +1139,9 @@ bin_location() { | ||
| 1163 | 1139 | ;; |
| 1164 | 1140 | synology) |
| 1165 | 1141 | echo "/usr/local/bin/nextdns" |
| 1166 | ;; | |
| 1142 | ;; | |
| 1167 | 1143 | darwin) |
| 1168 | echo "$(brew --prefix 2>/dev/null || echo /usr/local)/bin/nextdns" | |
| 1144 | echo "$(brew --prefix 2>/dev/null || echo /usr/local)/bin/nextdns" | |
| 1169 | 1145 | ;; |
| 1170 | 1146 | asuswrt-merlin|ddwrt) |
| 1171 | 1147 | echo "/jffs/nextdns/nextdns" |
| @@ -1272,4 +1248,4 @@ openssl_get() { | ||
| 1272 | 1248 | |
| 1273 | 1249 | umask 0022 |
| 1274 | 1250 | main |
| 1275 | #+end_src | |
| 1251 | ``` | |
content/blog/_index.md added +4
| @@ -0,0 +1,4 @@ | ||
| 1 | +++ | |
| 2 | title = "Blog" | |
| 3 | sort_by = "date" | |
| 4 | +++ | |
| \ No newline at end of file | ||
content/salary/index.md added +54
| @@ -0,0 +1,54 @@ | ||
| 1 | +++ | |
| 2 | title = "Salary" | |
| 3 | description = "A summary of my salaries throughout my career." | |
| 4 | +++ | |
| 5 | ||
| 6 | # Salary Transparency | |
| 7 | ||
| 8 | The data below details the base salary information for each job I've | |
| 9 | held. This information is posted publicly to ensure others in my | |
| 10 | position have a solid reference point when determining if their current | |
| 11 | or proposed salary is appropriate. | |
| 12 | ||
| 13 | While sites like Glassdoor are locking salary data behind a paywall, | |
| 14 | LinkedIn is discontinuing LinkedIn Salary, and helpful websites like Big | |
| 15 | 4 Transparency are extremely rare, I wanted to provide my personal data | |
| 16 | publicly and freely to those who need it. | |
| 17 | ||
| 18 | I have seen what can happen when great employees don't know the market | |
| 19 | values for their skills and I happily help those in my teams, so I'm | |
| 20 | happy to extend this information to the online community. | |
| 21 | ||
| 22 | As a final note, there are numerous reasons that people in the same role | |
| 23 | are paid differently (expertise, years of experience, certifications, | |
| 24 | education, etc.) and that the data in this table should only be used as | |
| 25 | a single point of reference, not the whole story. | |
| 26 | ||
| 27 | # Salary Data | |
| 28 | ||
| 29 | Note: When in a role that gives periodic raises, I will create a new | |
| 30 | record with the new base salary in the table below. See the KPMG records | |
| 31 | for an example of a raise while in the same role. | |
| 32 | ||
| 33 | | Title | Company | Location | Start Date | End Date | Base Salary | | |
| 34 | |---------------------------------------|------------------------|----------------|------------|----------|-------------| | |
| 35 | | Senior Technology Assurance Associate | KPMG | Omaha, NE | 2023-10 | Current | \$116,700 | | |
| 36 | | Senior Technology Assurance Associate | KPMG | Omaha, NE | 2022-06 | 2023-10 | \$110,000 | | |
| 37 | | Senior Technology Risk Consultant | Ernst & Young | Des Moines, IA | 2021-09 | 2022-06 | \$89,500 | | |
| 38 | | Senior IT Auditor | Ameritas | Lincoln, NE | 2021-05 | 2021-09 | \$72,000 | | |
| 39 | | IT Auditor | Ameritas | Lincoln, NE | 2020-04 | 2021-05 | \$65,000 | | |
| 40 | | IS Auditor II | Nelnet | Lincoln, NE | 2019-12 | 2020-04 | \$58,000 | | |
| 41 | | IS Auditor I | Nelnet | Lincoln, NE | 2019-06 | 2019-12 | \$20/hour | | |
| 42 | | Internal Audit Intern | Ameritas | Lincoln, NE | 2018-02 | 2019-06 | \$16/hour | | |
| 43 | | Teaching Assistant | University of Nebraska | Lincoln, NE | 2017-08 | 2018-05 | \$7/hour | | |
| 44 | | Community Management Intern | Walgreens | Lincoln, NE | 2017-06 | 2018-02 | \$14/hour | | |
| 45 | ||
| 46 | This page was inspired by [Xe](https://xeiaso.net/), and I'm quoting | |
| 47 | the following wording from them as I want to reiterate this piece: | |
| 48 | ||
| 49 | > Please consider publishing your salary data like this as well. By | |
| 50 | > open, voluntary transparency we can help to end stigmas around | |
| 51 | > discussing pay and help ensure that the next generations of people in | |
| 52 | > tech are treated fairly. Stigmas thrive in darkness but die in the | |
| 53 | > light of day. You can help end the stigma by playing your cards out in | |
| 54 | > the open like this. | |
content/services/index.md added +17
| @@ -0,0 +1,17 @@ | ||
| 1 | +++ | |
| 2 | title = "Services" | |
| 3 | description = "Publicly-available services hosted on cleberg.net." | |
| 4 | +++ | |
| 5 | ||
| 6 | - [AnonymousOverflow](https://ao.cleberg.net) - A StackOverflow proxy | |
| 7 | - [CyberChef](https://cyberchef.cleberg.net) - The Cyber Swiss Army Knife | |
| 8 | - [FileArchive](https://files.cleberg.net) - An interesting file archive | |
| 9 | - [FlashPaper](https://paste.cleberg.net) - One-time encrypted password/secret sharing | |
| 10 | - [GotHub](https://gh.cleberg.net) - An alternative front-end for GitHub | |
| 11 | - [ifconfig.php](https://ip.cleberg.net) - A \"whatsmyip\" tool | |
| 12 | - [Invidious](https://invidious.cleberg.net) - A YouTube proxy | |
| 13 | - [Nitter](https://nitter.cleberg.net) - A Twitter proxy | |
| 14 | - [Office](https://office.cleberg.net) - The world\'s smallest office suite | |
| 15 | - [SearXNG](https://search.cleberg.net) - A privacy-respecting, open metasearch engine | |
| 16 | ||
| 17 | See the [git log](https://git.cleberg.net/?p=cleberg.net.git;a=history;f=services/index.org;h=71ed6bcae7e52a8103f980f1821ca291ef3010ba;hb=HEAD) for this page if you want to see changes that have been made. | |
wiki/index.org → content/wiki/index.md renamed +48 −50
| @@ -1,79 +1,80 @@ | ||
| 1 | #+title: Wiki | |
| 1 | +++ | |
| 2 | title = "Wiki" | |
| 3 | description = "An informal wiki of sorts." | |
| 4 | +++ | |
| 2 | 5 | |
| 3 | 6 | An informal wiki of sorts. |
| 4 | 7 | |
| 5 | * Digital Garden | |
| 8 | # Digital Garden | |
| 6 | 9 | |
| 7 | #+BEGIN_QUOTE | |
| 8 | At times, wilderness is exactly what readers want: a rich collection of | |
| 9 | resources and links. At times, rigid formality suits readers perfectly, | |
| 10 | providing precisely the information they want, no more and no less. Indeed, | |
| 11 | individual hypertexts and Web sites may contain sections that tend toward | |
| 12 | each extreme. | |
| 13 | #+END_QUOTE | |
| 10 | > At times, wilderness is exactly what readers want: a rich collection | |
| 11 | > of resources and links. At times, rigid formality suits readers | |
| 12 | > perfectly, providing precisely the information they want, no more and | |
| 13 | > no less. Indeed, individual hypertexts and Web sites may contain | |
| 14 | > sections that tend toward each extreme. | |
| 14 | 15 | |
| 15 | #+BEGIN_QUOTE | |
| 16 | Often, however, designers should strive for the comfort, interest, and | |
| 17 | habitability of parks and gardens: places that invite visitors to remain, and | |
| 18 | that are designed to engage and delight them, to invite them to linger, to | |
| 19 | explore, and to reflect. | |
| 20 | #+END_QUOTE | |
| 16 | > Often, however, designers should strive for the comfort, interest, and | |
| 17 | > habitability of parks and gardens: places that invite visitors to | |
| 18 | > remain, and that are designed to engage and delight them, to invite | |
| 19 | > them to linger, to explore, and to reflect. | |
| 21 | 20 | |
| 22 | [[https://www.eastgate.com/garden/][Hypertext Garden]] | |
| 21 | [Hypertext Garden](https://www.eastgate.com/garden/) | |
| 23 | 22 | |
| 24 | * Git | |
| 23 | # Git | |
| 25 | 24 | |
| 26 | I want to get rid of all local modifications and go back to the working tree | |
| 27 | of the most recent commit: | |
| 25 | I want to get rid of all local modifications and go back to the working | |
| 26 | tree of the most recent commit: | |
| 28 | 27 | |
| 29 | #+BEGIN_SRC sh | |
| 28 | ```sh | |
| 30 | 29 | git restore . |
| 31 | #+END_SRC | |
| 30 | ``` | |
| 32 | 31 | |
| 33 | 32 | Revert a specified commit: |
| 34 | 33 | |
| 35 | #+BEGIN_SRC sh | |
| 34 | ```sh | |
| 36 | 35 | git revert commit-id |
| 37 | #+END_SRC | |
| 36 | ``` | |
| 38 | 37 | |
| 39 | 38 | Reset the repository to a specific commit in the git log: |
| 40 | 39 | |
| 41 | #+BEGIN_SRC sh | |
| 40 | ```sh | |
| 42 | 41 | git reset --mixed commit-id |
| 43 | #+END_SRC | |
| 42 | ``` | |
| 44 | 43 | |
| 45 | I need to commit and push changes to a remote that has been changed since my | |
| 46 | most recent pull: | |
| 44 | I need to commit and push changes to a remote that has been changed | |
| 45 | since my most recent pull: | |
| 47 | 46 | |
| 48 | #+BEGIN_SRC sh | |
| 47 | ```sh | |
| 49 | 48 | git pull --rebase |
| 50 | #+END_SRC | |
| 49 | ``` | |
| 51 | 50 | |
| 52 | * Hardware | |
| 51 | # Hardware | |
| 53 | 52 | |
| 54 | ** Laptops | |
| 53 | ## Laptops | |
| 54 | ||
| 55 | ### macOS | |
| 55 | 56 | |
| 56 | *** macOS | |
| 57 | 57 | |
| 58 | 58 | | Category | Details | |
| 59 | | -------- | --------------- | | |
| 59 | 60 | | Model | Macbook Pro 16" | |
| 60 | 61 | | CPU | Apple M2 Pro | |
| 61 | 62 | | RAM | 16GB | |
| 62 | 63 | | Storage | 512GB SSD | |
| 63 | 64 | |
| 64 | *** Linux | |
| 65 | ### Linux | |
| 65 | 66 | |
| 66 | 67 | | Category | Details | |
| 67 | |----------|---------------------------------------------| | |
| 68 | | -------- | ------------------------------------------- | | |
| 68 | 69 | | Model | Lenovo ThinkPad E15 Gen 4, model 21ED0048US | |
| 69 | 70 | | CPU | AMD Ryzen 5 5625U with Radeon Graphics | |
| 70 | 71 | | RAM | 16 GB | |
| 71 | 72 | | Storage | 256 GB SSD | |
| 72 | 73 | |
| 73 | ** Servers | |
| 74 | ## Servers | |
| 74 | 75 | |
| 75 | 76 | | Category | Details | |
| 76 | |--------------------|----------------------------------------| | |
| 77 | | ------------------ | -------------------------------------- | | |
| 77 | 78 | | Case | Rosewill RSV-R4100U 4U | |
| 78 | 79 | | Motherboard | NZXT B550 | |
| 79 | 80 | | CPU | AMD Ryzen 7 5700G with Radeon Graphics | |
| @@ -82,7 +83,7 @@ git pull --rebase | ||
| 82 | 83 | | Storage (HDD Bay) | 48TB HDD | |
| 83 | 84 | | PSU | Corsair RM850 PSU | |
| 84 | 85 | |
| 85 | ** Networking Equipment | |
| 86 | ## Networking Equipment | |
| 86 | 87 | |
| 87 | 88 | - UDM-Pro |
| 88 | 89 | - USW-24-PoE |
| @@ -94,18 +95,18 @@ git pull --rebase | ||
| 94 | 95 | - UVC G4 Doorbell Pro |
| 95 | 96 | - UP Chime |
| 96 | 97 | - USW 24-Port Patch Panel |
| 97 | - USW Switch Lite 8 PoE | |
| 98 | - USW Switch Lite 8 PoE | |
| 98 | 99 | |
| 99 | * Software | |
| 100 | # Software | |
| 100 | 101 | |
| 101 | ** Laptop | |
| 102 | ## Laptop | |
| 102 | 103 | |
| 103 | 104 | Alpine 3.18.2; no DE. |
| 104 | 105 | |
| 105 | I currently run my Alpine laptop via the default login shell - no desktop | |
| 106 | environment. From here, I use a mix of byobu and emacs to split my screen into | |
| 107 | tabs and panes. All programs run through the shell and do not use visual | |
| 108 | libraries such as X or Wayland. | |
| 106 | I currently run my Alpine laptop via the default login shell - no | |
| 107 | desktop environment. From here, I use a mix of byobu and emacs to split | |
| 108 | my screen into tabs and panes. All programs run through the shell and do | |
| 109 | not use visual libraries such as X or Wayland. | |
| 109 | 110 | |
| 110 | 111 | I have Sway installed and configured, but only launch it when I must. |
| 111 | 112 | |
| @@ -128,17 +129,14 @@ I have Sway installed and configured, but only launch it when I must. | ||
| 128 | 129 | - zola |
| 129 | 130 | - zsh |
| 130 | 131 | |
| 131 | ** Server | |
| 132 | ## Server | |
| 132 | 133 | |
| 133 | 134 | Ubuntu 22.04.1; no DE. |
| 134 | 135 | |
| 135 | See my services page for a list of the publicly-available services running on | |
| 136 | this server. | |
| 136 | See my services page for a list of the publicly-available services | |
| 137 | running on this server. | |
| 137 | 138 | |
| 138 | 139 | - certbot |
| 139 | 140 | - [docker, docker-compose] |
| 140 | 141 | - nginx |
| 141 | 142 | - zsh |
| 142 | ||
| 143 | ||
| 144 | ||
elisp/ox-rss.el deleted −434
| @@ -1,434 +0,0 @@ | ||
| 1 | ;;; ox-rss.el --- RSS 2.0 Back-End for Org Export Engine | |
| 2 | ||
| 3 | ;; Copyright (C) 2013-2021 Free Software Foundation, Inc. | |
| 4 | ||
| 5 | ;; Author: Bastien Guerry <bzg@gnu.org> | |
| 6 | ;; Maintainer: Benedict Wang <foss@bhw.name> | |
| 7 | ;; Version: 0.0.1 | |
| 8 | ;; Package-Requires: ((emacs "26.1") (org "9.3")) | |
| 9 | ;; Keywords: org, wp, blog, feed, rss | |
| 10 | ;; Homepage: https://github.com/benedicthw/ox-rss.git | |
| 11 | ||
| 12 | ;; This file is not part of GNU Emacs. | |
| 13 | ||
| 14 | ;; This program is free software: you can redistribute it and/or modify | |
| 15 | ;; it under the terms of the GNU General Public License as published by | |
| 16 | ;; the Free Software Foundation, either version 3 of the License, or | |
| 17 | ;; (at your option) any later version. | |
| 18 | ||
| 19 | ;; This program is distributed in the hope that it will be useful, | |
| 20 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 21 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 22 | ;; GNU General Public License for more details. | |
| 23 | ||
| 24 | ;; You should have received a copy of the GNU General Public License | |
| 25 | ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. | |
| 26 | ||
| 27 | ;;; Commentary: | |
| 28 | ||
| 29 | ;; This library implements an RSS 2.0 back-end for Org exporter, based | |
| 30 | ;; on the `html' back-end. | |
| 31 | ;; | |
| 32 | ;; It requires Emacs 24.1 at least. | |
| 33 | ;; | |
| 34 | ;; It provides two commands for export, depending on the desired output: | |
| 35 | ;; `org-rss-export-as-rss' (temporary buffer) and `org-rss-export-to-rss' | |
| 36 | ;; (as a ".xml" file). | |
| 37 | ;; | |
| 38 | ;; This backend understands three new option keywords: | |
| 39 | ;; | |
| 40 | ;; #+RSS_EXTENSION: xml | |
| 41 | ;; #+RSS_IMAGE_URL: https://myblog.org/mypicture.jpg | |
| 42 | ;; #+RSS_FEED_URL: https://myblog.org/feeds/blog.xml | |
| 43 | ;; | |
| 44 | ;; It uses #+HTML_LINK_HOME: to set the base url of the feed. | |
| 45 | ;; | |
| 46 | ;; Exporting an Org file to RSS modifies each top-level entry by adding a | |
| 47 | ;; PUBDATE property. If `org-rss-use-entry-url-as-guid', it will also add | |
| 48 | ;; an ID property, later used as the guid for the feed's item. | |
| 49 | ;; | |
| 50 | ;; The top-level headline is used as the title of each RSS item unless | |
| 51 | ;; an RSS_TITLE property is set on the headline. | |
| 52 | ;; | |
| 53 | ;; You typically want to use it within a publishing project like this: | |
| 54 | ;; | |
| 55 | ;; (add-to-list | |
| 56 | ;; 'org-publish-project-alist | |
| 57 | ;; '("homepage_rss" | |
| 58 | ;; :base-directory "~/myhomepage/" | |
| 59 | ;; :base-extension "org" | |
| 60 | ;; :rss-image-url "http://lumiere.ens.fr/~guerry/images/faces/15.png" | |
| 61 | ;; :html-link-home "http://lumiere.ens.fr/~guerry/" | |
| 62 | ;; :html-link-use-abs-url t | |
| 63 | ;; :rss-extension "xml" | |
| 64 | ;; :publishing-directory "/home/guerry/public_html/" | |
| 65 | ;; :publishing-function (org-rss-publish-to-rss) | |
| 66 | ;; :section-numbers nil | |
| 67 | ;; :exclude ".*" ;; To exclude all files... | |
| 68 | ;; :include ("index.org") ;; ... except index.org. | |
| 69 | ;; :table-of-contents nil)) | |
| 70 | ;; | |
| 71 | ;; ... then rsync /home/guerry/public_html/ with your server. | |
| 72 | ;; | |
| 73 | ;; By default, the permalink for a blog entry points to the headline. | |
| 74 | ;; You can specify a different one by using the :RSS_PERMALINK: | |
| 75 | ;; property within an entry. | |
| 76 | ||
| 77 | ;;; Code: | |
| 78 | ||
| 79 | (require 'ox-html) | |
| 80 | (declare-function url-encode-url "url-util" (url)) | |
| 81 | ||
| 82 | ;;; Variables and options | |
| 83 | ||
| 84 | (defgroup org-export-rss nil | |
| 85 | "Options specific to RSS export back-end." | |
| 86 | :tag "Org RSS" | |
| 87 | :group 'org-export | |
| 88 | :version "24.4" | |
| 89 | :package-version '(Org . "8.0")) | |
| 90 | ||
| 91 | (defcustom org-rss-image-url "https://orgmode.org/img/org-mode-unicorn-logo.png" | |
| 92 | "The URL of the image for the RSS feed." | |
| 93 | :group 'org-export-rss | |
| 94 | :type 'string) | |
| 95 | ||
| 96 | (defcustom org-rss-extension "xml" | |
| 97 | "File extension for the RSS 2.0 feed." | |
| 98 | :group 'org-export-rss | |
| 99 | :type 'string) | |
| 100 | ||
| 101 | (defcustom org-rss-categories 'from-tags | |
| 102 | "Where to extract items category information from. | |
| 103 | The default is to extract categories from the tags of the | |
| 104 | headlines. When set to another value, extract the category | |
| 105 | from the :CATEGORY: property of the entry." | |
| 106 | :group 'org-export-rss | |
| 107 | :type '(choice | |
| 108 | (const :tag "From tags" from-tags) | |
| 109 | (const :tag "From the category property" from-category))) | |
| 110 | ||
| 111 | (defcustom org-rss-use-entry-url-as-guid t | |
| 112 | "Use the URL for the <guid> metatag? | |
| 113 | When nil, Org will create ids using `org-icalendar-create-uid'." | |
| 114 | :group 'org-export-rss | |
| 115 | :type 'boolean) | |
| 116 | ||
| 117 | ;;; Define backend | |
| 118 | ||
| 119 | (org-export-define-derived-backend 'rss 'html | |
| 120 | :menu-entry | |
| 121 | '(?r "Export to RSS" | |
| 122 | ((?R "As RSS buffer" | |
| 123 | (lambda (a s v b) (org-rss-export-as-rss a s v))) | |
| 124 | (?r "As RSS file" (lambda (a s v b) (org-rss-export-to-rss a s v))) | |
| 125 | (?o "As RSS file and open" | |
| 126 | (lambda (a s v b) | |
| 127 | (if a (org-rss-export-to-rss t s v) | |
| 128 | (org-open-file (org-rss-export-to-rss nil s v))))))) | |
| 129 | :options-alist | |
| 130 | '((:description "DESCRIPTION" nil nil newline) | |
| 131 | (:keywords "KEYWORDS" nil nil space) | |
| 132 | (:with-toc nil nil nil) ;; Never include HTML's toc | |
| 133 | (:rss-extension "RSS_EXTENSION" nil org-rss-extension) | |
| 134 | (:rss-image-url "RSS_IMAGE_URL" nil org-rss-image-url) | |
| 135 | (:rss-feed-url "RSS_FEED_URL" nil nil t) | |
| 136 | (:rss-categories nil nil org-rss-categories)) | |
| 137 | :filters-alist '((:filter-final-output . org-rss-final-function)) | |
| 138 | :translate-alist '((headline . org-rss-headline) | |
| 139 | (comment . (lambda (&rest args) "")) | |
| 140 | (comment-block . (lambda (&rest args) "")) | |
| 141 | (timestamp . (lambda (&rest args) "")) | |
| 142 | (plain-text . org-rss-plain-text) | |
| 143 | (section . org-rss-section) | |
| 144 | (template . org-rss-template))) | |
| 145 | ||
| 146 | ;;; Export functions | |
| 147 | ||
| 148 | ;;;###autoload | |
| 149 | (defun org-rss-export-as-rss (&optional async subtreep visible-only) | |
| 150 | "Export current buffer to an RSS buffer. | |
| 151 | ||
| 152 | If narrowing is active in the current buffer, only export its | |
| 153 | narrowed part. | |
| 154 | ||
| 155 | If a region is active, export that region. | |
| 156 | ||
| 157 | A non-nil optional argument ASYNC means the process should happen | |
| 158 | asynchronously. The resulting buffer should be accessible | |
| 159 | through the `org-export-stack' interface. | |
| 160 | ||
| 161 | When optional argument SUBTREEP is non-nil, export the sub-tree | |
| 162 | at point, extracting information from the headline properties | |
| 163 | first. | |
| 164 | ||
| 165 | When optional argument VISIBLE-ONLY is non-nil, don't export | |
| 166 | contents of hidden elements. | |
| 167 | ||
| 168 | Export is done in a buffer named \"*Org RSS Export*\", which will | |
| 169 | be displayed when `org-export-show-temporary-export-buffer' is | |
| 170 | non-nil." | |
| 171 | (interactive) | |
| 172 | (let ((file (buffer-file-name (buffer-base-buffer)))) | |
| 173 | (org-icalendar-create-uid file 'warn-user) | |
| 174 | (org-rss-add-pubdate-property)) | |
| 175 | (org-export-to-buffer 'rss "*Org RSS Export*" | |
| 176 | async subtreep visible-only nil nil (lambda () (text-mode)))) | |
| 177 | ||
| 178 | ;;;###autoload | |
| 179 | (defun org-rss-export-to-rss (&optional async subtreep visible-only) | |
| 180 | "Export current buffer to an RSS file. | |
| 181 | ||
| 182 | If narrowing is active in the current buffer, only export its | |
| 183 | narrowed part. | |
| 184 | ||
| 185 | If a region is active, export that region. | |
| 186 | ||
| 187 | A non-nil optional argument ASYNC means the process should happen | |
| 188 | asynchronously. The resulting file should be accessible through | |
| 189 | the `org-export-stack' interface. | |
| 190 | ||
| 191 | When optional argument SUBTREEP is non-nil, export the sub-tree | |
| 192 | at point, extracting information from the headline properties | |
| 193 | first. | |
| 194 | ||
| 195 | When optional argument VISIBLE-ONLY is non-nil, don't export | |
| 196 | contents of hidden elements. | |
| 197 | ||
| 198 | Return output file's name." | |
| 199 | (interactive) | |
| 200 | (let ((file (buffer-file-name (buffer-base-buffer)))) | |
| 201 | (org-icalendar-create-uid file 'warn-user) | |
| 202 | (org-rss-add-pubdate-property)) | |
| 203 | (let ((outfile (org-export-output-file-name | |
| 204 | (concat "." org-rss-extension) subtreep))) | |
| 205 | (org-export-to-file 'rss outfile async subtreep visible-only))) | |
| 206 | ||
| 207 | ;;;###autoload | |
| 208 | (defun org-rss-publish-to-rss (plist filename pub-dir) | |
| 209 | "Publish an org file to RSS. | |
| 210 | ||
| 211 | FILENAME is the filename of the Org file to be published. PLIST | |
| 212 | is the property list for the given project. PUB-DIR is the | |
| 213 | publishing directory. | |
| 214 | ||
| 215 | Return output file name." | |
| 216 | (let ((bf (get-file-buffer filename))) | |
| 217 | (if bf | |
| 218 | (with-current-buffer bf | |
| 219 | (org-icalendar-create-uid filename 'warn-user) | |
| 220 | (org-rss-add-pubdate-property) | |
| 221 | (write-file filename)) | |
| 222 | (find-file filename) | |
| 223 | (org-icalendar-create-uid filename 'warn-user) | |
| 224 | (org-rss-add-pubdate-property) | |
| 225 | (write-file filename) (kill-buffer))) | |
| 226 | (org-publish-org-to | |
| 227 | 'rss filename (concat "." org-rss-extension) plist pub-dir)) | |
| 228 | ||
| 229 | ;;; Main transcoding functions | |
| 230 | ||
| 231 | (defun org-rss-headline (headline contents info) | |
| 232 | "Transcode HEADLINE element into RSS format. | |
| 233 | CONTENTS is the headline contents. INFO is a plist used as a | |
| 234 | communication channel." | |
| 235 | (if (> (org-export-get-relative-level headline info) 1) | |
| 236 | (org-export-data-with-backend headline 'html info) | |
| 237 | (unless (org-element-property :footnote-section-p headline) | |
| 238 | (let* ((email (org-export-data (plist-get info :email) info)) | |
| 239 | (author (and (plist-get info :with-author) | |
| 240 | (let ((auth (plist-get info :author))) | |
| 241 | (and auth (org-export-data auth info))))) | |
| 242 | (htmlext (plist-get info :html-extension)) | |
| 243 | (hl-number (org-export-get-headline-number headline info)) | |
| 244 | (hl-home (file-name-as-directory (plist-get info :html-link-home))) | |
| 245 | (hl-pdir (plist-get info :publishing-directory)) | |
| 246 | (hl-perm (org-element-property :RSS_PERMALINK headline)) | |
| 247 | (anchor | |
| 248 | (or (org-element-property :CUSTOM_ID headline) | |
| 249 | (org-export-get-reference headline info))) | |
| 250 | (category (org-rss-plain-text | |
| 251 | (or (org-element-property :CATEGORY headline) "") info)) | |
| 252 | (pubdate0 (org-element-property :PUBDATE headline)) | |
| 253 | (pubdate (let ((system-time-locale "C")) | |
| 254 | (if (and pubdate0 (not (string-empty-p pubdate0))) | |
| 255 | (format-time-string | |
| 256 | "%a, %d %b %Y %T %z" | |
| 257 | (org-time-string-to-time pubdate0))))) | |
| 258 | (title (org-rss-plain-text | |
| 259 | (or (org-element-property :RSS_TITLE headline) | |
| 260 | (replace-regexp-in-string | |
| 261 | org-link-bracket-re | |
| 262 | (lambda (m) (or (match-string 3 m) | |
| 263 | (match-string 1 m))) | |
| 264 | (org-element-property :raw-value headline))) info)) | |
| 265 | (publink | |
| 266 | (or (and hl-perm (concat (or hl-home hl-pdir) hl-perm)) | |
| 267 | (concat | |
| 268 | (or hl-home hl-pdir) | |
| 269 | (file-name-nondirectory | |
| 270 | (file-name-sans-extension | |
| 271 | (plist-get info :input-file))) "." htmlext "#" anchor))) | |
| 272 | (guid (if org-rss-use-entry-url-as-guid | |
| 273 | publink | |
| 274 | (org-rss-plain-text | |
| 275 | (or (org-element-property :ID headline) | |
| 276 | (org-element-property :CUSTOM_ID headline) | |
| 277 | publink) | |
| 278 | info)))) | |
| 279 | (if (not pubdate) "" ;; Skip entries with no PUBDATE prop | |
| 280 | (format | |
| 281 | (concat | |
| 282 | "<item>\n" | |
| 283 | "<title>%s</title>\n" | |
| 284 | "<link>%s</link>\n" | |
| 285 | "<author>%s (%s)</author>\n" | |
| 286 | "<guid isPermaLink=\"false\">%s</guid>\n" | |
| 287 | "<pubDate>%s</pubDate>\n" | |
| 288 | (org-rss-build-categories headline info) "\n" | |
| 289 | "<description><![CDATA[%s]]></description>\n" | |
| 290 | "</item>\n") | |
| 291 | title publink email author guid pubdate contents)))))) | |
| 292 | ||
| 293 | (defun org-rss-build-categories (headline info) | |
| 294 | "Build categories for the RSS item from INFO. | |
| 295 | Fallback to the HEADLINE :CATEGORY property." | |
| 296 | (if (eq (plist-get info :rss-categories) 'from-tags) | |
| 297 | (mapconcat | |
| 298 | (lambda (c) (format "<category><![CDATA[%s]]></category>" c)) | |
| 299 | (org-element-property :tags headline) | |
| 300 | "\n") | |
| 301 | (let ((c (org-element-property :CATEGORY headline))) | |
| 302 | (format "<category><![CDATA[%s]]></category>" c)))) | |
| 303 | ||
| 304 | (defun org-rss-template (contents info) | |
| 305 | "Return complete document string after RSS conversion. | |
| 306 | CONTENTS is the transcoded contents string. INFO is a plist used | |
| 307 | as a communication channel." | |
| 308 | (let ((style (plist-get info :rss-stylesheet))) | |
| 309 | (concat | |
| 310 | (format "<?xml version=\"1.0\" encoding=\"%s\"?>" | |
| 311 | (symbol-name org-html-coding-system)) | |
| 312 | (if style | |
| 313 | (format "\n<?xml-stylesheet type=\"text/xsl\" href=\"%s\"?>" | |
| 314 | style)) | |
| 315 | "\n<rss version=\"2.0\" | |
| 316 | xmlns:content=\"http://purl.org/rss/1.0/modules/content/\" | |
| 317 | xmlns:wfw=\"http://wellformedweb.org/CommentAPI/\" | |
| 318 | xmlns:dc=\"http://purl.org/dc/elements/1.1/\" | |
| 319 | xmlns:atom=\"http://www.w3.org/2005/Atom\" | |
| 320 | xmlns:sy=\"http://purl.org/rss/1.0/modules/syndication/\" | |
| 321 | xmlns:slash=\"http://purl.org/rss/1.0/modules/slash/\" | |
| 322 | xmlns:georss=\"http://www.georss.org/georss\" | |
| 323 | xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\" | |
| 324 | xmlns:media=\"http://search.yahoo.com/mrss/\">" | |
| 325 | "<channel>" | |
| 326 | (org-rss-build-channel-info info) "\n" | |
| 327 | contents | |
| 328 | "</channel>\n" | |
| 329 | "</rss>"))) | |
| 330 | ||
| 331 | (defun org-rss-build-channel-info (info) | |
| 332 | "Given plist INFO build the RSS channel information." | |
| 333 | (let* ((system-time-locale "C") | |
| 334 | (title (org-export-data (plist-get info :title) info)) | |
| 335 | (email (org-export-data (plist-get info :email) info)) | |
| 336 | (author (and (plist-get info :with-author) | |
| 337 | (let ((auth (plist-get info :author))) | |
| 338 | (and auth (org-export-data auth info))))) | |
| 339 | (date (format-time-string "%a, %d %b %Y %T %z")) ;; RFC 882 | |
| 340 | (description (org-export-data (plist-get info :description) info)) | |
| 341 | (lang (plist-get info :language)) | |
| 342 | (keywords (plist-get info :keywords)) | |
| 343 | (rssext (plist-get info :rss-extension)) | |
| 344 | (blogurl (or (plist-get info :html-link-home) | |
| 345 | (plist-get info :publishing-directory))) | |
| 346 | (image (url-encode-url (plist-get info :rss-image-url))) | |
| 347 | (ifile (plist-get info :input-file)) | |
| 348 | (publink | |
| 349 | (or (plist-get info :rss-feed-url) | |
| 350 | (concat (file-name-as-directory blogurl) | |
| 351 | (file-name-nondirectory | |
| 352 | (file-name-sans-extension ifile)) | |
| 353 | "." rssext)))) | |
| 354 | (format | |
| 355 | "\n<title>%s</title> | |
| 356 | <atom:link href=\"%s\" rel=\"self\" type=\"application/rss+xml\" /> | |
| 357 | <link>%s</link> | |
| 358 | <description><![CDATA[%s]]></description> | |
| 359 | <language>%s</language> | |
| 360 | <pubDate>%s</pubDate> | |
| 361 | <lastBuildDate>%s</lastBuildDate> | |
| 362 | <generator>%s</generator> | |
| 363 | <webMaster>%s (%s)</webMaster> | |
| 364 | <image> | |
| 365 | <url>%s</url> | |
| 366 | <title>%s</title> | |
| 367 | <link>%s</link> | |
| 368 | </image> | |
| 369 | " | |
| 370 | title publink blogurl description lang date date | |
| 371 | (concat (format "Emacs %d.%d" | |
| 372 | emacs-major-version | |
| 373 | emacs-minor-version) | |
| 374 | " Org-mode " (org-version)) | |
| 375 | email author image title blogurl))) | |
| 376 | ||
| 377 | (defun org-rss-section (section contents info) | |
| 378 | "Transcode SECTION element into RSS format. | |
| 379 | CONTENTS is the section contents. INFO is a plist used as | |
| 380 | a communication channel." | |
| 381 | contents) | |
| 382 | ||
| 383 | (defun org-rss-timestamp (timestamp contents info) | |
| 384 | "Transcode a TIMESTAMP object from Org to RSS. | |
| 385 | CONTENTS is nil. INFO is a plist holding contextual | |
| 386 | information." | |
| 387 | (org-html-encode-plain-text | |
| 388 | (org-timestamp-translate timestamp))) | |
| 389 | ||
| 390 | (defun org-rss-plain-text (contents info) | |
| 391 | "Convert plain text CONTENTS into RSS encoded text. | |
| 392 | INFO is a plist used as a communication channel" | |
| 393 | (let (output) | |
| 394 | (setq output (org-html-encode-plain-text contents) | |
| 395 | output (org-export-activate-smart-quotes | |
| 396 | output :html info)))) | |
| 397 | ||
| 398 | ;;; Filters | |
| 399 | ||
| 400 | (defun org-rss-final-function (contents backend info) | |
| 401 | "Prettify the RSS output. | |
| 402 | CONTENTS is the headline contents as a transcoded string. BACKEND | |
| 403 | is a symbol representation of the backend used. INFO is a plist | |
| 404 | used as a communication channel." | |
| 405 | (with-temp-buffer | |
| 406 | (xml-mode) | |
| 407 | (insert contents) | |
| 408 | (indent-region (point-min) (point-max)) | |
| 409 | (buffer-substring-no-properties (point-min) (point-max)))) | |
| 410 | ||
| 411 | ;;; Miscellaneous | |
| 412 | ||
| 413 | (defun org-rss-add-pubdate-property () | |
| 414 | "Set the PUBDATE property for top-level headlines." | |
| 415 | (let (msg) | |
| 416 | (org-map-entries | |
| 417 | (lambda () | |
| 418 | (let* ((entry (org-element-at-point)) | |
| 419 | (level (org-element-property :level entry))) | |
| 420 | (when (= level 1) | |
| 421 | (unless (org-entry-get (point) "PUBDATE") | |
| 422 | (setq msg t) | |
| 423 | (org-set-property | |
| 424 | "PUBDATE" (format-time-string | |
| 425 | (cdr org-time-stamp-formats))))))) | |
| 426 | nil nil 'comment 'archive) | |
| 427 | (when msg | |
| 428 | (message "Property PUBDATE added to top-level entries in %s" | |
| 429 | (buffer-file-name)) | |
| 430 | (sit-for 2)))) | |
| 431 | ||
| 432 | (provide 'ox-rss) | |
| 433 | ||
| 434 | ;;; ox-rss.el ends here | |
elisp/publish.el deleted −59
| @@ -1,59 +0,0 @@ | ||
| 1 | ;; Publish org files to HTML | |
| 2 | ||
| 3 | ;; Functions | |
| 4 | (defun org-rss-publish-to-rss (plist filename pub-dir) | |
| 5 | "Publish RSS with PLIST, only when FILENAME is 'rss.org'. | |
| 6 | PUB-DIR is when the output will be placed." | |
| 7 | (if (equal "rss.org" (file-name-nondirectory filename)) | |
| 8 | (org-rss-export-to-rss plist filename pub-dir))) | |
| 9 | ||
| 10 | ;; Project publishing settings | |
| 11 | (setq org-publish-project-alist | |
| 12 | '(("cleberg.net" | |
| 13 | :base-directory "~/Source/cleberg.net/" | |
| 14 | :publishing-function org-html-publish-to-html | |
| 15 | :publishing-directory "~/Source/cleberg.net/public/" | |
| 16 | :auto-sitemap t | |
| 17 | :recursive t | |
| 18 | :section-numbers nil | |
| 19 | :with-author nil | |
| 20 | :html-validation-link nil | |
| 21 | :with-tags t | |
| 22 | :html-htmlize-output-type 'inline-css | |
| 23 | :html-head-include-default-style nil | |
| 24 | :html-head-include-scripts nil | |
| 25 | :html-head "<link rel=stylesheet href=/static/styles.min.css>" | |
| 26 | :html-preamble "<nav><a href=/>Home</a><a href=/blog/>Blog</a><a href=/services/>Services</a><a href=/wiki/>Wiki</a></nav>") | |
| 27 | ||
| 28 | ("rss" | |
| 29 | :base-directory "~/Source/cleberg.net/blog/" | |
| 30 | :base-extension "org" | |
| 31 | :html-link-home "https://cleberg.net/" | |
| 32 | :html-link-use-abs-url t | |
| 33 | :rss-extension "xml" | |
| 34 | :publishing-directory "~/Source/cleberg.net/public/blog/" | |
| 35 | :publishing-function org-rss-publish-to-rss | |
| 36 | :section-numbers nil | |
| 37 | :exclude "index.org" | |
| 38 | :author "cmc" | |
| 39 | :email "" | |
| 40 | ;; test | |
| 41 | :html-link-org-files-as-html t | |
| 42 | :auto-sitemap t | |
| 43 | :sitemap-filename "rss.org" | |
| 44 | :sitemap-title "cleberg.net" | |
| 45 | :sitemap-style list | |
| 46 | :sitemap-sort-files anti-chronologically | |
| 47 | ;;:sitemap-function 'rw/format-rss-feed | |
| 48 | ;;:sitemap-format-entry 'rw/format-rss-feed-entry | |
| 49 | ;; end test | |
| 50 | ) | |
| 51 | ||
| 52 | ("static" | |
| 53 | :base-directory "~/Source/cleberg.net/static/" | |
| 54 | :base-extension "css\\|txt" | |
| 55 | :publishing-directory "~/Source/cleberg.net/public/static/" | |
| 56 | :publishing-function org-publish-attachment) | |
| 57 | ("website" :components ("cleberg.net" "rss" "static")))) | |
| 58 | ||
| 59 | (provide 'publish) | |
index.org deleted −26
| @@ -1,26 +0,0 @@ | ||
| 1 | #+title: cleberg.net | |
| 2 | #+OPTIONS: toc:nil | |
| 3 | ||
| 4 | #+BEGIN_QUOTE | |
| 5 | Do AIs dream in electric sheep? | |
| 6 | ||
| 7 | He hoped it had nanosecond nightmares. | |
| 8 | #+END_QUOTE | |
| 9 | ||
| 10 | ** Pages | |
| 11 | :PROPERTIES: | |
| 12 | :ID: A3B9560A-66B5-4526-ACA3-BA766C75CDDE | |
| 13 | :END: | |
| 14 | ||
| 15 | - [[./blog/][Blog]] | |
| 16 | - [[./services/][Services]] | |
| 17 | - [[./wiki/][Wiki]] | |
| 18 | ||
| 19 | ** Contact | |
| 20 | :PROPERTIES: | |
| 21 | :ID: A0D2C73E-264F-4DBC-BF87-E875EA36D6A5 | |
| 22 | :END: | |
| 23 | ||
| 24 | - hello [at] cleberg.net ([[./static/gpg.txt][GPG]]) | |
| 25 | - [[https://matrix.to/#/@cyborg:matrix.tchncs.de][@cyborg@matrix.tchncs.de]] | |
| 26 | - [[https://git.cleberg.net][Source Code]] | |
salary/index.org deleted −53
| @@ -1,53 +0,0 @@ | ||
| 1 | #+title: Salary | |
| 2 | #+date: 2023-10-01 | |
| 3 | ||
| 4 | * Salary Transparency | |
| 5 | ||
| 6 | The data below details the base salary information for each job I've held. This | |
| 7 | information is posted publicly to ensure others in my position have a solid | |
| 8 | reference point when determining if their current or proposed salary is | |
| 9 | appropriate. | |
| 10 | ||
| 11 | While sites like Glassdoor are locking salary data behind a paywall, LinkedIn is | |
| 12 | discontinuing LinkedIn Salary, and helpful websites like Big 4 Transparency are | |
| 13 | extremely rare, I wanted to provide my personal data publicly and freely to | |
| 14 | those who need it. | |
| 15 | ||
| 16 | I have seen what can happen when great employees don't know the market values | |
| 17 | for their skills and I happily help those in my teams, so I'm happy to extend | |
| 18 | this information to the online community. | |
| 19 | ||
| 20 | As a final note, there are numerous reasons that people in the same role are | |
| 21 | paid differently (expertise, years of experience, certifications, education, | |
| 22 | etc.) and that the data in this table should only be used as a single point of | |
| 23 | reference, not the whole story. | |
| 24 | ||
| 25 | * Salary Data | |
| 26 | ||
| 27 | Note: When in a role that gives periodic raises, I will create a new record with | |
| 28 | the new base salary in the table below. See the KPMG records for an example of a | |
| 29 | raise while in the same role. | |
| 30 | ||
| 31 | | Title | Company | Location | Start Date | End Date | Base Salary | | |
| 32 | |---------------------------------------+------------------------+----------------+------------+----------+-------------| | |
| 33 | | Senior Technology Assurance Associate | KPMG | Omaha, NE | 2023-10 | Current | $116,700 | | |
| 34 | | Senior Technology Assurance Associate | KPMG | Omaha, NE | 2022-06 | 2023-10 | $110,000 | | |
| 35 | | Senior Technology Risk Consultant | Ernst & Young | Des Moines, IA | 2021-09 | 2022-06 | $89,500 | | |
| 36 | | Senior IT Auditor | Ameritas | Lincoln, NE | 2021-05 | 2021-09 | $72,000 | | |
| 37 | | IT Auditor | Ameritas | Lincoln, NE | 2020-04 | 2021-05 | $65,000 | | |
| 38 | | IS Auditor II | Nelnet | Lincoln, NE | 2019-12 | 2020-04 | $58,000 | | |
| 39 | | IS Auditor I | Nelnet | Lincoln, NE | 2019-06 | 2019-12 | $20/hour | | |
| 40 | | Internal Audit Intern | Ameritas | Lincoln, NE | 2018-02 | 2019-06 | $16/hour | | |
| 41 | | Teaching Assistant | University of Nebraska | Lincoln, NE | 2017-08 | 2018-05 | $7/hour | | |
| 42 | | Community Management Intern | Walgreens | Lincoln, NE | 2017-06 | 2018-02 | $14/hour | | |
| 43 | ||
| 44 | This page was inspired by [[https://xeiaso.net/][Xe]], and I'm quoting the following wording from them as | |
| 45 | I want to reiterate this piece: | |
| 46 | ||
| 47 | #+BEGIN_QUOTE | |
| 48 | Please consider publishing your salary data like this as well. By open, | |
| 49 | voluntary transparency we can help to end stigmas around discussing pay and help | |
| 50 | ensure that the next generations of people in tech are treated fairly. Stigmas | |
| 51 | thrive in darkness but die in the light of day. You can help end the stigma by | |
| 52 | playing your cards out in the open like this. | |
| 53 | #+END_QUOTE | |
services/index.org deleted −16
| @@ -1,16 +0,0 @@ | ||
| 1 | #+title: Services | |
| 2 | #+OPTIONS: toc:nil | |
| 3 | ||
| 4 | - [[https://ao.cleberg.net][AnonymousOverflow]] - A StackOverflow proxy | |
| 5 | - [[https://cyberchef.cleberg.net][CyberChef]] - The Cyber Swiss Army Knife | |
| 6 | - [[https://files.cleberg.net][FileArchive]] - An interesting file archive | |
| 7 | - [[https://paste.cleberg.net][FlashPaper]] - One-time encrypted password/secret sharing | |
| 8 | - [[https://gh.cleberg.net][GotHub]] - An alternative front-end for GitHub | |
| 9 | - [[https://ip.cleberg.net][ifconfig.php]] - A "whatsmyip" tool | |
| 10 | - [[https://invidious.cleberg.net][Invidious]] - A YouTube proxy | |
| 11 | - [[https://nitter.cleberg.net][Nitter]] - A Twitter proxy | |
| 12 | - [[https://office.cleberg.net][Office]] - The world's smallest office suite | |
| 13 | - [[https://search.cleberg.net][SearXNG]] - A privacy-respecting, open metasearch engine | |
| 14 | ||
| 15 | See the [[https://git.cleberg.net/?p=cleberg.net.git;a=history;f=services/index.org;h=71ed6bcae7e52a8103f980f1821ca291ef3010ba;hb=HEAD][git log]] for this page if you want to see changes that have been made. | |
| 16 | ||
static/styles.css +424 −72
| @@ -1,3 +1,365 @@ | ||
| 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ | |
| 2 | ||
| 3 | /* Document | |
| 4 | ========================================================================== */ | |
| 5 | ||
| 6 | /** | |
| 7 | * 1. Correct the line height in all browsers. | |
| 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. | |
| 9 | */ | |
| 10 | ||
| 11 | html { | |
| 12 | line-height: 1.15; /* 1 */ | |
| 13 | -webkit-text-size-adjust: 100%; /* 2 */ | |
| 14 | } | |
| 15 | ||
| 16 | /* Sections | |
| 17 | ========================================================================== */ | |
| 18 | ||
| 19 | /** | |
| 20 | * Remove the margin in all browsers. | |
| 21 | */ | |
| 22 | ||
| 23 | body { | |
| 24 | margin: 0; | |
| 25 | } | |
| 26 | ||
| 27 | /** | |
| 28 | * Render the `main` element consistently in IE. | |
| 29 | */ | |
| 30 | ||
| 31 | main { | |
| 32 | display: block; | |
| 33 | } | |
| 34 | ||
| 35 | /** | |
| 36 | * Correct the font size and margin on `h1` elements within `section` and | |
| 37 | * `article` contexts in Chrome, Firefox, and Safari. | |
| 38 | */ | |
| 39 | ||
| 40 | h1 { | |
| 41 | font-size: 2em; | |
| 42 | margin: 0.67em 0; | |
| 43 | } | |
| 44 | ||
| 45 | /* Grouping content | |
| 46 | ========================================================================== */ | |
| 47 | ||
| 48 | /** | |
| 49 | * 1. Add the correct box sizing in Firefox. | |
| 50 | * 2. Show the overflow in Edge and IE. | |
| 51 | */ | |
| 52 | ||
| 53 | hr { | |
| 54 | box-sizing: content-box; /* 1 */ | |
| 55 | height: 0; /* 1 */ | |
| 56 | overflow: visible; /* 2 */ | |
| 57 | } | |
| 58 | ||
| 59 | /** | |
| 60 | * 1. Correct the inheritance and scaling of font size in all browsers. | |
| 61 | * 2. Correct the odd `em` font sizing in all browsers. | |
| 62 | */ | |
| 63 | ||
| 64 | pre { | |
| 65 | font-family: monospace, monospace; /* 1 */ | |
| 66 | font-size: 1em; /* 2 */ | |
| 67 | } | |
| 68 | ||
| 69 | /* Text-level semantics | |
| 70 | ========================================================================== */ | |
| 71 | ||
| 72 | /** | |
| 73 | * Remove the gray background on active links in IE 10. | |
| 74 | */ | |
| 75 | ||
| 76 | a { | |
| 77 | background-color: transparent; | |
| 78 | } | |
| 79 | ||
| 80 | /** | |
| 81 | * 1. Remove the bottom border in Chrome 57- | |
| 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. | |
| 83 | */ | |
| 84 | ||
| 85 | abbr[title] { | |
| 86 | border-bottom: none; /* 1 */ | |
| 87 | text-decoration: underline; /* 2 */ | |
| 88 | text-decoration: underline dotted; /* 2 */ | |
| 89 | } | |
| 90 | ||
| 91 | /** | |
| 92 | * Add the correct font weight in Chrome, Edge, and Safari. | |
| 93 | */ | |
| 94 | ||
| 95 | b, | |
| 96 | strong { | |
| 97 | font-weight: bolder; | |
| 98 | } | |
| 99 | ||
| 100 | /** | |
| 101 | * 1. Correct the inheritance and scaling of font size in all browsers. | |
| 102 | * 2. Correct the odd `em` font sizing in all browsers. | |
| 103 | */ | |
| 104 | ||
| 105 | code, | |
| 106 | kbd, | |
| 107 | samp { | |
| 108 | font-family: monospace, monospace; /* 1 */ | |
| 109 | font-size: 1em; /* 2 */ | |
| 110 | } | |
| 111 | ||
| 112 | /** | |
| 113 | * Add the correct font size in all browsers. | |
| 114 | */ | |
| 115 | ||
| 116 | small { | |
| 117 | font-size: 80%; | |
| 118 | } | |
| 119 | ||
| 120 | /** | |
| 121 | * Prevent `sub` and `sup` elements from affecting the line height in | |
| 122 | * all browsers. | |
| 123 | */ | |
| 124 | ||
| 125 | sub, | |
| 126 | sup { | |
| 127 | font-size: 75%; | |
| 128 | line-height: 0; | |
| 129 | position: relative; | |
| 130 | vertical-align: baseline; | |
| 131 | } | |
| 132 | ||
| 133 | sub { | |
| 134 | bottom: -0.25em; | |
| 135 | } | |
| 136 | ||
| 137 | sup { | |
| 138 | top: -0.5em; | |
| 139 | } | |
| 140 | ||
| 141 | /* Embedded content | |
| 142 | ========================================================================== */ | |
| 143 | ||
| 144 | /** | |
| 145 | * Remove the border on images inside links in IE 10. | |
| 146 | */ | |
| 147 | ||
| 148 | img { | |
| 149 | border-style: none; | |
| 150 | } | |
| 151 | ||
| 152 | /* Forms | |
| 153 | ========================================================================== */ | |
| 154 | ||
| 155 | /** | |
| 156 | * 1. Change the font styles in all browsers. | |
| 157 | * 2. Remove the margin in Firefox and Safari. | |
| 158 | */ | |
| 159 | ||
| 160 | button, | |
| 161 | input, | |
| 162 | optgroup, | |
| 163 | select, | |
| 164 | textarea { | |
| 165 | font-family: inherit; /* 1 */ | |
| 166 | font-size: 100%; /* 1 */ | |
| 167 | line-height: 1.15; /* 1 */ | |
| 168 | margin: 0; /* 2 */ | |
| 169 | } | |
| 170 | ||
| 171 | /** | |
| 172 | * Show the overflow in IE. | |
| 173 | * 1. Show the overflow in Edge. | |
| 174 | */ | |
| 175 | ||
| 176 | button, | |
| 177 | input { /* 1 */ | |
| 178 | overflow: visible; | |
| 179 | } | |
| 180 | ||
| 181 | /** | |
| 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. | |
| 183 | * 1. Remove the inheritance of text transform in Firefox. | |
| 184 | */ | |
| 185 | ||
| 186 | button, | |
| 187 | select { /* 1 */ | |
| 188 | text-transform: none; | |
| 189 | } | |
| 190 | ||
| 191 | /** | |
| 192 | * Correct the inability to style clickable types in iOS and Safari. | |
| 193 | */ | |
| 194 | ||
| 195 | button, | |
| 196 | [type="button"], | |
| 197 | [type="reset"], | |
| 198 | [type="submit"] { | |
| 199 | -webkit-appearance: button; | |
| 200 | } | |
| 201 | ||
| 202 | /** | |
| 203 | * Remove the inner border and padding in Firefox. | |
| 204 | */ | |
| 205 | ||
| 206 | button::-moz-focus-inner, | |
| 207 | [type="button"]::-moz-focus-inner, | |
| 208 | [type="reset"]::-moz-focus-inner, | |
| 209 | [type="submit"]::-moz-focus-inner { | |
| 210 | border-style: none; | |
| 211 | padding: 0; | |
| 212 | } | |
| 213 | ||
| 214 | /** | |
| 215 | * Restore the focus styles unset by the previous rule. | |
| 216 | */ | |
| 217 | ||
| 218 | button:-moz-focusring, | |
| 219 | [type="button"]:-moz-focusring, | |
| 220 | [type="reset"]:-moz-focusring, | |
| 221 | [type="submit"]:-moz-focusring { | |
| 222 | outline: 1px dotted ButtonText; | |
| 223 | } | |
| 224 | ||
| 225 | /** | |
| 226 | * Correct the padding in Firefox. | |
| 227 | */ | |
| 228 | ||
| 229 | fieldset { | |
| 230 | padding: 0.35em 0.75em 0.625em; | |
| 231 | } | |
| 232 | ||
| 233 | /** | |
| 234 | * 1. Correct the text wrapping in Edge and IE. | |
| 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. | |
| 236 | * 3. Remove the padding so developers are not caught out when they zero out | |
| 237 | * `fieldset` elements in all browsers. | |
| 238 | */ | |
| 239 | ||
| 240 | legend { | |
| 241 | box-sizing: border-box; /* 1 */ | |
| 242 | color: inherit; /* 2 */ | |
| 243 | display: table; /* 1 */ | |
| 244 | max-width: 100%; /* 1 */ | |
| 245 | padding: 0; /* 3 */ | |
| 246 | white-space: normal; /* 1 */ | |
| 247 | } | |
| 248 | ||
| 249 | /** | |
| 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. | |
| 251 | */ | |
| 252 | ||
| 253 | progress { | |
| 254 | vertical-align: baseline; | |
| 255 | } | |
| 256 | ||
| 257 | /** | |
| 258 | * Remove the default vertical scrollbar in IE 10+. | |
| 259 | */ | |
| 260 | ||
| 261 | textarea { | |
| 262 | overflow: auto; | |
| 263 | } | |
| 264 | ||
| 265 | /** | |
| 266 | * 1. Add the correct box sizing in IE 10. | |
| 267 | * 2. Remove the padding in IE 10. | |
| 268 | */ | |
| 269 | ||
| 270 | [type="checkbox"], | |
| 271 | [type="radio"] { | |
| 272 | box-sizing: border-box; /* 1 */ | |
| 273 | padding: 0; /* 2 */ | |
| 274 | } | |
| 275 | ||
| 276 | /** | |
| 277 | * Correct the cursor style of increment and decrement buttons in Chrome. | |
| 278 | */ | |
| 279 | ||
| 280 | [type="number"]::-webkit-inner-spin-button, | |
| 281 | [type="number"]::-webkit-outer-spin-button { | |
| 282 | height: auto; | |
| 283 | } | |
| 284 | ||
| 285 | /** | |
| 286 | * 1. Correct the odd appearance in Chrome and Safari. | |
| 287 | * 2. Correct the outline style in Safari. | |
| 288 | */ | |
| 289 | ||
| 290 | [type="search"] { | |
| 291 | -webkit-appearance: textfield; /* 1 */ | |
| 292 | outline-offset: -2px; /* 2 */ | |
| 293 | } | |
| 294 | ||
| 295 | /** | |
| 296 | * Remove the inner padding in Chrome and Safari on macOS. | |
| 297 | */ | |
| 298 | ||
| 299 | [type="search"]::-webkit-search-decoration { | |
| 300 | -webkit-appearance: none; | |
| 301 | } | |
| 302 | ||
| 303 | /** | |
| 304 | * 1. Correct the inability to style clickable types in iOS and Safari. | |
| 305 | * 2. Change font properties to `inherit` in Safari. | |
| 306 | */ | |
| 307 | ||
| 308 | ::-webkit-file-upload-button { | |
| 309 | -webkit-appearance: button; /* 1 */ | |
| 310 | font: inherit; /* 2 */ | |
| 311 | } | |
| 312 | ||
| 313 | /* Interactive | |
| 314 | ========================================================================== */ | |
| 315 | ||
| 316 | /* | |
| 317 | * Add the correct display in Edge, IE 10+, and Firefox. | |
| 318 | */ | |
| 319 | ||
| 320 | details { | |
| 321 | display: block; | |
| 322 | } | |
| 323 | ||
| 324 | /* | |
| 325 | * Add the correct display in all browsers. | |
| 326 | */ | |
| 327 | ||
| 328 | summary { | |
| 329 | display: list-item; | |
| 330 | } | |
| 331 | ||
| 332 | /* Misc | |
| 333 | ========================================================================== */ | |
| 334 | ||
| 335 | /** | |
| 336 | * Add the correct display in IE 10+. | |
| 337 | */ | |
| 338 | ||
| 339 | template { | |
| 340 | display: none; | |
| 341 | } | |
| 342 | ||
| 343 | /** | |
| 344 | * Add the correct display in IE 10. | |
| 345 | */ | |
| 346 | ||
| 347 | [hidden] { | |
| 348 | display: none; | |
| 349 | } | |
| 350 | ||
| 351 | ||
| 352 | /* CUSTOM CSS */ | |
| 353 | :root { | |
| 354 | --bg: #181a1b; | |
| 355 | --bg-bright: #2f3132; | |
| 356 | --fg: #c6c6c6; | |
| 357 | --fg-bright: #fff; | |
| 358 | --fg-dark: #575757; | |
| 359 | --blue: #6eb6ff; | |
| 360 | --pink: #ff91a5; | |
| 361 | } | |
| 362 | ||
| 1 | 363 | @media (prefers-reduced-motion: no-preference) { |
| 2 | 364 | :root { |
| 3 | 365 | scroll-behavior: smooth; |
| @@ -5,9 +367,9 @@ | ||
| 5 | 367 | } |
| 6 | 368 | |
| 7 | 369 | body { |
| 8 | background-color: #282828; | |
| 9 | color: #ebdbb2; | |
| 10 | font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; | |
| 370 | background-color: var(--bg); | |
| 371 | color: var(--fg); | |
| 372 | font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; | |
| 11 | 373 | font-size: 1rem; |
| 12 | 374 | line-height: 1.5; |
| 13 | 375 | max-width: 45em; |
| @@ -15,65 +377,51 @@ body { | ||
| 15 | 377 | padding: 0 1rem; |
| 16 | 378 | } |
| 17 | 379 | |
| 18 | nav, | |
| 380 | .site-nav, | |
| 19 | 381 | footer { |
| 20 | border-color: #ebdbb2; | |
| 382 | border-color: var(--fg); | |
| 21 | 383 | } |
| 22 | 384 | |
| 23 | h1 { | |
| 24 | color: #458588; | |
| 25 | } | |
| 26 | ||
| 27 | h2 { | |
| 28 | color: #d79921; | |
| 29 | } | |
| 30 | ||
| 31 | h3 { | |
| 32 | color: #b16286; | |
| 385 | footer { | |
| 386 | border-top: 1px dotted; | |
| 387 | padding-top: 1rem; | |
| 388 | margin: 1rem 0; | |
| 33 | 389 | } |
| 34 | 390 | |
| 35 | h4 { | |
| 36 | color: #cc241d; | |
| 391 | footer p { | |
| 392 | margin: 0; | |
| 37 | 393 | } |
| 38 | 394 | |
| 39 | h1::before { | |
| 40 | content: "* "; | |
| 395 | .site-nav { | |
| 396 | border-bottom: 1px dotted; | |
| 41 | 397 | } |
| 42 | 398 | |
| 43 | h2::before { | |
| 44 | content: "** "; | |
| 399 | .site-nav ul { | |
| 400 | list-style-type: none; | |
| 401 | display: flex; | |
| 402 | align-items: center; | |
| 403 | padding: 0; | |
| 45 | 404 | } |
| 46 | 405 | |
| 47 | h3::before { | |
| 48 | content: "*** "; | |
| 406 | .site-nav ul li { | |
| 407 | margin-right: 0.5rem; | |
| 49 | 408 | } |
| 50 | 409 | |
| 51 | h4::before { | |
| 52 | content: "**** "; | |
| 410 | h1, | |
| 411 | h2, | |
| 412 | h3, | |
| 413 | h4 { | |
| 414 | color: var(--fg-bright); | |
| 53 | 415 | } |
| 54 | 416 | |
| 55 | 417 | a, |
| 56 | 418 | a:visited { |
| 57 | color: #689d6a; | |
| 58 | text-decoration: underline; | |
| 419 | color: var(--blue); | |
| 420 | text-decoration: none; | |
| 59 | 421 | } |
| 60 | 422 | |
| 61 | 423 | a:hover { |
| 62 | background-color: #689d6a; | |
| 63 | color: #282828; | |
| 64 | } | |
| 65 | ||
| 66 | #preamble { | |
| 67 | padding: 1rem 0; | |
| 68 | } | |
| 69 | ||
| 70 | #preamble nav a { | |
| 71 | margin-right: 0.5rem; | |
| 72 | } | |
| 73 | ||
| 74 | #postamble { | |
| 75 | border-top: 1px solid #000; | |
| 76 | margin-top: 1rem; | |
| 424 | text-decoration: underline; | |
| 77 | 425 | } |
| 78 | 426 | |
| 79 | 427 | img { |
| @@ -82,15 +430,15 @@ img { | ||
| 82 | 430 | |
| 83 | 431 | table { |
| 84 | 432 | border-collapse: collapse; |
| 85 | color: #ebdbb2; | |
| 86 | border-color: #665c54; | |
| 433 | color: var(--fg); | |
| 434 | border-color: var(--fg-dark); | |
| 87 | 435 | } |
| 88 | 436 | |
| 89 | 437 | thead, |
| 90 | 438 | th, |
| 91 | 439 | tr, |
| 92 | 440 | td { |
| 93 | border: 1px solid #665c54; | |
| 441 | border: 1px solid var(--fg-dark); | |
| 94 | 442 | padding: 0.25rem; |
| 95 | 443 | } |
| 96 | 444 | |
| @@ -102,56 +450,60 @@ code { | ||
| 102 | 450 | } |
| 103 | 451 | |
| 104 | 452 | pre { |
| 105 | background-color: #3c3836; | |
| 453 | background-color: var(--bg-bright) !important; | |
| 454 | border-radius: 3px; | |
| 106 | 455 | margin: 0.5rem 0; |
| 456 | padding: 0.5rem; | |
| 107 | 457 | overflow-x: auto; |
| 108 | 458 | } |
| 109 | 459 | |
| 110 | pre::before, | |
| 111 | pre::after { | |
| 112 | background-color: #504945; | |
| 113 | display: block; | |
| 114 | white-space: pre; | |
| 115 | } | |
| 116 | ||
| 117 | pre::before { | |
| 118 | content: "#+BEGIN_SRC"; | |
| 119 | } | |
| 120 | ||
| 121 | pre::after { | |
| 122 | content: "#+END_SRC"; | |
| 123 | } | |
| 124 | ||
| 125 | 460 | :not(pre) > code { |
| 126 | color: #d79921; | |
| 461 | background-color: var(--bg-bright); | |
| 462 | color: var(--pink); | |
| 463 | padding: 0.15rem; | |
| 464 | border-radius: 3px; | |
| 127 | 465 | } |
| 128 | 466 | |
| 129 | 467 | aside { |
| 130 | background-color: #504945; | |
| 468 | background-color: var(--bg-bright); | |
| 131 | 469 | padding: 1rem; |
| 132 | 470 | } |
| 133 | 471 | |
| 134 | 472 | blockquote { |
| 473 | background-color: var(--bg-bright); | |
| 474 | border-left: 10px solid var(--blue); | |
| 135 | 475 | margin: 1rem 0; |
| 476 | padding: 0.5rem 1rem; | |
| 136 | 477 | } |
| 137 | 478 | |
| 138 | 479 | blockquote p { |
| 139 | 480 | margin: 0; |
| 140 | 481 | } |
| 141 | 482 | |
| 142 | blockquote::before, | |
| 143 | blockquote::after { | |
| 144 | background-color: #504945; | |
| 145 | display: block; | |
| 146 | white-space: pre; | |
| 483 | .post time { | |
| 484 | margin-right: 0.5rem; | |
| 485 | } | |
| 486 | ||
| 487 | .post-metadata { | |
| 488 | border: 1px dotted var(--fg); | |
| 489 | border-top: none; | |
| 490 | padding: 1rem; | |
| 491 | } | |
| 492 | ||
| 493 | .post-metadata h1 { | |
| 494 | margin: 0; | |
| 495 | } | |
| 496 | ||
| 497 | .post-metadata p { | |
| 498 | margin: 0; | |
| 147 | 499 | } |
| 148 | 500 | |
| 149 | blockquote::before { | |
| 150 | content: "#+BEGIN_QUOTE"; | |
| 501 | details { | |
| 502 | margin-top: 1rem; | |
| 151 | 503 | } |
| 152 | 504 | |
| 153 | blockquote::after { | |
| 154 | content: "#+END_QUOTE"; | |
| 505 | summary { | |
| 506 | font-weight: bold; | |
| 155 | 507 | } |
| 156 | 508 | |
| 157 | 509 | .footnote-definition { |
templates/base.html added +44
| @@ -0,0 +1,44 @@ | ||
| 1 | <!doctype html> | |
| 2 | <html lang="en"> | |
| 3 | <head> | |
| 4 | <title>cleberg.net</title> | |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 | {% if page %} | |
| 7 | {% if page.description %} | |
| 8 | <meta name="description" content="{{ page.description }}"> | |
| 9 | {% elif config.description %} | |
| 10 | <meta name="description" content="{{ config.description }}"> | |
| 11 | {% endif %} | |
| 12 | {% elif config.description %} | |
| 13 | <meta name="description" content="{{ config.description }}" /> | |
| 14 | {% endif %} | |
| 15 | ||
| 16 | {% block rss %} | |
| 17 | <link rel="alternate" type="application/atom+xml" title="RSS" href="{{ get_url(path="atom.xml", trailing_slash=false) }}"> | |
| 18 | {% endblock %} | |
| 19 | ||
| 20 | {% block css %} | |
| 21 | <link rel="stylesheet" href="/styles.css"> | |
| 22 | {% endblock css %} | |
| 23 | ||
| 24 | {% block extra_head %}{% endblock extra_head %} | |
| 25 | </head> | |
| 26 | <body> | |
| 27 | <nav class="site-nav"> | |
| 28 | <ul> | |
| 29 | <li><a href="/">Home</a></li> | |
| 30 | <li><a href="/blog/">Blog</a></li> | |
| 31 | <li><a href="/services/">Services</a></li> | |
| 32 | <li><a href="/wiki/">Wiki</a></li> | |
| 33 | </ul> | |
| 34 | </nav> | |
| 35 | ||
| 36 | <main>{% block content %}{% endblock content %}</main> | |
| 37 | ||
| 38 | <footer> | |
| 39 | <p>Last build: {{ now() | date(format="%Y-%m-%d %H:%M:%S") }}</p> | |
| 40 | <p><a href="https://git.sr.ht/~cyborg/cleberg.net" target="_blank" rel="noopener">Source Code</a></p> | |
| 41 | <p><a href="/atom.xml">Atom RSS Feed</a></p> | |
| 42 | </footer> | |
| 43 | </body> | |
| 44 | </html> | |
templates/index.html added +40
| @@ -0,0 +1,40 @@ | ||
| 1 | {% extends "base.html" %} | |
| 2 | ||
| 3 | {% block content %} | |
| 4 | <section> | |
| 5 | <h1>cleberg.net</h1> | |
| 6 | <blockquote>Do AIs dream in electric sheep?<br>He hoped it had nanosecond nightmares.</blockquote> | |
| 7 | </section> | |
| 8 | <section> | |
| 9 | <h2>Recent Blog Posts</h2> | |
| 10 | {% set section = get_section(path="blog/_index.md") %} | |
| 11 | {% set i = 0 %} | |
| 12 | {% for page in section.pages %} | |
| 13 | {% if i < 3%} | |
| 14 | {% set_global i = i + 1%} | |
| 15 | <div class="post"> | |
| 16 | <time datetime="{{ page.date | date(format='%Y-%m-%d %H:%M:%S') }}">{{ page.date }}</time> | |
| 17 | <a href="/blog/{{ page.slug }}/">{{ page.title }}</a> | |
| 18 | </div> | |
| 19 | {% endif %} | |
| 20 | {% endfor %} | |
| 21 | <br> | |
| 22 | <a href="/blog/">Older Posts →</a> | |
| 23 | </section> | |
| 24 | <section> | |
| 25 | <h2>Everything Else</h2> | |
| 26 | <ul> | |
| 27 | <li><a href="/salary/">Salary</a></li> | |
| 28 | <li><a href="/services/">Services</a></li> | |
| 29 | <li><a href="/wiki/">Wiki</a></li> | |
| 30 | </ul> | |
| 31 | </section> | |
| 32 | <section> | |
| 33 | <h2>Contact</h2> | |
| 34 | <ul> | |
| 35 | <li>hello [at] cleberg.net (<a href="/gpg.txt">GPG</a>)</li> | |
| 36 | <li><a href="https://matrix.to/#/@cyborg:matrix.tchncs.de" target="_blank" rel="noopener">@cyborg:@matrix.tchncs.de</a></li> | |
| 37 | <li><a href="https://git.sr.ht/~cyborg/" target="_blank" rel="noopener">Sourcehut</a></li> | |
| 38 | </ul> | |
| 39 | </section> | |
| 40 | {% endblock content %} | |
templates/page.html added +41
| @@ -0,0 +1,41 @@ | ||
| 1 | {% extends "base.html" %} | |
| 2 | ||
| 3 | {% block css %} | |
| 4 | <link rel="stylesheet" href="/syntax-theme-dark.css"> | |
| 5 | <link rel="stylesheet" href="/styles.css"> | |
| 6 | {% endblock css %} | |
| 7 | ||
| 8 | {% block content %} | |
| 9 | <section class="post-metadata"> | |
| 10 | <h1>{{ page.title }}</h1> | |
| 11 | {% if page.date %} | |
| 12 | <p><time datetime="{{ page.date | date(format='%Y-%m-%d %H:%M:%S') }}">{{ page.date }}</time></p> | |
| 13 | {% endif %} | |
| 14 | <p>{{ page.word_count }} words; {{ page.reading_time }} minute(s)</p> | |
| 15 | {% if page.toc %} | |
| 16 | <details open> | |
| 17 | <summary>Table of Contents</summary> | |
| 18 | <nav class="page-nav"> | |
| 19 | <ul> | |
| 20 | {% for h1 in page.toc %} | |
| 21 | <li> | |
| 22 | <a href="{{ h1.permalink | safe }}">{{ h1.title }}</a> | |
| 23 | {% if h1.children %} | |
| 24 | <ul> | |
| 25 | {% for h2 in h1.children %} | |
| 26 | <li> | |
| 27 | <a href="{{ h2.permalink | safe }}">{{ h2.title }}</a> | |
| 28 | </li> | |
| 29 | {% endfor %} | |
| 30 | </ul> | |
| 31 | {% endif %} | |
| 32 | </li> | |
| 33 | {% endfor %} | |
| 34 | </ul> | |
| 35 | </nav> | |
| 36 | </details> | |
| 37 | {% endif %} | |
| 38 | </section> | |
| 39 | ||
| 40 | {{ page.content | safe }} | |
| 41 | {% endblock content %} | |
templates/section.html added +18
| @@ -0,0 +1,18 @@ | ||
| 1 | {% extends "base.html" %} | |
| 2 | ||
| 3 | {% block content %} | |
| 4 | <h1>{{ section.title }}</h1> | |
| 5 | <p>Use <code>Ctrl + f</code> to search blog post titles for keywords. | |
| 6 | <br>You can also add the <a href="/atom.xml">Atom RSS Feed</a> to your feed reader.</p> | |
| 7 | {% set post_date = "1970-01-01" | date(format="%Y") %} | |
| 8 | {% for page in section.pages %} | |
| 9 | {% if post_date != page.date | date(format="%Y") %} | |
| 10 | {% set_global post_date = page.date | date(format="%Y") %} | |
| 11 | <h2>{{ post_date }}</h2> | |
| 12 | {% endif %} | |
| 13 | <div class="post"> | |
| 14 | <time datetime="{{ page.date | date(format='%Y-%m-%d %H:%M:%S') }}">{{ page.date }}</time> | |
| 15 | <a href="/blog/{{ page.slug }}/">{{ page.title }}</a> | |
| 16 | </div> | |
| 17 | {% endfor %} | |
| 18 | {% endblock content %} | |