¿Cuál es la (mejor) forma de administrar los permisos para los volúmenes compartidos de Docker?


345

He estado jugando con Docker por un tiempo y sigo encontrando el mismo problema cuando trato con datos persistentes.

Creo mi Dockerfiley expongo un volumen o lo uso --volumes-frompara montar una carpeta de host dentro de mi contenedor .

¿Qué permisos debo aplicar al volumen compartido en el host?

Se me ocurren dos opciones:

  • Hasta ahora les he dado a todos acceso de lectura / escritura, para poder escribir en la carpeta desde el contenedor Docker.

  • Asigne los usuarios del host al contenedor, para que pueda asignar permisos más granulares. Sin embargo, no estoy seguro de que esto sea posible y no he encontrado mucho al respecto. Hasta ahora, todo lo que puedo hacer es ejecutar el contenedor como un usuario: docker run -i -t -user="myuser" postgrespero este usuario tiene un UID diferente que mi host myuser, por lo que los permisos no funcionan. Además, no estoy seguro si mapear a los usuarios planteará algunos riesgos de seguridad.

¿Hay otras alternativas?

¿Cómo están ustedes / chicas lidiando con este problema?



1
También te puede interesar este hilo que trata este tema con cierto detalle: groups.google.com/forum/#!msg/docker-user/cVov44ZFg_c/…
btiernay


Por el momento, el equipo de Docker no planea implementar una solución nativa para montar directorios de host como un volumen con uid / gid especificado. Vea mi comentario y respuestas sobre este tema: github.com/docker/docker/issues/7198#issuecomment-230636074
Quinn Commandado el

Respuestas:


168

ACTUALIZACIÓN 2016-03-02 : A partir de Docker 1.9.0, Docker ha nombrado volúmenes que reemplazan los contenedores de solo datos . La respuesta a continuación, así como mi publicación de blog vinculada, todavía tiene valor en el sentido de cómo pensar sobre los datos dentro de la ventana acoplable, pero considere usar volúmenes con nombre para implementar el patrón que se describe a continuación en lugar de los contenedores de datos.


Creo que la forma canónica de resolver esto es mediante el uso de contenedores solo de datos . Con este enfoque, todo el acceso a los datos del volumen es a través de contenedores que usan -volumes-fromel contenedor de datos, por lo que el host uid / gid no importa.

Por ejemplo, un caso de uso dado en la documentación es hacer una copia de seguridad de un volumen de datos. Para hacer esto, se usa otro contenedor para hacer la copia de seguridad a través tar, y también se usa -volumes-frompara montar el volumen. Entonces, creo que el punto clave para asimilar es: en lugar de pensar en cómo obtener acceso a los datos en el host con los permisos adecuados, piense en cómo hacer lo que necesite: copias de seguridad, navegación, etc., a través de otro contenedor . Los contenedores en sí mismos necesitan usar uid / gids consistentes, pero no necesitan mapearse a nada en el host, por lo que permanecen portátiles.

Esto también es relativamente nuevo para mí, pero si tiene un caso de uso en particular, no dude en comentar y trataré de ampliar la respuesta.

ACTUALIZACIÓN : para el caso de uso dado en los comentarios, es posible que tenga una imagen some/graphitepara ejecutar grafito y una imagen some/graphitedatacomo contenedor de datos. Entonces, ignorando los puertos y demás, la Dockerfileimagen some/graphitedataes algo así como:

FROM debian:jessie
# add our user and group first to make sure their IDs get assigned consistently, regardless of other deps added later
RUN groupadd -r graphite \
  && useradd -r -g graphite graphite
RUN mkdir -p /data/graphite \
  && chown -R graphite:graphite /data/graphite
VOLUME /data/graphite
USER graphite
CMD ["echo", "Data container for graphite"]

Construye y crea el contenedor de datos:

docker build -t some/graphitedata Dockerfile
docker run --name graphitedata some/graphitedata

El some/graphiteDockerfile también debería obtener los mismos uid / gids, por lo tanto, podría verse así:

FROM debian:jessie
# add our user and group first to make sure their IDs get assigned consistently, regardless of other deps added later
RUN groupadd -r graphite \
  && useradd -r -g graphite graphite
# ... graphite installation ...
VOLUME /data/graphite
USER graphite
CMD ["/bin/graphite"]

Y se ejecutaría de la siguiente manera:

docker run --volumes-from=graphitedata some/graphite

Ok, ahora eso nos da nuestro contenedor de grafito y el contenedor de solo datos asociados con el usuario / grupo correcto (tenga en cuenta que también puede reutilizar el some/graphitecontenedor para el contenedor de datos, anulando el entradapoing / cmd cuando lo ejecute, pero tenerlos como imágenes separadas IMO es más claro).

Ahora, supongamos que desea editar algo en la carpeta de datos. Entonces, en lugar de vincular el montaje del volumen al host y editarlo allí, cree un nuevo contenedor para hacer ese trabajo. Vamos a llamarlo some/graphitetools. También creemos el usuario / grupo apropiado, al igual que la some/graphiteimagen.

FROM debian:jessie
# add our user and group first to make sure their IDs get assigned consistently, regardless of other deps added later
RUN groupadd -r graphite \
  && useradd -r -g graphite graphite
VOLUME /data/graphite
USER graphite
CMD ["/bin/bash"]

Puede hacer esto SECO heredando de some/graphiteo some/graphitedataen el Dockerfile, o en lugar de crear una nueva imagen, simplemente reutilice una de las existentes (anulando el punto de entrada / cmd según sea necesario).

Ahora, simplemente ejecutas:

docker run -ti --rm --volumes-from=graphitedata some/graphitetools

y luego vi /data/graphite/whatever.txt. Esto funciona perfectamente porque todos los contenedores tienen el mismo usuario de grafito con uid / gid coincidentes.

Como nunca monta /data/graphitedesde el host, no le importa cómo el uid / gid del host se asigna al uid / gid definido dentro de los contenedores graphitey graphitetools. Esos contenedores ahora se pueden implementar en cualquier host y continuarán funcionando perfectamente.

Lo bueno de esto es que graphitetoolspodría tener todo tipo de utilidades y scripts útiles, que ahora también puede implementar de manera portátil.

ACTUALIZACIÓN 2 : Después de escribir esta respuesta, decidí escribir una publicación de blog más completa sobre este enfoque. Espero que ayude.

ACTUALIZACIÓN 3 : Corrigí esta respuesta y agregué más detalles. Anteriormente contenía algunas suposiciones incorrectas sobre la propiedad y los permisos: la propiedad generalmente se asigna en el momento de la creación del volumen, es decir, en el contenedor de datos, porque es cuando se crea el volumen. Ver este blog . Sin embargo, esto no es un requisito: puede usar el contenedor de datos como "referencia / identificador" y establecer la propiedad / permisos en otro contenedor a través de chown en un punto de entrada, que termina con gosu para ejecutar el comando como el usuario correcto. Si alguien está interesado en este enfoque, comente y puedo proporcionar enlaces a una muestra utilizando este enfoque.


35
Me temo que esto no es una solución, ya que tendrá el mismo problema con los contenedores de solo datos. Al final del día, estos contenedores utilizarán volúmenes compartidos desde el host, por lo que aún necesitará administrar los permisos en esas carpetas compartidas.
Xabs

2
Tenga en cuenta que es posible que deba editar la carpeta de datos de mi host (es decir: eliminar una clave de grafito de prueba, eliminar mi carpeta de inicio de prueba JIRA o actualizarla con la última copia de seguridad de producción ...). Por lo que entiendo por su comentario, debería hacer cosas como actualizar los datos de JIRA a través de un tercer contenedor. En cualquier caso, ¿qué permisos aplicarías a una nueva carpeta de datos /data/newcontainer? Supongo que se ejecuta dockercomo root (¿es posible no hacerlo?) Además, ¿hay alguna diferencia en esos permisos si los datos se montan directamente en el contenedor principal o mediante un contenedor de solo datos ?
Xabs

2
Gracias por su elaborada respuesta. Probaré esto tan pronto como tenga la oportunidad. Además, buenas referencias tanto en su publicación de blog como sobre el uso de imágenes mínimas para contenedores de datos .
Xabs

3
El único problema con este enfoque es que es muy fácil eliminar un contenedor por error. Imagínese si resulta ser su contenedor de datos. Creo (CMIIW) que los datos aún estarán en /var/lib/dockeralgún lugar pero aún serán un gran dolor
lolski

3
"puede usar el contenedor de datos como" referencia / identificador "y establecer la propiedad / permisos en otro contenedor a través de chown en un punto de entrada" ... @Raman: Esta es la sección que finalmente me salvó después de tener numerosos problemas de permisos no resuelto. Usar un script de punto de entrada y establecer permisos en esto funciona para mí. Gracias por tu elaborada explicación. Es lo mejor que encontré en la web hasta ahora.
Vanderstaaij

59

Se puede ver una solución muy elegante en la imagen oficial de redis y, en general, en todas las imágenes oficiales.

Descrito en el proceso paso a paso:

  • Crear usuario / grupo redis antes que nada

Como se ve en los comentarios de Dockerfile:

agregue nuestro usuario y grupo primero para asegurarse de que sus ID se asignen de manera consistente, independientemente de las dependencias que se agreguen

  • Instalar gosu con Dockerfile

Gosu es una alternativa de su/ sudopara bajar fácilmente del usuario root. (Redis siempre se ejecuta con el redisusuario)

  • Configure el /datavolumen y configúrelo como workdir

Al configurar el volumen de datos / con el VOLUME /datacomando ahora tenemos un volumen separado que puede ser un volumen acoplable o montado en un directorio host.

Configurarlo como workdir ( WORKDIR /data) lo convierte en el directorio predeterminado desde donde se ejecutan los comandos.

  • Agregue el archivo docker-entrypoint y configúrelo como ENTRYPOINT con CMD redis-server predeterminado

Esto significa que todas las ejecuciones de contenedores se ejecutarán a través de la secuencia de comandos docker-entrypoint y, de forma predeterminada, el comando que se ejecutará es redis-server.

docker-entrypointes un script que realiza una función simple: cambiar la propiedad del directorio actual (/ datos) y descender de rootun redisusuario a otro para ejecutarlo redis-server. (Si el comando ejecutado no es redis-server, ejecutará el comando directamente).

Esto tiene el siguiente efecto

Si el directorio / data está montado en enlace al host, el punto de entrada de docker preparará los permisos de usuario antes de ejecutar redis-server bajo redisuser.

Esto le da la tranquilidad de que hay una configuración cero para ejecutar el contenedor en cualquier configuración de volumen.

Por supuesto, si necesita compartir el volumen entre diferentes imágenes, debe asegurarse de que usen el mismo ID de usuario / ID de grupo; de lo contrario, el último contenedor secuestrará los permisos de usuario del anterior.


11
La respuesta aceptada es informativa, pero solo me llevó por un camino de frustraciones de una semana con permisos para encontrar esta respuesta que realmente proporciona una forma canónica de resolver el problema.
m0meni


¿Entonces? ¿Cómo hago que el volumen se pueda escribir desde la ventana acoplable? chownEs dentro del ENTRYPOINTguión?
Gherman

34

Podría decirse que esta no es la mejor manera para la mayoría de las circunstancias, pero aún no se ha mencionado, por lo que quizás ayude a alguien.

  1. Volumen de host de montaje de enlace

    Host folder FOOBAR is mounted in container /volume/FOOBAR

  2. Modifique el script de inicio de su contenedor para encontrar el GID del volumen que le interesa

    $ TARGET_GID=$(stat -c "%g" /volume/FOOBAR)

  3. Asegúrese de que su usuario pertenezca a un grupo con este GID (puede que tenga que crear un nuevo grupo). Para este ejemplo, fingiré que mi software se ejecuta como el nobodyusuario cuando está dentro del contenedor, por lo que quiero asegurarme de que nobodypertenece a un grupo con una identificación de grupo igual aTARGET_GID

  EXISTS=$(cat /etc/group | grep $TARGET_GID | wc -l)

  # Create new group using target GID and add nobody user
  if [ $EXISTS == "0" ]; then
    groupadd -g $TARGET_GID tempgroup
    usermod -a -G tempgroup nobody
  else
    # GID exists, find group name and add
    GROUP=$(getent group $TARGET_GID | cut -d: -f1)
    usermod -a -G $GROUP nobody
  fi

Me gusta esto porque puedo modificar fácilmente los permisos de grupo en mis volúmenes de host y sé que esos permisos actualizados se aplican dentro del contenedor acoplable. Esto sucede sin ningún permiso o modificaciones de propiedad de mis carpetas / archivos de host, lo que me hace feliz.

No me gusta esto porque supone que no hay peligro en agregarse a un grupo arbitrario dentro del contenedor que está usando un GID que desea. No se puede usar con una USERcláusula en un Dockerfile (a menos que ese usuario tenga privilegios de root, supongo). Además, grita trabajo de pirateo ;-)

Si quiere ser hardcore, obviamente puede extender esto de muchas maneras, por ejemplo, buscar todos los grupos en cualquier subarchivo, múltiples volúmenes, etc.


44
¿Está dirigido a leer archivos del volumen montado? Estoy buscando una solución para escribir archivos sin que sean propiedad de otro usuario que no sea el que creó el contenedor acoplable.
ThorSummoner

Estoy usando este enfoque desde el 15 de agosto. Todo estuvo bien. Solo los permisos de los archivos creados dentro del contenedor eran distintos. Ambos, los usuarios (dentro y fuera del contenedor) tienen la propiedad de sus archivos, pero ambos tenían acceso de lectura porque pertenecían al mismo grupo creado por esta solución. El problema comenzó cuando un caso de uso impuso acceso de escritura a archivos comunes. El mayor problema era que el volumen compartido tenía archivos git (es un volumen para probar archivos fuente de desarrollo en el mismo contexto de producción). Git comenzó a advertir sobre problemas de acceso al código compartido.
platillo

Creo que $TARGET_GIDsería mejor usar grep grep ':$TARGET_GID:', de lo contrario, si el contenedor tiene, por ejemplo, gid 10001 y su host es 1000, esta verificación pasará pero no debería.
Robhudson

16

Ok, esto ahora está siendo rastreado en el docker número 7198

Por ahora, estoy lidiando con esto usando su segunda opción:

Asigne los usuarios del host al contenedor

Dockerfile

#=======
# Users
#=======
# TODO: Idk how to fix hardcoding uid & gid, specifics to docker host machine
RUN (adduser --system --uid=1000 --gid=1000 \
        --home /home/myguestuser --shell /bin/bash myguestuser)

CLI

# DIR_HOST and DIR_GUEST belongs to uid:gid 1000:1000
docker run -d -v ${DIR_HOST}:${DIR_GUEST} elgalu/myservice:latest

ACTUALIZACIÓN Actualmente estoy más inclinado a responder Hamy


1
utilizar el comando id -u <username>, id -g <username>, id -G <username>para obtener el ID de usuario y grupo de un usuario específico en lugar
lolski


15
Esto destruye la portabilidad del contenedor entre los hosts.
Raman

2
El problema de Docker # 7198 llegó a la conclusión de que no implementarán una solución nativa para esto. Vea mi comentario y respuestas en github.com/docker/docker/issues/7198#issuecomment-230636074
Quinn Commandado el


12

Al igual que usted, estaba buscando una forma de asignar usuarios / grupos desde el host a los contenedores acoplables y esta es la forma más corta que he encontrado hasta ahora:

  version: "3"
    services:
      my-service:
        .....
        volumes:
          # take uid/gid lists from host
          - /etc/passwd:/etc/passwd:ro
          - /etc/group:/etc/group:ro
          # mount config folder
          - path-to-my-configs/my-service:/etc/my-service:ro
        .....

Este es un extracto de mi docker-compose.yml.

La idea es montar (en modo de solo lectura) listas de usuarios / grupos desde el host al contenedor, por lo tanto, después de que el contenedor se inicie, tendrá las mismas coincidencias uid-> username (así como para grupos) con el host. Ahora puede configurar los ajustes de usuario / grupo para su servicio dentro del contenedor como si estuviera funcionando en su sistema host.

Cuando decide mover su contenedor a otro host, solo necesita cambiar el nombre de usuario en el archivo de configuración del servicio a lo que tiene en ese host.


Esta es una gran respuesta, muy simple si desea ejecutar contenedores que manejan archivos en un sistema base, sin exponer el resto del sistema.
icarito

Esta es mi respuesta favorita. Además, vi una recomendación similar en otra parte con el comando de ejecución de Docker donde pasas tu nombre de usuario / grupos actuales a través de -u $( id -u $USER ):$( id -g $USER )y ya no tienes que preocuparte por el nombre de usuario. Esto funciona bien para entornos de desarrollo local donde desea generar archivos (binarios, por ejemplo) a los que tiene acceso de lectura / escritura de forma predeterminada.
matthewcummings516

5

Aquí hay un enfoque que todavía usa un contenedor de solo datos pero no requiere que se sincronice con el contenedor de la aplicación (en términos de tener el mismo uid / gid).

Presumiblemente, desea ejecutar alguna aplicación en el contenedor como un $ USER no root sin un shell de inicio de sesión.

En el Dockerfile:

RUN useradd -s /bin/false myuser

# Set environment variables
ENV VOLUME_ROOT /data
ENV USER myuser

...

ENTRYPOINT ["./entrypoint.sh"]

Luego, en entrypoint.sh:

chown -R $USER:$USER $VOLUME_ROOT
su -s /bin/bash - $USER -c "cd $repo/build; $@"

5

Para la raíz y el cambio seguro para el envase de una ventana acoplable anfitrión intento utilización ventana acoplable --uidmapy --private-uidsopciones

https://github.com/docker/docker/pull/4572#issuecomment-38400893

También puede eliminar varias capacidades ( --cap-drop) en el contenedor docker por seguridad

http://opensource.com/business/14/9/security-for-docker

El soporte de ACTUALIZACIÓN debería venirdocker > 1.7.0

Versión ACTUALIZADA1.10.0 (2016-02-04) agregar --userns-remapbandera https://github.com/docker/docker/blob/master/CHANGELOG.md#security-2


Estoy ejecutando docker 1.3.2 build 39fa2fa (más reciente) y no veo rastros de --uidmapni las --private-uidsopciones. Parece que el RP no lo logró y no se fusionó.
Leo Gallucci

No se fusiona en el núcleo, si lo desea, puede usarlo como parche. Ahora solo es posible restringir algunas capacidades y ejecutar su aplicación en contenedor desde un usuario no root.
Montar el

Junio ​​de 2015 y no veo esto combinado en Docker 1.6.2. ¿Su respuesta sigue siendo válida?
Leo Gallucci

1
Problema aún abierto. El desarrollador debería agregar soporte en la versión 1.7. (opción --root) github.com/docker/docker/pull/12648
umount

2
Parece que los desarrolladores una vez más movieron el lanzamiento con esta funcionalidad. El desarrollador de Docker "icecrime" dice "We apparently do have so some of conflicting designs between libnetwork and user namespaces ... and something we'd like to get in for 1.8.0. So don't think we're dropping this, we're definitely going to take a break after all these, and see how we need to reconsider the current design and integration of libnetwork to make this possible. Thanks!" github.com/docker/docker/pull/12648 Así que creo que deberíamos esperar la próxima versión estable.
Montar el

4

Mi enfoque es detectar el UID / GID actual, luego crear dicho usuario / grupo dentro del contenedor y ejecutar el script debajo de él. Como resultado, todos los archivos que creará coincidirán con el usuario en el host (que es el script):

# get location of this script no matter what your current folder is, this might break between shells so make sure you run bash
LOCAL_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# get current IDs
USER_ID=$(id -u)
GROUP_ID=$(id -g)

echo "Mount $LOCAL_DIR into docker, and match the host IDs ($USER_ID:$GROUP_ID) inside the container."

docker run -v $LOCAL_DIR:/host_mount -i debian:9.4-slim bash -c "set -euo pipefail && groupadd -r -g $GROUP_ID lowprivgroup && useradd -u $USER_ID lowprivuser -g $GROUP_ID && cd /host_mount && su -c ./runMyScriptAsRegularUser.sh lowprivuser"

3

Imagen base

Utilice esta imagen: https://hub.docker.com/r/reduardo7/docker-host-user

o

Importante: esto destruye la portabilidad del contenedor entre los hosts .

1) init.sh

#!/bin/bash

if ! getent passwd $DOCKDEV_USER_NAME > /dev/null
  then
    echo "Creating user $DOCKDEV_USER_NAME:$DOCKDEV_GROUP_NAME"
    groupadd --gid $DOCKDEV_GROUP_ID -r $DOCKDEV_GROUP_NAME
    useradd --system --uid=$DOCKDEV_USER_ID --gid=$DOCKDEV_GROUP_ID \
        --home-dir /home --password $DOCKDEV_USER_NAME $DOCKDEV_USER_NAME
    usermod -a -G sudo $DOCKDEV_USER_NAME
    chown -R $DOCKDEV_USER_NAME:$DOCKDEV_GROUP_NAME /home
  fi

sudo -u $DOCKDEV_USER_NAME bash

2) Dockerfile

FROM ubuntu:latest
# Volumes
    VOLUME ["/home/data"]
# Copy Files
    COPY /home/data/init.sh /home
# Init
    RUN chmod a+x /home/init.sh

3) run.sh

#!/bin/bash

DOCKDEV_VARIABLES=(\
  DOCKDEV_USER_NAME=$USERNAME\
  DOCKDEV_USER_ID=$UID\
  DOCKDEV_GROUP_NAME=$(id -g -n $USERNAME)\
  DOCKDEV_GROUP_ID=$(id -g $USERNAME)\
)

cmd="docker run"

if [ ! -z "${DOCKDEV_VARIABLES}" ]; then
  for v in ${DOCKDEV_VARIABLES[@]}; do
    cmd="${cmd} -e ${v}"
  done
fi

# /home/usr/data contains init.sh
$cmd -v /home/usr/data:/home/data -i -t my-image /home/init.sh

4) Construir con docker

4) ¡Corre!

sh run.sh

0

En mi caso específico, estaba tratando de construir mi paquete de nodos con la imagen del acoplador de nodos para no tener que instalar npm en el servidor de implementación. Funcionó bien hasta que, fuera del contenedor y en la máquina host, intenté mover un archivo al directorio node_modules que había creado la imagen del acoplador de nodo, para lo cual se me negaron los permisos porque era propiedad de root. Me di cuenta de que podía solucionar esto copiando el directorio del contenedor en la máquina host. A través de docker docs ...

Los archivos copiados en la máquina local se crean con el UID: GID del usuario que invocó el comando docker cp.

Este es el código bash que utilicé para cambiar la propiedad del directorio creado por y dentro del contenedor acoplable.

NODE_IMAGE=node_builder
docker run -v $(pwd)/build:/build -w="/build" --name $NODE_IMAGE node:6-slim npm i --production
# node_modules is owned by root, so we need to copy it out 
docker cp $NODE_IMAGE:/build/node_modules build/lambda 
# you might have issues trying to remove the directory "node_modules" within the shared volume "build", because it is owned by root, so remove the image and its volumes
docker rm -vf $NODE_IMAGE || true

Si es necesario, puede eliminar el directorio con un segundo contenedor acoplable.

docker run -v $(pwd)/build:/build -w="/build" --name $RMR_IMAGE node:6-slim rm -r node_modules

0

Para compartir la carpeta entre el host de Docker y el contenedor de Docker, pruebe el siguiente comando

$ docker run -v "$ (pwd): $ (pwd)" -i -t ubuntu

El indicador -v monta el directorio de trabajo actual en el contenedor. Cuando el directorio de host de un volumen montado en enlace no existe, Docker creará automáticamente este directorio en el host para usted,

Sin embargo, hay 2 problemas que tenemos aquí:

  1. No puede escribir en el volumen montado si no era usuario root porque el archivo compartido será propiedad de otro usuario en el host,
  2. No debe ejecutar el proceso dentro de sus contenedores como root, pero incluso si lo ejecuta como un usuario codificado, no coincidirá con el usuario en su computadora portátil / Jenkins,

Solución:

Contenedor: cree un usuario que diga 'testuser', por defecto la identificación del usuario comenzará desde 1000,

Anfitrión: cree un grupo que diga 'testgroup' con la identificación de grupo 1000 y asigne el directorio al nuevo grupo (testgroup


-5

Si usa Docker Compose, inicie el contenedor en modo anterior:

wordpress:
    image: wordpress:4.5.3
    restart: always
    ports:
      - 8084:80
    privileged: true

2
Esto podría facilitar el montaje de volúmenes, pero ... ¿Wordpress se lanzó en modo privilegiado? Esa es una idea horrible, es pedir comprometerse. wpvulndb.com/wordpresses/453
Colin Harrington
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.