Search code examples
salt-project

how to set a random key once using saltstack


I install a configuration for rails that looks like this (simplified):

production:
    secret_key_base: 800afb35d5086b2c60ebd35c01b2bd2b522c2492
    db_username: ...
    db_password: ...

and so it gets installed from a template file

{{ role }}:
    secret_key_base: {{ secret_key }}
    db_username: {{ db_user }}
    db_password: {{ db_pass }}

And the role and db user/pass get pulled from pillar and installed in that file. The secret_key it would make sense to generate randomly, e.g., {{ salt['random.get_str'](length=80) }}. But I want to generate it once, not every time the template is rendered. (Changing the secret key invalidates cookies, not something to do on each salt run.)

The only solution I've found is two-phase: I have a template.in file

{{ role }}:
    secret_key_base: ||secret_key_base||
    db_username: {{ db_user }}
    db_password: {{ db_pass }}

that I sed into my template file on any given minion:

/srv/salt/rails/secrets.yml:
  cmd.run:
    # Fill in the secret key base (used for cookies).  We can't use
    # jinja2 for this, since jinja would complain about the other
    # variables that it doesn't know how to replace.  We want our
    # output to be a jinja template.
    - name: |
        cat /srv/salt/rails/secrets.yml.in | \
          sed -e 's/||secret_key_base||/{{ salt['random.get_str'](length=80) }}/;' | \
          cat > /srv/salt/rails/secrets.yml
        chmod 400 /srv/salt/rails/secrets.yml
    - creates: /srv/salt/rails/secrets.yml
    - runas: root

/var/railroad/{{host_role}}/shared/config/secrets.yml:
  file.managed:
    - source: salt://rails/secrets.yml
    - mode: 400
    - user: railroad-{{host_role}}
    - group: railroad-{{host_role}}
    - template: jinja
    - defaults:
        role: host_role
        db_username: m_u
        db_password: m_p

This works but has the disadvantage that a change to secrets.yml.in would not be propagated on to secrets.yml. (Suppose we add another key to the secrets file.) It also feels clunkier than necessary.

Is there a better way?

A better way

As noted in the comments, a better way is to just generate the secret by hand (after all, it's only done at host setup) and store it in pillar, where we anyway have to say a few things about each host.

Here is what the working code eventually looked like, unsimplified for those who might want to see something more complex. Much of the complexity is my host_credentials pillar data, which tries to characterise all that we need to know about each host.

{% set fqdn = grains.get('fqdn', 'unknown-host-fqdn') %}
{% set host_role = pillar['host_credentials']
                         [grains.get('fqdn')]
                         ['role'] %}
{% set examplecom_web_app = pillar['host_credentials']
                                  [grains.get('fqdn')]
                                  ['examplecom-web-app'] %}
{% set mysql_server_host = examplecom_web_app.mysql.host %}
{% set mysql_server_database = examplecom_web_app.mysql.database %}
{% set mysql_server_role = examplecom_web_app.mysql.role %}
{% set mysql_server_spec = pillar['host_credentials']
                                 [mysql_server_host]
                                 ['mysql'] %}
{% set mongodb_server_host = examplecom_web_app.mongodb.host %}
{% set mongodb_server_spec = pillar['host_credentials']
                                   [mongodb_server_host]
                                   ['mongodb'] %}
/var/examplecom/railroad/{{host_role}}/shared/config/secrets.yml:
  file.managed:
    - source: salt://rails/secrets.yml
    - mode: 400
    - user: railroad-{{host_role}}
    - group: railroad-{{host_role}}
    - template: jinja
    - defaults:
        role: {{ host_role }}
        secret_key_base: {{ examplecom_web_app.secret_key_base }}
        mysql_hostname: {{ mysql_server_host }}
        mysql_username: {{ mysql_server_spec[mysql_server_database]
                                            [mysql_server_role]
                                            ['username'] }}
        mysql_password: {{ mysql_server_spec[mysql_server_database]
                                            [mysql_server_role]
                                            ['password'] }}
        mongodb_hostname: {{ mongodb_server_host }}
        mongodb_username: {{ mongodb_server_spec.username }}
        mongodb_password: {{ mongodb_server_spec.password }}

As an aside, I was happy to discover that jinja2 is white-space agnostic, which helps enormously with readability on such lookups.


Solution

  • I recommend putting the secret in a pillar and generate the value once (manually) on the master. That way you can avoid doing the stateful on-the-fly-magic in your SLS file.

    jma updated his question to include an example solution.