483 lines
19 KiB
Org Mode
483 lines
19 KiB
Org Mode
|
#+INCLUDE: "inc/header.html" export html
|
||
|
#+options: toc:nil
|
||
|
#+OPTIONS: html-postamble:nil
|
||
|
#+OPTIONS: html-style:nil
|
||
|
#+OPTIONS: num:nil p:nil pri:nil stat:nil tags:nil tasks:nil tex:nil timestamp:nil toc:nil title:nil
|
||
|
#+TITLE: suragu.net - OpenBSD
|
||
|
#+HTML_HEAD_EXTRA: <link rel="stylesheet" type="text/css" href="css/styles.css"/>
|
||
|
#+EXPORT_FILE_NAME: openbsd.html
|
||
|
* Fear and Loathing in OpenBSD, or my experiences with OpenBSD
|
||
|
The other day I woke up and I thought "I'm going to migrate my server
|
||
|
to OpenBSD for absolutely no reason". And so I did. The operating
|
||
|
system have been a pain and a pleasure simultaneously, and in this
|
||
|
page I intend to give my experiences with it.
|
||
|
|
||
|
I will be updating this page as I have more experiences with
|
||
|
OpenBSD. So add this page to your booksmarks!
|
||
|
|
||
|
Last update. 2022-06-09
|
||
|
|
||
|
For questions or comments on this article feel free to reach me out at
|
||
|
teru-sama [at] riseup [dot] net
|
||
|
|
||
|
** Installation
|
||
|
The installation process was pretty straightforward, it was just
|
||
|
enter, enter, enter, altough I had to connect an ethernet cable for it
|
||
|
to download some necessary firmware (so I could use the network card)
|
||
|
and thus, the wireless connection.
|
||
|
|
||
|
** Setting up services
|
||
|
*** httpd(8)
|
||
|
The website you're in is the website I care the most, kill-9 can wait
|
||
|
because that's only a website in which i complain about
|
||
|
things. Complaining about everything is not good because in this life,
|
||
|
well, in Ozzy Osbourne words, "Learn how to love and forget how to
|
||
|
hate". So I'd rather focus on ebin.city and suragu.net for the
|
||
|
time.
|
||
|
|
||
|
OpenBSD ships with the =httpd= web server. A really simple and very
|
||
|
secure http server. The main config file is =/etc/httpd.conf= which
|
||
|
has a very simple, human readable syntax.
|
||
|
|
||
|
One of the features of this webserver is that chroots to a
|
||
|
directory. Meaning that, to the web server, anything before the given
|
||
|
directory (=/var/www= by default), does not exist. So if a vulnerability
|
||
|
is found, the attacker can't do much things, as the attacker can't go
|
||
|
beyond =/var/www=.
|
||
|
*** slowcgi(8)
|
||
|
OpenBSD comes out-of-the-box with a FastCGI implementation, which is
|
||
|
very simple to use. you only have to add =fastcgi= at the desired site
|
||
|
in =httpd.conf= . The following configuration file is enough to
|
||
|
execute CGI scripts.
|
||
|
#+begin_src conf-space
|
||
|
server "suragu.net" {
|
||
|
listen on * port 80
|
||
|
root "sites/suragu.net"
|
||
|
location "/*.cgi" {
|
||
|
fastcgi
|
||
|
root "sites/suragu.net"
|
||
|
}
|
||
|
}
|
||
|
#+end_src
|
||
|
|
||
|
But not so fast! Remember that httpd runs in a chroot? Well, your CGI
|
||
|
apps won't work by default, because the chroot lacks the binaries that
|
||
|
are necessary to execute the program. So if your CGI appliaction is a
|
||
|
perl script, you'll have to do something like =cp /usr/bin/perl
|
||
|
/var/www/bin=. If your Perl script uses third-party modules, you'll
|
||
|
have to copy them to anything that is in =@INC=, so
|
||
|
=/usr/var/www/usr/lib/perl5= or something like that. Also your chroot
|
||
|
will lack all the core utils. I just installed plan9port and copied
|
||
|
the files to =/var/www/bin=, which is more than enough.
|
||
|
|
||
|
As mentioned before. I haven't been able to setup werc in httpd. Not
|
||
|
sure why. I might try to reinstall it some other day. But I guess I'd
|
||
|
have to use another server to do that.
|
||
|
|
||
|
As of 2022-06-02 I got werc to werk under httpd. The config file is a
|
||
|
bit weird, but it worked. Altough I had to install an older werc
|
||
|
version because the most recent one had some issues. This is the
|
||
|
config that worked. Thanks to solene in IRC for the pattern!
|
||
|
|
||
|
#+begin_src conf-space
|
||
|
server "kill-9.xyz" {
|
||
|
alias "www.kill-9.xyz"
|
||
|
listen on 127.0.0.1 port 1340
|
||
|
|
||
|
listen on * tls port 443
|
||
|
|
||
|
tls {
|
||
|
certificate "/etc/ssl/kill9cert.pem"
|
||
|
key "/etc/ssl/kill9key.pem"
|
||
|
}
|
||
|
|
||
|
# If there's a dot in the URL (i.e. a file extension, don't run it as
|
||
|
# a CGI script.)
|
||
|
|
||
|
location match "%s*%.%s*" {
|
||
|
root "/werc/sites/kill-9.xyz"
|
||
|
no fastcgi
|
||
|
}
|
||
|
|
||
|
location match "/" {
|
||
|
fastcgi param SCRIPT_NAME "/werc/bin/werc.rc"
|
||
|
fastcgi param SCRIPT_FILENAME "/werc/bin/werc.rc"
|
||
|
fastcgi param DOCUMENT_ROOT "/werc/sites/kill-9.xyz"
|
||
|
}
|
||
|
root "/werc/sites/kill-9.xyz/"
|
||
|
|
||
|
|
||
|
}
|
||
|
#+end_src
|
||
|
|
||
|
And then I tried to install cgit. When clonning from httpd I got an
|
||
|
error I've never got before. Something like "Recieved HTTP/0.9 when
|
||
|
not allowed". I don't know what the hell =httpd(8)= meant by that. But
|
||
|
it was fixed by nuking the repos and pushing them again from
|
||
|
scratch. I guess it had something to do with file corruption. But you
|
||
|
have to be crazy on acid to think that sending HTTP/0.9 is a good way
|
||
|
to tell a file is corrupted.
|
||
|
*** Darknets
|
||
|
Not much difference from how you'd install Tor & I2P in a Linux
|
||
|
machine. Just install the tor and i2pd packages using =pkg_add(1)= and
|
||
|
configure them normally. I had to copy the old private keys from my
|
||
|
old machine to the new server. But that was not a big issue. I also
|
||
|
migrated from the classic I2P, written in Java to i2pd. Not because I
|
||
|
dislike the Java I2P, I think it is, along with Freenet, the only good
|
||
|
software written in Java. I just wanted to keep the server as light as
|
||
|
possible. And i2pd is way lighter than Java I2P.
|
||
|
|
||
|
** Programming in OpenBSD
|
||
|
OpenBSD claims to be the most secure UNIX out there. This is probably
|
||
|
true as it has much features, such as the W^X thing. Which means
|
||
|
memory can be written or executed, but not both. Basically an attacker
|
||
|
could not execute a buffer overflow attack. Because he can only
|
||
|
write. But not execute it. This thing was introdouced in 2003.
|
||
|
*** pledge(2)
|
||
|
OpenBSD takes security very seriously. This was the reason to be of
|
||
|
the =pledge()= syscall. Pledge does, as defined by the manpage:
|
||
|
"restrict system operations". This is the prototype:
|
||
|
=pledge(const char *promises, const char *execpromises);=
|
||
|
|
||
|
Check the manpage for the possible promises. This is a quick example:
|
||
|
|
||
|
|
||
|
#+begin_src c
|
||
|
#include <stdio.h> /* printf() */
|
||
|
#include <unistd.h> /* pledge() */
|
||
|
#include <sys/stat.h> /* chmod() */
|
||
|
|
||
|
int
|
||
|
main(void)
|
||
|
{
|
||
|
/* stdio promise allows basic input output operations. Check the
|
||
|
,* manpage for the syscalls this promise allows. */
|
||
|
pledge("stdio", "");
|
||
|
|
||
|
chmod("/etc/passwd",0644);
|
||
|
|
||
|
return 0;
|
||
|
/* This code will crash at runtime. the "stdio" promise doesn't
|
||
|
,* allow the chmod() syscall. */
|
||
|
}
|
||
|
#+end_src
|
||
|
|
||
|
Running that code will prodouce the following output:
|
||
|
|
||
|
#+begin_src sh
|
||
|
Abort trap (core dumped)
|
||
|
#+end_src
|
||
|
|
||
|
This is very useful, imagine that for example, some injects, somehow,
|
||
|
malicious code in your =cat(1)= program. Sending data to a remote
|
||
|
server. As cat didn't =pledge("inet",...)=, cat won't be able to
|
||
|
create a connection.
|
||
|
*** unveil(2)
|
||
|
I like this syscall more than =pledge(2)= according to the manpage:
|
||
|
"unveil parts of a restricted filesystem view" This means that, except
|
||
|
for the file specified in the =unveil()= calls, wont' exist for the
|
||
|
program. Consider the following code and its output:
|
||
|
|
||
|
#+begin_src c
|
||
|
#include <unistd.h> /* read(), write(), unveil() */
|
||
|
#include <fcntl.h> /* open() and flags */
|
||
|
#include <string.h> /* strerror() */
|
||
|
#include <errno.h> /* errno variable */
|
||
|
#include <stdio.h> /* fprintf() */
|
||
|
|
||
|
int
|
||
|
main(void)
|
||
|
{
|
||
|
/* This program can exclusively open /etc/httpd.conf for reading. */
|
||
|
unveil("/etc/httpd.conf","r");
|
||
|
/* This call disables further calls to unveil() */
|
||
|
unveil(NULL, NULL);
|
||
|
/* What happens if we try to open another file? */
|
||
|
int fd = open("/etc/passwd", O_RDONLY);
|
||
|
|
||
|
if(fd == -1) {
|
||
|
fprintf(stderr,"Error opening file: %s\n",
|
||
|
strerror(errno));
|
||
|
_exit(-1);
|
||
|
}
|
||
|
|
||
|
char buf[8192];
|
||
|
int bytes = read(fd, buf, 8192);
|
||
|
write(STDOUT_FILENO, buf, bytes);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
#+end_src
|
||
|
|
||
|
Output: =Error opening file: No such file or directory=
|
||
|
|
||
|
Yes, my /etc/passwd file exists.
|
||
|
*** perl(1)
|
||
|
Perl is the only scripting language that OpenBSD ships. And they have
|
||
|
their reasons you can read [[https://marc.info/?l=openbsd-misc&m=159041121804486&w=2][here]]. This means that Perl comes with
|
||
|
support for the OpenBSD weird features. This means that you can call
|
||
|
=pledge(2)= and =unveil(2)= from your Perl scripts! Here's an example
|
||
|
of that:
|
||
|
|
||
|
#+begin_src perl
|
||
|
#!/usr/bin/perl
|
||
|
|
||
|
# The syscalls come as modules, so you have to import them, the
|
||
|
# subroutines are exported by default.
|
||
|
|
||
|
use OpenBSD::Pledge;
|
||
|
use OpenBSD::Unveil;
|
||
|
# The manpage says that, without the "stdio" promise, perl is useless,
|
||
|
# so it is called by default no matter what you do.
|
||
|
pledge("inet rpath wpath unix"); # Some example promises...
|
||
|
unveil("/etc/httpd.conf","r");
|
||
|
unveil("/etc/pf.conf","r");
|
||
|
unveil(); # Restrict further calls to unveil()
|
||
|
|
||
|
#+end_src
|
||
|
*** strlcpy(3) and strlcat(3)
|
||
|
The well known =strncpy(3)= and =strncat(3)= functions copy no more
|
||
|
than =n= characters, but these functions are not guaranteed to add the
|
||
|
'\0' at the end of the string. =strlcpy(3)= and =strlcat(3)= guarante
|
||
|
that the string ends with '\0'
|
||
|
*** Makefiles
|
||
|
For some reasons, I wanted to rewrite the Makefile of one of my
|
||
|
programs. And I discovered that BSD make is much better than GNU
|
||
|
make. With GNU Make you have to declare pattern rules. And weird
|
||
|
syntax. And you have to write similar makefiles for each program. In
|
||
|
OpenBSD this is not necessary because makefile has some kind of
|
||
|
"templates". This is a perfectly working makefile, /with =clean= and
|
||
|
=install= targets/
|
||
|
#+begin_src makefile
|
||
|
PROG = sakisafecli
|
||
|
SRCS += funcs.c sakisafecli.c
|
||
|
MAN = sakisafecli.1 sakisafeclirc.5
|
||
|
LDADD = -lssl -lz -lpthread -lnghttp2 -lcurl -lconfig -lcrypto -L/usr/local/lib
|
||
|
CPPFLAGS = -I/usr/local/include
|
||
|
BINDIR=/usr/local/bin
|
||
|
.include <bsd.prog.mk>
|
||
|
#+end_src
|
||
|
|
||
|
This makefile also works in Linux, but using the =bmake= command
|
||
|
instead of =make=. It also works in FreeBSD but you'd have to repleace
|
||
|
CPPFLAGS with CFLAGS.
|
||
|
*** Libraries
|
||
|
OpenBSD, unlike every Linux distribution out there, thinks about
|
||
|
everyone. And when you download a library through the package manager,
|
||
|
it will install the shared objects (for dynamic linking), the header
|
||
|
files (which means, no -dev/-devel packages) AND the .a files. For
|
||
|
static linking!
|
||
|
** Software and the power it holds
|
||
|
OpenBSD comes with a lot of software that should be enough for your
|
||
|
normal tasks. But, it's not like OpenBSD grabbed some code and put it
|
||
|
in the code, no, they wrote their own versions of popular
|
||
|
software. And "ported" them to OpenBSD, so the software that comes
|
||
|
with the operating system uses the security features, they call
|
||
|
=pledge()=. and stuff like that.
|
||
|
|
||
|
*** mg(1)
|
||
|
This is a Emacs clone. For the people who, for any reason, can't (or
|
||
|
don't want) to run GNU Emacs. This clone is pretty complete, the only
|
||
|
thing it lacks, regarding emacs, is emacs lisp support and syntax
|
||
|
highlighting. But this is a good =nano(1)=, =ed(1)= or =vi(1)= repleacement.
|
||
|
*** signify(1)
|
||
|
GNU Privacy Guard is kinda heavy, and we don't have any other decent
|
||
|
OpenPGP implementation. This is the reason of why the OpenBSD devs
|
||
|
created =signify(1)=, a tool to cryptographically sign and verify
|
||
|
files and messages. And this is the way OpenBSD images are
|
||
|
verified. It's pretty simple to use:
|
||
|
|
||
|
#+begin_src shell-script
|
||
|
# Generate pub and sec key. They have to have the same name. Only
|
||
|
# changing the file extension
|
||
|
|
||
|
$ signify -G -c "raoul's signify key" -p raoul.pub -s raoul.sec
|
||
|
|
||
|
# Sign a file/message
|
||
|
|
||
|
$ echo "Hello world!" > message.txt
|
||
|
$ signify -S -s raoul.sec -m message.txt
|
||
|
|
||
|
# Verify file/message
|
||
|
|
||
|
$ signify -V -p raoul.pub -m message.txt
|
||
|
|
||
|
# Further examples in the manpage.
|
||
|
#+end_src
|
||
|
*** tmux(1)
|
||
|
Tmux, the legendary terminal multiplexer, that is way better than
|
||
|
screen, was initially developed for OpenBSD. I don't think I have to
|
||
|
talk a lot about tmux because everyone knows it. tmux in OpenBSD comes
|
||
|
with all the security features too.
|
||
|
*** doas(1)
|
||
|
This is a repleacement for sudo that has been developed by OpenBSD. it
|
||
|
has also emerged in the linux community. Altough it works best in
|
||
|
OpenBSD. I have some issues getting doas to work in Debian, but not in
|
||
|
Void Linux. doas is very simple to configure. No need to add yourself
|
||
|
to a group or anything like that. You can simply add this to
|
||
|
=/etc/doas.conf=
|
||
|
|
||
|
#+begin_src conf-space
|
||
|
permit nopass keepenv raoul as groq
|
||
|
# Allow user raoul to execute commands as groq. Keeping all the
|
||
|
# environment variables.
|
||
|
|
||
|
# raoul can't execute commands as any user that is not groq.
|
||
|
|
||
|
permit nopass keepenv qrog
|
||
|
|
||
|
# qrog can execute commands as any user.
|
||
|
#+end_src
|
||
|
*** openrsync(1)
|
||
|
Sometimes you want to syncronize files between your computers and
|
||
|
servers. And then you realize =cp(1)= is kinda bad for that and =tar=
|
||
|
isslow. Then you discover =rsync= and that just works. But this wasn't
|
||
|
the case for the OpenBSD guys, they wanted a rsync implementation
|
||
|
under the BSD license. So they wrote =[[http://openrsync.org][openrsync=]]. This works just like
|
||
|
rsync and, according to the manpage: "openrsync is compatible with
|
||
|
rsync protocol version 27 as supported by the samba.org implementation
|
||
|
of rsync". Meaning that if you don't have openrsync in other server,
|
||
|
it will just worke, and vice versa. This is an example of usage of
|
||
|
openrsync.
|
||
|
|
||
|
#+begin_src shell
|
||
|
openrsync --rsync-path=openrsync -av Xanopticon remote_server:/var/www/files/Music
|
||
|
#+end_src
|
||
|
|
||
|
As I don't have =rsync= installed in the remote server, but I have
|
||
|
=openrsync=, I specify that the path of =rsync= is =openrsync=. This
|
||
|
way it just works.
|
||
|
|
||
|
*** acme-client(1)
|
||
|
Today I recieved a mail telling me that the [[https://kill-9.xyz][kill -9]] certificate
|
||
|
expired. "Fuck's sake" --- I inmediatly thought. "I have to renew
|
||
|
it". But I was not going to install certbot in this OpenBSD server. So
|
||
|
I had to find a way. I remembered that OpenBSD ships with
|
||
|
=acme-client=. A program that helps you to generate your certificates
|
||
|
for TLS connections. And they can be signed with the Let's Encrypt
|
||
|
certificate authority. The config file is pretty simple and
|
||
|
intuitive. You can copy and paste it from
|
||
|
=/etc/examples/acme-client.conf= and only configure the revelant part
|
||
|
that would be your domain. In my case I have it like this:
|
||
|
|
||
|
#+begin_src conf-space
|
||
|
authority letsencrypt {
|
||
|
api url "https://acme-v02.api.letsencrypt.org/directory"
|
||
|
account key "/etc/acme/letsencrypt-privkey.pem"
|
||
|
}
|
||
|
|
||
|
domain suragu.net {
|
||
|
alternative names { "www.suragu.net", "files.suragu.net" }
|
||
|
domain key "/etc/ssl/suragu.key" ecdsa
|
||
|
domain full chain certificate "/etc/ssl/suragu.crt"
|
||
|
sign with letsencrypt
|
||
|
}
|
||
|
#+end_src
|
||
|
|
||
|
I used to use wildcard certificates. That were valid to any suragu.net
|
||
|
subdomain. But I couldn't make them work in =acme-client=. But as
|
||
|
=acme-client= is less of a pain in the ass than certbot. I can
|
||
|
certainly just modify the configuration file each time I create a new
|
||
|
subdomain. And that doesn't happen too often.
|
||
|
** Documentation
|
||
|
OpenBSD takes documentation very seriously. So seriously, if a manpage
|
||
|
is lacking in a sys util, it's considered a bug. So everything that
|
||
|
comes with your OpenBSD installation is very well documented. Config
|
||
|
files have their own manpages section, the section 5. so you can learn
|
||
|
how to write httpd config files by running =man httpd.conf= This is
|
||
|
something more developers should do. There's also the =/etc/examples=
|
||
|
directory which contains examples of most config files that you'd want
|
||
|
to setup. Those file are commented and everything. But =/etc/examples=
|
||
|
always lacks the config file I want or doesn't help at all.
|
||
|
** Backups
|
||
|
I am a self-proclaimed good sysadmin. This means I should be able to
|
||
|
do backups and restore them. Not gonna lie, before OpenBSD i haven't
|
||
|
had any backup. Though I have heard that you can do incremental
|
||
|
backups with =tar(1)=. I guess this could be useful. But OpenBSD comes
|
||
|
with it's features and things. These tools are =dump(8)= and
|
||
|
=restore(8)=. Those were a bit confusing to me until I learned how to
|
||
|
use them properly. You can read the manpages for [[https://man.openbsd.org/dump.8][=dump(8)=]] and
|
||
|
[[https://man.openbsd/restore.8][=restore(8)=]] which explains pretty well how to use the software. At
|
||
|
least that's what should have happened. Because it didn't. For some
|
||
|
reason OpenBSD insists in using tapes in 2022. So yeah, apparently the
|
||
|
-a flag is mandatory these days. You can use this command to backup a
|
||
|
directory, =/etc= in this case.
|
||
|
#+begin_src shell-script
|
||
|
# 0 means it is a level 0 backup. Next backup should be level 1, then
|
||
|
# 2... Read the manpage for more details.
|
||
|
$ doas dump -0uaf backup_etc.dump /etc
|
||
|
#+end_src
|
||
|
This will take some time depending how big the directory is. =/etc= is
|
||
|
usually not too big so this example will not take a lot of time.
|
||
|
|
||
|
After 1 hour of wondering why my backup wasn't working, i discovered
|
||
|
that =restore(8)= takes everything as relative paths. Meaning that it
|
||
|
will restore to the directory you're in, so if you do =restore
|
||
|
-xfbackup_etc.dump /etc/httpd.conf=, it will restore it to
|
||
|
=$PWD/etc/httpd.conf=, not to =/etc/httpd.conf=. So you should cd to /
|
||
|
when restoring backups, something like this:
|
||
|
|
||
|
#+begin_src sh
|
||
|
cd /
|
||
|
restore -xf /var/backups/backup_etc.dump /etc/
|
||
|
#+end_src
|
||
|
** Network
|
||
|
First I tried to use the wifi card my computer came with. But for some
|
||
|
reason it kept sayin =wpi0: device timeout=. Leaving my computer
|
||
|
without connection. So I had to connect the Ethernet cable. And I
|
||
|
thought that that would solve the connection problem. But today I woke
|
||
|
up and my computer did not have internet connection. But it had LAN
|
||
|
connection. Not sure what happened. And well, that's the reason of why
|
||
|
my site was down. I'll try to fix it.
|
||
|
*** Firewall
|
||
|
OpenBSD comes with a firewall, called pf, which stands for Packet
|
||
|
Filter. As every other software developed by OpenBSD, it uses its
|
||
|
simple config file.
|
||
|
|
||
|
I used the firewall to deny ssh requests from every IP address except
|
||
|
my local network (that is, 192.168.0.0/16) and from my static IPv6
|
||
|
address.
|
||
|
|
||
|
At first the rules were not working for the IPv6 address, because I
|
||
|
don't have IPv6 at home, I use a WireGuard interface for that for
|
||
|
that, and pf didn't know that. So I had to specify that those rules
|
||
|
should also apply to the wireguard interface, like this:
|
||
|
|
||
|
=pass in on {egress wg0} ...=
|
||
|
|
||
|
And that just worked.
|
||
|
|
||
|
Yiou can also limit the connection of an user. For example, the
|
||
|
following line will disable all the connection for the user 'groq':
|
||
|
|
||
|
#+begin_src conf-space
|
||
|
# Block outcoming connections to user raoul.
|
||
|
block return out proto {tcp udp} user raoul
|
||
|
# Block incoming connection to user raoul. Not sure how useful this is.
|
||
|
block return in proto {tcp udp} user raoul
|
||
|
#+end_Src
|
||
|
|
||
|
And I've also added some IP addresses that have tried to exploit,
|
||
|
ehem, WordPress vulnerabilities in my webserver. I added them to a
|
||
|
file, =/etc/spammers=. And used a =pf(8)= feature to block all of
|
||
|
them. And I also wanted that ssh would be disabled for everyone except
|
||
|
for the machines in the Local Area Network. So only people in my
|
||
|
network could ssh to my server. pf.conf has a very readable syntax,
|
||
|
which makes this very easy:
|
||
|
|
||
|
#+begin_src conf-space
|
||
|
# Good/Dreaded IP ranges
|
||
|
table <localnet> const { 192.168.0.0/16 }
|
||
|
table <spammers> const file "/etc/spammers"
|
||
|
|
||
|
# Block spammers requests to the server. Also requests to spammers.
|
||
|
|
||
|
block in on { egress wg0 } from <spammers> to any
|
||
|
|
||
|
# Allow SSH access from the LAN
|
||
|
|
||
|
block return in log proto tcp from any to port ssh pass in on egress
|
||
|
proto tcp from <localnet> to port 22
|
||
|
#+end_src
|
||
|
|