├── nft_ruleset ├── inet-filter-chain-output.nft ├── inet-filter-sets.nft ├── inet-filter-chain-input.nft ├── inet-filter-chain-forward.nft └── ruleset.nft ├── git_hooks ├── post-receive └── update ├── sudo_policy └── nft-git └── README.md /nft_ruleset/inet-filter-chain-output.nft: -------------------------------------------------------------------------------- 1 | # An example output chain 2 | chain output { 3 | type filter hook output priority 0 4 | counter accept 5 | } 6 | -------------------------------------------------------------------------------- /nft_ruleset/inet-filter-sets.nft: -------------------------------------------------------------------------------- 1 | set my_ipv4_addrs { 2 | type ipv4_addr; 3 | elements = { 1.1.1.1, 1.2.3.4} 4 | } 5 | 6 | set my_ipv6_addrs { 7 | type ipv6_addr; 8 | elements = { fe00::1, fe00::2} 9 | } 10 | -------------------------------------------------------------------------------- /nft_ruleset/inet-filter-chain-input.nft: -------------------------------------------------------------------------------- 1 | # An example input chain 2 | chain input { 3 | type filter hook input priority 0 4 | ct state established,related counter accept 5 | ct state invalid counter drop 6 | tcp dport {22, 80, 443} ct state new counter accept 7 | ip saddr @my_ipv4_addrs counter accept 8 | ip6 saddr @my_ipv6_addrs counter accept 9 | } 10 | -------------------------------------------------------------------------------- /nft_ruleset/inet-filter-chain-forward.nft: -------------------------------------------------------------------------------- 1 | # An example forward chain 2 | chain forward { 3 | type filter hook forward priority 0 4 | ct state established,related counter accept 5 | ct state invalid counter drop 6 | tcp dport {22, 80, 443} ct state new counter accept 7 | ip daddr @my_ipv4_addrs counter accept 8 | ip6 daddr @my_ipv6_addrs counter accept 9 | } 10 | -------------------------------------------------------------------------------- /nft_ruleset/ruleset.nft: -------------------------------------------------------------------------------- 1 | # don't delete this 2 | flush ruleset 3 | 4 | # The main table 5 | table inet inet-filter { 6 | include "./inet-filter-sets.nft" 7 | include "./inet-filter-chain-input.nft" 8 | include "./inet-filter-chain-forward.nft" 9 | include "./inet-filter-chain-output.nft" 10 | } 11 | 12 | # Other table in other family, just an example 13 | table bridge bridge-filter { 14 | chain forward { 15 | type filter hook forward priority 0; 16 | counter 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /git_hooks/post-receive: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NAME="hooks/post-receive" 4 | NFT_ROOT="/etc/nftables.d" 5 | RULESET="${NFT_ROOT}/ruleset.nft" 6 | export GIT_WORK_TREE="$NFT_ROOT" 7 | 8 | info() 9 | { 10 | echo "$NAME $1 ..." 11 | } 12 | 13 | info "checkout latest data to $GIT_WORK_TREE" 14 | sudo git checkout -f 15 | info "cleaning untracked files and dirs at $GIT_WORK_TREE" 16 | sudo git clean -f -d 17 | 18 | info "deploying new ruleset" 19 | set -e 20 | cd $NFT_ROOT && sudo nft -f $RULESET 21 | info "new ruleset deployment was OK" 22 | -------------------------------------------------------------------------------- /sudo_policy/nft-git: -------------------------------------------------------------------------------- 1 | User_Alias OPERATORS = user1, user2 2 | Defaults env_keep += "GIT_WORK_TREE" 3 | 4 | OPERATORS ALL=(ALL) NOPASSWD:/*/ip netns add nft-test-ruleset 5 | OPERATORS ALL=(ALL) NOPASSWD:/bin/ip netns list 6 | OPERATORS ALL=(ALL) NOPASSWD:/bin/ip netns exec nft-test-ruleset /usr/sbin/nft -f * 7 | OPERATORS ALL=(ALL) NOPASSWD:/bin/ip netns exec nft-test-ruleset /usr/sbin/nft flush ruleset 8 | OPERATORS ALL=(ALL) NOPASSWD:/bin/ip netns delete nft-test-ruleset 9 | OPERATORS ALL=(ALL) NOPASSWD:/usr/bin/git checkout -f 10 | OPERATORS ALL=(ALL) NOPASSWD:/usr/bin/git clean -f -d 11 | OPERATORS ALL=(ALL) NOPASSWD:/usr/sbin/nft -f * 12 | -------------------------------------------------------------------------------- /git_hooks/update: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NAME="hooks/update" 4 | 5 | info() 6 | { 7 | echo "$NAME $1 ..." 8 | } 9 | 10 | warning() 11 | { 12 | echo "W: ${NAME}: $1 ..." >&2 13 | rm -rf $DEST 2>/dev/null 14 | exit 0 15 | } 16 | 17 | error() 18 | { 19 | echo "E: ${NAME}: $1 ..." >&2 20 | rm -rf $DEST 2>/dev/null 21 | exit 1 22 | } 23 | 24 | DEST=$(mktemp -d) 25 | if [ ! -d "$DEST" ] ; then 26 | warning "unable to create temp dir" 27 | fi 28 | 29 | REV=$3 30 | if [ -z "$REV" ] ; then 31 | error "invalid input argument for revision" 32 | fi 33 | 34 | NFT="/usr/sbin/nft" 35 | if [ ! -x "$NFT" ] ; then 36 | warning "no nft binary found" 37 | fi 38 | 39 | RULESET="${DEST}/ruleset.nft" 40 | NETNS="nft-test-ruleset" 41 | 42 | info "exporting new revision" 43 | git archive --format=tar $REV | ( cd $DEST && tar xf -) 44 | if [ "$?" != "0" ] ; then 45 | warning "unable to export revision" 46 | fi 47 | 48 | if [ ! -r $RULESET ] ; then 49 | warning "$RULESET doesn't exists" 50 | fi 51 | 52 | info "testing new ruleset" 53 | # Create netns, ignore if alredy exists 54 | sudo ip netns add $NETNS 2>/dev/null 55 | 56 | # Check if exists 57 | NETNS_LIST=$(sudo ip netns list) 58 | grep $NETNS <<< $NETNS_LIST >/dev/null 2>/dev/null 59 | if [ "$?" != "0" ] ; then 60 | warning "unable to create netns $NETNS" 61 | fi 62 | 63 | # Load ruleset 64 | cd $DEST && sudo ip netns exec $NETNS $NFT -f $RULESET 65 | if [ "$?" != "0" ] ; then 66 | error "failed to load $RULESET" 67 | fi 68 | 69 | # Clear ruleset 70 | sudo ip netns exec $NETNS $NFT flush ruleset 71 | if [ "$?" != "0" ] ; then 72 | warning "failed to flush ruleset after testing" 73 | fi 74 | 75 | # Delete netns 76 | sudo ip netns delete $NETNS 77 | if [ "$?" != "0" ] ; then 78 | warning "failed to clean netns $NETNS" 79 | fi 80 | 81 | info "ruleset test was OK" 82 | rm -rf $DEST 83 | exit 0 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nftables ruleset managed with git 2 | 3 | In [this article](http://ral-arturo.blogspot.com.es/2015/02/nftables-ruleset-managed-with-git.html) I will be mixing some ideas and concepts both of my own and I've seen in the web: 4 | * git hooks to check content of new commit 5 | * linux net namespaces to check if ruleset could load 6 | * atomically replace a nftables ruleset 7 | 8 | Mainly, getting nftables hooked in git is matter of combing these elements: 9 | * a sudo security policy 10 | * a git config and workflow to handle the checks and deployments 11 | * a nftables workflow and ruleset architecture 12 | 13 | ## What we want to achieve 14 | 15 | We have a team of sysadmins who are working together to maintain a big ruleset (thousand of rules). Keeping track of changes is almost mandatory, and git does this thing very well. 16 | 17 | Also, we want to prevent some human mistakes in terms of bad nftables syntax. In the devel world, this is equivalent to prevent developers to push code which fails to build from source. A basic QA. 18 | 19 | If all is OK, replace the ruleset in an atomic way. 20 | 21 | ## Files layout 22 | 23 | I assume this dirs scheme: 24 | * the nftables ruleset is at `/etc/nftables.d` (don't touch this dir by hand!) 25 | * git repo in bare mode is at `/srv/git/nft-firewall.git` 26 | * sudoers policy is at `/etc/sudoers.d/nft-git` 27 | 28 | ## The git part 29 | 30 | Create a git repo in bare mode somewhere, for example `/srv/git/nft-firewall.git` (with `git init --bare`). 31 | 32 | Put update and post-receive hook scripts under `/srv/git/nft-firewall.git/hooks/` with proper execs permissions. 33 | 34 | The update hook will do the "QA" part when the user push changes from their local repos: 35 | 36 | 1. Create a temp dir under /tmp 37 | 2. Export the ruleset there (the new ruleset isn't in the filesystem yet, that's why git archive is used) 38 | 3. Create a linux netnamespace 39 | 4. Load the ruleset in the new network ns 40 | 5. If the ruleset fails to load, reject the commit. 41 | 6. If the ruleset loads, all is OK, do some cleanups and exit. 42 | 7. The post-receive hook will run after the update hook, and takes care of the effective deployment of the just-changed ruleset: 43 | 8. Checkout the git repo to /etc/nftables.d (this is, actually deploy the nftables config to the filesystem). 44 | 9. Clean untracked files and dirs at /etc/nftables.d (don't touch this dir by hand!) 45 | 10. Load the new ruleset. 46 | 47 | ## The nftables layout 48 | 49 | nftables comes with a set of handy options which will permit you to organize the ruleset (thus, the firewall) in very flexible ways. 50 | 51 | In this example, I use this layout: 52 | * `/etc/nftables.d/ruleset.nft` (the main file) 53 | * `/etc/nftables.d/inet-filter-chain-input.nft` (filter rules in the input chain for the inet family) 54 | * `/etc/nftables.d/inet-filter-chain-forward.nft` (filter rules in the forward chain for the inet family) 55 | * `/etc/nftables.d/inet-filter-chain-output.nft` (filter rules in the output chain for the inet family) 56 | * `/etc/nftables.d/inet-filter-sets.nft` (data sets for the inet filter table) 57 | 58 | So, we will be 'including' all the files from `ruleset.nft`, plus flushing the previous ruleset. 59 | In the other files, we only define each chain and rules. In the set file we define sets which are globals to be used by all rules in the inet filter table. 60 | The nftables ruleset is meant to be loaded with 'nft -f ruleset.nft' which will perform an atomic replacement of the ruleset. 61 | 62 | ## The sudo policy 63 | 64 | Before any other step, be sure you are following the security policy of your organization regarding this. 65 | In order to allow all these operations with a unprivileged users (which BTW is modifying the ruleset via a remote git push), we need a concrete sudo policy. 66 | 67 | Creating netnamespaces and modifying the nftables ruleset is a privileged operation. 68 | My recomendation at this point is a sudoers file like the one you can find at the github repo. 69 | The `NOPASSWD` option is required for sudo to don't ask user password while in the git hooks. 70 | 71 | You should take care also of standard unix users/groups layout. Give appropriate permissions to working dirs/scripts. For example, put your operators in a group `git`, give the git bare repo group `git` and give g+w. 72 | 73 | ## The workflow 74 | 75 | A sys-admin team member could end in a workflow like this: 76 | 77 | 1. Clone the nft-firewall.git repo 78 | 2. Make changes to the ruleset (add a rule, replace other... whatever) 79 | 3. Run some local tests (sure) 80 | 4. Commit on your local repo 81 | 5. Push to the git server 82 | 6. The git server will run the hook scripts, testing and deploying the new ruleset 83 | 7. Other sysadmin clones or pull the repo and go to step 2. 84 | 85 | ## More info 86 | 87 | This was inspired by this blogpost by [Vincent Bernat](http://vincent.bernat.im/en/blog/2014-netfilter-firewall-script.html). 88 | Check the [nftables wiki](http://wiki.nftables.org). 89 | Also, the official docs about [git hooks](http://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks). 90 | --------------------------------------------------------------------------------