Swagg::Blogg

or: How I Learned to Stop Worrying and Love the WWW

There exists in-depth documentation into how SELinux works. There's even more tutorials that begin with “Step one: disable SELinux.” This is neither. I wanted to bang out a quick write-up on how I “cope” with SELinux rather than either dedicating the rest of my life into exploring the depths of it or saying fuck it and turning it off. I think my approach is better than just disabling it and is generally Good Enough™. I won't pretend to explain the mechanics of SELinux as there's not enough caffeine on the planet for me to feign that level of understanding but I do hope this will encourage more mere mortals like myself to try leaving it on and feel the fuzzy warmth of the security blanket that is SELinux.

Modes

There's 3 modes to SELinux: disabled (wrong!), permissive and enforcing. The first thing I do to a freshly installed Red Hat(-like) system is put it into permissive mode:

# ruby -i.bak -pe 'sub(/^(SELINUX=)[a-z]+$/, %q{\1permissive})' /etc/selinux/config
# setenforce 0
# getenforce 
Permissive

The Ruby one-liner there will set up SELinux to boot into permissive mode. The setenforce command next will immediately set it to permissive (no reboot required). Finally, getenforce should return “Permissive” to confirm that we're in permissive mode. What the hell is permissive mode then? Think of it as an “alert only” or “fail open” mode. SELinux is still running and will log violations as expected but will not actually intervene to prevent the violation from occurring. What I like about this is we can take our new system for a spin for a couple days and log the violations we'll inevitably trigger. This gives us the logs we need to generate new policies to exempt these things without going through the painful process of getting denied, generating a single policy to get around the denial, ah shit now there's a new denial, rinse and repeat.

Finally there's disabled and enforcing mode. Enforcing is full-blown, “fail closed” SELinux. Disabled is what you think it is: SELinux is completely disabled. Switching to/from disabled will cost you a reboot but you shouldn't be doing that anyways you BOFH!

Labels and Types

In the spirit of doing things Good Enough™ I'm not going to tell you how to write your own policies or even get into how exactly policies work. Just know that SELinux policies reference file and/or process labels to determine what is or isn't allowed to happen. You can typically expose these labels by adding the -Z flag to many of the existing tools you're familiar with:

# ls -Z /etc/selinux/config
system_u:object_r:selinux_config_t:s0 /etc/selinux/config

The important field on this label is that second-to-last one, the “type” field which is denoted with the “_t” suffix. That's all I have to say on labels, let's talk about the tools that essentially handle all this for us.

Our Tools

On a RHEL-ish system you'll want to install the audit and policycoreutils-python-utils packages if you don't have them already. With my system in permissive mode and having run my favorite programs and poked at my files, I typically have some violations logged. We can check it out like so:

# ausearch -m avc -ts boot
----
time->Sat Oct 16 13:41:00 2021
type=AVC msg=audit(1634406060.965:325): avc:  denied  { watch } for  pid=2799 comm="dbus-daemon" path="/var/lib/snapd/dbus-1/services" dev="nvme0n1p4" ino=3792304 scontext=system_u:system_r:xdm_t:s0-s0:c0.c1023 tcontext=system_u:object_r:snappy_var_lib_t:s0 tclass=dir permissive=1

Yep, looks like snapd tried to do a thing that SELinux disagreed with. And yeah you can use that -ts flag with different values but typically I just go with “boot” which goes back to the system's last boot. Let's generate a policy to allow this:

# ausearch -m avc -ts boot | audit2allow -M snapd20211016
******************** IMPORTANT ***********************
To make this policy package active, execute:

semodule -i snapd20211016.pp

Now before we run that semodule command it wants us to run I like to look at something first. You see this tool actually produced two files for us. There's snapd20211016.pp but there's also this snapd20211016.te:

# file snapd20211016.??
snapd20211016.pp: SE Linux modular policy version 1, 1 sections, mod version 20, MLS, module name snapd20211016\003
snapd20211016.te: ASCII text

Think of the .te file as the plain-text “source code” for the policy while the .pp file is that “source code” in its compiled, ready-to-rock form. I always first peek at the plain-text version to see if this is something I'm ready to install:

# cat snapd20211016.te

module snapd20211016 1.0;

require {
	type snappy_var_lib_t;
	type xdm_t;
	class dir watch;
}

#============= xdm_t ==============
allow xdm_t snappy_var_lib_t:dir watch;

This looks fine to me so I'd go ahead and install it like so (just like we were told by audit2allow):

# semodule -i snapd20211016.pp

Under what circumstances would I not go ahead and install the policy? One of two scenarios:

  1. The name I specified with audit2allow -M isn't accurate. If you pipe multiple alerts specifically you might have a mix of unrelated violations. In the spirit of Good Enough™ I typically just change the name to something like local20211016 and make myself a big ol' polka policy.
  2. The policy contains a comment telling me I can use some boolean to allow this. This means the policy doesn't need to be installed at all.

What's an SELinux boolean? They're additional SELinux controls that exist outside of the whole policies/labels/types dance. If you don't want to bother with this then that's fine; install your policy and be at peace. Personally I find it easier in the long run to just flip the boolean however as it means you'll likely never see violations like this ever again, so I'll say a few things on booleans for those who've made it this far into the blog post with your sanity in tact.

Booleans

Here's one I see every time I fire up Counter-Strike 1.6:

# cat hl_linux10032021.te

module hl_linux10032021 1.0;

require {
	type unconfined_t;
	class process execheap;
}

#============= unconfined_t ==============

#!!!! This avc can be allowed using the boolean 'selinuxuser_execheap'
allow unconfined_t self:process execheap;

See that comment there that audit2allow was nice enough to exclaim several times for me? Whatever execheap is, I love it and I want some more of it. Let's not worry about installing this policy and instead flip the boolean to enable it for good:

# semanage boolean --modify --on selinuxuser_execheap

I don't think we'll ever be seeing that violation ever again.

Let's Get This Over With

Alright once we've got our policies installed and/or booleans flipped, let's complete our journey by going back into enforcing mode:

# cd /etc/selinux/
# cp config.bak config
# setenforce 1

Some Thoughts on SELinux Administration

I figured I'd just share some observations I've made after coping with SELinux for the past several years.

Typically I'm a huge believer in the RTFM philosophy but the man pages for tools like semanage are simply bad. Almost none of the command-line options are documented here. Oddly enough I have better luck using the bash completion to see what options are available but this isn't always available depending on your shell environment and even still there's plenty of command-line options missing. Best bet if you want to explore the tools more would be to create yourself a free Red Hat developer account to be granted full access to their online documentation. There's also a book written by the SELinux maintainer for Gentoo. There's a third edition out now as of 2020 but I couldn't find it anywhere except Amazon at the time I'm writing this. This could be an interesting read but if you get this far I think you're beyond Good Enough™ and probably know way too much to be bothered with this post.

Finally it's probably a good idea to use ausearch every now and then to check for any new violations. Particularly after installing/running a new program or performing a system upgrade. Speaking of system upgrade, I like to go ahead and just throw it back into permissive mode before doing stuff like this. Just keeps those potential violations out of our way while we run all kinds of stuff and poke all kinds of things in the process. Once in a blue moon I'll be working on some fun things and get opaque errors. Sometimes it'll say permission denied even though the file permissions look fine. Sometimes it'll be much more opaque. Sometimes I won't even get any apparent error message. Before you pull your hair out troubleshooting every damn thing, remember to check ausearch! I tend to forget SELinux exists on my servers where I don't have to think about it so often as things don't change so frequently on my servers and I end up troubleshooting everything but SELinux.

Don't be afraid to revert to permissive mode for a bit if you find yourself flabbergasted and need to come back to it later to get things sorted and back into enforcing mode.

Godspeed,

Dan

#fedora #redhat #rhel #selinux

I've put forth some effort recently towards exploring some other Perl-ish but not-Perl scripting languages to (I hope) reinforce my resume. I love Perl but if I hope to become a real professional with my programming then I think it behooves me to branch out a bit. I haven't strayed far from Perl however as the languages that have intrigued me the most so far have been Raku and Javascript.

What is or isn't “Perl-ish” I suppose is subjective but I think these two have a syntax I can find some familiarity with. I still haven't felt empowered enough yet to really want to use either of these for actual $workStuff yet. Oddly enough I've reached for another Perl-ish language for this that I initially only cared about for its killer app.

If there's one thing that kinda struck me as odd about Ruby it's how variables are “defined.” Honestly I have a tendancy to spend hours of trial an error when learning new things to spare me the ten minutes it takes to just read the documentation but I got to thinking about it and realized I had a knowledge gap in what makes a variable “defined”? If I declare a variable like so:

$ re.pl
$ my $string = 'ayy... lmao';

I've declared and defined the variable. But there's this other thing I've been doing without ever giving it much thought:

$ my ($string, %hash, @list);

So I just defined 3 different variables at once right? Turns out that is not the case as I learned in, of all things, a JS tutorial. I've declared these three variables but they are infact, undefined:

$ node
Welcome to Node.js v14.17.0.
Type ".help" for more information.
> let string, hash, list;
undefined
> string
undefined
> undeclaredVar
Uncaught ReferenceError: undeclaredVar is not defined

I recognized this as undef in Perl:

$ re.pl
$ my ($string, %hash, @list);
$ use Data::Dumper;
$ Dumper($string);
$VAR1 = undef;

Cool. So there's declared variables and then there's defined variables. But the thing about Ruby is there's no let or my keywords. The way you define a variable in Ruby reminds me more of bash:

$ irb
irb(main):001:0> string = 'ayy... lmao'
=> "ayy... lmao"

But what if I just want to “declare” a variable but not “define” it? The best solution I've found so far goes like this:

irb(main):001:0> string = nil
=> nil

This is actually different than:

irb(main):001:0> string = ''
=> ""

Coincidentally (or perhaps not, I'm using a filler word) only false and nil will evaluate as false in Ruby:

irb(main):001:0> string = ''
=> ""
irb(main):002:0> puts "ayy... lmao" if string
ayy... lmao
=> nil

That means this madness is real:

irb(main):003:0> puts "ayy... lmao" if 0
ayy... lmao
=> nil

If only Ruby had let or my so I could declare but not define variables... Wait. What if I did something like... ?:

irb(main):001:0> string, hash, list = nil
=> nil
irb(main):002:0> puts "ayy... lmao" if list
=> nil
irb(main):003:0> puts "ayy... lmao" if undeclaredVar
(irb):3:in `<main>': undefined local variable or method `undeclaredVar' for main:Object (NameError)
	from /usr/share/gems/gems/irb-1.3.5/exe/irb:11:in `<top (required)>'
	from /usr/bin/irb:23:in `load'
	from /usr/bin/irb:23:in `<main>'

Ultimately it seems Ruby gives me all the roap I need to hang myself. It just doesn't look right to me because I've grown fond of let and/or my. One could write their entire web backend and frontend both in JS but I'd rather use Ruby on Rails in my backend. Or in theory at least. I learned Ruby to learn Rails but I can't yet write anything useful with Rails. But with plain Ruby I can do some neat system-side stuff as I would with Perl. My homepage uses Perl on the backend because I already knew how to do it with Perl. And my frontend is more JS than CSS because I actually like JS the more I learn about it.

But I really need to stop dicking around and learn Rails already. I wrote a blog post for the first time in months to put off reading another chapter of my Rails book. On the brightside I won't have to learn SQL if I can master ActiveRecord. How nice would it be to just do everything in one language? SQL (pronounced like sequel) must be a conspiracy to sell more CS degrees. Or Oracle licenses.

I should probably just learn SQL so I can finally learn Rails.

Dan B

#perl #raku #javascript #ruby

So I mentioned in my previous post that, with the powers granted to me by Mojolicious, I have migrated from the venerable Apache server with all my server-side fancy stuff being CGI to Docker containers. Awesome! This morning my buddy n4vn33t shared with me an interesting blog post; I'll let the post speak for itself but my takeaway was essentially, sure that upstream Docker image I'm using ought to be patched and ready to rock but... What if it's not? It would help to add RUN apt-get -y upgrade to my Dockerfile to make sure I've got the latest and greatest stuff. And so I did. And then I ran my container locally to give it a quick test and now my remaining CGI scripts are returning 404s. If I run the app locally with morbo www-swagg.pl it “just works” so I must've borked my Docker container!

To troubleshoot, I comment out the apt-get -y upgrade stuff and rebuild. That should yield the same result as prior to the change right? Well, no it didn't. At this point I'm lost so I begin removing all traces of previous images, re-pulling and re-building things. Still got 404s for my beloved guestbook which I think goes without saying, is completely unacceptable! Then I thought about this line in my Dockerfile:

RUN cpanm RJBS/Getopt-Long-Descriptive-0.105.tar.gz

Why run that instead of cpanm Getopt::Long::Descriptive? You see, this module isn't one that I'm using in my scripts but rather it's a dependency; a module I'm using is using this module. One day (prior to this) my Docker container refused to build and I narrowed the problem down to v0.106 of Getopt::Long::Descriptive building and, as it's not a module I'm much concerned with (I just need it so the module I do need will build) I enter the command as you see above to force installation of the prior version that I know will build just fine. So I start by checking out the changelogs for my modules that I'm using in my scripts. Thankfully it didn't take me long to see this in the changelog for Mojolicious (first module I checked 🙃):

9.11 2021-03-20 – This release contains fixes for security issues, everybody should upgrade! – Disabled format detection by default to fix vulnerabilities in many Mojolicious applications. That means some of your routes that previously matched “/foo” and “/foo.json”, will only match “/foo” after upgrading. From now on you will have to explicitly declare the formats your routes are allowed to handle. # /foo # /foo.html # /foo.json $r->get('/foo')–>to('bar#yada'); becomes $r->get('/foo' => [format => ['html', 'json']])–>to('bar#yada', format => undef); And if you are certain that your application is not vulnerable, you also have the option to re-enable format detection for a route and all its nested routes. Due to the high risk of vulnerabilities, this feature is going to be removed again in a future release however. my $active = $r->any([format => 1]); $active->get('/foo')–>to('Test#first'); $active->put('/bar')–>to('Test#second'); ...

Ahh that's right... I'm doing this:

plugin 'Config';

# CGI scripts
plugin CGI => ['/cgi-bin/guest'  => './cgi-bin/guest_mm.cgi'];
plugin CGI => ['/cgi-bin/whoami' => './cgi-bin/whoami.cgi'  ];

The .cgi file extension wasn't technically necessary but I want that to still work because:

  1. I already have hyperlinks all over the place that use the extension
  2. It's a CGI script and I want it to “look” like that... Petty I know

So here's my v9.11+ compliant way of using the extension:

plugin CGI => ['/cgi-bin/guest.cgi'  => './cgi-bin/guest_mm.cgi'];
plugin CGI => ['/cgi-bin/whoami.cgi' => './cgi-bin/whoami.cgi'  ];

Excellent, we're back in business! Fighting issues like this can sometimes feel like a “waste” of an afternoon because at the end of the day... My site hasn't really changed. I gained no new fun buttons or GIFs but I can sleep easy tonight knowing that my site is just a bit more “hardened” against script kiddies who never cease to make our lives just a little bit more complicated. Let's that tag this puppy, push it to my cloud provider and call it a day:

# After we've run: docker build -t www-swagg .
docker tag www-swagg gcr.io/www-swagg/www-swagg
docker push gcr.io/www-swagg/www-swagg
# Bunch of output follows...

And now we're safe. Until tomorrow when the next round of vulnerabilities gets discovered anyways 🤦‍♂️

Until next time bug,

Dan B

#docker #perl #mojolicious

I've been spending the past few weekends with Mojolicious, a Perl web framework. There's plenty of great web frameworks out there; several of them for Perl if you're a Perl programmer like me (e.g. Dancer2, Catalyst, etc). I plan on making my next few posts about Mojolicious as it really has made web programming fun for me, trying new things and learning new things (sometimes breaking new things). In the process I've been migrating my homepage from Apache/CGI to Mojolicious::Lite.

One thing my website has had is an awesome MIDI (technically an mp3 now) soundtrack, which is great but has merely presented itself as a little player in the upper left corner of the page via this HTML:

<!-- Soundtrack -->
<!-- <embed> doesn't work anymore SAD
<embed src="/misc/Smashmouth_-_All_Star.mid" width="100%" height="60">-->
<audio src="/Music/Smashmouth-All-Star.mp3" autoplay controls
       preload="auto"></audio>

Using <embed> with a MIDI file is NFG nowadays so I've replaced it with an <audio> tag. The controls attritube lets you manually play the song since no modern browser will honor the autoplay attribute. I pieced together via some Stackoverflow posts such as this one that generally a browser won't play media without the user clicking on something. I decided one way to do this is with a fake GDPR compliance banner. People click accept on these all the time without even thinking about it the same way we do with software EULAs. This of course makes them completely useless and society would be better off without them but since they're seemingly here to stay we can at least try to make something useful out of them. What I came up with is some Javascript to play it like so:

<!-- Soundtrack -->
<audio id="soundtrack" src="/Music/Smashmouth-All-Star.mp3" preload="auto">
</audio>
<!-- "GDPR" banner -->
<div id="gdpr">
  <b>Notice:</b> This site uses kickass MIDI technology instead of cookies.
  <img alt="a compact disc playing music" src="/Pictures/Music_CD.gif"
       style="vertical-align: bottom"><br>
  <br>
  <button class="win95button" onclick="playIt();"><u>O</u>K</button>
  <button class="win95button" onclick="closeIt();"><u>C</u>ancel</button>
</div>
<script>
  function closeIt() {
      document.getElementById("gdpr").style.display = "none";
  }

  function playIt() {
      document.getElementById("soundtrack").play();
      closeIt();
  }
</script>

The way this works is the user clicks on “OK” (they always do!), then document.getElementById("soundtrack").play(); reliably triggers the music playback. Mission accomplished but now I have a new grievance: The banner keeps coming back no matter what. The real GDPR banner wouldn't do this even with all its own aggrivations. This is happening because HTTP is a stateless protocol. The irony in my fake GDPR banner is that I'll need to leverage session cookies to maintain state; to tell my homepage, “This user has already heard the joke.” I started by just adding another line to the closeIt() function:

function closeIt() {
    document.getElementById("gdpr").style.display = "none";
    document.cookie = "banner=seen; max-age=600;";
}

This sets my cookie as a plain-text cookie plus I set a max-age so it doesn't linger forever (I'll explain why in a bit). This is fine but because Mojolicious has a more sophisticated way of handling the session I wanted to let Mojolicious set the cookie. That posed a new challenge: HTTP is also a request/response conversation, thus Mojo would want to set its session cookie via a Set-Cookie: response header. However by the time the user sees the banner, the request is completed. I thought of two ways to get around this: Create a new route, let's say /session, then have my accept button point to that. The /session route could then redirect the user back to the value of the Referer: request header (that's the actual spelling) with the Set-Cookie: header in the 301 or 302 response.

Or, since I already have my plain-text cookie in place, why not just look for the presence of this cookie in subsequent requests and if we find it, then include our session cookie in the response? My max-age of 10 min means the plain-text cookie will eventually “roll off” if you will but the fancy, cryptographically signed session cookie will live on with its lifetime of an hour:

# Handle the session
under sub {
    my ($c) = @_;

    if ($c->cookie('banner') eq 'seen') {
        $c->session->{banner} //= 'seen'
    }

    1;
};

I'm using under in my Perl code (the controller) to save myself from having to repeat this logic in every route. Here's the end result according to the response headers:

[daniel@threadlake ~]$ curl -I -k -b 'banner=seen' https://www.swagg.net
HTTP/2 200
content-type: text/html;charset=UTF-8
set-cookie: mojolicious=eyJiYW5uZXIiOiJzZWVuIiwiZXhwaXJlcyI6MTYxNTc2Mjc0OX0---22455b29015766360fe791cb13cd16778e4fb197; expires=Sun, 14 Mar 2021 22:59:09 GMT; path=/; HttpOnly; SameSite=Lax
x-cloud-trace-context: 06fdb28b1bbd08a2b04c3d5d668a5afd;o=1
content-length: 5529
date: Sun, 14 Mar 2021 21:59:09 GMT
server: Google Frontend
via: 1.1 google
alt-svc: clear

We can decode the bit of the session cookie to the left of the three hyphens with, what else, perl:

[daniel@threadlake ~]$ perl -MMIME::Base64 -e 'print decode_base64("eyJiYW5uZXIiOiJzZWVuIiwiZXhwaXJlcyI6MTYxNTc2Mjc0OX0"), "\n";'
{"banner":"seen","expires":1615762749}

Expires in an hour rather than the 600 seconds (aka 10 minutes) of our plain-text cookie:

[daniel@threadlake ~]$ date -d @1615762749; echo "it is now: `date`"
Sun Mar 14 06:59:09 PM EDT 2021
it is now: Sun Mar 14 06:04:13 PM EDT 2021

That “stuff” to the right of the three hyphens is currently still a mystery to me. I imagine it has something to do with the HMAC-SHA1 signature that makes the session cookie tamper-proof. That's about all I can say about session cookies for one weekend. One last thing I want to share is another bonus use I found for my newfangled session. I can look for it in my templates/layouts to save the client from even having to download the mp3 file or GDPR banner resources on subsequent requests as well, sparing them some bandwidth:

<% unless (session('banner') eq 'seen') { %>
<!-- Soundtrack -->
<!-- <embed> doesn't work anymore SAD
<embed src="/misc/Smashmouth_-_All_Star.mid" width="100%" height="60">-->
<audio id="soundtrack" src="/Music/Smashmouth-All-Star.mp3" preload="auto">
</audio>
<!-- "GDPR" banner -->
<div id="gdpr">
  <b>Notice:</b> This site uses kickass MIDI technology instead of cookies.
  <img alt="a compact disc playing music" src="/Pictures/Music_CD.gif"
       style="vertical-align: bottom"><br>
  <br>
  <button class="win95button" onclick="playIt();"><u>O</u>K</button>
  <button class="win95button" onclick="closeIt();"><u>C</u>ancel</button>
</div>
<script>
  function closeIt() {
      document.getElementById("gdpr").style.display = "none";
      document.cookie = "banner=seen; max-age=600;";
  }

  function playIt() {
      document.getElementById("soundtrack").play();
      closeIt();
  }
</script>
<% } %>

Layouts and templates are also fun Mojo things I'll want to say more about in later posts. I look forward to sharing how I've actually deployed this thing via Docker container as I was pleasantly surprised on how painless this can be for someone who cut their teeth on Apache and CGI. Diving into Mojo has exposed me to newfangled concepts that I frankly wasn't particulary excited about before (I still maintain that “serverless” is a damn oxymoron!). I find myself more open to exploring them now and hopefully will give me cool things to muse on here in the future.

Source code for my cool new web 2.0 page is on my Codeberg and is, as always, “under construction”.

Happy GDPR-compliant cookie acceptance,

Dan

EDIT: Fixed some markdown formatting errors

#perl #mojolicious

Greetings fellow netizens. This will be just a quick introductory post so my newfangled blog isn't bare. My name is Dan B, aka swagg boi, and this blog will mostly focus on nerd stuff like programming, networks and open-source software. If I get around to posting something witty then all opinions expressed belong solely to me.

Happy surfing,

Dan

Enter your email to subscribe to updates.