Securely storing apikey/tokens for /tool/fetch... Approaches? == $SECRET

In V7, I’ve been trying to “port” some bash/javascript scripts we use. Most just call various REST APIs. Been using RouterOS script using /tool/fetch to replicate some of them, since running them directly on a router make scripting SOME stuff easier.

Problem is some REST API I call need an “apikey” or “token”, which is essentially a fancy password needed for the API.
e.g.

:global apikey [???????]
/tool/fetch url=... http-header-field="Authorization: bearer $apikey" ...

Since I’m just experiment with this approach, I’ve used them as :global variables loaded by script & this obviously works. But not very secure. Since the script code is pretty visible in the config (e.g. via :export or other uses). And, even load them from a file in /files, the files can’t be restricted to a single user either AFAIK.

While for stuff like AWS and other API, you can use the X.509 certificates – which are supported in /tool/fetch under V7, e.g.

/tool/fetch url=... certificate=...

But not all REST APIs support X.509 client certs.


Basically… I want to “stash” an external REST API’s apikey/token/password on a Mikrotik, that will be there after reboot, but not show up in an “:export”. Similar to how /certificate stuff work (e.g. in “backup” but not “:export”), except I’m dealing with 8-64 char strings, not certificates. Or, the concept of “encrypted-secrets”, which store “private data” used by GitHub repo/actions/etc (e.g. to avoid needing password aren’t kept in files/code) .

Curious if any one has any “nifty” solutions to this?

I’d seen “/ip/firewall/layer7-protocol/…” used to “cache” things, but that’s also not very “hidden”.

But that gave me an idea. I don’t normally use PPP, but they do have “secrets” and the password field there would at least hidden in most exports, but still persist. Anyway wrote a function that use

/ppp/secret $name set/get password=$apikey

for storing, well, “secrets”. Maybe there is a better approach – really was hoping there be something in /certificates to securely store a basic string, but couldn’t figure out any tricks there. Anyway, this is what I came up so far:

_Updated: See code in https://forum.mikrotik.com/viewtopic.php?p=916159#p916159_


Which then can be used like:

> $SECRET set MTforumpw password=ItIsASecretDontYouKnow

 > :put [$SECRET get MTforumpw]
ItIsASecretDontYouKnow
 
 > $SECRET print
Columns: NAME, SERVICE, PASSWORD, PROFILE
# NAME       SERVICE  PASSWORD                          PROFILE
;;; used by $SECRET
1 MTforumpw  async    ItIsASecretDontYouKnow            null

And my main use is for /tool/fetch, so looks like this with /tool/fetch’s headers:

{
# ...
:local headers "Authorization: bearer $[$SECRET get mtforumpw]"        
:local resp [/tool/fetch url="$url" http-method="$method" http-header-field="$headers" http-data=($payload) output="user" as-value]
:put $resp

Seems to work in a few tests – at least avoids the /export showing REST/etc API keys without show-sensitive, which was my biggest concern.
Still feel pretty “hack-ish”, so curious if anyone has any better ideas here…

Your (mis)use of PPP secrets is almost there. Since v7 passwords show up in export only when show-sensitive is specified (unlike in v6, where they were exported by default and hide-sensitive was needed to not export them), so nobody can export them by accident. And user must have “sensitive” permission to see them, otherwise it exports password=***** without showing actual password.

It’s probably good idea to make it official and add some dedicated storage with same properties. I’m not sure about possible per-user access, that could complicate things (e.g. it would probably require some “superadmin” permission for making backups containing secrets of all users).

I would very much hate following in steps of unexportable certificates and other things, that can be exported only as part of binary backup, which is terribly impractical (restoring it is all or nothing, it’s not possible to see what’s inside, so there’s no simple way to see differences between backups).

Yes, PPP is a poor knock-off of AAA.

You won’t get an argument from me :slight_smile:. Basically trying to not have “passwords” is my ROS script code.

While I do find the approach of “/ip/firewall/layer7-protocol/set $attr regexp=$value” from this post very cleaver – it shows the same need of “persisted variables” as built-in to RouterOS (with some “is-sensitive=yes” option I suppose :wink: ).

But good news is it’s easy to change my $SECRET function to do={# something else in future} – without changing any of the code that USED any “secrets”. As you point out, /ppp/secret has some /user/group policy for it (“sensitive”), although the :export with /system/script/… containing “passwords” was WAY bigger concern, than other admin users seeing anything.

Again, no argument.

But this is why I started my hunt for some “hack” in /certificates … I knew cert stuff is NOT in an /export – so TRIED to turn that negative, into a positive :slight_smile: … no such luck.

Did a quick test on this. /ppp/secret’s password can be up to 64k it seems – I would have though it be much lower. At least under V7, this test script show the limit:

:global ppppwdmax do={
    :for i from=1 to=[:tonum $1] step=($1/10) do={
        /ppp/secret/remove [find where comment="#removeme"]
        :local expected [:rndstr length=$i from=abc]
        /ppp/secret/add name="pwd$i" password=$expected comment="#removeme"
        :local actual [/ppp/secret/get "pwd$i" password]
        :put "/ppp/secret test loop=$i expected=$[:len $expected] actual=$[:len $expected]"
        /terminal/cuu
        :if ($expected!=$actual) do={
            :error "failed to created new /ppp/secret with password lengths of loop=$i expected=$[:len $expected] actual=$[:len $actual] "
        }  
    } 
}

# this will work
$ppppwdmax 60000
# /ppp/secret test loop=54001 expected=54001 actual=54001

# this won't and gets a very clear error with limit
$ppppwdmax 100000
# afraid to create strings larger than 64kB
1 Like

Updated the “persisted store using ppp secrets” script so it should work on both V6 and V7. Although again persisted variables are a “needed feature” – since this is still a hack, works well enough for my purposes, but no warranties here.


### $SECRET
#   get <name>
#   set <name> password=<password>
# . remove <name
#   print
:global SECRET
:set $SECRET do={
    :global SECRET

    # helpers
    :local fixprofile do={
        :if ([/ppp profile find name="null"]) do={:put "nothing"} else={
            /ppp profile add bridge-learning=no change-tcp-mss=no local-address=0.0.0.0 name="null" only-one=yes remote-address=0.0.0.0 session-timeout=1s use-compression=no use-encryption=no use-mpls=no use-upnp=no
        }
    }
    :local lppp [:len [/ppp secret find where name=$2]]
    :local checkexist do={
        :if (lppp=0) do={
            :error "\$SECRET: cannot find $2 in secret store"
        }
    }

    # $SECRET
    :if ([:typeof $1]!="str") do={
        :put "\$SECRET"
        :put "   uses /ppp/secrets to store stuff like REST apikeys, or other sensative data"
        :put "\t\$SECRET print - prints stored secret passwords"
        :put "\t\$SECRET get <name> - gets a stored secret"
        :put "\t\$SECRET set <name> password=\"YOUR_SECRET\" - sets a secret password" 
        :put "\t\$SECRET remove <name> - removes a secret" 
    }

    # $SECRET print
    :if ($1~"^pr") do={
        /ppp secret print where comment~"\\\$SECRET"
        :return [:nothing] 
    }

    # $SECRET get
    :if ($1~"get") do={
        $checkexist
       :return [/ppp secret get $2 password] 
    }

    # $SECRET set
    :if ($1~"set|add") do={
        :if ([:typeof $password]="str") do={} else={:error "\$SECRET: password= required"}
        :if (lppp=0) do={
            /ppp secret add name=$2 password=$password 
        } else={
            /ppp secret set $2 password=$password
        }
        $fixprofile
        /ppp secret set $2 comment="used by \$SECRET"
        /ppp secret set $2 profile="null"
        /ppp secret set $2 service="async"
        :return [$SECRET get $2]
    } 

    # $SECRET remove
    :if ($1~"rm|rem|del") do={
        $checkexist
        :return [/ppp secret remove $2]
    }
    :error "\$SECRET: bad command"
}

Here is an example of using the function:


$SECRET 
#$SECRET
#   uses /ppp/secrets to store stuff like REST apikeys, or other sensative data
#        $SECRET print - prints stored secret passwords
#        $SECRET get <name> - gets a stored secret
#        $SECRET set <name> password="YOUR_SECRET" - sets a secret password
#        $SECRET remove <name> - removes a secret
#$SECRET: bad command

$SECRET print
#Flags: X - disabled 
# #   NAME         SERVICE CALLER-ID      PASSWORD      PROFILE      REMOTE-ADDRESS 

$SECRET add "rest_apikey" password="mikrotik"
#

$SECRET print
#Flags: X - disabled 
# #   NAME         SERVICE CALLER-ID      PASSWORD      PROFILE      REMOTE-ADDRESS 
# 0   ;;; used by $SECRET
#     rest_apikey  async                  mikrotik      null        

:put [$SECRET get rest_apikey]
# mikrotik

$SECRET remove rest_apikey
# 

:put [$SECRET get rest_apikey]
# no such item

and more specific example from above of using as in /tool/fetch for common “API Keys” (TLS with Bearer auth header):

{
# ...
:local headers "Authorization: bearer $[$SECRET get mtforumpw]"        
:local resp [/tool/fetch url="$url" http-method="$method" http-header-field="$headers" http-data=($payload) output="user" as-value]
:put $resp
# ...
}
1 Like

This is very useful. I think it would be possible to enhance this by storing usernames in the caller-id field. That would allow storing username/password pairs that could be looked up by “name”. If this seems sensible I might have a go at that.

Hi Amm0,

I’m trying to use this script to store tokens in ROS 7.16.2 but I don’t seem to be able to retrieve the password in a script:

I create two scripts as follows:

/system script
add dont-require-permissions=no name=SECRET owner=admin policy=\
    read,write,test source="# Credit: https://forum.mikrotik.com/viewtopic.php\
    \?p=916159#p916159\r\
    \n### \$SECRET\r\
    \n#   get <name>\r\
    \n#   set <name> password=<password>\r\
    \n# . remove <name\r\
    \n#   print\r\
    \n:global SECRET\r\
    \n:set \$SECRET do={\r\
    \n    :global SECRET\r\
    \n\r\
    \n    # helpers\r\
    \n    :local fixprofile do={\r\
    \n        :if ([/ppp profile find name=\"null\"]) do={:put \"nothing\"} el\
    se={\r\
    \n            /ppp profile add bridge-learning=no change-tcp-mss=no local-\
    address=0.0.0.0 name=\"null\" only-one=yes remote-address=0.0.0.0 session-\
    timeout=1s use-compression=no use-encryption=no use-mpls=no use-upnp=no\r\
    \n        }\r\
    \n    }\r\
    \n    :local lppp [:len [/ppp secret find where name=\$2]]\r\
    \n    :local checkexist do={\r\
    \n        :if (lppp=0) do={\r\
    \n            :error \"\\\$SECRET: cannot find \$2 in secret store\"\r\
    \n        }\r\
    \n    }\r\
    \n\r\
    \n    # \$SECRET\r\
    \n    :if ([:typeof \$1]!=\"str\") do={\r\
    \n        :put \"\\\$SECRET\"\r\
    \n        :put \"   uses /ppp/secrets to store stuff like REST apikeys, or\
    \_other sensative data\"\r\
    \n        :put \"\\t\\\$SECRET print - prints stored secret passwords\"\r\
    \n        :put \"\\t\\\$SECRET get <name> - gets a stored secret\"\r\
    \n        :put \"\\t\\\$SECRET set <name> password=\\\"YOUR_SECRET\\\" - s\
    ets a secret password\" \r\
    \n        :put \"\\t\\\$SECRET remove <name> - removes a secret\" \r\
    \n    }\r\
    \n\r\
    \n    # \$SECRET print\r\
    \n    :if (\$1~\"^pr\") do={\r\
    \n        /ppp secret print where comment~\"\\\\\\\$SECRET\"\r\
    \n        :return [:nothing] \r\
    \n    }\r\
    \n\r\
    \n    # \$SECRET get\r\
    \n    :if (\$1~\"get\") do={\r\
    \n        \$checkexist\r\
    \n       :return [/ppp secret get \$2 password] \r\
    \n    }\r\
    \n\r\
    \n    # \$SECRET set\r\
    \n    :if (\$1~\"set|add\") do={\r\
    \n        :if ([:typeof \$password]=\"str\") do={} else={:error \"\\\$SECR\
    ET: password= required\"}\r\
    \n        :if (lppp=0) do={\r\
    \n            /ppp secret add name=\$2 password=\$password \r\
    \n        } else={\r\
    \n            /ppp secret set \$2 password=\$password\r\
    \n        }\r\
    \n        \$fixprofile\r\
    \n        /ppp secret set \$2 comment=\"used by \\\$SECRET\"\r\
    \n        /ppp secret set \$2 profile=\"null\"\r\
    \n        /ppp secret set \$2 service=\"async\"\r\
    \n        :return [\$SECRET get \$2]\r\
    \n    } \r\
    \n\r\
    \n    # \$SECRET remove\r\
    \n    :if (\$1~\"rm|rem|del\") do={\r\
    \n        \$checkexist\r\
    \n        :return [/ppp secret remove \$2]\r\
    \n    }\r\
    \n    :error \"\\\$SECRET: bad command\"\r\
    \n}"
add dont-require-permissions=no name=testscript owner=admin policy=\
    read,write,test source=":global SECRET\r\
    \n:local user \"avggeek\"\r\
    \n:local thepass\r\
    \n:set thepass \"\$[\$SECRET get rest_apikey]\"\r\
    \n:log info \"Pass is: \$thepass\""

I can run the

$SECRET

command on the terminal without issues:

>  $SECRET add "rest_apikey" password="mikrotik"
nothing
> :put [$SECRET get rest_apikey]                          
mikrotik

But I run the script

/system script run testscript

, I can only see

Pass is:

in the Log. Is there some permissions issue or something else I’m missing?

Try to add the policy policy :open_mouth:
I.e.:
policy=read,write,test,policy

Not that I recommend it, but the primitive method I use when testing scripts is giving them all possible permissions, then remove them one by one until it stops working:
http://forum.mikrotik.com/t/netwatch-with-fetch-stopped-working-after-7-13/172865/12

Adding policy to the script permissions did the trick - thanks!

Yeah the whole idea of $SECRET is that it uses /ppp/profile password= variable, which in RouterOS policy is “sensitive” - you indeed you do need policy permission for it.

Now the main benefit of using a “sensitive” attribute to store the “secret” is that stuff like API keys would not appear in :export to avoid leaking API keys. But unfortunately the same policy permission is required to use it.

In an ideal world RouterOS scripting would support persistent secure variables, since often some key/etc is needed for any /tool/fetch call to cloud services… $SECRET is still better than just embedding password in the script itself IMO, since even a “read only” user can see any keys in a /system/script source= attribute.