Where to store secret keys DJANGO
Asked Answered
C

10

119

For the life of me, I have been looking for this everywhere and have not found the answer. I hope I am not posting a duplicate.

It is advised everywhere that you should keep your secret keys in a separate file from your general settings.py. Also, that you should never commit your "secret.py" file that contains keys such as SECRET_KEY, AWS_SECRET_KEY and so on.

My question is: In your production server, you need to reference your secret keys, that means that your "secret.py" settings file, should live somewhere around the server right? If so, how do you protect your secret keys in production?

Cloninger answered 4/3, 2013 at 19:54 Comment(0)
L
51

See the Django deployment docs for a discussion on this.

There's quite a few options for production. The way I do it is by setting my sensitive data variables as environmental variables on the production environments. Then I retrieve the variables in the settings.py via os.environ like so:

import os
SECRET_KEY = os.environ['SECRET_KEY']

Another possible option is to copy in the secret.py file via your deploy script.

I'm sure there are also other specific options for different web servers.

Lindsley answered 4/3, 2013 at 20:3 Comment(6)
For linux: unix.stackexchange.com/questions/21598/… . For my example above, you would add export secret_KEY = 'ABABABABABDSFJKEWLSK' in your .bash_profile, .bash_login or .profile - depending on which exist.Lindsley
I moved my secret key to .bash_profile and used os.environ.get, and it totally broke my site, even though echo $SECRET_KEY worked fine.Wyler
@BabkenVardanyan That is not true. Only the user has read permission to it. Check for yourself with stat /proc/$PID/environPliant
@DanHoerst small note, on my machine removing the spaces surrounding the equal-sign was error free: export secret_KEY='ABABABABABDSFJKEWLSK'Dur
@DanHoerst I tried this, but my secret key has a "#" in it, so when I saved it as an environment variable everything looks fine. However, when I call it, it only returns everything before the "#". Any ideas on how to fix this?Chevaldefrise
gunicorn can't access environment variables, so why is this recommended?Stoecker
R
126

I wanted to add a new answer because, as a beginner, the previous accepted answer didn't make a lot of sense to me (it was only one part of the puzzle).

So here's how I store my keys both LOCALLY and in PRODUCTION (Heroku, and others).

Note: You really only have to do this if you plan on putting your project online. If it's just a local project, no need.

I also made a video tutorial for people who prefer that format.

1) Install python-dotenv to create a local project environment to store your secret key.

pip install python-dotenv

2) Create a .env file in your base directory (where manage.py is).

YourDjangoProject
├───project
│   ├───__init__.py
│   ├───asgi.py
│   ├───settings.py
│   ├───urls.py
│   └───wsgi.py
├───.env
├───manage.py
└───db.sqlite3

If you have a Heroku project, it should look something like this:

YourDjangoProject
├───.git
├───project
│   ├───__init__.py
│   ├───asgi.py
│   ├───settings.py
│   ├───urls.py
│   └───wsgi.py
├───venv
├───.env
├───.gitignore
├───manage.py
├───Procfile
├───requirements.txt
└───runtime.txt

3) Add .env to your .gitignore file.

echo .env > .gitignore  # Or just open your .gitignore and type in .env

This is how you keep your secret key more secure because you don't upload your .env file to git or heroku (or wherever else).

4) Add your SECRET_KEY from your settings.py file into the .env file like so (without quotes)

**Inside of your .env file**
SECRET_KEY=qolwvjicds5p53gvod1pyrz*%2uykjw&a^&c4moab!w=&16ou7 # <- Example key, SECRET_KEY=yoursecretkey

5) Inside of your settings.py file, add the following settings:

import os
import dotenv # <- New

# Add .env variables anywhere before SECRET_KEY
dotenv_file = os.path.join(BASE_DIR, ".env")
if os.path.isfile(dotenv_file):
    dotenv.load_dotenv(dotenv_file)

# UPDATE secret key
SECRET_KEY = os.environ['SECRET_KEY'] # Instead of your actual secret key

or, thanks to @Ashkay Chandran's answer down below (please give him an upvote if you use it 🙏 I always use this):

from dotenv import load_dotenv, find_dotenv

load_dotenv(find_dotenv())

SECRET_KEY = os.environ['SECRET_KEY']

And now your secret key is successfully stored locally.

Update: I found out you can also use the config method from the package python-decouple that seems to be a bit easier:

from decouple import config

SECRET_KEY = config('SECRET_KEY')

Now you don't need to import os or use dotenv because it takes care of those parts for you AND will still use the .env file. I started using this in all of my projects.

6) Add the SECRET_KEY environment variable on your host (such as Heroku).


I work mostly with Heroku sites, so if you're wanting to use Heroku for a Django project, this part is for you.

This assumes that you already have a Heroku project setup and have Heroku CLI downloaded on your computer.

You have 2 options:

  1. From Command Line / Terminal, you can enter the following command in your project's directory:
heroku config:set SECRET_KEY=yoursecretkey # Again, no quotes.
  1. You can go to your Heroku dashboard, click on your app, go to your apps settings, and see the "Config Vars" section and click "Reveal Vars" or "Add Vars" and add your SECRET_KEY there.

Then, when you push your project to Heroku through git, it should be working properly without any issue.

and that's it! 🙂

This answer was targeted towards total beginners / intermediates to hopefully cut through any confusion (because it was definitely confusing for me).

Roping answered 26/4, 2020 at 8:24 Comment(2)
thx for the config method from decouple. One hint: you need to install pip install python-decouple (And not pip install decouple)Artistic
Thanks for the python-decouple suggestion. That seems like the most precise way to do it. And also as @Artistic mentioned, pip install python-decouple needs to be done first.Jareb
L
51

See the Django deployment docs for a discussion on this.

There's quite a few options for production. The way I do it is by setting my sensitive data variables as environmental variables on the production environments. Then I retrieve the variables in the settings.py via os.environ like so:

import os
SECRET_KEY = os.environ['SECRET_KEY']

Another possible option is to copy in the secret.py file via your deploy script.

I'm sure there are also other specific options for different web servers.

Lindsley answered 4/3, 2013 at 20:3 Comment(6)
For linux: unix.stackexchange.com/questions/21598/… . For my example above, you would add export secret_KEY = 'ABABABABABDSFJKEWLSK' in your .bash_profile, .bash_login or .profile - depending on which exist.Lindsley
I moved my secret key to .bash_profile and used os.environ.get, and it totally broke my site, even though echo $SECRET_KEY worked fine.Wyler
@BabkenVardanyan That is not true. Only the user has read permission to it. Check for yourself with stat /proc/$PID/environPliant
@DanHoerst small note, on my machine removing the spaces surrounding the equal-sign was error free: export secret_KEY='ABABABABABDSFJKEWLSK'Dur
@DanHoerst I tried this, but my secret key has a "#" in it, so when I saved it as an environment variable everything looks fine. However, when I call it, it only returns everything before the "#". Any ideas on how to fix this?Chevaldefrise
gunicorn can't access environment variables, so why is this recommended?Stoecker
B
12

You should store your settings in a modular way. By that I mean to spread your settings across multiple files.

For example, you can have base_settings.py to store all your base settings; dev_settings.py for your development server settings; and finally prod_base_settings.py for all production settings. All non-base settings files will import all the base settings and then only change whatever is necessary:

# base_settings.py
...

# dev_settings.py
from base_settings import *
DEBUG = TRUE
...

# prod_base_settings.py
from base_settings import *
DEBUG = FALSE
...

This approach allows you to have different settings from different setups. You can also commit all these files except then on the production server you can create the actual production settings file prod_settings.py where you will specify all the sensitive settings. This file should not be committed anywhere and its content kept secure:

# prod_settings.py
from prod_base_settings import *
SECRET_KEY = 'foo'

As for the file names you can use whatever filenames you feel are appropriate. Personally I actually create a Python package for the settings and then keep the various settings inside the package:

project/
  project/
    settings/
      __init__.py
      base.py
      dev.py
      ...
  app1/
    models.py
    ...
  app2/
    models.py
    ...
Bogey answered 4/3, 2013 at 20:7 Comment(3)
Thank you for your reply. However, I was looking at how to protect these keys.Cloninger
By having all secret settings in the separate file is a way you protect it. It only does not protect in case server gets hacked where the file gets compromised. But in that case environment variables are only vulnerable, same as any other method I know of. There are methods for completely securing such information but all of them involve a third party storing the secure data and then your server can ask them for the info, but to make it secure, on each request, those services will send you notification where you have to validate the request so they are not completely automated.Bogey
How do you determine which set of settings to use. Is there an ìf``somewhere? In JS I check the host name (localhost or production server). That way I have a single code base and don't have to remember to make any manual changes when deploying.Effrontery
R
8

Storing secrets in the environment still places them in the environment; which can be exploited if an unauthorized user gains access to the environment. It is a trivial effort to list environment variables, and naming one SECRET makes is all the more helpful and obvious to a bad actor an unwanted user.

Yet secrets are necessary in production, so how to access them while minimizing attack surface? Encrypt each secret in a file with a tool like git-secret, then allow authorized users to read in the file, as mentioned in django's docs. Then "tell" a non-root user the secret so it can be read-in during initialization.

(Alternatively, one could also use Hashicorp's Vault, and access the secrets stored in Vault via the HVAC python module.)

Once this non-root user is told, something like this is easy:

# Remember that './secret_key.txt' is encrypted until it's needed, and only read by a non-root user
with open('./secret_key.txt') as f:
    SECRET_KEY = f.read().strip() 

This isn't perfect, and, yes, an attacker could enumerate variables and access it -- but it's very difficult to do so during run-time, and Django does a good job of protecting its keys from such a threat vector.

This is a much safer approach than storing secrets in the environment.

Recall answered 18/9, 2020 at 7:56 Comment(1)
Thank you! I've been trying to figure out how to hide SECRET_KEY for days, but everyone leaps straight to the "environment variable" solution without questioning it. This post is the first acknowledgement I've found of the obvious danger of putting a secret into the environment where any process can access it. I thought I was going crazy.Bindman
K
6

I know it has been a long time, but I just opensourced a small Django app I am using to generate a new secret key if it does not exist yet. It is called django-generate-secret-key.

pip install django-generate-secret-key

Then, when provisioning / deploying a new server running my Django project, I run the following command (from Ansible):

python manage.py generate_secret_key

It simply:

  • checks if a secret key needs to be generated
  • generates it in a secretkey.txt file (can be customized)

All you need then is to have in your settings file:

with open('/path/to/the/secretkey.txt') as f:
    SECRET_KEY = f.read().strip()

You can now benefit from a fully automated provisioning process without having to store a static secret key in your repository.

Kursh answered 13/3, 2016 at 14:20 Comment(3)
Hmm, with latest django (1.11) am getting: FileNotFoundError: [Errno 2] No such file or directory: '/home/.../project/secretkey.txt'Subkingdom
@BabkenVardanyan did you run python manage.py generate_secret_key first? If it did not create the file or if anything is wrong, then please open an issue here: github.com/MickaelBergem/django-generate-secret-key/issues/new so we can talk about thisIleus
what happens when you add a server?Dirkdirks
J
5

Instead of if/then logic you should use a tool designed for factoring out sensitive data. I use YamJam https://pypi.python.org/pypi/yamjam/ . It allows all the advantages of the os.environ method but is simpler -- you still have to set those environ variables, you'll need to put them in a script somewhere. YamJam stores these config settings in a machine config store and also allows a project by project ability to override.

from YamJam import yamjam

variable = yamjam()['myproject']['variable']

Is the basic usage. And like the os.environ method, it is not framework specific, you can use it with Django or any other app/framework. I've tried them all, multiple settings.py files, brittle logic of if/then and environment wrangling. In the end, I switched to yamjam and haven't regretted it.

Judges answered 3/5, 2014 at 16:12 Comment(0)
O
3

Adding to zack-plauch's answer, To get the path to the .env file, when using python-dotenv module, the find_dotenv method can be used,

from dotenv import load_dotenv, find_dotenv

load_dotenv(find_dotenv())

SECRET_KEY = os.environ['SECRET_KEY']

The find_dotenv() looks for a ".env" file in the path, so it can be saved inside the same directory too,

Also, if a name is used for the .env file like "django-config.env", load_dotenv(find_dotenv("django-config.env"), will fetch and load that to host-machine environment variable mappings.

Overshine answered 22/12, 2020 at 15:4 Comment(1)
I added this to the main answer 🙂 thanks!Obligation
S
0

I am surprised that noone has talked about django-environ. I usually create a .env file like this:

SECRET_KEY=blabla
OTHER_SECRET=blabla

This file should be added in .gitignore

You can checkin in git, an example file named .env.example just for others to know which env var they need. The content of .env.example file will look like this (just keys without any values)

SECRET_KEY=
OTHER_SECRETS=
Shorter answered 10/10, 2020 at 18:44 Comment(0)
L
0

Where to store SECRET_KEY DJANGO

Store your django SECRET_KEY in an environmental variable or separate file, instead of directly encoding In your configuration module settings.py

settings.py

#from an environment variable
import os

SECRET_KEY = os.environ.get('SECRET_KEY')

#from an file
with open('/etc/secret_key.txt') as f:
    SECRET_KEY = f.read().strip()

How to generate Django SECRET_KEY manually:

$ python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

7^t+3on^bca+t7@)w%2pedaf0m&$_gnne#^s4zk3a%4uu5ly86

import string
import secrets

c = string.ascii_letters + string.digits + string.punctuation

secret_key = ''.join(secrets.choice(c) for i in range(67))

print(secret_key) 

df&)ok{ZL^6Up$\y2*">LqHx:D,_f_of#P,~}n&\zs*:y{OTU4CueQNrMz1UH*mhocD

Make sure the key used in production is not used elsewhere and avoid sending it to source control.

Limner answered 22/6, 2021 at 14:20 Comment(0)
G
0

A couple of alternatives to anyone skittish to store secrets in environment variables (see e.g., this thread):

1. SOPS

  1. Set up SOPS (see its README).

  2. Put your secrets in one of the supported configuration file format (such as YAML, JSON, ENV, INI and BINARY formats).

  3. Encrypt it.

    In my case, I used the following command:

    sops --encrypt --azure-kv  <endpoint_url> secrets.json > secrets.sops.json
    
  4. Add the value-encrypted file to source control (in the case above, secrets.json), and either delete the original or back it up somewhere secure.

  5. In settings.py, add the following (or similar) to load the secrets.

    For example:

    import json
    import subprocess
    
    decrypted = subprocess.run(["sops", "--decrypt", "secrets.sops.json"], capture_output=True)
    config = json.loads(decrypted.stdout)
    
  6. Add secrets wherever needed.

    For example:

    SECRET_KEY = config['SECRET_KEY']
    ALLOWED_HOSTS = config['ALLOWED_HOSTS'] + ['192.168.64.4']
    DATABASES = config['DATABASES']
    

    where DATABASE looks like this in my JSON file:

    { ... ,
      "DATABASES": {
        "default": {
          "ENGINE": "ENC[AES256_GCM,data:...,iv:.../pc=,tag:...,type:str]",
          "NAME": "ENC[AES256_GCM,data:...,iv:.../pc=,tag:...,type:str]",
          "USER": "ENC[AES256_GCM,data:...,iv:.../pc=,tag:...,type:str]",
          "PASSWORD": "ENC[AES256_GCM,data:...,iv:.../pc=,tag:...,type:str]",
          "HOST": "ENC[AES256_GCM,data:...,iv:.../pc=,tag:...,type:str]",
          "PORT": "ENC[AES256_GCM,data:...,iv:.../pc=,tag:...,type:str]"
        }   
      },  
      ...
    }
    

KeePassXC

  1. Install KeePassXC.

  2. Create a KeePass database for the project.

    This Reddit thread is a great security settings best practices reference.

  3. Create a new entry, and add settings.py as attachment.

  4. Delete settings.py from the repo (or back it up securely somewhere else), and add to .gitignore (if using Git, that is).

  5. Add the newly created KeePass database to the repo (e.g., settings.kdbx).

  6. Extract settings.py from the database whenever needed:

                                    <--database-> <----entry-----> <attachment> 
    keepassxc-cli attachment-export settings.kdbx project_settings settings.py --stdout
    

The combination of SOPS and KeePassXC

The SOPS route is more intimidating, but I like that I can keep settings.py and the secrets file in the repo, and only the actual secrets are "redacted".

I actually use them together:

  1. A KeePass database holds a shell script as attachment that exports the environment variables necessary to use the dedicated Azure service principal.

    The following command will extract the attachment and run the script in one go in Bash:

    source <(keepassxc-cli attachment-export <db> <entry> export.sh --stdout)
    
  2. The development environment is set up via a shell.nix script that sets up a PostgreSQL instance and the database using the secrets from the JSON:

    d () { sops --decrypt secrets/lynx_settings.sops.json | \
             jq -r ".[\"DATABASES\"][\"default\"][\"$1\"]"; \
    }
    
    createdb $(d "NAME") --host=$PGDATA --port=$(d "PORT")
    
      psql \                                                                                                                                                                                                                                                        
        --host=$PGDATA \                                                                                                                                                                                                                                           
        --username=$(whoami) \                                                                                                                                                                                                                                      
        --dbname=$(d "NAME")  \                                                                                                                                                                                                                                     
        --command="CREATE ROLE $(d 'USER') WITH LOGIN PASSWORD '$(d 'PASSWORD')'
    
  3. settings.py is set up as described above so any Django command will work as expected.

Godhood answered 9/4, 2023 at 19:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.